Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d929c69
refactor(program): a program carries the trees the typesetter prints,…
claude Sep 23, 2026
f8614b9
refactor(program): a program mirrors the file, descriptions and curve…
claude Sep 23, 2026
6306a74
refactor(language): every rule that reads across declarations runs in…
claude Sep 23, 2026
e29809d
test(advice): a program still carrying a curve is refused, and a file…
claude Sep 23, 2026
336b407
Merge branch 'claude/confident-faraday-coc7mj-2' into claude/confiden…
claude Sep 23, 2026
1d8607f
refactor(language): a model loads without writing its curves out, and…
claude Sep 23, 2026
4b257ac
feat(language): a model's program is an attribute of the model, and t…
claude Sep 23, 2026
43e1970
chore(model): a model's program is a cached property, forced once as …
claude Sep 23, 2026
a2db22c
chore(resolution): the expression walk and the where walk are two mod…
claude Sep 23, 2026
f3f3321
chore(piecewise): the rules a curve is held to read the declaration t…
claude Sep 23, 2026
3c4f665
chore(typeset): the legend is its own module, reads what the equation…
claude Sep 23, 2026
6edc16f
Merge remote-tracking branch 'origin/main' into claude/confident-fara…
claude Sep 23, 2026
3eaaa7e
Merge branch 'claude/confident-faraday-coc7mj' into claude/confident-…
claude Sep 23, 2026
8aeac2a
Merge branch 'claude/confident-faraday-coc7mj-2' into claude/confiden…
claude Sep 23, 2026
4a4b969
Merge branch 'claude/confident-faraday-coc7mj-3' into claude/confiden…
claude Sep 23, 2026
51c02bf
Merge branch 'claude/confident-faraday-coc7mj-4' into claude/confiden…
claude Sep 23, 2026
c4da095
Merge branch 'claude/confident-faraday-coc7mj-6' into claude/confiden…
claude Sep 23, 2026
e252384
Merge branch 'claude/confident-faraday-coc7mj-7' into claude/confiden…
claude Sep 23, 2026
59d845b
Merge branch 'claude/confident-faraday-coc7mj-8' into claude/confiden…
claude Sep 23, 2026
9b72b13
refactor(language): the top-level surface is the two states, the door…
claude Sep 23, 2026
24e56b4
refactor(language): the closed operator set stays public, as the one …
claude Sep 23, 2026
1169b1f
feat(language): a curve left as written is refused in one wording, an…
claude Sep 23, 2026
f0dc3e3
feat(language): nothing writes a formulation out unasked, and the che…
claude Sep 23, 2026
b535973
refactor(language): a refusal's wording belongs to whoever refuses
claude Sep 23, 2026
5b4496d
fix(advice): the refusal of a curve says it is still a curve, and say…
claude Sep 23, 2026
655ebda
feat(language): a piecewise block states its dims and names its links…
FBumann Sep 23, 2026
279997a
feat(advice): a curve is advised on as written, as a set already was
claude Sep 23, 2026
fa71b03
Merge the head of #661 into claude/adoring-galileo-mu42ir
claude Sep 24, 2026
fab8e39
Record main's squash of #649–#661 as merged into claude/adoring-galil…
claude Sep 24, 2026
a4b48c4
Merge main into claude/adoring-galileo-mu42ir
claude Sep 24, 2026
0e4b1b1
fix(language): a piecewise block its expansion cannot write is refuse…
FBumann Sep 24, 2026
acd7ef5
Merge remote-tracking branch 'origin/main' into sync/630
claude Sep 25, 2026
d97fb41
Merge origin/main into claude/adoring-galileo-mu42ir
claude Sep 29, 2026
de8ab94
Merge origin/main into claude/adoring-galileo-mu42ir
claude Sep 29, 2026
21e31bd
test(piecewise): a block mask reaches a walk into several columns
claude Sep 29, 2026
2b91913
Merge origin/main into claude/adoring-galileo-mu42ir
claude Sep 29, 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
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ CHANGELOG.md
docs/examples/dispatch.md
docs/examples/commitment.md
docs/examples/operators.md
docs/examples/piecewise.md
docs/examples/piecewise_adjacency.md
docs/examples/sos.md
docs/examples/piecewise_lp.md
docs/examples/pypsa.md
docs/examples/pypsa_linearized_uc.md
docs/examples/library/surface.md
Expand Down
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): a piecewise block states its dims and names its links, and its where reaches links that walk a relation ([#630](https://github.com/energy-models/mathspec/pull/630))
- 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))
- fix(language): a file whose terms read each other's sums is refused at load ([#780](https://github.com/energy-models/mathspec/pull/780))
Expand Down
18 changes: 8 additions & 10 deletions docs/examples/dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,45 +49,43 @@ Least-cost dispatch of a generator fleet against an hourly load.

