Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0341278
feat(language): a parameter may declare the value a missing row reads as
claude Oct 1, 2026
a021ef9
test(typesetting): the golden model declares a default, so the legend…
claude Oct 1, 2026
a0a6c3f
docs(changelog): name #810 under the upcoming version
claude Oct 1, 2026
6e6f620
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
claude Oct 1, 2026
0f240b1
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
claude Oct 2, 2026
5f63706
feat(language): `missing:` says what a missing row means, and a table…
claude Oct 2, 2026
37b8ac0
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
claude Oct 2, 2026
a0ce903
feat(language): a missing row the data must not have is `refused`, no…
claude Oct 2, 2026
7a2449a
fix(language): the schema publishes `refused` as the default of `miss…
claude Oct 2, 2026
70cb9f2
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
claude Oct 2, 2026
cfc0a69
Merge origin/main into claude/nice-goldberg-tum8xc
claude Oct 2, 2026
118cec5
docs(pypsa): a start-up or shut-down ramp limit with no row means no …
claude Oct 2, 2026
6064346
fix(language): a spec and its expansion read a curve's parameters alike
claude Oct 2, 2026
a107752
fix(language): a curve's completeness assumption no longer says a mis…
claude Oct 2, 2026
52ef2eb
docs: the compose how-to and the pypsa page name `missing:`, not `abs…
claude Oct 2, 2026
bf743d0
fix(language): an overlap refusal says a parameter has no row, not th…
claude Oct 2, 2026
54010b7
test(language): the soundness fuzz draws the value a missing row read…
claude Oct 2, 2026
714aa67
feat(typesetting): the legend says what a missing row means for every…
claude Oct 2, 2026
df65c48
docs(changelog): the #810 line names what breaks
claude Oct 2, 2026
03ba739
feat(language)!: a curve's parameter takes `missing:`, and declares i…
claude Oct 2, 2026
475ac3e
feat(typesetting): the legend says what a masked-out variable means w…
claude Oct 2, 2026
d7e0864
Merge origin/main into claude/nice-goldberg-tum8xc
claude Oct 6, 2026
d0f0e26
Merge origin/main into claude/nice-goldberg-tum8xc
claude Oct 6, 2026
4bf299a
Merge origin/main into claude/nice-goldberg-tum8xc
FabianHofmann Oct 6, 2026
daf87d9
fix(language)!: a default `missing:` written out is the one left out,…
FabianHofmann Oct 6, 2026
3d53073
fix(language): a str parameter refuses `missing: neutral`, as a relat…
FabianHofmann Oct 6, 2026
b002cc4
docs: a relation is total unless it declares `missing: absent`
FabianHofmann Oct 6, 2026
99f629d
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
FabianHofmann Oct 6, 2026
ccfb24e
Merge remote-tracking branch 'origin/main' into claude/nice-goldberg-…
FabianHofmann Oct 6, 2026
500fe31
docs(pypsa): the CVaR columns from #849 read `missing: neutral`, and …
FabianHofmann Oct 6, 2026
48f43e9
fix(language): a bare int or str parameter under missing: refused is …
FabianHofmann Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp

## Upcoming version

- feat(language)!: `missing:` says what a missing row means, a table short of a row is refused unless the file says otherwise, and a variable's `absence:` is now `missing:` ([#810](https://github.com/energy-models/mathspec/pull/810))
- docs(pypsa): a risk preference with weight zero builds the CVaR variables and refuses quadratic costs, as in PyPSA ([#849](https://github.com/energy-models/mathspec/pull/849))
- docs(pypsa): storage that retires before the last counted snapshot closes a global limit at its last active level ([#850](https://github.com/energy-models/mathspec/pull/850))
- refactor(program)!: the node a use of a named expression stands as is `NamedExpression`, beside `NamedMask`, and `Named` is gone ([#822](https://github.com/energy-models/mathspec/pull/822))
Expand Down
11 changes: 7 additions & 4 deletions docs/about/relations-as-linear-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dimensions:
generator: { dtype: str }
zone: { dtype: str }
relations:
gen_zone: { key: [generator, snapshot], values: zone }
gen_zone: { key: [generator, snapshot], values: zone, missing: absent }
parameters:
zone_cap: { dims: [snapshot, zone] }
variables:
Expand Down Expand Up @@ -47,8 +47,10 @@ $`f: K \to V`$, and
\mathbf{1}_R(k, v) = [\, f(k) = v \,].
```

The function is partial where a key tuple has no row. A bare relation is a
subset and nothing more. Above, `gen_zone` is the graph of
The function is total by default: a key tuple with no row is
[refused](../reference/language/relations.md#the-data-contract) when the data
is attached. Under `missing: absent`, as above, it is partial, and a key tuple
with no row is off its domain. A bare relation is a subset and nothing more. Above, `gen_zone` is the graph of
$`f: \mathcal{G} \times \mathcal{T} \to \mathcal{Z}`$. The
[data contract](../reference/language/relations.md#the-data-contract) makes it
one: the loader checks one row per key tuple when the data is attached.
Expand Down Expand Up @@ -182,7 +184,8 @@ consumer builds is not the matrix.
undefined where $`f`$ is, so `at` is absent there and [absence
spreads](../reference/language/absence.md#how-absence-travels) to the row.
`where: gen_zone` on `looked_up` writes the domain of $`f`$ on the page, so a
reader sees which rows exist without opening the data.
reader sees which rows exist without opening the data. Under the default,
`refused`, the domain is every key tuple and there is nothing off it.

## Partitions and tests

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/library/composed.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ variables:
Port_p:
dims: [snapshot, port]
domain: continuous
absence: undefined
missing: absent
description: what a port puts into its bus in a snapshot, negative for a withdrawal
Generator_p:
dims: [snapshot, generator]
bounds: {lower: 0.0, upper: Generator_p_nom}
domain: continuous
absence: undefined
missing: absent
description: '`Generator-p` — what a generator produces in a snapshot'
constraints:
Bus_nodal_balance:
Expand Down
194 changes: 97 additions & 97 deletions docs/examples/pypsa.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/examples/pypsa/carrier.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameters:
`multi_investment_periods` (`global_constraints.py:220-221`), so data
prep feeds no value otherwise
dims: [carrier]
missing: neutral
Carrier_max_relative_growth:
description: >-
share of the previous period's additions that may be added on top — the
Expand Down Expand Up @@ -73,7 +74,7 @@ constraints:

| Symbol | Meaning |
|---|---|
| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise |
| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$, `neutral` where the data has no row — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise |
| $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep |

#### Given
Expand Down
17 changes: 9 additions & 8 deletions docs/examples/pypsa/cost.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,28 @@ dimensions:

parameters:
CVaR_alpha:
description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability
description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega`
dims: []
missing: neutral

variables:
CVaR_a:
description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not"
dims: [scenario]
where: CVaR_omega
absence: zero
missing: neutral
bounds:
lower: 0
CVaR_theta:
description: "`CVaR-theta` — where the tail starts, the value at risk"
dims: []
where: CVaR_omega
absence: zero
missing: neutral
CVaR:
description: "`CVaR` — the tail's average cost, what the objective prices at `omega`"
dims: []
where: CVaR_omega
absence: zero
missing: neutral

given:
parameters:
Expand Down Expand Up @@ -73,15 +74,15 @@ constraints:

| Symbol | Meaning |
|---|---|
| $`\alpha`$ | `CVaR_alpha` (scalar) — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability |
| $`\alpha`$ | `CVaR_alpha` (scalar), `neutral` where the data has no row — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` |

#### Variables

| Symbol | Meaning |
|---|---|
| $`a`$ | `CVaR_a` over $`\Xi`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not |
| $`\theta`$ | `CVaR_theta` (scalar) — `CVaR-theta` — where the tail starts, the value at risk |
| $`CVaR`$ | `CVaR` (scalar) — `CVaR` — the tail's average cost, what the objective prices at `omega` |
| $`a`$ | `CVaR_a` over $`\Xi`$, `neutral` where the mask leaves it out — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not |
| $`\theta`$ | `CVaR_theta` (scalar), `neutral` where the mask leaves it out — `CVaR-theta` — where the tail starts, the value at risk |
| $`CVaR`$ | `CVaR` (scalar), `neutral` where the mask leaves it out — `CVaR` — the tail's average cost, what the objective prices at `omega` |

#### Given

Expand Down
18 changes: 12 additions & 6 deletions docs/examples/pypsa/generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ parameters:
Generator_p_nom_mod:
description: the module size a build comes in whole numbers of; no value means the build is continuous
dims: [generator]
missing: neutral
Generator_modules_installed:
description: >-
how many whole modules a committable build has in place: `Generator_p_nom
Expand Down Expand Up @@ -100,6 +101,7 @@ parameters:
Generator_p_set:
description: a given output schedule; a generator without one has no row here
dims: [scenario, snapshot, generator]
missing: neutral
Generator_p_nom_min:
description: least nominal power an extendable generator may be built at
dims: [scenario, generator]
Expand All @@ -117,6 +119,7 @@ parameters:
Generator_p_nom_set:
description: a given nominal power for an extendable generator; one without a value has no row here
dims: [generator]
missing: neutral
Generator_e_sum_min:
description: least energy over the horizon; minus infinity where no floor is meant
dims: [scenario, generator]
Expand All @@ -129,15 +132,18 @@ parameters:
`co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator
of an unweighted carrier has no row
dims: [scenario, global_constraint, snapshot, generator]
missing: neutral
Generator_operational_limit_weight:
description: one where the generator is in the row's set — data prep; one outside it has no row
dims: [scenario, global_constraint, generator]
missing: neutral
Generator_tech_capacity_weight:
description: >-
one where the generator is in the row's carrier-and-bus set — data
prep; one outside it, or one that does not stand in the row's
`investment_period`, has no row
dims: [global_constraint, generator]
missing: neutral

variables:
Generator_p:
Expand Down Expand Up @@ -320,22 +326,22 @@ assumptions:
| $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one unit of output |
| $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output |
| $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power. PyPSA refuses one that differs by scenario (`constants.py:43`) |
| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$ — the module size a build comes in whole numbers of; no value means the build is continuous |
| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous |
| $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\Xi \times \mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules |
| $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep |
| $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep |
| $`\mathrm{W}`$ | `Generator_capital_weight` over $`\mathcal{G}`$ — the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep |
| $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) |
| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here |
| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — a given output schedule; a generator without one has no row here |
| $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\Xi \times \mathcal{G}`$ — least nominal power an extendable generator may be built at |
| $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\Xi \times \mathcal{G}`$ — most nominal power an extendable generator may be built at |
| $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\Xi \times \mathcal{G}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep |
| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$ — a given nominal power for an extendable generator; one without a value has no row here |
| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$, `neutral` where the data has no row — a given nominal power for an extendable generator; one without a value has no row here |
| $`\underline{\mathrm{E}}`$ | `Generator_e_sum_min` over $`\Xi \times \mathcal{G}`$ — least energy over the horizon; minus infinity where no floor is meant |
| $`\overline{\mathrm{E}}`$ | `Generator_e_sum_max` over $`\Xi \times \mathcal{G}`$ — most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant |
| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row |
| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{L} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row |
| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{L} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row |
| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row |
| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{L} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's set — data prep; one outside it has no row |
| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{L} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row |

#### Variables

Expand Down
3 changes: 2 additions & 1 deletion docs/examples/pypsa/generator_commitment.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ parameters:
(`components.py:1050-1121`). Below the output a solve wants, it caps that
output; data prep
dims: [scenario, generator]
missing: neutral

variables:
Generator_status:
Expand Down Expand Up @@ -325,7 +326,7 @@ constraints:
| $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one start in this snapshot |
| $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one stop in this snapshot |
| $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on |
| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$ — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep |
| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep |

#### Variables

Expand Down
Loading
Loading