A simple Python 3.6 calculator

Calculate optimal ratios for feeding recipes, search through the research-tree, specialized tools to view game-information.
Post Reply
User avatar
Omnifarious
Filter Inserter
Filter Inserter
Posts: 267
Joined: Wed Jul 26, 2017 3:24 pm
Contact:

A simple Python 3.6 calculator

Post by Omnifarious »

I love f'' strings, so, no Python 3.5 even.

Code: Select all

import pickle
import os
import os.path as _osp
from fractions import Fraction as _F

class ProductionItem:
    __slots__ = ('_name', '_time', '_ingredients', '_produced', '__weakref__')

    def __init__(self, name, time, ingredients, produced=1, **kargs):
        super().__init__(**kargs)
        self._produced = produced
        self._name = name
        self._time = time
        self._ingredients = ingredients

    def __hash__(self):
        return hash(self._name)

    def __eq__(self, other):
        if isinstance(other, ProductionItem):
            return self._name == other._name

    def __repr__(self):
        return f'ProductionItem({self._name!r}, {self._time}, ' \
            f'{self._ingredients!r}, produced={self._produced})'

    @property
    def base_rate(self):
        base_rate = (self._produced / _F(1,1)) / (self._time / _F(1,1))
        return base_rate

    def factories(self, rate):
        return rate / self.base_rate

    def rate_with_factories(self, numfactories):
        return numfactories * self.base_rate

class ItemSet(set):
    def __init__(self, *args, **kargs):
        super().__init__(*args, **kargs)
        self._by_name = dict()

    def __getitem__(self, name):
        item = self._by_name.get(name)
        if item is not None:
            return item
        else:
            for item in self:
                if item._name == name:
                    self._by_name[item._name] = item
                    return item
        raise KeyError(name)

_mod_dir = _osp.dirname(__file__)
db_fname = _osp.join(_mod_dir, 'item-db.pickle')

if _osp.exists(db_fname):
    with open(db_fname, 'rb') as _item_f:
        item_db = pickle.load(_item_f)
else:
    item_db = set()

def save_items():
    tmp_new = db_fname + '.new'
    with open(tmp_new, 'wb') as item_f:
        pickle.dump(item_db, item_f, -1)
    os.unlink(db_fname)
    os.link(tmp_new, db_fname)
    os.unlink(tmp_new)

def production_rate(dest_item, rate, source_item):
    if dest_item is source_item:
        return rate
    if dest_item._produced is None:
        return 0
    produced = dest_item._produced / _F(1,1)
    scale = rate / produced
    print(f"name, scale == {dest_item._name}, {scale}")
    total = 0
    for sub_item_ct, sub_item in dest_item._ingredients:
        sub_rate = production_rate(sub_item, scale * sub_item_ct, source_item)
        total += sub_rate
    return total

def how_many_produced(source_item, rate, dest_item):
    forward_rate = production_rate(dest_item, _F(1,1), source_item)
    return rate / forward_rate
It's a work in progress. It also builds a database on the fly as you add stuff to it. Importing the module loads the database from the directory the module is in. Calling the save_items function saves it. I did this because I don't know where there's a good, accurate database out there. And, since it's a personal use tool, I'll just add the items I want as I need them.

It will calculate out how many factories of a given type you need for a given intermediate if you have the end goal of a certain production rate on your source. It uses fractions/rational numbers for everything. For best results, non-integer times (and other values) should be entered into the database as fractions.

User avatar
Omnifarious
Filter Inserter
Filter Inserter
Posts: 267
Joined: Wed Jul 26, 2017 3:24 pm
Contact:

Re: A simple Python 3.6 calculator

Post by Omnifarious »

An updated version, plus an example:

Code: Select all

import pickle
import os
import os.path as _osp
from fractions import Fraction as _F
import sys
from collections import namedtuple

class ProductionItem:
    __slots__ = ('_name', '_time', '_ingredients', '_produced', '__weakref__')

    def __init__(self, name, time, ingredients, produced=1, **kargs):
        super().__init__(**kargs)
        self._produced = produced
        self._name = name
        self._time = time
        self._ingredients = ingredients

    def __hash__(self):
        return hash(self._name)

    def __eq__(self, other):
        if isinstance(other, ProductionItem):
            return self._name == other._name

    def __repr__(self):
        return f'ProductionItem({self._name!r}, {self._time}, ' \
            f'{self._ingredients!r}, produced={self._produced})'

    @property
    def base_rate(self):
        if self._produced is None:
            return None
        else:
            base_rate = (self._produced / _F(1,1)) / (self._time / _F(1,1))
            return base_rate

    def factories(self, rate):
        if self._produced is None:
            return None
        else:
            return rate / self.base_rate

    def rate_with_factories(self, numfactories):
        if self._produced is None:
            return None
        else:
            return numfactories * self.base_rate

class ItemSet(set):
    def __init__(self, *args, **kargs):
        super().__init__(*args, **kargs)
        self._by_name = dict()

    def __getitem__(self, name):
        item = self._by_name.get(name)
        if item is not None:
            return item
        else:
            for item in self:
                if item._name == name:
                    self._by_name[item._name] = item
                    return item
        raise KeyError(name)

