Skip to content

Build whole models from mathspec (whole-model-only) - #958

Open
FabianHofmann wants to merge 67 commits into
masterfrom
spec-whole-model
Open

FabianHofmann wants to merge 67 commits into
masterfrom
spec-whole-model

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Note

The following content was generated by AI.

Changes proposed in this Pull Request

A mathspec spec (a YAML declaration of a linear model) builds a whole linopy model from linopy/spec/. mathspec (>= 0.2, from PyPI) is the optional extra linopy[spec] (Python >= 3.12) and imported lazily, so import linopy never pulls it in.

How this differs from #922: this branch drops that PR's layer and binding machinery; a spec builds one whole model into an empty Model and cannot be layered onto, or bound into, an existing one.

Building

  • Model.add_spec(spec, sources, retain="report", build_expressions=True) builds variables, SOS, constraints and the objective into an empty v1 model; Model.from_spec(spec, sources, retain=..., **model_kwargs) is sugar over it on a fresh model. spec is a path, YAML text, dict or mathspec.Spec. add_spec on a non-empty model is refused.
  • linopy.spec.attach(program, sources, retain=...) turns a lowered Program plus user data into an Attached: master coordinates, lookups and on-demand parameters. sources is any Mapping pulled by key and never iterated, or a single xr.Dataset. mathspec's attachment rules are enforced; unknown labels, duplicate rows, wrong rank or dtype raise SpecDataError. Aligned data is never copied.
  • Absence is uniform: a missing parameter row is refused wherever it is used. A Sources adapter reads each key once; a key naming nothing the spec declares is ignored and listed on Attached.unused, and a near-miss key warns as a likely typo.
  • Every assumptions: entry of the spec is checked against the data before building; a failing one raises SpecDataError with its first coordinates.
  • A row that absence empties (e.g. the first snapshot of a bare shift) is not built, as mathspec specifies; a present row left with no variable is refused. Whether this should become strict is tracked in spec: rows emptied by absence are dropped silently; revisit strict refusal #993. Arithmetic on present data that turns non-finite (0/0, 1/0, inf - inf) is refused rather than read as absence.

Stamping and named expressions

  • Every variable, constraint and expression a spec builds carries the spec's name in attrs["spec"], read through a spec property (str | None) on Variable, Constraint, LinearExpression and QuadraticExpression. A derivative of a stamped object (expr * 2, linopy.merge, add_constraints(expr >= 0)) carries None. The name is the spec file's stem, else "spec".
  • A named expression whose body holds a variable term is built into model.expressions under its declared name at add_spec time and stamped, so model.expressions[name] and model.spec.expressions[name].expression are one object. A data-only body or one reading a constraint's dual stays spec-only and folds on read. build_expressions=False keeps them lazy; the flag is not persisted.
  • remove_variables, remove_constraints and remove_expressions refuse to drop a name the spec built. unspecified reports what the spec does not declare, so a whole-spec model's repr carries no tags; tags appear only once hand-built items sit beside the spec.

Reading back and typesetting

  • model.spec is a ModelSpec exposing program, text, parameters, coords, lookups and expressions. model.spec.expressions[name] is a NamedExpression with .node, .dims, .expression and .solution (the fold over the solved model). model.spec.evaluate(name, sources) reattaches parameters afresh for retain="none".
  • model.spec.to_latex/to_markdown/to_typst render the whole model; declaration(name) renders one line. In a notebook the accessor renders itself for MathJax.

Persistence

  • to_netcdf writes the spec under a spec- prefix: the spec text and name, master coordinates and lookups, and object-dtype parameters as pandas.factorize codes and categories, so partial maps keep holes and dtypes on both netcdf engines. A version header names the mathspec version and layout; a version mismatch warns on read, and a file of another layout warns and loads as a plain model without model.spec. read_netcdf re-lowers the Program from its text, and a file without a spec loads without mathspec installed.
  • Model.copy() carries the spec; assert_model_equal compares the spec text, name and parameters including dtypes and the per-object stamps.

Docs, CI, benchmarks

  • Notebook examples/building-models-from-specs.ipynb, wired into the user guide and executed on Read the Docs and in the notebook CI job with the spec extra installed. API pages for linopy.spec and release notes added.
  • The 3.12 and 3.13 test jobs install the spec extra; mypy runs with mathspec installed. Tests skip without it. The sweep over mathspec's example specs runs when MATHSPEC_EXAMPLES points at a mathspec checkout; CI clones the examples at the installed mathspec's tag.
