Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2902cdc
feat(language): a term names the sum it adds to with `adds_to:`, and …
claude Sep 28, 2026
db36e92
Add the changelog link for #763
claude Sep 28, 2026
0df34f9
A term fills only a name every fragment reads
claude Sep 28, 2026
cf5b6cd
Write the library how-to from #762 with `adds_to:`
claude Sep 28, 2026
7978d43
A reported expression is no read of the sum, and the tools keep every…
claude Sep 28, 2026
71e7b6d
Merge main into feat/adds-to, with #767's advice fix
claude Sep 29, 2026
b8d768e
Merge main into feat/adds-to
claude Sep 29, 2026
d9d0367
A term on a name its own file defines says so, and names the body to …
claude Sep 29, 2026
baebae2
Merge main into feat/adds-to, with #780's loop check on `adds_to:`
claude Sep 29, 2026
19c0c89
The splitter keeps a term written as a flow mapping or a continued line
claude Sep 29, 2026
1c637b3
A term keeps its target under a one-line patch, and merge refuses wha…
claude Sep 29, 2026
fcd7a11
Merge main into feat/adds-to, with #782
claude Sep 29, 2026
02ddb92
Merge main into feat/adds-to, with #788
claude Sep 29, 2026
5e51689
feat(language): a term adds to a body another file defines
claude Sep 30, 2026
09cb6f2
test(composition): a sum merged in steps equals one merged in one list
claude Sep 30, 2026
e80c1fc
docs(language): adds_to: adds an expression as a term to the sum it n…
claude Sep 30, 2026
320da5e
docs: the composition how-to shows a sum that one file defines and ot…
FBumann Sep 30, 2026
cbb79d5
feat(language): merge refuses a second objective, and a composed obje…
FBumann Oct 1, 2026
7a6a254
Merge main into feat/adds-to, with #795
claude Oct 1, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp

## Upcoming version