_mod_dir = _osp.dirname(__file__)
db_fname = _osp.join(_mod_dir, 'item-db.pickle')

if _osp.exists(db_fname):
    with open(db_fname, 'rb') as _item_f:
        item_db = pickle.load(_item_f)
else:
    item_db = set()

def save_items():
    tmp_new = db_fname + '.new'
    with open(tmp_new, 'wb') as item_f:
        pickle.dump(item_db, item_f, -1)
    os.unlink(db_fname)
    os.link(tmp_new, db_fname)
    os.unlink(tmp_new)

def production_rate(dest_item, rate, source_item):
    if dest_item is source_item:
        return rate
    if dest_item._produced is None:
        return 0
    produced = dest_item._produced / _F(1,1)
    scale = rate / produced
#    print(f"name, scale == {dest_item._name}, {scale}")
    total = 0
    for sub_item_ct, sub_item in dest_item._ingredients:
        sub_rate = production_rate(sub_item, scale * sub_item_ct, source_item)
        total += sub_rate
    return total

def how_many_produced(source_item, rate, dest_item):
    forward_rate = production_rate(dest_item, _F(1,1), source_item)
    return rate / forward_rate

FactoryInfo = namedtuple('FactoryInfo', ['factories', 'fractional_factories',
                                         'target_rate', 'item'])

def factories_for_each(dest_item, rate):
    items_so_far = set()
    factory_list = []
    def recursive_count(dest_item, rate, cur_source=None):
        if cur_source is None:
            cur_source = dest_item
        if cur_source in items_so_far:
            return
        items_so_far.add(cur_source)
        source_rate = production_rate(dest_item, rate, cur_source)
        if cur_source._produced is None:
            factory_list.append(FactoryInfo(None, None, source_rate, cur_source))
        else:
            factories = cur_source.factories(source_rate)
            int_fact = factories // _F(1,1)
            if (factories - int_fact) > 0:
                int_fact += 1
            assert(int_fact >= factories)
            factory_list.append(FactoryInfo(int_fact, factories,
                                            source_rate, cur_source))
            for _, next_source in cur_source._ingredients:
                recursive_count(dest_item, rate, next_source)
    recursive_count(dest_item, rate)
    return factory_list

def actual_production(dest_item, factory_list):
    def produced_for_each(dest_item, factory_list):
        for int_fact, _, _, item in factory_list:
            if int_fact is not None:
                rate = (_F(int_fact, 1) * item._produced) / item._time
                cur_produced = how_many_produced(item, rate, dest_item)
                yield cur_produced
    return min(produced_for_each(dest_item, factory_list))

def print_factories(factory_list, file=None):
    if file is None:
        file = sys.stdout
    raw = []
    cooked = []
    print("Factories   (as a fraction)   Rate      Name", file=file)
    print("---------   ---------------   -------   ---------------------",
          file=file)
    for fi in factory_list:
        if fi.factories is None:
            raw.append(fi)
        else:
            print(f'{fi.factories:9}   {fi.fractional_factories!s:>15}   '
                  f'{fi.target_rate!s:>7}   {fi.item._name}', file=file)
    for fi in raw:
        print('                              '
              f'{fi.target_rate!s:>7}   {fi.item._name}', file=file)
And here is how you might use it (rates are always in items/sec):

Code: Select all

import factorio_calc
from fractions import Fraction as F
factorio_calc.print_factories(factorio_calc.factories_for_each(factorio_calc.item_db['Processing unit'], F(1,1)))
Factories   (as a fraction)   Rate      Name
---------   ---------------   -------   ---------------------
       10                10         1   Processing unit
       12                12        24   Circuit
       20                20        80   Wire
       12                12         2   Adv Circuit
        2                 2         4   Plastic
        1              1/10         5   Sulfuric Acid
        1               1/4       1/2   Sulfur
                               241/10   Iron
                                   40   Copper
                                    2   Coal
                                 55/2   Pet Gas
                                 35/2   Water
Here is how I add stuff:

Code: Select all

factorio_calc.item_db.add(factorio_calc.ProductionItem('Copper', None, (), _produced=None))
factorio_calc.item_db.add(factorio_calc.ProdctionItem('Wire', F(1,2), ((1, factorio_calc.item_db['Copper']),), _produced=2)
In that example, I treat copper as a raw material, which works for how I've set up my factory to severely overproduce the things I consider raw materials. A Wire factory takes a half second (aka F(1,2)) to produce 2 units of Wire from 1 Copper.

User avatar
Omnifarious
Filter Inserter
Filter Inserter
Posts: 267
Joined: Wed Jul 26, 2017 3:24 pm
Contact:

Re: A simple Python 3.6 calculator

Post by Omnifarious »

I posted mine on BitBucket (and mirrored it on GitHub) where it join the others with (in some ways) nicer UIs, though the one I tested presented output that was very pretty, but also very confusing. Mine is painful to use. But the output is very clear. :-) I put it on BitBucket primarily because I love Mercurial. :-)

https://www.bitbucket.org/omnifarious/factorio_calc

https://www.github.com/Omnifarious/factorio_calc

Post Reply

Return to “Cheatsheets / Calculators / Viewers”