Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
28ac1ad
feat(spec): xarray data binder for math-spec programs
FabianHofmann Sep 3, 2026
a4e6dc3
refactor(spec): compact binder and tests; report duplicate coordinate…
FabianHofmann Sep 3, 2026
9bf025a
fix(spec): review fixes for the binder
FabianHofmann Sep 3, 2026
5610909
ci(spec): skip doctest collection of linopy/spec without math-spec; t…
FabianHofmann Sep 3, 2026
cc69e5a
ci(spec): install math-spec from a pinned git commit via a dependency…
FabianHofmann Sep 3, 2026
27c9ab5
doc: mention the spec dependency group in the contributing guide
FabianHofmann Sep 3, 2026
9b7f3ad
feat(spec): build models from math-spec programs and fold named expre…
FabianHofmann Sep 3, 2026
c121d97
fix(spec): walk into powers, refuse relabelled evaluate sources, hard…
FabianHofmann Sep 3, 2026
399908a
feat(spec): round trip a spec-built model through netcdf
FabianHofmann Sep 3, 2026
e0a29c9
fix(spec): keep parameter dtypes and one coordinate dtype per dimension
FabianHofmann Sep 3, 2026
b947409
feat(spec): refuse a missing parameter row wherever it is used
FabianHofmann Sep 4, 2026
2ae9df9
doc(spec): add a notebook building models from specs
FabianHofmann Sep 4, 2026
c936682
feat(spec): expose a named expression as three views
FabianHofmann Sep 4, 2026
494becf
Merge branch 'master' into spec-binder
FabianHofmann Sep 4, 2026
2a33908
Merge branch 'spec-binder' into spec-builder
FabianHofmann Sep 4, 2026
4875f8f
ci: skip spec notebook until math-spec is on PyPI
FabianHofmann Sep 4, 2026
8147de7
Merge branch 'master' into spec-builder
FabianHofmann Sep 7, 2026
aafba16
wip: datarecord + pypsa dependency groups
FabianHofmann Sep 7, 2026
2427112
Merge remote-tracking branch 'origin/master' into spec-builder
FabianHofmann Sep 7, 2026
42a6d79
Merge remote-tracking branch 'origin/master' into spec-builder
FabianHofmann Sep 7, 2026
4bca6d6
Merge branch 'master' into spec-builder
FabianHofmann Sep 7, 2026
ef354af
Merge remote-tracking branch 'origin/spec-builder' into spec-builder
FabianHofmann Sep 7, 2026
e6623af
fix(spec): restamp CSRConstraint grid after the csr module refactor (…
FabianHofmann Sep 7, 2026
0254d47
fix(spec): bind pandas extension strings as numpy object arrays
FabianHofmann Sep 7, 2026
3776e66
ci(spec): type-check with math-spec installed; declare pyyaml and pya…
FabianHofmann Sep 7, 2026
0cfbb3a
refac(spec): one owner per seam; generic netcdf dtype and coordinate …
FabianHofmann Sep 7, 2026
966e0b5
refac(spec): evaluator in its own module; coverage checked in one tre…
FabianHofmann Sep 7, 2026
88fe411
test(spec): split builder tests into accessor, operators and curves; …
FabianHofmann Sep 7, 2026
2cb5c9f
doc(spec): wire the notebook and API into the docs; run the notebook …
FabianHofmann Sep 7, 2026
d5bd61a
feat(spec): emit EvolvingAPIWarning once per session from add_spec, f…
FabianHofmann Sep 7, 2026
d2f82a6
ci(docs): install spec group on Read the Docs
FabianHofmann Sep 8, 2026
b774e77
refac(spec): rename data attachment to attach/Attached
FabianHofmann Sep 8, 2026
46c6f27
doc(spec): execute the spec notebook on Read the Docs
FabianHofmann Sep 8, 2026
b7a8115
refac(spec): track math-spec a75, unwrapping ExpressionDeclaration
FabianHofmann Sep 8, 2026
f243de2
feat(spec): read a constraint's dual in a reported expression
FabianHofmann Sep 8, 2026
2b12d10
feat(spec): typeset a single named expression
FabianHofmann Sep 8, 2026
74de654
feat(spec): typeset any declaration, not just named expressions
FabianHofmann Sep 8, 2026
84e35ab
feat(spec): summarise the whole model in the ModelSpec repr
FabianHofmann Sep 8, 2026
8b88ac7
feat(spec): show the spec in the Model repr
FabianHofmann Sep 8, 2026
52edc8a
chore(spec): pin math-spec alpha.76 and drop the Power walk workaroun…
FBumann Sep 8, 2026
48b267d
refac(spec): the spec owns its parameters, and one read path for a na…
FBumann Sep 8, 2026
87b2ff1
fix(spec): four fixes from reviewing the spec builder (#950)
FBumann Sep 8, 2026
b1f4a02
Typeset by format, and say what the render leaves out (#951)
FBumann Sep 10, 2026
ed8855f
Extend a built model with spec layers that bind existing variables (#…
FabianHofmann Sep 15, 2026
b1512ea
test(spec): cap the fixture below the hand-built bound so the bound l…
FabianHofmann Sep 10, 2026
623a6c0
feat(spec): name bound variables in the render and refuse removing wh…
FabianHofmann Sep 14, 2026
9b8a62d
feat(spec): stamp what a layer builds and hold its named expressions …
FabianHofmann Sep 15, 2026
b23c536
chore(spec): align with math-spec alpha.88
FabianHofmann Sep 15, 2026
996efb8
fix(spec): tell a dual with no symbolic form from one not solved yet
FabianHofmann Sep 15, 2026
0aa9d80
feat(spec): check dimension labels and refuse a foreign binding
FabianHofmann Sep 15, 2026
793e9b9
fix(spec): refuse a constraint row the data emptied of variables
FabianHofmann Sep 15, 2026
47dd157
feat(spec): refuse what linopy cannot build instead of guessing
FabianHofmann Sep 15, 2026
1bbe761
feat(spec): one normalize_spec for every spec form, and a layer name …
FabianHofmann Sep 15, 2026
060e19e
feat(spec): a Sources adapter that reads by key and reports what noth…
FabianHofmann Sep 15, 2026
25d9e37
feat(spec): an Ownership registry the model containers consult
FabianHofmann Sep 15, 2026
84d7837
feat(spec): Model.remove_spec takes a layer off, and layers are read-…
FabianHofmann Sep 15, 2026
59cf731
feat(spec): a version header in the netcdf file, and a plain-model fa…
FabianHofmann Sep 15, 2026
e38ce52
fix(spec): refuse a list removal as a whole before dropping any name
FabianHofmann Sep 15, 2026
7135b82
docs(spec): install text, notebook fixes, sparse docs, grammar and dr…
FabianHofmann Sep 15, 2026
005389f
fix(spec): verb agreement in the removal refusal, and the test it mat…
FabianHofmann Sep 15, 2026
6950713
docs(spec): boil the release notes down to four bullets
FabianHofmann Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-notebooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Install package and dependencies
run: |
python -m pip install uv
uv pip install --system -e ".[docs]"
uv pip install --system -e ".[docs]" --group spec

- name: Execute notebooks
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
- name: Install package and dependencies
run: |
python -m pip install uv
uv pip install --system "$(ls dist/*.whl)[dev,solvers,oetc]"
uv pip install --system "$(ls dist/*.whl)[dev,solvers,oetc]" --group spec

- name: Test with pytest
env:
Expand Down Expand Up @@ -120,7 +120,7 @@ jobs:
- name: Install package and dependencies
run: |
python -m pip install uv
uv pip install --system "$(ls dist/*.whl)[dev]"
uv pip install --system "$(ls dist/*.whl)[dev]" --group spec

- name: Run type checker (mypy)
run: |
Expand Down
6 changes: 6 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ build:
jobs:
pre_system_dependencies:
- git fetch --unshallow # Needed to get version tags
post_install:
# The spec API is documented via autodoc, which imports linopy.spec and so
# needs math-spec. It lives in the `spec` dependency group, not the docs
# extra; --group needs pip >= 25.1, hence the upgrade.
- python -m pip install --upgrade pip
- python -m pip install --group spec
python:
install:
- method: pip
Expand Down
1 change: 1 addition & 0 deletions benchmarks/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
qp,
sos,
sparse_network,
spec_pypsa,
storage,
)
55 changes: 55 additions & 0 deletions benchmarks/models/spec_pypsa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Model built from math-spec's ``pypsa.yaml`` example (requires math-spec).

The subject is :meth:`linopy.Model.from_spec`: lowering a spec of PyPSA's full
statement, binding synthetic data to it and building every variable and
constraint it declares. The example lives outside the wheel, so its directory
comes from ``MATH_SPEC_EXAMPLES`` and the case skips without it. A sweep
value is the number of labels per dimension; 40 of them is about 20k
variables.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import TYPE_CHECKING

from benchmarks.registry import BUILD, FROM_NETCDF, TO_NETCDF, BenchSpec, register

if TYPE_CHECKING:
import linopy

SIZES = (5, 40)

EXAMPLES = os.environ.get("MATH_SPEC_EXAMPLES")
EXAMPLE = Path(EXAMPLES, "pypsa.yaml") if EXAMPLES else None


def build_spec_pypsa(n: int) -> linopy.Model:
"""Lower ``pypsa.yaml`` and build it with ``n`` labels per dimension."""
import pytest

if EXAMPLE is None or not EXAMPLE.exists():
pytest.skip("set MATH_SPEC_EXAMPLES to a math-spec examples directory")
import math_spec

import linopy
from linopy.spec.testing import synthetic_sources

path = str(EXAMPLE)
sources = synthetic_sources(math_spec.to_program(path), n)
with linopy.options as options:
options["semantics"] = "v1"
return linopy.Model.from_spec(path, sources)


SPEC = register(
BenchSpec(
name="spec_pypsa",
build=build_spec_pypsa,
sweep=SIZES,
phases=frozenset({BUILD, TO_NETCDF, FROM_NETCDF}),
requires=("math_spec",),
)
)
9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Root pytest configuration for ``--doctest-modules`` collection of ``linopy/``."""

from __future__ import annotations

from importlib.util import find_spec

collect_ignore: list[str] = []
if find_spec("math_spec") is None:
collect_ignore.append("linopy/spec")
30 changes: 30 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,36 @@ IO
model.Model.to_netcdf
io.read_netcdf

Building from specs
-------------------

Build a model from a `math-spec
<https://github.com/energy-models/math-spec>`__ YAML program attached to
data. Requires the ``spec`` dependency group.

A spec's grouped sums and windows build dense by default, which is wasteful on
a skewed topology (a lookup with a few large groups and many small ones, or a
wide ``sum_back`` window). Set ``linopy.options["sparse_groupby"] = True`` (v1
semantics) to back grouped sums with :mod:`linopy.csr`, and build the model
with ``Model(freeze_constraints=True)`` to keep the constraints CSR-backed
instead of densifying them.

.. autosummary::
:toctree: generated/

model.Model.add_spec
model.Model.from_spec
model.Model.spec
spec.ModelSpec
spec.Layer
spec.NamedExpressions
spec.NamedExpression
spec.Declaration
spec.Unspecified
spec.attach
spec.Attached
spec.SpecDataError


Variable
========
Expand Down
3 changes: 3 additions & 0 deletions doc/building-models-from-specs.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../examples/building-models-from-specs.ipynb"
}
3 changes: 3 additions & 0 deletions doc/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ To run the test suite:
# Install development dependencies
uv sync --extra dev --extra solvers

# Also run the math-spec binder tests (needs Python >= 3.12)
uv sync --extra dev --extra solvers --group spec

# Run all tests
pytest

Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ This package is published under MIT license.
coordinate-alignment
migrating-to-v1
manipulating-models
building-models-from-specs

.. toctree::
:hidden:
Expand Down
11 changes: 11 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ Upcoming Version
* Every operation whose result changes under v1 emits a ``LinopySemanticsWarning`` under legacy, naming the fix — so a model can be migrated incrementally before opting in. The full rules are specified in :doc:`the arithmetic convention <design/convention>`.


*Build a model from a math-spec program*

* ``Model.from_spec`` / ``model.add_spec`` build a model from a `math-spec <https://github.com/energy-models/math-spec>`__ program attached to data, and ``model.spec`` reads it back as named layers, each with its ``program``, ``parameters``, ``coords`` and ``lookups``. Requires the ``spec`` dependency group (``uv sync --group spec``, Python >= 3.12) and v1 semantics. The API emits an :class:`linopy.EvolvingAPIWarning` once per session while it stabilises. See :doc:`building-models-from-specs`.

* A spec can extend a hand-built model: a ``Variable`` passed in ``sources`` under a declared name binds it instead of building one. Everything a layer builds carries its layer name in ``.spec``; ``model.remove_spec(name)`` unwinds a layer, and removing a spec-owned name any other way is refused. ``model.spec.unspecified`` reports what the model holds beyond its specs.

* ``model.spec.expressions`` evaluates the spec's named expressions against the solved model, and ``model.spec.typeset`` (``to_latex`` / ``to_markdown`` / ``to_typst``) renders the spec, warning where the model has drifted from it.

* Spec models round-trip through ``to_netcdf`` / ``read_netcdf``; a file read without ``math-spec`` installed loads as a plain model with a warning.


*Numerical scaling*

* Variables, constraints and the objective accept a ``scaling`` factor that rewrites the problem into better-behaved units for the solver, without changing the answer. Variable scaling is column-like, constraint and objective scaling are row-like, and primal values, duals and the objective are transformed back to the original units after solving. See the :doc:`numerical-scaling` tutorial and the *Numerical scaling* section of the :doc:`user-guide`.
Expand Down
14 changes: 14 additions & 0 deletions doc/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ bound, swap a constraint, or copy it for what-if analysis.
variables.


Building a model from a spec
-----------------------------

Instead of calling ``add_variables`` / ``add_constraints`` directly,
you can declare a model as a `math-spec
<https://github.com/energy-models/math-spec>`__ YAML program attached to
data, and let linopy build it.

- :doc:`building-models-from-specs` — ``Model.from_spec`` and
``model.add_spec``, attaching data to a spec, and reading named
expressions back through ``model.spec`` after solving. Requires the
``spec`` dependency group and v1 semantics.


Where to go next
----------------

Expand Down
Loading
Loading