A small Python DSL for calculating Factorio production flows. It models machines, recipes, modules, item sources/sinks, quality, symbolic variables, constraints, and linear solves.
The default catalog is Space Age 2.0.55. The project is tested on Python 3.11 and 3.12, and is licensed under the MIT License.
Use Python 3.11 or newer. The solver depends on highspy.
python3 -m venv .venv
. .venv/bin/activate
pip install -e .Run the example script:
python examples/basic_gear.py
python examples/electronic_circuits.py 10
python examples/quality_recycling.pyRun the test suite with the project virtual environment:
pip install -e ".[dev]"
.venv/bin/python -m unittest discover -vImport the default catalog registries:
from factorio_flow import flow, item, machine, module, print_flow, rate, recipeCreate a process by calling a machine with a recipe:
line = machine.assembling_machine_3(recipe.iron_gear_wheel)
print_flow(line)Processes compose with + and scale with *:
line = (
machine.assembling_machine_3(recipe.iron_gear_wheel) * 4
+ machine.assembling_machine_3(recipe.engine_unit)
)Add modules with .modules(...):
line = machine.assembling_machine_3(recipe.iron_gear_wheel).modules(
module.speed_module_3,
module.speed_module_3,
)flow(line) returns a mapping-compatible FlowResult:
result = flow(line)
print(result[item.iron_plate])
print(result.rates)
print(result.variables)Index by item when reading rates:
result = flow(machine.assembling_machine_3(recipe.iron_gear_wheel))
print(result[item.iron_plate])
print(result[item.iron_gear_wheel])Call an item to create a source or sink process:
from factorio_flow import item
source = item.iron_plate(10) # +10/s
sink = item.iron_gear_wheel(-10, seconds=10) # -1/s
line = source + sinkPositive rates add supply. Negative rates consume or remove items from the flow, which is useful for boxes, buses, or external demand.
Use rate.<item> expressions in .where(...):
line = (
machine.assembling_machine_3(recipe.iron_gear_wheel) * 4
).where(
(rate.iron_gear_wheel >= 1)
& (rate.iron_plate <= 10)
)Supported expression operators include:
- arithmetic:
+,-, unary-,*,/ - comparisons:
<,<=,>,>=,==,!= - predicates:
&,|,~
Use parentheses around comparisons because Python operator precedence applies. Equality uses a small floating-point tolerance.
Helpers are also available:
from factorio_flow import all_of, any_of, not_Raw callables are accepted for concrete validation:
line.where(lambda values: values.get(item.iron_plate, 0.0) <= 10)Create symbolic variables with var(...) or integer variables with
int_var(...):
from factorio_flow import int_var, var
machines = int_var("machines")
sink = var("sink")
line = (
machine.assembling_machine_3(recipe.iron_gear_wheel) * machines
+ item.iron_gear_wheel(-sink)
).where(
(machines >= 0)
& (sink >= 0)
)Bind values with .bind(...):
concrete = flow(line.bind(machines, 4).bind(sink, 10))Unbound variables remain symbolic in flow(...). Explicit bindings appear in
FlowResult.variables.
Use .solve(min=[...], max=[...]) to solve linear constraints with HiGHS:
machines = var("machines")
line = (
machine.assembling_machine_3(recipe.iron_gear_wheel) * machines
).where(
(machines >= 0)
& (rate.iron_gear_wheel == 10)
).solve(min=[machines])
result = flow(line)
print(result.variables[machines])Objectives are lexicographic: all min variables are optimized in order, then
all max variables in order. Only objective variables are bound back into the
result; other variables may remain symbolic.
The solver supports linear constraints from where(...), including constraints
over rate.<item> expressions. It rejects nonlinear expressions, raw callable
predicates, |, ~, and != during solve.
Solver errors:
from factorio_flow import (
InfeasibleSolveError,
UnboundedSolveError,
UnsupportedSolveError,
)Quality is part of Item identity. Unqualified items, recipes, machines,
modules, and rates mean normal quality.
line = item.iron_plate.rare(1)
result = flow(line)
print(result[item.iron_plate.rare])Quality tier attributes:
item.iron_plate.normal
item.iron_plate.uncommon
item.iron_plate.rare
item.iron_plate.epic
item.iron_plate.legendaryThe same tier attributes work on recipes, machines, modules, and rates:
line = machine.assembling_machine_3.legendary(
recipe.iron_gear_wheel.rare
).modules(
module.quality_module_3.legendary,
)
predicate = rate.iron_gear_wheel.rare >= 1Recipe quality selects input/output item quality. Fluids stay unqualified and cannot be converted to non-normal quality:
item.water.rare # raises ValueErrorQuality modules split qualityable item outputs by expected value through legendary. Recipe output probabilities are modeled as expected throughput, not random samples.
Machine and module quality follows Factorio quality scaling:
- machine crafting speed scales by quality
- positive module effects scale by quality
- negative module effects do not scale
- legendary stat scaling is
2.5x
print_flow(process) prints item rates and any bound or solved variables:
print_flow(line)Numeric rates use readable time units:
/ sfor rates at least1/s/ minfor slower rates at least1/min/ hfor very slow rates
Symbolic rates print as expressions per second.
The package exports the default registries:
from factorio_flow import item, rate, recipe, machine, moduleLoad a different catalog explicitly:
from factorio_flow import DEFAULT_CATALOG_PATH, load_catalog
catalog = load_catalog(DEFAULT_CATALOG_PATH.with_name("vanilla-2.0.55.json"))
line = catalog.machines.assembling_machine_3(
catalog.recipes.iron_gear_wheel
)Attribute names are normalized from Factorio prototype ids:
iron-plate->iron_plateassembling-machine-3->assembling_machine_3quality-module-3->quality_module_3
See docs/data-format.md for the normalized JSON catalog format and importer
notes.