Skip to content

Build and change models via math-spec - #922

Draft
FabianHofmann wants to merge 39 commits into
masterfrom
spec-builder
Draft

Build and change models via math-spec#922
FabianHofmann wants to merge 39 commits into
masterfrom
spec-builder

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Follow the plan in #919; support math-spec for model definition and modification.

Note

The following content was generated by AI.

Changes proposed in this Pull Request

lpspec's linopy lane (fluxopt/lpspec#1518) ported into linopy/spec/, typed, on top of the data attachment previously drafted in #921 (merged into this PR). math-spec is pinned to a git commit (alpha.75) through the PEP 735 dependency group spec, Python >= 3.12; import linopy never imports math_spec, the model methods import it lazily.

Building

  • Model.add_spec(spec, sources, retain="report") builds variables, SOS, constraints and objective into an empty v1 model; Model.from_spec(spec, sources, retain=..., **model_kwargs) is sugar over it. spec is a path, YAML text, dict or math_spec.Spec. Both raise under legacy semantics or on a non-empty model, and emit EvolvingAPIWarning once per session while the API stabilises.
  • 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 (a lazy view over PyPSA's n.c.<component>.da works) or a single xr.Dataset. math-spec's binding rules are enforced: members only from the dimension's own source, source order never sorted, parameters never read for labels. 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 (coefficient, bound, constant side, divisor); mask the coordinate out with a where or fill the data. Divisor coverage is checked on named-expression bodies, unaligned operands raise SpecDataError, a constraint with its term on the right is swapped with a flipped sense.
  • Modules: attach, builder (declarations), evaluate (recursive evaluator, fold), coverage (one tree walk collecting divisor, constant-side and coefficient obligations), operators (sum_over, grouped_sum, at, shift, sum_back), where, curves (piecewise), terms, parameters (resolution and derivation), groups, context, nodes (node walk that also descends into Power, which math-spec's children() skips; static dims_of), netcdf, testing (synthetic_sources).

Reading back

  • model.spec (ModelSpec) holds the Program and the spec text and reads coordinates, lookups and retained parameters off model.parameters. Its repr summarises dimensions, variables, constraints, objective and named expressions.
  • model.spec.expressions[name] is a NamedExpression with .node (lowered formula), .dims (static, no data bound), .expression (the unsolved linopy expression) and .solution (the fold over the solved model, substituting Variable.solution, retained parameters and, since alpha.75, a constraint's dual). model.spec.evaluate(name, sources) returns the same object with parameters attached afresh (for retain="none"), refusing sources labelled unlike the model.
  • Typesetting: model.spec.to_latex/to_markdown/to_typst render the whole model; model.spec.declaration(name) and a NamedExpression render one declaration as a bare line. All three render as Markdown in a notebook.
  • The Model repr says the model is built from a math-spec, prints the spec description, lists named expressions with their dims and tags spec-owned items [spec] once hand-added variables or constraints exist.

Persistence

  • to_netcdf writes the spec text as the top-level attribute _linopy_spec; read_netcdf re-lowers the Program from it, and a file without a spec loads without math-spec installed.
  • A spec- sub-dataset carries master coordinates and lookups plus object-dtype parameters as pandas.factorize codes and categories, so partial maps keep holes and dtypes on both netcdf engines. Every parameter records its in-memory dtype and is cast back on read; master coordinates are stamped onto every container. Model.copy() carries the spec; assert_model_equal compares spec text and parameter datasets including dtypes.
  • pandas extension strings (Arrow-backed under pandas 3) are attached as numpy object arrays.

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 group installed. API pages for linopy.spec, release notes added.
  • The 3.12 and 3.13 test jobs install the spec group; mypy runs with math-spec installed. Tests skip without it.
  • benchmarks/models/spec_pypsa.py: from_spec on math-spec's examples/pypsa.yaml plus the two netcdf phases; skips without math-spec or MATH_SPEC_EXAMPLES.

Not ported / upstream

  • lpspec's reads_off_the_solution grade predicate (linopy always folds) and the polars loader (replaced by attach). Variable divisors and degree-2 named expressions are refused by math-spec at lowering.
  • math-spec program.children() does not descend into Power, worked around in linopy/spec/nodes.py.
Verification (8b88ac7)
uv run pytest test/test_spec_*.py test/test_io.py -q
520 passed, 190 skipped
uv run mypy linopy benchmarks
Success: no issues found in 82 source files

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.

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.
@github-actions

github-actions Bot commented Sep 3, 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.07784 (1.09)   │   15.03 (1.00) 
 (v1)        0.07112 (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.08402 (1.09)   │   12.04 (1.00) 
 (v1)        0.07713 (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.034 (1.08)   │   15.20 (1.0) 
 (v1)       0.03138 (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.05018 (1.07)   │   44.93 (1.0) 
 (v1)        0.04711 (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.03699 (1.08)   │   11.51 (1.0) 
 (v1)        0.03418 (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.08831 (1.08)   │   24.34 (1.06) 
 (v1)        0.08153 (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.09665 (1.07)   │   16.12 (1.00) 
 (v1)        0.09017 (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.02083 (1.08)   │   752.18 (1.10) 
 (v1)        0.01926 (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.02034 (1.07)   │   3.12 (1.33) 
 (v1)        0.01895 (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.05956 (1.25)   │   126.16 (1.44) 
 (v1)        0.04748 (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.05806 (1.25)   │   126.16 (1.44) 
 (v1)         0.0464 (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.05859 (1.25)   │   126.16 (1.44) 
 (v1)        0.04702 (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.04769 (1.03)   │    715.12 (1.0) 
 (v1)        0.04635 (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.0464 (1.10)   │   4.54 (1.27) 
 (v1)        0.0422 (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.3257 (1.04)   │   704.12 (1.09) 
 (v1)         0.313 (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.3428 (1.04)   │   18.34 (1.0) 
 (v1)        0.3299 (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.3395 (1.05)   │   9.54 (1.0) 
 (v1)        0.3221 (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.06614 (1.11)   │   3.77 (1.12) 
 (v1)        0.05983 (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.06669 (1.11)   │   216.59 (1.10) 
 (v1)        0.05985 (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.0338 (1.09)   │   938.49 (1.0) 
 (v1)       0.03092 (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.03499 (1.09)   │   9.66 (1.0) 
 (v1)        0.03208 (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.03444 (1.10)   │   5.32 (1.0) 
 (v1)        0.03138 (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.01893 (1.00)   │   1.47 (1.0) 
 (v1)        0.01886 (1.0)   │   1.47 (1.0) 

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

benchmarks/drivers/test_build.py::test_build[nodal_balance_sparse-severity=50]
                  time (s)       peak (MiB) 
 name                 mean   │         mean 
────────────────────────────────────────────
 (legacy)   0.01883 (1.00)   │   1.47 (1.0) 
 (v1)        0.01878 (1.0)   │   1.47 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=1000]
                 time (s)          peak (KiB) 
 name                mean   │            mean 
──────────────────────────────────────────────
 (legacy)   0.1655 (1.07)   │   946.85 (1.06) 
 (v1)        0.1547 (1.0)   │    891.54 (1.0) 

benchmarks/drivers/test_build.py::test_build[piecewise-n=10]
                time (s)         peak (KiB) 
 name               mean   │           mean 
────────────────────────────────────────────
 (legacy)   0.162 (1.06)   │   12.01 (1.00) 
 (v1)       0.1529 (1.0)   │    11.99 (1.0) 

benchmarks/drivers/test_build.py::test_build[qp-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04186 (1.08)   │   147.70 (1.06) 
 (v1)        0.03879 (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.04166 (1.08)   │   2.60 (1.09) 
 (v1)        0.03855 (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.03457 (1.09)   │   696.75 (1.03) 
 (v1)        0.03166 (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.08081 (1.09)   │   137.97 (1.0) 
 (v1)        0.07425 (1.0)   │   137.97 (1.0) 

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

benchmarks/drivers/test_build.py::test_build[sos-n=1000]
                  time (s)          peak (KiB) 
 name                 mean   │            mean 
───────────────────────────────────────────────
 (legacy)   0.04013 (1.11)   │   402.33 (1.00) 
 (v1)        0.03612 (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.03959 (1.11)   │   3.19 (1.19) 
 (v1)        0.03554 (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.04352 (1.06)   │   29.00 (1.54) 
 (v1)        0.04111 (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.05383 (1.13)   │   37.95 (1.43) 
 (v1)         0.0475 (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.0838 (1.01)   │    410.93 (1.0) 
 (v1)       0.08332 (1.0)   │   427.84 (1.04) 

benchmarks/drivers/test_build.py::test_build[storage-n=250]
                  time (s)         peak (MiB) 
 name                 mean   │           mean 
──────────────────────────────────────────────
 (legacy)    0.08824 (1.0)   │     9.94 (1.0) 
 (v1)       0.09035 (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 changed the title Build models from math-spec programs and fold named expressions Build models from math-spec programs, fold named expressions, round trip through netcdf Sep 3, 2026
… 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.
@FabianHofmann
FabianHofmann changed the base branch from spec-binder to master September 7, 2026 07:40
@FabianHofmann FabianHofmann changed the title Build models from math-spec programs, fold named expressions, round trip through netcdf Build and change models via math-spec Sep 7, 2026
@codspeed-hq

codspeed-hq Bot commented Sep 7, 2026

Copy link
Copy Markdown

Merging this PR will regress 2 benchmarks

⚠️ 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

⚡ 4 improved benchmarks
❌ 2 regressed benchmarks
✅ 175 untouched benchmarks
⏩ 181 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_to_lp[merge_balance-severity=0] 2.7 MB 3.3 MB -18.23%
test_to_lp[nodal_balance-severity=50] 3.3 MB 3.7 MB -10.82%
test_to_lp[sparse_network-n=10] 1,338.9 KB 733.8 KB +82.47%
test_to_lp[storage-n=250] 37 MB 30.2 MB +22.52%
test_to_lp[rolling-severity=0] 3.1 MB 2.8 MB +12.37%
test_to_lp[rolling-severity=50] 429.9 MB 390.7 MB +10.02%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing spec-builder (8b88ac7) with master (718c0c1)

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.

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.
autodoc imports linopy.spec, which needs math-spec. RTD installed only
the docs extra, so the Sphinx build failed with ImportError. Add a
post_install job installing the spec dependency group (pip >= 25.1).
lpspec reserves "bound" for a variable/constraint limit and calls the
data operation "attach". Rename bind()->attach(), Bound->Attached and
binder.py->attach.py so "bound" names one thing. Variable-limit names
(_bound, check_bounds_cover) stay untouched.
RTD now installs the spec group, so the notebook can import math-spec
and solve. Drop `nbsphinx.execute: never` so it runs at build time and
its outputs render (nbstripout keeps them out of git).
named_expressions now maps each name to an ExpressionDeclaration that
carries the body and whether the math reads it, so read .expression at
the five sites that take a body: the evaluator, the builder's coverage
check, the report closure and the accessor's node view.
math-spec a75 lets a reported expression hold a dual(constraint) node.
Evaluate it as the solved constraint's dual over its own frame, and
refuse it before a solve the way an unsolved variable is refused.
Add to_latex/to_markdown/to_typst on NamedExpression via math-spec's
typeset_declaration, rendering one expression as a bare line, and fix
its _repr_markdown_ to show only itself. Tests, release note and the
notebook updated.
Extract a Declaration base carrying to_latex/to_markdown/to_typst (reused by
NamedExpression) and add ModelSpec.declaration(name) to typeset a named
expression, constraint or variable as one bare line. Tests, release note and
the notebook updated.
Show dimensions, variables, constraints, objective and named
expressions, one capped line each, instead of only the expression
names.
Header says the model is built from a math-spec and prints the spec's
description. Spec named expressions are listed with their static dims;
spec-owned items are tagged [spec] once hand-added ones exist.
Comment thread linopy/model.py
)
return self._spec

def add_spec(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FabianHofmann This method is doing the same as Model.from_spec(), but with a misleading name.

WHat this method is trying to do seems much harder than building a linopy Model from scratch.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants