Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c2a4086
feat(language): a lookup is a relation between dimensions, walked in …
claude Sep 9, 2026
0af69a6
feat(language): from= and to= take a list, so one table lands on a pr…
claude Sep 9, 2026
f76f92b
fix(typesetting): a bare relation in a where prints as membership of …
claude Sep 9, 2026
21ef4c6
feat(language): a walk lands into= its produced columns, and a partit…
claude Sep 9, 2026
af7eb16
feat(language): a partition groups by the value columns into= names, …
claude Sep 9, 2026
dfea7ca
fix(language): a partition may group by a pair of buses, and neither …
claude Sep 9, 2026
2a7e3ce
docs(language): a key reads as the columns that are unique together, …
claude Sep 9, 2026
75840fd
refactor(program): a grouped position carries the walk it counts with…
FBumann Sep 9, 2026
684fff5
feat(language): a partition groups within a column, and a window has …
claude Sep 11, 2026
cf7ca1b
feat(language): a reduction says what it consumes, and a walk says wh…
claude Sep 11, 2026
1baa338
feat(language): a lookup declares its columns, and only a partition's…
claude Sep 11, 2026
e7a95bc
fix(language): the refusal for a key column in a where prints the ope…
claude Sep 11, 2026
9d10420
feat(language): a sum toward a lookup's key is refused, since it read…
claude Sep 14, 2026
08ebbdc
docs(language): re-align the walk and dim-set tables prettier reformats
claude Sep 15, 2026
fec2851
Merge branch 'main' into claude/lookup-relations-vhvfjd
claude Sep 15, 2026
3d261e4
feat(language): relations replace lookups, walked with over= and into…
FBumann Sep 15, 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
2 changes: 1 addition & 1 deletion .claude/skills/docs-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ not
## 6. Vocabulary

