diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6471c87..83ab8671f 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)!: `missing:` says what a missing row means, a table short of a row is refused unless the file says otherwise, and a variable's `absence:` is now `missing:` ([#810](https://github.com/energy-models/mathspec/pull/810)) - docs(pypsa): a risk preference with weight zero builds the CVaR variables and refuses quadratic costs, as in PyPSA ([#849](https://github.com/energy-models/mathspec/pull/849)) - docs(pypsa): storage that retires before the last counted snapshot closes a global limit at its last active level ([#850](https://github.com/energy-models/mathspec/pull/850)) - refactor(program)!: the node a use of a named expression stands as is `NamedExpression`, beside `NamedMask`, and `Named` is gone ([#822](https://github.com/energy-models/mathspec/pull/822)) diff --git a/docs/about/relations-as-linear-maps.md b/docs/about/relations-as-linear-maps.md index e95304655..22d202299 100644 --- a/docs/about/relations-as-linear-maps.md +++ b/docs/about/relations-as-linear-maps.md @@ -16,7 +16,7 @@ dimensions: generator: { dtype: str } zone: { dtype: str } relations: - gen_zone: { key: [generator, snapshot], values: zone } + gen_zone: { key: [generator, snapshot], values: zone, missing: absent } parameters: zone_cap: { dims: [snapshot, zone] } variables: @@ -47,8 +47,10 @@ $`f: K \to V`$, and \mathbf{1}_R(k, v) = [\, f(k) = v \,]. ``` -The function is partial where a key tuple has no row. A bare relation is a -subset and nothing more. Above, `gen_zone` is the graph of +The function is total by default: a key tuple with no row is +[refused](../reference/language/relations.md#the-data-contract) when the data +is attached. Under `missing: absent`, as above, it is partial, and a key tuple +with no row is off its domain. A bare relation is a subset and nothing more. 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 it one: the loader checks one row per key tuple when the data is attached. @@ -182,7 +184,8 @@ consumer builds is not the matrix. undefined where $`f`$ is, 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`$ on the page, so a - reader sees which rows exist without opening the data. + reader sees which rows exist without opening the data. Under the default, + `refused`, the domain is every key tuple and there is nothing off it. ## Partitions and tests diff --git a/docs/examples/library/composed.md b/docs/examples/library/composed.md index 66c7d0b7b..e6bf24d12 100644 --- a/docs/examples/library/composed.md +++ b/docs/examples/library/composed.md @@ -67,13 +67,13 @@ variables: Port_p: dims: [snapshot, port] domain: continuous - absence: undefined + missing: absent description: what a port puts into its bus in a snapshot, negative for a withdrawal Generator_p: dims: [snapshot, generator] bounds: {lower: 0.0, upper: Generator_p_nom} domain: continuous - absence: undefined + missing: absent description: '`Generator-p` — what a generator produces in a snapshot' constraints: Bus_nodal_balance: diff --git a/docs/examples/pypsa.md b/docs/examples/pypsa.md index 8e4b4d12a..4e260da72 100644 --- a/docs/examples/pypsa.md +++ b/docs/examples/pypsa.md @@ -132,7 +132,7 @@ def build(): | PyPSA | status | note | | ----------------------------------------------------- | ------ | ------------------------------------------------------------- | | [`StorageUnit-p_dispatch`, `-p_store`, `-state_of_charge`, `Store-e`, `Store-p`](#variable-domains) | done | | -| [`StorageUnit-spill`](#variable-domains) | done | `where: inflow > 0`, `absence: zero`; bounds on the variable, as PyPSA's | +| [`StorageUnit-spill`](#variable-domains) | done | `where: inflow > 0`, `missing: neutral`; bounds on the variable, as PyPSA's | | [`StorageUnit-fix-*`](#storageunit-fix-p_dispatch-lower), [`Store-fix-e-*`](#store-fix-e-lower) | done | | | [`StorageUnit-energy_balance`](#storageunit-energy_balance) | done | the charge carried into a snapshot is a cased quantity — cyclic, opening, carried; `(1-loss)**eh` is prep | | [`StorageUnit-energy_balance`](#storageunit-energy_balance) with efficiencies per snapshot | done | rung 60 | @@ -5307,7 +5307,7 @@ file stated the CVaR columns always, and the rows and the refusal only where `omega > 0`, so this rung got the columns and neither the rows nor the refusal. Now the columns, the rows and the refusal stand where `CVaR_omega` has a row, and `0.0` is a row. Data prep writes it only where a risk -preference is set. The columns take `absence: zero`, so the objective term +preference is set. The columns take `missing: neutral`, so the objective term `CVaR_omega * CVaR` stands without them. | PyPSA | status | note | @@ -5464,10 +5464,10 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\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}`$ — 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}`$ — passive branches between two buses, their flow set by impedance and tap ratio, with a phase shift fixed or optimised | +| $`\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{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}`$ — 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{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{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` | @@ -5486,30 +5486,30 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | | $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power. PyPSA refuses one that differs by scenario (`constants.py:43`) | | $`\mathrm{com}`$ | `Generator_committable` over $`\mathcal{G}`$ — whether output is gated by an on/off status decision | -| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | -| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | +| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — most output in the snapshot a unit starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — most output in the snapshot before a unit stops, per unit of nominal power; no value means no limit | | $`\mathrm{UT}`$ | `Generator_min_up_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays on once started | | $`\mathrm{DT}`$ | `Generator_min_down_time` over $`\Xi \times \mathcal{G}`$ — least snapshots a unit stays off once stopped | | $`\mathrm{u}^{0}`$ | `Generator_status_initial` over $`\Xi \times \mathcal{G}`$ — one where the unit was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\Xi \times \mathcal{G}`$ — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | +| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | | $`\mathrm{hold}`$ | `Generator_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the up time a unit brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}`$ | `Generator_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true while the down time a unit brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | -| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\Xi \times \mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$ — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep | +| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep | | $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma`$ | `Generator_maintenance_pu` over $`\Xi \times \mathcal{G}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\Xi \times \mathcal{G}`$ — how many maintenance events the horizon holds | -| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | -| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | | $`\mathrm{f}^{\mathrm{nom}}`$ | `Link_p_nom` over $`\Xi \times \mathcal{L}`$ — nominal power | | $`\mathrm{ext}^{f}`$ | `Link_p_nom_extendable` over $`\mathcal{L}`$ — whether the nominal power is a decision | | $`\underline{\mathrm{f}}`$ | `Link_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — least flow, per unit of nominal power — negative for a link that carries both ways | @@ -5520,25 +5520,25 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | | $`\mathrm{com}^{f}`$ | `Link_committable` over $`\mathcal{L}`$ — whether flow is gated by an on/off status decision | -| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot a link starts, per unit of nominal power | -| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot before a link stops, per unit of nominal power | +| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — most flow in the snapshot a link starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — most flow in the snapshot before a link stops, per unit of nominal power; no value means no limit | | $`\mathrm{UT}^{f}`$ | `Link_min_up_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays on once started | | $`\mathrm{DT}^{f}`$ | `Link_min_down_time` over $`\Xi \times \mathcal{L}`$ — least snapshots a link stays off once stopped | | $`\mathrm{u}^{f,0}`$ | `Link_status_initial` over $`\Xi \times \mathcal{L}`$ — one where the link was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{f}^{0}`$ | `Link_p_init` over $`\Xi \times \mathcal{L}`$ — the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot | +| $`\mathrm{f}^{0}`$ | `Link_p_init` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot | | $`\mathrm{hold}^{f}`$ | `Link_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the up time a link brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}^{f}`$ | `Link_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true while the down time a link brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{f,\mathrm{dn}}`$ | `Link_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{f,\mathrm{on}}`$ | `Link_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one snapshot spent on | -| $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\Xi \times \mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\Xi \times \mathcal{L}`$ — the bound a committed extendable link's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep | +| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the bound a committed extendable link's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep | | $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\Xi \times \mathcal{L}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\Xi \times \mathcal{L}`$ — how many maintenance events the horizon holds | -| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\Xi \times \mathcal{L}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{z}^{\mathrm{nom}}`$ | `Process_p_nom` over $`\Xi \times \mathcal{J}`$ — nominal internal power | | $`\mathrm{ext}^{z}`$ | `Process_p_nom_extendable` over $`\mathcal{J}`$ — whether the nominal internal power is a decision | @@ -5549,40 +5549,40 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. The same in every scenario, as the delay | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | | $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | -| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | +| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — a given internal power schedule; a process without one has no row here | | $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\Xi \times \mathcal{J}`$ — least nominal power an extendable process may be built at | | $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\Xi \times \mathcal{J}`$ — most nominal power an extendable process may be built at | | $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\Xi \times \mathcal{J}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | +| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$, `neutral` where the data has no row — a given nominal power for an extendable process; one without a value has no row here | | $`\mathrm{com}^{z}`$ | `Process_committable` over $`\mathcal{J}`$ — whether internal power is gated by an on/off status decision | -| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot a process starts, per unit of nominal power | -| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot before a process stops, per unit of nominal power | +| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — most internal power in the snapshot a process starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — most internal power in the snapshot before a process stops, per unit of nominal power; no value means no limit | | $`\mathrm{UT}^{z}`$ | `Process_min_up_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays on once started | | $`\mathrm{DT}^{z}`$ | `Process_min_down_time` over $`\Xi \times \mathcal{J}`$ — least snapshots a process stays off once stopped | | $`\mathrm{u}^{z,0}`$ | `Process_status_initial` over $`\Xi \times \mathcal{J}`$ — one where the process was on before the first snapshot, zero where off — PyPSA's `up_time_before > 0`, data prep | -| $`\mathrm{z}^{0}`$ | `Process_p_init` over $`\Xi \times \mathcal{J}`$ — the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot | +| $`\mathrm{z}^{0}`$ | `Process_p_init` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot | | $`\mathrm{hold}^{z}`$ | `Process_must_stay_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the up time a process brought into the horizon still binds — data prep, since `position()` compares against a literal rather than a parameter | | $`\mathrm{rest}^{z}`$ | `Process_must_stay_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true while the down time a process brought into the horizon still binds — PyPSA's `min_down_time - down_time_before` snapshots, where `down_time_before > 0`, data prep for the same reason | | $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{z,\mathrm{dn}}`$ | `Process_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{z,\mathrm{on}}`$ | `Process_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one snapshot spent on | -| $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\Xi \times \mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | -| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\Xi \times \mathcal{J}`$ — the bound a committed extendable process's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep | +| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the bound a committed extendable process's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep | | $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\Xi \times \mathcal{J}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\Xi \times \mathcal{J}`$ — how many maintenance events the horizon holds | -| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | | $`\mathrm{load}`$ | `Load_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{D}`$ — demand | | $`\mathrm{sgn}^{\mathrm{load}}`$ | `Load_sign` over $`\mathcal{D}`$ — the sign a load's demand enters its bus's balance with — PyPSA's `sign`, `-1` unless given, `1` for a load that feeds its bus. PyPSA refuses one that differs by scenario (`constants.py:43`) | | $`\mathrm{on}^{\mathrm{load}}`$ | `Load_active` over $`\mathcal{D}`$ — whether a load stands in the model — PyPSA's `active`. A load has no build year and no lifetime, so the flag holds in every snapshot. PyPSA refuses one that differs by scenario (`constants.py:51`) | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | -| $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one | -| $`\alpha`$ | `CVaR_alpha` (scalar) — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability | +| $`\omega`$ | `CVaR_omega` (scalar), `neutral` where the data has no row — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one | +| $`\alpha`$ | `CVaR_alpha` (scalar), `neutral` where the data has no row — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` | | $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs; PyPSA reads it only under `multi_investment_periods`, so data prep feeds one otherwise, whatever the column holds (`optimize.py:205-207`, `:264-266`) | | $`\mathrm{w}^{\mathrm{yr}}`$ | `period_weight_years` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.years` — what a period's energy weighs in a `primary_energy` or `operational_limit` row; PyPSA reads it only under `multi_investment_periods`, so data prep feeds one otherwise | | $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | @@ -5605,33 +5605,33 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{new}^{e}`$ | `Store_first_active` over $`\mathcal{Y} \times \mathcal{V}`$ — one in the first period a store stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | | $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | | $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | -| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise | +| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$, `neutral` where the data has no row — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep | -| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | -| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | +| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — a given output schedule; a generator without one has no row here | +| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — a given flow schedule; a link without one has no row here | | $`\mathrm{w}^{\mathrm{sto}}`$ | `snapshot_weightings_stores` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance | | $`\mathrm{w}^{\mathrm{gen}}`$ | `snapshot_weightings_generators` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.generators` — hours a snapshot stands for in an energy total | | $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\Xi \times \mathcal{G}`$ — least nominal power an extendable generator may be built at | | $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\Xi \times \mathcal{G}`$ — most nominal power an extendable generator may be built at | | $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\Xi \times \mathcal{G}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$ — a given nominal power for an extendable generator; one without a value has no row here | +| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$, `neutral` where the data has no row — a given nominal power for an extendable generator; one without a value has no row here | | $`\underline{\mathrm{E}}`$ | `Generator_e_sum_min` over $`\Xi \times \mathcal{G}`$ — least energy over the horizon; minus infinity where no floor is meant | | $`\overline{\mathrm{E}}`$ | `Generator_e_sum_max` over $`\Xi \times \mathcal{G}`$ — most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant | | $`\underline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_min` over $`\Xi \times \mathcal{L}`$ — least nominal power an extendable link may be built at | | $`\overline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_max` over $`\Xi \times \mathcal{L}`$ — most nominal power an extendable link may be built at | | $`\mathrm{c}^{\mathrm{cap},f}`$ | `Link_capital_cost` over $`\Xi \times \mathcal{L}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\mathcal{L}`$ — a given nominal power for an extendable link; one without a value has no row here | +| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\mathcal{L}`$, `neutral` where the data has no row — a given nominal power for an extendable link; one without a value has no row here | | $`\underline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_min` over $`\Xi \times \mathcal{S}`$ — least nominal power an extendable storage unit may be built at | | $`\overline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_max` over $`\Xi \times \mathcal{S}`$ — most nominal power an extendable storage unit may be built at | | $`\mathrm{c}^{\mathrm{cap},h}`$ | `StorageUnit_capital_cost` over $`\Xi \times \mathcal{S}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\mathcal{S}`$ — a given nominal power for an extendable storage unit; one without a value has no row here | +| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\mathcal{S}`$, `neutral` where the data has no row — a given nominal power for an extendable storage unit; one without a value has no row here | | $`\underline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_min` over $`\Xi \times \mathcal{V}`$ — least nominal capacity an extendable store may be built at | | $`\overline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_max` over $`\Xi \times \mathcal{V}`$ — most nominal capacity an extendable store may be built at | | $`\mathrm{c}^{\mathrm{cap},e}`$ | `Store_capital_cost` over $`\Xi \times \mathcal{V}`$ — cost of one unit of nominal capacity for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\mathcal{V}`$ — a given nominal capacity for an extendable store; one without a value has no row here | +| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\mathcal{V}`$, `neutral` where the data has no row — a given nominal capacity for an extendable store; one without a value has no row here | | $`\mathrm{h}^{\mathrm{nom}}`$ | `StorageUnit_p_nom` over $`\Xi \times \mathcal{S}`$ — nominal power | | $`\mathrm{ext}^{h}`$ | `StorageUnit_p_nom_extendable` over $`\mathcal{S}`$ — whether the nominal power is a decision | -| $`\mathrm{h}^{\mathrm{mod}}`$ | `StorageUnit_p_nom_mod` over $`\mathcal{S}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{h}^{\mathrm{mod}}`$ | `StorageUnit_p_nom_mod` over $`\mathcal{S}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\underline{\mathrm{h}}`$ | `StorageUnit_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most storing, per unit of nominal power and negated | | $`\overline{\mathrm{h}}`$ | `StorageUnit_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most dispatch, per unit of nominal power | | $`\mathrm{T}^{h}`$ | `StorageUnit_max_hours` over $`\Xi \times \mathcal{S}`$ — energy capacity, as hours of dispatch at nominal power | @@ -5650,13 +5650,13 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | -| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charging schedule; a unit without one has no row here | -| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charge schedule; a unit without one has no row here | +| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given net dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given charging schedule; a unit without one has no row here | +| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given charge schedule; a unit without one has no row here | | $`\mathrm{e}^{\mathrm{nom}}`$ | `Store_e_nom` over $`\Xi \times \mathcal{V}`$ — nominal energy capacity | | $`\mathrm{ext}^{e}`$ | `Store_e_nom_extendable` over $`\mathcal{V}`$ — whether the nominal energy capacity is a decision | -| $`\mathrm{e}^{\mathrm{mod}}`$ | `Store_e_nom_mod` over $`\mathcal{V}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{e}^{\mathrm{mod}}`$ | `Store_e_nom_mod` over $`\mathcal{V}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\underline{\mathrm{e}}`$ | `Store_e_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — least energy held, per unit of nominal capacity — negative for a store that may go short | | $`\overline{\mathrm{e}}`$ | `Store_e_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — most energy held, per unit of nominal capacity | | $`\mathrm{sgn}^{q}`$ | `Store_sign` over $`\mathcal{V}`$ — the sign the power a store delivers enters its bus's balance with — PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by scenario (`constants.py:43`) | @@ -5670,39 +5670,39 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | | $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | -| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | -| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given schedule of power delivered; a store without one has no row here | +| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$, `neutral` where the data has no row — a given energy schedule; a store without one has no row here | +| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$, `neutral` where the data has no row — a given schedule of power delivered; a store without one has no row here | | $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\Xi \times \mathcal{K}`$ — nominal apparent power | | $`\mathrm{ext}^{s}`$ | `Line_s_nom_extendable` over $`\mathcal{K}`$ — whether the nominal apparent power is a decision | -| $`\mathrm{s}^{\mathrm{mod}}`$ | `Line_s_nom_mod` over $`\mathcal{K}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{s}^{\mathrm{mod}}`$ | `Line_s_nom_mod` over $`\mathcal{K}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | | $`\underline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_min` over $`\Xi \times \mathcal{K}`$ — least nominal apparent power an extendable line may be built at | | $`\overline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_max` over $`\Xi \times \mathcal{K}`$ — most nominal apparent power an extendable line may be built at | | $`\mathrm{c}^{\mathrm{cap},s}`$ | `Line_capital_cost` over $`\Xi \times \mathcal{K}`$ — cost of one unit of nominal apparent power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | -| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | -| $`\overline{\delta}`$ | `Line_v_ang_max` over $`\Xi \times \mathcal{K}`$ — the most the voltage angle difference across a line may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. A line whose carrier is not AC has no row either. The deprecated `v_ang_min` is ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) | +| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$, `neutral` where the data has no row — a given nominal apparent power for an extendable line; one without a value has no row here | +| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$, `neutral` where the data has no row — a given flow schedule; a line without one has no row here | +| $`\overline{\delta}`$ | `Line_v_ang_max` over $`\Xi \times \mathcal{K}`$, `neutral` where the data has no row — the most the voltage angle difference across a line may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. A line whose carrier is not AC has no row either. The deprecated `v_ang_min` is ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) | | $`\mathrm{x}^{\mathrm{eff}}`$ | `Line_x_pu_eff` over $`\Xi \times \mathcal{K}`$ — the line's effective series reactance — PyPSA's `x_pu_eff`, `x` over the square of its bus's nominal voltage, data prep | -| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) | -| $`\beta`$ | `Line_BODF` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the PTDF of the sub-network the period's active branches form, data prep; a row only where the line and the outage are active in the period and share a sub-network, -1 at the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) | +| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{C}`$, `neutral` where the data has no row — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) | +| $`\beta`$ | `Line_BODF` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$, `neutral` where the data has no row — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the PTDF of the sub-network the period's active branches form, data prep; a row only where the line and the outage are active in the period and share a sub-network, -1 at the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) | | $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run. A security-constrained run over a network with passive branches builds no loss: PyPSA does not hand the keyword to `create_model` (`abstract.py:528-532`) but to the solver (`:548`), so data prep feeds false there | | $`\overline{\ell}`$ | `Line_loss_max` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | | $`\mathrm{a}`$ | `Line_loss_slope` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | | $`\mathrm{b}`$ | `Line_loss_offset` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{B}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | | $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\Xi \times \mathcal{M}`$ — nominal apparent power | | $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | -| $`\sigma^{\mathrm{mod}}`$ | `Transformer_s_nom_mod` over $`\mathcal{M}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\sigma^{\mathrm{mod}}`$ | `Transformer_s_nom_mod` over $`\mathcal{M}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | | $`\underline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_min` over $`\Xi \times \mathcal{M}`$ — least nominal apparent power an extendable transformer may be built at | | $`\overline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_max` over $`\Xi \times \mathcal{M}`$ — most nominal apparent power an extendable transformer may be built at | | $`\mathrm{c}^{\mathrm{cap},\sigma}`$ | `Transformer_capital_cost` over $`\Xi \times \mathcal{M}`$ — cost of one unit of nominal apparent power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | -| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | -| $`\overline{\delta}^{\sigma}`$ | `Transformer_v_ang_max` over $`\Xi \times \mathcal{M}`$ — the most the voltage angle difference across a transformer, its phase shift included, may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's | +| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$, `neutral` where the data has no row — a given nominal apparent power for an extendable transformer; one without a value has no row here | +| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the data has no row — a given flow schedule; a transformer without one has no row here | +| $`\overline{\delta}^{\sigma}`$ | `Transformer_v_ang_max` over $`\Xi \times \mathcal{M}`$, `neutral` where the data has no row — the most the voltage angle difference across a transformer, its phase shift included, may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's | | $`\mathrm{x}^{\mathrm{eff},\sigma}`$ | `Transformer_x_pu_eff` over $`\Xi \times \mathcal{M}`$ — the transformer's effective series reactance — PyPSA's `x_pu_eff`, `x` over its `s_nom` times its tap ratio, data prep | -| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's | -| $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network | -| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{C}`$ — a fixed transformer's phase shift in radians at each snapshot, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row | +| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{C}`$, `neutral` where the data has no row — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's | +| $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$, `neutral` where the data has no row — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network | +| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{C}`$, `neutral` where the data has no row — a fixed transformer's phase shift in radians at each snapshot, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row | | $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift`. The shift parameters carry no scenario, while PyPSA reads them per scenario (`variables.py:433-443`, `constraints.py:1652`, `:1735`): a parity gap (\#783) | | $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | | $`\mathrm{Transformer\_phase\_shift\_max}`$ | `Transformer_phase_shift_max` over $`\mathcal{M}`$ — the most a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer | @@ -5714,23 +5714,23 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`\mathrm{type}`$ | `GlobalConstraint_type` over $`\mathcal{I}`$ — which formula the row takes — `primary_energy`, `operational_limit`, `transmission_volume_expansion_limit`, `transmission_expansion_cost_limit` or `tech_capacity_expansion_limit` | | $`\mathrm{sense}`$ | `GlobalConstraint_sense` over $`\Xi \times \mathcal{I}`$ — which way the row binds in each scenario — `<=`, `>=` or `==`; PyPSA reads a row's sense per scenario (`global_constraints.py:551`, `:749`, `:829`) | | $`\mathrm{K}`$ | `GlobalConstraint_constant` over $`\Xi \times \mathcal{I}`$ — the constant the total is held against; what a variable cannot carry — an initial charge, times its period's years for each counted period where the storage reopens per period, or a non-extendable build — is folded in here by data prep. PyPSA reads it per scenario (`global_constraints.py:552`, `:750`, `:829`) | -| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\Xi \times \mathcal{I} \times \mathcal{T}`$ — whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:372`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:364-369`) | -| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{T} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row | -| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | -| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | -| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | -| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | -| $`\mathrm{len}`$ | `Line_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$ — the line's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$ — the link's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | -| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | -| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\Xi \times \mathcal{I} \times \mathcal{T}`$, `neutral` where the data has no row — whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:372`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:364-369`) | +| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row | +| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$, `neutral` where the data has no row — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | +| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$, `neutral` where the data has no row — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | +| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{S}`$, `neutral` where the data has no row — one where the storage unit is in the row's set — data prep; one outside it has no row | +| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\Xi \times \mathcal{I} \times \mathcal{V}`$, `neutral` where the data has no row — one where the store is in the row's set — data prep; one outside it has no row | +| $`\mathrm{len}`$ | `Line_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$, `neutral` where the data has no row — the line's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$, `neutral` where the data has no row — the link's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{K}`$, `neutral` where the data has no row — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | +| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\Xi \times \mathcal{I} \times \mathcal{L}`$, `neutral` where the data has no row — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | +| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{L}`$, `neutral` where the data has no row — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{K}`$, `neutral` where the data has no row — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{S}`$, `neutral` where the data has no row — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{V}`$, `neutral` where the data has no row — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{I} \times \mathcal{J}`$, `neutral` where the data has no row — one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables @@ -5742,38 +5742,38 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`h^{+}`$ | `StorageUnit_p_dispatch` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_dispatch` — power delivered to the bus | | $`h^{-}`$ | `StorageUnit_p_store` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_store` — power drawn from the bus into charge | | $`\mathit{soc}`$ | `StorageUnit_state_of_charge` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-state_of_charge` — energy held at the end of a snapshot | -| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | +| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the mask leaves it out — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | | $`e`$ | `Store_e` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — `Store-e` — energy held at the end of a snapshot | | $`q`$ | `Store_p` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — `Store-p` — power delivered to the bus; charging is negative | | $`N`$ | `Generator_n_mod` over $`\mathcal{G}`$ — `Generator-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | | $`u`$ | `Generator_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-status` — how much of a committable unit is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}`$ | `Generator_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-start_up` — how much of a committable unit turns on this snapshot, capped as the status is | | $`\mathit{dn}`$ | `Generator_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-shut_down` — how much of a committable unit turns off this snapshot, capped as the status is | -| $`\mu`$ | `Generator_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{\mathrm{nom}}`$ | `Generator_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu`$ | `Generator_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{\mathrm{nom}}`$ | `Generator_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`N^{f}`$ | `Link_n_mod` over $`\mathcal{L}`$ — `Link-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | | $`u^{f}`$ | `Link_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-status` — how much of a committable link is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}^{f}`$ | `Link_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-start_up` — how much of a committable link turns on this snapshot, capped as the status is | | $`\mathit{dn}^{f}`$ | `Link_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-shut_down` — how much of a committable link turns off this snapshot, capped as the status is | -| $`\mu^{f}`$ | `Link_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance` — whether a maintainable link is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{f,\mathrm{up}}`$ | `Link_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{f,\mathrm{nom}}`$ | `Link_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{f,u}`$ | `Link_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu^{f}`$ | `Link_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance` — whether a maintainable link is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{f,\mathrm{up}}`$ | `Link_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{f,\mathrm{nom}}`$ | `Link_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{f,u}`$ | `Link_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`N^{z}`$ | `Process_n_mod` over $`\mathcal{J}`$ — `Process-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | | $`u^{z}`$ | `Process_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-status` — how much of a committable process is on: an integer the rows below cap at one, or at the module count where the build is modular | | $`\mathit{up}^{z}`$ | `Process_start_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-start_up` — how much of a committable process turns on this snapshot, capped as the status is | | $`\mathit{dn}^{z}`$ | `Process_shut_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-shut_down` — how much of a committable process turns off this snapshot, capped as the status is | -| $`\mu^{z}`$ | `Process_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance` — whether a maintainable process is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{z,\mathrm{up}}`$ | `Process_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{z,\mathrm{nom}}`$ | `Process_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{z,u}`$ | `Process_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu^{z}`$ | `Process_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance` — whether a maintainable process is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{z,\mathrm{up}}`$ | `Process_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{z,\mathrm{nom}}`$ | `Process_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{z,u}`$ | `Process_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | -| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | +| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$, `neutral` where the mask leaves it out — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | -| $`\ell^{\sigma}`$ | `Transformer_loss` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-loss` — what a transformer dissipates carrying its flow, as a line does; absent, and zero in the balance, where the network is lossless | -| $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | +| $`\ell^{\sigma}`$ | `Transformer_loss` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the mask leaves it out — `Transformer-loss` — what a transformer dissipates carrying its flow, as a line does; absent, and zero in the balance, where the network is lossless | +| $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the mask leaves it out — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`N^{s}`$ | `Line_n_mod` over $`\mathcal{K}`$ — `Line-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | | $`P`$ | `Generator_p_nom_ext` over $`\mathcal{G}`$ — `Generator-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | @@ -5785,9 +5785,9 @@ A plain `n.optimize()`, and its multi-period and stochastic classes, in one file | $`N^{h}`$ | `StorageUnit_n_mod` over $`\mathcal{S}`$ — `StorageUnit-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | | $`E`$ | `Store_e_nom_ext` over $`\mathcal{V}`$ — `Store-e_nom` — nominal capacity where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`N^{e}`$ | `Store_n_mod` over $`\mathcal{V}`$ — `Store-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | -| $`a`$ | `CVaR_a` over $`\Xi`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | -| $`\theta`$ | `CVaR_theta` (scalar) — `CVaR-theta` — where the tail starts, the value at risk | -| $`CVaR`$ | `CVaR` (scalar) — `CVaR` — the tail's average cost, what the objective prices at `omega` | +| $`a`$ | `CVaR_a` over $`\Xi`$, `neutral` where the mask leaves it out — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | +| $`\theta`$ | `CVaR_theta` (scalar), `neutral` where the mask leaves it out — `CVaR-theta` — where the tail starts, the value at risk | +| $`CVaR`$ | `CVaR` (scalar), `neutral` where the mask leaves it out — `CVaR` — the tail's average cost, what the objective prices at `omega` | #### Definitions diff --git a/docs/examples/pypsa/carrier.md b/docs/examples/pypsa/carrier.md index 4f03b7117..64114deba 100644 --- a/docs/examples/pypsa/carrier.md +++ b/docs/examples/pypsa/carrier.md @@ -26,6 +26,7 @@ parameters: `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise dims: [carrier] + missing: neutral Carrier_max_relative_growth: description: >- share of the previous period's additions that may be added on top — the @@ -73,7 +74,7 @@ constraints: | Symbol | Meaning | |---|---| -| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$ — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise | +| $`\overline{\Delta}`$ | `Carrier_max_growth` over $`\mathcal{I}`$, `neutral` where the data has no row — most capacity of a carrier that may be added in a period; no value means no limit. The least over the scenarios, as PyPSA takes it (`global_constraints.py:227-231`), data prep. PyPSA reads it only under `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise | | $`\mathrm{r}`$ | `Carrier_max_relative_growth` over $`\mathcal{I}`$ — share of the previous period's additions that may be added on top — the least over the scenarios, as PyPSA takes it, data prep | #### Given diff --git a/docs/examples/pypsa/cost.md b/docs/examples/pypsa/cost.md index ecfe6f2e5..00c086e4e 100644 --- a/docs/examples/pypsa/cost.md +++ b/docs/examples/pypsa/cost.md @@ -15,27 +15,28 @@ dimensions: parameters: CVaR_alpha: - description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability + description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` dims: [] + missing: neutral variables: CVaR_a: description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not" dims: [scenario] where: CVaR_omega - absence: zero + missing: neutral bounds: lower: 0 CVaR_theta: description: "`CVaR-theta` — where the tail starts, the value at risk" dims: [] where: CVaR_omega - absence: zero + missing: neutral CVaR: description: "`CVaR` — the tail's average cost, what the objective prices at `omega`" dims: [] where: CVaR_omega - absence: zero + missing: neutral given: parameters: @@ -73,15 +74,15 @@ constraints: | Symbol | Meaning | |---|---| -| $`\alpha`$ | `CVaR_alpha` (scalar) — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability | +| $`\alpha`$ | `CVaR_alpha` (scalar), `neutral` where the data has no row — PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` | #### Variables | Symbol | Meaning | |---|---| -| $`a`$ | `CVaR_a` over $`\Xi`$ — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | -| $`\theta`$ | `CVaR_theta` (scalar) — `CVaR-theta` — where the tail starts, the value at risk | -| $`CVaR`$ | `CVaR` (scalar) — `CVaR` — the tail's average cost, what the objective prices at `omega` | +| $`a`$ | `CVaR_a` over $`\Xi`$, `neutral` where the mask leaves it out — `CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not | +| $`\theta`$ | `CVaR_theta` (scalar), `neutral` where the mask leaves it out — `CVaR-theta` — where the tail starts, the value at risk | +| $`CVaR`$ | `CVaR` (scalar), `neutral` where the mask leaves it out — `CVaR` — the tail's average cost, what the objective prices at `omega` | #### Given diff --git a/docs/examples/pypsa/generator.md b/docs/examples/pypsa/generator.md index cd875b285..295a43b95 100644 --- a/docs/examples/pypsa/generator.md +++ b/docs/examples/pypsa/generator.md @@ -72,6 +72,7 @@ parameters: Generator_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [generator] + missing: neutral Generator_modules_installed: description: >- how many whole modules a committable build has in place: `Generator_p_nom @@ -100,6 +101,7 @@ parameters: Generator_p_set: description: a given output schedule; a generator without one has no row here dims: [scenario, snapshot, generator] + missing: neutral Generator_p_nom_min: description: least nominal power an extendable generator may be built at dims: [scenario, generator] @@ -117,6 +119,7 @@ parameters: Generator_p_nom_set: description: a given nominal power for an extendable generator; one without a value has no row here dims: [generator] + missing: neutral Generator_e_sum_min: description: least energy over the horizon; minus infinity where no floor is meant dims: [scenario, generator] @@ -129,15 +132,18 @@ parameters: `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row dims: [scenario, global_constraint, snapshot, generator] + missing: neutral Generator_operational_limit_weight: description: one where the generator is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, generator] + missing: neutral Generator_tech_capacity_weight: description: >- one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, generator] + missing: neutral variables: Generator_p: @@ -320,22 +326,22 @@ assumptions: | $`\mathrm{c}`$ | `Generator_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one unit of output | | $`\mathrm{c}^{(2)}`$ | `Generator_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of the square of one unit of output | | $`\mathrm{sgn}`$ | `Generator_sign` over $`\mathcal{G}`$ — the sign output enters its bus's balance with — PyPSA's `sign`, `1` unless given, `-1` for a unit that draws power. PyPSA refuses one that differs by scenario (`constants.py:43`) | -| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{p}^{\mathrm{mod}}`$ | `Generator_p_nom_mod` over $`\mathcal{G}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{\mathrm{fix}}`$ | `Generator_modules_installed` over $`\Xi \times \mathcal{G}`$ — how many whole modules a committable build has in place: `Generator_p_nom / Generator_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{nonneg}`$ | `Generator_p_min_pu_nonneg` over $`\mathcal{G}`$ — true where none of the generator's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{on}`$ | `Generator_active` over $`\mathcal{T} \times \mathcal{G}`$ — whether a generator stands in a snapshot's period — PyPSA's `active`, from build year and lifetime, data prep | | $`\mathrm{W}`$ | `Generator_capital_weight` over $`\mathcal{G}`$ — the sum of period weights a generator stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{new}`$ | `Generator_first_active` over $`\mathcal{Y} \times \mathcal{G}`$ — one in the first period a generator stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | -| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — a given output schedule; a generator without one has no row here | +| $`\mathrm{p}^{\mathrm{set}}`$ | `Generator_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — a given output schedule; a generator without one has no row here | | $`\underline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_min` over $`\Xi \times \mathcal{G}`$ — least nominal power an extendable generator may be built at | | $`\overline{\mathrm{p}}^{\mathrm{nom}}`$ | `Generator_p_nom_max` over $`\Xi \times \mathcal{G}`$ — most nominal power an extendable generator may be built at | | $`\mathrm{c}^{\mathrm{cap}}`$ | `Generator_capital_cost` over $`\Xi \times \mathcal{G}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$ — a given nominal power for an extendable generator; one without a value has no row here | +| $`\mathrm{p}^{\mathrm{nom,set}}`$ | `Generator_p_nom_set` over $`\mathcal{G}`$, `neutral` where the data has no row — a given nominal power for an extendable generator; one without a value has no row here | | $`\underline{\mathrm{E}}`$ | `Generator_e_sum_min` over $`\Xi \times \mathcal{G}`$ — least energy over the horizon; minus infinity where no floor is meant | | $`\overline{\mathrm{E}}`$ | `Generator_e_sum_max` over $`\Xi \times \mathcal{G}`$ — most energy over the horizon — a fuel or emission budget in energy terms; infinity where no cap is meant | -| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{G}`$ — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row | -| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{L} \times \mathcal{G}`$ — one where the generator is in the row's set — data prep; one outside it has no row | -| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{L} \times \mathcal{G}`$ — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{a}`$ | `Generator_primary_energy_weight` over $`\Xi \times \mathcal{L} \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row | +| $`\mathrm{b}`$ | `Generator_operational_limit_weight` over $`\Xi \times \mathcal{L} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's set — data prep; one outside it has no row | +| $`\mathrm{m}`$ | `Generator_tech_capacity_weight` over $`\mathcal{L} \times \mathcal{G}`$, `neutral` where the data has no row — one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables diff --git a/docs/examples/pypsa/generator_commitment.md b/docs/examples/pypsa/generator_commitment.md index bc6be516e..2ced3a7a1 100644 --- a/docs/examples/pypsa/generator_commitment.md +++ b/docs/examples/pypsa/generator_commitment.md @@ -80,6 +80,7 @@ parameters: (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep dims: [scenario, generator] + missing: neutral variables: Generator_status: @@ -325,7 +326,7 @@ constraints: | $`\mathrm{c}^{\mathrm{up}}`$ | `Generator_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{\mathrm{dn}}`$ | `Generator_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{\mathrm{on}}`$ | `Generator_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — cost of one snapshot spent on | -| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$ — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep | +| $`\mathrm{M}`$ | `Generator_big_m` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the bound a committed extendable generator's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep | #### Variables diff --git a/docs/examples/pypsa/generator_maintenance.md b/docs/examples/pypsa/generator_maintenance.md index 7e2937961..e9dd6d6eb 100644 --- a/docs/examples/pypsa/generator_maintenance.md +++ b/docs/examples/pypsa/generator_maintenance.md @@ -51,6 +51,7 @@ parameters: `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, generator] + missing: neutral Generator_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -67,7 +68,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -75,7 +76,7 @@ variables: description: "`Generator-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral domain: binary Generator_maintenance_capacity: description: >- @@ -83,7 +84,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, generator] where: Generator_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Generator_maintenance_status: @@ -96,7 +97,7 @@ variables: Generator_maintainable AND Generator_committed AND NOT (Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 @@ -260,17 +261,17 @@ assumptions: | $`\mathrm{mnt}`$ | `Generator_maintainable` over $`\mathcal{G}`$ — whether a generator must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma`$ | `Generator_maintenance_pu` over $`\Xi \times \mathcal{G}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{\mathrm{mnt}}`$ | `Generator_maintenance_events` over $`\Xi \times \mathcal{G}`$ — how many maintenance events the horizon holds | -| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{\mathrm{mnt}}`$ | `Generator_maintenance_duration` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the generator is not maintainable. No row reads it: data prep turns it into `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}`$ | `Generator_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the generator does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | #### Variables | Symbol | Meaning | |---|---| -| $`\mu`$ | `Generator_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{\mathrm{nom}}`$ | `Generator_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu`$ | `Generator_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance` — whether a maintainable generator is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{\mathrm{up}}`$ | `Generator_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{\mathrm{nom}}`$ | `Generator_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{u}`$ | `Generator_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the mask leaves it out — `Generator-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | #### Given diff --git a/docs/examples/pypsa/generator_ramping.md b/docs/examples/pypsa/generator_ramping.md index df8d27118..16556a673 100644 --- a/docs/examples/pypsa/generator_ramping.md +++ b/docs/examples/pypsa/generator_ramping.md @@ -33,21 +33,26 @@ parameters: Generator_ramp_limit_up: description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_down: description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_start_up: - description: most output in the snapshot a unit starts, per unit of nominal power + description: most output in the snapshot a unit starts, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_ramp_limit_shut_down: - description: most output in the snapshot before a unit stops, per unit of nominal power + description: most output in the snapshot before a unit stops, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_p_init: description: >- the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot dims: [scenario, generator] + missing: neutral given: parameters: @@ -269,11 +274,11 @@ assumptions: | Symbol | Meaning | |---|---| -| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$ — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot a unit starts, per unit of nominal power | -| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$ — most output in the snapshot before a unit stops, per unit of nominal power | -| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\Xi \times \mathcal{G}`$ — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | +| $`\mathrm{ru}`$ | `Generator_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}`$ | `Generator_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{G}`$, `neutral` where the data has no row — most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{ru}^{\mathrm{up}}`$ | `Generator_ramp_limit_start_up` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — most output in the snapshot a unit starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{\mathrm{dn}}`$ | `Generator_ramp_limit_shut_down` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — most output in the snapshot before a unit stops, per unit of nominal power; no value means no limit | +| $`\mathrm{p}^{0}`$ | `Generator_p_init` over $`\Xi \times \mathcal{G}`$, `neutral` where the data has no row — the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot | #### Given diff --git a/docs/examples/pypsa/line.md b/docs/examples/pypsa/line.md index da780d87f..2c0c043a2 100644 --- a/docs/examples/pypsa/line.md +++ b/docs/examples/pypsa/line.md @@ -82,6 +82,7 @@ parameters: Line_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [line] + missing: neutral Line_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, line] @@ -102,9 +103,11 @@ parameters: Line_s_nom_set: description: a given nominal apparent power for an extendable line; one without a value has no row here dims: [line] + missing: neutral Line_s_set: description: a given flow schedule; a line without one has no row here dims: [scenario, snapshot, line] + missing: neutral Line_v_ang_max: description: >- the most the voltage angle difference across a line may be either way, @@ -113,6 +116,7 @@ parameters: ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) dims: [scenario, line] + missing: neutral Line_x_pu_eff: description: >- the line's effective series reactance — PyPSA's `x_pu_eff`, `x` over @@ -124,6 +128,7 @@ parameters: the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) dims: [period, line, cycle] + missing: neutral Line_loss_max: description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep dims: [scenario, snapshot, line] @@ -145,6 +150,7 @@ parameters: line outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, line] + missing: neutral Line_expansion_cost_weight: description: >- the line's capital cost where its carrier is in the row's set, times @@ -155,12 +161,14 @@ parameters: `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, line] + missing: neutral Line_tech_capacity_weight: description: >- one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, line] + missing: neutral variables: Line_s: @@ -176,7 +184,7 @@ variables: the network is lossless dims: [scenario, snapshot, line] where: Line_lossy - absence: zero + missing: neutral bounds: lower: 0 Line_s_nom_ext: @@ -357,29 +365,29 @@ constraints: | $`\mathrm{new}^{s}`$ | `Line_first_active` over $`\mathcal{Y} \times \mathcal{K}`$ — one in the first period a line stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | | $`\mathrm{s}^{\mathrm{nom}}`$ | `Line_s_nom` over $`\Xi \times \mathcal{K}`$ — nominal apparent power | | $`\mathrm{ext}^{s}`$ | `Line_s_nom_extendable` over $`\mathcal{K}`$ — whether the nominal apparent power is a decision | -| $`\mathrm{s}^{\mathrm{mod}}`$ | `Line_s_nom_mod` over $`\mathcal{K}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{s}^{\mathrm{mod}}`$ | `Line_s_nom_mod` over $`\mathcal{K}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\overline{\mathrm{s}}`$ | `Line_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — most flow either way, per unit of nominal apparent power | | $`\underline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_min` over $`\Xi \times \mathcal{K}`$ — least nominal apparent power an extendable line may be built at | | $`\overline{\mathrm{s}}^{\mathrm{nom}}`$ | `Line_s_nom_max` over $`\Xi \times \mathcal{K}`$ — most nominal apparent power an extendable line may be built at | | $`\mathrm{c}^{\mathrm{cap},s}`$ | `Line_capital_cost` over $`\Xi \times \mathcal{K}`$ — cost of one unit of nominal apparent power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$ — a given nominal apparent power for an extendable line; one without a value has no row here | -| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — a given flow schedule; a line without one has no row here | -| $`\overline{\delta}`$ | `Line_v_ang_max` over $`\Xi \times \mathcal{K}`$ — the most the voltage angle difference across a line may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. A line whose carrier is not AC has no row either. The deprecated `v_ang_min` is ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) | +| $`\mathrm{s}^{\mathrm{nom,set}}`$ | `Line_s_nom_set` over $`\mathcal{K}`$, `neutral` where the data has no row — a given nominal apparent power for an extendable line; one without a value has no row here | +| $`\mathrm{s}^{\mathrm{set}}`$ | `Line_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$, `neutral` where the data has no row — a given flow schedule; a line without one has no row here | +| $`\overline{\delta}`$ | `Line_v_ang_max` over $`\Xi \times \mathcal{K}`$, `neutral` where the data has no row — the most the voltage angle difference across a line may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. A line whose carrier is not AC has no row either. The deprecated `v_ang_min` is ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) | | $`\mathrm{x}^{\mathrm{eff}}`$ | `Line_x_pu_eff` over $`\Xi \times \mathcal{K}`$ — the line's effective series reactance — PyPSA's `x_pu_eff`, `x` over the square of its bus's nominal voltage, data prep | -| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{C}`$ — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) | +| $`\mathrm{x}`$ | `Line_cycle_weight` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{C}`$, `neutral` where the data has no row — the line's series impedance, signed by its orientation in the cycle — the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) | | $`\overline{\ell}`$ | `Line_loss_max` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep | | $`\mathrm{a}`$ | `Line_loss_slope` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{E}`$ — the slope of a cut to the loss curve — a tangent's `2 * r_pu_eff * p_k` at its segment's flow, a secant's `r_pu_eff * (p_k + p_k+1)` between consecutive breakpoints, data prep | | $`\mathrm{b}`$ | `Line_loss_offset` over $`\Xi \times \mathcal{T} \times \mathcal{K} \times \mathcal{E}`$ — where that cut meets the loss axis — a tangent's `loss_k - slope_k * p_k`, a secant's `-r_pu_eff * p_k * p_k+1`, negative, data prep | -| $`\mathrm{len}`$ | `Line_volume_weight` over $`\Xi \times \mathcal{G} \times \mathcal{K}`$ — the line's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\Xi \times \mathcal{G} \times \mathcal{K}`$ — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | -| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{K}`$ — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{len}`$ | `Line_volume_weight` over $`\Xi \times \mathcal{G} \times \mathcal{K}`$, `neutral` where the data has no row — the line's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a line outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{cc}`$ | `Line_expansion_cost_weight` over $`\Xi \times \mathcal{G} \times \mathcal{K}`$, `neutral` where the data has no row — the line's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a line outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | +| $`\mathrm{m}^{l}`$ | `Line_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{K}`$, `neutral` where the data has no row — one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables | Symbol | Meaning | |---|---| | $`s`$ | `Line_s` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-s` — PyPSA's `p0`, the flow measured at the `Line_bus0` end: a positive value withdraws there and injects at `Line_bus1`, lossless | -| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$ — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | +| $`\ell`$ | `Line_loss` over $`\Xi \times \mathcal{T} \times \mathcal{K}`$, `neutral` where the mask leaves it out — `Line-loss` — what a line dissipates carrying its flow, pushed down by the cost and held up by the cuts; absent, and zero in the balance, where the network is lossless | | $`S`$ | `Line_s_nom_ext` over $`\mathcal{K}`$ — `Line-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`N^{s}`$ | `Line_n_mod` over $`\mathcal{K}`$ — `Line-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | diff --git a/docs/examples/pypsa/link.md b/docs/examples/pypsa/link.md index 00fb87ccd..5e83cd127 100644 --- a/docs/examples/pypsa/link.md +++ b/docs/examples/pypsa/link.md @@ -106,6 +106,7 @@ parameters: Link_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [link] + missing: neutral Link_modules_installed: description: >- how many whole modules a committable build has in place: `Link_p_nom @@ -134,6 +135,7 @@ parameters: Link_p_set: description: a given flow schedule; a link without one has no row here dims: [scenario, snapshot, link] + missing: neutral Link_p_nom_min: description: least nominal power an extendable link may be built at dims: [scenario, link] @@ -151,6 +153,7 @@ parameters: Link_p_nom_set: description: a given nominal power for an extendable link; one without a value has no row here dims: [link] + missing: neutral Link_volume_weight: description: >- the link's length in each scenario where its carrier is in the row's @@ -158,6 +161,7 @@ parameters: link outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, link] + missing: neutral Link_expansion_cost_weight: description: >- the link's capital cost where its carrier is in the row's set, times @@ -168,12 +172,14 @@ parameters: `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, link] + missing: neutral Link_tech_capacity_weight: description: >- one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, link] + missing: neutral variables: Link_p: @@ -365,20 +371,20 @@ assumptions: | $`\mathrm{cyc}^{f}`$ | `Link_output_cyclic_delay` over $`\mathcal{O}`$ — whether a delayed port's flow wraps from the end of its investment period — PyPSA's `cyclic_delay`, `cyclic_delay2`, …; where it does not, the flow still in transit at each period's first snapshots is lost. The same in every scenario, as the delay | | $`\mathrm{c}^{f}`$ | `Link_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one unit of flow | | $`\mathrm{c}^{f,(2)}`$ | `Link_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of the square of one unit of flow | -| $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{f}^{\mathrm{mod}}`$ | `Link_p_nom_mod` over $`\mathcal{L}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{f,\mathrm{fix}}`$ | `Link_modules_installed` over $`\Xi \times \mathcal{L}`$ — how many whole modules a committable build has in place: `Link_p_nom / Link_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{nonneg}^{f}`$ | `Link_p_min_pu_nonneg` over $`\mathcal{L}`$ — true where none of the link's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{on}^{f}`$ | `Link_active` over $`\mathcal{T} \times \mathcal{L}`$ — whether a link stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{W}^{f}`$ | `Link_capital_weight` over $`\mathcal{L}`$ — the sum of period weights a link stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{new}^{f}`$ | `Link_first_active` over $`\mathcal{Y} \times \mathcal{L}`$ — one in the first period a link stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | -| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — a given flow schedule; a link without one has no row here | +| $`\mathrm{f}^{\mathrm{set}}`$ | `Link_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — a given flow schedule; a link without one has no row here | | $`\underline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_min` over $`\Xi \times \mathcal{L}`$ — least nominal power an extendable link may be built at | | $`\overline{\mathrm{f}}^{\mathrm{nom}}`$ | `Link_p_nom_max` over $`\Xi \times \mathcal{L}`$ — most nominal power an extendable link may be built at | | $`\mathrm{c}^{\mathrm{cap},f}`$ | `Link_capital_cost` over $`\Xi \times \mathcal{L}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\mathcal{L}`$ — a given nominal power for an extendable link; one without a value has no row here | -| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\Xi \times \mathcal{G} \times \mathcal{L}`$ — the link's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | -| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\Xi \times \mathcal{G} \times \mathcal{L}`$ — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | -| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{L}`$ — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{f}^{\mathrm{nom,set}}`$ | `Link_p_nom_set` over $`\mathcal{L}`$, `neutral` where the data has no row — a given nominal power for an extendable link; one without a value has no row here | +| $`\mathrm{len}^{f}`$ | `Link_volume_weight` over $`\Xi \times \mathcal{G} \times \mathcal{L}`$, `neutral` where the data has no row — the link's length in each scenario where its carrier is in the row's set (`global_constraints.py:847-848`) — data prep; a link outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{cc}^{f}`$ | `Link_expansion_cost_weight` over $`\Xi \times \mathcal{G} \times \mathcal{L}`$, `neutral` where the data has no row — the link's capital cost where its carrier is in the row's set, times the objective weights of the periods it stands in where the row names no `investment_period` under `multi_investment_periods` — data prep; a link outside the set, or one that does not stand in the row's period, has no row. The capital cost is PyPSA's `capital_cost` property, which is `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) | +| $`\mathrm{m}^{f}`$ | `Link_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{L}`$, `neutral` where the data has no row — one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables diff --git a/docs/examples/pypsa/link_commitment.md b/docs/examples/pypsa/link_commitment.md index 321b2d9f1..07336f8f9 100644 --- a/docs/examples/pypsa/link_commitment.md +++ b/docs/examples/pypsa/link_commitment.md @@ -80,6 +80,7 @@ parameters: (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep dims: [scenario, link] + missing: neutral variables: Link_status: @@ -325,7 +326,7 @@ constraints: | $`\mathrm{c}^{f,\mathrm{up}}`$ | `Link_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{f,\mathrm{dn}}`$ | `Link_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{f,\mathrm{on}}`$ | `Link_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — cost of one snapshot spent on | -| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\Xi \times \mathcal{L}`$ — the bound a committed extendable link's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep | +| $`\mathrm{M}^{f}`$ | `Link_big_m` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the bound a committed extendable link's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep | #### Variables diff --git a/docs/examples/pypsa/link_maintenance.md b/docs/examples/pypsa/link_maintenance.md index c057d6987..e5fd496a0 100644 --- a/docs/examples/pypsa/link_maintenance.md +++ b/docs/examples/pypsa/link_maintenance.md @@ -51,6 +51,7 @@ parameters: `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, link] + missing: neutral Link_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -67,7 +68,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -75,7 +76,7 @@ variables: description: "`Link-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral domain: binary Link_maintenance_capacity: description: >- @@ -83,7 +84,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, link] where: Link_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Link_maintenance_status: @@ -93,7 +94,7 @@ variables: maintenance may also be off dims: [scenario, snapshot, link] where: Link_maintainable AND Link_committed AND NOT (Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 @@ -257,17 +258,17 @@ assumptions: | $`\mathrm{mnt}^{f}`$ | `Link_maintainable` over $`\mathcal{L}`$ — whether a link must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma^{f}`$ | `Link_maintenance_pu` over $`\Xi \times \mathcal{L}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{f,\mathrm{mnt}}`$ | `Link_maintenance_events` over $`\Xi \times \mathcal{L}`$ — how many maintenance events the horizon holds | -| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\Xi \times \mathcal{L}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{f,\mathrm{mnt}}`$ | `Link_maintenance_duration` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the link is not maintainable. No row reads it: data prep turns it into `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{f}`$ | `Link_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the link does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | #### Variables | Symbol | Meaning | |---|---| -| $`\mu^{f}`$ | `Link_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance` — whether a maintainable link is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{f,\mathrm{up}}`$ | `Link_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{f,\mathrm{nom}}`$ | `Link_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{f,u}`$ | `Link_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — `Link-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu^{f}`$ | `Link_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance` — whether a maintainable link is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{f,\mathrm{up}}`$ | `Link_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{f,\mathrm{nom}}`$ | `Link_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{f,u}`$ | `Link_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the mask leaves it out — `Link-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | #### Given diff --git a/docs/examples/pypsa/link_ramping.md b/docs/examples/pypsa/link_ramping.md index 41bc9db68..70b96c3b0 100644 --- a/docs/examples/pypsa/link_ramping.md +++ b/docs/examples/pypsa/link_ramping.md @@ -33,21 +33,26 @@ parameters: Link_ramp_limit_up: description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_ramp_limit_down: description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_ramp_limit_start_up: - description: most flow in the snapshot a link starts, per unit of nominal power + description: most flow in the snapshot a link starts, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_ramp_limit_shut_down: - description: most flow in the snapshot before a link stops, per unit of nominal power + description: most flow in the snapshot before a link stops, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_p_init: description: >- the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot dims: [scenario, link] + missing: neutral given: parameters: @@ -257,11 +262,11 @@ assumptions: | Symbol | Meaning | |---|---| -| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$ — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot a link starts, per unit of nominal power | -| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\Xi \times \mathcal{L}`$ — most flow in the snapshot before a link stops, per unit of nominal power | -| $`\mathrm{f}^{0}`$ | `Link_p_init` over $`\Xi \times \mathcal{L}`$ — the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot | +| $`\mathrm{ru}^{f}`$ | `Link_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{f}`$ | `Link_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{L}`$, `neutral` where the data has no row — most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{ru}^{f,\mathrm{up}}`$ | `Link_ramp_limit_start_up` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — most flow in the snapshot a link starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{f,\mathrm{dn}}`$ | `Link_ramp_limit_shut_down` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — most flow in the snapshot before a link stops, per unit of nominal power; no value means no limit | +| $`\mathrm{f}^{0}`$ | `Link_p_init` over $`\Xi \times \mathcal{L}`$, `neutral` where the data has no row — the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot | #### Given diff --git a/docs/examples/pypsa/process.md b/docs/examples/pypsa/process.md index bb17a7037..49b362565 100644 --- a/docs/examples/pypsa/process.md +++ b/docs/examples/pypsa/process.md @@ -103,6 +103,7 @@ parameters: Process_p_set: description: a given internal power schedule; a process without one has no row here dims: [scenario, snapshot, process] + missing: neutral Process_p_nom_min: description: least nominal power an extendable process may be built at dims: [scenario, process] @@ -120,9 +121,11 @@ parameters: Process_p_nom_set: description: a given nominal power for an extendable process; one without a value has no row here dims: [process] + missing: neutral Process_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [process] + missing: neutral Process_modules_installed: description: >- how many whole modules a committable build has in place: `Process_p_nom @@ -154,6 +157,7 @@ parameters: outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, process] + missing: neutral variables: Process_p: @@ -336,18 +340,18 @@ assumptions: | $`\mathrm{cyc}^{z}`$ | `Process_output_cyclic_delay` over $`\mathcal{R}`$ — whether a delayed port's transfer wraps from the end of its investment period — PyPSA's `cyclic_delay0`, `cyclic_delay1`, …; where it does not, the energy still in transit at each period's first snapshots is lost. The same in every scenario, as the delay | | $`\mathrm{c}^{z}`$ | `Process_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one unit of internal power | | $`\mathrm{c}^{z,(2)}`$ | `Process_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of the square of one unit of internal power | -| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — a given internal power schedule; a process without one has no row here | +| $`\mathrm{z}^{\mathrm{set}}`$ | `Process_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — a given internal power schedule; a process without one has no row here | | $`\underline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_min` over $`\Xi \times \mathcal{J}`$ — least nominal power an extendable process may be built at | | $`\overline{\mathrm{z}}^{\mathrm{nom}}`$ | `Process_p_nom_max` over $`\Xi \times \mathcal{J}`$ — most nominal power an extendable process may be built at | | $`\mathrm{c}^{\mathrm{cap},z}`$ | `Process_capital_cost` over $`\Xi \times \mathcal{J}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$ — a given nominal power for an extendable process; one without a value has no row here | -| $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{z}^{\mathrm{nom,set}}`$ | `Process_p_nom_set` over $`\mathcal{J}`$, `neutral` where the data has no row — a given nominal power for an extendable process; one without a value has no row here | +| $`\mathrm{z}^{\mathrm{mod}}`$ | `Process_p_nom_mod` over $`\mathcal{J}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\mathrm{N}^{z,\mathrm{fix}}`$ | `Process_modules_installed` over $`\Xi \times \mathcal{J}`$ — how many whole modules a committable build has in place: `Process_p_nom / Process_p_nom_mod` where a fixed build is modular, one where it is not, data prep. PyPSA refuses a fixed modular build whose nominal power is not a whole number of modules | | $`\mathrm{nonneg}^{z}`$ | `Process_p_min_pu_nonneg` over $`\mathcal{J}`$ — true where none of the process's own minimums-per-unit is negative — PyPSA's per-unit `(p_min_pu >= 0).all()` over every snapshot and scenario, data prep | | $`\mathrm{on}^{z}`$ | `Process_active` over $`\mathcal{T} \times \mathcal{J}`$ — whether a process stands in a snapshot's period — PyPSA's `active`, data prep | | $`\mathrm{W}^{z}`$ | `Process_capital_weight` over $`\mathcal{J}`$ — the sum of period weights a process stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\mathrm{new}^{z}`$ | `Process_first_active` over $`\mathcal{Y} \times \mathcal{J}`$ — one in the first period a process stands in, zero elsewhere, data prep. PyPSA takes `active & (active.cumsum() == 1)` (`global_constraints.py:265`) | -| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{J}`$ — one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{m}^{z}`$ | `Process_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{J}`$, `neutral` where the data has no row — one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables diff --git a/docs/examples/pypsa/process_commitment.md b/docs/examples/pypsa/process_commitment.md index 97d8f13fa..5decfd5f4 100644 --- a/docs/examples/pypsa/process_commitment.md +++ b/docs/examples/pypsa/process_commitment.md @@ -80,6 +80,7 @@ parameters: (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep dims: [scenario, process] + missing: neutral variables: Process_status: @@ -325,7 +326,7 @@ constraints: | $`\mathrm{c}^{z,\mathrm{up}}`$ | `Process_start_up_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one start in this snapshot | | $`\mathrm{c}^{z,\mathrm{dn}}`$ | `Process_shut_down_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one stop in this snapshot | | $`\mathrm{c}^{z,\mathrm{on}}`$ | `Process_stand_by_cost` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — cost of one snapshot spent on | -| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\Xi \times \mathcal{J}`$ — the bound a committed extendable process's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep | +| $`\mathrm{M}^{z}`$ | `Process_big_m` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the bound a committed extendable process's big-M rows release it by — the build cap `p_nom_max` times the highest `p_max_pu`, where the cap is finite and positive. Elsewhere it is `committable_big_m` times the highest `p_max_pu`, and where that keyword is not given, ten times the largest of the peak total load and the component's largest finite `p_nom` and `p_nom_max`, or 1e6 where there is none of them (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep | #### Variables diff --git a/docs/examples/pypsa/process_maintenance.md b/docs/examples/pypsa/process_maintenance.md index ea2b545a7..16750539f 100644 --- a/docs/examples/pypsa/process_maintenance.md +++ b/docs/examples/pypsa/process_maintenance.md @@ -51,6 +51,7 @@ parameters: `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, process] + missing: neutral Process_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -67,7 +68,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -75,7 +76,7 @@ variables: description: "`Process-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral domain: binary Process_maintenance_capacity: description: >- @@ -83,7 +84,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, process] where: Process_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Process_maintenance_status: @@ -96,7 +97,7 @@ variables: Process_maintainable AND Process_committed AND NOT (Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 @@ -260,17 +261,17 @@ assumptions: | $`\mathrm{mnt}^{z}`$ | `Process_maintainable` over $`\mathcal{J}`$ — whether a process must be taken off for maintenance within the horizon — in any scenario, as PyPSA takes the union over them (`components.py:1016-1019`) | | $`\gamma^{z}`$ | `Process_maintenance_pu` over $`\Xi \times \mathcal{J}`$ — the share of the build a maintenance event takes off | | $`\mathrm{n}^{z,\mathrm{mnt}}`$ | `Process_maintenance_events` over $`\Xi \times \mathcal{J}`$ — how many maintenance events the horizon holds | -| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$ — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | +| $`\tau^{z,\mathrm{mnt}}`$ | `Process_maintenance_duration` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the hours of generator weightings one maintenance event covers — PyPSA's `maintenance_duration`; no value where the process is not maintainable. No row reads it: data prep turns it into `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon | | $`\mathrm{blk}^{z}`$ | `Process_maintenance_start_blocked` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — true where no maintenance event may start, because the snapshots it would cover run past the end of the horizon or into one the process does not stand in — PyPSA's `active & ~valid`, from `maintenance_duration` and the generator weightings, data prep | #### Variables | Symbol | Meaning | |---|---| -| $`\mu^{z}`$ | `Process_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance` — whether a maintainable process is in maintenance: continuous, and one exactly where an event covers the snapshot | -| $`\mu^{z,\mathrm{up}}`$ | `Process_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_start` — whether a maintenance event starts in this snapshot | -| $`\mu^{z,\mathrm{nom}}`$ | `Process_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | -| $`\mu^{z,u}`$ | `Process_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — `Process-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | +| $`\mu^{z}`$ | `Process_maintenance` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance` — whether a maintainable process is in maintenance: continuous, and one exactly where an event covers the snapshot | +| $`\mu^{z,\mathrm{up}}`$ | `Process_maintenance_start` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_start` — whether a maintenance event starts in this snapshot | +| $`\mu^{z,\mathrm{nom}}`$ | `Process_maintenance_capacity` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_capacity` — the chosen build while in maintenance, zero otherwise: the product the `maintcap` rows linearize | +| $`\mu^{z,u}`$ | `Process_maintenance_status` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the mask leaves it out — `Process-maintenance_status` — the status while in maintenance, zero otherwise: the product the `maint-status` rows linearize, so a unit in maintenance may also be off | #### Given diff --git a/docs/examples/pypsa/process_ramping.md b/docs/examples/pypsa/process_ramping.md index eaeb5ea59..095f2f6bc 100644 --- a/docs/examples/pypsa/process_ramping.md +++ b/docs/examples/pypsa/process_ramping.md @@ -33,21 +33,26 @@ parameters: Process_ramp_limit_up: description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_ramp_limit_down: description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_ramp_limit_start_up: - description: most internal power in the snapshot a process starts, per unit of nominal power + description: most internal power in the snapshot a process starts, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_ramp_limit_shut_down: - description: most internal power in the snapshot before a process stops, per unit of nominal power + description: most internal power in the snapshot before a process stops, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_p_init: description: >- the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot dims: [scenario, process] + missing: neutral given: parameters: @@ -263,11 +268,11 @@ assumptions: | Symbol | Meaning | |---|---| -| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$ — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | -| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot a process starts, per unit of nominal power | -| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\Xi \times \mathcal{J}`$ — most internal power in the snapshot before a process stops, per unit of nominal power | -| $`\mathrm{z}^{0}`$ | `Process_p_init` over $`\Xi \times \mathcal{J}`$ — the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot | +| $`\mathrm{ru}^{z}`$ | `Process_ramp_limit_up` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{rd}^{z}`$ | `Process_ramp_limit_down` over $`\Xi \times \mathcal{T} \times \mathcal{J}`$, `neutral` where the data has no row — most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time | +| $`\mathrm{ru}^{z,\mathrm{up}}`$ | `Process_ramp_limit_start_up` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — most internal power in the snapshot a process starts, per unit of nominal power; no value means no limit | +| $`\mathrm{rd}^{z,\mathrm{dn}}`$ | `Process_ramp_limit_shut_down` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — most internal power in the snapshot before a process stops, per unit of nominal power; no value means no limit | +| $`\mathrm{z}^{0}`$ | `Process_p_init` over $`\Xi \times \mathcal{J}`$, `neutral` where the data has no row — the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot | #### Given diff --git a/docs/examples/pypsa/security.md b/docs/examples/pypsa/security.md index e63786a4f..f35eb0591 100644 --- a/docs/examples/pypsa/security.md +++ b/docs/examples/pypsa/security.md @@ -39,10 +39,12 @@ relations: 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 parameters: Line_BODF: @@ -54,12 +56,14 @@ parameters: the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) dims: [period, line, outage] + missing: neutral Transformer_BODF: description: >- the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network dims: [period, transformer, outage] + missing: neutral given: parameters: @@ -189,17 +193,17 @@ constraints: |---|---| | $`\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}`$ — 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}`$ — 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}`$ — 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}`$ | 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` | #### Parameters | Symbol | Meaning | |---|---| -| $`\beta`$ | `Line_BODF` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the PTDF of the sub-network the period's active branches form, data prep; a row only where the line and the outage are active in the period and share a sub-network, -1 at the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) | -| $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$ — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network | +| $`\beta`$ | `Line_BODF` over $`\mathcal{Y} \times \mathcal{K} \times \mathcal{K}^{\mathrm{out}}`$, `neutral` where the data has no row — the share of an outaged branch's flow a line takes on when that branch goes out — PyPSA's `BODF`, from the PTDF of the sub-network the period's active branches form, data prep; a row only where the line and the outage are active in the period and share a sub-network, -1 at the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) | +| $`\beta^{\sigma}`$ | `Transformer_BODF` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{K}^{\mathrm{out}}`$, `neutral` where the data has no row — the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network | #### Given diff --git a/docs/examples/pypsa/settings.md b/docs/examples/pypsa/settings.md index cc2f7e6cd..f2d61ba6b 100644 --- a/docs/examples/pypsa/settings.md +++ b/docs/examples/pypsa/settings.md @@ -41,6 +41,7 @@ parameters: CVaR_omega: description: PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one dims: [] + missing: neutral period_weight_objective: description: >- PyPSA's `investment_period_weightings.objective` — what a period's cost @@ -81,6 +82,7 @@ parameters: on a row that names a period without it (`global_constraints.py:364-369`) dims: [scenario, global_constraint, snapshot] dtype: bool + missing: neutral given: expressions: @@ -168,13 +170,13 @@ objective: |---|---| | $`\mathrm{w}`$ | `snapshot_weightings_objective` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.objective` — hours a snapshot stands for in the cost | | $`\pi`$ | `scenario_weight` over $`\Xi`$ — PyPSA's `scenario_weightings.weight` — the probability of a future | -| $`\omega`$ | `CVaR_omega` (scalar) — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one | +| $`\omega`$ | `CVaR_omega` (scalar), `neutral` where the data has no row — PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one | | $`\mathrm{w}^{y}`$ | `period_weight_objective` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.objective` — what a period's cost weighs; PyPSA reads it only under `multi_investment_periods`, so data prep feeds one otherwise, whatever the column holds (`optimize.py:205-207`, `:264-266`) | | $`\mathrm{w}^{\mathrm{yr}}`$ | `period_weight_years` over $`\mathcal{Y}`$ — PyPSA's `investment_period_weightings.years` — what a period's energy weighs in a `primary_energy` or `operational_limit` row; PyPSA reads it only under `multi_investment_periods`, so data prep feeds one otherwise | | $`\mathrm{w}^{\mathrm{sto}}`$ | `snapshot_weightings_stores` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance | | $`\mathrm{w}^{\mathrm{gen}}`$ | `snapshot_weightings_generators` over $`\mathcal{T}`$ — PyPSA's `snapshot_weightings.generators` — hours a snapshot stands for in an energy total | | $`\mathrm{lossy}`$ | `transmission_losses` (scalar) — whether the network dissipates transmission losses — PyPSA's `transmission_losses` read as a flag; its mode, tangents or secants, only decides how data prep fills the `segment` axis, the rows are the same; false with no segments is a lossless run. A security-constrained run over a network with passive branches builds no loss: PyPSA does not hand the keyword to `create_model` (`abstract.py:528-532`) but to the solver (`:548`), so data prep feeds false there | -| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\Xi \times \mathcal{G} \times \mathcal{T}`$ — whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:372`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:364-369`) | +| $`\mathrm{in}`$ | `GlobalConstraint_counts_snapshot` over $`\Xi \times \mathcal{G} \times \mathcal{T}`$, `neutral` where the data has no row — whether a row counts a snapshot in a scenario — PyPSA's `investment_period`: every snapshot where the row names none, and only that period's where it names one, data prep. A row that names a period the run does not model has no label here, as PyPSA skips it (`global_constraints.py:372`); PyPSA reads the column only under `multi_investment_periods`, and fails on a row that names a period without it (`global_constraints.py:364-369`) | #### Given diff --git a/docs/examples/pypsa/storage_unit.md b/docs/examples/pypsa/storage_unit.md index 7105faa69..8c80f5337 100644 --- a/docs/examples/pypsa/storage_unit.md +++ b/docs/examples/pypsa/storage_unit.md @@ -73,6 +73,7 @@ parameters: StorageUnit_p_nom_set: description: a given nominal power for an extendable storage unit; one without a value has no row here dims: [storage_unit] + missing: neutral StorageUnit_p_nom: description: nominal power dims: [scenario, storage_unit] @@ -83,6 +84,7 @@ parameters: StorageUnit_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [storage_unit] + missing: neutral StorageUnit_p_min_pu: description: most storing, per unit of nominal power and negated dims: [scenario, snapshot, storage_unit] @@ -162,27 +164,34 @@ parameters: StorageUnit_p_set: description: a given net dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_dispatch_set: description: a given dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_store_set: description: a given charging schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_state_of_charge_set: description: a given charge schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_primary_energy_weight: description: the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral StorageUnit_operational_limit_weight: description: one where the storage unit is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral StorageUnit_tech_capacity_weight: description: >- one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, storage_unit] + missing: neutral variables: StorageUnit_p_dispatch: @@ -204,7 +213,7 @@ variables: the variable rather than as rows dims: [scenario, snapshot, storage_unit] where: "StorageUnit_inflow > 0 AND StorageUnit_active" - absence: zero + missing: neutral bounds: lower: 0 upper: StorageUnit_inflow @@ -541,10 +550,10 @@ assumptions: | $`\underline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_min` over $`\Xi \times \mathcal{S}`$ — least nominal power an extendable storage unit may be built at | | $`\overline{\mathrm{h}}^{\mathrm{nom}}`$ | `StorageUnit_p_nom_max` over $`\Xi \times \mathcal{S}`$ — most nominal power an extendable storage unit may be built at | | $`\mathrm{c}^{\mathrm{cap},h}`$ | `StorageUnit_capital_cost` over $`\Xi \times \mathcal{S}`$ — cost of one unit of nominal power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\mathcal{S}`$ — a given nominal power for an extendable storage unit; one without a value has no row here | +| $`\mathrm{h}^{\mathrm{nom,set}}`$ | `StorageUnit_p_nom_set` over $`\mathcal{S}`$, `neutral` where the data has no row — a given nominal power for an extendable storage unit; one without a value has no row here | | $`\mathrm{h}^{\mathrm{nom}}`$ | `StorageUnit_p_nom` over $`\Xi \times \mathcal{S}`$ — nominal power | | $`\mathrm{ext}^{h}`$ | `StorageUnit_p_nom_extendable` over $`\mathcal{S}`$ — whether the nominal power is a decision | -| $`\mathrm{h}^{\mathrm{mod}}`$ | `StorageUnit_p_nom_mod` over $`\mathcal{S}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{h}^{\mathrm{mod}}`$ | `StorageUnit_p_nom_mod` over $`\mathcal{S}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\underline{\mathrm{h}}`$ | `StorageUnit_p_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most storing, per unit of nominal power and negated | | $`\overline{\mathrm{h}}`$ | `StorageUnit_p_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — most dispatch, per unit of nominal power | | $`\mathrm{T}^{h}`$ | `StorageUnit_max_hours` over $`\Xi \times \mathcal{S}`$ — energy capacity, as hours of dispatch at nominal power | @@ -563,13 +572,13 @@ assumptions: | $`\mathrm{c}^{h,(2)}`$ | `StorageUnit_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of the square of one unit of dispatch; storing is not charged | | $`\mathrm{c}^{\mathrm{soc}}`$ | `StorageUnit_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of charge held over one snapshot | | $`\mathrm{c}^{\mathrm{spill}}`$ | `StorageUnit_spill_cost` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — cost of one unit of inflow passed on unused | -| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given net dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given dispatch schedule; a unit without one has no row here | -| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charging schedule; a unit without one has no row here | -| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — a given charge schedule; a unit without one has no row here | -| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\Xi \times \mathcal{G} \times \mathcal{S}`$ — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | -| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\Xi \times \mathcal{G} \times \mathcal{S}`$ — one where the storage unit is in the row's set — data prep; one outside it has no row | -| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{S}`$ — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{h}^{\mathrm{set}}`$ | `StorageUnit_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given net dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{+,\mathrm{set}}`$ | `StorageUnit_p_dispatch_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given dispatch schedule; a unit without one has no row here | +| $`\mathrm{h}^{-,\mathrm{set}}`$ | `StorageUnit_p_store_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given charging schedule; a unit without one has no row here | +| $`\mathrm{soc}^{\mathrm{set}}`$ | `StorageUnit_state_of_charge_set` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the data has no row — a given charge schedule; a unit without one has no row here | +| $`\mathrm{a}^{h}`$ | `StorageUnit_primary_energy_weight` over $`\Xi \times \mathcal{G} \times \mathcal{S}`$, `neutral` where the data has no row — the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row | +| $`\mathrm{b}^{h}`$ | `StorageUnit_operational_limit_weight` over $`\Xi \times \mathcal{G} \times \mathcal{S}`$, `neutral` where the data has no row — one where the storage unit is in the row's set — data prep; one outside it has no row | +| $`\mathrm{m}^{h}`$ | `StorageUnit_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{S}`$, `neutral` where the data has no row — one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables @@ -578,7 +587,7 @@ assumptions: | $`h^{+}`$ | `StorageUnit_p_dispatch` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_dispatch` — power delivered to the bus | | $`h^{-}`$ | `StorageUnit_p_store` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-p_store` — power drawn from the bus into charge | | $`\mathit{soc}`$ | `StorageUnit_state_of_charge` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-state_of_charge` — energy held at the end of a snapshot | -| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$ — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | +| $`\mathit{spill}`$ | `StorageUnit_spill` over $`\Xi \times \mathcal{T} \times \mathcal{S}`$, `neutral` where the mask leaves it out — `StorageUnit-spill` — inflow passed on unused. Zero where there is no inflow, so the balance keeps its row there; the bounds are PyPSA's, on the variable rather than as rows | | $`H`$ | `StorageUnit_p_nom_ext` over $`\mathcal{S}`$ — `StorageUnit-p_nom` — nominal power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`N^{h}`$ | `StorageUnit_n_mod` over $`\mathcal{S}`$ — `StorageUnit-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | diff --git a/docs/examples/pypsa/store.md b/docs/examples/pypsa/store.md index 224aaa4ee..73ffdfb26 100644 --- a/docs/examples/pypsa/store.md +++ b/docs/examples/pypsa/store.md @@ -73,6 +73,7 @@ parameters: Store_e_nom_set: description: a given nominal capacity for an extendable store; one without a value has no row here dims: [store] + missing: neutral Store_e_nom: description: nominal energy capacity dims: [scenario, store] @@ -83,6 +84,7 @@ parameters: Store_e_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [store] + missing: neutral Store_e_min_pu: description: least energy held, per unit of nominal capacity — negative for a store that may go short dims: [scenario, snapshot, store] @@ -147,21 +149,26 @@ parameters: Store_e_set: description: a given energy schedule; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Store_p_set: description: a given schedule of power delivered; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Store_primary_energy_weight: description: the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row dims: [scenario, global_constraint, store] + missing: neutral Store_operational_limit_weight: description: one where the store is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, store] + missing: neutral Store_tech_capacity_weight: description: >- one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, store] + missing: neutral variables: Store_e: @@ -426,10 +433,10 @@ assumptions: | $`\underline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_min` over $`\Xi \times \mathcal{V}`$ — least nominal capacity an extendable store may be built at | | $`\overline{\mathrm{e}}^{\mathrm{nom}}`$ | `Store_e_nom_max` over $`\Xi \times \mathcal{V}`$ — most nominal capacity an extendable store may be built at | | $`\mathrm{c}^{\mathrm{cap},e}`$ | `Store_capital_cost` over $`\Xi \times \mathcal{V}`$ — cost of one unit of nominal capacity for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\mathcal{V}`$ — a given nominal capacity for an extendable store; one without a value has no row here | +| $`\mathrm{e}^{\mathrm{nom,set}}`$ | `Store_e_nom_set` over $`\mathcal{V}`$, `neutral` where the data has no row — a given nominal capacity for an extendable store; one without a value has no row here | | $`\mathrm{e}^{\mathrm{nom}}`$ | `Store_e_nom` over $`\Xi \times \mathcal{V}`$ — nominal energy capacity | | $`\mathrm{ext}^{e}`$ | `Store_e_nom_extendable` over $`\mathcal{V}`$ — whether the nominal energy capacity is a decision | -| $`\mathrm{e}^{\mathrm{mod}}`$ | `Store_e_nom_mod` over $`\mathcal{V}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\mathrm{e}^{\mathrm{mod}}`$ | `Store_e_nom_mod` over $`\mathcal{V}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\underline{\mathrm{e}}`$ | `Store_e_min_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — least energy held, per unit of nominal capacity — negative for a store that may go short | | $`\overline{\mathrm{e}}`$ | `Store_e_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — most energy held, per unit of nominal capacity | | $`\mathrm{sgn}^{q}`$ | `Store_sign` over $`\mathcal{V}`$ — the sign the power a store delivers enters its bus's balance with — PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by scenario (`constants.py:43`) | @@ -443,11 +450,11 @@ assumptions: | $`\mathrm{c}^{q}`$ | `Store_marginal_cost` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of power delivered | | $`\mathrm{c}^{q,(2)}`$ | `Store_marginal_cost_quadratic` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of the square of the net power delivered, so charging costs as much as delivering | | $`\mathrm{c}^{e}`$ | `Store_marginal_cost_storage` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — cost of one unit of energy held over one snapshot | -| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given energy schedule; a store without one has no row here | -| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$ — a given schedule of power delivered; a store without one has no row here | -| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\Xi \times \mathcal{G} \times \mathcal{V}`$ — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | -| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\Xi \times \mathcal{G} \times \mathcal{V}`$ — one where the store is in the row's set — data prep; one outside it has no row | -| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{V}`$ — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | +| $`\mathrm{e}^{\mathrm{set}}`$ | `Store_e_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$, `neutral` where the data has no row — a given energy schedule; a store without one has no row here | +| $`\mathrm{q}^{\mathrm{set}}`$ | `Store_p_set` over $`\Xi \times \mathcal{T} \times \mathcal{V}`$, `neutral` where the data has no row — a given schedule of power delivered; a store without one has no row here | +| $`\mathrm{a}^{e}`$ | `Store_primary_energy_weight` over $`\Xi \times \mathcal{G} \times \mathcal{V}`$, `neutral` where the data has no row — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row | +| $`\mathrm{b}^{e}`$ | `Store_operational_limit_weight` over $`\Xi \times \mathcal{G} \times \mathcal{V}`$, `neutral` where the data has no row — one where the store is in the row's set — data prep; one outside it has no row | +| $`\mathrm{m}^{e}`$ | `Store_tech_capacity_weight` over $`\mathcal{G} \times \mathcal{V}`$, `neutral` where the data has no row — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row | #### Variables diff --git a/docs/examples/pypsa/transformer.md b/docs/examples/pypsa/transformer.md index 53ae70f08..55505d98d 100644 --- a/docs/examples/pypsa/transformer.md +++ b/docs/examples/pypsa/transformer.md @@ -69,6 +69,7 @@ parameters: Transformer_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [transformer] + missing: neutral Transformer_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, transformer] @@ -89,9 +90,11 @@ parameters: Transformer_s_nom_set: description: a given nominal apparent power for an extendable transformer; one without a value has no row here dims: [transformer] + missing: neutral Transformer_s_set: description: a given flow schedule; a transformer without one has no row here dims: [scenario, snapshot, transformer] + missing: neutral Transformer_v_ang_max: description: >- the most the voltage angle difference across a transformer, its phase @@ -99,6 +102,7 @@ parameters: infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's dims: [scenario, transformer] + missing: neutral Transformer_x_pu_eff: description: >- the transformer's effective series reactance — PyPSA's `x_pu_eff`, `x` @@ -111,6 +115,7 @@ parameters: basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's dims: [period, transformer, cycle] + missing: neutral Transformer_phase_shift_weight: description: >- a fixed transformer's phase shift in radians at each snapshot, signed by @@ -119,6 +124,7 @@ parameters: the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row dims: [snapshot, transformer, cycle] + missing: neutral Transformer_phase_shift_varying: description: >- whether a transformer's phase shift is a decision — PyPSA's @@ -184,7 +190,7 @@ variables: lossless dims: [scenario, snapshot, transformer] where: Transformer_lossy - absence: zero + missing: neutral bounds: lower: 0 Transformer_phase_shift: @@ -195,7 +201,7 @@ variables: where the shift is fixed dims: [scenario, snapshot, transformer] where: Transformer_phase_shift_varying AND Transformer_active - absence: zero + missing: neutral bounds: lower: Transformer_phase_shift_min upper: Transformer_phase_shift_max @@ -373,17 +379,17 @@ constraints: | $`\mathrm{W}^{\sigma}`$ | `Transformer_capital_weight` over $`\mathcal{M}`$ — the sum of period weights a transformer stands in — PyPSA's `active * period_weighting`, summed, data prep | | $`\sigma^{\mathrm{nom}}`$ | `Transformer_s_nom` over $`\Xi \times \mathcal{M}`$ — nominal apparent power | | $`\mathrm{ext}^{\sigma}`$ | `Transformer_s_nom_extendable` over $`\mathcal{M}`$ — whether the nominal apparent power is a decision | -| $`\sigma^{\mathrm{mod}}`$ | `Transformer_s_nom_mod` over $`\mathcal{M}`$ — the module size a build comes in whole numbers of; no value means the build is continuous | +| $`\sigma^{\mathrm{mod}}`$ | `Transformer_s_nom_mod` over $`\mathcal{M}`$, `neutral` where the data has no row — the module size a build comes in whole numbers of; no value means the build is continuous | | $`\overline{\sigma}`$ | `Transformer_s_max_pu` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — most flow either way, per unit of nominal apparent power | | $`\underline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_min` over $`\Xi \times \mathcal{M}`$ — least nominal apparent power an extendable transformer may be built at | | $`\overline{\sigma}^{\mathrm{nom}}`$ | `Transformer_s_nom_max` over $`\Xi \times \mathcal{M}`$ — most nominal apparent power an extendable transformer may be built at | | $`\mathrm{c}^{\mathrm{cap},\sigma}`$ | `Transformer_capital_cost` over $`\Xi \times \mathcal{M}`$ — cost of one unit of nominal apparent power for the modelled horizon — PyPSA's `periodized_cost`: `overnight_cost` as an annuity over `lifetime` at `discount_rate`, times `nyears`, where it is given, and `capital_cost` where it is not, plus `fom_cost` (`components.py:1126-1147`, `costs.py:102-203`), data prep | -| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$ — a given nominal apparent power for an extendable transformer; one without a value has no row here | -| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — a given flow schedule; a transformer without one has no row here | -| $`\overline{\delta}^{\sigma}`$ | `Transformer_v_ang_max` over $`\Xi \times \mathcal{M}`$ — the most the voltage angle difference across a transformer, its phase shift included, may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's | +| $`\sigma^{\mathrm{nom,set}}`$ | `Transformer_s_nom_set` over $`\mathcal{M}`$, `neutral` where the data has no row — a given nominal apparent power for an extendable transformer; one without a value has no row here | +| $`\sigma^{\mathrm{set}}`$ | `Transformer_s_set` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the data has no row — a given flow schedule; a transformer without one has no row here | +| $`\overline{\delta}^{\sigma}`$ | `Transformer_v_ang_max` over $`\Xi \times \mathcal{M}`$, `neutral` where the data has no row — the most the voltage angle difference across a transformer, its phase shift included, may be either way, in degrees — PyPSA's `v_ang_max`; infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's | | $`\mathrm{x}^{\mathrm{eff},\sigma}`$ | `Transformer_x_pu_eff` over $`\Xi \times \mathcal{M}`$ — the transformer's effective series reactance — PyPSA's `x_pu_eff`, `x` over its `s_nom` times its tap ratio, data prep | -| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{C}`$ — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's | -| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{C}`$ — a fixed transformer's phase shift in radians at each snapshot, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row | +| $`\mathrm{x}^{\sigma}`$ | `Transformer_cycle_weight` over $`\mathcal{Y} \times \mathcal{M} \times \mathcal{C}`$, `neutral` where the data has no row — the transformer's effective series reactance, `x` times its tap ratio, signed by its orientation in the cycle — PyPSA's `x_pu_eff`, the cycle basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's | +| $`\vartheta`$ | `Transformer_phase_shift_weight` over $`\mathcal{T} \times \mathcal{M} \times \mathcal{C}`$, `neutral` where the data has no row — a fixed transformer's phase shift in radians at each snapshot, signed by its orientation in the cycle — a constant added to the cycle sum, data prep; zero for a varying transformer, whose shift is a decision instead, so the constant and the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row | | $`\mathrm{Transformer\_phase\_shift\_varying}`$ | `Transformer_phase_shift_varying` over $`\mathcal{M}`$ — whether a transformer's phase shift is a decision — PyPSA's `phase_shift_min < phase_shift_max`, read as a flag in data prep; false is a fixed shift carried by `phase_shift`. The shift parameters carry no scenario, while PyPSA reads them per scenario (`variables.py:433-443`, `constraints.py:1652`, `:1735`): a parity gap (\#783) | | $`\mathrm{Transformer\_phase\_shift\_min}`$ | `Transformer_phase_shift_min` over $`\mathcal{M}`$ — the least a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_min`; where it is below `phase_shift_max` the shift is a decision, otherwise the transformer keeps its fixed `phase_shift` | | $`\mathrm{Transformer\_phase\_shift\_max}`$ | `Transformer_phase_shift_max` over $`\mathcal{M}`$ — the most a varying transformer's phase shift may take, in degrees — PyPSA's `phase_shift_max`; equal to `phase_shift_min` for a fixed transformer | @@ -398,8 +404,8 @@ constraints: | Symbol | Meaning | |---|---| | $`\sigma`$ | `Transformer_s` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-s` — PyPSA's `p0`, the flow measured at the `Transformer_bus0` end: a positive value withdraws there and injects at `Transformer_bus1`, lossless | -| $`\ell^{\sigma}`$ | `Transformer_loss` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-loss` — what a transformer dissipates carrying its flow, as a line does; absent, and zero in the balance, where the network is lossless | -| $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$ — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | +| $`\ell^{\sigma}`$ | `Transformer_loss` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the mask leaves it out — `Transformer-loss` — what a transformer dissipates carrying its flow, as a line does; absent, and zero in the balance, where the network is lossless | +| $`\mathit{Transformer\_phase\_shift}`$ | `Transformer_phase_shift` over $`\Xi \times \mathcal{T} \times \mathcal{M}`$, `neutral` where the mask leaves it out — `Transformer-phase_shift` — a phase-shifting transformer's voltage angle shift in degrees, chosen per snapshot to redistribute the flows around its cycles without moving active power; absent, and zero in the cycle sum, where the shift is fixed | | $`\Sigma`$ | `Transformer_s_nom_ext` over $`\mathcal{M}`$ — `Transformer-s_nom` — nominal apparent power where it is a decision; the parameter of the same PyPSA name carries the fixed regime | | $`N^{\sigma}`$ | `Transformer_n_mod` over $`\mathcal{M}`$ — `Transformer-n_mod` — how many modules of an extendable modular build; none for a build that stands in no snapshot | diff --git a/docs/howto/compose.md b/docs/howto/compose.md index a6bcdf651..7fe945150 100644 --- a/docs/howto/compose.md +++ b/docs/howto/compose.md @@ -202,6 +202,7 @@ which each component pins at its own port. | a dimension or a relation | every fragment may declare it, and the ones that do say the same thing about it | | a `description` on a shared dimension or relation | it is prose rather than a claim, and the first wording in the list is carried | | `ordered: true` on a shared dimension | it is a claim about the dimension rather than the dimension, so the dimension is ordered if one fragment says so | +| `missing:` on a shared relation | it is a claim about the data, and the fragments that declare the relation say the same one. `missing: refused` is the same as no `missing:` | | any other declaration | 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 declaration where the introducer wrote none | | a given expression | the definition's body carries no dimension the reader's `dims` do not name | @@ -249,7 +250,7 @@ constraints: ``` ```text -fragment 'emissions.yaml' reads the given variable 'Generator_p' as {'dims': ['snapshot', 'generator'], 'domain': 'binary'}, where 'generator.yaml' introduces it as {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0.0, 'upper': 'Generator_p_nom'}, 'domain': 'continuous', 'absence': 'undefined'}. A given declaration says the same as the declaration it is folded into, or less: restate the frame as the introducer declares it, or leave the field out. +fragment 'emissions.yaml' reads the given variable 'Generator_p' as {'dims': ['snapshot', 'generator'], 'domain': 'binary'}, where 'generator.yaml' introduces it as {'dims': ['snapshot', 'generator'], 'bounds': {'lower': 0.0, 'upper': 'Generator_p_nom'}, 'domain': 'continuous', 'missing': 'absent'}. A given declaration says the same as the declaration it is folded into, or less: restate the frame as the introducer declares it, or leave the field out. ``` Two fragments that both only read a column have to read it the same way, and @@ -359,6 +360,7 @@ Given variable 'Generator_p' collides with the variable of the same name. Names | `null` under a section's name | it is refused | | a dimension or a relation | 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 | it changes what a key the map leaves out means, as on a parameter | | an entry under one kind of `given:` | it is edited, added or removed like any declaration, and the other kinds stay | | `version`, `description` | the patch's value replaces the base's | | a field an earlier patch writes | the later patch's value replaces it | @@ -397,6 +399,11 @@ already step along it: patch 'unordered.yaml' says the dimension 'snapshot' is not ordered, where its base declares it ordered. A construct in the base may step along it, and a patch adds the claim of order but never withdraws it: leave `ordered` out of the patch. ``` +`missing:` is a claim about the data, not the coordinate space. A patch may +change it on a relation as on a parameter, and may name it alone: +`relations: {gen_bus: {missing: absent}}`. `null` puts the default, `refused`, +back. + ## A section set to `null` A `null` removes the declaration it names. A section holds declarations rather diff --git a/docs/howto/see-an-expansion.md b/docs/howto/see-an-expansion.md index 152d00797..c7ad947ec 100644 --- a/docs/howto/see-an-expansion.md +++ b/docs/howto/see-an-expansion.md @@ -245,8 +245,8 @@ the set out too. holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. ``` @@ -352,8 +352,8 @@ the set out too. holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. ``` diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 70b42029e..987b28ec4 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -82,9 +82,11 @@ on a declaration, and the coordinates it admits. row that reads it is not built ([absence](language/absence.md)). **Missing row** -: A coordinate that a parameter's table has no row for. It is not absence: it -reads as `0` in arithmetic and as false in a `where` -([what creates absence](language/absence.md#what-creates-absence)). +: A coordinate that a parameter's or a relation's table has no row for. Its +`missing:` says what it is: refused when the data is attached (`refused`, the +default), absence (`absent`), `0` in arithmetic and false in a `where` +(`neutral`), or a value +([a missing row](language/declarations.md#a-missing-row)). ## Kinds of construct diff --git a/docs/reference/language/absence.md b/docs/reference/language/absence.md index 8ac362f2e..8957df9f1 100644 --- a/docs/reference/language/absence.md +++ b/docs/reference/language/absence.md @@ -26,23 +26,35 @@ The [grammar](expressions.md#where-strings) says what a `where:` may hold. ## What creates absence -| Construct | What is absent | -| --------------------------------------------- | --------------------------------------------------------------------------- | -| `where:` on a variable | the variable, at the masked coordinates | -| `where:` on a constraint | the row | -| `shift(x, along=d, offset=n)` without `edge=` | the vacated edge coordinate ([shift](operators.md#shift)) | -| a label a relation does not map | that label's group membership ([relations](relations.md#the-data-contract)) | - -Nothing else creates absence. **A missing parameter row is not absence.** It -reads as the value that contributes nothing: `0` as a coefficient, and `false` -in a `where`. -A missing row is the only gap a parameter has: a null or NaN value is -[refused](declarations.md#parameters) when the data is attached. - -Where no such value exists, loading is refused. There are four such positions: -a divisor, a `bounds:` entry, the whole constant side of a comparison, and a -[`piecewise:`](piecewise.md) breakpoint. For a bound only where the data has -one, supply the bound in the data, where `inf` is a value, or mask the variable. +| Construct | What is absent | +| ------------------------------------------------- | --------------------------------------------------------------------------- | +| `where:` on a variable | the variable, at the masked coordinates | +| `where:` on a constraint | the row | +| `shift(x, along=d, offset=n)` without `edge=` | the vacated edge coordinate ([shift](operators.md#shift)) | +| a label a `missing: absent` relation does not map | that label's group membership ([relations](relations.md#the-data-contract)) | +| a missing row of a `missing: absent` parameter | the parameter, at that coordinate | + +Nothing else creates absence. A missing row is the only gap a parameter has: a +null or NaN value is [refused](declarations.md#parameters) when the data is +attached. **The parameter's [`missing:`](declarations.md#a-missing-row) says +what a missing row is:** + +| `missing:` | A missing row | +| --------------------- | ------------------------------------------------------------------------------------------- | +| `refused`, by default | is refused when the data is attached | +| `absent` | is absence, by the rules on this page | +| `neutral` | reads as the value that contributes nothing: `0` as a coefficient, and `false` in a `where` | +| a value | reads as that value | + +`neutral` has no value in three positions: a divisor, a `bounds:` entry, and +the whole constant side of a comparison. A missing row there is refused when +the data is attached. A curve needs a row at each breakpoint its `points:` +admits. [Missing breakpoints](piecewise.md#missing-breakpoints) says what a +missing row of a table a curve reads means. Under `absent`, a missing divisor +or constant side is absence and takes the row, and a missing bound leaves that +side of the variable open. For a bound only where the +data has one, declare the parameter `missing: absent` or `missing: .inf`, or mask +the variable. ## How absence travels @@ -77,9 +89,10 @@ constraints: expression: x - rel_max * y <= 0 ``` -Where the variable `y` is masked, the row is gone. Where the parameter `rel_max` -has no row, it reads as `0`, and the row stands as `x <= 0`. To drop the row -there instead, write `where: rel_max` on the constraint. +Where the variable `y` is masked, the row is gone. Where a `missing: neutral` +parameter `rel_max` has no row, it reads as `0`, and the row stands as +`x <= 0`. To drop the row there instead, declare `rel_max` `missing: absent`, or +write `where: rel_max` on the constraint. | Operator | An output slot reads | An absent input | | -------------------------------- | ------------------------------- | ------------------------------------ | @@ -100,10 +113,10 @@ variables: spill: dims: [storage] where: has_inflow - absence: zero # outside the mask spill is 0 and the row stands + missing: neutral # outside the mask spill is 0 and the row stands soc: dims: [storage] - where: has_store # the default, absence: undefined — no row + where: has_store # the default, missing: absent — no row constraints: balance: dims: [storage] @@ -113,11 +126,13 @@ constraints: At a storage with a store and no inflow, `balance` reads `inflow - soc == 0`. At a storage with inflow and no store, there is no row. -`absence: zero` needs a `where:`. It changes nothing inside a summing operator. +`missing: neutral` needs a `where:`. It changes nothing inside a summing operator. +The typeset legend prints `` `neutral` where the mask leaves it out `` beside +the variable. ## Rows with no variable terms -A missing parameter row can leave a row with nothing to decide, such as +A `neutral` parameter row can leave a row with nothing to decide, such as `0 == load` at a bus with no generator. Such a row is not built. An expression that names no variable _in the file_ is refused at load. @@ -128,6 +143,6 @@ the solved numbers it reads, by the rules above. A quotient is absent where its divisor is absent, and where its divisor is exactly zero, whether a solve or the data gave that zero. A solved value is read as the engine reads it back, and an engine may read a value within its solver's tolerance of zero as zero. -A divisor parameter with a missing row where the quotient is read is still -refused, because a missing row is not absence. A deleted row has +A divisor parameter with a missing row where the quotient is read is refused, +unless the parameter is `missing: absent` or names a value. A deleted row has [no dual](named.md#reading-a-constraints-dual). diff --git a/docs/reference/language/declarations.md b/docs/reference/language/declarations.md index 7a14dfc74..fd8792e44 100644 --- a/docs/reference/language/declarations.md +++ b/docs/reference/language/declarations.md @@ -23,11 +23,12 @@ parameters: dims: [] # a scalar ``` -| Field | | | -| ------------- | -------------------------------------------------------------- | --------------- | -| `dims` | required. The dimensions it is indexed by. `[]` means a scalar | | -| `dtype` | `float`, `int`, `bool`, `str` | default `float` | -| `description` | free text | default `null` | +| Field | | | +| ------------- | -------------------------------------------------------------- | ----------------- | +| `dims` | required. The dimensions it is indexed by. `[]` means a scalar | | +| `dtype` | `float`, `int`, `bool`, `str` | default `float` | +| `missing` | what a missing row means ([a missing row](#a-missing-row)) | default `refused` | +| `description` | free text | default `null` | The column has to match the `dtype`: @@ -47,6 +48,59 @@ parameter is a mask: each selects rows in a a term or a divisor is a load error. A `0` or `1` that is meant to be multiplied by is declared `dtype: int`. +### A missing row + +`missing:` says what a coordinate the `dims` reach with no row means. A table +that lost a row in preparation and a table that never had one look the same in +the data, so the file says which was meant. + +```yaml +dimensions: + generator: { dtype: str } +parameters: + cost: { dims: [generator] } # refused: every generator has a cost + ramp_limit: { dims: [generator], missing: neutral } # no row means no limit + p_set: { dims: [generator], missing: absent } # no row, no fixing row + efficiency: { dims: [generator], missing: 1 } + p_nom_max: { dims: [generator], missing: .inf } + active: { dims: [generator], dtype: bool, missing: true } +``` + +| `missing:` | A missing row | +| --------------------- | ----------------------------------------------------------------------------------------------------- | +| `refused`, by default | is refused when the data is attached, and the refusal names the coordinate | +| `absent` | is [absence](absence.md): it takes the row of a term that reads it, and is one summand fewer in a sum | +| `neutral` | reads as the value that contributes nothing: `0` as a coefficient, and `false` in a `where` | +| a value | reads as that value, wherever a value is read | + +A consumer does not build the model from data with a refused row. How it says +so is its own: it may raise at the first gap, or list every missing coordinate. + +A bare numeric name in a `where` asks whether the data has a row under every +reading, so `where: p_nom_max` selects the rows the data gives. Under `refused`, every coordinate has a row, so a bare `float` name asks only "is it finite?", and a bare `int` or `str` name is a load error. A comparison +reads the value: `where: efficiency <= 1` is true at a missing row of +`efficiency` above, which reads `1` there. Under `absent` and `neutral`, a +comparison at a missing row is false. A bare `bool` name reads its value, so +`where: active` reads `true` there. + +A value has the parameter's dtype: + +| `dtype` | a value | +| ------- | -------------------------------------------------------- | +| `float` | a number. `.inf`, `inf`, `-.inf` and `-inf` are numbers | +| `int` | an integer. An integer column cannot hold `inf` | +| `bool` | `true` or `false` | +| `str` | none. A label has no value to fill, and no `neutral` one | + +A NaN, a quoted number and `missing: null` are refused. A `given:` parameter +has no `missing:`. The file that declares the parameter owns it. A parameter a +[`piecewise:`](piecewise.md#missing-breakpoints) block reads takes `missing:` +too. A values parameter of a curve with `points:` declares one other than +`refused`. + +The typeset legend prints what a missing row means beside the parameter, such +as `` `neutral` where the data has no row ``. It prints nothing for `refused`. + ## `variables` A variable is what the solver decides. There is one column per coordinate of @@ -73,7 +127,7 @@ variables: | `where` | which coordinates exist ([absence](absence.md)) | default `null` | | `bounds.lower` / `bounds.upper` | a finite number, or the name of a `float` or `int` parameter. `null` leaves that side open | default `null` | | `domain` | `continuous`, `integer` or `binary`. `binary` carries fixed 0/1 bounds | default `continuous` | -| `absence` | `undefined` or `zero`: what a masked-out coordinate means ([absence](absence.md#what-a-missing-coordinate-means)) | default `undefined` | +| `missing` | `absent` or `neutral`: what a masked-out coordinate means ([absence](absence.md#what-a-missing-coordinate-means)) | default `absent` | | `description` | free text | default `null` | An open side is `null`. A bound is never infinite: `.inf` and `-.inf` are diff --git a/docs/reference/language/expressions.md b/docs/reference/language/expressions.md index cdbcfb3c2..4326f0917 100644 --- a/docs/reference/language/expressions.md +++ b/docs/reference/language/expressions.md @@ -143,28 +143,32 @@ COLUMNS ::= NAME "[" NAME { "," NAME } "]" QUOTED ::= "'" chars "'" | '"' chars '"' ``` -| Written as | Names a… | Meaning | -| ----------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `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 | -| `name` (bare) | variable | The variable exists at this coordinate | -| `name` (bare) | relation | A row exists, read at the relation's key. A relation may be [partial](relations.md#the-data-contract), and this selects the labels that do map | -| `name` (bare) | dimension | A load error. It would be true everywhere | -| `name` (bare) | mask | The mask's predicate ([masks](named.md#masks)). A given mask is true or false at each coordinate, as a `bool` parameter is | -| `name OP value` | parameter | Element-wise, and a null 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 | -| `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 | -| `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 | -| `count(where_expr, over=name) OP i` | a predicate | How many coordinates along the dimension the predicate admits ([counting what a predicate admits](#counting-what-a-predicate-admits)) | -| `shift(where_expr, along=name, offset=i)` | a predicate | The predicate read `i` coordinates back, and false where that vacates | -| `at(where_expr, by=relation[c])` | a predicate | The predicate read through the relation ([reading a predicate through a relation](#reading-a-predicate-through-a-relation)), and false where the relation has no row | -| `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 a declaration with no rows | +| Written as | Names a… | Meaning | +| ----------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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) | dimension | A load error. It would be true everywhere | +| `name` (bare) | mask | The mask's predicate ([masks](named.md#masks)). A given mask 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 | +| `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 | +| `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 | +| `count(where_expr, over=name) OP i` | a predicate | How many coordinates along the dimension the predicate admits ([counting what a predicate admits](#counting-what-a-predicate-admits)) | +| `shift(where_expr, along=name, offset=i)` | a predicate | The predicate read `i` coordinates back, and false where that vacates | +| `at(where_expr, by=relation[c])` | a predicate | The predicate read through the relation ([reading a predicate through a relation](#reading-a-predicate-through-a-relation)), and false where the relation has no row | +| `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 a declaration with no rows | A bare name that is not declared is a load error. +A comparison at a missing row reads the value the parameter's +[`missing:`](declarations.md#a-missing-row) names, as does a bare `bool` name. +Under every reading, a bare numeric name asks whether the data has a row. + ### Counting what a predicate admits `count(, over=)` is how many coordinates along that diff --git a/docs/reference/language/index.md b/docs/reference/language/index.md index c9c24bad2..bc2e30e6e 100644 --- a/docs/reference/language/index.md +++ b/docs/reference/language/index.md @@ -53,7 +53,7 @@ names the fix. | 5 | `a + b` carries the dimensions of `a` and of `b` together. A constraint's expression carries exactly its `dims`, and the objective carries none. | [How dimensions combine](expressions.md#how-dimensions-combine) | | 6 | A variable's `where:` deletes the variable at the masked coordinates. A constraint's `where:` deletes the row. | [Absence](absence.md) | | 7 | A deleted variable takes its row with it. Inside a `sum` it is one term fewer, and the row stays. | [Absence](absence.md#how-absence-travels) | -| 8 | A parameter row missing from the table reads as `0` in arithmetic and as false in a `where`. Where `0` would change the meaning, the row is refused. | [Absence](absence.md#what-creates-absence) | +| 8 | A row missing from a table is refused, unless the declaration's `missing:` says it is absent, neutral, or a value. | [Absence](absence.md#what-creates-absence) | | 9 | Two variables may be multiplied in the objective and in a constraint, and nowhere else. `x / y` and `a ** b` need their divisor, base and exponent free of variables. | [Expressions](expressions.md) | | 10 | The operators are `sum`, `sum_back`, `at` and `shift`, plus `dual` in a reported expression. A file cannot add one. | [Operators](operators.md) | | 11 | A construct that steps or counts along a dimension needs it declared `ordered: true`: `shift`, `sum_back`, `position`, `piecewise` and an `sos` of `type: 2`. | [Order](dimensions.md#order) | diff --git a/docs/reference/language/named.md b/docs/reference/language/named.md index 76ebc2caf..5dc6494d4 100644 --- a/docs/reference/language/named.md +++ b/docs/reference/language/named.md @@ -125,7 +125,7 @@ A named expression carries **exactly one** of `expression:` and `cases:`. A claimed coordinate can still have no value: the `otherwise:` above has none at the first snapshot, where a case claims every unit. To close such a hole, -widen a `when`, give the `shift` an `edge=`, or set `absence: zero` on the +widen a `when`, give the `shift` an `edge=`, or set `missing: neutral` on the masked variable. `cases:` is not accepted inside a `macros:` template. diff --git a/docs/reference/language/piecewise.md b/docs/reference/language/piecewise.md index 6a14472b1..b9cc22fed 100644 --- a/docs/reference/language/piecewise.md +++ b/docs/reference/language/piecewise.md @@ -71,13 +71,13 @@ variables: ``` Where the gate does not exist, the curve is ungated. To have no curve there -instead, put `absence: zero` on the gate. +instead, put `missing: neutral` on the gate. ### `points` -A values parameter short of a row does not build a shorter curve: the missing -row reads as a breakpoint at the origin. A curve with fewer breakpoints than the -dimension holds says so with `points:`. Name one of the block's own values +A values parameter short of a row does not build a shorter curve: the block's +[assumption](assumptions.md#what-a-curve-assumes) refuses the data. A curve with +fewer breakpoints than the dimension holds says so with `points:`. Name one of the block's own values parameters, and the curve is as long as that parameter has rows: ```yaml @@ -90,10 +90,34 @@ piecewise: - [op_cost, bp_y] ``` -A row missing from `bp_y` is still refused. Where the length is its own data, +Each values parameter declares `missing: neutral`, because its table stops +where the curve stops ([below](#missing-breakpoints)). A row missing from `bp_y` +where `bp_x` has one is still refused. Where the length is its own data, name a boolean parameter instead. The marked breakpoints are one consecutive run, anywhere on the axis. +### Missing breakpoints + +A parameter a block reads takes [`missing:`](declarations.md#a-missing-row) +like any other parameter, and reads it alike inside and outside the curve. +**A values parameter of a curve with `points:` is not `refused`.** The curve +stops where the mask stops, so the table has no rows past it by design. +`refused` says that every row is there: the curve never runs short, and a +mask that names the table marks every breakpoint. The load is refused: + +```text +parameter 'bp_y' is refused where a row is missing, and piecewise 'cost_curve' reads it under points: 'bp_x', which stops the curve where its rows stop. Declare missing: neutral, absent, or a value of its dtype. +``` + +Declare `missing: neutral` or `absent` on each values parameter of the block. +The two build the same curve: the curve reads a table only inside the mask, +and there the `_complete` assumption still refuses a missing row. They +differ where an expression outside the curve reads the table: `neutral` reads +`0` there, and `absent` drops the term. Use `absent` where no number stands +for a breakpoint the curve does not have, and `neutral` where `0` does. A +values parameter of a curve with no `points:`, and a boolean `points:` mask, +read the default, `refused`. + ### `method` `method` says how the weights are restricted once they exist. @@ -174,6 +198,6 @@ shows a spec before and after. - **Every name written out starts with the name of the block.** The weights of the curve `curve` are `curve_lam`. - **No formulation emits a parameter.** The same data attaches to a spec and its - expansion. + expansion, and reads alike in both. - **The assumptions a `method:` implies become `assumptions:` entries** with the same names. diff --git a/docs/reference/language/relations.md b/docs/reference/language/relations.md index 26f7f0e45..7339bcfd3 100644 --- a/docs/reference/language/relations.md +++ b/docs/reference/language/relations.md @@ -28,11 +28,12 @@ The **key** is the combination of dimensions that is unique per row: 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. Omitted, the key is every column | default none | -| `description` | free text | default `null` | +| Field | | | +| ------------- | ---------------------------------------------------------------- | ----------------- | +| `key` | required. The columns that identify a row | | +| `values` | the columns the key determines. Omitted, the key is every column | default none | +| `missing` | `refused` or `absent`: what a key the map leaves out means | 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 @@ -65,12 +66,35 @@ column per declared column, named after it. is attached. - **Every value is a label of its dimension.** A value that matches none is refused. -- **A partial map is the rows it has.** A generator in no row sits on no bus, - which is [absence](absence.md). +- **A relation with `values:` has a row for every key coordinate.** A + generator the table leaves out is refused when the data is attached, and the + refusal names it. +- **Under `missing: absent`, a partial map is the rows it has.** A generator in + no row sits on no bus, which is [absence](absence.md): its terms land in no + group. - **A null in any column is refused.** - **Row order carries nothing.** The order is the [dimension's](dimensions.md). +A generator on no bus and a port nobody wired look the same in the data, so +`missing:` says which was meant: + +```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 +``` + +A label is data, so no value fills a gap in a map, and `neutral` is refused. A +bare relation takes no `missing:`: its rows are its membership, so a pair it +leaves out is not missing. The typeset legend prints +`` `line_to` is `absent` where the data has no row `` beside each set the map +joins. It prints nothing for `refused`. + ## How a relation is used The declaration fixes no direction. A call **joins** the operand to the diff --git a/docs/reference/notation.md b/docs/reference/notation.md index cb315baea..aa687e273 100644 --- a/docs/reference/notation.md +++ b/docs/reference/notation.md @@ -39,7 +39,7 @@ dimensions: relations: gen_bus: { key: generator, values: bus } zone_of: { key: bus, values: zone } - area_of: { key: bus, values: zone } # a second map into the same set, to compare against + 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 @@ -47,20 +47,20 @@ relations: gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once parameters: - p_max: { dims: [generator] } + p_max: { dims: [generator], missing: .inf } # a value: the legend says what a missing row reads as p_min: { dims: [generator] } - cost: { dims: [generator] } + cost: { dims: [generator], missing: neutral } # a reading: the legend names it load: { dims: [snapshot, bus] } - is_flexible: { dims: [generator], dtype: bool } + is_flexible: { dims: [generator], dtype: bool, missing: false } zone_cap: { dims: [zone] } - tech_cap: { dims: [bus, technology] } + tech_cap: { dims: [bus, technology], missing: absent } min_up: { dims: [generator], dtype: int } eta: { dims: [generator] } # a Greek name that is *given*, so the rule wins and it prints as the word lead: { dims: [generator], dtype: int } budget: { dims: [] } # scalar: the legend says so rather than printing an empty product growth: { dims: [] } # the base of a power; the exponent is `lead`, a column - bp_x: { dims: [generator, bp] } # the x-axis of every curve below, and what a derived mask is read from - bp_y: { dims: [generator, bp] } + bp_x: { dims: [generator, bp], missing: neutral } # the x-axis of every curve below, and what a derived mask is read from + bp_y: { dims: [generator, bp], missing: neutral } bp_heat: { dims: [generator, bp] } bp_run: { dims: [generator, bp], dtype: bool } # how far each curve runs, so a block has a mask to print ``` @@ -71,8 +71,8 @@ parameters: |---|---| | $`\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}`$ | -| $`\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}`$ | -| $`\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}`$ | +| $`\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}`$, `area_of` 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}`$, `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}`$ | | $`\mathcal{A}`$ | index $`a`$ — `bp` | @@ -81,20 +81,20 @@ parameters: | Symbol | Meaning | |---|---| -| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$ | +| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$, $`\infty`$ where the data has no row | | $`\mathrm{p}^{\mathrm{min}}`$ | `p_min` over $`\mathcal{G}`$ | -| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$ | +| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$, `neutral` where the data has no row | | $`\mathrm{load}`$ | `load` over $`\mathcal{T} \times \mathcal{B}`$ | -| $`\mathrm{is\_flexible}`$ | `is_flexible` over $`\mathcal{G}`$ | +| $`\mathrm{is\_flexible}`$ | `is_flexible` over $`\mathcal{G}`$, `false` where the data has no row | | $`\mathrm{zone\_cap}`$ | `zone_cap` over $`\mathcal{Z}`$ | -| $`\mathrm{tech\_cap}`$ | `tech_cap` over $`\mathcal{B} \times \mathcal{E}`$ | +| $`\mathrm{tech\_cap}`$ | `tech_cap` over $`\mathcal{B} \times \mathcal{E}`$, `absent` where the data has no row | | $`\mathrm{min\_up}`$ | `min_up` over $`\mathcal{G}`$ | | $`\mathrm{eta}`$ | `eta` over $`\mathcal{G}`$ | | $`\mathrm{lead}`$ | `lead` over $`\mathcal{G}`$ | | $`\mathrm{budget}`$ | `budget` (scalar) | | $`\mathrm{growth}`$ | `growth` (scalar) | -| $`\mathrm{bp\_x}`$ | `bp_x` over $`\mathcal{G} \times \mathcal{A}`$ | -| $`\mathrm{bp\_y}`$ | `bp_y` over $`\mathcal{G} \times \mathcal{A}`$ | +| $`\mathrm{bp\_x}`$ | `bp_x` over $`\mathcal{G} \times \mathcal{A}`$, `neutral` where the data has no row | +| $`\mathrm{bp\_y}`$ | `bp_y` over $`\mathcal{G} \times \mathcal{A}`$, `neutral` where the data has no row | | $`\mathrm{bp\_heat}`$ | `bp_heat` over $`\mathcal{G} \times \mathcal{A}`$ | | $`\mathrm{bp\_run}`$ | `bp_run` over $`\mathcal{G} \times \mathcal{A}`$ | @@ -110,7 +110,7 @@ parameters: | $`\mathit{units}`$ | `units` over $`\mathcal{G}`$ | | $`\mathit{spare}`$ | `spare` over $`\mathcal{G}`$ | | $`\mathit{reserve}`$ | `reserve` (scalar) | -| $`\mathit{headroom}`$ | `headroom` (scalar) | +| $`\mathit{headroom}`$ | `headroom` (scalar), `neutral` where the mask leaves it out | | $`\mathit{weight}`$ | `weight` over $`\mathcal{T} \times \mathcal{G}`$ | | $`\mathit{fuel}`$ | `fuel` over $`\mathcal{T} \times \mathcal{G}`$ | | $`\mathit{heat}`$ | `heat` over $`\mathcal{T} \times \mathcal{G}`$ | @@ -269,13 +269,14 @@ variables: #### Scalar variable with a condition -scalar too, but masked, so the condition stands with no set beside it +scalar too, but masked, so the condition stands with no set beside it; neutral, so the legend names the reading ```yaml variables: headroom: dims: [] where: "budget" + missing: neutral bounds: { lower: 0 } ``` diff --git a/docs/reference/reading.md b/docs/reference/reading.md index 3be20cace..40b107f60 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -119,6 +119,18 @@ written = assumption_message('cost_is_never_negative', program.assumptions['cost written # "assumption 'cost_is_never_negative' does not hold for the data attached to 'bp_y' — a negative cost is a gain the objective would chase" ``` +`program.parameters[name].missing` says what a missing row of that parameter +means: `'refused'`, `'absent'`, `'neutral'`, or the value it reads as. It is +`None` for a given parameter, whose declaring file says. The program of +`spec.expand(...)` reports the same readings. `program.relations[name].missing` is `'refused'` or `'absent'`, and +`None` for a bare relation. Under `'refused'` the engine does not build the model from a +table with a missing row, and names the coordinate; whether it raises at the +first gap or lists them all is its own. A value is read wherever a value is read: an expression, +a bound, and a comparison in a mask. A `ParameterDefined` on a numeric +parameter asks whether the data has a row, so a value does not answer it. A +`ParameterDefined` on a `bool` parameter reads the value. Under `'absent'`, a +bound that reads a missing row leaves that side open. + ## Nodes and masks The node classes live in `mathspec.program`, for `isinstance` tests and field diff --git a/examples/piecewise_ragged.yaml b/examples/piecewise_ragged.yaml index fd6b0ec14..f6804da18 100644 --- a/examples/piecewise_ragged.yaml +++ b/examples/piecewise_ragged.yaml @@ -6,9 +6,8 @@ description: >- The same least-cost dispatch as `piecewise_lp.yaml`, with curves of different lengths. A large unit is metered at every breakpoint and a small one at the first few, so the breakpoint table is ragged. `points:` is what says how far - each curve runs: without it every curve claims the whole axis, and a - breakpoint with no row is read as a zero rather than as a shorter curve — - which sits the curve on the origin instead of ending it. + each curve runs: without it every curve claims the whole axis, and the data + is refused at a breakpoint with no row. dimensions: snapshot: @@ -38,9 +37,11 @@ parameters: bp_x: description: breakpoint dispatch levels, bound where `metered` is true dims: [generator, bp] + missing: neutral bp_y: description: cost at each breakpoint, bound where `metered` is true dims: [generator, bp] + missing: neutral variables: dispatch: diff --git a/examples/pypsa.yaml b/examples/pypsa.yaml index e1329f9b9..84c131473 100644 --- a/examples/pypsa.yaml +++ b/examples/pypsa.yaml @@ -114,15 +114,19 @@ parameters: Generator_ramp_limit_up: description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_down: description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_start_up: - description: most output in the snapshot a unit starts, per unit of nominal power + description: most output in the snapshot a unit starts, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_ramp_limit_shut_down: - description: most output in the snapshot before a unit stops, per unit of nominal power + description: most output in the snapshot before a unit stops, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_min_up_time: description: least snapshots a unit stays on once started dims: [scenario, generator] @@ -141,6 +145,7 @@ parameters: only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot dims: [scenario, generator] + missing: neutral Generator_must_stay_up: description: >- true while the up time a unit brought into the horizon still binds — @@ -167,6 +172,7 @@ parameters: Generator_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [generator] + missing: neutral Generator_modules_installed: description: >- how many whole modules a committable build has in place: `Generator_p_nom @@ -185,6 +191,7 @@ parameters: (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep dims: [scenario, generator] + missing: neutral Generator_p_min_pu_nonneg: description: >- true where none of the generator's own minimums-per-unit is negative — @@ -212,6 +219,7 @@ parameters: `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, generator] + missing: neutral Generator_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -223,9 +231,11 @@ parameters: Link_ramp_limit_up: description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_ramp_limit_down: description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_p_nom: description: nominal power dims: [scenario, link] @@ -274,11 +284,13 @@ parameters: dims: [link] dtype: bool Link_ramp_limit_start_up: - description: most flow in the snapshot a link starts, per unit of nominal power + description: most flow in the snapshot a link starts, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_ramp_limit_shut_down: - description: most flow in the snapshot before a link stops, per unit of nominal power + description: most flow in the snapshot before a link stops, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_min_up_time: description: least snapshots a link stays on once started dims: [scenario, link] @@ -297,6 +309,7 @@ parameters: only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot dims: [scenario, link] + missing: neutral Link_must_stay_up: description: >- true while the up time a link brought into the horizon still binds — @@ -323,6 +336,7 @@ parameters: Link_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [link] + missing: neutral Link_modules_installed: description: >- how many whole modules a committable build has in place: `Link_p_nom @@ -341,6 +355,7 @@ parameters: (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep dims: [scenario, link] + missing: neutral Link_p_min_pu_nonneg: description: >- true where none of the link's own minimums-per-unit is negative — @@ -368,6 +383,7 @@ parameters: `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, link] + missing: neutral Link_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -423,12 +439,15 @@ parameters: Process_ramp_limit_up: description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_ramp_limit_down: description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_p_set: description: a given internal power schedule; a process without one has no row here dims: [scenario, snapshot, process] + missing: neutral Process_p_nom_min: description: least nominal power an extendable process may be built at dims: [scenario, process] @@ -446,16 +465,19 @@ parameters: Process_p_nom_set: description: a given nominal power for an extendable process; one without a value has no row here dims: [process] + missing: neutral Process_committable: description: whether internal power is gated by an on/off status decision dims: [process] dtype: bool Process_ramp_limit_start_up: - description: most internal power in the snapshot a process starts, per unit of nominal power + description: most internal power in the snapshot a process starts, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_ramp_limit_shut_down: - description: most internal power in the snapshot before a process stops, per unit of nominal power + description: most internal power in the snapshot before a process stops, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_min_up_time: description: least snapshots a process stays on once started dims: [scenario, process] @@ -474,6 +496,7 @@ parameters: only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot dims: [scenario, process] + missing: neutral Process_must_stay_up: description: >- true while the up time a process brought into the horizon still binds — @@ -500,6 +523,7 @@ parameters: Process_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [process] + missing: neutral Process_modules_installed: description: >- how many whole modules a committable build has in place: `Process_p_nom @@ -518,6 +542,7 @@ parameters: (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep dims: [scenario, process] + missing: neutral Process_p_min_pu_nonneg: description: >- true where none of the process's own minimums-per-unit is negative — @@ -545,6 +570,7 @@ parameters: `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, process] + missing: neutral Process_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -575,9 +601,11 @@ parameters: CVaR_omega: description: PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one dims: [] + missing: neutral CVaR_alpha: - description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability + description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` dims: [] + missing: neutral period_weight_objective: description: >- PyPSA's `investment_period_weightings.objective` — what a period's cost @@ -678,6 +706,7 @@ parameters: `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise dims: [carrier] + missing: neutral Carrier_max_relative_growth: description: >- share of the previous period's additions that may be added on top — the @@ -686,9 +715,11 @@ parameters: Generator_p_set: description: a given output schedule; a generator without one has no row here dims: [scenario, snapshot, generator] + missing: neutral Link_p_set: description: a given flow schedule; a link without one has no row here dims: [scenario, snapshot, link] + missing: neutral snapshot_weightings_stores: description: PyPSA's `snapshot_weightings.stores` — hours a snapshot stands for in a storage balance dims: [snapshot] @@ -712,6 +743,7 @@ parameters: Generator_p_nom_set: description: a given nominal power for an extendable generator; one without a value has no row here dims: [generator] + missing: neutral Generator_e_sum_min: description: least energy over the horizon; minus infinity where no floor is meant dims: [scenario, generator] @@ -735,6 +767,7 @@ parameters: Link_p_nom_set: description: a given nominal power for an extendable link; one without a value has no row here dims: [link] + missing: neutral StorageUnit_p_nom_min: description: least nominal power an extendable storage unit may be built at dims: [scenario, storage_unit] @@ -752,6 +785,7 @@ parameters: StorageUnit_p_nom_set: description: a given nominal power for an extendable storage unit; one without a value has no row here dims: [storage_unit] + missing: neutral Store_e_nom_min: description: least nominal capacity an extendable store may be built at dims: [scenario, store] @@ -769,6 +803,7 @@ parameters: Store_e_nom_set: description: a given nominal capacity for an extendable store; one without a value has no row here dims: [store] + missing: neutral StorageUnit_p_nom: description: nominal power dims: [scenario, storage_unit] @@ -779,6 +814,7 @@ parameters: StorageUnit_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [storage_unit] + missing: neutral StorageUnit_p_min_pu: description: most storing, per unit of nominal power and negated dims: [scenario, snapshot, storage_unit] @@ -858,15 +894,19 @@ parameters: StorageUnit_p_set: description: a given net dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_dispatch_set: description: a given dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_store_set: description: a given charging schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_state_of_charge_set: description: a given charge schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral Store_e_nom: description: nominal energy capacity dims: [scenario, store] @@ -877,6 +917,7 @@ parameters: Store_e_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [store] + missing: neutral Store_e_min_pu: description: least energy held, per unit of nominal capacity — negative for a store that may go short dims: [scenario, snapshot, store] @@ -941,9 +982,11 @@ parameters: Store_e_set: description: a given energy schedule; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Store_p_set: description: a given schedule of power delivered; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Line_s_nom: description: nominal apparent power dims: [scenario, line] @@ -954,6 +997,7 @@ parameters: Line_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [line] + missing: neutral Line_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, line] @@ -974,9 +1018,11 @@ parameters: Line_s_nom_set: description: a given nominal apparent power for an extendable line; one without a value has no row here dims: [line] + missing: neutral Line_s_set: description: a given flow schedule; a line without one has no row here dims: [scenario, snapshot, line] + missing: neutral Line_v_ang_max: description: >- the most the voltage angle difference across a line may be either way, @@ -985,6 +1031,7 @@ parameters: ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) dims: [scenario, line] + missing: neutral Line_x_pu_eff: description: >- the line's effective series reactance — PyPSA's `x_pu_eff`, `x` over @@ -996,6 +1043,7 @@ parameters: the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) dims: [period, line, cycle] + missing: neutral Line_BODF: description: >- the share of an outaged branch's flow a line takes on when that branch @@ -1005,6 +1053,7 @@ parameters: the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) dims: [period, line, outage] + missing: neutral transmission_losses: description: >- whether the network dissipates transmission losses — PyPSA's @@ -1040,6 +1089,7 @@ parameters: Transformer_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [transformer] + missing: neutral Transformer_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, transformer] @@ -1060,9 +1110,11 @@ parameters: Transformer_s_nom_set: description: a given nominal apparent power for an extendable transformer; one without a value has no row here dims: [transformer] + missing: neutral Transformer_s_set: description: a given flow schedule; a transformer without one has no row here dims: [scenario, snapshot, transformer] + missing: neutral Transformer_v_ang_max: description: >- the most the voltage angle difference across a transformer, its phase @@ -1070,6 +1122,7 @@ parameters: infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's dims: [scenario, transformer] + missing: neutral Transformer_x_pu_eff: description: >- the transformer's effective series reactance — PyPSA's `x_pu_eff`, `x` @@ -1082,12 +1135,14 @@ parameters: basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's dims: [period, transformer, cycle] + missing: neutral Transformer_BODF: description: >- the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network dims: [period, transformer, outage] + missing: neutral Transformer_phase_shift_weight: description: >- a fixed transformer's phase shift in radians at each snapshot, signed by @@ -1096,6 +1151,7 @@ parameters: the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row dims: [snapshot, transformer, cycle] + missing: neutral Transformer_phase_shift_varying: description: >- whether a transformer's phase shift is a decision — PyPSA's @@ -1178,27 +1234,34 @@ parameters: on a row that names a period without it (`global_constraints.py:364-369`) dims: [scenario, global_constraint, snapshot] dtype: bool + missing: neutral Generator_primary_energy_weight: description: >- the constrained attribute per unit of energy at the bus — the carrier's `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row dims: [scenario, global_constraint, snapshot, generator] + missing: neutral StorageUnit_primary_energy_weight: description: the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral Store_primary_energy_weight: description: the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row dims: [scenario, global_constraint, store] + missing: neutral Generator_operational_limit_weight: description: one where the generator is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, generator] + missing: neutral StorageUnit_operational_limit_weight: description: one where the storage unit is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral Store_operational_limit_weight: description: one where the store is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, store] + missing: neutral Line_volume_weight: description: >- the line's length in each scenario where its carrier is in the row's @@ -1206,6 +1269,7 @@ parameters: line outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, line] + missing: neutral Link_volume_weight: description: >- the link's length in each scenario where its carrier is in the row's @@ -1213,6 +1277,7 @@ parameters: link outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, link] + missing: neutral Line_expansion_cost_weight: description: >- the line's capital cost where its carrier is in the row's set, times @@ -1223,6 +1288,7 @@ parameters: `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, line] + missing: neutral Link_expansion_cost_weight: description: >- the link's capital cost where its carrier is in the row's set, times @@ -1233,42 +1299,49 @@ parameters: `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, link] + missing: neutral Generator_tech_capacity_weight: description: >- one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, generator] + missing: neutral Link_tech_capacity_weight: description: >- one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, link] + missing: neutral Line_tech_capacity_weight: description: >- one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, line] + missing: neutral StorageUnit_tech_capacity_weight: description: >- one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, storage_unit] + missing: neutral Store_tech_capacity_weight: description: >- one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, store] + missing: neutral Process_tech_capacity_weight: description: >- one where the process is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, process] + missing: neutral relations: snapshot_period: @@ -1361,10 +1434,12 @@ relations: 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 Generator_maintenance_cover: description: >- the snapshots a maintenance event covers, by the snapshot it starts in — @@ -1428,7 +1503,7 @@ variables: the variable rather than as rows dims: [scenario, snapshot, storage_unit] where: "StorageUnit_inflow > 0 AND StorageUnit_active" - absence: zero + missing: neutral bounds: lower: 0 upper: StorageUnit_inflow @@ -1477,7 +1552,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -1485,7 +1560,7 @@ variables: description: "`Generator-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral domain: binary Generator_maintenance_capacity: description: >- @@ -1493,7 +1568,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, generator] where: Generator_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Generator_maintenance_status: @@ -1506,7 +1581,7 @@ variables: Generator_maintainable AND Generator_committed AND NOT (Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 Link_n_mod: @@ -1546,7 +1621,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -1554,7 +1629,7 @@ variables: description: "`Link-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral domain: binary Link_maintenance_capacity: description: >- @@ -1562,7 +1637,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, link] where: Link_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Link_maintenance_status: @@ -1572,7 +1647,7 @@ variables: maintenance may also be off dims: [scenario, snapshot, link] where: Link_maintainable AND Link_committed AND NOT (Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 Process_n_mod: @@ -1612,7 +1687,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -1620,7 +1695,7 @@ variables: description: "`Process-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral domain: binary Process_maintenance_capacity: description: >- @@ -1628,7 +1703,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, process] where: Process_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Process_maintenance_status: @@ -1641,7 +1716,7 @@ variables: Process_maintainable AND Process_committed AND NOT (Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 Line_s: @@ -1657,7 +1732,7 @@ variables: the network is lossless dims: [scenario, snapshot, line] where: Line_lossy - absence: zero + missing: neutral bounds: lower: 0 Transformer_s: @@ -1674,7 +1749,7 @@ variables: lossless dims: [scenario, snapshot, transformer] where: Transformer_lossy - absence: zero + missing: neutral bounds: lower: 0 Transformer_phase_shift: @@ -1685,7 +1760,7 @@ variables: where the shift is fixed dims: [scenario, snapshot, transformer] where: Transformer_phase_shift_varying AND Transformer_active - absence: zero + missing: neutral bounds: lower: Transformer_phase_shift_min upper: Transformer_phase_shift_max @@ -1763,19 +1838,19 @@ variables: description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not" dims: [scenario] where: CVaR_omega - absence: zero + missing: neutral bounds: lower: 0 CVaR_theta: description: "`CVaR-theta` — where the tail starts, the value at risk" dims: [] where: CVaR_omega - absence: zero + missing: neutral CVaR: description: "`CVaR` — the tail's average cost, what the objective prices at `omega`" dims: [] where: CVaR_omega - absence: zero + missing: neutral expressions: Generator_previous_status: diff --git a/examples/pypsa/carrier.yaml b/examples/pypsa/carrier.yaml index f1bc04297..4558b09a8 100644 --- a/examples/pypsa/carrier.yaml +++ b/examples/pypsa/carrier.yaml @@ -19,6 +19,7 @@ parameters: `multi_investment_periods` (`global_constraints.py:220-221`), so data prep feeds no value otherwise dims: [carrier] + missing: neutral Carrier_max_relative_growth: description: >- share of the previous period's additions that may be added on top — the diff --git a/examples/pypsa/cost.yaml b/examples/pypsa/cost.yaml index 081e138f5..90de34271 100644 --- a/examples/pypsa/cost.yaml +++ b/examples/pypsa/cost.yaml @@ -8,27 +8,28 @@ dimensions: parameters: CVaR_alpha: - description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability + description: PyPSA's `risk_preference['alpha']` — the confidence level; the tail holds the other `1 - alpha` of the probability. Data prep writes a row where it writes `CVaR_omega` dims: [] + missing: neutral variables: CVaR_a: description: "`CVaR-a` — how far a scenario's operating cost exceeds the tail's start; nothing where it does not" dims: [scenario] where: CVaR_omega - absence: zero + missing: neutral bounds: lower: 0 CVaR_theta: description: "`CVaR-theta` — where the tail starts, the value at risk" dims: [] where: CVaR_omega - absence: zero + missing: neutral CVaR: description: "`CVaR` — the tail's average cost, what the objective prices at `omega`" dims: [] where: CVaR_omega - absence: zero + missing: neutral given: parameters: diff --git a/examples/pypsa/generator.yaml b/examples/pypsa/generator.yaml index ea4e97a01..f4be3bb2b 100644 --- a/examples/pypsa/generator.yaml +++ b/examples/pypsa/generator.yaml @@ -65,6 +65,7 @@ parameters: Generator_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [generator] + missing: neutral Generator_modules_installed: description: >- how many whole modules a committable build has in place: `Generator_p_nom @@ -93,6 +94,7 @@ parameters: Generator_p_set: description: a given output schedule; a generator without one has no row here dims: [scenario, snapshot, generator] + missing: neutral Generator_p_nom_min: description: least nominal power an extendable generator may be built at dims: [scenario, generator] @@ -110,6 +112,7 @@ parameters: Generator_p_nom_set: description: a given nominal power for an extendable generator; one without a value has no row here dims: [generator] + missing: neutral Generator_e_sum_min: description: least energy over the horizon; minus infinity where no floor is meant dims: [scenario, generator] @@ -122,15 +125,18 @@ parameters: `co2_emissions` over the generator's efficiency at the snapshot, data prep; a generator of an unweighted carrier has no row dims: [scenario, global_constraint, snapshot, generator] + missing: neutral Generator_operational_limit_weight: description: one where the generator is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, generator] + missing: neutral Generator_tech_capacity_weight: description: >- one where the generator is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, generator] + missing: neutral variables: Generator_p: diff --git a/examples/pypsa/generator_commitment.yaml b/examples/pypsa/generator_commitment.yaml index f325c3249..d944d1778 100644 --- a/examples/pypsa/generator_commitment.yaml +++ b/examples/pypsa/generator_commitment.yaml @@ -73,6 +73,7 @@ parameters: (`components.py:1050-1121`). Below the output a solve wants, it caps that output; data prep dims: [scenario, generator] + missing: neutral variables: Generator_status: diff --git a/examples/pypsa/generator_maintenance.yaml b/examples/pypsa/generator_maintenance.yaml index c42dd07df..e6c8f7218 100644 --- a/examples/pypsa/generator_maintenance.yaml +++ b/examples/pypsa/generator_maintenance.yaml @@ -44,6 +44,7 @@ parameters: `Generator_maintenance_cover` and `Generator_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, generator] + missing: neutral Generator_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -60,7 +61,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -68,7 +69,7 @@ variables: description: "`Generator-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, generator] where: Generator_maintainable AND Generator_active - absence: zero + missing: neutral domain: binary Generator_maintenance_capacity: description: >- @@ -76,7 +77,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, generator] where: Generator_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Generator_maintenance_status: @@ -89,7 +90,7 @@ variables: Generator_maintainable AND Generator_committed AND NOT (Generator_p_nom_extendable AND NOT (Generator_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 diff --git a/examples/pypsa/generator_ramping.yaml b/examples/pypsa/generator_ramping.yaml index 6f021f288..df0122a5d 100644 --- a/examples/pypsa/generator_ramping.yaml +++ b/examples/pypsa/generator_ramping.yaml @@ -26,21 +26,26 @@ parameters: Generator_ramp_limit_up: description: most a generator may raise its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_down: description: most a generator may lower its output between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, generator] + missing: neutral Generator_ramp_limit_start_up: - description: most output in the snapshot a unit starts, per unit of nominal power + description: most output in the snapshot a unit starts, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_ramp_limit_shut_down: - description: most output in the snapshot before a unit stops, per unit of nominal power + description: most output in the snapshot before a unit stops, per unit of nominal power; no value means no limit dims: [scenario, generator] + missing: neutral Generator_p_init: description: >- the output a unit brought into the horizon — PyPSA's `p_init`, read only where the unit came in running; no value means it is unknown, so the unit carries no ramp row at the first snapshot dims: [scenario, generator] + missing: neutral given: parameters: diff --git a/examples/pypsa/line.yaml b/examples/pypsa/line.yaml index 66c5138a7..15febfc3b 100644 --- a/examples/pypsa/line.yaml +++ b/examples/pypsa/line.yaml @@ -75,6 +75,7 @@ parameters: Line_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [line] + missing: neutral Line_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, line] @@ -95,9 +96,11 @@ parameters: Line_s_nom_set: description: a given nominal apparent power for an extendable line; one without a value has no row here dims: [line] + missing: neutral Line_s_set: description: a given flow schedule; a line without one has no row here dims: [scenario, snapshot, line] + missing: neutral Line_v_ang_max: description: >- the most the voltage angle difference across a line may be either way, @@ -106,6 +109,7 @@ parameters: ignored, as PyPSA ignores it with a `DeprecationWarning` (`constraints.py:1702-1709`) dims: [scenario, line] + missing: neutral Line_x_pu_eff: description: >- the line's effective series reactance — PyPSA's `x_pu_eff`, `x` over @@ -117,6 +121,7 @@ parameters: the cycle basis, data prep; a line in no cycle has no row. PyPSA builds the cycle basis from the first scenario only (`networks.py:1430-1437`) dims: [period, line, cycle] + missing: neutral Line_loss_max: description: the loss at a line's rating — PyPSA's `r_pu_eff * (s_max_pu * s_nom_max)**2`, data prep dims: [scenario, snapshot, line] @@ -138,6 +143,7 @@ parameters: line outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, line] + missing: neutral Line_expansion_cost_weight: description: >- the line's capital cost where its carrier is in the row's set, times @@ -148,12 +154,14 @@ parameters: `Line_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, line] + missing: neutral Line_tech_capacity_weight: description: >- one where the line is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, line] + missing: neutral variables: Line_s: @@ -169,7 +177,7 @@ variables: the network is lossless dims: [scenario, snapshot, line] where: Line_lossy - absence: zero + missing: neutral bounds: lower: 0 Line_s_nom_ext: diff --git a/examples/pypsa/link.yaml b/examples/pypsa/link.yaml index 06591cb21..b945d3f35 100644 --- a/examples/pypsa/link.yaml +++ b/examples/pypsa/link.yaml @@ -99,6 +99,7 @@ parameters: Link_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [link] + missing: neutral Link_modules_installed: description: >- how many whole modules a committable build has in place: `Link_p_nom @@ -127,6 +128,7 @@ parameters: Link_p_set: description: a given flow schedule; a link without one has no row here dims: [scenario, snapshot, link] + missing: neutral Link_p_nom_min: description: least nominal power an extendable link may be built at dims: [scenario, link] @@ -144,6 +146,7 @@ parameters: Link_p_nom_set: description: a given nominal power for an extendable link; one without a value has no row here dims: [link] + missing: neutral Link_volume_weight: description: >- the link's length in each scenario where its carrier is in the row's @@ -151,6 +154,7 @@ parameters: link outside it, or one that does not stand in the row's `investment_period`, has no row dims: [scenario, global_constraint, link] + missing: neutral Link_expansion_cost_weight: description: >- the link's capital cost where its carrier is in the row's set, times @@ -161,12 +165,14 @@ parameters: `Link_capital_cost` without `fom_cost` (`components.py:1151-1169`, `global_constraints.py:870-879`) dims: [scenario, global_constraint, link] + missing: neutral Link_tech_capacity_weight: description: >- one where the link is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, link] + missing: neutral variables: Link_p: diff --git a/examples/pypsa/link_commitment.yaml b/examples/pypsa/link_commitment.yaml index 3583a238c..1977359f6 100644 --- a/examples/pypsa/link_commitment.yaml +++ b/examples/pypsa/link_commitment.yaml @@ -73,6 +73,7 @@ parameters: (`components.py:1050-1121`). Below the flow a solve wants, it caps that flow; data prep dims: [scenario, link] + missing: neutral variables: Link_status: diff --git a/examples/pypsa/link_maintenance.yaml b/examples/pypsa/link_maintenance.yaml index 4388395b6..a3503814a 100644 --- a/examples/pypsa/link_maintenance.yaml +++ b/examples/pypsa/link_maintenance.yaml @@ -44,6 +44,7 @@ parameters: `Link_maintenance_cover` and `Link_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, link] + missing: neutral Link_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -60,7 +61,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -68,7 +69,7 @@ variables: description: "`Link-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, link] where: Link_maintainable AND Link_active - absence: zero + missing: neutral domain: binary Link_maintenance_capacity: description: >- @@ -76,7 +77,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, link] where: Link_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Link_maintenance_status: @@ -86,7 +87,7 @@ variables: maintenance may also be off dims: [scenario, snapshot, link] where: Link_maintainable AND Link_committed AND NOT (Link_p_nom_extendable AND NOT (Link_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 diff --git a/examples/pypsa/link_ramping.yaml b/examples/pypsa/link_ramping.yaml index 66db2331a..24b0aeffc 100644 --- a/examples/pypsa/link_ramping.yaml +++ b/examples/pypsa/link_ramping.yaml @@ -26,21 +26,26 @@ parameters: Link_ramp_limit_up: description: most a link may raise its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_ramp_limit_down: description: most a link may lower its flow between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, link] + missing: neutral Link_ramp_limit_start_up: - description: most flow in the snapshot a link starts, per unit of nominal power + description: most flow in the snapshot a link starts, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_ramp_limit_shut_down: - description: most flow in the snapshot before a link stops, per unit of nominal power + description: most flow in the snapshot before a link stops, per unit of nominal power; no value means no limit dims: [scenario, link] + missing: neutral Link_p_init: description: >- the flow a link brought into the horizon — PyPSA's `p_init`, read only where the link came in running; no value means it is unknown, so the link carries no ramp row at the first snapshot dims: [scenario, link] + missing: neutral given: parameters: diff --git a/examples/pypsa/process.yaml b/examples/pypsa/process.yaml index 0317a14a0..6c1361448 100644 --- a/examples/pypsa/process.yaml +++ b/examples/pypsa/process.yaml @@ -96,6 +96,7 @@ parameters: Process_p_set: description: a given internal power schedule; a process without one has no row here dims: [scenario, snapshot, process] + missing: neutral Process_p_nom_min: description: least nominal power an extendable process may be built at dims: [scenario, process] @@ -113,9 +114,11 @@ parameters: Process_p_nom_set: description: a given nominal power for an extendable process; one without a value has no row here dims: [process] + missing: neutral Process_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [process] + missing: neutral Process_modules_installed: description: >- how many whole modules a committable build has in place: `Process_p_nom @@ -147,6 +150,7 @@ parameters: outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, process] + missing: neutral variables: Process_p: diff --git a/examples/pypsa/process_commitment.yaml b/examples/pypsa/process_commitment.yaml index c1751f27b..2319bc585 100644 --- a/examples/pypsa/process_commitment.yaml +++ b/examples/pypsa/process_commitment.yaml @@ -73,6 +73,7 @@ parameters: (`components.py:1050-1121`). Below the internal power a solve wants, it caps that internal power; data prep dims: [scenario, process] + missing: neutral variables: Process_status: diff --git a/examples/pypsa/process_maintenance.yaml b/examples/pypsa/process_maintenance.yaml index 444353e63..0e6b6329b 100644 --- a/examples/pypsa/process_maintenance.yaml +++ b/examples/pypsa/process_maintenance.yaml @@ -44,6 +44,7 @@ parameters: `Process_maintenance_cover` and `Process_maintenance_start_blocked`, and the assumptions hold it to the horizon dims: [scenario, process] + missing: neutral Process_maintenance_start_blocked: description: >- true where no maintenance event may start, because the snapshots it @@ -60,7 +61,7 @@ variables: continuous, and one exactly where an event covers the snapshot dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral bounds: lower: 0 upper: 1 @@ -68,7 +69,7 @@ variables: description: "`Process-maintenance_start` — whether a maintenance event starts in this snapshot" dims: [scenario, snapshot, process] where: Process_maintainable AND Process_active - absence: zero + missing: neutral domain: binary Process_maintenance_capacity: description: >- @@ -76,7 +77,7 @@ variables: otherwise: the product the `maintcap` rows linearize dims: [scenario, snapshot, process] where: Process_maint_ext - absence: zero + missing: neutral bounds: lower: 0 Process_maintenance_status: @@ -89,7 +90,7 @@ variables: Process_maintainable AND Process_committed AND NOT (Process_p_nom_extendable AND NOT (Process_p_nom_mod > 0)) - absence: zero + missing: neutral bounds: lower: 0 diff --git a/examples/pypsa/process_ramping.yaml b/examples/pypsa/process_ramping.yaml index 7fb3d2169..8ddad17f9 100644 --- a/examples/pypsa/process_ramping.yaml +++ b/examples/pypsa/process_ramping.yaml @@ -26,21 +26,26 @@ parameters: Process_ramp_limit_up: description: most a process may raise its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_ramp_limit_down: description: most a process may lower its internal power between snapshots, per unit of nominal power; no value means no limit — read at the later of the two snapshots, so the limit may change over time dims: [scenario, snapshot, process] + missing: neutral Process_ramp_limit_start_up: - description: most internal power in the snapshot a process starts, per unit of nominal power + description: most internal power in the snapshot a process starts, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_ramp_limit_shut_down: - description: most internal power in the snapshot before a process stops, per unit of nominal power + description: most internal power in the snapshot before a process stops, per unit of nominal power; no value means no limit dims: [scenario, process] + missing: neutral Process_p_init: description: >- the internal power a process brought into the horizon — PyPSA's `p_init`, read only where the process came in running; no value means it is unknown, so the process carries no ramp row at the first snapshot dims: [scenario, process] + missing: neutral given: parameters: diff --git a/examples/pypsa/security.yaml b/examples/pypsa/security.yaml index e09c6e1ad..38b8688a4 100644 --- a/examples/pypsa/security.yaml +++ b/examples/pypsa/security.yaml @@ -32,10 +32,12 @@ relations: 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 parameters: Line_BODF: @@ -47,12 +49,14 @@ parameters: the outaged line itself. The same in every scenario: PyPSA takes the factors of the first scenario (`abstract.py:534`) dims: [period, line, outage] + missing: neutral Transformer_BODF: description: >- the share of an outaged branch's flow a transformer takes on when that branch goes out, as a line's; a row only where the transformer and the outage are active in the period and share a sub-network dims: [period, transformer, outage] + missing: neutral given: parameters: diff --git a/examples/pypsa/settings.yaml b/examples/pypsa/settings.yaml index 68cba36ba..f97757a4a 100644 --- a/examples/pypsa/settings.yaml +++ b/examples/pypsa/settings.yaml @@ -34,6 +34,7 @@ parameters: CVaR_omega: description: PyPSA's `risk_preference['omega']` — the share of operating cost priced at the tail rather than in expectation. Data prep writes a row only where a risk preference is set. With none there are no CVaR variables or rows; with `omega = 0` PyPSA builds them, and the optimum is the risk-neutral one dims: [] + missing: neutral period_weight_objective: description: >- PyPSA's `investment_period_weightings.objective` — what a period's cost @@ -74,6 +75,7 @@ parameters: on a row that names a period without it (`global_constraints.py:364-369`) dims: [scenario, global_constraint, snapshot] dtype: bool + missing: neutral given: expressions: diff --git a/examples/pypsa/storage_unit.yaml b/examples/pypsa/storage_unit.yaml index 6199b1983..e59f948f7 100644 --- a/examples/pypsa/storage_unit.yaml +++ b/examples/pypsa/storage_unit.yaml @@ -66,6 +66,7 @@ parameters: StorageUnit_p_nom_set: description: a given nominal power for an extendable storage unit; one without a value has no row here dims: [storage_unit] + missing: neutral StorageUnit_p_nom: description: nominal power dims: [scenario, storage_unit] @@ -76,6 +77,7 @@ parameters: StorageUnit_p_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [storage_unit] + missing: neutral StorageUnit_p_min_pu: description: most storing, per unit of nominal power and negated dims: [scenario, snapshot, storage_unit] @@ -155,27 +157,34 @@ parameters: StorageUnit_p_set: description: a given net dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_dispatch_set: description: a given dispatch schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_p_store_set: description: a given charging schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_state_of_charge_set: description: a given charge schedule; a unit without one has no row here dims: [scenario, snapshot, storage_unit] + missing: neutral StorageUnit_primary_energy_weight: description: the constrained attribute per unit of charge depleted — data prep; an unweighted unit has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral StorageUnit_operational_limit_weight: description: one where the storage unit is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, storage_unit] + missing: neutral StorageUnit_tech_capacity_weight: description: >- one where the storage unit is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, storage_unit] + missing: neutral variables: StorageUnit_p_dispatch: @@ -197,7 +206,7 @@ variables: the variable rather than as rows dims: [scenario, snapshot, storage_unit] where: "StorageUnit_inflow > 0 AND StorageUnit_active" - absence: zero + missing: neutral bounds: lower: 0 upper: StorageUnit_inflow diff --git a/examples/pypsa/store.yaml b/examples/pypsa/store.yaml index 30183ec6f..58c8b28c0 100644 --- a/examples/pypsa/store.yaml +++ b/examples/pypsa/store.yaml @@ -66,6 +66,7 @@ parameters: Store_e_nom_set: description: a given nominal capacity for an extendable store; one without a value has no row here dims: [store] + missing: neutral Store_e_nom: description: nominal energy capacity dims: [scenario, store] @@ -76,6 +77,7 @@ parameters: Store_e_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [store] + missing: neutral Store_e_min_pu: description: least energy held, per unit of nominal capacity — negative for a store that may go short dims: [scenario, snapshot, store] @@ -140,21 +142,26 @@ parameters: Store_e_set: description: a given energy schedule; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Store_p_set: description: a given schedule of power delivered; a store without one has no row here dims: [scenario, snapshot, store] + missing: neutral Store_primary_energy_weight: description: the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row dims: [scenario, global_constraint, store] + missing: neutral Store_operational_limit_weight: description: one where the store is in the row's set — data prep; one outside it has no row dims: [scenario, global_constraint, store] + missing: neutral Store_tech_capacity_weight: description: >- one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's `investment_period`, has no row dims: [global_constraint, store] + missing: neutral variables: Store_e: diff --git a/examples/pypsa/transformer.yaml b/examples/pypsa/transformer.yaml index 88f8b3c04..7d42b080c 100644 --- a/examples/pypsa/transformer.yaml +++ b/examples/pypsa/transformer.yaml @@ -62,6 +62,7 @@ parameters: Transformer_s_nom_mod: description: the module size a build comes in whole numbers of; no value means the build is continuous dims: [transformer] + missing: neutral Transformer_s_max_pu: description: most flow either way, per unit of nominal apparent power dims: [scenario, snapshot, transformer] @@ -82,9 +83,11 @@ parameters: Transformer_s_nom_set: description: a given nominal apparent power for an extendable transformer; one without a value has no row here dims: [transformer] + missing: neutral Transformer_s_set: description: a given flow schedule; a transformer without one has no row here dims: [scenario, snapshot, transformer] + missing: neutral Transformer_v_ang_max: description: >- the most the voltage angle difference across a transformer, its phase @@ -92,6 +95,7 @@ parameters: infinite, and so no row, by default. The deprecated `v_ang_min` is ignored, as a line's dims: [scenario, transformer] + missing: neutral Transformer_x_pu_eff: description: >- the transformer's effective series reactance — PyPSA's `x_pu_eff`, `x` @@ -104,6 +108,7 @@ parameters: basis, data prep; a transformer in no cycle has no row. From the first scenario only, as a line's dims: [period, transformer, cycle] + missing: neutral Transformer_phase_shift_weight: description: >- a fixed transformer's phase shift in radians at each snapshot, signed by @@ -112,6 +117,7 @@ parameters: the variable term never both count a shift. A transformer with no shift or in no cycle of its snapshot's period has no row dims: [snapshot, transformer, cycle] + missing: neutral Transformer_phase_shift_varying: description: >- whether a transformer's phase shift is a decision — PyPSA's @@ -177,7 +183,7 @@ variables: lossless dims: [scenario, snapshot, transformer] where: Transformer_lossy - absence: zero + missing: neutral bounds: lower: 0 Transformer_phase_shift: @@ -188,7 +194,7 @@ variables: where the shift is fixed dims: [scenario, snapshot, transformer] where: Transformer_phase_shift_varying AND Transformer_active - absence: zero + missing: neutral bounds: lower: Transformer_phase_shift_min upper: Transformer_phase_shift_max diff --git a/schema/mathspec.schema.json b/schema/mathspec.schema.json index 35de691f7..7343eafce 100644 --- a/schema/mathspec.schema.json +++ b/schema/mathspec.schema.json @@ -427,7 +427,7 @@ }, "GivenParameterBlock": { "additionalProperties": false, - "description": "Data this file reads and another file declares.\n\nIt says what a [`ParameterBlock`][] says, because the frame and the\ndtype are all a parameter declaration holds: a where compares against the\ndtype, and the dim rules read the frame.", + "description": "Data this file reads and another file declares.\n\nIt says the frame and the dtype of a [`ParameterBlock`][], which are what\nthis file reads: a where compares against the dtype, and the dim rules read\nthe frame. What a missing row means is the declaring file's ``missing:``.", "properties": { "description": { "anyOf": [ @@ -621,7 +621,7 @@ }, "ParameterBlock": { "additionalProperties": false, - "description": "A declared parameter with dims and dtype.", + "description": "A declared parameter with dims and dtype, and what a missing row of its data means.", "properties": { "description": { "anyOf": [ @@ -652,6 +652,31 @@ ], "title": "Dtype", "type": "string" + }, + "missing": { + "anyOf": [ + { + "enum": [ + "refused", + "absent", + "neutral" + ] + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "enum": [ + "inf", + "-inf" + ] + } + ], + "default": "refused", + "title": "Missing" } }, "required": [ @@ -808,6 +833,14 @@ ], "title": "Key" }, + "missing": { + "default": "refused", + "enum": [ + "refused", + "absent" + ], + "title": "Missing" + }, "values": { "anyOf": [ { @@ -884,15 +917,6 @@ "additionalProperties": false, "description": "A declared decision variable.", "properties": { - "absence": { - "default": "undefined", - "enum": [ - "undefined", - "zero" - ], - "title": "Absence", - "type": "string" - }, "bounds": { "$ref": "#/$defs/BoundsBlock", "default": { @@ -929,6 +953,15 @@ "title": "Domain", "type": "string" }, + "missing": { + "default": "absent", + "enum": [ + "absent", + "neutral" + ], + "title": "Missing", + "type": "string" + }, "where": { "anyOf": [ { diff --git a/src/mathspec/_where_resolver.py b/src/mathspec/_where_resolver.py index 8fb2f08ad..bf2366e1d 100644 --- a/src/mathspec/_where_resolver.py +++ b/src/mathspec/_where_resolver.py @@ -151,6 +151,19 @@ def _where_name(self, node: NameNode) -> Predicate | UnresolvedWhereNode: return node match kind: case 'parameter': + declared = ns.schema.parameters.get(node.name) + if declared is not None and declared.missing == 'refused' and declared.dtype in ('int', 'str'): + rewrite = ( + f'Declare `missing: absent`, or compare the label: where: "{node.name} == \'...\'".' + if declared.dtype == 'str' + else f'Declare `missing: neutral` or `absent`, or compare the value: where: "{node.name} > 0".' + ) + self.errors.append( + f"{context}: '{node.name}' is declared dtype: {declared.dtype} and missing: refused, so it " + f'has a row at every coordinate and the bare name is true at every coordinate — the mask ' + f'has no effect. {rewrite}' + ) + return node return ParameterDefined(node.name, ns.leaf_dims[node.name]) case 'dimension': self.errors.append( diff --git a/src/mathspec/composition.py b/src/mathspec/composition.py index d69680684..3e5292337 100644 --- a/src/mathspec/composition.py +++ b/src/mathspec/composition.py @@ -27,6 +27,9 @@ 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. + A relation's ``missing:`` 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 declaration is owned.** A name two fragments declare is refused, both named. * **One fragment sets the objective.** A second one is refused, both named. @@ -80,6 +83,8 @@ ``relations`` entry 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. + A relation's ``missing:`` 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.** A declaration set to ``null`` is removed, and a removal of what the base does not declare is refused. A field set to ``null`` is dropped, and takes its default when the result loads: @@ -842,7 +847,9 @@ def _shared(declared: dict[str, object], patch: dict[str, object], section: str, reading of the coordinate space as one that names another value, and a field written at its default is no change. A dimension's ``ordered`` folds by [`_joined`][], so a patch may add the claim; writing it false over a - base that makes it is the one narrowing [`_joined`][] cannot see. + base that makes it is the one narrowing [`_joined`][] cannot see. A + relation's ``missing:`` is laid over as a field and left out of the + comparison, being a claim about the data. """ out = dict(declared) singular = _singular(section) @@ -856,6 +863,11 @@ def _shared(declared: dict[str, object], patch: dict[str, object], section: str, if key not in out: out[key] = block continue + if section == 'relations' and 'missing' in _mapping(block): + out[key] = _reread(out[key], _mapping(block)) + block = _without(block, 'missing') + if not block: + continue base, laid = _declared(section, out[key]), _declared(section, block) if base.get('ordered') and _mapping(block).get('ordered') is False: raise LanguageError( @@ -863,7 +875,7 @@ def _shared(declared: dict[str, object], patch: dict[str, object], section: str, f'ordered. A construct in the base may step along it, and a patch adds the claim of order ' f'but never withdraws it: leave `ordered` out of the patch.' ) - if (joined := _joined(section, base, laid, lambda written: written)) is None: + if (joined := _joined(section, base, laid, lambda written: _without(written, 'missing'))) is None: raise LanguageError( f"patch '{name}' declares the {singular} '{key}' as {block!r}, where its base " f'declares {out[key]!r}. A patch adjusts the math, not the coordinate space the math is ' @@ -874,6 +886,12 @@ def _shared(declared: dict[str, object], patch: dict[str, object], section: str, return out +def _reread(declared: object, patch: dict[str, object]) -> dict[str, object]: + """*declared* reading a missing key as *patch* says, where ``null`` puts back the default.""" + kept = _mapping(_without(declared, 'missing')) + return kept if patch['missing'] is None else {**kept, 'missing': patch['missing']} + + def _owned( declared: dict[str, object], patch: dict[str, object], label: str, cls: type[BaseModel], name: str ) -> dict[str, object]: diff --git a/src/mathspec/exclusivity.py b/src/mathspec/exclusivity.py index 893b0cf54..3de84a08a 100644 --- a/src/mathspec/exclusivity.py +++ b/src/mathspec/exclusivity.py @@ -63,7 +63,11 @@ class Undecidable(Exception): # noqa: N818 """A pair this procedure will not reason about. Carries the rewrite.""" -def overlapping(cases: Mapping[str, Predicate], dtypes: Mapping[str, DeclaredDtype]) -> Iterator[str]: +def overlapping( + cases: Mapping[str, Predicate], + dtypes: Mapping[str, DeclaredDtype], + defaults: Mapping[str, bool | float], +) -> Iterator[str]: """One refusal per pair of cases that could both claim a coordinate. Args: @@ -71,6 +75,8 @@ def overlapping(cases: Mapping[str, Predicate], dtypes: Mapping[str, DeclaredDty block's ``otherwise`` is not among them: it claims what the rest leave, so it overlaps nothing by construction. dtypes: The declared dtype of every name a mask compares against. + defaults: The value a missing row reads as, for every parameter whose + ``missing:`` names one. Yields: A sentence per pair, naming both cases and either a coordinate they @@ -79,7 +85,7 @@ def overlapping(cases: Mapping[str, Predicate], dtypes: Mapping[str, DeclaredDty """ for (first, left), (second, right) in itertools.combinations(cases.items(), 2): try: - witness = _witness(left, right, dtypes) + witness = _witness(left, right, dtypes, defaults) except Undecidable as exc: yield ( f"cases '{first}' and '{second}' cannot be told apart before the data arrives: {exc}. " @@ -96,10 +102,15 @@ def overlapping(cases: Mapping[str, Predicate], dtypes: Mapping[str, DeclaredDty ) -def _witness(first: Predicate, second: Predicate, dtypes: Mapping[str, DeclaredDtype]) -> str | None: +def _witness( + first: Predicate, + second: Predicate, + dtypes: Mapping[str, DeclaredDtype], + defaults: Mapping[str, bool | float], +) -> str | None: """A coordinate both masks claim, rendered — ``None`` where no cell holds both.""" masks = (Mask(first), Mask(second)) - grid = _Grid.of(masks, dtypes) + grid = _Grid.of(masks, dtypes, defaults) if grid.size > CELL_BUDGET: msg = ( f'{grid.size} regions to check exceeds the budget of {CELL_BUDGET} — ' @@ -172,9 +183,12 @@ class _Grid: domains: dict[Subject, list[Cell]] subjects: dict[int, Subject] + defaults: Mapping[str, bool | float] @classmethod - def of(cls, masks: Iterable[Mask], dtypes: Mapping[str, DeclaredDtype]) -> _Grid: + def of( + cls, masks: Iterable[Mask], dtypes: Mapping[str, DeclaredDtype], defaults: Mapping[str, bool | float] + ) -> _Grid: values: dict[Subject, set[_Literal]] = {} subjects: dict[int, Subject] = {} for mask in masks: @@ -182,7 +196,7 @@ def of(cls, masks: Iterable[Mask], dtypes: Mapping[str, DeclaredDtype]) -> _Grid subject = _subject_of(node) subjects[id(node)] = subject _observe(node, subject, values.setdefault(subject, set()), dtypes) - return cls({s: _cells_for(s, seen, dtypes) for s, seen in values.items()}, subjects) + return cls({s: _cells_for(s, seen, dtypes) for s, seen in values.items()}, subjects, defaults) @property def size(self) -> int: @@ -193,7 +207,7 @@ def cells(self) -> Iterator[dict[Subject, Cell]]: yield dict(zip(self.domains, combination, strict=True)) def witness(self, cell: dict[Subject, Cell]) -> str: - return ', '.join(f'{subject} is {_shown(subject, value)}' for subject, value in cell.items()) + return ', '.join(_clause(subject, value) for subject, value in cell.items()) #: A comparator against its mirror, for a test written with its sides swapped. @@ -457,13 +471,20 @@ def _rank_cells(subject: Subject, positions_seen: set[int]) -> list[Cell]: return cells +def _clause(subject: Subject, value: Cell) -> str: + """*subject* in one cell, in words. A null cell says the data has no row, never a ``missing:`` reading's name.""" + if value is Special.NULL: + return f'{subject} has no value' if subject.kind == 'expression' else f'{subject} has no row' + return f'{subject} is {_shown(subject, value)}' + + def _shown(subject: Subject, value: Cell) -> str: if subject.kind == 'rank': return str(value) if subject.kind == 'relation_pair': return 'equal' if value else 'different' if isinstance(value, Special): - return {Special.NULL: 'absent', Special.OTHER: 'anything else'}.get(value, value.value) + return 'anything else' if value is Special.OTHER else value.value if isinstance(value, bool): return 'true' if value else 'false' return f'{value!r}' @@ -495,7 +516,7 @@ def _evaluate(node: Predicate, cell: dict[Subject, Cell], grid: _Grid) -> bool: def _atom(node: TypedPredicate, cell: dict[Subject, Cell], grid: _Grid) -> bool: subject = grid.subjects[id(node)] - value = cell[subject] + value = _read(node, cell[subject], grid) match node: case ParameterDefined() | RelationDefined(): if isinstance(value, bool): @@ -526,6 +547,20 @@ def _atom(node: TypedPredicate, cell: dict[Subject, Cell], grid: _Grid) -> bool: assert_never(node) +def _read(node: TypedPredicate, value: Cell, grid: _Grid) -> Cell: + """What *node* reads in a cell where its parameter has no row: in a comparison, the value ``missing:`` names. + + A bare name keeps the null. A numeric one asks whether the data has a row. + A ``bool`` one reads that value there, and the cell holding that value is + already in the grid, so the null cell adds no witness either way. + """ + if value is Special.NULL and isinstance(node, ParameterComparison): + default = grid.defaults.get(node.name) + if default is not None: + return default + return value + + def _compare(value: Cell, op: PredicateOperator, literal: _Literal) -> bool: """One atom's truth in one cell. Both sides are already this cell's frame. diff --git a/src/mathspec/lowering.py b/src/mathspec/lowering.py index 173decc57..fa1ca962c 100644 --- a/src/mathspec/lowering.py +++ b/src/mathspec/lowering.py @@ -20,7 +20,7 @@ from mathspec.dimensions import check_schema, dims_of from mathspec.errors import SchemaError, did_you_mean, prefixed from mathspec.expansion import expand, parse_template -from mathspec.piecewise import assumptions_of, curve_frame, lp_domain_refusal, resolve_links +from mathspec.piecewise import assumptions_of, curve_frame, lp_domain_refusal, refused_under_points, resolve_links from mathspec.program import ( Assumption, BooleanLiteral, @@ -154,7 +154,7 @@ def lower(schema: Spec) -> Program: lower=lower_bound, upper=upper_bound, domain=vdef.domain, - absence=vdef.absence, + missing=vdef.missing, description=vdef.description, ) @@ -217,7 +217,7 @@ def lower(schema: Spec) -> Program: program = Program( parameters={ - name: ParameterDeclaration(tuple(pdef.dims), pdef.dtype, pdef.description) + name: ParameterDeclaration(tuple(pdef.dims), pdef.dtype, pdef.missing, pdef.description) for name, pdef in schema.parameters.items() }, variables=variables, @@ -247,7 +247,7 @@ def lower(schema: Spec) -> Program: masks=masks, given=GivenTargets( parameters={ - name: ParameterDeclaration(tuple(g.dims), g.dtype, g.description) + name: ParameterDeclaration(tuple(g.dims), g.dtype, None, g.description) for name, g in schema.given.parameters.items() }, variables={ @@ -263,7 +263,7 @@ def lower(schema: Spec) -> Program: ), description=schema.description, ) - if errors := emitted_name_errors(schema, program): + if errors := [*emitted_name_errors(schema, program), *refused_under_points(schema)]: raise SchemaError('\n'.join(errors)) check_schema(schema, program) return program diff --git a/src/mathspec/piecewise.py b/src/mathspec/piecewise.py index e4dafb83f..d9de03f43 100644 --- a/src/mathspec/piecewise.py +++ b/src/mathspec/piecewise.py @@ -26,7 +26,7 @@ from mathspec.spec import AssumptionBlock, Curvature, PiecewiseBlock, Spec, VariableBlock if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Iterable, Iterator from mathspec.program import Expression from mathspec.resolution import Namespace @@ -95,9 +95,10 @@ def lp_domain_refusal(name: str, pw: PiecewiseBlock, links: tuple[Expression, .. def assumptions_of(block: str, pw: PiecewiseDeclaration) -> dict[str, AssumptionBlock]: """What *block* assumes of its numbers, by the name the document prints and a refusal quotes. - Every curve assumes its breakpoints are there: a missing parameter row is - not absence, it is a zero, so an undeclared breakpoint sits the curve on - the origin rather than shortening it. A curve has an x-axis only where two + Every curve assumes its breakpoints are there, because a missing row does + not shorten a curve. Under ``points:`` its tables are not ``refused``, so + a breakpoint missing inside the mask would read as their ``missing:`` and, + under ``neutral``, sit the curve on the origin. A curve has an x-axis only where two links tie it, so the increasing condition — and the shape it is checked with — exist only there; ``lp`` alone needs a segment to state a line for; a mask must be one run. @@ -115,8 +116,7 @@ def assumptions_of(block: str, pw: PiecewiseDeclaration) -> dict[str, Assumption holds=' AND '.join(dict.fromkeys(link.values for link in pw.links)), where=mask, description=f"piecewise '{block}': every breakpoint the curve runs through needs a row in " - f'{_quoted(link.values for link in pw.links)} — a missing row is read as a zero rather than as a ' - f'shorter curve, so it sits the curve on the origin. ' + f'{_quoted(link.values for link in pw.links)} — a missing row does not shorten the curve. ' + ( f"Attach the rows, or narrow points: '{mask}' to where the curve runs." if mask is not None @@ -302,9 +302,9 @@ def leaves_ungated(gate: VariableBlock | VariableDeclaration | None) -> bool: """Whether a curve gated by *gate* runs ungated where the gate does not exist, which takes a second convexity row. A masked gate is absent off its mask, and there the curve sums to 1; - ``absence: zero`` reads the gate as 0 there instead, which one row states. + ``missing: neutral`` reads the gate as 0 there instead, which one row states. """ - return gate is not None and gate.where is not None and gate.absence != 'zero' + return gate is not None and gate.where is not None and gate.missing != 'neutral' def curve_frame(schema: Spec, name: str, pw: PiecewiseBlock, links: Iterable[Expression]) -> tuple[str, ...]: @@ -441,7 +441,7 @@ def _gate_rows(self) -> tuple[tuple[str, str | None, str], ...]: reduction, so the right-hand side would take the row with it and leave the weights without the convexity that makes them a curve at all (#1158). - ``absence: zero`` is the other reading and stays one row — the gate is 0 + ``missing: neutral`` is the other reading and stays one row — the gate is 0 where it does not exist, so the curve is pinned off there. """ activity = self.pw.activity @@ -481,6 +481,26 @@ def _segment_lines(self) -> None: ) +def refused_under_points(schema: Spec) -> Iterator[str]: + """A refusal for each values parameter of a curve with ``points:`` that reads a missing row as ``refused``. + + ``points:`` says the curve stops short of the dimension, so its tables + have no row past the mask by design. ``refused`` says every row is + there: the curve never runs short, and a mask that names the table marks + every breakpoint. + """ + for block, pw in schema.piecewise.items(): + if pw.points is None: + continue + for name in dict.fromkeys(link.values for link in pw.links): + if schema.parameters[name].missing == 'refused': + yield ( + f"parameter '{name}' is refused where a row is missing, and piecewise '{block}' reads it under " + f"points: '{pw.points}', which stops the curve where its rows stop. Declare missing: neutral, " + f'absent, or a value of its dtype.' + ) + + def expand_piecewise(schema: Spec) -> Spec: """*schema* with every ``piecewise:`` block written out — *schema* itself where it declares none. diff --git a/src/mathspec/program.py b/src/mathspec/program.py index 60d9d7b73..65c760b3d 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -67,6 +67,8 @@ 'Link', 'Mask', 'MaskDeclaration', + 'Missing', + 'MissingReading', 'Multiply', 'NamedExpression', 'NamedMask', @@ -93,6 +95,7 @@ 'RelationComparison', 'RelationDeclaration', 'RelationDefined', + 'RelationMissing', 'RelationPairComparison', 'Separability', 'SosDeclaration', @@ -102,10 +105,10 @@ 'TranslatedPredicate', 'TypedPredicate', 'Variable', - 'VariableAbsence', 'VariableDeclaration', 'VariableDefined', 'VariableDomain', + 'VariableMissing', 'WindowSum', 'assumption_message', 'carries_variable', @@ -146,11 +149,21 @@ #: The domain a variable may declare. VariableDomain = Literal['continuous', 'integer', 'binary'] -#: What a masked variable's non-existence *means* where it does not exist. -#: ``undefined`` is the absence rules' default — a term carrying it takes its -#: row. ``zero`` says the quantity *is* zero there, so the term contributes -#: nothing and the row stands. -VariableAbsence = Literal['undefined', 'zero'] +#: What a missing row means. ``refused`` refuses the data, ``absent`` takes the +#: row of a term that reads it, and ``neutral`` reads the value that +#: contributes nothing: ``0`` as a coefficient, ``false`` in a ``where``. +MissingReading = Literal['refused', 'absent', 'neutral'] + +#: A parameter's ``missing:``: a reading, or the value a missing row reads as. +Missing = MissingReading | bool | float + +#: A relation's ``missing:``. A label the map leaves out is refused, or belongs to no group. +RelationMissing = Literal['refused', 'absent'] + +#: A variable's ``missing:``: what a coordinate its ``where`` masks out means. +#: ``absent`` takes the row of a term that reads it; ``neutral`` says the +#: quantity *is* zero there, so the term contributes nothing and the row stands. +VariableMissing = Literal['absent', 'neutral'] #: Which way an objective is optimised (the declaration rules). ObjectiveSense = Literal['minimize', 'maximize'] @@ -481,6 +494,9 @@ class RelationDeclaration: columns: tuple[tuple[str, str], ...] key: tuple[str, ...] + #: What a key the map leaves out means, or ``None`` for a bare relation, + #: whose rows are its membership and so have no gap. + missing: RelationMissing | None = 'refused' description: str | None = None @property @@ -678,6 +694,10 @@ class ParameterDeclaration: dims: tuple[str, ...] dtype: ParameterDtype = 'float' + #: What a missing row means, or the value it reads as wherever a value is + #: read; ``None`` for a given parameter, whose declaring file says. A bare + #: numeric name in a ``where`` still asks whether the data has a row. + missing: Missing | None = 'refused' description: str | None = None @@ -691,7 +711,7 @@ class VariableDeclaration: #: As [`lower`][], for the other side. upper: Expression | None = None domain: VariableDomain = 'continuous' - absence: VariableAbsence = 'undefined' + missing: VariableMissing = 'absent' description: str | None = None diff --git a/src/mathspec/resolution.py b/src/mathspec/resolution.py index affcc977c..d044e43f5 100644 --- a/src/mathspec/resolution.py +++ b/src/mathspec/resolution.py @@ -76,6 +76,7 @@ class Namespace: '_named', 'bodies', 'constraints', + 'defaults', 'dimensions', 'dtypes', 'given_masks', @@ -114,9 +115,15 @@ def __init__(self, schema: Spec) -> None: **{m: cast('DeclaredDtype', 'bool') for m in schema.given.masks}, **{d: dd.dtype for d, dd in schema.dimensions.items()}, } + #: parameter name -> the value its ``missing:`` reads a missing row as, + #: for the parameters that name one; what a where comparison reads there. + self.defaults: dict[str, bool | float] = { + p: pd.missing for p, pd in schema.parameters.items() if not isinstance(pd.missing, str) + } #: relation name -> its columns and key, as declared. self.relations: dict[str, RelationDeclaration] = { - n: RelationDeclaration(lk.pairs, lk.key_roles, lk.description) for n, lk in schema.relations.items() + n: RelationDeclaration(lk.pairs, lk.key_roles, lk.missing, lk.description) + for n, lk in schema.relations.items() } #: parameter or variable name -> the dims it is read through — #: parameters by their ``dims``, variables by their frame. Stamped onto @@ -507,7 +514,7 @@ def _named(name: str, block: ExpressionBlock, ns: Namespace, errors: list[str]) fallback = resolve_expression_text(block.otherwise, ns, case_context(name, None), errors, ceiling=None) if len(errors) > found or fallback is None: return None - errors.extend(f'{context}: {problem}' for problem in overlapping(masks, ns.dtypes)) + errors.extend(f'{context}: {problem}' for problem in overlapping(masks, ns.dtypes, ns.defaults)) if len(errors) > found: return None left_over = Region(remainder(region.when for region in regions), fallback) diff --git a/src/mathspec/spec.py b/src/mathspec/spec.py index 098e7d996..d27d5a85d 100644 --- a/src/mathspec/spec.py +++ b/src/mathspec/spec.py @@ -22,6 +22,7 @@ ValidationError, ValidationInfo, ValidatorFunctionWrapHandler, + WithJsonSchema, field_validator, model_serializer, model_validator, @@ -31,13 +32,15 @@ from mathspec.errors import did_you_mean, schema_error from mathspec.program import ( DimensionDtype, + MissingReading, ObjectiveSense, ParameterDtype, PiecewiseMethod, Program, + RelationMissing, SosType, - VariableAbsence, VariableDomain, + VariableMissing, ) if TYPE_CHECKING: @@ -97,6 +100,38 @@ def _reject_unknown_keys(cls, data: object) -> object: #: term, a divisor, a bound. A label selects and a flag masks; neither is one. NUMERIC_DTYPES: frozenset[ParameterDtype] = frozenset({'float', 'int'}) +#: What a parameter's ``missing:`` is written as. The validator takes ``inf`` +#: and ``-inf`` as the numbers they name, which an editor would otherwise flag. +_MISSING_SCHEMA: dict[str, object] = { + 'anyOf': [ + {'enum': list(get_args(MissingReading))}, + {'type': 'boolean'}, + {'type': 'number'}, + {'enum': ['inf', '-inf']}, + ], +} + +#: The spellings of a missing row, for a refusal to list. +_READINGS = ', '.join(get_args(MissingReading)) + + +def _without_refused(written: dict[str, object]) -> dict[str, object]: + """*written* with ``missing: refused`` left out, so the default written out and left out write one text.""" + if written.get('missing') == 'refused': + written.pop('missing') + return written + + +def _as_yaml(v: object) -> str: + """*v* as the file spells it, so a refusal quotes the line the author wrote.""" + return str(v).lower() if isinstance(v, bool) else str(v) + + +def _missing_null(kind: str, rewrite: str) -> ValueError: + """``missing: null`` reads as either the default or an absent row, so it is refused for the word that says which.""" + return ValueError(f'missing: null on {kind} names no reading. {rewrite}') + + #: Every formulation, in the order [`Spec.expand`][] writes them out: a curve #: emits a set, and no set emits a curve. FORMULATIONS: tuple[Formulation, ...] = ('piecewise', 'sos') @@ -162,8 +197,51 @@ class RelationBlock(_StrictBlock): key: str | list[str] | dict[str, str] values: str | list[str] | dict[str, str] | None = None + #: What a key the map leaves out means: ``refused`` where the file writes + #: nothing, and ``None`` for a bare relation, whose rows are its membership. + missing: Annotated[RelationMissing | None, WithJsonSchema({'enum': list(get_args(RelationMissing))})] = Field( + default=None, json_schema_extra={'default': 'refused'} + ) description: str | None = None + @field_validator('missing', mode='before') + @classmethod + def _refused_or_absent(cls, v: object) -> object: + """A label is data, so no value fills a gap in a map, and neutral reads as absent wherever a map is read.""" + if v is None: + raise _missing_null( + 'a relation', 'Write absent for a key the map leaves out, or leave the key out for refused.' + ) + if v not in get_args(RelationMissing): + msg = ( + f'missing: {_as_yaml(v)} on a relation, which takes refused or absent. A label the map leaves out ' + f'is refused, or belongs to no group.' + ) + raise ValueError(msg) + return v + + @model_validator(mode='after') + def _a_bare_relation_has_no_gap(self) -> RelationBlock: + """A bare relation's rows are its membership, so a key it leaves out is not missing. + + A map left unwritten reads ``refused``, held as the value, so the spec + that writes the default and the one that leaves it out are one spec. + """ + if self.missing is not None and self.values is None: + msg = ( + f'missing: {self.missing} on a relation with no `values:`. Its rows are its membership, ' + f'so a key it leaves out is not missing: drop the key.' + ) + raise ValueError(msg) + if self.values is not None and self.missing is None: + self.missing = 'refused' + return self + + @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. @@ -221,14 +299,61 @@ def _as_written(self, handler: SerializerFunctionWrapHandler) -> dict[str, objec class ParameterBlock(_StrictBlock): - """A declared parameter with dims and dtype.""" + """A declared parameter with dims and dtype, and what a missing row of its data means.""" _label: ClassVar[str] = 'a parameter declaration' dims: list[str] dtype: ParameterDtype = 'float' + missing: Annotated[MissingReading | bool | int | float, WithJsonSchema(_MISSING_SCHEMA)] = 'refused' description: str | None = None + @field_validator('missing', mode='before') + @classmethod + def _a_reading_or_a_value(cls, v: object) -> object: + """``inf`` is a string to YAML and a number to the expression grammar, so it is taken as the number.""" + if v is None: + raise _missing_null( + 'a parameter', 'Write absent for a row that is not there, or leave the key out for refused.' + ) + if v in ('inf', '-inf'): + return float(cast('str', v)) + if isinstance(v, str) and v not in get_args(MissingReading): + msg = f'missing is the string {v!r}. It takes {_READINGS}, or a value: a number, true or false.' + raise ValueError(msg) + if isinstance(v, float) and math.isnan(v): + msg = 'missing is nan, and a parameter has no nan value. Write a number, or one of ' + _READINGS + '.' + raise ValueError(msg) + return v + + @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))) + + @model_validator(mode='after') + def _a_value_fits_the_dtype(self) -> ParameterBlock: + """A value fills rows of the column, so it has the column's dtype, and a label has no neutral value.""" + v = self.missing + if self.dtype == 'str' and v not in ('refused', 'absent'): + msg = ( + f'missing: {_as_yaml(v)} on a str parameter, which takes refused or absent. A label has no value to fill, ' + f'and no neutral one.' + ) + raise ValueError(msg) + if isinstance(v, str): + return self + if self.dtype == 'bool' and not isinstance(v, bool): + msg = f'missing: {_as_yaml(v)} on a bool parameter, which takes true or false.' + raise ValueError(msg) + if self.dtype != 'bool' and isinstance(v, bool): + msg = f'missing: {_as_yaml(v)} on a {self.dtype} parameter, which takes a number. Write 1 or 0.' + raise ValueError(msg) + if self.dtype == 'int' and isinstance(v, float): + msg = f'missing: {_as_yaml(v)} on an int parameter, which takes an integer. Write one, or declare dtype: float.' + raise ValueError(msg) + return self + class BoundsBlock(_StrictBlock): """Variable bounds — each side is a finite number, a parameter name, or ``None`` where it is open. @@ -281,16 +406,30 @@ class VariableBlock(_StrictBlock): where: str | None = None bounds: BoundsBlock = BoundsBlock() domain: VariableDomain = 'continuous' - absence: VariableAbsence = 'undefined' + missing: VariableMissing = 'absent' description: str | None = None + @field_validator('missing', mode='before') + @classmethod + def _absent_or_neutral(cls, v: object) -> object: + """A variable has no data, so the readings of data have nothing to read.""" + if v is None: + raise _missing_null('a variable', 'Write neutral, or leave the key out for absent.') + if v not in get_args(VariableMissing): + msg = ( + f'missing: {_as_yaml(v)} on a variable, which takes absent or neutral. A variable has no data: ' + f'refused and a value are readings of a parameter.' + ) + raise ValueError(msg) + return v + @model_validator(mode='after') - def _absence_needs_a_mask(self) -> VariableBlock: - """``absence:`` says what a *missing* coordinate means, so one must be missable.""" - if self.absence != 'undefined' and self.where is None: + def _neutral_needs_a_mask(self) -> VariableBlock: + """``missing:`` says what a masked-out coordinate means, so one must be missable.""" + if self.missing != 'absent' and self.where is None: msg = ( - f'absence: {self.absence} needs a `where:` — a variable with no mask exists at every ' - f'coordinate of its dims, so there is no absence for it to describe. Add the mask, ' + f'missing: {self.missing} needs a `where:` — a variable with no mask exists at every ' + f'coordinate of its dims, so nothing is missing for it to describe. Add the mask, ' f'or drop the key.' ) raise ValueError(msg) @@ -300,9 +439,9 @@ def _absence_needs_a_mask(self) -> VariableBlock: class GivenParameterBlock(_StrictBlock): """Data this file reads and another file declares. - It says what a [`ParameterBlock`][] says, because the frame and the - dtype are all a parameter declaration holds: a where compares against the - dtype, and the dim rules read the frame. + It says the frame and the dtype of a [`ParameterBlock`][], which are what + this file reads: a where compares against the dtype, and the dim rules read + the frame. What a missing row means is the declaring file's ``missing:``. """ _label: ClassVar[str] = 'a given parameter declaration' @@ -751,6 +890,11 @@ class PiecewiseBlock(_StrictBlock): points: str | None = None description: str | None = None + @property + def consumes(self) -> frozenset[str]: + """The parameters the block reads: each link's values, and the ``points:`` mask.""" + return frozenset({link.values for link in self.links} | ({self.points} if self.points else set())) + @property def nominated(self) -> str | None: """The block's own values parameter ``points:`` names, so the mask is derived from it — or ``None``.""" diff --git a/src/mathspec/typesetting/format.py b/src/mathspec/typesetting/format.py index 00193b6ed..ddd23d42c 100644 --- a/src/mathspec/typesetting/format.py +++ b/src/mathspec/typesetting/format.py @@ -9,6 +9,7 @@ from __future__ import annotations +import math import re from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar, Literal, Protocol, get_args @@ -261,6 +262,20 @@ def note(self, text: str) -> str: def document(self, blocks: list[str], *, standalone: bool) -> str: ... +def number(value: float, fmt: Format) -> str: + """*value* as math: an integer without its point, an infinity as its sign, a large or small value in powers of ten.""" + if math.isinf(value): + infinity = fmt.operators['infinity'] + return infinity if value > 0 else f'{fmt.operators["minus"]}{infinity}' + if value == int(value): + return str(int(value)) + mantissa, _, exponent = repr(value).partition('e') + if not exponent: + return mantissa + power = fmt.superscript('10', str(int(exponent))) + return power if mantissa == '1' else f'{mantissa} {fmt.operators["times"]} {power}' + + def aligned_rows(lines: list[Line], fmt: Format, *, gap: str) -> list[str]: """One alignment row per line — label, left, right, condition — *gap* around the relation, trailing empty cells stripped.""" return [ diff --git a/src/mathspec/typesetting/legend.py b/src/mathspec/typesetting/legend.py index 28d244511..861c73676 100644 --- a/src/mathspec/typesetting/legend.py +++ b/src/mathspec/typesetting/legend.py @@ -25,12 +25,12 @@ WindowSum, walk_regions, ) -from mathspec.typesetting.format import Entry +from mathspec.typesetting.format import Entry, number if TYPE_CHECKING: from collections.abc import Iterable - from mathspec.program import Expression, Mask, Program, RelationDeclaration + from mathspec.program import Expression, Mask, Missing, Program, RelationDeclaration from mathspec.typesetting.format import Format, OperatorName from mathspec.typesetting.symbols import Symbols @@ -123,6 +123,13 @@ def masks(mask: Mask) -> None: return Noticed(frozenset(policies), grouped, frozenset(positions), frozenset(numeric)) +#: Where a parameter's or a relation's ``missing:`` applies, as the legend says it. +_NO_ROW = 'where the data has no row' + +#: Where a variable's ``missing:`` applies, as the legend says it. +_MASKED_OUT = 'where the mask leaves it out' + + @dataclass(frozen=True) class Legend: """The glossary and the notes, spelled with one program's symbols in one format.""" @@ -150,11 +157,19 @@ def glossaries(self, noticed: Noticed, defined: Iterable[str]) -> list[tuple[str for d, block in program.dimensions.items() ] parameters = [ - self._entry(self.symbols.name[p], f'{fmt.mono(p)}{self._over(list(block.dims))}', block.description) + self._entry( + self.symbols.name[p], + f'{fmt.mono(p)}{self._over(list(block.dims))}{self._missing(block.missing)}', + block.description, + ) for p, block in program.parameters.items() ] variables = [ - self._entry(self.symbols.name[v], f'{fmt.mono(v)}{self._over(list(block.dims))}', block.description) + self._entry( + self.symbols.name[v], + f'{fmt.mono(v)}{self._over(list(block.dims))}{self._missing(block.missing, "absent", _MASKED_OUT)}', + block.description, + ) for v, block in program.variables.items() ] given = [ @@ -230,6 +245,16 @@ def _over(self, dims: list[str]) -> str: product = self.format.joined([self.symbols.set[d] for d in dims], self._op('times')) return f' over {self.format.math(product)}' + def _missing(self, value: Missing | None, default: str = 'refused', where: str = _NO_ROW) -> str: + """What *where* means: the reading or the value ``missing:`` names, and nothing for the *default* reading.""" + if value is None or value == default: + return '' + if isinstance(value, str | bool): + shown = self.format.mono(str(value).lower()) + else: + shown = self.format.math(number(value, self.format)) + return f', {shown} {where}' + def _signature(self, name: str, lk: RelationDeclaration) -> str: """A relation in the legend: a function from its key sets to its value sets, or a relation inside the product.""" @@ -241,7 +266,7 @@ def product(roles: Iterable[str]) -> str: return f'{self.format.upright(name)} {self._op("subset_of")} {product(lk.roles)}' def _coords(self, dim: str, noticed: Noticed) -> str: - """The dimension's carried structure: each relation with a column over it, as the map or relation it is. + """The dimension's carried structure: each relation with a column over it, as the map or relation it is, and each map that may leave a key out. The dtype is named only where an equation compared the index against a number, the one place "position 3" and "the coordinate 3" are both @@ -254,6 +279,11 @@ def _coords(self, dim: str, noticed: Noticed) -> str: if carried: maps = self.format.joined([self._signature(c, lk) for c, lk in carried.items()], '') clauses.append(f' with {self.format.math(maps)}') + clauses.extend( + f', {self.format.mono(c)} is {self.format.mono("absent")} {_NO_ROW}' + for c, lk in carried.items() + if lk.missing == 'absent' + ) return ''.join(clauses) def convention_notes(self) -> list[str]: diff --git a/src/mathspec/typesetting/walk.py b/src/mathspec/typesetting/walk.py index bc0e7879c..69f55b6d7 100644 --- a/src/mathspec/typesetting/walk.py +++ b/src/mathspec/typesetting/walk.py @@ -56,7 +56,7 @@ VariableDefined, WindowSum, ) -from mathspec.typesetting.format import Line, OperatorName +from mathspec.typesetting.format import Line, OperatorName, number from mathspec.typesetting.legend import TranslationPolicy, policy_of if TYPE_CHECKING: @@ -301,15 +301,7 @@ def _context(self, frame: Iterable[str] = ()) -> _Context: return _Context(self, bound=tuple(frame)) def _number(self, value: float) -> str: - if value == float('inf'): - return self._op('infinity') - if value == int(value): - return str(int(value)) - mantissa, _, exponent = repr(value).partition('e') - if not exponent: - return mantissa - power = self.format.superscript('10', str(int(exponent))) - return power if mantissa == '1' else f'{mantissa} {self._op("times")} {power}' + return number(value, self.format) # -- arithmetic -------------------------------------------------------- @@ -958,7 +950,7 @@ def _gate(self, block: PiecewiseDeclaration, ctx: _Context) -> str: the curve is built for, the factor is the gate where it exists and 1 where it does not — the two rows the expansion writes there, because a variable that does not exist takes its row with it and would leave the - curve unstated rather than ungated. ``absence: zero`` is the other + curve unstated rather than ungated. ``missing: neutral`` is the other reading and pins the curve off, which is the factor on its own. """ if (activity := block.activity) is None: @@ -966,7 +958,7 @@ def _gate(self, block: PiecewiseDeclaration, ctx: _Context) -> str: gate = self.program.variables[activity] symbol = ctx.indexed(self.symbols.name[activity], list(gate.dims)) mask = gate.where - if mask is None or gate.absence == 'zero': + if mask is None or gate.missing == 'neutral': return symbol where = self._predicate(mask.root, ctx, need=_WHERE_PRECEDENCE['and']) return self.format.cases( diff --git a/tests/expand/curve-activity/after.yaml b/tests/expand/curve-activity/after.yaml index 21767569c..63b51f44b 100644 --- a/tests/expand/curve-activity/after.yaml +++ b/tests/expand/curve-activity/after.yaml @@ -40,6 +40,6 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. diff --git a/tests/expand/curve-adjacency-points/after.yaml b/tests/expand/curve-adjacency-points/after.yaml index 1ce7bc261..66f82c2c0 100644 --- a/tests/expand/curve-adjacency-points/after.yaml +++ b/tests/expand/curve-adjacency-points/after.yaml @@ -2,8 +2,8 @@ dimensions: bp: { dtype: int, ordered: true } parameters: - x_bp: { dims: [bp] } - y_bp: { dims: [bp] } + x_bp: { dims: [bp], missing: neutral } + y_bp: { dims: [bp], missing: neutral } variables: x: { dims: [], bounds: { lower: 0 } } @@ -42,8 +42,8 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or narrow + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or narrow points: 'x_bp' to where the curve runs. curve_contiguous: holds: count(x_bp AND NOT shift(x_bp, along=bp, offset=1), over=bp) == 1 diff --git a/tests/expand/curve-adjacency-points/before.yaml b/tests/expand/curve-adjacency-points/before.yaml index 6573f78b4..7e4717d48 100644 --- a/tests/expand/curve-adjacency-points/before.yaml +++ b/tests/expand/curve-adjacency-points/before.yaml @@ -2,8 +2,8 @@ dimensions: bp: { dtype: int, ordered: true } parameters: - x_bp: { dims: [bp] } - y_bp: { dims: [bp] } + x_bp: { dims: [bp], missing: neutral } + y_bp: { dims: [bp], missing: neutral } variables: x: { dims: [], bounds: { lower: 0 } } diff --git a/tests/expand/curve-adjacency/after.yaml b/tests/expand/curve-adjacency/after.yaml index 3b607d0f9..e50f5e414 100644 --- a/tests/expand/curve-adjacency/after.yaml +++ b/tests/expand/curve-adjacency/after.yaml @@ -39,6 +39,6 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. diff --git a/tests/expand/curve-convex/after.yaml b/tests/expand/curve-convex/after.yaml index 05e0f3b5e..a4786fafc 100644 --- a/tests/expand/curve-convex/after.yaml +++ b/tests/expand/curve-convex/after.yaml @@ -29,8 +29,8 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. curve_increasing: where: position(bp) > 0 diff --git a/tests/expand/curve-lp-points/after.yaml b/tests/expand/curve-lp-points/after.yaml index 6918dbe25..31478d3b4 100644 --- a/tests/expand/curve-lp-points/after.yaml +++ b/tests/expand/curve-lp-points/after.yaml @@ -2,8 +2,8 @@ dimensions: bp: { dtype: int, ordered: true } parameters: - x_bp: { dims: [bp] } - y_bp: { dims: [bp] } + x_bp: { dims: [bp], missing: neutral } + y_bp: { dims: [bp], missing: neutral } variables: x: { dims: [], bounds: { lower: 0 } } @@ -32,8 +32,8 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or narrow + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or narrow points: 'x_bp' to where the curve runs. curve_increasing: where: x_bp AND shift(x_bp, along=bp, offset=1) diff --git a/tests/expand/curve-lp-points/before.yaml b/tests/expand/curve-lp-points/before.yaml index 8c9210046..50bfa949d 100644 --- a/tests/expand/curve-lp-points/before.yaml +++ b/tests/expand/curve-lp-points/before.yaml @@ -2,8 +2,8 @@ dimensions: bp: { dtype: int, ordered: true } parameters: - x_bp: { dims: [bp] } - y_bp: { dims: [bp] } + x_bp: { dims: [bp], missing: neutral } + y_bp: { dims: [bp], missing: neutral } variables: x: { dims: [], bounds: { lower: 0 } } diff --git a/tests/expand/curve-lp/after.yaml b/tests/expand/curve-lp/after.yaml index 961a6861c..d9dda5c5b 100644 --- a/tests/expand/curve-lp/after.yaml +++ b/tests/expand/curve-lp/after.yaml @@ -31,8 +31,8 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. curve_increasing: where: position(bp) > 0 diff --git a/tests/expand/curve-sos2-piecewise/after.yaml b/tests/expand/curve-sos2-piecewise/after.yaml index ff4466034..16a9e9448 100644 --- a/tests/expand/curve-sos2-piecewise/after.yaml +++ b/tests/expand/curve-sos2-piecewise/after.yaml @@ -35,6 +35,6 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. diff --git a/tests/expand/curve-sos2/after.yaml b/tests/expand/curve-sos2/after.yaml index 3b607d0f9..e50f5e414 100644 --- a/tests/expand/curve-sos2/after.yaml +++ b/tests/expand/curve-sos2/after.yaml @@ -39,6 +39,6 @@ assumptions: holds: x_bp AND y_bp description: >- piecewise 'curve': every breakpoint the curve runs through needs a row in - 'x_bp', 'y_bp' — a missing row is read as a zero rather than as a shorter - curve, so it sits the curve on the origin. Attach the rows, or declare + 'x_bp', 'y_bp' — a missing row does not shorten the + curve. Attach the rows, or declare points: to say how far the curve runs. diff --git a/tests/test_composition.py b/tests/test_composition.py index feb0b4593..fdae211be 100644 --- a/tests/test_composition.py +++ b/tests/test_composition.py @@ -25,7 +25,7 @@ import mathspec.spec as spec_module from mathspec import LanguageError, merge, override, to_markdown, to_spec from mathspec.canonical import canonical_yaml -from tests.fixtures import DISPATCH_MODEL, varied +from tests.fixtures import DISPATCH_MODEL, SMALL_MODEL, varied if TYPE_CHECKING: from pathlib import Path @@ -223,6 +223,40 @@ def test_a_dimension_one_fragment_declares_ordered_is_ordered_in_the_composition assert merge(fragments).dimensions['snapshot'].ordered, 'ordered is a claim one fragment adds to the space' +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}} + + +@pytest.mark.parametrize( + ('fragments', 'missing'), + [ + 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' + ), + ], +) +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 + + +@pytest.mark.parametrize( + 'fragments', + [ + pytest.param([SURFACE, _mapped('absent')], id='refused-against-absent'), + pytest.param([_mapped('absent'), SURFACE], id='absent-against-refused'), + ], +) +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: + merge(fragments) + assert 'make the two identical' in str(raised.value), 'the refusal names the rewrite' + + @pytest.mark.parametrize( ('owner', 'carried'), [ @@ -560,6 +594,57 @@ 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'}) + + +@pytest.mark.parametrize( + ('base', 'relation', '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(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'), + ], +) +def test_a_patch_changes_what_a_key_the_map_leaves_out_means(base, relation, 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 + + +@pytest.mark.parametrize( + ('base', 'written', 'missing'), + [ + pytest.param(DISPATCH_MODEL, 'neutral', 'neutral', id='a-reading'), + pytest.param( + varied(DISPATCH_MODEL, **{'parameters.cost.missing': 'absent'}), + None, + 'refused', + id='null-puts-back-the-default', + ), + ], +) +def test_a_patch_changes_what_a_missing_row_of_a_parameter_means(base, written, missing): + assert override(base, [{'parameters': {'cost': {'missing': written}}}]).parameters['cost'].missing == missing + + +@pytest.mark.parametrize( + ('section', 'name', 'declared'), + [ + pytest.param('parameters', 'c', {'dims': ['g']}, id='a-parameter'), + pytest.param('relations', 'lk', {'key': 'g', 'values': 'h'}, id='a-relation'), + ], +) +def test_the_default_missing_written_out_is_the_spec_that_leaves_it_out(section, name, declared): + """`missing: refused` loaded to a different spec than no `missing:`, and wrote a different canonical text.""" + omitted = to_spec(varied(SMALL_MODEL, **{f'{section}.{name}': declared})) + written = to_spec(varied(SMALL_MODEL, **{f'{section}.{name}': {**declared, 'missing': 'refused'}})) + assert written == omitted, 'the default written out is the default' + assert canonical_yaml(written) == canonical_yaml(omitted), 'so both spellings write one text' + + @pytest.mark.parametrize( 'patch', [ diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index 845b7e775..35cd76b62 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -629,7 +629,7 @@ def test_the_frame_check_and_the_reading_walk_the_same_leaves(namespace): ('predicate', 'expected'), [ pytest.param('p_max > 0', {'p_max'}, id='a-parameter-comparison-names-the-parameter'), - pytest.param('spinup', {'spinup'}, id='a-parameter-bare-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'), diff --git a/tests/test_exclusivity.py b/tests/test_exclusivity.py index 3b6f592cf..5018d6a72 100644 --- a/tests/test_exclusivity.py +++ b/tests/test_exclusivity.py @@ -22,6 +22,7 @@ from mathspec.program import And, Mask, Not, Or from mathspec.resolution import Namespace, resolve_where from mathspec.validation import to_spec +from tests.fixtures import varied if TYPE_CHECKING: from mathspec.program import Predicate @@ -39,7 +40,7 @@ 'parameters': { 'cyclic': {'dims': ['storage'], 'dtype': 'bool'}, 'committable': {'dims': ['storage'], 'dtype': 'bool'}, - 'kind': {'dims': ['storage'], 'dtype': 'str'}, + 'kind': {'dims': ['storage'], 'dtype': 'str', 'missing': 'absent'}, 'soc_initial': {'dims': ['storage']}, 'capacity': {'dims': ['storage']}, 'age': {'dims': ['storage'], 'dtype': 'int'}, @@ -57,7 +58,13 @@ def schema() -> Spec: def refusals(schema: Spec, cases: dict[str, str]) -> list[str]: """Resolve each case's `when` against *schema*, then decide every pair.""" namespace = Namespace(schema) - return list(overlapping({name: _mask(when, namespace, name) for name, when in cases.items()}, namespace.dtypes)) + return list( + overlapping( + {name: _mask(when, namespace, name) for name, when in cases.items()}, + namespace.dtypes, + namespace.defaults, + ) + ) def _mask(text: str, namespace: Namespace, name: str) -> Predicate: @@ -252,7 +259,8 @@ class TestSoundness: The claim the check rests on is that its regions cover every value a subject can take, so "no witness among the cells" means "no witness". This walks a concrete grid — several points inside single cells, both - infinities, an absent value, labels the masks never name — and asserts that + infinities, a missing row read as each value `missing:` may name, labels + the masks never name — and asserts that nothing it proved apart has a point claimed by both. Only pairs the check proves apart are walked; a complement pair asserts X and not X and cannot fail. @@ -282,6 +290,13 @@ class TestSoundness: 'storage': [0, 1, 2], } + #: The value a missing row reads as, drawn per pair: `None` is a reading + #: with no value, and a value sits on a literal, between two, or past them. + READINGS: ClassVar[dict[str, list[Any]]] = { + 'capacity': [None, 0.0, 7.0, 10.0, float('inf')], + 'cyclic': [None, True, False], + } + def _random_mask(self, rng: random.Random, atoms: list[Any], depth: int = 0) -> Any: if depth >= 2 or rng.random() < 0.45: atom = rng.choice(atoms) @@ -311,11 +326,35 @@ def test_a_pair_proved_apart_stays_apart_on_a_finer_grid(self, schema: Spec, see proved = 0 for _ in range(2000): first, second = self._random_mask(rng, atoms), self._random_mask(rng, atoms) - if list(overlapping({'a': first, 'b': second}, dtypes)): + drawn = {name: rng.choice(values) for name, values in self.READINGS.items()} + defaults = {name: value for name, value in drawn.items() if value is not None} + if list(overlapping({'a': first, 'b': second}, dtypes, defaults)): continue proved += 1 - cells = _Grid.of([Mask(first), Mask(second)], dtypes) + cells = _Grid.of([Mask(first), Mask(second)], dtypes, defaults) for point in grid: both = _evaluate(first, point, cells) and _evaluate(second, point, cells) assert not both, f'both cases claim {point} — the cells hid a witness' assert proved > 150, f'only {proved} pairs proved apart; the fuzz is not exercising the check' + + +class TestAMissingRow: + CASES: ClassVar[dict[str, str]] = {'lossless': 'efficiency == 1', 'not_given': 'not efficiency'} + + def test_a_missing_row_compares_as_the_value_it_reads_as(self): + """Both cases claim a storage with no `efficiency` row: it is not given, and it reads 1. + + The prover read a missing row as false in every comparison, so it proved + the two apart, and a coordinate the data leaves out had two values. The + witness said `efficiency is absent`, the name of a reading this + parameter does not have. + """ + schema = to_spec(varied(STORAGE, **{'parameters.efficiency': {'dims': ['storage'], 'missing': 1}})) + [refusal] = refusals(schema, self.CASES) + assert 'efficiency has no row' in refusal, 'the witness is the missing row' + assert 'absent' not in refusal, 'the witness does not use the name of a reading for a missing row' + + @pytest.mark.parametrize('reading', ['refused', 'absent', 'neutral']) + def test_under_a_reading_a_missing_row_compares_false(self, reading: str): + schema = to_spec(varied(STORAGE, **{'parameters.efficiency': {'dims': ['storage'], 'missing': reading}})) + assert refusals(schema, self.CASES) == [], 'a missing row with no value is in neither case' diff --git a/tests/test_expand.py b/tests/test_expand.py index 49b1a90ed..c080c0a1b 100644 --- a/tests/test_expand.py +++ b/tests/test_expand.py @@ -11,6 +11,7 @@ from __future__ import annotations +import copy import re from pathlib import Path from typing import TYPE_CHECKING @@ -18,7 +19,7 @@ import pytest from mathspec import piecewise, to_spec -from tests.fixtures import DISPATCH_MODEL, EXAMPLES, schema_of, varied +from tests.fixtures import DISPATCH_MODEL, EXAMPLES, SMALL_MODEL, schema_of, varied from tests.test_sos import CURVE from tools.render_tex import models @@ -30,6 +31,8 @@ MASKED = varied( CURVE, **{ + 'parameters.bp_x.missing': 'neutral', + 'parameters.bp_y.missing': 'neutral', 'piecewise.cost_curve.method': 'lp', 'piecewise.cost_curve.points': 'bp_x', 'piecewise.cost_curve.links': [['p', 'bp_x'], ['op_cost', 'bp_y', '>=']], @@ -122,7 +125,9 @@ def test_an_expansion_declares_exactly_the_parameters_the_file_declared(): schema = schema_of(MASKED) expanded = schema.expand() - assert expanded.parameters == schema.parameters, 'a curve emits no parameter, so the same data attaches to both' + assert {name: (p.dims, p.dtype) for name, p in expanded.parameters.items()} == { + name: (p.dims, p.dtype) for name, p in schema.parameters.items() + }, 'a curve emits no parameter, so the same data attaches to both' assert schema_of(expanded.to_yaml()).to_dict() == expanded.to_dict(), ( 'the expansion is a file like any other, and loading it back changes nothing' ) @@ -176,3 +181,54 @@ def test_a_model_with_no_formulation_expands_to_itself(): spec = schema_of(DISPATCH_MODEL) assert spec.expand() is spec, 'nothing to write out returns the same object, not a copy' + + +#: `fixtures.SMALL_MODEL` plus a two-link curve over its second dimension, so a +#: block's own parameters stand beside ordinary ones in one model. +SMALL_CURVE = { + 'parameters.bx': {'dims': ['h']}, + 'parameters.by': {'dims': ['h']}, + 'variables.s': {'dims': ['g']}, + 'piecewise.curve': {'over': 'h', 'links': [['p', 'bx'], ['s', 'by']], 'method': 'convex'}, +} + + +#: A curve under `points:` names what a missing row of each values table means. +UNDER_POINTS = {'piecewise.curve.points': 'bx', 'parameters.bx.missing': 'neutral', 'parameters.by.missing': 'neutral'} + + +@pytest.mark.parametrize( + 'points', + [ + pytest.param(UNDER_POINTS, id='a-curve-that-says-how-far-it-runs'), + pytest.param( + {**UNDER_POINTS, 'parameters.run': {'dims': ['h'], 'dtype': 'bool'}, 'piecewise.curve.points': 'run'}, + id='a-boolean-mask', + ), + pytest.param({}, id='a-curve-over-every-breakpoint'), + ], +) +def test_a_spec_and_its_expansion_read_a_missing_row_alike(points): + """Lowering reported `None` for every parameter a block consumes, and the expansion declared it `neutral`.""" + spec = schema_of(varied(SMALL_MODEL, **copy.deepcopy(SMALL_CURVE), **points)) + declared = {name: p.missing for name, p in spec.program.parameters.items()} + expanded = {name: p.missing for name, p in spec.expand('piecewise').program.parameters.items()} + assert declared == expanded, 'a spec and its expansion read a missing row of every parameter alike' + + +@pytest.mark.parametrize('missing', ['absent', 'neutral', 0]) +def test_a_curve_parameter_takes_the_missing_it_declares(missing): + """`missing:` on a curve's parameter was refused at load, so a row outside the curve could not say what it reads.""" + spec = to_spec( + varied( + SMALL_MODEL, + **copy.deepcopy(SMALL_CURVE), + **{ + **UNDER_POINTS, + 'constraints.cap': {'dims': ['h'], 'expression': 'r <= by'}, + 'parameters.by.missing': missing, + }, + ) + ) + assert spec.program.parameters['by'].missing == missing + assert spec.expand('piecewise').program.parameters['by'].missing == missing diff --git a/tests/test_lowering.py b/tests/test_lowering.py index ad19cd47d..83fa33cdb 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -1031,7 +1031,10 @@ def test_the_lowered_regions_are_still_proved_apart(): regions = _cases_in(spec.program).regions named = {f'region{i}': r.when.root for i, r in enumerate(regions)} - assert list(overlapping(named, Namespace(spec).dtypes)) == [], 'no two lowered regions can claim one coordinate' + namespace = Namespace(spec) + assert list(overlapping(named, namespace.dtypes, namespace.defaults)) == [], ( + 'no two lowered regions can claim one coordinate' + ) def test_a_cased_expression_is_readable_by_the_name_the_file_wrote(): diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py index 4c2cfae75..300a70eed 100644 --- a/tests/test_piecewise.py +++ b/tests/test_piecewise.py @@ -70,8 +70,10 @@ 'variables.running': {'dims': ['snapshot'], 'domain': 'binary'}, }, ) +#: The values tables of a curve under ``points:``, which stop where the curve stops. +RAGGED = {'parameters.bp_x.missing': 'neutral', 'parameters.bp_y.missing': 'neutral'} #: The ``lp`` curve masked by one of its own values-parameters, so every check a block can carry is on it. -LP_MASKED = varied(LP, **{'piecewise.cost_curve.points': 'bp_x'}) +LP_MASKED = varied(LP, **RAGGED, **{'piecewise.cost_curve.points': 'bp_x'}) #: Two dims in the frame, so the emitted ``dims`` has an order to get wrong. TWO_DIM = varied( raw_of(NONCONVEX_YAML), @@ -575,7 +577,11 @@ def test_a_masked_lp_curve_sits_its_rows_on_predicates_rather_than_on_parameters def test_a_file_supplied_mask_is_what_the_contiguity_condition_reads(): """A ``points:`` naming a parameter the file declared is bound like any other, and the mask check names it.""" program = expanded( - varied(LP, **{'parameters.reach': {'dims': ['bp'], 'dtype': 'bool'}, 'piecewise.cost_curve.points': 'reach'}), + varied( + LP, + **RAGGED, + **{'parameters.reach': {'dims': ['bp'], 'dtype': 'bool'}, 'piecewise.cost_curve.points': 'reach'}, + ), 'piecewise', ).program @@ -603,6 +609,7 @@ def test_a_gap_is_explained_by_the_rows_the_method_writes(method, reason): ) spec = schema_of( NONCONVEX_YAML, + **RAGGED, **{ 'piecewise.cost_curve.method': method, 'piecewise.cost_curve.points': 'bp_x', diff --git a/tests/test_pypsa_references.py b/tests/test_pypsa_references.py index 493ca5179..622dbbd99 100644 --- a/tests/test_pypsa_references.py +++ b/tests/test_pypsa_references.py @@ -167,7 +167,7 @@ def test_every_declared_row_is_built_by_some_reference(): def _admits(mask: Mask, component: str, unit: dict[str, bool | float]) -> bool: """Whether a mask over one component's own parameters holds for a unit with those values, read by the exclusivity check.""" cell = {Subject('param', f'{component}_{name}'): value for name, value in unit.items()} - return _evaluate(mask.root, cell, _Grid({}, {id(atom): _subject_of(atom) for atom in mask.atoms})) + return _evaluate(mask.root, cell, _Grid({}, {id(atom): _subject_of(atom) for atom in mask.atoms}, {})) @pytest.mark.parametrize('component', ['Generator', 'Link', 'Process']) @@ -212,7 +212,7 @@ def test_the_cvar_blocks_stand_exactly_where_a_risk_preference_is_set(section: s """ mask = getattr(BASE.program, section)[name].where assert mask is not None, 'the block stands only where a risk preference is set' - grid = _Grid({}, {id(atom): _subject_of(atom) for atom in mask.atoms}) + grid = _Grid({}, {id(atom): _subject_of(atom) for atom in mask.atoms}, {}) stands = { omega: _evaluate(mask.root, {Subject('param', 'CVaR_omega'): omega}, grid) for omega in (Special.NULL, 0.0, 0.3) } diff --git a/tests/test_schema.py b/tests/test_schema.py index f6bf0c500..2b25eef2a 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -58,7 +58,9 @@ def test_no_definition_refers_only_to_itself(): [ pytest.param('ObjectiveBlock', 'sense', spec.ObjectiveSense, id='sense'), pytest.param('VariableBlock', 'domain', spec.VariableDomain, id='domain'), - pytest.param('VariableBlock', 'absence', spec.VariableAbsence, id='absence'), + pytest.param('VariableBlock', 'missing', spec.VariableMissing, id='variable-missing'), + pytest.param('RelationBlock', 'missing', spec.RelationMissing, id='relation-missing'), + pytest.param('ParameterBlock', 'missing', spec.MissingReading, id='parameter-missing'), pytest.param('ParameterBlock', 'dtype', spec.ParameterDtype, id='parameter-dtype'), pytest.param('DimensionBlock', 'dtype', spec.DimensionDtype, id='dimension-dtype'), pytest.param('PiecewiseBlock', 'method', spec.PiecewiseMethod, id='method'), @@ -74,6 +76,20 @@ def test_a_closed_vocabulary_is_published_as_an_enum(block, field, alias): assert enum == list(get_args(alias)), f'{block}.{field} stopped publishing its closed vocabulary' +@pytest.mark.parametrize( + ('block', 'default'), + [ + pytest.param('ParameterBlock', 'refused', id='parameter'), + pytest.param('RelationBlock', 'refused', id='relation'), + pytest.param('VariableBlock', 'absent', id='variable'), + ], +) +def test_the_schema_publishes_the_reading_a_file_gets_by_leaving_missing_out(block, default): + """The parameter and the relation hold `None` until read, and the schema published that `null`, not `refused`.""" + published = json.loads(schema.PATH.read_text())['$defs'][block]['properties']['missing'] + assert published['default'] == default + + def test_the_piecewise_method_vocabulary_has_one_home(): """`PiecewiseMethod` types the field and `PIECEWISE_METHODS` says what each emits.""" assert set(get_args(spec.PiecewiseMethod)) == set(spec.PIECEWISE_METHODS), ( diff --git a/tests/test_validation.py b/tests/test_validation.py index f7b17a6a3..8bb76fb47 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -8,11 +8,12 @@ import copy import re +from pathlib import Path from typing import TYPE_CHECKING, Any import pytest -from mathspec._yaml import parse_yaml +from mathspec._yaml import parse_yaml, read_yaml from mathspec.errors import DimensionError, LanguageError, SchemaError from mathspec.program import DimensionPosition from mathspec.resolution import Namespace @@ -1187,7 +1188,7 @@ class TestRulesDecidedWithoutData: ), pytest.param( {'relations.tag': {'key': 'g', 'dtype': 'str'}}, - ("unknown key 'dtype' in a relation declaration. Valid keys: description, key, values.",), + ("unknown key 'dtype' in a relation declaration. 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'), @@ -1415,7 +1416,7 @@ class TestRulesDecidedWithoutData: id='a-relation-naming-one-value-column', ), pytest.param( - {'variables.p.absence': 'zero'}, ('absence: zero needs a `where:`',), id='absence-without-a-mask' + {'variables.p.missing': 'neutral'}, ('missing: neutral needs a `where:`',), id='neutral-without-a-mask' ), pytest.param( {'variables.p.bounds.upper': 'c * 2'}, @@ -1578,6 +1579,27 @@ class TestRulesDecidedWithoutData: ('a bare dimension name is true at every coordinate',), id='where-a-bare-dimension', ), + pytest.param( + {'parameters.n': {'dims': ['g'], 'dtype': 'int'}, 'variables.p.where': 'n'}, + ( + "'n' is declared dtype: int and missing: refused", + 'Declare `missing: neutral` or `absent`, or compare the value', + ), + id='where-a-bare-int-parameter-under-refused', + ), + pytest.param( + {'variables.p.where': 'tag'}, + ( + "'tag' is declared dtype: str and missing: refused", + 'Declare `missing: absent`, or compare the label', + ), + id='where-a-bare-str-parameter-under-refused', + ), + pytest.param( + {'parameters.n': {'dims': ['g'], 'dtype': 'int'}, 'masks.held': 'NOT n'}, + ("'n' is declared dtype: int and missing: refused",), + id='a-mask-reading-a-bare-int-parameter-under-refused', + ), pytest.param( { 'macros.scaled': {'args': ['x'], 'kwargs': ['n'], 'template': 'x * n'}, @@ -2332,6 +2354,171 @@ def test_an_infinite_bound_is_refused_with_the_null_that_opens_a_side(side, valu assert f'{side}: null' in message, 'the refusal names the spelling of an open side' +@pytest.mark.parametrize( + ('dtype', 'written', 'read'), + [ + pytest.param('float', None, 'refused', id='left-out-is-refused'), + pytest.param('float', 'absent', 'absent', id='absent'), + pytest.param('str', 'absent', 'absent', id='absent-on-a-label'), + pytest.param('float', 1, 1, id='a-number'), + pytest.param('float', float('inf'), float('inf'), id='yaml-dot-inf'), + pytest.param('float', 'inf', float('inf'), id='the-expression-spelling-of-inf'), + pytest.param('float', '-inf', float('-inf'), id='the-expression-spelling-of-minus-inf'), + pytest.param('int', 2, 2, id='an-integer'), + pytest.param('bool', True, True, id='a-flag'), + ], +) +def test_a_parameter_says_what_a_missing_row_means(dtype, written, read): + declared = {'dims': ['g'], 'dtype': dtype} | ({} if written is None else {'missing': written}) + spec = to_spec(varied(SMALL_MODEL, **{'parameters.c': declared})) + assert spec.program.parameters['c'].missing == read + + +def test_inf_and_dot_inf_load_as_one_number(): + """YAML reads `inf` as a string and `.inf` as a number; the expression grammar takes both.""" + read = [ + to_spec(parse_yaml(f'dimensions: {{g: {{}}}}\nparameters: {{c: {{dims: [g], missing: {spelling}}}}}')) + .program.parameters['c'] + .missing + for spelling in ('inf', '.inf') + ] + assert read == [float('inf'), float('inf')], 'the string `inf` and the number `.inf` read as the same infinity' + + +@pytest.mark.parametrize( + ('dtype', 'written', 'fragment'), + [ + pytest.param('float', None, 'missing: null on a parameter names no reading', id='null'), + pytest.param('float', 'zero', "missing is the string 'zero'. It takes refused, absent, neutral", id='a-word'), + pytest.param('float', '1', "missing is the string '1'", id='a-quoted-number'), + pytest.param('float', float('nan'), 'missing is nan', id='nan'), + pytest.param( + 'float', True, 'missing: true on a float parameter, which takes a number', id='a-flag-on-a-number' + ), + pytest.param('int', 1.5, 'missing: 1.5 on an int parameter, which takes an integer', id='a-fraction-on-an-int'), + pytest.param('int', float('inf'), 'missing: inf on an int parameter', id='an-infinity-on-an-int'), + pytest.param('bool', 1, 'missing: 1 on a bool parameter, which takes true or false', id='a-number-on-a-flag'), + pytest.param('str', 1, 'A label has no value to fill', id='a-number-on-a-label'), + pytest.param( + 'str', + 'neutral', + 'missing: neutral on a str parameter, which takes refused or absent', + id='neutral-on-a-label', + ), + ], +) +def test_a_parameter_missing_that_reads_nothing_is_refused(dtype, written, fragment): + message = _refusal(**{'parameters.c': {'dims': ['g'], 'dtype': dtype, 'missing': written}}) + assert fragment in message + + +#: A curve under `points: x_bp`, both values tables declared `missing: neutral`. +CURVE_UNDER_POINTS = Path(__file__).parent / 'expand' / 'curve-lp-points' / 'before.yaml' + + +@pytest.mark.parametrize('parameter', ['x_bp', 'y_bp']) +@pytest.mark.parametrize( + 'declared', + [ + pytest.param({'dims': ['bp']}, id='left-out'), + pytest.param({'dims': ['bp'], 'missing': 'refused'}, id='refused-written'), + ], +) +def test_a_values_table_under_points_is_not_refused(parameter, declared): + """`points: x_bp` with `x_bp` declared `refused` loaded, and named a mask every breakpoint is in. + + `refused` says the table has a row at every breakpoint, so the curve never ran short of the dimension, and the + refusal of a curve parameter read outside the curve suggested that reading. + """ + message = _refusal(read_yaml(CURVE_UNDER_POINTS), **{f'parameters.{parameter}': declared}) + assert f"parameter '{parameter}' is refused where a row is missing, and piecewise 'curve'" in message + assert 'Declare missing: neutral, absent, or a value of its dtype' in message, 'the refusal names the rewrite' + + +@pytest.mark.parametrize( + 'patch', + [ + pytest.param({'parameters.n': {'dims': ['g']}}, id='a-float-may-hold-inf'), + pytest.param({'parameters.n': {'dims': ['g'], 'dtype': 'bool'}}, id='a-flag-reads-its-value'), + pytest.param( + {'parameters.n': {'dims': ['g'], 'dtype': 'int', 'missing': 'neutral'}}, id='an-int-under-neutral' + ), + pytest.param({'parameters.n': {'dims': ['g'], 'dtype': 'int', 'missing': 0}}, id='an-int-under-a-value'), + pytest.param({'parameters.n': {'dims': ['g'], 'dtype': 'str', 'missing': 'absent'}}, id='a-label-under-absent'), + pytest.param({'given.parameters.n': {'dims': ['g'], 'dtype': 'int'}}, id='a-given-int'), + ], +) +def test_a_bare_parameter_whose_answer_needs_the_data_loads(patch): + """The declaring file owns a given parameter's `missing:`, so this file cannot decide its rows.""" + spec = to_spec(varied(SMALL_MODEL, **patch, **{'variables.p.where': 'n'})) + assert spec.program.variables['p'].where is not None, 'the bare name stays a mask' + + +def test_a_given_parameter_has_no_missing(): + """The file that declares the parameter owns what a missing row means, as it owns a variable's bounds.""" + message = _refusal(**{'given.parameters.d': {'dims': ['g'], 'missing': 'neutral'}}) + assert "unknown key 'missing' in a given parameter declaration" in message + + +@pytest.mark.parametrize( + ('written', 'read'), + [ + pytest.param({}, 'refused', id='left-out-is-refused'), + pytest.param({'missing': 'refused'}, 'refused', id='refused'), + pytest.param({'missing': 'absent'}, 'absent', id='absent'), + ], +) +def test_a_relation_says_what_a_label_it_leaves_out_means(written, read): + spec = to_spec(varied(SMALL_MODEL, **{'relations.lk': {'key': 'g', 'values': 'h', **written}})) + assert spec.program.relations['lk'].missing == read + + +def test_a_bare_relation_has_no_missing_rows(): + """Its rows are its membership: a pair it leaves out is not a gap, so `refused` would refuse every sparse set.""" + spec = to_spec(varied(SMALL_MODEL, **{'relations.pair': {'key': ['g', 'h']}})) + assert spec.program.relations['pair'].missing is None + + +@pytest.mark.parametrize( + ('relation', 'fragment'), + [ + pytest.param( + {'key': 'g', 'values': 'h', 'missing': 'neutral'}, + 'missing: neutral on a relation, which takes refused or absent', + id='neutral', + ), + 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 names no reading', id='null' + ), + pytest.param( + {'key': ['g', 'h'], 'missing': 'refused'}, + 'missing: refused on a relation with no `values:`', + id='a-bare-relation', + ), + ], +) +def test_a_relation_missing_that_reads_nothing_is_refused(relation, fragment): + assert fragment in _refusal(**{'relations.lk': relation}) + + +@pytest.mark.parametrize( + ('written', 'fragment'), + [ + pytest.param('refused', 'missing: refused on a variable, which takes absent or neutral', id='refused'), + pytest.param(0, 'missing: 0 on a variable, which takes absent or neutral', id='a-value'), + pytest.param(None, 'missing: null on a variable names no reading', id='null'), + ], +) +def test_a_variable_missing_that_reads_data_is_refused(written, fragment): + assert fragment in _refusal(**{'variables.p': {'dims': ['g'], 'where': 'c', 'missing': written}}) + + #: One dimension every construct below reads the order of, and a breakpoint #: dimension for the two formulations; neither declares its order. UNORDERED: dict[str, Any] = { diff --git a/tests/typesetting/golden/latex.out b/tests/typesetting/golden/latex.out index da63aa76b..f5b0bbaeb 100644 --- a/tests/typesetting/golden/latex.out +++ b/tests/typesetting/golden/latex.out @@ -11,8 +11,8 @@ \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}$ -\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}$ -\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}$ +\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}$, \texttt{area\_of} 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}$, \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}$ \item[{$\mathcal{A}$}] index $a$ --- \texttt{bp} @@ -20,20 +20,20 @@ \paragraph{Parameters} \begin{description} -\item[{$\mathrm{p}^{\mathrm{max}}$}] \texttt{p\_max} over $\mathcal{G}$ +\item[{$\mathrm{p}^{\mathrm{max}}$}] \texttt{p\_max} over $\mathcal{G}$, $\infty$ where the data has no row \item[{$\mathrm{p}^{\mathrm{min}}$}] \texttt{p\_min} over $\mathcal{G}$ -\item[{$\mathrm{cost}$}] \texttt{cost} over $\mathcal{G}$ +\item[{$\mathrm{cost}$}] \texttt{cost} over $\mathcal{G}$, \texttt{neutral} where the data has no row \item[{$\mathrm{load}$}] \texttt{load} over $\mathcal{T} \times \mathcal{B}$ -\item[{$\mathrm{is\_flexible}$}] \texttt{is\_flexible} over $\mathcal{G}$ +\item[{$\mathrm{is\_flexible}$}] \texttt{is\_flexible} over $\mathcal{G}$, \texttt{false} where the data has no row \item[{$\mathrm{zone\_cap}$}] \texttt{zone\_cap} over $\mathcal{Z}$ -\item[{$\mathrm{tech\_cap}$}] \texttt{tech\_cap} over $\mathcal{B} \times \mathcal{E}$ +\item[{$\mathrm{tech\_cap}$}] \texttt{tech\_cap} over $\mathcal{B} \times \mathcal{E}$, \texttt{absent} where the data has no row \item[{$\mathrm{min\_up}$}] \texttt{min\_up} over $\mathcal{G}$ \item[{$\mathrm{eta}$}] \texttt{eta} over $\mathcal{G}$ \item[{$\mathrm{lead}$}] \texttt{lead} over $\mathcal{G}$ \item[{$\mathrm{budget}$}] \texttt{budget} (scalar) \item[{$\mathrm{growth}$}] \texttt{growth} (scalar) -\item[{$\mathrm{bp\_x}$}] \texttt{bp\_x} over $\mathcal{G} \times \mathcal{A}$ -\item[{$\mathrm{bp\_y}$}] \texttt{bp\_y} over $\mathcal{G} \times \mathcal{A}$ +\item[{$\mathrm{bp\_x}$}] \texttt{bp\_x} over $\mathcal{G} \times \mathcal{A}$, \texttt{neutral} where the data has no row +\item[{$\mathrm{bp\_y}$}] \texttt{bp\_y} over $\mathcal{G} \times \mathcal{A}$, \texttt{neutral} where the data has no row \item[{$\mathrm{bp\_heat}$}] \texttt{bp\_heat} over $\mathcal{G} \times \mathcal{A}$ \item[{$\mathrm{bp\_run}$}] \texttt{bp\_run} over $\mathcal{G} \times \mathcal{A}$ \end{description} @@ -48,7 +48,7 @@ \item[{$\mathit{units}$}] \texttt{units} over $\mathcal{G}$ \item[{$\mathit{spare}$}] \texttt{spare} over $\mathcal{G}$ \item[{$\mathit{reserve}$}] \texttt{reserve} (scalar) -\item[{$\mathit{headroom}$}] \texttt{headroom} (scalar) +\item[{$\mathit{headroom}$}] \texttt{headroom} (scalar), \texttt{neutral} where the mask leaves it out \item[{$\mathit{weight}$}] \texttt{weight} over $\mathcal{T} \times \mathcal{G}$ \item[{$\mathit{fuel}$}] \texttt{fuel} over $\mathcal{T} \times \mathcal{G}$ \item[{$\mathit{heat}$}] \texttt{heat} over $\mathcal{T} \times \mathcal{G}$ diff --git a/tests/typesetting/golden/markdown.out b/tests/typesetting/golden/markdown.out index 1be024bb3..64a4e157c 100644 --- a/tests/typesetting/golden/markdown.out +++ b/tests/typesetting/golden/markdown.out @@ -8,8 +8,8 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 |---|---| | $`\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}`$ | -| $`\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}`$ | -| $`\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}`$ | +| $`\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}`$, `area_of` 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}`$, `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}`$ | | $`\mathcal{A}`$ | index $`a`$ — `bp` | @@ -18,20 +18,20 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 | Symbol | Meaning | |---|---| -| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$ | +| $`\mathrm{p}^{\mathrm{max}}`$ | `p_max` over $`\mathcal{G}`$, $`\infty`$ where the data has no row | | $`\mathrm{p}^{\mathrm{min}}`$ | `p_min` over $`\mathcal{G}`$ | -| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$ | +| $`\mathrm{cost}`$ | `cost` over $`\mathcal{G}`$, `neutral` where the data has no row | | $`\mathrm{load}`$ | `load` over $`\mathcal{T} \times \mathcal{B}`$ | -| $`\mathrm{is\_flexible}`$ | `is_flexible` over $`\mathcal{G}`$ | +| $`\mathrm{is\_flexible}`$ | `is_flexible` over $`\mathcal{G}`$, `false` where the data has no row | | $`\mathrm{zone\_cap}`$ | `zone_cap` over $`\mathcal{Z}`$ | -| $`\mathrm{tech\_cap}`$ | `tech_cap` over $`\mathcal{B} \times \mathcal{E}`$ | +| $`\mathrm{tech\_cap}`$ | `tech_cap` over $`\mathcal{B} \times \mathcal{E}`$, `absent` where the data has no row | | $`\mathrm{min\_up}`$ | `min_up` over $`\mathcal{G}`$ | | $`\mathrm{eta}`$ | `eta` over $`\mathcal{G}`$ | | $`\mathrm{lead}`$ | `lead` over $`\mathcal{G}`$ | | $`\mathrm{budget}`$ | `budget` (scalar) | | $`\mathrm{growth}`$ | `growth` (scalar) | -| $`\mathrm{bp\_x}`$ | `bp_x` over $`\mathcal{G} \times \mathcal{A}`$ | -| $`\mathrm{bp\_y}`$ | `bp_y` over $`\mathcal{G} \times \mathcal{A}`$ | +| $`\mathrm{bp\_x}`$ | `bp_x` over $`\mathcal{G} \times \mathcal{A}`$, `neutral` where the data has no row | +| $`\mathrm{bp\_y}`$ | `bp_y` over $`\mathcal{G} \times \mathcal{A}`$, `neutral` where the data has no row | | $`\mathrm{bp\_heat}`$ | `bp_heat` over $`\mathcal{G} \times \mathcal{A}`$ | | $`\mathrm{bp\_run}`$ | `bp_run` over $`\mathcal{G} \times \mathcal{A}`$ | @@ -47,7 +47,7 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 | $`\mathit{units}`$ | `units` over $`\mathcal{G}`$ | | $`\mathit{spare}`$ | `spare` over $`\mathcal{G}`$ | | $`\mathit{reserve}`$ | `reserve` (scalar) | -| $`\mathit{headroom}`$ | `headroom` (scalar) | +| $`\mathit{headroom}`$ | `headroom` (scalar), `neutral` where the mask leaves it out | | $`\mathit{weight}`$ | `weight` over $`\mathcal{T} \times \mathcal{G}`$ | | $`\mathit{fuel}`$ | `fuel` over $`\mathcal{T} \times \mathcal{G}`$ | | $`\mathit{heat}`$ | `heat` over $`\mathcal{T} \times \mathcal{G}`$ | diff --git a/tests/typesetting/golden/model.yaml b/tests/typesetting/golden/model.yaml index 81cef7890..6c0ea887a 100644 --- a/tests/typesetting/golden/model.yaml +++ b/tests/typesetting/golden/model.yaml @@ -24,7 +24,7 @@ dimensions: relations: gen_bus: { key: generator, values: bus } zone_of: { key: bus, values: zone } - area_of: { key: bus, values: zone } # a second map into the same set, to compare against + 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 @@ -32,20 +32,20 @@ relations: gen_bt: { key: generator, values: [bus, technology] } # one table with two value columns, read to both at once parameters: - p_max: { dims: [generator] } + p_max: { dims: [generator], missing: .inf } # a value: the legend says what a missing row reads as p_min: { dims: [generator] } - cost: { dims: [generator] } + cost: { dims: [generator], missing: neutral } # a reading: the legend names it load: { dims: [snapshot, bus] } - is_flexible: { dims: [generator], dtype: bool } + is_flexible: { dims: [generator], dtype: bool, missing: false } zone_cap: { dims: [zone] } - tech_cap: { dims: [bus, technology] } + tech_cap: { dims: [bus, technology], missing: absent } min_up: { dims: [generator], dtype: int } eta: { dims: [generator] } # a Greek name that is *given*, so the rule wins and it prints as the word lead: { dims: [generator], dtype: int } budget: { dims: [] } # scalar: the legend says so rather than printing an empty product growth: { dims: [] } # the base of a power; the exponent is `lead`, a column - bp_x: { dims: [generator, bp] } # the x-axis of every curve below, and what a derived mask is read from - bp_y: { dims: [generator, bp] } + bp_x: { dims: [generator, bp], missing: neutral } # the x-axis of every curve below, and what a derived mask is read from + bp_y: { dims: [generator, bp], missing: neutral } bp_heat: { dims: [generator, bp] } bp_run: { dims: [generator, bp], dtype: bool } # how far each curve runs, so a block has a mask to print @@ -75,9 +75,10 @@ variables: reserve: # an empty dims: a scalar declaration, whose line carries no quantifier dims: [] bounds: { lower: 0 } - headroom: # scalar too, but masked, so the condition stands with no set beside it + headroom: # scalar too, but masked, so the condition stands with no set beside it; neutral, so the legend names the reading dims: [] where: "budget" + missing: neutral bounds: { lower: 0 } weight: # the family a sos runs along dims: [snapshot, generator] diff --git a/tests/typesetting/golden/typst.out b/tests/typesetting/golden/typst.out index 261e376e3..93f3eb4ad 100644 --- a/tests/typesetting/golden/typst.out +++ b/tests/typesetting/golden/typst.out @@ -6,27 +6,27 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 == 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)$ -/ $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)$ -/ $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)$ +/ $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)$, `area_of` 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)$, `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)$ / $cal(A)$: index $a$ --- `bp` == Parameters -/ $upright("p")^(upright("max"))$: `p_max` over $cal(G)$ +/ $upright("p")^(upright("max"))$: `p_max` over $cal(G)$, $infinity$ where the data has no row / $upright("p")^(upright("min"))$: `p_min` over $cal(G)$ -/ $upright("cost")$: `cost` over $cal(G)$ +/ $upright("cost")$: `cost` over $cal(G)$, `neutral` where the data has no row / $upright("load")$: `load` over $cal(T) times cal(B)$ -/ $upright("is_flexible")$: `is_flexible` over $cal(G)$ +/ $upright("is_flexible")$: `is_flexible` over $cal(G)$, `false` where the data has no row / $upright("zone_cap")$: `zone_cap` over $cal(Z)$ -/ $upright("tech_cap")$: `tech_cap` over $cal(B) times cal(E)$ +/ $upright("tech_cap")$: `tech_cap` over $cal(B) times cal(E)$, `absent` where the data has no row / $upright("min_up")$: `min_up` over $cal(G)$ / $upright("eta")$: `eta` over $cal(G)$ / $upright("lead")$: `lead` over $cal(G)$ / $upright("budget")$: `budget` (scalar) / $upright("growth")$: `growth` (scalar) -/ $upright("bp_x")$: `bp_x` over $cal(G) times cal(A)$ -/ $upright("bp_y")$: `bp_y` over $cal(G) times cal(A)$ +/ $upright("bp_x")$: `bp_x` over $cal(G) times cal(A)$, `neutral` where the data has no row +/ $upright("bp_y")$: `bp_y` over $cal(G) times cal(A)$, `neutral` where the data has no row / $upright("bp_heat")$: `bp_heat` over $cal(G) times cal(A)$ / $upright("bp_run")$: `bp_run` over $cal(G) times cal(A)$ @@ -39,7 +39,7 @@ every character a notation escapes, set as text: link\_to, 100% & \#1 costs \$5 / $italic("units")$: `units` over $cal(G)$ / $italic("spare")$: `spare` over $cal(G)$ / $italic("reserve")$: `reserve` (scalar) -/ $italic("headroom")$: `headroom` (scalar) +/ $italic("headroom")$: `headroom` (scalar), `neutral` where the mask leaves it out / $italic("weight")$: `weight` over $cal(T) times cal(G)$ / $italic("fuel")$: `fuel` over $cal(T) times cal(G)$ / $italic("heat")$: `heat` over $cal(T) times cal(G)$ diff --git a/tests/typesetting/test_symbols.py b/tests/typesetting/test_symbols.py index d415c39ab..406615b04 100644 --- a/tests/typesetting/test_symbols.py +++ b/tests/typesetting/test_symbols.py @@ -6,12 +6,14 @@ from __future__ import annotations +import math from typing import TYPE_CHECKING import pytest from mathspec.errors import SchemaError from mathspec.typesetting import SymbolTable, to_latex, to_markdown, to_typst, typeset +from mathspec.typesetting.format import number from mathspec.validation import to_spec from tests.fixtures import DISPATCH_MODEL, varied from tests.typesetting.fixtures import EVERY_FORMAT, TYPST_SYMBOLS @@ -95,6 +97,62 @@ def test_a_named_expression_has_a_legend_row_exactly_while_its_symbol_prints(nam assert 'what a snapshot costs' not in typeset(DESCRIBED, name, inline_expressions=True) +DEFAULTED = varied( + DISPATCH_MODEL, + **{ + 'dimensions.bus': {'dtype': 'str'}, + 'relations.gen_bus': {'key': 'generator', 'values': 'bus', 'missing': 'absent'}, + 'parameters.p_max.missing': 'inf', + 'parameters.cost.missing': 1, + 'parameters.load.missing': 'neutral', + 'parameters.floor': {'dims': [], 'missing': '-inf'}, + 'parameters.online': {'dims': ['generator'], 'dtype': 'bool', 'missing': True}, + 'parameters.p_set': {'dims': ['generator'], 'missing': 'absent'}, + 'parameters.ramp': {'dims': ['generator']}, + }, +) + + +@EVERY_FORMAT +def test_the_legend_prints_what_a_missing_row_means(name: FormatName, fmt: Format): + """The legend printed a value and left out a reading, so `absent`, `neutral` and a partial map looked complete.""" + out = typeset(DEFAULTED, name) + for shown in ( + fmt.math(number(math.inf, fmt)), + fmt.math(number(-math.inf, fmt)), + fmt.math('1'), + fmt.mono('true'), + fmt.mono('neutral'), + fmt.mono('absent'), + f'{fmt.mono("gen_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, ( + 'six parameters and the map in each of its two sets say what a missing row means; ' + '`ramp` keeps the default, refused, and says nothing' + ) + + +#: The dispatch model with a masked variable of each reading. +MASKED = varied( + DISPATCH_MODEL, + **{ + 'parameters.on': {'dims': ['generator'], 'dtype': 'bool', 'missing': 'neutral'}, + 'variables.spill': {'dims': ['snapshot', 'generator'], 'where': 'on', 'missing': 'neutral'}, + 'variables.store': {'dims': ['snapshot', 'generator'], 'where': 'on'}, + }, +) + + +@EVERY_FORMAT +def test_the_legend_prints_what_a_masked_out_variable_means(name: FormatName, fmt: Format): + """The legend printed a parameter's reading and not a variable's, so `missing: neutral` on a variable was unseen.""" + out = typeset(MASKED, name) + [row] = [line for line in out.splitlines() if fmt.mono('spill') in line and ' over ' in line] + assert f'{fmt.mono("spill")} over ' in row and f', {fmt.mono("neutral")} where the mask leaves it out' in row, row + assert out.count('where the mask leaves it out') == 1, '`store` keeps the default, absent, and says nothing' + + #: The dispatch model with a curve on it, so one model has two readings and one #: table has to spell both. CURVED = varied( diff --git a/tests/typesetting/test_walk.py b/tests/typesetting/test_walk.py index beadadb46..de86a1ba1 100644 --- a/tests/typesetting/test_walk.py +++ b/tests/typesetting/test_walk.py @@ -1020,7 +1020,11 @@ def test_a_condition_a_method_states_is_a_line_that_may_be_asked_for_before_it_i id='a-bounded-link-states-one-side-of-the-curve', ), pytest.param( - {'piecewise.curve.points': 'reaches'}, + { + 'piecewise.curve.points': 'reaches', + 'parameters.bp_x.missing': 'neutral', + 'parameters.bp_y.missing': 'neutral', + }, r'\mathrm{pwl}_{b \in \mathcal{B} \,:\, \mathrm{reaches}_{b}}', id='points-narrows-the-breakpoints-to-the-ones-it-admits', ),