Verification (81c8eed, mathspec 0.2.0)
uv run mypy linopy
Found 28 errors in 1 file   # all in linopy/solvers.py, only with xpress installed (#994); none in linopy/spec
uv run ruff check . && ruff format --check .
All checks passed!
# XPAUTH_PATH unset locally, see #994; no -k filter ("not xpress" also deselects "expression" tests)
MATHSPEC_EXAMPLES=<mathspec v0.2.0>/examples uv run pytest -q test/test_spec_*.py test/test_io.py
643 passed, 309 skipped, 4 xfailed
MATHSPEC_EXAMPLES=<mathspec v0.2.0>/examples uv run pytest -q --ignore=benchmarks --ignore=test/remote
8702 passed, 1106 skipped, 4 xfailed

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

Turns a lowered math-spec Program plus user data into master coordinates,
padded lookups and on-demand parameter arrays under the three binding
rules. Missing rows stay NaN for the builder. Sources are pulled by key,
never iterated, and aligned arrays keep their buffer.
Validate retain, check a scalar's dtype before casting, bind empty
sources as all-NaN, check label-space lookup dtypes, report unknown
labels in source order on every path, re-stamp coordinates onto the
master dtype without copying, and pin the remaining binder rules.
… group

math-spec is not on PyPI and needs Python >= 3.12. A PEP 735 dependency
group keeps the git pin out of the wheel metadata; the 3.12 and 3.13 test
jobs install it so the binder tests and their coverage run in CI.
…ssions

Port lpspec's linopy lane onto the binder: builder, where, operators,
coverage and curves, wired to Bound and SpecDataError. Add Model.add_spec,
Model.from_spec and the model.spec accessor with expressions and evaluate.
…en windows

Coverage and the retain closure now descend into a Power's operands;
evaluate() refuses sources labelled unlike the model; an all-null window
width is a window of nothing; cases fold through the aligned combine.
Persist the spec text, the master coordinates and the lookups alongside the
model, re-lowering the program from the text on read; math-spec is imported
only for a file that carries a spec. Lookups and arrays of labels are stored
as codes into a category table, so partial maps keep their holes and dtypes.
Write the in-memory dtype of every parameter and cast it back on read, and
stamp the master coordinates onto every container, so no engine leaves a
model disagreeing with itself. assert_model_equal now compares dataset
dtypes, and synthetic_sources moves to linopy/spec/testing.py for both users.
A missing parameter row was read as a silent zero when it stood as a
coefficient, while a bound, constant side or divisor already refused it.
Refuse it as a coefficient too, so every position behaves alike and a
hole is never filled without the modeller saying so: mask the coordinate
out with a where, or fill the value into the data.
A runnable, nbconvert-clean walkthrough of the spec feature: the dispatch
program, binding data, folding named expressions, retain and evaluate,
the uniform absence rule, lookups and grouped sums, temporal shift, and
the netCDF round trip.
m.spec.expressions[name] returns a NamedExpression bundling .node (the
lowered formula), .expression (the unsolved linopy expression) and
.solution (the fold over the model's solution). evaluate() returns the
same object. Add ModelSpec.to_latex/to_markdown/to_typst for whole-model
typesetting, rendered as Markdown in a notebook.
building-models-from-specs.ipynb imports math_spec, which the docs CI environment does not install, so the notebook job failed on import. Skip it like the other special-setup notebooks.
pandas 3 hands strings over as StringDtype, Arrow-backed when pyarrow is
installed. xarray keeps the extension array, refuses it in positional
indexing and reports no np.dtype, so the netcdf dtype round trip broke.
…repair moves to io

parameters.py owns resolution and derivation, groups.py the axis partition,
nodes.amounts_of the parameter-named amounts, Context.lookup the lookups.
io records and restores parameter dtypes for every model and owns
restamp_coords and the module-level prefix helpers spec/netcdf reuses.
…e walk

evaluate.py holds the recursive evaluator, builder.py the declarations.
check_coverage collects divisor, constant-side and coefficient obligations
in one walk, so cases: masks are evaluated once per declaration.
Public docstrings in numpy style so the API pages render.
…rom_spec and bind

warn_evolving_api moves to linopy.constants so piecewise and spec share
the once-per-key dedup; the pytest filter silences the spec prefix.
…ift notes

Point the math-spec ImportError at the spec dependency group and the
3.12 floor, fix the notebook's stale model.parameters check, document
sparse_groupby/freeze_constraints for skewed topologies in api.rst and
the notebook, and add release notes for the spec hardening work.
A str is YAML text when it holds a newline, opens a mapping or a sequence,
or holds a ':' and names no file; every other str is a path, and a missing
one raises FileNotFoundError instead of a read error. An open file is
refused by name. A spec that declares no dimension, parameter or variable,
or that is not a mapping of sections, is refused before any data is read.
…ing read

sources is wrapped once, keys() is called once and everything after that is
read through it, so what a build actually read is known. An extra key is
ignored by default rather than refused, so one mapping can feed several
specs, and Attached.unused reports it; strict=True restores the refusal.
A key close to a declared name warns as a likely typo either way, and a
sources without keys() is refused by name instead of by AttributeError.
A dual has no symbolic form, so reading .expression says so instead of
asking for a solve the model may already have had; folding one the model
does not hold raises the spec's own 'no dual yet' rather than linopy's
AttributeError. NamedExpression.solution folds afresh on every read.
A dimension index is one flat axis: a MultiIndex, or labels pandas
tuple-izes into one, is refused by name. Its dtype is checked against the
declaration with the same rule parameter values pass, so a declared type is
a claim about the labels too.
…n the model

Variables, constraints and expressions a spec builds carry the spec's name in attrs and a spec property.
Variable-bearing named expressions are built into model.expressions at add_spec time; build_expressions=False keeps them lazy.
remove_variables/constraints/expressions refuse spec-built names; repr tags read the stamp; the name round trips through netcdf.
@codspeed

codspeed Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 13.91%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 180 untouched benchmarks
⏩ 181 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_build[piecewise-n=10] 15.2 KB 13.4 KB +13.91%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing spec-whole-model (fd3c8d5) with master (1b2ea76)

Open in CodSpeed

Footnotes

  1. 181 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Build cost — v1 vs legacy

v1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).