- **Gloss house vocabulary at first use** — _spec_, _program_, _declaration_,
_dimension_, _coordinate_, _frame_, _lookup_, _absence_, _macro_, _named
_dimension_, _coordinate_, _frame_, _relation_, _absence_, _macro_, _named
expression_, _reported expression_, _escape_. One clause with a concrete
instance: "one point of it, one generator in one snapshot, is a coordinate".
- **Gloss every acronym and domain term at first use**, in parentheses, six
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ with no data and no solver.**
A math-spec file declares four things: the axes the model runs over, such as
`snapshot` and `generator`; the data it expects, such as `load` and `cost`; the
decisions the solver makes, such as `dispatch`; and the rules those decisions obey, such
as `sum(dispatch, over=generator) == load`. The file [below](#example) is a complete
as `sum(dispatch, consume=generator) == load`. The file [below](#example) is a complete
model.

math-spec reads that file, checks everything that can be checked without data,
Expand Down Expand Up @@ -211,7 +211,7 @@ through are a dependency rather than one engine's internals. The keys themselves
which are YAML math, a block per component, `dims:` and a `where:` string,
come from [Calliope](https://github.com/calliope-project/calliope).
[linopy](https://github.com/PyPSA/linopy) supplies the vocabulary that
`sum(over=)` and the dimension rules are named against. Issue numbers in these
`sum(consume=)` and the dimension rules are named against. Issue numbers in these
pages point at lpspec, where the arguments happened.

## Status
Expand Down
20 changes: 10 additions & 10 deletions docs/about/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ macro can write `over=d` and let the caller supply `d`. It could not do that if
the dimension were the keyword itself.

**An operator may read the whole table. It pays one full pass over the data.**
`sum(p, over=g)` reads one row per generator. `shift(p, over=t, offset=1)` reads
`sum(p, over=g)` reads one row per generator. `shift(p, along=t, offset=1)` reads
one row, the one before it. `x * y * a` reads the rows of `a` that pair an `x`
with a `y`. Each reads a bounded number of rows per output row, so an engine
builds the model one chunk of rows at a time.
Expand All @@ -55,14 +55,14 @@ the list of snapshots, not at the data.
**An operator that calls itself is refused.** Nothing bounds how far it expands,
so no number of passes over the data is enough.

| The operator | Allowed? |
| ---------------------------------------------------- | ----------------------------------------------- |
| filters rows on a column they already carry | yes |
| joins each row against a parameter or a lookup table | yes |
| reads a fixed number of neighbouring rows | yes |
| reads only the coordinate labels | yes |
| reads every row | yes, at one full pass before any chunk builds |
| calls itself | no, and the message names what to write instead |
| The operator | Allowed? |
| ------------------------------------------------ | ----------------------------------------------- |
| filters rows on a column they already carry | yes |
| joins each row against a parameter or a relation | yes |
| reads a fixed number of neighbouring rows | yes |
| reads only the coordinate labels | yes |
| reads every row | yes, at one full pass before any chunk builds |
| calls itself | no, and the message names what to write instead |

**Degree is not a third test.** `p * q` at one coordinate is a join of a table
with itself, so the objective and the constraints take it. Two things limit the
Expand Down Expand Up @@ -138,7 +138,7 @@ sentence tells them apart:
A cycle basis is the first kind. It needs the network's topology, which only the
data has, so `cycle_incidence` arrives as a parameter. A minimum up time is the
second kind. `min_up_time` is a column the model already binds, and the window
"the last `min_up_time` hours" follows from it, so `sum_back(within=min_up_time)`
"the last `min_up_time` hours" follows from it, so `sum_back(window=min_up_time)`
reads the width off the column and you ship no window mask
([#849](https://github.com/fluxopt/lpspec/issues/849)).

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/commitment.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ expressions:
boundary:
when: "committable and position(snapshot) == 0"
expression: status_initial
otherwise: shift(status, over=snapshot, offset=1)
otherwise: shift(status, along=snapshot, offset=1)

constraints:
power_balance:
Expand All @@ -80,7 +80,7 @@ constraints:
`ramp_limit`, a unit starting up to `start_up_limit`.
dims: [snapshot, generator]
expression: >-
p - shift(p, over=snapshot, offset=1, edge=0)
p - shift(p, along=snapshot, offset=1, edge=0)
<= ramp_limit * previous_status + start_up_limit * (1 - previous_status)

objective:
Expand Down
76 changes: 38 additions & 38 deletions docs/examples/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,23 @@ objective: { sense: minimize, expression: sum(p) }

$`\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\, t \in \mathcal{T}`$

### `sum(array, by=lookup)`
### `sum(array, by=relation)`

`examples/operators/sum_by.yaml`

```yaml
description: >-
The membership reduction — `sum(array, by=lookup)` lands the result on the
dimension the lookup maps into, which is what makes topology data rather than
The membership reduction — `sum(array, by=relation)` lands the result on the
column the relation is walked to, which is what makes topology data rather than
structure.

dimensions:
snapshot: { dtype: int }
generator: { dtype: str }
bus: { dtype: str }

lookups:
gen_bus: { over: generator, into: bus }
relations:
gen_bus: { columns: [generator, bus], key: generator }

parameters:
limit: { dims: [snapshot, bus] }
Expand All @@ -109,14 +109,14 @@ objective: { sense: minimize, expression: sum(p) }

$`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}`$

### `sum(array, by=[lookup, …])`
### `sum(array, by=[relation, …])`

`examples/operators/sum_by_lookups.yaml`
`examples/operators/sum_by_relations.yaml`

```yaml
description: >-
Grouping through several maps at once — `sum(array, by=[lookup, …])` lands
the result on every dimension the lookups map into, which is one grouping
Grouping through several maps at once — `sum(array, by=[relation, …])` lands
the result on every dimension the relations map into, which is one grouping
rather than a composition of two: the generator dimension is consumed once.

dimensions:
Expand All @@ -125,9 +125,9 @@ dimensions:
bus: { dtype: str }
technology: { dtype: str }

lookups:
gen_bus: { over: generator, into: bus }
gen_tech: { over: generator, into: technology }
relations:
gen_bus: { columns: [generator, bus], key: generator }
gen_tech: { columns: [generator, technology], key: generator }

parameters:
limit: { dims: [snapshot, bus, technology] }
Expand All @@ -147,21 +147,21 @@ objective: { sense: minimize, expression: sum(p) }

$`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{limit}_{t,b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E}`$

### `at(array, by=lookup)`
### `at(array, by=relation)`

`examples/operators/at.yaml`

```yaml
description: >-
The adjoint of the membership reduction — `at(array, by=lookup)` reads one
The adjoint of the membership reduction — `at(array, by=relation)` reads one
coarse value once per fine label pointing at it.

dimensions:
snapshot: { dtype: int }
period: { dtype: int }

lookups:
period_of: { over: snapshot, into: period }
relations:
period_of: { columns: [snapshot, period], key: snapshot }

parameters:
cap: { dims: [period] }
Expand All @@ -181,7 +181,7 @@ objective: { sense: minimize, expression: sum(p) }

$`p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}`$

### `shift(array, over=dim, offset=n)`
### `shift(array, along=dim, offset=n)`

`examples/operators/shift.yaml`

Expand All @@ -201,14 +201,14 @@ variables:
constraints:
no_faster_than_before:
dims: [snapshot]
expression: p <= shift(p, over=snapshot, offset=1)
expression: p <= shift(p, along=snapshot, offset=1)

objective: { sense: minimize, expression: sum(p) }
```

$`p_{t} \le p_{t - 1} \qquad \forall\, t \in \mathcal{T}`$

### `shift(array, over=dim, offset=n, edge='wrap')`
### `shift(array, along=dim, offset=n, edge='wrap')`

`examples/operators/shift_wrap.yaml`

Expand All @@ -228,14 +228,14 @@ variables:
constraints:
no_faster_than_before:
dims: [snapshot]
expression: p <= shift(p, over=snapshot, offset=1, edge='wrap')
expression: p <= shift(p, along=snapshot, offset=1, edge='wrap')

objective: { sense: minimize, expression: sum(p) }
```

$`p_{t} \le p_{t \ominus 1} \qquad \forall\, t \in \mathcal{T}`$

### `shift(array, over=dim, offset=n, edge=v)`
### `shift(array, along=dim, offset=n, edge=v)`

`examples/operators/shift_edge.yaml`

Expand All @@ -255,14 +255,14 @@ variables:
constraints:
no_faster_than_before:
dims: [snapshot]
expression: p <= shift(p, over=snapshot, offset=1, edge=0)
expression: p <= shift(p, along=snapshot, offset=1, edge=0)

objective: { sense: minimize, expression: sum(p) }
```

$`p_{t} \le p_{t \boxminus_{0} 1} \qquad \forall\, t \in \mathcal{T}`$

### `shift(array, over=dim, offset=p, edge=…)`
### `shift(array, along=dim, offset=p, edge=…)`

`examples/operators/shift_by_parameter.yaml`

Expand All @@ -288,14 +288,14 @@ variables:
constraints:
arrives_after_its_lead:
dims: [technology, month]
expression: shift(order, over=month, offset=lead, edge=0) >= demand
expression: shift(order, along=month, offset=lead, edge=0) >= demand

objective: { sense: minimize, expression: sum(order) }
```

$`\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\, t \in \mathcal{T},\ m \in \mathcal{M}`$

### `shift(array, over=dim, offset=n, by=lookup)`
### `shift(array, along=dim, offset=n, by=relation)`

`examples/operators/shift_partitioned.yaml`

Expand All @@ -308,8 +308,8 @@ dimensions:
snapshot: { dtype: int }
season: { dtype: str }

lookups:
season_of: { over: snapshot, into: season }
relations:
season_of: { columns: [snapshot, season], key: snapshot }

variables:
p:
Expand All @@ -319,14 +319,14 @@ variables:
constraints:
no_faster_than_before_in_season:
dims: [snapshot]
expression: p <= shift(p, over=snapshot, offset=1, edge='wrap', by=season_of)
expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', by=season_of)

objective: { sense: minimize, expression: sum(p) }
```

$`p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\, t \in \mathcal{T}`$

### `sum_back(array, over=dim, within=n)`
### `sum_back(array, along=dim, window=n)`

`examples/operators/sum_back.yaml`

Expand All @@ -353,14 +353,14 @@ variables:
constraints:
stays_up_its_own_time:
dims: [unit, hour]
expression: sum_back(started, over=hour, within=3) <= on
expression: sum_back(started, along=hour, window=3) <= on

objective: { sense: minimize, expression: sum(on) }
```

$`\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$

### `sum_back(array, over=dim, within=p)`
### `sum_back(array, along=dim, window=p)`

`examples/operators/sum_back_by_parameter.yaml`

Expand All @@ -387,14 +387,14 @@ variables:
constraints:
stays_up_its_own_time:
dims: [unit, hour]
expression: sum_back(started, over=hour, within=min_up) <= on
expression: sum_back(started, along=hour, window=min_up) <= on

objective: { sense: minimize, expression: sum(on) }
```

$`\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$

### `sum_back(array, over=dim, within=p, edge='wrap')`
### `sum_back(array, along=dim, window=p, edge='wrap')`

`examples/operators/sum_back_wrap.yaml`

Expand All @@ -421,14 +421,14 @@ variables:
constraints:
stays_up_its_own_time:
dims: [unit, hour]
expression: sum_back(started, over=hour, within=min_up, edge='wrap') <= on
expression: sum_back(started, along=hour, window=min_up, edge='wrap') <= on

objective: { sense: minimize, expression: sum(on) }
```

$`\sum_{h' \in \mathcal{H} \,:\, 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$

### `sum_back(array, over=dim, within=n, by=lookup)`
### `sum_back(array, along=dim, window=n, by=relation)`

`examples/operators/sum_back_partitioned.yaml`

Expand All @@ -443,8 +443,8 @@ dimensions:
hour: { dtype: int }
day: { dtype: str }

lookups:
day_of: { over: hour, into: day }
relations:
day_of: { columns: [hour, day], key: hour }

variables:
started:
Expand All @@ -457,7 +457,7 @@ variables:
constraints:
stays_up_inside_its_day:
dims: [unit, hour]
expression: sum_back(started, over=hour, within=3, by=day_of) <= on
expression: sum_back(started, along=hour, window=3, by=day_of) <= on

objective: { sense: minimize, expression: sum(on) }
```
Expand Down
Loading
Loading