From 608e1410abb3c595b6d07aacd2c276bc11da074c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:45:13 +0200 Subject: [PATCH] chore(spec): pin math-spec alpha.76 and drop the Power walk workaround math-spec's `program.children()` had no branch for `Power`, so every walk stopped there and a parameter written `d ** 2` was invisible to it. linopy worked around it with its own `children()` in `linopy/spec/nodes.py`. energy-models/math-spec#404 fixed it upstream, so the `spec` group pins the release that carries it, `v0.0.0-alpha.76`, and the walks call `math_spec.program.children` directly. The coverage tests for a constant side and a divisor hidden under a power stay as they are and now exercise the upstream walk. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_017AvhrToZD5bch9Zcdtiduo --- examples/building-models-from-specs.ipynb | 2 +- linopy/spec/coverage.py | 4 ++-- linopy/spec/nodes.py | 13 +++---------- pyproject.toml | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/examples/building-models-from-specs.ipynb b/examples/building-models-from-specs.ipynb index 0b6bdfcb..09495cd4 100644 --- a/examples/building-models-from-specs.ipynb +++ b/examples/building-models-from-specs.ipynb @@ -925,7 +925,7 @@ "cell_type": "markdown", "id": "49", "metadata": {}, - "source": "## Where the code lives, and an upstream note\n\nThe feature is a small package, `linopy/spec/`, imported only when you call\n`add_spec`/`from_spec` — `import linopy` never pulls in `math_spec`. Roughly:\n\n- `accessor.py` — `model.spec`, the `NamedExpression` views, `evaluate`, and\n typesetting: the whole model (`m.spec.to_latex` / `.to_markdown` /\n `.to_typst`) and any single declaration — a named expression, constraint or\n variable — via `m.spec.declaration(name)` and math-spec's\n `typeset_declaration`.\n- `attach.py` — the three attachment rules; data onto master coordinates.\n- `builder.py` — emits variables, constraints, objective; folds expressions.\n- `operators.py` — `sum`, `by=`, `shift`, `at`, `sum_back`.\n- `where.py` — `where:` predicates as boolean masks.\n- `coverage.py` / `terms.py` — the absence rule from section 6: a missing row\n is refused wherever it is used.\n- `curves.py` — the data side of `piecewise:` blocks.\n- `netcdf.py` — the factorize-based persistence from section 10.\n- `nodes.py` — walks over expression nodes. One workaround lives here:\n math-spec alpha.73's `program.children()` does not descend into a `Power`\n node, so parameters hidden under `**` would be missed; `nodes.py` walks into\n the base and exponent itself.\n\n### Summary\n\nA spec is the maths over labelled axes; the sources are the numbers. `linopy`\nattaches them into an ordinary model, hands each named expression back as three\nviews — its formula, its unsolved linopy expression and its solution — refuses a\nmissing parameter row wherever it is used (as a coefficient, bound, constant\nside or divisor alike, with `where:` and filling the data as the escape\nhatches), and round-trips the lot through netCDF by keeping the spec as text\nbeside factorized labels." + "source": "## Where the code lives\n\nThe feature is a small package, `linopy/spec/`, imported only when you call\n`add_spec`/`from_spec` — `import linopy` never pulls in `math_spec`. Roughly:\n\n- `accessor.py` — `model.spec`, the `NamedExpression` views, `evaluate`, and\n typesetting: the whole model (`m.spec.to_latex` / `.to_markdown` /\n `.to_typst`) and any single declaration — a named expression, constraint or\n variable — via `m.spec.declaration(name)` and math-spec's\n `typeset_declaration`.\n- `attach.py` — the three attachment rules; data onto master coordinates.\n- `builder.py` — emits variables, constraints, objective; folds expressions.\n- `operators.py` — `sum`, `by=`, `shift`, `at`, `sum_back`.\n- `where.py` — `where:` predicates as boolean masks.\n- `coverage.py` / `terms.py` — the absence rule from section 6: a missing row\n is refused wherever it is used.\n- `curves.py` — the data side of `piecewise:` blocks.\n- `netcdf.py` — the factorize-based persistence from section 10.\n- `nodes.py` — walks over expression nodes, and the dimensions a node\n spans before any data is bound.\n\n### Summary\n\nA spec is the maths over labelled axes; the sources are the numbers. `linopy`\nattaches them into an ordinary model, hands each named expression back as three\nviews — its formula, its unsolved linopy expression and its solution — refuses a\nmissing parameter row wherever it is used (as a coefficient, bound, constant\nside or divisor alike, with `where:` and filling the data as the escape\nhatches), and round-trips the lot through netCDF by keeping the spec as text\nbeside factorized labels." } ], "metadata": { diff --git a/linopy/spec/coverage.py b/linopy/spec/coverage.py index 401c6898..11da5e04 100644 --- a/linopy/spec/coverage.py +++ b/linopy/spec/coverage.py @@ -21,7 +21,7 @@ from linopy.spec import terms from linopy.spec.context import Context from linopy.spec.errors import SpecDataError -from linopy.spec.nodes import amounts_of, children, parameters_of +from linopy.spec.nodes import amounts_of, parameters_of from linopy.spec.where import evaluate_where Rows = xr.DataArray | None @@ -101,7 +101,7 @@ def _collect( narrowed = inside if rows is None else rows & inside _collect(region.value, ctx, narrowed, constant, into) return - for child in children(node): + for child in ms.children(node): _collect(child, ctx, rows, constant, into) diff --git a/linopy/spec/nodes.py b/linopy/spec/nodes.py index 7c158a4e..9c95c968 100644 --- a/linopy/spec/nodes.py +++ b/linopy/spec/nodes.py @@ -1,4 +1,4 @@ -"""Walks over expression nodes that descend into every operand, a ``Power``'s included.""" +"""Walks over a program's expression nodes, and the dimensions a node spans.""" from __future__ import annotations @@ -7,18 +7,11 @@ from math_spec import program as ms -def children(node: ms.ExpressionNode) -> tuple[ms.ExpressionNode, ...]: - """The operands of *node*: ``math_spec.program.children`` plus a power's base and exponent.""" - if isinstance(node, ms.Power): - return (node.base, node.exponent) - return ms.children(node) - - def walk(*nodes: ms.ExpressionNode) -> Iterator[ms.ExpressionNode]: """Every node under *nodes*, each of them included, parents first.""" for node in nodes: yield node - yield from walk(*children(node)) + yield from walk(*ms.children(node)) def amounts_of(node: ms.ExpressionNode) -> Iterator[str]: @@ -55,4 +48,4 @@ def _dims(node: ms.ExpressionNode, program: ms.Program) -> frozenset[str]: return (_dims(node.operand, program) - {node.over}) | set(node.into) if isinstance(node, ms.Cases): return frozenset().union(*(_dims(r.value, program) for r in node.regions)) - return frozenset().union(*(_dims(c, program) for c in children(node))) + return frozenset().union(*(_dims(c, program) for c in ms.children(node))) diff --git a/pyproject.toml b/pyproject.toml index 482334e6..f8b3ec14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ gpu = [ # keeps the git pin out of the published wheel metadata, which PyPI rejects. # Install with `uv sync --group spec` or `uv pip install --group spec`. spec = [ - "math-spec @ git+https://github.com/energy-models/math-spec.git@67aeedb988ee95d196456b4cbe50821e3799edaa ; python_version >= '3.12'", + "math-spec @ git+https://github.com/energy-models/math-spec.git@v0.0.0-alpha.76 ; python_version >= '3.12'", "pyyaml ; python_version >= '3.12'", "pyarrow ; python_version >= '3.12'", ]