Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/reference/language/declarations.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ storage_balance:

storage_balance_initial:
foreach: [snapshot, storage]
where: "snapshot == index(snapshot, 0)"
where: "position(snapshot) == 0"
expression: soc == soc_initial
```

Expand Down
55 changes: 33 additions & 22 deletions docs/reference/language/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,28 @@ A `where:` is a boolean mask, and true means "this coordinate exists".
```text
where_expr ::= atom | "NOT" where_expr | where_expr ("AND"|"OR") where_expr
| "(" where_expr ")"
atom ::= NAME | NAME COMPARATOR value | "True" | "False"
atom ::= NAME | NAME COMPARATOR value | POSITION COMPARATOR INTEGER
| "True" | "False"
COMPARATOR ::= "<=" | ">=" | "==" | "!=" | "<" | ">"
value ::= NUMBER | QUOTED | NAME_OR_STRING | POSITION
POSITION ::= "index" "(" NAME "," INTEGER ")"
value ::= NUMBER | QUOTED | NAME_OR_STRING
POSITION ::= "position" "(" NAME [ "," "by" "=" NAME ] ")"
QUOTED ::= "'" chars "'" | '"' chars '"'
```

| Surface | Names a… | Meaning |
| ----------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` (bare) | parameter | what defined means is the **declaration's** to say: a `bool` is its own answer, a `str` is defined wherever the table has a row, and a number has to be finite as well — `0.0` counts, `inf` does not, though it is a value everywhere else |
| `name` (bare) | variable | the variable exists at this coordinate — the counterpart of the parameter row, and how you say which coordinates the row-dropping rule applies to |
| `name` (bare) | dimension | load error: it is true everywhere, so it reads as a condition and is not one. Compare it instead |
| `name OP value` | parameter | element-wise; a null compares false. The right-hand side is a literal number, or a bare name read as a string coordinate |
| `name OP value` | dimension | a filter on the frame's own coordinate column |
| `name` (bare) | lookup | defined: the label maps somewhere. A lookup may be [partial](dimensions.md#lookups), and this is how a declaration asks for the labels that do map |
| `name OP value` | lookup | a filter on the lookup's column of its `over` dimension's index — which therefore has to be in the frame. A null value is **false**, whatever the comparator |
| `name OP name` | two lookups | the one comparison whose both sides are structure. Legal only where both map out of the **same** dimension _and_ into the **same** one — `from != to` excludes a self-loop |
| `name OP index(name, i)` | one dimension, twice | the coordinate at position `i` of that dimension's own order — negative counts from the end. Both names must be the **same** dimension |
| `name OP index(name, i, by=lookup)` | a dimension and a lookup over it | the same, counted **within each group** the lookup makes — every period's first snapshot, whatever each period's length |
| `AND` `OR` `NOT` | — | case-insensitive; `NOT` binds tighter than `AND`, which binds tighter than `OR` |
| `True` / `False` | — | literals; `True` is the same as no `where` |
| Surface | Names a… | Meaning |
| -------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` (bare) | parameter | what defined means is the **declaration's** to say: a `bool` is its own answer, a `str` is defined wherever the table has a row, and a number has to be finite as well — `0.0` counts, `inf` does not, though it is a value everywhere else |
| `name` (bare) | variable | the variable exists at this coordinate — the counterpart of the parameter row, and how you say which coordinates the row-dropping rule applies to |
| `name` (bare) | dimension | load error: it is true everywhere, so it reads as a condition and is not one. Compare it instead |
| `name OP value` | parameter | element-wise; a null compares false. The right-hand side is a literal number, or a bare name read as a string coordinate |
| `name OP value` | dimension | a filter on the frame's own coordinate column |
| `name` (bare) | lookup | defined: the label maps somewhere. A lookup may be [partial](dimensions.md#lookups), and this is how a declaration asks for the labels that do map |
| `name OP value` | lookup | a filter on the lookup's column of its `over` dimension's index — which therefore has to be in the frame. A null value is **false**, whatever the comparator |
| `name OP name` | two lookups | the one comparison whose both sides are structure. Legal only where both map out of the **same** dimension _and_ into the **same** one — `from != to` excludes a self-loop |
| `position(name) OP i` | one dimension | where the row sits along that dimension's own order, as an integer — `0` is first, negative counts from the end. Both sides are integers, so every comparator reads the one way |
| `position(name, by=lookup) OP i` | a dimension and a lookup over it | the same, counted **within each group** the lookup makes — every period's first snapshot, whatever each period's length |
| `AND` `OR` `NOT` | — | case-insensitive; `NOT` binds tighter than `AND`, which binds tighter than `OR` |
| `True` / `False` | — | literals; `True` is the same as no `where` |

The mask's dims must not exceed the frame it sits in
([dim algebra](#dim-algebra)), and an undeclared bare name is a
Expand Down Expand Up @@ -226,8 +227,8 @@ load error naming the fix. A datetime boundary is a quoted ISO date —
`snapshot > '2030-01-01'`, or `'2030-01-01T06:00'` with a time. Calendar
arithmetic, resampling and timezone conversion stay data prep.

**`index(dim, i)` names a coordinate by where it sits**, so a boundary clause
survives the index being relabelled:
**`position(dim)` converts a dimension to where the row sits along it**, so a
boundary clause survives the index being relabelled:

```yaml
dimensions:
Expand All @@ -239,20 +240,30 @@ variables:
constraints:
soc_start:
foreach: [snapshot]
where: "snapshot == index(snapshot, 0)" # not: snapshot == 0
where: "position(snapshot) == 0" # not: snapshot == 0
expression: soc == soc_initial
```

A recurrence needs its first position seeded, and the label that happens to be
there is a property of the data — relabel `[0, 1, 2]` to `[1, 2, 3]` and
`snapshot == 0` matches nothing, leaving the recurrence unanchored. `-1` is the
last coordinate, `-2` the one before it. A position no coordinate occupies is
last position, `-2` the one before it. A position no coordinate occupies is
an **error at bind**, not an empty mask: the clause exists to seed a row, and
seeding none is the failure it was written to prevent.

The order counted along is the dimension's own — the one `shift` walks, and the
one the index declares — not the bytewise order a label comparison uses.

**The conversion is on the left, and that is what makes an ordering readable.**
`position(snapshot) > 0` is "not the first row", on any axis, because both
sides are integers. Naming the coordinate _at_ a position and comparing
coordinates against it would have made the same clause mean either that or "a
coordinate sorting after the first one" — two different masks wherever the
coordinates do not arrive sorted, and nothing in a file says they do
([#32](https://github.com/energy-models/math-spec/issues/32)). A comparison of
_values_ is still written against the dimension itself, where it always was:
`snapshot > '2030-01-01'`.

**`by=` counts inside each group a lookup makes**, which is the boundary a
multi-period model wants — one seeded row per period rather than one per
horizon:
Expand All @@ -270,7 +281,7 @@ variables:
constraints:
soc_start:
foreach: [snapshot]
where: "snapshot == index(snapshot, 0, by=period_of)"
where: "position(snapshot, by=period_of) == 0"
expression: soc == at(soc_initial, by=period_of)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/language/piecewise.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ weights with nothing making them a curve.

**The breakpoint order is `over`'s index order**, the one every dimension has:
the order its labels are first written in, which `shift` walks and
`index(bp, 0)` names. So the `bp` index is the curve's x-axis, and a values
`position(bp) == 0` names. So the `bp` index is the curve's x-axis, and a values
parameter is a lookup against it — a table is a function of its coordinates and
the order its rows arrive in means nothing, on either lane. "Strictly
increasing breakpoints" below is increasing _in that order_: write the index
Expand Down
31 changes: 25 additions & 6 deletions docs/reference/notation.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ parameters:

| Symbol | Meaning |
|---|---|
| $\mathcal{T}$ | index $t$ — `snapshot` with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}$ |
| $\mathcal{T}$ | index $t$ — `snapshot` (`int` coordinates) with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}$ |
| $\mathcal{G}$ | index $g$ — `generator` with $\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\enspace \mathrm{gen\_tech}: \mathcal{G} \to \mathcal{E}$ carrying label $\mathrm{tech}$ |
| $\mathcal{B}$ | index $b$ — `bus` with $\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\enspace \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z}$ |
| $\mathcal{Z}$ | index $z$ — `zone` |
Expand Down Expand Up @@ -121,6 +121,12 @@ $t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leav

$t \ominus^{\mathrm{lookup}(t)} k$ denotes a translation counted inside the group a lookup puts $t$ in (`shift(by=lookup)`), so a term never crosses out of its own group. The two modifiers take different slots — the group above, the fill below — so $t \boxminus_{v}^{\mathrm{lookup}(t)} k$ is both at once.

$\mathrm{pos}(t)$ denotes where index $t$ sits along its dimension's own order — the order `shift` walks, 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.

$\mathrm{pos}_{\mathrm{lookup}(t)}(t)$ counts within the group a lookup puts $t$ in: the subscript names the map, $\mathcal{T}_{\mathrm{lookup}(t)}$ is the group it lands in, and that group has a first position of its own.

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

### The objective

#### `objective`
Expand Down Expand Up @@ -378,11 +384,24 @@ a position in a dimension, and the same position within a group
```yaml
first:
foreach: [snapshot, generator]
where: "snapshot == index(snapshot, 0) OR snapshot == index(snapshot, 0, by=season_of)"
where: "position(snapshot) == 0 OR position(snapshot, by=season_of) == 0"
expression: on == 1
```

$$\mathit{on}_{t,g} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( t = \mathrm{index}(\mathcal{T}, 0) \vee t = \mathrm{index}(\mathcal{T}, 0, \mathrm{season\_of}(t)) \right)$$
$$\mathit{on}_{t,g} = 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = 0 \right)$$

#### `last`

the same two counted from the end, which print against a size rather than as themselves

```yaml
last:
foreach: [snapshot, generator]
where: "position(snapshot) == -1 OR position(snapshot, by=season_of) == -1"
expression: on == 0
```

$$\mathit{on}_{t,g} = 0 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G} \thinspace:\thinspace \left( \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season\_of}(t)} \rvert - 1 \right)$$

#### `northern`

Expand Down Expand Up @@ -715,11 +734,11 @@ cost_curve:
method: lp
```

$$\mathit{op\_cost}_{t,g} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \ge \left( \mathrm{y}_{g,b} - \mathrm{y}_{g,b \boxminus_{0} 1} \right) \cdot \left( p_{t,g} - \mathrm{x}_{g,b} \right) + \mathrm{y}_{g,b} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b \neq \mathrm{index}(\mathcal{B}, 0)$$
$$\mathit{op\_cost}_{t,g} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \ge \left( \mathrm{y}_{g,b} - \mathrm{y}_{g,b \boxminus_{0} 1} \right) \cdot \left( p_{t,g} - \mathrm{x}_{g,b} \right) + \mathrm{y}_{g,b} \cdot \left( \mathrm{x}_{g,b} - \mathrm{x}_{g,b \boxminus_{0} 1} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{pos}(b) \neq 0$$

$$p_{t,g} \ge \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, 0)$$
$$p_{t,g} \ge \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{pos}(b) = 0$$

$$p_{t,g} \le \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace b = \mathrm{index}(\mathcal{B}, -1)$$
$$p_{t,g} \le \mathrm{x}_{g,b} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G},\enspace b \in \mathcal{B} \thinspace:\thinspace \mathrm{pos}(b) = \lvert \mathcal{B} \rvert - 1$$

