Skip to content

A global constraint's sense is data, so one requirement is three declarations differing only by an operator #239

Description

@FBumann

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_constant
GlobalConstraint_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_constant
GlobalConstraint_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.

Why the constructs in flight do not reach it

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_constant column, a capacity row copied once per scenario, a binary bounded at 1 and capped by a row as well.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    design: declinedArgued against; closes as not planned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions