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
138 changes: 138 additions & 0 deletions docs/examples/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,144 @@ 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`.
37 changes: 37 additions & 0 deletions docs/reference/language/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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_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 |

`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 @@ -146,6 +149,36 @@ 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.

## `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 @@ -367,6 +400,10 @@ language is rendered the same way, on one page:
| `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 @@ -299,6 +299,42 @@ seasonal_window:

$$\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}$$

#### `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_outlook`

the leading window partitioned, so it stops at the season's end

```yaml
seasonal_outlook:
foreach: [snapshot, generator]
expression: sum_forward(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
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) }
29 changes: 29 additions & 0 deletions examples/operators/sum_forward_by_parameter.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 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) }
31 changes: 31 additions & 0 deletions examples/operators/sum_forward_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_forward(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_wrap.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 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) }
2 changes: 1 addition & 1 deletion src/math_spec/degree.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def _multi_term(node: ExpressionNode) -> bool:
#: The operators that fold several coordinates onto one, and so turn a term
#: into a sum of terms. ``at`` and ``shift`` re-index and are not here: they
#: move a term, leaving one term where there was one.
_REDUCTIONS = frozenset({'sum', 'sum_back'})
_REDUCTIONS = frozenset({'sum', 'sum_back', 'sum_forward'})


def check_expression(node: ExpressionNode, context: str, *, ceiling: int = 1) -> None:
Expand Down
Loading
Loading