Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ data. Requires the ``spec`` dependency group.
spec.ModelSpec
spec.NamedExpressions
spec.NamedExpression
spec.Declaration
spec.Unspecified
spec.attach
spec.Attached
spec.SpecDataError
Expand Down
4 changes: 3 additions & 1 deletion doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ Upcoming Version

* ``model.spec.expressions`` (a ``linopy.spec.NamedExpressions`` mapping) returns a ``linopy.spec.NamedExpression`` for each declared name, with three views: ``.node`` (the lowered formula), ``.expression`` (the unsolved linopy expression — a ``LinearExpression``, bare ``Variable``, array or scalar) and ``.solution`` (the expression folded over the solved model). ``model.spec.evaluate(name, sources)`` returns the same object with its parameters attached afresh.

* ``model.spec.to_latex`` / ``.to_markdown`` / ``.to_typst`` typeset the whole model, and ``model.spec.declaration(name)`` returns a ``linopy.spec.Declaration`` whose same three methods typeset one named expression, constraint or variable as a single line (math only, no document); a ``NamedExpression`` carries those methods too. A ``ModelSpec``, a ``Declaration`` and a ``NamedExpression`` all render as Markdown in a notebook.
* ``model.spec.typeset(fmt)`` typesets the spec in any format math-spec knows, with ``.to_latex`` / ``.to_markdown`` / ``.to_typst`` spelling the three it knows today, and ``model.spec.declaration(name)`` returns a ``linopy.spec.Declaration`` whose same three methods typeset one named expression, constraint or variable as a single line (math only, no document); a ``NamedExpression`` carries those methods too. A ``ModelSpec``, a ``Declaration`` and a ``NamedExpression`` all render as Markdown in a notebook.

* Typesetting renders the *spec*, which need not be the whole model: a spec-built model goes on taking everything linopy can add to it, and none of that carries a math-spec declaration to typeset. ``model.spec.unspecified`` (a ``linopy.spec.Unspecified``) reports the drift -- variables, constraints and expressions the spec does not declare, special-ordered sets it does not declare, piecewise formulations added beside it, and whether ``add_objective`` has replaced its objective. Where there is any, typesetting warns, opens the rendered text with a comment of the format's own -- gone once compiled, there in the source -- and adds a visible note to the Markdown a notebook displays. A spec's own ``piecewise:`` and ``sos:`` are not drift: math-spec lowers them into ordinary declarations, which typeset like any other.


*Numerical scaling*
Expand Down
4 changes: 2 additions & 2 deletions examples/building-models-from-specs.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@
"cell_type": "markdown",
"id": "16",
"metadata": {},
"source": "### The model as maths\n\nThe accessor typesets the whole model, delegating to math-spec:\n`m.spec.to_latex()`, `.to_markdown()` and `.to_typst()`. In a notebook the\naccessor renders as Markdown on its own; here we show it explicitly.\n\nAny single declaration typesets on its own too. `m.spec.declaration(name)`\ntakes a named expression, a constraint or a variable and hands back a\n`Declaration` with the same three methods; a `NamedExpression` carries them\ndirectly. These render **one** line — math only, no surrounding document — so\nthe string drops straight into a docstring or a table cell, and both a\n`Declaration` and a `NamedExpression` render as their own formula in a notebook."
"source": "### The spec as maths\n\nThe accessor typesets the spec, delegating to math-spec: `m.spec.typeset(fmt)`\nfor any format math-spec knows, with `m.spec.to_latex()`, `.to_markdown()` and\n`.to_typst()` spelling the three it knows today. In a notebook the accessor\nrenders as Markdown on its own; here we show it explicitly.\n\nThe **spec**, which need not be the whole model: a spec-built model goes on\ntaking everything linopy can add to it, and none of that carries a math-spec\ndeclaration to typeset. `m.spec.unspecified` reports the drift — variables,\nconstraints and expressions the spec does not declare, special-ordered sets it\ndoes not declare, piecewise formulations added beside it, and whether\n`add_objective` has replaced its objective, which is the one that makes the\nrender *wrong* rather than incomplete. Where there is any, typesetting warns\nand opens the rendered text with a comment of the format's own — `%` in LaTeX,\ngone once compiled, there in the source.\n\nA spec's own `piecewise:` and `sos:` are not drift: math-spec lowers them into\nordinary declarations, which typeset like any other.\n\nAny single declaration typesets on its own too. `m.spec.declaration(name)`\ntakes a named expression, a constraint or a variable and hands back a\n`Declaration` with the same methods; a `NamedExpression` carries them\ndirectly. A declaration is reached through the spec, so it can never be out of\nstep with the model the way the whole-spec render can. These render **one** line — math only, no surrounding document — so\nthe string drops straight into a docstring or a table cell, and both a\n`Declaration` and a `NamedExpression` render as their own formula in a notebook."
},
{
"cell_type": "code",
Expand Down Expand Up @@ -943,7 +943,7 @@
"cell_type": "markdown",
"id": "49",
"metadata": {},
"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."
"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 spec (`m.spec.typeset`, with `to_latex` / `to_markdown` /\n `to_typst` as its named formats), the drift `m.spec.unspecified` reports,\n 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": {
Expand Down
15 changes: 9 additions & 6 deletions linopy/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,12 +716,11 @@ def __repr__(self) -> str:
if self._spec is not None:
model_string += ", built from a math-spec"
program = self._spec.program
spec_vars = set(program.variables)
spec_cons = set(program.constraints)
if any(v not in spec_vars for v in self.variables):
var_tag = spec_vars
if any(c not in spec_cons for c in self.constraints):
con_tag = spec_cons
unspecified = self._spec.unspecified
if unspecified.variables:
var_tag = set(program.variables)
if unspecified.constraints:
con_tag = set(program.constraints)
eager = expr_string if len(self.expressions) else ""
spec = "".join(
f" * {name} ({', '.join(e.dims)}) [spec]\n"
Expand Down Expand Up @@ -1628,6 +1627,10 @@ def add_objective(
self.objective.expression = expr
self.objective.sense = sense
self.objective.scaling = scaling
if self._spec is not None:
# A spec sets its objective through here during its own build,
# while `_spec` is still unset, so only a later call reaches this.
self._spec._objective_replaced = True

def remove_variables(self, name: str) -> None:
"""
Expand Down
2 changes: 2 additions & 0 deletions linopy/spec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NamedExpression,
NamedExpressions,
SpecLike,
Unspecified,
)
from linopy.spec.attach import Attached, Retain, attach
from linopy.spec.errors import SpecDataError
Expand All @@ -35,5 +36,6 @@
"Retain",
"SpecDataError",
"SpecLike",
"Unspecified",
"attach",
]
Loading
Loading