| Symbol | Meaning |
|---|---|
| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods |
| $`\mathcal{S}`$ | index $`s`$ — `snapshot` — dispatch periods |
| $`\mathcal{G}`$ | index $`g`$ — `generator` — generating units |

#### Parameters

| Symbol | Meaning |
|---|---|
| $`\mathrm{capacity}`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
| $`\mathrm{load}`$ | `load` over $`\mathcal{T}`$ — demand to be met |
| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$ — marginal cost |
| $`\bar p`$ | `capacity` over $`\mathcal{G}`$ — installed capacity |
| $`\ell`$ | `load` over $`\mathcal{S}`$ — demand to be met |
| $`c`$ | `cost` over $`\mathcal{G}`$ — marginal cost |

#### Variables

| Symbol | Meaning |
|---|---|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{T} \times \mathcal{G}`$ — output of a generator in a snapshot |

Upright is what the data supplies — a parameter such as $`\mathrm{capacity}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{dispatch}`$. An index is italic too, being what a quantifier chooses, and a set is script.
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{S} \times \mathcal{G}`$ — output of a generator in a snapshot |

#### Objective

```math
\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{dispatch}_{t,g} \cdot \mathrm{cost}_{g}
\min \sum_{s \in \mathcal{S},\ g \in \mathcal{G}} \mathit{dispatch}_{s,g} \cdot c_{g}
```

#### Subject to

**`power_balance`**

```math
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{s,g} = \ell_{s} \qquad \forall\, s \in \mathcal{S}
```

#### Variable domains

**`dispatch`**

```math
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{capacity}_{g} > 0
0 \le \mathit{dispatch}_{s,g} \le \bar p_{g} \qquad \forall\, s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \bar p_{g} > 0
```
<!-- gallery:end -->

Expand Down
11 changes: 11 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ Every spec is a file under `examples/` in the repository.
by region, so a single inequality covers both regimes.
- [One construct per spec](operators.md) declares each operator in the smallest
file that can, and prints the equation beside it.
- [A curve by convex combination](piecewise.md) is the floor of the `piecewise`
family: one weight per breakpoint, one row summing them to 1, and one row per
link. The three pages after it are the same spec, restricted another way.
- [A curve that is not convex](piecewise_adjacency.md) adds a binary per segment
and the two rows that hold the weights on it. This is what the default method
builds.
- [A curve as a special-ordered set](sos.md) hands that same restriction to the
solver. The binaries and their rows are gone, and a declaration stands where
they were.
- [A curve as segment lines](piecewise_lp.md) states the curve as inequalities
instead of breakpoints, and declares no auxiliary variable at all.
- [A component library](library/index.md) is several files that compose into
one spec. Each file reads the coupling surface and prints on its own, and the
composed page shows what `merge` returns.
Expand Down
173 changes: 173 additions & 0 deletions docs/examples/piecewise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<!--
SPDX-FileCopyrightText: math-spec contributors
SPDX-License-Identifier: CC-BY-4.0
-->

# A curve by convex combination

A generator's cost curve, tied to its dispatch through one weight per
breakpoint. This is the floor of the [`piecewise`](../reference/language/piecewise.md)
family: the three pages after it are this same model with the weights
restricted a different way.

Read the math for what the block expands to. The file declares no weight, and
`cost_curve_lam` appears below because the block emits it. One row makes the
weights sum to 1, and one row per link ties that link's expression to the
weighted breakpoints. `method: convex` adds nothing further. A convex curve
under a minimised cost settles on one segment without being held there.

<!-- gallery:begin -->
```yaml
description: >-
Least-cost dispatch where each generator's cost curve is piecewise-linear in
its output, expanded into a lambda formulation.

dimensions:
snapshot:
description: dispatch periods
dtype: int
generator:
description: dispatchable units
dtype: str
bp:
description: breakpoints of the cost curve
dtype: int

parameters:
capacity:
description: maximum dispatch
dims: [generator]
load:
description: demand to be met
dims: [snapshot]
bp_x:
description: breakpoint dispatch levels, one curve per generator
dims: [generator, bp]
bp_y:
description: cost at each breakpoint, one curve per generator
dims: [generator, bp]

variables:
dispatch:
description: dispatched power
dims: [snapshot, generator]
bounds:
lower: 0
upper: capacity
op_cost:
description: operating cost, piecewise-linear in dispatch
dims: [snapshot, generator]
bounds:
lower: 0

piecewise:
cost_curve:
description: >-
cost read off the generator's curve — convex, so the weights need no
binaries to keep them on one segment
along: bp
dims: [snapshot, generator]
links:
dispatch: [dispatch, bp_x]
op_cost: [op_cost, bp_y]
method: convex

constraints:
balance:
dims: [snapshot]
expression: sum(dispatch, over=generator) == load

objective:
sense: minimize
description: total operating cost, taken off the curves rather than from a marginal rate
expression: sum(op_cost)
```

