Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
174 changes: 174 additions & 0 deletions docs/examples/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,180 @@ objective: { sense: minimize, expression: sum(on) }
```

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

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

`examples/operators/sum_back_partitioned.yaml`

```yaml
description: >-
A window that stops at each group's edge: representative days are separate
samples rather than consecutive hours, so a window must not reach across the
seam between two of them.

dimensions:
unit: { dtype: str }
hour: { dtype: int }
day: { dtype: str }

lookups:
day_of: { over: hour, into: day }

variables:
started:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
stays_up_inside_its_day:
foreach: [unit, hour]
expression: sum_back(started, over=hour, within=3, by=day_of) <= on

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

$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h -^{\mathrm{day\_of}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$

### `sum_forward(array, over=dim, within=n)`

`examples/operators/sum_forward.yaml`

```yaml
description: >-
A leading window of a fixed width: a unit that shuts down in the next three
hours is running now, so it runs its minimum time *before* it may stop.

dimensions:
unit: { dtype: str }
hour: { dtype: int }

parameters:
min_up: { dims: [unit], dtype: int }

variables:
stopped:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
runs_before_it_stops:
foreach: [unit, hour]
expression: sum_forward(stopped, over=hour, within=3) <= on

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

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

### `sum_forward(array, over=dim, within=p)`

`examples/operators/sum_forward_by_parameter.yaml`

```yaml
description: >-
A leading window whose width is data — `within:` names an integer parameter,
so each unit runs for its *own* minimum time before it may stop.

dimensions:
unit: { dtype: str }
hour: { dtype: int }

parameters:
min_up: { dims: [unit], dtype: int }

variables:
stopped:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
runs_before_it_stops:
foreach: [unit, hour]
expression: sum_forward(stopped, over=hour, within=min_up) <= on

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

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

### `sum_forward(array, over=dim, within=p, edge='wrap')`

`examples/operators/sum_forward_wrap.yaml`

```yaml
description: >-
A leading window on a representative period that repeats, so the window at
the last hour reaches around into the first.

dimensions:
unit: { dtype: str }
hour: { dtype: int }

parameters:
min_up: { dims: [unit], dtype: int }

variables:
stopped:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
runs_before_it_stops:
foreach: [unit, hour]
expression: sum_forward(stopped, over=hour, within=min_up, edge='wrap') <= on

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

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

### `sum_forward(array, over=dim, within=n, by=lookup)`

`examples/operators/sum_forward_partitioned.yaml`

```yaml
description: >-
A window that stops at each group's edge: representative days are separate
samples rather than consecutive hours, so a window must not reach across the
seam between two of them.

dimensions:
unit: { dtype: str }
hour: { dtype: int }
day: { dtype: str }

lookups:
day_of: { over: hour, into: day }

variables:
started:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
stays_up_inside_its_day:
foreach: [unit, hour]
expression: sum_forward(started, over=hour, within=3, by=day_of) <= on

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

$\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h' -^{\mathrm{day\_of}(h)} h < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$
<!-- gallery:end -->

Regenerate with `pixi run python -m tools.gallery`.
83 changes: 83 additions & 0 deletions docs/reference/language/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Dimension arguments are name-checked at load time, so
| `sum_back(array, over=dim, within=n)` | the sum of the last `n` positions along `dim`, ending at _t_ |
| `sum_back(array, over=dim, within=p)` | `p` an integer parameter: each entity gets **its own** window length |
| `sum_back(array, over=dim, within=p, edge='wrap')` | the window reaches around the axis rather than stopping short at its start |
| `sum_back(array, over=dim, within=n, by=lookup)` | the window stops at each group's edge rather than reaching across it |
| `sum_forward(array, over=dim, within=n)` | the sum of the next `n` positions along `dim`, starting at _t_ |
| `sum_forward(array, over=dim, within=p)` | `p` an integer parameter: each entity gets **its own** window length |
| `sum_forward(array, over=dim, within=p, edge='wrap')` | the window reaches around the axis rather than stopping short at its end |
| `sum_forward(array, over=dim, within=n, by=lookup)` | the window stops at each group's edge rather than reaching across it |

`array` is any expression of the right dim set, so these read a **parameter**
as readily as a variable. Each row as the typesetter prints it is
Expand Down Expand Up @@ -145,6 +150,79 @@ there is a load error, because adding a constant is something the expression can
say for itself. `edge='wrap'` makes the window reach around instead, which is
what a representative period that repeats asks for.

### A window that stops at each group's edge

`by=` partitions the axis the window walks, so it reaches only within the group
the row being written is in. It is the same `by=` as
[`shift`'s](#a-translation-that-stops-at-each-groups-edge), and takes a lookup
**over the dimension being summed over**.

```yaml
dimensions:
unit: { dtype: str }
hour: { dtype: int }
day: { dtype: str }
lookups:
day_of: { over: hour, into: day }
variables:
started: { foreach: [unit, hour], domain: binary }
on: { foreach: [unit, hour], domain: binary }
constraints:
stays_up_inside_its_day:
foreach: [unit, hour]
expression: sum_back(started, over=hour, within=3, by=day_of) <= on
```

What asks for this is a model built on **representative periods**. A dozen typical
days stand in for a year, each carrying a weight, and their hours are numbered
consecutively — but that numbering is a storage order, not a timeline. The last
hour of one representative day does not precede the first hour of the next in any
physical sense; they are separate samples.

An unpartitioned window sums straight across that seam. It builds, it solves, and
the commitment it returns couples two independent days through a boundary that
does not exist — which nothing warns about, because the axis is ordered and the
language has no other way to know the order is not time.

The `edge=` rules then read one group at a time, exactly as they do for `shift`:
bare, a window at a group's opening is **short**, reaching no further back than
the group starts; `edge='wrap'` closes **each group** onto its own end, which is
what a representative day standing for a day that repeats asks for.

A coordinate the lookup sends nowhere is in no group, so its window holds only
itself. `sum_forward` partitions identically — the group bounds the direction it
reaches, whichever that is.

## `sum_forward`

`sum_forward(x, over=d, within=n)` is [`sum_back`](#sum_back) the other way: the
sum of the next `n` positions along `d`, **starting** at the one being written.
Every rule above holds unchanged — the dimension survives, a width of `1` is `x`
itself, `within=` may name an integer parameter under the same two conditions,
and `edge=` takes `'wrap'` or nothing.

Only the edge it runs off changes. A window that reaches past the *end* of the
axis is short, not empty, so again no row is lost; `edge='wrap'` reaches around
into the start instead.

```yaml
constraints:
runs_before_it_stops:
foreach: [unit, hour]
expression: sum_forward(stopped, over=hour, within=min_up) <= on
```

As math the two are transposes — the primed index moves to the other side of the
subtraction, $0 \le t - t' < n$ becoming $0 \le t' - t < n$, and nothing else in
the equation moves.

A fixed width composes out of `shift` in either direction, but a forward window
does not reduce to one: `shift` past the end **drops** the rows it vacates or
fills them with a number, where a short window keeps them. And a width that is a
parameter has no forward spelling at all, since it would need a negative named
offset — which the language refuses, so that a translation's direction is never
something the data decides ([`shift`](#shift)).

## `shift`

`shift(x, over=d, offset=n)` reaches along an axis: it is the value at _t−n_, in
Expand Down Expand Up @@ -360,6 +438,11 @@ language is rendered the same way, on one page:
| `sum_back(array, over=dim, within=n)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_back(array, over=dim, within=p)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_back(array, over=dim, within=p, edge='wrap')` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_back(array, over=dim, within=n, by=lookup)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h -^{\mathrm{day\_of}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_forward(array, over=dim, within=n)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h' - h < 3} \mathit{stopped}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_forward(array, over=dim, within=p)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h' - h < \mathrm{min\_up}} \mathit{stopped}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_forward(array, over=dim, within=p, edge='wrap')` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h' \ominus h < \mathrm{min\_up}} \mathit{stopped}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |
| `sum_forward(array, over=dim, within=n, by=lookup)` | $\sum_{h' \in \mathcal{H} \thinspace:\thinspace 0 \le h' -^{\mathrm{day\_of}(h)} h < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\thinspace u \in \mathcal{U},\enspace h \in \mathcal{H}$ |

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

Expand Down
36 changes: 36 additions & 0 deletions docs/reference/notation.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,42 @@ history:

$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### `horizon`

a leading window: the same three positions, counted the other way

```yaml
horizon:
foreach: [snapshot, generator]
expression: sum_forward(on, over=snapshot, within=3) <= units
```

$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t' - t < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### `outlook`

the same leading window, its width in the data and its edge wrapped

```yaml
outlook:
foreach: [snapshot, generator]
expression: sum_forward(on, over=snapshot, within=min_up, edge='wrap') <= units
```

$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t' \ominus t < \mathrm{min\_up}} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### `seasonal_window`

a window partitioned by a lookup: the group rides on the operator

```yaml
seasonal_window:
foreach: [snapshot, generator]
expression: sum_back(on, over=snapshot, within=3, by=season_of) <= units
```

$$\sum_{t' \in \mathcal{T} \thinspace:\thinspace 0 \le t -^{\mathrm{season\_of}(t)} t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### `pullback`

at(), which re-indexes through a lookup instead of an offset
Expand Down
31 changes: 31 additions & 0 deletions examples/operators/sum_back_partitioned.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: math-spec Contributors
#
# SPDX-License-Identifier: MIT

description: >-
A window that stops at each group's edge: representative days are separate
samples rather than consecutive hours, so a window must not reach across the
seam between two of them.

dimensions:
unit: { dtype: str }
hour: { dtype: int }
day: { dtype: str }

lookups:
day_of: { over: hour, into: day }

variables:
started:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
stays_up_inside_its_day:
foreach: [unit, hour]
expression: sum_back(started, over=hour, within=3, by=day_of) <= on

objective: { sense: minimize, expression: sum(on) }
29 changes: 29 additions & 0 deletions examples/operators/sum_forward.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: math-spec Contributors
#
# SPDX-License-Identifier: MIT

description: >-
A leading window of a fixed width: a unit that shuts down in the next three
hours is running now, so it runs its minimum time *before* it may stop.

dimensions:
unit: { dtype: str }
hour: { dtype: int }

parameters:
min_up: { dims: [unit], dtype: int }

variables:
stopped:
foreach: [unit, hour]
domain: binary
on:
foreach: [unit, hour]
domain: binary

constraints:
runs_before_it_stops:
foreach: [unit, hour]
expression: sum_forward(stopped, over=hour, within=3) <= on

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