From 6e945dfccc86a894a68eaec95783156a5f52faa1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:50:06 +0000 Subject: [PATCH 1/2] feat(language)!: sets and properties replace relations, and a relations section is refused Every relation in examples/, the fixtures and the docs is rewritten as sets and properties, and the generated pages and golden files are regenerated. `relations:` leaves the schema, so a file that still declares it is refused with the closed schema's own message. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6yvGzZN2iR4WyA9RumvvJ --- docs/about/limits.md | 30 +- ... => sets-and-properties-as-linear-maps.md} | 102 +-- docs/examples/library/composed.md | 42 +- docs/examples/library/generator.md | 14 +- docs/examples/library/load.md | 14 +- docs/examples/library/surface.md | 14 +- docs/examples/operators.md | 85 +-- docs/examples/pypsa.md | 288 ++++----- docs/examples/pypsa/generator.md | 49 +- docs/examples/pypsa/generator_commitment.md | 18 +- docs/examples/pypsa/generator_maintenance.md | 11 +- docs/examples/pypsa/generator_ramping.md | 20 +- docs/examples/pypsa/line.md | 68 +- docs/examples/pypsa/link.md | 82 ++- docs/examples/pypsa/link_commitment.md | 18 +- docs/examples/pypsa/link_maintenance.md | 11 +- docs/examples/pypsa/link_ramping.md | 20 +- docs/examples/pypsa/load.md | 18 +- docs/examples/pypsa/process.md | 73 ++- docs/examples/pypsa/process_commitment.md | 18 +- docs/examples/pypsa/process_maintenance.md | 11 +- docs/examples/pypsa/process_ramping.md | 20 +- docs/examples/pypsa/security.md | 93 ++- docs/examples/pypsa/settings.md | 18 +- docs/examples/pypsa/storage_unit.md | 73 ++- docs/examples/pypsa/store.md | 71 ++- docs/examples/pypsa/transformer.md | 49 +- docs/first-spec.md | 4 +- docs/howto/compose.md | 61 +- docs/howto/curve-by-hand.md | 6 +- docs/howto/declare-a-column.md | 12 +- docs/reference/glossary.md | 10 +- docs/reference/language/absence.md | 20 +- docs/reference/language/dimensions.md | 4 +- docs/reference/language/errors.md | 2 +- docs/reference/language/expressions.md | 96 +-- docs/reference/language/file.md | 5 +- docs/reference/language/index.md | 2 +- docs/reference/language/operators.md | 61 +- .../parameters-variables-constraints.md | 25 +- docs/reference/language/relations.md | 256 -------- docs/reference/language/sets.md | 135 +++- docs/reference/notation.md | 189 +++--- docs/reference/reading.md | 27 +- docs/several-files.md | 13 +- examples/library/generator.yaml | 8 +- examples/library/load.yaml | 8 +- examples/library/surface.yaml | 8 +- examples/operators/at.yaml | 7 +- examples/operators/at_columns.yaml | 8 +- examples/operators/shift_partitioned.yaml | 7 +- examples/operators/sum_back_partitioned.yaml | 7 +- examples/operators/sum_by.yaml | 7 +- examples/operators/sum_by_column_lists.yaml | 11 +- examples/operators/sum_by_columns.yaml | 10 +- examples/pypsa.yaml | 369 ++++++----- examples/pypsa/generator.yaml | 33 +- examples/pypsa/generator_commitment.yaml | 12 +- examples/pypsa/generator_maintenance.yaml | 11 +- examples/pypsa/generator_ramping.yaml | 12 +- examples/pypsa/line.yaml | 48 +- examples/pypsa/link.yaml | 60 +- examples/pypsa/link_commitment.yaml | 12 +- examples/pypsa/link_maintenance.yaml | 11 +- examples/pypsa/link_ramping.yaml | 12 +- examples/pypsa/load.yaml | 12 +- examples/pypsa/process.yaml | 51 +- examples/pypsa/process_commitment.yaml | 12 +- examples/pypsa/process_maintenance.yaml | 11 +- examples/pypsa/process_ramping.yaml | 12 +- examples/pypsa/security.yaml | 65 +- examples/pypsa/settings.yaml | 12 +- examples/pypsa/storage_unit.yaml | 47 +- examples/pypsa/store.yaml | 45 +- examples/pypsa/transformer.yaml | 37 +- mkdocs.yml | 3 +- schema/mathspec.schema.json | 88 +-- src/mathspec/_advising.py | 4 +- src/mathspec/_composition.py | 29 +- src/mathspec/_condition_parser.py | 6 +- src/mathspec/_condition_resolver.py | 117 ++-- src/mathspec/_dimensions.py | 10 +- src/mathspec/_exclusivity.py | 24 +- src/mathspec/_expansion.py | 4 +- src/mathspec/_expression_parser.py | 4 +- src/mathspec/_expression_resolver.py | 92 ++- src/mathspec/_lowering.py | 6 +- src/mathspec/_operators.py | 24 +- src/mathspec/_resolution.py | 36 +- src/mathspec/_typesetting/legend.py | 14 +- src/mathspec/_typesetting/walk.py | 62 +- src/mathspec/_validation.py | 46 +- src/mathspec/program.py | 4 +- src/mathspec/spec.py | 95 +-- tests/fixtures.py | 4 +- tests/fixtures/every_program_node.yaml | 10 +- tests/fixtures/pypsa_standard_shape.yaml | 28 +- tests/test_advice.py | 12 +- tests/test_boundedness.py | 4 +- tests/test_canonical.py | 3 +- tests/test_composition.py | 50 +- tests/test_degree.py | 4 +- tests/test_dimensions.py | 192 +++--- tests/test_exclusivity.py | 8 +- tests/test_expansion.py | 43 +- tests/test_given.py | 20 +- tests/test_lowering.py | 133 ++-- tests/test_property_calls.py | 11 +- tests/test_pypsa_collapses.py | 2 +- tests/test_pypsa_references.py | 2 +- tests/test_pypsa_split.py | 2 +- tests/test_schema.py | 6 +- tests/test_separability.py | 40 +- tests/test_sets.py | 2 +- tests/test_terms.py | 8 +- tests/test_validation.py | 580 +++++++----------- tests/typesetting/golden/latex.out | 60 +- tests/typesetting/golden/markdown.out | 60 +- tests/typesetting/golden/model.yaml | 90 +-- tests/typesetting/golden/typst.out | 60 +- tests/typesetting/test_formats.py | 4 +- tests/typesetting/test_symbols.py | 4 +- tests/typesetting/test_walk.py | 72 ++- tools/notation.py | 42 +- tools/pypsa_split.py | 21 +- tools/spec_math.py | 14 +- 126 files changed, 2490 insertions(+), 2992 deletions(-) rename docs/about/{relations-as-linear-maps.md => sets-and-properties-as-linear-maps.md} (68%) delete mode 100644 docs/reference/language/relations.md diff --git a/docs/about/limits.md b/docs/about/limits.md index b328a04a3..0ced37863 100644 --- a/docs/about/limits.md +++ b/docs/about/limits.md @@ -53,7 +53,7 @@ nothing bounds how far it expands. | The operator | Allowed? | | ------------------------------------------------ | ----------------------------------------------- | | filters rows on a column they already carry | yes | -| joins each row against a parameter or a relation | yes | +| joins each row against a parameter or a property | yes | | reads a fixed number of neighbouring rows | yes | | reads only the coordinate labels | yes | | reads every row | yes, at one full pass before any chunk builds | @@ -100,17 +100,17 @@ attaches the numbers runs it. Each row is a request the language refuses, with the reason and what to write instead. -| Request | Why refused | Instead | -| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Resampling, clustering, file IO, unit conversion | not math | do it in data preparation, and pass a parameter | -| Unit checking at load | a `unit: MW` on a parameter is a claim that nothing checks against the column, and it needs a grammar of units the language then maintains | convert to one unit system in data preparation, and name it in the `description:`. A range the data has to meet is an [`assumptions:`](../reference/language/assumptions.md) entry | -| Array operations such as `merge` and `reindex` | there is no end to them | data preparation | -| Helpers for one domain, such as `reduce_carrier_dim` | writes one field's vocabulary into the language | a component library of macros over the operators that exist | -| A vocabulary for tracked metrics: `impacts:`, `effects:`, a `costs` axis | a named expression already does this | an `impact` dimension and one named expression. Cap it with a constraint, weight it in the objective, read it back after the solve | -| `**` with a variable in the base or the exponent | the exponent would decide the degree, and `to_spec` reads no data | `x * x` for a square. `**` over parameters and numbers is allowed | -| Normalisation, `x / sum(x)` | dividing by a variable is not a polynomial, and no solver takes it | write the ratio as a constraint, or fix the denominator | -| An `if`, a loop, or entries that depend on the data | `to_spec` could no longer read the file without the data | `where:` masks and `dims:` dimensions. A tool may loop over specs | -| A Python API for building specs | the spec is the file you review and diff | YAML, or a `dict` with the same keys, merged before `to_spec` | -| A `where` comparing a relation column against the dimension it maps into | the relation already pairs the two, and a mask over the pair is the same fact in a bigger shape | place the quantity with `sum(by=)`, or read it with `at(by=)` ([operators](../reference/language/operators.md#sum)) | -| A `join` operator apart from `sum` and `at`, such as `sum(join(p, gen_bus), over=generator)` | a join adds an axis for each column that the sum then removes. That axis is not a dimension, so no entry can carry it. One call adds the axis and removes it again, so no file can leave an axis in place | `sum(p, over=generator, by=gen_bus[bus])` adds up each group, and `at(x, by=gen_bus[bus])` reads one row ([relations](../reference/language/relations.md#how-a-relation-is-used)) | -| A `count(...)` compared against a parameter or an expression | a count is a whole number of coordinates, and the loader decides it without data. A bound read from a column can only be decided when the data is attached | data preparation supplies the number, and the spec trusts it. The PyPSA example does this for `StorageUnit_inactive_snapshots` and `Store_inactive_snapshots`: no assumption checks them against `count(NOT {c}_active, over=snapshot)`. Compare against a literal ([counting](../reference/language/expressions.md#counting-what-a-condition-admits)) | +| Request | Why refused | Instead | +| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Resampling, clustering, file IO, unit conversion | not math | do it in data preparation, and pass a parameter | +| Unit checking at load | a `unit: MW` on a parameter is a claim that nothing checks against the column, and it needs a grammar of units the language then maintains | convert to one unit system in data preparation, and name it in the `description:`. A range the data has to meet is an [`assumptions:`](../reference/language/assumptions.md) entry | +| Array operations such as `merge` and `reindex` | there is no end to them | data preparation | +| Helpers for one domain, such as `reduce_carrier_dim` | writes one field's vocabulary into the language | a component library of macros over the operators that exist | +| A vocabulary for tracked metrics: `impacts:`, `effects:`, a `costs` axis | a named expression already does this | an `impact` dimension and one named expression. Cap it with a constraint, weight it in the objective, read it back after the solve | +| `**` with a variable in the base or the exponent | the exponent would decide the degree, and `to_spec` reads no data | `x * x` for a square. `**` over parameters and numbers is allowed | +| Normalisation, `x / sum(x)` | dividing by a variable is not a polynomial, and no solver takes it | write the ratio as a constraint, or fix the denominator | +| An `if`, a loop, or entries that depend on the data | `to_spec` could no longer read the file without the data | `where:` masks and `dims:` dimensions. A tool may loop over specs | +| A Python API for building specs | the spec is the file you review and diff | YAML, or a `dict` with the same keys, merged before `to_spec` | +| A `where` comparing a property against the dimension it maps into | the property already pairs the two, and a mask over the pair is the same fact in a bigger shape | place the quantity with `sum(by=)`, or read it with `at(by=)` ([operators](../reference/language/operators.md#sum)) | +| A `join` operator apart from `sum` and `at`, such as `sum(join(p, bus), over=generator)` | a join adds an axis for each column that the sum then removes. That axis is not a dimension, so no entry can carry it. One call adds the axis and removes it again, so no file can leave an axis in place | `sum(p, over=generator, by=generator[bus])` adds up each group, and `at(x, by=generator[bus])` reads one row ([sets and properties](../reference/language/sets.md#how-a-set-or-a-property-is-read)) | +| A `count(...)` compared against a parameter or an expression | a count is a whole number of coordinates, and the loader decides it without data. A bound read from a column can only be decided when the data is attached | data preparation supplies the number, and the spec trusts it. The PyPSA example does this for `StorageUnit_inactive_snapshots` and `Store_inactive_snapshots`: no assumption checks them against `count(NOT {c}_active, over=snapshot)`. Compare against a literal ([counting](../reference/language/expressions.md#counting-what-a-condition-admits)) | diff --git a/docs/about/relations-as-linear-maps.md b/docs/about/sets-and-properties-as-linear-maps.md similarity index 68% rename from docs/about/relations-as-linear-maps.md rename to docs/about/sets-and-properties-as-linear-maps.md index 602f80337..6b540ca13 100644 --- a/docs/about/relations-as-linear-maps.md +++ b/docs/about/sets-and-properties-as-linear-maps.md @@ -3,11 +3,12 @@ SPDX-FileCopyrightText: mathspec contributors SPDX-License-Identifier: CC-BY-4.0 --> -# Relations as linear maps +# Sets and properties as linear maps You can read a `sum` or an `at` through a -[relation](../reference/language/relations.md) as the product of a matrix and -a vector. The join and the group-by on the relations page are the two steps of +[property](../reference/language/sets.md#properties) as the product of a matrix +and a vector. The join and the group-by on the +[sets and properties page](../reference/language/sets.md#sums) are the two steps of that product, and the product is the sum that a paper prints. Read on if "joined on" and "grouped by" read to you as database words, and you want the math behind them. Every formula below reads this spec: @@ -17,8 +18,11 @@ dimensions: snapshot: { dtype: int } generator: { dtype: str } zone: { dtype: str } -relations: - gen_zone: { key: [generator, snapshot], values: zone, missing: absent } +sets: + generator_snapshot: { dims: [generator, snapshot], missing: absent } +properties: + generator_snapshot: + zone: zone parameters: zone_cap: { dims: [snapshot, zone] } variables: @@ -26,43 +30,45 @@ variables: constraints: zonal: dims: [snapshot, zone] - expression: sum(p, over=generator, by=gen_zone[zone]) <= zone_cap + expression: sum(p, over=generator, by=generator_snapshot[zone]) <= zone_cap looked_up: dims: [snapshot, generator] - where: gen_zone - expression: p <= at(zone_cap, by=gen_zone[zone]) + where: generator_snapshot + expression: p <= at(zone_cap, by=generator_snapshot[zone]) objective: sense: minimize expression: sum(p) ``` -## The indicator of a relation +## The indicator of a set and a property -A relation with columns over the dimensions $`D_1, \dots, D_n`$ is a set of -rows, so it is a subset $`R \subseteq D_1 \times \dots \times D_n`$. Its +A set with members over the dimensions $`D_1, \dots, D_n`$ is a set of rows, +so it is a subset $`R \subseteq D_1 \times \dots \times D_n`$. Its **indicator** $`\mathbf{1}_R`$ is $`1`$ at a row of the table and $`0`$ -everywhere else. `key:` says that the set has one row per key tuple. So a -relation with `key: K` and `values: V` is the graph of a function -$`f: K \to V`$, and +everywhere else. A property's table has one row per row of its subject that has +the property. So a property of the subject $`K`$ into the dimension $`V`$ is the +graph of a function $`f: K \to V`$, and its table $`R \subseteq K \times V`$ +has the indicator ```math \mathbf{1}_R(k, v) = [\, f(k) = v \,]. ``` -By default the function is total: when the data is attached, a key tuple with -no row is [refused](../reference/language/relations.md#the-data-contract). -Under `missing: absent`, as in the spec above, the function is partial: it has -no value at a key tuple with no row. A bare relation is a subset, with no -function behind it. The same default holds for it, so a bare relation that is -not every key tuple declares `missing: absent`. In the spec above, `gen_zone` is the graph of -$`f: \mathcal{G} \times \mathcal{T} \to \mathcal{Z}`$. The -[data contract](../reference/language/relations.md#the-data-contract) makes -sure of this, because the loader checks for one row per key tuple when the -data is attached. +By default the function is total: when the data is attached, a row of the +subject with no value is [refused](../reference/language/sets.md#a-missing-row). +Under `missing: absent` the function is partial: it has no value at a row of +the subject that lacks the property. A set alone is a subset, with no function +behind it. The same default holds for it, so a set that is not every +combination of its dimensions declares `missing: absent`, as +`generator_snapshot` does above. Its property `zone` is the graph of a function +from the rows of `generator_snapshot` into $`\mathcal{Z}`$. The +[data contract](../reference/reading.md#the-tables-of-sets-and-properties) makes +sure of this, because the engine checks for one row per row of the subject when +the data is attached. ## A join and group-by as a contraction -`sum(p, over=generator, by=gen_zone[zone])` multiplies two arrays, and sums +`sum(p, over=generator, by=generator_snapshot[zone])` multiplies two arrays, and sums over the one index that they share and that the call names: ```math @@ -71,16 +77,16 @@ y_{t,z} = \sum_{g} \mathbf{1}_R(g, t, z) \cdot p_{t,g} = \sum_{g \,:\, f(g,\,t) The typesetter [prints](../reference/notation.md) the right-hand form. The left-hand form is a **tensor contraction**, which multiplies arrays and sums -over an index they share. The relations page asks two questions of each -column, and each pair of answers is a position that an index can take in the +over an index they share. The sets and properties page asks two questions of +each column, and each pair of answers is a position that an index can take in the formula: -| The relations page says | In the formula | +| The reference page says | In the formula | | -------------------------------- | -------------------------------------------------------------------- | | joined on, the column in `over=` | $`g`$ is on both factors and summed. The result loses it. | | grouped by, the column in `by=` | $`z`$ is on the indicator alone and not summed. The result gains it. | | joined on and grouped by | $`t`$ is on both factors and not summed. The result keeps it. | -| neither | a value column not in the formula. Nothing reads it. | +| neither | a property not in the formula. Nothing reads it. | The result carries the **free indices**, which are the indices that are not summed. They are the indices of the operand and of the indicator together, @@ -97,21 +103,21 @@ sum that breaks one of three conditions, and the formula needs each of them: - The operand carries no column grouped by. Otherwise $`z`$ would sit on both factors and not be summed, so the product would match the two occurrences of $`z`$ and not add an axis. You write that match outside the operator: - `load * sum(p, over=generator, by=gen_bus[bus])`. + `load * sum(p, over=generator, by=generator[bus])`. So "joined on" and "grouped by" are the two positions that an index can take -in the summation convention, applied to one product. The relations page has no +in the summation convention, applied to one product. The reference page has no rule beyond this product. The key column that the call does not name makes the matrix **block-diagonal**, with one block for each value of that column. At each $`t`$, $`M_t[z, g] = \mathbf{1}_R(g, t, z)`$ is a $`|\mathcal{Z}| \times |\mathcal{G}|`$ matrix of zeros and ones, and -$`y_t = M_t\, p_t`$. A relation keyed by one column has one block. +$`y_t = M_t\, p_t`$. A property of a dimension has one block. ## A join alone -`at(zone_cap, by=gen_zone[zone])` joins the same table, and binds the other +`at(zone_cap, by=generator_snapshot[zone])` joins the same table, and binds the other index: ```math @@ -130,14 +136,14 @@ it from the key alone. Where the columns a call groups by hold the whole key, every group is one row, and the group-by adds nothing. `JoinColumns.one_row_per_group` in `src/mathspec/program.py` names this case. A `sum` with one row per group adds up nothing, so the loader refuses it and -names `at`. An `at` reads value columns at the whole key, so each of its +names `at`. An `at` reads properties at the whole key, so each of its groups is always one row. The -[relations page](../reference/language/relations.md#sums-through-a-relation) +[reference page](../reference/language/sets.md#sums) quotes the message. The two maps are **adjoint**: an inner product does not change when the matrix moves to the other side as its transpose. For -`gen_bus: { key: generator, values: bus }`, with $`x`$ over generators and +`generator: {bus: bus}`, with $`x`$ over generators and $`y`$ over buses, ```math @@ -146,16 +152,16 @@ $`y`$ over buses, which is why the program holds `at` as a `Join` node, and `sum(by=)` as the same `Join` under a `Sum`. The join keeps the column that the sum removes as -an axis of its own, `Axis('generator', Column('gen_zone', 'generator'))`. That -axis runs over `generator` and stands for the column of the relation, and the +an axis of its own, `Axis('generator', Column('generator_snapshot.zone', 'generator'))`. That +axis runs over `generator` and stands for the column of the property's table, and the `Sum` sums over it. So a map into its own dimension, which removes one dimension and adds the same one, still has two separate axes between the join and the sum. -A bare relation has the same matrix, but no function behind it. A column of -$`M`$ may then hold several ones, so the join returns several rows for one -row of the operand, and no group is one row. That is why `at` through a bare -relation is refused. +A set read by its members alone has the same matrix, but no function behind +it. A column of $`M`$ may then hold several ones, so the join returns several +rows for one row of the operand, and no group is one row. That is why `at` +through a set read by its members is refused. ## The join and the aggregate @@ -203,20 +209,20 @@ model that an engine builds is not the matrix product. $`\mathrm{zone\_cap} \circ f`$ is undefined where $`f`$ is undefined, so `at` is absent there, and [absence spreads](../reference/language/absence.md#how-absence-travels) to the row. - `where: gen_zone` on `looked_up` writes the domain of $`f`$ into the spec, + `where: generator_snapshot` on `looked_up` writes the domain of $`f`$ into the spec, so a reader sees which rows exist without opening the data. Under the - default, `missing: refused`, the domain is every key tuple, so no key tuple - is outside it. + default, `missing: refused`, the domain is every combination of the + dimensions, so no combination is outside it. ## Partitions and tests -The other two uses of a relation do not multiply by $`\mathbf{1}_R`$ and sum. +The other two uses of a set or a property do not multiply by $`\mathbf{1}_R`$ and sum. - A partition steps inside a **fibre**, which is the set of members that $`f`$ sends to one value. - `shift(x, along=snapshot, offset=1, within=season_of[season])` reads the + `shift(x, along=snapshot, offset=1, within=snapshot[season])` reads the neighbour $`t'`$ of $`t`$ with $`f(t') = f(t)`$. The fibres of $`f`$ partition the axis, and the dimensions do not change. -- A test reads the indicator itself. The name of a relation in a `where` +- A test reads the indicator itself. The name of a set in a `where` evaluates $`\mathbf{1}_R`$ at the coordinate of the entry, and keeps the coordinate where $`\mathbf{1}_R`$ is $`1`$. diff --git a/docs/examples/library/composed.md b/docs/examples/library/composed.md index 9a555e175..c0b7c8231 100644 --- a/docs/examples/library/composed.md +++ b/docs/examples/library/composed.md @@ -46,10 +46,10 @@ dimensions: port: {dtype: str, description: 'the connections components make, one label per connection'} generator: {dtype: str, description: 'generating units, each on one port'} load: {dtype: str, description: 'demands, each on one port'} -relations: - Port_bus: {key: port, values: bus} - Generator_port: {key: generator, values: port} - Load_port: {key: load, values: port} +properties: + port: {bus: bus} + generator: {port: port} + load: {port: port} parameters: Generator_p_nom: dims: [generator] @@ -85,16 +85,16 @@ objective: {sense: minimize, expression: total_cost} constraints: Bus_nodal_balance: dims: [snapshot, bus] - expression: sum(Port_p, over=port, by=Port_bus[bus]) == 0 + expression: sum(Port_p, over=port, by=port[bus]) == 0 description: '`Bus-nodal_balance` — what the ports on a bus put in nets to nothing' Generator_injection: dims: [snapshot, generator] - expression: at(Port_p, by=Generator_port[port]) == Generator_p + expression: at(Port_p, by=generator[port]) == Generator_p description: 'what a generator produces is what its port injects. No PyPSA row stands for this: PyPSA writes the generator into the balance instead' Load_withdrawal: dims: [snapshot, load] - expression: at(Port_p, by=Load_port[port]) == -Load_p_set + expression: at(Port_p, by=load[port]) == -Load_p_set description: 'what a load takes is what its port withdraws. No PyPSA row stands for this: PyPSA writes the load into the balance instead' ``` @@ -106,10 +106,10 @@ constraints: | Symbol | Meaning | |---|---| | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | - | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | - | $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N},\ \mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J},\ \mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | - | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | - | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | + | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | + | $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N},\ \mathrm{port}: \mathcal{G} \to \mathcal{J},\ \mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | + | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | + | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | #### Parameters @@ -172,19 +172,19 @@ constraints: **`Bus_nodal_balance`** ```math - \sum_{j \in \mathcal{J} \,:\, \mathrm{Port\_bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} + \sum_{j \in \mathcal{J} \,:\, \mathrm{bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} ``` **`Generator_injection`** ```math - f_{t,\mathrm{Generator\_port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} + f_{t,\mathrm{port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` **`Load_withdrawal`** ```math - f_{t,\mathrm{Load\_port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} + f_{t,\mathrm{port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} ``` === "With commitment" @@ -214,10 +214,10 @@ constraints: | Symbol | Meaning | |---|---| | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | - | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | - | $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N},\ \mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J},\ \mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | - | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | - | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | + | $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | + | $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N},\ \mathrm{port}: \mathcal{G} \to \mathcal{J},\ \mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | + | $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | + | $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | #### Parameters @@ -288,19 +288,19 @@ constraints: **`Bus_nodal_balance`** ```math - \sum_{j \in \mathcal{J} \,:\, \mathrm{Port\_bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} + \sum_{j \in \mathcal{J} \,:\, \mathrm{bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} ``` **`Generator_injection`** ```math - f_{t,\mathrm{Generator\_port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} + f_{t,\mathrm{port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` **`Load_withdrawal`** ```math - f_{t,\mathrm{Load\_port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} + f_{t,\mathrm{port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} ``` **`Generator_com_p_upper`** diff --git a/docs/examples/library/generator.md b/docs/examples/library/generator.md index 397a21be8..e477c49dd 100644 --- a/docs/examples/library/generator.md +++ b/docs/examples/library/generator.md @@ -43,8 +43,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } port: { dtype: str, description: "the connections components make, one label per connection" } generator: { dtype: str, description: "generating units, each on one port" } -relations: - Generator_port: { key: generator, values: port } +properties: + generator: + port: port + parameters: Generator_p_nom: { dims: [generator], description: nominal power } Generator_marginal_cost: { dims: [generator], description: cost of one unit of output } @@ -59,7 +61,7 @@ constraints: what a generator produces is what its port injects. No PyPSA row stands for this: PyPSA writes the generator into the balance instead dims: [snapshot, generator] - expression: at(Port_p, by=Generator_port[port]) == Generator_p + expression: at(Port_p, by=generator[port]) == Generator_p expressions: Generator_cost: expression: sum(Generator_p * Generator_marginal_cost) @@ -80,8 +82,8 @@ PyPSA's `Generator`, wired to a port rather than straight to a bus, and cut to w | Symbol | Meaning | |---|---| | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J}`$ — the connections components make, one label per connection | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | +| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{port}: \mathcal{G} \to \mathcal{J}`$ — the connections components make, one label per connection | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{port}: \mathcal{G} \to \mathcal{J}`$ — generating units, each on one port | #### Parameters @@ -123,6 +125,6 @@ PyPSA's `Generator`, wired to a port rather than straight to a bus, and cut to w **`Generator_injection`** ```math -f_{t,\mathrm{Generator\_port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +f_{t,\mathrm{port}(g)} = p_{t,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` diff --git a/docs/examples/library/load.md b/docs/examples/library/load.md index 62eeb9f72..716fad722 100644 --- a/docs/examples/library/load.md +++ b/docs/examples/library/load.md @@ -24,8 +24,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } port: { dtype: str, description: "the connections components make, one label per connection" } load: { dtype: str, description: "demands, each on one port" } -relations: - Load_port: { key: load, values: port } +properties: + load: + port: port + parameters: Load_p_set: { dims: [snapshot, load], description: "`Load-p_set` — what a load takes in a snapshot" } constraints: @@ -34,7 +36,7 @@ constraints: what a load takes is what its port withdraws. No PyPSA row stands for this: PyPSA writes the load into the balance instead dims: [snapshot, load] - expression: at(Port_p, by=Load_port[port]) == -Load_p_set + expression: at(Port_p, by=load[port]) == -Load_p_set ``` PyPSA's `Load`, wired to a port rather than straight to a bus. What it takes is data, so it decides nothing. @@ -50,8 +52,8 @@ PyPSA's `Load`, wired to a port rather than straight to a bus. What it takes is | Symbol | Meaning | |---|---| | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | +| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — the connections components make, one label per connection | +| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{port}: \mathcal{D} \to \mathcal{J}`$ — demands, each on one port | #### Parameters @@ -64,6 +66,6 @@ PyPSA's `Load`, wired to a port rather than straight to a bus. What it takes is **`Load_withdrawal`** ```math -f_{t,\mathrm{Load\_port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} +f_{t,\mathrm{port}(d)} = -\mathrm{load}_{t,d} \qquad \forall\, t \in \mathcal{T},\ d \in \mathcal{D} ``` diff --git a/docs/examples/library/surface.md b/docs/examples/library/surface.md index 7801ec630..58bc6c4cb 100644 --- a/docs/examples/library/surface.md +++ b/docs/examples/library/surface.md @@ -35,8 +35,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } bus: { dtype: str, description: network nodes } port: { dtype: str, description: "the connections components make, one label per connection" } -relations: - Port_bus: { key: port, values: bus } +properties: + port: + bus: bus + variables: Port_p: dims: [snapshot, port] @@ -45,7 +47,7 @@ constraints: Bus_nodal_balance: description: "`Bus-nodal_balance` — what the ports on a bus put in nets to nothing" dims: [snapshot, bus] - expression: sum(Port_p, over=port, by=Port_bus[bus]) == 0 + expression: sum(Port_p, over=port, by=port[bus]) == 0 objective: sense: minimize expression: total_cost @@ -64,8 +66,8 @@ The coupling surface every component in this library is written against: one flo | Symbol | Meaning | |---|---| | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{Port\_bus}: \mathcal{J} \to \mathcal{N}`$ — the connections components make, one label per connection | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{J}`$ | index $`j`$ — `port` with $`\mathrm{bus}: \mathcal{J} \to \mathcal{N}`$ — the connections components make, one label per connection | #### Variables @@ -92,6 +94,6 @@ f_{t,j} \in \mathbb{R} \qquad \forall\, t \in \mathcal{T},\ j \in \mathcal{J} **`Bus_nodal_balance`** ```math -\sum_{j \in \mathcal{J} \,:\, \mathrm{Port\_bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{j \in \mathcal{J} \,:\, \mathrm{bus}(j) = n} f_{t,j} = 0 \qquad \forall\, t \in \mathcal{T},\ n \in \mathcal{N} ``` diff --git a/docs/examples/operators.md b/docs/examples/operators.md index e2fc20fa8..863b2083e 100644 --- a/docs/examples/operators.md +++ b/docs/examples/operators.md @@ -99,7 +99,7 @@ objective: { sense: minimize, expression: sum(p) } $`\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g,c} \le \mathrm{limit}_{c} \qquad \forall\, c \in \mathcal{C}`$ -### `sum(array, over=dim, by=relation[c])` +### `sum(array, over=dim, by=subject[p])` `examples/operators/sum_by.yaml` @@ -114,8 +114,9 @@ dimensions: generator: { dtype: str } bus: { dtype: str } -relations: - gen_bus: { key: generator, values: bus } +properties: + generator: + bus: bus parameters: limit: { dims: [snapshot, bus] } @@ -128,14 +129,14 @@ variables: constraints: bus_total: dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) <= limit + expression: sum(p, over=generator, by=generator[bus]) <= limit objective: { sense: minimize, expression: sum(p) } ``` -$`\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_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}`$ -### `sum(array, over=dim, by=relation[c]), joining on the rest of the key` +### `sum(array, over=dim, by=subject[p]), joining on the other members` `examples/operators/sum_by_columns.yaml` @@ -151,8 +152,12 @@ dimensions: period: { dtype: int } zone: { dtype: str } -relations: - zone_of: { key: [generator, period], values: zone } +sets: + generator_period: { dims: [generator, period] } + +properties: + generator_period: + zone: zone parameters: demand: { dims: [zone, period] } @@ -165,14 +170,14 @@ variables: constraints: zone_balance: dims: [zone, period] - expression: sum(p, over=generator, by=zone_of[zone]) >= demand + expression: sum(p, over=generator, by=generator_period[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_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}`$ -### `sum(array, over=[dim, …], by=relation[c, …])` +### `sum(array, over=[dim, …], by=subject[p, …])` `examples/operators/sum_by_column_lists.yaml` @@ -187,8 +192,13 @@ dimensions: bus: { dtype: str } technology: { dtype: str } -relations: - slot_of: { key: [generator, period], values: [bus, technology] } +sets: + generator_period: { dims: [generator, period] } + +properties: + generator_period: + bus: bus + technology: technology parameters: cap: { dims: [bus, technology] } @@ -201,14 +211,14 @@ variables: constraints: slot_cap: dims: [bus, technology] - expression: sum(p, over=[generator, period], by=slot_of[bus, technology]) <= cap + expression: sum(p, over=[generator, period], by=generator_period[bus, technology]) <= cap objective: { sense: minimize, expression: sum(p) } ``` -$`\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_{g \in \mathcal{G},\ e \in \mathcal{E} \,:\, \mathrm{bus}(g,\ e) = b \wedge \mathrm{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[c])` +### `at(array, by=subject[p])` `examples/operators/at.yaml` @@ -221,8 +231,9 @@ dimensions: snapshot: { dtype: int } period: { dtype: int } -relations: - period_of: { key: snapshot, values: period } +properties: + snapshot: + period: period parameters: cap: { dims: [period] } @@ -235,14 +246,14 @@ variables: constraints: within_cap: dims: [snapshot] - expression: p <= at(cap, by=period_of[period]) + expression: p <= at(cap, by=snapshot[period]) objective: { sense: minimize, expression: sum(p) } ``` -$`p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}`$ +$`p_{t} \le \mathrm{cap}_{\mathrm{period}(t)} \qquad \forall\, t \in \mathcal{T}`$ -### `at(array, by=relation[c]), two columns over one dimension` +### `at(array, by=subject[p]), two properties into one dimension` `examples/operators/at_columns.yaml` @@ -255,8 +266,10 @@ dimensions: line: { dtype: str } bus: { dtype: str } -relations: - ends: { key: line, values: { bus0: bus, bus1: bus } } +properties: + line: + bus0: bus + bus1: bus parameters: cap: { dims: [bus] } @@ -269,12 +282,12 @@ variables: constraints: sending_cap: dims: [line] - expression: f <= at(cap, by=ends[bus0]) + expression: f <= at(cap, by=line[bus0]) objective: { sense: minimize, expression: sum(f) } ``` -$`f_{l} \le \mathrm{cap}_{\mathrm{ends.bus0}(l)} \qquad \forall\, l \in \mathcal{L}`$ +$`f_{l} \le \mathrm{cap}_{\mathrm{bus0}(l)} \qquad \forall\, l \in \mathcal{L}`$ ### `shift(array, along=dim, offset=n)` @@ -390,7 +403,7 @@ objective: { sense: minimize, expression: sum(order) } $`\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\, t \in \mathcal{T},\ m \in \mathcal{M}`$ -### `shift(array, along=dim, offset=n, within=relation[c])` +### `shift(array, along=dim, offset=n, within=subject[p])` `examples/operators/shift_partitioned.yaml` @@ -403,8 +416,9 @@ dimensions: snapshot: { dtype: int, ordered: true } season: { dtype: str } -relations: - season_of: { key: snapshot, values: season } +properties: + snapshot: + season: season variables: p: @@ -414,12 +428,12 @@ variables: constraints: no_faster_than_before_in_season: dims: [snapshot] - expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[season]) objective: { sense: minimize, expression: sum(p) } ``` -$`p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\, t \in \mathcal{T}`$ +$`p_{t} \le p_{t \ominus^{\mathrm{season}(t)} 1} \qquad \forall\, t \in \mathcal{T}`$ ### `sum_back(array, along=dim, window=n)` @@ -523,7 +537,7 @@ objective: { sense: minimize, expression: sum(on) } $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ -### `sum_back(array, along=dim, window=n, within=relation[c])` +### `sum_back(array, along=dim, window=n, within=subject[p])` `examples/operators/sum_back_partitioned.yaml` @@ -538,8 +552,9 @@ dimensions: hour: { dtype: int, ordered: true } day: { dtype: str } -relations: - day_of: { key: hour, values: day } +properties: + hour: + day: day variables: started: @@ -552,12 +567,12 @@ variables: constraints: stays_up_inside_its_day: dims: [unit, hour] - expression: sum_back(started, along=hour, window=3, within=day_of[day]) <= on + expression: sum_back(started, along=hour, window=3, within=hour[day]) <= on objective: { sense: minimize, expression: sum(on) } ``` -$`\sum_{h' \in \mathcal{H} \,:\, 0 \le h -^{\mathrm{day\_of}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ +$`\sum_{h' \in \mathcal{H} \,:\, 0 \le h -^{\mathrm{day}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ ### `dual(constraint)` diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index db2aff67c..9b23c1f64 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -5477,24 +5477,24 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` with $`\mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$, `Generator_maintenance_cover` is `absent` where the data has no row, `Link_maintenance_cover` is `absent` where the data has no row, `Process_maintenance_cover` is `absent` where the data has no row — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y},\ \mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$, `Generator_maintenance_cover` is `absent` where the data has no row, `Link_maintenance_cover` is `absent` where the data has no row, `Process_maintenance_cover` is `absent` where the data has no row — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T}`$, `Generator_maintenance_cover` is `absent` where the data has no row — generating units, each on one bus | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T}`$, `Link_maintenance_cover` is `absent` where the data has no row — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | -| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T}`$, `Process_maintenance_cover` is `absent` where the data has no row — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | -| $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | -| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | -| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$, `Outage_line` is `absent` where the data has no row — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `Outage_transformer` is `absent` where the data has no row — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T},\ \mathrm{period}: \mathcal{T} \to \mathcal{Y}`$, `Generator_maintenance_cover` is `absent` where the data has no row, `Link_maintenance_cover` is `absent` where the data has no row, `Process_maintenance_cover` is `absent` where the data has no row — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{G} \to \mathcal{N},\ \mathrm{bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{bus}: \mathcal{O} \to \mathcal{N},\ \mathrm{bus}: \mathcal{R} \to \mathcal{N},\ \mathrm{bus}: \mathcal{D} \to \mathcal{N},\ \mathrm{bus}: \mathcal{S} \to \mathcal{N},\ \mathrm{bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{bus}: \mathcal{V} \to \mathcal{N},\ \mathrm{bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_maintenance\_cover} \subseteq \Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{T},\ \mathrm{carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{bus}: \mathcal{G} \to \mathcal{N}`$, `Generator_maintenance_cover` is `absent` where the data has no row — generating units, each on one bus | +| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_maintenance\_cover} \subseteq \Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{T},\ \mathrm{carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{link}: \mathcal{O} \to \mathcal{L}`$, `Link_maintenance_cover` is `absent` where the data has no row — controllable connections, each from one bus to the buses it delivers to | +| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{link}: \mathcal{O} \to \mathcal{L},\ \mathrm{bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_maintenance\_cover} \subseteq \Xi \times \mathcal{J} \times \mathcal{T} \times \mathcal{T},\ \mathrm{carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{process}: \mathcal{R} \to \mathcal{J}`$, `Process_maintenance_cover` is `absent` where the data has no row — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{process}: \mathcal{R} \to \mathcal{J},\ \mathrm{bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | +| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | +| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | +| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | +| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{K} \to \mathcal{N},\ \mathrm{line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$, `outage[line]` is `absent` where the data has no row — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{M} \to \mathcal{N},\ \mathrm{transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `outage[transformer]` is `absent` where the data has no row — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep. Each period has its own basis, of the branches that stand in it; a label is a position in that period's basis, so one label names a different cycle in another period | -| $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `Outage_line` is `absent` where the data has no row, `Outage_transformer` is `absent` where the data has no row — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | +| $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `outage[line]` is `absent` where the data has no row, `outage[transformer]` is `absent` where the data has no row — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | | $`\mathcal{B}`$ | index $`b`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{I}`$ | index $`i`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{carrier}: \mathcal{V} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -5956,9 +5956,9 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | | $`t \boxminus_{v} k`$ | `edge=v` — $`t-k`$, and $`v`$ past the edge | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | | $`\lvert \mathcal{T} \rvert`$ | the size of $`\mathcal{T}`$; $`\lvert \mathcal{T} \rvert - 1`$ is the last position | ### `Generator_com_ext` @@ -6022,12 +6022,12 @@ Generator_ramps_from_previous: period's first, and the horizon's first where the generator comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init)) ``` ```math -\mathrm{prev}_{g,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \qquad \forall\, g \in \mathcal{G},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}_{g,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \qquad \forall\, g \in \mathcal{G},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Link_com_ext` @@ -6091,12 +6091,12 @@ Link_ramps_from_previous: period's first, and the horizon's first where the link comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init)) ``` ```math -\mathrm{prev}^{f}_{l,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \qquad \forall\, l \in \mathcal{L},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}^{f}_{l,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \qquad \forall\, l \in \mathcal{L},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Process_com_ext` @@ -6160,12 +6160,12 @@ Process_ramps_from_previous: period's first, and the horizon's first where the process comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init)) ``` ```math -\mathrm{prev}^{z}_{j,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \qquad \forall\, j \in \mathcal{J},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}^{z}_{j,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \qquad \forall\, j \in \mathcal{J},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `StorageUnit_fix` @@ -7170,17 +7170,17 @@ StorageUnit_charge_carried_in: when: StorageUnit_cyclic_state_of_charge_per_period expression: >- StorageUnit_retention - * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: when: >- StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND position(snapshot, within=snapshot_period[period]) == 0 + AND position(snapshot, within=snapshot[period]) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) ``` ```math -\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \neg \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{\xi,s} \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \mathrm{reset}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \neg \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{\xi,s} \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \mathrm{reset}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = 0 \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_energy_carried_in` @@ -7211,15 +7211,15 @@ Store_energy_carried_in: expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: - when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot_period[period]) == 0 + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot[period]) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) ``` ```math -\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Link_output_arrival` @@ -7238,12 +7238,12 @@ Link_output_arrival: cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot_period[period]) * Link_efficiency - otherwise: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot_period[period]) * Link_efficiency + expression: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot[period]) * Link_efficiency + otherwise: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot[period]) * Link_efficiency ``` ```math -\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,t,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,t,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{period}(t)} \mathrm{d}^{f},\mathrm{link}(o)} \cdot \eta_{\xi,t,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{period}(t)} \mathrm{d}^{f},\mathrm{link}(o)} \cdot \eta_{\xi,t,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` ### `Process_output_arrival` @@ -7262,12 +7262,12 @@ Process_output_arrival: cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot_period[period]) * Process_rate - otherwise: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot_period[period]) * Process_rate + expression: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot[period]) * Process_rate + otherwise: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot[period]) * Process_rate ``` ```math -\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,t,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,t,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{period}(t)} \mathrm{d}^{z},\mathrm{process}(r)} \cdot \alpha_{\xi,t,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{period}(t)} \mathrm{d}^{z},\mathrm{process}(r)} \cdot \alpha_{\xi,t,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` ### `GlobalConstraint_energy_weight` @@ -7282,12 +7282,12 @@ GlobalConstraint_energy_weight: cases: counted: when: GlobalConstraint_counts_snapshot - expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period[period]) + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot[period]) otherwise: 0 ``` ```math -\mathit{w}^{\mathrm{gc}}_{\xi,i,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{in}_{\xi,i,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T} +\mathit{w}^{\mathrm{gc}}_{\xi,i,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{in}_{\xi,i,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T} ``` ### `StorageUnit_last_counted_active` @@ -7325,8 +7325,8 @@ StorageUnit_closing_weight: dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: - when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_last_counted_active @@ -7334,7 +7334,7 @@ StorageUnit_closing_weight: ``` ```math -\mathit{w}^{h}_{\xi,i,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}^{h}_{\xi,i,t,s} & \text{if } \neg \mathrm{reset}_{\xi,s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ s \in \mathcal{S} +\mathit{w}^{h}_{\xi,i,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{period}(t)} \rvert - 1 \\ \mathit{last}^{h}_{\xi,i,t,s} & \text{if } \neg \mathrm{reset}_{\xi,s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ s \in \mathcal{S} ``` ### `Store_last_counted_active` @@ -7372,8 +7372,8 @@ Store_closing_weight: dims: [scenario, global_constraint, snapshot, store] cases: per_period: - when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT Store_e_initial_per_period expression: Store_last_counted_active @@ -7381,7 +7381,7 @@ Store_closing_weight: ``` ```math -\mathit{w}^{e}_{\xi,i,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}^{e}_{\xi,i,t,v} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ v \in \mathcal{V} +\mathit{w}^{e}_{\xi,i,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{period}(t)} \rvert - 1 \\ \mathit{last}^{e}_{\xi,i,t,v} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ i \in \mathcal{I},\ t \in \mathcal{T},\ v \in \mathcal{V} ``` ### `Generator_primary_energy` @@ -7659,12 +7659,12 @@ tech_capacity_expansion: ```yaml Generator_opex: expression: >- - sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) - + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) ``` ```math -\mathit{Generator\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Generator\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Generator_commitment_opex` @@ -7672,13 +7672,13 @@ Generator_opex: ```yaml Generator_commitment_opex: expression: >- - sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) ``` ```math -\mathit{Generator\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,t,g} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,t,g} \qquad \forall\, \xi \in \Xi +\mathit{Generator\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,t,g} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,t,g} \qquad \forall\, \xi \in \Xi ``` ### `Link_opex` @@ -7686,12 +7686,12 @@ Generator_commitment_opex: ```yaml Link_opex: expression: >- - sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) - + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) ``` ```math -\mathit{Link\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Link\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Link_commitment_opex` @@ -7699,13 +7699,13 @@ Link_opex: ```yaml Link_commitment_opex: expression: >- - sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) ``` ```math -\mathit{Link\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,t,l} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,t,l} \qquad \forall\, \xi \in \Xi +\mathit{Link\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,t,l} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,t,l} \qquad \forall\, \xi \in \Xi ``` ### `Process_opex` @@ -7713,12 +7713,12 @@ Link_commitment_opex: ```yaml Process_opex: expression: >- - sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) - + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) ``` ```math -\mathit{Process\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Process\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Process_commitment_opex` @@ -7726,13 +7726,13 @@ Process_opex: ```yaml Process_commitment_opex: expression: >- - sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) ``` ```math -\mathit{Process\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,t,j} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,t,j} \qquad \forall\, \xi \in \Xi +\mathit{Process\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,t,j} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,t,j} \qquad \forall\, \xi \in \Xi ``` ### `StorageUnit_opex` @@ -7740,14 +7740,14 @@ Process_commitment_opex: ```yaml StorageUnit_opex: expression: >- - sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) + sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) ``` ```math -\mathit{StorageUnit\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{StorageUnit\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `Store_opex` @@ -7755,13 +7755,13 @@ StorageUnit_opex: ```yaml Store_opex: expression: >- - sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) + sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) ``` ```math -\mathit{Store\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Store\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` ### `scenario_opex` @@ -7906,11 +7906,11 @@ total_cost: ```yaml Generator_additions: expression: >- - sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=Generator_carrier[carrier]) + sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=generator[carrier]) ``` ```math -\mathit{Generator\_additions}_{i,y} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Generator\_additions}_{i,y} = \sum_{g \in \mathcal{G} \,:\, \mathrm{carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `Line_additions` @@ -7918,11 +7918,11 @@ Generator_additions: ```yaml Line_additions: expression: >- - sum(Line_s_nom_ext * Line_first_active, over=line, by=Line_carrier[carrier]) + sum(Line_s_nom_ext * Line_first_active, over=line, by=line[carrier]) ``` ```math -\mathit{Line\_additions}_{i,y} = \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Line\_additions}_{i,y} = \sum_{k \in \mathcal{K} \,:\, \mathrm{carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `Link_additions` @@ -7930,11 +7930,11 @@ Line_additions: ```yaml Link_additions: expression: >- - sum(Link_p_nom_ext * Link_first_active, over=link, by=Link_carrier[carrier]) + sum(Link_p_nom_ext * Link_first_active, over=link, by=link[carrier]) ``` ```math -\mathit{Link\_additions}_{i,y} = \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Link\_additions}_{i,y} = \sum_{l \in \mathcal{L} \,:\, \mathrm{carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `Process_additions` @@ -7942,11 +7942,11 @@ Link_additions: ```yaml Process_additions: expression: >- - sum(Process_p_nom_ext * Process_first_active, over=process, by=Process_carrier[carrier]) + sum(Process_p_nom_ext * Process_first_active, over=process, by=process[carrier]) ``` ```math -\mathit{Process\_additions}_{i,y} = \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Process\_additions}_{i,y} = \sum_{j \in \mathcal{J} \,:\, \mathrm{carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `StorageUnit_additions` @@ -7954,11 +7954,11 @@ Process_additions: ```yaml StorageUnit_additions: expression: >- - sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=StorageUnit_carrier[carrier]) + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=storage_unit[carrier]) ``` ```math -\mathit{StorageUnit\_additions}_{i,y} = \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{StorageUnit\_additions}_{i,y} = \sum_{s \in \mathcal{S} \,:\, \mathrm{carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `Store_additions` @@ -7966,11 +7966,11 @@ StorageUnit_additions: ```yaml Store_additions: expression: >- - sum(Store_e_nom_ext * Store_first_active, over=store, by=Store_carrier[carrier]) + sum(Store_e_nom_ext * Store_first_active, over=store, by=store[carrier]) ``` ```math -\mathit{Store\_additions}_{i,y} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Store\_additions}_{i,y} = \sum_{v \in \mathcal{V} \,:\, \mathrm{carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` ### `Carrier_additions` @@ -8074,23 +8074,23 @@ Outage_s: transformer's flow before it goes out dims: [scenario, snapshot, outage] cases: - line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line[line])" } - otherwise: at(Transformer_s_monitored, by=Outage_transformer[transformer]) + line: { when: "outage[line]", expression: "at(Line_s_monitored, by=outage[line])" } + otherwise: at(Transformer_s_monitored, by=outage[transformer]) ``` ```math -\hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{Outage\_line}(\kappa)} & \text{if } \mathrm{Outage\_line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{Outage\_transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} +\hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{line}(\kappa)} & \text{if } \mathrm{line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} ``` ### `Generator_injection` ```yaml Generator_injection: - expression: sum(Generator_sign * Generator_p, over=generator, by=Generator_bus[bus]) + expression: sum(Generator_sign * Generator_p, over=generator, by=generator[bus]) ``` ```math -\mathit{Generator\_injection}_{n,\xi,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Generator\_injection}_{n,\xi,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Line_injection` @@ -8098,14 +8098,14 @@ Generator_injection: ```yaml Line_injection: expression: >- - -sum(Line_s, over=line, by=Line_bus0[bus]) - + sum(Line_s, over=line, by=Line_bus1[bus]) - - (0.5 * sum(Line_loss, over=line, by=Line_bus0[bus])) - - (0.5 * sum(Line_loss, over=line, by=Line_bus1[bus])) + -sum(Line_s, over=line, by=line[bus0]) + + sum(Line_s, over=line, by=line[bus1]) + - (0.5 * sum(Line_loss, over=line, by=line[bus0])) + - (0.5 * sum(Line_loss, over=line, by=line[bus1])) ``` ```math -\mathit{Line\_injection}_{n,\xi,t} = -\left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Line\_injection}_{n,\xi,t} = -\left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} \ell_{\xi,t,k} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Link_injection` @@ -8113,22 +8113,22 @@ Line_injection: ```yaml Link_injection: expression: >- - -sum(Link_p, over=link, by=Link_bus0[bus]) - + sum(Link_output_arrival, over=link_output, by=Link_output_bus[bus]) + -sum(Link_p, over=link, by=link[bus0]) + + sum(Link_output_arrival, over=link_output, by=link_output[bus]) ``` ```math -\mathit{Link\_injection}_{n,\xi,t} = -\left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Link\_injection}_{n,\xi,t} = -\left( \sum_{l \in \mathcal{L} \,:\, \mathrm{bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{bus}(o) = n} \overrightarrow{f}_{\xi,t,o} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Load_injection` ```yaml -Load_injection: sum(Load_demand, over=load, by=Load_bus[bus]) +Load_injection: sum(Load_demand, over=load, by=load[bus]) ``` ```math -\mathrm{Load\_injection}_{n,\xi,t} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{Load\_injection}_{n,\xi,t} = \sum_{d \in \mathcal{D} \,:\, \mathrm{bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Process_injection` @@ -8136,11 +8136,11 @@ Load_injection: sum(Load_demand, over=load, by=Load_bus[bus]) ```yaml Process_injection: expression: >- - sum(Process_output_arrival, over=process_output, by=Process_output_bus[bus]) + sum(Process_output_arrival, over=process_output, by=process_output[bus]) ``` ```math -\mathit{Process\_injection}_{n,\xi,t} = \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Process\_injection}_{n,\xi,t} = \sum_{r \in \mathcal{R} \,:\, \mathrm{bus}(r) = n} \overrightarrow{z}_{\xi,t,r} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `StorageUnit_injection` @@ -8148,21 +8148,21 @@ Process_injection: ```yaml StorageUnit_injection: expression: >- - sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=StorageUnit_bus[bus]) + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=storage_unit[bus]) ``` ```math -\mathit{StorageUnit\_injection}_{n,\xi,t} = \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{StorageUnit\_injection}_{n,\xi,t} = \sum_{s \in \mathcal{S} \,:\, \mathrm{bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Store_injection` ```yaml -Store_injection: sum(Store_sign * Store_p, over=store, by=Store_bus[bus]) +Store_injection: sum(Store_sign * Store_p, over=store, by=store[bus]) ``` ```math -\mathit{Store\_injection}_{n,\xi,t} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Store\_injection}_{n,\xi,t} = \sum_{v \in \mathcal{V} \,:\, \mathrm{bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Transformer_injection` @@ -8170,14 +8170,14 @@ Store_injection: sum(Store_sign * Store_p, over=store, by=Store_bus[bus]) ```yaml Transformer_injection: expression: >- - -sum(Transformer_s, over=transformer, by=Transformer_bus0[bus]) - + sum(Transformer_s, over=transformer, by=Transformer_bus1[bus]) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus0[bus])) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus1[bus])) + -sum(Transformer_s, over=transformer, by=transformer[bus0]) + + sum(Transformer_s, over=transformer, by=transformer[bus1]) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus0])) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus1])) ``` ```math -\mathit{Transformer\_injection}_{n,\xi,t} = -\left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Transformer\_injection}_{n,\xi,t} = -\left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Bus_injection` @@ -8207,11 +8207,11 @@ Bus_injection: ### `Line_angle_sum` ```yaml -Line_angle_sum: sum(Line_s * at(Line_cycle_weight, by=snapshot_period[period]), over=line) +Line_angle_sum: sum(Line_s * at(Line_cycle_weight, by=snapshot[period]), over=line) ``` ```math -\mathit{Line\_angle\_sum}_{c,\xi,t} = \sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{snapshot\_period}(t),k,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Line\_angle\_sum}_{c,\xi,t} = \sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{period}(t),k,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Transformer_angle_sum` @@ -8219,13 +8219,13 @@ Line_angle_sum: sum(Line_s * at(Line_cycle_weight, by=snapshot_period[period]), ```yaml Transformer_angle_sum: expression: >- - sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot_period[period]), over=transformer) + sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot[period]), over=transformer) + sum(Transformer_phase_shift_weight, over=transformer) - + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot_period[period]), over=transformer) + + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot[period]), over=transformer) ``` ```math -\mathit{Transformer\_angle\_sum}_{c,\xi,t} = \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{snapshot\_period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{snapshot\_period}(t),m,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Transformer\_angle\_sum}_{c,\xi,t} = \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{period}(t),m,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} ``` ### `Cycle_angle_sum` @@ -10904,12 +10904,12 @@ Line_v_ang_lower: voltage angle difference across it, `x_pu_eff` times the flow in radians, is the negative of its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s >= -Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff ``` ```math -s_{\xi,t,k} \ge \frac{-\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{Line\_carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} \ge \frac{-\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-v_ang-upper` @@ -10922,12 +10922,12 @@ Line_v_ang_upper: `Line-v_ang-upper` — an AC line carries at most the flow at which the voltage angle difference across it reaches its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s <= Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff ``` ```math -s_{\xi,t,k} \le \frac{\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{Line\_carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} \le \frac{\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} ``` ### `Line-loss_upper` @@ -11272,12 +11272,12 @@ Line_fix_s_lower_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom ``` ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` ### `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -11295,12 +11295,12 @@ Line_fix_s_upper_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom ``` ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` ### `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -11319,12 +11319,12 @@ Line_ext_s_lower_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext ``` ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` ### `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -11342,12 +11342,12 @@ Line_ext_s_upper_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext ``` ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -11365,12 +11365,12 @@ Transformer_fix_s_lower_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom ``` ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` ### `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -11388,12 +11388,12 @@ Transformer_fix_s_upper_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom ``` ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -11412,12 +11412,12 @@ Transformer_ext_s_lower_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` ### `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -11436,12 +11436,12 @@ Transformer_ext_s_upper_security: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext ``` ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` ### `Kirchhoff-Voltage-Law` @@ -11465,7 +11465,7 @@ Kirchhoff_Voltage_Law: ``` ```math -\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{snapshot\_period}(t),k,c} + \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{snapshot\_period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{snapshot\_period}(t),m,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} +\sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{period}(t),k,c} + \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{period}(t),m,c} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ c \in \mathcal{C} ``` ### `Generator-p-ramp_limit_up` @@ -12363,7 +12363,7 @@ Bus_nodal_balance: ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} + \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} + \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} - \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} \ell_{\xi,t,k} \right) - \left( \sum_{l \in \mathcal{L} \,:\, \mathrm{bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{bus}(o) = n} \overrightarrow{f}_{\xi,t,o} + \sum_{d \in \mathcal{D} \,:\, \mathrm{bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} + \sum_{r \in \mathcal{R} \,:\, \mathrm{bus}(r) = n} \overrightarrow{z}_{\xi,t,r} + \sum_{s \in \mathcal{S} \,:\, \mathrm{bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) + \sum_{v \in \mathcal{V} \,:\, \mathrm{bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} - \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} ``` ### `Carrier-growth_limit` @@ -12918,7 +12918,7 @@ StorageUnit_primary_energy_carried_over_has_unit_years: ```yaml StorageUnit_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — @@ -12926,7 +12926,7 @@ StorageUnit_operational_limit_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{b}^{h}_{\xi,i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S},\ i \in \mathcal{I} \,:\, \mathrm{b}^{h}_{\xi,i,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,i,t} ``` ### `Store_primary_energy_per_period_closes_over_the_horizon` @@ -12966,7 +12966,7 @@ Store_primary_energy_carried_over_has_unit_years: ```yaml Store_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — @@ -12974,7 +12974,7 @@ Store_operational_limit_carried_over_has_unit_years: ``` ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{\xi,i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ i \in \mathcal{I} \,:\, \mathrm{b}^{e}_{\xi,i,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,i,t} ``` ### `Generator_marginal_cost_quadratic_without_risk_preference` diff --git a/docs/examples/pypsa/generator.md b/docs/examples/pypsa/generator.md index 00eee14be..cc298a081 100644 --- a/docs/examples/pypsa/generator.md +++ b/docs/examples/pypsa/generator.md @@ -51,19 +51,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Generator_carrier: - description: the carrier a generator converts from - key: generator - values: carrier - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + generator: + carrier: + description: the carrier a generator converts from + into: carrier + bus: + description: the bus a generator sits on + into: bus parameters: Generator_p_nom: @@ -223,18 +222,18 @@ expressions: adds_to: tech_capacity_expansion Generator_opex: expression: >- - sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) - + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) adds_to: scenario_opex Generator_capex: expression: sum(scenario_weight * Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) adds_to: total_cost Generator_additions: expression: >- - sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=Generator_carrier[carrier]) + sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=generator[carrier]) adds_to: Carrier_additions Generator_injection: - expression: sum(Generator_sign * Generator_p, over=generator, by=Generator_bus[bus]) + expression: sum(Generator_sign * Generator_p, over=generator, by=generator[bus]) adds_to: Bus_injection conditions: @@ -339,12 +338,12 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{Generator\_bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{G} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{carrier}: \mathcal{G} \to \mathcal{I},\ \mathrm{bus}: \mathcal{G} \to \mathcal{N}`$ — generating units, each on one bus | | $`\mathcal{L}`$ | index $`l`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Generator\_carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{G} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -466,7 +465,7 @@ P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \ **`Generator_opex`** ```math -\mathit{Generator\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Generator\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot \mathrm{c}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} p_{\xi,t,g} \cdot p_{\xi,t,g} \cdot \mathrm{c}^{(2)}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` **`Generator_capex`** @@ -478,13 +477,13 @@ P_{g} \in \mathbb{R} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{ext}_{g} \ **`Generator_additions`** ```math -\mathit{Generator\_additions}_{i,y} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Generator\_additions}_{i,y} = \sum_{g \in \mathcal{G} \,:\, \mathrm{carrier}(g) = i} P_{g} \cdot \mathrm{new}_{y,g} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`Generator_injection`** ```math -\mathit{Generator\_injection}_{n,\xi,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{Generator\_bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Generator\_injection}_{n,\xi,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = n} \mathrm{sgn}_{g} \cdot p_{\xi,t,g} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to diff --git a/docs/examples/pypsa/generator_commitment.md b/docs/examples/pypsa/generator_commitment.md index 60de7a02b..daca106e0 100644 --- a/docs/examples/pypsa/generator_commitment.md +++ b/docs/examples/pypsa/generator_commitment.md @@ -47,11 +47,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Generator_committable: @@ -153,7 +153,7 @@ expressions: otherwise: shift(Generator_status, along=snapshot, offset=1) Generator_commitment_opex: expression: >- - sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) adds_to: scenario_opex @@ -341,9 +341,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{G}`$ | index $`g`$ — `generator` — generating units, each on one bus | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -433,7 +433,7 @@ u_{\xi,t,g} \ge 0, u_{\xi,t,g} \in \mathbb{Z} \qquad \forall\, \xi \in \Xi,\ t \ **`Generator_commitment_opex`** ```math -\mathit{Generator\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,t,g} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,t,g} \qquad \forall\, \xi \in \Xi +\mathit{Generator\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} u_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{on}}_{\xi,t,g} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{up}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{up}}_{\xi,t,g} + \sum_{t \in \mathcal{T}} \sum_{g \in \mathcal{G}} \mathit{dn}_{\xi,t,g} \cdot \mathrm{c}^{\mathrm{dn}}_{\xi,t,g} \qquad \forall\, \xi \in \Xi ``` #### Subject to diff --git a/docs/examples/pypsa/generator_maintenance.md b/docs/examples/pypsa/generator_maintenance.md index e4714788c..73ceb8e17 100644 --- a/docs/examples/pypsa/generator_maintenance.md +++ b/docs/examples/pypsa/generator_maintenance.md @@ -34,15 +34,14 @@ dimensions: generator: description: generating units, each on one bus -relations: +sets: Generator_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a generator that is - not maintainable - key: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a generator that is not maintainable + dims: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/docs/examples/pypsa/generator_ramping.md b/docs/examples/pypsa/generator_ramping.md index ad135cbb1..25d289067 100644 --- a/docs/examples/pypsa/generator_ramping.md +++ b/docs/examples/pypsa/generator_ramping.md @@ -44,11 +44,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Generator_ramp_limit_up: @@ -153,7 +153,7 @@ conditions: period's first, and the horizon's first where the generator comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init)) constraints: @@ -286,9 +286,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{G}`$ | index $`g`$ — `generator` — generating units, each on one bus | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -323,14 +323,14 @@ assumptions: | Symbol | Meaning | |---|---| | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | #### Conditions **`Generator_ramps_from_previous`** ```math -\mathrm{prev}_{g,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \qquad \forall\, g \in \mathcal{G},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}_{g,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{0}_{\xi,g} = 0 \vee \mathrm{p}^{0}_{\xi,g} \text{ is defined} \right) \qquad \forall\, g \in \mathcal{G},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Definitions diff --git a/docs/examples/pypsa/line.md b/docs/examples/pypsa/line.md index c9c011f15..91f05f4ae 100644 --- a/docs/examples/pypsa/line.md +++ b/docs/examples/pypsa/line.md @@ -54,23 +54,21 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Line_carrier: - description: the carrier a line carries - key: line - values: carrier - Line_bus0: - description: the bus a line's flow is measured at - key: line - values: bus - Line_bus1: - description: the bus at a line's other end - key: line - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + line: + carrier: + description: the carrier a line carries + into: carrier + bus0: + description: the bus a line's flow is measured at + into: bus + bus1: + description: the bus at a line's other end + into: bus parameters: Line_active: @@ -238,7 +236,7 @@ expressions: adds_to: total_cost Line_additions: expression: >- - sum(Line_s_nom_ext * Line_first_active, over=line, by=Line_carrier[carrier]) + sum(Line_s_nom_ext * Line_first_active, over=line, by=line[carrier]) adds_to: Carrier_additions Line_s_monitored: description: >- @@ -251,13 +249,13 @@ expressions: otherwise: 0 Line_injection: expression: >- - -sum(Line_s, over=line, by=Line_bus0[bus]) - + sum(Line_s, over=line, by=Line_bus1[bus]) - - (0.5 * sum(Line_loss, over=line, by=Line_bus0[bus])) - - (0.5 * sum(Line_loss, over=line, by=Line_bus1[bus])) + -sum(Line_s, over=line, by=line[bus0]) + + sum(Line_s, over=line, by=line[bus1]) + - (0.5 * sum(Line_loss, over=line, by=line[bus0])) + - (0.5 * sum(Line_loss, over=line, by=line[bus1])) adds_to: Bus_injection Line_angle_sum: - expression: sum(Line_s * at(Line_cycle_weight, by=snapshot_period[period]), over=line) + expression: sum(Line_s * at(Line_cycle_weight, by=snapshot[period]), over=line) adds_to: Cycle_angle_sum conditions: @@ -319,14 +317,14 @@ constraints: voltage angle difference across it, `x_pu_eff` times the flow in radians, is the negative of its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s >= -Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_v_ang_upper: description: >- `Line-v_ang-upper` — an AC line carries at most the flow at which the voltage angle difference across it reaches its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s <= Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_loss_upper: description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" @@ -370,14 +368,14 @@ constraints: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{Line\_bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{Line\_bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{K} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{carrier}: \mathcal{K} \to \mathcal{I},\ \mathrm{bus0}: \mathcal{K} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{K} \to \mathcal{N}`$ — passive branches, each between two buses, their flow set by impedance | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep. Each period has its own basis, of the branches that stand in it; a label is a position in that period's basis, so one label names a different cycle in another period | | $`\mathcal{E}`$ | index $`e`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Line\_carrier}: \mathcal{K} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{K} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -497,7 +495,7 @@ N^{s}_{k} \ge 0, N^{s}_{k} \in \mathbb{Z} \qquad \forall\, k \in \mathcal{K} \,: **`Line_additions`** ```math -\mathit{Line\_additions}_{i,y} = \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Line\_additions}_{i,y} = \sum_{k \in \mathcal{K} \,:\, \mathrm{carrier}(k) = i} S_{k} \cdot \mathrm{new}^{s}_{y,k} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`Line_s_monitored`** @@ -509,13 +507,13 @@ N^{s}_{k} \ge 0, N^{s}_{k} \in \mathbb{Z} \qquad \forall\, k \in \mathcal{K} \,: **`Line_injection`** ```math -\mathit{Line\_injection}_{n,\xi,t} = -\left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{Line\_bus1}(k) = n} \ell_{\xi,t,k} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Line\_injection}_{n,\xi,t} = -\left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} s_{\xi,t,k} \right) + \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} s_{\xi,t,k} - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus0}(k) = n} \ell_{\xi,t,k} \right) - 0.5 \cdot \left( \sum_{k \in \mathcal{K} \,:\, \mathrm{bus1}(k) = n} \ell_{\xi,t,k} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` **`Line_angle_sum`** ```math -\mathit{Line\_angle\_sum}_{c,\xi,t} = \sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{snapshot\_period}(t),k,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Line\_angle\_sum}_{c,\xi,t} = \sum_{k \in \mathcal{K}} s_{\xi,t,k} \cdot \mathrm{x}_{\mathrm{period}(t),k,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to @@ -577,13 +575,13 @@ s_{\xi,t,k} = \mathrm{s}^{\mathrm{set}}_{\xi,t,k} \qquad \forall\, \xi \in \Xi,\ **`Line_v_ang_lower`** ```math -s_{\xi,t,k} \ge \frac{-\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{Line\_carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} \ge \frac{-\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} ``` **`Line_v_ang_upper`** ```math -s_{\xi,t,k} \le \frac{\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{Line\_carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} +s_{\xi,t,k} \le \frac{\overline{\delta}_{\xi,k} \cdot \frac{3.141592653589793}{180}}{\mathrm{x}^{\mathrm{eff}}_{\xi,k}} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K} \,:\, \overline{\delta}_{\xi,k} \text{ is defined} \wedge \mathrm{carrier}(k) = \text{'}\mathrm{AC}\text{'} \wedge \mathrm{on}^{s}_{t,k} ``` **`Line_loss_upper`** diff --git a/docs/examples/pypsa/link.md b/docs/examples/pypsa/link.md index b4089d6c1..4ca234fdb 100644 --- a/docs/examples/pypsa/link.md +++ b/docs/examples/pypsa/link.md @@ -55,30 +55,28 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Link_carrier: - description: the carrier a link converts from - key: link - values: carrier - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + link: + carrier: + description: the carrier a link converts from + into: carrier + bus0: + description: the bus a link leaves + into: bus + link_output: + link: + description: the link an output port belongs to + into: link + bus: + description: >- + the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. + A link of three output ports is three labels here rather than a third + relation, so the file states any number of them + into: bus parameters: Link_p_nom: @@ -260,8 +258,8 @@ expressions: cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot_period[period]) * Link_efficiency - otherwise: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot_period[period]) * Link_efficiency + expression: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot[period]) * Link_efficiency + otherwise: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot[period]) * Link_efficiency Link_transmission_volume_expansion: expression: sum(Link_p_nom_ext * Link_volume_weight, over=link) adds_to: transmission_volume_expansion @@ -273,20 +271,20 @@ expressions: adds_to: tech_capacity_expansion Link_opex: expression: >- - sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) - + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) adds_to: scenario_opex Link_capex: expression: sum(scenario_weight * Link_p_nom_ext * Link_capital_cost * Link_capital_weight) adds_to: total_cost Link_additions: expression: >- - sum(Link_p_nom_ext * Link_first_active, over=link, by=Link_carrier[carrier]) + sum(Link_p_nom_ext * Link_first_active, over=link, by=link[carrier]) adds_to: Carrier_additions Link_injection: expression: >- - -sum(Link_p, over=link, by=Link_bus0[bus]) - + sum(Link_output_arrival, over=link_output, by=Link_output_bus[bus]) + -sum(Link_p, over=link, by=link[bus0]) + + sum(Link_output_arrival, over=link_output, by=link_output[bus]) adds_to: Bus_injection conditions: @@ -390,13 +388,13 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{Link\_bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{Link\_output\_link}: \mathcal{O} \to \mathcal{L},\ \mathrm{Link\_output\_bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{bus}: \mathcal{O} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{L}`$ | index $`l`$ — `link` with $`\mathrm{carrier}: \mathcal{L} \to \mathcal{I},\ \mathrm{bus0}: \mathcal{L} \to \mathcal{N},\ \mathrm{link}: \mathcal{O} \to \mathcal{L}`$ — controllable connections, each from one bus to the buses it delivers to | +| $`\mathcal{O}`$ | index $`o`$ — `link_output` with $`\mathrm{link}: \mathcal{O} \to \mathcal{L},\ \mathrm{bus}: \mathcal{O} \to \mathcal{N}`$ — a link's output ports, one label per port a link declares — PyPSA's `bus1`, `bus2`, … columns read long, so a link of any number of output ports is one term in the balance, data prep | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Link\_carrier}: \mathcal{L} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{L} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -462,7 +460,7 @@ assumptions: |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | | $`t \boxminus_{v} k`$ | `edge=v` — $`t-k`$, and $`v`$ past the edge | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | #### Conditions @@ -509,7 +507,7 @@ F_{l} \in \mathbb{R} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{ **`Link_output_arrival`** ```math -\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,t,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{f},\mathrm{Link\_output\_link}(o)} \cdot \eta_{\xi,t,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} +\overrightarrow{f}_{\xi,t,o} = \begin{cases} f_{\xi,t \ominus^{\mathrm{period}(t)} \mathrm{d}^{f},\mathrm{link}(o)} \cdot \eta_{\xi,t,o} & \text{if } \mathrm{cyc}^{f}_{o} \\ f_{\xi,t \boxminus_{0}^{\mathrm{period}(t)} \mathrm{d}^{f},\mathrm{link}(o)} \cdot \eta_{\xi,t,o} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ o \in \mathcal{O} ``` **`Link_transmission_volume_expansion`** @@ -533,7 +531,7 @@ F_{l} \in \mathbb{R} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{ **`Link_opex`** ```math -\mathit{Link\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Link\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot \mathrm{c}^{f}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} f_{\xi,t,l} \cdot f_{\xi,t,l} \cdot \mathrm{c}^{f,(2)}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` **`Link_capex`** @@ -545,13 +543,13 @@ F_{l} \in \mathbb{R} \qquad \forall\, l \in \mathcal{L} \,:\, \mathrm{ext}^{f}_{ **`Link_additions`** ```math -\mathit{Link\_additions}_{i,y} = \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Link\_additions}_{i,y} = \sum_{l \in \mathcal{L} \,:\, \mathrm{carrier}(l) = i} F_{l} \cdot \mathrm{new}^{f}_{y,l} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`Link_injection`** ```math -\mathit{Link\_injection}_{n,\xi,t} = -\left( \sum_{l \in \mathcal{L} \,:\, \mathrm{Link\_bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{Link\_output\_bus}(o) = n} \overrightarrow{f}_{\xi,t,o} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Link\_injection}_{n,\xi,t} = -\left( \sum_{l \in \mathcal{L} \,:\, \mathrm{bus0}(l) = n} f_{\xi,t,l} \right) + \sum_{o \in \mathcal{O} \,:\, \mathrm{bus}(o) = n} \overrightarrow{f}_{\xi,t,o} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to diff --git a/docs/examples/pypsa/link_commitment.md b/docs/examples/pypsa/link_commitment.md index cf27e4f88..dec94289a 100644 --- a/docs/examples/pypsa/link_commitment.md +++ b/docs/examples/pypsa/link_commitment.md @@ -47,11 +47,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Link_committable: @@ -153,7 +153,7 @@ expressions: otherwise: shift(Link_status, along=snapshot, offset=1) Link_commitment_opex: expression: >- - sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) adds_to: scenario_opex @@ -341,9 +341,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{L}`$ | index $`l`$ — `link` — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -433,7 +433,7 @@ u^{f}_{\xi,t,l} \ge 0, u^{f}_{\xi,t,l} \in \mathbb{Z} \qquad \forall\, \xi \in \ **`Link_commitment_opex`** ```math -\mathit{Link\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,t,l} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,t,l} \qquad \forall\, \xi \in \Xi +\mathit{Link\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} u^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{on}}_{\xi,t,l} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{up}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{up}}_{\xi,t,l} + \sum_{t \in \mathcal{T}} \sum_{l \in \mathcal{L}} \mathit{dn}^{f}_{\xi,t,l} \cdot \mathrm{c}^{f,\mathrm{dn}}_{\xi,t,l} \qquad \forall\, \xi \in \Xi ``` #### Subject to diff --git a/docs/examples/pypsa/link_maintenance.md b/docs/examples/pypsa/link_maintenance.md index 2c8ab7738..6d653e323 100644 --- a/docs/examples/pypsa/link_maintenance.md +++ b/docs/examples/pypsa/link_maintenance.md @@ -34,15 +34,14 @@ dimensions: link: description: controllable connections, each from one bus to the buses it delivers to -relations: +sets: Link_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a link that is - not maintainable - key: { scenario: scenario, link: link, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a link that is not maintainable + dims: { scenario: scenario, link: link, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/docs/examples/pypsa/link_ramping.md b/docs/examples/pypsa/link_ramping.md index 457d01ae7..f6c72c3d9 100644 --- a/docs/examples/pypsa/link_ramping.md +++ b/docs/examples/pypsa/link_ramping.md @@ -44,11 +44,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Link_ramp_limit_up: @@ -153,7 +153,7 @@ conditions: period's first, and the horizon's first where the link comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init)) constraints: @@ -274,9 +274,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{L}`$ | index $`l`$ — `link` — controllable connections, each from one bus to the buses it delivers to | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -311,14 +311,14 @@ assumptions: | Symbol | Meaning | |---|---| | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | #### Conditions **`Link_ramps_from_previous`** ```math -\mathrm{prev}^{f}_{l,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \qquad \forall\, l \in \mathcal{L},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}^{f}_{l,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{f,0}_{\xi,l} = 0 \vee \mathrm{f}^{0}_{\xi,l} \text{ is defined} \right) \qquad \forall\, l \in \mathcal{L},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Definitions diff --git a/docs/examples/pypsa/load.md b/docs/examples/pypsa/load.md index f696d2b68..06b0eab13 100644 --- a/docs/examples/pypsa/load.md +++ b/docs/examples/pypsa/load.md @@ -25,11 +25,11 @@ dimensions: load: description: demands, each on one bus -relations: - Load_bus: - description: the bus a load sits on - key: load - values: bus +properties: + load: + bus: + description: the bus a load sits on + into: bus parameters: Load_p_set: @@ -60,7 +60,7 @@ expressions: active: { when: Load_active, expression: Load_sign * Load_p_set } otherwise: 0 Load_injection: - expression: sum(Load_demand, over=load, by=Load_bus[bus]) + expression: sum(Load_demand, over=load, by=load[bus]) adds_to: Bus_injection ``` @@ -76,8 +76,8 @@ expressions: |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | | $`\mathcal{T}`$ | index $`t`$ — `snapshot` — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{Load\_bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{D} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{D}`$ | index $`d`$ — `load` with $`\mathrm{bus}: \mathcal{D} \to \mathcal{N}`$ — demands, each on one bus | #### Parameters @@ -105,6 +105,6 @@ expressions: **`Load_injection`** ```math -\mathrm{Load\_injection}_{n,\xi,t} = \sum_{d \in \mathcal{D} \,:\, \mathrm{Load\_bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{Load\_injection}_{n,\xi,t} = \sum_{d \in \mathcal{D} \,:\, \mathrm{bus}(d) = n} \check{\mathrm{load}}_{\xi,t,d} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` diff --git a/docs/examples/pypsa/process.md b/docs/examples/pypsa/process.md index d88a98462..cb57d8b4b 100644 --- a/docs/examples/pypsa/process.md +++ b/docs/examples/pypsa/process.md @@ -53,26 +53,25 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Process_carrier: - description: the carrier a process converts from - key: process - values: carrier - Process_output_process: - description: the process a port belongs to - key: process_output - values: process - Process_output_bus: - description: >- - the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … - columns. A process of three ports is three labels here rather than a - third relation, so the file states any number of them - key: process_output - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + process: + carrier: + description: the carrier a process converts from + into: carrier + process_output: + process: + description: the process a port belongs to + into: process + bus: + description: >- + the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … + columns. A process of three ports is three labels here rather than a + third relation, so the file states any number of them + into: bus parameters: Process_p_nom: @@ -238,26 +237,26 @@ expressions: cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot_period[period]) * Process_rate - otherwise: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot_period[period]) * Process_rate + expression: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot[period]) * Process_rate + otherwise: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot[period]) * Process_rate Process_tech_capacity_expansion: expression: sum(Process_p_nom_ext * Process_tech_capacity_weight, over=process) adds_to: tech_capacity_expansion Process_opex: expression: >- - sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) - + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) adds_to: scenario_opex Process_capex: expression: sum(scenario_weight * Process_p_nom_ext * Process_capital_cost * Process_capital_weight) adds_to: total_cost Process_additions: expression: >- - sum(Process_p_nom_ext * Process_first_active, over=process, by=Process_carrier[carrier]) + sum(Process_p_nom_ext * Process_first_active, over=process, by=process[carrier]) adds_to: Carrier_additions Process_injection: expression: >- - sum(Process_output_arrival, over=process_output, by=Process_output_bus[bus]) + sum(Process_output_arrival, over=process_output, by=process_output[bus]) adds_to: Bus_injection conditions: @@ -354,13 +353,13 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | -| $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{Process\_output\_process}: \mathcal{R} \to \mathcal{J},\ \mathrm{Process\_output\_bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{R} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{J}`$ | index $`j`$ — `process` with $`\mathrm{carrier}: \mathcal{J} \to \mathcal{I},\ \mathrm{process}: \mathcal{R} \to \mathcal{J}`$ — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | +| $`\mathcal{R}`$ | index $`r`$ — `process_output` with $`\mathrm{process}: \mathcal{R} \to \mathcal{J},\ \mathrm{bus}: \mathcal{R} \to \mathcal{N}`$ — a process's ports, one label per port a process declares — PyPSA's `bus0`, `bus1`, … each carry a signed `rate`, so a process of any number of ports is one term in the balance, data prep | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Process\_carrier}: \mathcal{J} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{J} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -422,7 +421,7 @@ assumptions: |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | | $`t \boxminus_{v} k`$ | `edge=v` — $`t-k`$, and $`v`$ past the edge | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | #### Conditions @@ -469,7 +468,7 @@ Z_{j} \in \mathbb{R} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{ **`Process_output_arrival`** ```math -\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,t,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{snapshot\_period}(t)} \mathrm{d}^{z},\mathrm{Process\_output\_process}(r)} \cdot \alpha_{\xi,t,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} +\overrightarrow{z}_{\xi,t,r} = \begin{cases} z_{\xi,t \ominus^{\mathrm{period}(t)} \mathrm{d}^{z},\mathrm{process}(r)} \cdot \alpha_{\xi,t,r} & \text{if } \mathrm{cyc}^{z}_{r} \\ z_{\xi,t \boxminus_{0}^{\mathrm{period}(t)} \mathrm{d}^{z},\mathrm{process}(r)} \cdot \alpha_{\xi,t,r} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ r \in \mathcal{R} ``` **`Process_tech_capacity_expansion`** @@ -481,7 +480,7 @@ Z_{j} \in \mathbb{R} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{ **`Process_opex`** ```math -\mathit{Process\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Process\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot \mathrm{c}^{z}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} z_{\xi,t,j} \cdot z_{\xi,t,j} \cdot \mathrm{c}^{z,(2)}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` **`Process_capex`** @@ -493,13 +492,13 @@ Z_{j} \in \mathbb{R} \qquad \forall\, j \in \mathcal{J} \,:\, \mathrm{ext}^{z}_{ **`Process_additions`** ```math -\mathit{Process\_additions}_{i,y} = \sum_{j \in \mathcal{J} \,:\, \mathrm{Process\_carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Process\_additions}_{i,y} = \sum_{j \in \mathcal{J} \,:\, \mathrm{carrier}(j) = i} Z_{j} \cdot \mathrm{new}^{z}_{y,j} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`Process_injection`** ```math -\mathit{Process\_injection}_{n,\xi,t} = \sum_{r \in \mathcal{R} \,:\, \mathrm{Process\_output\_bus}(r) = n} \overrightarrow{z}_{\xi,t,r} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Process\_injection}_{n,\xi,t} = \sum_{r \in \mathcal{R} \,:\, \mathrm{bus}(r) = n} \overrightarrow{z}_{\xi,t,r} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to diff --git a/docs/examples/pypsa/process_commitment.md b/docs/examples/pypsa/process_commitment.md index 62ad99980..793244ce7 100644 --- a/docs/examples/pypsa/process_commitment.md +++ b/docs/examples/pypsa/process_commitment.md @@ -47,11 +47,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Process_committable: @@ -153,7 +153,7 @@ expressions: otherwise: shift(Process_status, along=snapshot, offset=1) Process_commitment_opex: expression: >- - sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) adds_to: scenario_opex @@ -341,9 +341,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{J}`$ | index $`j`$ — `process` — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -433,7 +433,7 @@ u^{z}_{\xi,t,j} \ge 0, u^{z}_{\xi,t,j} \in \mathbb{Z} \qquad \forall\, \xi \in \ **`Process_commitment_opex`** ```math -\mathit{Process\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,t,j} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,t,j} \qquad \forall\, \xi \in \Xi +\mathit{Process\_commitment\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} u^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{on}}_{\xi,t,j} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{up}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{up}}_{\xi,t,j} + \sum_{t \in \mathcal{T}} \sum_{j \in \mathcal{J}} \mathit{dn}^{z}_{\xi,t,j} \cdot \mathrm{c}^{z,\mathrm{dn}}_{\xi,t,j} \qquad \forall\, \xi \in \Xi ``` #### Subject to diff --git a/docs/examples/pypsa/process_maintenance.md b/docs/examples/pypsa/process_maintenance.md index 9ce7c226d..16e98c71f 100644 --- a/docs/examples/pypsa/process_maintenance.md +++ b/docs/examples/pypsa/process_maintenance.md @@ -34,15 +34,14 @@ dimensions: process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate -relations: +sets: Process_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a process that is - not maintainable - key: { scenario: scenario, process: process, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a process that is not maintainable + dims: { scenario: scenario, process: process, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/docs/examples/pypsa/process_ramping.md b/docs/examples/pypsa/process_ramping.md index 29b555d56..6c35738b3 100644 --- a/docs/examples/pypsa/process_ramping.md +++ b/docs/examples/pypsa/process_ramping.md @@ -44,11 +44,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Process_ramp_limit_up: @@ -153,7 +153,7 @@ conditions: period's first, and the horizon's first where the process comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init)) constraints: @@ -280,9 +280,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{J}`$ | index $`j`$ — `process` — generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -317,14 +317,14 @@ assumptions: | Symbol | Meaning | |---|---| | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | #### Conditions **`Process_ramps_from_previous`** ```math -\mathrm{prev}^{z}_{j,\xi,t} \iff \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \qquad \forall\, j \in \mathcal{J},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathrm{prev}^{z}_{j,\xi,t} \iff \mathrm{pos}_{\mathrm{period}(t)}(t) > 0 \vee \mathrm{pos}(t) = 0 \wedge \left( \mathrm{u}^{z,0}_{\xi,j} = 0 \vee \mathrm{z}^{0}_{\xi,j} \text{ is defined} \right) \qquad \forall\, j \in \mathcal{J},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Definitions diff --git a/docs/examples/pypsa/security.md b/docs/examples/pypsa/security.md index 759a577b0..9a1df9187 100644 --- a/docs/examples/pypsa/security.md +++ b/docs/examples/pypsa/security.md @@ -45,21 +45,20 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Outage_line: - description: the line an outage takes out; an outage of a transformer has no row - key: outage - values: line - missing: absent - Outage_transformer: - description: the transformer an outage takes out; an outage of a line has no row - key: outage - values: transformer - missing: absent +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + outage: + line: + description: the line an outage takes out; an outage of a transformer has no row + into: line + missing: absent + transformer: + description: the transformer an outage takes out; an outage of a line has no row + into: transformer + missing: absent parameters: Line_BODF: @@ -87,8 +86,8 @@ expressions: transformer's flow before it goes out dims: [scenario, snapshot, outage] cases: - line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line[line])" } - otherwise: at(Transformer_s_monitored, by=Outage_transformer[transformer]) + line: { when: "outage[line]", expression: "at(Line_s_monitored, by=outage[line])" } + otherwise: at(Transformer_s_monitored, by=outage[transformer]) constraints: Line_fix_s_lower_security: @@ -101,8 +100,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper_security: description: >- `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -113,8 +112,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom Line_ext_s_lower_security: description: >- `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — @@ -126,8 +125,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper_security: description: >- `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -138,8 +137,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext Transformer_fix_s_lower_security: description: >- `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -150,8 +149,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom Transformer_fix_s_upper_security: description: >- `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -162,8 +161,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom Transformer_ext_s_lower_security: description: >- `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -175,8 +174,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_upper_security: description: >- `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -188,8 +187,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext ``` #### Given @@ -212,11 +211,11 @@ constraints: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$, `Outage_line` is `absent` where the data has no row — passive branches, each between two buses, their flow set by impedance | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `Outage_transformer` is `absent` where the data has no row — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | -| $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{Outage\_line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{Outage\_transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `Outage_line` is `absent` where the data has no row, `Outage_transformer` is `absent` where the data has no row — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{K}`$ | index $`k`$ — `line` with $`\mathrm{line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K}`$, `outage[line]` is `absent` where the data has no row — passive branches, each between two buses, their flow set by impedance | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `outage[transformer]` is `absent` where the data has no row — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{K}^{\mathrm{out}}`$ | index $`\kappa`$ — `outage` with $`\mathrm{line}: \mathcal{K}^{\mathrm{out}} \to \mathcal{K},\ \mathrm{transformer}: \mathcal{K}^{\mathrm{out}} \to \mathcal{M}`$, `outage[line]` is `absent` where the data has no row, `outage[transformer]` is `absent` where the data has no row — the passive branches a security-constrained run takes out one at a time — PyPSA's `branch_outages`, each a line or a transformer; none on a plain run | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -236,7 +235,7 @@ constraints: **`Outage_s`** ```math -\hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{Outage\_line}(\kappa)} & \text{if } \mathrm{Outage\_line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{Outage\_transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} +\hat{s}_{\xi,t,\kappa} = \begin{cases} \check{s}_{\xi,t,\mathrm{line}(\kappa)} & \text{if } \mathrm{line}(\kappa) \text{ is defined} \\ \check{\sigma}_{\xi,t,\mathrm{transformer}(\kappa)} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ \kappa \in \mathcal{K}^{\mathrm{out}} ``` #### Subject to @@ -244,48 +243,48 @@ constraints: **`Line_fix_s_lower_security`** ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` **`Line_fix_s_upper_security`** ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot \mathrm{s}^{\mathrm{nom}}_{\xi,k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` **`Line_ext_s_lower_security`** ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` **`Line_ext_s_upper_security`** ```math -\check{s}_{\xi,t,k} + \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{snapshot\_period}(t),k,\kappa} \text{ is defined} +\check{s}_{\xi,t,k} + \beta_{\mathrm{period}(t),k,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\mathrm{s}}_{\xi,t,k} \cdot S_{k} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ k \in \mathcal{K},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{s}_{k} \wedge \beta_{\mathrm{period}(t),k,\kappa} \text{ is defined} ``` **`Transformer_fix_s_lower_security`** ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` **`Transformer_fix_s_upper_security`** ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \sigma^{\mathrm{nom}}_{\xi,m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \neg \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` **`Transformer_ext_s_lower_security`** ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \ge -\overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` **`Transformer_ext_s_upper_security`** ```math -\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{snapshot\_period}(t),m,\kappa} \text{ is defined} +\check{\sigma}_{\xi,t,m} + \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \cdot \hat{s}_{\xi,t,\kappa} \le \overline{\sigma}_{\xi,t,m} \cdot \Sigma_{m} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ m \in \mathcal{M},\ \kappa \in \mathcal{K}^{\mathrm{out}} \,:\, \mathrm{ext}^{\sigma}_{m} \wedge \beta^{\sigma}_{\mathrm{period}(t),m,\kappa} \text{ is defined} ``` diff --git a/docs/examples/pypsa/settings.md b/docs/examples/pypsa/settings.md index bb3a9024d..efd0feb5c 100644 --- a/docs/examples/pypsa/settings.md +++ b/docs/examples/pypsa/settings.md @@ -77,11 +77,11 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: snapshot_weightings_objective: @@ -146,7 +146,7 @@ expressions: cases: counted: when: GlobalConstraint_counts_snapshot - expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period[period]) + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot[period]) otherwise: 0 objective: @@ -172,9 +172,9 @@ objective: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | | $`\mathcal{I}`$ | index $`i`$ — `carrier` — energy carriers, what a growth limit is set per | #### Parameters @@ -202,7 +202,7 @@ objective: **`GlobalConstraint_energy_weight`** ```math -\mathit{w}^{\mathrm{gc}}_{\xi,g,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{in}_{\xi,g,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T} +\mathit{w}^{\mathrm{gc}}_{\xi,g,t} = \begin{cases} \mathrm{w}^{\mathrm{gen}}_{t} \cdot \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{in}_{\xi,g,t} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T} ``` #### Objective diff --git a/docs/examples/pypsa/storage_unit.md b/docs/examples/pypsa/storage_unit.md index 1852ad455..bcf87b7b3 100644 --- a/docs/examples/pypsa/storage_unit.md +++ b/docs/examples/pypsa/storage_unit.md @@ -47,19 +47,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - StorageUnit_carrier: - description: the carrier a storage unit converts from - key: storage_unit - values: carrier - StorageUnit_bus: - description: the bus a storage unit sits on - key: storage_unit - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + storage_unit: + carrier: + description: the carrier a storage unit converts from + into: carrier + bus: + description: the bus a storage unit sits on + into: bus parameters: StorageUnit_active: @@ -288,11 +287,11 @@ expressions: when: StorageUnit_cyclic_state_of_charge_per_period expression: >- StorageUnit_retention - * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: when: >- StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND position(snapshot, within=snapshot_period[period]) == 0 + AND position(snapshot, within=snapshot[period]) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) StorageUnit_last_counted_active: @@ -318,8 +317,8 @@ expressions: dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: - when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_last_counted_active @@ -337,21 +336,21 @@ expressions: adds_to: tech_capacity_expansion StorageUnit_opex: expression: >- - sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) + sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) adds_to: scenario_opex StorageUnit_capex: expression: sum(scenario_weight * StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) adds_to: total_cost StorageUnit_additions: expression: >- - sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=StorageUnit_carrier[carrier]) + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=storage_unit[carrier]) adds_to: Carrier_additions StorageUnit_injection: expression: >- - sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=StorageUnit_bus[bus]) + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=storage_unit[bus]) adds_to: Bus_injection conditions: @@ -522,7 +521,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:443`) StorageUnit_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — @@ -561,12 +560,12 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{StorageUnit\_bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{S} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{S}`$ | index $`s`$ — `storage_unit` with $`\mathrm{carrier}: \mathcal{S} \to \mathcal{I},\ \mathrm{bus}: \mathcal{S} \to \mathcal{N}`$ — storage units, dispatch and store behind one bus connection | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{StorageUnit\_carrier}: \mathcal{S} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{S} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -647,9 +646,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | | $`\lvert \mathcal{T} \rvert`$ | the size of $`\mathcal{T}`$; $`\lvert \mathcal{T} \rvert - 1`$ is the last position | #### Conditions @@ -709,7 +708,7 @@ N^{h}_{s} \ge 0, N^{h}_{s} \in \mathbb{Z} \qquad \forall\, s \in \mathcal{S} \,: **`StorageUnit_charge_carried_in`** ```math -\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \neg \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{\xi,s} \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \mathrm{reset}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} +\overleftarrow{\mathit{soc}}_{\xi,t,s} = \begin{cases} \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,\left( t \ominus \mathrm{idle} \right) \ominus 1,s} & \text{if } \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \neg \mathrm{cyc}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}_{t,s} \right) \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t \ominus^{\mathrm{period}(t)} 1,s} & \text{if } \mathrm{cyc}^{y}_{\xi,s} \\ \mathrm{soc}^{0}_{\xi,s} & \text{if } \mathrm{reset}_{\xi,s} \wedge \neg \mathrm{cyc}^{y}_{\xi,s} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = 0 \\ \rho_{\xi,t,s} \cdot \mathit{soc}_{\xi,t - 1,s} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S} ``` **`StorageUnit_last_counted_active`** @@ -721,7 +720,7 @@ N^{h}_{s} \ge 0, N^{h}_{s} \in \mathbb{Z} \qquad \forall\, s \in \mathcal{S} \,: **`StorageUnit_closing_weight`** ```math -\mathit{w}^{h}_{\xi,g,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,g,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}^{h}_{\xi,g,t,s} & \text{if } \neg \mathrm{reset}_{\xi,s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T},\ s \in \mathcal{S} +\mathit{w}^{h}_{\xi,g,t,s} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,g,t} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{period}(t)} \rvert - 1 \\ \mathit{last}^{h}_{\xi,g,t,s} & \text{if } \neg \mathrm{reset}_{\xi,s} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T},\ s \in \mathcal{S} ``` **`StorageUnit_primary_energy`** @@ -745,7 +744,7 @@ N^{h}_{s} \ge 0, N^{h}_{s} \in \mathbb{Z} \qquad \forall\, s \in \mathcal{S} \,: **`StorageUnit_opex`** ```math -\mathit{StorageUnit\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{StorageUnit\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} h^{+}_{\xi,t,s} \cdot h^{+}_{\xi,t,s} \cdot \mathrm{c}^{h,(2)}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{soc}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{soc}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{s \in \mathcal{S}} \mathit{spill}_{\xi,t,s} \cdot \mathrm{c}^{\mathrm{spill}}_{\xi,t,s} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` **`StorageUnit_capex`** @@ -757,13 +756,13 @@ N^{h}_{s} \ge 0, N^{h}_{s} \in \mathbb{Z} \qquad \forall\, s \in \mathcal{S} \,: **`StorageUnit_additions`** ```math -\mathit{StorageUnit\_additions}_{i,y} = \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{StorageUnit\_additions}_{i,y} = \sum_{s \in \mathcal{S} \,:\, \mathrm{carrier}(s) = i} H_{s} \cdot \mathrm{new}^{h}_{y,s} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`StorageUnit_injection`** ```math -\mathit{StorageUnit\_injection}_{n,\xi,t} = \sum_{s \in \mathcal{S} \,:\, \mathrm{StorageUnit\_bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{StorageUnit\_injection}_{n,\xi,t} = \sum_{s \in \mathcal{S} \,:\, \mathrm{bus}(s) = n} \mathrm{sgn}^{h}_{s} \cdot \left( h^{+}_{\xi,t,s} - h^{-}_{\xi,t,s} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to @@ -929,7 +928,7 @@ h^{-}_{\xi,t,s} = \mathrm{h}^{-,\mathrm{set}}_{\xi,t,s} \qquad \forall\, \xi \in **`StorageUnit_operational_limit_carried_over_has_unit_years`** ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \mathrm{b}^{h}_{\xi,g,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,g,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ s \in \mathcal{S},\ g \in \mathcal{G} \,:\, \mathrm{b}^{h}_{\xi,g,s} \text{ is defined} \wedge \neg \mathrm{reset}_{\xi,s} \wedge \mathrm{in}_{\xi,g,t} ``` **`StorageUnit_marginal_cost_quadratic_without_risk_preference`** diff --git a/docs/examples/pypsa/store.md b/docs/examples/pypsa/store.md index eb9bdbe02..7a94e908a 100644 --- a/docs/examples/pypsa/store.md +++ b/docs/examples/pypsa/store.md @@ -47,19 +47,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Store_carrier: - description: the carrier a store holds - key: store - values: carrier - Store_bus: - description: the bus a store sits on - key: store - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + store: + carrier: + description: the carrier a store holds + into: carrier + bus: + description: the bus a store sits on + into: bus parameters: Store_active: @@ -246,9 +245,9 @@ expressions: expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: - when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot_period[period]) == 0 + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot[period]) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) Store_last_counted_active: @@ -274,8 +273,8 @@ expressions: dims: [scenario, global_constraint, snapshot, store] cases: per_period: - when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT Store_e_initial_per_period expression: Store_last_counted_active @@ -293,19 +292,19 @@ expressions: adds_to: tech_capacity_expansion Store_opex: expression: >- - sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) + sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) adds_to: scenario_opex Store_capex: expression: sum(scenario_weight * Store_e_nom_ext * Store_capital_cost * Store_capital_weight) adds_to: total_cost Store_additions: expression: >- - sum(Store_e_nom_ext * Store_first_active, over=store, by=Store_carrier[carrier]) + sum(Store_e_nom_ext * Store_first_active, over=store, by=store[carrier]) adds_to: Carrier_additions Store_injection: - expression: sum(Store_sign * Store_p, over=store, by=Store_bus[bus]) + expression: sum(Store_sign * Store_p, over=store, by=store[bus]) adds_to: Bus_injection constraints: @@ -405,7 +404,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:495`) Store_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — @@ -444,12 +443,12 @@ assumptions: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus}: \mathcal{V} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{V}`$ | index $`v`$ — `store` with $`\mathrm{carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{bus}: \mathcal{V} \to \mathcal{N}`$ — pure energy stores, each on one bus | | $`\mathcal{G}`$ | index $`g`$ — `global_constraint` — PyPSA's `GlobalConstraint` rows, one label per declared limit | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | -| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{I}`$ | index $`i`$ — `carrier` with $`\mathrm{carrier}: \mathcal{V} \to \mathcal{I}`$ — energy carriers, what a growth limit is set per | #### Parameters @@ -514,9 +513,9 @@ assumptions: | Symbol | Meaning | |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | | $`\lvert \mathcal{T} \rvert`$ | the size of $`\mathcal{T}`$; $`\lvert \mathcal{T} \rvert - 1`$ is the last position | #### Variable domains @@ -550,7 +549,7 @@ N^{e}_{v} \ge 0, N^{e}_{v} \in \mathbb{Z} \qquad \forall\, v \in \mathcal{V} \,: **`Store_energy_carried_in`** ```math -\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} +\overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} ``` **`Store_last_counted_active`** @@ -562,7 +561,7 @@ N^{e}_{v} \ge 0, N^{e}_{v} \in \mathbb{Z} \qquad \forall\, v \in \mathcal{V} \,: **`Store_closing_weight`** ```math -\mathit{w}^{e}_{\xi,g,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}^{e}_{\xi,g,t,v} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T},\ v \in \mathcal{V} +\mathit{w}^{e}_{\xi,g,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} \wedge \mathrm{pos}_{\mathrm{period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{period}(t)} \rvert - 1 \\ \mathit{last}^{e}_{\xi,g,t,v} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T},\ v \in \mathcal{V} ``` **`Store_primary_energy`** @@ -586,7 +585,7 @@ N^{e}_{v} \ge 0, N^{e}_{v} \in \mathbb{Z} \qquad \forall\, v \in \mathcal{V} \,: **`Store_opex`** ```math -\mathit{Store\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi +\mathit{Store\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{period}(t)} \qquad \forall\, \xi \in \Xi ``` **`Store_capex`** @@ -598,13 +597,13 @@ N^{e}_{v} \ge 0, N^{e}_{v} \in \mathbb{Z} \qquad \forall\, v \in \mathcal{V} \,: **`Store_additions`** ```math -\mathit{Store\_additions}_{i,y} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} +\mathit{Store\_additions}_{i,y} = \sum_{v \in \mathcal{V} \,:\, \mathrm{carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} \qquad \forall\, i \in \mathcal{I},\ y \in \mathcal{Y} ``` **`Store_injection`** ```math -\mathit{Store\_injection}_{n,\xi,t} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Store\_injection}_{n,\xi,t} = \sum_{v \in \mathcal{V} \,:\, \mathrm{bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to @@ -710,7 +709,7 @@ q_{\xi,t,v} = \mathrm{q}^{\mathrm{set}}_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ **`Store_operational_limit_carried_over_has_unit_years`** ```math -\mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ g \in \mathcal{G} \,:\, \mathrm{b}^{e}_{\xi,g,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} +\mathrm{w}^{\mathrm{yr}}_{\mathrm{period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ g \in \mathcal{G} \,:\, \mathrm{b}^{e}_{\xi,g,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} ``` **`Store_marginal_cost_quadratic_without_risk_preference`** diff --git a/docs/examples/pypsa/transformer.md b/docs/examples/pypsa/transformer.md index 3b1d30d8c..d3da999ae 100644 --- a/docs/examples/pypsa/transformer.md +++ b/docs/examples/pypsa/transformer.md @@ -46,19 +46,18 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Transformer_bus0: - description: the bus a transformer's flow is measured at - key: transformer - values: bus - Transformer_bus1: - description: the bus at a transformer's other end - key: transformer - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + transformer: + bus0: + description: the bus a transformer's flow is measured at + into: bus + bus1: + description: the bus at a transformer's other end + into: bus parameters: Transformer_active: @@ -249,16 +248,16 @@ expressions: otherwise: 0 Transformer_injection: expression: >- - -sum(Transformer_s, over=transformer, by=Transformer_bus0[bus]) - + sum(Transformer_s, over=transformer, by=Transformer_bus1[bus]) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus0[bus])) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus1[bus])) + -sum(Transformer_s, over=transformer, by=transformer[bus0]) + + sum(Transformer_s, over=transformer, by=transformer[bus1]) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus0])) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus1])) adds_to: Bus_injection Transformer_angle_sum: expression: >- - sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot_period[period]), over=transformer) + sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot[period]), over=transformer) + sum(Transformer_phase_shift_weight, over=transformer) - + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot_period[period]), over=transformer) + + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot[period]), over=transformer) adds_to: Cycle_angle_sum conditions: @@ -383,12 +382,12 @@ constraints: | Symbol | Meaning | |---|---| | $`\Xi`$ | index $`\xi`$ — `scenario` — the futures dispatch is chosen in, each with a weight | -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | -| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | -| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{Transformer\_bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{Transformer\_bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — dispatch periods | +| $`\mathcal{N}`$ | index $`n`$ — `bus` with $`\mathrm{bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{M} \to \mathcal{N}`$ — network nodes | +| $`\mathcal{M}`$ | index $`m`$ — `transformer` with $`\mathrm{bus0}: \mathcal{M} \to \mathcal{N},\ \mathrm{bus1}: \mathcal{M} \to \mathcal{N}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | | $`\mathcal{C}`$ | index $`c`$ — `cycle` — independent cycles of the passive network graph — the cycle basis, data prep. Each period has its own basis, of the branches that stand in it; a label is a position in that period's basis, so one label names a different cycle in another period | | $`\mathcal{S}`$ | index $`s`$ — `segment` — the cuts a passive branch's loss curve is held above — PyPSA's tangents, as many as its `segments` count, or its secants, as many as its tolerance loop places; none in a lossless run | -| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | +| $`\mathcal{Y}`$ | index $`y`$ — `period` with $`\mathrm{period}: \mathcal{T} \to \mathcal{Y}`$ — investment periods — PyPSA's `investment_periods` | #### Parameters @@ -501,13 +500,13 @@ N^{\sigma}_{m} \ge 0, N^{\sigma}_{m} \in \mathbb{Z} \qquad \forall\, m \in \math **`Transformer_injection`** ```math -\mathit{Transformer\_injection}_{n,\xi,t} = -\left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{Transformer\_bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Transformer\_injection}_{n,\xi,t} = -\left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \sigma_{\xi,t,m} \right) + \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \sigma_{\xi,t,m} - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus0}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) - 0.5 \cdot \left( \sum_{m \in \mathcal{M} \,:\, \mathrm{bus1}(m) = n} \ell^{\sigma}_{\xi,t,m} \right) \qquad \forall\, n \in \mathcal{N},\ \xi \in \Xi,\ t \in \mathcal{T} ``` **`Transformer_angle_sum`** ```math -\mathit{Transformer\_angle\_sum}_{c,\xi,t} = \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{snapshot\_period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{snapshot\_period}(t),m,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} +\mathit{Transformer\_angle\_sum}_{c,\xi,t} = \sum_{m \in \mathcal{M}} \sigma_{\xi,t,m} \cdot \mathrm{x}^{\sigma}_{\mathrm{period}(t),m,c} + \sum_{m \in \mathcal{M}} \vartheta_{t,m,c} + \sum_{m \in \mathcal{M}} \mathit{Transformer\_phase\_shift}_{\xi,t,m} \cdot \mathrm{Transformer\_phase\_shift\_cycle\_weight}_{\mathrm{period}(t),m,c} \qquad \forall\, c \in \mathcal{C},\ \xi \in \Xi,\ t \in \mathcal{T} ``` #### Subject to diff --git a/docs/first-spec.md b/docs/first-spec.md index 8d9d44f36..5234fe4f1 100644 --- a/docs/first-spec.md +++ b/docs/first-spec.md @@ -31,8 +31,8 @@ python -m mathspec check dispatch.yaml The check accepts the file, but advises that nothing uses the dimensions yet: ```text -dimension 'snapshot' is never used: nothing is indexed by it, nothing sums into it, and no relation has a column over it. Remove it, or keep it if the entries that use it are still to be written. -dimension 'generator' is never used: nothing is indexed by it, nothing sums into it, and no relation has a column over it. Remove it, or keep it if the entries that use it are still to be written. +dimension 'snapshot' is never used: nothing is indexed by it, nothing sums into it, and no set or property has a column over it. Remove it, or keep it if the entries that use it are still to be written. +dimension 'generator' is never used: nothing is indexed by it, nothing sums into it, and no set or property has a column over it. Remove it, or keep it if the entries that use it are still to be written. ``` ## Parameters and a variable diff --git a/docs/howto/compose.md b/docs/howto/compose.md index cecdfc4af..e22e1b328 100644 --- a/docs/howto/compose.md +++ b/docs/howto/compose.md @@ -60,8 +60,9 @@ and you can nest them as `override(merge([…]), […])`. snapshot: { dtype: int } bus: { dtype: str } generator: { dtype: str } - relations: - Generator_bus: { key: generator, values: bus } + properties: + generator: + bus: bus parameters: Generator_p_nom: { dims: [generator] } Generator_marginal_cost: { dims: [generator] } @@ -69,7 +70,7 @@ and you can nest them as `override(merge([…]), […])`. Generator_p: { dims: [snapshot, generator], bounds: { lower: 0, upper: Generator_p_nom } } expressions: Generator_injection: - expression: sum(Generator_p, over=generator, by=Generator_bus[bus]) + expression: sum(Generator_p, over=generator, by=generator[bus]) adds_to: Bus_injection Generator_cost: expression: sum(Generator_p * Generator_marginal_cost) @@ -84,13 +85,14 @@ and you can nest them as `override(merge([…]), […])`. snapshot: { dtype: int } bus: { dtype: str } load: { dtype: str } - relations: - Load_bus: { key: load, values: bus } + properties: + load: + bus: bus parameters: Load_p_set: { dims: [snapshot, load] } expressions: Load_injection: - expression: -sum(Load_p_set, over=load, by=Load_bus[bus]) + expression: -sum(Load_p_set, over=load, by=load[bus]) adds_to: Bus_injection ``` @@ -128,8 +130,9 @@ and you can nest them as `override(merge([…]), […])`. snapshot: { dtype: int, ordered: true } bus: { dtype: str } store: { dtype: str } - relations: - Store_bus: { key: store, values: bus } + properties: + store: + bus: bus parameters: Store_e_nom: { dims: [store] } variables: @@ -141,7 +144,7 @@ and you can nest them as `override(merge([…]), […])`. expression: Store_e == shift(Store_e, along=snapshot, offset=1, edge='wrap') - Store_p expressions: Store_injection: - expression: sum(Store_p, over=store, by=Store_bus[bus]) + expression: sum(Store_p, over=store, by=store[bus]) adds_to: Bus_injection ``` @@ -200,22 +203,22 @@ which each component pins at its own port. ## What a fragment may share -| The entry | What happens | -| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| a dimension, a relation or a set | every fragment may declare it, and the ones that do say the same thing about it | -| the properties of one subject | every fragment may give the subject properties, and the subject has all of them. A property that two fragments declare says the same thing in both ([properties](../reference/language/sets.md#properties)) | -| a `description` on a shared entry | it is prose rather than a claim, and the first wording in the list is carried | -| `ordered: true` on a shared dimension | it says that the members have an order and does not change them, so the dimension is ordered if one fragment says so | -| `missing:` on a shared relation, set or property | it says what a missing row of the data means, so every fragment that declares the entry gives the same value. `missing: refused` is the same as no `missing:` | -| any other entry | one fragment declares it, and a second is refused | -| an entry under `given:` | it is checked against the fragment that introduces the name, then folded into it. Its description fills the entry where the introducer wrote none | -| a given expression | the body of the definition carries no dimension that the `dims` of the reading fragment do not name | -| a given condition | the `dims` of the reading fragment name exactly the dimensions that the condition of the defining fragment reads | -| a given entry no fragment introduces | it stays under `given:` until a host model provides it | -| an expression with `adds_to:` | the sum it names is the body one fragment defines, if any, followed by every term by its name, in the order of the list. [Terms](../reference/language/parameters-variables-constraints.md#terms) gives what is refused | -| `objective` | one fragment sets it, and a second is refused. Several fragments contribute to it as terms of a sum the objective reads | -| `version` | every fragment is written against the same one | -| `description` at the top of a fragment | it is about the fragment and is not carried. Pass the composed spec's as `description=` | +| The entry | What happens | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| a dimension or a set | every fragment may declare it, and the ones that do say the same thing about it | +| the properties of one subject | every fragment may give the subject properties, and the subject has all of them. A property that two fragments declare says the same thing in both ([properties](../reference/language/sets.md#properties)) | +| a `description` on a shared entry | it is prose rather than a claim, and the first wording in the list is carried | +| `ordered: true` on a shared dimension | it says that the members have an order and does not change them, so the dimension is ordered if one fragment says so | +| `missing:` on a shared set or property | it says what a missing row of the data means, so every fragment that declares the entry gives the same value. `missing: refused` is the same as no `missing:` | +| any other entry | one fragment declares it, and a second is refused | +| an entry under `given:` | it is checked against the fragment that introduces the name, then folded into it. Its description fills the entry where the introducer wrote none | +| a given expression | the body of the definition carries no dimension that the `dims` of the reading fragment do not name | +| a given condition | the `dims` of the reading fragment name exactly the dimensions that the condition of the defining fragment reads | +| a given entry no fragment introduces | it stays under `given:` until a host model provides it | +| an expression with `adds_to:` | the sum it names is the body one fragment defines, if any, followed by every term by its name, in the order of the list. [Terms](../reference/language/parameters-variables-constraints.md#terms) gives what is refused | +| `objective` | one fragment sets it, and a second is refused. Several fragments contribute to it as terms of a sum the objective reads | +| `version` | every fragment is written against the same one | +| `description` at the top of a fragment | it is about the fragment and is not carried. Pass the composed spec's as `description=` | ## A name two fragments declare @@ -365,10 +368,10 @@ Given variable 'Generator_p' collides with the variable of the same name. Rename | `null` under an entry's name | it is removed | | `null` on a field of an entry | the field takes its default, and the rest of the entry stays | | `null` under a section's name | it is refused | -| a dimension, a relation, a set | it is added, or restated as the base declares it | +| a dimension or a set | it is added, or restated as the base declares it | | a property | it is added, or restated as the base declares it | | `ordered:` on a restated dimension | `true` makes the dimension ordered; `false` over an ordered one is refused | -| `missing:` on a relation, a set | it changes whether every key combination needs a row, as on a parameter | +| `missing:` on a set | it changes whether every combination needs a row, as on a parameter | | `missing:` on a property | it changes whether every row of the subject has the property | | an entry under one kind of `given:` | it is edited, added or removed like any entry, and the other kinds stay | | `version`, `description` | the patch's value replaces the base's | @@ -392,7 +395,7 @@ as the base spells it. ## A dimension redeclared -A patch may add a dimension, a relation, a set or a property, and may +A patch may add a dimension, a set or a property, and may restate one the base declares. The restatement says what the base says: half an entry is a second reading of the same name, and a field written at its default is the same as one left out. Changing one under the expressions already written @@ -412,7 +415,7 @@ patch 'unordered.yaml' says the dimension 'snapshot' is not ordered, where its b ``` `missing:` is a claim about the data, not the coordinate space. A patch may -change it on a relation, a set or a property as on a parameter, and may name +change it on a set or a property as on a parameter, and may name it alone: `properties: {line: {bus1: {missing: absent}}}`. `null` puts the default, `refused`, back. diff --git a/docs/howto/curve-by-hand.md b/docs/howto/curve-by-hand.md index c24a411d0..3dca4124a 100644 --- a/docs/howto/curve-by-hand.md +++ b/docs/howto/curve-by-hand.md @@ -31,8 +31,8 @@ and the data sets the number of flows. on_one_segment: { variable: weight, along: bp, type: 2 } ``` -3. **Write the convexity row, and one row per flow.** The relation - `converter_of` maps each flow to its converter, so the data sets how many +3. **Write the convexity row, and one row per flow.** The property + `flow[converter]` maps each flow to its converter, so the data sets how many flows a converter has: ```yaml @@ -42,7 +42,7 @@ and the data sets the number of flows. expression: sum(weight, over=bp) == 1 on_the_curve: # one row per flow dims: [flow, time] - expression: rate == sum(at(weight, by=converter_of[converter]) * bp_rate, over=bp) + expression: rate == sum(at(weight, by=flow[converter]) * bp_rate, over=bp) ``` A converter with a fourth flow is then one more row in the data. diff --git a/docs/howto/declare-a-column.md b/docs/howto/declare-a-column.md index 36ca1f8e8..3db2ae7af 100644 --- a/docs/howto/declare-a-column.md +++ b/docs/howto/declare-a-column.md @@ -7,22 +7,22 @@ SPDX-License-Identifier: CC-BY-4.0 Decide whether a column of your data is a [dimension](../reference/language/dimensions.md), a -[relation](../reference/language/relations.md) or a +[property](../reference/language/sets.md#properties) or a [parameter](../reference/language/parameters-variables-constraints.md#parameters), from what the math does with the column. | The column… | is declared as | | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | is an axis: something is indexed by it, or a sum groups terms by it | a `dimension` | -| 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` | -| relates members of two dimensions many-to-many, with nothing to weigh — which buses a generator may connect to | a bare `relation`, with no `values:` | +| 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 `property` of that dimension or set | +| relates members of two dimensions many-to-many, with nothing to weigh — which buses a generator may connect to | a `set` over both | | 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 | -| is a label set the spec only selects on or counts within — a period, a season, a zone | a `dimension`, and a `relation` onto it | +| is a label set the spec only selects on or counts within — a period, a season, a zone | a `dimension`, and a `property` into it | | scales terms — a coefficient, a bound, an offset | a `parameter` (`float` or `int`) | | is a per-row attribute the math only selects on — a fuel, a constraint's sense | a `str` parameter | | is a mask | a `bool` parameter | Two rules decide the cases the table does not list: -1. If `b` has one value per `a`, declare `b` as a relation keyed by `a`. -2. Declare two dimensions that depend on each other as one relation. +1. If `b` has one value per `a`, declare `b` as a property of `a`. +2. Declare two dimensions that depend on each other as one set. diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 5e2242fae..70d3e27d3 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -46,7 +46,7 @@ A consumer is responsible for checking that the host provides each given name ([what a program does not build](reading.md#what-a-program-does-not-build)). **Section** -: One of the fifteen top-level keys of a file, such as `parameters:` or +: One of the fourteen top-level keys of a file, such as `parameters:` or `given:`, and the entries under it ([file shape](language/file.md)). **Entry** @@ -72,7 +72,7 @@ The data of an expression, a condition and a bound parameter must fit inside the ([how dimensions combine](language/expressions.md#how-dimensions-combine)). **Group** -: The labels of a relation `value` column, used for grouping the relation's keys. +: The labels of a property, used for grouping the rows of its subject. `within=` can be used to apply `shift`, `sum_back` or `position()` separately per group. ## Conditions, masks and absence @@ -91,7 +91,7 @@ condition is true, and leaves out the rest. row that reads it is not built ([absence](language/absence.md)). **Missing row** -: A coordinate that a parameter's or a relation's table has no row for. Its +: A coordinate that a parameter's, a set's or a property's table has no row for. Its `missing:` says what it is: refused when the data is attached (`refused`, the default), absence (`absent`), or a value, such as `0` ([a missing row](language/parameters-variables-constraints.md#a-missing-row)). @@ -115,8 +115,8 @@ The meaning of some words changes depending on the context in which they appear | Word | One sense | The other sense | | -------- | -------------------------------------------------- | ------------------------------------------------------------- | -| row | a constraint at one coordinate | one line of a parameter's or a relation's table | -| column | a variable at one coordinate | one column of a data table or a relation | +| row | a constraint at one coordinate | one line of a parameter's, a set's or a property's table | +| column | a variable at one coordinate | one column of a data table, a set or a property | | set | an `sos:` entry | the set symbol of a dimension, $\mathcal{G}$, in the legend | | regime | one case under [`cases:`](language/named.md#cases) | one of two constraints, each under its own `where:` | | domain | a variable's `continuous`, `integer` or `binary` | the rows that hold a curve's link inside its breakpoint range | diff --git a/docs/reference/language/absence.md b/docs/reference/language/absence.md index 164adecbb..06f37bfb4 100644 --- a/docs/reference/language/absence.md +++ b/docs/reference/language/absence.md @@ -27,14 +27,14 @@ hold. ## What creates absence -| Construct | What is absent | -| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `where:` on a variable | the variable, at the masked coordinates | -| `where:` on a parameter | the parameter, at the masked coordinates ([a parameter's mask](parameters-variables-constraints.md#a-parameters-mask)) | -| `where:` on a constraint | the row | -| `shift(x, along=d, offset=n)` without `edge=` | the vacated edge coordinate ([shift](operators.md#shift)) | -| a key combination a `missing: absent` relation has no row for | that combination's place in the relation ([relations](relations.md#a-missing-row)) | -| a missing row of a `missing: absent` parameter | the parameter, at that coordinate | +| Construct | What is absent | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `where:` on a variable | the variable, at the masked coordinates | +| `where:` on a parameter | the parameter, at the masked coordinates ([a parameter's mask](parameters-variables-constraints.md#a-parameters-mask)) | +| `where:` on a constraint | the row | +| `shift(x, along=d, offset=n)` without `edge=` | the vacated edge coordinate ([shift](operators.md#shift)) | +| a row a `missing: absent` set or property has no row for | that row's place in the set or the property ([sets](sets.md#a-missing-row)) | +| a missing row of a `missing: absent` parameter | the parameter, at that coordinate | Nothing else creates absence. Inside its mask, a missing row is the only gap a parameter can have, because a null or NaN value is @@ -164,10 +164,10 @@ write `where: rel_max` on the constraint. | 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, over=d, by=relation[c])` | every member of the group | is one summand fewer; the row stands | +| `sum(x, over=d, by=subject[p])` | 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 | -| `at(x, by=relation[c])` | one position, through the map | _is_ the output, so it spreads | +| `at(x, by=subject[p])` | one position, through the map | _is_ the output, so it spreads | ## A table with gaps on purpose diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 79e40cb98..204bfd779 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: CC-BY-4.0 A **dimension** is a set of labels that entries are indexed by, such as `snapshot` or `generator`. `sum` reduces over a dimension. A map from one -dimension onto another is a [relation](relations.md). +dimension onto another is a [property](sets.md#properties). ## `dimensions` @@ -54,5 +54,5 @@ A `dtype` does not make a dimension ordered: an `int` dimension may number things that have no order. An `sos` entry with `type: 1` reads no order, and `edge='wrap'` makes one `shift` cyclic without changing the dimension. -To decide whether a column of data is a dimension, a relation or a parameter, +To decide whether a column of data is a dimension, a property or a parameter, read [declare a column of data](../../howto/declare-a-column.md). diff --git a/docs/reference/language/errors.md b/docs/reference/language/errors.md index 6c29a8ada..684a73a81 100644 --- a/docs/reference/language/errors.md +++ b/docs/reference/language/errors.md @@ -33,7 +33,7 @@ warning, so the file still loads. | `kind` | The file has… | The advice says… | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | -| `never-an-axis` | a dimension nothing is indexed by, nothing aggregates into and no relation targets | remove it, or keep it knowingly if its entries are still to come | +| `never-an-axis` | a dimension nothing is indexed by, nothing aggregates into and no set or property reads | remove it, or keep it knowingly if its entries are still to come | | `given` | a parameter, column, named expression or row family it reads and does not build ([given](parameters-variables-constraints.md#given)) | the model this one is layered onto provides it | | `unbounded` | a variable that no constraint, set or curve uses, whose objective term pushes it towards a bound it does not have | give it a finite bound, or the constraint that was meant to define it | diff --git a/docs/reference/language/expressions.md b/docs/reference/language/expressions.md index bde469c8a..2c6a13ced 100644 --- a/docs/reference/language/expressions.md +++ b/docs/reference/language/expressions.md @@ -72,21 +72,21 @@ a square. ## Name resolution -One flat namespace covers dimensions, relations, parameters, variables, named +One flat namespace covers dimensions, sets, parameters, variables, named expressions, named conditions, macros and the built-in operators. Two entries with one name are a load error, and the message names both, so no name hides another. The place where a name stands decides which kinds of name are legal: -| Position | Legal kinds | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| expression (`p * cost`) | a variable, or a parameter whose values are numbers ([dtype](parameters-variables-constraints.md#parameters)) | -| dimension argument (`over=`, `along=`) | a dimension, or a list of them for `over=`. With `by=`, a name in `over=` is a column of the relation, if it has one | -| column argument (`by=`, `within=`) | columns of one relation, or members and properties of one dimension or set, written `name[column, …]` | -| condition | a parameter, variable, dimension, relation, set or named condition ([conditions](#conditions)) | -| `bounds.lower` / `bounds.upper` | a parameter name, or a number | -| the `edge` key of `shift` | `'wrap'` in quotes, or a bare number | -| `dual` argument (`dual(c)`) | a constraint. It resolves against the constraints alone ([named expressions](named.md#reading-a-constraints-dual)) | +| Position | Legal kinds | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| expression (`p * cost`) | a variable, or a parameter whose values are numbers ([dtype](parameters-variables-constraints.md#parameters)) | +| dimension argument (`over=`, `along=`) | a dimension, or a list of them for `over=`. With `by=`, a name in `over=` is a column of the subject, if it has one | +| column argument (`by=`, `within=`) | members and properties of one dimension or set, written `subject[property, …]` | +| condition | a parameter, variable, dimension, set, property or named condition ([conditions](#conditions)) | +| `bounds.lower` / `bounds.upper` | a parameter name, or a number | +| the `edge` key of `shift` | `'wrap'` in quotes, or a bare number | +| `dual` argument (`dual(c)`) | a constraint. It resolves against the constraints alone ([named expressions](named.md#reading-a-constraints-dual)) | A bare word in the value of a keyword argument is a name to resolve, but a keyword's key is never a name. @@ -103,18 +103,18 @@ constraint of the same name. `to_spec` knows the dimension set of every expression before any data is attached: -| 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, over=d, by=l[c])` | `(dims(x) − joined) ∪ grouped` | the refusals under [how a relation is used](relations.md#how-a-relation-is-used) | -| `at(x, by=l[c])` | `(dims(x) − joined) ∪ grouped` | the same | -| `shift(x, along=d, offset=n)` | `dims(x)` | error if `d ∉ dims(x)` | -| `sum_back(x, along=d, window=n)` | `dims(x)` | error if `d ∉ dims(x)` | +| Node | Dim set | Error | +| -------------------------------- | ------------------------------ | --------------------------------------- | +| number | `{}` | | +| parameter / variable | its `dims` | | +| `-x`, `+x` | `dims(x)` | | +| `a + b`, `a * b`, `a / b` | `dims(a) ∪ dims(b)` | | +| `sum(x)` | `{}` | error if `dims(x)` is already empty | +| `sum(x, over=d)` | `dims(x) − {d}` | error if `d ∉ dims(x)` | +| `sum(x, over=d, by=s[c])` | `(dims(x) − joined) ∪ grouped` | the refusals under [sums](sets.md#sums) | +| `at(x, by=s[c])` | `(dims(x) − joined) ∪ grouped` | the same | +| `shift(x, along=d, offset=n)` | `dims(x)` | error if `d ∉ dims(x)` | +| `sum_back(x, along=d, window=n)` | `dims(x)` | error if `d ∉ dims(x)` | An outer product is allowed. The dimensions an entry ranges over are its **frame**, and an entry may not disagree with its expression: @@ -153,22 +153,20 @@ QUOTED ::= "'" chars "'" | '"' chars '"' | ---------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` (bare) | parameter | The value is defined here. A `bool` is its own answer. A `str` is defined wherever the table has a row. A number has to have a row and be finite, and `0.0` is a row: write `inflow != 0` for non-zero. An `int` or `str` under `missing: refused` is a load error: it has a row at every coordinate | | `name` (bare) | variable | The variable exists at this coordinate | -| `name` (bare) | relation | A row exists, read at the relation's key. A `missing: absent` relation is [partial](relations.md#the-data-contract), and this selects the labels that do map | | `name` (bare) | set | A row exists, read at the set's members ([sets](sets.md#entries-over-a-set)) | | `name[p, …]` | dimension or set | The row has every property named ([reading a property](sets.md#how-a-set-or-a-property-is-read)) | -| `name[p] OP value`, `name[a] OP name[b]` | property | As for a relation column, read from the property's own table | +| `name[p] OP value` | property | A filter on the value of the property, read at its subject's members. A comparison reads one property. Where the row lacks the property, the comparison is false for every operator, `!=` included, so `NOT` of it is true there | | `name` (bare) | dimension | A load error. It would be true everywhere | | `name` (bare) | named condition | Its condition ([conditions](named.md#conditions)). A given condition is true or false at each coordinate, as a `bool` parameter is | | `name OP value` | parameter | Element-wise. A missing row with no `missing:` value compares false | | `name OP value` | dimension | A filter on the frame's own coordinate column | -| `name OP value`, `name[col] OP value` | relation | A filter on a value column, read at the relation's key. Name the column where the key determines several. A comparison reads one column. Where the relation has no row, the comparison is false for every operator, `!=` included, so `NOT` of it is true there | -| `name OP name`, `name[a] OP name[b]` | two relation columns | Legal where both relations are keyed over the same dimensions and both columns are over one dimension. `ends[bus0] != ends[bus1]` excludes a self-loop. Where either relation has no row, the comparison is false | +| `name[a] OP name[b]` | two properties | Legal where both subjects are over the same dimensions and both properties map into one dimension. `line[bus0] != line[bus1]` excludes a self-loop. Where either row lacks its property, the comparison is false | | `expression OP expression` | arithmetic over parameters | Coordinate by coordinate, over every dimension either side carries ([arithmetic in a comparison](#arithmetic-in-a-comparison)). A side with no value at a coordinate compares false | | `position(name) OP i` | dimension | Where the row sits along the dimension's own order. `0` is first, and a negative number counts from the end | -| `position(name, within=relation[c])` | dimension | The same, counted within each group the relation makes | +| `position(name, within=s[p])` | dimension | The same, counted within each group the property makes | | `count(condition, over=name) OP i` | a condition | How many coordinates along the dimension the condition admits ([counting what a condition admits](#counting-what-a-condition-admits)) | | `shift(condition, along=name, offset=i)` | a condition | The condition read `i` coordinates back, and false where that vacates | -| `at(condition, by=relation[c])` | a condition | The condition read through the relation ([reading a condition through a relation](#reading-a-condition-through-a-relation)), and false where the relation has no row | +| `at(condition, by=s[p])` | a condition | The condition read through the property ([reading a condition through a property](#reading-a-condition-through-a-property)), and false where the row lacks the property | | `AND` `OR` `NOT` | — | Case-insensitive. `NOT` binds tighter than `AND`, and `AND` tighter than `OR` | | `True` / `False` | — | `True` is the same as no `where`; `False` gives an entry with no rows | @@ -230,39 +228,40 @@ A negative `offset` reads forwards. This form accepts neither `within=` nor `edge='wrap'`. For a grouped or cyclic translation, compare the arithmetic `shift`. -### Reading a condition through a relation +### Reading a condition through a property -`at(, by=[])` reads a condition over +`at(, by=[])` reads a condition over coarse coordinates at fine ones, as [`at`](operators.md#at) reads an array. It -is true where the relation has a row and the condition holds at the coordinate -that row maps to, and **false** where the relation has no row. +is true where the row has the property and the condition holds at the +coordinate the property maps it to, and **false** where the row lacks it. ```yaml dimensions: converter: { dtype: str } flow: { dtype: str } -relations: - converter_of: { key: flow, values: converter } +properties: + flow: + converter: converter parameters: has_curve: { dims: [converter], dtype: bool } cap: { dims: [flow] } variables: rate: dims: [flow] - where: "at(has_curve, by=converter_of[converter])" + where: "at(has_curve, by=flow[converter])" bounds: { lower: 0, upper: cap } objective: sense: minimize expression: sum(rate, over=flow) ``` -$$0 \le \mathit{rate}_{f} \le \mathrm{cap}_{f} \qquad \forall\thinspace f \in \mathcal{F} \thinspace : \thinspace \mathrm{has\_curve}_{\mathrm{converter\_of}(f)}$$ +$$0 \le \mathit{rate}_{f} \le \mathrm{cap}_{f} \qquad \forall\thinspace f \in \mathcal{F} \thinspace : \thinspace \mathrm{has\_curve}_{\mathrm{converter}(f)}$$ -The read removes the column it reads, `converter`, and adds the key, `flow`, +The read removes the dimension it reads, `converter`, and adds the subject, `flow`, so the condition above is over `flow` alone. The rules are those of -[`at`](relations.md#lookups) in an expression. `by=` names a value column, or -several as `relation[a, …]`. The read groups by the key of the relation, and -the condition carries every dimension that the read joins on. +[`at`](sets.md#lookups) in an expression. `by=` names a property, or several +as `subject[a, …]`. The read groups by the members of the subject, and the +condition carries every dimension that the read joins on. ### The right-hand side of a comparison @@ -278,7 +277,7 @@ compared against a quoted ISO date such as `'2030-01-01'` or Only a number or a `datetime` compares with `<`, `<=`, `>` and `>=`. A `str` or `bool` compares with `==` and `!=`, and an inequality on it is a load error. -A relation column reads the dtype of its target dimension, so the rule holds +A property reads the dtype of the dimension it maps into, so the rule holds there too. `generator < 'b9'` is refused, because it would compare the labels byte by byte, and `'b10'` would come before `'b9'` whatever the dimension's order. To compare along the order of a dimension declared `ordered: true`, use @@ -293,9 +292,9 @@ instead. Either side of a comparison may be an expression over parameters: `p_min <= 0.5 * p_max`, or -`p_max <= at(bus_cap, by=bus_of[bus])`. The side is read as an +`p_max <= at(bus_cap, by=generator[bus])`. The side is read as an [expression](#expressions) is, macros and named expressions included. A -variable and a `dual()` are refused. A relation column and a quoted label are +variable and a `dual()` are refused. A property and a quoted label are compared on their own, and are not read in arithmetic. A side absent at a coordinate compares false there, but under a summing @@ -341,15 +340,16 @@ constraints: A position that no coordinate occupies is an error when the data is attached. -`within=` counts inside each group a [partition](relations.md#partitions) +`within=` counts inside each group a [partition](sets.md#partitions) makes, so each period gets one seeded row: ```yaml dimensions: snapshot: { dtype: int, ordered: true } period: { dtype: int } -relations: - period_of: { key: snapshot, values: period } +properties: + snapshot: + period: period parameters: soc_initial: { dims: [period] } variables: @@ -357,6 +357,6 @@ variables: constraints: soc_start: dims: [snapshot] - where: "position(snapshot, within=period_of[period]) == 0" - expression: soc == at(soc_initial, by=period_of[period]) + where: "position(snapshot, within=snapshot[period]) == 0" + expression: soc == at(soc_initial, by=snapshot[period]) ``` diff --git a/docs/reference/language/file.md b/docs/reference/language/file.md index 88a5bd2d8..c69ff7bd6 100644 --- a/docs/reference/language/file.md +++ b/docs/reference/language/file.md @@ -5,14 +5,13 @@ SPDX-License-Identifier: CC-BY-4.0 # File shape -A spec file is a YAML mapping with **fifteen section keys**, plus -`version` and `description`, and any subset of the fifteen is accepted. +A spec file is a YAML mapping with **fourteen section keys**, plus +`version` and `description`, and any subset of the fourteen is accepted. | Key | | | ------------- | --------------------------------------------------------------------------------------------------------- | | `given` | what this file reads but does not build ([given](parameters-variables-constraints.md#given)) | | `dimensions` | the dimensions ([dimensions](dimensions.md)) | -| `relations` | named relations between dimensions ([relations](relations.md)) | | `sets` | which combinations of labels exist ([sets](sets.md#sets)) | | `properties` | a label for each row of a dimension or a set ([properties](sets.md#properties)) | | `parameters` | the data the spec expects ([parameters](parameters-variables-constraints.md#parameters)) | diff --git a/docs/reference/language/index.md b/docs/reference/language/index.md index 7a2903fb2..e413f59eb 100644 --- a/docs/reference/language/index.md +++ b/docs/reference/language/index.md @@ -47,7 +47,7 @@ names the fix. | # | Rule | | | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -| 1 | A file has fifteen section keys, plus `version` and `description`. An unknown key is refused, with the nearest valid key named. | [File shape](file.md) | +| 1 | A file has fourteen section keys,plus `version` and `description`. An unknown key is refused, with the nearest valid key named. | [File shape](file.md) | | 2 | `to_spec` checks everything it can check without data when the file loads. | [Errors](errors.md) | | 3 | Every name is declared once. A parameter and a dimension both called `snapshot` is refused. | [Names](expressions.md#name-resolution) | | 4 | The kind of a name decides where it may stand. A dimension follows `over=` or `along=`, and is never multiplied. | [Names](expressions.md#name-resolution) | diff --git a/docs/reference/language/operators.md b/docs/reference/language/operators.md index a33d64a80..5acf9a2b1 100644 --- a/docs/reference/language/operators.md +++ b/docs/reference/language/operators.md @@ -15,18 +15,18 @@ of them, and a file cannot add an operator. Write a composition of them as a [ma | `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, over=[a, …])` | Each dimension in the list collapses. `array` must carry each one, and the list names each one once | -| `sum(array, over=dim, by=relation[c])` | `dim` collapses, and the result is grouped by the relation's column `c`. The other key columns are joined on, so the array carries them and the result keeps them | -| `sum(array, over=[dim, …], by=relation[c, …])` | The same with several dimensions and several columns of one relation. The call joins on them together, and groups by their product | -| `at(array, by=relation[c])` | `array` read at the value of the relation's column `c`, once per key of the relation. The result gains the key | +| `sum(array, over=dim, by=subject[p])` | `dim` collapses, and the result is grouped by the property `p`. The other members of the subject are joined on, so the array carries them and the result keeps them | +| `sum(array, over=[dim, …], by=subject[p, …])` | The same with several dimensions and several properties of one subject. The call joins on them together, and groups by their product | +| `at(array, by=subject[p])` | `array` read at the value of the property `p`, once per row of the subject. The result gains the subject's members | | `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 | | `shift(array, along=dim, offset=p, edge=…)` | `p` is an integer parameter, so each entity is reached by its own offset | -| `shift(array, along=dim, offset=n, within=relation[c])` | The translation steps inside each group that the relation's column `c` makes. Neighbours, edges and a wrap all belong to that group | +| `shift(array, along=dim, offset=n, within=subject[p])` | The translation steps inside each group that the property `p` makes. Neighbours, edges and a wrap all belong to that group | | `sum_back(array, along=dim, window=n)` | The sum of the last `n` positions along `dim`, ending at the position being written | | `sum_back(array, along=dim, window=p)` | `p` is an integer parameter, so each entity gets its own window length | | `sum_back(array, along=dim, window=p, edge='wrap')` | The window reaches around the axis, instead of stopping short at its start | -| `sum_back(array, along=dim, window=n, within=relation[c])` | The window stays inside each group that the relation's column `c` makes | +| `sum_back(array, along=dim, window=n, within=subject[p])` | The window stays inside each group that the property `p` makes | `array` is any expression with the right dimension set, a parameter or a variable. [Every operator as math](#every-operator-as-math) shows how each row @@ -43,10 +43,12 @@ dimensions: bus: { dtype: str } generator: { dtype: str } line: { dtype: str } -relations: - gen_bus: { key: generator, values: bus } - line_from: { key: line, values: bus } - line_to: { key: line, values: bus } +properties: + generator: + bus: bus + line: + from_bus: bus + to_bus: bus parameters: load: { dims: [bus] } variables: @@ -56,21 +58,21 @@ constraints: nodal_balance: dims: [bus] expression: >- - sum(p, over=generator, by=gen_bus[bus]) - + sum(f, over=line, by=line_to[bus]) - - sum(f, over=line, by=line_from[bus]) + sum(p, over=generator, by=generator[bus]) + + sum(f, over=line, by=line[to_bus]) + - sum(f, over=line, by=line[from_bus]) == load ``` -[How a relation is used](relations.md#how-a-relation-is-used) says what a call -through a relation reads and carries. +[How a set or a property is read](sets.md#how-a-set-or-a-property-is-read) says +what a call through a set or a property reads and carries. ## `at` -`at` joins the relation with no group-by, so it reads one coarse value once for -each fine label that points at it ([lookups](relations.md#lookups)). One +`at` joins a property with no group-by, so it reads one coarse value once for +each fine label that points at it ([lookups](sets.md#lookups)). One decision per bus, read by every line that touches the bus, is -`at(decision, by=line_bus[bus])`. +`at(decision, by=line[from_bus])`. ## `sum_back` @@ -105,7 +107,7 @@ dimension being summed. `edge=`, a window that reaches past the start of the dimension is **short**, and no row is lost. -`within=` takes a [partition](relations.md#partitions). +`within=` takes a [partition](sets.md#partitions). ## `shift` @@ -142,15 +144,16 @@ Two rules hold for `edge=`: ### Translation within groups -`within=` takes a [partition](relations.md#partitions), and the neighbour of a +`within=` takes a [partition](sets.md#partitions), and the neighbour of a coordinate is the one before it in its own group, such as a season: ```yaml dimensions: snapshot: { dtype: int, ordered: true } season: { dtype: str } -relations: - season_of: { key: snapshot, values: season } +properties: + snapshot: + season: season parameters: inflow: { dims: [snapshot] } variables: @@ -158,7 +161,7 @@ variables: constraints: season_balance: dims: [snapshot] - expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + inflow + expression: soc == shift(soc, along=snapshot, offset=1, edge='wrap', within=snapshot[season]) + inflow objective: { sense: minimize, expression: sum(soc) } ``` @@ -213,20 +216,20 @@ and the [typesetter](../typeset.md) prints it. The specs themselves are on | `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, over=[a, …])` | $`\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g,c} \le \mathrm{limit}_{c} \qquad \forall\, c \in \mathcal{C}`$ | -| `sum(array, over=dim, by=relation[c])` | $`\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, over=dim, by=relation[c]), joining on the rest of the key` | $`\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, over=[dim, …], by=relation[c, …])` | $`\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[c])` | $`p_{t} \le \mathrm{cap}_{\mathrm{period\_of}(t)} \qquad \forall\, t \in \mathcal{T}`$ | -| `at(array, by=relation[c]), two columns over one dimension` | $`f_{l} \le \mathrm{cap}_{\mathrm{ends.bus0}(l)} \qquad \forall\, l \in \mathcal{L}`$ | +| `sum(array, over=dim, by=subject[p])` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} \le \mathrm{limit}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}`$ | +| `sum(array, over=dim, by=subject[p]), joining on the other members` | $`\sum_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ e) = z} p_{g,e} \ge \mathrm{demand}_{z,e} \qquad \forall\, z \in \mathcal{Z},\ e \in \mathcal{E}`$ | +| `sum(array, over=[dim, …], by=subject[p, …])` | $`\sum_{g \in \mathcal{G},\ e \in \mathcal{E} \,:\, \mathrm{bus}(g,\ e) = b \wedge \mathrm{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=subject[p])` | $`p_{t} \le \mathrm{cap}_{\mathrm{period}(t)} \qquad \forall\, t \in \mathcal{T}`$ | +| `at(array, by=subject[p]), two properties into one dimension` | $`f_{l} \le \mathrm{cap}_{\mathrm{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}`$ | | `shift(array, along=dim, offset=p, edge=…)` | $`\mathit{order}_{t,m \boxminus_{0} \mathrm{lead}} \ge \mathrm{demand}_{t,m} \qquad \forall\, t \in \mathcal{T},\ m \in \mathcal{M}`$ | -| `shift(array, along=dim, offset=n, within=relation[c])` | $`p_{t} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1} \qquad \forall\, t \in \mathcal{T}`$ | +| `shift(array, along=dim, offset=n, within=subject[p])` | $`p_{t} \le p_{t \ominus^{\mathrm{season}(t)} 1} \qquad \forall\, t \in \mathcal{T}`$ | | `sum_back(array, along=dim, window=n)` | $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ | | `sum_back(array, along=dim, window=p)` | $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h - h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ | | `sum_back(array, along=dim, window=p, edge='wrap')` | $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h \ominus h' < \mathrm{min\_up}} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ | -| `sum_back(array, along=dim, window=n, within=relation[c])` | $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h -^{\mathrm{day\_of}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ | +| `sum_back(array, along=dim, window=n, within=subject[p])` | $`\sum_{h' \in \mathcal{H} \,:\, 0 \le h -^{\mathrm{day}(h)} h' < 3} \mathit{started}_{u,h'} \le \mathit{on}_{u,h} \qquad \forall\, u \in \mathcal{U},\ h \in \mathcal{H}`$ | | `dual(constraint)` | $`\mathit{price}_{t} = \lambda_{\mathrm{balance},t} \qquad \forall\, t \in \mathcal{T}`$ | diff --git a/docs/reference/language/parameters-variables-constraints.md b/docs/reference/language/parameters-variables-constraints.md index bd34a4163..3af24166e 100644 --- a/docs/reference/language/parameters-variables-constraints.md +++ b/docs/reference/language/parameters-variables-constraints.md @@ -122,8 +122,8 @@ dimensions: entity: { dtype: str } bus: { dtype: str } snapshot: { dtype: int } -relations: - connection: { key: [entity, bus], missing: absent } # the pairs that are wired +sets: + connection: { dims: [entity, bus], missing: absent } # the pairs that are wired parameters: efficiency: { dims: [entity, bus, snapshot], where: connection, missing: 1 } variables: @@ -158,8 +158,9 @@ A row outside the mask is not an error. To refuse such a row, state it as an `{holds: "connection", where: "efficiency"}`. A mask reads what comes before the parameters. [Dimensions](dimensions.md) -say which labels exist, and [relations](relations.md) say which combinations of -labels exist. A relation has no `where:`, so a mask can read any relation. A +say which labels exist, [sets](sets.md#sets) say which combinations of labels +exist, and [properties](sets.md#properties) map one label to another. Neither +has a `where:`, so a mask can read any set or property. A parameter that has no mask comes before the masked ones too. So the mask is read from the data alone, before the parameter itself, and four masks are refused at load: @@ -170,7 +171,7 @@ load: - a mask over a dimension that the parameter does not have. ```text -Parameter 'efficiency': its `where:` reads variable 'p', and a parameter's mask is evaluated from the data alone, before any variable has a value. Test a parameter or a relation instead. +Parameter 'efficiency': its `where:` reads variable 'p', and a parameter's mask is evaluated from the data alone, before any variable has a value. Test a parameter, a set or a property instead. ``` The typeset legend prints the mask beside the dimensions, as `only where` and @@ -272,14 +273,15 @@ dimensions: snapshot: { dtype: int } port: { dtype: str } generator: { dtype: str } -relations: - gen_port: { key: generator, values: port } +properties: + generator: + port: port variables: gen_p: { dims: [snapshot, generator], bounds: { lower: 0 } } constraints: gen_injects: dims: [snapshot, generator] - expression: at(flow, by=gen_port[port]) == gen_p + expression: at(flow, by=generator[port]) == gen_p ``` | Field | | | @@ -394,14 +396,15 @@ dimensions: snapshot: { dtype: int } bus: { dtype: str } generator: { dtype: str } -relations: - gen_bus: { key: generator, values: bus } +properties: + generator: + bus: bus variables: gen_p: { dims: [snapshot, generator], bounds: { lower: 0 } } expressions: generation: description: what the fleet puts into a bus - expression: sum(gen_p, over=generator, by=gen_bus[bus]) + expression: sum(gen_p, over=generator, by=generator[bus]) adds_to: injection ``` diff --git a/docs/reference/language/relations.md b/docs/reference/language/relations.md deleted file mode 100644 index b8a464275..000000000 --- a/docs/reference/language/relations.md +++ /dev/null @@ -1,256 +0,0 @@ - - -# Relations - -A **relation** is a mapping between [dimensions](dimensions.md): a generator's -bus, a snapshot's period, or the buses a generator may connect to. You declare -it as a table, and the data supplies its rows. - -## `relations` - -```yaml -dimensions: - bus: { dtype: str } - generator: { dtype: str } - snapshot: { dtype: int } - period: { dtype: int } -relations: - gen_bus: { key: generator, values: bus } # each generator on one bus - period_of: { key: snapshot, values: period } - connection: { key: [generator, bus], missing: absent } # no values: a generator may connect to several buses -``` - -The **key** is the combination of dimensions that is unique per row: -`key: generator` says the table has one row per generator. The **values** are -what that row determines: its bus. With no `values:`, the key is every column, -and the table is a **bare relation**. - -| Field | | | -| ------------- | --------------------------------------------------------------------- | ----------------- | -| `key` | required. The columns that identify a row | | -| `values` | the columns the key determines. When omitted, the key is every column | default none | -| `missing` | `refused` or `absent`: whether every key combination needs a row | default `refused` | -| `description` | free text | default `null` | - -A column is named after its dimension, or after no dimension. Where two columns -share a dimension, the mapping form names them: `{bus0: bus, bus1: bus}`. A -column named after a dimension it is not over, `{values: {period: zone}}`, is -refused. A call reads `period` as the name of the column, and the column and -the dimension must then agree. - -### Cardinalities - -| intention | written | cardinality | -| -------------------------------------------- | -------------------------------------------------------- | ------------------------------------------- | -| each generator has one bus | `{key: generator, values: bus}` | many-to-one | -| a generator may connect to several buses | `{key: [generator, bus], missing: absent}` | many-to-many | -| a generator has one zone in each period | `{key: [generator, period], values: zone}` | many-to-one, keyed by a pair | -| a snapshot has a month, a week and a weekday | `{key: snapshot, values: [month, week, weekday]}` | many-to-one, several values | -| a line has two ends, both buses | `{key: line, values: {bus0: bus, bus1: bus}}` | many-to-one, two columns over one dimension | -| a snapshot has a representative snapshot | `{key: snapshot, values: {rep: snapshot}}` | many-to-one, onto itself | -| a snapshot has neighbours | `{key: {from: snapshot, to: snapshot}, missing: absent}` | many-to-many, onto itself | - -A key that determines a value holds one column per dimension, so -`{key: {bus0: bus, bus1: bus}, values: line}` is refused, but a bare relation -may key two columns over one dimension. - -### The data contract - -The data for `gen_bus` arrives under the key `gen_bus`, as a table with one -column per declared column, named after it. - -- One row per key tuple, so a generator on two buses is refused when the data - is attached. -- Every value is a label of its dimension, and a value that matches none is - refused. -- A relation has a row for every combination of its key's dimensions, unless - it declares `missing: absent` ([a missing row](#a-missing-row)). -- A null in any column is refused. -- Row order has no meaning, because the order comes from the - [dimension](dimensions.md). - -These checks read only the labels of the relation's dimensions. A relation has -no `where:`, and nothing else decides its rows. So a relation comes before the -entries that read it: a [parameter's mask](parameters-variables-constraints.md#a-parameters-mask), -a variable's or a constraint's `where:`, and an assumption. To state that a -relation must have certain rows, write an [assumption](assumptions.md). For -`line_to` [below](#a-missing-row), `{holds: "line_to", where: "closed"}` says -that every closed line has an end, where `closed` is a `bool` parameter over -`line`. - -### A missing row - -`missing:` says whether every combination of the key's dimensions needs a row. -For `gen_bus`, the key is `generator`, so every generator needs a row. A -generator that is on no bus looks the same in the data as a line end that -nobody wired, so the file says which one you mean: - -```yaml -dimensions: - generator: { dtype: str } - bus: { dtype: str } - line: { dtype: str } -relations: - gen_bus: { key: generator, values: bus } # refused: every generator is on a bus - line_to: { key: line, values: bus, missing: absent } # an open end is meant - connection: { key: [generator, bus], missing: absent } # most pairs are not connected -``` - -| `missing:` | A key combination with no row | -| --------------------- | ------------------------------------------------------------------------------------------------------ | -| `refused`, by default | is refused when the data is attached, and the message names it | -| `absent` | is not in the relation, so a line with no `line_to` row ends at no bus, which is [absence](absence.md) | - -`missing:` checks only the key. The values cannot be missing, because a null is -refused and every value must be a label of its dimension. A label is data, so -no value fills a gap in a map. - -A bare relation follows the same rule, and its key is every column. Under the -default `refused`, `connection` needs a row for every pair of a generator and a -bus. Most bare relations are sparse, so they declare `missing: absent`, and a -pair with no row is then not in the relation. - -The typeset legend prints `` `line_to` is `absent` where the data has no row `` -beside each set that the map joins, and prints nothing for `refused`. - -## How a relation is used - -The entry fixes no direction. A call **joins** the operand to the -relation on the columns they share, and **groups** what the join produces. A -call names columns of a relation as `relation[column]`, or as -`relation[column, …]` for several columns of one table. The relation is written -once, so one call reads one table. - -| kind | what it does | written as | -| ----------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | -| join and group by | rows of the operand match rows of the relation, and each group is added up | `sum(x, over=d, by=l[c])` | -| join | each row of the operand matches one row of the relation, and reads its value | `at(x, by=l[c])` | -| partition | the dimensions stay, and the rows are grouped | `shift`, `sum_back`, `position` with `within=l[c]` | -| test | a row's presence keeps or cuts a coordinate | `l`, or `l[c]` in a comparison, in a condition | - -Four rules hold for every use: - -1. A call names every column it reads, so `at(x, by=gen_bus)` is refused. - Write `at(x, by=gen_bus[bus])`. -2. A value column the call does not name is not read. -3. The key is fixed, so to change it, declare a new relation. -4. A dimension the relation does not name passes through to the result. - -### Sums through a relation - -`sum(x, over=d, by=l[c])` sums `x` over the columns in `over=`, grouped by -the columns in `by=`. As in `sum(x, over=d)`, `over=` names what the result -loses, and `by=` names what the result gains. Either may be a list. - -Each column of the relation is either **joined on** or not, and either -**grouped by** or not. The call decides both: - -| column of the relation | joined on | grouped by | in the result | -| ------------------------------------- | --------- | ---------- | ------------- | -| a column in `over=` | yes | no | removed | -| a column in `by=` | no | yes | added | -| a key column the call does not name | yes | yes | kept | -| a value column the call does not name | no | no | not read | - -With `zone_of: { key: [generator, period], values: zone }`, the bare -`connection: { key: [generator, bus], missing: absent }` and `p` over `[generator, period]`: - -| call | joins on | groups by | result | -| -------------------------------------------- | ------------------- | ----------------- | ------------------- | -| `sum(p, over=generator, by=zone_of[zone])` | `generator, period` | `zone, period` | `[zone, period]` | -| `sum(p, over=period, by=zone_of[zone])` | `period, generator` | `zone, generator` | `[generator, zone]` | -| `sum(p, over=generator, by=connection[bus])` | `generator` | `bus` | `[bus, period]` | - -- The result is the operand, less the dimensions in `over=`, plus the columns - in `by=`. The operand carries every column joined on, and none that the call - groups by, because a column the operand carries is joined on, not grouped - by. `sum(load * p, over=generator, by=gen_bus[bus])` is refused. Write - `load * sum(p, over=generator, by=gen_bus[bus])`. -- A name in `over=` is a column of the relation, or else a dimension. With - `rep_of: { key: snapshot, values: { rep: snapshot } }`, - `sum(x, over=snapshot, by=rep_of[rep])` joins on the key column `snapshot` - and groups by `rep`. Where two columns are over one dimension, the name says - which: `sum(x, over=from, by=nbr[to])`. A call writes every column it - reads, so you can add a value column to a relation and no call changes its - meaning. A name in both `over=` and `by=` is refused. -- A dimension in `over=` that no column is named after is summed away after the - group-by. `sum(p, over=[generator, snapshot], by=gen_bus[bus])` adds up each - bus over every snapshot. -- A sum adds up several rows per group. Where the columns grouped by hold the - whole key, every group is one row. That is a join with no group-by, which is - `at`, so `to_spec` refuses the sum and names `at`: - - ```text - Constraint 'cap': sum(by=zone_of[generator, period]): the columns this sum groups by, ['generator', 'period'], hold the whole key ['generator', 'period'], so every group is one row and the sum adds nothing. Write at(..., by=zone_of[zone]), or group by a value column. - ``` - -### Lookups - -`at(x, by=l[c])` reads `x` at the value of column `c`, once for every row of -the relation. It joins `x` on the columns in `by=` and groups by the key, so -every group is one row. - -- A lookup reads value columns. A key column in `by=` is refused, and so is a - bare relation, where a key tuple can have several rows. Sum through them - instead. -- The result gains the key columns, except where the operand carries them - already. A key column over a dimension the operand carries is joined on too, - unless a column in `by=` already matches that dimension. With `price` over - `[zone, period]`, `at(price, by=zone_of[zone])` joins on `zone` and - `period`, and its result is `[generator, period]`. `at(x, by=rep_of[rep])` - joins on `rep` alone, so the result gains the key column `snapshot`. -- No two columns in `by=` are over one dimension, because the operand carries - each dimension once, and so has one coordinate to read at. - -### Partitions - -`shift(x, along=d, within=l[c])`, `sum_back(x, along=d, within=l[c])` and -`position(d, within=l[c])` step along the key column over `d`, join on the -other key columns, and partition the rows by the value columns in `within=`. -The dimensions do not change. `within=` may name two columns over one -dimension, but not a key column. A bare relation partitions nothing, and a -coordinate the relation sends nowhere is in no group. - -### Tests - -A condition uses a relation at the coordinates of its own entry. It -compares a column under `values:`, written `l[c]`, against a label, compares -two columns of one table, or tests that a row exists -([conditions](expressions.md#conditions)). A comparison reads one column, -so `calendar[month, weekday] == 'x'` is refused. - -A bare relation name in a `where:` tests that a row exists, read at the -relation's key. On a variable, it builds the variable only at the rows of the -relation: - -```yaml -dimensions: - generator: { dtype: str } - bus: { dtype: str } - snapshot: { dtype: int } -relations: - connection: { key: [generator, bus], missing: absent } # the pairs that are wired -parameters: - load: { dims: [bus, snapshot] } -variables: - flow: - dims: [generator, bus, snapshot] - where: connection - bounds: { lower: 0 } -constraints: - balance: - dims: [bus, snapshot] - expression: sum(flow, over=generator) == load -``` - -$$\mathit{flow}_{g,b,t} \ge 0 \qquad \forall\, g \in \mathcal{G},\ b \in \mathcal{B},\ t \in \mathcal{T} \,:\, (g, b) \in \mathrm{connection}$$ - -$$\sum_{g \in \mathcal{G}} \mathit{flow}_{g,b,t} = \mathrm{load}_{b,t} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T}$$ - -`flow` has a column only at a pair that `connection` lists. At every other pair -it is [absent](absence.md), so `balance` adds up only the generators wired to -each bus. The mask reads `(generator, bus)`, so it is the same at every -snapshot. diff --git a/docs/reference/language/sets.md b/docs/reference/language/sets.md index 16468f28f..fc3eb6ca5 100644 --- a/docs/reference/language/sets.md +++ b/docs/reference/language/sets.md @@ -151,18 +151,111 @@ declares `missing: absent`, and prints nothing for `refused`. ## How a set or a property is read -A call names a subject and what it reads of it as `subject[name, …]`. Each name -is a member of the subject or a property of it. The call joins and groups by -the rules of a [relation](relations.md#how-a-relation-is-used), where a member -is a key column and a property is a value column. - -| kind | written as | -| ----------------- | ------------------------------------------------------------- | -| join and group by | `sum(p, over=generator, by=generator[bus])` | -| join | `at(price, by=generator_period[zone])` | -| partition | `shift(x, along=snapshot, offset=1, within=snapshot[period])` | -| test a value | `generator[bus] == 'b1'`, or `line[bus0] != line[bus1]` | -| test a property | `line[bus1]`, true where the line has the property | +A call reads one subject and names what it reads as `subject[name, …]`. Each +name is a member of the subject or a property of it. The call **joins** the +operand to the subject's table on the columns they share, and **groups** what +the join produces. A member is a key column of that table, and a property is a +value column. + +| kind | what it does | written as | +| ----------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| join and group by | rows of the operand match rows of the table, and each group is added up | `sum(p, over=generator, by=generator[bus])` | +| join | each row of the operand matches one row of the table, and reads its value | `at(price, by=generator_period[zone])` | +| partition | the dimensions stay, and the rows are grouped | `shift(x, along=snapshot, offset=1, within=snapshot[period])` | +| test | a row's value or its presence keeps or cuts a coordinate | `generator[bus] == 'b1'`, `line[bus0] != line[bus1]`, `line[bus1]` | + +Four rules hold for every call: + +1. A call names every column it reads, so `at(price, by=generator)` is + refused. Write `at(price, by=generator[bus])`. +2. A property the call does not name is not read. +3. A call reads one subject. To read through two, nest the calls: + `sum(sum(p, over=generator, by=generator[bus]), over=bus, by=bus[zone])`. +4. A dimension the subject does not name passes through to the result. + +### Sums + +`sum(x, over=d, by=s[c])` sums `x` over the columns in `over=`, grouped by the +columns in `by=`. As in `sum(x, over=d)`, `over=` names what the result loses, +and `by=` names what the result gains. Either may be a list. + +Each column of the subject's table is either **joined on** or not, and either +**grouped by** or not. The call decides both: + +| column of the table | joined on | grouped by | in the result | +| --------------------------------- | --------- | ---------- | ------------- | +| a column in `over=` | yes | no | removed | +| a column in `by=` | no | yes | added | +| a member the call does not name | yes | yes | kept | +| a property the call does not name | no | no | not read | + +With the set `generator_period: { dims: [generator, period] }` and its +property `zone`, the set `connection: { dims: [generator, bus] }`, and `p` +over `[generator, period]`: + +| call | joins on | groups by | result | +| --------------------------------------------------- | ------------------- | ----------------- | ------------------- | +| `sum(p, over=generator, by=generator_period[zone])` | `generator, period` | `zone, period` | `[period, zone]` | +| `sum(p, over=period, by=generator_period[zone])` | `period, generator` | `zone, generator` | `[generator, zone]` | +| `sum(p, over=generator, by=connection[bus])` | `generator` | `bus` | `[bus, period]` | + +- The result is the operand, less the dimensions in `over=`, plus the columns + in `by=`. The operand carries every column joined on, and none that the call + groups by. With `load` over `bus`, `sum(load * p, over=generator, by=generator[bus])` + is refused. Write `load * sum(p, over=generator, by=generator[bus])`. +- A name in `over=` is a column of the subject's table, or else a dimension. + With `snapshot: {rep: snapshot}`, `sum(x, over=snapshot, by=snapshot[rep])` + joins on the member `snapshot` and groups by `rep`. A name in both `over=` and + `by=` is refused. +- A dimension in `over=` that no column is named after is summed away after the + group-by. `sum(p, over=[generator, period], by=generator[bus])` adds up each + bus over every period. +- A sum adds up several rows per group. Where the columns grouped by are every + member of the subject, every group is one row. That is a join with no + group-by, which is `at`, so `to_spec` refuses the sum and names `at`: + + ```text + Named expression 'e': sum(by=generator_period[generator, period]): the columns this sum groups by, ['generator', 'period'], hold the whole key ['generator', 'period'], so every group is one row and the sum adds nothing. Write at(..., by=generator_period[zone]), or group by a property. + ``` + +### Lookups + +`at(x, by=s[c])` reads `x` at the value of property `c`, once for every row of +the subject. It joins `x` on the properties in `by=` and groups by the members, +so every group is one row. + +- A lookup reads properties. A member in `by=` is refused, and so is a set read + by its members alone, where a row is its own key. Sum through it instead. +- The result gains the members, except where the operand carries them already. + A member over a dimension the operand carries is joined on too, unless a + property in `by=` already maps into that dimension. With `price` over + `[zone, period]`, `at(price, by=generator_period[zone])` joins on `zone` and + `period`, and its result is `[generator, period]`. +- No two properties in `by=` map into one dimension, because the operand + carries each dimension once. + +### Partitions + +`shift(x, along=d, within=s[c])`, `sum_back(x, along=d, within=s[c])` and +`position(d, within=s[c])` step along the member over `d`, join on the other +members, and partition the rows by the properties in `within=`. The +dimensions do not change. A set read by its members alone partitions nothing, +and a coordinate with no value for the property is in no group. + +### Tests + +A condition reads a subject at the coordinates of its own entry: + +- `generator[bus] == 'b1'` compares a property against a label, and + `line[bus0] != line[bus1]` compares two properties into one dimension. A + comparison reads one property, and it is false where the row lacks it, + whatever the operator. +- `line[bus1]` alone is true where the line has the property, and + `generator[bus, carrier]` is true where the row has both. +- A set's bare name is true where the set has a row + ([entries over a set](#entries-over-a-set)). + +### Several properties ```yaml dimensions: @@ -191,15 +284,11 @@ $$p_{g,t} \ge 0 \qquad \forall\, g \in \mathcal{G},\ t \in \mathcal{T} \,:\, \ma $$\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b \wedge \mathrm{carrier}(g) = c} p_{g,t} \le \mathrm{limit}_{b,c} \qquad \forall\, b \in \mathcal{B},\ c \in \mathcal{C},\ t \in \mathcal{T}$$ -- A call that names several properties reads their **inner join**. A generator - with no `carrier` has no row in it, so `carrier_limit` adds up only the - generators that have a bus and a carrier. -- `subject[property]` alone in a condition is true where the row has the - property. `generator[bus, carrier]` is true where the row has both. -- A call reads one subject. To read through two, nest the calls: - `sum(sum(p, over=generator, by=generator[bus]), over=bus, by=bus[zone])`. -- A dimension is read by its properties, because it has no table of its own: +A call that names several properties reads their **inner join**. A generator +with no `carrier` has no row in it, so `carrier_limit` adds up only the +generators that have a bus and a carrier. A dimension is read by its +properties, because it has no table of its own: - ```text - Constraint 'carrier_limit': sum(by=generator[generator]) reads no property of 'generator', and a dimension has no table of its own. Name one of its properties, ['bus', 'carrier']. - ``` +```text +Constraint 'carrier_limit': sum(by=generator[generator]) reads no property of 'generator', and a dimension has no table of its own. Name one of its properties, ['bus', 'carrier']. +``` diff --git a/docs/reference/notation.md b/docs/reference/notation.md index 143328ef1..deafa8c26 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -24,7 +24,7 @@ nothing else. ### Legend -A dimension, a relation and a parameter declare no equation; what they print is the legend every spec opens with. +A dimension, a set, a property and a parameter declare no equation; what they print is the legend every spec opens with. ```yaml dimensions: @@ -36,15 +36,26 @@ dimensions: technology: { dtype: str } bp: { dtype: int, ordered: true } # the breakpoints every curve below runs through -relations: - gen_bus: { key: generator, values: bus } - zone_of: { key: bus, values: zone } - area_of: { key: bus, values: zone, missing: absent } # a second map into the same set, to compare against; a bus may be in no area - season_of: { key: snapshot, values: season } - gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call sums one away and joins on the other - rep_of: { key: snapshot, values: { rep: snapshot } } # a map into its own dimension: the representative snapshot - connection: { key: [generator, bus], missing: absent } # a bare relation, with no value columns: many-to-many, read only by sum with both ends named - gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once +sets: + dispatch_pair: { dims: [generator, bus], missing: absent } # a set: the pairs that exist, and the subject of a property below + generator_snapshot: { dims: [generator, snapshot] } # a set over two dimensions: a call through its property sums one away and joins on the other + connection: { dims: [generator, bus], missing: absent } # a set with no property: many-to-many, read only by sum with both members named + +properties: + generator: + fuel: { into: technology, missing: absent } # a property of a dimension: the legend names it after itself, and says a generator may have none + bus: bus + technology: technology # with bus, two properties read at once: the call reads their inner join + bus: + zone: zone + area: { into: zone, missing: absent } # a second property into the same dimension, to compare against; a bus may be in no area + snapshot: + season: season + rep: snapshot # a property into its own dimension: the representative snapshot + generator_snapshot: + zone: zone + dispatch_pair: + area: zone # a property of a set: the legend maps it from the set by name parameters: p_max: { dims: [generator], missing: .inf } # a value: the legend says what a missing row reads as @@ -70,12 +81,12 @@ parameters: | Symbol | Meaning | |---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` (`int` coordinates) with $`\mathrm{season\_of}: \mathcal{T} \to \mathcal{S},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{rep\_of}: \mathcal{T} \to \mathcal{T}`$ | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row | -| $`\mathcal{B}`$ | index $`b`$ — `bus` with $`\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `area_of` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row | -| $`\mathcal{Z}`$ | index $`z`$ — `zone` with $`\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `area_of` is `absent` where the data has no row | -| $`\mathcal{S}`$ | index $`s`$ — `season` with $`\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}`$ | -| $`\mathcal{E}`$ | index $`e`$ — `technology` with $`\mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E}`$, `generator[fuel]` is `absent` where the data has no row | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` (`int` coordinates) with $`\mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{season}: \mathcal{T} \to \mathcal{S},\ \mathrm{rep}: \mathcal{T} \to \mathcal{T},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z}`$ | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row | +| $`\mathcal{B}`$ | index $`b`$ — `bus` with $`\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `bus[area]` is `absent` where the data has no row | +| $`\mathcal{Z}`$ | index $`z`$ — `zone` with $`\mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `bus[area]` is `absent` where the data has no row | +| $`\mathcal{S}`$ | index $`s`$ — `season` with $`\mathrm{season}: \mathcal{T} \to \mathcal{S}`$ | +| $`\mathcal{E}`$ | index $`e`$ — `technology` with $`\mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E}`$, `generator[fuel]` is `absent` where the data has no row | | $`\mathcal{A}`$ | index $`a`$ — `bp` | #### Parameters @@ -144,9 +155,9 @@ parameters: |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | | $`t \boxminus_{v} k`$ | `edge=v` — $`t-k`$, and $`v`$ past the edge | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | | $`\lvert \mathcal{T} \rvert`$ | the size of $`\mathcal{T}`$; $`\lvert \mathcal{T} \rvert - 1`$ is the last position | ### Variable domains @@ -375,19 +386,19 @@ objective: \max \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot p_{t,g} \cdot \mathrm{cost}_{g} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g} \cdot \mathrm{growth}^{\mathrm{lead}_{g}} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \left( \mathrm{growth}^{\mathrm{lead}_{g}} \right)^{2} + \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{p}^{\mathrm{max}}_{g} - \mathit{reserve} - \mathit{headroom} ``` -### Relations +### Sets and properties -#### Sum through a relation +#### Sum through a property ```yaml constraints: balance: dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) + spill - slack == load + expression: sum(p, over=generator, by=generator[bus]) + spill - slack == load ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` #### Sum over every dimension @@ -405,22 +416,22 @@ constraints: \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \le \mathrm{budget} ``` -#### `at` through a relation +#### `at` through a property -at(), which re-indexes through a relation instead of an offset +at(), which re-indexes through a property instead of an offset ```yaml constraints: lookup: dims: [snapshot, bus] - expression: spill <= at(zone_cap, by=zone_of[zone]) + expression: spill <= at(zone_cap, by=bus[zone]) ``` ```math -\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` -#### Sum into two value columns +#### Sum into two properties one table read to two value columns: the domain carries a condition per column @@ -428,14 +439,14 @@ one table read to two value columns: the domain carries a condition per column constraints: grouped_once: dims: [snapshot, bus, technology] - expression: sum(p, over=generator, by=gen_bt[bus, technology]) <= tech_cap + expression: sum(p, over=generator, by=generator[bus, technology]) <= tech_cap ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bt.bus}(g) = b \wedge \mathrm{gen\_bt.technology}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b \wedge \mathrm{technology}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} ``` -#### `at` through two value columns +#### `at` through two properties the same table joined the other way, reading one slot through two columns @@ -443,14 +454,14 @@ the same table joined the other way, reading one slot through two columns constraints: looked_up_once: dims: [generator] - expression: units <= at(tech_cap, by=gen_bt[bus, technology]) + expression: units <= at(tech_cap, by=generator[bus, technology]) ``` ```math -\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{gen\_bt.bus}(g),\mathrm{gen\_bt.technology}(g)} \qquad \forall\, g \in \mathcal{G} +\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{bus}(g),\mathrm{technology}(g)} \qquad \forall\, g \in \mathcal{G} ``` -#### Shift within one value column +#### Shift within one of two properties a partition grouped by one named value column of a two-value table, and a position within both @@ -458,17 +469,17 @@ a partition grouped by one named value column of a two-value table, and a positi constraints: within_bus: dims: [generator] - where: "position(generator, within=gen_bt[bus, technology]) == 0" - expression: units <= shift(units, along=generator, offset=1, edge=0, within=gen_bt[bus]) + where: "position(generator, within=generator[bus, technology]) == 0" + expression: units <= shift(units, along=generator, offset=1, edge=0, within=generator[bus]) ``` ```math -\mathit{units}_{g} \le \mathit{units}_{g \boxminus_{0}^{\mathrm{gen\_bt.bus}(g)} 1} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{gen\_bt.bus}(g),\ \mathrm{gen\_bt.technology}(g) \right)}(g) = 0 +\mathit{units}_{g} \le \mathit{units}_{g \boxminus_{0}^{\mathrm{bus}(g)} 1} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{bus}(g),\ \mathrm{technology}(g) \right)}(g) = 0 ``` -#### Sum through a bare relation +#### Sum through a set -a sum through a bare relation: the domain is a row of the relation rather than a function's value +a sum through a set: the domain is a row of the set rather than a function's value ```yaml constraints: @@ -481,9 +492,9 @@ constraints: \sum_{g \in \mathcal{G} \,:\, \left( g,\ b \right) \in \mathrm{connection}} p_{t,g} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` -#### Bare relation as a condition +#### Set as a condition -a bare relation as a where: the row of the frame has to be a member of the relation +a set as a where: the row of the frame has to be a row of the set ```yaml constraints: @@ -497,7 +508,7 @@ constraints: p_{t,g} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \left( g,\ b \right) \in \mathrm{connection} ``` -#### Map into its own dimension +#### Property into its own dimension a map into its own dimension, read both ways: the frame is unchanged and the index is primed @@ -505,11 +516,11 @@ a map into its own dimension, read both ways: the frame is unchanged and the ind constraints: representative: dims: [snapshot] - expression: sum(spill, over=snapshot, by=rep_of[rep]) <= at(spill, by=rep_of[rep]) + expression: sum(spill, over=snapshot, by=snapshot[rep]) <= at(spill, by=snapshot[rep]) ``` ```math -\sum_{t' \in \mathcal{T} \,:\, \mathrm{rep\_of}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep\_of}(t)} \qquad \forall\, t \in \mathcal{T} +\sum_{t' \in \mathcal{T} \,:\, \mathrm{rep}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep}(t)} \qquad \forall\, t \in \mathcal{T} ``` #### Sum through a property of a dimension @@ -558,7 +569,7 @@ constraints: p_{t,g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{fuel}(g) \text{ is defined} ``` -#### Sum through a two-key map +#### Sum over one member of a set, through its property a grouping through a two-key map, summing one key away: the condition reads the other, and the row keeps it @@ -566,14 +577,14 @@ a grouping through a two-key map, summing one key away: the condition reads the constraints: zonal: dims: [snapshot, zone] - expression: sum(p, over=generator, by=gen_zone[zone]) <= zone_cap + expression: sum(p, over=generator, by=generator_snapshot[zone]) <= zone_cap ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} +\sum_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} ``` -#### Sum over the other key of a two-key map +#### Sum over the other member of a set the same table summing its other key away @@ -581,14 +592,14 @@ the same table summing its other key away constraints: zonal_history: dims: [generator, zone] - expression: sum(p, over=snapshot, by=gen_zone[zone]) <= zone_cap + expression: sum(p, over=snapshot, by=generator_snapshot[zone]) <= zone_cap ``` ```math -\sum_{t \in \mathcal{T} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} +\sum_{t \in \mathcal{T} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} ``` -#### Sum between the two keys of a map +#### Sum between the two members of a set the same table read between its two key columns: no value column is read, so the domain asks only that the row is there @@ -596,14 +607,14 @@ the same table read between its two key columns: no value column is read, so the constraints: zonal_membership: dims: [snapshot] - expression: sum(units, over=generator, by=gen_zone[snapshot]) <= budget + expression: sum(units, over=generator, by=generator_snapshot[snapshot]) <= budget ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) \text{ is defined}} \mathit{units}_{g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} +\sum_{g \in \mathcal{G} \,:\, \left( g,\ t \right) \in \mathrm{generator\_snapshot}} \mathit{units}_{g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} ``` -#### `at` through a two-key map +#### `at` through a property of a set the same table joined the other way, reading the slot the row's own snapshot puts the generator in @@ -611,12 +622,12 @@ the same table joined the other way, reading the slot the row's own snapshot put constraints: zonal_lookup: dims: [snapshot, generator] - where: "gen_zone == 'north' AND position(generator, within=gen_zone[zone]) == 0" - expression: p <= at(spill * zone_cap, by=gen_zone[zone]) + where: "generator_snapshot[zone] == 'north' AND position(generator, within=generator_snapshot[zone]) == 0" + expression: p <= at(spill * zone_cap, by=generator_snapshot[zone]) ``` ```math -p_{t,g} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{gen\_zone}(g,\ t)} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{gen\_zone}(g,\ t)}(g) = 0 +p_{t,g} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{zone}(g,\ t)} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{zone}(g,\ t)}(g) = 0 ``` ### Arithmetic and literals @@ -693,11 +704,11 @@ names the signed sum on the right of a plus: inlined, its minus prints as a subt constraints: netted: dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) + net == load + expression: sum(p, over=generator, by=generator[bus]) + net == load ``` ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{net}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{net}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` #### Cased expression in a constraint @@ -783,12 +794,12 @@ expressions: dims: [snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: cost } - winter: { when: "position(snapshot) > 0 and season_of == 'winter'", expression: cost * 2 } + winter: { when: "position(snapshot) > 0 and snapshot[season] == 'winter'", expression: cost * 2 } otherwise: 0 ``` ```math -\mathrm{startup\_cost}_{t,g} = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season\_of}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\mathrm{startup\_cost}_{t,g} = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` #### Data-only expression @@ -1002,17 +1013,17 @@ p_{t \boxminus_{0} \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \fo #### Cyclic shift within a group -a translation partitioned by a relation: the group rides on the operator +a translation partitioned by a property: the group rides on the operator ```yaml constraints: in_season: dims: [snapshot, generator] - expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[season]) ``` ```math -p_{t,g} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +p_{t,g} \le p_{t \ominus^{\mathrm{season}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` #### Filled shift within a group @@ -1023,11 +1034,11 @@ the same group, with a fill: each season's opening row is kept and given a zero constraints: held_in_season: dims: [snapshot, generator] - expression: p <= shift(p, along=snapshot, offset=1, edge=0, within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge=0, within=snapshot[season]) ``` ```math -p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` ### Trailing windows @@ -1064,17 +1075,17 @@ constraints: #### Window within a group -a window partitioned by a relation: the group rides on the operator +a window partitioned by a property: the group rides on the operator ```yaml constraints: seasonal_window: dims: [snapshot, generator] - expression: sum_back(on, along=snapshot, window=3, within=season_of[season]) <= units + expression: sum_back(on, along=snapshot, window=3, within=snapshot[season]) <= units ``` ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season\_of}(t)} t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season}(t)} t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` ### Piecewise curves @@ -1391,17 +1402,17 @@ assumptions: \mathrm{lead}_{g} \le 3 \qquad \forall\, g \in \mathcal{G} ``` -#### Two relations compared +#### Two properties compared two maps into one set, compared row by row ```yaml assumptions: - zones_agree: "zone_of == area_of" + zones_agree: "bus[zone] == bus[area]" ``` ```math -\mathrm{zone\_of}(b) = \mathrm{area\_of}(b) \qquad \forall\, b \in \mathcal{B} +\mathrm{zone}(b) = \mathrm{area}(b) \qquad \forall\, b \in \mathcal{B} ``` #### Reduction in an assumption @@ -1447,19 +1458,19 @@ assumptions: \mathrm{p}^{\mathrm{min}}_{g} < \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{is\_flexible}_{g} ``` -#### Assumption with a relation condition +#### Assumption with a property condition -a relation comparison as the where, over a frame two dims wide +a property comparison as the where, over a frame two dims wide ```yaml assumptions: northern_demand_is_real: holds: "load >= 0" - where: "zone_of == 'north'" + where: "bus[zone] == 'north'" ``` ```math -\mathrm{load}_{t,b} \ge 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} +\mathrm{load}_{t,b} \ge 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} ``` ### Where conditions @@ -1504,12 +1515,12 @@ a position in a dimension, and the same position within a group constraints: first: dims: [snapshot, generator] - where: "position(snapshot) == 0 OR position(snapshot, within=season_of[season]) == 0" + where: "position(snapshot) == 0 OR position(snapshot, within=snapshot[season]) == 0" expression: on == 1 ``` ```math -\mathit{on}_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = 0 +\mathit{on}_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = 0 ``` #### Position counted from the end @@ -1520,28 +1531,28 @@ the same two counted from the end, which print against a size rather than as the constraints: last: dims: [snapshot, generator] - where: "position(snapshot) == -1 OR position(snapshot, within=season_of[season]) == -1" + where: "position(snapshot) == -1 OR position(snapshot, within=snapshot[season]) == -1" expression: on == 0 ``` ```math -\mathit{on}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season\_of}(t)} \rvert - 1 +\mathit{on}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season}(t)} \rvert - 1 ``` #### Relation compared to a label -a relation compared to a label, to another relation, and to nothing +a property compared to a label, to another property, and to nothing ```yaml constraints: northern: dims: [snapshot, bus] - where: "zone_of == 'north' AND zone_of != area_of AND zone_of" + where: "bus[zone] == 'north' AND bus[zone] != bus[area] AND bus[zone]" expression: slack <= load ``` ```math -\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} +\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone}(b) \neq \mathrm{area}(b) \wedge \mathrm{zone}(b) \text{ is defined} ``` #### Constant true condition @@ -1610,18 +1621,18 @@ p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g #### Shift and lookup in a condition -a translation under a comparison names its edge, a lookup reads through a relation, and the position keeps the vacated row out +a translation under a comparison names its edge, a lookup reads through a property, and the position keeps the vacated row out ```yaml constraints: ramped: dims: [snapshot, bus] - where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=zone_of[zone]) AND position(snapshot) > 0" + where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=bus[zone]) AND position(snapshot) > 0" expression: slack <= load ``` ```math -\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \wedge \mathrm{pos}(t) > 0 +\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} \wedge \mathrm{pos}(t) > 0 ``` #### Reduction in a scalar condition @@ -1688,19 +1699,19 @@ constraints: \mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} \text{ is defined} \wedge \neg \left( \mathrm{load}_{t - 1,b} \text{ is defined} \right) ``` -#### Condition through a relation +#### Condition through a property -a condition read through a relation: a bus is held only where its zone has a cap at all +a condition read through a property: a bus is held only where its zone has a cap at all ```yaml constraints: zoned: dims: [bus] - where: "at(zone_cap, by=zone_of[zone])" + where: "at(zone_cap, by=bus[zone])" expression: theta <= budget ``` ```math -\theta_{b} \le \mathrm{budget} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \text{ is defined} +\theta_{b} \le \mathrm{budget} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone}(b)} \text{ is defined} ``` diff --git a/docs/reference/reading.md b/docs/reference/reading.md index f3498d77b..401dd302d 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -125,7 +125,7 @@ means: `'refused'`, `'absent'`, or the value that the row reads as. A given parameter is a `GivenParameter` under `program.given.parameters`, with its `dims` and `dtype` and no `missing` or `where`, because the file that declares the parameter says. The program of `spec.expand(...)` reports the same values. -`program.relations[name].missing` is `'refused'` or `'absent'`. +`program.relations[name].missing` is `'refused'` or `'absent'`, for a set and a property alike. Under `'refused'`, the engine does not build the model from a table with a missing row, and names the coordinate. Each engine decides whether it raises at @@ -141,7 +141,7 @@ it the parameter is absent wherever it is read, and the engine drops the rows of the data there when it attaches. Then it applies `missing:` to the coordinates the mask keeps: a value fills a missing row there, and `'refused'` asks for a row only there. A mask reads no variable and no masked parameter, so the -engine can evaluate each mask from the dimensions, the relations and the +engine can evaluate each mask from the dimensions, the sets, the properties and the unmasked parameters, before it reads any masked parameter ([a parameter's mask](language/parameters-variables-constraints.md#a-parameters-mask)). @@ -150,10 +150,9 @@ An engine must refuse a divisor that is `0` in a row it builds, whatever made it ([zero divisors in the data](language/absence.md#zero-divisors-in-the-data)). A `RelationComparison` or a `RelationPairComparison` is false at a coordinate -where a relation it reads has no row, whatever its operator: `!=` is false -there too. A `Not` of it is then true. So `connection != 'input'` keeps no -coordinate that `connection` leaves out, and `NOT connection == 'input'` keeps -every one. +where a table it reads has no row, whatever its operator: `!=` is false +there too. A `Not` of it is then true. So `line[bus1] != 'b1'` keeps no +line that has no `bus1`, and `NOT line[bus1] == 'b1'` keeps every one. ## The tables of sets and properties @@ -177,7 +176,7 @@ table for each set and one for each property, and no table holds a null. relation with no value columns, under its own name. A property is a relation under `subject.property`, keyed by the subject's members, with one value column, and its `subject` names the dimension or set whose rows its key ranges -over. A relation from `relations:` has `subject` set to `None`. +over. A set has `subject` set to `None`. ```python from mathspec import to_spec @@ -232,7 +231,7 @@ a walk reads through it. Every expression node has `dims`: the dimensions over which it is indexed, sorted by name. A sum has the dimensions its operand has, -less the ones it sums over. A `Join` under a sum through a relation has the +less the ones it sums over. A `Join` under a sum through a property has the dimensions of that sum. A `Join` also opens one `Axis` for each column it drops, but an axis is not a dimension and is not in `dims`. An `ExpressionReference` has the dimensions of its body. @@ -259,8 +258,8 @@ The mask also answers four questions: A comparison of expressions arrives as an `ExpressionComparison`. Its two sides are program expressions like a constraint's, and its `dims` are every -dimension either side carries. Its `names_read` are every parameter and relation -that the sides read, including the relation that a grouping reads through. +dimension either side carries. Its `names_read` are every parameter and table +that the sides read, including the tables that a grouping reads through. A name compared against a literal does not arrive this way. `p_max > 5` is a `ParameterComparison` and `1 * p_max > 5` is an `ExpressionComparison`, though @@ -270,10 +269,10 @@ Three conditions read another condition rather than an entry. A `CountComparison` carries the mask it counts and the dimension it counts away. A `TranslatedCondition` carries the mask it reads at a neighbouring coordinate. A `JoinedCondition` carries the mask it reads through a -relation, and the `JoinColumns` it joins on and groups by. Each holds that +property, and the `JoinColumns` it joins on and groups by. Each holds that mask as a `Mask`, where a connective holds a bare condition, so the walk recurses through a connective and stops at these. `.names_read` and `.dims` see -through all three, and the relation a `JoinedCondition` reads is in its +through all three, and the tables a `JoinedCondition` reads are in its `.names_read`. `Mask(condition)` answers the same four questions of any resolved condition, @@ -370,7 +369,7 @@ introduced is named under the entry the expansion emitted. shift, or a set. In brackets, each entry names the one change that would remove the tie. - `undecided` lists each read whose reach only the data can decide, as a `Reach`: - the entry, the parameter or relation it reads, and the kind of read. A + the entry, the parameter or table it reads, and the kind of read. A caller that holds the data hands the smallest value of each named parameter to `resolved`, which returns the report with those reads decided. - `restarts` names each entry that counts a `position()` along the axis. @@ -416,7 +415,7 @@ spec.to_yaml(canonical=True) == to_spec(spec.to_yaml(canonical=True)).to_yaml(ca ``` - The sections come in one order, whatever order the file wrote them in: - `version`, `description`, `given`, `dimensions`, `relations`, `parameters`, + `version`, `description`, `given`, `dimensions`, `sets`, `properties`, `parameters`, `conditions`, `variables`, `expressions`, `objective`, `constraints`, `sos`, `piecewise`, `assumptions`, `macros`. The keys of an entry also come in one order. diff --git a/docs/several-files.md b/docs/several-files.md index eb694115e..2a002b7e9 100644 --- a/docs/several-files.md +++ b/docs/several-files.md @@ -80,8 +80,11 @@ dimensions: bus: { dtype: str } generator: { description: generating units } -relations: - gen_bus: { key: generator, values: bus, description: the bus a generator sits on } +properties: + generator: + bus: + description: the bus a generator sits on + into: bus parameters: capacity: { dims: [generator], description: installed capacity } @@ -96,7 +99,7 @@ variables: expressions: generation: description: what the fleet puts into a bus - expression: sum(dispatch, over=generator, by=gen_bus[bus]) + expression: sum(dispatch, over=generator, by=generator[bus]) adds_to: injection generation_cost: description: what running the fleet costs @@ -146,7 +149,7 @@ feasibility problem: **`generation`** ```math - \mathit{generation}_{b,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T} + \mathit{generation}_{b,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T} ``` **`generation_cost`** @@ -233,7 +236,7 @@ print(ms.to_markdown(spec, legend=False)) **`generation`** ```math - \mathit{generation}_{b,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T} + \mathit{generation}_{b,t} = \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} \mathit{dispatch}_{t,g} \qquad \forall\, b \in \mathcal{B},\ t \in \mathcal{T} ``` **`generation_cost`** diff --git a/examples/library/generator.yaml b/examples/library/generator.yaml index d99bf6922..b3cf8eb39 100644 --- a/examples/library/generator.yaml +++ b/examples/library/generator.yaml @@ -17,8 +17,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } port: { dtype: str, description: "the connections components make, one label per connection" } generator: { dtype: str, description: "generating units, each on one port" } -relations: - Generator_port: { key: generator, values: port } +properties: + generator: + port: port + parameters: Generator_p_nom: { dims: [generator], description: nominal power } Generator_marginal_cost: { dims: [generator], description: cost of one unit of output } @@ -33,7 +35,7 @@ constraints: what a generator produces is what its port injects. No PyPSA row stands for this: PyPSA writes the generator into the balance instead dims: [snapshot, generator] - expression: at(Port_p, by=Generator_port[port]) == Generator_p + expression: at(Port_p, by=generator[port]) == Generator_p expressions: Generator_cost: expression: sum(Generator_p * Generator_marginal_cost) diff --git a/examples/library/load.yaml b/examples/library/load.yaml index 102f3e01d..f997880e5 100644 --- a/examples/library/load.yaml +++ b/examples/library/load.yaml @@ -12,8 +12,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } port: { dtype: str, description: "the connections components make, one label per connection" } load: { dtype: str, description: "demands, each on one port" } -relations: - Load_port: { key: load, values: port } +properties: + load: + port: port + parameters: Load_p_set: { dims: [snapshot, load], description: "`Load-p_set` — what a load takes in a snapshot" } constraints: @@ -22,4 +24,4 @@ constraints: what a load takes is what its port withdraws. No PyPSA row stands for this: PyPSA writes the load into the balance instead dims: [snapshot, load] - expression: at(Port_p, by=Load_port[port]) == -Load_p_set + expression: at(Port_p, by=load[port]) == -Load_p_set diff --git a/examples/library/surface.yaml b/examples/library/surface.yaml index f126035ba..e1ac676d2 100644 --- a/examples/library/surface.yaml +++ b/examples/library/surface.yaml @@ -15,8 +15,10 @@ dimensions: snapshot: { dtype: datetime, description: dispatch periods } bus: { dtype: str, description: network nodes } port: { dtype: str, description: "the connections components make, one label per connection" } -relations: - Port_bus: { key: port, values: bus } +properties: + port: + bus: bus + variables: Port_p: dims: [snapshot, port] @@ -25,7 +27,7 @@ constraints: Bus_nodal_balance: description: "`Bus-nodal_balance` — what the ports on a bus put in nets to nothing" dims: [snapshot, bus] - expression: sum(Port_p, over=port, by=Port_bus[bus]) == 0 + expression: sum(Port_p, over=port, by=port[bus]) == 0 objective: sense: minimize expression: total_cost diff --git a/examples/operators/at.yaml b/examples/operators/at.yaml index 4b038e041..75a8e0383 100644 --- a/examples/operators/at.yaml +++ b/examples/operators/at.yaml @@ -10,8 +10,9 @@ dimensions: snapshot: { dtype: int } period: { dtype: int } -relations: - period_of: { key: snapshot, values: period } +properties: + snapshot: + period: period parameters: cap: { dims: [period] } @@ -24,6 +25,6 @@ variables: constraints: within_cap: dims: [snapshot] - expression: p <= at(cap, by=period_of[period]) + expression: p <= at(cap, by=snapshot[period]) objective: { sense: minimize, expression: sum(p) } diff --git a/examples/operators/at_columns.yaml b/examples/operators/at_columns.yaml index e138a4789..efd89ce00 100644 --- a/examples/operators/at_columns.yaml +++ b/examples/operators/at_columns.yaml @@ -10,8 +10,10 @@ dimensions: line: { dtype: str } bus: { dtype: str } -relations: - ends: { key: line, values: { bus0: bus, bus1: bus } } +properties: + line: + bus0: bus + bus1: bus parameters: cap: { dims: [bus] } @@ -24,6 +26,6 @@ variables: constraints: sending_cap: dims: [line] - expression: f <= at(cap, by=ends[bus0]) + expression: f <= at(cap, by=line[bus0]) objective: { sense: minimize, expression: sum(f) } diff --git a/examples/operators/shift_partitioned.yaml b/examples/operators/shift_partitioned.yaml index 62e7c895e..40281142f 100644 --- a/examples/operators/shift_partitioned.yaml +++ b/examples/operators/shift_partitioned.yaml @@ -10,8 +10,9 @@ dimensions: snapshot: { dtype: int, ordered: true } season: { dtype: str } -relations: - season_of: { key: snapshot, values: season } +properties: + snapshot: + season: season variables: p: @@ -21,6 +22,6 @@ variables: constraints: no_faster_than_before_in_season: dims: [snapshot] - expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[season]) objective: { sense: minimize, expression: sum(p) } diff --git a/examples/operators/sum_back_partitioned.yaml b/examples/operators/sum_back_partitioned.yaml index 9892d4e20..afaa71735 100644 --- a/examples/operators/sum_back_partitioned.yaml +++ b/examples/operators/sum_back_partitioned.yaml @@ -12,8 +12,9 @@ dimensions: hour: { dtype: int, ordered: true } day: { dtype: str } -relations: - day_of: { key: hour, values: day } +properties: + hour: + day: day variables: started: @@ -26,6 +27,6 @@ variables: constraints: stays_up_inside_its_day: dims: [unit, hour] - expression: sum_back(started, along=hour, window=3, within=day_of[day]) <= on + expression: sum_back(started, along=hour, window=3, within=hour[day]) <= on objective: { sense: minimize, expression: sum(on) } diff --git a/examples/operators/sum_by.yaml b/examples/operators/sum_by.yaml index 34a4bbd11..4450f3571 100644 --- a/examples/operators/sum_by.yaml +++ b/examples/operators/sum_by.yaml @@ -12,8 +12,9 @@ dimensions: generator: { dtype: str } bus: { dtype: str } -relations: - gen_bus: { key: generator, values: bus } +properties: + generator: + bus: bus parameters: limit: { dims: [snapshot, bus] } @@ -26,6 +27,6 @@ variables: constraints: bus_total: dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) <= limit + expression: sum(p, over=generator, by=generator[bus]) <= 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 6a10c80a9..4d0c7909a 100644 --- a/examples/operators/sum_by_column_lists.yaml +++ b/examples/operators/sum_by_column_lists.yaml @@ -12,8 +12,13 @@ dimensions: bus: { dtype: str } technology: { dtype: str } -relations: - slot_of: { key: [generator, period], values: [bus, technology] } +sets: + generator_period: { dims: [generator, period] } + +properties: + generator_period: + bus: bus + technology: technology parameters: cap: { dims: [bus, technology] } @@ -26,6 +31,6 @@ variables: constraints: slot_cap: dims: [bus, technology] - expression: sum(p, over=[generator, period], by=slot_of[bus, technology]) <= cap + expression: sum(p, over=[generator, period], by=generator_period[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 446881141..37e580e76 100644 --- a/examples/operators/sum_by_columns.yaml +++ b/examples/operators/sum_by_columns.yaml @@ -13,8 +13,12 @@ dimensions: period: { dtype: int } zone: { dtype: str } -relations: - zone_of: { key: [generator, period], values: zone } +sets: + generator_period: { dims: [generator, period] } + +properties: + generator_period: + zone: zone parameters: demand: { dims: [zone, period] } @@ -27,6 +31,6 @@ variables: constraints: zone_balance: dims: [zone, period] - expression: sum(p, over=generator, by=zone_of[zone]) >= demand + expression: sum(p, over=generator, by=generator_period[zone]) >= demand objective: { sense: minimize, expression: sum(p) } diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index a909a7321..18c9f2257 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -1400,131 +1400,120 @@ parameters: dims: [global_constraint, process] missing: absent -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Generator_carrier: - description: the carrier a generator converts from - key: generator - values: carrier - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus - Link_carrier: - description: the carrier a link converts from - key: link - values: carrier - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus - Process_carrier: - description: the carrier a process converts from - key: process - values: carrier - Process_output_process: - description: the process a port belongs to - key: process_output - values: process - Process_output_bus: - description: >- - the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … - columns. A process of three ports is three labels here rather than a - third relation, so the file states any number of them - key: process_output - values: bus - Load_bus: - description: the bus a load sits on - key: load - values: bus - StorageUnit_carrier: - description: the carrier a storage unit converts from - key: storage_unit - values: carrier - StorageUnit_bus: - description: the bus a storage unit sits on - key: storage_unit - values: bus - Line_carrier: - description: the carrier a line carries - key: line - values: carrier - Line_bus0: - description: the bus a line's flow is measured at - key: line - values: bus - Line_bus1: - description: the bus at a line's other end - key: line - values: bus - Store_carrier: - description: the carrier a store holds - key: store - values: carrier - Store_bus: - description: the bus a store sits on - key: store - values: bus - Transformer_bus0: - description: the bus a transformer's flow is measured at - key: transformer - values: bus - Transformer_bus1: - description: the bus at a transformer's other end - key: transformer - values: bus - Outage_line: - description: the line an outage takes out; an outage of a transformer has no row - key: outage - values: line - missing: absent - Outage_transformer: - description: the transformer an outage takes out; an outage of a line has no row - key: outage - values: transformer - missing: absent +sets: Generator_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a generator that is - not maintainable - key: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a generator that is not maintainable + dims: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } missing: absent Link_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a link that is - not maintainable - key: { scenario: scenario, link: link, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a link that is not maintainable + dims: { scenario: scenario, link: link, start: snapshot, covered: snapshot } missing: absent Process_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a process that is - not maintainable - key: { scenario: scenario, process: process, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a process that is not maintainable + dims: { scenario: scenario, process: process, start: snapshot, covered: snapshot } missing: absent +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + generator: + carrier: + description: the carrier a generator converts from + into: carrier + bus: + description: the bus a generator sits on + into: bus + link: + carrier: + description: the carrier a link converts from + into: carrier + bus0: + description: the bus a link leaves + into: bus + link_output: + link: + description: the link an output port belongs to + into: link + bus: + description: >- + the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. + A link of three output ports is three labels here rather than a third + relation, so the file states any number of them + into: bus + process: + carrier: + description: the carrier a process converts from + into: carrier + process_output: + process: + description: the process a port belongs to + into: process + bus: + description: >- + the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … + columns. A process of three ports is three labels here rather than a + third relation, so the file states any number of them + into: bus + load: + bus: + description: the bus a load sits on + into: bus + storage_unit: + carrier: + description: the carrier a storage unit converts from + into: carrier + bus: + description: the bus a storage unit sits on + into: bus + line: + carrier: + description: the carrier a line carries + into: carrier + bus0: + description: the bus a line's flow is measured at + into: bus + bus1: + description: the bus at a line's other end + into: bus + store: + carrier: + description: the carrier a store holds + into: carrier + bus: + description: the bus a store sits on + into: bus + transformer: + bus0: + description: the bus a transformer's flow is measured at + into: bus + bus1: + description: the bus at a transformer's other end + into: bus + outage: + line: + description: the line an outage takes out; an outage of a transformer has no row + into: line + missing: absent + transformer: + description: the transformer an outage takes out; an outage of a line has no row + into: transformer + missing: absent + variables: Generator_p: description: "`Generator-p` — output of a generator in a snapshot" @@ -2251,11 +2240,11 @@ expressions: when: StorageUnit_cyclic_state_of_charge_per_period expression: >- StorageUnit_retention - * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: when: >- StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND position(snapshot, within=snapshot_period[period]) == 0 + AND position(snapshot, within=snapshot[period]) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) Store_energy_carried_in: @@ -2283,9 +2272,9 @@ expressions: expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: - when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot_period[period]) == 0 + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot[period]) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) Link_output_arrival: @@ -2301,8 +2290,8 @@ expressions: cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot_period[period]) * Link_efficiency - otherwise: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot_period[period]) * Link_efficiency + expression: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot[period]) * Link_efficiency + otherwise: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot[period]) * Link_efficiency Process_output_arrival: description: >- what a process transfers at a port at a snapshot — its internal power @@ -2316,8 +2305,8 @@ expressions: cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot_period[period]) * Process_rate - otherwise: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot_period[period]) * Process_rate + expression: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot[period]) * Process_rate + otherwise: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot[period]) * Process_rate GlobalConstraint_energy_weight: description: >- what one unit of power at a snapshot counts for in a row — the @@ -2327,7 +2316,7 @@ expressions: cases: counted: when: GlobalConstraint_counts_snapshot - expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period[period]) + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot[period]) otherwise: 0 StorageUnit_last_counted_active: description: >- @@ -2352,8 +2341,8 @@ expressions: dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: - when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_last_counted_active @@ -2381,8 +2370,8 @@ expressions: dims: [scenario, global_constraint, snapshot, store] cases: per_period: - when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT Store_e_initial_per_period expression: Store_last_counted_active @@ -2470,42 +2459,42 @@ expressions: the row's carrier-and-bus set Generator_opex: expression: >- - sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) - + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) Generator_commitment_opex: expression: >- - sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) Link_opex: expression: >- - sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) - + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) Link_commitment_opex: expression: >- - sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) Process_opex: expression: >- - sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) - + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) Process_commitment_opex: expression: >- - sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) StorageUnit_opex: expression: >- - sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) + sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) Store_opex: expression: >- - sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) + sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) scenario_opex: dims: [scenario] expression: >- @@ -2555,22 +2544,22 @@ expressions: scenarios, and a share of it at the tail Generator_additions: expression: >- - sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=Generator_carrier[carrier]) + sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=generator[carrier]) Line_additions: expression: >- - sum(Line_s_nom_ext * Line_first_active, over=line, by=Line_carrier[carrier]) + sum(Line_s_nom_ext * Line_first_active, over=line, by=line[carrier]) Link_additions: expression: >- - sum(Link_p_nom_ext * Link_first_active, over=link, by=Link_carrier[carrier]) + sum(Link_p_nom_ext * Link_first_active, over=link, by=link[carrier]) Process_additions: expression: >- - sum(Process_p_nom_ext * Process_first_active, over=process, by=Process_carrier[carrier]) + sum(Process_p_nom_ext * Process_first_active, over=process, by=process[carrier]) StorageUnit_additions: expression: >- - sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=StorageUnit_carrier[carrier]) + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=storage_unit[carrier]) Store_additions: expression: >- - sum(Store_e_nom_ext * Store_first_active, over=store, by=Store_carrier[carrier]) + sum(Store_e_nom_ext * Store_first_active, over=store, by=store[carrier]) Carrier_additions: dims: [period, carrier] expression: >- @@ -2624,35 +2613,35 @@ expressions: transformer's flow before it goes out dims: [scenario, snapshot, outage] cases: - line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line[line])" } - otherwise: at(Transformer_s_monitored, by=Outage_transformer[transformer]) + line: { when: "outage[line]", expression: "at(Line_s_monitored, by=outage[line])" } + otherwise: at(Transformer_s_monitored, by=outage[transformer]) Generator_injection: - expression: sum(Generator_sign * Generator_p, over=generator, by=Generator_bus[bus]) + expression: sum(Generator_sign * Generator_p, over=generator, by=generator[bus]) Line_injection: expression: >- - -sum(Line_s, over=line, by=Line_bus0[bus]) - + sum(Line_s, over=line, by=Line_bus1[bus]) - - (0.5 * sum(Line_loss, over=line, by=Line_bus0[bus])) - - (0.5 * sum(Line_loss, over=line, by=Line_bus1[bus])) + -sum(Line_s, over=line, by=line[bus0]) + + sum(Line_s, over=line, by=line[bus1]) + - (0.5 * sum(Line_loss, over=line, by=line[bus0])) + - (0.5 * sum(Line_loss, over=line, by=line[bus1])) Link_injection: expression: >- - -sum(Link_p, over=link, by=Link_bus0[bus]) - + sum(Link_output_arrival, over=link_output, by=Link_output_bus[bus]) - Load_injection: sum(Load_demand, over=load, by=Load_bus[bus]) + -sum(Link_p, over=link, by=link[bus0]) + + sum(Link_output_arrival, over=link_output, by=link_output[bus]) + Load_injection: sum(Load_demand, over=load, by=load[bus]) Process_injection: expression: >- - sum(Process_output_arrival, over=process_output, by=Process_output_bus[bus]) + sum(Process_output_arrival, over=process_output, by=process_output[bus]) StorageUnit_injection: expression: >- - sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=StorageUnit_bus[bus]) - Store_injection: sum(Store_sign * Store_p, over=store, by=Store_bus[bus]) + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=storage_unit[bus]) + Store_injection: sum(Store_sign * Store_p, over=store, by=store[bus]) Transformer_injection: expression: >- - -sum(Transformer_s, over=transformer, by=Transformer_bus0[bus]) - + sum(Transformer_s, over=transformer, by=Transformer_bus1[bus]) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus0[bus])) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus1[bus])) + -sum(Transformer_s, over=transformer, by=transformer[bus0]) + + sum(Transformer_s, over=transformer, by=transformer[bus1]) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus0])) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus1])) Bus_injection: dims: [scenario, snapshot, bus] expression: >- @@ -2668,12 +2657,12 @@ expressions: what every component puts into a bus, less what it takes out of it; PyPSA writes each term into the balance, and a load on its right-hand side - Line_angle_sum: sum(Line_s * at(Line_cycle_weight, by=snapshot_period[period]), over=line) + Line_angle_sum: sum(Line_s * at(Line_cycle_weight, by=snapshot[period]), over=line) Transformer_angle_sum: expression: >- - sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot_period[period]), over=transformer) + sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot[period]), over=transformer) + sum(Transformer_phase_shift_weight, over=transformer) - + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot_period[period]), over=transformer) + + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot[period]), over=transformer) Cycle_angle_sum: dims: [scenario, snapshot, cycle] expression: Line_angle_sum + Transformer_angle_sum @@ -2713,7 +2702,7 @@ conditions: period's first, and the horizon's first where the generator comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init)) Link_com_ext: description: >- @@ -2746,7 +2735,7 @@ conditions: period's first, and the horizon's first where the link comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init)) Process_com_ext: description: >- @@ -2779,7 +2768,7 @@ conditions: period's first, and the horizon's first where the process comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init)) StorageUnit_fix: description: >- @@ -3775,14 +3764,14 @@ constraints: voltage angle difference across it, `x_pu_eff` times the flow in radians, is the negative of its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s >= -Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_v_ang_upper: description: >- `Line-v_ang-upper` — an AC line carries at most the flow at which the voltage angle difference across it reaches its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s <= Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_loss_upper: description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" @@ -3912,8 +3901,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper_security: description: >- `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -3924,8 +3913,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom Line_ext_s_lower_security: description: >- `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — @@ -3937,8 +3926,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper_security: description: >- `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -3949,8 +3938,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext Transformer_fix_s_lower_security: description: >- `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -3961,8 +3950,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom Transformer_fix_s_upper_security: description: >- `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -3973,8 +3962,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom Transformer_ext_s_lower_security: description: >- `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -3986,8 +3975,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_upper_security: description: >- `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -3999,8 +3988,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext Kirchhoff_Voltage_Law: description: >- `Kirchhoff-Voltage-Law` — around every independent cycle the @@ -4590,7 +4579,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:443`) StorageUnit_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — @@ -4611,7 +4600,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:495`) Store_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — diff --git a/examples/pypsa/generator.yaml b/examples/pypsa/generator.yaml index 1c617370d..1224edcce 100644 --- a/examples/pypsa/generator.yaml +++ b/examples/pypsa/generator.yaml @@ -44,19 +44,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Generator_carrier: - description: the carrier a generator converts from - key: generator - values: carrier - Generator_bus: - description: the bus a generator sits on - key: generator - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + generator: + carrier: + description: the carrier a generator converts from + into: carrier + bus: + description: the bus a generator sits on + into: bus parameters: Generator_p_nom: @@ -216,18 +215,18 @@ expressions: adds_to: tech_capacity_expansion Generator_opex: expression: >- - sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) - + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_p * Generator_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + + sum(sum((((Generator_p * Generator_p) * Generator_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) adds_to: scenario_opex Generator_capex: expression: sum(scenario_weight * Generator_p_nom_ext * Generator_capital_cost * Generator_capital_weight) adds_to: total_cost Generator_additions: expression: >- - sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=Generator_carrier[carrier]) + sum(Generator_p_nom_ext * Generator_first_active, over=generator, by=generator[carrier]) adds_to: Carrier_additions Generator_injection: - expression: sum(Generator_sign * Generator_p, over=generator, by=Generator_bus[bus]) + expression: sum(Generator_sign * Generator_p, over=generator, by=generator[bus]) adds_to: Bus_injection conditions: diff --git a/examples/pypsa/generator_commitment.yaml b/examples/pypsa/generator_commitment.yaml index 6160407ef..88c3beee6 100644 --- a/examples/pypsa/generator_commitment.yaml +++ b/examples/pypsa/generator_commitment.yaml @@ -40,11 +40,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Generator_committable: @@ -146,7 +146,7 @@ expressions: otherwise: shift(Generator_status, along=snapshot, offset=1) Generator_commitment_opex: expression: >- - sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=generator), over=snapshot) + sum(sum(((Generator_status * Generator_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=generator), over=snapshot) + sum(sum(Generator_start_up * Generator_start_up_cost, over=generator), over=snapshot) + sum(sum(Generator_shut_down * Generator_shut_down_cost, over=generator), over=snapshot) adds_to: scenario_opex diff --git a/examples/pypsa/generator_maintenance.yaml b/examples/pypsa/generator_maintenance.yaml index 7fa8e97c4..db3e34cc6 100644 --- a/examples/pypsa/generator_maintenance.yaml +++ b/examples/pypsa/generator_maintenance.yaml @@ -27,15 +27,14 @@ dimensions: generator: description: generating units, each on one bus -relations: +sets: Generator_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a generator that is - not maintainable - key: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a generator that is not maintainable + dims: { scenario: scenario, generator: generator, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/examples/pypsa/generator_ramping.yaml b/examples/pypsa/generator_ramping.yaml index 0e32719a4..25912b799 100644 --- a/examples/pypsa/generator_ramping.yaml +++ b/examples/pypsa/generator_ramping.yaml @@ -37,11 +37,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Generator_ramp_limit_up: @@ -146,7 +146,7 @@ conditions: period's first, and the horizon's first where the generator comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Generator_status_initial == 0 OR Generator_p_init)) constraints: diff --git a/examples/pypsa/line.yaml b/examples/pypsa/line.yaml index bb3261356..0621eb0d8 100644 --- a/examples/pypsa/line.yaml +++ b/examples/pypsa/line.yaml @@ -47,23 +47,21 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Line_carrier: - description: the carrier a line carries - key: line - values: carrier - Line_bus0: - description: the bus a line's flow is measured at - key: line - values: bus - Line_bus1: - description: the bus at a line's other end - key: line - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + line: + carrier: + description: the carrier a line carries + into: carrier + bus0: + description: the bus a line's flow is measured at + into: bus + bus1: + description: the bus at a line's other end + into: bus parameters: Line_active: @@ -231,7 +229,7 @@ expressions: adds_to: total_cost Line_additions: expression: >- - sum(Line_s_nom_ext * Line_first_active, over=line, by=Line_carrier[carrier]) + sum(Line_s_nom_ext * Line_first_active, over=line, by=line[carrier]) adds_to: Carrier_additions Line_s_monitored: description: >- @@ -244,13 +242,13 @@ expressions: otherwise: 0 Line_injection: expression: >- - -sum(Line_s, over=line, by=Line_bus0[bus]) - + sum(Line_s, over=line, by=Line_bus1[bus]) - - (0.5 * sum(Line_loss, over=line, by=Line_bus0[bus])) - - (0.5 * sum(Line_loss, over=line, by=Line_bus1[bus])) + -sum(Line_s, over=line, by=line[bus0]) + + sum(Line_s, over=line, by=line[bus1]) + - (0.5 * sum(Line_loss, over=line, by=line[bus0])) + - (0.5 * sum(Line_loss, over=line, by=line[bus1])) adds_to: Bus_injection Line_angle_sum: - expression: sum(Line_s * at(Line_cycle_weight, by=snapshot_period[period]), over=line) + expression: sum(Line_s * at(Line_cycle_weight, by=snapshot[period]), over=line) adds_to: Cycle_angle_sum conditions: @@ -312,14 +310,14 @@ constraints: voltage angle difference across it, `x_pu_eff` times the flow in radians, is the negative of its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s >= -Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_v_ang_upper: description: >- `Line-v_ang-upper` — an AC line carries at most the flow at which the voltage angle difference across it reaches its limit dims: [scenario, snapshot, line] - where: Line_v_ang_max AND Line_carrier == 'AC' AND Line_active + where: Line_v_ang_max AND line[carrier] == 'AC' AND Line_active expression: Line_s <= Line_v_ang_max * (3.141592653589793 / 180) / Line_x_pu_eff Line_loss_upper: description: "`Line-loss_upper` — a line dissipates at most the loss at its rating" diff --git a/examples/pypsa/link.yaml b/examples/pypsa/link.yaml index 3da9106a9..a6e8fed5c 100644 --- a/examples/pypsa/link.yaml +++ b/examples/pypsa/link.yaml @@ -48,30 +48,28 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Link_carrier: - description: the carrier a link converts from - key: link - values: carrier - Link_bus0: - description: the bus a link leaves - key: link - values: bus - Link_output_link: - description: the link an output port belongs to - key: link_output - values: link - Link_output_bus: - description: >- - the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. - A link of three output ports is three labels here rather than a third - relation, so the file states any number of them - key: link_output - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + link: + carrier: + description: the carrier a link converts from + into: carrier + bus0: + description: the bus a link leaves + into: bus + link_output: + link: + description: the link an output port belongs to + into: link + bus: + description: >- + the bus an output port delivers to — PyPSA's `bus1`, `bus2`, … columns. + A link of three output ports is three labels here rather than a third + relation, so the file states any number of them + into: bus parameters: Link_p_nom: @@ -253,8 +251,8 @@ expressions: cases: wrapping: when: Link_output_cyclic_delay - expression: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot_period[period]) * Link_efficiency - otherwise: shift(at(Link_p, by=Link_output_link[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot_period[period]) * Link_efficiency + expression: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge='wrap', within=snapshot[period]) * Link_efficiency + otherwise: shift(at(Link_p, by=link_output[link]), along=snapshot, offset=Link_output_delay, edge=0, within=snapshot[period]) * Link_efficiency Link_transmission_volume_expansion: expression: sum(Link_p_nom_ext * Link_volume_weight, over=link) adds_to: transmission_volume_expansion @@ -266,20 +264,20 @@ expressions: adds_to: tech_capacity_expansion Link_opex: expression: >- - sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) - + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_p * Link_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + + sum(sum((((Link_p * Link_p) * Link_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) adds_to: scenario_opex Link_capex: expression: sum(scenario_weight * Link_p_nom_ext * Link_capital_cost * Link_capital_weight) adds_to: total_cost Link_additions: expression: >- - sum(Link_p_nom_ext * Link_first_active, over=link, by=Link_carrier[carrier]) + sum(Link_p_nom_ext * Link_first_active, over=link, by=link[carrier]) adds_to: Carrier_additions Link_injection: expression: >- - -sum(Link_p, over=link, by=Link_bus0[bus]) - + sum(Link_output_arrival, over=link_output, by=Link_output_bus[bus]) + -sum(Link_p, over=link, by=link[bus0]) + + sum(Link_output_arrival, over=link_output, by=link_output[bus]) adds_to: Bus_injection conditions: diff --git a/examples/pypsa/link_commitment.yaml b/examples/pypsa/link_commitment.yaml index 4c4885e3f..5ee51e322 100644 --- a/examples/pypsa/link_commitment.yaml +++ b/examples/pypsa/link_commitment.yaml @@ -40,11 +40,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Link_committable: @@ -146,7 +146,7 @@ expressions: otherwise: shift(Link_status, along=snapshot, offset=1) Link_commitment_opex: expression: >- - sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=link), over=snapshot) + sum(sum(((Link_status * Link_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=link), over=snapshot) + sum(sum(Link_start_up * Link_start_up_cost, over=link), over=snapshot) + sum(sum(Link_shut_down * Link_shut_down_cost, over=link), over=snapshot) adds_to: scenario_opex diff --git a/examples/pypsa/link_maintenance.yaml b/examples/pypsa/link_maintenance.yaml index 60627d2b3..dcd47ba63 100644 --- a/examples/pypsa/link_maintenance.yaml +++ b/examples/pypsa/link_maintenance.yaml @@ -27,15 +27,14 @@ dimensions: link: description: controllable connections, each from one bus to the buses it delivers to -relations: +sets: Link_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a link that is - not maintainable - key: { scenario: scenario, link: link, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a link that is not maintainable + dims: { scenario: scenario, link: link, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/examples/pypsa/link_ramping.yaml b/examples/pypsa/link_ramping.yaml index 22fa4dabf..7b22cc2e2 100644 --- a/examples/pypsa/link_ramping.yaml +++ b/examples/pypsa/link_ramping.yaml @@ -37,11 +37,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Link_ramp_limit_up: @@ -146,7 +146,7 @@ conditions: period's first, and the horizon's first where the link comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Link_status_initial == 0 OR Link_p_init)) constraints: diff --git a/examples/pypsa/load.yaml b/examples/pypsa/load.yaml index d75ae2d43..dab732ee3 100644 --- a/examples/pypsa/load.yaml +++ b/examples/pypsa/load.yaml @@ -18,11 +18,11 @@ dimensions: load: description: demands, each on one bus -relations: - Load_bus: - description: the bus a load sits on - key: load - values: bus +properties: + load: + bus: + description: the bus a load sits on + into: bus parameters: Load_p_set: @@ -53,5 +53,5 @@ expressions: active: { when: Load_active, expression: Load_sign * Load_p_set } otherwise: 0 Load_injection: - expression: sum(Load_demand, over=load, by=Load_bus[bus]) + expression: sum(Load_demand, over=load, by=load[bus]) adds_to: Bus_injection diff --git a/examples/pypsa/process.yaml b/examples/pypsa/process.yaml index f84d8b99e..7c7d71634 100644 --- a/examples/pypsa/process.yaml +++ b/examples/pypsa/process.yaml @@ -46,26 +46,25 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Process_carrier: - description: the carrier a process converts from - key: process - values: carrier - Process_output_process: - description: the process a port belongs to - key: process_output - values: process - Process_output_bus: - description: >- - the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … - columns. A process of three ports is three labels here rather than a - third relation, so the file states any number of them - key: process_output - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + process: + carrier: + description: the carrier a process converts from + into: carrier + process_output: + process: + description: the process a port belongs to + into: process + bus: + description: >- + the bus a port draws from or delivers to — PyPSA's `bus0`, `bus1`, … + columns. A process of three ports is three labels here rather than a + third relation, so the file states any number of them + into: bus parameters: Process_p_nom: @@ -231,26 +230,26 @@ expressions: cases: wrapping: when: Process_output_cyclic_delay - expression: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot_period[period]) * Process_rate - otherwise: shift(at(Process_p, by=Process_output_process[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot_period[period]) * Process_rate + expression: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge='wrap', within=snapshot[period]) * Process_rate + otherwise: shift(at(Process_p, by=process_output[process]), along=snapshot, offset=Process_output_delay, edge=0, within=snapshot[period]) * Process_rate Process_tech_capacity_expansion: expression: sum(Process_p_nom_ext * Process_tech_capacity_weight, over=process) adds_to: tech_capacity_expansion Process_opex: expression: >- - sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) - + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_p * Process_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + + sum(sum((((Process_p * Process_p) * Process_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) adds_to: scenario_opex Process_capex: expression: sum(scenario_weight * Process_p_nom_ext * Process_capital_cost * Process_capital_weight) adds_to: total_cost Process_additions: expression: >- - sum(Process_p_nom_ext * Process_first_active, over=process, by=Process_carrier[carrier]) + sum(Process_p_nom_ext * Process_first_active, over=process, by=process[carrier]) adds_to: Carrier_additions Process_injection: expression: >- - sum(Process_output_arrival, over=process_output, by=Process_output_bus[bus]) + sum(Process_output_arrival, over=process_output, by=process_output[bus]) adds_to: Bus_injection conditions: diff --git a/examples/pypsa/process_commitment.yaml b/examples/pypsa/process_commitment.yaml index 2a1431960..2c57d68ae 100644 --- a/examples/pypsa/process_commitment.yaml +++ b/examples/pypsa/process_commitment.yaml @@ -40,11 +40,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Process_committable: @@ -146,7 +146,7 @@ expressions: otherwise: shift(Process_status, along=snapshot, offset=1) Process_commitment_opex: expression: >- - sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=process), over=snapshot) + sum(sum(((Process_status * Process_stand_by_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=process), over=snapshot) + sum(sum(Process_start_up * Process_start_up_cost, over=process), over=snapshot) + sum(sum(Process_shut_down * Process_shut_down_cost, over=process), over=snapshot) adds_to: scenario_opex diff --git a/examples/pypsa/process_maintenance.yaml b/examples/pypsa/process_maintenance.yaml index da1e2c28d..7c373c72f 100644 --- a/examples/pypsa/process_maintenance.yaml +++ b/examples/pypsa/process_maintenance.yaml @@ -27,15 +27,14 @@ dimensions: process: description: generalized multi-port converters, each with an internal power that every port draws or delivers at its own rate -relations: +sets: Process_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — - in a scenario, the start and the snapshots after it, until their - generator weightings reach that scenario's `maintenance_duration`, data prep; no - rows for a process that is - not maintainable - key: { scenario: scenario, process: process, start: snapshot, covered: snapshot } + in a scenario, the start and the snapshots after it, until their generator + weightings reach that scenario's `maintenance_duration`, data prep; no + rows for a process that is not maintainable + dims: { scenario: scenario, process: process, start: snapshot, covered: snapshot } missing: absent parameters: diff --git a/examples/pypsa/process_ramping.yaml b/examples/pypsa/process_ramping.yaml index 0127b4152..49068f78b 100644 --- a/examples/pypsa/process_ramping.yaml +++ b/examples/pypsa/process_ramping.yaml @@ -37,11 +37,11 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: Process_ramp_limit_up: @@ -146,7 +146,7 @@ conditions: period's first, and the horizon's first where the process comes in off or carries an initial output condition: >- - position(snapshot, within=snapshot_period[period]) > 0 OR (position(snapshot) == 0 AND + position(snapshot, within=snapshot[period]) > 0 OR (position(snapshot) == 0 AND (Process_status_initial == 0 OR Process_p_init)) constraints: diff --git a/examples/pypsa/security.yaml b/examples/pypsa/security.yaml index b0d7ccd81..05d59dc19 100644 --- a/examples/pypsa/security.yaml +++ b/examples/pypsa/security.yaml @@ -38,21 +38,20 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Outage_line: - description: the line an outage takes out; an outage of a transformer has no row - key: outage - values: line - missing: absent - Outage_transformer: - description: the transformer an outage takes out; an outage of a line has no row - key: outage - values: transformer - missing: absent +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + outage: + line: + description: the line an outage takes out; an outage of a transformer has no row + into: line + missing: absent + transformer: + description: the transformer an outage takes out; an outage of a line has no row + into: transformer + missing: absent parameters: Line_BODF: @@ -80,8 +79,8 @@ expressions: transformer's flow before it goes out dims: [scenario, snapshot, outage] cases: - line: { when: Outage_line, expression: "at(Line_s_monitored, by=Outage_line[line])" } - otherwise: at(Transformer_s_monitored, by=Outage_transformer[transformer]) + line: { when: "outage[line]", expression: "at(Line_s_monitored, by=outage[line])" } + otherwise: at(Transformer_s_monitored, by=outage[transformer]) constraints: Line_fix_s_lower_security: @@ -94,8 +93,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom Line_fix_s_upper_security: description: >- `Line-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -106,8 +105,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom + where: not Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom Line_ext_s_lower_security: description: >- `Line-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` — @@ -119,8 +118,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s >= -Line_s_max_pu * Line_s_nom_ext Line_ext_s_upper_security: description: >- `Line-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` — @@ -131,8 +130,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, line, outage] - where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot_period[period]) - expression: Line_s_monitored + at(Line_BODF, by=snapshot_period[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext + where: Line_s_nom_extendable AND at(Line_BODF, by=snapshot[period]) + expression: Line_s_monitored + at(Line_BODF, by=snapshot[period]) * Outage_s <= Line_s_max_pu * Line_s_nom_ext Transformer_fix_s_lower_security: description: >- `Transformer-fix-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -143,8 +142,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom Transformer_fix_s_upper_security: description: >- `Transformer-fix-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -155,8 +154,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom + where: not Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom Transformer_ext_s_lower_security: description: >- `Transformer-ext-s-lower-security-for-{c}-outage-in-sub-network-{n}` @@ -168,8 +167,8 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s >= -Transformer_s_max_pu * Transformer_s_nom_ext Transformer_ext_s_upper_security: description: >- `Transformer-ext-s-upper-security-for-{c}-outage-in-sub-network-{n}` @@ -181,5 +180,5 @@ constraints: `-period-{p}` for each period `p`. This entry states them all over the outage dimension dims: [scenario, snapshot, transformer, outage] - where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot_period[period]) - expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot_period[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext + where: Transformer_s_nom_extendable AND at(Transformer_BODF, by=snapshot[period]) + expression: Transformer_s_monitored + at(Transformer_BODF, by=snapshot[period]) * Outage_s <= Transformer_s_max_pu * Transformer_s_nom_ext diff --git a/examples/pypsa/settings.yaml b/examples/pypsa/settings.yaml index 8b23acc95..6f21d325e 100644 --- a/examples/pypsa/settings.yaml +++ b/examples/pypsa/settings.yaml @@ -70,11 +70,11 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period parameters: snapshot_weightings_objective: @@ -139,7 +139,7 @@ expressions: cases: counted: when: GlobalConstraint_counts_snapshot - expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot_period[period]) + expression: snapshot_weightings_generators * at(period_weight_years, by=snapshot[period]) otherwise: 0 objective: diff --git a/examples/pypsa/storage_unit.yaml b/examples/pypsa/storage_unit.yaml index 21d98aa54..bea5df8db 100644 --- a/examples/pypsa/storage_unit.yaml +++ b/examples/pypsa/storage_unit.yaml @@ -40,19 +40,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - StorageUnit_carrier: - description: the carrier a storage unit converts from - key: storage_unit - values: carrier - StorageUnit_bus: - description: the bus a storage unit sits on - key: storage_unit - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + storage_unit: + carrier: + description: the carrier a storage unit converts from + into: carrier + bus: + description: the bus a storage unit sits on + into: bus parameters: StorageUnit_active: @@ -281,11 +280,11 @@ expressions: when: StorageUnit_cyclic_state_of_charge_per_period expression: >- StorageUnit_retention - * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + * shift(StorageUnit_state_of_charge, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: when: >- StorageUnit_state_of_charge_initial_per_period AND NOT StorageUnit_cyclic_state_of_charge_per_period - AND position(snapshot, within=snapshot_period[period]) == 0 + AND position(snapshot, within=snapshot[period]) == 0 expression: StorageUnit_state_of_charge_initial otherwise: StorageUnit_retention * shift(StorageUnit_state_of_charge, along=snapshot, offset=1) StorageUnit_last_counted_active: @@ -311,8 +310,8 @@ expressions: dims: [scenario, global_constraint, snapshot, storage_unit] cases: per_period: - when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT StorageUnit_state_of_charge_initial_per_period expression: StorageUnit_last_counted_active @@ -330,21 +329,21 @@ expressions: adds_to: tech_capacity_expansion StorageUnit_opex: expression: >- - sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) - + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=storage_unit), over=snapshot) + sum(sum(((StorageUnit_p_dispatch * StorageUnit_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum((((StorageUnit_p_dispatch * StorageUnit_p_dispatch) * StorageUnit_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_state_of_charge * StorageUnit_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) + + sum(sum(((StorageUnit_spill * StorageUnit_spill_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=storage_unit), over=snapshot) adds_to: scenario_opex StorageUnit_capex: expression: sum(scenario_weight * StorageUnit_p_nom_ext * StorageUnit_capital_cost * StorageUnit_capital_weight) adds_to: total_cost StorageUnit_additions: expression: >- - sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=StorageUnit_carrier[carrier]) + sum(StorageUnit_p_nom_ext * StorageUnit_first_active, over=storage_unit, by=storage_unit[carrier]) adds_to: Carrier_additions StorageUnit_injection: expression: >- - sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=StorageUnit_bus[bus]) + sum(StorageUnit_sign * (StorageUnit_p_dispatch - StorageUnit_p_store), over=storage_unit, by=storage_unit[bus]) adds_to: Bus_injection conditions: @@ -515,7 +514,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:443`) StorageUnit_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "StorageUnit_operational_limit_weight AND NOT StorageUnit_state_of_charge_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — diff --git a/examples/pypsa/store.yaml b/examples/pypsa/store.yaml index 73c4b4e90..8ad0c8da0 100644 --- a/examples/pypsa/store.yaml +++ b/examples/pypsa/store.yaml @@ -40,19 +40,18 @@ dimensions: carrier: description: energy carriers, what a growth limit is set per -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Store_carrier: - description: the carrier a store holds - key: store - values: carrier - Store_bus: - description: the bus a store sits on - key: store - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + store: + carrier: + description: the carrier a store holds + into: carrier + bus: + description: the bus a store sits on + into: bus parameters: Store_active: @@ -239,9 +238,9 @@ expressions: expression: Store_e_initial period_cyclic: when: Store_e_cyclic_per_period - expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot_period[period]) + expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', within=snapshot[period]) period_opening: - when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot_period[period]) == 0 + when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, within=snapshot[period]) == 0 expression: Store_e_initial otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1) Store_last_counted_active: @@ -267,8 +266,8 @@ expressions: dims: [scenario, global_constraint, snapshot, store] cases: per_period: - when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot_period[period]) == -1 - expression: at(period_weight_years, by=snapshot_period[period]) + when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, within=snapshot[period]) == -1 + expression: at(period_weight_years, by=snapshot[period]) carried_over: when: NOT Store_e_initial_per_period expression: Store_last_counted_active @@ -286,19 +285,19 @@ expressions: adds_to: tech_capacity_expansion Store_opex: expression: >- - sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) - + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period[period]), over=store), over=snapshot) + sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) + + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot[period]), over=store), over=snapshot) adds_to: scenario_opex Store_capex: expression: sum(scenario_weight * Store_e_nom_ext * Store_capital_cost * Store_capital_weight) adds_to: total_cost Store_additions: expression: >- - sum(Store_e_nom_ext * Store_first_active, over=store, by=Store_carrier[carrier]) + sum(Store_e_nom_ext * Store_first_active, over=store, by=store[carrier]) adds_to: Carrier_additions Store_injection: - expression: sum(Store_sign * Store_p, over=store, by=Store_bus[bus]) + expression: sum(Store_sign * Store_p, over=store, by=store[bus]) adds_to: Bus_injection constraints: @@ -398,7 +397,7 @@ assumptions: level — PyPSA refuses it where any period's years is not one (`global_constraints.py:495`) Store_operational_limit_carried_over_has_unit_years: - holds: "at(period_weight_years == 1, by=snapshot_period[period])" + holds: "at(period_weight_years == 1, by=snapshot[period])" where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot" description: >- the same for an `operational_limit` row, over the periods it counts — diff --git a/examples/pypsa/transformer.yaml b/examples/pypsa/transformer.yaml index 911cc35fc..c60003e5c 100644 --- a/examples/pypsa/transformer.yaml +++ b/examples/pypsa/transformer.yaml @@ -39,19 +39,18 @@ dimensions: dtype: int ordered: true -relations: - snapshot_period: - description: the investment period a snapshot falls in - key: snapshot - values: period - Transformer_bus0: - description: the bus a transformer's flow is measured at - key: transformer - values: bus - Transformer_bus1: - description: the bus at a transformer's other end - key: transformer - values: bus +properties: + snapshot: + period: + description: the investment period a snapshot falls in + into: period + transformer: + bus0: + description: the bus a transformer's flow is measured at + into: bus + bus1: + description: the bus at a transformer's other end + into: bus parameters: Transformer_active: @@ -242,16 +241,16 @@ expressions: otherwise: 0 Transformer_injection: expression: >- - -sum(Transformer_s, over=transformer, by=Transformer_bus0[bus]) - + sum(Transformer_s, over=transformer, by=Transformer_bus1[bus]) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus0[bus])) - - (0.5 * sum(Transformer_loss, over=transformer, by=Transformer_bus1[bus])) + -sum(Transformer_s, over=transformer, by=transformer[bus0]) + + sum(Transformer_s, over=transformer, by=transformer[bus1]) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus0])) + - (0.5 * sum(Transformer_loss, over=transformer, by=transformer[bus1])) adds_to: Bus_injection Transformer_angle_sum: expression: >- - sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot_period[period]), over=transformer) + sum(Transformer_s * at(Transformer_cycle_weight, by=snapshot[period]), over=transformer) + sum(Transformer_phase_shift_weight, over=transformer) - + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot_period[period]), over=transformer) + + sum(Transformer_phase_shift * at(Transformer_phase_shift_cycle_weight, by=snapshot[period]), over=transformer) adds_to: Cycle_angle_sum conditions: diff --git a/mkdocs.yml b/mkdocs.yml index 12fd603a7..1fb596534 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,7 +51,6 @@ nav: - File shape: reference/language/file.md - Parameters, variables, constraints and the objective: reference/language/parameters-variables-constraints.md - Dimensions: reference/language/dimensions.md - - Relations: reference/language/relations.md - Sets and properties: reference/language/sets.md - Expressions: reference/language/expressions.md - Named expressions, conditions and macros: reference/language/named.md @@ -78,7 +77,7 @@ nav: - About: - The limits: about/limits.md - Versions: about/versions.md - - Relations as linear maps: about/relations-as-linear-maps.md + - Sets and properties as linear maps: about/sets-and-properties-as-linear-maps.md # Everything a spec writer does not need, grouped by reader: whoever # writes a tool against `Spec` and `Program` (an engine such as specsolve, a # renderer, a checker), whoever changes mathspec itself, and the proofs of diff --git a/schema/mathspec.schema.json b/schema/mathspec.schema.json index 200a731d7..b8ce07043 100644 --- a/schema/mathspec.schema.json +++ b/schema/mathspec.schema.json @@ -167,7 +167,7 @@ }, "DimensionSpec": { "additionalProperties": false, - "description": "A declared dimension, the dtype its coordinates must be, and whether their order means anything.\n\nA dimension is an axis and nothing else: it declares that the axis exists\nand what its coordinates are typed as, never which coordinates there are \u2014\nthose are data, and arrive when the data is attached. The maps its members carry \u2014 a\ngenerator's bus, a snapshot's period \u2014 are top-level ``relations:``\n([`RelationSpec`][]), keyed by their own name.", + "description": "A declared dimension, the dtype its coordinates must be, and whether their order means anything.\n\nA dimension is an axis and nothing else: it declares that the axis exists\nand what its coordinates are typed as, never which coordinates there are \u2014\nthose are data, and arrive when the data is attached. The maps its members carry \u2014 a\ngenerator's bus, a snapshot's period \u2014 are its ``properties:``\n([`PropertySpec`][]), and the combinations of its labels with another\ndimension's that exist are ``sets:`` ([`SetSpec`][]).", "properties": { "description": { "anyOf": [ @@ -851,82 +851,6 @@ } ] }, - "RelationSpec": { - "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 when the data is attached: one row per key tuple, so every ``values:`` column is a\nfunction of it. A relation with no ``values:`` is **bare** \u2014 every column is\nin its key, a row is its own identity, and nothing reads it::\n\n relations:\n gen_bus: {key: generator, values: bus}\n gen_bt: {key: [generator], values: [bus, technology]}\n zone_of: {key: [generator, period], values: zone}\n ends: {key: line, values: {bus0: bus, bus1: bus}}\n connection: {key: [generator, bus]}\n\nA sum joins the table on the columns ``over=`` names and every other key\ncolumn, and groups by the columns ``by=relation[...]`` names. A lookup\njoins on the columns it names. The entry fixes no direction. A\ncolumn is named after its own dimension or after none, so a name in a\ncall reads the same as a column and as a dimension. The map itself is\ndata, and arrives with the rest of it, under the relation's name, one\ncolumn per role.", - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Description" - }, - "key": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - } - ], - "title": "Key" - }, - "missing": { - "default": "refused", - "enum": [ - "refused", - "absent" - ], - "title": "Missing", - "type": "string" - }, - "values": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - { - "type": "null" - } - ], - "default": null, - "title": "Values" - } - }, - "required": [ - "key" - ], - "title": "RelationSpec", - "type": "object" - }, "SetSpec": { "additionalProperties": false, "description": "A named set: which combinations of the labels of its dimensions exist.\n\n``dims:`` lists the dimensions a row combines, or maps each member's name to\nits dimension where two members share one. No entry is over a set. An entry\nis over the member dimensions, and ``where:`` names the set::\n\n sets:\n connection: {dims: [entity, port], missing: absent}\n cover: {dims: {generator: generator, start: snapshot, covered: snapshot}, missing: absent}\n\nThe rows are data, and arrive under the set's name, one column per member.\nA set is also the subject of the [`PropertySpec`][] entries declared under\nits name.", @@ -1093,7 +1017,7 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, [`model_validate`][], the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n[`LanguageError`][mathspec.errors.LanguageError] on a spec the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the fifteen sections plus ``version`` and\n``description``, three ways back out \u2014 [`to_dict`][] for the spec as\ndata, [`to_yaml`][] for the file a reviewer reads, [`expand`][] for the\nspec with its formulations written out as plain rows \u2014 and [`program`][], the\nspec typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", + "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, [`model_validate`][], the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n[`LanguageError`][mathspec.errors.LanguageError] on a spec the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the fourteen sections plus ``version`` and\n``description``, three ways back out \u2014 [`to_dict`][] for the spec as\ndata, [`to_yaml`][] for the file a reviewer reads, [`expand`][] for the\nspec with its formulations written out as plain rows \u2014 and [`program`][], the\nspec typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", "properties": { "assumptions": { "additionalProperties": { @@ -1203,14 +1127,6 @@ "title": "Properties", "type": "object" }, - "relations": { - "additionalProperties": { - "$ref": "#/$defs/RelationSpec" - }, - "default": {}, - "title": "Relations", - "type": "object" - }, "sets": { "additionalProperties": { "$ref": "#/$defs/SetSpec" diff --git a/src/mathspec/_advising.py b/src/mathspec/_advising.py index dd4f701b8..6789629f1 100644 --- a/src/mathspec/_advising.py +++ b/src/mathspec/_advising.py @@ -91,7 +91,7 @@ def _term_note(name: str) -> str: def _never_an_axis(program: Program) -> list[Advice]: """One piece of advice per dimension nothing reaches. - A dimension a relation has a column over is reached: its members are the + A dimension a set or a property has a column over is reached: its members are the labels that column is checked against, and a ``where`` selects on them, so it is in use even where nothing is indexed by it. A dimension only a given entry indexes is reached too: the column exists, in another @@ -116,7 +116,7 @@ def _never_an_axis(program: Program) -> list[Advice]: 'never-an-axis', name, f"dimension '{name}' is never used: nothing is indexed by it, nothing " - f'sums into it, and no relation has a column over it. Remove it, or keep it ' + f'sums into it, and no set or property has a column over it. Remove it, or keep it ' f'if the entries that use it are still to be written.', ) for name in program.dimensions diff --git a/src/mathspec/_composition.py b/src/mathspec/_composition.py index a2b4916d2..75f87bbfa 100644 --- a/src/mathspec/_composition.py +++ b/src/mathspec/_composition.py @@ -19,18 +19,17 @@ [`merge`][] writes nothing a fragment did not write, except a ``+``. It joins fragments' text in one place, a named expression that terms add to; every other entry is copied as written, refused where two fragments own it, -or held identical where it is a dimension, a relation, a set or a property. +or held identical where it is a dimension, a set or a property. What that means for each section: -* **A dimension, a relation, a set or a property every fragment may - declare**, and the ones that do have to say the same thing about it. The +* **A dimension, a set or a property every fragment may declare**, and the ones that do have to say the same thing about it. The properties of one subject are joined, so two fragments may each give ``generator`` a property of its own. Prose is not a claim, so two descriptions of one dimension agree, and the first one given is carried. ``ordered`` is a claim about the space, not the space, so a dimension one fragment declares ordered is ordered. - The ``missing:`` of a relation, a set or a property is a claim about the - data, and the fragments agree on it: ``absent`` and ``refused`` change the result differently, so + The ``missing:`` of a set or a property is a claim about the data, and + the fragments agree on it: ``absent`` and ``refused`` change the result differently, so neither folds into the other. ``missing: refused`` is the same as none. * **Every other entry is owned.** A name two fragments declare is refused, both named. @@ -82,11 +81,11 @@ earlier patch laid on it, so a later patch wins a field an earlier one writes, and edits or removes an entry an earlier one creates. * **A patch adjusts the math, not the coordinate space.** A dimension, a - relation, a set or a property may be added or restated as the schema reads - its base, never changed and never removed. A restated dimension may add + set or a property may be added or restated as the schema reads its base, + never changed and never removed. A restated dimension may add ``ordered: true``, and may not write it false over a base that makes it. - The ``missing:`` of a relation, a set or a property is a claim about the - data, not the space: a patch may change it as it changes a parameter's, and + The ``missing:`` of a set or a property is a claim about the data, not the + space: a patch may change it as it changes a parameter's, and may name it alone. * ``null`` **makes what it names absent.** An entry set to ``null`` is removed, and a removal of what the base does not declare is refused. A field @@ -117,7 +116,7 @@ from mathspec.program import Program -SHARED_SECTIONS = ('dimensions', 'relations', 'sets') +SHARED_SECTIONS = ('dimensions', 'sets') #: Every section a fragment owns. ``properties:`` is shared too, one property #: at a time, which [`_flat`][] lays out for the rules of a shared entry. @@ -162,7 +161,7 @@ def merge(fragments: Sequence[Source], description: str | None = None) -> Spec: Raises: LanguageError: A fragment does not load on its own; two fragments declare one name; two fragments say different things about one - dimension, relation, set, property or given entry; a fragment reads a name as + dimension, set, property or given entry; a fragment reads a name as something other than what its sibling introduces, as another kind of thing, or over fewer dimensions than its body carries; a fragment adds a term to a variable, a parameter, a constraint or a @@ -345,7 +344,7 @@ def _flat(properties: object) -> dict[str, object]: """``properties:`` as one entry per property, keyed ``subject.property`` and in the long form. The rules for a shared entry then read each property as they read a - relation. A subject and a property are names, so neither holds a dot, and + set. A subject and a property are names, so neither holds a dot, and [`_nested`][] splits the key back without doubt. """ return { @@ -733,7 +732,7 @@ def override(base: Source, patches: Sequence[Source]) -> Spec: LanguageError: The base does not load; the patched spec does not load; a patch edits or removes an entry its base does not declare; a patch creates one that is not whole; a patch redeclares - or removes a dimension or a relation; or a patch sets a whole + or removes a dimension, a set or a property; or a patch sets a whole section to ``null``. FileNotFoundError: A ``str`` with no newline that names no file. TypeError: *patches* is one path rather than a list. @@ -898,7 +897,7 @@ def _shared( ) -> dict[str, object]: """One shared section: a patch adds an entry or restates one, never changes or drops it. - The section is ``dimensions``, ``relations`` or ``sets``, or the properties + The section is ``dimensions`` or ``sets``, or the properties as [`_flat`][] lays them out, which pass their *label* and *cls*. The restatement is compared as the schema reads both sides rather than @@ -975,7 +974,7 @@ def _owned( def _properties(declared: dict[str, object], patch: dict[str, object], name: str) -> dict[str, object]: - """The ``properties:`` section, each property laid over as a relation is: added, restated, or its ``missing:`` changed. + """The ``properties:`` section, each property laid over as a set is: added, restated, or its ``missing:`` changed. Both sides are read in the long form first, so a patch that writes only a property's ``missing:`` changes that field of a property the base diff --git a/src/mathspec/_condition_parser.py b/src/mathspec/_condition_parser.py index 2fe026946..96e38b8e6 100644 --- a/src/mathspec/_condition_parser.py +++ b/src/mathspec/_condition_parser.py @@ -55,7 +55,7 @@ @dataclass(frozen=True) class ColumnNode: - """``relation[column]`` on a side of a comparison, which reads one column at the frame's coordinates.""" + """``subject[property]`` on a side of a comparison, which reads one column at the frame's coordinates.""" relation: str column: str @@ -246,7 +246,7 @@ def _named_rewrite(text: str, loc: int) -> str | None: ``!=``, ``<`` and ``>`` are legal here, so only the tokens no condition admits are diagnosed. A comparison reads one column, so the grammar gives - up at the bracket of ``relation[a, b]`` inside a comparison, and at the + up at the bracket of ``subject[a, b]`` inside a comparison, and at the comparator after ``subject[a, b]``, which alone is a test of presence. """ rest = text[loc:].lstrip() @@ -276,7 +276,7 @@ def _named_rewrite(text: str, loc: int) -> str | None: #: The rewrite for a condition that nests too deep. The file can carry part #: of a long chain of conditions as data, so this rewrite comes first. _DEEP_REWRITE = ( - 'Declare a parameter or relation carrying part of the test and name that here, or split the ' + 'Declare a parameter or a set carrying part of the test and name that here, or split the ' 'entry into two, each masked by one half.' ) diff --git a/src/mathspec/_condition_resolver.py b/src/mathspec/_condition_resolver.py index 61c3dd829..d9c9c3b63 100644 --- a/src/mathspec/_condition_resolver.py +++ b/src/mathspec/_condition_resolver.py @@ -141,7 +141,7 @@ def _child(self, node: Condition | UnresolvedConditionNode) -> Condition: return cast('Condition', self.build(node)) def _bare_name(self, node: NameNode) -> Condition | UnresolvedConditionNode: - """A bare name: a named condition's body, a parameter's or relation's definedness, or a variable's existence.""" + """A bare name: a named condition's body, a parameter's or a set's definedness, or a variable's existence.""" ns, context = self.ns, self.context if node.name in ns.conditions: try: @@ -180,14 +180,14 @@ def _bare_name(self, node: NameNode) -> Condition | UnresolvedConditionNode: f'name is true at every coordinate. ' f'Remove it, or compare it: where: "{node.name} > 0".' ) - case 'relation': - shape = ns.relations[node.name] + case 'set': + shape = ns.sets[node.name] dims = tuple(shape.dim(k) for k in shape.key) if len(set(dims)) < len(dims): self.errors.append( - f"{context}: '{node.name}' has two columns over one dimension ({list(shape.roles)}), so a " - f'bare name is ambiguous. Compare a column: ' - f'{node.name}[{shape.values[0] if shape.values else shape.roles[-1]}] == ....' + f"{context}: '{node.name}' has two members over one dimension ({list(shape.roles)}), so the " + f"bare name cannot say which one the frame's coordinate is. Read it through a sum: " + f'sum(..., over={shape.roles[0]}, by={node.name}[{shape.roles[-1]}]).' ) return node return RelationDefined(node.name, dims) @@ -202,7 +202,7 @@ def _bare_name(self, node: NameNode) -> Condition | UnresolvedConditionNode: return node def _condition_call(self, node: UnresolvedConditionCallNode) -> Condition | UnresolvedConditionNode: - """``shift(, along=, offset=)`` or ``at(, by=relation[column])`` — the two operators that read a condition and answer one. + """``shift(, along=, offset=)`` or ``at(, by=subject[property])`` — the two operators that read a condition and answer one. ``count`` answers a number, so it stands on a comparison's side and [`_count`][] reads it there. Anything else naming a condition is @@ -258,7 +258,7 @@ def _condition_call(self, node: UnresolvedConditionCallNode) -> Condition | Unre return TranslatedCondition(mask, along.name, int(offset.value), tuple(sorted(mask.dims))) def _joined(self, node: UnresolvedConditionCallNode, mask: Mask) -> Condition | UnresolvedConditionNode: - """``at(, by=relation[column])`` — the condition read through a relation, as ``at`` reads an array. + """``at(, by=subject[property])`` — the condition read through a property, as ``at`` reads an array. The columns are read by the rules an expression's ``at`` is, so the one refusal a file meets for a bad read is the same in a ``where:`` and in @@ -333,7 +333,7 @@ def _presence(self, node: UnresolvedPresenceNode) -> Condition | UnresolvedCondi ns, context = self.ns, self.context subject, named = node.columns.relation, node.columns.columns properties = ns.properties.get(subject, {}) - if subject in ns.relations and not set(named) & set(properties): + if subject in ns.sets and not set(named) & set(properties): self.errors.append( f"{context}: '{node.columns}' alone tests no property of '{subject}'. Test whether '{subject}' has " f"a row with its bare name, '{subject}'." @@ -344,7 +344,7 @@ def _presence(self, node: UnresolvedPresenceNode) -> Condition | UnresolvedCondi ns.unknown(subject, context, allow_dims=True) if ns.kind(subject) is None else f"{context}: '{node.columns}' reads a column of '{subject}', which is " - f'{_declared_as(ns, subject)}. Only a relation, a set or a dimension has columns.' + f'{_declared_as(ns, subject)}. Only a set or a dimension has properties.' ) return node if unknown := [c for c in named if c not in properties]: @@ -473,13 +473,13 @@ def _expression_comparison(self, node: UnresolvedComparisonNode) -> ExpressionCo def _position( self, call: FunctionCallNode, node: UnresolvedComparisonNode ) -> DimensionPosition | UnresolvedComparisonNode: - """``position(dim[, within=relation[column]]) i``: the name a dimension, the relation keyed over it.""" + """``position(dim[, within=subject[property]]) i``: the name a dimension, the property it counts within.""" ns, context = self.ns, self.context shape = _position_shape(call) if shape is None: self.errors.append( f'{context}: this position() call is not of the form ' - f'position([, within=[]]). Write it in that form.' + f'position([, within=[]]). Write it in that form.' ) return node dimension, within = shape @@ -539,7 +539,7 @@ def _plain_comparison(self, node: UnresolvedComparisonNode, plain: _Plain) -> Co if left_column and ns.columns(left_name) is None: self.errors.append( f"{context}: '{plain.name}' reads a column of '{left_name}', which is {_declared_as(ns, left_name)}. " - f'Only a relation, a set or a dimension has columns.' + f'Only a set or a dimension has properties.' ) return node read: tuple[str, str] | None = None @@ -566,8 +566,8 @@ def _plain_comparison(self, node: UnresolvedComparisonNode, plain: _Plain) -> Co return ParameterComparison(left_name, plain.op, value, ns.leaf_dims[left_name]) case 'dimension': return DimensionComparison(left_name, plain.op, value) - case 'relation': - raise AssertionError('a relation is read as a table above, with or without a column') + case 'set': + raise AssertionError('a set is read as a table above, with or without a column') case 'variable' if left_name in ns.spec.given.expressions: self.errors.append( f"{context}: where references the given expression '{left_name}'. {GIVEN_IN_A_CONDITION}" @@ -582,57 +582,30 @@ def _plain_comparison(self, node: UnresolvedComparisonNode, plain: _Plain) -> Co def _read(self, name: str, column: str | None, spelling: str, op: ComparisonOperator) -> tuple[str, str] | None: """The table and the column a side of a where-comparison reads, or ``None`` with the refusal appended. - A property is read from its own table, ``subject.property``, and a - relation's or a set's own column from the relation or the set. + A property is read from its own table, ``subject.property``. A set + holds no value, and a member is a coordinate, so neither is compared. """ ns, context = self.ns, self.context properties = ns.properties.get(name, {}) if column is not None and column in properties: return f'{name}.{column}', column - if name in ns.relations: - read = self._relation_column(name, column, spelling, op) - return None if read is None else (name, read) - self.errors.append( - f"{context}: '{spelling}' reads '{column}', which is no property of '{name}'. " - f'{did_you_mean(column or "", properties, label="Properties")}' - ) - return None - - def _relation_column(self, name: str, column: str | None, spelling: str, op: ComparisonOperator) -> str | None: - """The value column a where-comparison on relation *name* reads, or the refusal. - - A comparison reads one value per coordinate, so the relation is keyed - and the column is one the key determines; unsaid, it is the one value - column where there is exactly one. - """ - ns, context = self.ns, self.context - shape = ns.relations[name] - if not shape.values: - self.errors.append( - f"{context}: '{spelling}' compares a column of '{name}', which has every column in its key. " - f"Declare that column under values:, or test the bare name '{name}' for whether a row exists." - ) - return None - if column is None: - if len(shape.values) != 1: - self.errors.append( - f"{context}: '{spelling}': '{name}' has {len(shape.values)} value columns ({list(shape.values)}), " - f'so name one: {name}[{shape.values[0]}].' - ) - return None - return shape.values[0] - if column not in shape.roles: + if column is None and name in ns.sets: self.errors.append( - f"{context}: '{spelling}': '{column}' is not a column of '{name}', whose columns are {list(shape.roles)}." + f"{context}: '{spelling}' compares the set '{name}', which holds no value. Compare one of its " + f"properties, {name}[] {op} ..., or test the bare name '{name}' for whether a row exists." ) return None - if column in shape.key: + if column is not None and column in dict(ns.subjects.get(name, ())): self.errors.append( - f"{context}: '{spelling}': '{column}' is a key column of '{name}'. Compare the coordinate itself, " - f'{shape.dim(column)} {op} ..., or a value column.' + f"{context}: '{spelling}': '{column}' is a member of '{name}'. Compare the coordinate itself, " + f'{dict(ns.subjects[name])[column]} {op} ..., or a property.' ) return None - return column + self.errors.append( + f"{context}: '{spelling}' reads '{column}', which is no property of '{name}'. " + f'{did_you_mean(column or "", properties, label="Properties")}' + ) + return None def _inequality_rewrite(self, node: _Plain, dtype: DeclaredDtype) -> str: """What to write in place of an inequality on a value whose dtype does not define one. @@ -643,8 +616,8 @@ def _inequality_rewrite(self, node: _Plain, dtype: DeclaredDtype) -> str: """ if dtype == 'bool': return f"Test the bare name, '{node.name}' or 'not {node.name}'" - name = _split_column(node.name)[0] - if self.ns.kind(name) == 'dimension' and not self.ns.unordered(name): + name, column = _split_column(node.name) + if not column and self.ns.kind(name) == 'dimension' and not self.ns.unordered(name): return f'Select members by their place with position({name}) {node.op} n, or compare it with == or !=' return 'Compare it with == or !=, or precompute the comparison as a boolean parameter' @@ -736,7 +709,7 @@ def _side_name_or_none(side: ArithmeticNode | ColumnNode | KeywordNode) -> str | def _side_name(side: ArithmeticNode | ColumnNode) -> str | None: - """The name a side of a where-comparison spells — bare or ``relation[column]`` — or ``None`` where it is arithmetic.""" + """The name a side of a where-comparison spells — bare or ``subject[property]`` — or ``None`` where it is arithmetic.""" if isinstance(side, NameNode): return side.name if isinstance(side, ColumnNode): @@ -746,11 +719,11 @@ def _side_name(side: ArithmeticNode | ColumnNode) -> str | None: def _reads(ns: Namespace, name: str, column: str) -> bool: """Whether a side spelled *name* or ``name[column]`` reads a table: a relation or a set by its bare name, or any subject with a column.""" - return ns.kind(name) == 'relation' or (bool(column) and ns.columns(name) is not None) + return ns.kind(name) == 'set' or (bool(column) and ns.columns(name) is not None) def _split_column(name: str) -> tuple[str, str]: - """``relation[column]`` as its two names, and a bare name with an empty column: the one reader of a side's spelling.""" + """``subject[property]`` as its two names, and a bare name with an empty column: the one reader of a side's spelling.""" relation, _, column = name.partition('[') return relation, column.removesuffix(']') @@ -813,8 +786,8 @@ def _not_arithmetic(context: str, side: ColumnNode | KeywordNode) -> str: """Why a relation column or a quoted label may not stand on a side of a comparison of expressions.""" if isinstance(side, ColumnNode): return ( - f"{context}: '{side.shown}' is a relation column in arithmetic. Compare it alone against a literal " - f'or a column, or carry the value in a parameter.' + f"{context}: '{side.shown}' is a property in arithmetic. Compare it alone against a literal " + f'or a property, or carry the value in a parameter.' ) return ( f"{context}: '{side.value}' is a quoted label, which is compared against one name. Put the name alone on " @@ -823,7 +796,7 @@ def _not_arithmetic(context: str, side: ColumnNode | KeywordNode) -> str: def _declared_rhs_error(context: str, node: _Plain, value: str, kind: str, *, given: bool = False) -> str: - """Why the right-hand side of a where-comparison may not name a variable, a relation or a dimension. + """Why the right-hand side of a where-comparison may not name a variable, a set or a dimension. A given expression is filed with the variables, and *given* says the name is one. """ @@ -835,10 +808,10 @@ def _declared_rhs_error(context: str, node: _Plain, value: str, kind: str, *, gi f'{context}: {comparison} compares against variable {value!r}. ' f'Test only parameters and dimension coordinates.' ) - if kind == 'relation': + if kind == 'set': return ( - f'{context}: {comparison} compares {node.name!r} against relation {value!r}. Compare against a ' - f'literal, or put a relation over the same dimensions on the left.' + f'{context}: {comparison} compares {node.name!r} against the set {value!r}, which holds no value. ' + f'Compare against a literal, or against a property over the same dimensions.' ) return ( f'{context}: {comparison} compares against dimension {value!r}, and two dimensions cannot be ' @@ -865,19 +838,19 @@ def _relation_pair_error( left_keys, right_keys = {ls.dim(k) for k in ls.key}, {rs.dim(k) for k in rs.key} if left_keys != right_keys: return ( - f'{context}: {comparison} compares relations keyed over different dimensions ' - f"('{left_name}' by {sorted(left_keys)}, '{right_name}' by {sorted(right_keys)}). Compare relations " - f'whose keys are over the same dimensions.' + f'{context}: {comparison} compares properties of subjects over different dimensions ' + f"('{left_name}' over {sorted(left_keys)}, '{right_name}' over {sorted(right_keys)}). Compare " + f'properties whose subjects are over the same dimensions.' ) if ls.dim(left) != rs.dim(right): return ( - f"{context}: {comparison} compares '{node.name}' (a column over '{ls.dim(left)}') with " - f"'{other}' (a column over '{rs.dim(right)}'). Compare two columns over the same dimension." + f"{context}: {comparison} compares '{node.name}' (into '{ls.dim(left)}') with " + f"'{other}' (into '{rs.dim(right)}'). Compare two properties into the same dimension." ) dtype = ns.dtypes[ls.dim(left)] if node.op not in ('==', '!=') and dtype not in _INEQUALITY_DTYPES: return ( - f"{context}: {comparison} compares two columns over '{ls.dim(left)}', of dtype '{dtype}', " + f"{context}: {comparison} compares two properties into '{ls.dim(left)}', of dtype '{dtype}', " f'on which {node.op} is not defined. Compare them with == or !=, or precompute the comparison as a ' 'boolean parameter.' ) diff --git a/src/mathspec/_dimensions.py b/src/mathspec/_dimensions.py index efd85e4f4..0b4f31d54 100644 --- a/src/mathspec/_dimensions.py +++ b/src/mathspec/_dimensions.py @@ -78,7 +78,7 @@ def _check_sum(node: Sum, inner: frozenset[str], context: str) -> None: every one, and the join's own rules are the sum's. """ if isinstance(node.operand, Join) and node.operand.columns.axes: - assert node.over == node.operand.columns.axes, 'a sum through a relation closes exactly the axes its join opens' + assert node.over == node.operand.columns.axes, 'a sum through a property closes exactly the axes its join opens' return for summed in node.over: if summed.dimension not in inner: @@ -172,8 +172,8 @@ def _check_joined(call: str, use: JoinColumns | Partition, inner: frozenset[str] ) if twice := sorted({d for d in dims if dims.count(d) > 1}): raise DimensionError( - f"{context}: {call} joins '{use.name}' on {twice} through more than one column. Join on " - f'different dimensions, or use a relation whose key columns are over different dimensions.' + f"{context}: {call} joins '{use.written}' on {twice} through more than one column. Join on " + f'different dimensions, or read a subject whose members are over different dimensions.' ) @@ -197,7 +197,7 @@ def _check_named_amount( raise DimensionError( f"{context}: {verb}({kwarg}={amount}): '{amount}' varies over {stray}, which the expression does " f"not carry (dims {sorted(inner)}). Declare '{amount}' over dims the expression carries, or group " - f'by a relation into one of {stray}.' + f'by a property into one of {stray}.' ) @@ -349,7 +349,7 @@ def _check_condition_dims( case DimensionComparison() | DimensionPosition(): leaf = f"where-dimension '{atom.name}'" case RelationComparison() | RelationPairComparison() | RelationDefined(): - leaf = f"where-relation '{atom.name}'" + leaf = f"the read of '{atom.name.replace('.', '[', 1) + ']' if '.' in atom.name else atom.name}'" case ExpressionComparison(): leaf = 'a comparison of expressions' case CountComparison(): diff --git a/src/mathspec/_exclusivity.py b/src/mathspec/_exclusivity.py index 51bb4e3e3..b179395d8 100644 --- a/src/mathspec/_exclusivity.py +++ b/src/mathspec/_exclusivity.py @@ -154,18 +154,24 @@ class Subject: kind: Literal['param', 'expression', 'dim', 'rank', 'relation', 'relation_pair', 'variable'] name: str qualifier: str | None = None - #: A rank's group columns: two positions by one relation into different columns are two subjects. + #: A rank's group columns: two positions within one subject by different properties are two subjects. group: tuple[str, ...] = () def __str__(self) -> str: if self.kind == 'rank': - within = f' within {self.qualifier}' if self.qualifier else '' + within = f' within {_written(self.qualifier)}' if self.qualifier else '' return f'the position of {self.name}{within}' if self.kind == 'relation_pair': - return f'{self.name} vs {self.qualifier}' + return f'{_written(self.name)} vs {_written(self.qualifier or "")}' if self.kind == 'relation' and self.qualifier: - return f'{self.name}[{self.qualifier}]' - return self.name + return f'{self.name.partition(".")[0]}[{self.qualifier}]' + return _written(self.name) + + +def _written(table: str) -> str: + """A table as the file reads it: ``generator.bus`` as ``generator[bus]``, a set by its name.""" + subject, _, prop = table.partition('.') + return f'{subject}[{prop}]' if prop else subject @dataclass(frozen=True) @@ -280,7 +286,7 @@ def _observe(node: TypedCondition, subject: Subject, values: set[_Literal]) -> N if node.op not in ('==', '!='): msg = ( f'{subject} uses {node.op!r}, and this analysis does not decide an inequality between two ' - 'relation columns. Compare them with == or !=, or precompute the comparison as a boolean ' + 'properties. Compare them with == or !=, or precompute the comparison as a boolean ' 'parameter and test that' ) raise Undecidable(msg) @@ -313,7 +319,7 @@ def _subject_of(node: TypedCondition) -> Subject: case TranslatedCondition(): return Subject('expression', 'a condition read at a neighbouring coordinate') case JoinedCondition(): - return Subject('expression', 'a condition read through a relation') + return Subject('expression', 'a condition read through a property') case _: assert_never(node) @@ -434,7 +440,7 @@ def _rank_cells(subject: Subject, positions_seen: set[int]) -> list[Cell]: """ positions = sorted(positions_seen) if positions and positions[0] < 0 <= positions[-1]: - within = f' within each {subject.qualifier} group' if subject.qualifier else '' + within = f' within each {_written(subject.qualifier)} group' if subject.qualifier else '' msg = ( f'{subject.name} is split at positions counted from both ends{within} ' f'({", ".join(str(position) for position in positions)}), which name the same row when it ' @@ -516,7 +522,7 @@ def _atom(node: TypedCondition, cell: dict[Subject, Cell], grid: _Grid) -> bool: raise AssertionError(msg) case CountComparison() | TranslatedCondition() | JoinedCondition(): msg = ( - 'a condition read as a count, at a neighbour or through a relation is refused as undecidable ' + 'a condition read as a count, at a neighbour or through a property is refused as undecidable ' 'before any cell is read' ) raise AssertionError(msg) diff --git a/src/mathspec/_expansion.py b/src/mathspec/_expansion.py index 6915cb8be..3cb1ac8b0 100644 --- a/src/mathspec/_expansion.py +++ b/src/mathspec/_expansion.py @@ -152,7 +152,7 @@ def _names(name: str, listed: NameListNode, bindings: dict[str, ArithmeticNode], def _substitute_columns(node: ColumnsNode, bindings: dict[str, ArithmeticNode], caller: str) -> ColumnsNode: - """``relation[column, …]`` with each formal in it replaced by the bare name bound to it. + """``subject[name, …]`` with each formal in it replaced by the bare name bound to it. A selection holds names only, so a formal there takes a name and nothing else: a number or an expression has no place between the brackets. @@ -163,7 +163,7 @@ def _substitute_columns(node: ColumnsNode, bindings: dict[str, ArithmeticNode], if bound is not None and not isinstance(bound, NameNode): msg = ( f"{caller}: the formal '{name}' is inside {node}, where only a name is allowed, and the call " - f'passes {bound}. Pass the name of a relation or of one of its columns.' + f'passes {bound}. Pass the name of a set or a dimension, or of one of its members or properties.' ) raise SchemaError(msg) names.append(name if bound is None else bound.name) diff --git a/src/mathspec/_expression_parser.py b/src/mathspec/_expression_parser.py index b13672e14..fa0f3a066 100644 --- a/src/mathspec/_expression_parser.py +++ b/src/mathspec/_expression_parser.py @@ -80,7 +80,7 @@ def __str__(self) -> str: @dataclass(frozen=True) class ColumnsNode: - """Columns of one relation in a kwarg value — ``sum(x, over=g, by=gen_bus[bus])``. + """The members and properties of one subject in a kwarg value — ``sum(x, over=g, by=generator[bus])``. The relation is written once and its columns after it, so one call cannot name columns of two tables. Unresolved: which columns the kwarg admits is @@ -444,7 +444,7 @@ def parse_expression(text: str) -> ParsedNode: def names_in(value: ArithmeticNode) -> tuple[str, ...]: - """The names a relation kwarg carries: one bare, several bracketed, none otherwise.""" + """The names a ``by=`` or ``within=`` kwarg carries: one bare, several bracketed, none otherwise.""" if isinstance(value, NameNode): return (value.name,) return value.names if isinstance(value, NameListNode) else () diff --git a/src/mathspec/_expression_resolver.py b/src/mathspec/_expression_resolver.py index 07e5b094a..9683314c0 100644 --- a/src/mathspec/_expression_resolver.py +++ b/src/mathspec/_expression_resolver.py @@ -153,8 +153,8 @@ def arith(self, node: ArithmeticNode) -> Expression | None: return None if isinstance(node, ColumnsNode): self.errors.append( - f'{self.context}: {node} names columns of a relation, which is allowed only as an operator ' - f'argument, such as at(x, by={node}). A relation is not a value.' + f'{self.context}: {node} reads a set or a property, which is allowed only as an operator ' + f'argument, such as at(x, by={node}). A set or a property is not a value.' ) return None assert_never(node) @@ -180,7 +180,7 @@ def _binary(self, node: BinaryOperatorNode) -> Expression | None: assert_never(node.op) def _name(self, node: NameNode) -> Expression | None: - """A bare name as the variable, parameter or named expression it declares; a dimension or relation is not a value. + """A bare name as the variable, parameter or named expression it declares; a dimension or a set is not a value. A named expression arrives as the one node [`Namespace.named`][] built for it; the cast is the one place a @@ -213,10 +213,10 @@ def _name(self, node: NameNode) -> Expression | None: f'not a value. To use its coordinates as data, declare a parameter over it.' ) return None - case 'relation': + case 'set': self.errors.append( - f"{self.context}: '{node.name}' is a relation, and a relation is not a value. Use its " - f'columns as sum(x, over=, by={node.name}[]) or in a condition, or declare a ' + f"{self.context}: '{node.name}' is a set, and a set is not a value. Read it as " + f'sum(x, over=, by={node.name}[]) or in a condition, or declare a ' f'parameter to carry numbers.' ) return None @@ -282,7 +282,7 @@ def _call(self, node: FunctionCallNode) -> Expression | None: return self._translation(node.name, operand, along, amounts, edge, partition) def _sum(self, operand: Expression, over: tuple[str, ...] | None, columns: ColumnsNode | None) -> Expression | None: - """A plain sum over the dims *over* names, or a sum through a relation, grouped by *columns*. + """A plain sum over the dims *over* names, or a sum through a subject, grouped by *columns*. Through a relation, the join and the sum over the axes it opens are one call, so no axis is ever left open. A name in *over* that is no column @@ -321,7 +321,7 @@ def _checked[N: Sum | Join | Translate | WindowSum](self, node: N) -> N | None: return node def _at(self, operand: Expression, columns: ColumnsNode) -> Expression | None: - """``at(x, by=relation[column])``: *operand* read at the value of *columns* each row of the relation holds.""" + """``at(x, by=subject[property])``: *operand* read at the value of *columns* each row of the subject holds.""" join = self.lookup(columns, frozenset(operand.dims)) return None if join is None else self._joined(operand, join) @@ -495,7 +495,7 @@ def _dual(self, node: FunctionCallNode) -> Dual | None: return Dual(value.name, dims=sorted_dims(declared.dims)) def _over(self, value: ArithmeticNode, operator: str, *, relation: bool) -> tuple[str, ...] | None: - """``over=``: a dimension or a list of them, and beside ``by=`` the columns of the relation too, each a bare name. + """``over=``: a dimension or a list of them, and beside ``by=`` the columns of the subject too, each a bare name. Beside ``by=`` a name is read against the relation in [`_grouping`][], so only a plain sum checks its names here. @@ -504,9 +504,9 @@ def _over(self, value: ArithmeticNode, operator: str, *, relation: bool) -> tupl return None if isinstance(value, ColumnsNode): self.errors.append( - f'{self.context}: {operator}(over={value}) names a relation, and over= takes bare names. Write ' - f'over={shown(value.columns)}. Beside by={value.relation}[...], a name in over= is a column of ' - f"'{value.relation}', or a dimension." + f'{self.context}: {operator}(over={value}) reads a subject, and over= takes bare names. Write ' + f'over={shown(value.columns)}. Beside by={value.relation}[...], a name in over= is a member or ' + f"a property of '{value.relation}', or a dimension." ) return None names = names_in(value) @@ -530,7 +530,7 @@ def _over(self, value: ArithmeticNode, operator: str, *, relation: bool) -> tupl return names if len(self.errors) == found else None def columns_ref(self, value: ArithmeticNode, operator: str, key: str) -> ColumnsNode | None: - """A kwarg naming columns of one relation, ``relation[column, …]``, each a column the relation declares. + """A kwarg naming what one subject reads, ``subject[name, …]``, each a member or a property of it. The relation is written once and its columns after it, so one call reads one table by the grammar alone. @@ -542,11 +542,11 @@ def columns_ref(self, value: ArithmeticNode, operator: str, key: str) -> Columns return None if value.relation in self.formals: return None - if (problem := self.not_a_relation(value.relation, operator, key)) is not None: + if (problem := self.not_a_subject(value.relation, operator, key)) is not None: self.errors.append(problem) return None admitted = self.ns.columns(value.relation) - assert admitted is not None, 'not_a_relation refuses a name that takes no brackets' + assert admitted is not None, 'not_a_subject refuses a name that takes no brackets' if unknown := [c for c in value.columns if c not in admitted and c not in self.formals]: self.errors.append( f'{self.context}: {operator}({key}={value}) names {unknown}, which is no ' @@ -571,30 +571,26 @@ def columns_ref(self, value: ArithmeticNode, operator: str, key: str) -> Columns def _not_columns(self, value: ArithmeticNode, operator: str, key: str) -> str: """Why *value* is not a column selection, with the selection it most likely meant.""" ns, context = self.ns, self.context - if isinstance(value, NameNode) and value.name in ns.relations: + if isinstance(value, NameNode) and value.name in ns.sets: return ( - f'{context}: {operator}({key}={value.name}) names the relation and none of its columns. Write ' - f"{key}={value.name}[], with a column of '{value.name}': " + f'{context}: {operator}({key}={value.name}) names the set and none of its members or properties. ' + f"Write {key}={value.name}[], with a member or a property of '{value.name}': " f'{list(ns.columns(value.name) or ())}.' ) if isinstance(value, NameNode) and value.name in ns.dimensions: over_here = ns.reads_over(value.name) - hint = ( - f'Columns over {value.name!r}: {over_here}' - if over_here - else f'No relation has a column over {value.name!r}.' - ) + hint = f'Reads over {value.name!r}: {over_here}' if over_here else f'Nothing is over {value.name!r}.' return ( f"{context}: {operator}({key}={value.name}): '{value.name}' is a dimension, and {key}= takes " - f'columns of a relation, written relation[column]. {hint}' + f'what one subject reads, written subject[property]. {hint}' ) return ( - f'{context}: {operator}({key}=...) takes columns of one relation. Write relation[column] or ' - f'relation[column, ...].' + f'{context}: {operator}({key}=...) takes what one subject reads. Write subject[property] or ' + f'subject[property, ...].' ) def _grouping(self, columns: ColumnsNode, over: tuple[str, ...]) -> tuple[JoinColumns, tuple[str, ...]] | None: - """How ``sum(x, over=..., by=relation[...])`` joins the relation, and the dims it sums away with no column. + """How ``sum(x, over=..., by=subject[...])`` joins the subject's table, and the dims it sums away with no column. A name in *over* is a column of the relation where it has one, and a dimension otherwise, summed away after the group-by. The entry @@ -608,7 +604,7 @@ def _grouping(self, columns: ColumnsNode, over: tuple[str, ...]) -> tuple[JoinCo call = f'sum(by={columns})' from_roles = tuple(n for n in over if n in admitted) plain = tuple(n for n in over if n not in admitted) - noun, nouns = _columns_noun(ns, name) + noun, nouns = 'column', 'columns' if unknown := [n for n in plain if n not in ns.dimensions]: self.errors.append( f"{self.context}: {call}: over={unknown[0]} names no {noun} of '{name}', whose {nouns} are " @@ -631,7 +627,7 @@ def _grouping(self, columns: ColumnsNode, over: tuple[str, ...]) -> tuple[JoinCo self.errors.append( f'{self.context}: {call}: the columns this sum groups by, {list(join.grouped)}, hold the whole key ' f'{list(shape.key)}, so every group is one row and the sum adds nothing. Write ' - f'{_lookup_rewrite(name, from_roles, plain)}, or group by a value column.' + f'{_lookup_rewrite(name, from_roles, plain)}, or group by a property.' ) return None return join, plain @@ -648,14 +644,14 @@ def lookup(self, columns: ColumnsNode, inner: frozenset[str]) -> JoinColumns | N table, shape = self.ns.table(name, columns.columns) if not shape.values: self.errors.append( - f"{self.context}: {call}: '{name}' has every column in its key, so it has no value column for at() " + f"{self.context}: {call}: '{name}' is read by its members alone, so it has no property for at() " f'to read. Sum through it instead.' ) return None if keyed := [r for r in columns.columns if r in shape.key]: self.errors.append( - f"{self.context}: {call} names {keyed}, a key column of '{name}'. A lookup reads value columns " - f'at the key. Name one of the value columns, {list(shape.values)}, or sum through the key column.' + f"{self.context}: {call} names {keyed}, a member of '{name}'. A lookup reads properties at the " + f'members. Name one of its properties, {list(shape.values)}, or sum through the member.' ) return None matched = inner - {shape.dim(r) for r in columns.columns} @@ -682,7 +678,7 @@ def _checked_join( return JoinColumns(table, shape, (*from_roles, *kept), (*into_roles, *kept)) def partition(self, columns: ColumnsNode, operator: str, along_dim: str) -> Partition | None: - """How a partition (``shift``, ``sum_back``, ``position``) steps along the relation *columns* names. + """How a partition (``shift``, ``sum_back``, ``position``) steps along the table *columns* reads. It steps along the one key column over *along_dim* (a key has one column per dimension), joins on the other key columns and groups by the @@ -695,42 +691,40 @@ def partition(self, columns: ColumnsNode, operator: str, along_dim: str) -> Part call = f'{operator}(within={columns})' if not shape.values: self.errors.append( - f"{context}: {call}: '{name}' has every column in its key, so it makes no groups. Move the group " - f'columns under values:, and keep under key: the column {operator} steps along.' + f"{context}: {call}: '{name}' is read by its members alone, so it makes no groups. Group by a " + f"property of '{name}'." ) return None over_keys = [r for r in shape.key if shape.dim(r) == along_dim] if not over_keys: self.errors.append( - f"{context}: {call}: '{name}' has no key column over '{along_dim}'. Its key is {list(shape.key)}." + f"{context}: {call}: '{name}' has no member over '{along_dim}'. Its members are {list(shape.key)}." ) return None if keyed := [r for r in within_roles if r in shape.key]: self.errors.append( - f"{context}: {call}: within= names {keyed}, a key column of '{name}'. Name one of its value " - f'columns, {list(shape.values)}.' + f"{context}: {call}: within= names {keyed}, a member of '{name}'. Name one of its properties, " + f'{list(shape.values)}.' ) return None (along,) = over_keys joined = tuple(r for r in shape.key if r != along) return Partition(table, shape, along, within_roles, joined) - def not_a_relation(self, name: str, operator: str, key: str) -> str | None: - """Why *name*, written before the columns of a selection, is not a relation; ``None`` where it is one.""" + def not_a_subject(self, name: str, operator: str, key: str) -> str | None: + """Why *name*, written before the brackets of a read, is no set and no dimension with properties; ``None`` where it is one.""" ns, context = self.ns, self.context - if name in ns.relations or name in ns.properties: + if name in ns.sets or name in ns.properties: return None if name in ns.dimensions: return ( f"{context}: {operator}({key}={name}[...]): '{name}' is a dimension with no properties. Declare " - f'one under properties: {name}: {{: }}, or write a relation first and its ' - f'columns after it: {key}=[].' + f'one under properties: {name}: {{: }}.' ) return ( - f'{context}: {operator}({key}={name}[...]) does not name a relation{_or_a_formal(self.formals)}. ' - f'{did_you_mean(name, ns.relations, label="Relations")}\n' - f"Declare it under 'relations:' as {name}: {{key: , " - f'values: }}.' + f'{context}: {operator}({key}={name}[...]) does not name a set or a dimension' + f'{_or_a_formal(self.formals)}. ' + f'{did_you_mean(name, [*ns.sets, *ns.properties], label="Sets and dimensions with properties")}' ) @@ -775,9 +769,7 @@ def _columns_over(shape: Relation, dims: tuple[str, ...]) -> str: def _columns_noun(ns: Namespace, name: str) -> tuple[str, str]: - """What one name, and several, inside ``name[...]`` are called: a relation's columns, a dimension's properties, a set's members and properties.""" - if name in ns.spec.relations: - return 'column', 'columns' + """What one name, and several, inside ``name[...]`` are called: a dimension's properties, a set's members and properties.""" if name in ns.dimensions: return 'property', 'properties' return 'member or property', 'members and properties' diff --git a/src/mathspec/_lowering.py b/src/mathspec/_lowering.py index 8f012b2f1..6aa3b48c7 100644 --- a/src/mathspec/_lowering.py +++ b/src/mathspec/_lowering.py @@ -403,15 +403,15 @@ def _parameter_masks(spec: Spec, ns: Namespace, errors: list[str]) -> dict[str, continue errors.extend( f"{context}: its `where:` reads variable '{v}', and a parameter's mask is evaluated from the data " - f'alone, before any variable has a value. Test a parameter or a relation instead.' + f'alone, before any variable has a value. Test a parameter, a set or a property instead.' for v in sorted(mask.names_read & {*spec.variables, *spec.given.variables}) ) errors.extend( f"{context}: its `where:` reads '{pname}' itself, and whether a row is there is what the mask " - f'decides. Test a relation or another parameter.' + f'decides. Test a set, a property or another parameter.' if read == pname else f"{context}: its `where:` reads '{read}', which has a mask of its own. Test what the mask of " - f"'{read}' tests, or a relation." + f"'{read}' tests, or a set or a property." for read in sorted(mask.names_read & masked) ) return masks diff --git a/src/mathspec/_operators.py b/src/mathspec/_operators.py index 4be944704..e7da88a16 100644 --- a/src/mathspec/_operators.py +++ b/src/mathspec/_operators.py @@ -24,10 +24,10 @@ class Builtin: Keyword arguments come in four kinds, and the kind decides what resolution turns the value into: ``dimension_kwargs`` name a dimension or a list of - them (``sum(x, over=generator)``); ``column_kwargs`` name columns of one - relation, written ``relation[column, …]`` (``sum(x, over=generator, - by=gen_bus[bus])``), and beside one a ``dimension_kwargs`` name is a - column of that relation where it has one; ``edge_kwargs`` take a closed + them (``sum(x, over=generator)``); ``column_kwargs`` name what one subject + reads, written ``subject[name, …]`` (``sum(x, over=generator, + by=generator[bus])``), and beside one a ``dimension_kwargs`` name is a + column of that subject's table where it has one; ``edge_kwargs`` take a closed keyword or a number; ``required_value_kwargs`` are ordinary values that must be present — a number, never a name to resolve (``shift(..., offset=1)``). @@ -71,26 +71,26 @@ def kind_of(self, kwarg: str) -> Literal['dimension', 'columns', 'edge', 'value' return None -#: The closed operator set. ``relation[column, …]`` is the one form that -#: addresses a relation, and a relation carries its own dimensions, so no +#: The closed operator set. ``subject[name, …]`` is the one form that +#: reads a set or a property, and its table carries its own dimensions, so no #: sibling kwarg restates them. ``by=`` on ``sum`` names the columns a sum #: groups by, and on ``at`` the columns a lookup reads. ``within=`` on #: ``shift`` and ``sum_back`` partitions the axis the operator steps along: it #: says which rows are neighbours, not which group a term is added to. BUILTINS: dict[str, Builtin] = { 'sum': Builtin( - 'sum(), sum(, over=) or sum(, over=, by=[])', + 'sum(), sum(, over=) or sum(, over=, by=[])', dimension_kwargs=('over',), column_kwargs=('by',), optional_kwargs=('over', 'by'), with_columns=('over',), ), 'at': Builtin( - 'at(, by=[])', + 'at(, by=[])', column_kwargs=('by',), ), 'sum_back': Builtin( - "sum_back(, along=, window=[, edge='wrap'][, within=[]])", + "sum_back(, along=, window=[, edge='wrap'][, within=[]])", dimension_kwargs=('along',), column_kwargs=('within',), required_value_kwargs=('window',), @@ -98,7 +98,7 @@ def kind_of(self, kwarg: str) -> Literal['dimension', 'columns', 'edge', 'value' optional_kwargs=('within',), ), 'shift': Builtin( - "shift(, along=, offset=[, edge='wrap'|][, within=[]])", + "shift(, along=, offset=[, edge='wrap'|][, within=[]])", dimension_kwargs=('along',), column_kwargs=('within',), required_value_kwargs=('offset',), @@ -160,8 +160,8 @@ def call_shape_error(name: str, positional: int, kwargs: Iterable[str]) -> str | optional = {*builtin.edge_kwargs, *builtin.optional_kwargs} if keys & set(builtin.column_kwargs) and (unsaid := sorted(frozenset(builtin.with_columns) - keys)): return ( - f'{name}() through a relation does not name {", ".join(f"{k}=" for k in unsaid)}. A sum through a ' - f'relation names in over= the columns it sums away.\n' + f'{name}() through a set or a property does not name {", ".join(f"{k}=" for k in unsaid)}. A sum ' + f'through a set or a property names in over= the members it sums away.\n' f'Write: {builtin.usage}' ) fits = positional == 1 and keys - optional == builtin.required diff --git a/src/mathspec/_resolution.py b/src/mathspec/_resolution.py index 6cb58355a..8e72cb1ad 100644 --- a/src/mathspec/_resolution.py +++ b/src/mathspec/_resolution.py @@ -62,7 +62,7 @@ #: What a name a file may write turns out to be. Answered by #: [`Namespace.kind`][], so a pass reading a name switches over this rather #: than over the stores it would otherwise have to try in order. -NameKind = Literal['variable', 'parameter', 'dimension', 'relation'] +NameKind = Literal['variable', 'parameter', 'dimension', 'set'] class Namespace: @@ -87,7 +87,7 @@ class Namespace: 'leaf_dims', 'parameters', 'properties', - 'relations', + 'sets', 'spec', 'subjects', 'tables', @@ -131,9 +131,9 @@ def __init__(self, spec: Spec) -> None: #: Every table the file declares, as [`tables`][] lowers it; what the #: program carries as its relations. self.tables = tables(spec) - #: relation or set name -> its columns and key: the tables a file - #: writes by name, which a property's dotted name is not. - self.relations: dict[str, Relation] = {n: lk for n, lk in self.tables.items() if lk.subject is None} + #: set name -> the relation it lowers to: the tables a file writes by + #: name, which a property's dotted name is not. + self.sets: dict[str, Relation] = {n: lk for n, lk in self.tables.items() if lk.subject is None} #: dimension or set -> its key as ``(member, dimension)`` pairs: a #: dimension is its own one member, and a set has its own. self.subjects: dict[str, tuple[tuple[str, str], ...]] = { @@ -302,11 +302,9 @@ def unordered(self, name: str) -> bool: def columns(self, name: str) -> tuple[str, ...] | None: """What ``name[...]`` may name, or ``None`` where *name* takes no brackets. - A relation's columns; a set's members and properties; a dimension's - own name, which is its one member, and its properties. + A set's members and properties, and a dimension's own name, which is + its one member, and its properties. """ - if name in self.spec.relations: - return self.relations[name].roles if name in self.subjects: return (*(m for m, _ in self.subjects[name]), *self.properties.get(name, {})) return None @@ -314,8 +312,7 @@ def columns(self, name: str) -> tuple[str, ...] | None: def table(self, name: str, columns: Iterable[str]) -> tuple[str, Relation]: """The table ``name[columns]`` reads: the name a program node carries, and its shape. - A relation, and a set where *columns* names none of its properties, - read their own table. One property is read from its own table, + A set read by its members alone reads its own table. One property is read from its own table, ``subject.property``, which carries the subject's members too. Several are read from their inner join, a relation named after the subject, keyed by its members, with one value column per property in the order @@ -327,7 +324,7 @@ def table(self, name: str, columns: Iterable[str]) -> tuple[str, Relation]: properties = self.properties.get(name, {}) read = [c for c in dict.fromkeys(columns) if c in properties] if not read: - return name, self.relations[name] + return name, self.sets[name] if len(read) == 1: return f'{name}.{read[0]}', self.tables[f'{name}.{read[0]}'] key = self.subjects[name] @@ -338,7 +335,7 @@ def table(self, name: str, columns: Iterable[str]) -> tuple[str, Relation]: def reads_over(self, dimension: str) -> list[str]: """Every ``name[column]`` a file may write whose column is over *dimension*, for the writer who named the dimension.""" return [ - *(f'{n}[{r}]' for n, shape in self.relations.items() for r, dim in shape.columns if dim == dimension), + *(f'{n}[{r}]' for n, shape in self.sets.items() for r, dim in shape.columns if dim == dimension), *(f'{s}[{p}]' for s, group in self.properties.items() for p, into in group.items() if into == dimension), ] @@ -350,8 +347,8 @@ def kind(self, name: str) -> NameKind | None: return 'parameter' if name in self.dimensions: return 'dimension' - if name in self.relations: - return 'relation' + if name in self.sets: + return 'set' return None def unknown(self, name: str, context: str, *, allow_dims: bool, formals: Iterable[str] = ()) -> str: @@ -361,8 +358,8 @@ def unknown(self, name: str, context: str, *, allow_dims: bool, formals: Iterabl name: The name the file wrote. context: The entry it was found in. allow_dims: Whether a dimension would have been accepted there. It marks a - condition, which reads a relation as readily as a parameter, so the - listing carries the relations too; an expression, where a relation is not a + condition, which reads a set as readily as a parameter, so the + listing carries the sets too; an expression, where a set is not a value, lists the variables instead. formals: A macro's formals, listed first when there are any. """ @@ -371,7 +368,7 @@ def unknown(self, name: str, context: str, *, allow_dims: bool, formals: Iterabl [ ('Parameters', self.parameters), ('Dimensions', self.dimensions), - ('Relations', self.relations), + ('Sets', self.sets), *([('Conditions', self.conditions)] if self.conditions else []), ] if allow_dims @@ -406,8 +403,7 @@ def tables(spec: Spec) -> dict[str, Relation]: [`reference_errors`][mathspec._validation.reference_errors] passed, so every subject is a dimension or a set. """ - relations = {n: Relation(lk.pairs, lk.key_roles, lk.missing, lk.description) for n, lk in spec.relations.items()} - relations |= {n: Relation(st.pairs, st.members, st.missing, st.description) for n, st in spec.sets.items()} + relations = {n: Relation(st.pairs, st.members, st.missing, st.description) for n, st in spec.sets.items()} for subject, properties in spec.properties.items(): key = spec.sets[subject].pairs if subject in spec.sets else ((subject, subject),) relations |= { diff --git a/src/mathspec/_typesetting/legend.py b/src/mathspec/_typesetting/legend.py index c00aa6e72..5e91fb0c4 100644 --- a/src/mathspec/_typesetting/legend.py +++ b/src/mathspec/_typesetting/legend.py @@ -264,7 +264,7 @@ def _missing(self, value: ParameterMissing, default: str = 'refused', where: str return f', {shown} {where}' def _signature(self, name: str, lk: Relation) -> str: - """A relation in the legend: a function from its key sets to its value sets, or a relation inside the product. + """A table in the legend: a property as a function from its subject, or a set inside the product. A property is named after itself and maps from its subject: the set of a dimension, or a set by its own name, since a property of a sparse set @@ -278,8 +278,6 @@ def product(roles: Iterable[str]) -> str: (value,) = lk.values domain = product(lk.key) if lk.subject in self.program.dimensions else self.format.upright(lk.subject) return f'{self.format.upright(value)}: {domain} {self._op("maps_to")} {product(lk.values)}' - if lk.values: - return f'{self.format.upright(name)}: {product(lk.key)} {self._op("maps_to")} {product(lk.values)}' return f'{self.format.upright(name)} {self._op("subset_of")} {product(lk.roles)}' def _coords(self, dim: str, noticed: Noticed) -> str: @@ -310,7 +308,7 @@ def _notation(self, noticed: Noticed) -> list[LegendItem]: holds no claim the language reference does not already make. """ fmt, dash = self.format, self.format.dash - relation = fmt.apply(fmt.upright('relation'), 't') + group = fmt.apply(fmt.upright('property'), 't') shifted, t = fmt.math('t-k'), fmt.math('t') items = [] if 'wrap' in noticed.policies: @@ -326,8 +324,8 @@ def _notation(self, noticed: Noticed) -> list[LegendItem]: if noticed.grouped: items.append( LegendItem( - f't {fmt.superscript(self._op("cyclic_minus"), relation)} k', - f'{fmt.mono("within=relation[...]")} {dash} {shifted} inside the group of {t}', + f't {fmt.superscript(self._op("cyclic_minus"), group)} k', + f'{fmt.mono("within=subject[...]")} {dash} {shifted} inside the group of {t}', ) ) if noticed.positions: @@ -340,8 +338,8 @@ def _notation(self, noticed: Noticed) -> list[LegendItem]: if 'grouped' in noticed.positions: items.append( LegendItem( - fmt.apply(fmt.subscript(self._op('position'), [relation]), 't'), - f'{fmt.mono("position(within=relation[...])")} {dash} the position of {t} inside its group', + fmt.apply(fmt.subscript(self._op('position'), [group]), 't'), + f'{fmt.mono("position(within=subject[...])")} {dash} the position of {t} inside its group', ) ) if 'from_end' in noticed.positions: diff --git a/src/mathspec/_typesetting/walk.py b/src/mathspec/_typesetting/walk.py index 0bbe52571..ab4128887 100644 --- a/src/mathspec/_typesetting/walk.py +++ b/src/mathspec/_typesetting/walk.py @@ -263,30 +263,26 @@ def _translation(self, step: _Step) -> str: return operator return self.format.superscript(operator, step.within) - def _relation_read(self, name: str, lk: Relation, at: Mapping[str, str], read: str) -> str: - """Relation *name*'s column *read* as a function at the columns *at* fixes: ``bus(g)``, ``zone_of(g, p)`` or ``ends.bus0(l)``. - - *lk* is the relation's shape, which a call carries, since an inner join - of several properties is no table of the program. *at* maps each key - role to the index it is read at. A property is named after itself, as - the file writes it in ``subject[property]``. A relation is named after - itself where the key determines one column, and after the column read - otherwise. + def _relation_read(self, lk: Relation, at: Mapping[str, str], read: str) -> str: + """Property *read* as a function at the members *at* fixes: ``bus(g)`` or ``zone(g, t)``. + + *lk* is the shape a call carries, since an inner join of several + properties is no table of the program. *at* maps each member to the + index it is read at. A property is named after itself, as the file + writes it in ``subject[property]``. """ - function = _function(name, lk, read) - return self.format.apply(self.format.upright(function), self.format.joined([at[k] for k in lk.key], '')) + return self.format.apply(self.format.upright(read), self.format.joined([at[k] for k in lk.key], '')) def _relation_row(self, name: str, lk: Relation, at: Mapping[str, str]) -> str: - """That relation *name* has a row at the key *at* fixes. + """That table *name* has a row at the members *at* fixes. - A keyed table is a function, so the claim is that it is defined there: - ``gen_zone(g, t) is defined``. A bare one is a set of rows, and every - column of it is a key column, so the row is written out: - ``(g, b) ∈ connection``. + A property is a function, so the claim is that it is defined there: + ``bus1(l) is defined``. A set is a set of rows, and every column of it + is a member, so the row is written out: ``(g, b) ∈ connection``. """ key = self.format.joined([at[k] for k in lk.key], '') if lk.values: - reads = [self.format.apply(self.format.upright(_function(name, lk, v)), key) for v in lk.values] + reads = [self.format.apply(self.format.upright(v), key) for v in lk.values] return f'{self.format.joined(reads, ", ")} {self.format.prose(" is defined")}' return f'{self.format.parenthesise(key)} {self._op("in")} {self.format.upright(name)}' @@ -294,9 +290,9 @@ def _frame_key(self, lk: Relation, ctx: _Context) -> dict[str, str]: """The key roles of *lk* at the frame's own indices of their dimensions.""" return {k: ctx.subscript(lk.dim(k)) for k in lk.key} - def _value_read(self, name: str, lk: Relation, column: str, ctx: _Context) -> str: - """A keyed relation's value *column* read at the frame's own indices of its key: ``period_of(t)``.""" - return self._relation_read(name, lk, self._frame_key(lk, ctx), column) + def _value_read(self, lk: Relation, column: str, ctx: _Context) -> str: + """Property *column* read at the frame's own indices of its subject's members: ``period(t)``.""" + return self._relation_read(lk, self._frame_key(lk, ctx), column) def _tuple(self, reads: list[str]) -> str: """Several reads as one group label: the read alone where there is one, a bracketed tuple otherwise.""" @@ -469,7 +465,7 @@ def _looked_up(self, columns: JoinColumns, ctx: _Context) -> _Context: """*ctx* with each dimension *columns* drops read at the relation, as ``at`` re-indexes a leaf.""" at = {r: ctx.subscript(columns.dim(r)) for r in columns.grouped} for read in columns.dropped: - ctx = ctx.looked_up(columns.dim(read), self._relation_read(columns.name, columns.relation, at, read)) + ctx = ctx.looked_up(columns.dim(read), self._relation_read(columns.relation, at, read)) return ctx def _grouping(self, join: JoinColumns, dummies: Mapping[str, str], ctx: _Context) -> list[str]: @@ -488,7 +484,7 @@ def _grouping(self, join: JoinColumns, dummies: Mapping[str, str], ctx: _Context fixed = [r for r in join.relation.values if r in at] if not fixed: return [self._relation_row(join.name, join.relation, at)] - return [f'{self._relation_read(join.name, join.relation, at, r)} {self._op("equal")} {at[r]}' for r in fixed] + return [f'{self._relation_read(join.relation, at, r)} {self._op("equal")} {at[r]}' for r in fixed] def _group(self, partition: Partition | None) -> str: """A ``within=`` as the superscript its translation operator carries. @@ -500,7 +496,7 @@ def _group(self, partition: Partition | None) -> str: if partition is None: return '' at = {r: self.symbols.index[partition.dim(r)] for r in (partition.along, *partition.joined)} - return self._tuple([self._relation_read(partition.name, partition.relation, at, r) for r in partition.grouped]) + return self._tuple([self._relation_read(partition.relation, at, r) for r in partition.grouped]) def _width(self, width: int | str) -> str: """``sum_back``'s ``window=``: a number, or a parameter's own symbol. @@ -603,21 +599,16 @@ def sides(self, node: AlignedComparison, ctx: _Context) -> tuple[str, str]: grouping = ( None if node.partition is None - else self._tuple( - [ - self._value_read(node.partition.name, node.partition.relation, c, ctx) - for c in node.partition.grouped - ] - ) + else self._tuple([self._value_read(node.partition.relation, c, ctx) for c in node.partition.grouped]) ) left = self._position(ctx.subscript(node.name), grouping) right = self._ordinal(node.name, node.position, grouping) elif isinstance(node, RelationComparison): lk = self.program.relations[node.name] - left, right = self._value_read(node.name, lk, node.column, ctx), self._literal(node.value) + left, right = self._value_read(lk, node.column, ctx), self._literal(node.value) elif isinstance(node, RelationPairComparison): - left = self._value_read(node.name, self.program.relations[node.name], node.column, ctx) - right = self._value_read(node.other, self.program.relations[node.other], node.other_column, ctx) + left = self._value_read(self.program.relations[node.name], node.column, ctx) + right = self._value_read(self.program.relations[node.other], node.other_column, ctx) elif isinstance(node, CountComparison): index, inner = ctx.reducing(node.over) counted = self.format.set_of( @@ -987,10 +978,3 @@ def _bound(self, ctx: _Context, value: Expression) -> str: def _sorted(self, dims: frozenset[str]) -> list[str]: order = list(self.program.dimensions) return sorted(dims, key=order.index) - - -def _function(name: str, lk: Relation, read: str) -> str: - """The function a read of column *read* of *name* prints as: the property, the relation, or ``relation.column``.""" - if lk.subject is not None: - return read - return name if len(lk.values) == 1 else f'{name}.{read}' diff --git a/src/mathspec/_validation.py b/src/mathspec/_validation.py index 05dd373a5..a9f9cbfe8 100644 --- a/src/mathspec/_validation.py +++ b/src/mathspec/_validation.py @@ -28,7 +28,7 @@ from mathspec._sos import coefficients from mathspec._yaml import read_spec from mathspec.errors import SchemaError, did_you_mean, unordered -from mathspec.spec import NUMERIC_DTYPES, Spec, side_columns +from mathspec.spec import NUMERIC_DTYPES, Spec if TYPE_CHECKING: from collections.abc import Iterable, Iterator @@ -83,7 +83,6 @@ def reference_errors(spec: Spec) -> list[str]: return [ *_name_collisions(spec), *_frame_dimensions(spec), - *_relation_targets(spec), *_set_members(spec), *_property_targets(spec), *_bound_names(spec), @@ -103,7 +102,6 @@ def _flat_namespace(spec: Spec) -> list[tuple[str, Iterable[str]]]: """Each kind of entry whose names share the one namespace an expression reads, in file order.""" return [ ('dimension', spec.dimensions), - ('relation', spec.relations), ('set', spec.sets), ('parameter', spec.parameters), ('given parameter', spec.given.parameters), @@ -169,48 +167,6 @@ def _frame_dimensions(spec: Spec) -> Iterator[str]: ) -def _relation_targets(spec: Spec) -> Iterator[str]: - """A relation has at least two columns over declared dimensions, each named once, and a key naming some of them.""" - for lname, lk in spec.relations.items(): - if len(lk.pairs) < 2: - yield ( - f"Relation '{lname}' has {len(lk.pairs)} column(s), and needs at least two under 'key:' and " - f"'values:'. For a label on one dimension, declare a parameter instead." - ) - if not lk.key_roles: - yield f"Relation '{lname}' names no key column. Name the columns that identify a row under 'key:'." - for side, written in (('key', lk.key), ('values', lk.values)): - yield from ( - f"Relation '{lname}' names dimension '{d}' twice under '{side}:'. Give the two columns roles: " - f'{side}: {{{d}0: {d}, {d}1: {d}}}.' - for d, count in Counter(dim for _, dim in side_columns(written)).items() - if count > 1 and not isinstance(written, dict) - ) - yield from ( - f"Relation '{lname}' names column '{role}' under both 'key:' and 'values:'. Rename the value column: " - f'values: {{: {dict(lk.pairs)[role]}}}.' - for role in dict.fromkeys(lk.key_roles) - if role in lk.value_roles - ) - yield from ( - undeclared_dimension('Relation', lname, d) for d in dict.fromkeys(lk.dims) if d not in spec.dimensions - ) - yield from ( - f"Relation '{lname}' names column '{role}' after dimension '{role}', but the column is over " - f"'{dim}'. Rename the column after what it holds." - for role, dim in lk.pairs - if role in spec.dimensions and role != dim - ) - if lk.value_roles: - yield from ( - f"Relation '{lname}' has two key columns over '{d}' " - f'({[k for k in lk.key_roles if dict(lk.pairs)[k] == d]}). Key the table by one column over ' - f"each dimension, or move one of them to 'values:'." - for d, count in Counter(dict(lk.pairs)[k] for k in lk.key_roles).items() - if count > 1 - ) - - def _over_a_set(kind: str, name: str, dims: Iterable[str], set_name: str, spec: Spec) -> str: """The refusal for an entry over a set, naming the frame over its member dimensions and the mask.""" members = [d for _, d in spec.sets[set_name].pairs] diff --git a/src/mathspec/program.py b/src/mathspec/program.py index a019b93dc..70e25724f 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -276,7 +276,7 @@ class Column: name: str def __str__(self) -> str: - """The column as a file writes it, ``relation[column]``.""" + """The column as a file writes it, ``subject[property]``.""" return f'{self.relation}[{self.name}]' @@ -521,7 +521,7 @@ class Relation: missing: RelationMissing = 'refused' description: str | None = None #: The dimension or set whose rows the key ranges over, on a relation - #: lowered from a property; ``None`` on a set and on a relation. Every row + #: lowered from a property; ``None`` on a set. Every row #: here is a row of the subject, and ``missing`` asks whether every row of #: the subject is a row here, rather than every combination of the key's #: dimensions. diff --git a/src/mathspec/spec.py b/src/mathspec/spec.py index 631d74413..d82bdd254 100644 --- a/src/mathspec/spec.py +++ b/src/mathspec/spec.py @@ -77,7 +77,6 @@ 'PiecewiseLinkSpec', 'PiecewiseSpec', 'PropertySpec', - 'RelationSpec', 'SetSpec', 'SosSpec', 'Spec', @@ -174,7 +173,7 @@ def _refused_or_absent(v: object, kind: str, may: str, outcome: str) -> object: """*v* where it is ``refused`` or ``absent``, the two words a ``missing:`` on a table takes. A label is data, so no value fills a gap in a map: the ``missing:`` of a - relation, a set or a property reads a key, never a value. + set or a property reads a key, never a value. """ if v is None: raise _missing_null(kind, f'Write absent where {may}, or leave the key out for refused.') @@ -208,10 +207,10 @@ def _also_written_as( def side_columns(written: str | list[str] | dict[str, str] | None) -> tuple[tuple[str, str], ...]: - """``(role, dimension)`` per column of one side of a relation, in written order. + """``(member, dimension)`` per member of a set, in written order. - A bare name or a list names each column after the dimension it is over; a - mapping names the roles, which is what two columns over one dimension need. + A bare name or a list names each member after the dimension it is over; a + mapping names the members, which is what two members over one dimension need. """ if written is None: return () @@ -220,84 +219,6 @@ def side_columns(written: str | list[str] | dict[str, str] | None) -> tuple[tupl return tuple((d, d) for d in ((written,) if isinstance(written, str) else written)) -class RelationSpec(_StrictSpec): - """A named relation between dimensions: the columns a row is keyed by, and the columns that key determines. - - Each side is a dimension, a list of them, or a mapping of column name to - dimension where two columns share one. ``key:`` is the claim the language - checks when the data is attached: one row per key tuple, so every ``values:`` column is a - function of it. A relation with no ``values:`` is **bare** — every column is - in its key, a row is its own identity, and nothing reads it:: - - relations: - gen_bus: {key: generator, values: bus} - gen_bt: {key: [generator], values: [bus, technology]} - zone_of: {key: [generator, period], values: zone} - ends: {key: line, values: {bus0: bus, bus1: bus}} - connection: {key: [generator, bus]} - - A sum joins the table on the columns ``over=`` names and every other key - column, and groups by the columns ``by=relation[...]`` names. A lookup - joins on the columns it names. The entry fixes no direction. A - column is named after its own dimension or after none, so a name in a - call reads the same as a column and as a dimension. The map itself is - data, and arrives with the rest of it, under the relation's name, one - column per role. - """ - - _label: ClassVar[str] = 'a relation entry' - - key: str | list[str] | dict[str, str] - values: str | list[str] | dict[str, str] | None = None - #: Whether every combination of the key's dimensions needs a row: - #: ``refused`` refuses data that leaves one out, and ``absent`` reads it - #: as no row. - missing: RelationMissing = 'refused' - description: str | None = None - - @field_validator('missing', mode='before') - @classmethod - def _refused_or_absent(cls, v: object) -> object: - return _refused_or_absent( - v, - 'a relation', - 'a key combination may have no row', - 'A key combination with no row is refused, or is not in the relation.', - ) - - @model_serializer(mode='wrap') - def _as_written(self, handler: SerializerFunctionWrapHandler) -> dict[str, object]: - """``missing`` is written where it is not ``refused``: refused is what leaving it out says.""" - return _without_refused(cast('dict[str, object]', handler(self))) - - @property - def pairs(self) -> tuple[tuple[str, str], ...]: - """``(role, dimension)`` per column, the key's columns first. - - The program calls the same thing [`columns`][mathspec.program.Relation.columns]; - here the table has no field of its own, being what the two sides make. - """ - return (*side_columns(self.key), *side_columns(self.values)) - - @property - def roles(self) -> tuple[str, ...]: - return tuple(role for role, _ in self.pairs) - - @property - def dims(self) -> tuple[str, ...]: - return tuple(dim for _, dim in self.pairs) - - @property - def key_roles(self) -> tuple[str, ...]: - """The key roles, however ``key:`` was written.""" - return tuple(role for role, _ in side_columns(self.key)) - - @property - def value_roles(self) -> tuple[str, ...]: - """The roles the key determines; empty for a bare relation.""" - return tuple(role for role, _ in side_columns(self.values)) - - class SetSpec(_StrictSpec): """A named set: which combinations of the labels of its dimensions exist. @@ -418,8 +339,9 @@ class DimensionSpec(_StrictSpec): A dimension is an axis and nothing else: it declares that the axis exists and what its coordinates are typed as, never which coordinates there are — those are data, and arrive when the data is attached. The maps its members carry — a - generator's bus, a snapshot's period — are top-level ``relations:`` - ([`RelationSpec`][]), keyed by their own name. + generator's bus, a snapshot's period — are its ``properties:`` + ([`PropertySpec`][]), and the combinations of its labels with another + dimension's that exist are ``sets:`` ([`SetSpec`][]). """ _label: ClassVar[str] = 'a dimension entry' @@ -1173,7 +1095,7 @@ class Spec(_StrictSpec): [`LanguageError`][mathspec.errors.LanguageError] on a spec the language refuses. Holding one is the proof, so nothing downstream checks it again. - The API is the fifteen sections plus ``version`` and + The API is the fourteen sections plus ``version`` and ``description``, three ways back out — [`to_dict`][] for the spec as data, [`to_yaml`][] for the file a reviewer reads, [`expand`][] for the spec with its formulations written out as plain rows — and [`program`][], the @@ -1194,7 +1116,6 @@ class is pydantic's, not a contract this package keeps. #: What this file reads and does not build ([`GivenSpec`][]). Empty in a file that stands alone. given: GivenSpec = GivenSpec() dimensions: dict[str, DimensionSpec] = {} - relations: dict[str, RelationSpec] = {} sets: dict[str, SetSpec] = {} #: subject -> property name -> the property, where the subject is a #: dimension or a set. diff --git a/tests/fixtures.py b/tests/fixtures.py index 0f95c104e..cf35bb37c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -47,13 +47,13 @@ 'objective': {'sense': 'minimize', 'expression': 'sum(p * cost)'}, } -#: Two dimensions, a relation between them, a numeric, a scalar, a boolean and a +#: Two dimensions, a property of one into the other, a numeric, a scalar, a boolean and a #: label parameter, a variable on each frame — one entry of every kind #: a rule can name, and no objective, so a test adds what it judges. `p` and `r` #: share no dimension, which is what a rule about *different* dims needs. SMALL_MODEL: dict[str, Any] = { 'dimensions': {'g': {'dtype': 'str', 'ordered': True}, 'h': {'dtype': 'str', 'ordered': True}}, - 'relations': {'lk': {'key': 'g', 'values': 'h'}}, + 'properties': {'g': {'h': 'h'}}, 'parameters': { 'c': {'dims': ['g']}, 'k': {'dims': []}, diff --git a/tests/fixtures/every_program_node.yaml b/tests/fixtures/every_program_node.yaml index 711da00c0..07cc94e80 100644 --- a/tests/fixtures/every_program_node.yaml +++ b/tests/fixtures/every_program_node.yaml @@ -7,8 +7,10 @@ dimensions: t: { dtype: int, ordered: true } g: { dtype: str } zone: { dtype: str } -relations: - zone_of: { key: g, values: zone } +properties: + g: + zone: zone + parameters: cost: { dims: [g] } load: { dims: [zone] } @@ -37,10 +39,10 @@ constraints: expression: "sum(p) >= 0" grouped: dims: [t, zone] - expression: "sum(p, over=g, by=zone_of[zone]) - q <= load" + expression: "sum(p, over=g, by=g[zone]) - q <= load" looked_up: dims: [t, g] - expression: "p - at(q, by=zone_of[zone]) <= 0" + expression: "p - at(q, by=g[zone]) <= 0" translated: dims: [t, g] expression: "p - shift(p, along=t, offset=lead, edge='wrap') <= 0" diff --git a/tests/fixtures/pypsa_standard_shape.yaml b/tests/fixtures/pypsa_standard_shape.yaml index 60796da35..555296b6a 100644 --- a/tests/fixtures/pypsa_standard_shape.yaml +++ b/tests/fixtures/pypsa_standard_shape.yaml @@ -949,20 +949,20 @@ parameters: snapshot_weightings_stores: - snapshot transmission_losses: [] -relations: - - Generator_bus - - Line_bus0 - - Line_bus1 - - Link_bus0 - - Link_output_bus - - Link_output_link - - Load_bus - - Process_output_bus - - Process_output_process - - StorageUnit_bus - - Store_bus - - Transformer_bus0 - - Transformer_bus1 +properties: + - generator.bus + - line.bus0 + - line.bus1 + - link.bus0 + - link_output.bus + - link_output.link + - load.bus + - process_output.bus + - process_output.process + - storage_unit.bus + - store.bus + - transformer.bus0 + - transformer.bus1 variables: Generator_n_mod: - generator diff --git a/tests/test_advice.py b/tests/test_advice.py index 5a9107c10..d15b822d0 100644 --- a/tests/test_advice.py +++ b/tests/test_advice.py @@ -23,15 +23,15 @@ EXAMPLES = Path(__file__).resolve().parents[1] / 'examples' -#: ``h`` is the target of ``lk`` and nothing else reaches it; ``g`` is an axis. +#: ``h`` is what ``g[h]`` maps into and nothing else reaches it; ``g`` is an axis. TARGET_ONLY = varied( SMALL_MODEL, variables={'p': {'dims': ['g']}}, objective={'sense': 'minimize', 'expression': 'sum(p * c)'}, ) -#: The same with the relation gone, so nothing reaches ``h`` at all. -UNREACHED = varied(TARGET_ONLY, relations={}) +#: The same with the property gone, so nothing reaches ``h`` at all. +UNREACHED = varied(TARGET_ONLY, properties={}) #: A curve on ``p``, so the program of the file as written carries a piecewise entry and #: the program of its expansion carries the rows. The objective drives ``p`` @@ -55,9 +55,9 @@ def test_a_dimension_nothing_reaches_is_named(): @pytest.mark.parametrize( 'patch', [ - pytest.param({}, id='targeted-by-a-relation'), + pytest.param({}, id='mapped-into-by-a-property'), pytest.param( - {'constraints': {'cap': {'dims': ['h'], 'expression': 'sum(p, over=g, by=lk[h]) <= k'}}}, + {'constraints': {'cap': {'dims': ['h'], 'expression': 'sum(p, over=g, by=g[h]) <= k'}}}, id='grouping-into-it', ), pytest.param({'variables.r': {'dims': ['h']}}, id='indexing-by-it'), @@ -65,7 +65,7 @@ def test_a_dimension_nothing_reaches_is_named(): ) def test_a_dimension_something_reaches_is_in_use(patch): assert not advice(varied(TARGET_ONLY, **patch)), ( - 'a dimension a relation targets, an entry indexes or a grouping groups by is in use' + 'a dimension a property maps into, an entry indexes or a grouping groups by is in use' ) diff --git a/tests/test_boundedness.py b/tests/test_boundedness.py index 19e9a57f8..1ffe2ef8c 100644 --- a/tests/test_boundedness.py +++ b/tests/test_boundedness.py @@ -136,8 +136,8 @@ def test_nothing_is_claimed_where_the_file_does_not_decide_it(patch): '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)'}, - # `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[h]), over=g)'}, + # `at` reads onto the property's subject, so the variable it drives is on `h` + 'at': {'variables.u': {'dims': ['h']}, 'objective.expression': 'sum(at(u, by=g[h]), over=g)'}, } #: `dual` is refused in any objective, and boundedness walks the objective — diff --git a/tests/test_canonical.py b/tests/test_canonical.py index af26baf75..ecb582da5 100644 --- a/tests/test_canonical.py +++ b/tests/test_canonical.py @@ -139,7 +139,8 @@ def test_the_entries_of_a_section_are_sorted_by_name(): 'description', 'given', 'dimensions', - 'relations', + 'sets', + 'properties', 'parameters', 'conditions', 'variables', diff --git a/tests/test_composition.py b/tests/test_composition.py index e884871b5..d4378fd1f 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -36,23 +36,21 @@ #: each is a spec on its own as well as a piece of the composition. SURFACE = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'bus': {'dtype': 'str'}}, - 'relations': {'port_bus': {'key': 'port', 'values': 'bus'}}, + 'properties': {'port': {'bus': 'bus'}}, 'variables': {'flow': {'dims': ['snapshot', 'port']}}, - 'constraints': { - 'balance': {'dims': ['snapshot', 'bus'], 'expression': 'sum(flow, over=port, by=port_bus[bus]) == 0'} - }, + 'constraints': {'balance': {'dims': ['snapshot', 'bus'], 'expression': 'sum(flow, over=port, by=port[bus]) == 0'}}, } SUPPLY = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'generator': {'dtype': 'str'}}, - 'relations': {'gen_port': {'key': 'generator', 'values': 'port'}}, + 'properties': {'generator': {'port': 'port'}}, 'given': {'variables': {'flow': {'dims': ['snapshot', 'port']}}}, 'parameters': {'gen_cost': {'dims': ['generator']}, 'gen_p_max': {'dims': ['generator']}}, 'variables': {'gen_p': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0, 'upper': 'gen_p_max'}}}, 'constraints': { 'gen_injects': { 'dims': ['snapshot', 'generator'], - 'expression': 'at(flow, by=gen_port[port]) == gen_p', + 'expression': 'at(flow, by=generator[port]) == gen_p', } }, 'objective': {'sense': 'minimize', 'expression': 'sum(gen_p * gen_cost)'}, @@ -60,13 +58,13 @@ DEMAND = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'demand': {'dtype': 'str'}}, - 'relations': {'dem_port': {'key': 'demand', 'values': 'port'}}, + 'properties': {'demand': {'port': 'port'}}, 'given': {'variables': {'flow': {'dims': ['snapshot', 'port']}}}, 'parameters': {'dem_load': {'dims': ['snapshot', 'demand']}}, 'constraints': { 'dem_withdraws': { 'dims': ['snapshot', 'demand'], - 'expression': 'at(flow, by=dem_port[port]) == -dem_load', + 'expression': 'at(flow, by=demand[port]) == -dem_load', } }, } @@ -119,14 +117,14 @@ def test_the_balance_does_not_grow_when_a_component_type_is_added(): three = merge(LIBRARY).constraints['balance'].expression storage = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'store': {'dtype': 'str'}}, - 'relations': {'st_port': {'key': 'store', 'values': 'port'}}, + 'properties': {'store': {'port': 'port'}}, 'given': {'variables': {'flow': {'dims': ['snapshot', 'port']}}}, 'parameters': {'st_capacity': {'dims': ['store']}}, 'variables': {'st_p': {'dims': ['snapshot', 'store'], 'bounds': {'lower': 0, 'upper': 'st_capacity'}}}, 'constraints': { 'st_injects': { 'dims': ['snapshot', 'store'], - 'expression': 'at(flow, by=st_port[port]) == st_p', + 'expression': 'at(flow, by=store[port]) == st_p', } }, } @@ -225,9 +223,9 @@ def test_a_dimension_one_fragment_declares_ordered_is_ordered_in_the_composition def _mapped(missing: str | None) -> dict[str, object]: - """A fragment that declares only `port_bus`, with *missing* written, or no `missing:` where it is ``None``.""" - relation = {'key': 'port', 'values': 'bus'} | ({} if missing is None else {'missing': missing}) - return {'dimensions': {'port': {'dtype': 'str'}, 'bus': {'dtype': 'str'}}, 'relations': {'port_bus': relation}} + """A fragment that declares only `port[bus]`, with *missing* written, or no `missing:` where it is ``None``.""" + bus = {'into': 'bus'} | ({} if missing is None else {'missing': missing}) + return {'dimensions': {'port': {'dtype': 'str'}, 'bus': {'dtype': 'str'}}, 'properties': {'port': {'bus': bus}}} @pytest.mark.parametrize( @@ -235,13 +233,15 @@ def _mapped(missing: str | None) -> dict[str, object]: [ pytest.param([SURFACE, _mapped('refused')], 'refused', id='the-default-written-out'), pytest.param( - [varied(SURFACE, **{'relations.port_bus.missing': 'absent'}), _mapped('absent')], 'absent', id='both-absent' + [varied(SURFACE, **{'properties.port.bus': {'into': 'bus', 'missing': 'absent'}}), _mapped('absent')], + 'absent', + id='both-absent', ), ], ) def test_two_fragments_that_say_one_missing_agree(fragments, missing): """A written `missing: refused` was refused against an omitted one, as two fragments saying different things.""" - assert merge(fragments).relations['port_bus'].missing == missing + assert merge(fragments).properties['port']['bus'].missing == missing @pytest.mark.parametrize( @@ -253,7 +253,7 @@ def test_two_fragments_that_say_one_missing_agree(fragments, missing): ) def test_two_fragments_that_read_a_missing_key_apart_are_refused(fragments): """Neither reading settles the other: a key left out is refused under one, and belongs to no group under the other.""" - with pytest.raises(LanguageError, match=r"say different things about the relation 'port_bus'") as raised: + with pytest.raises(LanguageError, match=r"say different things about the property 'port.bus'") as raised: merge(fragments) assert 'Make the two identical' in str(raised.value), 'the refusal names the rewrite' @@ -600,24 +600,24 @@ def test_a_patch_that_rewrites_a_dimension_is_refused(patch, says): assert says in str(raised.value), 'the refusal names the rewrite rather than only what is wrong' -#: `SURFACE` with `port_bus` declared `missing: absent`. -ABSENT_SURFACE = varied(SURFACE, **{'relations.port_bus.missing': 'absent'}) +#: `SURFACE` with `port[bus]` declared `missing: absent`. +ABSENT_SURFACE = varied(SURFACE, **{'properties.port.bus': {'into': 'bus', 'missing': 'absent'}}) @pytest.mark.parametrize( - ('base', 'relation', 'missing'), + ('base', 'written', 'missing'), [ pytest.param(SURFACE, {'missing': 'absent'}, 'absent', id='the-reading-alone'), - pytest.param(SURFACE, {'key': 'port', 'values': 'bus', 'missing': 'absent'}, 'absent', id='restated-absent'), + pytest.param(SURFACE, {'into': 'bus', 'missing': 'absent'}, 'absent', id='restated-absent'), pytest.param(ABSENT_SURFACE, {'missing': 'refused'}, 'refused', id='back-to-the-default'), pytest.param(ABSENT_SURFACE, {'missing': None}, 'refused', id='null-puts-back-the-default'), - pytest.param(SURFACE, {'key': 'port', 'values': 'bus', 'missing': 'refused'}, 'refused', id='default-written'), - pytest.param(ABSENT_SURFACE, {'key': 'port', 'values': 'bus'}, 'absent', id='restated-without-one'), + pytest.param(SURFACE, {'into': 'bus', 'missing': 'refused'}, 'refused', id='default-written'), + pytest.param(ABSENT_SURFACE, {'into': 'bus'}, 'absent', id='restated-without-one'), ], ) -def test_a_patch_changes_what_a_key_the_map_leaves_out_means(base, relation, missing): +def test_a_patch_changes_what_a_key_the_map_leaves_out_means(base, written, missing): """`missing:` is a claim about the data, not the coordinate space, and a patch that wrote it was refused as one.""" - assert override(base, [{'relations': {'port_bus': relation}}]).relations['port_bus'].missing == missing + assert override(base, [{'properties': {'port': {'bus': written}}}]).properties['port']['bus'].missing == missing @pytest.mark.parametrize( @@ -640,7 +640,7 @@ def test_a_patch_changes_what_a_missing_row_of_a_parameter_means(base, written, ('section', 'name', 'declared'), [ pytest.param('parameters', 'c', {'dims': ['g']}, id='a-parameter'), - pytest.param('relations', 'lk', {'key': 'g', 'values': 'h'}, id='a-relation'), + pytest.param('sets', 'pair', {'dims': ['g', 'h']}, id='a-set'), ], ) def test_the_default_missing_written_out_is_the_spec_that_leaves_it_out(section, name, declared): diff --git a/tests/test_degree.py b/tests/test_degree.py index f17fbf447..956d53230 100644 --- a/tests/test_degree.py +++ b/tests/test_degree.py @@ -70,7 +70,7 @@ def test_the_affine_ceiling_refuses_and_names_the_rewrite(text, fragment): 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'), - pytest.param('at(r, by=lk[h]) * at(r, by=lk[h])', id='two-lookups-are-one-term-each'), + pytest.param('at(r, by=g[h]) * at(r, by=g[h])', id='two-lookups-are-one-term-each'), ], ) def test_the_objective_takes_degree_two(text): @@ -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, over=g, by=lk[h]) * sum(q, over=g)', 'outer product', id='a-grouped-sum-is-a-reduction'), + pytest.param('sum(p, over=g, by=g[h]) * sum(q, over=g)', 'outer product', id='a-grouped-sum-is-a-reduction'), 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'), ], diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index ad039784d..8c647b333 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -27,9 +27,7 @@ Expression, ExpressionReference, Join, - Mask, ParameterReference, - RelationPairComparison, Sum, Translate, VariableReference, @@ -46,7 +44,7 @@ #: `fixtures.DISPATCH_MODEL` plus buses: a dim rule is mostly about an #: expression carrying a dim its frame does not, which needs three dims to -#: state. `snap_bus` is over `snapshot` so it can partition the axis the +#: state. `snapshot[bus]` is over `snapshot` so it can partition the axis the #: translations step along; `spinup` and `horizon` are the named amount that #: obeys the position rules and the one that spans that axis; `bus_lead` is #: over a dim `p` does not carry, so it is readable only through a `by=`. @@ -57,13 +55,11 @@ 'bus': {'dtype': 'str'}, 'zone': {'dtype': 'str'}, }, - 'relations': { - 'gen_bus': {'key': 'generator', 'values': 'bus'}, - 'snap_bus': {'key': 'snapshot', 'values': 'bus'}, - 'gen_zone': {'key': ['generator', 'snapshot'], 'values': 'zone'}, - 'rep_of': {'key': 'snapshot', 'values': {'rep': 'snapshot'}}, - 'gen_bz': {'key': 'generator', 'values': ['bus', 'zone']}, - 'pair': {'key': {'g': 'generator'}, 'values': {'b0': 'bus', 'b1': 'bus'}}, + 'sets': {'generator_snapshot': {'dims': ['generator', 'snapshot']}}, + 'properties': { + 'generator': {'bus': 'bus', 'zone': 'zone', 'b0': 'bus', 'b1': 'bus'}, + 'snapshot': {'bus': 'bus', 'rep': 'snapshot'}, + 'generator_snapshot': {'zone': 'zone'}, }, 'parameters': { 'p_max': {'dims': ['generator']}, @@ -80,7 +76,7 @@ 'constraints': { 'balance': { 'dims': ['snapshot', 'bus'], - 'expression': 'sum(p, over=generator, by=gen_bus[bus]) == load', + 'expression': 'sum(p, over=generator, by=generator[bus]) == load', } }, 'objective': {'sense': 'minimize', 'expression': 'sum(p * cost)'}, @@ -120,100 +116,100 @@ def namespace() -> Namespace: ('sum(p * cost, over=generator)', {'snapshot'}), pytest.param('sum(p, over=[snapshot, generator])', set(), id='a-list-sums-every-dim-it-names'), pytest.param('sum(p, over=[generator])', {'snapshot'}, id='a-list-of-one-is-that-dim'), - ('sum(p, over=generator, by=gen_bus[bus])', {'snapshot', 'bus'}), + ('sum(p, over=generator, by=generator[bus])', {'snapshot', 'bus'}), ("shift(p, along=snapshot, offset=1, edge='wrap')", {'snapshot', 'generator'}), ("shift(p, along=snapshot, offset=spinup, edge='wrap')", {'snapshot', 'generator'}), ('sum_back(p, along=snapshot, window=spinup)', {'snapshot', 'generator'}), pytest.param( - "shift(p, along=snapshot, offset=bus_lead, edge='wrap', within=snap_bus[bus])", + "shift(p, along=snapshot, offset=bus_lead, edge='wrap', within=snapshot[bus])", {'snapshot', 'generator'}, id='a-by-makes-an-offset-over-another-dim-readable-one-lag-per-group', ), pytest.param( - 'sum_back(p, along=snapshot, window=bus_lead, within=snap_bus[bus])', + 'sum_back(p, along=snapshot, window=bus_lead, within=snapshot[bus])', {'snapshot', 'generator'}, id='a-by-makes-a-width-over-another-dim-readable-one-window-per-group', ), pytest.param('p + 1', {'snapshot', 'generator'}, id='a-scalar-broadcasts'), pytest.param( - 'sum(p, over=generator, by=gen_zone[zone])', + 'sum(p, over=generator, by=generator_snapshot[zone])', {'snapshot', 'zone'}, - id='a-two-key-relation-sums-away-the-key-it-names-and-keeps-the-other', + id='a-property-of-a-set-sums-away-the-member-it-names-and-keeps-the-other', ), pytest.param( - 'sum(p, over=snapshot, by=gen_zone[zone])', + 'sum(p, over=snapshot, by=generator_snapshot[zone])', {'generator', 'zone'}, id='the-same-table-read-along-its-other-key', ), pytest.param( - 'at(zone_load, by=gen_zone[zone])', + 'at(zone_load, by=generator_snapshot[zone])', {'snapshot', 'generator'}, id='its-lookup-keeps-the-joined-key-too', ), pytest.param( - "shift(p, along=generator, offset=1, edge='wrap', within=gen_zone[zone])", + "shift(p, along=generator, offset=1, edge='wrap', within=generator_snapshot[zone])", {'snapshot', 'generator'}, id='a-partition-along-one-key-joined-on-the-other', ), pytest.param( - "shift(p, along=generator, offset=1, edge='wrap', within=gen_bz[bus])", + "shift(p, along=generator, offset=1, edge='wrap', within=generator[bus])", {'snapshot', 'generator'}, - id='a-partition-grouped-by-one-value-column-of-a-two-value-table', + id='a-partition-grouped-by-one-of-two-properties', ), pytest.param( - 'sum_back(p, along=generator, window=2, within=gen_bz[bus, zone])', + 'sum_back(p, along=generator, window=2, within=generator[bus, zone])', {'snapshot', 'generator'}, - id='a-window-grouped-by-both-value-columns-named', + id='a-window-grouped-by-both-properties-named', ), pytest.param( - "shift(p, along=generator, offset=1, edge='wrap', within=pair[b0, b1])", + "shift(p, along=generator, offset=1, edge='wrap', within=generator[b0, b1])", {'snapshot', 'generator'}, - id='a-partition-grouped-by-two-columns-over-one-dimension-adds-nothing', + id='a-partition-grouped-by-two-properties-into-one-dimension-adds-nothing', ), pytest.param( - 'sum(p, over=generator, by=gen_bus[bus])', + 'sum(p, over=generator, by=generator[bus])', {'snapshot', 'bus'}, - id='the-dot-is-legal-on-a-one-key-relation', + id='a-property-of-a-dimension', ), pytest.param( - 'sum(p, over=generator, by=gen_bz[bus, zone])', + 'sum(p, over=generator, by=generator[bus, zone])', {'snapshot', 'bus', 'zone'}, - id='a-to-list-lands-on-a-product-from-one-table', + id='a-to-list-lands-on-a-product-from-one-subject', ), pytest.param( - 'at(bz, by=gen_bz[bus, zone])', + 'at(bz, by=generator[bus, zone])', {'generator'}, - id='a-from-list-reads-two-value-columns-at-once', + id='a-from-list-reads-two-properties-at-once', ), pytest.param( - 'sum(p, over=[generator, snapshot], by=gen_zone[zone])', + 'sum(p, over=[generator, snapshot], by=generator_snapshot[zone])', {'zone'}, - id='an-over-list-joins-on-two-key-columns-at-once', + id='an-over-list-joins-on-two-members-at-once', ), pytest.param( - 'sum(p, over=generator, by=gen_bz[bus])', + 'sum(p, over=generator, by=generator[bus])', {'snapshot', 'bus'}, - id='a-value-column-not-named-is-not-read', + id='a-property-not-named-is-not-read', ), pytest.param( - 'sum(p, over=generator, by=gen_bz[bus])', + 'sum(p, over=generator, by=generator[bus])', {'snapshot', 'bus'}, id='by-and-over-compose', ), pytest.param( - 'sum(p, over=snapshot, by=rep_of[rep])', + 'sum(p, over=snapshot, by=snapshot[rep])', {'snapshot', 'generator'}, id='a-map-into-its-own-dimension-keeps-the-frame', ), - pytest.param('at(p, by=rep_of[rep])', {'snapshot', 'generator'}, id='and-so-does-its-lookup'), + pytest.param('at(p, by=snapshot[rep])', {'snapshot', 'generator'}, id='and-so-does-its-lookup'), pytest.param('sum(p, over=[generator, snapshot])', set(), id='a-plain-sum-over-a-list'), pytest.param( - 'sum(p, over=[generator, snapshot], by=gen_bus[bus])', + 'sum(p, over=[generator, snapshot], by=generator[bus])', {'bus'}, - id='a-dim-the-relation-has-no-column-over-is-summed-after-the-group-by', + id='a-dim-the-subject-has-no-column-over-is-summed-after-the-group-by', ), pytest.param( - "shift(p, along=snapshot, offset=1, edge='wrap', within=rep_of[rep])", + "shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[rep])", {'snapshot', 'generator'}, id='a-partition-into-its-own-dimension', ), @@ -234,7 +230,7 @@ def _dims_with(expr: str, **overrides) -> frozenset[str]: pytest.param( 'sum(p, over=generator, by=connection[bus])', {'snapshot', 'bus'}, - id='a-sum-through-a-bare-relation-groups-by-a-key-column', + id='a-sum-through-a-set-groups-by-a-member', ), pytest.param( 'sum(load, over=bus, by=connection[generator])', @@ -243,23 +239,23 @@ def _dims_with(expr: str, **overrides) -> frozenset[str]: ), ], ) -def test_a_bare_relation_is_summed_between_its_key_columns(expr, expected): - """A bare relation holds no value column, so the column a sum groups by is a key column.""" - assert _dims_with(expr, **{'relations.connection': {'key': ['generator', 'bus']}}) == expected +def test_a_set_is_summed_between_its_members(expr, expected): + """A set holds no property here, so the column a sum groups by is a member.""" + assert _dims_with(expr, **{'sets.connection': {'dims': ['generator', 'bus']}}) == expected -def test_a_value_column_added_to_a_relation_changes_no_call(): - """A name in `over=` is a column of the relation, so `rep_of` gaining `alt` over `snapshot` leaves every call as it was. +def test_a_property_added_to_a_subject_changes_no_call(): + """A name in `over=` is a column of the subject, so `snapshot` gaining `alt` into `snapshot` leaves every call as it was. `over=snapshot` read as a dimension would match two columns once `alt` arrives, and the call that loaded would stop loading. Read as a column, it is the key column either way, and the new column is named where it is meant. """ - wider = {'relations.rep_of': {'key': 'snapshot', 'values': {'rep': 'snapshot', 'alt': 'snapshot'}}} - before = _dims('sum(p, over=snapshot, by=rep_of[rep])') - assert _dims_with('sum(p, over=snapshot, by=rep_of[rep])', **wider) == before == {'snapshot', 'generator'} - assert _dims_with('sum(p, over=snapshot, by=rep_of[alt])', **wider) == before, ( + wider = {'properties.snapshot.alt': 'snapshot'} + before = _dims('sum(p, over=snapshot, by=snapshot[rep])') + assert _dims_with('sum(p, over=snapshot, by=snapshot[rep])', **wider) == before == {'snapshot', 'generator'} + assert _dims_with('sum(p, over=snapshot, by=snapshot[alt])', **wider) == before, ( 'the new column is grouped by where it is named' ) @@ -267,30 +263,32 @@ def test_a_value_column_added_to_a_relation_changes_no_call(): def test_a_lookup_carries_the_whole_key_and_what_the_operand_brings_beside_it(): """A lookup groups by the key however the call splits it, and a dim the operand carries and the lookup does not join on rides along. - `gen_bz` is keyed by `generator` alone, so the whole key is grouped by; the + `generator[zone]` is keyed by `generator` alone, so the whole key is grouped by; the operand's `snapshot` is neither joined on nor part of the key, and the result keeps it. """ - assert _dims('at(zone_load, by=gen_bz[zone])') == {'generator', 'snapshot'} + assert _dims('at(zone_load, by=generator[zone])') == {'generator', 'snapshot'} def test_a_join_opens_an_axis_for_the_column_it_drops_and_the_sum_over_it_closes_it(): """A map into its own dimension drops and adds one dimension, so the axis the join opens stands for the column. The dropped column's axis runs over `snapshot` and stands for - `rep_of[snapshot]`, so it is not the dimension's own axis. The sum over + `snapshot[rep]`'s key column `snapshot`, so it is not the dimension's own axis. The sum over the join closes it, and the frame keeps the `snapshot` the row groups by. """ s = _spec() - node = expression_of('sum(p, over=snapshot, by=rep_of[rep])', Namespace(s), 't') + node = expression_of('sum(p, over=snapshot, by=snapshot[rep])', Namespace(s), 't') assert isinstance(node, Sum) and isinstance(node.operand, Join) - assert node.over == (Axis('snapshot', Column('rep_of', 'snapshot')),), 'the sum stands over the axis the join opens' + assert node.over == (Axis('snapshot', Column('snapshot.rep', 'snapshot')),), ( + 'the sum stands over the axis the join opens' + ) assert set(node.dims) == {'generator', 'snapshot'}, 'and the sum over it leaves the frame the row keeps' def test_a_sum_joins_on_a_key_column_and_a_value_column_together(): """The columns a sum joins on and sums away are not one kind: it needs one key column, and may name a value column beside it.""" - assert _dims('sum(p * load, over=[generator, bus], by=gen_bz[zone])') == {'snapshot', 'zone'} + assert _dims('sum(p * load, over=[generator, bus], by=generator[zone])') == {'snapshot', 'zone'} def test_a_dual_carries_the_constraints_own_frame(): @@ -340,9 +338,9 @@ def test_a_bare_name_reaches_the_variable_a_dual_the_same_named_constraint(): id='and-the-same-fault-under-a-sum-over-a-named-dim', ), pytest.param( - 'sum(load, over=generator, by=gen_bus[bus])', + 'sum(load, over=generator, by=generator[bus])', DimensionError, - r"sum\(by=gen_bus\[bus\]\) joins on \['generator'\] to sum it away", + r"sum\(by=generator\[bus\]\) joins on \['generator'\] to sum it away", id='sum-requires-the-grouped-dim', ), pytest.param( @@ -394,15 +392,15 @@ 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, over=generator, by=gen_zone[zone])', + 'sum(cost, over=generator, by=generator_snapshot[zone])', DimensionError, - r"sum\(by=gen_zone\[zone\]\) joins on \['snapshot'\]", + r"sum\(by=generator_snapshot\[zone\]\) joins on \['snapshot'\]", id='a-grouped-sum-needs-the-keys-it-joins-on', ), pytest.param( - "shift(cost, along=generator, offset=1, edge='wrap', within=gen_zone[zone])", + "shift(cost, along=generator, offset=1, edge='wrap', within=generator_snapshot[zone])", DimensionError, - r"within=gen_zone\[zone\]\) joins on \['snapshot'\]", + r"within=generator_snapshot\[zone\]\) joins on \['snapshot'\]", id='a-partition-needs-the-keys-it-joins-on', ), ], @@ -423,7 +421,7 @@ def test_an_ill_dimensioned_expression_is_rejected(expr, error, match): [ pytest.param( 'sum(p, over=k, by=diag[z])', - {'key': {'k': 'generator', 'j': 'generator', 'z': 'zone'}}, + {'dims': {'k': 'generator', 'j': 'generator', 'z': 'zone'}}, id='a-sum-summing-away-a-column-over-a-dimension-it-also-joins-on', ), ], @@ -431,27 +429,27 @@ def test_an_ill_dimensioned_expression_is_rejected(expr, error, match): def test_two_joined_columns_do_not_share_a_dimension(expr, diag): """The operand carries one coordinate per dimension, so the `over=` column and an unnamed key column cannot share one.""" with pytest.raises(DimensionError, match=r"joins 'diag' on \[.*\] through more than one column"): - _dims_with(expr, **{'relations.diag': diag}) + _dims_with(expr, **{'sets.diag': diag}) @pytest.mark.parametrize( - ('expr', 'relation', 'expected'), + ('expr', 'patch', 'expected'), [ pytest.param( - 'at(zone_cap, by=gen_zone[zone])', - {'key': ['generator', 'snapshot'], 'values': 'zone'}, + 'at(zone_cap, by=generator_snapshot[zone])', + {}, {'generator', 'snapshot'}, - id='a-key-column-the-operand-lacks-arrives', + id='a-member-the-operand-lacks-arrives', ), pytest.param( - 'at(load, by=gen_zone[rep])', - {'key': ['snapshot', 'generator'], 'values': {'rep': 'snapshot'}}, + 'at(load, by=generator_snapshot[rep])', + {'properties.generator_snapshot.rep': 'snapshot'}, {'bus', 'snapshot', 'generator'}, - id='a-key-column-over-the-dimension-the-read-column-matches-arrives', + id='a-member-over-the-dimension-the-read-property-matches-arrives', ), ], ) -def test_a_lookup_joins_on_the_key_columns_the_operand_carries_and_the_read_does_not_match(expr, relation, expected): +def test_a_lookup_joins_on_the_members_the_operand_carries_and_the_read_does_not_match(expr, patch, expected): """`at` joins on each key column over a dim the operand carries, unless the column it reads already matches that dim. `at(load, by=diag, over=rep, into=generator)` once joined on the key's @@ -459,9 +457,7 @@ def test_a_lookup_joins_on_the_key_columns_the_operand_carries_and_the_read_does call names only the column it reads now, so the key's `snapshot` arrives and the result is `load` read at `rep(t, g)` for every `(t, g)`. """ - assert _dims_with(expr, **{'relations.gen_zone': relation}) == frozenset(expected), ( - 'the frame is the operand less the dim read, plus the key' - ) + assert _dims_with(expr, **patch) == frozenset(expected), 'the frame is the operand less the dim read, plus the key' def test_an_outer_product_is_legal_and_carries_both_dim_sets(): @@ -598,22 +594,24 @@ def test_a_zero_step_vacates_nothing_and_needs_no_edge(self): pytest.param('p_max > 0', {'generator'}, id='a-parameter-through-its-own-dims'), pytest.param('snapshot == 0', {'snapshot'}, id='a-dimension-through-itself'), pytest.param('position(snapshot) == 0', {'snapshot'}, id='a-position-through-the-axis-it-counts'), - pytest.param('snap_bus == "b1"', {'snapshot'}, id='a-relation-through-the-dim-it-maps-out-of'), - pytest.param('gen_zone == "z1"', {'generator', 'snapshot'}, id='a-two-key-relation-through-both-keys'), - pytest.param('gen_zone', {'generator', 'snapshot'}, id='a-bare-two-key-relation-the-same'), - pytest.param('rep_of == 3', {'snapshot'}, id='a-map-into-its-own-dimension-through-its-key'), + pytest.param('snapshot[bus] == "b1"', {'snapshot'}, id='a-property-through-its-subject'), pytest.param( - 'position(snapshot, within=rep_of[rep]) == 0', {'snapshot'}, id='a-position-within-a-representative' + 'generator_snapshot[zone] == "z1"', {'generator', 'snapshot'}, id='a-property-of-a-set-through-both-members' ), + pytest.param('generator_snapshot', {'generator', 'snapshot'}, id='a-bare-set-the-same'), + pytest.param('snapshot[rep] == 3', {'snapshot'}, id='a-property-into-its-own-dimension-through-its-subject'), pytest.param( - 'position(generator, within=gen_zone[zone]) == 0', + 'position(snapshot, within=snapshot[rep]) == 0', {'snapshot'}, id='a-position-within-a-representative' + ), + pytest.param( + 'position(generator, within=generator_snapshot[zone]) == 0', {'generator', 'snapshot'}, - id='a-position-within-a-group-of-a-two-key-relation-reads-both-keys', + id='a-position-within-a-group-of-a-property-of-a-set-reads-both-members', ), pytest.param( - 'position(generator, within=gen_bz[zone]) == 0', + 'position(generator, within=generator[zone]) == 0', {'generator'}, - id='a-position-within-one-named-value-column-reads-the-key', + id='a-position-within-one-named-property-reads-the-subject', ), pytest.param('p_max > 0 AND snapshot == 0', {'generator', 'snapshot'}, id='a-conjunction-reads-both-sides'), pytest.param('NOT p_max > 0', {'generator'}, id='a-negation-reads-what-it-negates'), @@ -655,12 +653,14 @@ def test_the_frame_check_and_the_reading_walk_the_same_leaves(namespace): pytest.param('p_max > 0', {'p_max'}, id='a-parameter-comparison-names-the-parameter'), pytest.param('cost', {'cost'}, id='a-parameter-bare-names-the-parameter'), pytest.param('p', {'p'}, id='a-variable-bare-names-the-variable'), - pytest.param('snap_bus == "b1"', {'snap_bus'}, id='a-relation-comparison-names-the-relation'), - pytest.param('gen_bus', {'gen_bus'}, id='a-relation-bare-names-the-relation'), + pytest.param('snapshot[bus] == "b1"', {'snapshot.bus'}, id='a-property-comparison-names-its-table'), + pytest.param('generator[bus]', {'generator.bus'}, id='a-property-alone-names-its-table'), pytest.param('snapshot == 0', set(), id='a-dimension-names-nothing-it-is-a-coordinate'), pytest.param('position(snapshot) == 0', set(), id='a-position-names-nothing'), pytest.param('p_max > 0 AND snapshot == 0', {'p_max'}, id='a-conjunction-drops-the-dimension-side'), - pytest.param('p_max > 0 AND snap_bus == "b1"', {'p_max', 'snap_bus'}, id='a-conjunction-unions-both-names'), + pytest.param( + 'p_max > 0 AND snapshot[bus] == "b1"', {'p_max', 'snapshot.bus'}, id='a-conjunction-unions-both-names' + ), pytest.param('NOT p_max > 0', {'p_max'}, id='a-negation-names-what-it-negates'), pytest.param('False', set(), id='a-literal-names-nothing'), ], @@ -673,15 +673,11 @@ def test_a_condition_names_the_entries_its_leaves_test(namespace, condition, exp assert where.names_read == expected -def test_names_read_takes_both_sides_of_a_relation_pair(): - """The one leaf that names two entries — two maps compared on the dimension they share. - - BASE has one relation per dimension, so the pair is built directly rather than - resolved from a condition string. - """ - where = RelationPairComparison('from_bus', 'bus', 'to_bus', 'bus', '!=', ('line',)) - - assert Mask(where).names_read == {'from_bus', 'to_bus'}, 'a relation pair names both maps it compares' +def test_names_read_takes_both_sides_of_a_pair(): + """The one leaf that names two entries — two properties compared on the dimension they share.""" + where = where_of('generator[b0] != generator[b1]', Namespace(_spec()), 'test') + assert where is not None + assert where.names_read == {'generator.b0', 'generator.b1'}, 'a pair names both tables it compares' # --------------------------------------------------------------------------- @@ -846,7 +842,7 @@ def test_every_expression_node_states_its_dims_sorted_by_name(path): { 'constraints.read': { 'dims': ['generator'], - 'where': 'at(cost > 0, by=gen_bus[bus])', + 'where': 'at(cost > 0, by=generator[bus])', 'expression': 'sum(p, over=snapshot) <= 1', } }, diff --git a/tests/test_exclusivity.py b/tests/test_exclusivity.py index ceb17edcf..dd7ea9d19 100644 --- a/tests/test_exclusivity.py +++ b/tests/test_exclusivity.py @@ -36,7 +36,7 @@ 'storage': {'ordered': True}, 'period': {'dtype': 'int'}, }, - 'relations': {'period_of': {'key': 'snapshot', 'values': 'period'}}, + 'properties': {'snapshot': {'period': 'period'}}, 'parameters': { 'cyclic': {'dims': ['storage'], 'dtype': 'bool'}, 'committable': {'dims': ['storage'], 'dtype': 'bool'}, @@ -194,11 +194,11 @@ def test_both_ends_of_one_axis(self, spec: Spec): def test_a_group_is_named_as_the_group_it_is(self, spec: Spec): """`within=` counts within each group, and the refusal says which.""" cases = { - 'first': 'position(snapshot, within=period_of[period]) == 0', - 'last': 'position(snapshot, within=period_of[period]) == -1', + 'first': 'position(snapshot, within=snapshot[period]) == 0', + 'last': 'position(snapshot, within=snapshot[period]) == -1', } [refusal] = refusals(spec, cases) - assert 'within each period_of group' in refusal + assert 'within each snapshot[period] group' in refusal def test_a_literal_written_first_is_named_as_the_order_it_is(self, spec: Spec): """The advice was to do what the author had already done. diff --git a/tests/test_expansion.py b/tests/test_expansion.py index 4d9a73305..84e6e1a9f 100644 --- a/tests/test_expansion.py +++ b/tests/test_expansion.py @@ -283,18 +283,19 @@ def test_macro_collisions_rejected(patch, match): ), pytest.param( {'grouped': {'args': ['x'], 'template': 'sum(x, over=g, by=nope[h])'}}, - r"Macro 'grouped'.*sum\(by=nope\[\.\.\.\]\) does not name a relation", - id='a-typo-in-a-relation-kwarg', + r"Macro 'grouped'.*sum\(by=nope\[\.\.\.\]\) does not name a set or a dimension", + id='a-typo-in-a-subject-kwarg', ), pytest.param( {'grouped': {'args': ['x'], 'template': 'sum(x, over=g, by=[nope, also])'}}, - r"Macro 'grouped'.*sum\(by=\.\.\.\) takes columns of one relation. Write relation\[column\]", + r"Macro 'grouped'.*sum\(by=\.\.\.\) takes what one subject reads. Write subject\[property\]", id='a-list-of-names-for-columns', ), pytest.param( {'grouped': {'args': ['x', 'a', 'b'], 'template': 'sum(x, over=a, by=nope[b])'}}, - r"Macro 'grouped'.*sum\(by=nope\[\.\.\.\]\) does not name a relation or a formal argument of this macro", - id='a-typo-in-a-relation-beside-formal-columns', + r"Macro 'grouped'.*sum\(by=nope\[\.\.\.\]\) does not name a set or a dimension or a formal argument " + r'of this macro', + id='a-typo-in-a-subject-beside-formal-columns', ), pytest.param( {'reduced': {'args': ['x'], 'template': 'sum(x, over=nope)'}}, @@ -333,7 +334,7 @@ def test_an_entry_nothing_reads_is_held_to_the_rules_a_use_is(): [ pytest.param('x * tag', "Macro 'm': 'tag' is declared dtype: str", id='a-label-parameter-as-a-value'), pytest.param( - 'sum(x, over=g, by=lk[nope])', r"names \['nope'\], which is no column of 'lk'", id='a-typo-in-a-column' + 'sum(x, over=g, by=g[nope])', r"names \['nope'\], which is no property of 'g'", id='a-typo-in-a-column' ), ], ) @@ -362,21 +363,19 @@ def test_an_unknown_operator_names_no_construct_the_language_lacks(): [ pytest.param(['x', 'e'], 'shift(x, along=g, offset=1, edge=e)', id='an-edge'), pytest.param(['row'], 'dual(row)', id='a-constraint'), - pytest.param(['x', 'rel', 'a', 'b'], 'sum(x, over=a, by=rel[b])', id='a-relation-and-its-columns'), - pytest.param(['x', 'a', 'b'], 'sum(x, over=a, by=lk[b])', id='the-columns-of-a-declared-relation'), + pytest.param(['x', 'rel', 'a', 'b'], 'sum(x, over=a, by=rel[b])', id='a-subject-and-its-columns'), + pytest.param(['x', 'a', 'b'], 'sum(x, over=a, by=g[b])', id='the-columns-of-a-declared-subject'), pytest.param( - ['x', 'd'], 'shift(x, along=d, offset=1, within=lk[h])', id='the-dimension-a-partition-steps-along' - ), - pytest.param( - ['x', 'd'], 'sum_back(x, along=d, window=2, within=lk[h])', id='the-dimension-a-window-runs-along' + ['x', 'd'], 'shift(x, along=d, offset=1, within=g[h])', id='the-dimension-a-partition-steps-along' ), + pytest.param(['x', 'd'], 'sum_back(x, along=d, window=2, within=g[h])', id='the-dimension-a-window-runs-along'), ], ) def test_a_formal_stands_where_a_call_site_will_bind_it(formals, template): """A formal has no kind until a call binds it, so the template check leaves it bare in every slot. A formal `along=` beside a `within=` was handed to the partition as if it were - a dimension, and refused as one the relation has no key column over. + a dimension, and refused as one the subject has no member over. """ assert spec_of(SMALL_MODEL, macros={'m': {'args': formals, 'template': template}}).macros['m'].template == template @@ -401,11 +400,11 @@ def test_a_formal_inside_a_list_binds_a_name_or_a_list_of_names(call, match): def test_a_call_binding_the_dimension_a_partition_steps_along_builds_it(): """The call site is where the formal gets its kind, so the partition is built there.""" - template = 'shift(x, along=d, offset=1, within=lk[h])' + template = 'shift(x, along=d, offset=1, within=g[h])' ns = Namespace(spec_of(SMALL_MODEL, macros={'m': {'args': ['x', 'd'], 'template': template}})) node = expression_of('m(p, g)', ns, 'expression') assert isinstance(node, Translate) and node.along == 'g' - assert node.partition is not None and node.partition.name == 'lk', 'the relation is read once along= is bound' + assert node.partition is not None and node.partition.name == 'g.h', 'the property is read once along= is bound' def test_a_named_expression_is_resolved_once_however_many_uses(monkeypatch): @@ -437,7 +436,7 @@ def test_a_use_of_a_refused_named_expression_names_it_rather_than_repeating_its_ GROUPED_MODEL = { 'dimensions': {'generator': {'dtype': 'str'}, 'bus': {'dtype': 'str'}}, - 'relations': {'gen_bus': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'parameters': {'cap': {'dims': ['bus']}}, 'variables': {'p': {'dims': ['generator']}}, 'objective': {'sense': 'minimize', 'expression': 'sum(p)'}, @@ -447,11 +446,11 @@ def test_a_use_of_a_refused_named_expression_names_it_rather_than_repeating_its_ @pytest.mark.parametrize( ('template', 'call'), [ - pytest.param('sum(x, over=d, by=rel[col])', 'grouped(p, d=generator, rel=gen_bus, col=bus)', id='names'), - pytest.param('sum(x, over=d, by=cols)', 'grouped(p, d=generator, cols=gen_bus[bus])', id='a-selection'), + pytest.param('sum(x, over=d, by=rel[col])', 'grouped(p, d=generator, rel=generator, col=bus)', id='names'), + pytest.param('sum(x, over=d, by=cols)', 'grouped(p, d=generator, cols=generator[bus])', id='a-selection'), ], ) -def test_a_call_site_binds_the_relation_and_its_columns(template, call): +def test_a_call_site_binds_the_subject_and_its_columns(template, call): """A formal inside `rel[col]` takes the bare name the call passes, as a whole selection bound to one formal does.""" kwargs = [k for k in ('d', 'rel', 'col', 'cols') if f'{k}=' in call] model = GROUPED_MODEL | { @@ -459,7 +458,7 @@ def test_a_call_site_binds_the_relation_and_its_columns(template, call): 'constraints': {'k': {'dims': ['bus'], 'expression': f'{call} <= cap'}}, } written = GROUPED_MODEL | { - 'constraints': {'k': {'dims': ['bus'], 'expression': 'sum(p, over=generator, by=gen_bus[bus]) <= cap'}} + 'constraints': {'k': {'dims': ['bus'], 'expression': 'sum(p, over=generator, by=generator[bus]) <= cap'}} } assert to_spec(model).program.constraints['k'] == to_spec(written).program.constraints['k'], ( 'the macro expands to the call written out' @@ -470,10 +469,10 @@ def test_a_selection_bound_into_arithmetic_is_refused(): """A selection names columns, not values, so a formal bound to one cannot stand as a term.""" model = GROUPED_MODEL | { 'macros': {'plus': {'args': ['x'], 'kwargs': ['cols'], 'template': 'x + cols'}}, - 'constraints': {'k': {'dims': ['generator'], 'expression': 'plus(p, cols=gen_bus[bus]) <= 1'}}, + 'constraints': {'k': {'dims': ['generator'], 'expression': 'plus(p, cols=generator[bus]) <= 1'}}, } with pytest.raises( - LanguageError, match=r'gen_bus\[bus\] names columns of a relation, which is allowed only as an operator' + LanguageError, match=r'generator\[bus\] reads a set or a property, which is allowed only as an operator' ): to_spec(model) diff --git a/tests/test_given.py b/tests/test_given.py index 54eadd2f3..0d1b432e3 100644 --- a/tests/test_given.py +++ b/tests/test_given.py @@ -26,14 +26,14 @@ SUPPLY = { 'description': 'A fleet of generators, each on one port.', 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'generator': {'dtype': 'str'}}, - 'relations': {'gen_port': {'key': 'generator', 'values': 'port'}}, + 'properties': {'generator': {'port': 'port'}}, 'given': {'variables': {'flow': {'dims': ['snapshot', 'port'], 'description': 'what a port puts into its bus'}}}, 'parameters': {'gen_cost': {'dims': ['generator']}, 'gen_p_max': {'dims': ['generator']}}, 'variables': {'gen_p': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0, 'upper': 'gen_p_max'}}}, 'constraints': { 'gen_injects': { 'dims': ['snapshot', 'generator'], - 'expression': 'at(flow, by=gen_port[port]) == gen_p', + 'expression': 'at(flow, by=generator[port]) == gen_p', } }, 'objective': {'sense': 'minimize', 'expression': 'sum(gen_p * gen_cost)'}, @@ -42,11 +42,9 @@ #: The fragment that introduces `flow`, with the bounds and the balance that go with it. SURFACE = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'port': {'dtype': 'str'}, 'bus': {'dtype': 'str'}}, - 'relations': {'port_bus': {'key': 'port', 'values': 'bus'}}, + 'properties': {'port': {'bus': 'bus'}}, 'variables': {'flow': {'dims': ['snapshot', 'port'], 'bounds': {'lower': -1000, 'upper': 1000}}}, - 'constraints': { - 'balance': {'dims': ['snapshot', 'bus'], 'expression': 'sum(flow, over=port, by=port_bus[bus]) == 0'} - }, + 'constraints': {'balance': {'dims': ['snapshot', 'bus'], 'expression': 'sum(flow, over=port, by=port[bus]) == 0'}}, } @@ -482,10 +480,10 @@ def test_the_advice_names_a_given_parameter_as_data_a_consumer_binds(): #: A component file that defines the injection. INJECTOR = { 'dimensions': {'snapshot': {'dtype': 'int'}, 'bus': {'dtype': 'str'}, 'generator': {'dtype': 'str'}}, - 'relations': {'gen_bus': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'parameters': {'load': {'dims': ['snapshot', 'bus']}}, 'variables': {'gen_p': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0}}}, - 'expressions': {'injection': {'expression': 'sum(gen_p, over=generator, by=gen_bus[bus]) - load'}}, + 'expressions': {'injection': {'expression': 'sum(gen_p, over=generator, by=generator[bus]) - load'}}, 'objective': {'sense': 'minimize', 'expression': 'sum(gen_p)'}, } @@ -547,9 +545,9 @@ def test_a_definition_over_a_dimension_the_reader_does_not_state_is_refused(): #: An injector whose output does not vary by snapshot: its injection is over `bus` alone. FLAT_INJECTOR = { 'dimensions': {'bus': {'dtype': 'str'}, 'generator': {'dtype': 'str'}}, - 'relations': {'gen_bus': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'variables': {'gen_p': {'dims': ['generator'], 'bounds': {'lower': 0}}}, - 'expressions': {'injection': {'expression': 'sum(gen_p, over=generator, by=gen_bus[bus])'}}, + 'expressions': {'injection': {'expression': 'sum(gen_p, over=generator, by=generator[bus])'}}, 'objective': {'sense': 'minimize', 'expression': 'sum(gen_p)'}, } @@ -592,7 +590,7 @@ def test_the_composed_model_holds_a_definition_to_the_rules_of_where_it_is_read( } squared = { **INJECTOR, - 'expressions': {'injection': {'expression': 'sum(gen_p * gen_p, over=generator, by=gen_bus[bus])'}}, + 'expressions': {'injection': {'expression': 'sum(gen_p * gen_p, over=generator, by=generator[bus])'}}, } assert to_spec(squares) and to_spec(squared), 'each file loads on its own' with pytest.raises(LanguageError, match='degree'): diff --git a/tests/test_lowering.py b/tests/test_lowering.py index 7fc709e41..b9ac6172b 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -82,21 +82,21 @@ 'constraints': {'c': {'dims': [], 'expression': 'sum(p, over=g) >= 1'}}, } -#: `lk` as `sum` joins it: joined on the key, grouped by the value, no key column left unnamed. -LK = Relation((('g', 'g'), ('h', 'h')), ('g',)) -LK2 = Relation((('g', 'g'), ('z', 'z')), ('g',)) -LK_JOIN = JoinColumns('lk', LK, ('g',), ('h',)) +#: `g[h]` as `sum` joins it: joined on the key, grouped by the value, no key column left unnamed. +LK = Relation((('g', 'g'), ('h', 'h')), ('g',), subject='g') +LK2 = Relation((('g', 'g'), ('z', 'z')), ('g',), subject='g') +LK_JOIN = JoinColumns('g.h', LK, ('g',), ('h',)) AT_BUS = Relation((('g', 'g'), ('bus', 'bus')), ('g',)) -#: `fixtures.SMALL_MODEL` plus a second relation and an integer +#: `fixtures.SMALL_MODEL` plus a second property and an integer #: offset. Which node a construct becomes is mostly a claim about the dim it #: joins on and the dim it groups by, and stating that needs a third dimension -#: and two relations over one of them. +#: and two properties of one of them. SHAPES_MODEL = varied( SMALL_MODEL, **{ 'dimensions.z': {'dtype': 'str'}, - 'relations.lk2': {'key': 'g', 'values': 'z'}, + 'properties.g.z': 'z', 'parameters.lead': {'dims': [], 'dtype': 'int'}, }, ) @@ -281,11 +281,11 @@ def test_a_lowered_where_is_a_mask_that_answers_from_its_root(dispatch_program): [ pytest.param( 'q', - "lk == 'east' and position(h) == 0", + "g[h] == 'east' and position(h) == 0", {'g', 'h'}, 2, 2, - id='a-relation-is-read-at-the-dim-it-maps-out-of-and-a-position-at-its-own', + id='a-property-is-read-at-its-subject-and-a-position-at-its-own', ), pytest.param('p', 'k > 0', set(), 1, 1, id='a-scalar-parameter-is-read-at-no-coordinate'), pytest.param( @@ -413,7 +413,7 @@ def test_a_comparison_of_expressions_lowers_to_program_expressions_on_both_sides 'variables.p.where': 'c <= 0.5 * k', 'constraints.w': { 'dims': ['g'], - 'where': 'c <= at(zc, by=lk2[z]) + sum_back(c, along=g, window=2, within=lk2[z])', + 'where': 'c <= at(zc, by=g[z]) + sum_back(c, along=g, window=2, within=g[z])', 'expression': 'p <= c', }, }, @@ -430,8 +430,8 @@ def test_a_comparison_of_expressions_lowers_to_program_expressions_on_both_sides mask = program.constraints['w'].where assert mask is not None and isinstance(mask.root, ExpressionComparison) assert isinstance(mask.root.right, Add) and isinstance(mask.root.right.left, Join) - assert mask.names_read == frozenset({'c', 'zc', 'lk2'}), ( - 'the relation a lookup and a partition read through is data the consumer attaches too' + assert mask.names_read == frozenset({'c', 'zc', 'g.z'}), ( + 'the property a lookup and a partition read through is data the consumer attaches too' ) @@ -474,8 +474,8 @@ def test_a_translated_condition_keeps_what_it_reads_in_reach(): assert sorted(mask.dims) == ['g'] -def test_a_condition_read_through_a_relation_is_lowered_and_keeps_the_relation_in_reach(): - """The comparison under the read is rebuilt, and the relation is data the consumer attaches as well as the operand.""" +def test_a_condition_read_through_a_property_is_lowered_and_keeps_its_table_in_reach(): + """The comparison under the read is rebuilt, and the property is data the consumer attaches as well as the operand.""" program = to_spec( varied( SHAPES_MODEL, @@ -483,7 +483,7 @@ def test_a_condition_read_through_a_relation_is_lowered_and_keeps_the_relation_i 'parameters.zcap': {'dims': ['z']}, 'constraints.w': { 'dims': ['g'], - 'where': 'at(zcap <= 0.5 * k, by=lk2[z])', + 'where': 'at(zcap <= 0.5 * k, by=g[z])', 'expression': 'p <= c', }, }, @@ -497,8 +497,8 @@ def test_a_condition_read_through_a_relation_is_lowered_and_keeps_the_relation_i Multiply(Constant(0.5, dims=()), ParameterReference('k', dims=()), dims=()), ('z',), ), 'the read condition is rebuilt, not handed through with the resolved comparison still in it' - assert mask.names_read == frozenset({'zcap', 'k', 'lk2'}) - assert sorted(mask.dims) == ['g'], 'z is read at lk2(g), so the mask is over g alone' + assert mask.names_read == frozenset({'zcap', 'k', 'g.z'}) + assert sorted(mask.dims) == ['g'], 'z is read at z(g), so the mask is over g alone' def test_assumptions_carry_the_file_s_entries_and_the_curves_behind_them(): @@ -560,7 +560,7 @@ def test_an_assumption_refuses_in_the_words_the_file_wrote(): def test_a_cased_side_reads_the_data_its_regions_are_decided_by(): - """`names_read` promised every parameter and relation the sides read, and dropped the + """`names_read` promised every parameter and table the sides read, and dropped the `when:` of a cased entry: the walk descends a `Cases` by its values alone.""" program = to_spec( varied( @@ -568,7 +568,7 @@ def test_a_cased_side_reads_the_data_its_regions_are_decided_by(): **{ 'expressions.e': { 'dims': ['g'], - 'cases': {'linked': {'when': 'flag AND lk2', 'expression': 'c'}}, + 'cases': {'linked': {'when': 'flag AND g[z]', 'expression': 'c'}}, 'otherwise': 'k', }, 'variables.p.where': 'e > 0', @@ -577,8 +577,8 @@ def test_a_cased_side_reads_the_data_its_regions_are_decided_by(): ).program where = program.variables['p'].where assert where is not None - assert where.names_read == frozenset({'c', 'k', 'flag', 'lk2'}), ( - 'the flag and the relation decide which region applies, so the consumer attaches them too' + assert where.names_read == frozenset({'c', 'k', 'flag', 'g.z'}), ( + 'the flag and the property decide which region applies, so the consumer attaches them too' ) @@ -605,17 +605,17 @@ def test_a_power_resolves_to_a_node_of_its_own(dispatch_spec): id='an-over-sums-away-the-dim-it-names', ), pytest.param( - 'sum(p, over=g, by=lk[h])', + 'sum(p, over=g, by=g[h])', Sum( Join(VariableReference('p', dims=('g',)), LK_JOIN, dims=('h',)), - (Axis('g', Column('lk', 'g')),), + (Axis('g', Column('g.h', 'g')),), dims=('h',), ), id='a-grouped-sum-is-a-sum-over-the-axis-its-join-opens', ), pytest.param( - 'at(r, by=lk[h])', - Join(VariableReference('r', dims=('h',)), JoinColumns('lk', LK, ('h',), ('g',)), dims=('g',)), + 'at(r, by=g[h])', + Join(VariableReference('r', dims=('h',)), JoinColumns('g.h', LK, ('h',), ('g',)), dims=('g',)), id='an-at-is-the-same-join-the-other-way-with-no-sum-over-it', ), pytest.param( @@ -639,17 +639,17 @@ def test_a_power_resolves_to_a_node_of_its_own(dispatch_spec): id='a-named-offset-written-with-a-plus-is-the-parameter', ), pytest.param( - 'shift(p, along=g, offset=1, within=lk[h], edge=0)', + 'shift(p, along=g, offset=1, within=g[h], edge=0)', Translate( VariableReference('p', dims=('g',)), 'g', offset=1, wrap=False, fill=0.0, - partition=Partition('lk', LK, 'g', ('h',), ()), + partition=Partition('g.h', LK, 'g', ('h',), ()), dims=('g',), ), - id='a-translation-stops-at-the-edges-of-the-relation-it-names', + id='a-translation-stops-at-the-edges-of-the-group-it-names', ), pytest.param( 'sum_back(p, along=g, window=3)', @@ -662,16 +662,16 @@ def test_a_power_resolves_to_a_node_of_its_own(dispatch_spec): id='a-named-width-crosses-as-the-parameter-name', ), pytest.param( - 'sum_back(p, along=g, window=2, within=lk[h])', + 'sum_back(p, along=g, window=2, within=g[h])', WindowSum( VariableReference('p', dims=('g',)), 'g', width=2, wrap=False, - partition=Partition('lk', LK, 'g', ('h',), ()), + partition=Partition('g.h', LK, 'g', ('h',), ()), dims=('g',), ), - id='a-window-stops-at-the-edges-of-the-relation-it-names', + id='a-window-stops-at-the-edges-of-the-group-it-names', ), ], ) @@ -680,31 +680,31 @@ def test_a_construct_resolves_to_its_node(shapes_spec, expression, expected): assert resolved(expression, shapes_spec) == expected, 'the whole frozen node, so no field is asserted by omission' -def test_a_partition_keeps_its_group_when_the_relation_gains_a_value_column(): - """`within=` is what the call groups by, so a calendar that gains a `week` column regroups no `shift` through it (#538). +def test_a_partition_keeps_its_group_when_the_subject_gains_a_property(): + """`within=` is what the call groups by, so an hour that gains a `week` property regroups no `shift` through it (#538). With `within=` optional, an omitted one meant every value column, and the same call grouped by `('day',)` on one calendar and `('day', 'week')` on the next. """ grouping = {} - for values in ('day', ['day', 'week']): + for values in ({'day': 'day'}, {'day': 'day', 'week': 'week'}): program = to_spec( { 'dimensions': {'hour': {'dtype': 'int', 'ordered': True}, 'day': {}, 'week': {}}, - 'relations': {'cal': {'key': 'hour', 'values': values}}, + 'properties': {'hour': values}, 'variables': {'p': {'dims': ['hour']}}, 'constraints': { 'k': { 'dims': ['hour'], - 'expression': 'p >= shift(p, along=hour, offset=1, edge=0, within=cal[day])', + 'expression': 'p >= shift(p, along=hour, offset=1, edge=0, within=hour[day])', } }, } ).program - grouping[str(values)] = _partition_of(program.constraints['k']).grouped - assert grouping == {'day': ('day',), "['day', 'week']": ('day',)}, ( - 'the group is the columns the call named, on both calendars' + grouping[str(sorted(values))] = _partition_of(program.constraints['k']).grouped + assert grouping == {"['day']": ('day',), "['day', 'week']": ('day',)}, ( + 'the group is the properties the call named, with one property and with two' ) @@ -715,45 +715,47 @@ def _partition_of(row): return partition -def test_a_relation_lowers_with_the_join_each_call_names(): - """Every node reading a relation carries its columns, its key and the join, so a consumer joins on the right columns.""" +def test_a_property_of_a_set_lowers_with_the_join_each_call_names(): + """Every node reading a property carries its columns, its key and the join, so a consumer joins on the right columns.""" program = to_spec( { 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'generator': {'ordered': True}, 'zone': {}}, - 'relations': {'zone_of': {'key': ['generator', 'snapshot'], 'values': 'zone'}}, + 'sets': {'gs': {'dims': ['generator', 'snapshot']}}, + 'properties': {'gs': {'zone': 'zone'}}, 'parameters': {'price': {'dims': ['snapshot', 'zone']}}, 'variables': { - 'p': {'dims': ['snapshot', 'generator'], 'where': "zone_of == 'A' AND zone_of"}, + 'p': {'dims': ['snapshot', 'generator'], 'where': "gs[zone] == 'A' AND gs[zone]"}, 'first': { 'dims': ['snapshot', 'generator'], - 'where': 'position(generator, within=zone_of[zone]) == 0', + 'where': 'position(generator, within=gs[zone]) == 0', }, }, 'constraints': { 'zonal': { 'dims': ['snapshot', 'zone'], - 'expression': 'sum(p, over=generator, by=zone_of[zone]) <= 1', + 'expression': 'sum(p, over=generator, by=gs[zone]) <= 1', }, 'priced': { 'dims': ['snapshot', 'generator'], - 'expression': 'p <= at(price, by=zone_of[zone])', + 'expression': 'p <= at(price, by=gs[zone])', }, 'history': { 'dims': ['generator', 'zone'], - 'expression': 'sum(p, over=snapshot, by=zone_of[zone]) <= 1', + 'expression': 'sum(p, over=snapshot, by=gs[zone]) <= 1', }, }, } ).program columns = (('generator', 'generator'), ('snapshot', 'snapshot'), ('zone', 'zone')) - declared = Relation(columns, ('generator', 'snapshot')) - assert program.relations == {'zone_of': declared}, 'the relation sits once in the program, under its name' + declared = Relation(columns, ('generator', 'snapshot'), subject='gs') + rows = Relation((('generator', 'generator'), ('snapshot', 'snapshot')), ('generator', 'snapshot')) + assert program.relations == {'gs': rows, 'gs.zone': declared}, 'the set and its property, each once, under its name' zonal = program.constraints['zonal'].lhs - columns = JoinColumns('zone_of', declared, ('generator', 'snapshot'), ('zone', 'snapshot')) + columns = JoinColumns('gs.zone', declared, ('generator', 'snapshot'), ('zone', 'snapshot')) assert zonal == Sum( Join(VariableReference('p', dims=('generator', 'snapshot')), columns, dims=('snapshot', 'zone')), - (Axis('generator', Column('zone_of', 'generator')),), + (Axis('generator', Column('gs.zone', 'generator')),), dims=('snapshot', 'zone'), ), ( 'a grouped sum is a sum over a join: the join names the column over the over= dim and the unnamed key ' @@ -766,22 +768,22 @@ def test_a_relation_lowers_with_the_join_each_call_names(): ('zone',), ('snapshot',), ), 'the dims a consumer reads are read off the join: dropped, added, and the key columns kept' - assert zonal.operand.columns.relation is program.relations['zone_of'], ( + assert zonal.operand.columns.relation is program.relations['gs.zone'], ( 'the join holds the one entry the program holds, not an equal copy built again' ) assert program.constraints['history'].lhs == Sum( Join( VariableReference('p', dims=('generator', 'snapshot')), - JoinColumns('zone_of', declared, ('snapshot', 'generator'), ('zone', 'generator')), + JoinColumns('gs.zone', declared, ('snapshot', 'generator'), ('zone', 'generator')), dims=('generator', 'zone'), ), - (Axis('snapshot', Column('zone_of', 'snapshot')),), + (Axis('snapshot', Column('gs.zone', 'snapshot')),), dims=('generator', 'zone'), ), 'the same table joined on its other key column' priced = program.constraints['priced'].rhs assert priced == Join( ParameterReference('price', dims=('snapshot', 'zone')), - JoinColumns('zone_of', declared, ('zone', 'snapshot'), ('generator', 'snapshot')), + JoinColumns('gs.zone', declared, ('zone', 'snapshot'), ('generator', 'snapshot')), dims=('generator', 'snapshot'), ), 'and an at is the bare join, on the value column, grouped by the key columns' assert isinstance(priced, Join) @@ -795,7 +797,7 @@ def test_a_relation_lowers_with_the_join_each_call_names(): assert [(type(a).__name__, a.dims) for a in p_where.atoms] == [ ('RelationComparison', ('generator', 'snapshot')), ('RelationDefined', ('generator', 'snapshot')), - ], 'a comparison and an existence are both read at the key of a keyed relation' + ], 'a comparison and a presence test are both read at the members of the set' first_where = program.variables['first'].where assert first_where is not None assert first_where.dims == {'generator', 'snapshot'}, 'a position within a group is read at every key column' @@ -873,25 +875,22 @@ def test_walk_is_the_node_column_of_walk_regions(): assert list(walk(NESTED)) == [node for node, _ in walk_regions(NESTED)] -def test_a_relation_is_declared_as_the_file_declares_it(): - """One group keyed by name, each entry its columns and its key, and nothing nested under a dimension.""" +def test_a_property_is_declared_as_the_file_declares_it(): + """One group keyed by table name, each entry its columns and its key, and nothing nested under a dimension.""" program = to_spec( varied( TINY, dimensions={'g': {}, 'bus': {}, 'season': {}}, - relations={ - 'season_of': {'key': 'g', 'values': 'season'}, - 'at_bus': {'key': 'g', 'values': 'bus'}, - }, + properties={'g': {'season': 'season', 'bus': 'bus'}}, ) ).program assert program.relations == { - 'season_of': Relation((('g', 'g'), ('season', 'season')), ('g',)), - 'at_bus': Relation((('g', 'g'), ('bus', 'bus')), ('g',)), - }, 'every relation under its own name, in file order' - assert program.relations['season_of'].values == ('season',), 'and each says what its key determines' - assert program.dimensions['g'] == Dimension(dtype='str'), 'a dimension carries its dtype and no relation' + 'g.season': Relation((('g', 'g'), ('season', 'season')), ('g',), subject='g'), + 'g.bus': Relation((('g', 'g'), ('bus', 'bus')), ('g',), subject='g'), + }, 'every property under its table name, in file order' + assert program.relations['g.season'].values == ('season',), 'and each says what its key determines' + assert program.dimensions['g'] == Dimension(dtype='str'), 'a dimension carries its dtype and no property' @pytest.mark.parametrize('ordered', [pytest.param(True, id='ordered'), pytest.param(False, id='unordered')]) diff --git a/tests/test_property_calls.py b/tests/test_property_calls.py index aa59f5b52..8232b39f1 100644 --- a/tests/test_property_calls.py +++ b/tests/test_property_calls.py @@ -201,12 +201,6 @@ def test_a_position_within_a_property_reads_its_table(): @pytest.mark.parametrize( ('patch', 'text', 'tables'), [ - pytest.param( - {'relations.period_of': {'key': 'snapshot', 'values': 'period'}}, - 'position(snapshot, within=period_of[period]) == 0', - {'period_of'}, - id='a-relation', - ), pytest.param({}, 'position(snapshot, within=snapshot[period]) == 0', {'snapshot.period'}, id='a-property'), pytest.param( {}, @@ -267,8 +261,7 @@ def test_a_position_within_a_partition_reads_its_tables(patch, text, tables): ), pytest.param( {'variables.x.where': 'cap[bus]'}, - "'cap[bus]' reads a column of 'cap', which is a parameter. Only a relation, a set or a dimension has " - 'columns.', + "'cap[bus]' reads a column of 'cap', which is a parameter. Only a set or a dimension has properties.", id='a-presence-test-of-a-parameter', ), pytest.param( @@ -295,7 +288,7 @@ def test_a_position_within_a_partition_reads_its_tables(patch, text, tables): ), pytest.param( {'variables.x.where': "x[bus] == 'b1'"}, - "'x[bus]' reads a column of 'x', which is a variable. Only a relation, a set or a dimension has columns.", + "'x[bus]' reads a column of 'x', which is a variable. Only a set or a dimension has properties.", id='a-column-of-a-variable', ), ], diff --git a/tests/test_pypsa_collapses.py b/tests/test_pypsa_collapses.py index 4c2deb64e..4b296bc78 100644 --- a/tests/test_pypsa_collapses.py +++ b/tests/test_pypsa_collapses.py @@ -47,7 +47,7 @@ def _dims(entry: Any) -> list[str]: def test_every_standard_name_survives(): sections = { 'parameters': ALL.parameters, - 'relations': ALL.relations, + 'properties': {f'{s}.{p}' for s, group in ALL.properties.items() for p in group}, 'variables': ALL.variables, 'expressions': ALL.expressions, 'constraints': ALL.constraints, diff --git a/tests/test_pypsa_references.py b/tests/test_pypsa_references.py index 8cb5db5c9..75676f3a3 100644 --- a/tests/test_pypsa_references.py +++ b/tests/test_pypsa_references.py @@ -143,7 +143,7 @@ def test_a_patch_keeps_the_pypsa_name_of_what_it_changes(page: str): """A patch rewrites entries of the base; a rewritten name keeps its PyPSA name and its dtype, or it has drifted.""" own = SPECS[page] drifted = [] - for section in ('parameters', 'relations', 'variables', 'constraints'): + for section in ('parameters', 'sets', 'variables', 'constraints'): theirs, ours = getattr(BASE, section), getattr(own, section) for name in set(theirs) & set(ours): if (theirs[name].description or '').split(' — ')[0] != (ours[name].description or '').split(' — ')[0]: diff --git a/tests/test_pypsa_split.py b/tests/test_pypsa_split.py index ca17d0d44..80e0ebe4f 100644 --- a/tests/test_pypsa_split.py +++ b/tests/test_pypsa_split.py @@ -161,7 +161,7 @@ def test_the_split_index_names_a_hub_once_per_fragment_and_needs_a_described_rea **FLEET, 'expressions': { **FLEET['expressions'], - 'curtailment': {'expression': 'sum(gen_p, over=generator, by=gen_bus[bus])', 'adds_to': 'injection'}, + 'curtailment': {'expression': 'sum(gen_p, over=generator, by=generator[bus])', 'adds_to': 'injection'}, }, } specs = {'balance': to_spec(BALANCE), 'fleet': to_spec(twice), 'demand': to_spec(DEMAND)} diff --git a/tests/test_schema.py b/tests/test_schema.py index 31c409289..07f7824e3 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -59,7 +59,7 @@ def test_no_definition_refers_only_to_itself(): pytest.param('ObjectiveSpec', 'sense', spec.ObjectiveSense, id='sense'), pytest.param('VariableSpec', 'domain', spec.VariableDomain, id='domain'), pytest.param('VariableSpec', 'missing', Literal['absent'], id='variable-missing'), - pytest.param('RelationSpec', 'missing', spec.RelationMissing, id='relation-missing'), + pytest.param('SetSpec', 'missing', spec.RelationMissing, id='set-missing'), pytest.param('ParameterSpec', 'missing', spec.RelationMissing, id='parameter-missing'), pytest.param('ParameterSpec', 'dtype', spec.ParameterDtype, id='parameter-dtype'), pytest.param('DimensionSpec', 'dtype', spec.DimensionDtype, id='dimension-dtype'), @@ -80,12 +80,12 @@ def test_a_closed_vocabulary_is_published_as_an_enum(entry, field, alias): ('entry', 'default'), [ pytest.param('ParameterSpec', 'refused', id='parameter'), - pytest.param('RelationSpec', 'refused', id='relation'), + pytest.param('SetSpec', 'refused', id='set'), pytest.param('VariableSpec', 'absent', id='variable'), ], ) def test_the_schema_publishes_the_reading_a_file_gets_by_leaving_missing_out(entry, default): - """The parameter and the relation hold `None` until read, and the schema published that `null`, not `refused`.""" + """The parameter and the set hold `None` until read, and the schema published that `null`, not `refused`.""" published = json.loads(schema.PATH.read_text())['$defs'][entry]['properties']['missing'] assert published['default'] == default diff --git a/tests/test_separability.py b/tests/test_separability.py index 43d02c96e..822b59a61 100644 --- a/tests/test_separability.py +++ b/tests/test_separability.py @@ -30,7 +30,7 @@ 'zone': {'dtype': 'str'}, 'day': {'dtype': 'int'}, }, - 'relations': {'zone_of': {'key': 'u', 'values': 'zone'}, 'day_of': {'key': 'h', 'values': 'day'}}, + 'properties': {'u': {'zone': 'zone'}, 'h': {'day': 'day'}}, 'parameters': { 'cost': {'dims': ['u']}, 'budget': {'dims': []}, @@ -87,8 +87,8 @@ def test_a_model_the_axis_ties_together_names_what_ties_it(patch, fragment): ('patch', 'reach'), [ pytest.param( - _rows('p >= shift(p, along=h, offset=1, within=day_of[day], edge=0)'), - Reach("constraint 'k'", 'day_of', 'partition'), + _rows('p >= shift(p, along=h, offset=1, within=h[day], edge=0)'), + Reach("constraint 'k'", 'h.day', 'partition'), id='a-shift-inside-groups', ), pytest.param( @@ -99,7 +99,7 @@ def test_a_model_the_axis_ties_together_names_what_ties_it(patch, fragment): ], ) def test_a_reach_only_data_can_say_names_what_says_it(patch, reach): - """The verdict names the parameter or relation and what it stands as, rather + """The verdict names the parameter or table and what it stands as, rather than refusing the model, so a driver holding the data knows what to read and `resolved` knows how to fold it.""" verdict = _verdict(**patch) @@ -131,20 +131,20 @@ def test_a_named_reach_resolves_to_the_lookahead_its_values_need(patch, least, a assert verdict.ahead == ahead, 'the value decides the lookahead, by its sign' -def test_resolving_keeps_the_static_reach_and_what_a_relation_decides(): +def test_resolving_keeps_the_static_reach_and_what_a_property_decides(): verdict = _verdict( constraints={ 'fixed': {'dims': ['h', 'u'], 'expression': 'p >= shift(p, along=h, offset=-2, edge=0)'}, 'named': {'dims': ['h', 'u'], 'expression': 'p >= shift(p, along=h, offset=width, edge=0)'}, 'grouped': { 'dims': ['h', 'u'], - 'expression': 'p >= shift(p, along=h, offset=1, within=day_of[day], edge=0)', + 'expression': 'p >= shift(p, along=h, offset=1, within=h[day], edge=0)', }, } ).resolved({'width': -1}) assert verdict.ahead == 2, 'a folded value never narrows what the model reads on its own' - assert verdict.undecided == (Reach("constraint 'grouped'", 'day_of', 'partition'),), ( - 'a reach a relation decides is not a value and stays undecided' + assert verdict.undecided == (Reach("constraint 'grouped'", 'h.day', 'partition'),), ( + 'a reach a property decides is not a value and stays undecided' ) assert not verdict.windowable, 'so the axis is still not windowable' @@ -154,13 +154,13 @@ def test_resolving_a_name_nothing_waits_on_is_refused(): _verdict(**_rows('p >= shift(p, along=h, offset=width, edge=0)')).resolved({'depth': 0}) -def test_a_read_through_a_relation_is_undecided_on_the_axis_it_reads(): - """`at(cap, by=zone_of[zone])` reads `zone` at whatever coordinate the relation - chooses, so how far that reaches along `zone` is the relation's data to say.""" - verdict = _verdict('zone', **_rows('p - at(cap, by=zone_of[zone]) <= 0')) - assert not verdict.windowable and not verdict.coupled, 'undecided until the relation is attached' - assert verdict.undecided == (Reach("constraint 'k'", 'zone_of', 'coordinate'),), ( - 'the report names the relation a driver has to read' +def test_a_read_through_a_property_is_undecided_on_the_axis_it_reads(): + """`at(cap, by=u[zone])` reads `zone` at whatever coordinate the property + chooses, so how far that reaches along `zone` is the property's data to say.""" + verdict = _verdict('zone', **_rows('p - at(cap, by=u[zone]) <= 0')) + assert not verdict.windowable and not verdict.coupled, 'undecided until the property is attached' + assert verdict.undecided == (Reach("constraint 'k'", 'u.zone', 'coordinate'),), ( + 'the report names the table a driver has to read' ) @@ -172,13 +172,13 @@ def test_a_sum_over_a_lookup_is_a_sum_and_a_lookup_not_a_grouping(): from its columns, not from the node above it. """ variables = {**BASE['variables'], 'q': {'dims': ['h', 'zone'], 'bounds': {'lower': 0}}} - rows = _rows('sum(at(q, by=zone_of[zone]), over=u) <= budget', dims=['h']) + rows = _rows('sum(at(q, by=u[zone]), over=u) <= budget', dims=['h']) program = to_spec({**BASE, 'variables': variables, **rows}).program assert list(program.separability['u'].coupled.values()) == [ 'sums over u (use sum_back(window=n) for a rolling sum)' ], 'the sum over u is a plain sum, reported as one' - assert program.separability['zone'].undecided == (Reach("constraint 'k'", 'zone_of', 'coordinate'),), ( - 'the lookup under it still reads zone at a coordinate the relation chooses' + assert program.separability['zone'].undecided == (Reach("constraint 'k'", 'u.zone', 'coordinate'),), ( + 'the lookup under it still reads zone at a coordinate the property chooses' ) @@ -239,7 +239,7 @@ def test_a_grouping_that_sums_the_axis_away_couples_it(): program = to_spec( { **BASE, - 'constraints': {'z': {'dims': ['h', 'zone'], 'expression': 'sum(p, over=u, by=zone_of[zone]) <= cap'}}, + 'constraints': {'z': {'dims': ['h', 'zone'], 'expression': 'sum(p, over=u, by=u[zone]) <= cap'}}, } ).program verdict = program.separability['u'] @@ -247,7 +247,7 @@ def test_a_grouping_that_sums_the_axis_away_couples_it(): assert verdict.coupled == { "constraint 'z'": 'groups u into zone (window that dimension instead, or cut only at the group edges)' }, 'the sum over the join is the grouping, reported once' - assert not verdict.undecided, 'the join under the sum is not also a lookup waiting on the relation' + assert not verdict.undecided, 'the join under the sum is not also a lookup waiting on the property' def test_every_declared_axis_has_a_verdict_and_nothing_else_does(): diff --git a/tests/test_sets.py b/tests/test_sets.py index b388098a1..66f6dc6af 100644 --- a/tests/test_sets.py +++ b/tests/test_sets.py @@ -141,7 +141,7 @@ def test_the_normal_form_sorts_the_properties_of_a_subject(): def test_an_error_lists_no_property_as_a_name_to_write(): message = _refusal(**{'variables.inj.where': 'wired'}) - assert "Relations: ['connection', 'cover', 'generator_period']" in message, ( + assert "Sets: ['connection', 'cover', 'generator_period']" in message, ( 'a property has no name a file can write, so the listing leaves it out' ) diff --git a/tests/test_terms.py b/tests/test_terms.py index 6d97ab3ea..3d23ff9a3 100644 --- a/tests/test_terms.py +++ b/tests/test_terms.py @@ -25,12 +25,12 @@ #: A generator fleet: what it puts in is its term. FLEET = { 'dimensions': {**BUS_DIMS, 'generator': {'dtype': 'str'}}, - 'relations': {'gen_bus': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'variables': {'gen_p': {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0}}}, 'given': {'expressions': {'injection': {'dims': BUS_FRAME}}}, 'expressions': { 'generator_injection': { - 'expression': 'sum(gen_p, over=generator, by=gen_bus[bus])', + 'expression': 'sum(gen_p, over=generator, by=generator[bus])', 'description': 'what the generators put in', 'adds_to': 'injection', } @@ -49,11 +49,11 @@ #: A store, added in a second merge. STORAGE = { 'dimensions': {**BUS_DIMS, 'store': {'dtype': 'str'}}, - 'relations': {'store_bus': {'key': 'store', 'values': 'bus'}}, + 'properties': {'store': {'bus': 'bus'}}, 'variables': {'store_p': {'dims': ['snapshot', 'store']}}, 'given': {'expressions': {'injection': {'dims': BUS_FRAME}}}, 'expressions': { - 'store_injection': {'expression': 'sum(store_p, over=store, by=store_bus[bus])', 'adds_to': 'injection'} + 'store_injection': {'expression': 'sum(store_p, over=store, by=store[bus])', 'adds_to': 'injection'} }, } diff --git a/tests/test_validation.py b/tests/test_validation.py index a527ee491..e5594f598 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -85,9 +85,12 @@ class TestValidateExpressions: id='an-unknown-name-in-a-where-used-to-evaluate-to-false', ), pytest.param( - {'constraints': {'cap': {'dims': ['g'], 'where': 'lkk', 'expression': 'p <= c'}}}, - ("'lkk' not found", "Relations: ['lk']"), - id='a-mistyped-relation-in-a-where-lists-the-relations', + { + 'sets.pair': {'dims': ['g', 'h']}, + 'constraints': {'cap': {'dims': ['g'], 'where': 'pairr', 'expression': 'p <= c'}}, + }, + ("'pairr' not found", "Sets: ['pair']"), + id='a-mistyped-set-in-a-where-lists-the-sets', ), ], ) @@ -129,9 +132,9 @@ def test_a_fault_in_a_macro_or_an_entry_hides_no_other_fault(self, patch): assert "Constraint 'c': 'also_nope' not found" in message, 'every entry is read, whatever an entry did' def test_a_refused_call_is_not_read_by_the_call_around_it(self): - """`sum(sum(p, over=lk[g]))`: the inner call writes a selection in `over=`, and the outer bare sum then said its operand was already a scalar, because the refused call was still built.""" - message = _refusal(objective={'expression': 'sum(sum(p, over=lk[g]))'}) - assert 'over= takes bare names. Write over=g' in message + """`sum(sum(p, over=g[h]))`: the inner call writes a selection in `over=`, and the outer bare sum then said its operand was already a scalar, because the refused call was still built.""" + message = _refusal(objective={'expression': 'sum(sum(p, over=g[h]))'}) + assert 'over= takes bare names. Write over=h' in message assert 'already a scalar' not in message, 'a refused call builds nothing for the call around it to read' @pytest.mark.parametrize( @@ -205,9 +208,9 @@ def test_an_unreferenced_nonlinear_entry_loads_and_is_reported(self): def _kwarg_model(expression: str, dims: list[str] | None = None) -> dict[str, Any]: - """A model over (snapshot, generator), with `zone` a relation into `bus`. + """A model over (snapshot, generator), with `generator[bus]` a property into `bus`. - `zone` deliberately targets a dim `p` does *not* carry: grouping into + `bus` deliberately targets a dim `p` does *not* carry: grouping into one it already has needs that dim twice, which is its own error. An explicit ``dims=[]`` is a scalar constraint; ``None`` is the default frame over `snapshot`. @@ -218,7 +221,7 @@ def _kwarg_model(expression: str, dims: list[str] | None = None) -> dict[str, An 'bus': {'dtype': 'str'}, 'generator': {'dtype': 'str'}, }, - 'relations': {'zone': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'parameters': {'load': {'dims': ['snapshot']}}, 'variables': {'p': {'dims': ['snapshot', 'generator']}}, 'constraints': {'c': {'dims': ['snapshot'] if dims is None else dims, 'expression': expression}}, @@ -316,13 +319,13 @@ class TestDimensionKwargs: ), pytest.param( 'sum(p, over=generator, by=bus) == load', - ("'bus' is a dimension, and by= takes columns of a relation", "Columns over 'bus': ['zone[bus]']"), + ("'bus' is a dimension, and by= takes what one subject reads", "Reads over 'bus': ['generator[bus]']"), id='by-names-a-dimension', ), pytest.param( - 'sum(p, over=generator, by=zne[bus]) == load', - ('does not name a relation', "Did you mean 'zone'?"), - id='by-relation-typo', + 'sum(p, over=generator, by=generatr[bus]) == load', + ('does not name a set or a dimension', "Did you mean 'generator'?"), + id='by-subject-typo', ), pytest.param( 'shift(p, along=snapshto, offset=1) == load', @@ -340,7 +343,7 @@ def test_a_dim_kwarg_typo_is_rejected(self, expression, fragments): ('expression', 'dims'), [ pytest.param('sum(p, over=generator) == load', ['snapshot'], id='a-sum'), - pytest.param('sum(p, over=generator, by=zone[bus]) == load', ['snapshot', 'bus'], id='a-grouped-sum'), + pytest.param('sum(p, over=generator, by=generator[bus]) == load', ['snapshot', 'bus'], id='a-grouped-sum'), pytest.param( "shift(p, along=snapshot, offset=1, edge='wrap') == load", ['snapshot', 'generator'], @@ -370,9 +373,7 @@ def test_declared_dimensions_still_pass(self, expression, dims): "sum(over=[generator, snapshot, generator]) names 'generator' twice", id='a-dimension-named-twice', ), - pytest.param( - "sum(p, over=g, by='lk')", 'sum(by=...) takes columns of one relation', id='a-quoted-relation' - ), + pytest.param("sum(p, over=g, by='g')", 'sum(by=...) takes what one subject reads', id='a-quoted-subject'), ], ) def test_a_kwarg_that_names_nothing_is_refused(self, expression, fragment): @@ -429,9 +430,9 @@ def test_a_where_comparison_must_match_the_declared_dtype(self, dtype, where, ma id='parameter', ), pytest.param( - {'variables.p.where': "lk > 'b'"}, - ["'lk' has dtype 'str', on which > is not defined", _NO_INEQUALITY_REWRITE], - id='relation-column', + {'variables.p.where': "g[h] > 'b'"}, + ["'g[h]' has dtype 'str', on which > is not defined", _NO_INEQUALITY_REWRITE], + id='a-property', ), pytest.param( {'conditions.late': "g <= 'b'", 'variables.p.where': 'late'}, @@ -439,12 +440,12 @@ def test_a_where_comparison_must_match_the_declared_dtype(self, dtype, where, ma id='named-condition', ), pytest.param( - {'relations.lk2': {'key': 'g', 'values': 'h'}, 'variables.p.where': 'lk < lk2'}, + {'properties.g.h2': 'h', 'variables.p.where': 'g[h] < g[h2]'}, [ - "compares two columns over 'h', of dtype 'str', on which < is not defined", + "compares two properties into 'h', of dtype 'str', on which < is not defined", 'Compare them with == or !=, or precompute the comparison as a boolean parameter.', ], - id='two-relation-columns', + id='two-properties', ), pytest.param( {'variables.p.where': 'flag > 0'}, @@ -473,7 +474,7 @@ class TestAnUndeclaredKeywordIsRefusedOnce: SIGNATURE = ( 'The objective: shift() expects ' - "shift(, along=, offset=[, edge='wrap'|][, within=[]])" + "shift(, along=, offset=[, edge='wrap'|][, within=[]])" ) @pytest.mark.parametrize( @@ -501,7 +502,7 @@ def test_a_typo_of_a_real_keyword_is_the_signature_not_advice_about_its_value(se TEMPLATE_SIGNATURE = ( "Macro 'm': shift() expects " - "shift(, along=, offset=[, edge='wrap'|][, within=[]])" + "shift(, along=, offset=[, edge='wrap'|][, within=[]])" ) @pytest.mark.parametrize( @@ -616,14 +617,11 @@ def test_the_version_gates_no_behaviour(self): assert _spec().model_dump(exclude={'version'}) == _spec(version=0).model_dump(exclude={'version'}) -#: `position(dim)` needs a relation over *that* dimension, so one over it and one into it. +#: `position(dim)` needs a property of *that* dimension, so one of it and one into it. POSITION_SPEC = to_spec( { 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'period': {'dtype': 'int'}}, - 'relations': { - 'period_of': {'key': 'snapshot', 'values': 'period'}, - 'starts_at': {'key': 'period', 'values': 'snapshot'}, - }, + 'properties': {'snapshot': {'period': 'period'}, 'period': {'starts_at': 'snapshot'}}, 'parameters': {'load': {'dims': ['snapshot']}}, 'variables': {'p': {'dims': ['snapshot']}}, } @@ -633,16 +631,16 @@ def test_the_version_gates_no_behaviour(self): class TestPositionResolves: """`position(dim)` — the conversion #32 put on the left-hand side. - A `by=` has to be a relation over *that* dimension, and that is the whole - test: a relation over anything else carries no row for a position to be a - position in. + A `within=` has to read a property of *that* dimension, and that is the + whole test: a property of anything else carries no row for a position to + be a position in. """ @pytest.mark.parametrize( ('mask', 'position', 'by'), [ ('position(snapshot) == 0', 0, None), - ('position(snapshot, within=period_of[period]) == 0', 0, 'period_of'), + ('position(snapshot, within=snapshot[period]) == 0', 0, 'snapshot.period'), ], ids=['first', 'first of each period'], ) @@ -662,14 +660,14 @@ def test_it_resolves(self, mask: str, position: int, by: str | None): ('position(nope) == 0', ["'nope' is not declared"]), ( 'position(snapshot, within=load[snapshot]) == 0', - ['position(within=load[...]) does not name a relation', "Declare it under 'relations:'"], + ['position(within=load[...]) does not name a set or a dimension'], ), ( - 'position(snapshot, within=starts_at[snapshot]) == 0', - ["no key column over 'snapshot'", "Its key is ['period']"], + 'position(snapshot, within=period[starts_at]) == 0', + ["'period' has no member over 'snapshot'", "Its members are ['period']"], ), ], - ids=['a parameter', 'undeclared', 'within= is not a relation', 'within= is over another dim'], + ids=['a parameter', 'undeclared', 'within= is not a subject', 'within= is over another dim'], ) def test_it_refuses(self, mask: str, fragments: list[str]): with pytest.raises(LanguageError) as excinfo: @@ -700,17 +698,17 @@ class TestAWhereSideIsReadInResolution: ), pytest.param( 'position(g, h) == 0', - ('this position() call is not of the form position([, within=[]])',), + ('this position() call is not of the form position([, within=[]])',), id='a-position-with-two-dimensions', ), pytest.param( 'position(g, edge=1) == 0', - ('this position() call is not of the form position([, within=[]])',), + ('this position() call is not of the form position([, within=[]])',), id='a-position-with-a-kwarg-it-lacks', ), pytest.param( - 'position(g, by=[lk, lk2]) == 0', - ('this position() call is not of the form position([, within=[]])',), + 'position(g, by=[g, h]) == 0', + ('this position() call is not of the form position([, within=[]])',), id='a-position-by-a-list', ), ], @@ -738,8 +736,8 @@ def test_a_signed_literal_and_inf_are_numbers_on_a_side(self): pytest.param({'expressions.e': 'c * 2'}, 'k < e', id='a-named-expression-on-the-right'), pytest.param( {'parameters.d': {'dims': ['h']}}, - 'c <= at(d, by=lk[h])', - id='a-lookup-through-a-relation', + 'c <= at(d, by=g[h])', + id='a-lookup-through-a-property', ), pytest.param( {}, 'c - shift(c, along=g, offset=1, edge=0) <= k AND position(g) > 0', id='a-translation-with-its-edge' @@ -795,9 +793,9 @@ def test_a_reduction_on_a_side_leaves_the_frame_it_reduced(self): id='a-side-outside-the-frame', ), pytest.param( - {'variables.p.where': 'lk[h] > 2 * k'}, - ("'lk[h]' is a relation column in arithmetic", 'Compare it alone against a literal or a column'), - id='a-relation-column-inside-arithmetic', + {'variables.p.where': 'g[h] > 2 * k'}, + ("'g[h]' is a property in arithmetic", 'Compare it alone against a literal or a property'), + id='a-property-inside-arithmetic', ), pytest.param( {'variables.p.where': "2 * k == 'x'"}, @@ -898,9 +896,9 @@ class TestAConditionIsAnOperand: pytest.param('shift(flag, along=g, offset=1)', id='a-translated-mask'), pytest.param('shift(flag, along=g, offset=-1)', id='a-translation-forwards'), pytest.param('count(shift(flag, along=g, offset=1), over=g) >= 1', id='a-translation-under-a-count'), - pytest.param('at(r, by=lk[h])', id='a-mask-read-through-a-relation'), - pytest.param("flag AND NOT at(h == 'north', by=lk[h])", id='a-read-under-connectives'), - pytest.param('count(at(r, by=lk[h]), over=g) >= 1', id='a-read-under-a-count'), + pytest.param('at(r, by=g[h])', id='a-mask-read-through-a-property'), + pytest.param("flag AND NOT at(h == 'north', by=g[h])", id='a-read-under-connectives'), + pytest.param('count(at(r, by=g[h]), over=g) >= 1', id='a-read-under-a-count'), ], ) def test_a_shape_the_language_admits(self, where): @@ -911,21 +909,21 @@ def test_a_shape_the_language_admits(self, where): ('where', 'dims', 'reads'), [ pytest.param( - 'at(has_curve, by=cost_of[curve])', + 'at(has_curve, by=flow_effect[curve])', ['effect', 'flow'], - {'has_curve', 'cost_of'}, + {'has_curve', 'flow_effect.curve'}, id='the-key-that-arrives-is-several-columns', ), pytest.param( - 'at(has_cost, by=pair_of[flow, effect])', + 'at(has_cost, by=curve[flow, effect])', ['curve'], - {'has_cost', 'pair_of'}, + {'has_cost', 'curve.flow', 'curve.effect'}, id='the-read-names-several-columns', ), ], ) def test_a_read_names_several_columns_as_an_expression_does(self, where, dims, reads): - """A where string reads several columns of one relation, as an expression does (#781). + """A where string reads several properties of one subject, as an expression does (#781). A piecewise entry whose mask is read through a walk into `[flow, effect]` writes this `where:`, and the load failed on its own assertion. @@ -933,10 +931,8 @@ def test_a_read_names_several_columns_as_an_expression_does(self, where, dims, r spec = to_spec( { 'dimensions': {name: {'dtype': 'str'} for name in ('curve', 'effect', 'flow')}, - 'relations': { - 'cost_of': {'key': ['flow', 'effect'], 'values': 'curve'}, - 'pair_of': {'key': 'curve', 'values': ['flow', 'effect']}, - }, + 'sets': {'flow_effect': {'dims': ['flow', 'effect']}}, + 'properties': {'flow_effect': {'curve': 'curve'}, 'curve': {'flow': 'flow', 'effect': 'effect'}}, 'parameters': { 'has_curve': {'dims': ['curve'], 'dtype': 'bool'}, 'has_cost': {'dims': ['flow', 'effect'], 'dtype': 'bool'}, @@ -949,24 +945,24 @@ def test_a_read_names_several_columns_as_an_expression_does(self, where, dims, r mask = spec.program.constraints['k'].where assert mask is not None assert sorted(mask.dims) == dims, 'the read lands on the columns the other end names' - assert mask.names_read == reads, 'a consumer attaches the relation as well as the operand' + assert mask.names_read == reads, 'a consumer attaches the tables of the properties as well as the operand' assert to_spec(spec.to_yaml()).program == spec.program, 'the where string reads back to the same mask' for fmt in ('markdown', 'latex', 'typst'): printed = typeset_line(spec, 'k', fmt).replace('\\_', '_') - assert all(name in printed for name in reads), ( - f'{fmt} prints the operand and the relation it is read through' + assert all(name.rsplit('.', 1)[-1] in printed for name in reads), ( + f'{fmt} prints the operand and the properties it is read through' ) @pytest.mark.parametrize( ('where', 'fragments'), [ pytest.param( - 'count(flag, by=lk) >= 2', + 'count(flag, by=g) >= 2', ("count() needs 'over='",), id='a-count-with-no-over', ), pytest.param( - 'count(flag, over=g, by=lk) >= 2', + 'count(flag, over=g, by=g) >= 2', ("does not take 'by='", "It takes only 'over='."), id='a-count-with-a-keyword-it-lacks', ), @@ -1056,29 +1052,29 @@ def test_a_read_names_several_columns_as_an_expression_does(self, where, dims, r id='a-translation-by-a-parameter', ), pytest.param( - 'at(r, by=lk)', - ('at(by=lk) names the relation and none of its columns',), + 'at(r, by=g)', + ("at(by=g): 'g' is a dimension, and by= takes what one subject reads", "Nothing is over 'g'."), id='a-read-naming-no-columns', ), pytest.param( - 'at(r, by=lk[h], edge=0)', + 'at(r, by=g[h], edge=0)', ("does not take 'edge='", "It takes only 'by='."), id='a-read-with-a-keyword-it-lacks', ), pytest.param( - 'at(flag, by=lk[h])', - ("at(by=lk[h]) joins on ['h'], which the condition does not carry",), + 'at(flag, by=g[h])', + ("at(by=g[h]) joins on ['h'], which the condition does not carry",), id='a-read-through-a-dim-the-condition-lacks', ), pytest.param( - 'at(flag, by=lk[g])', - ("at(by=lk[g]) names ['g'], a key column of 'lk'. A lookup reads value columns at the key",), - id='a-read-of-a-key-column', + 'at(flag, by=g[g])', + ("at(by=g[g]) reads no property of 'g', and a dimension has no table of its own",), + id='a-read-of-the-subject-itself', ), pytest.param( 'at(r, by=nope[h])', - ('at(by=nope[...]) does not name a relation',), - id='a-read-through-no-relation', + ('at(by=nope[...]) does not name a set or a dimension',), + id='a-read-through-no-subject', ), pytest.param( 'sum_back(flag, along=g, window=2)', @@ -1098,7 +1094,7 @@ def test_a_shape_the_language_refuses(self, where, fragments): [ pytest.param('count(nope, over=g) >= 2', id='under-a-count'), pytest.param('shift(nope, along=g, offset=1)', id='under-a-translation'), - pytest.param('at(nope, by=lk[h])', id='under-a-read'), + pytest.param('at(nope, by=g[h])', id='under-a-read'), ], ) def test_a_name_the_operand_does_not_declare_is_reported_rather_than_walked(self, where): @@ -1128,18 +1124,18 @@ def test_a_count_is_undecidable_in_a_case_when(self): ) assert 'it counts the coordinates a condition admits, which only the data decides' in message - def test_an_inequality_between_two_relation_columns_is_undecidable_in_a_case_when(self): - """The analysis does not decide an inequality between two relation columns, whatever their dtype. + def test_an_inequality_between_two_properties_is_undecidable_in_a_case_when(self): + """The analysis does not decide an inequality between two properties, whatever their dtype. Before, the refusal said that two relations have no order, which is false for two int columns. """ message = _refusal( - **{'dimensions.h': {'dtype': 'int'}, 'relations.lk2': {'key': 'g', 'values': 'h'}}, + **{'dimensions.h': {'dtype': 'int'}, 'properties.g.h2': 'h'}, expressions={ 'pick': { 'dims': ['g'], 'cases': { - 'below': {'when': 'lk < lk2', 'expression': '1'}, + 'below': {'when': 'g[h] < g[h2]', 'expression': '1'}, 'some': {'when': 'c > 0', 'expression': '2'}, }, 'otherwise': '0', @@ -1147,23 +1143,23 @@ def test_an_inequality_between_two_relation_columns_is_undecidable_in_a_case_whe }, constraints={'cap': {'dims': ['g'], 'expression': 'p <= pick'}}, ) - assert 'this analysis does not decide an inequality between two relation columns' in message, message + assert 'this analysis does not decide an inequality between two properties' in message, message assert 'no order' not in message, message @pytest.mark.parametrize( ('first', 'second', 'witness'), [ - pytest.param('two[a] > 3', 'two[b] <= 3', 'two[a] is 4.0, two[b] is 2.0', id='inequalities'), - pytest.param('two[a] == 3', 'two[b] == 4', 'two[a] is 3.0, two[b] is 4.0', id='equalities'), + pytest.param('g[a] > 3', 'g[b] <= 3', 'g[a] is 4.0, g[b] is 2.0', id='inequalities'), + pytest.param('g[a] == 3', 'g[b] == 4', 'g[a] is 3.0, g[b] is 4.0', id='equalities'), ], ) - def test_two_columns_of_one_relation_are_not_proved_apart_in_a_case_when(self, first, second, witness): - """One row holds a value in each column, so a test on one column does not exclude a test on the other. + def test_two_properties_of_one_subject_are_not_proved_apart_in_a_case_when(self, first, second, witness): + """One row holds a value for each property, so a test on one does not exclude a test on the other. Before, both columns were read as one value, and two cases that a row can satisfy together loaded. """ message = _refusal( - **{'dimensions.n': {'dtype': 'int'}, 'relations.two': {'key': 'g', 'values': {'a': 'n', 'b': 'n'}}}, + **{'dimensions.n': {'dtype': 'int'}, 'properties.g.a': 'n', 'properties.g.b': 'n'}, expressions={ 'pick': { 'dims': ['g'], @@ -1175,14 +1171,14 @@ def test_two_columns_of_one_relation_are_not_proved_apart_in_a_case_when(self, f ) assert f'both claim the value where {witness}' in message, message - def test_a_read_through_a_relation_is_undecidable_in_a_case_when(self): - """Which rows a relation maps onto a coordinate is the data's to say, so two cases split by one are not proved apart.""" + def test_a_read_through_a_property_is_undecidable_in_a_case_when(self): + """Which rows a property maps onto a coordinate is the data's to say, so two cases split by one are not proved apart.""" message = _refusal( expressions={ 'pick': { 'dims': ['g'], 'cases': { - 'mapped': {'when': 'at(r, by=lk[h])', 'expression': '1'}, + 'mapped': {'when': 'at(r, by=g[h])', 'expression': '1'}, 'some': {'when': 'c > 0', 'expression': '2'}, }, 'otherwise': '0', @@ -1190,25 +1186,25 @@ def test_a_read_through_a_relation_is_undecidable_in_a_case_when(self): }, constraints={'cap': {'dims': ['g'], 'expression': 'p <= pick'}}, ) - assert "it reads a condition through 'lk', which only the data decides" in message + assert "it reads a condition through 'g', which only the data decides" in message - def test_a_read_landing_outside_the_frame_names_the_relation(self): + def test_a_read_landing_outside_the_frame_names_the_subject(self): """The read adds the dims it lands on, so a mask over the coarse side cannot carry the fine one.""" - message = _refusal(constraints={'cap': {'dims': ['h'], 'where': 'at(r, by=lk[h])', 'expression': 'r <= 1'}}) - assert "a condition read through 'lk' reads dims ['g'] outside the declared dims ['h']" in message + message = _refusal(constraints={'cap': {'dims': ['h'], 'where': 'at(r, by=g[h])', 'expression': 'r <= 1'}}) + assert "a condition read through 'g' reads dims ['g'] outside the declared dims ['h']" in message def test_a_read_given_an_edge_is_not_told_about_translations(self): """The edge sentence explains a shift; under a read it would explain an operator the file did not write.""" with pytest.raises(LanguageError) as caught: - where_of('at(r, by=lk[h], edge=0)', Namespace(_spec()), 'probe') + where_of('at(r, by=g[h], edge=0)', Namespace(_spec()), 'probe') assert 'translation' not in str(caught.value) - def test_a_read_lands_on_the_dims_it_produces_and_reads_the_relation(self): - """The mask is over what the relation maps onto, and a consumer attaches the relation as well as the operand.""" - mask = where_of("at(h == 'north', by=lk[h])", Namespace(_spec()), 'probe') + def test_a_read_lands_on_the_dims_it_produces_and_reads_the_property(self): + """The mask is over what the property maps from, and a consumer attaches its table as well as the operand.""" + mask = where_of("at(h == 'north', by=g[h])", Namespace(_spec()), 'probe') assert mask is not None - assert sorted(mask.dims) == ['g'], "'h' is read at lk(g), so g is all the mask is over" - assert mask.names_read == frozenset({'lk'}), 'the relation is data a consumer attaches, the label is not' + assert sorted(mask.dims) == ['g'], "'h' is read at h(g), so g is all the mask is over" + assert mask.names_read == frozenset({'g.h'}), 'the property is data a consumer attaches, the label is not' def test_a_count_reduces_the_dim_it_counts_along_away(self): """The count is one number per remaining coordinate, so a claim about each group needs no word for the group.""" @@ -1313,236 +1309,198 @@ class TestRulesDecidedWithoutData: ("its expansion writes variable 's_seg'",), id='sos-whose-expansion-collides-with-an-entry', ), - pytest.param( - {'relations.tag': {'key': 'g', 'dtype': 'str'}}, - ("unknown key 'dtype' in a relation entry. Valid keys: description, key, missing, values.",), - id='relation-with-a-dtype-of-its-own', - ), - pytest.param({'relations.tag': {'key': 'g'}}, ('has 1 column(s)',), id='relation-with-one-column'), - pytest.param( - {'relations.lk.values': 'z'}, ("references undeclared dimension 'z'",), id='relation-over-undeclared' - ), - pytest.param( - {'relations.lk.key': ['g', 'h']}, - ("names column 'h' under both 'key:' and 'values:'", 'values: {: h}'), - id='relation-naming-one-column-on-both-sides', - ), - pytest.param( - {'relations.lk': {'key': [], 'values': ['g', 'h']}}, - ('names no key column', "Name the columns that identify a row under 'key:'"), - id='relation-with-an-empty-key', - ), - pytest.param( - {'relations.pair': {'key': {'g0': 'g', 'g1': 'g'}, 'values': 'h'}}, - ("has two key columns over 'g' (['g0', 'g1'])", 'Key the table by one column over each dimension'), - id='relation-keyed-twice-over-one-dimension', - ), - pytest.param( - {'relations.odd': {'key': {'h': 'g'}, 'values': {'x': 'h'}}}, - ("names column 'h' after dimension 'h', but the column is over 'g'",), - id='relation-column-named-after-a-dimension-it-is-not-over', - ), - pytest.param( - {'relations.lk.key': ['g', 'z']}, - ("references undeclared dimension 'z'",), - id='relation-key-undeclared', - ), - pytest.param( - {'relations.lk': {'key': ['g', 'g']}}, - ("names dimension 'g' twice under 'key:'", 'key: {g0: g, g1: g}'), - id='relation-keying-a-dim-twice-without-roles', - ), - pytest.param( - {'relations.lk.values': ['h', 'h']}, - ("names dimension 'h' twice under 'values:'", 'values: {h0: h, h1: h}'), - id='relation-valuing-a-dim-twice-without-roles', - ), - pytest.param({'relations.lk': {'key': []}}, ('has 0 column(s)',), id='relation-with-no-columns'), pytest.param( { 'dimensions.z': {}, - 'relations.lk': {'key': ['g', 'z'], 'values': 'h'}, + 'sets.gz': {'dims': ['g', 'z']}, + 'properties.gz.h': 'h', 'variables.q.dims': ['g', 'h', 'z'], - 'objective': {'expression': 'sum(sum(q, over=g, by=lk[h]))'}, + 'objective': {'expression': 'sum(sum(q, over=g, by=gz[h]))'}, }, - ("sum(by=lk[h]) groups by ['h'], which the expression already carries",), + ("sum(by=gz[h]) groups by ['h'], which the expression already carries",), id='grouping-by-a-dim-the-operand-carries', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, over=g, by=lk[z]))'}}, - ("sum(by=lk[z]) names ['z'], which is no column of 'lk'. Name one of its columns, ['g', 'h']",), - id='a-column-the-relation-lacks', + {'objective': {'expression': 'sum(sum(p, over=g, by=g[z]))'}}, + ("sum(by=g[z]) names ['z'], which is no property of 'g'. Name one of its properties, ['h']",), + id='a-property-the-subject-lacks', ), pytest.param( { 'dimensions.z': {}, - 'relations.bare': {'key': {'a': 'g', 'b': 'g', 'm': 'z'}}, + 'sets.bare': {'dims': {'a': 'g', 'b': 'g', 'm': 'z'}}, 'objective': {'expression': 'sum(sum(p, over=g, by=bare[m]))'}, }, ( "over=g names no column of 'bare', so the sum reads nothing through 'bare'", "the columns over 'g' are ['a', 'b']", ), - id='a-dimension-two-columns-are-over-is-no-column', + id='a-dimension-two-members-are-over-is-no-column', ), pytest.param( { 'dimensions.z': {}, - 'relations.lz': {'key': 'g', 'values': 'z'}, - 'objective': {'expression': 'sum(sum(q, over=h, by=lz[z]))'}, + 'properties.h.z': 'z', + 'objective': {'expression': 'sum(sum(p, over=g, by=h[z]))'}, }, - ("over=h names no column of 'lz', so the sum reads nothing through 'lz'", "its columns are ['g', 'z']"), - id='a-sum-reading-nothing-through-its-relation', + ("over=g names no column of 'h', so the sum reads nothing through 'h'", "its columns are ['h', 'z']"), + id='a-sum-reading-nothing-through-its-subject', ), pytest.param( - { - 'dimensions.z': {}, - 'relations.lz': {'key': 'g', 'values': 'z'}, - 'objective': {'expression': 'sum(sum(p, over=lz[g], by=lk[h]))'}, - }, - ('sum(over=lz[g]) names a relation, and over= takes bare names. Write over=g',), - id='a-selection-in-over-beside-by', + {'objective': {'expression': 'sum(sum(p, over=g[h], by=g[h]))'}}, + ('sum(over=g[h]) reads a subject, and over= takes bare names. Write over=h',), + id='a-read-in-over-beside-by', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, over=h, by=lk[h]))'}}, + {'objective': {'expression': 'sum(sum(p, over=h, by=g[h]))'}}, ("over= and by= both name ['h']",), id='over-and-by-the-same-column', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, over=nope, by=lk[h]))'}}, - ("over=nope names no column of 'lk', whose columns are ['g', 'h'], and no dimension",), + {'objective': {'expression': 'sum(sum(p, over=nope, by=g[h]))'}}, + ("over=nope names no column of 'g', whose columns are ['g', 'h'], and no dimension",), id='a-name-neither-a-column-nor-a-dimension', ), pytest.param( { 'dimensions.z': {}, - 'relations.lz': {'key': 'g', 'values': ['h', 'z']}, - 'objective': {'expression': 'sum(sum(p, over=g, by=lz[h, h]))'}, + 'properties.g.z': 'z', + 'objective': {'expression': 'sum(sum(p, over=g, by=g[h, h]))'}, }, - ('sum(by=lz[h, h]) names a column twice',), + ('sum(by=g[h, h]) names a column twice',), id='a-to-list-naming-a-column-twice', ), pytest.param( { - 'relations.lz': {'key': 'g', 'values': {'h0': 'h', 'h1': 'h'}}, - 'objective': {'expression': 'sum(sum(p, over=[h0, h1], by=lz[g]))'}, + 'dimensions.z': {}, + 'properties.g': {'h0': 'h', 'h1': 'h', 'z': 'z'}, + 'objective': {'expression': 'sum(sum(p, over=[h0, h1], by=g[z]))'}, }, ("['h0', 'h1'] are columns over one dimension, ['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, within=lk[h], over=g))'}}, + {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=g[h], over=g))'}}, ( "shift() expects shift(, along=, offset=[, edge='wrap'|]" - '[, within=[]])', + '[, within=[]])', ), id='a-partition-takes-no-from', ), pytest.param( { 'dimensions.z': {}, - 'relations.lz': {'key': 'g', 'values': ['h', 'z']}, - 'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=lz[g]))'}, + 'sets.gz': {'dims': ['g', 'z']}, + 'properties.gz.w': 'h', + 'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=gz[g, w]))'}, }, - ("within= names ['g'], a key column of 'lz'. Name one of its value columns",), - id='a-partition-grouped-within-a-key-column', + ("within= names ['g'], a member of 'gz'. Name one of its properties, ['w']",), + id='a-partition-grouped-within-a-member', ), pytest.param( - {'variables.q.where': 'position(g, within=lk[z]) == 0'}, - ( - "position(within=lk[z]) names ['z'], which is no column of 'lk'. Name one of its columns, ['g', 'h']", - ), - id='position-within-a-column-the-relation-lacks', + {'variables.q.where': 'position(g, within=g[z]) == 0'}, + ("position(within=g[z]) names ['z'], which is no property of 'g'. Name one of its properties, ['h']",), + id='position-within-a-property-the-subject-lacks', ), pytest.param( - {'objective': {'expression': 'sum(sum(p, over=lk[g]))'}}, - ('sum(over=lk[g]) names a relation, and over= takes bare names. Write over=g',), - id='a-selection-in-over-without-by', + {'objective': {'expression': 'sum(sum(p, over=g[h]))'}}, + ('sum(over=g[h]) reads a subject, and over= takes bare names. Write over=h',), + id='a-read-in-over-without-by', ), pytest.param( - {'relations.rel': {'key': ['g', 'h']}, 'objective': {'expression': 'sum(sum(p, by=rel[h]))'}}, - ('sum() through a relation does not name over=', 'names in over= the columns it sums away'), - id='a-sum-through-a-relation-names-what-leaves', + {'sets.rel': {'dims': ['g', 'h']}, 'objective': {'expression': 'sum(sum(p, by=rel[h]))'}}, + ( + 'sum() through a set or a property does not name over=', + 'names in over= the members it sums away', + ), + id='a-sum-through-a-set-names-what-leaves', ), pytest.param( - {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=lk))'}}, - ('shift(within=lk) names the relation and none of its columns. Write within=lk[]',), - id='a-partition-names-the-columns-it-groups-by', + { + 'sets.rel': {'dims': ['g', 'h']}, + 'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=rel))'}, + }, + ('shift(within=rel) names the set and none of its members or properties. Write within=rel[]',), + id='a-partition-names-what-it-groups-by', ), pytest.param( - {'objective': {'expression': 'sum(sum_back(p, along=g, window=2, within=lk))'}}, - ('sum_back(within=lk) names the relation and none of its columns',), - id='a-window-names-the-columns-it-groups-by', + { + 'sets.rel': {'dims': ['g', 'h']}, + 'objective': {'expression': 'sum(sum_back(p, along=g, window=2, within=rel))'}, + }, + ('sum_back(within=rel) names the set and none of its members or properties',), + id='a-window-names-what-it-groups-by', ), pytest.param( - {'variables.q.where': 'position(g, within=lk) == 0'}, + {'sets.rel': {'dims': ['g', 'h']}, 'variables.q.where': 'position(g, within=rel) == 0'}, ( - 'position(within=lk) names the relation and none of its columns', - 'within=lk[]', - "with a column of 'lk': ['g', 'h']", + 'position(within=rel) names the set and none of its members or properties', + 'within=rel[]', + "with a member or a property of 'rel': ['g', 'h']", ), - id='a-position-names-the-columns-it-counts-within', + id='a-position-names-what-it-counts-within', ), pytest.param( - { - 'relations.rel': {'key': ['g', 'h']}, - 'objective': {'expression': 'sum(at(r, by=rel[h]))'}, - }, - ("'rel' has every column in its key", 'no value column for at() to read. Sum through it instead.'), - id='at-through-a-bare-relation', + {'sets.rel': {'dims': ['g', 'h']}, 'objective': {'expression': 'sum(at(r, by=rel[h]))'}}, + ( + "'rel' is read by its members alone, so it has no property for at() to read. Sum through it instead.", + ), + id='at-through-a-set', ), pytest.param( - {'objective': {'expression': 'sum(sum(q, over=h, by=lk[g]))'}}, + { + 'dimensions.z': {}, + 'sets.gz': {'dims': ['g', 'z']}, + 'properties.gz.w': 'h', + 'objective': {'expression': 'sum(sum(q, over=w, by=gz[g, z]))'}, + }, ( - "the columns this sum groups by, ['g'], hold the whole key ['g']", + "the columns this sum groups by, ['g', 'z'], hold the whole key ['g', 'z']", 'so every group is one row and the sum adds nothing', - 'at(..., by=lk[h])', + 'at(..., by=gz[w])', ), id='a-sum-grouped-by-the-whole-key-is-a-lookup', ), pytest.param( { - 'relations.rel': {'key': ['g', 'h']}, + 'sets.rel': {'dims': ['g', 'h']}, 'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=0, within=rel[h]))'}, }, - ("'rel' has every column in its key", 'so it makes no groups'), - id='a-partition-through-a-bare-relation', - ), - pytest.param( - {'relations.rel': {'key': ['g', 'h']}, 'variables.q.where': "rel == 'x'"}, - ("compares a column of 'rel', which has every column in its key", 'Declare that column under values:'), - id='where-compares-a-bare-relation', + ("'rel' is read by its members alone, so it makes no groups",), + id='a-partition-through-a-set', ), pytest.param( - {'variables.q.where': "lk[g] == 'x'"}, + {'sets.rel': {'dims': ['g', 'h']}, 'variables.q.where': "rel == 'x'"}, ( - "'g' is a key column of 'lk'. Compare the coordinate itself", - 'g == ...', + "'rel' compares the set 'rel', which holds no value", + 'Compare one of its properties, rel[]', ), - id='where-compares-a-key-column', + id='where-compares-a-set', ), pytest.param( - { - 'relations.pair': {'key': {'g0': 'g', 'g1': 'g'}}, - 'variables.q.where': 'pair', - }, - ('has two columns over one dimension', 'Compare a column: pair[g1] =='), - id='where-bare-name-of-a-relation-with-two-columns-over-one-dim', + {'variables.q.where': "g[g] == 'x'"}, + ("'g[g]': 'g' is a member of 'g'. Compare the coordinate itself", 'g == ...'), + id='where-compares-a-member', ), pytest.param( - {'relations.g': {'key': 'h', 'values': 'g'}}, - ("Relation 'g' collides with the dimension",), - id='relation-named-after-a-dimension', + {'sets.pair': {'dims': {'g0': 'g', 'g1': 'g'}}, 'variables.q.where': 'pair'}, + ('has two members over one dimension', 'sum(..., over=g0, by=pair[g1])'), + id='where-bare-name-of-a-set-with-two-members-over-one-dim', ), pytest.param( - {'relations.lk.rows': {'g1': 'h1'}}, - ("unknown key 'rows' in a relation entry", 'Valid keys'), - id='a-relation-declaring-its-map', + {'sets.g': {'dims': ['g', 'h']}}, + ("Set 'g' collides with the dimension",), + id='set-named-after-a-dimension', ), pytest.param( - {'relations.lk.value': 'h'}, - ("unknown key 'value' in a relation entry", "Did you mean 'values'?"), - id='a-relation-naming-one-value-column', + {'properties.g.h': {'into': 'h', 'rows': {'g1': 'h1'}}}, + ("unknown key 'rows' in a property entry", 'Valid keys'), + id='a-property-declaring-its-map', + ), + pytest.param( + {'sets.rel': {'dim': ['g', 'h']}}, + ("unknown key 'dim' in a set entry", "Did you mean 'dims'?"), + id='a-set-misspelling-dims', ), pytest.param({'variables.p.missing': 0}, ('missing: 0 needs a `where:`',), id='zero-without-a-mask'), pytest.param( @@ -1620,9 +1578,9 @@ class TestRulesDecidedWithoutData: id='a-dimension-as-a-value', ), pytest.param( - {'objective': {'expression': 'sum(lk + p, over=g)'}}, - ("'lk' is a relation, and a relation is not a value",), - id='a-relation-as-a-value', + {'sets.rel': {'dims': ['g', 'h']}, 'objective': {'expression': 'sum(rel + p, over=g)'}}, + ("'rel' is a set, and a set is not a value",), + id='a-set-as-a-value', ), pytest.param( {'objective': {'expression': 'sum(shift(p, along=g, offset=1, edge=wrap), over=g)'}}, @@ -1653,25 +1611,20 @@ class TestRulesDecidedWithoutData: id='an-edge-that-is-an-expression', ), pytest.param( - { - 'relations.lk2': {'key': 'g', 'values': 'z'}, - 'dimensions.z': {}, - 'objective': {'expression': 'sum(sum(p, over=g, by=[lk, lk2]))'}, - }, - ('sum(by=...) takes columns of one relation. Write relation[column]',), - id='several-relations-in-one-by', + {'objective': {'expression': 'sum(sum(p, over=g, by=[g, h]))'}}, + ('sum(by=...) takes what one subject reads. Write subject[property]',), + id='several-subjects-in-one-by', ), pytest.param( - {'objective': {'expression': 'sum(at(c, by=lk[h]))'}}, - ("at(by=lk[h]) joins on ['h'], which the expression does not carry (dims ['g'])",), + {'objective': {'expression': 'sum(at(c, by=g[h]))'}}, + ("at(by=g[h]) joins on ['h'], which the expression does not carry (dims ['g'])",), id='a-lookup-whose-operand-lacks-the-column-it-joins-on', ), pytest.param( { 'dimensions.z': {}, - # only a bare relation may key two columns over one dimension: a key that - # determines a value is refused for it at the entry - 'relations.bare': {'key': {'k': 'g', 'j0': 'h', 'j1': 'h', 'm': 'z'}}, + # only a set with no property may have two members over one dimension + 'sets.bare': {'dims': {'k': 'g', 'j0': 'h', 'j1': 'h', 'm': 'z'}}, 'objective': {'expression': 'sum(sum(q, over=k, by=bare[m]))'}, }, ("joins 'bare' on ['h'] through more than one column",), @@ -1680,11 +1633,12 @@ class TestRulesDecidedWithoutData: pytest.param( { 'dimensions.z': {}, - 'relations.lz': {'key': ['g', 'z'], 'values': 'h'}, - 'variables.q.where': 'lk != lz', + 'sets.gz': {'dims': ['g', 'z']}, + 'properties.gz.h': 'h', + 'variables.q.where': 'g[h] != gz[h]', }, - ('compares relations keyed over different dimensions',), - id='where-two-relations-with-different-keys', + ('compares properties of subjects over different dimensions',), + id='where-two-properties-of-subjects-over-different-dimensions', ), pytest.param( {'variables.p.where': 'c > q'}, @@ -1692,9 +1646,9 @@ class TestRulesDecidedWithoutData: id='where-against-a-variable', ), pytest.param( - {'variables.p.where': 'c > lk'}, - ('against relation', 'Compare against a literal'), - id='where-against-a-relation', + {'sets.rel': {'dims': ['g', 'h']}, 'variables.p.where': 'c > rel'}, + ("against the set 'rel', which holds no value", 'Compare against a literal'), + id='where-against-a-set', ), pytest.param( {'variables.p.where': 'c > h'}, @@ -1767,7 +1721,7 @@ class TestRulesDecidedWithoutData: {'variables.p.where': "c[h] == 'x'"}, ( "'c[h]' reads a column of 'c', which is a parameter", - 'Only a relation, a set or a dimension has columns', + 'Only a set or a dimension has properties', ), id='where-a-column-of-a-parameter', ), @@ -1777,22 +1731,17 @@ class TestRulesDecidedWithoutData: id='where-a-variable-on-the-left', ), pytest.param( - {'dimensions.z': {}, 'relations.lk.values': ['h', 'z'], 'variables.p.where': "lk == 'x'"}, - ("'lk' has 2 value columns (['h', 'z'])", 'so name one: lk[h]'), - id='where-a-relation-of-two-value-columns-read-bare', + {'variables.p.where': "g[zz] == 'x'"}, + ("'g[zz]' reads 'zz', which is no property of 'g'",), + id='where-a-property-the-subject-lacks', ), pytest.param( - {'variables.p.where': "lk[zz] == 'x'"}, - ("'zz' is not a column of 'lk', whose columns are ['g', 'h']",), - id='where-a-column-the-relation-lacks', - ), - pytest.param( - {'dimensions.z': {}, 'relations.lz': {'key': 'g', 'values': 'z'}, 'variables.p.where': 'lk == lz'}, + {'dimensions.z': {}, 'properties.g.z': 'z', 'variables.p.where': 'g[h] == g[z]'}, ( - "compares 'lk' (a column over 'h') with 'lz' (a column over 'z')", - 'Compare two columns over the same dimension', + "compares 'g[h]' (into 'h') with 'g[z]' (into 'z')", + 'Compare two properties into the same dimension', ), - id='where-two-relations-whose-columns-are-over-different-dimensions', + id='where-two-properties-into-different-dimensions', ), ], ) @@ -1804,10 +1753,10 @@ def test_a_rule_decided_without_data(self, patch, fragments): @pytest.mark.parametrize( ('operand', 'call', 'patch'), [ - pytest.param('q', 'sum(q, over=h, by=lk[g])', {}, id='over-names-only-columns'), + pytest.param('q', 'sum(q, over=w, by=gz[g, z])', {}, id='over-names-only-columns'), pytest.param( 'qt', - 'sum(qt, over=[h, t], by=lk[g])', + 'sum(qt, over=[w, t], by=gz[g, z])', {'dimensions.t': {}, 'variables.qt': {'dims': ['g', 'h', 't']}}, id='over-names-a-dimension-beside-the-columns', ), @@ -1820,7 +1769,7 @@ def test_the_rewrite_a_sum_grouped_by_the_whole_key_names_is_one_the_language_ta for grouping by the whole key with `at(..., over=, into=)` — which `at` refuses in turn, for grouping in a way that leaves several rows per group. Later, the message held out the bare - `at(..., by=lk[h])` where `over=` also named a dimension with no + `at(..., by=g[h])` where `over=` also named a dimension with no column, and that `at` left the dimension in the frame. The rewrite moves the columns in `over=` into `at`'s `by=`, and sums each dimension in `over=` that has no column around the `at`. @@ -1829,32 +1778,12 @@ def test_the_rewrite_a_sum_grouped_by_the_whole_key_names_is_one_the_language_ta point: a fragment can agree with a message that names a call nothing accepts. """ - message = _refusal(**{'constraints.k': {'dims': ['g'], 'expression': f'{call} >= 0'}}, **patch) + patch = {'dimensions.z': {}, 'sets.gz': {'dims': ['g', 'z']}, 'properties.gz.w': 'h', **patch} + message = _refusal(**{'constraints.k': {'dims': ['g', 'z'], 'expression': f'{call} >= 0'}}, **patch) named = re.search(r'Write (.*?), or group', message) assert named is not None, f'the refusal holds out no call to write instead: {message}' rewrite = named.group(1).replace('...', operand) - _spec(**{'constraints.k': {'dims': ['g'], 'expression': f'{rewrite} >= 0'}}, **patch) - - @pytest.mark.parametrize( - ('patch', 'where'), - [ - pytest.param( - {'dimensions.z': {}, 'relations.lk.values': ['h', 'z']}, - "lk == 'x'", - id='a-relation-of-two-value-columns-read-bare', - ), - ], - ) - def test_the_column_a_bare_comparison_is_told_to_read_is_one_the_language_takes(self, patch, where): - """A refusal that names a column is holding out a rewrite, so the rewrite has to load. - - Was: the message named the column as `lk.h`, which the where grammar - no longer parses. A column is written `lk[h]`. - """ - message = _refusal(**{'variables.p.where': where}, **patch) - named = re.search(r'so name one: (\S+)\.$', message) - assert named is not None, f'the refusal holds out no column to read instead: {message}' - _spec(**{'variables.p.where': where.replace('lk', named.group(1), 1)}, **patch) + _spec(**{'constraints.k': {'dims': ['g', 'z'], 'expression': f'{rewrite} >= 0'}}, **patch) class TestAssumptions: @@ -1924,7 +1853,7 @@ def test_an_entry_the_language_refuses(self, entry, fragments): pytest.param('c <= k', id='two-parameters'), pytest.param('c <= 0.5 * k', id='arithmetic-on-a-side'), pytest.param('sum(c, over=g) >= k', id='a-reduction-on-a-side'), - pytest.param("lk == 'north' AND c > 0", id='a-relation-and-a-connective'), + pytest.param("g[h] == 'north' AND c > 0", id='a-property-and-a-connective'), pytest.param({'holds': 'c > 0', 'where': 'flag'}, id='a-where-of-its-own'), pytest.param({'holds': 'c > 0', 'description': 'costs are positive'}, id='a-description'), ], @@ -1996,7 +1925,7 @@ def test_an_empty_list_survives_the_round_trip(self): def test_an_empty_section_is_not_written(self): written = to_spec(DISPATCH_MODEL).to_yaml() - assert 'relations' not in written and 'macros' not in written, 'a section declaring nothing says nothing' + assert 'sets' not in written and 'macros' not in written, 'a section declaring nothing says nothing' def test_a_default_is_written_out_and_an_absence_is_not(self): written = to_spec(DISPATCH_MODEL).to_dict() @@ -2270,7 +2199,7 @@ class TestAnEntryIsNamed: 'section', [ 'dimensions', - 'relations', + 'sets', 'parameters', 'variables', 'expressions', @@ -2283,7 +2212,7 @@ class TestAnEntryIsNamed: def test_a_name_no_expression_could_write_is_refused(self, section: str, name: str): entries: dict[str, Any] = { 'dimensions': {'dtype': 'str'}, - 'relations': {'key': 'g', 'values': 'h'}, + 'sets': {'dims': ['g', 'h']}, 'parameters': {'dims': ['g']}, 'variables': {'dims': ['g']}, 'expressions': {'expression': 'c'}, @@ -2582,9 +2511,9 @@ def test_a_bare_parameter_whose_answer_needs_the_data_loads(patch): assert spec.program.variables['p'].where is not None, 'the bare name stays a mask' -#: A parameter over the pairs of `g` and `h` that a bare relation lists, with a fill. +#: A parameter over the pairs of `g` and `h` that a set lists, with a fill. _MASKED = { - 'relations.pair': {'key': ['g', 'h'], 'missing': 'absent'}, + 'sets.pair': {'dims': ['g', 'h'], 'missing': 'absent'}, 'parameters.w': {'dims': ['g', 'h'], 'where': 'pair', 'missing': 1}, } @@ -2593,7 +2522,7 @@ def test_a_parameter_mask_is_part_of_the_program(): """A consumer reads where a parameter has coordinates off the program, and the file writes the mask back.""" spec = _spec(**_MASKED) assert spec.program.parameters['w'].where is not None, 'the mask reaches the program' - assert spec.program.parameters['w'].where.names_read == {'pair'}, 'the mask reads the relation and nothing else' + assert spec.program.parameters['w'].where.names_read == {'pair'}, 'the mask reads the set and nothing else' assert spec.program.parameters['c'].where is None, 'a parameter with no `where:` has every coordinate' assert to_spec(spec.to_yaml()).program == spec.program, 'the mask survives a round trip through YAML' @@ -2622,9 +2551,9 @@ def test_a_parameter_mask_is_part_of_the_program(): id='a-mask-that-reads-its-own-parameter-through-a-named-condition', ), pytest.param( - {'parameters.c.where': 'flag', 'parameters.flag.where': "lk == 'x'"}, + {'parameters.c.where': 'flag', 'parameters.flag.where': "g[h] == 'x'"}, "Parameter 'c': its `where:` reads 'flag', which has a mask of its own. Test what the mask of 'flag' " - 'tests, or a relation.', + 'tests, or a set or a property.', id='a-mask-that-reads-a-masked-parameter', ), pytest.param( @@ -2669,7 +2598,7 @@ def test_the_legend_says_where_a_parameter_has_coordinates(fmt: FormatName): """The mask decides which rows of the data count, so a reader of the legend sees it beside the dims.""" spec = _spec(**_MASKED, **{'variables.q.bounds': {'upper': 'w'}}) row = next(line for line in typeset(spec, fmt).splitlines() if 'only where' in line) - assert 'pair' in row, 'the legend row names the relation the mask reads' + assert 'pair' in row, 'the legend row names the set the mask reads' assert row.index('only where') < row.index('where the data has no row'), 'the mask comes before the fill' @@ -2679,45 +2608,6 @@ def test_a_given_parameter_has_no_missing(): assert "unknown key 'missing' in a given parameter entry" in message -@pytest.mark.parametrize( - ('declared', 'read'), - [ - pytest.param({'key': 'g', 'values': 'h'}, 'refused', id='left-out-is-refused'), - pytest.param({'key': 'g', 'values': 'h', 'missing': 'refused'}, 'refused', id='refused'), - pytest.param({'key': 'g', 'values': 'h', 'missing': 'absent'}, 'absent', id='absent'), - pytest.param({'key': ['g', 'h']}, 'refused', id='bare-left-out-is-refused'), - pytest.param({'key': ['g', 'h'], 'missing': 'absent'}, 'absent', id='bare-absent'), - ], -) -def test_a_relation_says_whether_every_key_combination_needs_a_row(declared, read): - """A bare relation's key is every column, and it takes `missing:` and its default as a map does.""" - spec = to_spec(varied(SMALL_MODEL, **{'relations.pair': declared})) - assert spec.program.relations['pair'].missing == read - - -@pytest.mark.parametrize( - ('relation', 'fragment'), - [ - pytest.param( - {'key': 'g', 'values': 'h', 'missing': 0}, - 'missing: 0 on a relation, which takes refused or absent', - id='a-number-as-a-value', - ), - pytest.param( - {'key': 'g', 'values': 'h', 'missing': 'h1'}, 'missing: h1 on a relation', id='a-label-as-a-value' - ), - pytest.param( - {'key': 'g', 'values': 'h', 'missing': True}, 'missing: true on a relation', id='a-flag-as-a-value' - ), - pytest.param( - {'key': 'g', 'values': 'h', 'missing': None}, 'missing: null on a relation says nothing', id='null' - ), - ], -) -def test_a_relation_missing_that_reads_nothing_is_refused(relation, fragment): - assert fragment in _refusal(**{'relations.lk': relation}) - - @pytest.mark.parametrize( ('written', 'fragment'), [ @@ -2817,7 +2707,7 @@ def test_a_variable_missing_inside_what_the_variable_takes_loads(declared): pytest.param( { 'parameters.d': {'dims': ['h'], 'missing': 0}, - 'expressions.e': 'c + at(d, by=lk[h])', + 'expressions.e': 'c + at(d, by=g[h])', 'constraints.cap': {'dims': ['g'], 'expression': 'p / e <= 1'}, }, id='looked-up-in-every-addend', diff --git a/tests/typesetting/golden/latex.out b/tests/typesetting/golden/latex.out index 91eb2d345..725771061 100644 --- a/tests/typesetting/golden/latex.out +++ b/tests/typesetting/golden/latex.out @@ -9,12 +9,12 @@ \paragraph{Sets} \begin{description} -\item[{$\mathcal{T}$}] index $t$ --- \texttt{snapshot} (\texttt{int} coordinates) with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{rep\_of}: \mathcal{T} \to \mathcal{T}$ -\item[{$\mathcal{G}$}] index $g$ --- \texttt{generator} with $\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{connection} is \texttt{absent} where the data has no row, \texttt{dispatch\_pair} is \texttt{absent} where the data has no row, \texttt{generator[fuel]} is \texttt{absent} where the data has no row -\item[{$\mathcal{B}$}] index $b$ --- \texttt{bus} with $\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{area\_of} is \texttt{absent} where the data has no row, \texttt{connection} is \texttt{absent} where the data has no row, \texttt{dispatch\_pair} is \texttt{absent} where the data has no row -\item[{$\mathcal{Z}$}] index $z$ --- \texttt{zone} with $\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{area\_of} is \texttt{absent} where the data has no row -\item[{$\mathcal{S}$}] index $s$ --- \texttt{season} with $\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}$ -\item[{$\mathcal{E}$}] index $e$ --- \texttt{technology} with $\mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E}$, \texttt{generator[fuel]} is \texttt{absent} where the data has no row +\item[{$\mathcal{T}$}] index $t$ --- \texttt{snapshot} (\texttt{int} coordinates) with $\mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{season}: \mathcal{T} \to \mathcal{S},\ \mathrm{rep}: \mathcal{T} \to \mathcal{T},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z}$ +\item[{$\mathcal{G}$}] index $g$ --- \texttt{generator} with $\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{dispatch\_pair} is \texttt{absent} where the data has no row, \texttt{connection} is \texttt{absent} where the data has no row, \texttt{generator[fuel]} is \texttt{absent} where the data has no row +\item[{$\mathcal{B}$}] index $b$ --- \texttt{bus} with $\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{dispatch\_pair} is \texttt{absent} where the data has no row, \texttt{connection} is \texttt{absent} where the data has no row, \texttt{bus[area]} is \texttt{absent} where the data has no row +\item[{$\mathcal{Z}$}] index $z$ --- \texttt{zone} with $\mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}$, \texttt{bus[area]} is \texttt{absent} where the data has no row +\item[{$\mathcal{S}$}] index $s$ --- \texttt{season} with $\mathrm{season}: \mathcal{T} \to \mathcal{S}$ +\item[{$\mathcal{E}$}] index $e$ --- \texttt{technology} with $\mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E}$, \texttt{generator[fuel]} is \texttt{absent} where the data has no row \item[{$\mathcal{A}$}] index $a$ --- \texttt{bp} \end{description} @@ -78,9 +78,9 @@ \begin{description} \item[{$t \ominus k$}] \texttt{edge='wrap'} --- $t-k$, cyclic \item[{$t \boxminus_{v} k$}] \texttt{edge=v} --- $t-k$, and $v$ past the edge -\item[{$t \ominus^{\mathrm{relation}(t)} k$}] \texttt{within=relation[...]} --- $t-k$ inside the group of $t$ +\item[{$t \ominus^{\mathrm{property}(t)} k$}] \texttt{within=subject[...]} --- $t-k$ inside the group of $t$ \item[{$\mathrm{pos}(t)$}] \texttt{position()} --- the position of $t$, counted from $0$; $t$ is the label -\item[{$\mathrm{pos}_{\mathrm{relation}(t)}(t)$}] \texttt{position(within=relation[...])} --- the position of $t$ inside its group +\item[{$\mathrm{pos}_{\mathrm{property}(t)}(t)$}] \texttt{position(within=subject[...])} --- the position of $t$ inside its group \item[{$\lvert \mathcal{T} \rvert$}] the size of $\mathcal{T}$; $\lvert \mathcal{T} \rvert - 1$ is the last position \end{description} @@ -116,7 +116,7 @@ \text{lcoe} && \mathit{lcoe} & = \frac{\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} \cdot \mathrm{cost}_{g}}{\sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g}} \\ \text{net} && \mathit{net}_{t} & = -\mathit{spill}_{t} + \mathit{slack}_{t} && \forall\, t \in \mathcal{T} \\ \text{marginal\_price} && \mathit{marginal\_price}_{b,t} & = \lambda_{\mathrm{balance},t,b} && \forall\, b \in \mathcal{B},\ t \in \mathcal{T} \\ -\text{startup\_cost} && \mathrm{startup\_cost}_{t,g} & = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season\_of}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\text{startup\_cost} && \mathrm{startup\_cost}_{t,g} & = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \end{align} \paragraph{Objective} @@ -129,8 +129,8 @@ \text{budgeted} && \mathit{spend}_{t} & \le \mathrm{budget} && \forall\, t \in \mathcal{T} \\ \text{starts} && p_{t,g} & \le \mathrm{startup\_cost}_{t,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{under\_rating} && p_{t,g} & \le \mathrm{rating}_{g,t} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{balance} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} & = \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ -\text{netted} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{net}_{t} & = \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ +\text{balance} && \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} & = \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ +\text{netted} && \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{net}_{t} & = \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ \text{ramp} && p_{t,g} - p_{t \ominus 1,g} & \le p_{t - 1,g} + \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{edges} && p_{t \boxminus_{0} 1,g} & \le p_{t \boxplus_{0} 1,g} + \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{ahead} && p_{t,g} & \le p_{t \oplus 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ @@ -138,44 +138,44 @@ \text{uncomposed} && p_{\left( t - 1 \right) \boxminus_{0} \mathrm{lead},g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{crossed} && p_{t \ominus 1,g + 1} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{lead\_time} && p_{t \boxminus_{0} \mathrm{lead},g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{in\_season} && p_{t,g} & \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{held\_in\_season} && p_{t,g} & \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{in\_season} && p_{t,g} & \le p_{t \ominus^{\mathrm{season}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{held\_in\_season} && p_{t,g} & \le p_{t \boxminus_{0}^{\mathrm{season}(t)} 1,g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{window} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t - t' < 3} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{history} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t \ominus t' < \mathrm{min\_up}} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{seasonal\_window} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season\_of}(t)} t' < 3} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ -\text{lookup} && \mathit{spill}_{t} & \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ -\text{grouped\_once} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bt.bus}(g) = b \wedge \mathrm{gen\_bt.technology}(g) = e} p_{t,g} & \le \mathrm{tech\_cap}_{b,e} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} \\ -\text{looked\_up\_once} && \mathit{units}_{g} & \le \mathrm{tech\_cap}_{\mathrm{gen\_bt.bus}(g),\mathrm{gen\_bt.technology}(g)} && \forall\, g \in \mathcal{G} \\ -\text{within\_bus} && \mathit{units}_{g} & \le \mathit{units}_{g \boxminus_{0}^{\mathrm{gen\_bt.bus}(g)} 1} && \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{gen\_bt.bus}(g),\ \mathrm{gen\_bt.technology}(g) \right)}(g) = 0 \\ +\text{seasonal\_window} && \sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season}(t)} t' < 3} \mathit{on}_{t',g} & \le \mathit{units}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ +\text{lookup} && \mathit{spill}_{t} & \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ +\text{grouped\_once} && \sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b \wedge \mathrm{technology}(g) = e} p_{t,g} & \le \mathrm{tech\_cap}_{b,e} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} \\ +\text{looked\_up\_once} && \mathit{units}_{g} & \le \mathrm{tech\_cap}_{\mathrm{bus}(g),\mathrm{technology}(g)} && \forall\, g \in \mathcal{G} \\ +\text{within\_bus} && \mathit{units}_{g} & \le \mathit{units}_{g \boxminus_{0}^{\mathrm{bus}(g)} 1} && \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{bus}(g),\ \mathrm{technology}(g) \right)}(g) = 0 \\ \text{relational} && \sum_{g \in \mathcal{G} \,:\, \left( g,\ b \right) \in \mathrm{connection}} p_{t,g} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \\ \text{connected} && p_{t,g} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \left( g,\ b \right) \in \mathrm{connection} \\ -\text{representative} && \sum_{t' \in \mathcal{T} \,:\, \mathrm{rep\_of}(t') = t} \mathit{spill}_{t'} & \le \mathit{spill}_{\mathrm{rep\_of}(t)} && \forall\, t \in \mathcal{T} \\ +\text{representative} && \sum_{t' \in \mathcal{T} \,:\, \mathrm{rep}(t') = t} \mathit{spill}_{t'} & \le \mathit{spill}_{\mathrm{rep}(t)} && \forall\, t \in \mathcal{T} \\ \text{by\_fuel} && \sum_{g \in \mathcal{G} \,:\, \mathrm{fuel}(g) = e} p_{t,g} & \le \mathrm{budget} && \forall\, t \in \mathcal{T},\ e \in \mathcal{E} \\ \text{by\_area} && \sum_{g \in \mathcal{G},\ b \in \mathcal{B} \,:\, \mathrm{area}(g,\ b) = z} p_{t,g} \cdot \mathrm{link\_eff}_{g,b} & \le \mathrm{zone\_cap}_{z} && \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} \\ \text{fuelled} && p_{t,g} & \le \mathrm{budget} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{fuel}(g) \text{ is defined} \\ -\text{zonal} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} & \le \mathrm{zone\_cap}_{z} && \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} \\ -\text{zonal\_history} && \sum_{t \in \mathcal{T} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} & \le \mathrm{zone\_cap}_{z} && \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} \\ -\text{zonal\_membership} && \sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) \text{ is defined}} \mathit{units}_{g} & \le \mathrm{budget} && \forall\, t \in \mathcal{T} \\ -\text{zonal\_lookup} && p_{t,g} & \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{gen\_zone}(g,\ t)} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{gen\_zone}(g,\ t)}(g) = 0 \\ +\text{zonal} && \sum_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} & \le \mathrm{zone\_cap}_{z} && \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} \\ +\text{zonal\_history} && \sum_{t \in \mathcal{T} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} & \le \mathrm{zone\_cap}_{z} && \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} \\ +\text{zonal\_membership} && \sum_{g \in \mathcal{G} \,:\, \left( g,\ t \right) \in \mathrm{generator\_snapshot}} \mathit{units}_{g} & \le \mathrm{budget} && \forall\, t \in \mathcal{T} \\ +\text{zonal\_lookup} && p_{t,g} & \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{zone}(g,\ t)} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{zone}(g,\ t)}(g) = 0 \\ \text{arithmetic} && \sum_{g \in \mathcal{G}} \left( \frac{p_{t,g}}{2} - \mathrm{cost}_{g} + 10^{-5} \cdot p_{t,g} + 2.5 \times 10^{-7} \cdot \mathrm{cost}_{g} + 0.5 \cdot p_{t,g} \right) & \ge -\left( \sum_{g \in \mathcal{G}} p_{t,g} \right) \cdot \left( -3 \right) && \forall\, t \in \mathcal{T} \\ \text{total} && \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} & \le \mathrm{budget} \\ \text{scalar} && \mathit{units}_{g} & \le \mathrm{budget} && \forall\, g \in \mathcal{G} \,:\, \mathrm{cost}_{g} \text{ is defined} \\ \text{running} && \theta_{b} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \theta_{b} \text{ exists} \wedge t \ge 3 \\ -\text{first} && \mathit{on}_{t,g} & = 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = 0 \\ -\text{last} && \mathit{on}_{t,g} & = 0 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season\_of}(t)} \rvert - 1 \\ -\text{northern} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} \\ +\text{first} && \mathit{on}_{t,g} & = 1 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = 0 \\ +\text{last} && \mathit{on}_{t,g} & = 0 && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season}(t)} \rvert - 1 \\ +\text{northern} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone}(b) \neq \mathrm{area}(b) \wedge \mathrm{zone}(b) \text{ is defined} \\ \text{efficiency} && p_{t,g} & \le \mathrm{eta}_{g} \cdot \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \\ \text{ceiling} && \theta_{b} & \le \infty && \forall\, b \in \mathcal{B} \\ \text{always} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \\ \text{redundant} && \mathit{spill}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \mathit{spill}_{t} \text{ exists} \\ \text{never} && \mathit{slack}_{t} & \ge 0 && \forall\, t \in \mathcal{T} \,:\, \bot \\ \text{margin} && p_{t,g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{p}^{\mathrm{max}}_{g} - \mathrm{p}^{\mathrm{min}}_{g} > \frac{\mathrm{cost}_{g}}{2} \\ -\text{ramped} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \wedge \mathrm{pos}(t) > 0 \\ +\text{ramped} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} \wedge \mathrm{pos}(t) > 0 \\ \text{covered} && \sum_{t \in \mathcal{T},\ g \in \mathcal{G}} p_{t,g} & \le \mathrm{budget} && \text{where } \sum_{g \in \mathcal{G}} \mathrm{p}^{\mathrm{max}}_{g} \ge \mathrm{budget} \\ \text{counted} && \theta_{b} & \le \mathrm{budget} && \forall\, b \in \mathcal{B} \,:\, \lvert \{ e \in \mathcal{E} \,:\, \mathrm{tech\_cap}_{b,e} > 0 \} \rvert \ge 2 \\ \text{counted\_here} && \theta_{b} & \le \mathrm{tech\_cap}_{b,e} && \forall\, b \in \mathcal{B},\ e \in \mathcal{E} \,:\, \lvert \{ e' \in \mathcal{E} \,:\, \mathrm{tech\_cap}_{b,e'} > 0 \} \rvert \ge 2 \\ \text{run\_start} && \mathit{slack}_{t} & \le \mathrm{load}_{t,b} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} \text{ is defined} \wedge \neg \left( \mathrm{load}_{t - 1,b} \text{ is defined} \right) \\ -\text{zoned} && \theta_{b} & \le \mathrm{budget} && \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \text{ is defined} \\ +\text{zoned} && \theta_{b} & \le \mathrm{budget} && \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone}(b)} \text{ is defined} \\ \text{capped} && p_{t,g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{spend}^{\mathrm{cap}}_{g} > 0 \vee \neg \mathrm{is\_flexible}_{g} \\ \text{masked} && p_{t,g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{priced\_dispatch}_{g} \wedge \mathrm{pos}(t) > 0 \\ \text{adjacent} && \left( \mathit{weight}_{t,g} \right)_{g \in \mathcal{G}} & \in \mathrm{SOS}2 && \forall\, t \in \mathcal{T} \\ @@ -191,11 +191,11 @@ \text{bounds\_do\_not\_cross} && \mathrm{p}^{\mathrm{min}}_{g} & \le \mathrm{p}^{\mathrm{max}}_{g} && \forall\, g \in \mathcal{G} \\ \text{efficiency\_is\_a\_fraction} && \mathrm{eta}_{g} > 0 \wedge \mathrm{eta}_{g} \le 1 & && \forall\, g \in \mathcal{G} \\ \text{lead\_times\_are\_short} && \mathrm{lead}_{g} & \le 3 && \forall\, g \in \mathcal{G} \\ -\text{zones\_agree} && \mathrm{zone\_of}(b) & = \mathrm{area\_of}(b) && \forall\, b \in \mathcal{B} \\ +\text{zones\_agree} && \mathrm{zone}(b) & = \mathrm{area}(b) && \forall\, b \in \mathcal{B} \\ \text{budget\_covers\_the\_peak} && \sum_{g \in \mathcal{G}} \mathrm{p}^{\mathrm{max}}_{g} & \ge \mathrm{budget} \\ \text{ramps\_are\_gentle} && \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} & \le \mathrm{budget} && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{pos}(t) > 0 \\ \text{flexible\_units\_have\_headroom} && \mathrm{p}^{\mathrm{min}}_{g} & < \mathrm{p}^{\mathrm{max}}_{g} && \forall\, g \in \mathcal{G} \,:\, \mathrm{is\_flexible}_{g} \\ -\text{northern\_demand\_is\_real} && \mathrm{load}_{t,b} & \ge 0 && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} \\ +\text{northern\_demand\_is\_real} && \mathrm{load}_{t,b} & \ge 0 && \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} \\ \text{fuel\_curve\_complete} && \mathrm{bp\_x}_{g,a} \text{ is defined} \wedge \mathrm{bp\_y}_{g,a} \text{ is defined} \wedge \mathrm{bp\_heat}_{g,a} \text{ is defined} & && \forall\, g \in \mathcal{G},\ a \in \mathcal{A} \\ \text{cost\_curve\_complete} && \mathrm{bp\_x}_{g,a} \text{ is defined} \wedge \mathrm{bp\_y}_{g,a} \text{ is defined} & && \forall\, g \in \mathcal{G},\ a \in \mathcal{A} \,:\, \mathrm{bp\_run}_{g,a} \\ \text{cost\_curve\_contiguous} && \lvert \{ a \in \mathcal{A} \,:\, \mathrm{bp\_run}_{g,a} \wedge \neg \mathrm{bp\_run}_{g,a - 1} \} \rvert & = 1 && \forall\, g \in \mathcal{G} \\ diff --git a/tests/typesetting/golden/markdown.out b/tests/typesetting/golden/markdown.out index 235c0c725..7cda2a793 100644 --- a/tests/typesetting/golden/markdown.out +++ b/tests/typesetting/golden/markdown.out @@ -6,12 +6,12 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 | Symbol | Meaning | |---|---| -| $`\mathcal{T}`$ | index $`t`$ — `snapshot` (`int` coordinates) with $`\mathrm{season\_of}: \mathcal{T} \to \mathcal{S},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{rep\_of}: \mathcal{T} \to \mathcal{T}`$ | -| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row | -| $`\mathcal{B}`$ | index $`b`$ — `bus` with $`\mathrm{gen\_bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `area_of` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row | -| $`\mathcal{Z}`$ | index $`z`$ — `zone` with $`\mathrm{zone\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area\_of}: \mathcal{B} \to \mathcal{Z},\ \mathrm{gen\_zone}: \mathcal{G} \times \mathcal{T} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `area_of` is `absent` where the data has no row | -| $`\mathcal{S}`$ | index $`s`$ — `season` with $`\mathrm{season\_of}: \mathcal{T} \to \mathcal{S}`$ | -| $`\mathcal{E}`$ | index $`e`$ — `technology` with $`\mathrm{gen\_bt}: \mathcal{G} \to \mathcal{B} \times \mathcal{E},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E}`$, `generator[fuel]` is `absent` where the data has no row | +| $`\mathcal{T}`$ | index $`t`$ — `snapshot` (`int` coordinates) with $`\mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{season}: \mathcal{T} \to \mathcal{S},\ \mathrm{rep}: \mathcal{T} \to \mathcal{T},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z}`$ | +| $`\mathcal{G}`$ | index $`g`$ — `generator` with $`\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{generator\_snapshot} \subseteq \mathcal{G} \times \mathcal{T},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row | +| $`\mathcal{B}`$ | index $`b`$ — `bus` with $`\mathrm{dispatch\_pair} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{connection} \subseteq \mathcal{G} \times \mathcal{B},\ \mathrm{bus}: \mathcal{G} \to \mathcal{B},\ \mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `bus[area]` is `absent` where the data has no row | +| $`\mathcal{Z}`$ | index $`z`$ — `zone` with $`\mathrm{zone}: \mathcal{B} \to \mathcal{Z},\ \mathrm{area}: \mathcal{B} \to \mathcal{Z},\ \mathrm{zone}: \mathrm{generator\_snapshot} \to \mathcal{Z},\ \mathrm{area}: \mathrm{dispatch\_pair} \to \mathcal{Z}`$, `bus[area]` is `absent` where the data has no row | +| $`\mathcal{S}`$ | index $`s`$ — `season` with $`\mathrm{season}: \mathcal{T} \to \mathcal{S}`$ | +| $`\mathcal{E}`$ | index $`e`$ — `technology` with $`\mathrm{fuel}: \mathcal{G} \to \mathcal{E},\ \mathrm{technology}: \mathcal{G} \to \mathcal{E}`$, `generator[fuel]` is `absent` where the data has no row | | $`\mathcal{A}`$ | index $`a`$ — `bp` | #### Parameters @@ -80,9 +80,9 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 |---|---| | $`t \ominus k`$ | `edge='wrap'` — $`t-k`$, cyclic | | $`t \boxminus_{v} k`$ | `edge=v` — $`t-k`$, and $`v`$ past the edge | -| $`t \ominus^{\mathrm{relation}(t)} k`$ | `within=relation[...]` — $`t-k`$ inside the group of $`t`$ | +| $`t \ominus^{\mathrm{property}(t)} k`$ | `within=subject[...]` — $`t-k`$ inside the group of $`t`$ | | $`\mathrm{pos}(t)`$ | `position()` — the position of $`t`$, counted from $`0`$; $`t`$ is the label | -| $`\mathrm{pos}_{\mathrm{relation}(t)}(t)`$ | `position(within=relation[...])` — the position of $`t`$ inside its group | +| $`\mathrm{pos}_{\mathrm{property}(t)}(t)`$ | `position(within=subject[...])` — the position of $`t`$ inside its group | | $`\lvert \mathcal{T} \rvert`$ | the size of $`\mathcal{T}`$; $`\lvert \mathcal{T} \rvert - 1`$ is the last position | #### Conditions @@ -226,7 +226,7 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 **`startup_cost`** ```math -\mathrm{startup\_cost}_{t,g} = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season\_of}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\mathrm{startup\_cost}_{t,g} = \begin{cases} \mathrm{cost}_{g} & \text{if } \mathrm{pos}(t) = 0 \\ \mathrm{cost}_{g} \cdot 2 & \text{if } \mathrm{pos}(t) > 0 \wedge \mathrm{season}(t) = \text{'}\mathrm{winter}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` #### Objective @@ -258,13 +258,13 @@ p_{t,g} \le \mathrm{rating}_{g,t} \qquad \forall\, t \in \mathcal{T},\ g \in \ma **`balance`** ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{spill}_{t} - \mathit{slack}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` **`netted`** ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bus}(g) = b} p_{t,g} + \mathit{net}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b} p_{t,g} + \mathit{net}_{t} = \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` **`ramp`** @@ -312,13 +312,13 @@ p_{t \boxminus_{0} \mathrm{lead},g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \fo **`in_season`** ```math -p_{t,g} \le p_{t \ominus^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +p_{t,g} \le p_{t \ominus^{\mathrm{season}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` **`held_in_season`** ```math -p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season}(t)} 1,g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` **`window`** @@ -336,31 +336,31 @@ p_{t,g} \le p_{t \boxminus_{0}^{\mathrm{season\_of}(t)} 1,g} \qquad \forall\, t **`seasonal_window`** ```math -\sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season\_of}(t)} t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} +\sum_{t' \in \mathcal{T} \,:\, 0 \le t -^{\mathrm{season}(t)} t' < 3} \mathit{on}_{t',g} \le \mathit{units}_{g} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} ``` **`lookup`** ```math -\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} +\mathit{spill}_{t} \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} ``` **`grouped_once`** ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_bt.bus}(g) = b \wedge \mathrm{gen\_bt.technology}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} +\sum_{g \in \mathcal{G} \,:\, \mathrm{bus}(g) = b \wedge \mathrm{technology}(g) = e} p_{t,g} \le \mathrm{tech\_cap}_{b,e} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B},\ e \in \mathcal{E} ``` **`looked_up_once`** ```math -\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{gen\_bt.bus}(g),\mathrm{gen\_bt.technology}(g)} \qquad \forall\, g \in \mathcal{G} +\mathit{units}_{g} \le \mathrm{tech\_cap}_{\mathrm{bus}(g),\mathrm{technology}(g)} \qquad \forall\, g \in \mathcal{G} ``` **`within_bus`** ```math -\mathit{units}_{g} \le \mathit{units}_{g \boxminus_{0}^{\mathrm{gen\_bt.bus}(g)} 1} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{gen\_bt.bus}(g),\ \mathrm{gen\_bt.technology}(g) \right)}(g) = 0 +\mathit{units}_{g} \le \mathit{units}_{g \boxminus_{0}^{\mathrm{bus}(g)} 1} \qquad \forall\, g \in \mathcal{G} \,:\, \mathrm{pos}_{\left( \mathrm{bus}(g),\ \mathrm{technology}(g) \right)}(g) = 0 ``` **`relational`** @@ -378,7 +378,7 @@ p_{t,g} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ g \in \math **`representative`** ```math -\sum_{t' \in \mathcal{T} \,:\, \mathrm{rep\_of}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep\_of}(t)} \qquad \forall\, t \in \mathcal{T} +\sum_{t' \in \mathcal{T} \,:\, \mathrm{rep}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep}(t)} \qquad \forall\, t \in \mathcal{T} ``` **`by_fuel`** @@ -402,25 +402,25 @@ p_{t,g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{ **`zonal`** ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} +\sum_{g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, t \in \mathcal{T},\ z \in \mathcal{Z} ``` **`zonal_history`** ```math -\sum_{t \in \mathcal{T} \,:\, \mathrm{gen\_zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} +\sum_{t \in \mathcal{T} \,:\, \mathrm{zone}(g,\ t) = z} p_{t,g} \le \mathrm{zone\_cap}_{z} \qquad \forall\, g \in \mathcal{G},\ z \in \mathcal{Z} ``` **`zonal_membership`** ```math -\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) \text{ is defined}} \mathit{units}_{g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} +\sum_{g \in \mathcal{G} \,:\, \left( g,\ t \right) \in \mathrm{generator\_snapshot}} \mathit{units}_{g} \le \mathrm{budget} \qquad \forall\, t \in \mathcal{T} ``` **`zonal_lookup`** ```math -p_{t,g} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{gen\_zone}(g,\ t)} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{gen\_zone}(g,\ t)}(g) = 0 +p_{t,g} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{zone}(g,\ t)} \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{zone}(g,\ t) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{pos}_{\mathrm{zone}(g,\ t)}(g) = 0 ``` **`arithmetic`** @@ -450,19 +450,19 @@ p_{t,g} \le \mathit{spill}_{t} \cdot \mathrm{zone\_cap}_{\mathrm{gen\_zone}(g,\ **`first`** ```math -\mathit{on}_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = 0 +\mathit{on}_{t,g} = 1 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = 0 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = 0 ``` **`last`** ```math -\mathit{on}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season\_of}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season\_of}(t)} \rvert - 1 +\mathit{on}_{t,g} = 0 \qquad \forall\, t \in \mathcal{T},\ g \in \mathcal{G} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \vee \mathrm{pos}_{\mathrm{season}(t)}(t) = \lvert \mathcal{T}_{\mathrm{season}(t)} \rvert - 1 ``` **`northern`** ```math -\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone\_of}(b) \neq \mathrm{area\_of}(b) \wedge \mathrm{zone\_of}(b) \text{ is defined} +\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} \wedge \mathrm{zone}(b) \neq \mathrm{area}(b) \wedge \mathrm{zone}(b) \text{ is defined} ``` **`efficiency`** @@ -504,7 +504,7 @@ p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g **`ramped`** ```math -\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \wedge \mathrm{pos}(t) > 0 +\mathit{slack}_{t} \le \mathrm{load}_{t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{load}_{t,b} - \mathrm{load}_{t \boxminus_{0} 1,b} \le \mathrm{zone\_cap}_{\mathrm{zone}(b)} \wedge \mathrm{pos}(t) > 0 ``` **`covered`** @@ -534,7 +534,7 @@ p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g **`zoned`** ```math -\theta_{b} \le \mathrm{budget} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone\_of}(b)} \text{ is defined} +\theta_{b} \le \mathrm{budget} \qquad \forall\, b \in \mathcal{B} \,:\, \mathrm{zone\_cap}_{\mathrm{zone}(b)} \text{ is defined} ``` **`capped`** @@ -608,7 +608,7 @@ p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g **`zones_agree`** ```math -\mathrm{zone\_of}(b) = \mathrm{area\_of}(b) \qquad \forall\, b \in \mathcal{B} +\mathrm{zone}(b) = \mathrm{area}(b) \qquad \forall\, b \in \mathcal{B} ``` **`budget_covers_the_peak`** @@ -632,7 +632,7 @@ p_{t,g} \le \mathrm{p}^{\mathrm{max}}_{g} \qquad \forall\, t \in \mathcal{T},\ g **`northern_demand_is_real`** ```math -\mathrm{load}_{t,b} \ge 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone\_of}(b) = \text{'}\mathrm{north}\text{'} +\mathrm{load}_{t,b} \ge 0 \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B} \,:\, \mathrm{zone}(b) = \text{'}\mathrm{north}\text{'} ``` **`fuel_curve_complete`** diff --git a/tests/typesetting/golden/model.yaml b/tests/typesetting/golden/model.yaml index 934ad2ed5..515181b2a 100644 --- a/tests/typesetting/golden/model.yaml +++ b/tests/typesetting/golden/model.yaml @@ -21,22 +21,24 @@ dimensions: technology: { dtype: str } bp: { dtype: int, ordered: true } # the breakpoints every curve below runs through -relations: - gen_bus: { key: generator, values: bus } - zone_of: { key: bus, values: zone } - area_of: { key: bus, values: zone, missing: absent } # a second map into the same set, to compare against; a bus may be in no area - season_of: { key: snapshot, values: season } - gen_zone: { key: [generator, snapshot], values: zone } # a map keyed by two dimensions: a call sums one away and joins on the other - rep_of: { key: snapshot, values: { rep: snapshot } } # a map into its own dimension: the representative snapshot - connection: { key: [generator, bus], missing: absent } # a bare relation, with no value columns: many-to-many, read only by sum with both ends named - gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once - sets: dispatch_pair: { dims: [generator, bus], missing: absent } # a set: the pairs that exist, and the subject of a property below + generator_snapshot: { dims: [generator, snapshot] } # a set over two dimensions: a call through its property sums one away and joins on the other + connection: { dims: [generator, bus], missing: absent } # a set with no property: many-to-many, read only by sum with both members named properties: generator: fuel: { into: technology, missing: absent } # a property of a dimension: the legend names it after itself, and says a generator may have none + bus: bus + technology: technology # with bus, two properties read at once: the call reads their inner join + bus: + zone: zone + area: { into: zone, missing: absent } # a second property into the same dimension, to compare against; a bus may be in no area + snapshot: + season: season + rep: snapshot # a property into its own dimension: the representative snapshot + generator_snapshot: + zone: zone dispatch_pair: area: zone # a property of a set: the legend maps it from the set by name @@ -167,7 +169,7 @@ expressions: dims: [snapshot, generator] cases: opening: { when: "position(snapshot) == 0", expression: cost } - winter: { when: "position(snapshot) > 0 and season_of == 'winter'", expression: cost * 2 } + winter: { when: "position(snapshot) > 0 and snapshot[season] == 'winter'", expression: cost * 2 } otherwise: 0 conditions: @@ -186,12 +188,12 @@ constraints: under_rating: # names the expression with declared dims, read in the row's own order dims: [snapshot, generator] expression: p <= rating - balance: # sum over a relation + balance: # sum through a property dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) + spill - slack == load + expression: sum(p, over=generator, by=generator[bus]) + spill - slack == load netted: # names the signed sum on the right of a plus: inlined, its minus prints as a subtraction dims: [snapshot, bus] - expression: sum(p, over=generator, by=gen_bus[bus]) + net == load + expression: sum(p, over=generator, by=generator[bus]) + net == load ramp: # roll (cyclic) and shift (acyclic) in one equation dims: [snapshot, generator] expression: p - shift(p, along=snapshot, offset=1, edge='wrap') <= shift(p, along=snapshot, offset=1) + p_max @@ -215,44 +217,44 @@ constraints: lead_time: # an offset the data carries, so it prints as a symbol rather than a number dims: [snapshot, generator] expression: shift(p, along=snapshot, offset=lead, edge=0) <= p_max - in_season: # a translation partitioned by a relation: the group rides on the operator + in_season: # a translation partitioned by a property: the group rides on the operator dims: [snapshot, generator] - expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge='wrap', within=snapshot[season]) held_in_season: # the same group, with a fill: each season's opening row is kept and given a zero dims: [snapshot, generator] - expression: p <= shift(p, along=snapshot, offset=1, edge=0, within=season_of[season]) + expression: p <= shift(p, along=snapshot, offset=1, edge=0, within=snapshot[season]) window: # a trailing window of fixed width dims: [snapshot, generator] expression: sum_back(on, along=snapshot, window=3) <= units history: # the same window, its width in the data and its edge wrapped dims: [snapshot, generator] expression: sum_back(on, along=snapshot, window=min_up, edge='wrap') <= units - seasonal_window: # a window partitioned by a relation: the group rides on the operator + seasonal_window: # a window partitioned by a property: the group rides on the operator dims: [snapshot, generator] - expression: sum_back(on, along=snapshot, window=3, within=season_of[season]) <= units - lookup: # at(), which re-indexes through a relation instead of an offset + expression: sum_back(on, along=snapshot, window=3, within=snapshot[season]) <= units + lookup: # at(), which re-indexes through a property instead of an offset dims: [snapshot, bus] - expression: spill <= at(zone_cap, by=zone_of[zone]) + expression: spill <= at(zone_cap, by=bus[zone]) grouped_once: # one table read to two value columns: the domain carries a condition per column dims: [snapshot, bus, technology] - expression: sum(p, over=generator, by=gen_bt[bus, technology]) <= tech_cap + expression: sum(p, over=generator, by=generator[bus, technology]) <= tech_cap looked_up_once: # the same table joined the other way, reading one slot through two columns dims: [generator] - expression: units <= at(tech_cap, by=gen_bt[bus, technology]) + expression: units <= at(tech_cap, by=generator[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, within=gen_bt[bus, technology]) == 0" - expression: units <= shift(units, along=generator, offset=1, edge=0, within=gen_bt[bus]) - relational: # a sum through a bare relation: the domain is a row of the relation rather than a function's value + where: "position(generator, within=generator[bus, technology]) == 0" + expression: units <= shift(units, along=generator, offset=1, edge=0, within=generator[bus]) + relational: # a sum through a set: the domain is a row of the set rather than a function's value dims: [snapshot, bus] expression: sum(p, over=generator, by=connection[bus]) <= load - connected: # a bare relation as a where: the row of the frame has to be a member of the relation + connected: # a set as a where: the row of the frame has to be a row of the set dims: [snapshot, generator, bus] where: "connection" expression: p <= load representative: # a map into its own dimension, read both ways: the frame is unchanged and the index is primed dims: [snapshot] - expression: sum(spill, over=snapshot, by=rep_of[rep]) <= at(spill, by=rep_of[rep]) + expression: sum(spill, over=snapshot, by=snapshot[rep]) <= at(spill, by=snapshot[rep]) by_fuel: # a sum through a property of a dimension, read as the property's own name dims: [snapshot, technology] expression: sum(p, over=generator, by=generator[fuel]) <= budget @@ -265,17 +267,17 @@ constraints: expression: p <= budget zonal: # a grouping through a two-key map, summing one key away: the condition reads the other, and the row keeps it dims: [snapshot, zone] - expression: sum(p, over=generator, by=gen_zone[zone]) <= zone_cap + expression: sum(p, over=generator, by=generator_snapshot[zone]) <= zone_cap zonal_history: # the same table summing its other key away dims: [generator, zone] - expression: sum(p, over=snapshot, by=gen_zone[zone]) <= zone_cap + expression: sum(p, over=snapshot, by=generator_snapshot[zone]) <= zone_cap zonal_membership: # the same table read between its two key columns: no value column is read, so the domain asks only that the row is there dims: [snapshot] - expression: sum(units, over=generator, by=gen_zone[snapshot]) <= budget + expression: sum(units, over=generator, by=generator_snapshot[snapshot]) <= budget zonal_lookup: # the same table joined the other way, reading the slot the row's own snapshot puts the generator in dims: [snapshot, generator] - where: "gen_zone == 'north' AND position(generator, within=gen_zone[zone]) == 0" - expression: p <= at(spill * zone_cap, by=gen_zone[zone]) + where: "generator_snapshot[zone] == 'north' AND position(generator, within=generator_snapshot[zone]) == 0" + expression: p <= at(spill * zone_cap, by=generator_snapshot[zone]) arithmetic: # division, both unary signs, a sign beside a sign, floats with and without an exponent, bracketing dims: [snapshot] expression: >- @@ -294,15 +296,15 @@ constraints: expression: theta <= load first: # a position in a dimension, and the same position within a group dims: [snapshot, generator] - where: "position(snapshot) == 0 OR position(snapshot, within=season_of[season]) == 0" + where: "position(snapshot) == 0 OR position(snapshot, within=snapshot[season]) == 0" expression: on == 1 last: # the same two counted from the end, which print against a size rather than as themselves dims: [snapshot, generator] - where: "position(snapshot) == -1 OR position(snapshot, within=season_of[season]) == -1" + where: "position(snapshot) == -1 OR position(snapshot, within=snapshot[season]) == -1" expression: on == 0 - northern: # a relation compared to a label, to another relation, and to nothing + northern: # a property compared to a label, to another property, and to nothing dims: [snapshot, bus] - where: "zone_of == 'north' AND zone_of != area_of AND zone_of" + where: "bus[zone] == 'north' AND bus[zone] != bus[area] AND bus[zone]" expression: slack <= load efficiency: # a Greek-named parameter, which is given — so the convention wins and it prints as the word dims: [snapshot, generator] @@ -326,9 +328,9 @@ constraints: dims: [snapshot, generator] where: "p_max - p_min > cost / 2" expression: p <= p_max - ramped: # a translation under a comparison names its edge, a lookup reads through a relation, and the position keeps the vacated row out + ramped: # a translation under a comparison names its edge, a lookup reads through a property, and the position keeps the vacated row out dims: [snapshot, bus] - where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=zone_of[zone]) AND position(snapshot) > 0" + where: "load - shift(load, along=snapshot, offset=1, edge=0) <= at(zone_cap, by=bus[zone]) AND position(snapshot) > 0" expression: slack <= load covered: # a reduction on a side of a scalar mask, so nothing is left to quantify dims: [] @@ -346,9 +348,9 @@ constraints: dims: [snapshot, bus] where: "load AND NOT shift(load, along=snapshot, offset=1)" expression: slack <= load - zoned: # a condition read through a relation: a bus is held only where its zone has a cap at all + zoned: # a condition read through a property: a bus is held only where its zone has a cap at all dims: [bus] - where: "at(zone_cap, by=zone_of[zone])" + where: "at(zone_cap, by=bus[zone])" expression: theta <= budget capped: # an expressions: entry on a side, read by the name the file gave it dims: [snapshot, generator] @@ -363,7 +365,7 @@ assumptions: # what the data has to satisfy for the answer to mean anything bounds_do_not_cross: "p_min <= p_max" # two parameters, which is arithmetic like any other efficiency_is_a_fraction: "eta > 0 AND eta <= 1" # a connective, so the line has no relation to align on lead_times_are_short: "lead <= 3" # one parameter against a literal - zones_agree: "zone_of == area_of" # two maps into one set, compared row by row + zones_agree: "bus[zone] == bus[area]" # two maps into one set, compared row by row budget_covers_the_peak: "sum(p_max, over=generator) >= budget" # a reduction on a side, leaving nothing to quantify ramps_are_gentle: # a translation inside arithmetic, and a position keeping the vacated row out holds: "load - shift(load, along=snapshot, offset=1, edge=0) <= budget" @@ -372,9 +374,9 @@ assumptions: # what the data has to satisfy for the answer to mean anything holds: "p_min < p_max" where: "is_flexible" description: a unit that cannot be turned down needs somewhere to go - northern_demand_is_real: # a relation comparison as the where, over a frame two dims wide + northern_demand_is_real: # a property comparison as the where, over a frame two dims wide holds: "load >= 0" - where: "zone_of == 'north'" + where: "bus[zone] == 'north'" objective: # a sense, a product of two variables, a power over two parameters, a power of one of those, and the summations a scalar objective spells out beside two scalar terms sense: maximize diff --git a/tests/typesetting/golden/typst.out b/tests/typesetting/golden/typst.out index 1e1e5a016..b7419ffd4 100644 --- a/tests/typesetting/golden/typst.out +++ b/tests/typesetting/golden/typst.out @@ -4,12 +4,12 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 {net} \~ ^ \\ \*star\* \@ref \, and `a_name` in backticks set in code == Sets -/ $cal(T)$: index $t$ --- `snapshot` (`int` coordinates) with $upright("season_of"): cal(T) arrow.r cal(S), upright("gen_zone"): cal(G) times cal(T) arrow.r cal(Z), upright("rep_of"): cal(T) arrow.r cal(T)$ -/ $cal(G)$: index $g$ --- `generator` with $upright("gen_bus"): cal(G) arrow.r cal(B), upright("gen_zone"): cal(G) times cal(T) arrow.r cal(Z), upright("connection") subset.eq cal(G) times cal(B), upright("gen_bt"): cal(G) arrow.r cal(B) times cal(E), upright("dispatch_pair") subset.eq cal(G) times cal(B), upright("fuel"): cal(G) arrow.r cal(E), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row -/ $cal(B)$: index $b$ --- `bus` with $upright("gen_bus"): cal(G) arrow.r cal(B), upright("zone_of"): cal(B) arrow.r cal(Z), upright("area_of"): cal(B) arrow.r cal(Z), upright("connection") subset.eq cal(G) times cal(B), upright("gen_bt"): cal(G) arrow.r cal(B) times cal(E), upright("dispatch_pair") subset.eq cal(G) times cal(B), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `area_of` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `dispatch_pair` is `absent` where the data has no row -/ $cal(Z)$: index $z$ --- `zone` with $upright("zone_of"): cal(B) arrow.r cal(Z), upright("area_of"): cal(B) arrow.r cal(Z), upright("gen_zone"): cal(G) times cal(T) arrow.r cal(Z), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `area_of` is `absent` where the data has no row -/ $cal(S)$: index $s$ --- `season` with $upright("season_of"): cal(T) arrow.r cal(S)$ -/ $cal(E)$: index $e$ --- `technology` with $upright("gen_bt"): cal(G) arrow.r cal(B) times cal(E), upright("fuel"): cal(G) arrow.r cal(E)$, `generator[fuel]` is `absent` where the data has no row +/ $cal(T)$: index $t$ --- `snapshot` (`int` coordinates) with $upright("generator_snapshot") subset.eq cal(G) times cal(T), upright("season"): cal(T) arrow.r cal(S), upright("rep"): cal(T) arrow.r cal(T), upright("zone"): upright("generator_snapshot") arrow.r cal(Z)$ +/ $cal(G)$: index $g$ --- `generator` with $upright("dispatch_pair") subset.eq cal(G) times cal(B), upright("generator_snapshot") subset.eq cal(G) times cal(T), upright("connection") subset.eq cal(G) times cal(B), upright("fuel"): cal(G) arrow.r cal(E), upright("bus"): cal(G) arrow.r cal(B), upright("technology"): cal(G) arrow.r cal(E), upright("zone"): upright("generator_snapshot") arrow.r cal(Z), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `generator[fuel]` is `absent` where the data has no row +/ $cal(B)$: index $b$ --- `bus` with $upright("dispatch_pair") subset.eq cal(G) times cal(B), upright("connection") subset.eq cal(G) times cal(B), upright("bus"): cal(G) arrow.r cal(B), upright("zone"): cal(B) arrow.r cal(Z), upright("area"): cal(B) arrow.r cal(Z), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `dispatch_pair` is `absent` where the data has no row, `connection` is `absent` where the data has no row, `bus[area]` is `absent` where the data has no row +/ $cal(Z)$: index $z$ --- `zone` with $upright("zone"): cal(B) arrow.r cal(Z), upright("area"): cal(B) arrow.r cal(Z), upright("zone"): upright("generator_snapshot") arrow.r cal(Z), upright("area"): upright("dispatch_pair") arrow.r cal(Z)$, `bus[area]` is `absent` where the data has no row +/ $cal(S)$: index $s$ --- `season` with $upright("season"): cal(T) arrow.r cal(S)$ +/ $cal(E)$: index $e$ --- `technology` with $upright("fuel"): cal(G) arrow.r cal(E), upright("technology"): cal(G) arrow.r cal(E)$, `generator[fuel]` is `absent` where the data has no row / $cal(A)$: index $a$ --- `bp` == Parameters @@ -63,9 +63,9 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 == Notation / $t minus.o k$: `edge='wrap'` --- $t-k$, cyclic / $t minus.square_(v) k$: `edge=v` --- $t-k$, and $v$ past the edge -/ $t minus.o^(upright("relation")(t)) k$: `within=relation[...]` --- $t-k$ inside the group of $t$ +/ $t minus.o^(upright("property")(t)) k$: `within=subject[...]` --- $t-k$ inside the group of $t$ / $upright("pos")(t)$: `position()` --- the position of $t$, counted from $0$; $t$ is the label -/ $upright("pos")_(upright("relation")(t))(t)$: `position(within=relation[...])` --- the position of $t$ inside its group +/ $upright("pos")_(upright("property")(t))(t)$: `position(within=subject[...])` --- the position of $t$ inside its group / $abs(cal(T))$: the size of $cal(T)$; $abs(cal(T)) - 1$ is the last position == Conditions @@ -98,7 +98,7 @@ $ upright("spend_cap") & upright("spend")^(upright("cap"))_(g) & = upright("cost upright("lcoe") & italic("lcoe") & = frac(sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("cost")_(g), sum_(t in cal(T), g in cal(G)) p_(t,g)) \ upright("net") & italic("net")_(t) & = -italic("spill")_(t) + italic("slack")_(t) & forall t in cal(T) \ upright("marginal_price") & italic("marginal_price")_(b,t) & = lambda_(upright("balance"),t,b) & forall b in cal(B), t in cal(T) \ - upright("startup_cost") & upright("startup_cost")_(t,g) & = cases(upright("cost")_(g) & upright("if ") upright("pos")(t) = 0, upright("cost")_(g) dot 2 & upright("if ") upright("pos")(t) > 0 and upright("season_of")(t) = upright("'winter'"), 0 & upright("otherwise")) & forall t in cal(T), g in cal(G) $ + upright("startup_cost") & upright("startup_cost")_(t,g) & = cases(upright("cost")_(g) & upright("if ") upright("pos")(t) = 0, upright("cost")_(g) dot 2 & upright("if ") upright("pos")(t) > 0 and upright("season")(t) = upright("'winter'"), 0 & upright("otherwise")) & forall t in cal(T), g in cal(G) $ == Objective #set math.equation(numbering: "(1)") @@ -109,8 +109,8 @@ $ & max & sum_(t in cal(T), g in cal(G)) p_(t,g) dot upright("cost")_(g) + sum_ $ upright("budgeted") & italic("spend")_(t) & <= upright("budget") & forall t in cal(T) \ upright("starts") & p_(t,g) & <= upright("startup_cost")_(t,g) & forall t in cal(T), g in cal(G) \ upright("under_rating") & p_(t,g) & <= upright("rating")_(g,t) & forall t in cal(T), g in cal(G) \ - upright("balance") & sum_(g in cal(G) colon upright("gen_bus")(g) = b) p_(t,g) + italic("spill")_(t) - italic("slack")_(t) & = upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ - upright("netted") & sum_(g in cal(G) colon upright("gen_bus")(g) = b) p_(t,g) + italic("net")_(t) & = upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ + upright("balance") & sum_(g in cal(G) colon upright("bus")(g) = b) p_(t,g) + italic("spill")_(t) - italic("slack")_(t) & = upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ + upright("netted") & sum_(g in cal(G) colon upright("bus")(g) = b) p_(t,g) + italic("net")_(t) & = upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ upright("ramp") & p_(t,g) - p_(t minus.o 1,g) & <= p_(t - 1,g) + upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("edges") & p_(t minus.square_(0) 1,g) & <= p_(t plus.square_(0) 1,g) + upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("ahead") & p_(t,g) & <= p_(t plus.o 1,g) & forall t in cal(T), g in cal(G) \ @@ -118,44 +118,44 @@ $ upright("budgeted") & italic("spend")_(t) & <= upright("budget") & forall t in upright("uncomposed") & p_((t - 1) minus.square_(0) upright("lead"),g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("crossed") & p_(t minus.o 1,g + 1) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("lead_time") & p_(t minus.square_(0) upright("lead"),g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ - upright("in_season") & p_(t,g) & <= p_(t minus.o^(upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ - upright("held_in_season") & p_(t,g) & <= p_(t minus.square_(0)^(upright("season_of")(t)) 1,g) & forall t in cal(T), g in cal(G) \ + upright("in_season") & p_(t,g) & <= p_(t minus.o^(upright("season")(t)) 1,g) & forall t in cal(T), g in cal(G) \ + upright("held_in_season") & p_(t,g) & <= p_(t minus.square_(0)^(upright("season")(t)) 1,g) & forall t in cal(T), g in cal(G) \ upright("window") & sum_(t' in cal(T) colon 0 <= t - t' < 3) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ upright("history") & sum_(t' in cal(T) colon 0 <= t minus.o t' < upright("min_up")) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ - upright("seasonal_window") & sum_(t' in cal(T) colon 0 <= t -^(upright("season_of")(t)) t' < 3) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ - upright("lookup") & italic("spill")_(t) & <= upright("zone_cap")_(upright("zone_of")(b)) & forall t in cal(T), b in cal(B) \ - upright("grouped_once") & sum_(g in cal(G) colon upright("gen_bt.bus")(g) = b and upright("gen_bt.technology")(g) = e) p_(t,g) & <= upright("tech_cap")_(b,e) & forall t in cal(T), b in cal(B), e in cal(E) \ - upright("looked_up_once") & italic("units")_(g) & <= upright("tech_cap")_(upright("gen_bt.bus")(g),upright("gen_bt.technology")(g)) & forall g in cal(G) \ - upright("within_bus") & italic("units")_(g) & <= italic("units")_(g minus.square_(0)^(upright("gen_bt.bus")(g)) 1) & forall g in cal(G) colon upright("pos")_((upright("gen_bt.bus")(g), upright("gen_bt.technology")(g)))(g) = 0 \ + upright("seasonal_window") & sum_(t' in cal(T) colon 0 <= t -^(upright("season")(t)) t' < 3) italic("on")_(t',g) & <= italic("units")_(g) & forall t in cal(T), g in cal(G) \ + upright("lookup") & italic("spill")_(t) & <= upright("zone_cap")_(upright("zone")(b)) & forall t in cal(T), b in cal(B) \ + upright("grouped_once") & sum_(g in cal(G) colon upright("bus")(g) = b and upright("technology")(g) = e) p_(t,g) & <= upright("tech_cap")_(b,e) & forall t in cal(T), b in cal(B), e in cal(E) \ + upright("looked_up_once") & italic("units")_(g) & <= upright("tech_cap")_(upright("bus")(g),upright("technology")(g)) & forall g in cal(G) \ + upright("within_bus") & italic("units")_(g) & <= italic("units")_(g minus.square_(0)^(upright("bus")(g)) 1) & forall g in cal(G) colon upright("pos")_((upright("bus")(g), upright("technology")(g)))(g) = 0 \ upright("relational") & sum_(g in cal(G) colon (g, b) in upright("connection")) p_(t,g) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) \ upright("connected") & p_(t,g) & <= upright("load")_(t,b) & forall t in cal(T), g in cal(G), b in cal(B) colon (g, b) in upright("connection") \ - upright("representative") & sum_(t' in cal(T) colon upright("rep_of")(t') = t) italic("spill")_(t') & <= italic("spill")_(upright("rep_of")(t)) & forall t in cal(T) \ + upright("representative") & sum_(t' in cal(T) colon upright("rep")(t') = t) italic("spill")_(t') & <= italic("spill")_(upright("rep")(t)) & forall t in cal(T) \ upright("by_fuel") & sum_(g in cal(G) colon upright("fuel")(g) = e) p_(t,g) & <= upright("budget") & forall t in cal(T), e in cal(E) \ upright("by_area") & sum_(g in cal(G), b in cal(B) colon upright("area")(g, b) = z) p_(t,g) dot upright("link_eff")_(g,b) & <= upright("zone_cap")_(z) & forall t in cal(T), z in cal(Z) \ upright("fuelled") & p_(t,g) & <= upright("budget") & forall t in cal(T), g in cal(G) colon upright("fuel")(g) upright(" is defined") \ - upright("zonal") & sum_(g in cal(G) colon upright("gen_zone")(g, t) = z) p_(t,g) & <= upright("zone_cap")_(z) & forall t in cal(T), z in cal(Z) \ - upright("zonal_history") & sum_(t in cal(T) colon upright("gen_zone")(g, t) = z) p_(t,g) & <= upright("zone_cap")_(z) & forall g in cal(G), z in cal(Z) \ - upright("zonal_membership") & sum_(g in cal(G) colon upright("gen_zone")(g, t) upright(" is defined")) italic("units")_(g) & <= upright("budget") & forall t in cal(T) \ - upright("zonal_lookup") & p_(t,g) & <= italic("spill")_(t) dot upright("zone_cap")_(upright("gen_zone")(g, t)) & forall t in cal(T), g in cal(G) colon upright("gen_zone")(g, t) = upright("'north'") and upright("pos")_(upright("gen_zone")(g, t))(g) = 0 \ + upright("zonal") & sum_(g in cal(G) colon upright("zone")(g, t) = z) p_(t,g) & <= upright("zone_cap")_(z) & forall t in cal(T), z in cal(Z) \ + upright("zonal_history") & sum_(t in cal(T) colon upright("zone")(g, t) = z) p_(t,g) & <= upright("zone_cap")_(z) & forall g in cal(G), z in cal(Z) \ + upright("zonal_membership") & sum_(g in cal(G) colon (g, t) in upright("generator_snapshot")) italic("units")_(g) & <= upright("budget") & forall t in cal(T) \ + upright("zonal_lookup") & p_(t,g) & <= italic("spill")_(t) dot upright("zone_cap")_(upright("zone")(g, t)) & forall t in cal(T), g in cal(G) colon upright("zone")(g, t) = upright("'north'") and upright("pos")_(upright("zone")(g, t))(g) = 0 \ upright("arithmetic") & sum_(g in cal(G)) (frac(p_(t,g), 2) - upright("cost")_(g) + 10^(-5) dot p_(t,g) + 2.5 times 10^(-7) dot upright("cost")_(g) + 0.5 dot p_(t,g)) & >= -(sum_(g in cal(G)) p_(t,g)) dot (-3) & forall t in cal(T) \ upright("total") & sum_(t in cal(T), g in cal(G)) p_(t,g) & <= upright("budget") \ upright("scalar") & italic("units")_(g) & <= upright("budget") & forall g in cal(G) colon upright("cost")_(g) upright(" is defined") \ upright("running") & theta_(b) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon theta_(b) upright(" exists") and t >= 3 \ - upright("first") & italic("on")_(t,g) & = 1 & forall t in cal(T), g in cal(G) colon upright("pos")(t) = 0 or upright("pos")_(upright("season_of")(t))(t) = 0 \ - upright("last") & italic("on")_(t,g) & = 0 & forall t in cal(T), g in cal(G) colon upright("pos")(t) = abs(cal(T)) - 1 or upright("pos")_(upright("season_of")(t))(t) = abs(cal(T)_(upright("season_of")(t))) - 1 \ - upright("northern") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("zone_of")(b) = upright("'north'") and upright("zone_of")(b) != upright("area_of")(b) and upright("zone_of")(b) upright(" is defined") \ + upright("first") & italic("on")_(t,g) & = 1 & forall t in cal(T), g in cal(G) colon upright("pos")(t) = 0 or upright("pos")_(upright("season")(t))(t) = 0 \ + upright("last") & italic("on")_(t,g) & = 0 & forall t in cal(T), g in cal(G) colon upright("pos")(t) = abs(cal(T)) - 1 or upright("pos")_(upright("season")(t))(t) = abs(cal(T)_(upright("season")(t))) - 1 \ + upright("northern") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("zone")(b) = upright("'north'") and upright("zone")(b) != upright("area")(b) and upright("zone")(b) upright(" is defined") \ upright("efficiency") & p_(t,g) & <= upright("eta")_(g) dot upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) \ upright("ceiling") & theta_(b) & <= infinity & forall b in cal(B) \ upright("always") & italic("spill")_(t) & >= 0 & forall t in cal(T) \ upright("redundant") & italic("spill")_(t) & >= 0 & forall t in cal(T) colon italic("spill")_(t) upright(" exists") \ upright("never") & italic("slack")_(t) & >= 0 & forall t in cal(T) colon bot \ upright("margin") & p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon upright("p")^(upright("max"))_(g) - upright("p")^(upright("min"))_(g) > frac(upright("cost")_(g), 2) \ - upright("ramped") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("load")_(t,b) - upright("load")_(t minus.square_(0) 1,b) <= upright("zone_cap")_(upright("zone_of")(b)) and upright("pos")(t) > 0 \ + upright("ramped") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("load")_(t,b) - upright("load")_(t minus.square_(0) 1,b) <= upright("zone_cap")_(upright("zone")(b)) and upright("pos")(t) > 0 \ upright("covered") & sum_(t in cal(T), g in cal(G)) p_(t,g) & <= upright("budget") & upright("where ") sum_(g in cal(G)) upright("p")^(upright("max"))_(g) >= upright("budget") \ upright("counted") & theta_(b) & <= upright("budget") & forall b in cal(B) colon abs({e in cal(E) colon upright("tech_cap")_(b,e) > 0}) >= 2 \ upright("counted_here") & theta_(b) & <= upright("tech_cap")_(b,e) & forall b in cal(B), e in cal(E) colon abs({e' in cal(E) colon upright("tech_cap")_(b,e') > 0}) >= 2 \ upright("run_start") & italic("slack")_(t) & <= upright("load")_(t,b) & forall t in cal(T), b in cal(B) colon upright("load")_(t,b) upright(" is defined") and not (upright("load")_(t - 1,b) upright(" is defined")) \ - upright("zoned") & theta_(b) & <= upright("budget") & forall b in cal(B) colon upright("zone_cap")_(upright("zone_of")(b)) upright(" is defined") \ + upright("zoned") & theta_(b) & <= upright("budget") & forall b in cal(B) colon upright("zone_cap")_(upright("zone")(b)) upright(" is defined") \ upright("capped") & p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon upright("spend")^(upright("cap"))_(g) > 0 or not upright("is_flexible")_(g) \ upright("masked") & p_(t,g) & <= upright("p")^(upright("max"))_(g) & forall t in cal(T), g in cal(G) colon upright("priced_dispatch")_(g) and upright("pos")(t) > 0 \ upright("adjacent") & (italic("weight")_(t,g))_(g in cal(G)) & in upright("SOS")2 & forall t in cal(T) \ @@ -170,11 +170,11 @@ $ upright("budgeted") & italic("spend")_(t) & <= upright("budget") & forall t in $ upright("bounds_do_not_cross") & upright("p")^(upright("min"))_(g) & <= upright("p")^(upright("max"))_(g) & forall g in cal(G) \ upright("efficiency_is_a_fraction") & upright("eta")_(g) > 0 and upright("eta")_(g) <= 1 & & forall g in cal(G) \ upright("lead_times_are_short") & upright("lead")_(g) & <= 3 & forall g in cal(G) \ - upright("zones_agree") & upright("zone_of")(b) & = upright("area_of")(b) & forall b in cal(B) \ + upright("zones_agree") & upright("zone")(b) & = upright("area")(b) & forall b in cal(B) \ upright("budget_covers_the_peak") & sum_(g in cal(G)) upright("p")^(upright("max"))_(g) & >= upright("budget") \ upright("ramps_are_gentle") & upright("load")_(t,b) - upright("load")_(t minus.square_(0) 1,b) & <= upright("budget") & forall t in cal(T), b in cal(B) colon upright("pos")(t) > 0 \ upright("flexible_units_have_headroom") & upright("p")^(upright("min"))_(g) & < upright("p")^(upright("max"))_(g) & forall g in cal(G) colon upright("is_flexible")_(g) \ - upright("northern_demand_is_real") & upright("load")_(t,b) & >= 0 & forall t in cal(T), b in cal(B) colon upright("zone_of")(b) = upright("'north'") \ + upright("northern_demand_is_real") & upright("load")_(t,b) & >= 0 & forall t in cal(T), b in cal(B) colon upright("zone")(b) = upright("'north'") \ upright("fuel_curve_complete") & upright("bp_x")_(g,a) upright(" is defined") and upright("bp_y")_(g,a) upright(" is defined") and upright("bp_heat")_(g,a) upright(" is defined") & & forall g in cal(G), a in cal(A) \ upright("cost_curve_complete") & upright("bp_x")_(g,a) upright(" is defined") and upright("bp_y")_(g,a) upright(" is defined") & & forall g in cal(G), a in cal(A) colon upright("bp_run")_(g,a) \ upright("cost_curve_contiguous") & abs({a in cal(A) colon upright("bp_run")_(g,a) and not upright("bp_run")_(g,a - 1)}) & = 1 & forall g in cal(G) \ diff --git a/tests/typesetting/test_formats.py b/tests/typesetting/test_formats.py index 0837cb619..db4b0e490 100644 --- a/tests/typesetting/test_formats.py +++ b/tests/typesetting/test_formats.py @@ -57,7 +57,7 @@ def test_markdown_keeps_names_out_of_the_math(): def test_markdown_delimits_math_the_one_way_github_hands_over_verbatim(): r"""GitHub runs Markdown's escape pass inside a `$…$` span, so the math has to be delimited out of its reach. - `\mathrm{gen\_bus}` reached MathJax as `\mathrm{gen_bus}` — a subscript, + `\mathrm{dispatch\_pair}` reached MathJax as `\mathrm{dispatch_pair}` — a subscript, and a *Double subscripts* refusal where the name had two underscores — and `\{0, 1\}` as `{0, 1}`, a binary domain printed with no braces. The verbatim pair is handed over untouched, which is what lets this format @@ -66,7 +66,7 @@ def test_markdown_delimits_math_the_one_way_github_hands_over_verbatim(): md = typeset(golden.MODEL, 'markdown', standalone=True) assert _math_spans(md), 'the golden model prints math to scan' assert r'$\mathrm' not in md and '$$' not in md, 'no span is left in the pair GitHub reaches into' - assert r'\mathrm{gen\_bus}' in md, 'the escape TeX needs survives, because nothing is going to eat it' + assert r'\mathrm{dispatch\_pair}' in md, 'the escape TeX needs survives, because nothing is going to eat it' @pytest.mark.parametrize('entry', sorted(_ENTRIES), ids=sorted(_ENTRIES)) diff --git a/tests/typesetting/test_symbols.py b/tests/typesetting/test_symbols.py index 0c20a13e4..77134661b 100644 --- a/tests/typesetting/test_symbols.py +++ b/tests/typesetting/test_symbols.py @@ -101,7 +101,7 @@ def test_a_named_expression_has_a_legend_row_exactly_while_its_symbol_prints(nam DISPATCH_MODEL, **{ 'dimensions.bus': {'dtype': 'str'}, - 'relations.gen_bus': {'key': 'generator', 'values': 'bus', 'missing': 'absent'}, + 'properties.generator.bus': {'into': 'bus', 'missing': 'absent'}, 'parameters.p_max.missing': 'inf', 'parameters.cost.missing': 1, 'parameters.load.missing': 0, @@ -124,7 +124,7 @@ def test_the_legend_prints_what_a_missing_row_means(name: FormatName, fmt: Forma fmt.mono('true'), fmt.math(number(0, fmt)), fmt.mono('absent'), - f'{fmt.mono("gen_bus")} is {fmt.mono("absent")}', + f'{fmt.mono("generator[bus]")} is {fmt.mono("absent")}', ): assert f'{shown} where the data has no row' in out assert out.count('where the data has no row') == 8, ( diff --git a/tests/typesetting/test_walk.py b/tests/typesetting/test_walk.py index b75f46bc0..d8b7cb830 100644 --- a/tests/typesetting/test_walk.py +++ b/tests/typesetting/test_walk.py @@ -147,26 +147,26 @@ def test_each_edge_policy_is_its_own_translation_symbol( def test_a_fill_and_a_group_take_the_operators_two_slots(name: FormatName, fmt: Format): """The fill subscripts the operator; the group superscripts it. - One slot each, so neither `\\boxminus_{0}_{season_of(t)}` — a *Double + One slot each, so neither `\\boxminus_{0}_{season(t)}` — a *Double subscript* error that stopped the page compiling — nor - `\\boxminus_{0,season_of(t)}`, which compiles and leaves a reader to guess + `\\boxminus_{0,season(t)}`, which compiles and leaves a reader to guess which of the two is the value standing at the boundary and which is the group the translation stays inside. """ model = { 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'season': {'dtype': 'str'}}, - 'relations': {'season_of': {'key': 'snapshot', 'values': 'season'}}, + 'properties': {'snapshot': {'season': 'season'}}, 'variables': {'p': {'dims': ['snapshot'], 'bounds': {'lower': 0}}}, 'constraints': { 'held': { 'dims': ['snapshot'], - 'expression': 'p <= shift(p, along=snapshot, offset=1, edge=0, within=season_of[season])', + 'expression': 'p <= shift(p, along=snapshot, offset=1, edge=0, within=snapshot[season])', } }, 'objective': {'sense': 'minimize', 'expression': 'sum(p)'}, } text = typeset(model, name, legend=False) - group = fmt.apply(fmt.upright('season_of'), 't') + group = fmt.apply(fmt.upright('season'), 't') filled = fmt.subscript(fmt.operators['edge_minus'], ['0']) assert fmt.superscript(filled, group) in text, 'the fill and the group are not in their own slots' assert fmt.subscript(fmt.operators['edge_minus'], ['0', group]) not in text, ( @@ -180,12 +180,12 @@ def test_a_translation_by_nothing_takes_no_legend_row(name: FormatName): translation as printed and explained a grouped operator the page never shows.""" model = { 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'season': {'dtype': 'str'}}, - 'relations': {'season_of': {'key': 'snapshot', 'values': 'season'}}, + 'properties': {'snapshot': {'season': 'season'}}, 'variables': {'p': {'dims': ['snapshot'], 'bounds': {'lower': 0}}}, 'constraints': { 'held': { 'dims': ['snapshot'], - 'expression': 'p <= shift(p, along=snapshot, offset=0, edge=0, within=season_of[season])', + 'expression': 'p <= shift(p, along=snapshot, offset=0, edge=0, within=snapshot[season])', } }, 'objective': {'sense': 'minimize', 'expression': 'sum(p)'}, @@ -200,7 +200,7 @@ def test_a_translation_under_a_lookup_survives_it(name: FormatName, fmt: Format) """``at`` and ``shift`` both re-index at the leaf, and the leaf has one subscript. Whoever wrote it last used to win: ``at(shift(cap, along=period, offset=1, - edge=0), by=period_of[period])`` printed `cap_{period_of(t)}`, dropping a + edge=0), by=snapshot[period])`` printed `cap_{period(t)}`, dropping a translation the plan builds. The subscript is a composition, so it renders as one. """ @@ -209,19 +209,19 @@ def test_a_translation_under_a_lookup_survives_it(name: FormatName, fmt: Format) 'snapshot': {'dtype': 'int', 'ordered': True}, 'period': {'dtype': 'int', 'ordered': True}, }, - 'relations': {'period_of': {'key': 'snapshot', 'values': 'period'}}, + 'properties': {'snapshot': {'period': 'period'}}, 'parameters': {'cap': {'dims': ['period']}}, 'variables': {'p': {'dims': ['snapshot'], 'bounds': {'lower': 0}}}, 'constraints': { 'within': { 'dims': ['snapshot'], - 'expression': 'p <= at(shift(cap, along=period, offset=1, edge=0), by=period_of[period])', + 'expression': 'p <= at(shift(cap, along=period, offset=1, edge=0), by=snapshot[period])', } }, } text = typeset(model, name, legend=False) assert fmt.operators['edge_minus'] in text, 'the shift under the at was dropped from the subscript' - assert fmt.apply(fmt.upright('period_of'), 't') in text, 'the lookup itself was dropped' + assert fmt.apply(fmt.upright('period'), 't') in text, 'the lookup itself was dropped' @EVERY_FORMAT @@ -315,10 +315,10 @@ def test_a_negative_fill_prints(name: FormatName, fmt: Format): def _selected(mask: str) -> dict[str, Any]: - """One constraint carrying *mask*, over a dimension a relation groups.""" + """One constraint carrying *mask*, over a dimension a property groups.""" return { 'dimensions': {'snapshot': {'dtype': 'int', 'ordered': True}, 'season': {'dtype': 'str'}}, - 'relations': {'season_of': {'key': 'snapshot', 'values': 'season'}}, + 'properties': {'snapshot': {'season': 'season'}}, 'variables': {'soc': {'dims': ['snapshot'], 'bounds': {'lower': 0}}}, 'constraints': {'seed': {'dims': ['snapshot'], 'where': mask, 'expression': 'soc == 0'}}, } @@ -339,11 +339,11 @@ def test_a_position_from_the_end_prints_against_the_size(name: FormatName, fmt: def test_a_grouped_position_rides_a_subscript_rather_than_a_second_argument(name: FormatName, fmt: Format): """The group is a modifier — which order is counted — not another position. - As ``pos(t, season_of(t))`` the second argument sits where a reader of the + As ``pos(t, season(t))`` the second argument sits where a reader of the first one expects an integer, and nothing says it means "within". """ - text = typeset(_selected('position(snapshot, within=season_of[season]) == 0'), name) - applied = fmt.apply(fmt.upright('season_of'), 't') + text = typeset(_selected('position(snapshot, within=snapshot[season]) == 0'), name) + applied = fmt.apply(fmt.upright('season'), 't') assert fmt.apply(fmt.subscript(fmt.operators['position'], [applied]), 't') in text @@ -367,7 +367,7 @@ def test_a_dimension_compared_against_a_number_says_what_its_coordinates_are(nam pytest.param('counted from', _selected('position(snapshot) == 0'), DISPATCH_MODEL, id='a-position'), pytest.param( 'inside its group', - _selected('position(snapshot, within=season_of[season]) == 0'), + _selected('position(snapshot, within=snapshot[season]) == 0'), _selected('position(snapshot) == 0'), id='a-grouped-position', ), @@ -658,9 +658,9 @@ def test_every_operator_probe_renders(path, name: FormatName, fmt: Format): # --------------------------------------------------------------------------- -#: Two tables a call can leave a value column unread. A sum between -#: `gen_zone`'s two key columns reads no value column at all, and a sum that -#: consumes one of `gen_bt`'s two value columns leaves the other one. +#: Two subjects a call can leave a property unread. A sum between the two +#: members of `generator_snapshot` reads no property at all, and a sum that +#: consumes one of `generator`'s two properties leaves the other one. UNREAD = { 'dimensions': { 'snapshot': {'dtype': 'int', 'ordered': True}, @@ -669,10 +669,8 @@ def test_every_operator_probe_renders(path, name: FormatName, fmt: Format): 'bus': {'dtype': 'str'}, 'technology': {'dtype': 'str'}, }, - 'relations': { - 'gen_zone': {'key': ['generator', 'snapshot'], 'values': 'zone'}, - 'gen_bt': {'key': 'generator', 'values': ['bus', 'technology']}, - }, + 'sets': {'generator_snapshot': {'dims': ['generator', 'snapshot']}}, + 'properties': {'generator_snapshot': {'zone': 'zone'}, 'generator': {'bus': 'bus', 'technology': 'technology'}}, 'parameters': {'cap': {'dims': []}}, 'variables': { 'p': {'dims': ['snapshot', 'generator']}, @@ -690,21 +688,21 @@ def _grouped(dims: list[str], expression: str) -> str: def test_a_sum_that_reads_no_value_column_asks_only_that_the_key_has_a_row(): - """`sum(u, over=generator, by=gen_zone[snapshot])` died with `KeyError: 'zone'`. + """`sum(u, over=generator, by=generator_snapshot[snapshot])` died with `KeyError: 'zone'`. The domain was written as a whole row of the table, which needs an index for every column, and this sum goes between the two key columns: it reads no value column, so there is no index to write in `zone`'s place. What the sum asks of the table is that the key it reads between has a row at all. """ - row = _grouped(['snapshot'], 'sum(u, over=generator, by=gen_zone[snapshot]) <= cap') - assert r'\sum_{g \in \mathcal{G} \,:\, \mathrm{gen\_zone}(g,\ t) \text{ is defined}}' in row, ( + row = _grouped(['snapshot'], 'sum(u, over=generator, by=generator_snapshot[snapshot]) <= cap') + assert r'\sum_{g \in \mathcal{G} \,:\, \left( g,\ t \right) \in \mathrm{generator\_snapshot}}' in row, ( 'the condition is that the row exists, and the unread value column is written nowhere' ) def test_a_value_column_the_call_consumes_is_a_condition_like_a_produced_one(): - """`sum(f, over=[generator, bus], by=gen_bt[technology])` bound `b` and then + """`sum(f, over=[generator, bus], by=generator[technology])` bound `b` and then said nothing about it, so the sum ran over every bus rather than over the one the table puts each generator on. @@ -712,11 +710,11 @@ def test_a_value_column_the_call_consumes_is_a_condition_like_a_produced_one(): column by consuming it too, and either way the column is one lookup at the key. """ - row = _grouped(['technology'], 'sum(f, over=[generator, bus], by=gen_bt[technology]) <= cap') + row = _grouped(['technology'], 'sum(f, over=[generator, bus], by=generator[technology]) <= cap') assert ( r'\sum_{g \in \mathcal{G},\ b \in \mathcal{B} \,:\, ' - r'\mathrm{gen\_bt.bus}(g) = b \wedge \mathrm{gen\_bt.technology}(g) = e}' - ) in row, 'both columns the call touches are read, in the order the table declares them' + r'\mathrm{bus}(g) = b \wedge \mathrm{technology}(g) = e}' + ) in row, 'both properties the call touches are read, in the order the join declares them' # --------------------------------------------------------------------------- @@ -724,7 +722,7 @@ def test_a_value_column_the_call_consumes_is_a_condition_like_a_produced_one(): # --------------------------------------------------------------------------- -#: Two frames over generators, a relation onto buses and a boolean mask — what the +#: Two frames over generators, a property into buses and a boolean mask — what the #: scope and bracketing cases are written against. BUSES = { 'dimensions': { @@ -732,7 +730,7 @@ def test_a_value_column_the_call_consumes_is_a_condition_like_a_produced_one(): 'generator': {'dtype': 'str', 'ordered': True}, 'bus': {'dtype': 'str'}, }, - 'relations': {'bus_of': {'key': 'generator', 'values': 'bus'}}, + 'properties': {'generator': {'bus': 'bus'}}, 'parameters': {'load': {'dims': ['snapshot']}, 'k': {'dims': []}, 'flag': {'dims': ['snapshot'], 'dtype': 'bool'}}, 'variables': {'p': {'dims': ['snapshot', 'generator']}, 'q': {'dims': ['snapshot', 'generator']}}, } @@ -751,15 +749,15 @@ def _row(expression: str, where: str | None = None, **patch: object) -> str: ('expression', 'expected'), [ pytest.param( - 'p == at(sum(q, over=generator, by=bus_of[bus]), by=bus_of[bus])', - r"\sum_{g' \in \mathcal{G} \,:\, \mathrm{bus\_of}(g') = \mathrm{bus\_of}(g)} q_{t,g'}", - id='grouped-by-a-relation', + 'p == at(sum(q, over=generator, by=generator[bus]), by=generator[bus])', + r"\sum_{g' \in \mathcal{G} \,:\, \mathrm{bus}(g') = \mathrm{bus}(g)} q_{t,g'}", + id='grouped-by-a-property', ), pytest.param('p == q - sum(q, over=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): - """Reusing `g` makes `bus_of(g) = bus_of(g)` a tautology — the sum of everything.""" + """Reusing `g` makes `bus(g) = bus(g)` a tautology — the sum of everything.""" assert expected in _row(expression), "the reduction's dummy is primed, so it is not the row's own g" diff --git a/tools/notation.py b/tools/notation.py index 7a604da14..e2783eb52 100644 --- a/tools/notation.py +++ b/tools/notation.py @@ -47,8 +47,8 @@ } BEGIN, END = '', '' -#: The sections of the fixture that declare math. ``dimensions``, ``relations`` -#: and ``parameters`` are absent on purpose: they declare no equation, and what +#: The sections of the fixture that declare math. ``dimensions``, ``sets``, +#: ``properties`` and ``parameters`` are absent on purpose: they declare no equation, and what #: they print is the legend, which the page shows once rather than a row at a #: time. SECTIONS = ('objective', 'constraints', 'expressions', 'conditions', 'variables', 'piecewise', 'sos', 'assumptions') @@ -80,23 +80,23 @@ 'Objective': { 'objective': 'Products and powers in the objective', }, - 'Relations': { - 'balance': 'Sum through a relation', + 'Sets and properties': { + 'balance': 'Sum through a property', 'total': 'Sum over every dimension', - 'lookup': '`at` through a relation', - 'grouped_once': 'Sum into two value columns', - 'looked_up_once': '`at` through two value columns', - 'within_bus': 'Shift within one value column', - 'relational': 'Sum through a bare relation', - 'connected': 'Bare relation as a condition', - 'representative': 'Map into its own dimension', + 'lookup': '`at` through a property', + 'grouped_once': 'Sum into two properties', + 'looked_up_once': '`at` through two properties', + 'within_bus': 'Shift within one of two properties', + 'relational': 'Sum through a set', + 'connected': 'Set as a condition', + 'representative': 'Property into its own dimension', 'by_fuel': 'Sum through a property of a dimension', 'by_area': 'Sum through a property of a set', 'fuelled': 'Property as a condition', - 'zonal': 'Sum through a two-key map', - 'zonal_history': 'Sum over the other key of a two-key map', - 'zonal_membership': 'Sum between the two keys of a map', - 'zonal_lookup': '`at` through a two-key map', + 'zonal': 'Sum over one member of a set, through its property', + 'zonal_history': 'Sum over the other member of a set', + 'zonal_membership': 'Sum between the two members of a set', + 'zonal_lookup': '`at` through a property of a set', }, 'Arithmetic and literals': { 'arithmetic': 'Signs, division and number literals', @@ -146,11 +146,11 @@ 'bounds_do_not_cross': 'Two parameters compared', 'efficiency_is_a_fraction': 'Connective in an assumption', 'lead_times_are_short': 'Parameter compared to a literal', - 'zones_agree': 'Two relations compared', + 'zones_agree': 'Two properties compared', 'budget_covers_the_peak': 'Reduction in an assumption', 'ramps_are_gentle': 'Shift in an assumption', 'flexible_units_have_headroom': 'Assumption with a boolean condition', - 'northern_demand_is_real': 'Assumption with a relation condition', + 'northern_demand_is_real': 'Assumption with a property condition', }, 'Where conditions': { 'scalar': 'Parameter as a condition', @@ -167,7 +167,7 @@ 'counted': 'Count over a dimension', 'counted_here': 'Count along a dimension of the frame', 'run_start': 'Condition at the previous coordinate', - 'zoned': 'Condition through a relation', + 'zoned': 'Condition through a property', }, } @@ -298,11 +298,11 @@ def legend() -> str: #: What the legend is made of. No equation comes from these, so they are shown #: once, together, above the tables they turn into. -DECLARED = ('dimensions', 'relations', 'parameters') +DECLARED = ('dimensions', 'sets', 'properties', 'parameters') def preamble(text: str) -> str: - """The fixture's ``dimensions``/``relations``/``parameters`` sections, verbatim.""" + """The fixture's ``dimensions``/``sets``/``properties``/``parameters`` sections, verbatim.""" sections = [] for name in DECLARED: body = text[text.index(f'\n{name}:') + 1 :] @@ -316,7 +316,7 @@ def block() -> str: rendered = to_markdown(MODEL, numbered=False) parts = [ '### Legend', - 'A dimension, a relation and a parameter declare no equation; what they ' + 'A dimension, a set, a property and a parameter declare no equation; what they ' 'print is the legend every spec opens with.', f'```yaml\n{preamble(MODEL.read_text())}\n```', legend(), diff --git a/tools/pypsa_split.py b/tools/pypsa_split.py index 46588ccb9..e2186adff 100644 --- a/tools/pypsa_split.py +++ b/tools/pypsa_split.py @@ -50,7 +50,8 @@ SOURCE = Path('examples/pypsa.yaml') SECTIONS = ( 'dimensions', - 'relations', + 'sets', + 'properties', 'parameters', 'variables', 'expressions', @@ -58,7 +59,7 @@ 'constraints', 'assumptions', ) -FRAME = ('dimensions', 'relations') +FRAME = ('dimensions', 'sets', 'properties') IDENT = re.compile(r'[A-Za-z_][A-Za-z0-9_]*') HEADER = '# SPDX-FileCopyrightText: mathspec Contributors\n#\n# SPDX-License-Identifier: MIT\n' @@ -198,7 +199,10 @@ def __init__(self, path: Path = SOURCE) -> None: text = path.read_text() self.data: dict[str, Any] = yaml.safe_load(text) self.entries = _sliced(text) - self.kind = {n: s for s in SECTIONS if s not in ('constraints', 'assumptions') for n in self.data.get(s, {})} + self.kind: dict[str, str] = {} + for s in SECTIONS: + if s not in ('constraints', 'assumptions', 'properties'): + self.kind |= dict.fromkeys(self.data.get(s, {}), s) #: hub -> the topic of each term -> the term, in the order the topics sort in. self.shares: dict[str, dict[str, str]] = {} for hub in HUBS: @@ -273,11 +277,22 @@ def fragments(model: Model) -> dict[str, str]: } frame_reads = read | set().union(*(model.names_in(dims) for dims in stated.values())) frame = {model.key(n) for n in frame_reads if model.key(n)[0] in FRAME} + frame |= _properties_read(model, mine | frame) frame |= {model.key(n) for key in list(frame) for n in model.names_in(model.data[key[0]][key[1]])} written[name] = _fragment(model, mine | frame, given, stated, adds, homes_here) return written +def _properties_read(model: Model, keys: set[Key]) -> set[Key]: + """The properties of each subject that the entries *keys* read as ``subject[...]``, with the whole subject's map. + + A subject's properties are one entry of the source, so a fragment that + reads one of them carries them all, and `merge` joins the copies. + """ + text = '\n'.join(model.entries[key] for key in keys) + return {('properties', s) for s in model.data.get('properties', {}) if re.search(rf'\b{s}\[', text)} + + def _dumped(name: str, entry: Mapping[str, object], indent: str = ' ') -> str: """A generated entry, its long text folded one term per line.""" lines = [f'{indent}{name}:'] diff --git a/tools/spec_math.py b/tools/spec_math.py index d58bc82a8..d1069952e 100644 --- a/tools/spec_math.py +++ b/tools/spec_math.py @@ -29,20 +29,20 @@ 'sum(array)': 'sum_all', 'sum(array, over=dim)': 'sum', 'sum(array, over=[a, …])': 'sum_list', - 'sum(array, over=dim, by=relation[c])': 'sum_by', - 'sum(array, over=dim, by=relation[c]), joining on the rest of the key': 'sum_by_columns', - 'sum(array, over=[dim, …], by=relation[c, …])': 'sum_by_column_lists', - 'at(array, by=relation[c])': 'at', - 'at(array, by=relation[c]), two columns over one dimension': 'at_columns', + 'sum(array, over=dim, by=subject[p])': 'sum_by', + 'sum(array, over=dim, by=subject[p]), joining on the other members': 'sum_by_columns', + 'sum(array, over=[dim, …], by=subject[p, …])': 'sum_by_column_lists', + 'at(array, by=subject[p])': 'at', + 'at(array, by=subject[p]), two properties into one dimension': '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', 'shift(array, along=dim, offset=p, edge=…)': 'shift_by_parameter', - 'shift(array, along=dim, offset=n, within=relation[c])': 'shift_partitioned', + 'shift(array, along=dim, offset=n, within=subject[p])': 'shift_partitioned', 'sum_back(array, along=dim, window=n)': 'sum_back', 'sum_back(array, along=dim, window=p)': 'sum_back_by_parameter', "sum_back(array, along=dim, window=p, edge='wrap')": 'sum_back_wrap', - 'sum_back(array, along=dim, window=n, within=relation[c])': 'sum_back_partitioned', + 'sum_back(array, along=dim, window=n, within=subject[p])': 'sum_back_partitioned', 'dual(constraint)': 'dual', } From 24b66780a0e7bf4955dfc25c72132026b587f021 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 23:53:26 +0000 Subject: [PATCH 2/2] docs(changelog): add the line for retiring relations Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6yvGzZN2iR4WyA9RumvvJ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f95e7833..dafc1e280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp ## Upcoming version +- feat(language)!: sets and properties replace relations, and a relations section is refused ([#5](https://github.com/fluxopt/mathspec/pull/5)) - feat(typesetting): the math names a property after itself, and the reading page gives the tables of sets and properties ([#4](https://github.com/fluxopt/mathspec/pull/4)) - feat(composition): fragments join the properties of a subject, and a set two fragments declare differently is refused ([#3](https://github.com/fluxopt/mathspec/pull/3)) - feat(language): a call reads a property of a dimension or a set, written subject[property] ([#2](https://github.com/fluxopt/mathspec/pull/2))