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
25 changes: 22 additions & 3 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 @@ -116,6 +116,12 @@ $t \ominus k$ denotes cyclic translation: index $t-k$ taken modulo the size of t

$t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leaves the dimension (`shift(edge=v)`), so the row at that boundary is built and carries $v$ rather than being dropped.

$\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 @@ -377,7 +383,20 @@ first:
expression: on == 1
```

$$\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}(t, \mathrm{season\_of}(t)) = 0 \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 @@ -645,7 +664,7 @@ $$\mathit{op\_cost}_{t,g} \cdot \left( \mathit{bp}^{\mathrm{x}}_{g,b} - \mathit{

$$p_{t,g} \ge \mathit{bp}^{\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 \mathit{bp}^{\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) = -1$$
$$p_{t,g} \le \mathit{bp}^{\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
1 change: 1 addition & 0 deletions src/math_spec/typeset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def typeset(
if legend:
blocks += [fmt.glossary(group.title, group.entries) for group in walk.glossaries()]
blocks += [fmt.note(text) for text in walk.translation_notes()]
blocks += [fmt.note(text) for text in walk.position_notes()]
return fmt.document([*blocks, *rendered], standalone=standalone)


Expand Down
9 changes: 9 additions & 0 deletions src/math_spec/typeset/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ def superscript(self, base: str, tail: str) -> str: ...

def parenthesise(self, inner: str) -> str: ...

def cardinality(self, inner: str) -> str:
"""How many members a set has: ``|T|``.

A fence rather than an entry in :data:`OPERATOR_NAMES`, which is a
vocabulary of *infix* spellings — ``tests/typeset/test_typeset.py``
compiles every one of them between two operands.
"""
...

def fraction(self, numerator: str, denominator: str) -> str: ...

def summation(self, domain: str, body: str) -> str: ...
Expand Down
3 changes: 3 additions & 0 deletions src/math_spec/typeset/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ def superscript(self, base: str, tail: str) -> str:
def parenthesise(self, inner: str) -> str:
return rf'\left( {inner} \right)'

def cardinality(self, inner: str) -> str:
return rf'\lvert {inner} \rvert'

def fraction(self, numerator: str, denominator: str) -> str:
return rf'\frac{{{numerator}}}{{{denominator}}}'

Expand Down
3 changes: 3 additions & 0 deletions src/math_spec/typeset/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def superscript(self, base: str, tail: str) -> str:
def parenthesise(self, inner: str) -> str:
return _LATEX.parenthesise(inner)

def cardinality(self, inner: str) -> str:
return _LATEX.cardinality(inner)

def fraction(self, numerator: str, denominator: str) -> str:
return _LATEX.fraction(numerator, denominator)

Expand Down
3 changes: 3 additions & 0 deletions src/math_spec/typeset/typst.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ def superscript(self, base: str, tail: str) -> str:
def parenthesise(self, inner: str) -> str:
return f'({inner})'

def cardinality(self, inner: str) -> str:
return f'abs({inner})'

def fraction(self, numerator: str, denominator: str) -> str:
return f'frac({numerator}, {denominator})'

Expand Down
94 changes: 87 additions & 7 deletions src/math_spec/typeset/walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ class Walk:
"""Walks a validated schema, emitting :class:`Line`s in one format.

Stateful only in what it has *noticed* — which edge policies appeared,
which the legend needs to explain the symbols they print.
which positional forms printed, and which dimensions were compared against
a coordinate that is a number; all three are things the legend has to
explain once the equations print them.
"""

def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fmt: Format) -> None:
Expand All @@ -223,6 +225,8 @@ def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fm
self.symbols = symbols
self.format = fmt
self.policies: set[str] = set()
self.positions: set[str] = set()
self.numeric_coordinates: set[str] = set()

def op(self, name: str) -> str:
return self.format.operators[name]
Expand Down Expand Up @@ -458,14 +462,20 @@ def _where(self, node: WhereNode, ctx: _Context) -> tuple[str, int]:
return f'{left} {self.op(_PREDICATES[node.op])} {self.literal(node.value)}', 2

if isinstance(node, DimensionComparisonNode):
if isinstance(node.value, (int, float)):
# a label that is a number is the one the legend has to place:
# every other coordinate prints as prose and cannot be read as
# a position in the first place
self.numeric_coordinates.add(node.name)
return f'{ctx.subscript(node.name)} {self.op(_PREDICATES[node.op])} {self.literal(node.value)}', 2

if isinstance(node, DimensionPositionNode):
grouping = (
None if node.by is None else self.format.apply(self.format.upright(node.by), ctx.subscript(node.name))
)
place = self.position(ctx.subscript(node.name), grouping)
return f'{place} {self.op(_PREDICATES[node.op])} {self.number(node.position)}', 2
ordinal = self.ordinal(node.name, node.position, grouping)
return f'{place} {self.op(_PREDICATES[node.op])} {ordinal}', 2

if isinstance(node, LookupComparisonNode):
applied = self.format.apply(self.format.upright(node.name), ctx.subscript(node.over))
Expand Down Expand Up @@ -506,12 +516,36 @@ def position(self, index: str, grouping: str | None = None) -> str:

Applied to the *row* rather than to the set, because that is what it
converts: a coordinate to where that coordinate sits. *grouping* is the
lookup already applied to the row, and prints as a second argument so
the group a position is counted within is visible where the position
is.
lookup already applied to the row, and rides as a **subscript** rather
than as a second argument — a modifier saying which order is being
counted, the way an edge fill rides its translation. As an argument it
sat where the first one's integer sits, and read as a second position.
"""
inner = index if grouping is None else f'{index}, {grouping}'
return self.format.apply(self.op('position'), inner)
self.positions.add('grouped' if grouping is not None else 'plain')
symbol = self.op('position')
if grouping is not None:
symbol = self.format.subscript(symbol, [grouping])
return self.format.apply(symbol, index)

def ordinal(self, dimension: str, at: int, grouping: str | None = None) -> str:
"""The position compared against, counted from the end where it is negative.

A position is a place in the order, and the legend runs that order
from ``0`` to the size less one — so ``-1`` printed as itself asserts a
position the page has just said cannot exist. What it counts back from
is that size, and the *group's* size where the count is grouped, which
is the set those positions are positions in.

``0`` prints as ``0``: the file is 0-based and so is the page, so a
clause can be read off one and written into the other.
"""
if at >= 0:
return self.number(at)
self.positions.add('from_end')
size = self.symbols.set[dimension]
if grouping is not None:
size = self.format.subscript(size, [grouping])
return f'{self.format.cardinality(size)} {self.op("minus")} {self.number(-at)}'

def conjoined(self, ctx: _Context, *nodes: WhereNode | None) -> str:
parts = [self.where(n, ctx, need=1) for n in nodes if n is not None]
Expand Down Expand Up @@ -677,6 +711,11 @@ def _coords(self, dim: str) -> str:
targeted = self.schema.targeted_of(dim)
labels = self.schema.labels_of(dim)
clauses = []
if dim in self.numeric_coordinates:
# only where an equation compared this index against a number,
# which is the one place "position 3" and "the coordinate 3" are
# both readings of the same line
clauses.append(f' ({self.format.mono(self.schema.dimensions[dim].dtype)} coordinates)')
if targeted:
maps = self.format.joined(
[
Expand Down Expand Up @@ -716,3 +755,44 @@ def translation_notes(self) -> list[str]:
f'at that boundary is built and carries {self.format.math("v")} rather than being dropped.'
)
return notes

def position_notes(self) -> list[str]:
"""A sentence for each positional symbol the model actually printed.

Gated the way the translation notes are, and the first of them is what
the page cannot go without. A reader arrives from papers where the
index *is* the ordinal — sets are written as ``{1, …, T}`` there, so
nothing is marked because nothing needs to be — and this language
indexes by coordinates instead. A page printing both
``pos(t) = 0`` and ``t >= 3`` therefore has to say once which of the
two is the position, or the reader recovers a different model from the
one the file holds.
"""
notes = []
if self.positions:
index = self.format.math('t')
place = self.format.math(self.format.apply(self.op('position'), 't'))
dash = self.format.dash
notes.append(
f"{place} denotes where index {index} sits along its dimension's own order {dash} the order "
f'{self.format.mono("shift")} walks, not the order labels sort in {dash} counted from '
f'{self.format.math("0")}. The index itself stays the coordinate, so {index} compares against '
f'labels and {place} against positions.'
)
if 'grouped' in self.positions:
applied = self.format.apply(self.format.upright('lookup'), 't')
grouped = self.format.math(self.format.apply(self.format.subscript(self.op('position'), [applied]), 't'))
group = self.format.math(self.format.subscript(self.format.script('T'), [applied]))
notes.append(
f'{grouped} counts within the group a lookup puts {self.format.math("t")} in: the subscript names '
f'the map, {group} is the group it lands in, and that group has a first position of its own.'
)
if 'from_end' in self.positions:
size = self.format.cardinality(self.format.script('T'))
last = self.format.math(f'{size} {self.op("minus")} {self.number(1)}')
notes.append(
f'{self.format.math(size)} denotes the size of the set being counted along, and a position '
f'counted from the end prints against it {self.format.dash} {last} is the last position, one '
f'less than the size because the first is {self.format.math("0")}.'
)
return notes
11 changes: 9 additions & 2 deletions tests/typeset/golden/latex.out
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

\paragraph{Sets}
\begin{description}
\item[$\mathcal{T}$] index $t$ --- \texttt{snapshot} with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}$
\item[$\mathcal{T}$] index $t$ --- \texttt{snapshot} (\texttt{int} coordinates) with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}$
\item[$\mathcal{G}$] index $g$ --- \texttt{generator} with $\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{gen\_tech}: \mathcal{G} \to \mathcal{E}$ carrying label $\mathrm{tech}$
\item[$\mathcal{B}$] index $b$ --- \texttt{bus} with $\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z}$
\item[$\mathcal{Z}$] index $z$ --- \texttt{zone}
Expand Down Expand Up @@ -51,6 +51,12 @@

\noindent $t \boxminus_{v} k$ denotes translation with $v$ standing where index $t-k$ leaves the dimension (\texttt{shift(edge=v)}), so the row at that boundary is built and carries $v$ rather than being dropped.

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

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

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

\paragraph{Objective}
\begin{align}
&& \max & \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathit{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathit{cost}_{g} \cdot \mathit{growth}^{\mathit{lead}_{g}} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p^{\mathrm{max}}_{g} - \mathit{reserve} + -\mathit{headroom}
Expand All @@ -77,7 +83,8 @@
\text{total} && \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} & \le \mathit{budget} \\
\text{scalar} && \mathit{units}_{g} & \le \mathit{budget} && \forall\, g \in \mathcal{G} \,:\, \mathit{cost}_{g} \text{ is defined} \\
\text{running} && \mathit{theta}_{b} & \le \mathit{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathit{theta}_{b} \text{ exists} \wedge t \ge 3 \\
\text{first} && \mathit{on}_{t,g} & = 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{pos}(t) = 0 \vee \mathrm{pos}(t, \mathrm{season\_of}(t)) = 0 \right) \\
\text{first} && \mathit{on}_{t,g} & = 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = 0 \right) \\
\text{last} && \mathit{on}_{t,g} & = 0 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \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) \\
\text{northern} && \mathit{slack}_{t} & \le \mathit{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{north} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} \\
\text{always} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \top \\
\text{never} && \mathit{slack}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \bot
Expand Down
Loading
Loading