seasons¶
A store that cycles inside each season rather than across the horizon, with seasons of different lengths — one balance row says it, because the wrap is the season's own and no level is carried from one season into the next.
The problem¶
A store that must come back to where it started needs its first position to read
its last. edge='wrap' says that about the axis:
On this instance that links snapshot 7 to snapshot 1 and makes the whole horizon one cycle: winter opens holding what summer left, and sells it at winter's best price. That is a different model. It solves, and its objective is higher.
The cycle a multi-period model means is per period, and by= says which:
soc == shift(soc, along=snapshot, offset=1, edge='wrap', by=season_of, within=season) + inflow - release
The translation walks inside the group the relation makes, so a season's first snapshot reads that season's last, whatever the season's length. Winter has four snapshots and summer three here, and nothing in the file says so.
The model¶
The same model, as math
A store that cycles inside each season rather than across the horizon, with seasons of different lengths — one balance row says it, because the wrap is the season's own and no level is carried from one season into the next.
Sets¶
| Symbol | Meaning |
|---|---|
| \(\mathcal{T}\) | index \(t\) — snapshot with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}\) — dispatch periods in order |
| \(\mathcal{S}\) | index \(s\) — season with \(\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}\) — the blocks the store cycles over |
Parameters¶
| Symbol | Meaning |
|---|---|
| \(\mathrm{inflow}\) | inflow over \(\mathcal{T}\) — energy arriving in a snapshot, whether or not it is wanted then |
| \(\mathrm{price}\) | price over \(\mathcal{T}\) — what one unit of release earns in a snapshot |
Variables¶
| Symbol | Meaning |
|---|---|
| \(\mathit{soc}\) | soc over \(\mathcal{T}\) — energy held at the end of a snapshot |
| \(\mathit{release}\) | release over \(\mathcal{T}\) — energy released in a snapshot |
Upright is what the data supplies — a parameter such as \(\mathrm{inflow}\), a coordinate map, a label — and italic is what the solver chooses, such as \(\mathit{soc}\). An index is italic too, being what a quantifier chooses, and a set is script.
\(t \ominus k\) denotes cyclic translation: index \(t-k\) taken modulo the size of the dimension (roll). Plain \(t-k\) (shift) has no wraparound — terms translated past the edge are simply absent.
\(t \ominus^{\mathrm{relation}(t)} k\) denotes a translation counted inside the group a relation puts \(t\) in (shift(by=relation)), so a term never crosses out of its own group.
Objective¶
Subject to¶
season_balance
Variable domains¶
soc
release
The tabs start from the instance's tables — one frame per parameter.
description: >-
A store that cycles inside each season rather than across the horizon, with
seasons of different lengths — one balance row says it, because the wrap is
the season's own and no level is carried from one season into the next.
dimensions:
snapshot:
description: dispatch periods in order
dtype: int
season:
description: the blocks the store cycles over
dtype: str
relations:
season_of:
description: the season a snapshot falls in
key: snapshot
values: season
parameters:
inflow:
description: energy arriving in a snapshot, whether or not it is wanted then
dims: [snapshot]
price:
description: what one unit of release earns in a snapshot
dims: [snapshot]
variables:
soc:
description: energy held at the end of a snapshot
dims: [snapshot]
bounds:
lower: 0
upper: 60
release:
description: energy released in a snapshot
dims: [snapshot]
bounds:
lower: 0
constraints:
season_balance:
description: >-
the level carried into a snapshot is the previous snapshot's, and a
season's first snapshot carries from that season's own last — so each
season ends where it began and hands the next one nothing
dims: [snapshot]
expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap', by=season_of, within=season) + inflow - release
objective:
sense: maximize
description: revenue from what the store releases
expression: sum(release * price)
What the answer looks like¶
snapshot season price inflow release soc
1 winter 1 0 0 0
2 winter 2 10 0 10
3 winter 5 0 10 0 ← winter's inflow, at winter's best price
4 winter 3 0 0 0 ← closes where it opened
5 summer 4 0 6 0 ← sells *before* its inflow arrives
6 summer 1 6 0 6
7 summer 2 0 0 6 ← closes where it opened, three snapshots later
Objective 74.0. Summer opens holding 6, sells it at the price-4 snapshot before its own inflow arrives, and the inflow at snapshot 6 puts the 6 back, so the season closes where it opened. Nothing constrains the level a season starts at, except that it must return to it.
The same instance written against the axis gives 80.0: the extra 6 comes out of winter, which had it only because summer's closing level leaked across the boundary.
Why it is one row¶
Per-season cycling can be written without by=, as a level each season begins
and ends at: a variable per season, an opening row that reads it, and a closing
row that pins it. Substituting the closing row into the opening one gives the
equation above, so the two are the same model.
With by= no first snapshot is named anywhere, so nothing needs re-checking
when the horizon is renumbered, extended, or cut into different seasons.
The grouping is data¶
season_of is a table passed beside the snapshot index, so the same file
expresses maintenance campaigns, representative days, market quarters or
contract windows. Move two snapshots between seasons by editing that table and
no clause changes:
shape: (7, 2)
┌──────────┬────────┐
│ snapshot ┆ season │
│ --- ┆ --- │
│ i64 ┆ str │
╞══════════╪════════╡
│ 1 ┆ winter │
│ 2 ┆ winter │
│ 3 ┆ winter │
│ 4 ┆ winter │
│ 5 ┆ summer │
│ 6 ┆ summer │
│ 7 ┆ summer │
└──────────┴────────┘
A snapshot the table has no row for belongs to no season, so it reaches nothing
and its row is not built. Absence reads the same way in
sum(by=).
Compare monthly budget, where such a column groups a sum, and multi-period, where it carries a capacity decision down onto the snapshots it covers. One relation, three jobs.