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
11 changes: 10 additions & 1 deletion docs/examples/commitment.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ a quantity: exactly one arm applies at every coordinate, so `ramp_up` can use
it the way it uses a parameter. A gap or an overlap is a load error naming a
witness for it.

It prints the way a paper writes it: `ramp_up` names the quantity, and the
block itself prints once below, under **Definitions**.

<!-- gallery:begin -->
```yaml
description: >-
Expand Down Expand Up @@ -142,7 +145,13 @@ $$p_{t,g} \ge \mathit{status}_{t,g} \cdot p^{\mathrm{min}}_{g} \qquad \forall\th

**`ramp_up`**

$$p_{t,g} - p_{t \boxminus_{0} 1,g} \le \mathit{ramp\_limit}_{g} \cdot \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} + \mathit{start\_up\_limit}_{g} \cdot \left( 1 - \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$
$$p_{t,g} - p_{t \boxminus_{0} 1,g} \le \mathit{ramp\_limit}_{g} \cdot \mathit{previous\_status}_{t,g} + \mathit{start\_up\_limit}_{g} \cdot \left( 1 - \mathit{previous\_status}_{t,g} \right) \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### Definitions

**`previous_status`**

$$\mathit{previous\_status}_{t,g} = \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### Variable domains

Expand Down
14 changes: 10 additions & 4 deletions docs/reference/language/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,17 @@ and each `when` must sit **inside** that frame; neither may widen it.
[`examples/commitment.yaml`](../../examples/commitment.md) is the whole model
this comes from, beside the math it prints.

**A reference carries the regions with it.** `no_restart` above names the
quantity once, so the inequality is written once and the case conditions ride
along into what it prints:
**A reference names the quantity; the block prints once.** A cased expression
is the one kind that does not read well inlined — three arms are three rows
tall, so whatever follows in the equation sits beside the middle one. So a use
prints the symbol,

$$\mathit{status}_{t,g} - \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$
$$\mathit{status}_{t,g} - \mathit{previous\_status}_{t,g} \le 1 \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

and the block prints under **Definitions**, which is where a paper states a
quantity defined by region:

$$\mathit{previous\_status}_{t,g} = \begin{cases} 1 & \text{if } \neg \mathit{committable}_{g} \cr \mathit{status}^{\mathrm{initial}}_{g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) = 0 \cr \mathit{status}_{t - 1,g} & \text{if } \mathit{committable}_{g} \wedge \mathrm{pos}(t) > 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

## Macros

Expand Down
41 changes: 23 additions & 18 deletions docs/reference/notation.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,6 @@ $\mathrm{pos}_{\mathrm{lookup}(t)}(t)$ counts within the group a lookup puts $t$

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

### Named expressions

A named expression is substituted where its name is used, so it prints nothing under its own name — its math is in the row of the constraint that names it. `cases:` is why the page shows the block: a value defined by region is a construct, and the regions read beside the declaration rather than at the use site.

```yaml
expressions:
startup_cost: # a value defined by region: the cases partition the frame, so exactly one arm applies at every coordinate
foreach: [snapshot, generator]
cases:
opening:
when: "position(snapshot) == 0"
expression: cost * p_max
later:
when: "position(snapshot) != 0"
expression: cost
```

### The objective

#### `objective`
Expand Down Expand Up @@ -437,7 +420,7 @@ started:
expression: slack >= on * startup_cost
```

