Skip to content

chore: examples/pypsa/ cuts pypsa.yaml into 24 topic fragments that merge back to its rows - #723

Closed
FBumann wants to merge 91 commits into
mainfrom
claude/great-galileo-6k1248
Closed

FBumann wants to merge 91 commits into
mainfrom
claude/great-galileo-6k1248

Conversation

@FBumann

@FBumann FBumann commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "please explore how we can decompose the big pypsa yaml file into many small, topic related yaml files. And how we can test if everything works. I imagine that the merging of those yaml files must ultimatively result in the same spec. Start from #620. We should probably merge main into it. Then use the feature from 690. For testing, we might need 718"

Prompt: "I think we can be sure that we need to add parameters and expressions to given... Lets just do it as a stacked PR onto 690. Then retry. Iterate until the split is clean, reasonable and its easy to add new features via a fragment. Present your final design of given, merge, overrride and potential additions as you are satisfied with the api, ux and typesetting."

Prompt: "Present your design, changes etc in a draft PR,"

Prompt: "What do you think of this idea? Then the flag on the reader's given: entry is the design." (and the follow-ups that settled it)

Note

The following content was generated by AI.

examples/pypsa.yaml splits into 24 topic fragments that merge back to its rows. given: reads parameters and expressions (#728), and a reader marks a sum that fragments add terms to (#729). A new component is a new family of files.

This PR presents the design and holds the experiment. Do not merge it. The language changes are reviewed in #728 and #729; the split lands in a migration PR once the stack is on main (plan in #722).

The stack

PR Base What it does
#571, #690 main fragments and patches: merge, override, given: for variables and constraints
#728 #690 given: also reads parameters and named expressions
#729 #728 a reader marks a given expression additive: true; contributors declare ordinary terms; merge sums them
#530, #718 main the canonical form, which the equality check compares in
#620 main the one-file PyPSA model
this PR #620 + all of the above the split, the splitter, the checks, two canonical-form fixes

The design

given: — what a file reads and does not declare

A fragment is a whole model: it loads, lowers and prints on its own. What it reads from a sibling it states under given:, one kind at a time.

Kind States This file reads it as New in
parameters dims, dtype a parameter: in expressions, where masks and bounds #728
variables dims, domain a column #571
expressions dims, additive a quantity over at most the frame, of degree one; the definer owns the body #728, #729
constraints dims a row family, named only in dual() #571
# examples/pypsa/generator_ramping.yaml
given:
  parameters:
    Generator_committable: { dims: [generator], dtype: bool }
  variables:
    Generator_status: { dims: [scenario, snapshot, generator], domain: integer }
  expressions:
    Generator_p_nom_committed: { dims: [scenario, generator] }

A given expression's dims are an upper bound. A definition whose body carries a dimension the reader does not state is refused. A body over fewer dimensions is folded, and the composed model decides: it refuses a row that would repeat, and accepts one where another term carries the dimension.

A sum other files add to

The file that gives a sum its meaning marks it once, with its frame and description. Every contributor declares an ordinary named expression under the name.

# examples/pypsa/network.yaml
given:
  expressions:
    Bus_injection:
      dims: [scenario, snapshot, bus]
      additive: true
      description: what every component puts into a bus, less what it takes out of it; ...
constraints:
  Bus_nodal_balance: { dims: [scenario, snapshot, bus], expression: Bus_injection == 0 }

# examples/pypsa/load.yaml
expressions:
  Bus_injection: sum(Load_demand, by=Load_bus, over=load, into=bus)

Three kinds of file, one rule:

The file Carries Reads the name as
a reader a given: entry; one reader marks it additive, others state the frame the sum
a contributor an ordinary expressions: entry, and the name nowhere else its term, alone
both, carrying the marked entry: a hub that adds its own term, or a composed model the marked entry and a term the sum so far

A contributor that reads the name without the marked entry would mean two models, its term alone and the sum composed, and merge refuses it.

merge — fragments as peers

The entry What merge does
a dimension or a relation every fragment may declare it, and those that do agree; the first description is carried
any other declaration one fragment declares it; a second is refused, both named
a given: entry checked against the fragment that declares the name, then folded into it
a given expression its definition carries no dimension the reader's dims do not name
a name read as one kind and declared as another refused, both fragments named
a name a given: entry marks additive every declaration is a term, summed in fragment-name order, each in parentheses; the marked entry is kept, so a later merge adds more
a term written as cases:, or over a dimension the sum does not state refused, the fragment named
a contributor that reads the sum without the marked entry refused, with one term or several
two declarations of a name no entry marks refused as a collision; the message says a sum needs a marked entry
a marked name no file adds to stays under given:; zero would leave its row without a variable
the objectives summed in fragment-name order; the senses agree; the first description is carried
a given entry nothing declares stays under given: for a host model

The result goes through to_spec, so the composed model is held to every rule again. In the composed program a marked entry that has a definition folds into it (ExpressionDeclaration.additive), so merged.program.given is empty for a whole model; the file keeps the entry.

override — patches over a base

Unchanged from #690. A patch edits, adds or removes (null) declarations field by field, and lays given: over one kind at a time. merge adds a term; override replaces one.

Typesetting

  • Given lists the names a file reads: "data another file declares", "an expression another file defines", and "a sum other files add terms to" for a marked entry.
  • Once a marked entry folds into a definition, Definitions lists it as "a sum other files add terms to". The line prints =: in any one model the sum is the terms that model has.

The split

Fragment Lines Holds
core 144 the scenario, snapshot and period weights, the risk preference, how a snapshot counts in a global constraint, and the marked entries of seven sums
network 39 marks Bus_injection; Bus_nodal_balance: Bus_injection == 0
power_flow 33 marks Cycle_angle_sum; Kirchhoff's voltage law
cost 52 the operating cost in expectation and at the tail (CVaR)
carrier 54 the growth limits
global_constraints 132 the primary-energy, operational and expansion limits
security 157 the branch-outage rows
generator, link, process 265, 304, 279 dispatch, capacity, bounds, modular builds
*_commitment 261 each status, start-up and shut-down, up and down times, their costs
*_ramping 237 each the ramp rows
*_maintenance 224 each maintenance scheduling
storage_unit, store, line, transformer, load 455, 351, 247, 252, 50

4980 lines in total against 4033: the frames each fragment declares to load alone, and its given: entries. Nothing is copied. Nine names are sums, each marked once, and each carries its description there.

Where a sum is marked. In the reader where a model without it is never wanted, so leaving the reader out collides: Bus_injection in network, Cycle_angle_sum in power_flow, since lines without Kirchhoff's law are not a PyPSA model. In core where the reader may go: scenario_opex, Carrier_additions and the five global-constraint totals.

Adding a feature is a fragment.

  • Leaving out any of carrier, cost, global_constraints, security, load, storage_unit, store, a generator, link or process family, or a *_ramping file alone still merges into a whole model.
  • security needs lines or transformers. Line and transformer, with security and power_flow, leave together, as a transport model.
  • Leaving out network, or power_flow with the branches kept, is refused as a collision that names the fix.

How it is tested

tests/test_pypsa_split.py, 49 cases:

  1. Each fragment loads on its own (24).
  2. Same model. merge(examples/pypsa/*.yaml) has the canonical form of pypsa.yaml with its hubs written as marked sums, objective description included, and merged.program.given is empty.
  3. Same rows. That rewritten file states the rows of pypsa.yaml. Each new hub is substituted back, every term of a row is moved to one side, and the terms are compared as a multiset. A flipped sign, a dropped term and a changed operator each fail it.
  4. The fragments are what tools/pypsa_split.py writes from pypsa.yaml.
  5. 16 ways of leaving topics out each leave a whole model; leaving out network, or power_flow alone, is refused (2).
  6. Every sum is marked in exactly one fragment, the one named above.
  7. Every fragment and the composition print in LaTeX, Typst and Markdown (3).

Open points and possible additions

The changes in each PR

#728 — given: reads parameters and expressions

  • GivenParameterBlock (dims, dtype, description) and GivenExpressionBlock (dims, description); GivenTargets.parameters and .expressions.
  • Resolution reads a given parameter as a parameter and a given expression as a Variable node; the dim rules, bounds, the flat namespace and the frame checks cover both.
  • merge folds both, bounds a definition by the reader's dims, and refuses a kind mismatch. override lays them over through GIVEN_KINDS.
  • The legend, typeset_declaration and advice cover both. Docs in declarations.md, compose.md, reading.md, errors.md, limits.md.
  • test_given.py from 40 cases to 67; six guards, each with a test that fails without it.

#729 — a sum its reader marks

  • GivenExpressionBlock.additive, GivenDeclaration.additive and ExpressionDeclaration.additive.
  • At load, where a file carries the marked entry and a term: the term's frame and form are checked, and the entry folds into the definition.
  • merge sums the terms of a marked name, keeps the entry, agrees readers on the frame, and refuses a cased term, a term over more than the sum and a self-reading contributor. The collision message names the fix. A composed objective keeps its first description.
  • The legend notes; docs in declarations.md, compose.md, reading.md.
  • test_additive.py, 24 cases; ten guards, each with a test that fails without it.

This PR

Hashes, how to reproduce, gates
Input Branch Commit
#620 pypsa-complete 60c279e559ceaef3a5a8ebdf10e098b7d0a79909
main main ca2e04b8cdd20c565edb84ca142a0c1dad21e9fd
#690 (carries #571) feat/compose-through-to-spec 4ff3483b6e8f9087140b6a34c1f46273021e2657
#718 (carries #530) claude/clever-ramanujan-vsgh97 1affd55225d05506cdc6eb76a8128e301b01d2dc
#728 feat/given-parameters-expressions d41d3ee31a56922376aad60b71b2835a699fb5d9
#729 feat/additive-expressions a98ad6ce81c06107a25de24843e383a6ecebbf45
this PR claude/great-galileo-6k1248 c7b1c878d5c59c4a14518f31522d82d907b8aee9

main has moved on since ca2e04b, so GitHub shows this PR as conflicting. It is not meant to merge, and was not brought up to date.

git checkout c7b1c878d5c59c4a14518f31522d82d907b8aee9
python -m tools.pypsa_split split /tmp/pypsa
python -m tools.pypsa_split check /tmp/pypsa
python -m pytest tests/test_pypsa_split.py

Pixi cannot be installed here. In a Python 3.12 uv virtualenv:

this PR    pytest 2433 passed, 26 skipped; ruff, reuse, typos, prettier clean; tools.gallery --check current
#728       pytest 1787 passed, 7 skipped; ruff, pyrefly (0 errors), typos clean
#729       pytest 1812 passed, 7 skipped; ruff, pyrefly (0 errors), reuse, typos clean

Not run: pixi run ci, compile-tex (the tectonic bundle download is blocked), mkdocs build --strict (the proxy refuses the docs.python.org inventory), reference.py --check, zizmor, taplo, and pyrefly on this branch.

No changelog line: this PR is not meant to merge, and #728 and #729 sit on a base that predates the ## Upcoming version rule.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1

FabianHofmann and others added 30 commits September 22, 2026 14:18
…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.
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.
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.
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.
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.
…s 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.
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.
…tment

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.
…nd 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.
…h file

Reverts 5109686. The commit is kept on docs/pypsa-mga and waits for #571.
The shell front keeps this branch's canonical verb and takes main's
docstring and its hoisted model for the typeset verbs. The reading page
now carries main's 22 claims and this branch's one, so the test counts
23. main removed to_program, so the canonical test reads a model's
program as spec.program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GpnFcQDuvt2bExGngb6MYk
Resolves the math_spec -> mathspec rename main made after #690's base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
The canonical tests skip patch files under variants/ and place given:
where Spec.to_dict writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…hecks that merge gives it back

Experiment for #722. It measures which declarations must share a file,
writes 14 topic fragments, and compares the canonical form of their
merge with the one file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…other file declares, under given:

given: takes parameters: and expressions: beside variables: and
constraints:. A given parameter reads as a parameter; a given expression
reads as a column over its frame. merge folds both into their
introducer, checks a given expression's frame against the body, and
refuses a name read as one kind and introduced as another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…e sums them

An expressions: entry marked additive: true is one share of a sum. merge
sums the shares of every fragment in fragment-name order and keeps the
flag, so a later merge adds to the sum. It refuses a share beside a whole
definition, and a fragment that adds a share and reads the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…agment gives it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…erge back to its rows

Each hub that sums a term per component (the bus balance, Kirchhoff's
voltage law, the operating cost, the carrier additions and the global
constraint totals) becomes an additive expression, and each component
adds its share. The canonical form now sorts the names under given:.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
@FBumann FBumann changed the title chore: a script splits examples/pypsa.yaml into topic files and checks that merge gives it back chore: examples/pypsa/ cuts pypsa.yaml into 24 topic fragments that merge back to its rows Sep 25, 2026
…e composed model decides the rest

merge refuses a definition whose body carries a dimension the reader does
not state. A body over fewer dimensions is folded, and the composed
model's load refuses a row that would repeat and accepts one another term
carries the dimension through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…s' into feat/additive-expressions

# Conflicts:
#	docs/howto/compose.md
… contributor declares an ordinary term

additive moves from a named expression to its given: entry. One reader
says the name is a sum over a frame; every file that adds to it declares a
plain named expression under the name. merge sums the terms of a marked
name and keeps the marked entry, so a later merge adds more. A file may
carry both, and then reads the sum so far; its term's frame and form are
checked at load. merge refuses a cased term, a term over a dimension the
sum does not state, and a contributor that reads the name without the
entry. Two terms of an unmarked name collide, and the message names the
fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
Resolves the math_spec -> mathspec rename again. The fragments under
examples/pypsa/ still mark their terms and are regenerated next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
…erm is a plain expression

Seven sums are marked in core, where their readers may be left out;
Bus_injection in network and Cycle_angle_sum in power_flow, where leaving
the reader out is never wanted and now collides. The canonical form
normalises a named expression written on one line, which it passed
through as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr4SCw5A7GUF5ARhdCaKR1
@FBumann

FBumann commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Superseeded by #736

@FBumann FBumann closed this Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: composition Assembling a model from files, and fixing a decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants