Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
92cbd80
docs(pypsa): a single file covers the standard, stochastic and multi-…
FabianHofmann Sep 22, 2026
f1ea95b
docs(pypsa): the spec covers the process and transformer components
FabianHofmann Sep 22, 2026
4c042cd
docs(pypsa): the spec covers quadratic marginal cost
FabianHofmann Sep 22, 2026
dc0ec06
docs(pypsa): the spec covers transmission losses
FabianHofmann Sep 22, 2026
02c5974
docs(pypsa): the spec covers the secant loss mode
FabianHofmann Sep 22, 2026
0ce8247
docs(pypsa): the secant loss mode carries a solved reference
FabianHofmann Sep 22, 2026
b0fba21
docs(pypsa): the spec covers the optimised transformer phase shift
FabianHofmann Sep 22, 2026
c125c56
docs(pypsa): the carrier growth limit binds every extendable component
FabianHofmann Sep 23, 2026
28926f0
docs(pypsa): the spec covers transformer transmission losses
FabianHofmann Sep 23, 2026
cdcc041
docs(pypsa): a committable unit serving its brought-in down time stay…
FabianHofmann Sep 23, 2026
a4baa2c
docs(pypsa): a committable link carries the generator's unit commitment
FabianHofmann Sep 23, 2026
6da41d9
docs(pypsa): a committable process carries the generator's unit commi…
FabianHofmann Sep 23, 2026
4f67f53
docs(pypsa): a ramp row follows pypsa for a modular committed build a…
FabianHofmann Sep 23, 2026
805d480
docs(pypsa): storage cycles or reopens per investment period, and a r…
FabianHofmann Sep 23, 2026
2a7d5e9
docs(pypsa): a security-constrained run limits every branch flow afte…
FabianHofmann Sep 23, 2026
5109686
docs(pypsa): an mga flag caps the system cost at a budget and minimis…
FabianHofmann Sep 23, 2026
3151519
docs(pypsa): mga leaves the standard spec until it can land as a patc…
FabianHofmann Sep 23, 2026
3f5192a
docs(pypsa): a storage built in a later period opens at its first act…
FabianHofmann Sep 23, 2026
5d0cb9f
Merge remote-tracking branch 'origin/main' into pypsa-complete
FabianHofmann Sep 24, 2026
f0e167b
docs(pypsa): a unit may be scheduled off for maintenance
FabianHofmann Sep 24, 2026
eec71d2
docs(pypsa): the spec states what maintenance and late-opening storag…
FabianHofmann Sep 24, 2026
64b50d0
docs(pypsa): a growth limit counts no transformer, as pypsa does
FabianHofmann Sep 24, 2026
76c5c24
docs(pypsa): a global constraint may count one investment period, wei…
FabianHofmann Sep 24, 2026
510b567
docs(pypsa): a process, a storage unit and a store may carry a quadra…
FabianHofmann Sep 24, 2026
a0777a4
docs(pypsa): a store's power and a storage unit's dispatch and chargi…
FabianHofmann Sep 24, 2026
d964649
docs(pypsa): a link's and a process's delay applies within each inves…
FabianHofmann Sep 24, 2026
c1fec47
docs(pypsa): a negative relative growth adds nothing to a carrier's g…
FabianHofmann Sep 24, 2026
49a8fbd
docs(pypsa): a tech capacity expansion limit on a network with severa…
FabianHofmann Sep 24, 2026
0f578c7
docs(pypsa): the carrier growth rung says a transformer counts in no …
FabianHofmann Sep 24, 2026
6bfa052
docs(pypsa): a global constraint takes its own constant and sense in …
FabianHofmann Sep 24, 2026
0db274a
docs(pypsa): the stochastic rung says which component data spans a sc…
FabianHofmann Sep 24, 2026
e52b90d
docs(pypsa): component data spans a scenario wherever pypsa reads it …
FabianHofmann Sep 24, 2026
15c2f43
docs(pypsa): a component's sign turns its term in the bus balance around
FabianHofmann Sep 25, 2026
49c9e85
docs(pypsa): the linearized commitment file keeps a unit down, ramps …
FabianHofmann Sep 25, 2026
19b1992
docs(pypsa): a ramp limit may change over time and lift at a snapshot
FabianHofmann Sep 25, 2026
d5b4e75
docs(pypsa): a unit that came in running ramps from its p_init into t…
FabianHofmann Sep 25, 2026
9998ede
docs(pypsa): the relaxed commitment file ramps every generator per sn…
FabianHofmann Sep 25, 2026
9270a71
docs(pypsa): a start and a stop cost what they cost, with no snapshot…
FabianHofmann Sep 25, 2026
e2410a5
docs(pypsa): a growth limit binds only under multi_investment_periods
FabianHofmann Sep 25, 2026
98e8297
docs(pypsa): a load that is not active draws nothing from its bus
FabianHofmann Sep 25, 2026
9bfb210
docs(pypsa): a security-constrained run dissipates no transmission loss
FabianHofmann Sep 25, 2026
85217ff
Merge remote-tracking branch 'origin/main' into pypsa-complete
FabianHofmann Sep 25, 2026
5bb1336
docs: the changelog lists the pypsa spec that covers every model clas…
FabianHofmann Sep 25, 2026
830632b
docs(pypsa): an efficiency, a rate or a phase shift may change from s…
FabianHofmann Sep 25, 2026
f196e94
docs: the changelog lists efficiencies per snapshot
FabianHofmann Sep 25, 2026
348d925
docs(pypsa): a growth limit counts an asset only in the first period …
FabianHofmann Sep 25, 2026
80a378e
docs(pypsa): a transmission cost or volume limit holds in every scenario
FabianHofmann Sep 25, 2026
f3e4211
docs(pypsa): each scenario delays a link's and a process's flow by it…
FabianHofmann Sep 25, 2026
720275c
docs(pypsa): transformer cycles, security-constrained runs, fixed bui…
FabianHofmann Sep 25, 2026
60c279e
docs(pypsa): the first-snapshot ramp refusals link the open PyPSA que…
FabianHofmann Sep 25, 2026
4548e4b
Merge remote-tracking branch 'origin/pypsa-complete' into docs/pypsa-…
FabianHofmann Sep 25, 2026
f58c4d1
feat(language): two files that state the same spec write one text, an…
claude Sep 25, 2026
1326aba
Add the changelog link for #731
claude Sep 25, 2026
cc7632f
feat(language): a spec is composed from files that each state part of…
claude Sep 25, 2026
0c3fa47
Add the changelog link for #732
claude Sep 25, 2026
a5156ff
refactor(language): a term of a sum is an ordinary named expression, …
claude Sep 25, 2026
2a1cf73
Add the changelog link for #734
claude Sep 25, 2026
c602205
The reader's description wins, and the term checks stay at load
claude Sep 25, 2026
2b74b60
feat(language): a file adds a term to an expression another file defi…
claude Sep 25, 2026
d587b52
Walk the loaded fragments by value
claude Sep 25, 2026
1975438
A term names an expression of its file, and merge adds it by name
claude Sep 25, 2026
74c87d9
Name the term in a term collision, and suggest only a name a term can…
claude Sep 25, 2026
107aaaa
Merge #620 (the single pypsa spec) into the composition branch
claude Sep 25, 2026
129aa56
Cut pypsa.yaml into topic fragments whose components add named terms
claude Sep 25, 2026
c351d41
Add the changelog line for #736
claude Sep 25, 2026
fcc2fa1
fix(language): a merged spec's descriptions do not depend on the orde…
FBumann Sep 25, 2026
1339df3
refactor(language): a term prints as the definition it is, and one ru…
FBumann Sep 25, 2026
c4447c9
Merge remote-tracking branch 'origin/feat/composition' into docs/pyps…
claude Sep 25, 2026
2641a3f
fix(typeset): a substituted term prints its leading minus as a subtra…
claude Sep 25, 2026
01ddae8
Expect the substituted sum over both dimensions, and link the changel…
claude Sep 25, 2026
ddba9f3
feat(language): a named expression may declare the frame it is read over
claude Sep 25, 2026
42df148
Add the changelog link for #741
claude Sep 25, 2026
bdf0ae6
Merge branch 'feat/expression-frame' into canon-stack
claude Sep 25, 2026
350fa15
Merge remote-tracking branch 'origin/feat/canonical-form' into comp-s…
claude Sep 25, 2026
80b694d
feat(language): a sum other files add terms to is declared as an expr…
claude Sep 25, 2026
a7b416b
Add the changelog link for #742
claude Sep 25, 2026
003e1b3
Merge remote-tracking branch 'origin/feat/empty-sum' into claude/vibr…
claude Sep 25, 2026
8862a8a
The owner of each PyPSA sum declares it with a frame and no body
claude Sep 25, 2026
640f6ea
The golden model substitutes a signed sum into a plus
claude Sep 25, 2026
be6b55b
docs: a file restates a shared dimension as its dtype alone, and merg…
FBumann Sep 26, 2026
eebbe38
An empty sum prints as a definition with an ellipsis for its body
claude Sep 26, 2026
0b0e4b0
Merge remote-tracking branch 'origin/feat/empty-sum' into feat/empty-sum
claude Sep 26, 2026
a70e8d1
Merge remote-tracking branch 'origin/feat/empty-sum' into claude/vibr…
claude Sep 26, 2026
695febe
The owner pages of the PyPSA split print their sums with an ellipsis
claude Sep 26, 2026
a4fcdef
Merge remote-tracking branch 'origin/main' into docs/pypsa-fragments
FabianHofmann Sep 28, 2026
638cc61
Merge remote-tracking branch 'origin/docs/pypsa-efficiency-per-snapsh…
FabianHofmann Sep 28, 2026
7baeab1
Merge main into docs/pypsa-fragments
claude Sep 28, 2026
1cbdeec
Merge #740 into docs/pypsa-fragments, and declare each sum `empty: true`
claude Sep 28, 2026
d69d0d8
Keep main's patch case in test_composition, which #740 carried from a…
claude Sep 28, 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
5 changes: 1 addition & 4 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ docs/examples/dispatch.md
docs/examples/commitment.md
docs/examples/operators.md
docs/examples/pypsa.md
docs/examples/pypsa_quadratic.md
docs/examples/pypsa_linearized_uc.md
docs/examples/pypsa_losses.md
docs/examples/pypsa_stochastic.md
docs/examples/pypsa_multi_period.md
docs/examples/library/surface.md
docs/examples/library/generator.md
docs/examples/library/load.md
docs/examples/library/composed.md
docs/examples/pypsa/*.md

# The PyPSA reference scripts write this file; prettier would reformat what
# they stamp, and the two would fight over it exactly as above.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp

## Upcoming version

- docs(pypsa): a single spec covers every pypsa model class and component ([#620](https://github.com/energy-models/mathspec/pull/620))
- docs(pypsa): the pypsa spec is also 24 topic files that merge back to it, each component adding its share of a sum by name ([#736](https://github.com/energy-models/mathspec/pull/736))
- fix(typeset): a substituted term that opens with a minus prints as a subtraction ([#759](https://github.com/energy-models/mathspec/pull/759))
- docs: a file restates a shared dimension as its dtype alone, and `merge` carries the one description written for it ([#743](https://github.com/energy-models/mathspec/pull/743))
- feat(language): a sum other files add terms to is declared as an expression with `empty: true` ([#742](https://github.com/energy-models/mathspec/pull/742))
Expand All @@ -23,6 +25,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp
- refactor(language): a term prints as the definition it is, and one rule folds every reading ([#738](https://github.com/energy-models/mathspec/pull/738))
- feat(language): two files that state the same spec write one text, and `canonical --check` fails a file that is not in it ([#731](https://github.com/energy-models/mathspec/pull/731))
- 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](https://github.com/energy-models/mathspec/pull/739))
- docs(pypsa): an efficiency, a rate or a phase shift may change from snapshot to snapshot ([#717](https://github.com/energy-models/mathspec/pull/717))

## 0.2.0 (2026-09-25)

Expand Down
3 changes: 2 additions & 1 deletion docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Every spec is a file under `examples/` in the repository.
composed page shows what `merge` returns.

The PyPSA parity pages, from [PyPSA in one file](pypsa.md) on, are a proof of
concept. They sit in the Development section.
concept. They sit in the Development section, and
[PyPSA in 24 files](pypsa/index.md) is the same spec composed from fragments.

[Typeset the math](../reference/typeset.md) prints your own.
11,228 changes: 9,782 additions & 1,446 deletions docs/examples/pypsa.md

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions docs/examples/pypsa/carrier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!--
SPDX-FileCopyrightText: mathspec contributors
SPDX-License-Identifier: CC-BY-4.0
-->

# Carriers

One of the [24 fragments](index.md) of `examples/pypsa.yaml`: the growth limits per carrier, which read `Carrier_additions`. It reads `Carrier_additions` under [`given`](../../reference/language/declarations.md#given).

<!-- gallery:begin -->
```yaml
dimensions:
period:
description: investment periods — PyPSA's `investment_periods`
dtype: int
carrier:
description: energy carriers, what a growth limit is set per

parameters:
Carrier_max_growth:
description: >-
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:226-230`), data prep. PyPSA reads it only under
`multi_investment_periods` (`global_constraints.py:219-220`), so data
prep feeds no value otherwise
dims: [carrier]
Carrier_max_relative_growth:
description: >-
share of the previous period's additions that may be added on top — the
least over the scenarios, as PyPSA takes it, data prep
dims: [carrier]

given:
expressions:
Carrier_additions: { dims: [period, carrier] }

expressions:
Carrier_relative_growth:
description: >-
the share of the previous period's additions a carrier's growth limit
reads — PyPSA's `max_relative_growth` clipped at zero, so a negative
share adds nothing and never tightens the limit
dims: [carrier]
cases:
positive: { when: Carrier_max_relative_growth > 0, expression: Carrier_max_relative_growth }
otherwise: 0

constraints:
Carrier_growth_limit:
description: >-
`Carrier-growth_limit` — what a carrier adds across its extendable components in a period,
counting each build in the first period it stands in, is at most its allowance plus a share of
what it added the period before; the first period has no predecessor, so `edge=0` leaves it the
bare allowance
dims: [carrier, period]
where: Carrier_max_growth
expression: >-
Carrier_additions
- shift(Carrier_additions, along=period, offset=1, edge=0) * Carrier_relative_growth
<= Carrier_max_growth
```

#### Sets

| Symbol | Meaning |
|---|---|
| $`\mathcal{Y}`$ | index $`y`$ — `period` — investment periods — PyPSA's `investment_periods` |
| $`\mathcal{I}`$ | index $`i`$ — `carrier` — energy carriers, what a growth limit is set per |

#### Parameters

| 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:226-230`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:219-220`), 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

| Symbol | Meaning |
|---|---|
| $`\mathit{Carrier\_additions}`$ | `Carrier_additions` over $`\mathcal{Y} \times \mathcal{I}`$, an expression another file defines |

#### Definitions

| Symbol | Meaning |
|---|---|
| $`\mathrm{r}^{+}`$ | `Carrier_relative_growth` over $`\mathcal{I}`$ — the share of the previous period's additions a carrier's growth limit reads — PyPSA's `max_relative_growth` clipped at zero, so a negative share adds nothing and never tightens the limit |

$`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k`$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $`v`$ rather than being dropped.

#### Subject to

**`Carrier_growth_limit`**

```math
\mathit{Carrier\_additions}_{y,i} - \mathit{Carrier\_additions}_{y \boxminus_{0} 1,i} \cdot \mathrm{r}^{+}_{i} \le \overline{\Delta}_{i} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} \,:\, \overline{\Delta}_{i} \text{ is defined}
```

#### Definitions

**`Carrier_relative_growth`**

```math
\mathrm{r}^{+}_{i} = \begin{cases} \mathrm{r}_{i} & \text{if } \mathrm{r}_{i} > 0 \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, i \in \mathcal{I}
```
<!-- gallery:end -->
129 changes: 129 additions & 0 deletions docs/examples/pypsa/cost.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<!--
SPDX-FileCopyrightText: mathspec contributors
SPDX-License-Identifier: CC-BY-4.0
-->

# The cost

One of the [24 fragments](index.md) of `examples/pypsa.yaml`: the objective: the expected operating cost and its tail, weighted by scenario. It reads `CVaR_omega`, `scenario_opex`, `scenario_weight` under [`given`](../../reference/language/declarations.md#given).

<!-- gallery:begin -->
```yaml
dimensions:
scenario:
description: the futures dispatch is chosen in, each with a weight

parameters:
CVaR_inv_tail:
description: PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor
dims: []

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]
bounds:
lower: 0
CVaR_theta:
description: "`CVaR-theta` — where the tail starts, the value at risk"
dims: []
CVaR:
description: "`CVaR` — the tail's average cost, what the objective prices at `omega`"
dims: []

given:
parameters:
scenario_weight: { dims: [scenario] }
CVaR_omega: { dims: [] }
expressions:
scenario_opex: { dims: [scenario] }

constraints:
CVaR_excess:
description: "`CVaR-excess-{s}` — a scenario's operating cost beyond the tail's start is its excess; PyPSA names one row per scenario"
dims: [scenario]
where: CVaR_omega > 0
expression: CVaR_a - scenario_opex + CVaR_theta >= 0
CVaR_def:
description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability"
dims: []
where: CVaR_omega > 0
expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR

objective:
sense: minimize
description: >-
capacity once per active period at its expected cost over the scenarios, operation in expectation over the scenarios, and a share of it at the tail
expression: >-
(1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario)
+ CVaR_omega * CVaR
```

#### Sets

| Symbol | Meaning |
|---|---|
| $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight |

#### Parameters

| Symbol | Meaning |
|---|---|
| $`\mathrm{v}`$ | `CVaR_inv_tail` (scalar) — PyPSA's `1 / (1 - alpha)` — the tail's own probability, inverted in data prep because a divisor is one factor |

#### 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` |

#### Given

| Symbol | Meaning |
|---|---|
| $`\pi`$ | `scenario_weight` over $`\Xi`$, data another file declares |
| $`\omega`$ | `CVaR_omega` (scalar), data another file declares |
| $`\mathit{scenario\_opex}`$ | `scenario_opex` over $`\Xi`$, an expression another file defines |

#### Objective

```math
\min \left( 1 - \omega \right) \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot \mathit{scenario\_opex}_{\xi} \right) + \omega \cdot CVaR
```

#### Subject to

**`CVaR_excess`**

```math
a_{\xi} - \mathit{scenario\_opex}_{\xi} + \theta \ge 0 \qquad \forall\, \xi \in \Xi \,:\, \omega > 0
```

**`CVaR_def`**

```math
\theta + \mathrm{v} \cdot \left( \sum_{\xi \in \Xi} \pi_{\xi} \cdot a_{\xi} \right) \le CVaR \qquad \text{where } \omega > 0
```

#### Variable domains

**`CVaR_a`**

```math
a_{\xi} \ge 0 \qquad \forall\, \xi \in \Xi
```

**`CVaR_theta`**

```math
\theta \in \mathbb{R}
```

**`CVaR`**

```math
CVaR \in \mathbb{R}
```
<!-- gallery:end -->
Loading
Loading