Ported models¶
Models somebody else already solved, said again in this language, and checked against an optimum that did not come from us.
Every other test compares farkas against farkas. Even the differential harness compares two lanes consuming the same resolved AST (hard rule 1) — which is what makes them an oracle for each other, and also what they cannot see: a shared misreading, both lanes agreeing on a meaning the modeller did not intend, passes the whole suite green. This is the net for that class, and the evidence behind the ceiling.
| Port | Reference | Optimum |
|---|---|---|
| Dantzig transport | published, GAMS model library #1 | 153.675 |
| PyPSA LOPF rung 1 | PyPSA 1.2.4, its own linopy 0.9.0 | 22000.0 |
| PyPSA LOPF rung 2 | PyPSA 1.2.4, its own linopy 0.9.0 | 18200.0 |
| PyPSA LOPF rung 3 | PyPSA 1.2.4, its own linopy 0.9.0 | 15253.178322993519 |
| PyPSA LOPF rung 4 | PyPSA 1.2.4, its own linopy 0.9.0 | 17228.77962151063 |
| PyPSA LOPF rung 5 | PyPSA 1.2.4, its own linopy 0.9.0 | 17000.0 |
| PyPSA unit commitment | PyPSA 1.2.4, its own linopy 0.9.0 | 24900.0 |
| Dantzig, economies of scale | linopy 0.9.0's own piecewise formulation | 8.786852757777865 |
| Stigler's diet | linopy 0.9.0; corroborated by Laderman (1947) | 0.10866227820675685 |
| Facility location | published by OR-Library (cap71) |
932615.750 |
| Travelling salesman, MTZ | published by TSPLIB (gr17) |
2085 |
Adding one is four files and five rules: CONTRIBUTING.md.
The ladder¶
Reproducing a full PyPSA objective means reproducing marginal and capital cost, ramp limits, storage cycling and KVL at once, and a mismatch then implicates five features instead of one. So each network is a ladder, one feature per rung, each switched off in PyPSA and reproduced here: 1 transport model ✔ · 2 ramp limits ✔ · 3 storage with state of charge ✔ · 4 cyclic boundary condition ✔ · 5 KVL ✔ — the ladder is complete. Unit commitment sits beside the ladder rather than on it — one bus, no network, because the feature under test is integrality.
A rung that matches is a row in the table above; one that cannot be said is a row in the ledger. Both are evidence, so no rung is wasted.
Rung 2 needed the instance widened before it meant anything. Rung 1's links run saturated, which fixes every generator's output exactly, so a ramp limit on that network can only make it infeasible — never change the answer. A rung that cannot bind is not evidence that it works.
The ladder finished without a new primitive. Rung 5 is Kirchhoff's voltage
law, and it needed nothing added to the language: a cycle basis is a sparse
(cycle, line) incidence parameter, and the constraint is one
sum(f * cycle_incidence, over=line) == 0. A line can belong to several
cycles, so the incidence cannot be a declared coordinate — that is the shape
finding, and it is the same "topology is data" claim the corpus started with.
Computing the basis is a graph algorithm and stays in data preparation, where
the ceiling puts it.
Rung 4 made the model smaller, which is the ladder paying off in the
direction nobody plans for. Closing the horizon deletes rung 3's boundary
equation outright: roll is cyclic already, so the wrap onto the last snapshot
is what it does unguarded, and the acyclic case is the one needing an extra
clause. Two rungs written a day apart, differing by one deleted where, is a
sharper statement about the language than either alone.
Ledger — what a port could not say¶
Feeds docs/ROADMAP.md, with the verdict CLAUDE.md asks for: macro, primitive, or escape.
| Port | What could not be said | Worked around by | Verdict |
|---|---|---|---|
| PyPSA rung 1 | a bound of -rating — PyPSA's p_min_pu = -1 |
shipping neg_rating as data |
primitive: bounds as expressions, #31. A second model asking for it |
| PyPSA unit commitment | min_up_time — a unit that starts must stay up for T snapshots |
left at 0, so the constraint is not written | split verdict, below |
| Travelling salesman | subtour cuts generated lazily inside branch-and-cut, which is how every serious TSP code works | MTZ, O(n²) and static | refused, and correctly: a solve loop is an algorithm, not a model |
min_up_time is the more interesting row, because the answer depends on
something the language cares about. The constraint is
sum(start_up over the last T snapshots) <= status. For a single T fixed in
the file that is start_up + shift(start_up, 1) + … — a macro, free. For
PyPSA's actual signature, where T is a column and each generator may have its
own, the number of terms is read from data, and
data-dependent structure inside an expression
is refused by design rather than unimplemented. The two halves of that answer
are worth keeping apart: one is a macro nobody has written, the other is the
ceiling doing its job.
Three rows from eleven ports — a rate worth watching once the corpus has hit the ceiling a few more times.
The TSP row is the one to read, and it is narrower than it first looked. Writing DFJ's subtour rows out in full is sayable — the subsets go in as data exactly the way KVL's cycle basis does, and an 8-city instance with all 246 subsets solves to a correct tour. There are 2ⁿ of them, so it stops being practical around twenty cities, but that is a data-size wall rather than a ceiling.
What is genuinely outside is lazy generation: solve, find the violated subsets, add rows, re-solve. That is an algorithm, and this language describes models. Since lazy generation is what every serious TSP code actually does, "farkas can express TSP" and "farkas is a good way to solve a large TSP" are different sentences and only the first is true.
An earlier draft of this ledger said DFJ was refused for having a data-dependent row count. That was wrong — the cycle basis has one too — and the corpus is where it got caught.
Each port has its own page in the gallery — the maths, the instance, the model as CI runs it, and a side-by-side against the reference implementation. Adding one is four files and five rules: CONTRIBUTING.md.