From f4166eccd5500ebdb64cac616854e7c78aff1b6b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 13:40:46 +0000 Subject: [PATCH] feat(language): a sum or a read walks a relation with consume= and produce= rather than over= and into= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keywords the call names a walk with go back to the pair #437 used and #477 renamed. `by=`, `along=`, `within=` and `window=` are unchanged, and so are `piecewise: over:` and `sos: over:`, which name a declaration's axis and not a call keyword. The rename is whole: the operator signatures, every error message, the reference pages, every example, the generated schema and the golden model. The three golden outputs are byte for byte what they were, which is the claim this spelling makes — it prints the same math. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017ZhdyUr2JZZYcBzTuJrRKx --- README.md | 6 +- docs/about/limits.md | 8 +-- docs/about/what-counts-as-language.md | 2 +- docs/examples/commitment.md | 2 +- docs/examples/dispatch.md | 4 +- docs/examples/operators.md | 24 +++---- docs/examples/pypsa.md | 36 +++++------ docs/examples/pypsa_losses.md | 2 +- docs/examples/pypsa_stochastic.md | 8 +-- docs/reference/language/absence.md | 8 +-- docs/reference/language/declarations.md | 2 +- docs/reference/language/dimensions.md | 39 ++++++------ docs/reference/language/errors.md | 2 +- docs/reference/language/expressions.md | 40 ++++++------ docs/reference/language/index.md | 4 +- docs/reference/language/operators.md | 32 +++++----- docs/reference/language/piecewise.md | 6 +- docs/reference/language/reading.md | 2 +- docs/reference/language/reported.md | 4 +- docs/reference/notation.md | 18 +++--- examples/commitment.yaml | 2 +- examples/dispatch.yaml | 2 +- examples/operators/at_columns.yaml | 4 +- examples/operators/sum.yaml | 4 +- examples/operators/sum_by_column_lists.yaml | 4 +- examples/operators/sum_by_columns.yaml | 4 +- examples/piecewise.yaml | 2 +- examples/piecewise_lp.yaml | 2 +- examples/ports/transport_pwl.yaml | 4 +- examples/pypsa.yaml | 36 +++++------ examples/pypsa_losses.yaml | 2 +- examples/pypsa_stochastic.yaml | 8 +-- examples/sos.yaml | 2 +- schema/math-spec.schema.json | 4 +- src/math_spec/_expression_parser.py | 4 +- src/math_spec/degree.py | 6 +- src/math_spec/dimensions.py | 8 +-- src/math_spec/lowering.py | 8 +-- src/math_spec/model.py | 6 +- src/math_spec/operators.py | 26 ++++---- src/math_spec/piecewise.py | 6 +- src/math_spec/resolution.py | 23 +++---- src/math_spec/typesetting/walk.py | 2 +- src/math_spec/validation.py | 2 +- tests/fixtures.py | 2 +- tests/test_boundedness.py | 38 +++++------ tests/test_degree.py | 12 ++-- tests/test_dimensions.py | 32 +++++----- tests/test_expansion.py | 24 +++---- tests/test_lowering.py | 46 +++++++------- tests/test_parser.py | 32 +++++----- tests/test_piecewise.py | 6 +- tests/test_separability.py | 8 +-- tests/test_validation.py | 70 +++++++++++---------- tests/test_yaml_loading.py | 2 +- tests/typesetting/golden/model.yaml | 18 +++--- tests/typesetting/test_cases.py | 2 +- tests/typesetting/test_declaration.py | 2 +- tests/typesetting/test_symbols.py | 2 +- tests/typesetting/test_walk.py | 12 ++-- tools/spec_math.py | 8 +-- 61 files changed, 371 insertions(+), 365 deletions(-) diff --git a/README.md b/README.md index 953aa399..e762ed40 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ with no data and no solver.** A math-spec file declares four things: the axes the model runs over, such as `snapshot` and `generator`; the data it expects, such as `load` and `cost`; the decisions the solver makes, such as `dispatch`; and the rules those decisions -obey, such as `sum(dispatch, over=generator) == load`. The file +obey, such as `sum(dispatch, consume=generator) == load`. The file [below](#example) is a complete model. math-spec reads that file, checks everything that can be checked without data, @@ -93,7 +93,7 @@ variables: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize @@ -381,7 +381,7 @@ through are a dependency rather than one engine's internals. The keys themselves which are YAML math, a block per component, `dims:` and a `where:` string, come from [Calliope](https://github.com/calliope-project/calliope). [linopy](https://github.com/PyPSA/linopy) supplies the vocabulary that -`sum(over=)` and the dimension rules are named against. Issue numbers in these +`sum(consume=)` and the dimension rules are named against. Issue numbers in these pages point at lpspec, where the arguments happened. ## Status diff --git a/docs/about/limits.md b/docs/about/limits.md index 776cddf3..31cf139a 100644 --- a/docs/about/limits.md +++ b/docs/about/limits.md @@ -36,12 +36,12 @@ instead. ### What a new primitive has to satisfy **A macro must be able to call it.** Everything a modeller might pass in goes in -the value of a keyword argument, such as `over=snapshot`, never in the key. A -macro can write `over=d` and let the caller supply `d`. It could not do that if +the value of a keyword argument, such as `consume=snapshot`, never in the key. A +macro can write `consume=d` and let the caller supply `d`. It could not do that if the dimension were the keyword itself. **An operator may read the whole table. It pays one full pass over the data.** -`sum(p, over=g)` reads one row per generator. `shift(p, along=t, offset=1)` reads +`sum(p, consume=g)` reads one row per generator. `shift(p, along=t, offset=1)` reads one row, the one before it. `x * y * a` reads the rows of `a` that pair an `x` with a `y`. Each reads a bounded number of rows per output row, so an engine builds the model one chunk of rows at a time. @@ -71,7 +71,7 @@ quadratic case: - **Where it stands.** More solvers and file formats take a quadratic objective than a quadratic constraint. Which ones is the [separate question below](#solver-capability). -- **A product of two sums.** `sum(x, over=i) * sum(y, over=j)` multiplies every +- **A product of two sums.** `sum(x, consume=i) * sum(y, consume=j)` multiplies every term of the first sum by every term of the second, and the file does not say how many terms either sum has. It is refused. `x[i] * y[j] * a[i, j]` is allowed, because the table `a` says which pairs exist. diff --git a/docs/about/what-counts-as-language.md b/docs/about/what-counts-as-language.md index 16988454..e044df8b 100644 --- a/docs/about/what-counts-as-language.md +++ b/docs/about/what-counts-as-language.md @@ -16,7 +16,7 @@ The test is one question: Suppose the engine sums `p` over `generator` and the renderer prints a sum over `snapshot`. The file now means two things, and that is a bug. So the language -decides what `sum(p, over=generator)` means, and both tools read the answer +decides what `sum(p, consume=generator)` means, and both tools read the answer instead of working it out. Suppose instead that the engine writes the model in one solver's file format and diff --git a/docs/examples/commitment.md b/docs/examples/commitment.md index 915ad9c2..c1426c5a 100644 --- a/docs/examples/commitment.md +++ b/docs/examples/commitment.md @@ -65,7 +65,7 @@ expressions: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load upper: description: a unit that is not running produces nothing dims: [snapshot, generator] diff --git a/docs/examples/dispatch.md b/docs/examples/dispatch.md index 72a08743..a6e64d7f 100644 --- a/docs/examples/dispatch.md +++ b/docs/examples/dispatch.md @@ -12,7 +12,7 @@ varies when it needs a base to change one thing in. The `where:` on `dispatch` deletes the rows where a generator has no capacity, so [absence](../reference/language/absence.md) is declared in the file rather than -checked at run time. `sum(dispatch, over=generator)` names the dimension it reduces, so +checked at run time. `sum(dispatch, consume=generator)` names the dimension it reduces, so the constraint's `dims` is what remains. @@ -38,7 +38,7 @@ variables: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize diff --git a/docs/examples/operators.md b/docs/examples/operators.md index 920904cd..31719a86 100644 --- a/docs/examples/operators.md +++ b/docs/examples/operators.md @@ -44,12 +44,12 @@ objective: { sense: minimize, expression: sum(p) } $`\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}`$ -### `sum(array, over=dim)` +### `sum(array, consume=dim)` `examples/operators/sum.yaml` ```yaml -description: The plain reduction — `sum(array, over=dim)` collapses one dimension. +description: The plain reduction — `sum(array, consume=dim)` collapses one dimension. dimensions: snapshot: { dtype: int } @@ -66,7 +66,7 @@ variables: constraints: fleet_total: dims: [snapshot] - expression: sum(p, over=generator) <= limit + expression: sum(p, consume=generator) <= limit objective: { sense: minimize, expression: sum(p) } ``` @@ -147,13 +147,13 @@ objective: { sense: minimize, expression: sum(p) } $`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{limit}_{t,b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E}`$ -### `sum(array, by=relation, over=a, into=b)` +### `sum(array, by=relation, consume=a, produce=b)` `examples/operators/sum_by_columns.yaml` ```yaml description: >- - A walk that names its ends — `sum(array, by=relation, over=a, into=b)` + A walk that names its ends — `sum(array, by=relation, consume=a, produce=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. @@ -176,20 +176,20 @@ variables: constraints: zone_balance: dims: [zone, period] - expression: sum(p, by=zone_of, over=generator, into=zone) >= demand + expression: sum(p, by=zone_of, consume=generator, produce=zone) >= demand objective: { sense: minimize, expression: sum(p) } ``` $`\sum_{g \in \mathcal{G} \,:\, \mathrm{zone\_of}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}`$ -### `sum(array, by=relation, over=[a, …], into=[b, …])` +### `sum(array, by=relation, consume=[a, …], produce=[b, …])` `examples/operators/sum_by_column_lists.yaml` ```yaml description: >- - A walk with several columns at each end — `sum(array, by=relation, over=[a, …], into=[b, …])` + A walk with several columns at each end — `sum(array, by=relation, consume=[a, …], produce=[b, …])` consumes both key columns at once and lands on the product of both value columns in one join. @@ -213,7 +213,7 @@ variables: constraints: slot_cap: dims: [bus, technology] - expression: sum(p, by=slot_of, over=[generator, period], into=[bus, technology]) <= cap + expression: sum(p, by=slot_of, consume=[generator, period], produce=[bus, technology]) <= cap objective: { sense: minimize, expression: sum(p) } ``` @@ -254,13 +254,13 @@ objective: { sense: minimize, expression: sum(p) } $`p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}`$ -### `at(array, by=relation, over=a, into=b)` +### `at(array, by=relation, consume=a, produce=b)` `examples/operators/at_columns.yaml` ```yaml description: >- - A read that names its ends — `at(array, by=relation, over=a, into=b)` + A read that names its ends — `at(array, by=relation, consume=a, produce=b)` reads column `a` where a table has two columns over one dimension, here the sending end of a line. @@ -282,7 +282,7 @@ variables: constraints: sending_cap: dims: [line] - expression: f <= at(cap, by=ends, over=bus0, into=line) + expression: f <= at(cap, by=ends, consume=bus0, produce=line) objective: { sense: minimize, expression: sum(f) } ``` diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index f71824cc..9acc0651 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -1578,7 +1578,7 @@ Generator_e_sum_min: description: "`Generator-e_sum_min` — energy over the horizon is at least its floor; a floor of minus infinity is no row" dims: [generator] where: Generator_e_sum_min - expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) >= Generator_e_sum_min + expression: sum(Generator_p * snapshot_weightings_generators, consume=snapshot) >= Generator_e_sum_min ``` ```math @@ -1594,7 +1594,7 @@ Generator_e_sum_max: description: "`Generator-e_sum_max` — energy over the horizon is at most its budget; a budget of infinity is no row" dims: [generator] where: Generator_e_sum_max - expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) <= Generator_e_sum_max + expression: sum(Generator_p * snapshot_weightings_generators, consume=snapshot) <= Generator_e_sum_max ``` ```math @@ -2362,7 +2362,7 @@ Kirchhoff_Voltage_Law: impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: sum(Line_s * Line_cycle_weight, consume=line) == 0 ``` ```math @@ -3291,9 +3291,9 @@ primary_energy: the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, over=snapshot), over=store) + sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, consume=snapshot), consume=generator) + - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, consume=snapshot), consume=storage_unit) + - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, consume=snapshot), consume=store) ``` ```math @@ -3309,9 +3309,9 @@ operational_limit: generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, over=snapshot), over=store) + sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, consume=snapshot), consume=generator) + - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, consume=snapshot), consume=storage_unit) + - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, consume=snapshot), consume=store) ``` ```math @@ -3324,8 +3324,8 @@ operational_limit: transmission_volume_expansion: description: what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches expression: >- - sum(Line_s_nom_ext * Line_volume_weight, over=line) - + sum(Link_p_nom_ext * Link_volume_weight, over=link) + sum(Line_s_nom_ext * Line_volume_weight, consume=line) + + sum(Link_p_nom_ext * Link_volume_weight, consume=link) ``` ```math @@ -3338,8 +3338,8 @@ transmission_volume_expansion: transmission_expansion_cost: description: what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches expression: >- - sum(Line_s_nom_ext * Line_expansion_cost_weight, over=line) - + sum(Link_p_nom_ext * Link_expansion_cost_weight, over=link) + sum(Line_s_nom_ext * Line_expansion_cost_weight, consume=line) + + sum(Link_p_nom_ext * Link_expansion_cost_weight, consume=link) ``` ```math @@ -3352,11 +3352,11 @@ transmission_expansion_cost: tech_capacity_expansion: description: what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set expression: >- - sum(Generator_p_nom_ext * Generator_tech_capacity_weight, over=generator) - + sum(Link_p_nom_ext * Link_tech_capacity_weight, over=link) - + sum(Line_s_nom_ext * Line_tech_capacity_weight, over=line) - + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, over=storage_unit) - + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + sum(Generator_p_nom_ext * Generator_tech_capacity_weight, consume=generator) + + sum(Link_p_nom_ext * Link_tech_capacity_weight, consume=link) + + sum(Line_s_nom_ext * Line_tech_capacity_weight, consume=line) + + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, consume=storage_unit) + + sum(Store_e_nom_ext * Store_tech_capacity_weight, consume=store) ``` ```math diff --git a/docs/examples/pypsa_losses.md b/docs/examples/pypsa_losses.md index 1dc42940..69829ac4 100644 --- a/docs/examples/pypsa_losses.md +++ b/docs/examples/pypsa_losses.md @@ -309,7 +309,7 @@ Kirchhoff_Voltage_Law: impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: sum(Line_s * Line_cycle_weight, consume=line) == 0 ``` ```math diff --git a/docs/examples/pypsa_stochastic.md b/docs/examples/pypsa_stochastic.md index d6b97cfc..eea1a765 100644 --- a/docs/examples/pypsa_stochastic.md +++ b/docs/examples/pypsa_stochastic.md @@ -123,7 +123,7 @@ objective: description: capacity once, operation in expectation, and a share of it at the tail expression: >- sum(Generator_p_nom_ext * Generator_capital_cost) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, consume=scenario) + CVaR_omega * CVaR ``` @@ -302,7 +302,7 @@ a_{s} - \mathit{scenario\_opex}_{s} + \theta \ge 0 \qquad \forall\, s \in \mathc CVaR_def: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] - expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR + expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, consume=scenario) <= CVaR ``` ```math @@ -315,8 +315,8 @@ CVaR_def: scenario_opex: description: what a future costs to run — the operating terms, before their weight expression: >- - sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, over=generator), over=snapshot) - + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, over=link), over=snapshot) + sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, consume=generator), consume=snapshot) + + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, consume=link), consume=snapshot) ``` ```math diff --git a/docs/reference/language/absence.md b/docs/reference/language/absence.md index 9310e453..773e2532 100644 --- a/docs/reference/language/absence.md +++ b/docs/reference/language/absence.md @@ -58,10 +58,10 @@ constraints: expression: x + y >= 1 # rows at wind and gas; no row at old total: dims: [] - expression: sum(x + y, over=g) >= 1 # x[wind] + y[wind] + x[gas] + y[gas] >= 1 + expression: sum(x + y, consume=g) >= 1 # x[wind] + y[wind] + x[gas] + y[gas] >= 1 split: dims: [] - expression: sum(x, over=g) + sum(y, over=g) >= 1 # x[old] is back in + expression: sum(x, consume=g) + sum(y, consume=g) >= 1 # x[old] is back in ``` `each` has no row at `old`, so there is no `x[old] >= 1`. `total` sums the @@ -88,7 +88,7 @@ does an output slot stand for several input slots, or for one? | Operator | An output slot reads | An absent input | | -------------------------------- | ------------------------------- | ------------------------------------ | -| `sum(x, over=d)` | every position along `d` | is one summand fewer; the row stands | +| `sum(x, consume=d)` | every position along `d` | is one summand fewer; the row stands | | `sum(x, by=relation)` | every member of the group | is one summand fewer; the row stands | | `sum_back(x, along=d, window=w)` | the positions the window covers | is one summand fewer; the row stands | | `shift(x, along=d, offset=n)` | one position, `n` back | _is_ the output, so it spreads | @@ -148,7 +148,7 @@ them, and that is the start of the recurrence rather than a bug. A [reported expression](reported.md) is arithmetic over solved numbers, so it inherits their absence by the same rule as above. Through pointwise arithmetic, a null spreads: `cost / delivered` has no value wherever either operand is -masked. Out of a summing operator, it does not: `sum(dispatch, over=g)` is one +masked. Out of a summing operator, it does not: `sum(dispatch, consume=g)` is one summand shorter where a `dispatch[g]` is masked, and stands as long as one slot does. diff --git a/docs/reference/language/declarations.md b/docs/reference/language/declarations.md index 9b604b55..fd18d200 100644 --- a/docs/reference/language/declarations.md +++ b/docs/reference/language/declarations.md @@ -130,7 +130,7 @@ variables: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load ``` | Field | | | diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 97d1b4ec..5e78e966 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -143,8 +143,7 @@ result keeps it, and keeps every dimension the relation does not name. `sum` consumes key columns and produces value columns. `at` consumes value columns and produces the key. -`over=` names the column consumed and `into=` the column produced. Name a column -only where the relation offers two. +Name a column only where the relation offers two. ```yaml dimensions: @@ -161,58 +160,58 @@ variables: constraints: zone_balance: # consumes generator, joins on period, produces zone: [generator, period] → [zone, period] dims: [zone, period] - expression: sum(p, by=zone_of, over=generator) >= demand + expression: sum(p, by=zone_of, consume=generator) >= demand history: # consumes period, joins on generator, produces zone: [generator, period] → [generator, zone] dims: [generator, zone] - expression: sum(p, by=zone_of, over=period) <= 100 + expression: sum(p, by=zone_of, consume=period) <= 100 capped_revenue: # consumes zone, joins on period, produces generator: [zone, period] → [generator, period] dims: [generator, period] - expression: at(price, by=zone_of, over=zone, into=generator) * p <= 1000 + expression: at(price, by=zone_of, consume=zone, produce=generator) * p <= 1000 ``` `zone_of` has one value column, `zone`. It is the only column `sum` can produce, -so `sum` leaves `into=zone` unsaid. It is the only column `at` can consume, so -`at` may leave `over=zone` unsaid too. `zone_of` has two key columns, and there -the call chooses: `sum` names the one it consumes, because `over=generator` and -`over=period` are different constraints, and `at` names the one it produces. +so `sum` leaves `produce=zone` unsaid. It is the only column `at` can consume, so +`at` may leave `consume=zone` unsaid too. `zone_of` has two key columns, and there +the call chooses: `sum` names the one it consumes, because `consume=generator` and +`consume=period` are different constraints, and `at` names the one it produces. `period` is joined on either way. With one key column and one value column, `sum(p, by=gen_bus)` and `at(price, by=gen_bus)` need neither keyword. A column left out where the relation offers two is refused, and the message lists the candidates: ``` -sum(by=zone_of): 'zone_of' has 2 key columns (['generator', 'period']), and the call has to say which over= names. +sum(by=zone_of): 'zone_of' has 2 key columns (['generator', 'period']), and the call has to say which consume= names. ``` `capped_revenue` reads the price of the zone this generator sat in that period. The typesetter prints it as $`\mathrm{price}_{\mathrm{zone\_of}(g,\ e),e}`$, and the joined `period` is the second subscript. -- **Either keyword takes a list.** `sum(p, by=gen_bt, into=[bus, technology])` +- **Either keyword takes a list.** `sum(p, by=gen_bt, produce=[bus, technology])` lands on the product `bus × technology` in one join. - `sum(p, by=zone_of, over=[generator, period])` consumes both key columns at - once. `at(tech_cap, by=gen_bt, over=[bus, technology])` reads `tech_cap` at + `sum(p, by=zone_of, consume=[generator, period])` consumes both key columns at + once. `at(tech_cap, by=gen_bt, consume=[bus, technology])` reads `tech_cap` at each generator's bus and technology together. - **A produced dimension the operand already carries is joined on.** In `sum(load * p, by=gen_bus)` with `load[snapshot, bus]`, the walk produces `bus` and `load` already carries it. So each generator's term is read at the bus the generator sits on, and the sum lands there. - **A value column that is not walked is not read.** - `sum(f, by=ends, over=line, into=bus1)` reads `bus1` and ignores `bus0` + `sum(f, by=ends, consume=line, produce=bus1)` reads `bus1` and ignores `bus0` ([roles](#roles)). - **`by=[a, b]` is one grouping onto what `a` and `b` produce together.** Each - relation is walked from its key to its value, so `over=` and `into=` have + relation is walked from its key to its value, so `consume=` and `produce=` have nothing to name. The relations consume the same dimension, and no two produce the same one. -- **`into=` needs a `by=`**, because a column belongs to a table. `over=` - without a `by=` names a dimension, as in `sum(p, over=period)`. +- **`produce=` needs a `by=`**, because a column belongs to a table. `consume=` + without a `by=` names a dimension, as in `sum(p, consume=period)`. Three refusals draw the line, and each message names the rewrite: | refused | message | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `at` on a bare relation | `at(by=connection): at reads one value per coordinate, and 'connection' is not single-valued in ['bus'] at the columns the operand fixes (['generator']) — its key is ['generator', 'bus']. Key the table by columns the read fixes, or read the other way.` | -| a `sum` that consumes no key column | `sum(by=zone_of): this sum walks to the key ['generator', 'period'], so each coordinate has one term and nothing is added up — that is a read, which is at()'s. Write at(..., by=zone_of, over=['zone'], into=['generator']), or sum toward a value column.` | +| a `sum` that consumes no key column | `sum(by=zone_of): this sum walks to the key ['generator', 'period'], so each coordinate has one term and nothing is added up — that is a read, which is at()'s. Write at(..., by=zone_of, consume=['zone'], produce=['generator']), or sum toward a value column.` | | an operand missing a joined dimension | `at(by=zone_of) joins on ['period'] (columns ['period'] of 'zone_of'), which the expression does not carry (dims ['zone']). A relation is walked between two of its columns and read at the others — index the operand by them, or walk between different columns.` | ### Partitions @@ -244,7 +243,7 @@ relations: rep_of: { key: snapshot, value: { rep: snapshot } } # the representative snapshot ``` -`sum(f, by=ends, over=line, into=bus1) - sum(f, by=ends, over=line, into=bus0)` +`sum(f, by=ends, consume=line, produce=bus1) - sum(f, by=ends, consume=line, produce=bus0)` is a nodal balance through one table: flow arriving at `bus1` less flow leaving `bus0`. `where: "ends.bus0 != ends.bus1"` excludes a line whose two ends are one bus. @@ -308,7 +307,7 @@ does with the column, not what the column holds: | 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 `value:` | `sum` walks 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)` | +| 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, consume=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](declarations.md#parameters)) | | is a per-row attribute the math only selects on — a fuel, a constraint's sense | a `str` parameter | it names rows rather than scaling them, and no set is declared to check its values against | diff --git a/docs/reference/language/errors.md b/docs/reference/language/errors.md index aaa3bdad..66273118 100644 --- a/docs/reference/language/errors.md +++ b/docs/reference/language/errors.md @@ -84,7 +84,7 @@ and [the limits](../../about/limits.md) gives the reasons. | Not in the language | Instead | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `variable * variable` in a bound or a `piecewise:` link | The objective and the constraints take it. Everywhere else, use a parameter coefficient ([expressions](expressions.md#where-a-product-of-two-variables-is-allowed)) | -| `sum(x, over=d) * sum(y, over=d)` | Multiply before you reduce, or constrain a variable to equal the reduction. A product of two sums pairs every term against every term | +| `sum(x, consume=d) * sum(y, consume=d)` | Multiply before you reduce, or constrain a variable to equal the reduction. A product of two sums pairs every term against every term | | degree 3 (`x * y * z`) | A variable constrained to equal one product, multiplied by the third | | `**` with a variable in it | `x * x` for a square. Over variable-free operands `**` is in the language ([expressions](expressions.md#where-a-product-of-two-variables-is-allowed)) | | arithmetic in `bounds:` | A name or a number. Ship the derived column as data ([#31](https://github.com/fluxopt/lpspec/issues/31)) | diff --git a/docs/reference/language/expressions.md b/docs/reference/language/expressions.md index 63922c8f..f09676fb 100644 --- a/docs/reference/language/expressions.md +++ b/docs/reference/language/expressions.md @@ -33,10 +33,10 @@ NUMBER ::= integer | float | "inf" | ".inf" ## Where a product of two variables is allowed The objective and the constraints take `variable * variable`. A quadratic cost is -`sum(p * p * wear, over=g)`, and a quadratic row is `p * q >= floor`. Three rules +`sum(p * p * wear, consume=g)`, and a quadratic row is `p * q >= floor`. Three rules bound it: -- **At most one factor may be a sum of terms.** `sum(p, over=g) * sum(q, over=g)` +- **At most one factor may be a sum of terms.** `sum(p, consume=g) * sum(q, consume=g)` is refused: it pairs every term of one sum against every term of the other, and nothing in the file says how many terms that is. Multiply before you reduce, or constrain a variable to equal the reduction, because a variable is @@ -90,8 +90,8 @@ fixed at load: | Position | Legal kinds | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | expression (`p * cost`) | a variable, or a parameter whose values are numbers ([dtype](declarations.md#parameters)) | -| dimension argument (`over=`, `along=`) | a dimension | -| relation argument (`by=` on `sum` / `at`) | a relation, and never a dimension. `over=` and `into=` name its columns | +| dimension argument (`consume=`, `along=`) | a dimension | +| relation argument (`by=` on `sum` / `at`) | a relation, and never a dimension. `consume=` and `produce=` name its columns | | `where` string | a parameter, variable, dimension or relation ([where strings](#where-strings)) | | `bounds.lower` / `bounds.upper` | a parameter name, or a number | | the `edge` key of `shift` | `'wrap'` in quotes, or a bare number. Never a dimension | @@ -120,19 +120,19 @@ A parameter and a variable both declare `dims`, and every dimension argument is name-checked. So **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)` | `(dims(x) − from(l)) ∪ into(l)` | error if `from(l) ⊄ dims(x)`, if a joined column's dimension is not in `dims(x)`, or if `l`'s key lies inside the columns `into=` names and the joined columns — that walk is a read, which is `at`'s | -| `sum(x, by=[l, m])` | `(dims(x) − from(l)) ∪ into(l) ∪ into(m)` | the same errors, plus an error if `l` and `m` consume different dimensions, or if they produce the same one | -| `at(x, by=l)` | `(dims(x) − from(l)) ∪ into(l)` | error if `from(l) ⊄ dims(x)`, if a joined column's dimension is not, or if `l` has no key inside the columns `into=` names | -| `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, consume=d)` | `dims(x) − {d}` | error if `d ∉ dims(x)` | +| `sum(x, by=l)` | `(dims(x) − from(l)) ∪ into(l)` | error if `from(l) ⊄ dims(x)`, if a joined column's dimension is not in `dims(x)`, or if `l`'s key lies inside the columns `produce=` names and the joined columns — that walk is a read, which is `at`'s | +| `sum(x, by=[l, m])` | `(dims(x) − from(l)) ∪ into(l) ∪ into(m)` | the same errors, plus an error if `l` and `m` consume different dimensions, or if they produce the same one | +| `at(x, by=l)` | `(dims(x) − from(l)) ∪ into(l)` | error if `from(l) ⊄ dims(x)`, if a joined column's dimension is not, or if `l` has no key inside the columns `produce=` names | +| `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 wherever the declaration's own dimensions cover the result. @@ -284,9 +284,9 @@ parameters: variables: p: { dims: [generator] } expressions: - total_generation: sum(p, over=generator) + total_generation: sum(p, consume=generator) emissions: - expression: sum(p * rate, over=generator) + expression: sum(p * rate, consume=generator) description: CO2 released, the quantity a cap would bound ``` @@ -420,7 +420,7 @@ value that a solve could report: weighted_sum: args: [array, weights] # positional formals, default [] kwargs: [over] # keyword formals, default [] - template: sum(array * weights, over=over) + template: sum(array * weights, consume=over) ``` - A template holds arithmetic, and no comparison. diff --git a/docs/reference/language/index.md b/docs/reference/language/index.md index c081f237..38916d83 100644 --- a/docs/reference/language/index.md +++ b/docs/reference/language/index.md @@ -30,7 +30,7 @@ variables: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize @@ -49,7 +49,7 @@ message that names the fix. These ten rules are what it checks. | 1 | A file has ten declaration keys, plus `version` and `description`. A key the schema does not know is refused, with the nearest valid key named: `boundz` → `bounds`. | [File shape](file.md) | | 2 | Everything that can be checked without data is checked when the file loads. | [Errors](errors.md) | | 3 | Every name is declared once. A parameter and a dimension both called `snapshot` is refused, and the message names both lines. | [Names](expressions.md#name-resolution) | -| 4 | Where a name may stand depends on what it is. A dimension may follow `over=` or `along=`, and may not be multiplied: `dispatch * snapshot` is refused, because `snapshot` is an axis and not a column of numbers. | [Names](expressions.md#name-resolution) | +| 4 | Where a name may stand depends on what it is. A dimension may follow `consume=` or `along=`, and may not be multiplied: `dispatch * snapshot` is refused, because `snapshot` is an axis and not a column of numbers. | [Names](expressions.md#name-resolution) | | 5 | `a + b` carries the dimensions of `a` and of `b` together. A constraint's expression must carry **exactly** its `dims`. The objective must carry none. A `where` or a bound may carry fewer dimensions than its declaration, never more. | [How dimensions combine](expressions.md#how-dimensions-combine) | | 6 | A variable's `where:` deletes the variable at the masked coordinates. There is no column there, not a column fixed at zero. A constraint's `where:` deletes the row. | [Absence](absence.md) | | 7 | A deleted variable takes its row with it: `x + y >= 1` has no row where `y` is deleted. Inside a `sum` it is one term fewer, and the row stays. So `sum(x + y)` and `sum(x) + sum(y)` are different constraints. | [Absence](absence.md#how-absence-travels) | diff --git a/docs/reference/language/operators.md b/docs/reference/language/operators.md index 8e885ba9..df03a87f 100644 --- a/docs/reference/language/operators.md +++ b/docs/reference/language/operators.md @@ -14,13 +14,13 @@ model can never depend on what a caller registered. A composition of them goes i | Operator | Result | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `sum(array)` | Every dimension that `array` carries collapses. The result is a scalar | -| `sum(array, over=dim)` | `dim` collapses. `array` must carry `dim` | +| `sum(array, consume=dim)` | `dim` collapses. `array` must carry `dim` | | `sum(array, by=relation)` | The relation's key column collapses onto its value column | | `sum(array, by=[relation, …])` | The same, onto every relation's value column. All the relations must consume the same dimension | -| `sum(array, by=relation, over=a, into=b)` | Column `a` collapses onto column `b`. The other key columns are joined on, so the array carries them and the result keeps them. Walked to the key, where each coordinate finds one row, it is a read — that is `at`'s | -| `sum(array, by=relation, over=[a, …], into=[b, …])` | The same with several columns on either side: consumed together, landed on a product | +| `sum(array, by=relation, consume=a, produce=b)` | Column `a` collapses onto column `b`. The other key columns are joined on, so the array carries them and the result keeps them. Walked to the key, where each coordinate finds one row, it is a read — that is `at`'s | +| `sum(array, by=relation, consume=[a, …], produce=[b, …])` | The same with several columns on either side: consumed together, landed on a product | | `at(array, by=relation)` | The relation's value column is replaced by its key column | -| `at(array, by=relation, over=a, into=b)` | Column `a` is replaced by column `b`, one value per coordinate, so the key lies in `b` and the joined columns. Either may be a list | +| `at(array, by=relation, consume=a, produce=b)` | Column `a` is replaced by column `b`, one value per coordinate, so the key lies in `b` and the joined columns. Either may be a list | | `shift(array, along=dim, offset=n)` | The value `n` positions earlier along `dim`. The vacated edge is **absent** | | `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 | @@ -33,22 +33,22 @@ model can never depend on what a caller registered. A composition of them goes i `array` is any expression with the right dimension set, so each operator reads a parameter as readily as a variable. Dimension arguments are name-checked at load, -so `sum(p, over=snapshto)` is an error rather than a silent no-op. +so `sum(p, consume=snapshto)` is an error rather than a silent no-op. [Every operator as math](#every-operator-as-math) shows how each row prints. ## `sum` -`sum(x, over=d)` adds up `x` along `d`, and `d` is gone from the result. +`sum(x, consume=d)` adds up `x` along `d`, and `d` is gone from the result. `sum(x)` names no dimension and reduces every dimension `x` carries, so its -result is a scalar. It is `sum(sum(x, over=a), over=b)` written once. +result is a scalar. It is `sum(sum(x, consume=a), consume=b)` written once. -An operand that is already scalar, and a `over=` naming a dimension the +An operand that is already scalar, and a `consume=` naming a dimension the operand does not carry, are both errors rather than no-ops. `sum(x, by=l)` sums through a [relation](dimensions.md#relations) and lands the result on the column it walks to: the value column, where the key draws the arrow, or -the one `into=` names. A nodal balance is one `sum(by=)` per kind of component, +the one `produce=` names. A nodal balance is one `sum(by=)` per kind of component, and the network's wiring stays in the relations: ```yaml @@ -78,8 +78,8 @@ constraints: The same `f` is summed twice through two relations, once as inflow and once as outflow, with no adjacency matrix and no join written by hand. -`sum(by=)` consumes a key column and produces a value column. `over=` and -`into=` name them where the relation offers two ([walks](dimensions.md#walks)), +`sum(by=)` consumes a key column and produces a value column. `consume=` and +`produce=` name them where the relation offers two ([walks](dimensions.md#walks)), and every other key column is joined on, so each group is one coordinate of it. A bare relation, one with no `value:`, is summed with both ends named. @@ -93,7 +93,7 @@ coordinate the data never covered is refused. See [absence](absence.md). `at(x, by=l)` walks the same 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, and a bare relation is never read by `at`. `over=` and `into=` +points at it, and a bare relation is never read by `at`. `consume=` and `produce=` name the columns where the relation offers two, and every other key column is read at the row's own coordinate ([walks](dimensions.md#walks)). @@ -335,13 +335,13 @@ language prints on [Every construct, as math](../notation.md). | Operator | Renders as | |---|---| | `sum(array)` | $`\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \le \mathrm{budget}`$ | -| `sum(array, over=dim)` | $`\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\, t \in \mathcal{T}`$ | +| `sum(array, consume=dim)` | $`\sum_{g \in \mathcal{G}} p_{t,g} \le \mathrm{limit}_{t} \qquad \forall\, t \in \mathcal{T}`$ | | `sum(array, by=relation)` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}`$ | | `sum(array, by=[relation, …])` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b \wedge \mathrm{gen\_tech}(g) = e} p_{t,g} \le \mathrm{limit}_{t,b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E}`$ | -| `sum(array, by=relation, over=a, into=b)` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{zone\_of}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}`$ | -| `sum(array, by=relation, over=[a, …], into=[b, …])` | $`\sum_{g \in \mathcal{G},\ e \in \mathcal{E} \,:\, \mathrm{slot\_of.bus}(g,\ e) = b \wedge \mathrm{slot\_of.technology}(g,\ e) = t} p_{g,e} \le \mathrm{cap}_{b,t} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T}`$ | +| `sum(array, by=relation, consume=a, produce=b)` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{zone\_of}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}`$ | +| `sum(array, by=relation, consume=[a, …], produce=[b, …])` | $`\sum_{g \in \mathcal{G},\ e \in \mathcal{E} \,:\, \mathrm{slot\_of.bus}(g,\ e) = b \wedge \mathrm{slot\_of.technology}(g,\ e) = t} p_{g,e} \le \mathrm{cap}_{b,t} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T}`$ | | `at(array, by=relation)` | $`p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}`$ | -| `at(array, by=relation, over=a, into=b)` | $`f_{l} \le \mathrm{cap}_{\mathrm{ends.bus0}(l)} \qquad \forall\, l \in \mathcal{L}`$ | +| `at(array, by=relation, consume=a, produce=b)` | $`f_{l} \le \mathrm{cap}_{\mathrm{ends.bus0}(l)} \qquad \forall\, l \in \mathcal{L}`$ | | `shift(array, along=dim, offset=n)` | $`p_{t} \le p_{t - 1} \qquad \forall\, t \in \mathcal{T}`$ | | `shift(array, along=dim, offset=n, edge='wrap')` | $`p_{t} \le p_{t \ominus 1} \qquad \forall\, t \in \mathcal{T}`$ | | `shift(array, along=dim, offset=n, edge=v)` | $`p_{t} \le p_{t \boxminus_{0} 1} \qquad \forall\, t \in \mathcal{T}`$ | diff --git a/docs/reference/language/piecewise.md b/docs/reference/language/piecewise.md index 305e15e8..3358dd02 100644 --- a/docs/reference/language/piecewise.md +++ b/docs/reference/language/piecewise.md @@ -93,7 +93,7 @@ running: Where the gate does not exist, the curve is ungated. The block emits the convexity row twice, under complementary masks: `== running` where the gate exists, and `== 1` where it does not. The row cannot be allowed to drop, because -it is `sum(lam, over=bp) == (activity)`, and +it is `sum(lam, consume=bp) == (activity)`, and [absence](absence.md#how-absence-travels) does not spread out of a reduction: an absent right-hand side would take the whole row, and leave the weights with nothing to make them a curve. @@ -204,10 +204,10 @@ sos: constraints: one_operating_point: dims: [converter, time] - expression: sum(weight, over=bp) == 1 + expression: sum(weight, consume=bp) == 1 on_the_curve: # one row per flow — this is where the count goes dims: [flow, time] - expression: rate == sum(at(weight, by=converter_of) * bp_rate, over=bp) + expression: rate == sum(at(weight, by=converter_of) * bp_rate, consume=bp) ``` Making the tie a row turns the count into data: a converter with a fourth flow is diff --git a/docs/reference/language/reading.md b/docs/reference/language/reading.md index d546b5ff..7f05a5f3 100644 --- a/docs/reference/language/reading.md +++ b/docs/reference/language/reading.md @@ -51,7 +51,7 @@ piecewise: constraints: target: dims: [] - expression: sum(p, over=generator) >= 100 + expression: sum(p, consume=generator) >= 100 objective: sense: minimize expression: sum(cost) diff --git a/docs/reference/language/reported.md b/docs/reference/language/reported.md index 6d6fd1a9..a0551781 100644 --- a/docs/reference/language/reported.md +++ b/docs/reference/language/reported.md @@ -18,8 +18,8 @@ parameters: variables: p: { dims: [snapshot, generator] } expressions: - system_cost: sum(sum(p * marginal_cost, over=generator), over=snapshot) - delivered: sum(sum(p, over=generator), over=snapshot) + system_cost: sum(sum(p * marginal_cost, consume=generator), consume=snapshot) + delivered: sum(sum(p, consume=generator), consume=snapshot) lcoe: system_cost / delivered objective: { sense: minimize, expression: system_cost } ``` diff --git a/docs/reference/notation.md b/docs/reference/notation.md index b28b260a..7711b319 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -388,7 +388,7 @@ one table walked to two value columns: the domain carries a condition per column ```yaml grouped_once: dims: [snapshot, bus, technology] - expression: sum(p, by=gen_bt, into=[bus, technology]) <= tech_cap + expression: sum(p, by=gen_bt, produce=[bus, technology]) <= tech_cap ``` ```math @@ -402,7 +402,7 @@ its adjoint, reading one slot through two columns of one table ```yaml pulled_back_once: dims: [generator] - expression: units <= at(tech_cap, by=gen_bt, over=[bus, technology]) + expression: units <= at(tech_cap, by=gen_bt, consume=[bus, technology]) ``` ```math @@ -431,7 +431,7 @@ a sum through a bare relation: the domain is a row of the relation rather than a ```yaml relational: dims: [snapshot, bus] - expression: sum(p, by=connection, over=generator, into=bus) <= load + expression: sum(p, by=connection, consume=generator, produce=bus) <= load ``` ```math @@ -502,7 +502,7 @@ a grouping through a two-key map, walked along one key: the condition reads the ```yaml zonal: dims: [snapshot, zone] - expression: sum(p, by=gen_zone, over=generator) <= zone_cap + expression: sum(p, by=gen_zone, consume=generator) <= zone_cap ``` ```math @@ -516,7 +516,7 @@ the same table walked along its other key ```yaml zonal_history: dims: [generator, zone] - expression: sum(p, by=gen_zone, over=snapshot) <= zone_cap + expression: sum(p, by=gen_zone, consume=snapshot) <= zone_cap ``` ```math @@ -531,7 +531,7 @@ its adjoint, reading the slot the row's own snapshot puts the generator in zonal_pullback: dims: [snapshot, generator] where: "gen_zone == 'north' AND position(generator, by=gen_zone) == 0" - expression: p <= at(spill * zone_cap, by=gen_zone, into=generator) + expression: p <= at(spill * zone_cap, by=gen_zone, produce=generator) ``` ```math @@ -546,8 +546,8 @@ division, both unary signs, a sign beside a sign, floats with and without an exp arithmetic: dims: [snapshot] expression: >- - sum(p / 2 + -cost - -1e-5 * p + 2.5e-7 * cost + 0.5 * p, over=generator) - >= -sum(+p, over=generator) * -3 + sum(p / 2 + -cost - -1e-5 * p + 2.5e-7 * cost + 0.5 * p, consume=generator) + >= -sum(+p, consume=generator) * -3 ``` ```math @@ -724,7 +724,7 @@ a plain named expression: its symbol prints where it is used, its body once as a ```yaml spend: - expression: sum(p * cost, over=generator) + expression: sum(p * cost, consume=generator) ``` ```math diff --git a/examples/commitment.yaml b/examples/commitment.yaml index 770ba19e..a645263c 100644 --- a/examples/commitment.yaml +++ b/examples/commitment.yaml @@ -49,7 +49,7 @@ expressions: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load upper: description: a unit that is not running produces nothing dims: [snapshot, generator] diff --git a/examples/dispatch.yaml b/examples/dispatch.yaml index 4d0bcef8..ba638648 100644 --- a/examples/dispatch.yaml +++ b/examples/dispatch.yaml @@ -23,7 +23,7 @@ variables: constraints: power_balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize diff --git a/examples/operators/at_columns.yaml b/examples/operators/at_columns.yaml index 46b34666..caafe11e 100644 --- a/examples/operators/at_columns.yaml +++ b/examples/operators/at_columns.yaml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT description: >- - A read that names its ends — `at(array, by=relation, over=a, into=b)` + A read that names its ends — `at(array, by=relation, consume=a, produce=b)` reads column `a` where a table has two columns over one dimension, here the sending end of a line. @@ -25,6 +25,6 @@ variables: constraints: sending_cap: dims: [line] - expression: f <= at(cap, by=ends, over=bus0, into=line) + expression: f <= at(cap, by=ends, consume=bus0, produce=line) objective: { sense: minimize, expression: sum(f) } diff --git a/examples/operators/sum.yaml b/examples/operators/sum.yaml index 347d8b9f..c5aea255 100644 --- a/examples/operators/sum.yaml +++ b/examples/operators/sum.yaml @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -description: The plain reduction — `sum(array, over=dim)` collapses one dimension. +description: The plain reduction — `sum(array, consume=dim)` collapses one dimension. dimensions: snapshot: { dtype: int } @@ -19,6 +19,6 @@ variables: constraints: fleet_total: dims: [snapshot] - expression: sum(p, over=generator) <= limit + expression: sum(p, consume=generator) <= limit objective: { sense: minimize, expression: sum(p) } diff --git a/examples/operators/sum_by_column_lists.yaml b/examples/operators/sum_by_column_lists.yaml index 9efb481e..6ba6e9e1 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 walk with several columns at each end — `sum(array, by=relation, consume=[a, …], produce=[b, …])` consumes both key columns at once and lands on the product of both value columns in one join. @@ -27,6 +27,6 @@ variables: constraints: slot_cap: dims: [bus, technology] - expression: sum(p, by=slot_of, over=[generator, period], into=[bus, technology]) <= cap + expression: sum(p, by=slot_of, consume=[generator, period], produce=[bus, technology]) <= cap objective: { sense: minimize, expression: sum(p) } diff --git a/examples/operators/sum_by_columns.yaml b/examples/operators/sum_by_columns.yaml index e1bc3710..54449b2c 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 walk that names its ends — `sum(array, by=relation, consume=a, produce=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. @@ -26,6 +26,6 @@ variables: constraints: zone_balance: dims: [zone, period] - expression: sum(p, by=zone_of, over=generator, into=zone) >= demand + expression: sum(p, by=zone_of, consume=generator, produce=zone) >= demand objective: { sense: minimize, expression: sum(p) } diff --git a/examples/piecewise.yaml b/examples/piecewise.yaml index 60953048..692c3416 100644 --- a/examples/piecewise.yaml +++ b/examples/piecewise.yaml @@ -58,7 +58,7 @@ piecewise: constraints: balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize diff --git a/examples/piecewise_lp.yaml b/examples/piecewise_lp.yaml index ff59a8c9..e37af564 100644 --- a/examples/piecewise_lp.yaml +++ b/examples/piecewise_lp.yaml @@ -63,7 +63,7 @@ piecewise: constraints: balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize diff --git a/examples/ports/transport_pwl.yaml b/examples/ports/transport_pwl.yaml index febb2e56..8a12a627 100644 --- a/examples/ports/transport_pwl.yaml +++ b/examples/ports/transport_pwl.yaml @@ -71,10 +71,10 @@ piecewise: constraints: within_capacity: dims: [plant] - expression: sum(shipment, over=market) <= capacity + expression: sum(shipment, consume=market) <= capacity meet_demand: dims: [market] - expression: sum(shipment, over=plant) >= demand + expression: sum(shipment, consume=plant) >= demand objective: sense: minimize diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index 117fdf5e..0789c07e 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -671,36 +671,36 @@ expressions: the charge left in weighted storage at the horizon's end; the initial charge it is compared against is folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, over=snapshot), over=store) + sum(sum(Generator_p * snapshot_weightings_generators * Generator_primary_energy_weight, consume=snapshot), consume=generator) + - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_primary_energy_weight, consume=snapshot), consume=storage_unit) + - sum(sum(Store_e * snapshot_is_last * Store_primary_energy_weight, consume=snapshot), consume=store) operational_limit: description: >- what an `operational_limit` row totals — the weighted energy its generators deliver, plus what its non-cyclic storage draws down; the initial charge it draws from is folded into the row's constant expression: >- - sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, over=snapshot), over=generator) - - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, over=snapshot), over=storage_unit) - - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, over=snapshot), over=store) + sum(sum(Generator_p * snapshot_weightings_generators * Generator_operational_limit_weight, consume=snapshot), consume=generator) + - sum(sum(StorageUnit_state_of_charge * snapshot_is_last * StorageUnit_operational_limit_weight, consume=snapshot), consume=storage_unit) + - sum(sum(Store_e * snapshot_is_last * Store_operational_limit_weight, consume=snapshot), consume=store) transmission_volume_expansion: description: what a `transmission_volume_expansion_limit` row totals — length times the chosen build of the row's branches expression: >- - sum(Line_s_nom_ext * Line_volume_weight, over=line) - + sum(Link_p_nom_ext * Link_volume_weight, over=link) + sum(Line_s_nom_ext * Line_volume_weight, consume=line) + + sum(Link_p_nom_ext * Link_volume_weight, consume=link) transmission_expansion_cost: description: what a `transmission_expansion_cost_limit` row totals — capital cost times the chosen build of the row's branches expression: >- - sum(Line_s_nom_ext * Line_expansion_cost_weight, over=line) - + sum(Link_p_nom_ext * Link_expansion_cost_weight, over=link) + sum(Line_s_nom_ext * Line_expansion_cost_weight, consume=line) + + sum(Link_p_nom_ext * Link_expansion_cost_weight, consume=link) tech_capacity_expansion: description: what a `tech_capacity_expansion_limit` row totals — the chosen build of the row's carrier-and-bus set expression: >- - sum(Generator_p_nom_ext * Generator_tech_capacity_weight, over=generator) - + sum(Link_p_nom_ext * Link_tech_capacity_weight, over=link) - + sum(Line_s_nom_ext * Line_tech_capacity_weight, over=line) - + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, over=storage_unit) - + sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store) + sum(Generator_p_nom_ext * Generator_tech_capacity_weight, consume=generator) + + sum(Link_p_nom_ext * Link_tech_capacity_weight, consume=link) + + sum(Line_s_nom_ext * Line_tech_capacity_weight, consume=line) + + sum(StorageUnit_p_nom_ext * StorageUnit_tech_capacity_weight, consume=storage_unit) + + sum(Store_e_nom_ext * Store_tech_capacity_weight, consume=store) constraints: Generator_fix_p_lower: @@ -752,12 +752,12 @@ constraints: description: "`Generator-e_sum_min` — energy over the horizon is at least its floor; a floor of minus infinity is no row" dims: [generator] where: Generator_e_sum_min - expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) >= Generator_e_sum_min + expression: sum(Generator_p * snapshot_weightings_generators, consume=snapshot) >= Generator_e_sum_min Generator_e_sum_max: description: "`Generator-e_sum_max` — energy over the horizon is at most its budget; a budget of infinity is no row" dims: [generator] where: Generator_e_sum_max - expression: sum(Generator_p * snapshot_weightings_generators, over=snapshot) <= Generator_e_sum_max + expression: sum(Generator_p * snapshot_weightings_generators, consume=snapshot) <= Generator_e_sum_max Link_ext_p_lower: description: "`Link-ext-p-lower` — an extendable link carries at least its minimum of the chosen build, negative for the other way" dims: [snapshot, link] @@ -1041,7 +1041,7 @@ constraints: impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: sum(Line_s * Line_cycle_weight, consume=line) == 0 Generator_p_ramp_limit_up: description: >- `Generator-p-ramp_limit_up` — a generator raises output no faster than diff --git a/examples/pypsa_losses.yaml b/examples/pypsa_losses.yaml index c985a6f2..af3e14a4 100644 --- a/examples/pypsa_losses.yaml +++ b/examples/pypsa_losses.yaml @@ -219,7 +219,7 @@ constraints: impedance-weighted flows sum to nothing, which is what makes the linear power flow physical rather than transport dims: [snapshot, cycle] - expression: sum(Line_s * Line_cycle_weight, over=line) == 0 + expression: sum(Line_s * Line_cycle_weight, consume=line) == 0 Bus_nodal_balance: description: >- `Bus-nodal_balance` — what is generated at a bus, plus what the links and diff --git a/examples/pypsa_stochastic.yaml b/examples/pypsa_stochastic.yaml index 8a792ad9..2402ea32 100644 --- a/examples/pypsa_stochastic.yaml +++ b/examples/pypsa_stochastic.yaml @@ -149,8 +149,8 @@ expressions: scenario_opex: description: what a future costs to run — the operating terms, before their weight expression: >- - sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, over=generator), over=snapshot) - + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, over=link), over=snapshot) + sum(sum(Generator_p * Generator_marginal_cost * snapshot_weightings_objective, consume=generator), consume=snapshot) + + sum(sum(Link_p * Link_marginal_cost * snapshot_weightings_objective, consume=link), consume=snapshot) constraints: Generator_fix_p_lower: @@ -209,12 +209,12 @@ constraints: CVaR_def: description: "`CVaR-def` — the tail's average is at least where it starts plus the expected excess over the tail's probability" dims: [] - expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, over=scenario) <= CVaR + expression: CVaR_theta + CVaR_inv_tail * sum(scenario_weight * CVaR_a, consume=scenario) <= CVaR objective: sense: minimize description: capacity once, operation in expectation, and a share of it at the tail expression: >- sum(Generator_p_nom_ext * Generator_capital_cost) - + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, over=scenario) + + (1 - CVaR_omega) * sum(scenario_weight * scenario_opex, consume=scenario) + CVaR_omega * CVaR diff --git a/examples/sos.yaml b/examples/sos.yaml index 94a2978e..3c0c28cc 100644 --- a/examples/sos.yaml +++ b/examples/sos.yaml @@ -59,7 +59,7 @@ piecewise: constraints: balance: dims: [snapshot] - expression: sum(dispatch, over=generator) == load + expression: sum(dispatch, consume=generator) == load objective: sense: minimize diff --git a/schema/math-spec.schema.json b/schema/math-spec.schema.json index a940d575..ff1245d6 100644 --- a/schema/math-spec.schema.json +++ b/schema/math-spec.schema.json @@ -112,7 +112,7 @@ "anyOf": [ { "additionalProperties": false, - "description": "A named quantity: one arithmetic expression, referenced by the math or read back after a solve.\n\nWritten in YAML as a bare string, or as a mapping once it carries a\n``description:`` \u2014 and serialised back to whichever form it was written in,\nso a round trip through :meth:`Spec.to_yaml` reproduces the file::\n\n expressions:\n total_generation: sum(p, over=generator)\n emissions:\n expression: sum(p * rate, over=generator)\n description: CO2 released, the quantity the cap bounds\n\nA quantity whose value varies by region is written as ``cases:`` over a\ndeclared ``dims:``, with an ``otherwise:`` for the rest \u2014 see the\nlanguage reference.", + "description": "A named quantity: one arithmetic expression, referenced by the math or read back after a solve.\n\nWritten in YAML as a bare string, or as a mapping once it carries a\n``description:`` \u2014 and serialised back to whichever form it was written in,\nso a round trip through :meth:`Spec.to_yaml` reproduces the file::\n\n expressions:\n total_generation: sum(p, consume=generator)\n emissions:\n expression: sum(p * rate, consume=generator)\n description: CO2 released, the quantity the cap bounds\n\nA quantity whose value varies by region is written as ``cases:`` over a\ndeclared ``dims:``, with an ``otherwise:`` for the rest \u2014 see the\nlanguage reference.", "properties": { "cases": { "additionalProperties": { @@ -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 ``value:`` column is a\nfunction of it. A relation with no ``value:`` 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, value: bus}\n gen_bt: {key: [generator], value: [bus, technology]}\n zone_of: {key: [generator, period], value: zone}\n ends: {key: line, value: {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 ``value:`` column is a\nfunction of it. A relation with no ``value:`` 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, value: bus}\n gen_bt: {key: [generator], value: [bus, technology]}\n zone_of: {key: [generator, period], value: zone}\n ends: {key: line, value: {bus0: bus, bus1: bus}}\n connection: {key: [generator, bus]}\n\nAn operator walks the table in the direction the call names\n(``consume=``, ``produce=``), 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 132a5182..53c5c7b5 100644 --- a/src/math_spec/_expression_parser.py +++ b/src/math_spec/_expression_parser.py @@ -91,7 +91,7 @@ class DualNode: class DimensionNode: """A resolved reference to a declared dimension. - Only legal in operator kwarg *values* (``sum(x, over=generator)``), never as + Only legal in operator kwarg *values* (``sum(x, consume=generator)``), never as a value in arithmetic — a dimension is a coordinate space, not data. """ @@ -509,7 +509,7 @@ def _named_rewrite(text: str, loc: int) -> str | None: return f"'{rest[0]}' is not a constraint sense — the senses are <=, >= and ==. Write the bound inclusive." if rest.startswith('='): return ( - "'=' on its own is how a kwarg is written inside a call, like sum(x, over=d). " + "'=' on its own is how a kwarg is written inside a call, like sum(x, consume=d). " 'Equality between two sides is written ==.' ) if rest.startswith('^'): diff --git a/src/math_spec/degree.py b/src/math_spec/degree.py index bff098d5..92d4faa5 100644 --- a/src/math_spec/degree.py +++ b/src/math_spec/degree.py @@ -12,7 +12,7 @@ (``ExpressionDeclaration.in_math``) is held to no degree. A degree-2 product has a second rule: **at most one factor may be a sum of -terms**. ``sum(x, over=i) * sum(y, over=j)`` is a cross join whose size the +terms**. ``sum(x, consume=i) * sum(y, consume=j)`` is a cross join whose size the file states nowhere. Factors carrying *different dims* are not that: ``x[i] * y[j]`` broadcasts. @@ -165,8 +165,8 @@ def _check_single_term_factor(node: BinaryOperatorNode, where: str) -> None: f'{where}both factors of this product are sums of more than one term, so it is an outer ' f'product — every term of one against every term of the other, and nothing in the file ' f'says how many that is.\n' - f'Multiply *before* reducing (``sum(x * y, over=d)`` rather than ' - f'``sum(x, over=d) * sum(y, over=d)``).' + f'Multiply *before* reducing (``sum(x * y, consume=d)`` rather than ' + f'``sum(x, consume=d) * sum(y, consume=d)``).' ) diff --git a/src/math_spec/dimensions.py b/src/math_spec/dimensions.py index dbb5e8a8..baac2d1f 100644 --- a/src/math_spec/dimensions.py +++ b/src/math_spec/dimensions.py @@ -136,20 +136,20 @@ 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.""" by = node.kwargs.get('by') - if by is None and 'over' not in node.kwargs: + if by is None and 'consume' not in node.kwargs: if not inner: raise DimensionError( - f'{context}: sum() with no over= or by= sums every dim the operand ' + f'{context}: sum() with no consume= or by= sums every dim the operand ' f'carries, and this one carries none — the expression is already a ' f'scalar. Drop the sum.' ) return frozenset() if by is None: - consumed = node.kwargs['over'] + consumed = node.kwargs['consume'] assert isinstance(consumed, DimensionNode) if consumed.name not in inner: raise DimensionError( - _not_carried(context, f'sum(over={consumed.name})', inner, 'drop the sum, or fix the dim') + _not_carried(context, f'sum(consume={consumed.name})', inner, 'drop the sum, or fix the dim') ) return inner - {consumed.name} diff --git a/src/math_spec/lowering.py b/src/math_spec/lowering.py index a78fab20..85cecea3 100644 --- a/src/math_spec/lowering.py +++ b/src/math_spec/lowering.py @@ -259,7 +259,7 @@ def _cases(self, node: CasesNode) -> program.Cases: return program.Cases(tuple(regions)) def sum(self, node: FunctionCallNode) -> program.ExpressionNode: - """``sum(x)``, ``sum(x, over=d)`` or ``sum(x, by=relation)``. + """``sum(x)``, ``sum(x, consume=d)`` or ``sum(x, by=relation)``. Two program nodes under one surface verb: reducing a dim away and reducing it *into* another are different relational shapes, so ``by=`` decides which @@ -267,11 +267,11 @@ def sum(self, node: FunctionCallNode) -> program.ExpressionNode: """ by_node = node.kwargs.get('by') operand = self.expr(node.args[0]) - if by_node is None and 'over' not in node.kwargs: + if by_node is None and 'consume' not in node.kwargs: return program.Sum(operand, tuple(sorted(dims_of(node.args[0], self.schema, self.context)))) if by_node is None: - consumed = node.kwargs['over'] - assert isinstance(consumed, DimensionNode), 'resolution refuses a over= that is not a dimension' + consumed = node.kwargs['consume'] + assert isinstance(consumed, DimensionNode), 'resolution refuses a consume= 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, walks=by_node.walks) diff --git a/src/math_spec/model.py b/src/math_spec/model.py index 135b3457..c52227b7 100644 --- a/src/math_spec/model.py +++ b/src/math_spec/model.py @@ -183,7 +183,7 @@ class RelationBlock(_StrictBlock): connection: {key: [generator, bus]} An operator walks the table in the direction the call names - (``over=``, ``into=``), joining on the other key columns; the + (``consume=``, ``produce=``), 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. """ @@ -392,9 +392,9 @@ class ExpressionBlock(_StrictBlock): so a round trip through :meth:`Spec.to_yaml` reproduces the file:: expressions: - total_generation: sum(p, over=generator) + total_generation: sum(p, consume=generator) emissions: - expression: sum(p * rate, over=generator) + expression: sum(p * rate, consume=generator) description: CO2 released, the quantity the cap bounds A quantity whose value varies by region is written as ``cases:`` over a diff --git a/src/math_spec/operators.py b/src/math_spec/operators.py index 5de809fe..4bfce196 100644 --- a/src/math_spec/operators.py +++ b/src/math_spec/operators.py @@ -23,7 +23,7 @@ class Builtin: Keyword arguments come in four kinds, and the kind decides what resolution turns the value into: ``dimension_kwargs`` name a dimension - (``sum(x, over=generator)``); ``relation_kwargs`` name a relation, which + (``sum(x, consume=generator)``); ``relation_kwargs`` name a relation, which carries its own dimensions, so it needs no sibling kwarg; ``edge_kwargs`` take a closed keyword or a number; ``required_value_kwargs`` are ordinary values that must be present — a @@ -38,12 +38,12 @@ class Builtin: usage: str 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. + #: Kwargs naming a column of the relation ``by=`` names — ``consume=`` and + #: ``produce=`` — which resolution folds into the relation's walk. 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. + #: ``by=`` names one. ``sum(x, consume=generator)`` reduces the dimension + #: away; ``sum(x, by=l, consume=c)`` names the column the walk consumes. #: One meaning — what leaves the frame — read in the namespace ``by=`` #: decides. dimension_or_role_kwargs: tuple[str, ...] = () @@ -96,17 +96,17 @@ def kind_of( #: ``within=`` names the columns whose values that group is read from. BUILTINS: dict[str, Builtin] = { 'sum': Builtin( - 'sum(), sum(, over=) or sum(, by=[, over=, into=])', + 'sum(), sum(, consume=) or sum(, by=[, consume=, produce=])', relation_kwargs=('by',), - role_kwargs=('into',), - dimension_or_role_kwargs=('over',), - optional_kwargs=('by', 'over', 'into'), + role_kwargs=('produce',), + dimension_or_role_kwargs=('consume',), + optional_kwargs=('by', 'consume', 'produce'), ), 'at': Builtin( - 'at(, by=[, over=, into=])', + 'at(, by=[, consume=, produce=])', relation_kwargs=('by',), - role_kwargs=('over', 'into'), - optional_kwargs=('over', 'into'), + role_kwargs=('consume', 'produce'), + optional_kwargs=('consume', 'produce'), ), 'sum_back': Builtin( "sum_back(, along=, window=[, edge='wrap'][, by=[, within=]])", @@ -154,7 +154,7 @@ def call_shape_error(name: str, positional: int, kwargs: Iterable[str]) -> str | alternatives = ' or '.join(f'{k}=' for k in builtin.at_most_one_of) return ( f'{name}() takes at most one of {alternatives} — a relation carries ' - f'its own dimensions, so by= leaves over= nothing to add.\n' + f'its own dimensions, so by= leaves consume= nothing to add.\n' f'Write: {builtin.usage}' ) optional = {*builtin.edge_kwargs, *builtin.at_most_one_of, *builtin.optional_kwargs} diff --git a/src/math_spec/piecewise.py b/src/math_spec/piecewise.py index 1df23845..383535c5 100644 --- a/src/math_spec/piecewise.py +++ b/src/math_spec/piecewise.py @@ -190,19 +190,19 @@ def _weights(self) -> None: ) gated = self._gate_rows() for suffix, where, rhs in gated: - self._constraint(self.convexity + suffix, list(self.frame), f'sum({self.lam}, over={d}) == {rhs}', where) + self._constraint(self.convexity + suffix, list(self.frame), f'sum({self.lam}, consume={d}) == {rhs}', where) for cname, link in zip(self.links, self.pw.links, strict=True): self._constraint( cname, list(self.frame), - f'({link.expression}) {link.sign} sum({self.lam} * {link.values}, over={d})', + f'({link.expression}) {link.sign} sum({self.lam} * {link.values}, consume={d})', ) if self.pw.method == 'sos2': self.raw.setdefault('sos', {})[self.name] = {'variable': self.lam, 'over': d, 'type': 2} elif self.pw.method == 'adjacency': self._weight(self.seg, domain='binary', bounds={}) for suffix, where, rhs in gated: - self._constraint(self.pick + suffix, list(self.frame), f'sum({self.seg}, over={d}) == {rhs}', where) + self._constraint(self.pick + suffix, list(self.frame), f'sum({self.seg}, consume={d}) == {rhs}', where) self._constraint( self.adjacency, [*self.frame, d], diff --git a/src/math_spec/resolution.py b/src/math_spec/resolution.py index 9ab84d90..83f9ad94 100644 --- a/src/math_spec/resolution.py +++ b/src/math_spec/resolution.py @@ -435,7 +435,7 @@ def _name(self, node: NameNode, *, amount: bool) -> ArithmeticNode: self.errors.append( f"{self.context}: '{node.name}' is a dimension, and a dimension is " f'not a value in an expression. Dimensions appear in ' - f"'dims:', in operator arguments (sum(x, over={node.name})), " + f"'dims:', in operator arguments (sum(x, consume={node.name})), " f'and in where-comparisons — to use its coordinates as data, ' f'declare a parameter over it.' ) @@ -575,10 +575,10 @@ def _relation_ref( roles: Mapping[str, ArithmeticNode], over: ArithmeticNode | None, ) -> ArithmeticNode: - """An operator's ``by=``, with the ``over=`` and ``into=`` that say how each relation is walked. + """An operator's ``by=``, with the ``consume=`` and ``produce=`` that say how each relation is walked. A relation carries its own dimensions, so the call names columns rather - than dims: ``over=`` the column consumed, ``into=`` the column produced, + than dims: ``consume=`` the column consumed, ``produce=`` the column produced, every other key column joined on — a value column not walked is not read, and a bare relation's columns are all key. Where the declaration leaves one choice @@ -611,7 +611,7 @@ def _relation_ref( over_dim = over.name if isinstance(over, NameNode | DimensionNode) else None walks = [self._partition_walk(n, operator, over_dim, named.get('within')) for n in names] else: - walks = [self._walk(n, operator, named.get('over'), named.get('into')) for n in names] + walks = [self._walk(n, operator, named.get('consume'), named.get('produce')) for n in names] if any(w is None for w in walks): return value resolved = [w for w in walks if w is not None] @@ -648,7 +648,7 @@ def coarse_of(w: Walk) -> tuple[str, ...]: return RelationNode(names, dimensions=fine_of(resolved[0]), into=coarse, walks=tuple(resolved)) 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.""" + """``consume=`` or ``produce=`` as the column names it must be — one bare name, or a bracketed list of them.""" if isinstance(value, NameNode): return (value.name,) if isinstance(value, NameListNode): @@ -678,29 +678,30 @@ def _walk( shape = ns.shape_of(name) call = f'{operator}(by={name})' if not ( - self._known_roles(name, call, from_roles, 'over') and self._known_roles(name, call, into_roles, 'into') + self._known_roles(name, call, from_roles, 'consume') + and self._known_roles(name, call, into_roles, 'produce') ): return None forward = operator == 'sum' if from_roles is None: side = shape.key if forward else shape.values - default = self._default_role(name, call, 'over', side, 'key' if forward else 'value') + default = self._default_role(name, call, 'consume', side, 'key' if forward else 'value') if default is None: return None from_roles = (default,) if into_roles is None: side = shape.values if forward else shape.key - default = self._default_role(name, call, 'into', side, 'value' if forward else 'key') + default = self._default_role(name, call, 'produce', side, 'value' if forward else 'key') if default is None: return None into_roles = (default,) 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}: consume= and produce= both name {both}, and a walk goes between two sets of columns.' ) return None - for kwarg, roles in (('over', from_roles), ('into', into_roles)): + for kwarg, roles in (('consume', from_roles), ('produce', into_roles)): dims = [shape.dim(r) for r in roles] if shared := sorted({d for d in dims if dims.count(d) > 1}): self.errors.append( @@ -722,7 +723,7 @@ def _walk( self.errors.append( f'{context}: {call}: this sum walks to 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'at(..., by={name}, consume={list(from_roles)}, produce={list(into_roles)}), or sum toward ' f'a value column.' ) return None diff --git a/src/math_spec/typesetting/walk.py b/src/math_spec/typesetting/walk.py index 936309d5..1615bded 100644 --- a/src/math_spec/typesetting/walk.py +++ b/src/math_spec/typesetting/walk.py @@ -465,7 +465,7 @@ def _call(self, node: FunctionCallNode, ctx: _Context) -> tuple[str, int]: 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"))}' ) - elif (consumed := node.kwargs.get('over')) is not None: + elif (consumed := node.kwargs.get('consume')) is not None: assert isinstance(consumed, DimensionNode) dummy, inner = ctx.reducing(consumed.name) domain = self._membership(consumed.name, dummy) diff --git a/src/math_spec/validation.py b/src/math_spec/validation.py index 431d82f2..20f1c6c7 100644 --- a/src/math_spec/validation.py +++ b/src/math_spec/validation.py @@ -106,7 +106,7 @@ def validate_expressions(schema: Spec) -> Resolved: - where strings parse *and* resolve — an unknown name there is an error, not a silently-empty mask; - macro formals may shadow model names but not a declared dimension, since - ``over=snapshot`` under a formal ``snapshot`` cannot say which it means; + ``consume=snapshot`` under a formal ``snapshot`` cannot say which it means; - every dim rule (``dimensions.check_schema``), once names resolve. Returns: diff --git a/tests/fixtures.py b/tests/fixtures.py index abda891d..4d989cf1 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -34,7 +34,7 @@ 'load': {'dims': ['snapshot']}, }, 'variables': {'p': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0, 'upper': 'p_max'}}}, - 'constraints': {'balance': {'dims': ['snapshot'], 'expression': 'sum(p, over=generator) == load'}}, + 'constraints': {'balance': {'dims': ['snapshot'], 'expression': 'sum(p, consume=generator) == load'}}, 'objective': {'sense': 'minimize', 'expression': 'sum(p * cost)'}, } diff --git a/tests/test_boundedness.py b/tests/test_boundedness.py index db36028f..c38fe02d 100644 --- a/tests/test_boundedness.py +++ b/tests/test_boundedness.py @@ -20,7 +20,7 @@ BASE = override( SMALL_MODEL, variables={'v': {'dims': ['g']}, 'w': {'dims': ['g']}}, - objective={'sense': 'minimize', 'expression': 'sum(v, over=g)'}, + objective={'sense': 'minimize', 'expression': 'sum(v, consume=g)'}, ) @@ -36,21 +36,23 @@ def _notes(**patch) -> list[str]: ('patch', 'side'), [ pytest.param({}, 'lower', id='minimize-a-positive-term-runs-down'), - pytest.param({'objective.expression': '-sum(v, over=g)'}, 'upper', id='minimize-a-negated-term-runs-up'), + pytest.param({'objective.expression': '-sum(v, consume=g)'}, 'upper', id='minimize-a-negated-term-runs-up'), pytest.param({'objective.sense': 'maximize'}, 'upper', id='maximize-a-positive-term-runs-up'), - pytest.param({'objective.expression': 'sum(c * w - v, over=g)'}, 'upper', id='the-right-of-a-minus-is-negated'), pytest.param( - {'objective.expression': 'sum(2 * v, over=g)'}, 'lower', id='a-literal-coefficient-keeps-the-sign' + {'objective.expression': 'sum(c * w - v, consume=g)'}, 'upper', id='the-right-of-a-minus-is-negated' ), - pytest.param({'objective.expression': 'sum(-3 * v, over=g)'}, 'upper', id='a-negative-literal-flips-it'), - pytest.param({'objective.expression': 'sum(v / 2, over=g)'}, 'lower', id='a-literal-divisor-keeps-it'), pytest.param( - {'objective.expression': 'sum(shift(v, along=g, offset=1), over=g)'}, + {'objective.expression': 'sum(2 * v, consume=g)'}, 'lower', id='a-literal-coefficient-keeps-the-sign' + ), + pytest.param({'objective.expression': 'sum(-3 * v, consume=g)'}, 'upper', id='a-negative-literal-flips-it'), + pytest.param({'objective.expression': 'sum(v / 2, consume=g)'}, 'lower', id='a-literal-divisor-keeps-it'), + pytest.param( + {'objective.expression': 'sum(shift(v, along=g, offset=1), consume=g)'}, 'lower', id='an-operator-argument-keeps-it', ), pytest.param( - {'objective.expression': '-sum(v, over=g)', 'variables.v.bounds': {'lower': 0}}, + {'objective.expression': '-sum(v, consume=g)', 'variables.v.bounds': {'lower': 0}}, 'upper', id='a-bound-on-the-side-it-runs-away-from-is-beside-the-point', ), @@ -72,13 +74,13 @@ def test_a_variable_the_objective_drives_unopposed_is_named_with_its_side(patch, pytest.param({'variables.v.domain': 'binary'}, id='a-binary-is-bounded-by-its-domain'), pytest.param({'constraints': {'k': {'dims': ['g'], 'expression': 'v >= c'}}}, id='named-by-a-constraint'), pytest.param({'sos': {'s': {'variable': 'v', 'over': 'g', 'type': 1}}}, id='carried-by-a-set'), - pytest.param({'objective.expression': 'sum(c * v, over=g)'}, id='a-parameter-coefficient-may-be-zero'), - pytest.param({'objective.expression': 'sum(v - v, over=g)'}, id='both-signs-may-cancel'), - pytest.param({'objective.expression': 'sum(v * v, over=g)'}, id='a-degree-two-term-carries-no-sign'), - pytest.param({'objective.expression': 'sum(0 * v, over=g)'}, id='a-zero-coefficient-is-not-a-term'), + pytest.param({'objective.expression': 'sum(c * v, consume=g)'}, id='a-parameter-coefficient-may-be-zero'), + pytest.param({'objective.expression': 'sum(v - v, consume=g)'}, id='both-signs-may-cancel'), + pytest.param({'objective.expression': 'sum(v * v, consume=g)'}, id='a-degree-two-term-carries-no-sign'), + pytest.param({'objective.expression': 'sum(0 * v, consume=g)'}, id='a-zero-coefficient-is-not-a-term'), pytest.param({'objective': None}, id='no-objective'), pytest.param( - {'objective.expression': '-sum(v, over=g)', 'variables.v.bounds': {'upper': 10}}, + {'objective.expression': '-sum(v, consume=g)', 'variables.v.bounds': {'upper': 10}}, id='bounded-on-the-improving-side-running-up', ), ], @@ -91,11 +93,11 @@ def test_nothing_is_claimed_where_the_file_does_not_decide_it(patch): #: operator and nothing else. Keyed by name rather than listed, so a fifth #: built-in arrives with a case of its own. THROUGH_EACH_OPERATOR = { - 'sum': {'objective.expression': 'sum(v, over=g)'}, - 'shift': {'objective.expression': 'sum(shift(v, along=g, offset=1), over=g)'}, - 'sum_back': {'objective.expression': 'sum(sum_back(v, along=g, window=2), over=g)'}, + 'sum': {'objective.expression': 'sum(v, consume=g)'}, + 'shift': {'objective.expression': 'sum(shift(v, along=g, offset=1), consume=g)'}, + 'sum_back': {'objective.expression': 'sum(sum_back(v, along=g, window=2), consume=g)'}, # `at` reads onto the relation's source, so the variable it drives is on `h` - 'at': {'variables.u': {'dims': ['h']}, 'objective.expression': 'sum(at(u, by=lk), over=g)'}, + 'at': {'variables.u': {'dims': ['h']}, 'objective.expression': 'sum(at(u, by=lk), consume=g)'}, } #: `dual` is refused in any objective, and boundedness walks the objective — @@ -124,7 +126,7 @@ def test_every_operator_hands_its_sign_to_its_operand(builtin): def test_every_unopposed_variable_is_named(): - advice = _advice(**{'objective.expression': 'sum(v + w, over=g)'}) + advice = _advice(**{'objective.expression': 'sum(v + w, consume=g)'}) assert [(a.kind, a.subject) for a in advice] == [('unbounded', 'v'), ('unbounded', 'w')], ( 'one piece of advice per variable, in objective order' ) diff --git a/tests/test_degree.py b/tests/test_degree.py index 84d6e2c9..f2318f54 100644 --- a/tests/test_degree.py +++ b/tests/test_degree.py @@ -33,7 +33,7 @@ def _ast(text: str): pytest.param('p / c', id='a-parameter-divisor'), pytest.param('c ** 2', id='a-power-over-parameters'), pytest.param('k ** c', id='a-parameter-exponent'), - pytest.param('sum(p * c, over=g)', id='a-reduction-of-affine-terms'), + pytest.param('sum(p * c, consume=g)', id='a-reduction-of-affine-terms'), pytest.param('p + q', id='a-sum-of-variables'), ], ) @@ -53,7 +53,7 @@ def test_an_affine_expression_passes_everywhere(text): pytest.param('p / q', 'the divisor contains variables', id='a-variable-divisor'), pytest.param('p / (c + 1)', 'a divisor must be a single Constant/Parameter factor', id='a-divisor-that-adds'), pytest.param( - 'p / sum(c + k, over=g)', 'a divisor must be a single', id='an-addition-under-a-reduction-divisor' + 'p / sum(c + k, consume=g)', 'a divisor must be a single', id='an-addition-under-a-reduction-divisor' ), ], ) @@ -67,8 +67,8 @@ def test_the_affine_ceiling_refuses_and_names_the_rewrite(text, fragment): 'text', [ pytest.param('p * q', id='one-product'), - pytest.param('sum(p * q, over=g)', id='multiplied-before-reducing'), - pytest.param('sum(p, over=g) * q', id='one-multi-term-factor'), + pytest.param('sum(p * q, consume=g)', id='multiplied-before-reducing'), + pytest.param('sum(p, consume=g) * q', id='one-multi-term-factor'), pytest.param('(p + q) * c * p', id='a-sum-against-one-term'), pytest.param('p * q / c', id='a-quadratic-over-a-parameter'), pytest.param('p * r * c', id='a-broadcast-product-of-disjoint-dims'), @@ -83,7 +83,7 @@ def test_the_objective_takes_degree_two(text): [ pytest.param('p * q * p', 'this product is degree 3', id='a-cubic'), pytest.param('(p * q) * (p * q)', 'this product is degree 4', id='a-quartic'), - pytest.param('sum(p, over=g) * sum(q, over=g)', 'outer product', id='two-reductions'), + pytest.param('sum(p, consume=g) * sum(q, consume=g)', 'outer product', id='two-reductions'), pytest.param('(p + q) * (p + q)', 'outer product', id='two-sums-of-variables'), pytest.param('sum_back(p, along=g, window=1) * (p - q)', 'outer product', id='a-window-against-a-difference'), ], @@ -126,7 +126,7 @@ def test_a_dual_carries_no_variable(): [ pytest.param('dual(lim)', True, id='bare'), pytest.param('dual(lim) * c', True, id='beside-affine-arithmetic'), - pytest.param('sum(dual(lim), over=g)', True, id='under-a-reduction'), + pytest.param('sum(dual(lim), consume=g)', True, id='under-a-reduction'), pytest.param('p * c', False, id='none'), ], ) diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index a28669a0..c0e389f0 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -91,8 +91,8 @@ def namespace() -> Namespace: ('p * cost', {'snapshot', 'generator'}), ('sum(p)', set()), ('sum(p * cost)', set()), - ('sum(p, over=generator)', {'snapshot'}), - ('sum(p * cost, over=generator)', {'snapshot'}), + ('sum(p, consume=generator)', {'snapshot'}), + ('sum(p * cost, consume=generator)', {'snapshot'}), ('sum(p, by=gen_bus)', {'snapshot', 'bus'}), ("shift(p, along=snapshot, offset=1, edge='wrap')", {'snapshot', 'generator'}), ("shift(p, along=snapshot, offset=spinup, edge='wrap')", {'snapshot', 'generator'}), @@ -114,17 +114,17 @@ def namespace() -> Namespace: id='a-produced-dim-the-operand-already-carries-is-joined-on-so-the-walk-is-a-masked-sum', ), pytest.param( - 'sum(p, by=gen_zone, over=generator)', + 'sum(p, by=gen_zone, consume=generator)', {'snapshot', 'zone'}, id='a-two-key-relation-consumes-the-key-it-walks-and-keeps-the-other', ), pytest.param( - 'sum(p, by=gen_zone, over=snapshot)', + 'sum(p, by=gen_zone, consume=snapshot)', {'generator', 'zone'}, id='the-same-table-walked-along-its-other-key', ), pytest.param( - 'at(zone_load, by=gen_zone, into=generator)', + 'at(zone_load, by=gen_zone, produce=generator)', {'snapshot', 'generator'}, id='its-pullback-keeps-the-joined-key-too', ), @@ -149,32 +149,32 @@ def namespace() -> Namespace: id='a-partition-grouped-by-two-columns-over-one-dimension-lands-nothing', ), pytest.param( - 'sum(p, by=gen_bus, over=generator)', {'snapshot', 'bus'}, id='the-dot-is-legal-on-a-one-key-relation' + 'sum(p, by=gen_bus, consume=generator)', {'snapshot', 'bus'}, id='the-dot-is-legal-on-a-one-key-relation' ), pytest.param( - 'sum(p, by=gen_bz, into=[bus, zone])', + 'sum(p, by=gen_bz, produce=[bus, zone])', {'snapshot', 'bus', 'zone'}, id='a-to-list-lands-on-a-product-from-one-table', ), pytest.param( - 'at(bz, by=gen_bz, over=[bus, zone])', + 'at(bz, by=gen_bz, consume=[bus, zone])', {'generator'}, id='a-from-list-reads-two-value-columns-at-once', ), pytest.param( - 'sum(p, by=gen_zone, over=[generator, snapshot])', + 'sum(p, by=gen_zone, consume=[generator, snapshot])', {'zone'}, id='a-from-list-consumes-two-key-columns-at-once', ), pytest.param( - 'sum(p, by=gen_bz, into=bus)', + 'sum(p, by=gen_bz, produce=bus)', {'snapshot', 'bus'}, id='a-value-column-not-walked-is-not-read', ), pytest.param( - 'sum(p, by=gen_bz, over=generator, into=bus)', + 'sum(p, by=gen_bz, consume=generator, produce=bus)', {'snapshot', 'bus'}, - id='by-and-over-compose', + id='by-and-consume-compose', ), pytest.param('sum(p, by=rep_of)', {'snapshot', 'generator'}, id='a-map-into-its-own-dimension-keeps-the-frame'), pytest.param('at(p, by=rep_of)', {'snapshot', 'generator'}, id='and-so-does-its-pullback'), @@ -211,8 +211,8 @@ def test_a_bare_name_reaches_the_variable_a_dual_the_same_named_constraint(): ('expr', 'match'), [ pytest.param( - 'sum(p, over=bus)', - r'sum\(over=bus\) but the expression has dims', + 'sum(p, consume=bus)', + r'sum\(consume=bus\) but the expression has dims', id='sum-consuming-an-absent-dim-is-an-error-not-a-noop', ), pytest.param( @@ -266,12 +266,12 @@ def test_a_bare_name_reaches_the_variable_a_dual_the_same_named_constraint(): id='a-named-offset-is-read-where-the-expression-has-a-coordinate', ), pytest.param( - 'sum(cost, by=gen_zone, over=generator)', + 'sum(cost, by=gen_zone, consume=generator)', r"sum\(by=gen_zone\) joins on \['snapshot'\]", id='a-grouped-sum-needs-the-keys-it-joins-on', ), pytest.param( - 'at(zone_cap, by=gen_zone, into=generator)', + 'at(zone_cap, by=gen_zone, produce=generator)', r"at\(by=gen_zone\) joins on \['snapshot'\]", id='a-pullback-needs-the-keys-it-joins-on', ), diff --git a/tests/test_expansion.py b/tests/test_expansion.py index b79bc6e6..e4f5668c 100644 --- a/tests/test_expansion.py +++ b/tests/test_expansion.py @@ -18,7 +18,7 @@ WEIGHTED_SUM = { 'args': ['array', 'weights'], 'kwargs': ['over'], - 'template': 'sum(array * weights, over=over)', + 'template': 'sum(array * weights, consume=over)', } schema = partial(schema_of, DISPATCH_MODEL) @@ -38,29 +38,29 @@ def _bodies(node): pytest.param( {'gen_cost': 'p * cost'}, {}, - 'sum(gen_cost, over=generator)', - 'sum(p * cost, over=generator)', + 'sum(gen_cost, consume=generator)', + 'sum(p * cost, consume=generator)', id='a-named-expression-splices', ), pytest.param( - {'gen_cost': 'p * cost', 'total_cost': 'sum(gen_cost, over=generator)'}, + {'gen_cost': 'p * cost', 'total_cost': 'sum(gen_cost, consume=generator)'}, {}, 'total_cost + 1', - 'sum(p * cost, over=generator) + 1', + 'sum(p * cost, consume=generator) + 1', id='named-expressions-nest', ), pytest.param( - {'total_gen': 'sum(p, over=generator)'}, + {'total_gen': 'sum(p, consume=generator)'}, {}, 'total_gen == load', - 'sum(p, over=generator) == load', + 'sum(p, consume=generator) == load', id='a-comparison-at-the-top', ), pytest.param( {}, {'weighted_sum': WEIGHTED_SUM}, 'weighted_sum(p, cost, over=generator)', - 'sum(p * cost, over=generator)', + 'sum(p * cost, consume=generator)', id='a-macro-expands', ), pytest.param( @@ -80,11 +80,11 @@ def _bodies(node): pytest.param( {}, { - 'total': {'args': ['x'], 'template': 'sum(x, over=generator)'}, + 'total': {'args': ['x'], 'template': 'sum(x, consume=generator)'}, 'total_cost': {'template': 'total(p * cost)'}, }, 'total_cost()', - 'sum(p * cost, over=generator)', + 'sum(p * cost, consume=generator)', id='a-macro-body-may-call-a-macro', ), pytest.param( @@ -112,7 +112,7 @@ def test_a_call_expands_to_core_ast(expressions, macros, call, want): def test_a_named_expression_arrives_under_the_node_carrying_its_name(): - expanded = parse_and_expand('sum(gen_cost, over=generator)', schema(expressions={'gen_cost': 'p * cost'}), 'e') + expanded = parse_and_expand('sum(gen_cost, consume=generator)', schema(expressions={'gen_cost': 'p * cost'}), 'e') assert expanded.args[0] == DefinitionNode('gen_cost', parse_expression('p * cost')), ( 'the body is inlined and the name kept, for the typesetter to define it once' ) @@ -125,7 +125,7 @@ def test_a_named_expression_arrives_under_the_node_carrying_its_name(): pytest.param({'bad': 'p == load'}, 'must not contain a comparison', id='a-comparison'), pytest.param({'load': 'p * cost'}, 'collides with the parameter of the same name', id='a-parameter-collision'), pytest.param( - {'broken': 'sum(nope, over=generator)'}, + {'broken': 'sum(nope, consume=generator)'}, "Named expression 'broken'", id='a-typo-in-a-named-expression', ), diff --git a/tests/test_lowering.py b/tests/test_lowering.py index bc89c918..e355dc46 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -79,7 +79,7 @@ 'dimensions': {'g': {}}, 'parameters': {'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['g'], 'bounds': {'lower': 0, 'upper': 1}}}, - 'constraints': {'c': {'dims': [], 'expression': 'sum(p, over=g) >= 1'}}, + 'constraints': {'c': {'dims': [], 'expression': 'sum(p, consume=g) >= 1'}}, } #: `lk` and `lk2` as `sum` walks them: key consumed, value produced, nothing joined. @@ -157,7 +157,7 @@ def test_lower_program_structure(dispatch_program): @pytest.mark.parametrize('sense', [pytest.param('minimize', id='minimize'), pytest.param('maximize', id='maximize')]) def test_the_objective_sense_crosses_untranslated(sense: str): """One spelling from the file to the program, in both directions — each sink translates at its own edge.""" - program = to_program(override(TINY, objective={'sense': sense, 'expression': 'sum(p * cost, over=g)'})) + program = to_program(override(TINY, objective={'sense': sense, 'expression': 'sum(p * cost, consume=g)'})) assert program.objective is not None assert program.objective.sense == sense, "the file's own word for the direction, unchanged" @@ -409,7 +409,7 @@ def test_a_power_lowers_to_a_node_of_its_own(dispatch_schema): ('expression', 'expected'), [ pytest.param('sum(q)', Sum(Variable('q'), ('g', 'h')), id='a-bare-sum-consumes-every-dim-the-operand-carries'), - pytest.param('sum(q, over=h)', Sum(Variable('q'), ('h',)), id='an-over-consumes-the-dim-it-names'), + pytest.param('sum(q, consume=h)', Sum(Variable('q'), ('h',)), id='an-over-consumes-the-dim-it-names'), pytest.param( 'sum(p, by=lk)', GroupSum(Variable('p'), walks=(LK_WALK,)), @@ -498,14 +498,14 @@ def test_a_relation_lowers_with_the_walk_each_call_takes(): 'first': {'dims': ['snapshot', 'generator'], 'where': 'position(generator, by=zone_of) == 0'}, }, 'constraints': { - 'zonal': {'dims': ['snapshot', 'zone'], 'expression': 'sum(p, by=zone_of, over=generator) <= 1'}, + 'zonal': {'dims': ['snapshot', 'zone'], 'expression': 'sum(p, by=zone_of, consume=generator) <= 1'}, 'priced': { 'dims': ['snapshot', 'generator'], - 'expression': 'p <= at(price, by=zone_of, into=generator)', + 'expression': 'p <= at(price, by=zone_of, produce=generator)', }, 'history': { 'dims': ['generator', 'zone'], - 'expression': 'sum(p, by=zone_of, over=snapshot) <= 1', + 'expression': 'sum(p, by=zone_of, consume=snapshot) <= 1', }, }, } @@ -674,8 +674,8 @@ def test_expressions_are_the_ones_a_row_is_built_from(): program = to_program( override( TINY, - expressions={'spend': 'sum(cost, over=g)'}, - objective={'sense': 'minimize', 'expression': 'sum(p * cost, over=g)'}, + expressions={'spend': 'sum(cost, consume=g)'}, + objective={'sense': 'minimize', 'expression': 'sum(p * cost, consume=g)'}, ) ) @@ -708,12 +708,12 @@ def test_the_footprint_says_which_position_a_quadratic_stands_in(): actually make — quadratic is bounded "by convexity and again by what it stands beside" — and leave the sink walking the program to recover it. """ - assert _footprint_of('p <= 1', 'sum(p * p, over=g)').quadratic == {'objective'}, 'a quadratic objective alone' - assert _footprint_of('p * p <= 1', 'sum(p, over=g)').quadratic == {'constraint'}, 'a quadratic constraint alone' - assert _footprint_of('p * p <= 1', 'sum(p * p, over=g)').quadratic == {'objective', 'constraint'}, ( + assert _footprint_of('p <= 1', 'sum(p * p, consume=g)').quadratic == {'objective'}, 'a quadratic objective alone' + assert _footprint_of('p * p <= 1', 'sum(p, consume=g)').quadratic == {'constraint'}, 'a quadratic constraint alone' + assert _footprint_of('p * p <= 1', 'sum(p * p, consume=g)').quadratic == {'objective', 'constraint'}, ( 'both positions, each named' ) - assert _footprint_of('p <= 1', 'sum(p, over=g)').quadratic == frozenset(), 'affine throughout is the empty set' + assert _footprint_of('p <= 1', 'sum(p, consume=g)').quadratic == frozenset(), 'affine throughout is the empty set' def test_a_construct_the_file_does_not_use_is_an_empty_set_rather_than_none(): @@ -722,7 +722,7 @@ def test_a_construct_the_file_does_not_use_is_an_empty_set_rather_than_none(): None would make three states out of two and put a null check in front of every read. """ - footprint = _footprint_of('p <= 1', 'sum(p, over=g)') + footprint = _footprint_of('p <= 1', 'sum(p, consume=g)') assert footprint.sos_types == frozenset(), 'a file declaring no sos' assert footprint.quadratic == frozenset(), 'a file with no quadratic anywhere' @@ -743,7 +743,7 @@ def test_the_footprint_is_walked_once_and_held(dispatch_program): def test_a_named_expression_is_not_in_the_footprint(): """It builds no row, so counting it would answer wrongly about what is solved.""" - program = to_program(override(TINY, expressions={'spend': 'sum(p * cost, over=g)'})) + program = to_program(override(TINY, expressions={'spend': 'sum(p * cost, consume=g)'})) assert Parameter not in program.footprint.shapes, "the named expression's parameter reaches no row" assert Parameter in {type(n) for n in walk(program.named_expressions['spend'].expression)}, ( @@ -875,7 +875,7 @@ def test_a_cased_expression_is_readable_by_the_name_the_file_wrote(): ), pytest.param({}, False, id='nothing-reads-it'), pytest.param( - {'expressions.ratio': 'spend / sum(p, over=g)'}, + {'expressions.ratio': 'spend / sum(p, consume=g)'}, False, id='only-an-entry-the-math-never-reads-inlines-it', ), @@ -883,7 +883,7 @@ def test_a_cased_expression_is_readable_by_the_name_the_file_wrote(): ) def test_an_entry_is_in_the_math_where_the_objective_or_a_constraint_inlines_it(patch, in_math): """`in_math` is usage, not shape: one affine body is in the math when a row inlines it, however indirectly, and a reported quantity when none does.""" - program = to_program(override(TINY, expressions={'spend': 'sum(p * cost, over=g)'}, **patch)) + program = to_program(override(TINY, expressions={'spend': 'sum(p * cost, consume=g)'}, **patch)) assert program.named_expressions['spend'].in_math is in_math @@ -892,7 +892,7 @@ def test_an_entry_reached_only_through_another_is_in_the_math_with_it(): program = to_program( override( TINY, - expressions={'spend': 'sum(p * cost, over=g)', 'twice': 'spend * 2'}, + expressions={'spend': 'sum(p * cost, consume=g)', 'twice': 'spend * 2'}, **{'constraints.c.expression': 'twice >= 1'}, ) ) @@ -907,9 +907,9 @@ def test_a_macro_formal_named_like_an_entry_keeps_the_entry_out_of_the_math(): program = to_program( override( TINY, - expressions={'spend': 'sum(p * cost, over=g)'}, + expressions={'spend': 'sum(p * cost, consume=g)'}, macros={'scaled': {'args': ['spend'], 'template': 'spend * 2'}}, - **{'constraints.c.expression': 'scaled(sum(p, over=g)) >= 1'}, + **{'constraints.c.expression': 'scaled(sum(p, consume=g)) >= 1'}, ) ) assert program.named_expressions['spend'].in_math is False, ( @@ -941,8 +941,8 @@ def test_a_lowered_spec_still_pickles_and_lowers_to_the_same_program(): 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, 'parameters': {'load': {'dims': ['t']}, 'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['t', 'g'], 'bounds': {'lower': 0}}}, - 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, over=g) >= load'}}, - 'expressions': {'spend': 'sum(p * cost, over=g)'}, + 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, consume=g) >= load'}}, + 'expressions': {'spend': 'sum(p * cost, consume=g)'}, 'objective': {'sense': 'minimize', 'expression': 'sum(spend)'}, } ) @@ -969,8 +969,8 @@ def test_a_lowered_program_pickles_and_is_the_same_program(): 'dimensions': {'t': {'dtype': 'int'}, 'g': {'dtype': 'str'}}, 'parameters': {'load': {'dims': ['t']}, 'cost': {'dims': ['g']}}, 'variables': {'p': {'dims': ['t', 'g'], 'bounds': {'lower': 0}}}, - 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, over=g) >= load'}}, - 'expressions': {'spend': 'sum(p * cost, over=g)'}, + 'constraints': {'balance': {'dims': ['t'], 'expression': 'sum(p, consume=g) >= load'}}, + 'expressions': {'spend': 'sum(p * cost, consume=g)'}, 'objective': {'sense': 'minimize', 'expression': 'sum(spend)'}, } ) diff --git a/tests/test_parser.py b/tests/test_parser.py index 3748a75f..851e3aef 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -58,8 +58,8 @@ def test_the_grammar_builds_the_program_s_own_node_classes(): pytest.param('a + b', BinaryOperatorNode, {'op': '+'}, id='a-binary-operator'), pytest.param('-x', UnaryOperatorNode, {'op': '-'}, id='a-unary-operator'), pytest.param('p <= p_max', ComparisonNode, {'op': '<='}, id='a-comparison'), - pytest.param('sum(p, over=g) == load', ComparisonNode, {'op': '=='}, id='a-comparison-over-a-call'), - pytest.param('sum(p, over=generator)', FunctionCallNode, {'name': 'sum'}, id='a-call'), + pytest.param('sum(p, consume=g) == load', ComparisonNode, {'op': '=='}, id='a-comparison-over-a-call'), + pytest.param('sum(p, consume=generator)', FunctionCallNode, {'name': 'sum'}, id='a-call'), ], ) def test_an_expression_parses_to_its_node(text, node_type, attrs): @@ -99,21 +99,21 @@ def test_precedence(text, tree): def test_a_call_carries_its_positional_and_keyword_arguments(): - node = parse_expression('sum(p * cost, over=generator)') + node = parse_expression('sum(p * cost, consume=generator)') assert len(node.args) == 1, 'one positional argument; the keyword is not among them' assert isinstance(node.args[0], BinaryOperatorNode), 'the argument is an expression, not just a name' - assert 'over' in node.kwargs + assert 'consume' in node.kwargs def test_a_parsed_node_pickles_and_stays_sealed(): """A node crosses a process, and its keyword arguments still refuse a write on the far side.""" import pickle - node = parse_expression('sum(p, over=snapshot)') + node = parse_expression('sum(p, consume=snapshot)') copy = pickle.loads(pickle.dumps(node)) assert copy == node with pytest.raises(TypeError, match='does not support item assignment'): - operator.setitem(copy.kwargs, 'over', NameNode('generator')) + operator.setitem(copy.kwargs, 'consume', NameNode('generator')) @pytest.mark.parametrize( @@ -123,7 +123,7 @@ def test_a_parsed_node_pickles_and_stays_sealed(): lambda node: setattr(node, 'op', '>='), FrozenInstanceError, 'cannot assign', id='a-comparison-sense' ), pytest.param( - lambda node: operator.setitem(node.left.kwargs, 'over', NameNode('snapshot')), + lambda node: operator.setitem(node.left.kwargs, 'consume', NameNode('snapshot')), TypeError, 'does not support item assignment', id='a-reduction-axis', @@ -135,19 +135,19 @@ def test_a_parsed_expression_cannot_be_rewritten_under_another_pass(rewrite, err The expression nodes were plain dataclasses while every where and program node was frozen (#197): `node.op = '<='` flipped a shared comparison and - `node.kwargs['over'] = ...` re-aimed a reduction, with no error anywhere. + `node.kwargs['consume'] = ...` re-aimed a reduction, with no error anywhere. """ - node = parse_expression('sum(p * cost, over=generator) == load') + node = parse_expression('sum(p * cost, consume=generator) == load') with pytest.raises(error, match=match): rewrite(node) def test_a_call_copies_the_kwargs_it_is_handed(): """A caller's own dict is copied on the way in, so holding it is not a back door either.""" - passed = {'over': NameNode('generator')} + passed = {'consume': NameNode('generator')} built = FunctionCallNode('sum', (NameNode('p'),), passed) - passed['over'] = NameNode('snapshot') - assert built.kwargs == {'over': NameNode('generator')}, 'the dict handed in was copied, not aliased' + passed['consume'] = NameNode('snapshot') + assert built.kwargs == {'consume': NameNode('generator')}, 'the dict handed in was copied, not aliased' assert isinstance(hash(built), int), 'kwargs sits outside the hash, so a call hashes like every other node' @@ -183,8 +183,8 @@ def test_an_exponent_may_be_negated_and_a_negation_stacked(): def test_a_keyword_given_twice_is_refused_not_overwritten(): - with pytest.raises(SchemaError, match='sum\\(over=\\) is given twice'): - parse_expression('sum(p, over=snapshot, over=generator)') + with pytest.raises(SchemaError, match='sum\\(consume=\\) is given twice'): + parse_expression('sum(p, consume=snapshot, consume=generator)') def test_a_list_of_names_is_a_kwarg_value(): @@ -200,7 +200,7 @@ def test_a_list_of_names_is_a_kwarg_value(): pytest.param('sum(p, by=[])', id='no-names-at-all'), pytest.param('sum(p, by=[a b])', id='a-missing-comma'), pytest.param('sum(p, by=[a)', id='an-unclosed-bracket'), - pytest.param('sum([p], over=g)', id='a-positional-argument'), + pytest.param('sum([p], consume=g)', id='a-positional-argument'), pytest.param('p + [c]', id='a-term'), pytest.param('[a, b]', id='the-whole-expression'), ], @@ -371,7 +371,7 @@ def test_an_unrelated_parse_failure_says_nothing_about_positions(): def test_a_string_parses_to_one_shared_tree(): """Drop the memo and this passes on `==` alone — `is` is the claim.""" - text = 'sum(p * cost, over=generator) == load' + text = 'sum(p * cost, consume=generator) == load' assert parse_expression(text) is parse_expression(text), 'the same expression string parses to one tree' assert parse_where('p_max > 0') is parse_where('p_max > 0'), 'and so does the same where string' diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py index 48d71537..f6c498c0 100644 --- a/tests/test_piecewise.py +++ b/tests/test_piecewise.py @@ -67,7 +67,7 @@ objective: sense: minimize - expression: sum(op_cost, over=snapshot) + expression: sum(op_cost, consume=snapshot) """ GATED = override( raw_of(NONCONVEX_YAML), @@ -93,7 +93,7 @@ 'parameters.bp_y.dims': ['generator', 'bp'], 'variables.p.dims': ['snapshot', 'generator'], 'variables.op_cost.dims': ['snapshot', 'generator'], - 'constraints.balance.expression': 'sum(p, over=generator) == load', + 'constraints.balance.expression': 'sum(p, consume=generator) == load', 'objective.expression': 'sum(op_cost)', }, ) @@ -289,7 +289,7 @@ def test_a_link_reading_a_nonlinear_entry_is_refused(): schema_of( NONCONVEX_YAML, **{ - 'expressions': {'ratio': 'op_cost / sum(p, over=snapshot)'}, + 'expressions': {'ratio': 'op_cost / sum(p, consume=snapshot)'}, 'piecewise.cost_curve.links': [['ratio', 'bp_x'], ['op_cost', 'bp_y']], }, ) diff --git a/tests/test_separability.py b/tests/test_separability.py index 27806174..fcb0c8cc 100644 --- a/tests/test_separability.py +++ b/tests/test_separability.py @@ -67,7 +67,7 @@ def test_a_separable_model_reports_the_lookahead_a_window_needs(patch, ahead): @pytest.mark.parametrize( ('patch', 'fragment'), [ - pytest.param(_rows('sum(p, over=h) <= budget', dims=['u']), 'sums over h', id='a-budget-over-the-horizon'), + pytest.param(_rows('sum(p, consume=h) <= budget', dims=['u']), 'sums over h', id='a-budget-over-the-horizon'), pytest.param(_rows("p >= shift(p, along=h, offset=1, edge='wrap')"), 'wraps around h', id='a-cyclic-shift'), ], ) @@ -157,7 +157,7 @@ def test_a_read_through_a_relation_is_undecided_on_the_axis_it_reads(): def test_a_coupling_names_the_change_that_would_lift_it(): - coupled = _verdict(**_rows('sum(p, over=h) <= budget', dims=['u'])).coupled["constraint 'k'"] + coupled = _verdict(**_rows('sum(p, consume=h) <= budget', dims=['u'])).coupled["constraint 'k'"] assert 'sum_back(window=n)' in coupled, 'a horizon total becomes a rolling one' wrapped = _verdict(**_rows("p >= shift(p, along=h, offset=1, edge='wrap')")).coupled["constraint 'k'"] assert 'position(h) == 0' in wrapped, 'a wrap becomes an opening-state seed' @@ -178,7 +178,7 @@ def test_a_sum_over_the_axis_couples_a_constraint_and_leaves_the_objective_alone every other. A verdict treating the two alike would refuse every windowable model there is — and `BASE`'s objective sums over `h` in every case above.""" assert _verdict(**_rows('p >= 0')).windowable, 'the objective sums over h and that is not a coupling' - coupled = _verdict(**_rows('sum(p, over=h) <= budget', dims=['u'])) + coupled = _verdict(**_rows('sum(p, consume=h) <= budget', dims=['u'])) assert not coupled.windowable, 'the same sum in a constraint is one' @@ -237,7 +237,7 @@ def test_every_node_a_program_can_carry_is_judged_without_raising(dimension): def test_a_reduction_over_several_axes_couples_every_one_of_them(): - """`sum(p)` with no `over=` collapses every dimension its operand carries, + """`sum(p)` with no `consume=` collapses every dimension its operand carries, so the verdict for each of them has to say so — a walk that read only the first would call the rest windowable.""" program = ms.to_program({**BASE, 'constraints': {'all': {'dims': [], 'expression': 'sum(p) <= budget'}}}) diff --git a/tests/test_validation.py b/tests/test_validation.py index e452a4dd..480ea49e 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -54,7 +54,7 @@ class TestValidateExpressions: id='a-constraint-without-a-comparison', ), pytest.param( - {'objective': {'expression': 'sum(p, over=g) <= 5'}}, + {'objective': {'expression': 'sum(p, consume=g) <= 5'}}, ('must not contain a comparison',), id='an-objective-with-a-comparison', ), @@ -69,7 +69,7 @@ class TestValidateExpressions: id='a-cubic-constraint', ), pytest.param( - {'objective': {'expression': 'sum(p ** 2, over=g)'}}, + {'objective': {'expression': 'sum(p ** 2, consume=g)'}}, ('The objective', '`**` is not in the language over variables'), id='a-variable-under-a-power', ), @@ -99,7 +99,7 @@ def test_a_bad_declaration_is_refused_at_load(self, patch, fragments): def test_the_objective_and_a_constraint_take_degree_two(self): _schema( constraints={'floor': {'dims': ['g'], 'expression': 'p * p >= 1'}}, - objective={'expression': 'sum(p * p * c, over=g)'}, + objective={'expression': 'sum(p * p * c, consume=g)'}, ) def test_multiple_errors_collected(self): @@ -276,12 +276,14 @@ def test_a_dual_loads_in_an_expressions_entry(self): class TestDimensionKwargs: - """A dim kwarg that names nothing is a silent no-op, not an error — `sum(p, over=snapshto)` used to load.""" + """A dim kwarg that names nothing is a silent no-op, not an error — `sum(p, consume=snapshto)` used to load.""" @pytest.mark.parametrize( ('expression', 'fragments'), [ - pytest.param('sum(p, over=snapshto) == load', ('silent no-op', 'sum(over=snapshto)'), id='sum-over-typo'), + pytest.param( + 'sum(p, consume=snapshto) == load', ('silent no-op', 'sum(consume=snapshto)'), id='sum-over-typo' + ), pytest.param( 'sum(p, by=bus) == load', ("'bus' is a dimension, and by= takes a relation",), @@ -307,7 +309,7 @@ def test_a_dim_kwarg_typo_is_rejected(self, expression, fragments): @pytest.mark.parametrize( ('expression', 'dims'), [ - pytest.param('sum(p, over=generator) == load', ['snapshot'], id='a-sum'), + pytest.param('sum(p, consume=generator) == load', ['snapshot'], id='a-sum'), pytest.param('sum(p, by=zone) == load', ['snapshot', 'bus'], id='a-grouped-sum'), pytest.param( "shift(p, along=snapshot, offset=1, edge='wrap') == load", @@ -327,7 +329,7 @@ def test_macro_formals_are_not_mistaken_for_dimensions(self): 'ws': { 'args': ['array', 'weights'], 'kwargs': ['over'], - 'template': 'sum(array * weights, over=over)', + 'template': 'sum(array * weights, consume=over)', } }, objective={'sense': 'minimize', 'expression': 'ws(p, c, over=g)'}, @@ -439,7 +441,7 @@ def _schema_with_typed_a(dtype: str, expression: str) -> Spec: pytest.param('p / a <= c', id='a-divisor'), pytest.param('p + a <= c', id='a-term'), pytest.param('-a * p <= c', id='a-negated-factor'), - pytest.param('sum(a * p, over=g) <= 1', id='under-an-operator'), + pytest.param('sum(a * p, consume=g) <= 1', id='under-an-operator'), ], ) def test_a_label_or_a_flag_is_not_a_value(self, dtype, expression): @@ -564,7 +566,7 @@ class TestRulesDecidedWithoutData: id='a-constraint-without-a-comparison', ), pytest.param( - {'objective': {'expression': 'sum(p, over=g) <= 5'}}, + {'objective': {'expression': 'sum(p, consume=g) <= 5'}}, ('must not contain a comparison',), id='an-objective-with-a-comparison', ), @@ -579,7 +581,7 @@ class TestRulesDecidedWithoutData: id='a-cubic-constraint', ), pytest.param( - {'objective': {'expression': 'sum(p ** 2, over=g)'}}, + {'objective': {'expression': 'sum(p ** 2, consume=g)'}}, ('The objective', '`**` is not in the language over variables'), id='a-variable-under-a-power', ), @@ -685,47 +687,47 @@ class TestRulesDecidedWithoutData: 'variables.q.dims': ['g', 'h', 'z'], 'objective': {'expression': 'sum(sum(q, by=lk))'}, }, - ("'lk' has 2 key columns (['g', 'z']), and the call has to say which over= names",), + ("'lk' has 2 key columns (['g', 'z']), and the call has to say which consume= names",), id='by-a-two-key-relation-without-from', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, by=lk, over=z))'}}, - ("over=z names no column of 'lk', whose columns are ['g', 'h']",), + {'objective': {'expression': 'sum(sum(p, by=lk, consume=z))'}}, + ("consume=z names no column of 'lk', whose columns are ['g', 'h']",), id='from-a-column-the-relation-lacks', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, by=lk, over=h, into=h))'}}, - ("over= and into= both name ['h']",), + {'objective': {'expression': 'sum(sum(p, by=lk, consume=h, produce=h))'}}, + ("consume= and produce= both name ['h']",), id='from-and-to-the-same-column', ), pytest.param( { 'dimensions.z': {}, 'relations.lz': {'key': 'g', 'value': ['h', 'z']}, - 'objective': {'expression': 'sum(sum(p, by=lz, into=[h, h]))'}, + 'objective': {'expression': 'sum(sum(p, by=lz, produce=[h, h]))'}, }, - ("into=['h', 'h'] names a column twice",), + ("produce=['h', 'h'] names a column twice",), id='a-to-list-naming-a-column-twice', ), pytest.param( { 'dimensions.z': {}, 'relations.lz': {'key': 'g', 'value': ['h', 'z']}, - 'objective': {'expression': 'sum(sum(p, by=lz, over=[g, h], into=h))'}, + 'objective': {'expression': 'sum(sum(p, by=lz, consume=[g, h], produce=h))'}, }, - ("over= and into= both name ['h']",), + ("consume= and produce= both name ['h']",), id='a-from-list-overlapping-to', ), pytest.param( { 'relations.lz': {'key': 'g', 'value': {'h0': 'h', 'h1': 'h'}}, - 'objective': {'expression': 'sum(sum(p, by=lz, over=[h0, h1], into=g))'}, + 'objective': {'expression': 'sum(sum(p, by=lz, consume=[h0, h1], produce=g))'}, }, - ("over=['h0', 'h1'] names two columns over ['h'], and the operand carries each dimension once",), + ("consume=['h0', 'h1'] names two columns over ['h'], and the operand carries each dimension once",), id='a-from-list-naming-two-columns-over-one-dimension', ), pytest.param( - {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, by=lk, over=g))'}}, + {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, by=lk, consume=g))'}}, ( "shift() expects shift(, along=, offset=[, edge='wrap'|]" '[, by=[, within=]])', @@ -747,9 +749,9 @@ class TestRulesDecidedWithoutData: id='position-within-a-column-the-relation-lacks', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, into=g))'}}, + {'objective': {'expression': 'sum(sum(p, produce=g))'}}, ('names a column of a relation, and no by= names the relation',), - id='into-without-by', + id='produce-without-by', ), pytest.param( {'relations.rel': {'key': ['g', 'h']}, 'objective': {'expression': 'sum(sum(p, by=rel))'}}, @@ -759,13 +761,13 @@ class TestRulesDecidedWithoutData: pytest.param( { 'relations.rel': {'key': ['g', 'h']}, - 'objective': {'expression': 'sum(at(r, by=rel, over=h, into=g))'}, + 'objective': {'expression': 'sum(at(r, by=rel, consume=h, produce=g))'}, }, ("at reads one value per coordinate, and 'rel' is not single-valued",), id='at-through-a-bare-relation', ), pytest.param( - {'objective': {'expression': 'sum(sum(q, by=lk, over=h, into=g))'}}, + {'objective': {'expression': 'sum(sum(q, by=lk, consume=h, produce=g))'}}, ("this sum walks to the key ['g']", 'that is a read, which is', 'at(..., by=lk'), id='a-sum-that-walks-to-the-key-is-a-read', ), @@ -886,40 +888,40 @@ class TestRulesDecidedWithoutData: id='one-name-two-kinds', ), pytest.param( - {'objective': {'expression': 'sum(g + p, over=g)'}}, + {'objective': {'expression': 'sum(g + p, consume=g)'}}, ("'g' is a dimension, and a dimension is not a value",), id='a-dimension-as-a-value', ), pytest.param( - {'objective': {'expression': 'sum(lk + p, over=g)'}}, + {'objective': {'expression': 'sum(lk + p, consume=g)'}}, ("'lk' is a relation, and a relation is structure",), id='a-relation-as-a-value', ), pytest.param( - {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=wrap), over=g)'}}, + {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=wrap), consume=g)'}}, ('is a bare name where a keyword belongs',), id='a-bare-edge-keyword', ), pytest.param( - {'objective': {'expression': "sum(shift(p, along=g, offset=1, edge='foo'), over=g)"}}, + {'objective': {'expression': "sum(shift(p, along=g, offset=1, edge='foo'), consume=g)"}}, ("edge='foo') is not an edge policy",), id='an-edge-policy-that-is-not-one', ), pytest.param( { 'parameters.off': {'dims': [], 'dtype': 'int'}, - 'objective': {'expression': 'sum(shift(p, along=g, offset=off + 0), over=g)'}, + 'objective': {'expression': 'sum(shift(p, along=g, offset=off + 0), consume=g)'}, }, ('shift(offset=) takes a number or the name of an integer parameter', 'Precompute it as a parameter'), id='an-amount-that-is-an-expression', ), pytest.param( - {'objective': {'expression': 'sum(sum_back(p, along=g, window=2 * 1), over=g)'}}, + {'objective': {'expression': 'sum(sum_back(p, along=g, window=2 * 1), consume=g)'}}, ('sum_back(window=) takes a number or the name of an integer parameter',), id='a-width-that-is-an-expression', ), pytest.param( - {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=1 + 1), over=g)'}}, + {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=1 + 1), consume=g)'}}, ('shift(edge=) is an expression, and an edge is the keyword',), id='an-edge-that-is-an-expression', ), @@ -949,7 +951,7 @@ class TestRulesDecidedWithoutData: { 'dimensions.z': {}, 'relations.lz': {'key': ['g', 'z'], 'value': 'h'}, - 'objective': {'expression': 'sum(sum(q, by=[lk, lz], over=g))'}, + 'objective': {'expression': 'sum(sum(q, by=[lk, lz], consume=g))'}, }, ('a list walks each relation by its declared key and value, so a column keyword has nothing to name',), id='by-a-list-with-from', diff --git a/tests/test_yaml_loading.py b/tests/test_yaml_loading.py index d4fd5a23..d47a0588 100644 --- a/tests/test_yaml_loading.py +++ b/tests/test_yaml_loading.py @@ -30,7 +30,7 @@ constraints: balance: dims: [snapshot] - expression: sum(p, over=generator) == 5 + expression: sum(p, consume=generator) == 5 objective: expression: sum(p * cost) """ diff --git a/tests/typesetting/golden/model.yaml b/tests/typesetting/golden/model.yaml index dbdddc6d..9cc91c12 100644 --- a/tests/typesetting/golden/model.yaml +++ b/tests/typesetting/golden/model.yaml @@ -88,7 +88,7 @@ sos: expressions: spend: # a plain named expression: its symbol prints where it is used, its body once as a definition description: what a snapshot's dispatch costs - expression: sum(p * cost, over=generator) + expression: sum(p * cost, consume=generator) lcoe: sum(p * cost) / sum(p) # nothing in the math reads it, so its divisor may carry a variable marginal_price: dual(balance) # the row dual of a constraint, the one builtin only an entry the math never reads may call startup_cost: # a quantity defined by region: no two cases overlap, and `otherwise` is the rest @@ -152,17 +152,17 @@ constraints: expression: spill <= at(zone_cap, by=zone_of) grouped_once: # one table walked to two value columns: the domain carries a condition per column dims: [snapshot, bus, technology] - expression: sum(p, by=gen_bt, into=[bus, technology]) <= tech_cap + expression: sum(p, by=gen_bt, produce=[bus, technology]) <= tech_cap pulled_back_once: # its adjoint, reading one slot through two columns of one table dims: [generator] - expression: units <= at(tech_cap, by=gen_bt, over=[bus, technology]) + expression: units <= at(tech_cap, by=gen_bt, consume=[bus, technology]) within_bus: # a partition grouped by one named value column of a two-value table, and a position within both dims: [generator] where: "position(generator, by=gen_bt, within=[bus, technology]) == 0" expression: units <= shift(units, along=generator, offset=1, edge=0, by=gen_bt, within=bus) relational: # a sum through a bare relation: the domain is a row of the relation rather than a function's value dims: [snapshot, bus] - expression: sum(p, by=connection, over=generator, into=bus) <= load + expression: sum(p, by=connection, consume=generator, produce=bus) <= load connected: # a bare relation as a where: the row of the frame has to be a member of the relation dims: [snapshot, generator, bus] where: "connection" @@ -178,19 +178,19 @@ constraints: expression: units <= at(tech_cap, by=[gen_bus, gen_tech]) zonal: # a grouping through a two-key map, walked along one key: the condition reads the other, and the row keeps it dims: [snapshot, zone] - expression: sum(p, by=gen_zone, over=generator) <= zone_cap + expression: sum(p, by=gen_zone, consume=generator) <= zone_cap zonal_history: # the same table walked along its other key dims: [generator, zone] - expression: sum(p, by=gen_zone, over=snapshot) <= zone_cap + expression: sum(p, by=gen_zone, consume=snapshot) <= zone_cap zonal_pullback: # its adjoint, reading the slot the row's own snapshot puts the generator in dims: [snapshot, generator] where: "gen_zone == 'north' AND position(generator, by=gen_zone) == 0" - expression: p <= at(spill * zone_cap, by=gen_zone, into=generator) + expression: p <= at(spill * zone_cap, by=gen_zone, produce=generator) arithmetic: # division, both unary signs, a sign beside a sign, floats with and without an exponent, bracketing dims: [snapshot] expression: >- - sum(p / 2 + -cost - -1e-5 * p + 2.5e-7 * cost + 0.5 * p, over=generator) - >= -sum(+p, over=generator) * -3 + sum(p / 2 + -cost - -1e-5 * p + 2.5e-7 * cost + 0.5 * p, consume=generator) + >= -sum(+p, consume=generator) * -3 total: # a sum naming no dim, whose domain is the one place the dims it took are said dims: [] expression: sum(p) <= budget diff --git a/tests/typesetting/test_cases.py b/tests/typesetting/test_cases.py index 70da82ee..9ef1d274 100644 --- a/tests/typesetting/test_cases.py +++ b/tests/typesetting/test_cases.py @@ -145,7 +145,7 @@ def test_the_table_may_rename_a_named_expression_cased_or_plain(): tex = to_latex(CASED, symbols={'notation': 'latex', 'names': {'headroom': r'\bar h'}}, legend=False) assert r'\bar h_{t,g}' in tex - plain = override(DISPATCH, **{'expressions.supply': 'sum(p, over=generator)'}) + plain = override(DISPATCH, **{'expressions.supply': 'sum(p, consume=generator)'}) tex = to_latex(plain, symbols={'notation': 'latex', 'names': {'supply': 's'}}, legend=False) assert 's_{t} & =' in tex, 'the definition prints under the spelling the table gave' diff --git a/tests/typesetting/test_declaration.py b/tests/typesetting/test_declaration.py index d1872884..fab8d25d 100644 --- a/tests/typesetting/test_declaration.py +++ b/tests/typesetting/test_declaration.py @@ -25,7 +25,7 @@ PLAIN = override( DISPATCH, **{ - 'expressions.spend': 'sum(p * cost, over=generator)', + 'expressions.spend': 'sum(p * cost, consume=generator)', 'expressions.total': 'sum(p)', 'expressions.priced': 'cost * 2', 'constraints.budgeted': {'dims': ['snapshot'], 'where': 'load > 0', 'expression': 'spend <= 10'}, diff --git a/tests/typesetting/test_symbols.py b/tests/typesetting/test_symbols.py index cabd21d4..8d638729 100644 --- a/tests/typesetting/test_symbols.py +++ b/tests/typesetting/test_symbols.py @@ -61,7 +61,7 @@ def test_the_table_prints_verbatim_and_the_rest_is_still_derived(render, symbols 'dimensions.generator.description': 'dispatchable units', 'parameters.p_max.description': 'installed capacity', 'variables.p.description': 'output of a generator in a snapshot', - 'expressions.spend': {'expression': 'sum(p * cost, over=generator)', 'description': 'what a snapshot costs'}, + 'expressions.spend': {'expression': 'sum(p * cost, consume=generator)', 'description': 'what a snapshot costs'}, 'objective.expression': 'sum(spend)', }, ) diff --git a/tests/typesetting/test_walk.py b/tests/typesetting/test_walk.py index b867d4d4..5cffdc74 100644 --- a/tests/typesetting/test_walk.py +++ b/tests/typesetting/test_walk.py @@ -73,7 +73,7 @@ def _masked(dtype: str) -> dict[str, object]: 'keep': {'dims': ['g'], 'where': 'flag', 'bounds': {'lower': 0, 'upper': 1}}, 'drop': {'dims': ['g'], 'where': 'NOT flag', 'bounds': {'lower': 0, 'upper': 1}}, }, - 'objective': {'sense': 'minimize', 'expression': 'sum(keep, over=g)'}, + 'objective': {'sense': 'minimize', 'expression': 'sum(keep, consume=g)'}, } @@ -377,7 +377,7 @@ def test_a_named_expression_prints_once_as_a_definition_and_by_symbol_where_used identity of its own, so it is expanded away either way.""" model = override( DISPATCH_MODEL, - **{'expressions.supply': 'sum(p, over=generator)', 'constraints.balance.expression': 'supply == load'}, + **{'expressions.supply': 'sum(p, consume=generator)', 'constraints.balance.expression': 'supply == load'}, ) symbol = fmt.subscript(fmt.italic('supply'), ['t']) text = typeset(model, name, legend=False) @@ -389,7 +389,7 @@ def test_inlining_substitutes_a_named_expression_where_it_is_used(name: FormatNa """What prints then is the math a backend builds, not the name it was spelled with.""" model = override( DISPATCH_MODEL, - **{'expressions.supply': 'sum(p, over=generator)', 'constraints.balance.expression': 'supply == load'}, + **{'expressions.supply': 'sum(p, consume=generator)', 'constraints.balance.expression': 'supply == load'}, ) assert 'supply' not in typeset(model, name, legend=False, inline_expressions=True), ( 'inlined, so its name never prints' @@ -411,7 +411,7 @@ def test_inlining_keeps_the_definition_of_an_entry_the_math_never_reads(name: Fo model = override( DISPATCH_MODEL, **{ - 'expressions.supply': 'sum(p, over=generator)', + 'expressions.supply': 'sum(p, consume=generator)', 'expressions.lcoe': 'sum(p * cost) / sum(p)', 'constraints.balance.expression': 'supply == load', }, @@ -663,7 +663,9 @@ def _row(expression: str, where: str | None = None, **patch: object) -> str: r"\sum_{g' \in \mathcal{G} \,:\, \mathrm{bus\_of}(g') = \mathrm{bus\_of}(g)} q_{t,g'}", id='grouped-by-a-relation', ), - pytest.param('p == q - sum(q, over=generator)', r"\sum_{g' \in \mathcal{G}} q_{t,g'}", id='over-the-whole-dim'), + pytest.param( + 'p == q - sum(q, consume=generator)', r"\sum_{g' \in \mathcal{G}} q_{t,g'}", id='over-the-whole-dim' + ), ], ) def test_a_reduction_under_its_own_dimension_takes_a_fresh_dummy(expression: str, expected: str): diff --git a/tools/spec_math.py b/tools/spec_math.py index a07785e0..4b104a4e 100644 --- a/tools/spec_math.py +++ b/tools/spec_math.py @@ -28,13 +28,13 @@ #: table's first cell verbatim. OPERATORS = { 'sum(array)': 'sum_all', - 'sum(array, over=dim)': 'sum', + 'sum(array, consume=dim)': 'sum', 'sum(array, by=relation)': 'sum_by', 'sum(array, by=[relation, …])': 'sum_by_relations', - 'sum(array, by=relation, over=a, into=b)': 'sum_by_columns', - 'sum(array, by=relation, over=[a, …], into=[b, …])': 'sum_by_column_lists', + 'sum(array, by=relation, consume=a, produce=b)': 'sum_by_columns', + 'sum(array, by=relation, consume=[a, …], produce=[b, …])': 'sum_by_column_lists', 'at(array, by=relation)': 'at', - 'at(array, by=relation, over=a, into=b)': 'at_columns', + 'at(array, by=relation, consume=a, produce=b)': 'at_columns', 'shift(array, along=dim, offset=n)': 'shift', "shift(array, along=dim, offset=n, edge='wrap')": 'shift_wrap', 'shift(array, along=dim, offset=n, edge=v)': 'shift_edge',