You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Prompt: Is there anything beside #168 to fill the gaps? — reading the PyPSA ladder's recorded deviations, 2026-08-28.
Note
The following content was generated by AI. Read off fluxopt/lpspec at 525bf8fa, which pins this repo at v0.0.0-alpha.43: its runner records 32 deviations from PyPSA in differential/pypsa/deviations.yaml, and five of them are this one. The declarations quoted are examples/pypsa.yaml's, as lpspec's rung projections carry them.
The gap
PyPSA's GlobalConstraint frame carries a sense column — ==, <=, >= — so one labelled constraint is one row whichever way it points. A declaration here states its relation inside the expression:, so a requirement whose sense is data is written once per sense, the copies held apart by a mask on that column:
GlobalConstraint_primary_energy_ub:
description: '`primary_energy` — its total, at most its constant'foreach: [global_constraint]where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '<='expression: primary_energy <= GlobalConstraint_constantGlobalConstraint_primary_energy_lb:
description: '`primary_energy` — its total, at least its constant'foreach: [global_constraint]where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '>='expression: primary_energy >= GlobalConstraint_constantGlobalConstraint_primary_energy_eq:
description: '`primary_energy` — its total, at its constant'foreach: [global_constraint]where: GlobalConstraint_type == 'primary_energy' AND GlobalConstraint_sense == '=='expression: primary_energy == GlobalConstraint_constant
Same foreach, same left side, same right side; the operator and one string literal in the mask are the whole difference. Five types × three senses is fifteen declarations for the five names PyPSA reports — and every one of the fifteen is built by some rung, so this is not a corner: primary_energy, operational_limit, tech_capacity_expansion_limit, transmission_expansion_cost_limit and transmission_volume_expansion_limit, three times each.
Nested constraints and expressions #2, branches on a named expression — the same answer, and sharply so: primary_energy above is a named expression, written once and read by all three declarations. Factoring the value out is already done; the operator is what is left.
The sense partitions rows at expansion, off a column, exactly as where: already does — nothing branches per cell, and no row count depends on a solved value. The three declarations are legal today and are the proof of it. What is missing is saying the shared part once.
The shape I would argue for
An arm per operator inside one declaration, with the expression's two sides written above them — so the operator stays a literal in the file. The alternative, reading the operator out of a parameter (expression: primary_energy <sense> GlobalConstraint_constant), moves a piece of the grammar into the data, where an unexpected string becomes a load error instead of an unreadable file.
Against doing anything at all: three declarations cost about ten lines and read perfectly well. The price is that read-back names three blocks where PyPSA names one, which the ladder records as a permanent deviation on five names. It is the only one of the ladder's deviation classes with no route to closing — the ten regime splits close with #168, and the remaining seventeen are PyPSA's own bookkeeping, which the file is right not to imitate: an objective_constant column, a capacity row copied once per scenario, a binary bounded at 1 and capped by a row as well.
Note
The following content was generated by AI. Read off fluxopt/lpspec at
525bf8fa, which pins this repo atv0.0.0-alpha.43: its runner records 32 deviations from PyPSA indifferential/pypsa/deviations.yaml, and five of them are this one. The declarations quoted areexamples/pypsa.yaml's, as lpspec's rung projections carry them.The gap
PyPSA's
GlobalConstraintframe carries asensecolumn —==,<=,>=— so one labelled constraint is one row whichever way it points. A declaration here states its relation inside theexpression:, so a requirement whose sense is data is written once per sense, the copies held apart by a mask on that column:Same
foreach, same left side, same right side; the operator and one string literal in the mask are the whole difference. Five types × three senses is fifteen declarations for the five names PyPSA reports — and every one of the fifteen is built by some rung, so this is not a corner:primary_energy,operational_limit,tech_capacity_expansion_limit,transmission_expansion_cost_limitandtransmission_volume_expansion_limit, three times each.Why the constructs in flight do not reach it
cases:gives a coordinate one value per region. Here the regions agree about the value on both sides and differ about the relation between them, which is not a value position.primary_energyabove is a named expression, written once and read by all three declarations. Factoring the value out is already done; the operator is what is left.Why it is inside the ceiling
The sense partitions rows at expansion, off a column, exactly as
where:already does — nothing branches per cell, and no row count depends on a solved value. The three declarations are legal today and are the proof of it. What is missing is saying the shared part once.The shape I would argue for
An arm per operator inside one declaration, with the expression's two sides written above them — so the operator stays a literal in the file. The alternative, reading the operator out of a parameter (
expression: primary_energy <sense> GlobalConstraint_constant), moves a piece of the grammar into the data, where an unexpected string becomes a load error instead of an unreadable file.Against doing anything at all: three declarations cost about ten lines and read perfectly well. The price is that read-back names three blocks where PyPSA names one, which the ladder records as a permanent deviation on five names. It is the only one of the ladder's deviation classes with no route to closing — the ten regime splits close with #168, and the remaining seventeen are PyPSA's own bookkeeping, which the file is right not to imitate: an
objective_constantcolumn, a capacity row copied once per scenario, a binary bounded at 1 and capped by a row as well.