### Sets carried to the solver

Expand Down
4 changes: 2 additions & 2 deletions src/math_spec/piecewise.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ def _expand_lp(
d = pw.over
run = f'({x_link.values} - shift({x_link.values}, over={d}, offset=1, edge=0))'
rise = f'({y_link.values} - shift({y_link.values}, over={d}, offset=1, edge=0))'
interior = f'{mask} AND NOT {name}_starts' if mask else f'{d} != index({d}, 0)'
interior = f'{mask} AND NOT {name}_starts' if mask else f'position({d}) != 0'
raw['constraints'][f'{name}_chord'] = {
'foreach': [*frame, d],
'where': interior,
Expand All @@ -264,7 +264,7 @@ def _expand_lp(
),
}
edges = (('domain_lo', '>=', f'{name}_starts'), ('domain_hi', '<=', f'{name}_ends'))
axis = (('domain_lo', '>=', f'{d} == index({d}, 0)'), ('domain_hi', '<=', f'{d} == index({d}, -1)'))
axis = (('domain_lo', '>=', f'position({d}) == 0'), ('domain_hi', '<=', f'position({d}) == -1'))
for suffix, sense, at in edges if mask else axis:
if mask:
raw.setdefault('parameters', {})[at] = {
Expand Down
Loading
Loading