Skip to content

farkas

Self-documenting optimisation models — at any scale.

Write the math in YAML, bind data at runtime, solve.

PyPI License: MIT Python 3.11+ Ruff

Write a model Browse the models


  • Declarative math


    Readable without knowing the implementation, and self-contained: no Python state changes what a file means. It diffs cleanly in review and travels as a research artefact.

  • Sparse by construction


    A mask is an absent row, never a NaN in a dense array — a model pays for the variables it has, not for its coordinate product. Labels are the solver's own row and column indices.

  • Fail early, fail loud


    Every expression, where string and even uncalled macro template is parsed and name-checked before a single source is bound. Errors name the problem and its rewrite.

  • A finite language, with a priced way out


    The ceiling is a closure (affine ∩ relational ∩ local), not a feature race. Genuinely unsayable math goes in an escape: island — visible in the file, billed before it runs.

  • Straight to the solver


    YAML and data in, a populated solver out, no LP file in between: 2–4x faster than the eager lane on four of five benchmark cases, lower peak memory on all five. The numbers

  • Checked against somebody else


    Eleven models in the gallery match an optimum this project did not compute — GAMS, PyPSA, OR-Library, TSPLIB — objectives and shadow prices. The corpus

flowchart LR
    Y["YAML + data"] --> AST["core AST"]
    AST --> R{"inside the<br/>language?"}
    R -->|"yes"| S["relational engine<br/>polars"]
    S --> OUT["solver (batched) / LP file"]
    R -->|"no"| ERR["load error<br/>naming the construct + rewrite"]
    AST -.->|"opt-in shim: same language,<br/>for models already in memory"| E["farkas.linopy"]
    E --> LS["linopy.Model → solve"]

    classDef stream fill:#f0f7f0,stroke:#3a7d44,stroke-width:2px,color:#111
    classDef linopylane fill:#eef1fb,stroke:#4a5fc1,stroke-width:2px,color:#111
    class S,OUT stream
    class E,LS linopylane
    class ERR err
    classDef err fill:#fdf3e7,stroke:#b7791f,color:#111

The whole thing, in one model

# dispatch.yaml
dimensions:
  snapshot: {dtype: int}
  generator: {values: [wind, solar, gas]}
parameters:
  p_max: {dims: [generator]}
  load:  {dims: [snapshot]}
  cost:  {dims: [generator]}
variables:
  p:
    foreach: [snapshot, generator]
    where: "p_max > 0"
    bounds: {lower: 0, upper: p_max}
constraints:
  power_balance:
    foreach: [snapshot]
    equations:
      - expression: sum(p, over=generator) == load
objectives:
  total_cost:
    sense: minimize
    equations:
      - expression: p * cost
import farkas as fk, polars as pl

generators = ["wind", "solar", "gas"]
sources = {
    "p_max": pl.DataFrame({"generator": generators, "value": [100.0, 60.0, 200.0]}),
    "cost":  pl.DataFrame({"generator": generators, "value": [1.0, 2.0, 50.0]}),
    "load":  pl.DataFrame({"snapshot": range(6),
                           "value": [80.0, 120.0, 150.0, 180.0, 140.0, 100.0]}),
}

result = fk.solve("dispatch.yaml", sources, coords={"snapshot": range(6)})
print(result.objective)     # 1920.0
print(result.primal("p"))   # a tidy frame: (snapshot, generator, value)
print(result.dual("power_balance"))  # the price at each snapshot

Sources can also be pandas or pyarrow objects, or parquet paths — anything exposing the Arrow PyCapsule protocol is accepted, and the recogniser imports none of them. Results come back as frames, so nothing has to be released and no dataframe library is a dependency: result.to_pandas("p"), .to_dataarray("p") and .to_parquet(dir) are the bridges out, each named for what it costs.

Where to next

  • Writing a model


    Five ideas — dimensions, absence, topology, roll, the dim algebra — each shown in a model that runs, and what the language will not do.

    The guide

  • Models


    Every model in the repo, what each exercises, and which ones are checked against an optimum from elsewhere.

    The gallery

  • Language reference


    What a YAML file may contain, and what it means.

    SPEC

  • Why it is shaped this way


    The hard rules, the expressive ceiling, the module map — and what we have decided never to build.

    Architecture · Roadmap

pip install farkas            # the relational engine (polars, highspy)
pip install "farkas[linopy]"  # adds linopy + xarray + pandas: the shim, the
                              # oracle, and to_pandas / to_dataarray

Alpha, pre-1.0

Breaking changes land without a deprecation cycle. When a construct is named wrong, a default is wrong, or a permissive input turns out to hide a silent wrong answer, it gets fixed rather than aliased. Pin an exact version if you depend on this, and read the changelog before upgrading — breaking commits are marked !, and every one names the rewrite. It is the surface that is not yet frozen, not the behaviour.