feat(language): a file says what it reads, and fragments and patches compose into one model - #571
Closed
FabianHofmann wants to merge 11 commits into
Closed
FabianHofmann wants to merge 11 commits into
FabianHofmann wants to merge 11 commits into
Conversation
A file declares the columns and row families it reads and does not build under one closed key, given: variables: and given: constraints:. A given name is in the namespace every expression resolves against, has a frame for the dimension check, and dual() may name a given row family. The file typesets on its own, with a Given legend group. The program carries the declarations under Program.given, lowering does not refuse, and advice gains the kind given, one note per declaration a consumer binds. A dimension only a given declaration indexes counts as reached. The schema is regenerated; the golden output does not move.
…ne, and given: sits beside variables: A given name asked of typeset_declaration was refused with "is not a named expression, constraint or variable". It now says that a given declaration prints no line of its own, and that it prints in the legend under Given. The `given` field moves after `variables` on Spec, so to_yaml writes the key where the file-shape table and the examples already put it. The schema and the golden output are unchanged. The symbols kwarg `given` is renamed to `upright`: it means the glyph, not the language's `given:` key. `balnce`, the did-you-mean input in tests/test_given.py, joins the typos allowance.
override(base, patches) lays each patch over the base a field at a time and hands back one mapping for to_spec. A partial entry must land on a declaration the base has, and a miss is refused with the near miss named. A whole entry creates. null at declaration level removes, and a stale removal is refused. Sibling patches must write disjoint paths, so their order never decides a model. A dimension or a relation may be added or restated exactly and never changed. The objective is one declaration laid over by the same rules. given: is laid over one kind at a time, entry by entry. Base and patches are copied, never mutated. override is exported from math_spec. The tests' own fixture override is renamed varied so the verb owns the word. docs/howto/compose.md carries the recipe and the four refusals, each produced by running the case.
…nd a null section is refused
`override` read `dimensions: {snapshot: null}` as a removal and `dimensions:
{snapshot: {}}` as a restatement, though the rule is that a patch adds a
dimension or a relation or restates one word for word. Both are refused now,
with a message that names the rewrite.
A section set to null, such as `constraints: null`, laid as an empty mapping
and changed nothing. It is refused too, and the message says that removal is
written one declaration at a time.
The page and the messages now say dimension and relation throughout, where
they said axis in some places and dimension in others.
… sibling introduces merge(fragments, description=None) composes peers before validation. A dimension or a relation every fragment may declare, and the ones that do say the same thing about it, prose excluded. Every other name is owned, and a second claim is refused naming both fragments. The objectives are summed, each term in parentheses, and the senses and the versions have to agree. A given declaration is folded into the declaration a sibling introduces, once the reader is checked to say the same or less, and two fragments that both only read a name have to read it the same way. merge is exported beside override, the given advice names merge() as the fragment's route, and compose.md, declarations.md and limits.md describe both verbs.
…annot read what it builds The composed objective joined its terms in fragment-iteration order, so the same fragments under two argument orders gave two expressions. The terms are summed in the fragments' name order now. A fragment that declares a name and reads it under given: too is refused, naming the fragment, the name and the rewrite. Such a file does not load on its own, and folding the reading away put it in a model that loads. merge writes version: only where a fragment declares one. Two fragments declaring different versions are still refused.
… file prints alone examples/library/ holds a coupling surface, a generator and a load as fragments, and variants/commitment.yaml as a patch over their composition. One symbol table serves the library, cut per page to what each model declares. tools/gallery.py prints a page per fragment and a composed page with one tab per variant; tab() moves to tools/_page.py; render_tex skips variants/. tests/test_library_example.py holds the library to what its pages claim. Sentence lengths on the five pages: median 8 to 15 words, three sentences over 25.
…t flow one way The layout rules keep the rule sentence and drop the clause that argued for it. The library pages say "surface" where one said "spine", and the component fragments call `Port_p` a flow, as the surface and its page already do. The composed page's variant test reads the page and asks for a tab per file under `variants/`, rather than comparing a glob with itself. The balance test compares each merge against the surface's own row, which is what the page claims. The gallery picks the library block by the model's directory.
FabianHofmann
requested review from
FBumann and
brynpickering
as code owners
September 19, 2026 17:34
Documentation build overview
51 files changed ·
|
Contributor
Author
Contributor
|
@FabianHofmann Does this mean i can close my 6-PR stack? |
Contributor
Author
yes, it does |
Contributor
|
Perfect |
Resolve conflicts from main's expressions rename and relations refactor: - Program field named_expressions -> expressions (main), keep given - adopt main's top-level program.relations API in test_a_relation_is_declared - drop test_an_unknown_dimension_is_a_near_miss (used removed program.dimension()) - keep the varied test fixture, since override is now a composition verb Regenerated schema and golden output.
Resolve conflicts: fold given names into the single-arg Namespace, combine the given/formulation kinds in limits.md, unify the override->varied fixture rename with main's new tests, and retype composition.py off explicit Any for main's tightened pyrefly. Schema regenerated.
26 of 37 tasks
This was referenced Sep 24, 2026
Contributor
This was referenced Sep 25, 2026
Closed
FBumann
added a commit
that referenced
this pull request
Sep 28, 2026
… it, and patched with files that each change part of it (#732) * feat(language): a spec is composed from files that each state part of it, and patched with files that each change part of it merge composes fragments as peers: each loads on its own, owns what it declares, and reads what a sibling declares under given:, which now takes parameters, variables, named expressions and row families. A given expression's dims bound what it reads. A given: expressions: entry marked additive: true says the name is a sum other files add terms to; each contributor declares an ordinary named expression, merge sums them and keeps the marked entry. override lays patches over a base, field by field, with null as the removal. Both return a loaded Spec. Squashes #571, #690, #728 and #729 onto #731, in #721's words. Adds the tutorial 'A spec in several files', with a test that runs every step, and sorts the names under given: in the canonical form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #732 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * refactor(language): a term of a sum is an ordinary named expression, and only `merge` holds it to the rules of the sum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #734 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The reader's description wins, and the term checks stay at load Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a file adds a term to an expression another file defines, under `given:` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Walk the loaded fragments by value Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * A term names an expression of its file, and merge adds it by name `term:` on a `given: expressions:` entry now names a named expression the same file declares, and no longer takes an expression written inline. The term is then free: it takes `cases:`, a description and every rule of a named expression, and it counts as read by the math. `merge` adds the terms by name, without brackets around a name, and keeps each term as a named expression of the composed spec. A refusal for terms that land on no name names only a near miss. The typeset term line is `name = ⋯ + term`. The tutorial, the reference, the how-to, the golden model and the notation page follow. The #734 line leaves the changelog; #732 carries the design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Name the term in a term collision, and suggest only a name a term can land on Two fragments that declare one term name now hear that terms share one namespace, not the dimension-rows advice. Terms that land on no name are refused with a near miss among the definitions and readings only, since a term name is no place for a term to land; `did_you_mean` takes `listing=False` for the case where only a near miss helps. The how-to says to name each term after its component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * fix(language): a merged spec's descriptions do not depend on the order the fragments are passed in, and a reader's fills one its owner left out (#739) * Choose a merged description by fragment name, and let a reader's fill a gap A shared dimension or relation, a reading several fragments share, and a sum built from terms took the description of whichever fragment was passed first. Each now takes the first description in the fragments' name order, as the objective already did, so the order of the arguments reaches no field. The order of the declarations still follows the order passed in, which is presentation. A folded given declaration dropped its reader's description even where the declaration it folds into had none; it now fills that gap, and yields to the owner's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(language): a term prints as the definition it is, and one rule folds every reading (#738) * refactor(language): a term prints as the definition it is, and merge takes the fragments in name order once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #738 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Retitle the changelog line: the name-order rule is #739's Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB --------- Co-authored-by: Claude <noreply@anthropic.com> * Attribute comments say what a name holds, and internal names carry none --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Fabian <fab.hof@gmx.de>
FBumann
added a commit
that referenced
this pull request
Sep 28, 2026
…ession with `empty: true` (#742) * feat(language): two files that state the same spec write one text, and canonical --check fails a file that is not in it Spec.to_yaml(canonical=True) writes the normal form: sections in one order, declarations sorted by name, every expression printed from its parsed tree with the terms of a sum and the factors of a product sorted, one term per line. python -m mathspec canonical writes it; --check exits 1 for a file not in the form and --write rewrites it. Squashes #530 and #718 onto main after #721, in its words: a file states a spec. Also normalises a named expression written on one line, which the form passed through as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #731 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * feat(language): a spec is composed from files that each state part of it, and patched with files that each change part of it merge composes fragments as peers: each loads on its own, owns what it declares, and reads what a sibling declares under given:, which now takes parameters, variables, named expressions and row families. A given expression's dims bound what it reads. A given: expressions: entry marked additive: true says the name is a sum other files add terms to; each contributor declares an ordinary named expression, merge sums them and keeps the marked entry. override lays patches over a base, field by field, with null as the removal. Both return a loaded Spec. Squashes #571, #690, #728 and #729 onto #731, in #721's words. Adds the tutorial 'A spec in several files', with a test that runs every step, and sorts the names under given: in the canonical form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #732 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * refactor(language): a term of a sum is an ordinary named expression, and only `merge` holds it to the rules of the sum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #734 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The reader's description wins, and the term checks stay at load Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a file adds a term to an expression another file defines, under `given:` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Walk the loaded fragments by value Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * A term names an expression of its file, and merge adds it by name `term:` on a `given: expressions:` entry now names a named expression the same file declares, and no longer takes an expression written inline. The term is then free: it takes `cases:`, a description and every rule of a named expression, and it counts as read by the math. `merge` adds the terms by name, without brackets around a name, and keeps each term as a named expression of the composed spec. A refusal for terms that land on no name names only a near miss. The typeset term line is `name = ⋯ + term`. The tutorial, the reference, the how-to, the golden model and the notation page follow. The #734 line leaves the changelog; #732 carries the design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Name the term in a term collision, and suggest only a name a term can land on Two fragments that declare one term name now hear that terms share one namespace, not the dimension-rows advice. Terms that land on no name are refused with a near miss among the definitions and readings only, since a term name is no place for a term to land; `did_you_mean` takes `listing=False` for the case where only a near miss helps. The how-to says to name each term after its component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * fix(language): a merged spec's descriptions do not depend on the order the fragments are passed in, and a reader's fills one its owner left out (#739) * Choose a merged description by fragment name, and let a reader's fill a gap A shared dimension or relation, a reading several fragments share, and a sum built from terms took the description of whichever fragment was passed first. Each now takes the first description in the fragments' name order, as the objective already did, so the order of the arguments reaches no field. The order of the declarations still follows the order passed in, which is presentation. A folded given declaration dropped its reader's description even where the declaration it folds into had none; it now fills that gap, and yields to the owner's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(language): a term prints as the definition it is, and one rule folds every reading (#738) * refactor(language): a term prints as the definition it is, and merge takes the fragments in name order once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #738 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Retitle the changelog line: the name-order rule is #739's Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(language): a named expression may declare the frame it is read over Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #741 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a sum other files add terms to is declared as an expression with a frame and no body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #742 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * docs: a file restates a shared dimension as its dtype alone, and merge carries the one description written for it (#743) * An empty sum prints as a definition with an ellipsis for its body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Attribute comments say what a name holds, and internal names carry none * An empty sum is written `empty: true`, so a frame with no body is refused A `dims:` with no body loaded as an empty sum, so a forgotten body went unnoticed. The flag is explicit: `empty: true` over a `dims:`, refused beside `expression:`, `cases:` or `otherwise:`. `empty` defaults to false and is written only where true. The program flag `owned` is renamed `empty`, to match the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQxVP5uX2V4rpvJNPhbyR2 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Fabian <fab.hof@gmx.de>
FBumann
added a commit
that referenced
this pull request
Sep 28, 2026
… it, each component adding its share of a sum by name (#736) * docs(pypsa): a single file covers the standard, stochastic and multi-period classes Fold pypsa_stochastic and pypsa_multi_period into examples/pypsa.yaml; retire the sibling pages, symbols and example files. Add the rung 14 and rung 15 sections to the gallery page, and guard the plain-run collapse to the standard model with a frozen shape fixture. * docs(pypsa): the spec covers the process and transformer components Add the Process component (a generalized multi-port converter, superset of Link) and the full-depth Transformer (a passive branch like Line, with tap ratio and a fixed phase shift in the KVL cycle) to examples/pypsa.yaml, with symbols, the regenerated gallery, rung 17 and rung 18 reference networks and oracles, and the collapse guard extended to the new standard names. * docs(pypsa): the spec covers quadratic marginal cost * docs(pypsa): the spec covers transmission losses * docs(pypsa): the spec covers the secant loss mode * docs(pypsa): the secant loss mode carries a solved reference rung_19 records the secant-mode triangle solved through the pinned pypsa, objective 10840.93, the same 150 rows as the tangent rung. The two loss-cut blocks now stand for both their tangent and secant PyPSA names, matched by a gallery helper that reads every backticked name before the dash. * docs(pypsa): the spec covers the optimised transformer phase shift A phase-shifting transformer's angle becomes a per-snapshot decision where phase_shift_min < phase_shift_max, bounded by them and entering the KVL cycle sum in place of the fixed constant; a plain run keeps the shift fixed and collapses byte-for-byte. rung_20 records the phase-shifter triangle solved through the pinned pypsa, objective 16455.0. * docs(pypsa): the carrier growth limit binds every extendable component * docs(pypsa): the spec covers transformer transmission losses A transformer carries its own loss under transmission_losses, as a line does: the loss counted against its rating, its cap, its tangent or secant cuts and half of it at either bus. A plain run collapses byte-for-byte. rung_22 and rung_23 record the transformer triangle in both modes, objectives 10643.48 and 10822.00. * docs(pypsa): a committable unit serving its brought-in down time stays off Generator_com_status_must_stay_down mirrors the must-stay-up block over a data-prep mask, status fixed to zero; PyPSA names the row Generator-com-status-min_down_time_must_stay_up. rung_24 records a cheap unit held off for two snapshots, objective 9007.5. * docs(pypsa): a committable link carries the generator's unit commitment Link gains the status, start-up and shut-down variables, the committable, big-M and modular p bounds, transitions, up and down times, both must-stay rules, the committed ramp rows, n_mod and its costs, mirroring Generator. rung_25 records five committable links, objective 14013.0. * docs(pypsa): a committable process carries the generator's unit commitment Process gains the same unit commitment blocks as Link, over its internal power. rung_26 restates rung 25's links as processes drawing a quarter more than they deliver, objective 15956.125. * docs(pypsa): a ramp row follows pypsa for a modular committed build and a start-up ramp alone A committable extendable modular unit takes the ordinary ramp rows against one module, not the big-M rows. A start-up or shut-down ramp alone builds the row, and a missing limit reads as the full build. rung_27 (45469.5) and rung_28 (83283.0) record both for Generator, Link and Process. * docs(pypsa): storage cycles or reopens per investment period, and a ramp restarts at a period start * docs(pypsa): a security-constrained run limits every branch flow after any one listed outage * docs(pypsa): an mga flag caps the system cost at a budget and minimises weighted builds * docs(pypsa): mga leaves the standard spec until it can land as a patch file Reverts 5109686. The commit is kept on docs/pypsa-mga and waits for #571. * docs(pypsa): a storage built in a later period opens at its first active snapshot * docs(pypsa): a unit may be scheduled off for maintenance * docs(pypsa): the spec states what maintenance and late-opening storage assume of the data * docs(pypsa): a growth limit counts no transformer, as pypsa does * docs(pypsa): a global constraint may count one investment period, weighted by its years * docs(pypsa): a process, a storage unit and a store may carry a quadratic marginal cost * docs(pypsa): a store's power and a storage unit's dispatch and charging may each be pinned to a schedule * docs(pypsa): a link's and a process's delay applies within each investment period * docs(pypsa): a negative relative growth adds nothing to a carrier's growth limit, as pypsa clips it at zero * docs(pypsa): a tech capacity expansion limit on a network with several scenarios is refused, as pypsa does * docs(pypsa): the carrier growth rung says a transformer counts in no carrier * docs(pypsa): a global constraint takes its own constant and sense in each scenario * docs(pypsa): the stochastic rung says which component data spans a scenario * docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario (#688) * docs(pypsa): component data spans a scenario wherever pypsa reads it per scenario * docs(pypsa): two rungs show operating and first-stage data that differ by scenario * docs(pypsa): a component's sign turns its term in the bus balance around * docs(pypsa): the linearized commitment file keeps a unit down, ramps at the full build where a limit is missing, and schedules maintenance * docs(pypsa): a ramp limit may change over time and lift at a snapshot * docs(pypsa): a unit that came in running ramps from its p_init into the first snapshot * docs(pypsa): the relaxed commitment file ramps every generator per snapshot and from its p_init, tightens at the full build and signs its balance * docs(pypsa): a start and a stop cost what they cost, with no snapshot or period weight * docs(pypsa): a growth limit binds only under multi_investment_periods * docs(pypsa): a load that is not active draws nothing from its bus * docs(pypsa): a security-constrained run dissipates no transmission loss * docs: the changelog lists the pypsa spec that covers every model class and component * docs(pypsa): an efficiency, a rate or a phase shift may change from snapshot to snapshot * docs: the changelog lists efficiencies per snapshot * docs(pypsa): a growth limit counts an asset only in the first period it stands in A rung may now record a PyPSA bug: its intended objective from an oracle, and what PyPSA 1.3.0 gives instead. Rung 51 records PyPSA/PyPSA#1938. * docs(pypsa): a transmission cost or volume limit holds in every scenario Rungs 52 and 53 record PyPSA/PyPSA#1939, where PyPSA 1.3.0 drops both rows. * docs(pypsa): each scenario delays a link's and a process's flow by its own delay Rung 54 records PyPSA/PyPSA#1941, where PyPSA 1.3.0 delivers the flow twice. * docs(pypsa): transformer cycles, security-constrained runs, fixed builds and committable units hold in every scenario Rungs 55 to 58 record PyPSA/PyPSA#1942 and PyPSA/PyPSA#1913, where PyPSA 1.3.0 raises. * docs(pypsa): the first-snapshot ramp refusals link the open PyPSA question * feat(language): two files that state the same spec write one text, and canonical --check fails a file that is not in it Spec.to_yaml(canonical=True) writes the normal form: sections in one order, declarations sorted by name, every expression printed from its parsed tree with the terms of a sum and the factors of a product sorted, one term per line. python -m mathspec canonical writes it; --check exits 1 for a file not in the form and --write rewrites it. Squashes #530 and #718 onto main after #721, in its words: a file states a spec. Also normalises a named expression written on one line, which the form passed through as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #731 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * feat(language): a spec is composed from files that each state part of it, and patched with files that each change part of it merge composes fragments as peers: each loads on its own, owns what it declares, and reads what a sibling declares under given:, which now takes parameters, variables, named expressions and row families. A given expression's dims bound what it reads. A given: expressions: entry marked additive: true says the name is a sum other files add terms to; each contributor declares an ordinary named expression, merge sums them and keeps the marked entry. override lays patches over a base, field by field, with null as the removal. Both return a loaded Spec. Squashes #571, #690, #728 and #729 onto #731, in #721's words. Adds the tutorial 'A spec in several files', with a test that runs every step, and sorts the names under given: in the canonical form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog link for #732 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * refactor(language): a term of a sum is an ordinary named expression, and only `merge` holds it to the rules of the sum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #734 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The reader's description wins, and the term checks stay at load Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a file adds a term to an expression another file defines, under `given:` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Walk the loaded fragments by value Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * A term names an expression of its file, and merge adds it by name `term:` on a `given: expressions:` entry now names a named expression the same file declares, and no longer takes an expression written inline. The term is then free: it takes `cases:`, a description and every rule of a named expression, and it counts as read by the math. `merge` adds the terms by name, without brackets around a name, and keeps each term as a named expression of the composed spec. A refusal for terms that land on no name names only a near miss. The typeset term line is `name = ⋯ + term`. The tutorial, the reference, the how-to, the golden model and the notation page follow. The #734 line leaves the changelog; #732 carries the design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Name the term in a term collision, and suggest only a name a term can land on Two fragments that declare one term name now hear that terms share one namespace, not the dimension-rows advice. Terms that land on no name are refused with a near miss among the definitions and readings only, since a term name is no place for a term to land; `did_you_mean` takes `listing=False` for the case where only a near miss helps. The how-to says to name each term after its component. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Cut pypsa.yaml into topic fragments whose components add named terms `examples/pypsa/` holds the 24 fragments `tools/pypsa_split.py` writes from #620's `examples/pypsa.yaml`. Each component's share of a sum (the bus balance, the cycle sum, the operating cost, the carrier additions and the five global-constraint totals) is a named expression of its own, such as `Generator_injection`, and the `term:` of its `given:` entry names it. The reader of each sum describes it: `network` and `power_flow` for the two rows, `core` for the rest. `check` merges the fragments to the canonical form of the one file with its sums written as named terms, and compares that file with `pypsa.yaml` row by row, every term substituted back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #736 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * fix(language): a merged spec's descriptions do not depend on the order the fragments are passed in, and a reader's fills one its owner left out (#739) * Choose a merged description by fragment name, and let a reader's fill a gap A shared dimension or relation, a reading several fragments share, and a sum built from terms took the description of whichever fragment was passed first. Each now takes the first description in the fragments' name order, as the objective already did, so the order of the arguments reaches no field. The order of the declarations still follows the order passed in, which is presentation. A folded given declaration dropped its reader's description even where the declaration it folds into had none; it now fills that gap, and yields to the owner's own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 * Add the changelog line for #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1 --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(language): a term prints as the definition it is, and one rule folds every reading (#738) * refactor(language): a term prints as the definition it is, and merge takes the fragments in name order once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #738 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Retitle the changelog line: the name-order rule is #739's Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB --------- Co-authored-by: Claude <noreply@anthropic.com> * fix(typeset): a substituted term prints its leading minus as a subtraction, and the PyPSA split has a page per fragment Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Expect the substituted sum over both dimensions, and link the changelog line to #740 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a named expression may declare the frame it is read over Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #741 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * feat(language): a sum other files add terms to is declared as an expression with a frame and no body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Add the changelog link for #742 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The owner of each PyPSA sum declares it with a frame and no body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The golden model substitutes a signed sum into a plus Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * docs: a file restates a shared dimension as its dtype alone, and merge carries the one description written for it (#743) * An empty sum prints as a definition with an ellipsis for its body Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * The owner pages of the PyPSA split print their sums with an ellipsis Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1F2gprjeiGPnvDaWjZwBB * Keep main's patch case in test_composition, which #740 carried from an earlier #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQxVP5uX2V4rpvJNPhbyR2 --------- Co-authored-by: Fabian <fab.hof@gmx.de> Co-authored-by: Claude <noreply@anthropic.com>
1 of 42 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
A file names what it reads under one closed
given:key,math_spec.mergecomposes fragments into one model,math_spec.overridelays patches over a base, and a component library underexamples/library/shows the three composing. This rebuilds the stack #505 → #520 on currentmain; it does not close #302, only its fragment half.Eight commits: the plan's four, each followed by the commit that applies its review.
pixi run ciis green on the final tree.Decisions to read, not skim
limits.md.given:is a declaration section, neither primitive, macro nor refused. How a new construct enters now lists four kinds.file.md,index.mdrule 1 and theSpecdocstring say so.advicekindgivenis a note.reading.mdtells a consumer with no host to refuse. The two agree only if the consumer acts on the note.Program.givenholdsGivenDeclaration(dims)per name.mergeandoverrideare two verbs. Peers and layers obey opposite laws. They compose asoverride(merge({…}), {…}).nullremove an axis while its docs said an axis is never changed. The rule is now one thing in code and prose.nullin a patch is refused. It was a silent no-op.mergerefuses it, so a composed model never hides a file that does not load alone.mergeomitsversionwhen no fragment declares it, and a silent fragment does not disagree with one that pins a version.domain: continuousfolds into an introducer that omitsdomain.Spec.to_dict()emits every default, so aSpec-loaded fragment needs this to merge at all.descriptionis prose and never a disagreement. The first author's wording is carried.Not done, on purpose
render_texcompiles the three fragments under default symbols, because it finds a symbol table by model stem. The gallery pages useexamples/symbols/library.yamlthroughsymbols_for. Fixing it is atools/change of its own.merge's return dumped byyaml.safe_dump, so its flow style differs from the hand-written fragments.compose.mdare read by nothing. To file after merge, as fix(language): a composition refusal calls a library file a fragment rather than a template #511 named.given: parameters/dimensions, a binder.Departures from Part 2 defaults
tests/fixtures.overrideis renamedvariedacross eleven test files, so the verb owns the word. The plan counted nine.pyproject.tomlallows the typobalnce; it is the did-you-mean input in thegiventests.givenintypesetting/symbols.pyis renamedupright, sogivenhas one meaning in the tree.Commit 1 — a file says what it reads under one given key (5ff1426, 84c7797)
model.py:GivenVariableBlock,GivenConstraintBlock,GivenBlock(closed,__bool__),Spec.givenbesidevariables. A given variable joins the namespace collision check; a given constraint also underconstraints:is refused; both frames join the dimension check;_names_are_namesreaches into the two inner mappings.resolution.py,dimensions.pyread the merged maps; thedual()refusal namesgiven: constraints:.program.py:GivenDeclaration,GivenTargets(seals its own mappings),Program.given.advice.py: kindgiven; a dimension only a given frame indexes is reached.boundedness.py: a given column in the objective with no constraint naming it no longer crashes the unboundedness pass with aKeyError; this guard the stack lacked. Typesetting:Givenlegend group, given names choosable in the symbol table,typeset_declarationsays a given name prints no line of its own.Docs:
declarations.md#given,file.md,index.md,reading.mdWhat a program does not build (executed bytest_reading_page.py),errors.mdadvice row,limits.mdfourth kind and templates → fragments.Mutation table (guard deleted, suite run, restored):
_names_are_namesreaches intogivenGivenTargetsseals its mappingsgivendual()refusal namesgiven: constraints:typeset_declarationnames the legend for a given nameSchema regenerated; golden output unchanged.
Commit 2 — a patch file extends a model rather than a copy of it (bf9bfeb, e65969f)
composition.pywithoverride(base, patches). A partial entry must land on a declaration the base has, with the near miss named. A whole entry creates.nullat declaration level removes; a stale removal is refused. Sibling patches must write disjoint paths. A dimension or relation may be added or restated equally, never changed or removed. The objective is one declaration laid over by the same rules. Base and patches are deep-copied, never mutated.given:is laid over one kind at a time, entry by entry, reading the entry class offGivenBlock.model_fields. An unknowngiven:kind passes through for the closed schema to refuse at load.Docs:
docs/howto/compose.mdwith the recipe, What a patch may say, and each refusal quoted from a real run. Nav entry;limits.mdlink.Commit 3 — fragments compose into one model, each reading what a sibling introduces (de802dd, 1da63b5)
merge(fragments, description=None)besideoverride, reusing its constants and helpers.dimensions/relationsmust agree with prose excluded. Every other name is owned and a second claim names both fragments. Objectives are summed in fragment-name order, each term parenthesised, senses must agree. Versions must agree where declared. A given declaration folds into its introducer when it says the same or less; two fragments that only read a name must read it the same way; a name nothing introduces stays undergiven:. A fragment that reads what it builds is refused.advice'sgivennote namesmerge().Docs:
compose.mdA library of components with two refusals from real runs;declarations.md#givenfolding sentences;limits.mdstates both verbs.Commit 4 — a component library composes into one model, and each file prints alone (cea9efe, 16777b6)
examples/library/{surface,generator,load}.yaml,variants/commitment.yaml,examples/symbols/library.yaml. Components key to a port, so the balance is written once in the surface.tools/_page.py::tabmoved out ofhome_math.py;tools/gallery.pyrenders the three fragments and a composed page with one tab per variant;render_tex.NOT_MODELSskipsvariants/. Five pages underdocs/examples/library/, nav,.prettierignore, catalogue row.tests/test_library_example.py: nine cases, including that the balance in the merged model equals the surface's own, and that every variant has a tab on the committed composed page.Gate
pixi run cion 16777b6, exit 0:Each of the eight commits was gated with
pixi run lint,pixi run testandpixi run docs-buildon its own tree. Each plan commit was reviewed by a second agent reading the full implementations and probing every rule; the followingfix/docscommit applies that review.