peak — v1 / legacy time — v1 / legacy
peak v1/legacy time v1/legacy
Full table (time + peak, mean)
benchmarks/drivers/test_build.py::test_build[basic-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.09022 (1.10)   │   15.03 (1.00) 
 (v1)        0.08229 (1.0)   │    15.00 (1.0) 

benchmarks/drivers/test_build.py::test_build[basic-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.09851 (1.12)   │   12.04 (1.00) 
 (v1)        0.08763 (1.0)   │    12.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=0]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.03844 (1.07)   │   15.20 (1.0) 
 (v1)        0.03598 (1.0)   │   15.20 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=100]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05552 (1.06)   │   44.93 (1.0) 
 (v1)        0.05256 (1.0)   │   44.93 (1.0) 

benchmarks/drivers/test_build.py::test_build[cumsum-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04195 (1.04)   │   11.51 (1.0) 
 (v1)        0.04034 (1.0)   │   11.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=10]
                 time (s)         peak (KiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1001 (1.06)   │   24.34 (1.06) 
 (v1)       0.09443 (1.0)   │    23.04 (1.0) 

benchmarks/drivers/test_build.py::test_build[expression_arithmetic-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1211 (1.17)   │   16.12 (1.00) 
 (v1)        0.1033 (1.0)   │    16.12 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=10000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.02413 (1.07)   │   752.18 (1.10) 
 (v1)         0.0225 (1.0)   │    685.15 (1.0) 

benchmarks/drivers/test_build.py::test_build[knapsack-n=100]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.02414 (1.08)   │   3.12 (1.33) 
 (v1)        0.02234 (1.0)   │    2.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=0]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06437 (1.21)   │   126.16 (1.44) 
 (v1)        0.05337 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=100]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06332 (1.21)   │   126.16 (1.44) 
 (v1)        0.05235 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[kvl_cycles-severity=50]
                  time (s)          peak (MiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.06326 (1.20)   │   126.16 (1.44) 
 (v1)        0.05269 (1.0)   │     87.71 (1.0) 

benchmarks/drivers/test_build.py::test_build[masked-n=100]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.05572 (1.02)   │    715.12 (1.0) 
 (v1)        0.05456 (1.0)   │   787.73 (1.10) 

benchmarks/drivers/test_build.py::test_build[masked-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.05359 (1.08)   │   4.54 (1.27) 
 (v1)        0.04942 (1.0)   │    3.57 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=0]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.3762 (1.05)   │   704.12 (1.09) 
 (v1)        0.3572 (1.0)   │    643.85 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=100]
                 time (s)        peak (MiB) 
 name                mean   │          mean 
────────────────────────────────────────────
 (legacy)   0.3941 (1.04)   │   18.34 (1.0) 
 (v1)        0.3798 (1.0)   │   18.34 (1.0) 

benchmarks/drivers/test_build.py::test_build[merge_balance-severity=50]
               time (s)       peak (MiB) 
 name              mean   │         mean 
─────────────────────────────────────────
 (legacy)   0.39 (1.04)   │   9.54 (1.0) 
 (v1)       0.374 (1.0)   │   9.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.07643 (1.07)   │   3.77 (1.12) 
 (v1)        0.07111 (1.0)   │    3.37 (1.0) 

