diff --git a/docs/examples/commitment.md b/docs/examples/commitment.md index f7b81791..901182f9 100644 --- a/docs/examples/commitment.md +++ b/docs/examples/commitment.md @@ -123,7 +123,7 @@ Upright is what the model is given — a parameter such as $`\mathrm{committable $`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}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, 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. #### Objective diff --git a/docs/examples/operators.md b/docs/examples/operators.md index eefbf74d..1679d0fa 100644 --- a/docs/examples/operators.md +++ b/docs/examples/operators.md @@ -76,7 +76,7 @@ $`\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\, t \in ```yaml description: >- The membership reduction — `sum(array, by=relation, over=a, into=b)` lands the result on the - column the relation is walked to, which is what makes topology data rather than + column the relation is read to, which is what makes topology data rather than structure. dimensions: @@ -111,7 +111,7 @@ $`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{li ```yaml description: >- - A walk that names its ends — `sum(array, by=relation, over=a, into=b)` + A call that names its ends — `sum(array, by=relation, over=a, into=b)` consumes column `a` and lands on column `b`, and the other key column is joined on, so each zone's total is taken per period. @@ -147,7 +147,7 @@ $`\sum_{g \in \mathcal{G} \,:\, \mathrm{zone\_of}(g,\ e) = z} p_{g,e} \ge \mathr ```yaml description: >- - A walk with several columns at each end — `sum(array, by=relation, over=[a, …], into=[b, …])` + A call with several columns at each end — `sum(array, by=relation, over=[a, …], into=[b, …])` consumes both key columns at once and lands on the product of both value columns in one join. diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 60700420..921b046f 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1385,7 +1385,7 @@ $`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size $`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}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, 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. ### Objective diff --git a/docs/examples/pypsa_linearized_uc.md b/docs/examples/pypsa_linearized_uc.md index 7b410b6e..6ef90674 100644 --- a/docs/examples/pypsa_linearized_uc.md +++ b/docs/examples/pypsa_linearized_uc.md @@ -150,7 +150,7 @@ The relaxed class of a plain `n.optimize()`: `linearized_unit_commitment`, state | $`\overleftarrow{u}`$ | `Generator_previous_status` over $`\mathcal{T} \times \mathcal{G}`$ — the commitment state a generator carries into a snapshot — the state it brought into the horizon at the first, the previous snapshot's after that | | $`\overleftarrow{p}`$ | `Generator_previous_p` over $`\mathcal{T} \times \mathcal{G}`$ — the output a generator carries into a snapshot — nothing at the start of the horizon, which is why a unit that came in running carries no ramp row there | -$`\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}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, 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. ### Objective diff --git a/docs/howto/declare-a-column.md b/docs/howto/declare-a-column.md index 9ae0ea7e..12c8c173 100644 --- a/docs/howto/declare-a-column.md +++ b/docs/howto/declare-a-column.md @@ -14,8 +14,8 @@ what the math does with the column. | The column… | is declared as | because | | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | is an axis: something is indexed by it, or an aggregation lands terms on it | a `dimension` | its members are the coordinate set every table over it is reindexed onto | -| has one value per member of a dimension, or per tuple of several — a generator's bus, a line's two ends, a generator's zone by period | a `relation` with that `key` | it is a map every operator walks, and its values are checked against the dimensions they name | -| relates members of two dimensions many-to-many, with nothing to weigh — which buses a generator may connect to | a bare `relation`, with no `values:` | `sum` walks it with both ends named, and a bare `where` tests it. Nothing reads it, because there is no one value to read | +| has one value per member of a dimension, or per tuple of several — a generator's bus, a line's two ends, a generator's zone by period | a `relation` with that `key` | it is a map every operator reads, and its values are checked against the dimensions they name | +| relates members of two dimensions many-to-many, with nothing to weigh — which buses a generator may connect to | a bare `relation`, with no `values:` | `sum` reads it with both ends named, and a bare `where` tests it. Nothing reads it, because there is no one value to read | | relates members of two dimensions many-to-many, with a weight per pair — a link's efficiency to each bus, a cycle's lines | a `parameter` over both | the weight is the data, its row set is the relation, and the aggregation is `sum(w * x, over=a)` | | is a label set the model only selects on or counts within — a period, a season, a zone | a `dimension`, and a `relation` onto it | its labels are checked, at the cost of one line and one table | | scales terms — a coefficient, a bound, an offset | a `parameter` (`float` or `int`) | arithmetic is over numbers ([dtype](../reference/language/declarations.md#parameters)) | diff --git a/docs/reference/language/expressions.md b/docs/reference/language/expressions.md index efe3ba55..15bf21e9 100644 --- a/docs/reference/language/expressions.md +++ b/docs/reference/language/expressions.md @@ -82,18 +82,18 @@ after a variable. The objective has no name at all. The dimension set of every expression is known before any data binds: -| Node | Dim set | Error | -| -------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------- | -| number | `{}` | | -| parameter / variable | its `dims` | | -| `-x`, `+x` | `dims(x)` | | -| `a + b`, `a * b`, `a / b` | `dims(a) ∪ dims(b)` | | -| `sum(x)` | `{}` | error if `dims(x)` is already empty | -| `sum(x, over=d)` | `dims(x) − {d}` | error if `d ∉ dims(x)` | -| `sum(x, by=l, over=a, into=b)` | `(dims(x) − consumed) ∪ produced` | the walk's refusals, under [how a relation is used](relations.md#how-a-relation-is-used) | -| `at(x, by=l, over=a, into=b)` | `(dims(x) − consumed) ∪ produced` | the same | -| `shift(x, along=d, offset=n)` | `dims(x)` | error if `d ∉ dims(x)` | -| `sum_back(x, along=d, window=n)` | `dims(x)` | error if `d ∉ dims(x)` | +| Node | Dim set | Error | +| -------------------------------- | --------------------------------- | -------------------------------------------------------------------------------- | +| number | `{}` | | +| parameter / variable | its `dims` | | +| `-x`, `+x` | `dims(x)` | | +| `a + b`, `a * b`, `a / b` | `dims(a) ∪ dims(b)` | | +| `sum(x)` | `{}` | error if `dims(x)` is already empty | +| `sum(x, over=d)` | `dims(x) − {d}` | error if `d ∉ dims(x)` | +| `sum(x, by=l, over=a, into=b)` | `(dims(x) − consumed) ∪ produced` | the refusals under [how a relation is used](relations.md#how-a-relation-is-used) | +| `at(x, by=l, over=a, into=b)` | `(dims(x) − consumed) ∪ produced` | the same | +| `shift(x, along=d, offset=n)` | `dims(x)` | error if `d ∉ dims(x)` | +| `sum_back(x, along=d, window=n)` | `dims(x)` | error if `d ∉ dims(x)` | A binary operator takes the **union** of the two dimension sets, so an outer product is allowed. The declaration's own dimensions are its **frame**, and a @@ -167,7 +167,7 @@ a boolean parameter instead. ### `position()` `position(dim)` is where the row sits along the dimension's own order, which is -the order `shift` walks. A boundary written with it survives a relabelling of +the order `shift` steps along. A boundary written with it survives a relabelling of the index: ```yaml diff --git a/docs/reference/language/operators.md b/docs/reference/language/operators.md index 867a8b2d..eeec1f3d 100644 --- a/docs/reference/language/operators.md +++ b/docs/reference/language/operators.md @@ -21,7 +21,7 @@ in a reported expression, are all of them. A composition of them goes in | `shift(array, along=dim, offset=n, edge='wrap')` | The value `n` positions earlier, counted cyclically, so nothing is vacated | | `shift(array, along=dim, offset=n, edge=v)` | The value `n` positions earlier, with the number `v` standing where the edge was vacated | | `shift(array, along=dim, offset=p, edge=…)` | `p` is an integer parameter, so each entity is reached by its own offset | -| `shift(array, along=dim, offset=n, by=relation, within=c)` | The translation walks inside each group that the relation's column `c` makes. Neighbours, edges and a wrap all belong to that group | +| `shift(array, along=dim, offset=n, by=relation, within=c)` | The translation steps inside each group that the relation's column `c` makes. Neighbours, edges and a wrap all belong to that group | | `sum_back(array, along=dim, window=n)` | The sum of the last `n` positions along `dim`, ending at the position being written | | `sum_back(array, along=dim, window=p)` | `p` is an integer parameter, so each entity gets its own window length | | `sum_back(array, along=dim, window=p, edge='wrap')` | The window reaches around the axis, instead of stopping short at its start | @@ -78,7 +78,7 @@ is null belongs to no group. ## `at` -`at(x, by=l, over=a, into=b)` walks the relation the other way. It consumes a +`at(x, by=l, over=a, into=b)` reads the relation the other way. It consumes a value column and produces the key, so it reads one coarse value once for each fine label that points at it ([reads](relations.md#aggregates-and-reads)). @@ -172,7 +172,7 @@ previous snapshot's duration. ### Translation within groups -`by=` partitions the axis the operator walks, so the neighbour of a coordinate +`by=` partitions the axis the operator steps along, so the neighbour of a coordinate is the coordinate before it in its own group. A group can be a season, an investment period or a representative day: @@ -197,8 +197,8 @@ Every `edge=` setting then applies one group at a time. Bare, the first coordinate of each group is vacated and its row drops. `edge='wrap'` closes each group onto its own last coordinate. `edge=v` puts `v` at the edge of each group. -`by=` takes a relation with a key column over the dimension being walked, and -`within=` names the value columns the group is made of +`by=` takes a relation with a key column over the dimension being stepped +along, and `within=` names the value columns the group is made of ([partitions](relations.md#partitions)). A coordinate the relation sends nowhere is in no group, so its row drops under every `edge=`. diff --git a/docs/reference/notation.md b/docs/reference/notation.md index 3dffe77b..9ccdb9ff 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -51,10 +51,10 @@ relations: zone_of: { key: bus, values: zone } area_of: { key: bus, values: zone } # a second map into the same set, to compare against season_of: { key: snapshot, values: season } - gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call walks one and joins on the other + gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call consumes one and joins on the other rep_of: { key: snapshot, values: { rep: snapshot } } # a map into its own dimension: the representative snapshot - connection: { key: [generator, bus] } # a bare relation, with no value columns: many-to-many, walked only by sum with both ends named - gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, walked to both at once + connection: { key: [generator, bus] } # a bare relation, with no value columns: many-to-many, read only by sum with both ends named + gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once parameters: p_max: { dims: [generator] } @@ -131,7 +131,7 @@ $`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k` $`t \ominus^{\mathrm{relation}(t)} k`$ denotes a translation counted inside the group a relation puts $`t`$ in (`shift(by=relation)`), 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{relation}(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}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, 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{relation}(t)}(t)`$ counts within the group a relation puts $`t`$ in: the subscript names the map, $`\mathcal{T}_{\mathrm{relation}(t)}`$ is the group it lands in, and that group has a first position of its own. @@ -382,7 +382,7 @@ pullback: #### `grouped_once` -one table walked to two value columns: the domain carries a condition per column +one table read to two value columns: the domain carries a condition per column ```yaml grouped_once: @@ -454,7 +454,7 @@ p_{t,g} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ g \in \math #### `representative` -a map into its own dimension, walked both ways: the frame is unchanged and the index is primed +a map into its own dimension, read both ways: the frame is unchanged and the index is primed ```yaml representative: @@ -468,7 +468,7 @@ representative: #### `zonal` -a grouping through a two-key map, walked along one key: the condition reads the other, and the row keeps it +a grouping through a two-key map, consuming one key: the condition reads the other, and the row keeps it ```yaml zonal: @@ -482,7 +482,7 @@ zonal: #### `zonal_history` -the same table walked along its other key +the same table consuming its other key ```yaml zonal_history: @@ -496,7 +496,7 @@ zonal_history: #### `zonal_membership` -the same table walked between its two key columns: no value column is read, so the domain asks only that the row is there +the same table read between its two key columns: no value column is read, so the domain asks only that the row is there ```yaml zonal_membership: diff --git a/examples/operators/sum_by.yaml b/examples/operators/sum_by.yaml index 258e1466..e02cf26c 100644 --- a/examples/operators/sum_by.yaml +++ b/examples/operators/sum_by.yaml @@ -4,7 +4,7 @@ description: >- The membership reduction — `sum(array, by=relation, over=a, into=b)` lands the result on the - column the relation is walked to, which is what makes topology data rather than + column the relation is read to, which is what makes topology data rather than structure. dimensions: diff --git a/examples/operators/sum_by_column_lists.yaml b/examples/operators/sum_by_column_lists.yaml index bb17afac..98f70b7f 100644 --- a/examples/operators/sum_by_column_lists.yaml +++ b/examples/operators/sum_by_column_lists.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT description: >- - A walk with several columns at each end — `sum(array, by=relation, over=[a, …], into=[b, …])` + A call with several columns at each end — `sum(array, by=relation, over=[a, …], into=[b, …])` consumes both key columns at once and lands on the product of both value columns in one join. diff --git a/examples/operators/sum_by_columns.yaml b/examples/operators/sum_by_columns.yaml index 8ed05f0d..683fcd3c 100644 --- a/examples/operators/sum_by_columns.yaml +++ b/examples/operators/sum_by_columns.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT description: >- - A walk that names its ends — `sum(array, by=relation, over=a, into=b)` + A call that names its ends — `sum(array, by=relation, over=a, into=b)` consumes column `a` and lands on column `b`, and the other key column is joined on, so each zone's total is taken per period. diff --git a/schema/math-spec.schema.json b/schema/math-spec.schema.json index 2cdc05fa..450866fc 100644 --- a/schema/math-spec.schema.json +++ b/schema/math-spec.schema.json @@ -450,7 +450,7 @@ }, "RelationBlock": { "additionalProperties": false, - "description": "A named relation between dimensions: the columns a row is keyed by, and the columns that key determines.\n\nEach side is a dimension, a list of them, or a mapping of column name to\ndimension where two columns share one. ``key:`` is the claim the language\nchecks at bind: one row per key tuple, so every ``values:`` column is a\nfunction of it. A relation with no ``values:`` is **bare** \u2014 every column is\nin its key, a row is its own identity, and nothing reads it::\n\n relations:\n gen_bus: {key: generator, values: bus}\n gen_bt: {key: [generator], values: [bus, technology]}\n zone_of: {key: [generator, period], values: zone}\n ends: {key: line, values: {bus0: bus, bus1: bus}}\n connection: {key: [generator, bus]}\n\nAn operator walks the table in the direction the call names\n(``over=``, ``into=``), joining on the other key columns; the\ndeclaration fixes no direction. The map itself is data, and arrives at bind\ntime under the relation's name, one column per role.", + "description": "A named relation between dimensions: the columns a row is keyed by, and the columns that key determines.\n\nEach side is a dimension, a list of them, or a mapping of column name to\ndimension where two columns share one. ``key:`` is the claim the language\nchecks at bind: one row per key tuple, so every ``values:`` column is a\nfunction of it. A relation with no ``values:`` is **bare** \u2014 every column is\nin its key, a row is its own identity, and nothing reads it::\n\n relations:\n gen_bus: {key: generator, values: bus}\n gen_bt: {key: [generator], values: [bus, technology]}\n zone_of: {key: [generator, period], values: zone}\n ends: {key: line, values: {bus0: bus, bus1: bus}}\n connection: {key: [generator, bus]}\n\nAn operator reads the table in the direction the call names\n(``over=``, ``into=``), joining on the other key columns; the\ndeclaration fixes no direction. The map itself is data, and arrives at bind\ntime under the relation's name, one column per role.", "properties": { "description": { "anyOf": [ diff --git a/src/math_spec/_expression_parser.py b/src/math_spec/_expression_parser.py index ed078ee6..2a462b65 100644 --- a/src/math_spec/_expression_parser.py +++ b/src/math_spec/_expression_parser.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator, Mapping - from math_spec.program import Walk, WhereNode + from math_spec.program import Direction, WhereNode #: The relation a comparison may carry — the three an expression may be #: written with, which is what a constraint's sense is read off. @@ -133,7 +133,7 @@ def __str__(self) -> str: @dataclass(frozen=True) class RelationNode: - """A resolved ``by=`` — the relation, and the walk the call takes through it. + """A resolved ``by=`` — the relation, and the direction the call reads it in. ``dimensions`` is the fine side — what ``sum`` consumes and ``at`` produces — and ``into`` the coarse dims, which ``sum`` produces and ``at`` @@ -144,7 +144,7 @@ class RelationNode: name: str dimensions: tuple[str, ...] into: tuple[str, ...] - walk: Walk + direction: Direction def __str__(self) -> str: return self.name diff --git a/src/math_spec/dimensions.py b/src/math_spec/dimensions.py index 82e5baf2..abb6aa6b 100644 --- a/src/math_spec/dimensions.py +++ b/src/math_spec/dimensions.py @@ -120,7 +120,7 @@ def _cases_dims(node: CasesNode, schema: Spec) -> frozenset[str]: def _not_carried(context: str, call: str, inner: frozenset[str], rewrite: str) -> str: - """The refusal for an operator walking a dim its operand does not carry; *rewrite* is the operator's own.""" + """The refusal for an operator reaching a dim its operand does not carry; *rewrite* is the operator's own.""" return ( f'{context}: {call} but the expression has dims {sorted(inner)}. An operator over a dim the ' f'operand does not carry is a no-op that builds and solves wrong — {rewrite}.' @@ -134,7 +134,7 @@ def _dims_call(node: FunctionCallNode, schema: Spec, context: str) -> frozenset[ def _sum_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spec, context: str) -> frozenset[str]: - """``sum`` reduces a dim away, or walks relations: the consumed dim goes, the produced dims arrive, the joined stay.""" + """``sum`` reduces a dim away, or reads a relation: the consumed dim goes, the produced dims arrive, the joined stay.""" by = node.kwargs.get('by') if by is None and 'over' not in node.kwargs: if not inner: @@ -158,7 +158,7 @@ def _sum_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spec, conte raise DimensionError( _not_carried( context, - f'sum(by={by}) consumes {missing}, the dims it walks from,', + f'sum(by={by}) consumes {missing}, the dims it reads from,', inner, 'drop the sum, or fix the dim', ) @@ -169,7 +169,7 @@ def _sum_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spec, conte def _at_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spec, context: str) -> frozenset[str]: - """``at`` is the adjoint of ``sum(by=)``: it consumes the dims the walks produce and produces the one they consume.""" + """``at`` is the adjoint of ``sum(by=)``: it consumes the dims a sum produces and produces the ones it consumes.""" by = node.kwargs['by'] assert isinstance(by, RelationNode) absent = sorted(set(by.into) - inner) @@ -195,7 +195,7 @@ def _translation_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spe context, f'{node.name}(along={over.name})', inner, - f'walk a dim the operand carries, or drop the {node.name}', + f'name a dim the operand carries, or drop the {node.name}', ) ) _check_named_amount(node, over.name, inner, schema, context) @@ -209,38 +209,38 @@ def _translation_dims(node: FunctionCallNode, inner: frozenset[str], schema: Spe def _check_lands_clear(call: str, produced: set[str], consumed: set[str], inner: frozenset[str], context: str) -> None: - """The dims a walk lands on are its own to bring, so the operand does not already carry one. + """The dims a call lands on are its own to bring, so the operand does not already carry one. - Where it does, the walk would tie the operand's axis to the one it - produces rather than adding it, and the call reads the same either way. + Where it does, the call would tie the operand's axis to the one it + produces rather than adding it, and it reads the same either way. A relation into its own dimension is not that case: there the dim landed on is the dim just consumed, so every factor is read at the coordinate - the walk sums over, and nothing is tied. + the sum runs over, and nothing is tied. """ if clash := sorted((produced & inner) - consumed): raise DimensionError( f'{context}: {call} lands on {clash}, which the expression already carries.\n' - f'A walk brings the dims it lands on, so that reading the call tells you what it ' - f'adds. Move the factor carrying {clash} outside the operator, or walk to a column ' + f'A call brings the dims it lands on, so that reading it tells you what it ' + f'adds. Move the factor carrying {clash} outside the operator, or read to a column ' f'over another dimension.' ) def _check_joined(call: str, by: RelationNode, inner: frozenset[str], context: str) -> None: - """The columns a walk joins on are read at their dimensions, so the operand carries every one, each once.""" - walk = by.walk - dims = walk.joined_dims + """The columns a call joins on are read at their dimensions, so the operand carries every one, each once.""" + direction = by.direction + dims = direction.joined_dims if missing := sorted(set(dims) - inner): raise DimensionError( - f'{context}: {call} joins on {missing} (columns {[r for r in walk.joined if walk.dim(r) in missing]} ' - f"of '{walk.name}'), which the expression does not carry (dims {sorted(inner)}). A relation is " - f'walked between two of its columns and read at the others — index the operand by them, or ' - f'walk between different columns.' + f'{context}: {call} joins on {missing} (columns {[r for r in direction.joined if direction.dim(r) in missing]} ' + f"of '{direction.name}'), which the expression does not carry (dims {sorted(inner)}). A relation is " + f'read between two of its columns and joined at the others — index the operand by them, or ' + f'read it between different columns.' ) if twice := sorted({d for d in dims if dims.count(d) > 1 or d in by.dimensions}): raise DimensionError( - f"{context}: {call} joins '{walk.name}' on {twice} through more than one column, and the operand " - f'carries each dimension once. Give the walk different columns, or a relation whose joined ' + f"{context}: {call} joins '{direction.name}' on {twice} through more than one column, and the operand " + f'carries each dimension once. Read between different columns, or use a relation whose joined ' f'columns are over distinct dimensions.' ) @@ -255,13 +255,13 @@ def _check_joined(call: str, by: RelationNode, inner: frozenset[str], context: s class _Amount(NamedTuple): - """What an axis-walking operator's errors say about the amount it takes.""" + """What the errors of an operator that steps along an axis say about the amount it takes.""" #: The word for the amount. noun: str #: Why negating a named one at the call site is not what the caller means. negated: str - #: What a named one that varies along the axis it walks becomes. + #: What a named one that varies over the axis it steps along becomes. varies: str #: The least whole number a literal may be. minimum: float @@ -292,7 +292,7 @@ class _Amount(NamedTuple): def _amount_of(node: FunctionCallNode) -> tuple[str, ArithmeticNode]: - """The kwarg an axis-walking operator takes its amount through, and the value written there.""" + """The kwarg an operator that steps along an axis takes its amount through, and the value written there.""" (kwarg,) = BUILTINS[node.name].required_value_kwargs return kwarg, node.kwargs[kwarg] @@ -415,21 +415,21 @@ def _check_named_amount(node: FunctionCallNode, over: str, inner: frozenset[str] ) if over in declared.dims: raise DimensionError( - f'{context}: {node.name}({kwarg}={amount.name}) walks ' + f'{context}: {node.name}({kwarg}={amount.name}) steps along ' f"'{over}', but '{amount.name}' is declared over {sorted(declared.dims)}, which " - f'carries it. A named {words.noun} that varies along the axis it walks is {words.varies} ' + f'carries it. A named {words.noun} that varies over the axis it steps along is {words.varies} ' f"— declare '{amount.name}' over dims '{over}' is not one of." ) partition = node.kwargs.get('by') groups = ( - frozenset(partition.walk.dim(v) for v in partition.walk.produced) + frozenset(partition.direction.dim(v) for v in partition.direction.produced) if isinstance(partition, RelationNode) else frozenset() ) if stray := sorted(frozenset(declared.dims) - inner - groups): raise DimensionError( f'{context}: {node.name}({kwarg}={amount.name}) reads its {words.noun} at the coordinate it ' - f"walks, but '{amount.name}' varies over {stray}, which that coordinate does not carry " + f"steps from, but '{amount.name}' varies over {stray}, which that coordinate does not carry " f'(dims {sorted(inner)}). A dim the coordinate does not have is no coordinate at all — ' f"declare '{amount.name}' over dims the expression carries, or group by a relation into " f'one of {stray}, so that each group is reached by its own {words.noun}.' diff --git a/src/math_spec/lowering.py b/src/math_spec/lowering.py index 41e11080..9638d091 100644 --- a/src/math_spec/lowering.py +++ b/src/math_spec/lowering.py @@ -273,13 +273,13 @@ def sum(self, node: FunctionCallNode) -> program.ExpressionNode: assert isinstance(consumed, DimensionNode), 'resolution refuses a over= that is not a dimension' return program.Sum(operand, (consumed.name,)) assert isinstance(by_node, RelationNode), 'resolution refuses a by= that is not a relation' - return program.GroupSum(operand, walk=by_node.walk) + return program.GroupSum(operand, direction=by_node.direction) def at(self, node: FunctionCallNode) -> program.ExpressionNode: """``at(x, by=relation)`` — the adjoint of :meth:`sum`'s ``by=`` form.""" by_node = node.kwargs['by'] assert isinstance(by_node, RelationNode), 'resolution refuses a by= that is not a relation' - return program.At(self.expr(node.args[0]), walk=by_node.walk) + return program.At(self.expr(node.args[0]), direction=by_node.direction) def sum_back(self, node: FunctionCallNode) -> program.ExpressionNode: """``sum_back(x, along=d, window=w)`` — a trailing window along one dimension. @@ -290,7 +290,7 @@ def sum_back(self, node: FunctionCallNode) -> program.ExpressionNode: ``by=`` names the relation the window stops at the edges of, and rides on the node the way it rides on a translation — the dim rules have already - held it to one relation over the walked dimension. + held it to one relation over the dimension stepped along. """ over_node = node.kwargs['along'] assert isinstance(over_node, DimensionNode), 'resolution refuses an along= that is not a dimension' @@ -341,10 +341,10 @@ def shift(self, node: FunctionCallNode) -> program.ExpressionNode: } -def _partition_of(node: FunctionCallNode) -> program.Walk | None: - """The walk a translation partitions by, if the call names a relation. +def _partition_of(node: FunctionCallNode) -> program.Direction | None: + """The direction a translation partitions by, if the call names a relation. - That it is a *single* relation, walked *along the translated dimension*, is + That it is a *single* relation, stepped *along the translated dimension*, is checked with the other dim rules (``math_spec.dimensions``), where a model is refused before any data is read. """ @@ -352,7 +352,7 @@ def _partition_of(node: FunctionCallNode) -> program.Walk | None: if by_node is None: return None assert isinstance(by_node, RelationNode) - return by_node.walk + return by_node.direction def _bound_expression(value: float | str) -> program.ExpressionNode: diff --git a/src/math_spec/model.py b/src/math_spec/model.py index fbc75667..91b9a91c 100644 --- a/src/math_spec/model.py +++ b/src/math_spec/model.py @@ -182,7 +182,7 @@ class RelationBlock(_StrictBlock): ends: {key: line, values: {bus0: bus, bus1: bus}} connection: {key: [generator, bus]} - An operator walks the table in the direction the call names + An operator reads the table in the direction the call names (``over=``, ``into=``), joining on the other key columns; the declaration fixes no direction. The map itself is data, and arrives at bind time under the relation's name, one column per role. diff --git a/src/math_spec/operators.py b/src/math_spec/operators.py index af87aeb0..50dd48a7 100644 --- a/src/math_spec/operators.py +++ b/src/math_spec/operators.py @@ -39,11 +39,11 @@ class Builtin: dimension_kwargs: tuple[str, ...] = () relation_kwargs: tuple[str, ...] = () #: Kwargs naming a column of the relation ``by=`` names — ``over=`` and - #: ``into=`` — which resolution folds into the relation's walk. + #: ``into=`` — which resolution folds into the direction it is read in. role_kwargs: tuple[str, ...] = () #: Kwargs naming a dimension on their own and a column of the relation where #: ``by=`` names one. ``sum(x, over=generator)`` reduces the dimension - #: away; ``sum(x, by=l, over=c)`` names the column the walk consumes. + #: away; ``sum(x, by=l, over=c)`` names the column the call consumes. #: One meaning — what leaves the frame — read in the namespace ``by=`` #: decides. dimension_or_role_kwargs: tuple[str, ...] = () @@ -52,7 +52,7 @@ class Builtin: #: Kwargs the call may omit. Their *kind* still comes from the tuples #: above — this says only that the operator has an answer without them. optional_kwargs: tuple[str, ...] = () - #: Kwargs required exactly when the call addresses a relation. A walk + #: Kwargs required exactly when the call addresses a relation. A call #: names both of its ends and a partition names the columns it groups by, #: so that adding a value column to the relation cannot change what an #: existing call means. @@ -91,7 +91,7 @@ def kind_of( #: The closed operator set. ``by=`` is the one keyword that addresses a relation, #: and a relation carries its own dimensions, so no sibling kwarg restates them. -#: On ``shift`` and ``sum_back`` it partitions the axis the operator walks: it +#: On ``shift`` and ``sum_back`` it partitions the axis the operator steps along: it #: says which rows are neighbours, not which group a term lands in, and #: ``within=`` names the value columns the group is made of, on every call #: that names a ``by=``. @@ -155,10 +155,10 @@ def call_shape_error(name: str, positional: int, kwargs: Iterable[str]) -> str | builtin = BUILTINS[name] keys = set(kwargs) optional = {*builtin.edge_kwargs, *builtin.optional_kwargs} - walks = bool(keys & set(builtin.relation_kwargs)) - required = builtin.required | frozenset(builtin.with_relation) if walks else builtin.required - optional |= set() if walks else set(builtin.with_relation) - if walks and (unsaid := sorted(frozenset(builtin.with_relation) - keys)): + reads = bool(keys & set(builtin.relation_kwargs)) + required = builtin.required | frozenset(builtin.with_relation) if reads else builtin.required + optional |= set() if reads else set(builtin.with_relation) + if reads and (unsaid := sorted(frozenset(builtin.with_relation) - keys)): return unsaid_ends_error(name, unsaid) fits = positional == 1 and keys - optional == required return None if fits else f'{name}() expects {builtin.usage}' @@ -173,11 +173,11 @@ def call_shape_error(name: str, positional: int, kwargs: Iterable[str]) -> str | def unsaid_ends_error(name: str, unsaid: list[str]) -> str: - """Why a call through a relation has to write every column it reads: both ends of a walk, the group of a partition.""" + """Why a call through a relation has to write every column it reads: both ends of a read, the group of a partition.""" reason = ( PARTITION_NAMES_ITS_GROUP if 'within' in BUILTINS[name].with_relation - else 'A walk names both of its ends, so that a relation may gain a value column without changing what this call means.' + else 'A call names both of its ends, so that a relation may gain a value column without changing what this call means.' ) return ( f'{name}() through a relation leaves {", ".join(f"{k}=" for k in unsaid)} unsaid.\n' diff --git a/src/math_spec/program.py b/src/math_spec/program.py index 42b59f51..6d76a5c3 100644 --- a/src/math_spec/program.py +++ b/src/math_spec/program.py @@ -56,6 +56,7 @@ 'DimensionDeclaration', 'DimensionDtype', 'DimensionPositionNode', + 'Direction', 'Divide', 'Dual', 'Expression', @@ -101,7 +102,6 @@ 'VariableDeclaration', 'VariableDefinedNode', 'VariableDomain', - 'Walk', 'WhereNode', 'Window', 'carries_variable', @@ -267,8 +267,8 @@ class Sum(Expression): class GroupSum(Expression): """Sum ``operand`` through a relation, consuming the dims ``over`` and producing ``into``. - ``walk`` says which columns are consumed, which produced and which joined - on, and is the one fact the node holds: ``over`` is the dims the walk + ``direction`` says which columns are consumed, which produced and which + joined on, and is the one fact the node holds: ``over`` is the dims it consumes, ``into`` the dims it produces, and ``joined`` the dims it joins on. The result replaces every dim in ``over`` with every dim in ``into`` and keeps every dim in ``joined``. The join keys on the consumed columns @@ -276,58 +276,58 @@ class GroupSum(Expression): """ operand: ExpressionNode - walk: Walk + direction: Direction @property def relation(self) -> str: - return self.walk.name + return self.direction.name @property def over(self) -> tuple[str, ...]: - return self.walk.consumed_dims + return self.direction.consumed_dims @property def into(self) -> tuple[str, ...]: - return self.walk.produced_dims + return self.direction.produced_dims @property def joined(self) -> tuple[str, ...]: - """The dims the walk joins on — the key columns neither consumed nor produced, which the operand carries.""" - return self.walk.joined_dims + """The dims it joins on — the key columns neither consumed nor produced, which the operand carries.""" + return self.direction.joined_dims @dataclass(frozen=True) class At(Expression): """Read ``operand`` through a relation — the adjoint of :class:`GroupSum`. - The same table, walked the other way: this consumes the dims in ``into`` + The same table read the other way: this consumes the dims in ``into`` and produces the dims in ``over``, one value per coordinate because the - walk reads value columns at a key the operand fixes - (``Walk.is_function_read``). The join fans out, many ``over`` tuples + read takes value columns at a key the operand fixes + (``Direction.is_function_read``). The join fans out, many ``over`` tuples sharing one ``into`` tuple — at each coordinate of the joined columns, which the operand carries and the result keeps. As on - :class:`GroupSum`, ``walk`` is the fact and the rest are read off it. + :class:`GroupSum`, ``direction`` is the fact and the rest are read off it. """ operand: ExpressionNode - walk: Walk + direction: Direction @property def relation(self) -> str: - return self.walk.name + return self.direction.name @property def over(self) -> tuple[str, ...]: - return self.walk.produced_dims + return self.direction.produced_dims @property def into(self) -> tuple[str, ...]: - return self.walk.consumed_dims + return self.direction.consumed_dims @property def joined(self) -> tuple[str, ...]: - """The dims the walk joins on — the key columns neither consumed nor produced, which the operand carries.""" - return self.walk.joined_dims + """The dims it joins on — the key columns neither consumed nor produced, which the operand carries.""" + return self.direction.joined_dims @dataclass(frozen=True) @@ -342,7 +342,7 @@ class Translate(Expression): ``offset`` is an integer, or the name of an integer parameter that does not depend on ``dimension`` and carries its sign in the values. - ``partition`` is a relation walked along ``dimension`` — its consumed + ``partition`` is a relation stepped along ``dimension`` — its consumed column is a key over that dimension, its produced columns are the group — and the translation then happens inside each group: the neighbour is the one before in the same group, the edge is the group's, and a wrap closes @@ -355,7 +355,7 @@ class Translate(Expression): offset: int | str wrap: bool fill: float | None = None - partition: Walk | None = None + partition: Direction | None = None @dataclass(frozen=True) @@ -383,7 +383,7 @@ class Window(Expression): dimension: str width: int | str wrap: bool - partition: Walk | None = None + partition: Direction | None = None @dataclass(frozen=True) @@ -511,16 +511,17 @@ def dim(self, role: str) -> str: return dict(self.columns)[role] -class Walk(NamedTuple): - """One relation as an operator walks it — which columns are consumed, which produced, which joined on. +class Direction(NamedTuple): + """One relation as one call reads it — which columns are consumed, which produced, which joined on. - ``consumed``, ``produced`` and ``joined`` are *roles* — column names of - ``relation``, which binds every role to its dimension and names the key. - ``joined`` is the key roles not walked (every role, for a bare relation): - the join keys on them, and a value role not walked is not read. For a - partition (``shift``, ``sum_back``, ``position``) ``consumed`` is the key - role over the dimension walked and ``produced`` the value roles that make - the group, which are the ones ``within=`` named. + The declaration fixes no direction; the call does, and this is the one it + named. ``consumed``, ``produced`` and ``joined`` are *roles* — column names + of ``relation``, which binds every role to its dimension and names the key. + ``joined`` is the key roles the call did not name (every role, for a bare + relation): the join keys on them, and a value role left unnamed is not + read. For a partition (``shift``, ``sum_back``, ``position``) ``consumed`` + is the key role over the dimension stepped along and ``produced`` the value roles + that make the group, which are the ones ``within=`` named. """ relation: RelationDeclaration @@ -562,7 +563,7 @@ def joined_dims(self) -> tuple[str, ...]: @property def is_function_read(self) -> bool: - """Whether the walk reads one value per coordinate: the key lies inside what is fixed.""" + """Whether the read is one value per coordinate: the key lies inside what is fixed.""" return bool(self.key) and set(self.key) <= {*self.joined, *self.produced} @@ -1212,7 +1213,7 @@ class DimensionPositionNode: Both sides are integers, negative counting from the end. With a ``partition`` the position is counted within each group the relation makes, - walked as :class:`Translate` walks one: its consumed column is the key + read as :class:`Translate` reads one: its consumed column is the key column over ``name``, the group is its produced columns, and its joined columns are the other key columns, whose dimensions the frame carries. """ @@ -1220,7 +1221,7 @@ class DimensionPositionNode: name: str op: PredicateOperator position: int - partition: Walk | None = None + partition: Direction | None = None @dataclass(frozen=True) diff --git a/src/math_spec/resolution.py b/src/math_spec/resolution.py index e1430eb1..38cb4fb4 100644 --- a/src/math_spec/resolution.py +++ b/src/math_spec/resolution.py @@ -66,6 +66,7 @@ BooleanLiteralNode, DimensionComparisonNode, DimensionPositionNode, + Direction, Mask, NotNode, OrNode, @@ -78,7 +79,6 @@ RelationPairComparisonNode, TypedPredicateNode, VariableDefinedNode, - Walk, WhereNode, ) @@ -576,11 +576,11 @@ def _relation_ref( roles: Mapping[str, ArithmeticNode], over: ArithmeticNode | None, ) -> ArithmeticNode: - """An operator's ``by=``, with the ``over=`` and ``into=`` that say how the relation is walked. + """An operator's ``by=``, with the ``over=`` and ``into=`` that say which direction it is read in. A relation carries its own dimensions, so the call names columns rather than dims: ``over=`` the column consumed, ``into=`` the column - produced, every other key column joined on. A value column not walked + produced, every other key column joined on. A value column not named is not read, and a bare relation's columns are all key. One call addresses one table, so several columns of one table are a list and several tables are not. @@ -592,7 +592,7 @@ def _relation_ref( if len(names) > 1: self.errors.append( f'{self.context}: {operator}({key}={shown(names)}) names {len(names)} relations, and one call ' - f'walks one table. Declare one relation with the columns of all of them, or walk them in turn, ' + f'reads one table. Declare one relation with the columns of all of them, or read them in turn, ' f'one call each.' ) return value @@ -609,20 +609,20 @@ def _relation_ref( if 'within' not in named: return value # the call shape refused it already, with the wording that names the rewrite over_dim = over.name if isinstance(over, NameNode | DimensionNode) else None - walk = self._partition_walk(name, operator, over_dim, named['within']) + direction = self._partition_direction(name, operator, over_dim, named['within']) else: if not ({'over', 'into'} <= set(named)): return value # the call shape refused it already, with the wording that names the rewrite - walk = self._walk(name, operator, named['over'], named['into']) - if walk is None: + direction = self._direction(name, operator, named['over'], named['into']) + if direction is None: return value - fine = walk.produced_dims if operator == 'at' else walk.consumed_dims + fine = direction.produced_dims if operator == 'at' else direction.consumed_dims if operator in ('shift', 'sum_back'): coarse: tuple[str, ...] = () else: - coarse = walk.consumed_dims if operator == 'at' else walk.produced_dims - return RelationNode(name, dimensions=fine, into=coarse, walk=walk) + coarse = direction.consumed_dims if operator == 'at' else direction.produced_dims + return RelationNode(name, dimensions=fine, into=coarse, direction=direction) def _role_name(self, value: ArithmeticNode, operator: str, key: str) -> tuple[str, ...] | None: """``over=`` or ``into=`` as the column names it must be — one bare name, or a bracketed list of them.""" @@ -635,19 +635,19 @@ def _role_name(self, value: ArithmeticNode, operator: str, key: str) -> tuple[st ) return None - def _walk( + def _direction( self, name: str, operator: str, from_roles: tuple[str, ...], into_roles: tuple[str, ...], - ) -> Walk | None: - """How ``sum`` or ``at`` walks relation *name*, between the columns the call named. + ) -> Direction | None: + """Which direction ``sum`` or ``at`` reads relation *name* in, between the columns the call named. - Both ends arrive written: the call shape refuses a walk that leaves + Both ends arrive written: the call shape refuses a call that leaves one unsaid, so that a relation may gain a value column without - changing what this call means. ``at`` needs the walk single-valued - and ``sum`` needs it not: a sum that walks to the key has one term + changing what this call means. ``at`` needs the read single-valued + and ``sum`` needs it not: a sum that lands on the key has one term per coordinate and adds up nothing, which is a read, so it is refused toward ``at``. """ @@ -662,7 +662,7 @@ def _walk( forward = operator == 'sum' if both := sorted(set(from_roles) & set(into_roles)): self.errors.append( - f'{context}: {call}: over= and into= both name {both}, and a walk goes between two sets of columns.' + f'{context}: {call}: over= and into= both name {both}, and a call reads between two sets of columns.' ) return None for kwarg, roles in (('over', from_roles), ('into', into_roles)): @@ -670,28 +670,28 @@ def _walk( if shared := sorted({d for d in dims if dims.count(d) > 1}): self.errors.append( f'{context}: {call}: {kwarg}={list(roles)} names two columns over {shared}, and the operand ' - f'carries each dimension once, so nothing says which column its coordinate is read at. Walk ' + f'carries each dimension once, so nothing says which column its coordinate is read at. Read ' f'between columns over distinct dimensions.' ) return None joined = tuple(r for r in (shape.key or shape.roles) if r not in from_roles and r not in into_roles) - walk = Walk(shape, from_roles, into_roles, joined) - if not forward and not walk.is_function_read: + direction = Direction(shape, from_roles, into_roles, joined) + if not forward and not direction.is_function_read: self.errors.append( f"{context}: {call}: at reads one value per coordinate, and '{name}' is not single-valued in " f'{list(from_roles)} at the columns the operand fixes ({[*into_roles, *joined]}) — its key is ' f'{list(shape.key)}. Key the table by columns the read fixes, or read the other way.' ) return None - if forward and walk.is_function_read: + if forward and direction.is_function_read: self.errors.append( - f'{context}: {call}: this sum walks to the key {list(shape.key)}, so each coordinate has one ' + f'{context}: {call}: this sum lands on the key {list(shape.key)}, so each coordinate has one ' f"term and nothing is added up — that is a read, which is at()'s. Write " f'at(..., by={name}, over={list(from_roles)}, into={list(into_roles)}), or sum toward ' f'a value column.' ) return None - return walk + return direction def _known_roles(self, name: str, call: str, roles: tuple[str, ...] | None, kwarg: str) -> bool: """Whether every role *kwarg* names is a column of relation *name*, each once; the refusal otherwise.""" @@ -708,12 +708,12 @@ def _known_roles(self, name: str, call: str, roles: tuple[str, ...] | None, kwar return False return True - def _partition_walk( - self, name: str, operator: str, walked_dim: str | None, within_roles: tuple[str, ...] - ) -> Walk | None: - """How a partition (``shift``, ``sum_back``, ``position``) walks relation *name* along *walked_dim*. + def _partition_direction( + self, name: str, operator: str, along_dim: str | None, within_roles: tuple[str, ...] + ) -> Direction | None: + """Which direction a partition (``shift``, ``sum_back``, ``position``) reads relation *name* in along *along_dim*. - It walks the one key column over that dimension (a key has one column + It takes the one key column over that dimension (a key has one column per dimension), joins on the other key columns and groups by the value columns *within_roles* names. ``None`` where the dimension is not one (already refused), the relation has no key column over it, or @@ -722,20 +722,20 @@ def _partition_walk( context = self.context shape = self.ns.shape_of(name) call = f'{operator}(by={name})' - if walked_dim is None or not self._known_roles(name, call, within_roles, 'within'): + if along_dim is None or not self._known_roles(name, call, within_roles, 'within'): return None if not shape.values: self.errors.append( f"{context}: {call}: '{name}' is a bare relation — every column is in its key — so it makes no " f'groups and no coordinate is in exactly one. Move the columns the group is made of under ' - f'values:, leaving key: the column {operator} walks.' + f'values:, leaving key: the column {operator} steps along.' ) return None - over_keys = [r for r in shape.key if shape.dim(r) == walked_dim] + over_keys = [r for r in shape.key if shape.dim(r) == along_dim] if not over_keys: self.errors.append( - f"{context}: {call}: '{name}' has no key column over '{walked_dim}' — its key is " - f'{list(shape.key)} — and a partition walks a key column over the dimension it groups.' + f"{context}: {call}: '{name}' has no key column over '{along_dim}' — its key is " + f'{list(shape.key)} — and a partition steps along a key column over the dimension it groups.' ) return None if keyed := [r for r in within_roles if r in shape.key]: @@ -744,9 +744,9 @@ def _partition_walk( f'value columns — its value columns are {list(shape.values)}.' ) return None - (walked,) = over_keys - joined = tuple(r for r in shape.key if r != walked) - return Walk(shape, (walked,), within_roles, joined) + (along,) = over_keys + joined = tuple(r for r in shape.key if r != along) + return Direction(shape, (along,), within_roles, joined) def _not_a_relation(self, name: str, operator: str, key: str) -> str | None: """Why *name* is not a relation; ``None`` where it is one.""" @@ -860,10 +860,10 @@ def _position(self, node: UnresolvedPositionNode) -> DimensionPositionNode | Unr f'are {list(ns.shape_of(node.by).values)}.' ) return node - walk = self._partition_walk(node.by, 'position', node.dimension, node.into) - if walk is None: + direction = self._partition_direction(node.by, 'position', node.dimension, node.into) + if direction is None: return node - return DimensionPositionNode(node.dimension, node.op, node.position, walk) + return DimensionPositionNode(node.dimension, node.op, node.position, direction) def _comparison(self, node: UnresolvedComparisonNode) -> WhereNode | UnresolvedWhereNode: """``name literal``, or the one structural form ``relation relation``.""" diff --git a/src/math_spec/typesetting/walk.py b/src/math_spec/typesetting/walk.py index bedc65e1..b2e40177 100644 --- a/src/math_spec/typesetting/walk.py +++ b/src/math_spec/typesetting/walk.py @@ -58,7 +58,7 @@ from collections.abc import Iterable, Mapping from math_spec.model import RelationBlock, SosBlock, _ExpandedSpec - from math_spec.program import Walk as RelationWalk + from math_spec.program import Direction from math_spec.typesetting.format import Format from math_spec.typesetting.symbols import Symbols @@ -274,15 +274,15 @@ def _translation(self, step: _Step) -> str: self.noticed.grouped = True return self.format.superscript(operator, step.within) - def _relation_read(self, walk: RelationWalk, at: Mapping[str, str], read: str) -> str: + def _relation_read(self, direction: Direction, at: Mapping[str, str], read: str) -> str: """A relation's column *read* as a function at the columns *at* fixes: ``bus(g)``, ``zone_of(g, p)`` or ``ends.bus0(l)``. *at* maps each key role to the index it is read at. The function is named after the relation alone where the key determines one column, and after the column read otherwise. """ - name = walk.name if len(walk.values) == 1 else f'{walk.name}.{read}' - return self.format.apply(self.format.upright(name), self.format.joined([at[k] for k in walk.key], '')) + name = direction.name if len(direction.values) == 1 else f'{direction.name}.{read}' + return self.format.apply(self.format.upright(name), self.format.joined([at[k] for k in direction.key], '')) def _relation_row(self, name: str, key: list[str]) -> str: """That relation *name* has a row at *key*, its key columns' indices in declared order. @@ -307,10 +307,12 @@ def _value_read(self, name: str, column: str, ctx: _Context) -> str: def _position_group(self, node: DimensionPositionNode, ctx: _Context) -> str: """The group a grouped position counts within: the relation's group columns read at the row's key.""" assert node.partition is not None - walk = node.partition - keyed = self.format.joined([ctx.subscript(walk.dim(k)) for k in walk.key], '') - single = len(walk.values) == 1 - reads = [self.format.apply(self._column(walk.name, column, single), keyed) for column in walk.produced] + direction = node.partition + keyed = self.format.joined([ctx.subscript(direction.dim(k)) for k in direction.key], '') + single = len(direction.values) == 1 + reads = [ + self.format.apply(self._column(direction.name, column, single), keyed) for column in direction.produced + ] return self._tuple(reads) def _tuple(self, reads: list[str]) -> str: @@ -457,10 +459,10 @@ def _call(self, node: FunctionCallNode, ctx: _Context) -> tuple[str, int]: by = node.kwargs['by'] assert isinstance(by, RelationNode) outer = ctx - walk = by.walk - at = {r: outer.subscript(walk.dim(r)) for r in (*walk.produced, *walk.joined)} - for read in walk.consumed: - ctx = ctx.pulled_back(walk.dim(read), self._relation_read(walk, at, read)) + direction = by.direction + at = {r: outer.subscript(direction.dim(r)) for r in (*direction.produced, *direction.joined)} + for read in direction.consumed: + ctx = ctx.pulled_back(direction.dim(read), self._relation_read(direction, at, read)) return self._arithmetic(node.args[0], ctx) if (by := node.kwargs.get('by')) is not None: @@ -469,7 +471,7 @@ def _call(self, node: FunctionCallNode, ctx: _Context) -> tuple[str, int]: inner = ctx for d in by.dimensions: dummies[d], inner = inner.reducing(d) - conditions = list(self._grouping(by.walk, dummies, ctx)) + conditions = list(self._grouping(by.direction, dummies, ctx)) domain = ( f'{self.format.joined([self._membership(d, dummies[d]) for d in by.dimensions], "")} ' f'{self._op("such_that")} {self.format.joined(conditions, self._op("and"))}' @@ -487,23 +489,23 @@ def _call(self, node: FunctionCallNode, ctx: _Context) -> tuple[str, int]: domain = self.format.joined(memberships, '') return self.format.summation(domain, self._reduction_body(node.args[0], inner)), _PRECEDENCE['+'] - def _grouping(self, walk: RelationWalk, dummies: Mapping[str, str], ctx: _Context) -> list[str]: - """The conditions a grouped sum's domain carries for one walk: what the walk fixes of the row it joins on. + def _grouping(self, direction: Direction, dummies: Mapping[str, str], ctx: _Context) -> list[str]: + """The conditions a grouped sum's domain carries for one direction: what it fixes of the row it joins on. - A walk fixes its relation's key either way, so each value column it - fixes — consumed or produced, one lookup at the key either way — is - that column read there. A walk that fixes none of them asks only that - the row is there, because a value column the walk does not touch is - not read, and a bare relation has no value column to read at all. + A direction fixes its relation's key either way, so each value column + it fixes — consumed or produced, one lookup at the key either way — is + that column read there. One that fixes none of them asks only that the + row is there, because a value column it does not touch is not read, + and a bare relation has no value column to read at all. """ at = { - **{r: dummies[walk.dim(r)] for r in walk.consumed}, - **{r: ctx.subscript(walk.dim(r)) for r in (*walk.joined, *walk.produced)}, + **{r: dummies[direction.dim(r)] for r in direction.consumed}, + **{r: ctx.subscript(direction.dim(r)) for r in (*direction.joined, *direction.produced)}, } - fixed = [r for r in walk.values if r in at] + fixed = [r for r in direction.values if r in at] if not fixed: - return [self._relation_row(walk.name, [at[k] for k in walk.key])] - return [f'{self._relation_read(walk, at, r)} {self._op("equal")} {at[r]}' for r in fixed] + return [self._relation_row(direction.name, [at[k] for k in direction.key])] + return [f'{self._relation_read(direction, at, r)} {self._op("equal")} {at[r]}' for r in fixed] def _group(self, by: ArithmeticNode | None, dim: str) -> str: """A ``by=`` as the superscript its translation operator carries. @@ -515,9 +517,9 @@ def _group(self, by: ArithmeticNode | None, dim: str) -> str: if by is None: return '' assert isinstance(by, RelationNode) - walk = by.walk - at = {r: self.symbols.index[walk.dim(r)] for r in (*walk.consumed, *walk.joined)} - return self._tuple([self._relation_read(walk, at, r) for r in walk.produced]) + direction = by.direction + at = {r: self.symbols.index[direction.dim(r)] for r in (*direction.consumed, *direction.joined)} + return self._tuple([self._relation_read(direction, at, r) for r in direction.produced]) def _width(self, node: ArithmeticNode) -> str: """``sum_back``'s ``window=``: a number, or a parameter's own symbol. @@ -982,7 +984,7 @@ def position_notes(self, noticed: Noticed) -> list[str]: 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.mono("shift")} steps along, 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.' ) diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index b39a0d18..e4dd5130 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -22,8 +22,8 @@ #: `fixtures.DISPATCH_MODEL` plus buses: a dim rule is mostly about an #: expression carrying a dim its frame does not, which needs three dims to #: state. `snap_bus` is over `snapshot` so it can partition the axis the -#: translations walk; `spinup` and `horizon` are the named amount that obeys -#: the position rules and the one that spans the axis walked; `bus_lead` is +#: translations step along; `spinup` and `horizon` are the named amount that +#: obeys the position rules and the one that spans that axis; `bus_lead` is #: over a dim `p` does not carry, so it is readable only through a `by=`. BASE = { 'dimensions': { @@ -111,12 +111,12 @@ def namespace() -> Namespace: pytest.param( 'sum(p, by=gen_zone, over=generator, into=zone)', {'snapshot', 'zone'}, - id='a-two-key-relation-consumes-the-key-it-walks-and-keeps-the-other', + id='a-two-key-relation-consumes-the-key-it-names-and-keeps-the-other', ), pytest.param( 'sum(p, by=gen_zone, over=snapshot, into=zone)', {'generator', 'zone'}, - id='the-same-table-walked-along-its-other-key', + id='the-same-table-read-along-its-other-key', ), pytest.param( 'at(zone_load, by=gen_zone, into=generator, over=zone)', @@ -166,7 +166,7 @@ def namespace() -> Namespace: pytest.param( 'sum(p, by=gen_bz, into=bus, over=generator)', {'snapshot', 'bus'}, - id='a-value-column-not-walked-is-not-read', + id='a-value-column-not-named-is-not-read', ), pytest.param( 'sum(p, by=gen_bz, over=generator, into=bus)', @@ -225,7 +225,7 @@ def test_a_bare_name_reaches_the_variable_a_dual_the_same_named_constraint(): ), pytest.param( 'sum(load, by=gen_bus, over=generator, into=bus)', - r"sum\(by=gen_bus\) consumes \['generator'\], the dims it walks from", + r"sum\(by=gen_bus\) consumes \['generator'\], the dims it reads from", id='sum-requires-the-grouped-dim', ), pytest.param( @@ -240,8 +240,8 @@ def test_a_bare_name_reaches_the_variable_a_dual_the_same_named_constraint(): ), pytest.param( "shift(p, along=snapshot, offset=horizon, edge='wrap')", - r'varies along the axis it walks is a permutation rather than a lag', - id='a-named-offset-does-not-span-the-axis-it-walks', + r'varies over the axis it steps along is a permutation rather than a lag', + id='a-named-offset-does-not-span-the-axis-it-steps-along', ), pytest.param( 'sum_back(p, along=snapshot, window=cost)', diff --git a/tests/test_lowering.py b/tests/test_lowering.py index 19a3f444..58f85e3f 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -31,6 +31,7 @@ Constant, DimensionComparisonNode, DimensionDeclaration, + Direction, Divide, Dual, ExpressionNode, @@ -51,7 +52,6 @@ Sum, Translate, Variable, - Walk, Window, children, divisor_parameters, @@ -83,10 +83,10 @@ 'constraints': {'c': {'dims': [], 'expression': 'sum(p, over=g) >= 1'}}, } -#: `lk` as `sum` walks it: key consumed, value produced, nothing joined. +#: `lk` as `sum` reads it: key consumed, value produced, nothing joined. LK = RelationDeclaration('lk', (('g', 'g'), ('h', 'h')), ('g',)) LK2 = RelationDeclaration('lk2', (('g', 'g'), ('z', 'z')), ('g',)) -LK_WALK = Walk(LK, ('g',), ('h',), ()) +LK_DIRECTION = Direction(LK, ('g',), ('h',), ()) AT_BUS = RelationDeclaration('at_bus', (('g', 'g'), ('bus', 'bus')), ('g',)) #: `fixtures.SMALL_MODEL` plus a second relation and a per-entity @@ -412,13 +412,13 @@ def test_a_power_lowers_to_a_node_of_its_own(dispatch_schema): pytest.param('sum(q, over=h)', Sum(Variable('q'), ('h',)), id='an-over-consumes-the-dim-it-names'), pytest.param( 'sum(p, by=lk, over=g, into=h)', - GroupSum(Variable('p'), walk=LK_WALK), + GroupSum(Variable('p'), direction=LK_DIRECTION), id='a-grouped-sum-names-the-dim-it-consumes-and-the-one-it-lands-on', ), pytest.param( 'at(r, by=lk, over=h, into=g)', - At(Variable('r'), walk=Walk(LK, ('h',), ('g',), ())), - id='a-pullback-walks-the-same-table-back', + At(Variable('r'), direction=Direction(LK, ('h',), ('g',), ())), + id='a-pullback-reads-the-same-table-back', ), pytest.param( "shift(p, along=g, offset=1, edge='wrap')", @@ -443,7 +443,7 @@ def test_a_power_lowers_to_a_node_of_its_own(dispatch_schema): offset=1, wrap=False, fill=0.0, - partition=Walk(LK, ('g',), ('h',), ()), + partition=Direction(LK, ('g',), ('h',), ()), ), id='a-translation-stops-at-the-edges-of-the-relation-it-names', ), @@ -464,7 +464,7 @@ def test_a_power_lowers_to_a_node_of_its_own(dispatch_schema): 'g', width=2, wrap=False, - partition=Walk(LK, ('g',), ('h',), ()), + partition=Direction(LK, ('g',), ('h',), ()), ), id='a-window-stops-at-the-edges-of-the-relation-it-names', ), @@ -511,8 +511,8 @@ def _partition_of(row): return partition -def test_a_relation_lowers_with_the_walk_each_call_takes(): - """Every node reading a relation carries its columns, its key and the walk, so a consumer joins on the right columns.""" +def test_a_relation_lowers_with_the_direction_each_call_names(): + """Every node reading a relation carries its columns, its key and the direction, so a consumer joins on the right columns.""" program = to_program( { 'dimensions': {'snapshot': {'dtype': 'int'}, 'generator': {}, 'zone': {}}, @@ -548,7 +548,7 @@ def test_a_relation_lowers_with_the_walk_each_call_takes(): assert program.dimension('zone').relations == (declared,), 'and under its last' assert program.relations == {'zone_of': declared}, 'and once in the program' zonal = program.constraints['zonal'].lhs - assert zonal == GroupSum(Variable('p'), walk=Walk(declared, ('generator',), ('zone',), ('snapshot',))), ( + assert zonal == GroupSum(Variable('p'), direction=Direction(declared, ('generator',), ('zone',), ('snapshot',))), ( 'a grouped sum names the column it consumes, the one it produces and the one it joins on' ) assert isinstance(zonal, GroupSum) @@ -557,12 +557,12 @@ def test_a_relation_lowers_with_the_walk_each_call_takes(): ('zone',), ('snapshot',), 'zone_of', - ), 'the dims a consumer reads are read off the walk' + ), 'the dims a consumer reads are read off the direction' assert program.constraints['history'].lhs == GroupSum( - Variable('p'), walk=Walk(declared, ('snapshot',), ('zone',), ('generator',)) - ), 'the same table walked from its other key column' + Variable('p'), direction=Direction(declared, ('snapshot',), ('zone',), ('generator',)) + ), 'the same table read from its other key column' priced = program.constraints['priced'].rhs - assert priced == At(Parameter('price'), walk=Walk(declared, ('zone',), ('generator',), ('snapshot',))), ( + assert priced == At(Parameter('price'), direction=Direction(declared, ('zone',), ('generator',), ('snapshot',))), ( 'and its adjoint consumes the value column and produces the key column' ) assert isinstance(priced, At) @@ -591,7 +591,7 @@ def test_a_divisor_under_a_pullback_is_still_named(): """`children` has to descend through every node, or a refusal loses its name.""" quotient = Divide(Variable('x'), Parameter('rate')) component_of = RelationDeclaration('component_of', (('flow', 'flow'), ('component', 'component')), ('flow',)) - pulled = At(quotient, walk=Walk(component_of, ('component',), ('flow',), ())) + pulled = At(quotient, direction=Direction(component_of, ('component',), ('flow',), ())) assert divisor_parameters(pulled) == frozenset({'rate'}), 'the walk descends through `At`' assert divisor_parameters(Sum(pulled, ('flow',))) == frozenset({'rate'}), 'and through a `Sum` over it' @@ -666,8 +666,8 @@ def test_walk_is_the_node_column_of_walk_regions(): Power(Parameter('c'), Constant(2.0)): 'one-to-one', Divide(Variable('p'), Parameter('c')): 'one-to-one', Sum(Variable('p'), ('g',)): 'many-to-one', - GroupSum(Variable('p'), walk=Walk(AT_BUS, ('g',), ('bus',), ())): 'many-to-one', - At(Variable('p'), walk=Walk(AT_BUS, ('bus',), ('g',), ())): 'one-to-one', + GroupSum(Variable('p'), direction=Direction(AT_BUS, ('g',), ('bus',), ())): 'many-to-one', + At(Variable('p'), direction=Direction(AT_BUS, ('bus',), ('g',), ())): 'one-to-one', Translate(Variable('p'), 't', offset=1, wrap=False, fill=0.0): 'one-to-one', Window(Variable('p'), 't', width=2, wrap=False): 'one-to-many', Cases((Region(Mask(ParameterDefinedNode('c', ('g',))), Variable('p')),)): 'one-to-one', diff --git a/tests/test_parser.py b/tests/test_parser.py index 4a0a73b1..87fb4d02 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -45,10 +45,10 @@ from math_spec.program import ( AndNode, BooleanLiteralNode, + Direction, NotNode, OrNode, RelationDeclaration, - Walk, _conjuncts, ) @@ -510,7 +510,7 @@ def test_a_node_prints_as_the_file_writes_it(text, printed): pytest.param(DimensionNode('t'), 't', id='a-dimension'), pytest.param(DualNode('budget'), 'dual(budget)', id='a-dual'), pytest.param( - RelationNode('zone_of', ('u',), ('zone',), Walk(RelationDeclaration('zone_of', ()), (), (), ())), + RelationNode('zone_of', ('u',), ('zone',), Direction(RelationDeclaration('zone_of', ()), (), (), ())), 'zone_of', id='a-relation', ), diff --git a/tests/test_validation.py b/tests/test_validation.py index 5724a311..2bb3c379 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -793,11 +793,11 @@ class TestRulesDecidedWithoutData: pytest.param( {'objective': {'expression': 'sum(sum(q, by=lk, over=h, into=g))'}}, ( - "this sum walks to the key ['g']", + "this sum lands on the key ['g']", 'that is a read, which is', "at(..., by=lk, over=['h'], into=['g'])", ), - id='a-sum-that-walks-to-the-key-is-a-read', + id='a-sum-that-lands-on-the-key-is-a-read', ), pytest.param( { @@ -964,7 +964,7 @@ class TestRulesDecidedWithoutData: 'dimensions.z': {}, 'objective': {'expression': 'sum(sum(p, by=[lk, lk2], over=g, into=[h, z]))'}, }, - ('names 2 relations, and one call walks one table',), + ('names 2 relations, and one call reads one table',), id='several-relations-in-one-by', ), pytest.param( @@ -981,7 +981,7 @@ class TestRulesDecidedWithoutData: 'objective': {'expression': 'sum(sum(q, by=bare, over=k, into=m))'}, }, ("joins 'bare' on ['h'] through more than one column",), - id='a-walk-joining-one-dimension-through-two-columns', + id='a-call-joining-one-dimension-through-two-columns', ), pytest.param( { @@ -1022,14 +1022,14 @@ def test_a_rule_decided_without_data(self, patch, fragments): for fragment in fragments: assert fragment in message - def test_the_at_a_sum_walking_to_the_key_names_is_one_the_language_takes(self): + def test_the_at_a_sum_landing_on_the_key_names_is_one_the_language_takes(self): """A refusal that names a call is holding out a rewrite, so the rewrite has to load. - Was: the message swapped the walk's ends, answering a sum refused for - walking to the key with `at(..., over=, into=)` — which + Was: the message swapped the direction's ends, answering a sum refused + for landing on the key with `at(..., over=, into=)` — which `at` refuses in turn, for reading a column that is not single-valued at the one the operand fixes. Both operators take `over=` as the - column the walk consumes, so the rewrite is the author's own spelling + column the read consumes, so the rewrite is the author's own spelling with `at` in place of `sum`. Reading the call out of the message rather than restating it is the diff --git a/tests/typesetting/golden/latex.out b/tests/typesetting/golden/latex.out index feb37d75..abeef120 100644 --- a/tests/typesetting/golden/latex.out +++ b/tests/typesetting/golden/latex.out @@ -63,7 +63,7 @@ \noindent $t \ominus^{\mathrm{relation}(t)} k$ denotes a translation counted inside the group a relation puts $t$ in (\texttt{shift(by=relation)}), 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{relation}(t)} k$ is both at once. -\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}(t)$ denotes where index $t$ sits along its dimension's own order --- the order \texttt{shift} steps along, 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{relation}(t)}(t)$ counts within the group a relation puts $t$ in: the subscript names the map, $\mathcal{T}_{\mathrm{relation}(t)}$ is the group it lands in, and that group has a first position of its own. diff --git a/tests/typesetting/golden/markdown.out b/tests/typesetting/golden/markdown.out index 09439aee..c4759172 100644 --- a/tests/typesetting/golden/markdown.out +++ b/tests/typesetting/golden/markdown.out @@ -62,7 +62,7 @@ $`t \boxminus_{v} k`$ denotes translation with $`v`$ standing where index $`t-k` $`t \ominus^{\mathrm{relation}(t)} k`$ denotes a translation counted inside the group a relation puts $`t`$ in (`shift(by=relation)`), 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{relation}(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}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, 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{relation}(t)}(t)`$ counts within the group a relation puts $`t`$ in: the subscript names the map, $`\mathcal{T}_{\mathrm{relation}(t)}`$ is the group it lands in, and that group has a first position of its own. diff --git a/tests/typesetting/golden/model.yaml b/tests/typesetting/golden/model.yaml index 33ffee55..321491ab 100644 --- a/tests/typesetting/golden/model.yaml +++ b/tests/typesetting/golden/model.yaml @@ -25,10 +25,10 @@ relations: zone_of: { key: bus, values: zone } area_of: { key: bus, values: zone } # a second map into the same set, to compare against season_of: { key: snapshot, values: season } - gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call walks one and joins on the other + gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call consumes one and joins on the other rep_of: { key: snapshot, values: { rep: snapshot } } # a map into its own dimension: the representative snapshot - connection: { key: [generator, bus] } # a bare relation, with no value columns: many-to-many, walked only by sum with both ends named - gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, walked to both at once + connection: { key: [generator, bus] } # a bare relation, with no value columns: many-to-many, read only by sum with both ends named + gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once parameters: p_max: { dims: [generator] } @@ -149,7 +149,7 @@ constraints: pullback: # at(), which re-indexes through a relation instead of an offset dims: [snapshot, bus] expression: spill <= at(zone_cap, by=zone_of, over=zone, into=bus) - grouped_once: # one table walked to two value columns: the domain carries a condition per column + grouped_once: # one table read to two value columns: the domain carries a condition per column dims: [snapshot, bus, technology] expression: sum(p, by=gen_bt, into=[bus, technology], over=generator) <= tech_cap pulled_back_once: # its adjoint, reading one slot through two columns of one table @@ -166,16 +166,16 @@ constraints: dims: [snapshot, generator, bus] where: "connection" expression: p <= load - representative: # a map into its own dimension, walked both ways: the frame is unchanged and the index is primed + representative: # a map into its own dimension, read both ways: the frame is unchanged and the index is primed dims: [snapshot] expression: sum(spill, by=rep_of, over=snapshot, into=rep) <= at(spill, by=rep_of, over=rep, into=snapshot) - zonal: # a grouping through a two-key map, walked along one key: the condition reads the other, and the row keeps it + zonal: # a grouping through a two-key map, consuming one key: the condition reads the other, and the row keeps it dims: [snapshot, zone] expression: sum(p, by=gen_zone, over=generator, into=zone) <= zone_cap - zonal_history: # the same table walked along its other key + zonal_history: # the same table consuming its other key dims: [generator, zone] expression: sum(p, by=gen_zone, over=snapshot, into=zone) <= zone_cap - zonal_membership: # the same table walked between its two key columns: no value column is read, so the domain asks only that the row is there + zonal_membership: # the same table read between its two key columns: no value column is read, so the domain asks only that the row is there dims: [snapshot] expression: sum(units, by=gen_zone, over=generator, into=snapshot) <= budget zonal_pullback: # its adjoint, reading the slot the row's own snapshot puts the generator in diff --git a/tests/typesetting/golden/typst.out b/tests/typesetting/golden/typst.out index 3a437390..ef848e20 100644 --- a/tests/typesetting/golden/typst.out +++ b/tests/typesetting/golden/typst.out @@ -51,7 +51,7 @@ $t minus.square_(v) k$ denotes translation with $v$ standing where index $t-k$ l $t minus.o^(upright("relation")(t)) k$ denotes a translation counted inside the group a relation puts $t$ in (`shift(by=relation)`), so a term never crosses out of its own group. The two modifiers take different slots --- the group above, the fill below --- so $t minus.square_(v)^(upright("relation")(t)) k$ is both at once. -$upright("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 $upright("pos")(t)$ against positions. +$upright("pos")(t)$ denotes where index $t$ sits along its dimension's own order --- the order `shift` steps along, not the order labels sort in --- counted from $0$. The index itself stays the coordinate, so $t$ compares against labels and $upright("pos")(t)$ against positions. $upright("pos")_(upright("relation")(t))(t)$ counts within the group a relation puts $t$ in: the subscript names the map, $cal(T)_(upright("relation")(t))$ is the group it lands in, and that group has a first position of its own. diff --git a/tests/typesetting/test_walk.py b/tests/typesetting/test_walk.py index 1fa637a9..c1c4cc34 100644 --- a/tests/typesetting/test_walk.py +++ b/tests/typesetting/test_walk.py @@ -632,12 +632,12 @@ def test_every_operator_probe_renders(path, name: FormatName, fmt: Format): # --------------------------------------------------------------------------- -# a grouped sum's domain — every column the walk fixes, and no column it does not +# a grouped sum's domain — every column the direction fixes, and no column it does not # --------------------------------------------------------------------------- -#: Two tables a walk can leave a value column unread. A walk between -#: `gen_zone`'s two key columns reads no value column at all, and a walk that +#: Two tables a call can leave a value column unread. A sum between +#: `gen_zone`'s two key columns reads no value column at all, and a sum that #: consumes one of `gen_bt`'s two value columns leaves the other one. UNREAD = { 'dimensions': { @@ -655,7 +655,7 @@ def test_every_operator_probe_renders(path, name: FormatName, fmt: Format): 'variables': { 'p': {'dims': ['snapshot', 'generator']}, 'f': {'dims': ['generator', 'bus']}, - # indexed by the key column the walk consumes alone, so the column it lands on is one it brings + # indexed by the key column the sum consumes alone, so the column it lands on is one it brings 'u': {'dims': ['generator']}, }, } @@ -667,13 +667,13 @@ def _grouped(dims: list[str], expression: str) -> str: return next(line for line in to_latex(model, legend=False).splitlines() if line.startswith(r'\text{c}')) -def test_a_walk_that_reads_no_value_column_asks_only_that_the_key_has_a_row(): +def test_a_sum_that_reads_no_value_column_asks_only_that_the_key_has_a_row(): """`sum(u, by=gen_zone, over=generator, into=snapshot)` died with `KeyError: 'zone'`. The domain was written as a whole row of the table, which needs an index - for every column, and this walk goes between the two key columns: it reads + for every column, and this sum goes between the two key columns: it reads no value column, so there is no index to write in `zone`'s place. What the - walk asks of the table is that the key it walks between has a row at all. + sum asks of the table is that the key it reads between has a row at all. """ row = _grouped(['snapshot'], 'sum(u, by=gen_zone, over=generator, into=snapshot) <= cap') assert r'\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) \text{ is defined}}' in row, ( @@ -681,12 +681,12 @@ def test_a_walk_that_reads_no_value_column_asks_only_that_the_key_has_a_row(): ) -def test_a_value_column_the_walk_consumes_is_a_condition_like_a_produced_one(): +def test_a_value_column_the_call_consumes_is_a_condition_like_a_produced_one(): """`sum(f, by=gen_bt, over=[generator, bus], into=technology)` bound `b` and then said nothing about it, so the sum ran over every bus rather than over the one the table puts each generator on. - The conditions were written per *produced* column. A walk fixes a value + The conditions were written per *produced* column. A call fixes a value column by consuming it too, and either way the column is one lookup at the key. """ @@ -694,7 +694,7 @@ def test_a_value_column_the_walk_consumes_is_a_condition_like_a_produced_one(): assert ( r'\sum_{g \in \mathcal{G},\ b \in \mathcal{B} \,:\, ' r'\mathrm{gen\_bt.bus}(g) = b \wedge \mathrm{gen\_bt.technology}(g) = e}' - ) in row, 'both columns the walk touches are read, in the order the table declares them' + ) in row, 'both columns the call touches are read, in the order the table declares them' # ---------------------------------------------------------------------------