$$\mathit{slack}_{t} \ge \mathit{on}_{t,g} \cdot \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \cr \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$
$$\mathit{slack}_{t} \ge \mathit{on}_{t,g} \cdot \mathit{startup\_cost}_{t,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### `always`

Expand Down Expand Up @@ -465,6 +448,28 @@ never:

$$\mathit{slack}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \bot$$

### Definitions

A named expression is substituted where its name is used, so it normally prints nothing under its own name. A cased one is the exception: its value is defined by region, which is a definition of its own, and the equations using it name it rather than repeating the block.

#### `startup_cost`

a value defined by region: the cases partition the frame, so exactly one arm applies at every coordinate

```yaml
startup_cost:
foreach: [snapshot, generator]
cases:
opening:
when: "position(snapshot) == 0"
expression: cost * p_max
later:
when: "position(snapshot) != 0"
expression: cost
```

$$\mathit{startup\_cost}_{t,g} = \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \cr \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

### Variable domains

#### `p`
Expand Down
12 changes: 9 additions & 3 deletions src/math_spec/typeset/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,16 @@ def typeset(
table = symbols if isinstance(symbols, SymbolTable) else SymbolTable.load(symbols)
walk = Walk(schema, Namespace.of(schema), Symbols(schema, fmt, table.checked_against(schema)), fmt)

# `definitions()` prints what the other sections reached, so it runs after
# them — a statement apart rather than a call inside the list, where the
# order it depends on would be invisible.
objective, constraints, variables = walk.objective(), walk.constraints(), walk.variables()
definitions = walk.definitions()
sections = [
('Objective', walk.objective()),
('Subject to', walk.constraints()),
('Variable domains', walk.variables()),
('Objective', objective),
('Subject to', constraints),
('Definitions', definitions),
('Variable domains', variables),
]
rendered = [fmt.section(title, fmt.equations(lines, numbered=numbered)) for title, lines in sections if lines]

Expand Down
6 changes: 4 additions & 2 deletions src/math_spec/typeset/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@ def summation(self, domain: str, body: str) -> str: ...
def cases(self, arms: list[tuple[str, str]]) -> str:
"""A value defined by region: ``(value, condition)`` per arm.

The arms partition the frame, so there is no otherwise-arm and no
order-dependence — a format may print them in any order it likes.
The arms partition the frame, so every one carries a condition and
there is no otherwise-arm to print last. They arrive in the order the
file declares them and print in it — nothing depends on the order, but
a reader comparing the page to the file does.
"""
...

Expand Down
18 changes: 15 additions & 3 deletions src/math_spec/typeset/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ def _derive_name_symbol(name: str, declared: frozenset[str], fmt: Format) -> str
return _word(name, fmt)


def printed_expressions(schema: Buildable) -> frozenset[str]:
"""The named expressions that reach the page under their own name.

A named expression is substituted where it is used, so it normally prints
nothing a symbol could stand for. A **cased** one is the exception: its
value is defined by region, which reads as a definition of its own and is
referred to by name from the equations that use it.
"""
return frozenset(name for name, block in schema.expressions.items() if block.cases)


class Symbols:
r"""How every declared name prints: overrides first, derivation for the rest.

Expand All @@ -82,11 +93,12 @@ def __init__(self, schema: Buildable, fmt: Format, table: SymbolTable) -> None:
f'and nothing translates between notations — write a {fmt.notation} table.'
)
raise SchemaError(msg)
declared = frozenset({*schema.dimensions, *schema.parameters, *schema.variables})
printed = printed_expressions(schema)
declared = frozenset({*schema.dimensions, *schema.parameters, *schema.variables, *printed})

self.name: dict[str, str] = {
name: table.names[name] if name in table.names else _derive_name_symbol(name, declared, fmt)
for name in (*schema.parameters, *schema.variables)
for name in (*schema.parameters, *schema.variables, *printed)
}
spoken_for = {s for s in self.name.values() if len(s) == 1}

Expand Down Expand Up @@ -204,7 +216,7 @@ def load(cls, source: str | Path | Mapping[str, Any]) -> SymbolTable:
def checked_against(self, schema: Buildable) -> SymbolTable:
"""Reject entries naming nothing in *schema*, with the near miss."""
dims = set(schema.dimensions)
everything = dims | set(schema.parameters) | set(schema.variables)
everything = dims | set(schema.parameters) | set(schema.variables) | printed_expressions(schema)
errors = [
*(_unknown_entry(d, 'dimensions', dims) for d in {*self.indices, *self.sets} - dims),
*(_unknown_entry(n, 'names', everything - dims) for n in set(self.names) - everything),
Expand Down
46 changes: 42 additions & 4 deletions src/math_spec/typeset/walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fm
self.policies: set[str] = set()
self.positions: set[str] = set()
self.numeric_coordinates: set[str] = set()
#: Cased expressions met while rendering, in first-use order. Each one
#: prints once, as a definition of its own; see :meth:`definitions`.
self.defined: dict[str, CasesNode] = {}

def op(self, name: str) -> str:
return self.format.operators[name]
Expand Down Expand Up @@ -290,10 +293,11 @@ def _arithmetic(self, node: ArithmeticNode, ctx: _Context) -> tuple[str, int]:
return self._call(node, ctx)

if isinstance(node, CasesNode):
# An atom: a cases block is self-delimiting, so it never needs a
# bracket around it however it sits in the surrounding arithmetic.
arms = [(self.arithmetic(arm.value, ctx), self.where(arm.when, ctx, need=1)) for arm in node.arms]
return self.format.cases(arms), _ATOM
# The symbol, not the cases: the block prints once as a definition
# of its own, and a use of it reads like any other quantity. See
# :meth:`definitions` for why.
self.defined.setdefault(node.name, node)
return ctx.indexed(self.symbols.name[node.name], list(node.foreach)), _ATOM

if isinstance(node, (NameNode, NameListNode, KeywordNode, DimensionNode, LookupNode, EdgeNode)):
msg = f'{type(node).__name__} reached the typesetter; resolve the expression first.'
Expand Down Expand Up @@ -605,6 +609,40 @@ def constraints(self) -> list[Line]:
)
return lines

def definitions(self) -> list[Line]:
"""One line per cased expression the equations used, defining it.

Inlining a cases block where its name stood is what the AST does, and
it is the wrong thing to print: a three-arm block is three rows tall,
so whatever follows it in the equation sits beside its middle arm and
reads as part of that arm's condition. Worse, a quantity written once
in the file would be written once per use on the page — the opposite of
what naming it was for.

So a use prints the symbol and the block prints here, which is how a
paper states a quantity defined by region. Run this **after** the
sections that use it: what lands here is what they reached, and an arm
may itself name another cased expression, so the loop runs to a
fixpoint rather than over one pass.
"""
lines: list[Line] = []
done: set[str] = set()
while pending := [name for name in self.defined if name not in done]:
for name in pending:
done.add(name)
node = self.defined[name]
ctx = self.context(ceiling=2)
arms = [(self.arithmetic(arm.value, ctx), self.where(arm.when, ctx, need=1)) for arm in node.arms]
lines.append(
Line(
label=name,
left=ctx.indexed(self.symbols.name[name], list(node.foreach)),
right=f'{self.op("equal")} {self.format.cases(arms)}',
condition=self.quantifier(list(node.foreach), ''),
)
)
return lines

def variables(self) -> list[Line]:
"""One line per variable, and one more for a set the variable carries.

Expand Down
7 changes: 6 additions & 1 deletion tests/typeset/golden/latex.out
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,16 @@
\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{started} && \mathit{slack}_{t} & \ge \mathit{on}_{t,g} \cdot \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\
\text{started} && \mathit{slack}_{t} & \ge \mathit{on}_{t,g} \cdot \mathit{startup\_cost}_{t,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\
\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
\end{align}

\paragraph{Definitions}
\begin{align}
\text{startup\_cost} && \mathit{startup\_cost}_{t,g} & = \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G}
\end{align}

\paragraph{Variable domains}
\begin{align}
\text{p} && p^{\mathrm{min}}_{g} \le p_{t,g} & \le p^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \left( p^{\mathrm{max}}_{g} > 0 \wedge \neg \mathit{is\_flexible}_{g} \vee p^{\mathrm{min}}_{g} > 0 \right) \\
Expand Down
8 changes: 7 additions & 1 deletion tests/typeset/golden/markdown.out
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ $$\mathit{slack}_{t} \le \mathit{load}_{t,b} \qquad \forall\thinspace t \in \mat

**`started`**

$$\mathit{slack}_{t} \ge \mathit{on}_{t,g} \cdot \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \cr \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$
$$\mathit{slack}_{t} \ge \mathit{on}_{t,g} \cdot \mathit{startup\_cost}_{t,g} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

**`always`**

Expand All @@ -161,6 +161,12 @@ $$\mathit{spill}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace

$$\mathit{slack}_{t} \ge 0 \qquad \forall\thinspace t \in \mathcal{T} \thinspace:\thinspace \bot$$

#### Definitions

**`startup_cost`**

$$\mathit{startup\_cost}_{t,g} = \begin{cases} \mathit{cost}_{g} \cdot p^{\mathrm{max}}_{g} & \text{if } \mathrm{pos}(t) = 0 \cr \mathit{cost}_{g} & \text{if } \mathrm{pos}(t) \neq 0 \end{cases} \qquad \forall\thinspace t \in \mathcal{T},\enspace g \in \mathcal{G}$$

#### Variable domains

**`p`**
Expand Down
6 changes: 5 additions & 1 deletion tests/typeset/golden/typst.out
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ $ upright("balance") & sum_(g in cal(G) colon upright("gen_bus")(g) = b) p_(t,g)
upright("first") & italic("on")_(t,g) & = 1 & forall t in cal(T), g in cal(G) colon (upright("pos")(t) = 0 or upright("pos")_(upright("season_of")(t))(t) = 0) \
upright("last") & italic("on")_(t,g) & = 0 & forall t in cal(T), g in cal(G) colon (upright("pos")(t) = abs(cal(T)) - 1 or upright("pos")_(upright("season_of")(t))(t) = abs(cal(T)_(upright("season_of")(t))) - 1) \
upright("northern") & italic("slack")_(t) & <= italic("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("zone_of")(b) = upright("north") and upright("zone_of")(b) != upright("area_of")(b) and upright("zone_of")(b) upright(" is defined") \
upright("started") & italic("slack")_(t) & >= italic("on")_(t,g) dot cases(italic("cost")_(g) dot p^(upright("max"))_(g) & upright("if ") upright("pos")(t) = 0, italic("cost")_(g) & upright("if ") upright("pos")(t) != 0) & forall t in cal(T), g in cal(G) \
upright("started") & italic("slack")_(t) & >= italic("on")_(t,g) dot italic("startup_cost")_(t,g) & forall t in cal(T), g in cal(G) \
upright("always") & italic("spill")_(t) & >= 0 & forall t in cal(T) colon top \
upright("never") & italic("slack")_(t) & >= 0 & forall t in cal(T) colon bot $

== Definitions
#set math.equation(numbering: "(1)")
$ upright("startup_cost") & italic("startup_cost")_(t,g) & = cases(italic("cost")_(g) dot p^(upright("max"))_(g) & upright("if ") upright("pos")(t) = 0, italic("cost")_(g) & upright("if ") upright("pos")(t) != 0) & forall t in cal(T), g in cal(G) $

== Variable domains
#set math.equation(numbering: "(1)")
$ upright("p") & p^(upright("min"))_(g) <= p_(t,g) & <= p^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon (p^(upright("max"))_(g) > 0 and not italic("is_flexible")_(g) or p^(upright("min"))_(g) > 0) \
Expand Down
Loading
Loading