benchmarks/drivers/test_build.py::test_build[milp-n=50]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.07651 (1.09)   │   216.59 (1.10) 
 (v1)        0.07003 (1.0)   │    196.23 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=0]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.03872 (1.07)   │   938.49 (1.0) 
 (v1)        0.03603 (1.0)   │   938.49 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=100]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.03975 (1.09)   │   9.66 (1.0) 
 (v1)        0.03659 (1.0)   │   9.66 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.03942 (1.09)   │   5.32 (1.0) 
 (v1)         0.0362 (1.0)   │   5.32 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=0]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01952 (1.00)   │   1.28 (1.0) 
 (v1)        0.01944 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=100]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01947 (1.01)   │   1.28 (1.0) 
 (v1)         0.0193 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01948 (1.02)   │   1.28 (1.0) 
 (v1)        0.01919 (1.0)   │   1.28 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=1000]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.1884 (1.02)   │   949.04 (1.06) 
 (v1)        0.1851 (1.0)   │    893.73 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=10]
                 time (s)         peak (KiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)   0.1863 (1.05)   │   10.03 (1.07) 
 (v1)        0.1782 (1.0)   │     9.38 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04827 (1.08)   │   147.70 (1.06) 
 (v1)        0.04489 (1.0)   │    139.87 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04751 (1.07)   │   2.60 (1.09) 
 (v1)        0.04431 (1.0)   │    2.38 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=0]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.03976 (1.09)   │   696.75 (1.03) 
 (v1)        0.03637 (1.0)   │    673.70 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=100]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)    0.08343 (1.0)   │   137.97 (1.0) 
 (v1)       0.08367 (1.00)   │   137.97 (1.0) 

benchmarks/drivers/test_build.py::test_build[rolling-severity=50]
                  time (s)        peak (MiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.06422 (1.12)   │   69.22 (1.0) 
 (v1)        0.05726 (1.0)   │   69.22 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04559 (1.10)   │   402.33 (1.00) 
 (v1)        0.04161 (1.0)   │    402.30 (1.0) 

benchmarks/drivers/test_build.py::test_build[sos-n=10]
                  time (s)        peak (KiB) 
 name                 mean   │          mean 
─────────────────────────────────────────────
 (legacy)   0.04495 (1.10)   │   3.19 (1.19) 
 (v1)        0.04083 (1.0)   │    2.69 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=10]
                  time (s)         peak (KiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.05029 (1.07)   │   29.00 (1.54) 
 (v1)        0.04702 (1.0)   │    18.84 (1.0) 

benchmarks/drivers/test_build.py::test_build[sparse_network-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)   0.06051 (1.10)   │   37.95 (1.43) 
 (v1)        0.05504 (1.0)   │    26.51 (1.0) 

benchmarks/drivers/test_build.py::test_build[storage-n=10]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)    0.09397 (1.0)   │    410.93 (1.0) 
 (v1)       0.09719 (1.03)   │   427.84 (1.04) 

benchmarks/drivers/test_build.py::test_build[storage-n=250]
                 time (s)         peak (MiB) 
 name                mean   │           mean 
─────────────────────────────────────────────
 (legacy)    0.1014 (1.0)   │     9.94 (1.0) 
 (v1)       0.1038 (1.02)   │   10.22 (1.03) 

📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run.

Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped.

@FabianHofmann FabianHofmann added enhancement New feature or request math-spec YAML/text math spec: schema, from_spec/add_spec, operators, expr container labels Sep 18, 2026
@FabianHofmann
FabianHofmann marked this pull request as ready for review September 18, 2026 07:45
Drop parameters_of/walk re-exports, present/live_rows, Context.relation
and the unreachable raises; collapse coverage checks into one kind table;
move wording helpers into errors.py; share dtype helpers with io.py.
…odel.__repr__

ModelSpec._schema becomes a cached_property, and ModelSpec.drift takes the
piecewise variables and constraints Model.__repr__ has already computed.
…coverage.py

Context, Parameters and the Term/Array/Value aliases live in context.py;
amounts_of and dims_of in coverage.py. 17 modules become 14.
Renamed program/expression/predicate types, split Walk into Direction
and Partition, and switched program lookups to mapping access.
@FabianHofmann FabianHofmann mentioned this pull request Sep 22, 2026
5 tasks
…ns API

Replace the deleted piecewise-checks/derivation layer with a generic
check over Program.assumptions; handle the new predicate kinds and drop
SosDeclaration.big_m.
…extra

Lower via to_spec().expand('piecewise'), open bounds as None, sos along, Named and at() nodes.
Rows emptied by absence are dropped as mathspec specifies (tracked in #993); non-finite
arithmetic on present data is refused. Old-layout netcdf files load as plain models.
@FabianHofmann FabianHofmann changed the title Build whole models from math-spec (whole-model-only) Build whole models from mathspec (whole-model-only) Sep 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request math-spec YAML/text math spec: schema, from_spec/add_spec, operators, expr container

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants