## A simple Python 3.6 calculator

Calculate optimal ratios for feeding recipes, search through the research-tree, specialized tools to view game-information.
Omnifarious
Fast Inserter
Posts: 105
Joined: Wed Jul 26, 2017 3:24 pm

### A simple Python 3.6 calculator

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:
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)

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

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.

Omnifarious
Fast Inserter
Posts: 105
Joined: Wed Jul 26, 2017 3:24 pm

### Re: A simple Python 3.6 calculator

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:
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)

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

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
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
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))
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.

Omnifarious
Fast Inserter
Posts: 105
Joined: Wed Jul 26, 2017 3:24 pm

### Re: A simple Python 3.6 calculator

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