- feat(language): merge refuses a second objective, and a composed objective reads a sum that each file adds its part to ([#763](https://github.com/energy-models/mathspec/pull/763))
- docs: the composition how-to shows a sum that one file defines and other files add terms to ([#800](https://github.com/energy-models/mathspec/pull/800))
- feat(language): a term names the sum it adds to with `adds_to:`, and no file marks a sum as open ([#763](https://github.com/energy-models/mathspec/pull/763))
- fix(language): a cases block with one case may compare expressions in its when: ([#795](https://github.com/energy-models/mathspec/pull/795))
- feat(language): a parameter value that is null or NaN is refused when the data is attached ([#788](https://github.com/energy-models/mathspec/pull/788))
- fix(language): a where string names several columns in at's over= and into=, as an expression does ([#782](https://github.com/energy-models/mathspec/pull/782))
Expand Down
60 changes: 54 additions & 6 deletions docs/examples/library/composed.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ fragment holds it, and nothing in the repository commits it. `Port_p` is one
declaration here. Each component fragment read it under `given:`, and merging
folded those readings into the surface's own declaration.

The objective is the generator's, carried as it was written, since no other
fragment prices anything. A second priced fragment would add its term to this
one, each term in parentheses.
The objective is the surface's, and it reads `total_cost`. The generator is
the one fragment that costs something, so `Generator_cost` is the one term of
that sum. A second priced fragment would add its own term to `total_cost`.

The math under the file has a tab per formulation. **As composed** is the spec
above. **With commitment** lays `variants/commitment.yaml` over it with
Expand Down Expand Up @@ -90,7 +90,13 @@ constraints:
expression: at(Port_p, by=Load_port, over=port, into=load) == -Load_p_set
description: 'what a load takes is what its port withdraws. No PyPSA row stands for this: PyPSA writes
the load into the balance instead'
objective: {sense: minimize, expression: sum(Generator_p * Generator_marginal_cost)}
objective: {sense: minimize, expression: total_cost}
expressions:
Generator_cost: sum(Generator_p * Generator_marginal_cost)
total_cost:
dims: []
expression: Generator_cost
description: what running the system costs
```

=== "As composed"
Expand Down Expand Up @@ -120,10 +126,17 @@ objective: {sense: minimize, expression: sum(Generator_p * Generator_marginal_co
| $`f`$ | `Port_p` over $`\mathcal{T} \times \mathcal{J}`$ — what a port puts into its bus in a snapshot, negative for a withdrawal |
| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — what a generator produces in a snapshot |

#### Definitions

| Symbol | Meaning |
|---|---|
| $`\mathit{Generator\_cost}`$ | `Generator_cost` (scalar) |
| $`\mathit{total\_cost}`$ | `total_cost` (scalar) — what running the system costs |

#### Objective

```math
\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
\min \mathit{total\_cost}
```

#### Subject to
Expand All @@ -146,6 +159,20 @@ objective: {sense: minimize, expression: sum(Generator_p * Generator_marginal_co
f_{t,\mathrm{Load\_port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D}
```

#### Definitions

**`Generator_cost`**

```math
\mathit{Generator\_cost} = \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
```

**`total_cost`**

```math
\mathit{total\_cost} = \mathit{Generator\_cost}
```

#### Variable domains

**`Port_p`**
Expand Down Expand Up @@ -209,10 +236,17 @@ objective: {sense: minimize, expression: sum(Generator_p * Generator_marginal_co
| $`p`$ | `Generator_p` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-p` — what a generator produces in a snapshot |
| $`u`$ | `Generator_status` over $`\mathcal{T} \times \mathcal{G}`$ — `Generator-status` — whether a unit is on in a snapshot |

#### Definitions

| Symbol | Meaning |
|---|---|
| $`\mathit{Generator\_cost}`$ | `Generator_cost` (scalar) |
| $`\mathit{total\_cost}`$ | `total_cost` (scalar) — what running the system costs |

#### Objective

```math
\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
\min \mathit{total\_cost}
```

#### Subject to
Expand Down Expand Up @@ -247,6 +281,20 @@ objective: {sense: minimize, expression: sum(Generator_p * Generator_marginal_co
p_{t,g} \ge \underline{\mathrm{p}}_{g} \cdot \mathrm{p}^{\mathrm{nom}}_{g} \cdot u_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```

#### Definitions

**`Generator_cost`**

```math
\mathit{Generator\_cost} = \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
```

**`total_cost`**

```math
\mathit{total\_cost} = \mathit{Generator\_cost}
```

#### Variable domains

**`Port_p`**
Expand Down
36 changes: 25 additions & 11 deletions docs/examples/library/generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ SPDX-License-Identifier: CC-BY-4.0
# Generators

PyPSA's `Generator`, as one fragment. It owns its dimension, its relation into
`port`, its parameters, its column and its cost. It reads `Port_p` from
[the surface](surface.md) under
[`given`](../../reference/language/declarations.md#given). `Generator_port`
stands where PyPSA writes `Generator_bus`.
`port`, its parameters, its column and its cost. It reads `Port_p` and
`total_cost` from [the surface](surface.md) under
[`given`](../../reference/language/declarations.md#given), and adds its cost to
`total_cost` as the term `Generator_cost`. `Generator_port` stands where PyPSA
writes `Generator_bus`. The surface sets the objective, so this file on its
own sets none.

The constraint is what makes the library composable.
`at(Port_p, by=Generator_port, over=port, into=generator)` pins the flow at
Expand Down Expand Up @@ -41,6 +43,8 @@ given:
Port_p:
dims: [snapshot, port]
description: the surface introduces this flow, and this file pins it at its own ports
expressions:
total_cost: { dims: [] }
parameters:
Generator_p_nom: { dims: [generator], description: nominal power }
Generator_marginal_cost: { dims: [generator], description: cost of one unit of output }
Expand All @@ -56,9 +60,10 @@ constraints:
for this: PyPSA writes the generator into the balance instead
dims: [snapshot, generator]
expression: at(Port_p, by=Generator_port, over=port, into=generator) == Generator_p
objective:
sense: minimize
expression: sum(Generator_p * Generator_marginal_cost)
expressions:
Generator_cost:
expression: sum(Generator_p * Generator_marginal_cost)
adds_to: total_cost
```

PyPSA's `Generator`, wired to a port rather than straight to a bus, and cut to what a dispatch spec needs: a fixed build, no availability profile, no ramp limits.
Expand Down Expand Up @@ -89,12 +94,13 @@ PyPSA's `Generator`, wired to a port rather than straight to a bus, and cut to w
| Symbol | Meaning |
|---|---|
| $`f`$ | `Port_p` over $`\mathcal{T} \times \mathcal{J}`$ — the surface introduces this flow, and this file pins it at its own ports |
| $`\mathit{total\_cost}`$ | `total_cost` (scalar), an expression this file adds `Generator_cost` to |

#### Objective
#### Definitions

```math
\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
```
| Symbol | Meaning |
|---|---|
| $`\mathit{Generator\_cost}`$ | `Generator_cost` (scalar) |

#### Subject to

Expand All @@ -104,6 +110,14 @@ PyPSA's `Generator`, wired to a port rather than straight to a bus, and cut to w
f_{t,\mathrm{Generator\_port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```

#### Definitions

**`Generator_cost`**

```math
\mathit{Generator\_cost} = \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{c}_{g}
```

#### Variable domains

**`Generator_p`**
Expand Down
6 changes: 3 additions & 3 deletions docs/examples/library/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ profile, no ramp limits.

```text
examples/library/
surface.yaml one flow per port, one balance per bus
surface.yaml one flow per port, one balance per bus, the objective
generator.yaml PyPSA's Generator
load.yaml PyPSA's Load
variants/
Expand All @@ -35,8 +35,8 @@ examples/library/

| Page | What it shows |
| ---------------------------------- | -------------------------------------------------------------- |
| [The coupling surface](surface.md) | the surface, and the sign convention |
| [Generators](generator.md) | a file that reads `Port_p` and prices its output |
| [The coupling surface](surface.md) | the surface, the sign convention and the objective |
| [Generators](generator.md) | a file that reads `Port_p` and adds its cost to `total_cost` |
| [Loads](load.md) | a file with no variable of its own |
| [The composed spec](composed.md) | what `merge` returns, and the math it prints with each variant |

Expand Down
34 changes: 28 additions & 6 deletions docs/examples/library/surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ SPDX-License-Identifier: CC-BY-4.0

The surface every other file in the library is written against. It declares one
`Port_p` per port, one balance per bus, and the relation that says which bus a
port sits on. Nothing in it names a component class, so it is the one file that
does not change when a component class is added.
port sits on. It sets the objective on `total_cost`, which it reads under
[`given`](../../reference/language/declarations.md#given): each component that
costs something adds its cost to that sum. Nothing in it names a component
class, so it is the one file that does not change when a component class is
added.

PyPSA gives each component class a bus column and sums the classes into
`Bus-nodal_balance`. Here a component is wired to a port and the port to a bus,
Expand All @@ -23,9 +26,10 @@ that convention, and no component restates it.
```yaml
description: >-
The coupling surface every component in this library is written against: one
flow per port, and one balance per bus. A component is wired to a port, the
port to a bus, and the balance names no component class. A flow is positive
where the port injects into its bus.
flow per port, one balance per bus, and one cost to minimise. A component is
wired to a port, the port to a bus, and the balance names no component class.
A flow is positive where the port injects into its bus. A component that
costs something adds its cost to `total_cost`.
dimensions:
snapshot: { dtype: datetime, description: dispatch periods }
bus: { dtype: str, description: network nodes }
Expand All @@ -41,9 +45,15 @@ constraints:
description: "`Bus-nodal_balance` — what the ports on a bus put in nets to nothing"
dims: [snapshot, bus]
expression: sum(Port_p, by=Port_bus, over=port, into=bus) == 0
given:
expressions:
total_cost: { dims: [], description: what running the system costs }
objective:
sense: minimize
expression: total_cost
```

The coupling surface every component in this library is written against: one flow per port, and one balance per bus. A component is wired to a port, the port to a bus, and the balance names no component class. A flow is positive where the port injects into its bus.
The coupling surface every component in this library is written against: one flow per port, one balance per bus, and one cost to minimise. A component is wired to a port, the port to a bus, and the balance names no component class. A flow is positive where the port injects into its bus. A component that costs something adds its cost to `total_cost`.

#### Sets

Expand All @@ -59,6 +69,18 @@ The coupling surface every component in this library is written against: one flo
|---|---|
| $`f`$ | `Port_p` over $`\mathcal{T} \times \mathcal{J}`$ — what a port puts into its bus in a snapshot, negative for a withdrawal |

#### Given

| Symbol | Meaning |
|---|---|
| $`\mathit{total\_cost}`$ | `total_cost` (scalar), an expression another file defines — what running the system costs |

#### Objective

```math
\min \mathit{total\_cost}
```

#### Subject to

**`Bus_nodal_balance`**
Expand Down
Loading
Loading