Least-cost dispatch where each generator's cost curve is piecewise-linear in its output, expanded into a lambda formulation.

#### Sets

| Symbol | Meaning |
|---|---|
| $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods |
| $`\mathcal{G}`$ | index $`g`$ — `generator` — dispatchable units |
| $`\mathcal{B}`$ | index $`b`$ — `bp` — breakpoints of the cost curve |

#### Parameters

| Symbol | Meaning |
|---|---|
| $`\mathrm{capacity}`$ | `capacity` over $`\mathcal{G}`$ — maximum dispatch |
| $`\mathrm{load}`$ | `load` over $`\mathcal{T}`$ — demand to be met |
| $`\mathrm{x}`$ | `bp_x` over $`\mathcal{G} \times \mathcal{B}`$ — breakpoint dispatch levels, one curve per generator |
| $`\mathrm{y}`$ | `bp_y` over $`\mathcal{G} \times \mathcal{B}`$ — cost at each breakpoint, one curve per generator |

#### Variables

| Symbol | Meaning |
|---|---|
| $`\mathit{dispatch}`$ | `dispatch` over $`\mathcal{T} \times \mathcal{G}`$ — dispatched power |
| $`\mathit{op\_cost}`$ | `op_cost` over $`\mathcal{T} \times \mathcal{G}`$ — operating cost, piecewise-linear in dispatch |

Upright is what the data supplies — a parameter such as $`\mathrm{capacity}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{dispatch}`$. An index is italic too, being what a quantifier chooses, and a set is script.

$`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.

$`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions.

$`\lvert \mathcal{T} \rvert`$ denotes the size of the set being counted along, and a position counted from the end prints against it — $`\lvert \mathcal{T} \rvert - 1`$ is the last position, one less than the size because the first is $`0`$.

#### Objective

```math
\min \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} \mathit{op\_cost}_{t,g}
```

#### Subject to

**`balance`**

```math
\sum_{g \in \mathcal{G}} \mathit{dispatch}_{t,g} = \mathrm{load}_{t} \qquad \forall\, t \in \mathcal{T}
```

**`cost_curve`**

```math
\left( \mathit{dispatch}_{t,g},\ \mathit{op\_cost}_{t,g} \right) \in \mathrm{conv}_{b \in \mathcal{B}}(\mathrm{x}_{g,b},\ \mathrm{y}_{g,b}) \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```

#### Variable domains

**`dispatch`**

```math
0 \le \mathit{dispatch}_{t,g} \le \mathrm{capacity}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```

**`op_cost`**

```math
\mathit{op\_cost}_{t,g} \ge 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
```

#### Assumptions

**`cost_curve_complete`**

```math
\mathrm{x}_{g,b} \text{ is defined} \wedge \mathrm{y}_{g,b} \text{ is defined} \qquad \forall\, g \in \mathcal{G},\ b \in \mathcal{B}
```

**`cost_curve_increasing`**

```math
\mathrm{x}_{g,b \boxminus_{0} 1} < \mathrm{x}_{g,b} \qquad \forall\, g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{pos}(b) > 0
```

**`cost_curve_curvature`**

```math
\lvert \{ b \in \mathcal{B} \,:\, \left( \mathrm{y}_{g,b} - \mathrm{y}_{g,b \boxminus_{0} 1} \right) \cdot \left( \mathrm{x}_{g,b \boxplus_{0} 1} - \mathrm{x}_{g,b} \right) > \left( \mathrm{y}_{g,b \boxplus_{0} 1} - \mathrm{y}_{g,b} \right) \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \wedge \mathrm{pos}(b) > 0 \wedge \mathrm{pos}(b) \neq \lvert \mathcal{B} \rvert - 1 \} \rvert = 0 \vee \lvert \{ b \in \mathcal{B} \,:\, \left( \mathrm{y}_{g,b} - \mathrm{y}_{g,b \boxminus_{0} 1} \right) \cdot \left( \mathrm{x}_{g,b \boxplus_{0} 1} - \mathrm{x}_{g,b} \right) < \left( \mathrm{y}_{g,b \boxplus_{0} 1} - \mathrm{y}_{g,b} \right) \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \wedge \mathrm{pos}(b) > 0 \wedge \mathrm{pos}(b) \neq \lvert \mathcal{B} \rvert - 1 \} \rvert = 0 \qquad \forall\, g \in \mathcal{G}
```
<!-- gallery:end -->
Loading
Loading