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',