diff --git a/.claude/skills/docs-writing/SKILL.md b/.claude/skills/docs-writing/SKILL.md index c30da12e..7d531ebb 100644 --- a/.claude/skills/docs-writing/SKILL.md +++ b/.claude/skills/docs-writing/SKILL.md @@ -66,8 +66,8 @@ not need, in three groups: and `Program`: an engine such as specsolve, a renderer, a checker. - **Contributing** is for someone who changes mathspec itself. - **Proofs of concept** holds the notation page, which renders the typesetting - test spec, and the PyPSA pages. The PyPSA pages stay in `docs/examples/`, - where `tools/gallery.py` writes them. + test spec, and the PyPSA and Calliope pages. Those stay in + `docs/examples/`, where `tools/gallery.py` writes them. A page in Development keeps the folder of its kind. diff --git a/.prettierignore b/.prettierignore index 9585e8fe..93c63914 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,6 +25,11 @@ docs/examples/library/generator.md docs/examples/library/load.md docs/examples/library/composed.md docs/examples/pypsa/*.md +docs/examples/calliope/*.md +docs/examples/calliope/extensions/*.md +docs/examples/calliope/variants/*.md +# The port record is written by hand, so prettier keeps its tables. +!docs/examples/calliope/port.md # The PyPSA reference scripts write this file; prettier would reformat what # they stamp, and the two would fight over it exactly as above. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fbe0dd1..a35ebf84 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 +- docs(calliope): all of calliope's math is a set of fragments that merge, with its modes laid over them as patches ([#770](https://github.com/energy-models/mathspec/pull/770)) - fix(language): dual(c) is the rate at which the optimal objective rises with the right side of c, so an equality has a sign too ([#751](https://github.com/energy-models/mathspec/pull/751)) - fix(language): a macro formal written inside a list takes the name the call binds to it ([#779](https://github.com/energy-models/mathspec/pull/779)) - feat(language): a sum names several dimensions in one over= list ([#778](https://github.com/energy-models/mathspec/pull/778)) diff --git a/docs/examples/calliope/area.md b/docs/examples/calliope/area.md new file mode 100644 index 00000000..8547fbaf --- /dev/null +++ b/docs/examples/calliope/area.md @@ -0,0 +1,178 @@ + + +# Area + +One of the base fragments of [Calliope in fragments](index.md). Area use, its limits, its tie to flow capacity, and its cost. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + area_use_min: + description: "`area_use_min` — least area use. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + area_use_max: + description: "`area_use_max` — most area use. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + area_use_per_flow_cap: + description: "`area_use_per_flow_cap` — area use per unit of flow capacity; given only where set" + dims: [nodes, techs] + available_area: + description: "`available_area` — the area every technology at a node may use; given only where set" + dims: [nodes] + cost_area_use: + description: "`cost_area_use` — the cost of one unit of area use" + dims: [nodes, techs, costs] + +variables: + area_use: + description: >- + `area_use` — the area a technology uses. Calliope builds it where + `area_use_min` is given at all; the least area use is data here, so + it is built where that is above zero + dims: [nodes, techs] + where: area_use_min > 0 OR area_use_max OR area_use_per_flow_cap OR sink_unit == per_area OR source_unit == per_area + bounds: { lower: area_use_min, upper: area_use_max } + absence: zero + +expressions: + cost_investment_area_use: + description: "`cost_investment_area_use` — the investment cost of area use" + expression: cost_area_use * area_use + +given: + parameters: + flow_cap_max: { dims: [nodes, techs] } + sink_unit: { dims: [nodes, techs], dtype: str } + source_unit: { dims: [nodes, techs], dtype: str } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_area_use } + +constraints: + force_zero_area_use: + description: "`force_zero_area_use` — a technology with no flow capacity uses no area" + dims: [nodes, techs] + where: area_use AND flow_cap_max == 0 + expression: area_use == 0 + area_use_per_flow_capacity: + description: "`area_use_per_flow_capacity` — area use follows flow capacity, where set" + dims: [nodes, techs, carriers] + where: flow_cap AND area_use AND area_use_per_flow_cap + expression: area_use == flow_cap * area_use_per_flow_cap + area_use_capacity_per_loc: + description: >- + `area_use_capacity_per_loc` — the technologies at a node use at most + its available area. Calliope's `where: area_use` over a node reads as + any technology there using area + dims: [nodes] + where: count(area_use, over=techs) >= 1 AND available_area + expression: sum(area_use, over=techs) <= available_area + +assumptions: + unbounded_area_use_cost: + description: Calliope's `unbounded_area_use_cost` — a negative area cost needs a finite maximum + holds: NOT cost_area_use < 0 OR area_use_max +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{area\_use\_min}`$ | `area_use_min` over $`\mathcal{N} \times \mathcal{I}`$ — `area_use_min` — least area use. Calliope's default is 0, and data prep fills it | +| $`\mathrm{area\_use\_max}`$ | `area_use_max` over $`\mathcal{N} \times \mathcal{I}`$ — `area_use_max` — most area use. Calliope's default is `.inf`, and data prep fills it | +| $`\mathrm{area\_use\_per\_flow\_cap}`$ | `area_use_per_flow_cap` over $`\mathcal{N} \times \mathcal{I}`$ — `area_use_per_flow_cap` — area use per unit of flow capacity; given only where set | +| $`\mathrm{available\_area}`$ | `available_area` over $`\mathcal{N}`$ — `available_area` — the area every technology at a node may use; given only where set | +| $`\mathrm{cost\_area\_use}`$ | `cost_area_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_area_use` — the cost of one unit of area use | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{area\_use}`$ | `area_use` over $`\mathcal{N} \times \mathcal{I}`$ — `area_use` — the area a technology uses. Calliope builds it where `area_use_min` is given at all; the least area use is data here, so it is built where that is above zero | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{flow\_cap\_max}`$ | `flow_cap_max` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{sink\_unit}`$ | `sink_unit` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{source\_unit}`$ | `source_unit` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_investment_area_use` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{cost\_investment\_area\_use}`$ | `cost_investment_area_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment_area_use` — the investment cost of area use | + +Upright is what the data supplies — a parameter such as $`\mathrm{area\_use\_min}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{area\_use}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`force_zero_area_use`** + +```math +\mathit{area\_use}_{n,i} = 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathit{area\_use}_{n,i} \text{ exists} \wedge \mathrm{flow\_cap\_max}_{n,i} = 0 +``` + +**`area_use_per_flow_capacity`** + +```math +\mathit{area\_use}_{n,i} = \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{area\_use\_per\_flow\_cap}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{area\_use}_{n,i} \text{ exists} \wedge \mathrm{area\_use\_per\_flow\_cap}_{n,i} \text{ is defined} +``` + +**`area_use_capacity_per_loc`** + +```math +\sum_{i \in \mathcal{I}} \mathit{area\_use}_{n,i} \le \mathrm{available\_area}_{n} \qquad \forall\, n \in \mathcal{N} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \mathit{area\_use}_{n,i} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{available\_area}_{n} \text{ is defined} +``` + +#### Definitions + +**`cost_investment_area_use`** + +```math +\mathit{cost\_investment\_area\_use}_{n,i,k} = \mathrm{cost\_area\_use}_{n,i,k} \cdot \mathit{area\_use}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`area_use`** + +```math +\mathrm{area\_use\_min}_{n,i} \le \mathit{area\_use}_{n,i} \le \mathrm{area\_use\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{area\_use\_min}_{n,i} > 0 \vee \mathrm{area\_use\_max}_{n,i} \text{ is defined} \vee \mathrm{area\_use\_per\_flow\_cap}_{n,i} \text{ is defined} \vee \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \vee \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} +``` + +#### Assumptions + +**`unbounded_area_use_cost`** + +```math +\neg \left( \mathrm{cost\_area\_use}_{n,i,k} < 0 \right) \vee \mathrm{area\_use\_max}_{n,i} \text{ is defined} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + diff --git a/docs/examples/calliope/balance.md b/docs/examples/calliope/balance.md new file mode 100644 index 00000000..a014562c --- /dev/null +++ b/docs/examples/calliope/balance.md @@ -0,0 +1,91 @@ + + +# The balance + +One of the base fragments of [Calliope in fragments](index.md). Calliope's `system_balance`: at each node, in each time step, a carrier's production equals its consumption. The file declares the sum `carrier_flow` empty, and every file that moves a carrier adds its term. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + carrier_in: + description: whether a technology consumes a carrier at a node + dims: [nodes, techs, carriers] + dtype: bool + carrier_out: + description: whether a technology produces a carrier at a node + dims: [nodes, techs, carriers] + dtype: bool + +expressions: + carrier_flow: + description: >- + what every technology and every other file puts into a node's carrier, + less what it takes out + dims: [nodes, carriers, timesteps] + empty: true + +constraints: + system_balance: + description: >- + `system_balance` — at every node, in every time step, a carrier's + production equals its consumption. Built where a technology at the + node produces or consumes the carrier + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + expression: carrier_flow == 0 +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_in}`$ | `carrier_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares — whether a technology consumes a carrier at a node | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares — whether a technology produces a carrier at a node | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{carrier\_flow}`$ | `carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — what every technology and every other file puts into a node's carrier, less what it takes out | + +#### Subject to + +**`system_balance`** + +```math +\mathit{carrier\_flow}_{n,c,t} = 0 \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_in}_{n,i,c} \} \rvert \ge 1 \vee \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 +``` + +#### Definitions + +**`carrier_flow`** + +```math +\mathit{carrier\_flow}_{n,c,t} = \cdots \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/conversion.md b/docs/examples/calliope/conversion.md new file mode 100644 index 00000000..31d2e322 --- /dev/null +++ b/docs/examples/calliope/conversion.md @@ -0,0 +1,65 @@ + + +# Conversion + +One of the base fragments of [Calliope in fragments](index.md). Calliope's `balance_conversion`, alone: a conversion technology puts out what it takes in. It declares nothing, and reads everything it needs. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_conversion: + description: "`balance_conversion` — a conversion technology puts out, before its losses, what it takes in after them" + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage + expression: sum(flow_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Subject to + +**`balance_conversion`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} +``` + diff --git a/docs/examples/calliope/cost.md b/docs/examples/calliope/cost.md new file mode 100644 index 00000000..612bab4d --- /dev/null +++ b/docs/examples/calliope/cost.md @@ -0,0 +1,191 @@ + + +# The cost + +One of the base fragments of [Calliope in fragments](index.md). How Calliope prices a technology: investment, annualised; variable operation; fixed operation. It declares `cost_investment` and `cost_operation_variable` empty, and `cost_operation_fixed` with a body of its own, and each capacity or flow adds its cost. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + cost_om_annual_investment_fraction: + description: "`cost_om_annual_investment_fraction` — the annual cost of operation, as a share of the investment cost" + dims: [nodes, techs, costs] + cost_depreciation_rate: + description: >- + `cost_depreciation_rate` — the share of the investment cost a year + carries; given only where set, and derived from the lifetime and the + interest rate elsewhere + dims: [nodes, techs, costs] + cost_interest_rate: + description: "`cost_interest_rate` — the interest rate an investment is annualised at" + dims: [nodes, techs, costs] + lifetime: + description: >- + `lifetime` — the years a technology lasts. Calliope's default is + `.inf`, and data prep fills it + dims: [nodes, techs] + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + objective_cost_weights: { dims: [costs] } + expressions: + system_cost: { dims: [], term: cost_of_techs } + +expressions: + cost_investment: + description: >- + `cost_investment` — the investment cost of a technology: flow, storage + and source capacity, and area use. Each file that builds a capacity + adds its own cost + dims: [nodes, techs, costs] + empty: true + cost_operation_variable: + description: >- + `cost_operation_variable` — the operating cost of a technology in a + time step. Each file that builds a flow adds its own cost + dims: [nodes, techs, costs, timesteps] + empty: true + cost_operation_fixed: + description: >- + `cost_operation_fixed` — the fixed annual operating cost of a + technology: its share of the investment cost here, and what each file + adds per unit of capacity + dims: [nodes, techs, costs] + expression: annualisation_weight * cost_investment * cost_om_annual_investment_fraction + annualisation_weight: + description: "`$annualisation_weight` — the share of a year the modelled time steps stand for" + expression: sum(timestep_resolution * timestep_weights, over=timesteps) / 8760 + depreciation_rate: + description: >- + `$depreciation_rate` of `cost_investment_annualised` — the share of the + investment cost a year carries: as given, one over the lifetime with + no interest, and the annuity factor with some + dims: [nodes, techs, costs] + cases: + given: + when: cost_depreciation_rate + expression: cost_depreciation_rate + no_interest: + when: NOT cost_depreciation_rate AND (NOT cost_interest_rate OR cost_interest_rate == 0) + expression: 1 / lifetime + otherwise: >- + cost_interest_rate * (1 + cost_interest_rate) ** lifetime + / ((1 + cost_interest_rate) ** lifetime - 1) + cost_investment_annualised: + description: "`cost_investment_annualised` — the investment cost, as a year's share scaled to the modelled time" + expression: annualisation_weight * depreciation_rate * cost_investment + cost: + description: "`cost` — the total cost of a technology: investment, variable and fixed operation" + expression: cost_investment_annualised + sum(cost_operation_variable, over=timesteps) + cost_operation_fixed + cost_of_techs: + description: "`sum(sum(cost, over=[nodes, techs]) * objective_cost_weights, over=costs)` of `min_cost_optimisation`" + expression: sum(sum(cost, over=[nodes, techs]) * objective_cost_weights) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{cost}^{\mathrm{om,annual,investment,fraction}}`$ | `cost_om_annual_investment_fraction` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_om_annual_investment_fraction` — the annual cost of operation, as a share of the investment cost | +| $`\mathrm{cost}^{\mathrm{depreciation,rate}}`$ | `cost_depreciation_rate` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_depreciation_rate` — the share of the investment cost a year carries; given only where set, and derived from the lifetime and the interest rate elsewhere | +| $`\mathrm{cost}^{\mathrm{interest,rate}}`$ | `cost_interest_rate` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_interest_rate` — the interest rate an investment is annualised at | +| $`\mathrm{lifetime}`$ | `lifetime` over $`\mathcal{N} \times \mathcal{I}`$ — `lifetime` — the years a technology lasts. Calliope's default is `.inf`, and data prep fills it | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{objective\_cost\_weights}`$ | `objective_cost_weights` over $`\mathcal{K}`$, data another file declares | +| $`\mathit{system\_cost}`$ | `system_cost` (scalar), an expression this file adds `cost_of_techs` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{cost}^{\mathrm{operation,fixed}}`$ | `cost_operation_fixed` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_operation_fixed` — the fixed annual operating cost of a technology: its share of the investment cost here, and what each file adds per unit of capacity | +| $`\mathrm{annualisation\_weight}`$ | `annualisation_weight` (scalar) — `$annualisation_weight` — the share of a year the modelled time steps stand for | +| $`\mathrm{depreciation\_rate}`$ | `depreciation_rate` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `$depreciation_rate` of `cost_investment_annualised` — the share of the investment cost a year carries: as given, one over the lifetime with no interest, and the annuity factor with some | +| $`\mathit{cost}^{\mathrm{investment,annualised}}`$ | `cost_investment_annualised` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment_annualised` — the investment cost, as a year's share scaled to the modelled time | +| $`\mathit{cost}`$ | `cost` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost` — the total cost of a technology: investment, variable and fixed operation | +| $`\mathit{cost}^{\mathrm{of,techs}}`$ | `cost_of_techs` (scalar) — `sum(sum(cost, over=[nodes, techs]) * objective_cost_weights, over=costs)` of `min_cost_optimisation` | +| $`\mathit{cost}^{\mathrm{investment}}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment` — the investment cost of a technology: flow, storage and source capacity, and area use. Each file that builds a capacity adds its own cost | +| $`\mathit{cost}^{\mathrm{operation,variable}}`$ | `cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ — `cost_operation_variable` — the operating cost of a technology in a time step. Each file that builds a flow adds its own cost | + +#### Definitions + +**`cost_operation_fixed`** + +```math +\mathit{cost}^{\mathrm{operation,fixed}}_{n,i,k} = \mathrm{annualisation\_weight} \cdot \mathit{cost}^{\mathrm{investment}}_{n,i,k} \cdot \mathrm{cost}^{\mathrm{om,annual,investment,fraction}}_{n,i,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`annualisation_weight`** + +```math +\mathrm{annualisation\_weight} = \frac{\sum_{t \in \mathcal{T}} \mathrm{timestep\_resolution}_{t} \cdot \mathrm{timestep\_weights}_{t}}{8760} +``` + +**`depreciation_rate`** + +```math +\mathrm{depreciation\_rate}_{n,i,k} = \begin{cases} \mathrm{cost}^{\mathrm{depreciation,rate}}_{n,i,k} & \text{if } \mathrm{cost}^{\mathrm{depreciation,rate}}_{n,i,k} \text{ is defined} \\ \frac{1}{\mathrm{lifetime}_{n,i}} & \text{if } \neg \left( \mathrm{cost}^{\mathrm{depreciation,rate}}_{n,i,k} \text{ is defined} \right) \wedge \left( \neg \left( \mathrm{cost}^{\mathrm{interest,rate}}_{n,i,k} \text{ is defined} \right) \vee \mathrm{cost}^{\mathrm{interest,rate}}_{n,i,k} = 0 \right) \\ \frac{\mathrm{cost}^{\mathrm{interest,rate}}_{n,i,k} \cdot \left( 1 + \mathrm{cost}^{\mathrm{interest,rate}}_{n,i,k} \right)^{\mathrm{lifetime}_{n,i}}}{\left( 1 + \mathrm{cost}^{\mathrm{interest,rate}}_{n,i,k} \right)^{\mathrm{lifetime}_{n,i}} - 1} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`cost_investment_annualised`** + +```math +\mathit{cost}^{\mathrm{investment,annualised}}_{n,i,k} = \mathrm{annualisation\_weight} \cdot \mathrm{depreciation\_rate}_{n,i,k} \cdot \mathit{cost}^{\mathrm{investment}}_{n,i,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`cost`** + +```math +\mathit{cost}_{n,i,k} = \mathit{cost}^{\mathrm{investment,annualised}}_{n,i,k} + \sum_{t \in \mathcal{T}} \mathit{cost}^{\mathrm{operation,variable}}_{n,i,k,t} + \mathit{cost}^{\mathrm{operation,fixed}}_{n,i,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`cost_of_techs`** + +```math +\mathit{cost}^{\mathrm{of,techs}} = \sum_{k \in \mathcal{K}} \left( \sum_{n \in \mathcal{N},\ i \in \mathcal{I}} \mathit{cost}_{n,i,k} \right) \cdot \mathrm{objective\_cost\_weights}_{k} +``` + +**`cost_investment`** + +```math +\mathit{cost}^{\mathrm{investment}}_{n,i,k} = \cdots \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`cost_operation_variable`** + +```math +\mathit{cost}^{\mathrm{operation,variable}}_{n,i,k,t} = \cdots \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/demand.md b/docs/examples/calliope/demand.md new file mode 100644 index 00000000..4ac445f7 --- /dev/null +++ b/docs/examples/calliope/demand.md @@ -0,0 +1,182 @@ + + +# Demand + +One of the base fragments of [Calliope in fragments](index.md). Demand technologies: the sink a technology puts into, required, capped or floored per time step. The sink scaler reads `area_use`, as the source scaler does. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + sink_use_min: + description: "`sink_use_min` — least sink use in a time step, per unit of `sink_unit`" + dims: [nodes, techs, timesteps] + sink_use_max: + description: "`sink_use_max` — most sink use in a time step, per unit of `sink_unit`; given only where set" + dims: [nodes, techs, timesteps] + sink_use_equals: + description: "`sink_use_equals` — the sink use required in a time step, such as a demand profile; given only where set" + dims: [nodes, techs, timesteps] + sink_unit: + description: >- + `sink_unit` — what the sink is per: `absolute`, `per_area` of area + use, or `per_cap` of flow capacity. Calliope's default is + `absolute`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + +expressions: + flow_cap_in: + description: "`where(flow_cap, carrier_in)` — the flow capacity of the carriers a technology consumes" + dims: [nodes, techs, carriers] + cases: + consumed: + when: carrier_in + expression: flow_cap + otherwise: 0 + sink_scaler: + description: "`$sink_scaler` — what the sink parameters are per: area use, flow capacity, or one" + dims: [nodes, techs] + cases: + per_area: + when: sink_unit == per_area + expression: area_use + per_cap: + when: sink_unit == per_cap + expression: sum(flow_cap_in, over=carriers) + otherwise: 1 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_in: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + area_use: { dims: [nodes, techs] } + expressions: + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_demand_equals: + description: "`balance_demand` where `sink_use_equals` is set — a demand technology takes in what its sink requires" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND base_tech == 'demand' AND sink_use_equals + expression: flow_in_inc_eff == sink_use_equals * sink_scaler + balance_demand_max: + description: "`balance_demand` where only `sink_use_max` is set — a demand technology takes in at most what its sink allows" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND base_tech == 'demand' AND NOT sink_use_equals AND sink_use_max + expression: flow_in_inc_eff <= sink_use_max * sink_scaler + balance_demand_min_use: + description: "`balance_demand_min_use` — a demand technology takes in at least its least sink use" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND sink_use_min AND NOT sink_use_equals AND base_tech == 'demand' + expression: flow_in_inc_eff >= sink_use_min * sink_scaler + +assumptions: + finite_sink_use: + description: Calliope's `finite_source_use`, for the sink — a required use is finite + holds: NOT sink_use_equals == inf + sink_unit_one_of: + description: Calliope's `one_of` on `sink_unit` + holds: sink_unit == absolute OR sink_unit == per_area OR sink_unit == per_cap + where: sink_unit +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{sink\_use\_min}`$ | `sink_use_min` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `sink_use_min` — least sink use in a time step, per unit of `sink_unit` | +| $`\mathrm{sink\_use\_max}`$ | `sink_use_max` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `sink_use_max` — most sink use in a time step, per unit of `sink_unit`; given only where set | +| $`\mathrm{sink\_use\_equals}`$ | `sink_use_equals` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `sink_use_equals` — the sink use required in a time step, such as a demand profile; given only where set | +| $`\mathrm{sink\_unit}`$ | `sink_unit` over $`\mathcal{N} \times \mathcal{I}`$ — `sink_unit` — what the sink is per: `absolute`, `per_area` of area use, or `per_cap` of flow capacity. Calliope's default is `absolute`, which is what a technology with no row reads as | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{carrier\_in}`$ | `carrier_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{area\_use}`$ | `area_use` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap\_in}`$ | `flow_cap_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `where(flow_cap, carrier_in)` — the flow capacity of the carriers a technology consumes | +| $`\mathit{sink\_scaler}`$ | `sink_scaler` over $`\mathcal{N} \times \mathcal{I}`$ — `$sink_scaler` — what the sink parameters are per: area use, flow capacity, or one | + +#### Subject to + +**`balance_demand_equals`** + +```math +\mathit{flow\_in\_inc\_eff}_{n,i,c,t} = \mathrm{sink\_use\_equals}_{n,i,t} \cdot \mathit{sink\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} \wedge \mathrm{sink\_use\_equals}_{n,i,t} \text{ is defined} +``` + +**`balance_demand_max`** + +```math +\mathit{flow\_in\_inc\_eff}_{n,i,c,t} \le \mathrm{sink\_use\_max}_{n,i,t} \cdot \mathit{sink\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} \wedge \neg \left( \mathrm{sink\_use\_equals}_{n,i,t} \text{ is defined} \right) \wedge \mathrm{sink\_use\_max}_{n,i,t} \text{ is defined} +``` + +**`balance_demand_min_use`** + +```math +\mathit{flow\_in\_inc\_eff}_{n,i,c,t} \ge \mathrm{sink\_use\_min}_{n,i,t} \cdot \mathit{sink\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} \wedge \mathrm{sink\_use\_min}_{n,i,t} \text{ is defined} \wedge \neg \left( \mathrm{sink\_use\_equals}_{n,i,t} \text{ is defined} \right) \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} +``` + +#### Definitions + +**`flow_cap_in`** + +```math +\mathit{flow\_cap\_in}_{n,i,c} = \begin{cases} \mathit{flow\_cap}_{n,i,c} & \text{if } \mathrm{carrier\_in}_{n,i,c} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`sink_scaler`** + +```math +\mathit{sink\_scaler}_{n,i} = \begin{cases} \mathit{area\_use}_{n,i} & \text{if } \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \\ \sum_{c \in \mathcal{C}} \mathit{flow\_cap\_in}_{n,i,c} & \text{if } \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_cap}\text{'} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +#### Assumptions + +**`finite_sink_use`** + +```math +\neg \left( \mathrm{sink\_use\_equals}_{n,i,t} = \infty \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +**`sink_unit_one_of`** + +```math +\mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{absolute}\text{'} \vee \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \vee \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_cap}\text{'} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{sink\_unit}_{n,i} \text{ is defined} +``` + diff --git a/docs/examples/calliope/export.md b/docs/examples/calliope/export.md new file mode 100644 index 00000000..04a17aab --- /dev/null +++ b/docs/examples/calliope/export.md @@ -0,0 +1,157 @@ + + +# Export + +One of the base fragments of [Calliope in fragments](index.md). Export out of the system: a technology may export a carrier it produces, and the export leaves the balance and has a cost. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + carrier_export: + description: "`carrier_export` — whether a technology may export a carrier it produces out of the system" + dims: [nodes, techs, carriers] + dtype: bool + export_min: + description: "`export_min` — least export. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs, carriers] + export_max: + description: "`export_max` — most export. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs, carriers] + cost_export: + description: "`cost_export` — the cost of one unit of export, usually negative" + dims: [nodes, techs, costs, timesteps] + +variables: + flow_export: + description: "`flow_export` — what a technology exports out of the system in a time step" + dims: [nodes, techs, carriers, timesteps] + where: carrier_export + bounds: { lower: export_min, upper: export_max } + absence: zero + +expressions: + export_carrier_flow: -sum(flow_export, over=techs) + export_cost_operation_variable: timestep_weights * sum(cost_export * flow_export, over=carriers) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_weights: { dims: [timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: export_carrier_flow } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: export_cost_operation_variable } + +constraints: + export_balance: + description: "`export_balance` — a technology exports at most what it puts out" + dims: [nodes, techs, carriers, timesteps] + where: flow_export + expression: flow_out >= flow_export + +assumptions: + export_only_for_outflows: + description: Calliope's `export_only_for_outflows` — an exported carrier is one the technology produces + holds: NOT carrier_export OR count(carrier_out, over=nodes) >= 1 +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_export}`$ | `carrier_export` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `carrier_export` — whether a technology may export a carrier it produces out of the system | +| $`\mathrm{export\_min}`$ | `export_min` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `export_min` — least export. Calliope's default is 0, and data prep fills it | +| $`\mathrm{export\_max}`$ | `export_max` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `export_max` — most export. Calliope's default is `.inf`, and data prep fills it | +| $`\mathrm{cost\_export}`$ | `cost_export` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ — `cost_export` — the cost of one unit of export, usually negative | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_export}`$ | `flow_export` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_export` — what a technology exports out of the system in a time step | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{carrier\_flow}`$ | `carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$, an expression this file adds `export_carrier_flow` to | +| $`\mathit{cost\_operation\_variable}`$ | `cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$, an expression this file adds `export_cost_operation_variable` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{export\_carrier\_flow}`$ | `export_carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{export\_cost\_operation\_variable}`$ | `export_cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T} \times \mathcal{K}`$ | + +Upright is what the data supplies — a parameter such as $`\mathrm{carrier\_export}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{flow\_export}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`export_balance`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge \mathit{flow\_export}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_export}_{n,i,c,t} \text{ exists} +``` + +#### Definitions + +**`export_carrier_flow`** + +```math +\mathit{export\_carrier\_flow}_{n,c,t} = -\left( \sum_{i \in \mathcal{I}} \mathit{flow\_export}_{n,i,c,t} \right) \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`export_cost_operation_variable`** + +```math +\mathit{export\_cost\_operation\_variable}_{n,i,t,k} = \mathrm{timestep\_weights}_{t} \cdot \left( \sum_{c \in \mathcal{C}} \mathrm{cost\_export}_{n,i,k,t} \cdot \mathit{flow\_export}_{n,i,c,t} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`flow_export`** + +```math +\mathrm{export\_min}_{n,i,c} \le \mathit{flow\_export}_{n,i,c,t} \le \mathrm{export\_max}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_export}_{n,i,c} +``` + +#### Assumptions + +**`export_only_for_outflows`** + +```math +\neg \mathrm{carrier\_export}_{n,i,c} \vee \lvert \{ n' \in \mathcal{N} \,:\, \mathrm{carrier\_out}_{n',i,c} \} \rvert \ge 1 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + diff --git a/docs/examples/calliope/extensions/annual_energy_balance.md b/docs/examples/calliope/extensions/annual_energy_balance.md new file mode 100644 index 00000000..491bc0f0 --- /dev/null +++ b/docs/examples/calliope/extensions/annual_energy_balance.md @@ -0,0 +1,169 @@ + + +# Annual energy balance + +An extension of [Calliope in fragments](../index.md). Calliope's example `annual_energy_balance.yaml`: limits on what a technology puts out, takes from its source or puts into its sink over the whole time. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + annual_flow_max: + description: "`annual_flow_max` — the most a technology puts out over the whole time; given only where set" + dims: [techs] + annual_flow_max_group: + description: >- + `annual_flow_max` as the group row reads it — the most the group puts + out over the whole time. Calliope reads one parameter at three shapes + and lets the data choose; a parameter here has one, so the group's + limit is a number of its own + dims: [] + annual_source_max: + description: "`annual_source_max` — the most a technology takes from its source over the whole time; given only where set" + dims: [techs] + annual_sink_max: + description: "`annual_sink_max` — the most a technology puts into its sink over the whole time; given only where set" + dims: [techs] + flow_max_group: + description: "`flow_max_group` — whether a technology is in the group the group limit holds for" + dims: [techs] + dtype: bool + +expressions: + flow_out_of_group: + description: "`flow_out[techs=$techs]` — outflow of the technologies in the group" + dims: [nodes, techs, carriers, timesteps] + cases: + in_group: + when: flow_max_group + expression: flow_out + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + source_use: { dims: [nodes, techs, timesteps] } + +constraints: + annual_energy_balance_per_tech_and_node: + description: "`annual_energy_balance_per_tech_and_node` — a technology at a node puts out at most its annual limit" + dims: [nodes, techs] + where: annual_flow_max + expression: sum(flow_out, over=[carriers, timesteps]) <= annual_flow_max + annual_energy_balance_global_per_tech: + description: "`annual_energy_balance_global_per_tech` — a technology puts out at most its annual limit over every node" + dims: [techs] + where: annual_flow_max + expression: sum(flow_out, over=[nodes, carriers, timesteps]) <= annual_flow_max + annual_energy_balance_global_multi_tech: + description: "`annual_energy_balance_global_multi_tech` — the group of technologies puts out at most its annual limit over every node" + dims: [] + where: annual_flow_max_group + expression: sum(flow_out_of_group) <= annual_flow_max_group + annual_energy_balance_total_source_availability: + description: >- + `annual_energy_balance_total_source_availability` — a technology takes + at most its annual limit from its source. Calliope's `where: + source_use` over a technology reads as the technology being a supply + one, which is where `source_use` is built + dims: [techs] + where: base_tech == 'supply' AND annual_source_max + expression: sum(source_use, over=[nodes, timesteps]) <= annual_source_max + annual_energy_balance_total_sink_availability: + description: "`annual_energy_balance_total_sink_availability` — a demand technology takes in at most its annual limit" + dims: [techs] + where: base_tech == 'demand' AND annual_sink_max + expression: sum(flow_in, over=[nodes, carriers, timesteps]) <= annual_sink_max +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{annual\_flow\_max}`$ | `annual_flow_max` over $`\mathcal{I}`$ — `annual_flow_max` — the most a technology puts out over the whole time; given only where set | +| $`\mathrm{annual\_flow\_max\_group}`$ | `annual_flow_max_group` (scalar) — `annual_flow_max` as the group row reads it — the most the group puts out over the whole time. Calliope reads one parameter at three shapes and lets the data choose; a parameter here has one, so the group's limit is a number of its own | +| $`\mathrm{annual\_source\_max}`$ | `annual_source_max` over $`\mathcal{I}`$ — `annual_source_max` — the most a technology takes from its source over the whole time; given only where set | +| $`\mathrm{annual\_sink\_max}`$ | `annual_sink_max` over $`\mathcal{I}`$ — `annual_sink_max` — the most a technology puts into its sink over the whole time; given only where set | +| $`\mathrm{flow\_max\_group}`$ | `flow_max_group` over $`\mathcal{I}`$ — `flow_max_group` — whether a technology is in the group the group limit holds for | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{source\_use}`$ | `source_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out\_of\_group}`$ | `flow_out_of_group` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[techs=$techs]` — outflow of the technologies in the group | + +#### Subject to + +**`annual_energy_balance_per_tech_and_node`** + +```math +\sum_{c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} \le \mathrm{annual\_flow\_max}_{i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{annual\_flow\_max}_{i} \text{ is defined} +``` + +**`annual_energy_balance_global_per_tech`** + +```math +\sum_{n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} \le \mathrm{annual\_flow\_max}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{annual\_flow\_max}_{i} \text{ is defined} +``` + +**`annual_energy_balance_global_multi_tech`** + +```math +\sum_{n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_out\_of\_group}_{n,i,c,t} \le \mathrm{annual\_flow\_max\_group} \qquad \text{where } \mathrm{annual\_flow\_max\_group} \text{ is defined} +``` + +**`annual_energy_balance_total_source_availability`** + +```math +\sum_{n \in \mathcal{N},\ t \in \mathcal{T}} \mathit{source\_use}_{n,i,t} \le \mathrm{annual\_source\_max}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \wedge \mathrm{annual\_source\_max}_{i} \text{ is defined} +``` + +**`annual_energy_balance_total_sink_availability`** + +```math +\sum_{n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_in}_{n,i,c,t} \le \mathrm{annual\_sink\_max}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} \wedge \mathrm{annual\_sink\_max}_{i} \text{ is defined} +``` + +#### Definitions + +**`flow_out_of_group`** + +```math +\mathit{flow\_out\_of\_group}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } \mathrm{flow\_max\_group}_{i} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/extensions/chp_htp.md b/docs/examples/calliope/extensions/chp_htp.md new file mode 100644 index 00000000..a21d3746 --- /dev/null +++ b/docs/examples/calliope/extensions/chp_htp.md @@ -0,0 +1,242 @@ + + +# CHP plants + +An extension of [Calliope in fragments](../index.md). Calliope's example `chp_htp.yaml`: the operating region of combined heat and power plants with extraction or backpressure turbines. Calliope rewrites `balance_conversion` for these plants; the new row is here, and [the CHP patch](../variants/chp_htp.md) keeps the base row off them. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + turbine_type: + description: "`turbine_type` — `extraction` or `backpressure`: the kind of turbine a combined heat and power plant has" + dims: [nodes, techs] + dtype: str + power_loss_factor: + description: "`power_loss_factor` — `cv`, the power an extraction turbine loses per unit of heat. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + power_to_heat_ratio: + description: "`power_to_heat_ratio` — `cb`, the backpressure ratio. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + boiler_eff: + description: "`boiler_eff` — the efficiency of the boiler fuel may be diverted to; given only where set" + dims: [nodes, techs] + +expressions: + chp_electricity_out: + description: "`flow_out[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out + otherwise: 0 + chp_heat_out: + description: "`flow_out[carriers=heat]`" + dims: [nodes, techs, carriers, timesteps] + cases: + heat: + when: carriers == heat + expression: flow_out + otherwise: 0 + chp_electricity_out_eff: + description: "`flow_out_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_eff + otherwise: 0 + chp_electricity_out_inc_eff: + description: "`flow_out_inc_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_inc_eff + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + flow_out_eff: { dims: [nodes, techs, carriers, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_conversion_backpressure: + description: >- + `balance_conversion` for a backpressure plant with no boiler — the + plant puts out, before losses, as much electricity as it takes in fuel + after them. The patch keeps the base row off these plants + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage AND turbine_type == backpressure AND NOT boiler_eff + expression: sum(chp_electricity_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) + chp_extraction_line: + description: "`chp_extraction_line` — an extraction plant puts out at most the electricity its fuel gives, less what the heat costs" + dims: [nodes, techs, timesteps] + where: turbine_type == extraction + expression: >- + sum(chp_electricity_out, over=carriers) + <= sum(flow_in_inc_eff, over=carriers) * sum(chp_electricity_out_eff, over=carriers) + - sum(chp_heat_out, over=carriers) * power_loss_factor + chp_backpressure_line_min: + description: "`chp_backpressure_line_min` — an extraction plant puts out at least the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == extraction + expression: sum(chp_electricity_out, over=carriers) >= sum(chp_heat_out, over=carriers) * power_to_heat_ratio + chp_backpressure_line_max: + description: "`chp_backpressure_line_max` — a backpressure plant with a boiler puts out at most the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND boiler_eff + expression: sum(chp_electricity_out, over=carriers) <= sum(chp_heat_out, over=carriers) * power_to_heat_ratio + chp_divert_fuel_to_boiler: + description: "`chp_divert_fuel_to_boiler` — a backpressure plant with a boiler puts out at most the heat its fuel gives through turbine and boiler" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND boiler_eff + expression: >- + sum(chp_heat_out, over=carriers) + <= sum(flow_in_inc_eff, over=carriers) * boiler_eff + - sum(chp_electricity_out, over=carriers) + * (boiler_eff / sum(chp_electricity_out_eff, over=carriers) - 1 / power_to_heat_ratio) + chp_backpressure_line_equals: + description: "`chp_backpressure_line_equals` — a backpressure plant with no boiler puts out the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND NOT boiler_eff + expression: sum(chp_electricity_out, over=carriers) == sum(chp_heat_out, over=carriers) * power_to_heat_ratio + +assumptions: + turbine_type_one_of: + description: Calliope's `one_of` on `turbine_type` + holds: turbine_type == extraction OR turbine_type == backpressure + where: turbine_type +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{turbine\_type}`$ | `turbine_type` over $`\mathcal{N} \times \mathcal{I}`$ — `turbine_type` — `extraction` or `backpressure`: the kind of turbine a combined heat and power plant has | +| $`\mathrm{power\_loss\_factor}`$ | `power_loss_factor` over $`\mathcal{N} \times \mathcal{I}`$ — `power_loss_factor` — `cv`, the power an extraction turbine loses per unit of heat. Calliope's default is 1, and data prep fills it | +| $`\mathrm{power\_to\_heat\_ratio}`$ | `power_to_heat_ratio` over $`\mathcal{N} \times \mathcal{I}`$ — `power_to_heat_ratio` — `cb`, the backpressure ratio. Calliope's default is 1, and data prep fills it | +| $`\mathrm{boiler\_eff}`$ | `boiler_eff` over $`\mathcal{N} \times \mathcal{I}`$ — `boiler_eff` — the efficiency of the boiler fuel may be diverted to; given only where set | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{flow\_out\_eff}`$ | `flow_out_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{chp\_electricity\_out}`$ | `chp_electricity_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=electricity]` | +| $`\mathit{chp\_heat\_out}`$ | `chp_heat_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=heat]` | +| $`\mathrm{chp\_electricity\_out\_eff}`$ | `chp_electricity_out_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_eff[carriers=electricity]` | +| $`\mathit{chp\_electricity\_out\_inc\_eff}`$ | `chp_electricity_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_inc_eff[carriers=electricity]` | + +#### Subject to + +**`balance_conversion_backpressure`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out\_inc\_eff}_{n,i,c,t} = \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} \wedge \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{backpressure}\text{'} \wedge \neg \left( \mathrm{boiler\_eff}_{n,i} \text{ is defined} \right) +``` + +**`chp_extraction_line`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out}_{n,i,c,t} \le \left( \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \right) \cdot \left( \sum_{c \in \mathcal{C}} \mathrm{chp\_electricity\_out\_eff}_{n,i,c,t} \right) - \left( \sum_{c \in \mathcal{C}} \mathit{chp\_heat\_out}_{n,i,c,t} \right) \cdot \mathrm{power\_loss\_factor}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{extraction}\text{'} +``` + +**`chp_backpressure_line_min`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out}_{n,i,c,t} \ge \left( \sum_{c \in \mathcal{C}} \mathit{chp\_heat\_out}_{n,i,c,t} \right) \cdot \mathrm{power\_to\_heat\_ratio}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{extraction}\text{'} +``` + +**`chp_backpressure_line_max`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out}_{n,i,c,t} \le \left( \sum_{c \in \mathcal{C}} \mathit{chp\_heat\_out}_{n,i,c,t} \right) \cdot \mathrm{power\_to\_heat\_ratio}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{backpressure}\text{'} \wedge \mathrm{boiler\_eff}_{n,i} \text{ is defined} +``` + +**`chp_divert_fuel_to_boiler`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_heat\_out}_{n,i,c,t} \le \left( \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \right) \cdot \mathrm{boiler\_eff}_{n,i} - \left( \sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out}_{n,i,c,t} \right) \cdot \left( \frac{\mathrm{boiler\_eff}_{n,i}}{\sum_{c \in \mathcal{C}} \mathrm{chp\_electricity\_out\_eff}_{n,i,c,t}} - \frac{1}{\mathrm{power\_to\_heat\_ratio}_{n,i}} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{backpressure}\text{'} \wedge \mathrm{boiler\_eff}_{n,i} \text{ is defined} +``` + +**`chp_backpressure_line_equals`** + +```math +\sum_{c \in \mathcal{C}} \mathit{chp\_electricity\_out}_{n,i,c,t} = \left( \sum_{c \in \mathcal{C}} \mathit{chp\_heat\_out}_{n,i,c,t} \right) \cdot \mathrm{power\_to\_heat\_ratio}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{backpressure}\text{'} \wedge \neg \left( \mathrm{boiler\_eff}_{n,i} \text{ is defined} \right) +``` + +#### Definitions + +**`chp_electricity_out`** + +```math +\mathit{chp\_electricity\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`chp_heat_out`** + +```math +\mathit{chp\_heat\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{heat}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`chp_electricity_out_eff`** + +```math +\mathrm{chp\_electricity\_out\_eff}_{n,i,c,t} = \begin{cases} \mathrm{flow\_out\_eff}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`chp_electricity_out_inc_eff`** + +```math +\mathit{chp\_electricity\_out\_inc\_eff}_{n,i,c,t} = \begin{cases} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +#### Assumptions + +**`turbine_type_one_of`** + +```math +\mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{extraction}\text{'} \vee \mathrm{turbine\_type}_{n,i} = \text{'}\mathrm{backpressure}\text{'} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{turbine\_type}_{n,i} \text{ is defined} +``` + diff --git a/docs/examples/calliope/extensions/demand_share_per_timestep_decision.md b/docs/examples/calliope/extensions/demand_share_per_timestep_decision.md new file mode 100644 index 00000000..4284fb3a --- /dev/null +++ b/docs/examples/calliope/extensions/demand_share_per_timestep_decision.md @@ -0,0 +1,194 @@ + + +# Demand share as a decision + +An extension of [Calliope in fragments](../index.md). Calliope's example `demand_share_per_timestep_decision.yaml`: a technology meets a share of a demand that the model decides, the same in every time step. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + decide_demand_share: + description: >- + `decide_demand_share` — the demand technology whose inflow a technology + meets a share of. Calliope reads it with `select_from_lookup_arrays`, + which is a read through the relation + key: techs + values: { demand: techs } + demand_share_carrier: + description: >- + `demand_share_carrier` — the carrier a share of demand is counted in. + Calliope slices `flow_out` by it, which is a test of the pair + key: [techs, carriers] + +parameters: + demand_share_relaxation: + description: "`demand_share_relaxation` — how far the share may stray from the one decided, as a fraction" + dims: [nodes, techs] + demand_share_limit: + description: "`demand_share_limit` — the share of demand the technologies meet together; given only where set" + dims: [nodes] + +variables: + demand_share_per_timestep_decision: + description: "`demand_share_per_timestep_decision` — the share of demand a technology meets, the same in every time step" + dims: [nodes, techs] + where: decide_demand_share + bounds: { lower: 0 } + absence: zero + +expressions: + demand_share_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of the carrier its share is counted in" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: demand_share_carrier + expression: flow_out + otherwise: 0 + demand_share_sink: + description: "`select_from_lookup_arrays(sink_use_equals, techs=decide_demand_share)` — the demand a technology meets a share of" + expression: at(sink_use_equals, by=decide_demand_share, over=demand, into=techs) + +given: + parameters: + sink_use_equals: { dims: [nodes, techs, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_per_timestep_decision_main_min: + description: "`demand_share_per_timestep_decision_main_min` — a technology puts out at least its decided share of demand, less the relaxation" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_decision + expression: >- + sum(demand_share_flow_out, over=carriers) + >= (1 - demand_share_relaxation) * demand_share_sink * demand_share_per_timestep_decision + demand_share_per_timestep_decision_main_max: + description: "`demand_share_per_timestep_decision_main_max` — a technology puts out at most its decided share of demand, plus the relaxation" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_decision + expression: >- + sum(demand_share_flow_out, over=carriers) + <= (1 + demand_share_relaxation) * demand_share_sink * demand_share_per_timestep_decision + demand_share_per_timestep_decision_sum: + description: >- + `demand_share_per_timestep_decision_sum` — the decided shares at a node + add up to the limit. Calliope's `where: demand_share_per_timestep_decision` + over a node reads as any technology there deciding a share. Calliope + builds the row in every time step, and it is the same in each; a row + repeated along a dimension it does not read is refused, so it is one + row per node + dims: [nodes] + where: count(demand_share_per_timestep_decision, over=techs) >= 1 AND demand_share_limit + expression: sum(demand_share_per_timestep_decision, over=techs) == demand_share_limit + +assumptions: + demand_share_is_fraction: + description: Calliope's `demand_share_is_fraction` — the demand share limit is a fraction + holds: demand_share_limit >= 0 AND demand_share_limit <= 1 + where: demand_share_limit +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` with $`\mathrm{decide\_demand\_share}: \mathcal{I} \to \mathcal{I},\ \mathrm{demand\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` with $`\mathrm{demand\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{demand\_share\_relaxation}`$ | `demand_share_relaxation` over $`\mathcal{N} \times \mathcal{I}`$ — `demand_share_relaxation` — how far the share may stray from the one decided, as a fraction | +| $`\mathrm{demand\_share\_limit}`$ | `demand_share_limit` over $`\mathcal{N}`$ — `demand_share_limit` — the share of demand the technologies meet together; given only where set | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{demand\_share\_per\_timestep\_decision}`$ | `demand_share_per_timestep_decision` over $`\mathcal{N} \times \mathcal{I}`$ — `demand_share_per_timestep_decision` — the share of demand a technology meets, the same in every time step | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{sink\_use\_equals}`$ | `sink_use_equals` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{demand\_share\_flow\_out}`$ | `demand_share_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=$carrier]` — a technology's outflow of the carrier its share is counted in | +| $`\mathrm{demand\_share\_sink}`$ | `demand_share_sink` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `select_from_lookup_arrays(sink_use_equals, techs=decide_demand_share)` — the demand a technology meets a share of | + +Upright is what the data supplies — a parameter such as $`\mathrm{demand\_share\_relaxation}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{demand\_share\_per\_timestep\_decision}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`demand_share_per_timestep_decision_main_min`** + +```math +\sum_{c \in \mathcal{C}} \mathit{demand\_share\_flow\_out}_{n,i,c,t} \ge \left( 1 - \mathrm{demand\_share\_relaxation}_{n,i} \right) \cdot \mathrm{demand\_share\_sink}_{n,i,t} \cdot \mathit{demand\_share\_per\_timestep\_decision}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{demand\_share\_per\_timestep\_decision}_{n,i} \text{ exists} +``` + +**`demand_share_per_timestep_decision_main_max`** + +```math +\sum_{c \in \mathcal{C}} \mathit{demand\_share\_flow\_out}_{n,i,c,t} \le \left( 1 + \mathrm{demand\_share\_relaxation}_{n,i} \right) \cdot \mathrm{demand\_share\_sink}_{n,i,t} \cdot \mathit{demand\_share\_per\_timestep\_decision}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{demand\_share\_per\_timestep\_decision}_{n,i} \text{ exists} +``` + +**`demand_share_per_timestep_decision_sum`** + +```math +\sum_{i \in \mathcal{I}} \mathit{demand\_share\_per\_timestep\_decision}_{n,i} = \mathrm{demand\_share\_limit}_{n} \qquad \forall\, n \in \mathcal{N} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \mathit{demand\_share\_per\_timestep\_decision}_{n,i} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{demand\_share\_limit}_{n} \text{ is defined} +``` + +#### Definitions + +**`demand_share_flow_out`** + +```math +\mathit{demand\_share\_flow\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } \left( i,\ c \right) \in \mathrm{demand\_share\_carrier} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`demand_share_sink`** + +```math +\mathrm{demand\_share\_sink}_{n,i,t} = \mathrm{sink\_use\_equals}_{n,\mathrm{decide\_demand\_share}(i),t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +#### Variable domains + +**`demand_share_per_timestep_decision`** + +```math +\mathit{demand\_share\_per\_timestep\_decision}_{n,i} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{decide\_demand\_share}(i) \text{ is defined} +``` + +#### Assumptions + +**`demand_share_is_fraction`** + +```math +\mathrm{demand\_share\_limit}_{n} \ge 0 \wedge \mathrm{demand\_share\_limit}_{n} \le 1 \qquad \forall\, n \in \mathcal{N} \,:\, \mathrm{demand\_share\_limit}_{n} \text{ is defined} +``` + diff --git a/docs/examples/calliope/extensions/fuel_dist.md b/docs/examples/calliope/extensions/fuel_dist.md new file mode 100644 index 00000000..703f79e4 --- /dev/null +++ b/docs/examples/calliope/extensions/fuel_dist.md @@ -0,0 +1,180 @@ + + +# Fuel distribution + +An extension of [Calliope in fragments](../index.md). Calliope's example `fuel_dist.yaml`: carriers that move between nodes with no network. Calliope restates `system_balance` and the objective whole to add the distributor; here it is a term of each sum, and no base file changes. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + allow_fuel_distribution: + description: "`allow_fuel_distribution` — whether a node takes part in distributing a carrier" + dims: [nodes, carriers] + dtype: bool + fuel_import_max: + description: "`fuel_import_max` — the most of a carrier a node imports in a time step; given only where set" + dims: [nodes, carriers] + fuel_export_max: + description: "`fuel_export_max` — the most of a carrier a node exports in a time step; given only where set" + dims: [nodes, carriers] + cost_fuel_distribution: + description: "`cost_fuel_distribution` — the cost of importing one unit of a carrier, and the revenue of exporting it" + dims: [nodes, carriers, costs] + +variables: + fuel_distributor: + description: >- + `fuel_distributor` — what a node imports of a carrier, with no network + behind it; an export is negative + dims: [nodes, carriers, timesteps] + where: allow_fuel_distribution + absence: zero + +expressions: + fuel_dist_carrier_flow: + description: "`+ fuel_distributor` — the term Calliope writes into `system_balance`, by restating it whole" + expression: fuel_distributor + cost_var_fuel_distribution: + description: "`cost_var_fuel_distribution` — the cost of importing, and the revenue of exporting, a carrier" + expression: timestep_weights * fuel_distributor * cost_fuel_distribution + fuel_dist_system_cost: + description: >- + `sum(cost_var_fuel_distribution, …) * objective_cost_weights` — the term + Calliope writes into the objective, by restating it whole + expression: sum(cost_var_fuel_distribution * objective_cost_weights) + +given: + parameters: + timestep_weights: { dims: [timesteps] } + objective_cost_weights: { dims: [costs] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: fuel_dist_carrier_flow } + system_cost: { dims: [], term: fuel_dist_system_cost } + +constraints: + restrict_total_imports_and_exports: + description: >- + `restrict_total_imports_and_exports` — what the nodes import of a + carrier is what they export. Calliope's `where: fuel_distributor` over + a carrier reads as any node distributing it + dims: [carriers, timesteps] + where: count(fuel_distributor, over=nodes) >= 1 + expression: sum(fuel_distributor, over=nodes) == 0 + restrict_nodal_imports: + description: "`restrict_nodal_imports` — a node imports at most its limit" + dims: [nodes, carriers, timesteps] + where: fuel_distributor AND fuel_import_max + expression: fuel_distributor <= fuel_import_max + restrict_nodal_exports: + description: "`restrict_nodal_exports` — a node exports at most its limit" + dims: [nodes, carriers, timesteps] + where: fuel_distributor AND fuel_export_max + expression: -1 * fuel_distributor <= fuel_export_max +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{allow\_fuel\_distribution}`$ | `allow_fuel_distribution` over $`\mathcal{N} \times \mathcal{C}`$ — `allow_fuel_distribution` — whether a node takes part in distributing a carrier | +| $`\mathrm{fuel\_import\_max}`$ | `fuel_import_max` over $`\mathcal{N} \times \mathcal{C}`$ — `fuel_import_max` — the most of a carrier a node imports in a time step; given only where set | +| $`\mathrm{fuel\_export\_max}`$ | `fuel_export_max` over $`\mathcal{N} \times \mathcal{C}`$ — `fuel_export_max` — the most of a carrier a node exports in a time step; given only where set | +| $`\mathrm{cost\_fuel\_distribution}`$ | `cost_fuel_distribution` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{K}`$ — `cost_fuel_distribution` — the cost of importing one unit of a carrier, and the revenue of exporting it | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{fuel\_distributor}`$ | `fuel_distributor` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — `fuel_distributor` — what a node imports of a carrier, with no network behind it; an export is negative | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{objective\_cost\_weights}`$ | `objective_cost_weights` over $`\mathcal{K}`$, data another file declares | +| $`\mathit{carrier\_flow}`$ | `carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$, an expression this file adds `fuel_dist_carrier_flow` to | +| $`\mathit{system\_cost}`$ | `system_cost` (scalar), an expression this file adds `fuel_dist_system_cost` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{fuel\_dist\_carrier\_flow}`$ | `fuel_dist_carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — `+ fuel_distributor` — the term Calliope writes into `system_balance`, by restating it whole | +| $`\mathit{cost\_var\_fuel\_distribution}`$ | `cost_var_fuel_distribution` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{K} \times \mathcal{T}`$ — `cost_var_fuel_distribution` — the cost of importing, and the revenue of exporting, a carrier | +| $`\mathit{fuel\_dist\_system\_cost}`$ | `fuel_dist_system_cost` (scalar) — `sum(cost_var_fuel_distribution, …) * objective_cost_weights` — the term Calliope writes into the objective, by restating it whole | + +Upright is what the data supplies — a parameter such as $`\mathrm{allow\_fuel\_distribution}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{fuel\_distributor}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`restrict_total_imports_and_exports`** + +```math +\sum_{n \in \mathcal{N}} \mathit{fuel\_distributor}_{n,c,t} = 0 \qquad \forall\, c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{fuel\_distributor}_{n,c,t} \text{ exists} \} \rvert \ge 1 +``` + +**`restrict_nodal_imports`** + +```math +\mathit{fuel\_distributor}_{n,c,t} \le \mathrm{fuel\_import\_max}_{n,c} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{fuel\_distributor}_{n,c,t} \text{ exists} \wedge \mathrm{fuel\_import\_max}_{n,c} \text{ is defined} +``` + +**`restrict_nodal_exports`** + +```math +-1 \cdot \mathit{fuel\_distributor}_{n,c,t} \le \mathrm{fuel\_export\_max}_{n,c} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{fuel\_distributor}_{n,c,t} \text{ exists} \wedge \mathrm{fuel\_export\_max}_{n,c} \text{ is defined} +``` + +#### Definitions + +**`fuel_dist_carrier_flow`** + +```math +\mathit{fuel\_dist\_carrier\_flow}_{n,c,t} = \mathit{fuel\_distributor}_{n,c,t} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`cost_var_fuel_distribution`** + +```math +\mathit{cost\_var\_fuel\_distribution}_{n,c,k,t} = \mathrm{timestep\_weights}_{t} \cdot \mathit{fuel\_distributor}_{n,c,t} \cdot \mathrm{cost\_fuel\_distribution}_{n,c,k} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ k \in \mathcal{K},\ t \in \mathcal{T} +``` + +**`fuel_dist_system_cost`** + +```math +\mathit{fuel\_dist\_system\_cost} = \sum_{n \in \mathcal{N},\ c \in \mathcal{C},\ k \in \mathcal{K},\ t \in \mathcal{T}} \mathit{cost\_var\_fuel\_distribution}_{n,c,k,t} \cdot \mathrm{objective\_cost\_weights}_{k} +``` + +#### Variable domains + +**`fuel_distributor`** + +```math +\mathit{fuel\_distributor}_{n,c,t} \in \mathbb{R} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{allow\_fuel\_distribution}_{n,c} +``` + diff --git a/docs/examples/calliope/extensions/max_time_varying.md b/docs/examples/calliope/extensions/max_time_varying.md new file mode 100644 index 00000000..54e9897d --- /dev/null +++ b/docs/examples/calliope/extensions/max_time_varying.md @@ -0,0 +1,74 @@ + + +# Time-varying flow limit + +An extension of [Calliope in fragments](../index.md). Calliope's example `max_time_varying.yaml`: outflow at most a share of the flow capacity that varies in time. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + flow_cap_max_relative_per_ts: + description: "`flow_cap_max_relative_per_ts` — the share of its flow capacity a technology may put out in a time step; given only where set" + dims: [nodes, techs, timesteps] + +given: + parameters: + flow_out_parasitic_eff: { dims: [nodes, techs, carriers, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_cap: { dims: [nodes, techs, carriers] } + +constraints: + max_time_varying_flow_cap: + description: "`max_time_varying_flow_cap` — outflow is at most a share of the flow capacity that varies in time" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND flow_cap_max_relative_per_ts + expression: flow_out <= flow_cap_max_relative_per_ts * flow_cap * flow_out_parasitic_eff +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{flow\_cap\_max\_relative\_per\_ts}`$ | `flow_cap_max_relative_per_ts` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `flow_cap_max_relative_per_ts` — the share of its flow capacity a technology may put out in a time step; given only where set | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{flow\_out\_parasitic\_eff}`$ | `flow_out_parasitic_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | + +#### Subject to + +**`max_time_varying_flow_cap`** + +```math +\mathit{flow\_out}_{n,i,c,t} \le \mathrm{flow\_cap\_max\_relative\_per\_ts}_{n,i,t} \cdot \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_out}_{n,i,c,t} \text{ exists} \wedge \mathrm{flow\_cap\_max\_relative\_per\_ts}_{n,i,t} \text{ is defined} +``` + diff --git a/docs/examples/calliope/extensions/milp.md b/docs/examples/calliope/extensions/milp.md new file mode 100644 index 00000000..8f77bc8b --- /dev/null +++ b/docs/examples/calliope/extensions/milp.md @@ -0,0 +1,582 @@ + + +# MILP + +An extension of [Calliope in fragments](../index.md). What Calliope's `milp.yaml` adds: whole units bought and run, asynchronous flow, and the capacity minimums the units scale. What it changes in the base is [the MILP patch](../variants/milp.md). The purchase cost is a term of `cost_investment`. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + cap_method: + description: >- + `cap_method` — `continuous` or `integer`: whether a technology's + capacity is bought in whole units. Calliope's default is + `continuous`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + integer_dispatch: + description: "`integer_dispatch` — whether a unit-bought technology runs in whole units" + dims: [nodes, techs] + dtype: bool + force_async_flow: + description: "`force_async_flow` — whether a technology may not take in and put out in one time step" + dims: [nodes, techs] + dtype: bool + flow_cap_per_unit: + description: "`flow_cap_per_unit` — the flow capacity of one unit; given only where set" + dims: [nodes, techs] + storage_cap_per_unit: + description: "`storage_cap_per_unit` — the storage capacity of one unit; given only where set" + dims: [nodes, techs] + purchased_units_min: + description: "`purchased_units_min` — least units bought. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + purchased_units_max: + description: "`purchased_units_max` — most units bought. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + purchased_units_min_systemwide: + description: "`purchased_units_min_systemwide` — least units of a technology bought over every node" + dims: [techs] + purchased_units_max_systemwide: + description: "`purchased_units_max_systemwide` — most units of a technology bought over every node; given only where set" + dims: [techs] + cost_purchase: + description: "`cost_purchase` — the cost of one unit bought" + dims: [nodes, techs, costs] + cost_purchase_per_distance: + description: "`cost_purchase_per_distance` — the cost of one unit of a link bought, per unit of distance" + dims: [nodes, techs, costs] + +variables: + purchased_units: + description: "`purchased_units` — how many units of a technology are bought" + dims: [nodes, techs] + where: cap_method == integer + domain: integer + bounds: { lower: purchased_units_min, upper: purchased_units_max } + absence: zero + operating_units: + description: "`operating_units` — how many bought units run in a time step" + dims: [nodes, techs, timesteps] + where: integer_dispatch AND cap_method == integer + domain: integer + bounds: { lower: 0 } + absence: zero + async_flow_switch: + description: "`async_flow_switch` — whether a technology puts out, rather than takes in, in a time step" + dims: [nodes, techs, timesteps] + where: force_async_flow + domain: binary + absence: zero + available_flow_cap: + description: "`available_flow_cap` — the flow capacity in a time step: the whole of it where the technology runs, none where it does not" + dims: [nodes, techs, carriers, timesteps] + where: flow_cap AND integer_dispatch AND flow_cap_max AND NOT flow_cap_per_unit + bounds: { lower: 0 } + absence: zero + +expressions: + cost_investment_purchase: + description: "`cost_investment_purchase` — the investment cost of the units bought; a link's cost is split between its two ends" + dims: [nodes, techs, costs] + cases: + transmission: + when: base_tech == 'transmission' + expression: (cost_purchase + cost_purchase_per_distance * distance) * purchased_units * 0.5 + otherwise: cost_purchase * purchased_units + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + distance: { dims: [techs] } + bigM: { dims: [] } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + flow_cap_min: { dims: [nodes, techs] } + flow_cap_max: { dims: [nodes, techs] } + flow_cap_min_systemwide: { dims: [techs, carriers] } + flow_out_min_relative: { dims: [nodes, techs, timesteps] } + flow_out_parasitic_eff: { dims: [nodes, techs, carriers, timesteps] } + storage_cap_min: { dims: [nodes, techs] } + storage_cap_max: { dims: [nodes, techs] } + area_use_min: { dims: [nodes, techs] } + source_cap_min: { dims: [nodes, techs] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + storage: { dims: [nodes, techs, timesteps] } + storage_cap: { dims: [nodes, techs] } + area_use: { dims: [nodes, techs] } + source_cap: { dims: [nodes, techs] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_purchase } + +constraints: + unit_commitment_milp: + description: "`unit_commitment_milp` — at most the units bought run" + dims: [nodes, techs, timesteps] + where: operating_units AND purchased_units + expression: operating_units <= purchased_units + flow_out_max_milp: + description: "`flow_out_max_milp` — outflow is at most what the running units can put out" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_cap_per_unit + expression: flow_out <= operating_units * timestep_resolution * flow_cap_per_unit * flow_out_parasitic_eff + flow_in_max_milp: + description: "`flow_in_max_milp` — inflow is at most what the running units can take in" + dims: [nodes, techs, carriers, timesteps] + where: flow_in AND operating_units AND flow_cap_per_unit + expression: flow_in <= operating_units * timestep_resolution * flow_cap_per_unit + flow_out_min_milp_per_unit: + description: "`flow_out_min_milp` where `flow_cap_per_unit` is set — outflow is at least the running units' least share" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_out_min_relative AND flow_cap_per_unit + expression: flow_out >= operating_units * timestep_resolution * flow_cap_per_unit * flow_out_min_relative + flow_out_min_milp_available: + description: "`flow_out_min_milp` where the available flow capacity is built — outflow is at least its least share of it" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_out_min_relative AND available_flow_cap + expression: flow_out >= available_flow_cap * timestep_resolution * flow_out_min_relative + storage_capacity_units_milp: + description: "`storage_capacity_units_milp` — storage capacity is the units bought times the capacity of one" + dims: [nodes, techs] + where: storage_cap AND purchased_units AND storage_cap_per_unit + expression: storage_cap == purchased_units * storage_cap_per_unit + flow_capacity_units_milp: + description: "`flow_capacity_units_milp` — flow capacity is the units bought times the capacity of one" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND flow_cap_per_unit + expression: flow_cap == purchased_units * flow_cap_per_unit + flow_capacity_max_purchase_milp: + description: "`flow_capacity_max_purchase_milp` where `flow_cap_max` is set — no flow capacity unless a unit is bought" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND flow_cap_max + expression: flow_cap <= flow_cap_max * purchased_units + flow_capacity_max_purchase_milp_big_m: + description: "`flow_capacity_max_purchase_milp` where `flow_cap_max` is not set — the same, with `bigM` for the maximum" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND NOT flow_cap_max + expression: flow_cap <= bigM * purchased_units + storage_capacity_max_purchase_milp: + description: "`storage_capacity_max_purchase_milp` — no storage capacity unless a unit is bought" + dims: [nodes, techs] + where: purchased_units AND storage_cap_max + expression: storage_cap <= storage_cap_max * purchased_units + unit_capacity_max_systemwide_milp: + description: "`unit_capacity_max_systemwide_milp` — the units of a technology bought over every node are at most its system-wide maximum" + dims: [techs] + where: count(purchased_units, over=nodes) >= 1 AND purchased_units_max_systemwide + expression: sum(purchased_units, over=nodes) <= purchased_units_max_systemwide + unit_capacity_min_systemwide_milp: + description: >- + `unit_capacity_min_systemwide_milp` — the units of a technology bought + over every node are at least its system-wide minimum. Calliope builds + it where the system-wide maximum is set, as here + dims: [techs] + where: count(purchased_units, over=nodes) >= 1 AND purchased_units_max_systemwide + expression: sum(purchased_units, over=nodes) >= purchased_units_min_systemwide + async_flow_in_milp: + description: "`async_flow_in_milp` — no inflow in a time step the switch gives to outflow" + dims: [nodes, techs, timesteps] + where: async_flow_switch + expression: sum(flow_in, over=carriers) <= (1 - async_flow_switch) * bigM + async_flow_out_milp: + description: "`async_flow_out_milp` — no outflow in a time step the switch gives to inflow" + dims: [nodes, techs, timesteps] + where: async_flow_switch + expression: sum(flow_out, over=carriers) <= async_flow_switch * bigM + available_flow_cap_continuous: + description: "`available_flow_cap_continuous` — the available flow capacity is at most the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap <= flow_cap + available_flow_cap_binary: + description: "`available_flow_cap_binary` — the available flow capacity is zero where no unit runs" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap <= flow_cap_max * operating_units + available_flow_cap_max_binary_continuous_switch: + description: "`available_flow_cap_max_binary_continuous_switch` — the available flow capacity is the whole flow capacity where the units run" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap >= flow_cap + (operating_units - purchased_units) * flow_cap_max + flow_capacity_minimum: + description: "`flow_capacity_minimum` where no unit is bought — flow capacity is at least its least" + dims: [nodes, techs, carriers] + where: flow_cap AND flow_cap_min AND NOT purchased_units + expression: flow_cap >= flow_cap_min + flow_capacity_minimum_purchased: + description: "`flow_capacity_minimum` where units are bought — flow capacity is at least its least, if a unit is bought" + dims: [nodes, techs, carriers] + where: flow_cap AND flow_cap_min AND purchased_units + expression: flow_cap >= flow_cap_min * purchased_units + storage_capacity_minimum: + description: "`storage_capacity_minimum` where no unit is bought — storage capacity is at least its least" + dims: [nodes, techs] + where: storage_cap_min AND NOT purchased_units + expression: storage_cap >= storage_cap_min + storage_capacity_minimum_purchased: + description: "`storage_capacity_minimum` where units are bought — storage capacity is at least its least, if a unit is bought" + dims: [nodes, techs] + where: storage_cap_min AND purchased_units + expression: storage_cap >= storage_cap_min * purchased_units + area_use_minimum: + description: "`area_use_minimum` where no unit is bought — area use is at least its least" + dims: [nodes, techs] + where: area_use_min AND NOT purchased_units + expression: area_use >= area_use_min + area_use_minimum_purchased: + description: "`area_use_minimum` where units are bought — area use is at least its least, if a unit is bought" + dims: [nodes, techs] + where: area_use_min AND purchased_units + expression: area_use >= area_use_min * purchased_units + source_capacity_minimum: + description: "`source_capacity_minimum` where no unit is bought — source capacity is at least its least" + dims: [nodes, techs] + where: base_tech == 'supply' AND source_cap_min AND NOT purchased_units + expression: source_cap >= source_cap_min + source_capacity_minimum_purchased: + description: "`source_capacity_minimum` where units are bought — source capacity is at least its least, if a unit is bought" + dims: [nodes, techs] + where: base_tech == 'supply' AND source_cap_min AND purchased_units + expression: source_cap >= source_cap_min * purchased_units + flow_capacity_systemwide_min_purchased: + description: >- + `flow_capacity_systemwide_min` where units are bought — the flow + capacity over every node is at least the system-wide minimum times the + units bought. The patch narrows the base row to where none are + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide AND count(purchased_units, over=nodes) >= 1 + expression: sum(flow_cap, over=nodes) >= flow_cap_min_systemwide * sum(purchased_units, over=nodes) + +assumptions: + distance_only_for_transmission_milp: + description: Calliope's `distance_only_for_transmission_milp` — only a link sets a per-distance purchase cost + holds: base_tech == 'transmission' OR NOT cost_purchase_per_distance + conflicting_flow_caps: + description: Calliope's `conflicting_flow_caps` — a technology sets a capacity per unit or a capacity range, not both + holds: NOT ((flow_cap_max OR flow_cap_min) AND flow_cap_per_unit) + unit_commitment_only_for_units: + description: Calliope's `unit_commitment_only_for_units` — integer dispatch needs integer units + holds: NOT integer_dispatch OR cap_method == integer + conflicting_storage_caps: + description: Calliope's `conflicting_storage_caps` — a technology sets a storage capacity per unit or a range, not both + holds: NOT ((storage_cap_max OR storage_cap_min) AND storage_cap_per_unit) + cap_method_one_of: + description: Calliope's `one_of` on `cap_method` + holds: cap_method == continuous OR cap_method == integer + where: cap_method +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{cap\_method}`$ | `cap_method` over $`\mathcal{N} \times \mathcal{I}`$ — `cap_method` — `continuous` or `integer`: whether a technology's capacity is bought in whole units. Calliope's default is `continuous`, which is what a technology with no row reads as | +| $`\mathrm{integer\_dispatch}`$ | `integer_dispatch` over $`\mathcal{N} \times \mathcal{I}`$ — `integer_dispatch` — whether a unit-bought technology runs in whole units | +| $`\mathrm{force\_async\_flow}`$ | `force_async_flow` over $`\mathcal{N} \times \mathcal{I}`$ — `force_async_flow` — whether a technology may not take in and put out in one time step | +| $`\mathrm{flow\_cap\_per\_unit}`$ | `flow_cap_per_unit` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_cap_per_unit` — the flow capacity of one unit; given only where set | +| $`\mathrm{storage}^{\mathrm{cap,per,unit}}`$ | `storage_cap_per_unit` over $`\mathcal{N} \times \mathcal{I}`$ — `storage_cap_per_unit` — the storage capacity of one unit; given only where set | +| $`\mathrm{purchased\_units\_min}`$ | `purchased_units_min` over $`\mathcal{N} \times \mathcal{I}`$ — `purchased_units_min` — least units bought. Calliope's default is 0, and data prep fills it | +| $`\mathrm{purchased\_units\_max}`$ | `purchased_units_max` over $`\mathcal{N} \times \mathcal{I}`$ — `purchased_units_max` — most units bought. Calliope's default is `.inf`, and data prep fills it | +| $`\mathrm{purchased\_units\_min\_systemwide}`$ | `purchased_units_min_systemwide` over $`\mathcal{I}`$ — `purchased_units_min_systemwide` — least units of a technology bought over every node | +| $`\mathrm{purchased\_units\_max\_systemwide}`$ | `purchased_units_max_systemwide` over $`\mathcal{I}`$ — `purchased_units_max_systemwide` — most units of a technology bought over every node; given only where set | +| $`\mathrm{cost\_purchase}`$ | `cost_purchase` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_purchase` — the cost of one unit bought | +| $`\mathrm{cost\_purchase\_per\_distance}`$ | `cost_purchase_per_distance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_purchase_per_distance` — the cost of one unit of a link bought, per unit of distance | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{purchased\_units}`$ | `purchased_units` over $`\mathcal{N} \times \mathcal{I}`$ — `purchased_units` — how many units of a technology are bought | +| $`\mathit{operating\_units}`$ | `operating_units` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `operating_units` — how many bought units run in a time step | +| $`\mathit{async\_flow\_switch}`$ | `async_flow_switch` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `async_flow_switch` — whether a technology puts out, rather than takes in, in a time step | +| $`\mathit{available\_flow\_cap}`$ | `available_flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `available_flow_cap` — the flow capacity in a time step: the whole of it where the technology runs, none where it does not | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{distance}`$ | `distance` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{bigM}`$ | `bigM` (scalar), data another file declares | +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{flow\_cap\_min}`$ | `flow_cap_min` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{flow\_cap\_max}`$ | `flow_cap_max` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{flow\_cap\_min\_systemwide}`$ | `flow_cap_min_systemwide` over $`\mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{flow\_out\_min\_relative}`$ | `flow_out_min_relative` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$, data another file declares | +| $`\mathrm{flow\_out\_parasitic\_eff}`$ | `flow_out_parasitic_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, data another file declares | +| $`\mathrm{storage}^{\mathrm{cap,min}}`$ | `storage_cap_min` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{storage}^{\mathrm{cap,max}}`$ | `storage_cap_max` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{area\_use\_min}`$ | `area_use_min` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{source\_cap\_min}`$ | `source_cap_min` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{storage}`$ | `storage` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ | +| $`\mathit{storage}^{\mathrm{cap}}`$ | `storage_cap` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{area\_use}`$ | `area_use` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{source\_cap}`$ | `source_cap` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_investment_purchase` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{cost\_investment\_purchase}`$ | `cost_investment_purchase` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment_purchase` — the investment cost of the units bought; a link's cost is split between its two ends | + +Upright is what the data supplies — a parameter such as $`\mathrm{cap\_method}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{purchased\_units}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`unit_commitment_milp`** + +```math +\mathit{operating\_units}_{n,i,t} \le \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{operating\_units}_{n,i,t} \text{ exists} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + +**`flow_out_max_milp`** + +```math +\mathit{flow\_out}_{n,i,c,t} \le \mathit{operating\_units}_{n,i,t} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_cap\_per\_unit}_{n,i} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_out}_{n,i,c,t} \text{ exists} \wedge \mathit{operating\_units}_{n,i,t} \text{ exists} \wedge \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} +``` + +**`flow_in_max_milp`** + +```math +\mathit{flow\_in}_{n,i,c,t} \le \mathit{operating\_units}_{n,i,t} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_cap\_per\_unit}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_in}_{n,i,c,t} \text{ exists} \wedge \mathit{operating\_units}_{n,i,t} \text{ exists} \wedge \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} +``` + +**`flow_out_min_milp_per_unit`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge \mathit{operating\_units}_{n,i,t} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_cap\_per\_unit}_{n,i} \cdot \mathrm{flow\_out\_min\_relative}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_out}_{n,i,c,t} \text{ exists} \wedge \mathit{operating\_units}_{n,i,t} \text{ exists} \wedge \mathrm{flow\_out\_min\_relative}_{n,i,t} \text{ is defined} \wedge \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} +``` + +**`flow_out_min_milp_available`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge \mathit{available\_flow\_cap}_{n,i,c,t} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_out\_min\_relative}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_out}_{n,i,c,t} \text{ exists} \wedge \mathit{operating\_units}_{n,i,t} \text{ exists} \wedge \mathrm{flow\_out\_min\_relative}_{n,i,t} \text{ is defined} \wedge \mathit{available\_flow\_cap}_{n,i,c,t} \text{ exists} +``` + +**`storage_capacity_units_milp`** + +```math +\mathit{storage}^{\mathrm{cap}}_{n,i} = \mathit{purchased\_units}_{n,i} \cdot \mathrm{storage}^{\mathrm{cap,per,unit}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathit{storage}^{\mathrm{cap}}_{n,i} \text{ exists} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} \wedge \mathrm{storage}^{\mathrm{cap,per,unit}}_{n,i} \text{ is defined} +``` + +**`flow_capacity_units_milp`** + +```math +\mathit{flow\_cap}_{n,i,c} = \mathit{purchased\_units}_{n,i} \cdot \mathrm{flow\_cap\_per\_unit}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} \wedge \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} +``` + +**`flow_capacity_max_purchase_milp`** + +```math +\mathit{flow\_cap}_{n,i,c} \le \mathrm{flow\_cap\_max}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} \wedge \mathrm{flow\_cap\_max}_{n,i} \text{ is defined} +``` + +**`flow_capacity_max_purchase_milp_big_m`** + +```math +\mathit{flow\_cap}_{n,i,c} \le \mathrm{bigM} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} \wedge \neg \left( \mathrm{flow\_cap\_max}_{n,i} \text{ is defined} \right) +``` + +**`storage_capacity_max_purchase_milp`** + +```math +\mathit{storage}^{\mathrm{cap}}_{n,i} \le \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathit{purchased\_units}_{n,i} \text{ exists} \wedge \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \text{ is defined} +``` + +**`unit_capacity_max_systemwide_milp`** + +```math +\sum_{n \in \mathcal{N}} \mathit{purchased\_units}_{n,i} \le \mathrm{purchased\_units\_max\_systemwide}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{purchased\_units}_{n,i} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{purchased\_units\_max\_systemwide}_{i} \text{ is defined} +``` + +**`unit_capacity_min_systemwide_milp`** + +```math +\sum_{n \in \mathcal{N}} \mathit{purchased\_units}_{n,i} \ge \mathrm{purchased\_units\_min\_systemwide}_{i} \qquad \forall\, i \in \mathcal{I} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{purchased\_units}_{n,i} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{purchased\_units\_max\_systemwide}_{i} \text{ is defined} +``` + +**`async_flow_in_milp`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_in}_{n,i,c,t} \le \left( 1 - \mathit{async\_flow\_switch}_{n,i,t} \right) \cdot \mathrm{bigM} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{async\_flow\_switch}_{n,i,t} \text{ exists} +``` + +**`async_flow_out_milp`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out}_{n,i,c,t} \le \mathit{async\_flow\_switch}_{n,i,t} \cdot \mathrm{bigM} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{async\_flow\_switch}_{n,i,t} \text{ exists} +``` + +**`available_flow_cap_continuous`** + +```math +\mathit{available\_flow\_cap}_{n,i,c,t} \le \mathit{flow\_cap}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{available\_flow\_cap}_{n,i,c,t} \text{ exists} +``` + +**`available_flow_cap_binary`** + +```math +\mathit{available\_flow\_cap}_{n,i,c,t} \le \mathrm{flow\_cap\_max}_{n,i} \cdot \mathit{operating\_units}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{available\_flow\_cap}_{n,i,c,t} \text{ exists} +``` + +**`available_flow_cap_max_binary_continuous_switch`** + +```math +\mathit{available\_flow\_cap}_{n,i,c,t} \ge \mathit{flow\_cap}_{n,i,c} + \left( \mathit{operating\_units}_{n,i,t} - \mathit{purchased\_units}_{n,i} \right) \cdot \mathrm{flow\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{available\_flow\_cap}_{n,i,c,t} \text{ exists} +``` + +**`flow_capacity_minimum`** + +```math +\mathit{flow\_cap}_{n,i,c} \ge \mathrm{flow\_cap\_min}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathrm{flow\_cap\_min}_{n,i} \text{ is defined} \wedge \neg \left( \mathit{purchased\_units}_{n,i} \text{ exists} \right) +``` + +**`flow_capacity_minimum_purchased`** + +```math +\mathit{flow\_cap}_{n,i,c} \ge \mathrm{flow\_cap\_min}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathrm{flow\_cap\_min}_{n,i} \text{ is defined} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + +**`storage_capacity_minimum`** + +```math +\mathit{storage}^{\mathrm{cap}}_{n,i} \ge \mathrm{storage}^{\mathrm{cap,min}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{storage}^{\mathrm{cap,min}}_{n,i} \text{ is defined} \wedge \neg \left( \mathit{purchased\_units}_{n,i} \text{ exists} \right) +``` + +**`storage_capacity_minimum_purchased`** + +```math +\mathit{storage}^{\mathrm{cap}}_{n,i} \ge \mathrm{storage}^{\mathrm{cap,min}}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{storage}^{\mathrm{cap,min}}_{n,i} \text{ is defined} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + +**`area_use_minimum`** + +```math +\mathit{area\_use}_{n,i} \ge \mathrm{area\_use\_min}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{area\_use\_min}_{n,i} \text{ is defined} \wedge \neg \left( \mathit{purchased\_units}_{n,i} \text{ exists} \right) +``` + +**`area_use_minimum_purchased`** + +```math +\mathit{area\_use}_{n,i} \ge \mathrm{area\_use\_min}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{area\_use\_min}_{n,i} \text{ is defined} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + +**`source_capacity_minimum`** + +```math +\mathit{source\_cap}_{n,i} \ge \mathrm{source\_cap\_min}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \wedge \mathrm{source\_cap\_min}_{n,i} \text{ is defined} \wedge \neg \left( \mathit{purchased\_units}_{n,i} \text{ exists} \right) +``` + +**`source_capacity_minimum_purchased`** + +```math +\mathit{source\_cap}_{n,i} \ge \mathrm{source\_cap\_min}_{n,i} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \wedge \mathrm{source\_cap\_min}_{n,i} \text{ is defined} \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + +**`flow_capacity_systemwide_min_purchased`** + +```math +\sum_{n \in \mathcal{N}} \mathit{flow\_cap}_{n,i,c} \ge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \cdot \left( \sum_{n \in \mathcal{N}} \mathit{purchased\_units}_{n,i} \right) \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \text{ is defined} \wedge \lvert \{ n \in \mathcal{N} \,:\, \mathit{purchased\_units}_{n,i} \text{ exists} \} \rvert \ge 1 +``` + +#### Definitions + +**`cost_investment_purchase`** + +```math +\mathit{cost\_investment\_purchase}_{n,i,k} = \begin{cases} \left( \mathrm{cost\_purchase}_{n,i,k} + \mathrm{cost\_purchase\_per\_distance}_{n,i,k} \cdot \mathrm{distance}_{i} \right) \cdot \mathit{purchased\_units}_{n,i} \cdot 0.5 & \text{if } \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \\ \mathrm{cost\_purchase}_{n,i,k} \cdot \mathit{purchased\_units}_{n,i} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`purchased_units`** + +```math +\mathrm{purchased\_units\_min}_{n,i} \le \mathit{purchased\_units}_{n,i} \le \mathrm{purchased\_units\_max}_{n,i}, \mathit{purchased\_units}_{n,i} \in \mathbb{Z} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{cap\_method}_{n,i} = \text{'}\mathrm{integer}\text{'} +``` + +**`operating_units`** + +```math +\mathit{operating\_units}_{n,i,t} \ge 0, \mathit{operating\_units}_{n,i,t} \in \mathbb{Z} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{integer\_dispatch}_{n,i} \wedge \mathrm{cap\_method}_{n,i} = \text{'}\mathrm{integer}\text{'} +``` + +**`async_flow_switch`** + +```math +\mathit{async\_flow\_switch}_{n,i,t} \in \{0, 1\} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{force\_async\_flow}_{n,i} +``` + +**`available_flow_cap`** + +```math +\mathit{available\_flow\_cap}_{n,i,c,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathrm{integer\_dispatch}_{n,i} \wedge \mathrm{flow\_cap\_max}_{n,i} \text{ is defined} \wedge \neg \left( \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} \right) +``` + +#### Assumptions + +**`distance_only_for_transmission_milp`** + +```math +\mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \vee \neg \left( \mathrm{cost\_purchase\_per\_distance}_{n,i,k} \text{ is defined} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`conflicting_flow_caps`** + +```math +\neg \left( \left( \mathrm{flow\_cap\_max}_{n,i} \text{ is defined} \vee \mathrm{flow\_cap\_min}_{n,i} \text{ is defined} \right) \wedge \mathrm{flow\_cap\_per\_unit}_{n,i} \text{ is defined} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +**`unit_commitment_only_for_units`** + +```math +\neg \mathrm{integer\_dispatch}_{n,i} \vee \mathrm{cap\_method}_{n,i} = \text{'}\mathrm{integer}\text{'} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +**`conflicting_storage_caps`** + +```math +\neg \left( \left( \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \text{ is defined} \vee \mathrm{storage}^{\mathrm{cap,min}}_{n,i} \text{ is defined} \right) \wedge \mathrm{storage}^{\mathrm{cap,per,unit}}_{n,i} \text{ is defined} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +**`cap_method_one_of`** + +```math +\mathrm{cap\_method}_{n,i} = \text{'}\mathrm{continuous}\text{'} \vee \mathrm{cap\_method}_{n,i} = \text{'}\mathrm{integer}\text{'} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{cap\_method}_{n,i} \text{ is defined} +``` + diff --git a/docs/examples/calliope/extensions/monthly_peak_flow_charge.md b/docs/examples/calliope/extensions/monthly_peak_flow_charge.md new file mode 100644 index 00000000..0e035199 --- /dev/null +++ b/docs/examples/calliope/extensions/monthly_peak_flow_charge.md @@ -0,0 +1,141 @@ + + +# Monthly peak flow charge + +An extension of [Calliope in fragments](../index.md). Calliope's example `monthly_peak_flow_charge.yaml`: a cost on the peak outflow of each month. Calliope restates `cost_operation_fixed` to add it; here it is a term. The month of a time step is a relation, so the row is one per time step, not one per time step and month. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + months: + description: Calliope's `months` — the months of the year + dtype: int + +relations: + lookup_month: + description: >- + `lookup_month` — the month a time step falls in. Calliope ships it as a + boolean table over time step and month, and builds its row over both; + as a relation the row is one per time step + key: timesteps + values: months + +parameters: + monthly_peak_mode: + description: "`monthly_peak_mode` — whether a technology's peak outflow in a month is priced" + dims: [nodes, techs, carriers] + dtype: bool + cost_month_peak: + description: "`cost_month_peak` — the cost of one unit of peak outflow in a month" + dims: [nodes, techs, costs] + +variables: + flow_peak_month: + description: "`flow_peak_month` — a technology's peak outflow in a month" + dims: [nodes, techs, carriers, months] + where: carrier_out AND monthly_peak_mode + bounds: { lower: 0, upper: flow_cap_max } + absence: zero + +expressions: + cost_month_peak_charge: + description: "`sum(cost_month_peak * flow_peak_month, over=[carriers, months])` — the term Calliope writes into `cost_operation_fixed`, by restating it whole" + expression: sum(cost_month_peak * flow_peak_month, over=[carriers, months]) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + flow_cap_max: { dims: [nodes, techs] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + cost_operation_fixed: { dims: [nodes, techs, costs], term: cost_month_peak_charge } + +constraints: + set_peak_month_flow: + description: "`set_peak_month_flow` — the peak outflow in a month is at least the outflow in each of its time steps" + dims: [nodes, techs, carriers, timesteps] + where: at(flow_peak_month, by=lookup_month, over=months, into=timesteps) + expression: flow_out <= at(flow_peak_month, by=lookup_month, over=months, into=timesteps) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` with $`\mathrm{lookup\_month}: \mathcal{T} \to \mathcal{M}`$ — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{M}`$ | index $`m`$ — `months` with $`\mathrm{lookup\_month}: \mathcal{T} \to \mathcal{M}`$ — Calliope's `months` — the months of the year | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{monthly\_peak\_mode}`$ | `monthly_peak_mode` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `monthly_peak_mode` — whether a technology's peak outflow in a month is priced | +| $`\mathrm{cost\_month\_peak}`$ | `cost_month_peak` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_month_peak` — the cost of one unit of peak outflow in a month | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_peak\_month}`$ | `flow_peak_month` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{M}`$ — `flow_peak_month` — a technology's peak outflow in a month | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{flow\_cap\_max}`$ | `flow_cap_max` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{cost\_operation\_fixed}`$ | `cost_operation_fixed` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_month_peak_charge` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{cost\_month\_peak\_charge}`$ | `cost_month_peak_charge` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `sum(cost_month_peak * flow_peak_month, over=[carriers, months])` — the term Calliope writes into `cost_operation_fixed`, by restating it whole | + +Upright is what the data supplies — a parameter such as $`\mathrm{monthly\_peak\_mode}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{flow\_peak\_month}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`set_peak_month_flow`** + +```math +\mathit{flow\_out}_{n,i,c,t} \le \mathit{flow\_peak\_month}_{n,i,c,\mathrm{lookup\_month}(t)} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_peak\_month}_{n,i,c,\mathrm{lookup\_month}(t)} \text{ exists} +``` + +#### Definitions + +**`cost_month_peak_charge`** + +```math +\mathit{cost\_month\_peak\_charge}_{n,i,k} = \sum_{c \in \mathcal{C},\ m \in \mathcal{M}} \mathrm{cost\_month\_peak}_{n,i,k} \cdot \mathit{flow\_peak\_month}_{n,i,c,m} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`flow_peak_month`** + +```math +0 \le \mathit{flow\_peak\_month}_{n,i,c,m} \le \mathrm{flow\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ m \in \mathcal{M} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \mathrm{monthly\_peak\_mode}_{n,i,c} +``` + diff --git a/docs/examples/calliope/extensions/net_import_share.md b/docs/examples/calliope/extensions/net_import_share.md new file mode 100644 index 00000000..f34935e2 --- /dev/null +++ b/docs/examples/calliope/extensions/net_import_share.md @@ -0,0 +1,193 @@ + + +# Net import share + +An extension of [Calliope in fragments](../index.md). Calliope's example `net_import_share.yaml`: imports over transmission at most a share of a node's own balance, per time step, per year, and over a group of nodes. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + net_import_share: + description: >- + `net_import_share` — the share of a node's flows that imports may make + up. Calliope's default is 1, and data prep fills it. Calliope reads it + per node and in a row over a group of nodes; a parameter here has one + shape, so it is one number + dims: [] + +expressions: + flow_out_transmission_techs: + description: "`flow_out_transmission_techs` — the outflow of transmission technologies, that is, imports" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: carrier_out AND base_tech == 'transmission' + expression: flow_out + otherwise: 0 + electricity_imports: + description: "`flow_out_transmission_techs[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_transmission_techs + otherwise: 0 + electricity_balance: + description: "`$total_energy_balance` — the outflow of electricity at a node, less its inflow" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out - flow_in + otherwise: 0 + node_group_heat_imports: + description: "`flow_out_transmission_techs[nodes=$node_group, carriers=$carrier]` — heat imports at nodes `a` and `c`" + dims: [nodes, techs, carriers, timesteps] + cases: + group: + when: (nodes == 'a' OR nodes == 'c') AND carriers == heat + expression: flow_out_transmission_techs + otherwise: 0 + node_group_heat_balance: + description: "`$total_energy_balance` of the node group — the outflow of heat at nodes `a` and `c`, less its inflow" + dims: [nodes, techs, carriers, timesteps] + cases: + group: + when: (nodes == 'a' OR nodes == 'c') AND carriers == heat + expression: flow_out - flow_in + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + net_import_share_max: + description: >- + `net_import_share_max` — electricity imports at a node are at most + their share of its electricity balance in each time step. Calliope's + `where: any(flow_out_transmission_techs, over=techs)` reads as a link + at the node putting out any carrier + dims: [nodes, timesteps] + where: count(count(carrier_out, over=carriers) >= 1 AND base_tech == 'transmission', over=techs) >= 1 + expression: >- + net_import_share * sum(electricity_imports, over=[techs, carriers]) + <= sum(electricity_balance, over=[techs, carriers]) + net_annual_import_share_max: + description: "`net_annual_import_share_max` — electricity imports at a node are at most their share of its electricity balance over the year" + dims: [nodes] + where: count(count(carrier_out, over=carriers) >= 1 AND base_tech == 'transmission', over=techs) >= 1 + expression: >- + net_import_share * sum(electricity_imports, over=[techs, carriers, timesteps]) + <= sum(electricity_balance, over=[techs, carriers, timesteps]) + net_annual_import_share_max_node_group: + description: "`net_annual_import_share_max_node_group` — heat imports at nodes `a` and `c` are at most their share of the group's heat balance over the year" + dims: [] + expression: net_import_share * sum(node_group_heat_imports) <= sum(node_group_heat_balance) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{net\_import\_share}`$ | `net_import_share` (scalar) — `net_import_share` — the share of a node's flows that imports may make up. Calliope's default is 1, and data prep fills it. Calliope reads it per node and in a row over a group of nodes; a parameter here has one shape, so it is one number | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out\_transmission\_techs}`$ | `flow_out_transmission_techs` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_transmission_techs` — the outflow of transmission technologies, that is, imports | +| $`\mathit{electricity\_imports}`$ | `electricity_imports` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_transmission_techs[carriers=electricity]` | +| $`\mathit{electricity\_balance}`$ | `electricity_balance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `$total_energy_balance` — the outflow of electricity at a node, less its inflow | +| $`\mathit{node\_group\_heat\_imports}`$ | `node_group_heat_imports` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_transmission_techs[nodes=$node_group, carriers=$carrier]` — heat imports at nodes `a` and `c` | +| $`\mathit{node\_group\_heat\_balance}`$ | `node_group_heat_balance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `$total_energy_balance` of the node group — the outflow of heat at nodes `a` and `c`, less its inflow | + +#### Subject to + +**`net_import_share_max`** + +```math +\mathrm{net\_import\_share} \cdot \left( \sum_{i \in \mathcal{I},\ c \in \mathcal{C}} \mathit{electricity\_imports}_{n,i,c,t} \right) \le \sum_{i \in \mathcal{I},\ c \in \mathcal{C}} \mathit{electricity\_balance}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ t \in \mathcal{T} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \lvert \{ c \in \mathcal{C} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \} \rvert \ge 1 +``` + +**`net_annual_import_share_max`** + +```math +\mathrm{net\_import\_share} \cdot \left( \sum_{i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{electricity\_imports}_{n,i,c,t} \right) \le \sum_{i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{electricity\_balance}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \lvert \{ c \in \mathcal{C} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \} \rvert \ge 1 +``` + +**`net_annual_import_share_max_node_group`** + +```math +\mathrm{net\_import\_share} \cdot \left( \sum_{n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{node\_group\_heat\_imports}_{n,i,c,t} \right) \le \sum_{n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{node\_group\_heat\_balance}_{n,i,c,t} +``` + +#### Definitions + +**`flow_out_transmission_techs`** + +```math +\mathit{flow\_out\_transmission\_techs}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } \mathrm{carrier\_out}_{n,i,c} \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`electricity_imports`** + +```math +\mathit{electricity\_imports}_{n,i,c,t} = \begin{cases} \mathit{flow\_out\_transmission\_techs}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`electricity_balance`** + +```math +\mathit{electricity\_balance}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} - \mathit{flow\_in}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`node_group_heat_imports`** + +```math +\mathit{node\_group\_heat\_imports}_{n,i,c,t} = \begin{cases} \mathit{flow\_out\_transmission\_techs}_{n,i,c,t} & \text{if } \left( n = \text{'}\mathrm{a}\text{'} \vee n = \text{'}\mathrm{c}\text{'} \right) \wedge c = \text{'}\mathrm{heat}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`node_group_heat_balance`** + +```math +\mathit{node\_group\_heat\_balance}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} - \mathit{flow\_in}_{n,i,c,t} & \text{if } \left( n = \text{'}\mathrm{a}\text{'} \vee n = \text{'}\mathrm{c}\text{'} \right) \wedge c = \text{'}\mathrm{heat}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/extensions/piecewise_linear_costs.md b/docs/examples/calliope/extensions/piecewise_linear_costs.md new file mode 100644 index 00000000..e3991760 --- /dev/null +++ b/docs/examples/calliope/extensions/piecewise_linear_costs.md @@ -0,0 +1,128 @@ + + +# Piecewise linear costs + +An extension of [Calliope in fragments](../index.md). Calliope's example `piecewise_linear_costs.yaml`: a convex investment cost as the upper envelope of lines, which needs the units of the MILP file. The cost is a term of `cost_investment`. It declares the variable the SOS2 example declares, so the two do not compose. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + pieces: + description: Calliope's `pieces` — the lines a piecewise curve is the upper envelope of + dtype: int + +parameters: + cost_flow_cap_piecewise_slopes: + description: "`cost_flow_cap_piecewise_slopes` — the slope of each line of a convex investment cost curve" + dims: [nodes, techs, costs, pieces] + cost_flow_cap_piecewise_intercept: + description: "`cost_flow_cap_piecewise_intercept` — the intercept of each line of a convex investment cost curve" + dims: [nodes, techs, costs, pieces] + +variables: + piecewise_cost_investment: + description: "`piecewise_cost_investment` — an investment cost that grows faster the more capacity is built" + dims: [nodes, techs, costs] + where: >- + count(cost_flow_cap_piecewise_slopes, over=pieces) >= 1 + AND count(cost_flow_cap_piecewise_intercept, over=pieces) >= 1 AND purchased_units + bounds: { lower: 0 } + absence: zero + +given: + variables: + flow_cap: { dims: [nodes, techs, carriers] } + purchased_units: { dims: [nodes, techs] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: piecewise_cost_investment_term } + +expressions: + piecewise_cost_investment_term: + description: "`+ piecewise_cost_investment` — the term Calliope writes into `cost_investment`, by restating it whole" + expression: piecewise_cost_investment + +constraints: + piecewise_costs: + description: "`piecewise_costs` — the investment cost is at least every line of the curve, so at least the curve" + dims: [nodes, techs, costs, pieces] + where: piecewise_cost_investment + expression: >- + piecewise_cost_investment >= sum(cost_flow_cap_piecewise_slopes * flow_cap, over=carriers) + + cost_flow_cap_piecewise_intercept * purchased_units +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{P}`$ | index $`p`$ — `pieces` — Calliope's `pieces` — the lines a piecewise curve is the upper envelope of | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{cost\_flow\_cap\_piecewise\_slopes}`$ | `cost_flow_cap_piecewise_slopes` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{P}`$ — `cost_flow_cap_piecewise_slopes` — the slope of each line of a convex investment cost curve | +| $`\mathrm{cost\_flow\_cap\_piecewise\_intercept}`$ | `cost_flow_cap_piecewise_intercept` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{P}`$ — `cost_flow_cap_piecewise_intercept` — the intercept of each line of a convex investment cost curve | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{piecewise\_cost\_investment}`$ | `piecewise_cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `piecewise_cost_investment` — an investment cost that grows faster the more capacity is built | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{purchased\_units}`$ | `purchased_units` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `piecewise_cost_investment_term` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{piecewise\_cost\_investment\_term}`$ | `piecewise_cost_investment_term` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `+ piecewise_cost_investment` — the term Calliope writes into `cost_investment`, by restating it whole | + +Upright is what the data supplies — a parameter such as $`\mathrm{cost\_flow\_cap\_piecewise\_slopes}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{piecewise\_cost\_investment}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`piecewise_costs`** + +```math +\mathit{piecewise\_cost\_investment}_{n,i,k} \ge \sum_{c \in \mathcal{C}} \mathrm{cost\_flow\_cap\_piecewise\_slopes}_{n,i,k,p} \cdot \mathit{flow\_cap}_{n,i,c} + \mathrm{cost\_flow\_cap\_piecewise\_intercept}_{n,i,k,p} \cdot \mathit{purchased\_units}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K},\ p \in \mathcal{P} \,:\, \mathit{piecewise\_cost\_investment}_{n,i,k} \text{ exists} +``` + +#### Definitions + +**`piecewise_cost_investment_term`** + +```math +\mathit{piecewise\_cost\_investment\_term}_{n,i,k} = \mathit{piecewise\_cost\_investment}_{n,i,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`piecewise_cost_investment`** + +```math +\mathit{piecewise\_cost\_investment}_{n,i,k} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} \,:\, \lvert \{ p \in \mathcal{P} \,:\, \mathrm{cost\_flow\_cap\_piecewise\_slopes}_{n,i,k,p} \text{ is defined} \} \rvert \ge 1 \wedge \lvert \{ p \in \mathcal{P} \,:\, \mathrm{cost\_flow\_cap\_piecewise\_intercept}_{n,i,k,p} \text{ is defined} \} \rvert \ge 1 \wedge \mathit{purchased\_units}_{n,i} \text{ exists} +``` + diff --git a/docs/examples/calliope/extensions/piecewise_linear_efficiency.md b/docs/examples/calliope/extensions/piecewise_linear_efficiency.md new file mode 100644 index 00000000..93ddb157 --- /dev/null +++ b/docs/examples/calliope/extensions/piecewise_linear_efficiency.md @@ -0,0 +1,86 @@ + + +# Piecewise linear efficiency + +An extension of [Calliope in fragments](../index.md). Calliope's example `piecewise_linear_efficiency.yaml`: inflow at least a convex curve of outflow, which needs the available flow capacity of the MILP file. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + pieces: + description: Calliope's `pieces` — the lines a piecewise curve is the upper envelope of + dtype: int + +parameters: + flow_eff_piecewise_slopes: + description: "`flow_eff_piecewise_slopes` — the slope of each line of a convex inflow curve" + dims: [nodes, techs, pieces] + flow_eff_piecewise_intercept: + description: "`flow_eff_piecewise_intercept` — the intercept of each line of a convex inflow curve" + dims: [nodes, techs, pieces] + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + available_flow_cap: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + piecewise_efficiency: + description: >- + `piecewise_efficiency` — inflow is at least every line of the curve of + outflow, so at least the curve. Calliope's `where: available_flow_cap` + over a technology reads as the technology having it for some carrier + dims: [nodes, techs, timesteps, pieces] + where: flow_eff_piecewise_slopes AND flow_eff_piecewise_intercept AND count(available_flow_cap, over=carriers) >= 1 + expression: >- + sum(flow_in, over=carriers) >= flow_eff_piecewise_slopes * sum(flow_out, over=carriers) + + flow_eff_piecewise_intercept * sum(available_flow_cap, over=carriers) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{P}`$ | index $`p`$ — `pieces` — Calliope's `pieces` — the lines a piecewise curve is the upper envelope of | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{flow\_eff\_piecewise\_slopes}`$ | `flow_eff_piecewise_slopes` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{P}`$ — `flow_eff_piecewise_slopes` — the slope of each line of a convex inflow curve | +| $`\mathrm{flow\_eff\_piecewise\_intercept}`$ | `flow_eff_piecewise_intercept` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{P}`$ — `flow_eff_piecewise_intercept` — the intercept of each line of a convex inflow curve | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{available\_flow\_cap}`$ | `available_flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | + +#### Subject to + +**`piecewise_efficiency`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_in}_{n,i,c,t} \ge \mathrm{flow\_eff\_piecewise\_slopes}_{n,i,p} \cdot \left( \sum_{c \in \mathcal{C}} \mathit{flow\_out}_{n,i,c,t} \right) + \mathrm{flow\_eff\_piecewise\_intercept}_{n,i,p} \cdot \left( \sum_{c \in \mathcal{C}} \mathit{available\_flow\_cap}_{n,i,c,t} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T},\ p \in \mathcal{P} \,:\, \mathrm{flow\_eff\_piecewise\_slopes}_{n,i,p} \text{ is defined} \wedge \mathrm{flow\_eff\_piecewise\_intercept}_{n,i,p} \text{ is defined} \wedge \lvert \{ c \in \mathcal{C} \,:\, \mathit{available\_flow\_cap}_{n,i,c,t} \text{ exists} \} \rvert \ge 1 +``` + diff --git a/docs/examples/calliope/extensions/share_all_timesteps.md b/docs/examples/calliope/extensions/share_all_timesteps.md new file mode 100644 index 00000000..8ae7a719 --- /dev/null +++ b/docs/examples/calliope/extensions/share_all_timesteps.md @@ -0,0 +1,145 @@ + + +# Flow share over the whole time + +An extension of [Calliope in fragments](../index.md). Calliope's example `share_all_timesteps.yaml`: a technology meets a share of a demand, or of a node's outflow of a carrier, over the whole time. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + demand_share_tech: + description: >- + `demand_share_tech` — the demand technology whose inflow a technology + meets a share of. Calliope slices `flow_in` by it; a read through the + relation is that slice + key: techs + values: { demand: techs } + supply_share_carrier: + description: >- + `supply_share_carrier` — the carrier a technology's share of outflow is + counted in. Calliope slices `flow_out` by it, which is a test of the + pair + key: [techs, carriers] + +parameters: + demand_share_equals: + description: "`demand_share_equals` — the share of a demand technology's inflow a technology meets; given only where set" + dims: [nodes, techs] + supply_share_equals: + description: "`supply_share_equals` — the share of a node's outflow of a carrier a technology puts out; given only where set" + dims: [nodes, techs] + +expressions: + supply_share_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of its share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: flow_out + otherwise: 0 + supply_share_all_flow_out: + description: "`sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: sum(flow_out, over=techs) + otherwise: 0 + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_equals_per_tech: + description: "`demand_share_equals_per_tech` — a technology puts out its share of a demand technology's inflow over the whole time" + dims: [nodes, techs] + where: demand_share_equals + expression: >- + sum(flow_out, over=[timesteps, carriers]) + == sum(at(flow_in, by=demand_share_tech, over=demand, into=techs), over=[timesteps, carriers]) + * demand_share_equals + supply_share_equals_per_tech: + description: "`supply_share_equals_per_tech` — a technology puts out its share of a node's outflow of a carrier over the whole time" + dims: [nodes, techs] + where: supply_share_equals + expression: >- + sum(supply_share_flow_out, over=[carriers, timesteps]) + == sum(supply_share_all_flow_out, over=[carriers, timesteps]) * supply_share_equals +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` with $`\mathrm{demand\_share\_tech}: \mathcal{I} \to \mathcal{I},\ \mathrm{supply\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` with $`\mathrm{supply\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{demand\_share\_equals}`$ | `demand_share_equals` over $`\mathcal{N} \times \mathcal{I}`$ — `demand_share_equals` — the share of a demand technology's inflow a technology meets; given only where set | +| $`\mathrm{supply\_share\_equals}`$ | `supply_share_equals` over $`\mathcal{N} \times \mathcal{I}`$ — `supply_share_equals` — the share of a node's outflow of a carrier a technology puts out; given only where set | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{supply\_share\_flow\_out}`$ | `supply_share_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=$carrier]` — a technology's outflow of its share carrier | +| $`\mathit{supply\_share\_all\_flow\_out}`$ | `supply_share_all_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier | + +#### Subject to + +**`demand_share_equals_per_tech`** + +```math +\sum_{c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} = \left( \sum_{c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{flow\_in}_{n,\mathrm{demand\_share\_tech}(i),c,t} \right) \cdot \mathrm{demand\_share\_equals}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{demand\_share\_equals}_{n,i} \text{ is defined} +``` + +**`supply_share_equals_per_tech`** + +```math +\sum_{c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{supply\_share\_flow\_out}_{n,i,c,t} = \left( \sum_{c \in \mathcal{C},\ t \in \mathcal{T}} \mathit{supply\_share\_all\_flow\_out}_{n,i,c,t} \right) \cdot \mathrm{supply\_share\_equals}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{supply\_share\_equals}_{n,i} \text{ is defined} +``` + +#### Definitions + +**`supply_share_flow_out`** + +```math +\mathit{supply\_share\_flow\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } \left( i,\ c \right) \in \mathrm{supply\_share\_carrier} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`supply_share_all_flow_out`** + +```math +\mathit{supply\_share\_all\_flow\_out}_{n,i,c,t} = \begin{cases} \sum_{i' \in \mathcal{I}} \mathit{flow\_out}_{n,i',c,t} & \text{if } \left( i,\ c \right) \in \mathrm{supply\_share\_carrier} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/extensions/share_per_timestep.md b/docs/examples/calliope/extensions/share_per_timestep.md new file mode 100644 index 00000000..339c6e95 --- /dev/null +++ b/docs/examples/calliope/extensions/share_per_timestep.md @@ -0,0 +1,149 @@ + + +# Flow share per time step + +An extension of [Calliope in fragments](../index.md). Calliope's example `share_per_timestep.yaml`: the same shares, in each time step. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + demand_share_tech: + description: >- + `demand_share_tech` — the demand technology whose inflow a technology + meets a share of. Calliope slices `flow_in` by it; a read through the + relation is that slice + key: techs + values: { demand: techs } + supply_share_carrier: + description: >- + `supply_share_carrier` — the carrier a technology's share of outflow is + counted in. Calliope slices `flow_out` by it, which is a test of the + pair + key: [techs, carriers] + +parameters: + demand_share_per_timestep_equals: + description: "`demand_share_per_timestep_equals` — the share of a demand technology's inflow a technology meets in each time step; given only where set" + dims: [nodes, techs, timesteps] + supply_share_per_timestep_equals: + description: "`supply_share_per_timestep_equals` — the share of a node's outflow of a carrier a technology puts out in each time step; given only where set" + dims: [nodes, techs, timesteps] + +expressions: + supply_share_timestep_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of its share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: flow_out + otherwise: 0 + supply_share_timestep_all_flow_out: + description: "`sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: sum(flow_out, over=techs) + otherwise: 0 + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_per_timestep_equals_per_tech: + description: "`demand_share_per_timestep_equals_per_tech` — a technology puts out its share of a demand technology's inflow in each time step" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_equals + expression: >- + sum(flow_out, over=carriers) + == sum(at(flow_in, by=demand_share_tech, over=demand, into=techs), over=carriers) + * demand_share_per_timestep_equals + supply_share_per_timestep_equals_per_tech: + description: >- + `supply_share_per_timestep_equals_per_tech` — a technology puts out its + share of a node's outflow of a carrier in each time step. Calliope's + row keeps the carrier dimension of the slice; the slice here is summed + over the one carrier it keeps + dims: [nodes, techs, timesteps] + where: supply_share_per_timestep_equals + expression: >- + sum(supply_share_timestep_flow_out, over=carriers) + == sum(supply_share_timestep_all_flow_out, over=carriers) * supply_share_per_timestep_equals +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` with $`\mathrm{demand\_share\_tech}: \mathcal{I} \to \mathcal{I},\ \mathrm{supply\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` with $`\mathrm{supply\_share\_carrier} \subseteq \mathcal{I} \times \mathcal{C}`$ — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{demand\_share\_per\_timestep\_equals}`$ | `demand_share_per_timestep_equals` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `demand_share_per_timestep_equals` — the share of a demand technology's inflow a technology meets in each time step; given only where set | +| $`\mathrm{supply\_share\_per\_timestep\_equals}`$ | `supply_share_per_timestep_equals` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `supply_share_per_timestep_equals` — the share of a node's outflow of a carrier a technology puts out in each time step; given only where set | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{supply\_share\_timestep\_flow\_out}`$ | `supply_share_timestep_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=$carrier]` — a technology's outflow of its share carrier | +| $`\mathit{supply\_share\_timestep\_all\_flow\_out}`$ | `supply_share_timestep_all_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier | + +#### Subject to + +**`demand_share_per_timestep_equals_per_tech`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out}_{n,i,c,t} = \left( \sum_{c \in \mathcal{C}} \mathit{flow\_in}_{n,\mathrm{demand\_share\_tech}(i),c,t} \right) \cdot \mathrm{demand\_share\_per\_timestep\_equals}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{demand\_share\_per\_timestep\_equals}_{n,i,t} \text{ is defined} +``` + +**`supply_share_per_timestep_equals_per_tech`** + +```math +\sum_{c \in \mathcal{C}} \mathit{supply\_share\_timestep\_flow\_out}_{n,i,c,t} = \left( \sum_{c \in \mathcal{C}} \mathit{supply\_share\_timestep\_all\_flow\_out}_{n,i,c,t} \right) \cdot \mathrm{supply\_share\_per\_timestep\_equals}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{supply\_share\_per\_timestep\_equals}_{n,i,t} \text{ is defined} +``` + +#### Definitions + +**`supply_share_timestep_flow_out`** + +```math +\mathit{supply\_share\_timestep\_flow\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } \left( i,\ c \right) \in \mathrm{supply\_share\_carrier} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`supply_share_timestep_all_flow_out`** + +```math +\mathit{supply\_share\_timestep\_all\_flow\_out}_{n,i,c,t} = \begin{cases} \sum_{i' \in \mathcal{I}} \mathit{flow\_out}_{n,i',c,t} & \text{if } \left( i,\ c \right) \in \mathrm{supply\_share\_carrier} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/extensions/sos2_piecewise_linear_costs.md b/docs/examples/calliope/extensions/sos2_piecewise_linear_costs.md new file mode 100644 index 00000000..b2f8ca94 --- /dev/null +++ b/docs/examples/calliope/extensions/sos2_piecewise_linear_costs.md @@ -0,0 +1,167 @@ + + +# Piecewise linear costs with SOS2 + +An extension of [Calliope in fragments](../index.md). Calliope's example `sos2_piecewise_linear_costs.yaml`: an investment cost with economies of scale, as a curve through breakpoints stated as an SOS2 set. The cost is a term of `cost_investment`. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + breakpoints: + description: Calliope's `breakpoints` — the corners of a piecewise-linear curve, in order + dtype: int + +parameters: + piecewise_cost_investment_x: + description: "`piecewise_cost_investment_x` — the flow capacity at each breakpoint" + dims: [techs, breakpoints] + piecewise_cost_investment_y: + description: "`piecewise_cost_investment_y` — the investment cost at each breakpoint" + dims: [techs, costs, breakpoints] + +variables: + piecewise_cost_investment: + description: "`piecewise_cost_investment` — an investment cost that grows more slowly the more capacity is built" + dims: [nodes, techs, carriers, costs] + where: count(piecewise_cost_investment_x, over=breakpoints) >= 1 AND count(piecewise_cost_investment_y, over=breakpoints) >= 1 + bounds: { lower: 0 } + absence: zero + piecewise_flow_cap: + description: >- + the flow capacity, where the technology has a cost curve. A + `piecewise:` block takes no `where:`, so its link rows would pin + `flow_cap` to the curve at every technology; a link over this copy is + built only where the copy is + dims: [nodes, techs, carriers, costs] + where: count(piecewise_cost_investment_x, over=breakpoints) >= 1 AND count(piecewise_cost_investment_y, over=breakpoints) >= 1 + +constraints: + piecewise_flow_cap_is_flow_cap: + description: the copy of the flow capacity the curve reads is the flow capacity + dims: [nodes, techs, carriers, costs] + where: piecewise_flow_cap + expression: piecewise_flow_cap == flow_cap + +piecewise: + sos2_piecewise_costs: + description: "`sos2_piecewise_costs` — the investment cost lies on the curve through the breakpoints, stated as an SOS2 set" + over: breakpoints + method: sos2 + points: piecewise_cost_investment_x + links: + - [piecewise_flow_cap, piecewise_cost_investment_x] + - [piecewise_cost_investment, piecewise_cost_investment_y] + +expressions: + cost_investment_piecewise: + description: "`sum(piecewise_cost_investment, over=carriers)` — the term Calliope writes into `cost_investment`" + expression: sum(piecewise_cost_investment, over=carriers) + +given: + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_piecewise } +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{B}`$ | index $`b`$ — `breakpoints` — Calliope's `breakpoints` — the corners of a piecewise-linear curve, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{piecewise\_cost\_investment\_x}`$ | `piecewise_cost_investment_x` over $`\mathcal{I} \times \mathcal{B}`$ — `piecewise_cost_investment_x` — the flow capacity at each breakpoint | +| $`\mathrm{piecewise\_cost\_investment\_y}`$ | `piecewise_cost_investment_y` over $`\mathcal{I} \times \mathcal{K} \times \mathcal{B}`$ — `piecewise_cost_investment_y` — the investment cost at each breakpoint | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{piecewise\_cost\_investment}`$ | `piecewise_cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{K}`$ — `piecewise_cost_investment` — an investment cost that grows more slowly the more capacity is built | +| $`\mathit{piecewise\_flow\_cap}`$ | `piecewise_flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{K}`$ — the flow capacity, where the technology has a cost curve. A `piecewise:` block takes no `where:`, so its link rows would pin `flow_cap` to the curve at every technology; a link over this copy is built only where the copy is | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_investment_piecewise` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{cost\_investment\_piecewise}`$ | `cost_investment_piecewise` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `sum(piecewise_cost_investment, over=carriers)` — the term Calliope writes into `cost_investment` | + +Upright is what the data supplies — a parameter such as $`\mathrm{piecewise\_cost\_investment\_x}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{piecewise\_cost\_investment}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`piecewise_flow_cap_is_flow_cap`** + +```math +\mathit{piecewise\_flow\_cap}_{n,i,c,k} = \mathit{flow\_cap}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} \,:\, \mathit{piecewise\_flow\_cap}_{n,i,c,k} \text{ exists} +``` + +**`sos2_piecewise_costs`** + +```math +\left( \mathit{piecewise\_flow\_cap}_{n,i,c,k},\ \mathit{piecewise\_cost\_investment}_{n,i,c,k} \right) \in \mathrm{pwl}_{b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined}}(\mathrm{piecewise\_cost\_investment\_x}_{i,b},\ \mathrm{piecewise\_cost\_investment\_y}_{i,k,b}) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} +``` + +#### Definitions + +**`cost_investment_piecewise`** + +```math +\mathit{cost\_investment\_piecewise}_{n,i,k} = \sum_{c \in \mathcal{C}} \mathit{piecewise\_cost\_investment}_{n,i,c,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`piecewise_cost_investment`** + +```math +\mathit{piecewise\_cost\_investment}_{n,i,c,k} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} \,:\, \lvert \{ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined} \} \rvert \ge 1 \wedge \lvert \{ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_y}_{i,k,b} \text{ is defined} \} \rvert \ge 1 +``` + +**`piecewise_flow_cap`** + +```math +\mathit{piecewise\_flow\_cap}_{n,i,c,k} \in \mathbb{R} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} \,:\, \lvert \{ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined} \} \rvert \ge 1 \wedge \lvert \{ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_y}_{i,k,b} \text{ is defined} \} \rvert \ge 1 +``` + +#### Assumptions + +**`sos2_piecewise_costs_complete`** + +```math +\mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined} \wedge \mathrm{piecewise\_cost\_investment\_y}_{i,k,b} \text{ is defined} \qquad \forall\, i \in \mathcal{I},\ k \in \mathcal{K},\ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined} +``` + +**`sos2_piecewise_costs_contiguous`** + +```math +\lvert \{ b \in \mathcal{B} \,:\, \mathrm{piecewise\_cost\_investment\_x}_{i,b} \text{ is defined} \wedge \neg \left( \mathrm{piecewise\_cost\_investment\_x}_{i,b - 1} \text{ is defined} \right) \} \rvert = 1 \qquad \forall\, i \in \mathcal{I} +``` + diff --git a/docs/examples/calliope/extensions/uptime_downtime_limits.md b/docs/examples/calliope/extensions/uptime_downtime_limits.md new file mode 100644 index 00000000..a117ae59 --- /dev/null +++ b/docs/examples/calliope/extensions/uptime_downtime_limits.md @@ -0,0 +1,148 @@ + + +# Uptime and downtime limits + +An extension of [Calliope in fragments](../index.md). Calliope's example `uptime_downtime_limits.yaml`: capacity factors over the whole time, forced downtime, and a cap on the time steps a unit-bought technology runs in. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + capacity_factor_min: + description: "`capacity_factor_min` — the least capacity factor a technology reaches over the whole time" + dims: [nodes, techs] + capacity_factor_max: + description: "`capacity_factor_max` — the most capacity factor a technology reaches over the whole time; given only where set" + dims: [nodes, techs] + uptime_limit: + description: "`uptime_limit` — the most time steps a technology runs in, weighted; given only where set" + dims: [nodes, techs] + downtime_periods: + description: "`downtime_periods` — whether a technology is down for maintenance in a time step" + dims: [nodes, techs, timesteps] + dtype: bool + +expressions: + total_time: + description: "`$total_time` — the hours the modelled time steps stand for" + expression: sum(timestep_resolution * timestep_weights, over=timesteps) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_cap: { dims: [nodes, techs, carriers] } + operating_units: { dims: [nodes, techs, timesteps] } + +constraints: + annual_capacity_factor_min: + description: "`annual_capacity_factor_min` — a technology's outflow over the whole time is at least its least capacity factor" + dims: [nodes, techs, carriers] + where: carrier_out AND capacity_factor_min + expression: sum(flow_out * timestep_weights, over=timesteps) >= flow_cap * capacity_factor_min * total_time + annual_capacity_factor_max: + description: "`annual_capacity_factor_max` — a technology's outflow over the whole time is at most its most capacity factor" + dims: [nodes, techs, carriers] + where: carrier_out AND capacity_factor_max + expression: sum(flow_out * timestep_weights, over=timesteps) <= flow_cap * capacity_factor_max * total_time + downtime_period: + description: "`downtime_period` — a technology puts out nothing in a time step it is down" + dims: [nodes, techs, timesteps] + where: downtime_periods + expression: sum(flow_out, over=carriers) == 0 + downtime_period_decision: + description: >- + `downtime_period_decision` — a unit-bought technology runs in at most + its limit of time steps. Calliope's `where: operating_units` over a + technology reads as the technology running in whole units at all + dims: [nodes, techs] + where: count(operating_units, over=timesteps) >= 1 AND uptime_limit + expression: sum(operating_units * timestep_weights, over=timesteps) <= uptime_limit +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{capacity\_factor\_min}`$ | `capacity_factor_min` over $`\mathcal{N} \times \mathcal{I}`$ — `capacity_factor_min` — the least capacity factor a technology reaches over the whole time | +| $`\mathrm{capacity\_factor\_max}`$ | `capacity_factor_max` over $`\mathcal{N} \times \mathcal{I}`$ — `capacity_factor_max` — the most capacity factor a technology reaches over the whole time; given only where set | +| $`\mathrm{uptime\_limit}`$ | `uptime_limit` over $`\mathcal{N} \times \mathcal{I}`$ — `uptime_limit` — the most time steps a technology runs in, weighted; given only where set | +| $`\mathrm{downtime\_periods}`$ | `downtime_periods` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `downtime_periods` — whether a technology is down for maintenance in a time step | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{operating\_units}`$ | `operating_units` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathrm{total\_time}`$ | `total_time` (scalar) — `$total_time` — the hours the modelled time steps stand for | + +#### Subject to + +**`annual_capacity_factor_min`** + +```math +\sum_{t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} \cdot \mathrm{timestep\_weights}_{t} \ge \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{capacity\_factor\_min}_{n,i} \cdot \mathrm{total\_time} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \mathrm{capacity\_factor\_min}_{n,i} \text{ is defined} +``` + +**`annual_capacity_factor_max`** + +```math +\sum_{t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} \cdot \mathrm{timestep\_weights}_{t} \le \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{capacity\_factor\_max}_{n,i} \cdot \mathrm{total\_time} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \mathrm{capacity\_factor\_max}_{n,i} \text{ is defined} +``` + +**`downtime_period`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out}_{n,i,c,t} = 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{downtime\_periods}_{n,i,t} +``` + +**`downtime_period_decision`** + +```math +\sum_{t \in \mathcal{T}} \mathit{operating\_units}_{n,i,t} \cdot \mathrm{timestep\_weights}_{t} \le \mathrm{uptime\_limit}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \lvert \{ t \in \mathcal{T} \,:\, \mathit{operating\_units}_{n,i,t} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{uptime\_limit}_{n,i} \text{ is defined} +``` + +#### Definitions + +**`total_time`** + +```math +\mathrm{total\_time} = \sum_{t \in \mathcal{T}} \mathrm{timestep\_resolution}_{t} \cdot \mathrm{timestep\_weights}_{t} +``` + diff --git a/docs/examples/calliope/extensions/urban_scale_chp.md b/docs/examples/calliope/extensions/urban_scale_chp.md new file mode 100644 index 00000000..c31ef7f5 --- /dev/null +++ b/docs/examples/calliope/extensions/urban_scale_chp.md @@ -0,0 +1,147 @@ + + +# Urban-scale CHP + +An extension of [Calliope in fragments](../index.md). The `additional_math.yaml` of Calliope's urban-scale example model: the technology `chp` puts out heat in a fixed ratio to its electricity. The new rows are here, and [its patch](../variants/urban_scale_chp.md) keeps the base `balance_conversion` off `chp`. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + heat_to_power_ratio: + description: "`heat_to_power_ratio` — the heat a combined heat and power plant puts out per unit of electricity. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + +expressions: + urban_electricity_out: + description: "`flow_out[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out + otherwise: 0 + urban_heat_out: + description: "`flow_out[carriers=heat]`" + dims: [nodes, techs, carriers, timesteps] + cases: + heat: + when: carriers == heat + expression: flow_out + otherwise: 0 + urban_electricity_out_inc_eff: + description: "`flow_out_inc_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_inc_eff + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + link_chp_outputs: + description: "`link_chp_outputs` — the technology `chp` puts out heat in a fixed ratio to its electricity" + dims: [nodes, techs, timesteps] + where: techs == chp + expression: sum(urban_electricity_out, over=carriers) * heat_to_power_ratio == sum(urban_heat_out, over=carriers) + balance_conversion_chp: + description: >- + `balance_conversion` for the technology `chp` — it puts out, before + losses, as much electricity as it takes in fuel after them. The patch + keeps the base row off it + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage AND techs == chp + expression: sum(urban_electricity_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{heat\_to\_power\_ratio}`$ | `heat_to_power_ratio` over $`\mathcal{N} \times \mathcal{I}`$ — `heat_to_power_ratio` — the heat a combined heat and power plant puts out per unit of electricity. Calliope's default is 1, and data prep fills it | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{urban\_electricity\_out}`$ | `urban_electricity_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=electricity]` | +| $`\mathit{urban\_heat\_out}`$ | `urban_heat_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out[carriers=heat]` | +| $`\mathit{urban\_electricity\_out\_inc\_eff}`$ | `urban_electricity_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_inc_eff[carriers=electricity]` | + +#### Subject to + +**`link_chp_outputs`** + +```math +\left( \sum_{c \in \mathcal{C}} \mathit{urban\_electricity\_out}_{n,i,c,t} \right) \cdot \mathrm{heat\_to\_power\_ratio}_{n,i} = \sum_{c \in \mathcal{C}} \mathit{urban\_heat\_out}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, i = \text{'}\mathrm{chp}\text{'} +``` + +**`balance_conversion_chp`** + +```math +\sum_{c \in \mathcal{C}} \mathit{urban\_electricity\_out\_inc\_eff}_{n,i,c,t} = \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} \wedge i = \text{'}\mathrm{chp}\text{'} +``` + +#### Definitions + +**`urban_electricity_out`** + +```math +\mathit{urban\_electricity\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`urban_heat_out`** + +```math +\mathit{urban\_heat\_out}_{n,i,c,t} = \begin{cases} \mathit{flow\_out}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{heat}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`urban_electricity_out_inc_eff`** + +```math +\mathit{urban\_electricity\_out\_inc\_eff}_{n,i,c,t} = \begin{cases} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} & \text{if } c = \text{'}\mathrm{electricity}\text{'} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/feasibility.md b/docs/examples/calliope/feasibility.md new file mode 100644 index 00000000..6d135044 --- /dev/null +++ b/docs/examples/calliope/feasibility.md @@ -0,0 +1,129 @@ + + +# Feasibility + +One of the base fragments of [Calliope in fragments](index.md). Unmet demand and unused supply at a high price, so a model that cannot balance still solves. Calliope builds them under `config.ensure_feasibility`; here the switch is whether this file is composed. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +variables: + unmet_demand: + description: >- + `unmet_demand` — a source of any carrier at any node, at a high price, + so a model that cannot meet its demand still solves. Calliope builds + it under `config.ensure_feasibility`; here it is this file + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + bounds: { lower: 0 } + absence: zero + unused_supply: + description: "`unused_supply` — a sink of any carrier at any node, at a high price, the counterpart of `unmet_demand`" + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + bounds: { upper: 0 } + absence: zero + +expressions: + feasibility_carrier_flow: unmet_demand + unused_supply + unmet_demand_penalty: + description: "`$unmet_demand` of `min_cost_optimisation` — what unmet demand and unused supply cost" + expression: sum(sum(unmet_demand - unused_supply, over=[carriers, nodes]) * timestep_weights) * bigM + unmet_sum: + description: "`unmet_sum` — net unmet demand; reported" + expression: unmet_demand + unused_supply + +given: + parameters: + carrier_in: { dims: [nodes, techs, carriers], dtype: bool } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_weights: { dims: [timesteps] } + bigM: { dims: [] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: feasibility_carrier_flow } + penalty: { dims: [], term: unmet_demand_penalty } +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{unmet\_demand}`$ | `unmet_demand` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — `unmet_demand` — a source of any carrier at any node, at a high price, so a model that cannot meet its demand still solves. Calliope builds it under `config.ensure_feasibility`; here it is this file | +| $`\mathit{unused\_supply}`$ | `unused_supply` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — `unused_supply` — a sink of any carrier at any node, at a high price, the counterpart of `unmet_demand` | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{carrier\_in}`$ | `carrier_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{bigM}`$ | `bigM` (scalar), data another file declares | +| $`\mathit{carrier\_flow}`$ | `carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$, an expression this file adds `feasibility_carrier_flow` to | +| $`\mathit{penalty}`$ | `penalty` (scalar), an expression this file adds `unmet_demand_penalty` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{feasibility\_carrier\_flow}`$ | `feasibility_carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{unmet\_demand\_penalty}`$ | `unmet_demand_penalty` (scalar) — `$unmet_demand` of `min_cost_optimisation` — what unmet demand and unused supply cost | +| $`\mathit{unmet\_sum}`$ | `unmet_sum` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ — `unmet_sum` — net unmet demand; reported | + +#### Definitions + +**`feasibility_carrier_flow`** + +```math +\mathit{feasibility\_carrier\_flow}_{n,c,t} = \mathit{unmet\_demand}_{n,c,t} + \mathit{unused\_supply}_{n,c,t} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`unmet_demand_penalty`** + +```math +\mathit{unmet\_demand\_penalty} = \left( \sum_{t \in \mathcal{T}} \left( \sum_{n \in \mathcal{N},\ c \in \mathcal{C}} \left( \mathit{unmet\_demand}_{n,c,t} - \mathit{unused\_supply}_{n,c,t} \right) \right) \cdot \mathrm{timestep\_weights}_{t} \right) \cdot \mathrm{bigM} +``` + +**`unmet_sum`** + +```math +\mathit{unmet\_sum}_{n,c,t} = \mathit{unmet\_demand}_{n,c,t} + \mathit{unused\_supply}_{n,c,t} \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +#### Variable domains + +**`unmet_demand`** + +```math +\mathit{unmet\_demand}_{n,c,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_in}_{n,i,c} \} \rvert \ge 1 \vee \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 +``` + +**`unused_supply`** + +```math +\mathit{unused\_supply}_{n,c,t} \le 0 \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_in}_{n,i,c} \} \rvert \ge 1 \vee \lvert \{ i \in \mathcal{I} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 +``` + diff --git a/docs/examples/calliope/flows.md b/docs/examples/calliope/flows.md new file mode 100644 index 00000000..da4b2c85 --- /dev/null +++ b/docs/examples/calliope/flows.md @@ -0,0 +1,490 @@ + + +# Flows + +One of the base fragments of [Calliope in fragments](index.md). The core of every technology: flow capacity, outflow and inflow, their efficiencies, their limits and ramping. It adds the flows to the balance and their costs to the three cost sums. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + link_from: + description: >- + `link_from` — the node a transmission technology links from. Calliope + reads it as `map_dim(nodes, link_from)`, a mask over technology and + node, which is the relation's own row test + key: [techs, nodes] + link_to: + description: >- + `link_to` — the node a transmission technology links to, read as + `link_from` is + key: [techs, nodes] + +parameters: + base_tech: + description: >- + `base_tech` — the abstract class a technology derives from: demand, + supply, conversion, storage or transmission + dims: [techs] + dtype: str + carrier_in: + description: "`carrier_in` — whether a technology consumes a carrier at a node" + dims: [nodes, techs, carriers] + dtype: bool + carrier_out: + description: "`carrier_out` — whether a technology produces a carrier at a node" + dims: [nodes, techs, carriers] + dtype: bool + include_storage: + description: >- + `include_storage` — whether a technology that is not a storage one + carries a store all the same + dims: [nodes, techs] + dtype: bool + one_way: + description: "`one_way` — whether a transmission technology carries flow only from `link_from` to `link_to`" + dims: [techs] + dtype: bool + flow_cap_min: + description: >- + `flow_cap_min` — least flow capacity. Calliope's default is 0; a bound + has a row wherever the variable has one, so data prep fills it + dims: [nodes, techs] + flow_cap_max: + description: >- + `flow_cap_max` — most flow capacity. Calliope's default is `.inf`, + which data prep fills, and a `where` reads as not given + dims: [nodes, techs] + flow_cap_min_systemwide: + description: "`flow_cap_min_systemwide` — least flow capacity of a technology over every node; given only where set" + dims: [techs, carriers] + flow_cap_max_systemwide: + description: "`flow_cap_max_systemwide` — most flow capacity of a technology over every node; given only where set" + dims: [techs, carriers] + flow_out_min_relative: + description: "`flow_out_min_relative` — least outflow, per unit of flow capacity; given only where set" + dims: [nodes, techs, timesteps] + flow_out_eff: + description: "`flow_out_eff` — the share of flow that leaves a technology as outflow. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_in_eff: + description: "`flow_in_eff` — the share of inflow that enters a technology. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_out_parasitic_eff: + description: "`flow_out_parasitic_eff` — what is left after the plant's own use. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_out_eff_per_distance: + description: "`flow_out_eff_per_distance` — the outflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_in_eff_per_distance: + description: "`flow_in_eff_per_distance` — the inflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + distance: + description: >- + `distance` — the length of a transmission link. Calliope's default is + 1, which data prep fills, where it does not derive one from the + coordinates of the nodes + dims: [techs] + flow_ramping: + description: "`flow_ramping` — the most flow may change in an hour, per unit of flow capacity; given only where set" + dims: [nodes, techs] + cost_flow_cap: + description: "`cost_flow_cap` — the cost of one unit of flow capacity" + dims: [nodes, techs, costs] + cost_flow_cap_per_distance: + description: "`cost_flow_cap_per_distance` — the cost of one unit of flow capacity per unit of link distance" + dims: [nodes, techs, costs] + cost_flow_out: + description: "`cost_flow_out` — the cost of one unit of outflow" + dims: [nodes, techs, costs, timesteps] + cost_flow_in: + description: "`cost_flow_in` — the cost of one unit of inflow" + dims: [nodes, techs, costs, timesteps] + cost_om_annual: + description: "`cost_om_annual` — the annual cost of one unit of flow capacity" + dims: [nodes, techs, costs] + +variables: + flow_cap: + description: "`flow_cap` — the flow capacity of a technology, its nominal or nameplate capacity" + dims: [nodes, techs, carriers] + where: carrier_in OR carrier_out + bounds: { lower: flow_cap_min, upper: flow_cap_max } + absence: zero + flow_out: + description: >- + `flow_out` — the outflow of a technology in a time step. A one-way link + has none at the node it links from + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND NOT (one_way AND link_from) + bounds: { lower: 0 } + absence: zero + flow_in: + description: >- + `flow_in` — the inflow to a technology in a time step. A one-way link + has none at the node it links to + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND NOT (one_way AND link_to) + bounds: { lower: 0 } + absence: zero + +expressions: + flow_out_inc_eff: + description: "`flow_out_inc_eff` — outflow before the losses on the way out" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: base_tech == 'transmission' + expression: flow_out / (flow_out_eff * flow_out_parasitic_eff * flow_out_eff_per_distance ** distance) + otherwise: flow_out / (flow_out_eff * flow_out_parasitic_eff) + flow_in_inc_eff: + description: "`flow_in_inc_eff` — inflow after the losses on the way in" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: base_tech == 'transmission' + expression: flow_in * flow_in_eff * flow_in_eff_per_distance ** distance + otherwise: flow_in * flow_in_eff + ramping_flow: + description: >- + `$flow` of `ramping_up` and `ramping_down` — the flow a ramping limit + holds, per hour: outflow, inflow, or their difference where a + technology has both + dims: [nodes, techs, carriers, timesteps] + cases: + out: + when: carrier_out AND NOT carrier_in + expression: flow_out / timestep_resolution + in: + when: carrier_in AND NOT carrier_out + expression: flow_in / timestep_resolution + otherwise: (flow_out - flow_in) / timestep_resolution + cost_flow_cap_sum: + description: >- + `$cost_sum` of `cost_investment_flow_cap` — what one unit of flow + capacity costs; a link's cost is split between its two ends + dims: [nodes, techs, costs] + cases: + transmission: + when: base_tech == 'transmission' + expression: (cost_flow_cap + cost_flow_cap_per_distance * distance) * 0.5 + otherwise: cost_flow_cap + cost_investment_flow_cap: + description: "`cost_investment_flow_cap` — the investment cost of flow capacity" + expression: cost_flow_cap_sum * flow_cap + flows_carrier_flow: sum(flow_out, over=techs) - sum(flow_in, over=techs) + flows_cost_investment: sum(cost_investment_flow_cap, over=carriers) + flows_cost_operation_variable: >- + timestep_weights * (sum(cost_flow_out * flow_out, over=carriers) + sum(cost_flow_in * flow_in, over=carriers)) + flows_cost_operation_fixed: annualisation_weight * sum(cost_om_annual * flow_cap, over=carriers) + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + expressions: + annualisation_weight: + description: the share of a year the modelled time steps stand for + dims: [] + carrier_flow: { dims: [nodes, carriers, timesteps], term: flows_carrier_flow } + cost_investment: { dims: [nodes, techs, costs], term: flows_cost_investment } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: flows_cost_operation_variable } + cost_operation_fixed: { dims: [nodes, techs, costs], term: flows_cost_operation_fixed } + +constraints: + flow_out_max: + description: "`flow_out_max` — outflow is at most the flow capacity over the time step, less the plant's own use" + dims: [nodes, techs, carriers, timesteps] + where: carrier_out + expression: flow_out <= flow_cap * timestep_resolution * flow_out_parasitic_eff + flow_out_min: + description: "`flow_out_min` — outflow is at least its least share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: flow_cap AND flow_out_min_relative + expression: flow_out >= flow_cap * timestep_resolution * flow_out_min_relative + flow_in_max: + description: "`flow_in_max` — inflow is at most the flow capacity over the time step" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in + expression: flow_in <= flow_cap * timestep_resolution + flow_capacity_systemwide_max: + description: "`flow_capacity_systemwide_max` — the flow capacity of a technology over every node is at most its system-wide maximum" + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_max_systemwide + expression: sum(flow_cap, over=nodes) <= flow_cap_max_systemwide + flow_capacity_systemwide_min: + description: "`flow_capacity_systemwide_min` — the flow capacity of a technology over every node is at least its system-wide minimum" + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide + expression: sum(flow_cap, over=nodes) >= flow_cap_min_systemwide + ramping_up: + description: "`ramping_up` — flow rises from one time step to the next by at most its ramping share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: (carrier_in OR carrier_out) AND flow_ramping AND position(timesteps) > 0 + expression: ramping_flow - shift(ramping_flow, along=timesteps, offset=1) <= flow_ramping * flow_cap + ramping_down: + description: "`ramping_down` — flow falls from one time step to the next by at most its ramping share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: (carrier_in OR carrier_out) AND flow_ramping AND position(timesteps) > 0 + expression: -1 * flow_ramping * flow_cap <= ramping_flow - shift(ramping_flow, along=timesteps, offset=1) + +assumptions: + must_have_base: + description: Calliope's `must_have_base` — every technology derives from an abstract class + holds: base_tech + base_tech_one_of: + description: Calliope's `one_of` on `base_tech` + holds: >- + base_tech == 'demand' OR base_tech == 'supply' OR base_tech == 'conversion' + OR base_tech == 'storage' OR base_tech == 'transmission' + distance_only_for_transmission: + description: >- + Calliope's `distance_only_for_transmission` — only a link sets a + distance or a per-distance value. Data prep fills the defaults, so a + technology that is not a link keeps them + holds: >- + distance == 1 AND flow_in_eff_per_distance == 1 + AND flow_out_eff_per_distance == 1 AND NOT cost_flow_cap_per_distance + where: NOT base_tech == 'transmission' + unbounded_flow_cap_cost: + description: Calliope's `unbounded_flow_cap_cost` — a negative flow capacity cost needs a finite maximum + holds: NOT cost_flow_cap < 0 OR flow_cap_max +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` with $`\mathrm{link\_from} \subseteq \mathcal{I} \times \mathcal{N},\ \mathrm{link\_to} \subseteq \mathcal{I} \times \mathcal{N}`$ — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` with $`\mathrm{link\_from} \subseteq \mathcal{I} \times \mathcal{N},\ \mathrm{link\_to} \subseteq \mathcal{I} \times \mathcal{N}`$ — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$ — `base_tech` — the abstract class a technology derives from: demand, supply, conversion, storage or transmission | +| $`\mathrm{carrier\_in}`$ | `carrier_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `carrier_in` — whether a technology consumes a carrier at a node | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `carrier_out` — whether a technology produces a carrier at a node | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$ — `include_storage` — whether a technology that is not a storage one carries a store all the same | +| $`\mathrm{one\_way}`$ | `one_way` over $`\mathcal{I}`$ — `one_way` — whether a transmission technology carries flow only from `link_from` to `link_to` | +| $`\mathrm{flow\_cap\_min}`$ | `flow_cap_min` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_cap_min` — least flow capacity. Calliope's default is 0; a bound has a row wherever the variable has one, so data prep fills it | +| $`\mathrm{flow\_cap\_max}`$ | `flow_cap_max` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_cap_max` — most flow capacity. Calliope's default is `.inf`, which data prep fills, and a `where` reads as not given | +| $`\mathrm{flow\_cap\_min\_systemwide}`$ | `flow_cap_min_systemwide` over $`\mathcal{I} \times \mathcal{C}`$ — `flow_cap_min_systemwide` — least flow capacity of a technology over every node; given only where set | +| $`\mathrm{flow\_cap\_max\_systemwide}`$ | `flow_cap_max_systemwide` over $`\mathcal{I} \times \mathcal{C}`$ — `flow_cap_max_systemwide` — most flow capacity of a technology over every node; given only where set | +| $`\mathrm{flow\_out\_min\_relative}`$ | `flow_out_min_relative` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `flow_out_min_relative` — least outflow, per unit of flow capacity; given only where set | +| $`\mathrm{flow\_out\_eff}`$ | `flow_out_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_eff` — the share of flow that leaves a technology as outflow. Calliope's default is 1, and data prep fills it | +| $`\mathrm{flow\_in\_eff}`$ | `flow_in_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_in_eff` — the share of inflow that enters a technology. Calliope's default is 1, and data prep fills it | +| $`\mathrm{flow\_out\_parasitic\_eff}`$ | `flow_out_parasitic_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_parasitic_eff` — what is left after the plant's own use. Calliope's default is 1, and data prep fills it | +| $`\mathrm{flow\_out\_eff\_per\_distance}`$ | `flow_out_eff_per_distance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_eff_per_distance` — the outflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it | +| $`\mathrm{flow\_in\_eff\_per\_distance}`$ | `flow_in_eff_per_distance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_in_eff_per_distance` — the inflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it | +| $`\mathrm{distance}`$ | `distance` over $`\mathcal{I}`$ — `distance` — the length of a transmission link. Calliope's default is 1, which data prep fills, where it does not derive one from the coordinates of the nodes | +| $`\mathrm{flow\_ramping}`$ | `flow_ramping` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_ramping` — the most flow may change in an hour, per unit of flow capacity; given only where set | +| $`\mathrm{cost\_flow\_cap}`$ | `cost_flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_flow_cap` — the cost of one unit of flow capacity | +| $`\mathrm{cost\_flow\_cap\_per\_distance}`$ | `cost_flow_cap_per_distance` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_flow_cap_per_distance` — the cost of one unit of flow capacity per unit of link distance | +| $`\mathrm{cost\_flow\_out}`$ | `cost_flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ — `cost_flow_out` — the cost of one unit of outflow | +| $`\mathrm{cost\_flow\_in}`$ | `cost_flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ — `cost_flow_in` — the cost of one unit of inflow | +| $`\mathrm{cost\_om\_annual}`$ | `cost_om_annual` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_om_annual` — the annual cost of one unit of flow capacity | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `flow_cap` — the flow capacity of a technology, its nominal or nameplate capacity | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out` — the outflow of a technology in a time step. A one-way link has none at the node it links from | +| $`\mathit{flow\_in}`$ | `flow_in` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_in` — the inflow to a technology in a time step. A one-way link has none at the node it links to | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{annualisation\_weight}`$ | `annualisation_weight` (scalar), an expression another file defines — the share of a year the modelled time steps stand for | +| $`\mathit{carrier\_flow}`$ | `carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$, an expression this file adds `flows_carrier_flow` to | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `flows_cost_investment` to | +| $`\mathit{cost\_operation\_variable}`$ | `cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$, an expression this file adds `flows_cost_operation_variable` to | +| $`\mathit{cost\_operation\_fixed}`$ | `cost_operation_fixed` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `flows_cost_operation_fixed` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_out_inc_eff` — outflow before the losses on the way out | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `flow_in_inc_eff` — inflow after the losses on the way in | +| $`\mathit{ramping\_flow}`$ | `ramping_flow` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `$flow` of `ramping_up` and `ramping_down` — the flow a ramping limit holds, per hour: outflow, inflow, or their difference where a technology has both | +| $`\mathrm{cost\_flow\_cap\_sum}`$ | `cost_flow_cap_sum` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `$cost_sum` of `cost_investment_flow_cap` — what one unit of flow capacity costs; a link's cost is split between its two ends | +| $`\mathit{cost\_investment\_flow\_cap}`$ | `cost_investment_flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{K}`$ — `cost_investment_flow_cap` — the investment cost of flow capacity | +| $`\mathit{flows\_carrier\_flow}`$ | `flows_carrier_flow` over $`\mathcal{N} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flows\_cost\_investment}`$ | `flows_cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ | +| $`\mathit{flows\_cost\_operation\_variable}`$ | `flows_cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ | +| $`\mathit{flows\_cost\_operation\_fixed}`$ | `flows_cost_operation_fixed` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ | + +Upright is what the data supplies — a parameter such as $`\mathrm{base\_tech}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{flow\_cap}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +$`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions. + +#### Subject to + +**`flow_out_max`** + +```math +\mathit{flow\_out}_{n,i,c,t} \le \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_out}_{n,i,c} +``` + +**`flow_out_min`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_out\_min\_relative}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathrm{flow\_out\_min\_relative}_{n,i,t} \text{ is defined} +``` + +**`flow_in_max`** + +```math +\mathit{flow\_in}_{n,i,c,t} \le \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} +``` + +**`flow_capacity_systemwide_max`** + +```math +\sum_{n \in \mathcal{N}} \mathit{flow\_cap}_{n,i,c} \le \mathrm{flow\_cap\_max\_systemwide}_{i,c} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{flow\_cap\_max\_systemwide}_{i,c} \text{ is defined} +``` + +**`flow_capacity_systemwide_min`** + +```math +\sum_{n \in \mathcal{N}} \mathit{flow\_cap}_{n,i,c} \ge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \text{ is defined} +``` + +**`ramping_up`** + +```math +\mathit{ramping\_flow}_{n,i,c,t} - \mathit{ramping\_flow}_{n,i,c,t - 1} \le \mathrm{flow\_ramping}_{n,i} \cdot \mathit{flow\_cap}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \left( \mathrm{carrier\_in}_{n,i,c} \vee \mathrm{carrier\_out}_{n,i,c} \right) \wedge \mathrm{flow\_ramping}_{n,i} \text{ is defined} \wedge \mathrm{pos}(t) > 0 +``` + +**`ramping_down`** + +```math +-1 \cdot \mathrm{flow\_ramping}_{n,i} \cdot \mathit{flow\_cap}_{n,i,c} \le \mathit{ramping\_flow}_{n,i,c,t} - \mathit{ramping\_flow}_{n,i,c,t - 1} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \left( \mathrm{carrier\_in}_{n,i,c} \vee \mathrm{carrier\_out}_{n,i,c} \right) \wedge \mathrm{flow\_ramping}_{n,i} \text{ is defined} \wedge \mathrm{pos}(t) > 0 +``` + +#### Definitions + +**`flow_out_inc_eff`** + +```math +\mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \begin{cases} \frac{\mathit{flow\_out}_{n,i,c,t}}{\mathrm{flow\_out\_eff}_{n,i,c,t} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t} \cdot \mathrm{flow\_out\_eff\_per\_distance}_{n,i,c,t}^{\mathrm{distance}_{i}}} & \text{if } \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \\ \frac{\mathit{flow\_out}_{n,i,c,t}}{\mathrm{flow\_out\_eff}_{n,i,c,t} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t}} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`flow_in_inc_eff`** + +```math +\mathit{flow\_in\_inc\_eff}_{n,i,c,t} = \begin{cases} \mathit{flow\_in}_{n,i,c,t} \cdot \mathrm{flow\_in\_eff}_{n,i,c,t} \cdot \mathrm{flow\_in\_eff\_per\_distance}_{n,i,c,t}^{\mathrm{distance}_{i}} & \text{if } \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \\ \mathit{flow\_in}_{n,i,c,t} \cdot \mathrm{flow\_in\_eff}_{n,i,c,t} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`ramping_flow`** + +```math +\mathit{ramping\_flow}_{n,i,c,t} = \begin{cases} \frac{\mathit{flow\_out}_{n,i,c,t}}{\mathrm{timestep\_resolution}_{t}} & \text{if } \mathrm{carrier\_out}_{n,i,c} \wedge \neg \mathrm{carrier\_in}_{n,i,c} \\ \frac{\mathit{flow\_in}_{n,i,c,t}}{\mathrm{timestep\_resolution}_{t}} & \text{if } \mathrm{carrier\_in}_{n,i,c} \wedge \neg \mathrm{carrier\_out}_{n,i,c} \\ \frac{\mathit{flow\_out}_{n,i,c,t} - \mathit{flow\_in}_{n,i,c,t}}{\mathrm{timestep\_resolution}_{t}} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`cost_flow_cap_sum`** + +```math +\mathrm{cost\_flow\_cap\_sum}_{n,i,k} = \begin{cases} \left( \mathrm{cost\_flow\_cap}_{n,i,k} + \mathrm{cost\_flow\_cap\_per\_distance}_{n,i,k} \cdot \mathrm{distance}_{i} \right) \cdot 0.5 & \text{if } \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \\ \mathrm{cost\_flow\_cap}_{n,i,k} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`cost_investment_flow_cap`** + +```math +\mathit{cost\_investment\_flow\_cap}_{n,i,c,k} = \mathrm{cost\_flow\_cap\_sum}_{n,i,k} \cdot \mathit{flow\_cap}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} +``` + +**`flows_carrier_flow`** + +```math +\mathit{flows\_carrier\_flow}_{n,c,t} = \sum_{i \in \mathcal{I}} \mathit{flow\_out}_{n,i,c,t} - \left( \sum_{i \in \mathcal{I}} \mathit{flow\_in}_{n,i,c,t} \right) \qquad \forall\, n \in \mathcal{N},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`flows_cost_investment`** + +```math +\mathit{flows\_cost\_investment}_{n,i,k} = \sum_{c \in \mathcal{C}} \mathit{cost\_investment\_flow\_cap}_{n,i,c,k} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`flows_cost_operation_variable`** + +```math +\mathit{flows\_cost\_operation\_variable}_{n,i,k,t} = \mathrm{timestep\_weights}_{t} \cdot \left( \sum_{c \in \mathcal{C}} \mathrm{cost\_flow\_out}_{n,i,k,t} \cdot \mathit{flow\_out}_{n,i,c,t} + \sum_{c \in \mathcal{C}} \mathrm{cost\_flow\_in}_{n,i,k,t} \cdot \mathit{flow\_in}_{n,i,c,t} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K},\ t \in \mathcal{T} +``` + +**`flows_cost_operation_fixed`** + +```math +\mathit{flows\_cost\_operation\_fixed}_{n,i,k} = \mathit{annualisation\_weight} \cdot \left( \sum_{c \in \mathcal{C}} \mathrm{cost\_om\_annual}_{n,i,k} \cdot \mathit{flow\_cap}_{n,i,c} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`flow_cap`** + +```math +\mathrm{flow\_cap\_min}_{n,i} \le \mathit{flow\_cap}_{n,i,c} \le \mathrm{flow\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathrm{carrier\_in}_{n,i,c} \vee \mathrm{carrier\_out}_{n,i,c} +``` + +**`flow_out`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \neg \left( \mathrm{one\_way}_{i} \wedge \left( i,\ n \right) \in \mathrm{link\_from} \right) +``` + +**`flow_in`** + +```math +\mathit{flow\_in}_{n,i,c,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} \wedge \neg \left( \mathrm{one\_way}_{i} \wedge \left( i,\ n \right) \in \mathrm{link\_to} \right) +``` + +#### Assumptions + +**`must_have_base`** + +```math +\mathrm{base\_tech}_{i} \text{ is defined} \qquad \forall\, i \in \mathcal{I} +``` + +**`base_tech_one_of`** + +```math +\mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \qquad \forall\, i \in \mathcal{I} +``` + +**`distance_only_for_transmission`** + +```math +\mathrm{distance}_{i} = 1 \wedge \mathrm{flow\_in\_eff\_per\_distance}_{n,i,c,t} = 1 \wedge \mathrm{flow\_out\_eff\_per\_distance}_{n,i,c,t} = 1 \wedge \neg \left( \mathrm{cost\_flow\_cap\_per\_distance}_{n,i,k} \text{ is defined} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K},\ t \in \mathcal{T} \,:\, \neg \left( \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} \right) +``` + +**`unbounded_flow_cap_cost`** + +```math +\neg \left( \mathrm{cost\_flow\_cap}_{n,i,k} < 0 \right) \vee \mathrm{flow\_cap\_max}_{n,i} \text{ is defined} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + diff --git a/docs/examples/calliope/index.md b/docs/examples/calliope/index.md new file mode 100644 index 00000000..575f7243 --- /dev/null +++ b/docs/examples/calliope/index.md @@ -0,0 +1,148 @@ + + +# Calliope in fragments + +[Calliope](https://github.com/calliope-project/calliope) states its math in +YAML: a base, modes laid over it, and examples of math a modeller adds. This +is all of it, from Calliope `v0.7.0` (`src/calliope/math/` and +`docs/user_defined_math/examples/`), as files under `examples/calliope/`. +[The port record](port.md) lists every Calliope block with how it is stated +here, and what mathspec is missing where the port is not one block for one. + +The math is Calliope's, copyright Calliope contributors, under the +[Apache License 2.0](https://github.com/calliope-project/calliope/blob/main/LICENSE). +The files under `examples/calliope/` are changed from it and are under the same +licence. [The port record](port.md) says what changed. + +Calliope composes its math by overriding: a mode or an example restates a +base block whole to change it. Here, three kinds of file do that work, and +[the PyPSA split](../pypsa/index.md) uses the first two. + +- **A base fragment** is one topic of Calliope's base math. `merge` composes + the fragments into one spec. +- **An extension** adds to the base: new rows, new decisions, and a term to a + sum the base declares. `merge` composes it with the base. Where Calliope + restates `system_balance`, `cost_investment`, `cost_operation_fixed` or the + objective to add one term, the extension adds the term and no base file + changes. +- **A variant** changes what the base states: a mode, or an example that + rewrites a base row. `override` lays it over the composition. A variant is + a patch, not a spec, and it prints as the declarations it writes. + +## Compose a model + +```python +from pathlib import Path + +import mathspec as ms + +here = Path('examples/calliope') +base = sorted(here.glob('*.yaml')) + +plan = ms.merge(base) +milp = ms.override(ms.merge([*base, here / 'extensions/milp.yaml']), [here / 'variants/milp.yaml']) +operate = ms.override(plan, [here / 'variants/operate.yaml']) +spores = ms.override(plan, [here / 'variants/spores.yaml']) +clustered = ms.override(plan, [here / 'variants/storage_inter_cluster.yaml']) +``` + +A mode and an extension compose as Calliope's do: the MILP fragment and its +patch, then operate mode and its MILP half, then an example. + +```python +fuel = ms.merge([*base, here / 'extensions/fuel_dist.yaml']) +chp = ms.override(ms.merge([*base, here / 'extensions/chp_htp.yaml']), [here / 'variants/chp_htp.yaml']) +``` + +## What a file needs beside it + +A fragment reads what it does not declare under +[`given`](../../reference/language/declarations.md#given), and loads and +prints alone. A model needs the file that declares each name a fragment +reads, and `merge` keeps what no file declares under `given:`. + +- `settings`, `balance`, `flows` and `cost` are the core. They read one + another, and the four compose with nothing left under `given:`. +- `conversion` and `transmission` read `flows` alone. `storage`, `export`, + `feasibility` and `reporting` read the core, and `reporting` reads `export`. +- `area`, `demand` and `supply` read one another. A source or a sink per unit + of area reads `area_use`, and `area_use` is built where a source or a sink + is per unit of area. Compose the three together. Without `demand` and + `supply`, `area` keeps `sink_unit` and `source_unit` under `given:`. +- `supply_storage` couples `supply` and `storage`, and needs both. +- `extensions/milp.yaml` reads the capacities of `storage`, `area` and + `supply`. `piecewise_linear_costs`, `piecewise_linear_efficiency` and + `uptime_downtime_limits` read the units it builds, so they need it. +- `piecewise_linear_costs` and `sos2_piecewise_linear_costs` both declare + `piecewise_cost_investment`, so `merge` refuses the two together. The CHP + variants both rewrite `balance_conversion`. Calliope's examples are + alternatives in the same way. + +## Conventions + +- **Names are Calliope's.** A declaration Calliope names is spelled as + Calliope spells it, and its description opens with that name. A sub-expression + Calliope writes as `$name` is a named expression. A name Calliope has no word + for, such as a term of a sum, carries the name of the file that adds it. +- **A parameter's default is data preparation.** Calliope reads a missing + value as the parameter's `default:` in arithmetic, and as not given in a + `where:`. Here a missing row reads as `0` and as false. Where a default is + not zero and the math reads the value, data prep fills it, and the + description says so. A bound has a row wherever its variable has one, so + data prep fills its default too. Where the math reads the parameter only + behind a `where:` that tests it, the description says "given only where set". +- **A variable with a mask is zero outside it.** Calliope's `default: 0` is + `absence: zero`, so a sum of costs keeps its terms where one of them is + masked. +- **A parameter carries the dimensions Calliope's examples give it.** The + timesteps are added where Calliope resamples the parameter. + + +### The sums + +| Sum | Over | Declared in | The terms, by the fragment that adds each | +| --- | --- | --- | --- | +| `cost_investment` | `nodes, techs, costs` | [cost](cost.md) | [`cost_investment_area_use`](area.md), [`flows_cost_investment`](flows.md), [`cost_investment_purchase`](extensions/milp.md), [`piecewise_cost_investment_term`](extensions/piecewise_linear_costs.md), [`cost_investment_piecewise`](extensions/sos2_piecewise_linear_costs.md), [`cost_investment_storage_cap`](storage.md), [`cost_investment_source_cap`](supply.md) | +| `carrier_flow` | `nodes, carriers, timesteps` | [balance](balance.md) | [`export_carrier_flow`](export.md), [`feasibility_carrier_flow`](feasibility.md), [`flows_carrier_flow`](flows.md), [`fuel_dist_carrier_flow`](extensions/fuel_dist.md) | +| `cost_operation_variable` | `nodes, techs, costs, timesteps` | [cost](cost.md) | [`export_cost_operation_variable`](export.md), [`flows_cost_operation_variable`](flows.md), [`supply_cost_operation_variable`](supply.md) | +| `system_cost` | nothing: one number | [settings](settings.md) | [`cost_of_techs`](cost.md), [`fuel_dist_system_cost`](extensions/fuel_dist.md) | +| `cost_operation_fixed` | `nodes, techs, costs` | [cost](cost.md) | [`flows_cost_operation_fixed`](flows.md), [`cost_month_peak_charge`](extensions/monthly_peak_flow_charge.md) | +| `penalty` | nothing: one number | [settings](settings.md) | [`unmet_demand_penalty`](feasibility.md) | + +### The fragments + +| Fragment | Parameters | Variables | Constraints | Reads | Adds to | +| --- | --- | --- | --- | --- | --- | +| [area](area.md) | 5 | 1 | 3 | 5 | `cost_investment` | +| [balance](balance.md) | 0 | 0 | 1 | 2 | | +| [conversion](conversion.md) | 0 | 0 | 1 | 4 | | +| [cost](cost.md) | 4 | 0 | 0 | 4 | `system_cost` | +| [demand](demand.md) | 4 | 0 | 3 | 5 | | +| [export](export.md) | 4 | 1 | 1 | 5 | `carrier_flow`, `cost_operation_variable` | +| [feasibility](feasibility.md) | 0 | 2 | 0 | 6 | `carrier_flow`, `penalty` | +| [flows](flows.md) | 22 | 3 | 7 | 7 | `carrier_flow`, `cost_investment`, `cost_operation_variable`, `cost_operation_fixed` | +| [reporting](reporting.md) | 0 | 0 | 0 | 6 | | +| [settings](settings.md) | 4 | 0 | 0 | 0 | | +| [storage](storage.md) | 10 | 2 | 6 | 7 | `cost_investment` | +| [supply](supply.md) | 10 | 2 | 6 | 10 | `cost_investment`, `cost_operation_variable` | +| [supply_storage](supply_storage.md) | 0 | 0 | 1 | 7 | | +| [transmission](transmission.md) | 0 | 0 | 2 | 5 | | +| [annual_energy_balance](extensions/annual_energy_balance.md) | 5 | 0 | 5 | 4 | | +| [chp_htp](extensions/chp_htp.md) | 4 | 0 | 6 | 6 | | +| [demand_share_per_timestep_decision](extensions/demand_share_per_timestep_decision.md) | 2 | 1 | 3 | 2 | | +| [fuel_dist](extensions/fuel_dist.md) | 4 | 1 | 3 | 4 | `carrier_flow`, `system_cost` | +| [max_time_varying](extensions/max_time_varying.md) | 1 | 0 | 1 | 3 | | +| [milp](extensions/milp.md) | 11 | 4 | 26 | 22 | `cost_investment` | +| [monthly_peak_flow_charge](extensions/monthly_peak_flow_charge.md) | 2 | 1 | 1 | 4 | `cost_operation_fixed` | +| [net_import_share](extensions/net_import_share.md) | 1 | 0 | 3 | 4 | | +| [piecewise_linear_costs](extensions/piecewise_linear_costs.md) | 2 | 1 | 1 | 3 | `cost_investment` | +| [piecewise_linear_efficiency](extensions/piecewise_linear_efficiency.md) | 2 | 0 | 1 | 3 | | +| [share_all_timesteps](extensions/share_all_timesteps.md) | 2 | 0 | 2 | 2 | | +| [share_per_timestep](extensions/share_per_timestep.md) | 2 | 0 | 2 | 2 | | +| [sos2_piecewise_linear_costs](extensions/sos2_piecewise_linear_costs.md) | 2 | 2 | 1 | 2 | `cost_investment` | +| [uptime_downtime_limits](extensions/uptime_downtime_limits.md) | 4 | 0 | 4 | 6 | | +| [urban_scale_chp](extensions/urban_scale_chp.md) | 1 | 0 | 2 | 5 | | + diff --git a/docs/examples/calliope/port.md b/docs/examples/calliope/port.md new file mode 100644 index 00000000..515e7b97 --- /dev/null +++ b/docs/examples/calliope/port.md @@ -0,0 +1,197 @@ + + +# The port record + +Every block of Calliope `v0.7.0`'s math, with the file that states it here. +A block is **done** where one block states it as Calliope does. It is +**split** where the same rows and the same optimum have a different +statement, such as two blocks for two `equations:`, or a term of a sum for a +restated sum. It is **prep** where a column the file cannot compute comes +from data preparation. It is **out** where the file deliberately states +nothing, and the note says why. + +Parameters are not listed: each one Calliope declares is a parameter of the +same name, in the file that reads it, with [the conventions](index.md#conventions) +for defaults and dimensions. The one exception is `storage_loss` in the +inter-cluster variant, a row below. + +## What mathspec is missing + +Each gap below is a place the port is not one block for one. None stops a +block from being stated. + +1. **Closed: a power or a quotient over a sum of parameters.** mathspec + refused `(1 - storage_loss) ** timestep_resolution` and Calliope's annuity + factor, whose base and divisor are sums. + [#757](https://github.com/energy-models/mathspec/pull/757) admits them, and + the port writes both as Calliope does. +2. **A default that is not zero.** Calliope's `default:` is the value + arithmetic reads and, at the same time, "not given" to a `where:`. A + mathspec parameter has one reading of a missing row, `0`, so data prep + fills each default the math reads. Two masks change: `area_use` is built + where `area_use_min > 0`, where Calliope builds it where `area_use_min` is + given at all, and `distance_only_for_transmission` checks the filled + defaults, not what the modeller wrote. +3. **A `where:` on a `piecewise:` block.** A curve has a row per link at every + coordinate of its frame. Calliope's SOS2 costs hold only where a technology + has breakpoints, and elsewhere the link row would pin `flow_cap` to zero. + The link reads `piecewise_flow_cap`, a copy of `flow_cap` masked to the + technologies with a curve. +4. **One label of a dimension.** Calliope writes + `flow_out[carriers=electricity]`. Here a named expression takes the value + where `carriers == electricity` and zero elsewhere, and a sum over + `carriers` reads it. The CHP, net-import and urban-scale files hold eleven + such expressions. +5. **A label read off the coordinate it indexes.** Calliope's `map_dim(nodes, +link_from)`, and a slice by a per-technology carrier, compare a lookup with + the dimension the operand already carries. `at` reads onto a dimension the + operand does not carry, and a `where:` does not compare a relation column + with its dimension. The port declares these lookups as bare relations over + the pair, `link_from: {key: [techs, nodes]}`, and tests the pair in a case. +6. **One parameter at several shapes.** Calliope's `annual_flow_max` and + `net_import_share` are read per technology, per node and over a group, and + the data decides. A mathspec parameter has one shape, so the group row of + `annual_energy_balance` reads a scalar of its own, and `net_import_share` + is one number. +7. **An empty sum that no file adds to.** An `empty: true` sum with no term is + a column the spec reads and does not build, not zero. `penalty` has the body + `0`, so a model with no feasibility file reads a zero penalty. +8. **Closed: `sum(over=[a, b])`.** Calliope sums over a list of dimensions. + [#778](https://github.com/energy-models/mathspec/pull/778) admits the list, + and [#779](https://github.com/energy-models/mathspec/pull/779) admits it in + a macro, so the port writes each sum as one call. +9. **A warning.** An assumption holds or the data is refused. Calliope's three + checks at `errors: warn` are out. + +The port also found two mathspec bugs, fixed in their own pull requests: +`advice` raised `KeyError` on an objective that reads a sum other files add +to ([#767](https://github.com/energy-models/mathspec/pull/767)), and `merge` +refused a given variable read by its frame alone where the introducer is an +integer ([#768](https://github.com/energy-models/mathspec/pull/768)). + +## `base.yaml` + +| Calliope | status | here | note | +| -------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| dimensions `carriers`, `costs`, `nodes`, `techs`, `timesteps` | done | every fragment | `clusters` and `datesteps` in the inter-cluster variant | +| lookups `base_tech`, `carrier_in`, `carrier_out`, `include_storage`, `one_way` | done | [flows](flows.md) | a `one_of` is an assumption | +| lookups `link_from`, `link_to` | split | [flows](flows.md), [transmission](transmission.md) | a bare relation over technology and node, gap 5 | +| lookups `carrier_export`, `cyclic_storage`, `cluster_first_timestep`, `source_cap_equals_flow_cap` | done | [export](export.md), [storage](storage.md), [supply](supply.md) | | +| lookup `lookup_cluster_last_timestep` | done | [storage](storage.md) | a relation from a time step onto a time step | +| lookups `sink_unit`, `source_unit` | done | [demand](demand.md), [supply](supply.md) | `absolute`, the default, is what no row reads as | +| lookups `latitude`, `longitude` | out | | read by no math; Calliope derives `distance` from them in data preparation | +| `flow_capacity_per_storage_capacity_min`, `_max` | done | [storage](storage.md) | | +| `source_capacity_equals_flow_capacity` | done | [supply](supply.md) | | +| `force_zero_area_use`, `area_use_per_flow_capacity` | done | [area](area.md) | | +| `area_use_capacity_per_loc` | split | [area](area.md) | `where: area_use` over a node is a count over technologies | +| `flow_capacity_systemwide_max`, `_min` | done | [flows](flows.md) | `any(…, over=nodes)` is `count(…, over=nodes) >= 1` | +| `balance_conversion` | done | [conversion](conversion.md) | | +| `flow_out_max`, `flow_out_min`, `flow_in_max` | done | [flows](flows.md) | | +| `source_max` | done | [supply](supply.md) | | +| `storage_max`, `storage_discharge_depth_limit` | done | [storage](storage.md) | | +| `system_balance` | split | [balance](balance.md) | `carrier_flow == 0`, the sum of a term from each file that moves a carrier | +| `balance_demand` | split | [demand](demand.md) | `balance_demand_equals` and `balance_demand_max`, one block per equation | +| `balance_demand_min_use` | done | [demand](demand.md) | | +| `balance_supply_no_storage` | done | [supply](supply.md) | | +| `balance_supply_with_storage` | done | [supply_storage](supply_storage.md) | | +| `source_availability_supply` | split | [supply](supply.md) | `_equals` and `_max`, one block per equation | +| `balance_supply_min_use` | done | [supply](supply.md) | | +| `balance_storage` | done | [storage](storage.md) | | +| `set_storage_initial` | split | [storage](storage.md) | one row per store, built at the last time step rather than read there | +| `balance_transmission`, `symmetric_transmission` | done | [transmission](transmission.md) | `where(flow_cap, map_dim(…))` is a case over the bare relation, gap 5 | +| `export_balance` | done | [export](export.md) | | +| `ramping_up`, `ramping_down` | done | [flows](flows.md) | the first time step is out of the row, so `roll` is a `shift` | +| variables `flow_cap`, `flow_out`, `flow_in`, `flow_export`, `source_use`, `source_cap`, `storage_cap`, `storage` | done | [flows](flows.md), [export](export.md), [supply](supply.md), [storage](storage.md) | `default: 0` is `absence: zero` | +| variable `area_use` | split | [area](area.md) | built where `area_use_min > 0`, gap 2 | +| variables `unmet_demand`, `unused_supply` | split | [feasibility](feasibility.md) | `config.ensure_feasibility` is whether the file is composed | +| objective `min_cost_optimisation` | split | [settings](settings.md) | `system_cost + penalty`, each a sum other files add to | +| `$unmet_demand` | done | [feasibility](feasibility.md) | `unmet_demand_penalty`, a term of `penalty` | +| `flow_out_inc_eff`, `flow_in_inc_eff` | done | [flows](flows.md) | | +| `cost_operation_variable` | split | [cost](cost.md) | a sum; flows, supply and export each add their cost. Calliope's two cases, supply or not, are one sum, since a supply technology has no inflow | +| `cost_investment_flow_cap`, `_storage_cap`, `_source_cap`, `_area_use` | done | [flows](flows.md), [storage](storage.md), [supply](supply.md), [area](area.md) | each is a term of `cost_investment`, and the flow capacity one through `flows_cost_investment` | +| `cost_investment` | split | [cost](cost.md) | a sum each capacity adds to | +| `cost_investment_annualised` | done | [cost](cost.md) | `$depreciation_rate` is a named expression with cases | +| `cost_operation_fixed` | split | [cost](cost.md) | its body here, and `flows_cost_operation_fixed` added | +| `cost` | done | [cost](cost.md) | | +| postprocessed `capacity_factor`, `systemwide_capacity_factor`, `total_generation`, `systemwide_levelised_cost`, `total_levelised_cost` | done | [reporting](reporting.md) | reported; the `where:` is the absence of what they read | +| postprocessed `unmet_sum` | done | [feasibility](feasibility.md) | reported | +| postprocessed `curtailment`, `total_curtailment` | done | [supply](supply.md) | reported | +| check `must_have_base` | done | [flows](flows.md) | an assumption | +| check `distance_only_for_transmission` | split | [flows](flows.md) | checks the filled defaults, gap 2 | +| checks `unbounded_*_cost` | done | [flows](flows.md), [storage](storage.md), [area](area.md), [supply](supply.md) | | +| check `finite_source_use` | split | [supply](supply.md), [demand](demand.md) | one assumption per parameter | +| check `all_or_nothing_lat_lon` | out | | the coordinates are data preparation | +| checks `export_only_for_outflows`, `storage_initial_max`, `cyclic_storage_needs_inter_cluster` | done | [export](export.md), [storage](storage.md) | | +| checks `flow_in_for_supply_tech`, `flow_out_for_demand_tech`, `no_storage_discharge_depth_when_clustering` | out | | warnings, gap 9 | + +## `milp.yaml` + +| Calliope | status | here | note | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------- | ----------------------------------------------------------------------------- | +| lookups `cap_method`, `integer_dispatch`, `force_async_flow` | done | [milp](extensions/milp.md) | | +| variables `purchased_units`, `operating_units`, `available_flow_cap` | done | [milp](extensions/milp.md) | | +| variable `async_flow_switch` | done | [milp](extensions/milp.md) | an integer in `[0, 1]` is `domain: binary` | +| bounds of `flow_cap`, `area_use`, `source_cap`, `storage_cap` | done | [variant](variants/milp.md) | | +| `unit_commitment_milp`, `flow_out_max_milp`, `flow_in_max_milp`, `storage_capacity_units_milp`, `flow_capacity_units_milp`, `storage_capacity_max_purchase_milp`, `unit_capacity_max_systemwide_milp`, `async_flow_in_milp`, `async_flow_out_milp`, `available_flow_cap_continuous`, `available_flow_cap_binary`, `available_flow_cap_max_binary_continuous_switch` | done | [milp](extensions/milp.md) | | +| `unit_capacity_min_systemwide_milp` | done | [milp](extensions/milp.md) | built where the system-wide maximum is set, as Calliope writes it | +| `flow_out_min_milp`, `flow_capacity_max_purchase_milp`, `flow_capacity_minimum`, `storage_capacity_minimum`, `area_use_minimum`, `source_capacity_minimum` | split | [milp](extensions/milp.md) | one block per equation | +| `flow_out_max`, `flow_out_min`, `flow_in_max` | done | [variant](variants/milp.md) | the `where:` only | +| `flow_capacity_systemwide_min` | split | [variant](variants/milp.md), [milp](extensions/milp.md) | the patch narrows the base row; the purchased case is a block of the fragment | +| `cost_investment_purchase` | done | [milp](extensions/milp.md) | a term of `cost_investment` | +| `cost_investment` | split | [milp](extensions/milp.md) | Calliope restates it whole; here the purchase is a term | +| descriptions of `flow_cap_min`, `flow_cap_min_systemwide`, `flow_out_min_relative` | done | [variant](variants/milp.md) | | +| checks | done | [milp](extensions/milp.md) | assumptions; `cap_method`'s `one_of` too | + +## `operate.yaml` + +| Calliope | status | here | note | +| -------------------------------------------------------------- | ------ | ------------------------------------------------------------------------ | -------------------------------------------------------------- | +| parameters `flow_cap`, `area_use`, `source_cap`, `storage_cap` | done | [variant](variants/operate.md) | each replaces the variable of its name | +| parameter `purchased_units` | done | [variant](variants/operate_milp.md) | | +| every `.active: false` | done | [operate](variants/operate.md), [operate_milp](variants/operate_milp.md) | a removal; the terms of the removed sums are removed with them | +| `cost` | done | [variant](variants/operate.md) | | +| check `operate_mode_cyclic_storage` | done | [variant](variants/operate.md) | | +| operate mode's window and horizon | out | | a loop of solves, not math | + +## `spores.yaml` + +| Calliope | status | here | note | +| ----------------------------------------------------------------------- | ------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| parameters | done | [variant](variants/spores.md) | | +| `total_system_cost_max` | split | [variant](variants/spores.md) | one row over `system_cost + penalty`. Calliope restates the objective, and misses a cost an example adds to it, such as fuel distribution | +| objective `min_spores` | done | [variant](variants/spores.md) | | +| postprocessed `spores_score_cumulative`, `spores_baseline_cost_tracked` | done | [variant](variants/spores.md) | reported | +| the SPORES iterations | out | | a loop of solves that updates `spores_score`, not math | + +## `storage_inter_cluster.yaml` + +| Calliope | status | here | note | +| ----------------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------- | ------------------------------------------------------------------------------------------ | +| lookups `timestep_cluster`, `lookup_datestep_cluster`, `lookup_datestep_last_cluster_timestep` | done | [variant](variants/storage_inter_cluster.md) | relations | +| `storage_max`, `cyclic_storage_needs_inter_cluster` off | done | [variant](variants/storage_inter_cluster.md) | removals | +| `$storage_previous_step` of `balance_supply_with_storage` and `balance_storage` | done | [variant](variants/storage_inter_cluster.md) | one named expression, patched once | +| `set_storage_initial` | split | [variant](variants/storage_inter_cluster.md) | built at the last day, as in the base | +| `storage_intra_max`, `storage_intra_min`, `storage_inter_max`, `storage_inter_min`, `balance_storage_inter` | done | [variant](variants/storage_inter_cluster.md) | a slice by a lookup is `at` through the relation | +| variables | done | [variant](variants/storage_inter_cluster.md) | | +| `storage_loss` over time steps | prep | [variant](variants/storage_inter_cluster.md) | `(1 - storage_loss) ** 24` reads it per day, so the variant declares it without time steps | + +## The examples + +| Calliope | status | here | note | +| ----------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `annual_energy_balance.yaml` | split | [annual_energy_balance](extensions/annual_energy_balance.md) | the group row reads `annual_flow_max_group`, gap 6; `where: source_use` over a technology is `base_tech == 'supply'` | +| `chp_htp.yaml` | split | [chp_htp](extensions/chp_htp.md), [variant](variants/chp_htp.md) | the rewritten `balance_conversion` is a patch and a block of its own; a carrier slice is a case, gap 4 | +| `demand_share_per_timestep_decision.yaml` | split | [demand_share_per_timestep_decision](extensions/demand_share_per_timestep_decision.md) | the sum row is one per node: Calliope repeats it in every time step, and mathspec refuses a row repeated along a dimension it does not read | +| `fuel_dist.yaml` | split | [fuel_dist](extensions/fuel_dist.md) | Calliope restates `system_balance` and the objective; here each gains a term | +| `max_time_varying.yaml` | done | [max_time_varying](extensions/max_time_varying.md) | | +| `monthly_peak_flow_charge.yaml` | split | [monthly_peak_flow_charge](extensions/monthly_peak_flow_charge.md) | the month is a relation, so the row is one per time step; the charge is a term of `cost_operation_fixed` | +| `net_import_share.yaml` | split | [net_import_share](extensions/net_import_share.md) | a carrier and a node group are cases, gap 4; the share is one number, gap 6 | +| `piecewise_linear_costs.yaml` | split | [piecewise_linear_costs](extensions/piecewise_linear_costs.md) | a term of `cost_investment` | +| `piecewise_linear_efficiency.yaml` | done | [piecewise_linear_efficiency](extensions/piecewise_linear_efficiency.md) | | +| `share_all_timesteps.yaml`, `share_per_timestep.yaml` | split | [share_all_timesteps](extensions/share_all_timesteps.md), [share_per_timestep](extensions/share_per_timestep.md) | the demand technology is `at` through a relation; the carrier is a case, gap 5 | +| `sos2_piecewise_linear_costs.yaml` | split | [sos2_piecewise_linear_costs](extensions/sos2_piecewise_linear_costs.md) | `piecewise:` with `method: sos2`, over a masked copy of `flow_cap`, gap 3 | +| `uptime_downtime_limits.yaml` | done | [uptime_downtime_limits](extensions/uptime_downtime_limits.md) | | +| urban-scale `additional_math.yaml` | split | [urban_scale_chp](extensions/urban_scale_chp.md), [variant](variants/urban_scale_chp.md) | as the CHP example | diff --git a/docs/examples/calliope/reporting.md b/docs/examples/calliope/reporting.md new file mode 100644 index 00000000..d9421c5f --- /dev/null +++ b/docs/examples/calliope/reporting.md @@ -0,0 +1,121 @@ + + +# Reporting + +One of the base fragments of [Calliope in fragments](index.md). Calliope's postprocessed results: capacity factors, total generation and levelised costs. Every entry is reported, so none of it is in the math, and a quotient may divide by a variable. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_export: { dims: [nodes, techs, carriers, timesteps] } + expressions: + cost: { dims: [nodes, techs, costs] } + +expressions: + capacity_factor: + description: "`capacity_factor` — the share of its flow capacity a technology puts out in a time step" + expression: flow_out / (flow_cap * timestep_resolution) + systemwide_capacity_factor: + description: "`systemwide_capacity_factor` — the share of its flow capacity a technology puts out over every node and time step" + expression: >- + sum(flow_out * timestep_weights, over=[nodes, timesteps]) + / (sum(flow_cap, over=nodes) * sum(timestep_resolution * timestep_weights, over=timesteps)) + total_generation: + description: >- + `total_generation` — outflow over every node and time step. Calliope + weights only the export, as written here + expression: sum(flow_out + flow_export * timestep_weights, over=[nodes, timesteps]) + systemwide_levelised_cost: + description: "`systemwide_levelised_cost` — a technology's cost per unit of what it generates, over every node" + expression: sum(cost, over=nodes) / total_generation + total_levelised_cost: + description: "`total_levelised_cost` — the system's cost per unit of a carrier generated" + expression: sum(cost, over=[nodes, techs]) / sum(total_generation, over=techs) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{flow\_out}`$ | `flow_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{flow\_export}`$ | `flow_export` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ | +| $`\mathit{cost}`$ | `cost` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression another file defines | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{capacity\_factor}`$ | `capacity_factor` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$ — `capacity_factor` — the share of its flow capacity a technology puts out in a time step | +| $`\mathit{systemwide\_capacity\_factor}`$ | `systemwide_capacity_factor` over $`\mathcal{I} \times \mathcal{C}`$ — `systemwide_capacity_factor` — the share of its flow capacity a technology puts out over every node and time step | +| $`\mathit{total\_generation}`$ | `total_generation` over $`\mathcal{I} \times \mathcal{C}`$ — `total_generation` — outflow over every node and time step. Calliope weights only the export, as written here | +| $`\mathit{systemwide\_levelised\_cost}`$ | `systemwide_levelised_cost` over $`\mathcal{I} \times \mathcal{C} \times \mathcal{K}`$ — `systemwide_levelised_cost` — a technology's cost per unit of what it generates, over every node | +| $`\mathit{total\_levelised\_cost}`$ | `total_levelised_cost` over $`\mathcal{C} \times \mathcal{K}`$ — `total_levelised_cost` — the system's cost per unit of a carrier generated | + +#### Definitions + +**`capacity_factor`** + +```math +\mathit{capacity\_factor}_{n,i,c,t} = \frac{\mathit{flow\_out}_{n,i,c,t}}{\mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t}} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} +``` + +**`systemwide_capacity_factor`** + +```math +\mathit{systemwide\_capacity\_factor}_{i,c} = \frac{\sum_{n \in \mathcal{N},\ t \in \mathcal{T}} \mathit{flow\_out}_{n,i,c,t} \cdot \mathrm{timestep\_weights}_{t}}{\left( \sum_{n \in \mathcal{N}} \mathit{flow\_cap}_{n,i,c} \right) \cdot \left( \sum_{t \in \mathcal{T}} \mathrm{timestep\_resolution}_{t} \cdot \mathrm{timestep\_weights}_{t} \right)} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`total_generation`** + +```math +\mathit{total\_generation}_{i,c} = \sum_{n \in \mathcal{N},\ t \in \mathcal{T}} \left( \mathit{flow\_out}_{n,i,c,t} + \mathit{flow\_export}_{n,i,c,t} \cdot \mathrm{timestep\_weights}_{t} \right) \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`systemwide_levelised_cost`** + +```math +\mathit{systemwide\_levelised\_cost}_{i,c,k} = \frac{\sum_{n \in \mathcal{N}} \mathit{cost}_{n,i,k}}{\mathit{total\_generation}_{i,c}} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C},\ k \in \mathcal{K} +``` + +**`total_levelised_cost`** + +```math +\mathit{total\_levelised\_cost}_{c,k} = \frac{\sum_{n \in \mathcal{N},\ i \in \mathcal{I}} \mathit{cost}_{n,i,k}}{\sum_{i \in \mathcal{I}} \mathit{total\_generation}_{i,c}} \qquad \forall\, c \in \mathcal{C},\ k \in \mathcal{K} +``` + diff --git a/docs/examples/calliope/settings.md b/docs/examples/calliope/settings.md new file mode 100644 index 00000000..95b67ce4 --- /dev/null +++ b/docs/examples/calliope/settings.md @@ -0,0 +1,108 @@ + + +# Settings + +One of the base fragments of [Calliope in fragments](index.md). The weightings every topic reads, and the objective. The objective is the system cost plus a penalty, and both are sums other files add to, so this file names no technology. + + +```yaml +dimensions: + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + timestep_resolution: + description: >- + `timestep_resolution` — hours a time step lasts. Calliope's default is + 1, and data prep fills it + dims: [timesteps] + timestep_weights: + description: >- + `timestep_weights` — how many times a time step counts, as after + clustering. Calliope's default is 1, and data prep fills it + dims: [timesteps] + objective_cost_weights: + description: >- + `objective_cost_weights` — what one unit of a cost class weighs in the + objective. Calliope's default is 1, and data prep fills it + dims: [costs] + bigM: + description: >- + `bigM` — a number larger than any decision can take. Calliope's + default is 1e6, and data prep fills it + dims: [] + +expressions: + system_cost: + description: >- + the weighted cost of the system, over every cost class — Calliope's + `min_cost_optimisation` less its unmet-demand penalty. The cost file and + every file that prices something outside a technology add to it + dims: [] + empty: true + penalty: + description: >- + what the objective adds to the system cost to keep a model feasible — + Calliope's `$unmet_demand` sub-expression. It is zero, and a file that + keeps a model feasible adds to it + dims: [] + expression: "0" + +objective: + description: >- + `min_cost_optimisation` — the weighted cost of installing and operating + every technology, plus the penalty on unmet demand + sense: minimize + expression: system_cost + penalty +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$ — `timestep_resolution` — hours a time step lasts. Calliope's default is 1, and data prep fills it | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$ — `timestep_weights` — how many times a time step counts, as after clustering. Calliope's default is 1, and data prep fills it | +| $`\mathrm{objective\_cost\_weights}`$ | `objective_cost_weights` over $`\mathcal{K}`$ — `objective_cost_weights` — what one unit of a cost class weighs in the objective. Calliope's default is 1, and data prep fills it | +| $`\mathrm{bigM}`$ | `bigM` (scalar) — `bigM` — a number larger than any decision can take. Calliope's default is 1e6, and data prep fills it | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathrm{penalty}`$ | `penalty` (scalar) — what the objective adds to the system cost to keep a model feasible — Calliope's `$unmet_demand` sub-expression. It is zero, and a file that keeps a model feasible adds to it | +| $`\mathit{system\_cost}`$ | `system_cost` (scalar) — the weighted cost of the system, over every cost class — Calliope's `min_cost_optimisation` less its unmet-demand penalty. The cost file and every file that prices something outside a technology add to it | + +#### Objective + +```math +\min \mathit{system\_cost} + \mathrm{penalty} +``` + +#### Definitions + +**`penalty`** + +```math +\mathrm{penalty} = 0 +``` + +**`system_cost`** + +```math +\mathit{system\_cost} = \cdots +``` + diff --git a/docs/examples/calliope/storage.md b/docs/examples/calliope/storage.md new file mode 100644 index 00000000..8c412246 --- /dev/null +++ b/docs/examples/calliope/storage.md @@ -0,0 +1,319 @@ + + +# Storage + +One of the base fragments of [Calliope in fragments](index.md). Storage capacity, the stored carrier, and how a store carries its fill from one time step to the next, clustered days included. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +relations: + lookup_cluster_last_timestep: + description: >- + `lookup_cluster_last_timestep` — the last time step of the cluster a + time step stands for, at the first time step of each clustered day + key: timesteps + values: { last: timesteps } + +parameters: + storage_cap_min: + description: "`storage_cap_min` — least storage capacity. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + storage_cap_max: + description: "`storage_cap_max` — most storage capacity. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + storage_discharge_depth: + description: "`storage_discharge_depth` — the least a store holds, as a share of its capacity" + dims: [nodes, techs, timesteps] + storage_initial: + description: "`storage_initial` — what a store holds at the start, as a share of its capacity; given only where set" + dims: [nodes, techs] + storage_loss: + description: "`storage_loss` — the share of what a store holds that it loses in an hour" + dims: [nodes, techs, timesteps] + cyclic_storage: + description: >- + `cyclic_storage` — whether a store ends where it starts. Calliope's + default is true, and data prep fills it + dims: [nodes, techs] + dtype: bool + cluster_first_timestep: + description: "`cluster_first_timestep` — whether a time step is the first of its clustered day" + dims: [timesteps] + dtype: bool + flow_cap_per_storage_cap_min: + description: "`flow_cap_per_storage_cap_min` — least flow capacity per unit of storage capacity; given only where set" + dims: [nodes, techs] + flow_cap_per_storage_cap_max: + description: "`flow_cap_per_storage_cap_max` — most flow capacity per unit of storage capacity; given only where set" + dims: [nodes, techs] + cost_storage_cap: + description: "`cost_storage_cap` — the cost of one unit of storage capacity" + dims: [nodes, techs, costs] + +variables: + storage_cap: + description: "`storage_cap` — the most a technology can store" + dims: [nodes, techs] + where: include_storage OR base_tech == 'storage' + bounds: { lower: storage_cap_min, upper: storage_cap_max } + absence: zero + storage: + description: "`storage` — what a technology holds at the end of a time step" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + bounds: { lower: 0 } + absence: zero + +expressions: + storage_previous_step: + description: >- + `$storage_previous_step` — what a store carries into a time step: + its initial fill at the first step of a store that is not cyclic, what + is left of the last step of its clustered day at the first step of a + cluster, and what is left of the step before everywhere else + dims: [nodes, techs, timesteps] + cases: + initial: + when: position(timesteps) == 0 AND NOT cyclic_storage + expression: storage_initial * storage_cap + cluster_start: + when: cluster_first_timestep AND NOT (position(timesteps) == 0 AND NOT cyclic_storage) + expression: >- + (1 - storage_loss) ** at(timestep_resolution, by=lookup_cluster_last_timestep, over=last, into=timesteps) + * at(storage, by=lookup_cluster_last_timestep, over=last, into=timesteps) + otherwise: >- + (1 - storage_loss) ** shift(timestep_resolution, along=timesteps, offset=1, edge='wrap') + * shift(storage, along=timesteps, offset=1, edge='wrap') + cost_investment_storage_cap: + description: "`cost_investment_storage_cap` — the investment cost of storage capacity" + expression: cost_storage_cap * storage_cap + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + timestep_resolution: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_storage_cap } + +constraints: + flow_capacity_per_storage_capacity_min: + description: "`flow_capacity_per_storage_capacity_min` — flow capacity is at least its least share of storage capacity" + dims: [nodes, techs, carriers] + where: flow_cap AND storage_cap AND flow_cap_per_storage_cap_min + expression: flow_cap >= storage_cap * flow_cap_per_storage_cap_min + flow_capacity_per_storage_capacity_max: + description: "`flow_capacity_per_storage_capacity_max` — flow capacity is at most its most share of storage capacity" + dims: [nodes, techs, carriers] + where: flow_cap AND storage_cap AND flow_cap_per_storage_cap_max + expression: flow_cap <= storage_cap * flow_cap_per_storage_cap_max + storage_max: + description: "`storage_max` — a store holds at most its capacity" + dims: [nodes, techs, timesteps] + where: storage + expression: storage <= storage_cap + storage_discharge_depth_limit: + description: "`storage_discharge_depth_limit` — a store holds at least its depth of discharge" + dims: [nodes, techs, timesteps] + where: storage AND storage_discharge_depth + expression: storage - storage_discharge_depth * storage_cap >= 0 + balance_storage: + description: >- + `balance_storage` — what a store holds at the end of a time step is + what it carried in, less what it put out before losses, plus what it + took in after them + dims: [nodes, techs, timesteps] + where: (include_storage OR base_tech == 'storage') AND NOT (base_tech == 'supply' OR base_tech == 'demand') + expression: >- + storage == storage_previous_step + - sum(flow_out_inc_eff, over=carriers) + sum(flow_in_inc_eff, over=carriers) + set_storage_initial: + description: >- + `set_storage_initial` — a cyclic store with an initial fill holds it + at the end, after the last step's loss. Calliope builds one row per + store and reads the last step; this builds that row at the last step + dims: [nodes, techs, timesteps] + where: position(timesteps) == -1 AND storage AND storage_initial AND cyclic_storage + expression: storage * (1 - storage_loss) ** timestep_resolution == storage_initial * storage_cap + +assumptions: + unbounded_storage_cap_cost: + description: Calliope's `unbounded_storage_cap_cost` — a negative storage capacity cost needs a finite maximum + holds: NOT cost_storage_cap < 0 OR storage_cap_max + storage_initial_max: + description: Calliope's `storage_initial_max` — the initial fill is a share + holds: storage_initial >= 0 AND storage_initial <= 1 + where: storage_initial + cyclic_storage_needs_inter_cluster: + description: >- + Calliope's `cyclic_storage_needs_inter_cluster` — a cyclic store under + clustering needs the inter-cluster patch + holds: NOT (cyclic_storage AND lookup_cluster_last_timestep) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` with $`\mathrm{lookup\_cluster\_last\_timestep}: \mathcal{T} \to \mathcal{T}`$ — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{storage}^{\mathrm{cap,min}}`$ | `storage_cap_min` over $`\mathcal{N} \times \mathcal{I}`$ — `storage_cap_min` — least storage capacity. Calliope's default is 0, and data prep fills it | +| $`\mathrm{storage}^{\mathrm{cap,max}}`$ | `storage_cap_max` over $`\mathcal{N} \times \mathcal{I}`$ — `storage_cap_max` — most storage capacity. Calliope's default is `.inf`, and data prep fills it | +| $`\mathrm{storage}^{\mathrm{discharge,depth}}`$ | `storage_discharge_depth` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `storage_discharge_depth` — the least a store holds, as a share of its capacity | +| $`\mathrm{storage}^{\mathrm{initial}}`$ | `storage_initial` over $`\mathcal{N} \times \mathcal{I}`$ — `storage_initial` — what a store holds at the start, as a share of its capacity; given only where set | +| $`\mathrm{storage}^{\mathrm{loss}}`$ | `storage_loss` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `storage_loss` — the share of what a store holds that it loses in an hour | +| $`\mathrm{cyclic\_storage}`$ | `cyclic_storage` over $`\mathcal{N} \times \mathcal{I}`$ — `cyclic_storage` — whether a store ends where it starts. Calliope's default is true, and data prep fills it | +| $`\mathrm{cluster\_first\_timestep}`$ | `cluster_first_timestep` over $`\mathcal{T}`$ — `cluster_first_timestep` — whether a time step is the first of its clustered day | +| $`\mathrm{flow\_cap\_per\_storage\_cap\_min}`$ | `flow_cap_per_storage_cap_min` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_cap_per_storage_cap_min` — least flow capacity per unit of storage capacity; given only where set | +| $`\mathrm{flow\_cap\_per\_storage\_cap\_max}`$ | `flow_cap_per_storage_cap_max` over $`\mathcal{N} \times \mathcal{I}`$ — `flow_cap_per_storage_cap_max` — most flow capacity per unit of storage capacity; given only where set | +| $`\mathrm{cost\_storage\_cap}`$ | `cost_storage_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_storage_cap` — the cost of one unit of storage capacity | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{storage}^{\mathrm{cap}}`$ | `storage_cap` over $`\mathcal{N} \times \mathcal{I}`$ — `storage_cap` — the most a technology can store | +| $`\mathit{storage}`$ | `storage` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `storage` — what a technology holds at the end of a time step | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_investment_storage_cap` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{storage}^{\mathrm{previous,step}}`$ | `storage_previous_step` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `$storage_previous_step` — what a store carries into a time step: its initial fill at the first step of a store that is not cyclic, what is left of the last step of its clustered day at the first step of a cluster, and what is left of the step before everywhere else | +| $`\mathit{cost\_investment\_storage\_cap}`$ | `cost_investment_storage_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment_storage_cap` — the investment cost of storage capacity | + +Upright is what the data supplies — a parameter such as $`\mathrm{storage}^{\mathrm{cap,min}}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{storage}^{\mathrm{cap}}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +$`t \ominus k`$ denotes cyclic translation: index $`t-k`$ taken modulo the size of the dimension (`roll`). Plain $`t-k`$ (`shift`) has no wraparound — terms translated past the edge are simply absent. + +$`\mathrm{pos}(t)`$ denotes where index $`t`$ sits along its dimension's own order — the order `shift` steps along, not the order labels sort in — counted from $`0`$. The index itself stays the coordinate, so $`t`$ compares against labels and $`\mathrm{pos}(t)`$ against positions. + +$`\lvert \mathcal{T} \rvert`$ denotes the size of the set being counted along, and a position counted from the end prints against it — $`\lvert \mathcal{T} \rvert - 1`$ is the last position, one less than the size because the first is $`0`$. + +#### Subject to + +**`flow_capacity_per_storage_capacity_min`** + +```math +\mathit{flow\_cap}_{n,i,c} \ge \mathit{storage}^{\mathrm{cap}}_{n,i} \cdot \mathrm{flow\_cap\_per\_storage\_cap\_min}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{storage}^{\mathrm{cap}}_{n,i} \text{ exists} \wedge \mathrm{flow\_cap\_per\_storage\_cap\_min}_{n,i} \text{ is defined} +``` + +**`flow_capacity_per_storage_capacity_max`** + +```math +\mathit{flow\_cap}_{n,i,c} \le \mathit{storage}^{\mathrm{cap}}_{n,i} \cdot \mathrm{flow\_cap\_per\_storage\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{storage}^{\mathrm{cap}}_{n,i} \text{ exists} \wedge \mathrm{flow\_cap\_per\_storage\_cap\_max}_{n,i} \text{ is defined} +``` + +**`storage_max`** + +```math +\mathit{storage}_{n,i,t} \le \mathit{storage}^{\mathrm{cap}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{storage}_{n,i,t} \text{ exists} +``` + +**`storage_discharge_depth_limit`** + +```math +\mathit{storage}_{n,i,t} - \mathrm{storage}^{\mathrm{discharge,depth}}_{n,i,t} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{storage}_{n,i,t} \text{ exists} \wedge \mathrm{storage}^{\mathrm{discharge,depth}}_{n,i,t} \text{ is defined} +``` + +**`balance_storage`** + +```math +\mathit{storage}_{n,i,t} = \mathit{storage}^{\mathrm{previous,step}}_{n,i,t} - \left( \sum_{c \in \mathcal{C}} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} \right) + \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \left( \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} \right) \wedge \neg \left( \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{demand}\text{'} \right) +``` + +**`set_storage_initial`** + +```math +\mathit{storage}_{n,i,t} \cdot \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i,t} \right)^{\mathrm{timestep\_resolution}_{t}} = \mathrm{storage}^{\mathrm{initial}}_{n,i} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{pos}(t) = \lvert \mathcal{T} \rvert - 1 \wedge \mathit{storage}_{n,i,t} \text{ exists} \wedge \mathrm{storage}^{\mathrm{initial}}_{n,i} \text{ is defined} \wedge \mathrm{cyclic\_storage}_{n,i} +``` + +#### Definitions + +**`storage_previous_step`** + +```math +\mathit{storage}^{\mathrm{previous,step}}_{n,i,t} = \begin{cases} \mathrm{storage}^{\mathrm{initial}}_{n,i} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} & \text{if } \mathrm{pos}(t) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \\ \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i,t} \right)^{\mathrm{timestep\_resolution}_{\mathrm{lookup\_cluster\_last\_timestep}(t)}} \cdot \mathit{storage}_{n,i,\mathrm{lookup\_cluster\_last\_timestep}(t)} & \text{if } \mathrm{cluster\_first\_timestep}_{t} \wedge \neg \left( \mathrm{pos}(t) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \right) \\ \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i,t} \right)^{\mathrm{timestep\_resolution}_{t \ominus 1}} \cdot \mathit{storage}_{n,i,t \ominus 1} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +**`cost_investment_storage_cap`** + +```math +\mathit{cost\_investment\_storage\_cap}_{n,i,k} = \mathrm{cost\_storage\_cap}_{n,i,k} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +#### Variable domains + +**`storage_cap`** + +```math +\mathrm{storage}^{\mathrm{cap,min}}_{n,i} \le \mathit{storage}^{\mathrm{cap}}_{n,i} \le \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage`** + +```math +\mathit{storage}_{n,i,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +#### Assumptions + +**`unbounded_storage_cap_cost`** + +```math +\neg \left( \mathrm{cost\_storage\_cap}_{n,i,k} < 0 \right) \vee \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \text{ is defined} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`storage_initial_max`** + +```math +\mathrm{storage}^{\mathrm{initial}}_{n,i} \ge 0 \wedge \mathrm{storage}^{\mathrm{initial}}_{n,i} \le 1 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{storage}^{\mathrm{initial}}_{n,i} \text{ is defined} +``` + +**`cyclic_storage_needs_inter_cluster`** + +```math +\neg \left( \mathrm{cyclic\_storage}_{n,i} \wedge \mathrm{lookup\_cluster\_last\_timestep}(t) \text{ is defined} \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + diff --git a/docs/examples/calliope/supply.md b/docs/examples/calliope/supply.md new file mode 100644 index 00000000..4fbfdf99 --- /dev/null +++ b/docs/examples/calliope/supply.md @@ -0,0 +1,339 @@ + + +# Supply + +One of the base fragments of [Calliope in fragments](index.md). Supply technologies: the source a technology takes from outside the system, its capacity and its availability. The source scaler reads `area_use`, so a model with a per-area source composes this file with the area file. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + source_eff: + description: "`source_eff` — the share of the source a supply technology takes in. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, timesteps] + source_use_min: + description: "`source_use_min` — least source use in a time step, per unit of `source_unit`" + dims: [nodes, techs, timesteps] + source_use_max: + description: "`source_use_max` — most source use in a time step, per unit of `source_unit`; given only where set" + dims: [nodes, techs, timesteps] + source_use_equals: + description: "`source_use_equals` — the source use required in a time step, per unit of `source_unit`; given only where set" + dims: [nodes, techs, timesteps] + source_unit: + description: >- + `source_unit` — what the source is per: `absolute`, `per_area` of + area use, or `per_cap` of flow capacity. Calliope's default is + `absolute`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + source_cap_min: + description: "`source_cap_min` — least source capacity. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + source_cap_max: + description: "`source_cap_max` — most source capacity. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + source_cap_equals_flow_cap: + description: "`source_cap_equals_flow_cap` — whether the source capacity equals the flow capacity" + dims: [nodes, techs] + dtype: bool + cost_source_use: + description: "`cost_source_use` — the cost of one unit of source use" + dims: [nodes, techs, costs, timesteps] + cost_source_cap: + description: "`cost_source_cap` — the cost of one unit of source capacity" + dims: [nodes, techs, costs] + +variables: + source_use: + description: "`source_use` — what a supply technology takes in from outside the system in a time step" + dims: [nodes, techs, timesteps] + where: base_tech == 'supply' + bounds: { lower: 0 } + absence: zero + source_cap: + description: "`source_cap` — the most a supply technology can take in from outside the system" + dims: [nodes, techs] + where: base_tech == 'supply' + bounds: { lower: source_cap_min, upper: source_cap_max } + absence: zero + +expressions: + flow_cap_out: + description: "`where(flow_cap, carrier_out)` — the flow capacity of the carriers a technology produces" + dims: [nodes, techs, carriers] + cases: + produced: + when: carrier_out + expression: flow_cap + otherwise: 0 + source_scaler: + description: "`$source_scaler` — what the source parameters are per: area use, flow capacity, or one" + dims: [nodes, techs] + cases: + per_area: + when: source_unit == per_area + expression: area_use + per_cap: + when: source_unit == per_cap + expression: sum(flow_cap_out, over=carriers) + otherwise: 1 + cost_investment_source_cap: + description: "`cost_investment_source_cap` — the investment cost of source capacity" + expression: cost_source_cap * source_cap + supply_cost_operation_variable: timestep_weights * cost_source_use * source_use + curtailment: + description: >- + `curtailment` — the share of the available source a supply technology + leaves unused in a time step; reported + expression: 1 - source_use / (source_use_max * source_scaler) + total_curtailment: + description: "`total_curtailment` — the share of the available source left unused over the whole time; reported" + expression: 1 - sum(source_use, over=timesteps) / sum(source_use_max * source_scaler, over=timesteps) + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + include_storage: { dims: [nodes, techs], dtype: bool } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + area_use: { dims: [nodes, techs] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_source_cap } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: supply_cost_operation_variable } + +constraints: + source_max: + description: "`source_max` — source use is at most the source capacity over the time step" + dims: [nodes, techs, timesteps] + where: source_cap + expression: source_use <= timestep_resolution * source_cap + source_capacity_equals_flow_capacity: + description: "`source_capacity_equals_flow_capacity` — a supply technology's source capacity equals its flow capacity, where set" + dims: [nodes, techs, carriers] + where: flow_cap AND source_cap AND source_cap_equals_flow_cap + expression: source_cap == flow_cap + balance_supply_no_storage: + description: "`balance_supply_no_storage` — a supply technology with no store puts out what it takes from its source" + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND base_tech == 'supply' AND NOT include_storage + expression: flow_out_inc_eff == source_use * source_eff + source_availability_supply_equals: + description: "`source_availability_supply` where `source_use_equals` is set — source use is what is available" + dims: [nodes, techs, timesteps] + where: source_use AND source_use_equals + expression: source_use == source_use_equals * source_scaler + source_availability_supply_max: + description: "`source_availability_supply` where only `source_use_max` is set — source use is at most what is available" + dims: [nodes, techs, timesteps] + where: source_use AND NOT source_use_equals AND source_use_max + expression: source_use <= source_use_max * source_scaler + balance_supply_min_use: + description: "`balance_supply_min_use` — source use is at least its least use" + dims: [nodes, techs, timesteps] + where: source_use_min AND NOT source_use_equals AND base_tech == 'supply' + expression: source_use >= source_use_min * source_scaler + +assumptions: + unbounded_source_use_cost: + description: Calliope's `unbounded_source_use_cost` — a negative source capacity cost needs a finite maximum + holds: NOT cost_source_cap < 0 OR source_cap_max + finite_source_use: + description: Calliope's `finite_source_use`, for the source — a required use is finite + holds: NOT source_use_equals == inf + source_unit_one_of: + description: Calliope's `one_of` on `source_unit` + holds: source_unit == absolute OR source_unit == per_area OR source_unit == per_cap + where: source_unit +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | +| $`\mathcal{K}`$ | index $`k`$ — `costs` — Calliope's `costs` — cost classes, such as monetary and CO2 | + +#### Parameters + +| Symbol | Meaning | +|---|---| +| $`\mathrm{source\_eff}`$ | `source_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `source_eff` — the share of the source a supply technology takes in. Calliope's default is 1, and data prep fills it | +| $`\mathrm{source\_use\_min}`$ | `source_use_min` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `source_use_min` — least source use in a time step, per unit of `source_unit` | +| $`\mathrm{source\_use\_max}`$ | `source_use_max` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `source_use_max` — most source use in a time step, per unit of `source_unit`; given only where set | +| $`\mathrm{source\_use\_equals}`$ | `source_use_equals` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `source_use_equals` — the source use required in a time step, per unit of `source_unit`; given only where set | +| $`\mathrm{source\_unit}`$ | `source_unit` over $`\mathcal{N} \times \mathcal{I}`$ — `source_unit` — what the source is per: `absolute`, `per_area` of area use, or `per_cap` of flow capacity. Calliope's default is `absolute`, which is what a technology with no row reads as | +| $`\mathrm{source\_cap\_min}`$ | `source_cap_min` over $`\mathcal{N} \times \mathcal{I}`$ — `source_cap_min` — least source capacity. Calliope's default is 0, and data prep fills it | +| $`\mathrm{source\_cap\_max}`$ | `source_cap_max` over $`\mathcal{N} \times \mathcal{I}`$ — `source_cap_max` — most source capacity. Calliope's default is `.inf`, and data prep fills it | +| $`\mathrm{source\_cap\_equals\_flow\_cap}`$ | `source_cap_equals_flow_cap` over $`\mathcal{N} \times \mathcal{I}`$ — `source_cap_equals_flow_cap` — whether the source capacity equals the flow capacity | +| $`\mathrm{cost\_source\_use}`$ | `cost_source_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$ — `cost_source_use` — the cost of one unit of source use | +| $`\mathrm{cost\_source\_cap}`$ | `cost_source_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_source_cap` — the cost of one unit of source capacity | + +#### Variables + +| Symbol | Meaning | +|---|---| +| $`\mathit{source\_use}`$ | `source_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `source_use` — what a supply technology takes in from outside the system in a time step | +| $`\mathit{source\_cap}`$ | `source_cap` over $`\mathcal{N} \times \mathcal{I}`$ — `source_cap` — the most a supply technology can take in from outside the system | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{include\_storage}`$ | `include_storage` over $`\mathcal{N} \times \mathcal{I}`$, data another file declares | +| $`\mathrm{timestep\_resolution}`$ | `timestep_resolution` over $`\mathcal{T}`$, data another file declares | +| $`\mathrm{timestep\_weights}`$ | `timestep_weights` over $`\mathcal{T}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{area\_use}`$ | `area_use` over $`\mathcal{N} \times \mathcal{I}`$ | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{cost\_investment}`$ | `cost_investment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$, an expression this file adds `cost_investment_source_cap` to | +| $`\mathit{cost\_operation\_variable}`$ | `cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K} \times \mathcal{T}`$, an expression this file adds `supply_cost_operation_variable` to | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap\_out}`$ | `flow_cap_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `where(flow_cap, carrier_out)` — the flow capacity of the carriers a technology produces | +| $`\mathit{source\_scaler}`$ | `source_scaler` over $`\mathcal{N} \times \mathcal{I}`$ — `$source_scaler` — what the source parameters are per: area use, flow capacity, or one | +| $`\mathit{cost\_investment\_source\_cap}`$ | `cost_investment_source_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{K}`$ — `cost_investment_source_cap` — the investment cost of source capacity | +| $`\mathit{supply\_cost\_operation\_variable}`$ | `supply_cost_operation_variable` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T} \times \mathcal{K}`$ | +| $`\mathit{curtailment}`$ | `curtailment` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ — `curtailment` — the share of the available source a supply technology leaves unused in a time step; reported | +| $`\mathit{total\_curtailment}`$ | `total_curtailment` over $`\mathcal{N} \times \mathcal{I}`$ — `total_curtailment` — the share of the available source left unused over the whole time; reported | + +Upright is what the data supplies — a parameter such as $`\mathrm{source\_eff}`$, a coordinate map, a label — and italic is what the solver chooses, such as $`\mathit{source\_use}`$. An index is italic too, being what a quantifier chooses, and a set is script. + +#### Subject to + +**`source_max`** + +```math +\mathit{source\_use}_{n,i,t} \le \mathrm{timestep\_resolution}_{t} \cdot \mathit{source\_cap}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{source\_cap}_{n,i} \text{ exists} +``` + +**`source_capacity_equals_flow_capacity`** + +```math +\mathit{source\_cap}_{n,i} = \mathit{flow\_cap}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathit{source\_cap}_{n,i} \text{ exists} \wedge \mathrm{source\_cap\_equals\_flow\_cap}_{n,i} +``` + +**`balance_supply_no_storage`** + +```math +\mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \mathit{source\_use}_{n,i,t} \cdot \mathrm{source\_eff}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} +``` + +**`source_availability_supply_equals`** + +```math +\mathit{source\_use}_{n,i,t} = \mathrm{source\_use\_equals}_{n,i,t} \cdot \mathit{source\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{source\_use}_{n,i,t} \text{ exists} \wedge \mathrm{source\_use\_equals}_{n,i,t} \text{ is defined} +``` + +**`source_availability_supply_max`** + +```math +\mathit{source\_use}_{n,i,t} \le \mathrm{source\_use\_max}_{n,i,t} \cdot \mathit{source\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathit{source\_use}_{n,i,t} \text{ exists} \wedge \neg \left( \mathrm{source\_use\_equals}_{n,i,t} \text{ is defined} \right) \wedge \mathrm{source\_use\_max}_{n,i,t} \text{ is defined} +``` + +**`balance_supply_min_use`** + +```math +\mathit{source\_use}_{n,i,t} \ge \mathrm{source\_use\_min}_{n,i,t} \cdot \mathit{source\_scaler}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{source\_use\_min}_{n,i,t} \text{ is defined} \wedge \neg \left( \mathrm{source\_use\_equals}_{n,i,t} \text{ is defined} \right) \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} +``` + +#### Definitions + +**`flow_cap_out`** + +```math +\mathit{flow\_cap\_out}_{n,i,c} = \begin{cases} \mathit{flow\_cap}_{n,i,c} & \text{if } \mathrm{carrier\_out}_{n,i,c} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`source_scaler`** + +```math +\mathit{source\_scaler}_{n,i} = \begin{cases} \mathit{area\_use}_{n,i} & \text{if } \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \\ \sum_{c \in \mathcal{C}} \mathit{flow\_cap\_out}_{n,i,c} & \text{if } \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_cap}\text{'} \\ 1 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +**`cost_investment_source_cap`** + +```math +\mathit{cost\_investment\_source\_cap}_{n,i,k} = \mathrm{cost\_source\_cap}_{n,i,k} \cdot \mathit{source\_cap}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`supply_cost_operation_variable`** + +```math +\mathit{supply\_cost\_operation\_variable}_{n,i,t,k} = \mathrm{timestep\_weights}_{t} \cdot \mathrm{cost\_source\_use}_{n,i,k,t} \cdot \mathit{source\_use}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T},\ k \in \mathcal{K} +``` + +**`curtailment`** + +```math +\mathit{curtailment}_{n,i,t} = 1 - \frac{\mathit{source\_use}_{n,i,t}}{\mathrm{source\_use\_max}_{n,i,t} \cdot \mathit{source\_scaler}_{n,i}} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +**`total_curtailment`** + +```math +\mathit{total\_curtailment}_{n,i} = 1 - \frac{\sum_{t \in \mathcal{T}} \mathit{source\_use}_{n,i,t}}{\sum_{t \in \mathcal{T}} \mathrm{source\_use\_max}_{n,i,t} \cdot \mathit{source\_scaler}_{n,i}} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +#### Variable domains + +**`source_use`** + +```math +\mathit{source\_use}_{n,i,t} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} +``` + +**`source_cap`** + +```math +\mathrm{source\_cap\_min}_{n,i} \le \mathit{source\_cap}_{n,i} \le \mathrm{source\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} +``` + +#### Assumptions + +**`unbounded_source_use_cost`** + +```math +\neg \left( \mathrm{cost\_source\_cap}_{n,i,k} < 0 \right) \vee \mathrm{source\_cap\_max}_{n,i} \text{ is defined} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`finite_source_use`** + +```math +\neg \left( \mathrm{source\_use\_equals}_{n,i,t} = \infty \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +**`source_unit_one_of`** + +```math +\mathrm{source\_unit}_{n,i} = \text{'}\mathrm{absolute}\text{'} \vee \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \vee \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_cap}\text{'} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{source\_unit}_{n,i} \text{ is defined} +``` + diff --git a/docs/examples/calliope/supply_storage.md b/docs/examples/calliope/supply_storage.md new file mode 100644 index 00000000..c69c3c63 --- /dev/null +++ b/docs/examples/calliope/supply_storage.md @@ -0,0 +1,74 @@ + + +# Supply with storage + +One of the base fragments of [Calliope in fragments](index.md). Calliope's `balance_supply_with_storage`, the one row that couples a supply technology to a store. It is a file of its own so that supply and storage each compose without the other. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + source_eff: { dims: [nodes, techs, timesteps] } + variables: + storage: { dims: [nodes, techs, timesteps] } + source_use: { dims: [nodes, techs, timesteps] } + expressions: + storage_previous_step: { dims: [nodes, techs, timesteps] } + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_supply_with_storage: + description: >- + `balance_supply_with_storage` — a supply technology with a store puts + in what it takes from its source and draws out what it puts out + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND storage AND base_tech == 'supply' + expression: storage == storage_previous_step + source_use * source_eff - flow_out_inc_eff +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathrm{source\_eff}`$ | `source_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$, data another file declares | +| $`\mathit{storage}`$ | `storage` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ | +| $`\mathit{source\_use}`$ | `source_use` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$ | +| $`\mathit{storage}^{\mathrm{previous,step}}`$ | `storage_previous_step` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Subject to + +**`balance_supply_with_storage`** + +```math +\mathit{storage}_{n,i,t} = \mathit{storage}^{\mathrm{previous,step}}_{n,i,t} + \mathit{source\_use}_{n,i,t} \cdot \mathrm{source\_eff}_{n,i,t} - \mathit{flow\_out\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \mathit{storage}_{n,i,t} \text{ exists} \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} +``` + diff --git a/docs/examples/calliope/transmission.md b/docs/examples/calliope/transmission.md new file mode 100644 index 00000000..bd9cdfe9 --- /dev/null +++ b/docs/examples/calliope/transmission.md @@ -0,0 +1,133 @@ + + +# Transmission + +One of the base fragments of [Calliope in fragments](index.md). Links between nodes: a link carries what it takes in at one end to the other, and has one capacity at both ends. + + +```yaml +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + link_from: + description: >- + `link_from` — the node a transmission technology links from. Calliope + reads it as `map_dim(nodes, link_from)`, a mask over technology and + node, which is the relation's own row test + key: [techs, nodes] + link_to: + description: >- + `link_to` — the node a transmission technology links to, read as + `link_from` is + key: [techs, nodes] + +expressions: + flow_cap_from: + description: "`where(flow_cap, map_dim(nodes, link_from))` — a link's flow capacity at the node it links from" + dims: [nodes, techs, carriers] + cases: + from: + when: link_from + expression: flow_cap + otherwise: 0 + flow_cap_to: + description: "`where(flow_cap, map_dim(nodes, link_to))` — a link's flow capacity at the node it links to" + dims: [nodes, techs, carriers] + cases: + to: + when: link_to + expression: flow_cap + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_transmission: + description: "`balance_transmission` — a link puts out at one end, before losses, what it takes in at the other after them" + dims: [techs, timesteps] + where: base_tech == 'transmission' + expression: >- + sum(flow_out_inc_eff, over=[nodes, carriers]) + == sum(flow_in_inc_eff, over=[nodes, carriers]) + symmetric_transmission: + description: "`symmetric_transmission` — a link has the same flow capacity at both ends" + dims: [techs, carriers] + where: count(carrier_out, over=nodes) >= 1 AND base_tech == 'transmission' + expression: sum(flow_cap_from, over=nodes) == sum(flow_cap_to, over=nodes) +``` + +#### Sets + +| Symbol | Meaning | +|---|---| +| $`\mathcal{N}`$ | index $`n`$ — `nodes` with $`\mathrm{link\_from} \subseteq \mathcal{I} \times \mathcal{N},\ \mathrm{link\_to} \subseteq \mathcal{I} \times \mathcal{N}`$ — Calliope's `nodes` — the places technologies stand at | +| $`\mathcal{I}`$ | index $`i`$ — `techs` with $`\mathrm{link\_from} \subseteq \mathcal{I} \times \mathcal{N},\ \mathrm{link\_to} \subseteq \mathcal{I} \times \mathcal{N}`$ — Calliope's `techs` — technologies | +| $`\mathcal{C}`$ | index $`c`$ — `carriers` — Calliope's `carriers` — energy and commodity carriers | +| $`\mathcal{T}`$ | index $`t`$ — `timesteps` — Calliope's `timesteps` — time steps, in order | + +#### Given + +| Symbol | Meaning | +|---|---| +| $`\mathrm{base\_tech}`$ | `base_tech` over $`\mathcal{I}`$, data another file declares | +| $`\mathrm{carrier\_out}`$ | `carrier_out` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$, data another file declares | +| $`\mathit{flow\_cap}`$ | `flow_cap` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ | +| $`\mathit{flow\_out\_inc\_eff}`$ | `flow_out_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | +| $`\mathit{flow\_in\_inc\_eff}`$ | `flow_in_inc_eff` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C} \times \mathcal{T}`$, an expression another file defines | + +#### Definitions + +| Symbol | Meaning | +|---|---| +| $`\mathit{flow\_cap\_from}`$ | `flow_cap_from` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `where(flow_cap, map_dim(nodes, link_from))` — a link's flow capacity at the node it links from | +| $`\mathit{flow\_cap\_to}`$ | `flow_cap_to` over $`\mathcal{N} \times \mathcal{I} \times \mathcal{C}`$ — `where(flow_cap, map_dim(nodes, link_to))` — a link's flow capacity at the node it links to | + +#### Subject to + +**`balance_transmission`** + +```math +\sum_{n \in \mathcal{N},\ c \in \mathcal{C}} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \sum_{n \in \mathcal{N},\ c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} +``` + +**`symmetric_transmission`** + +```math +\sum_{n \in \mathcal{N}} \mathit{flow\_cap\_from}_{n,i,c} = \sum_{n \in \mathcal{N}} \mathit{flow\_cap\_to}_{n,i,c} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathrm{carrier\_out}_{n,i,c} \} \rvert \ge 1 \wedge \mathrm{base\_tech}_{i} = \text{'}\mathrm{transmission}\text{'} +``` + +#### Definitions + +**`flow_cap_from`** + +```math +\mathit{flow\_cap\_from}_{n,i,c} = \begin{cases} \mathit{flow\_cap}_{n,i,c} & \text{if } \left( i,\ n \right) \in \mathrm{link\_from} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`flow_cap_to`** + +```math +\mathit{flow\_cap\_to}_{n,i,c} = \begin{cases} \mathit{flow\_cap}_{n,i,c} & \text{if } \left( i,\ n \right) \in \mathrm{link\_to} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + diff --git a/docs/examples/calliope/variants/chp_htp.md b/docs/examples/calliope/variants/chp_htp.md new file mode 100644 index 00000000..d53e4f9f --- /dev/null +++ b/docs/examples/calliope/variants/chp_htp.md @@ -0,0 +1,34 @@ + + +# The CHP patch + +A patch of [Calliope in fragments](../index.md). What Calliope's example `chp_htp.yaml` changes in the base: `balance_conversion` holds only for a plant with no turbine type. Its rows are [the CHP fragment](../extensions/chp_htp.md). A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base + ['extensions/chp_htp.yaml']), + ['variants/chp_htp.yaml'], +) +``` + +```yaml title="variants/chp_htp.yaml" +constraints: + balance_conversion: + description: >- + `balance_conversion` for a plant with no turbine type — a conversion + technology puts out, before its losses, what it takes in after them. + Extraction and backpressure plants have rows of their own + where: base_tech == 'conversion' AND NOT include_storage AND NOT turbine_type +``` + +**`balance_conversion`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} \wedge \neg \left( \mathrm{turbine\_type}_{n,i} \text{ is defined} \right) +``` + diff --git a/docs/examples/calliope/variants/milp.md b/docs/examples/calliope/variants/milp.md new file mode 100644 index 00000000..bf52b19d --- /dev/null +++ b/docs/examples/calliope/variants/milp.md @@ -0,0 +1,112 @@ + + +# The MILP patch + +A patch of [Calliope in fragments](../index.md). What Calliope's `milp.yaml` changes in the base: the capacity bounds open to zero, since the minimums become rows the units scale, and the continuous flow limits hold only where no unit runs. What it adds is [the MILP fragment](../extensions/milp.md). A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base + ['extensions/milp.yaml']), + ['variants/milp.yaml'], +) +``` + +```yaml title="variants/milp.yaml" +parameters: + flow_cap_min: + description: >- + `flow_cap_min` — least flow capacity, scaled by the units bought where + a technology buys units; given only where set, as no bound reads it + flow_cap_min_systemwide: + description: >- + `flow_cap_min_systemwide` — least flow capacity of a technology over + every node, scaled by the units bought where it buys units; given only + where set + flow_out_min_relative: + description: >- + `flow_out_min_relative` — least outflow, per unit of flow capacity. For + a continuous technology it holds in every time step; given only where + set + storage_cap_min: + description: "`storage_cap_min` — least storage capacity; given only where set, as no bound reads it" + area_use_min: + description: "`area_use_min` — least area use; given only where set, as no bound reads it" + source_cap_min: + description: "`source_cap_min` — least source capacity; given only where set, as no bound reads it" + +variables: + flow_cap: { bounds: { lower: 0 } } + area_use: { bounds: { lower: 0 } } + source_cap: { bounds: { lower: 0 } } + storage_cap: { bounds: { lower: 0 } } + +constraints: + flow_out_max: + description: "`flow_out_max` — a continuous technology's outflow is at most its flow capacity over the time step" + where: carrier_out AND NOT operating_units + flow_out_min: + description: "`flow_out_min` — a continuous technology's outflow is at least its least share of the flow capacity" + where: flow_cap AND flow_out_min_relative AND NOT operating_units + flow_in_max: + description: "`flow_in_max` — a continuous technology's inflow is at most its flow capacity over the time step" + where: carrier_in AND NOT operating_units + flow_capacity_systemwide_min: + description: >- + `flow_capacity_systemwide_min` where no unit is bought — the flow + capacity over every node is at least the system-wide minimum + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide AND NOT count(purchased_units, over=nodes) >= 1 +``` + +**`flow_cap`** + +```math +0 \le \mathit{flow\_cap}_{n,i,c} \le \mathrm{flow\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \mathrm{carrier\_in}_{n,i,c} \vee \mathrm{carrier\_out}_{n,i,c} +``` + +**`area_use`** + +```math +0 \le \mathit{area\_use}_{n,i} \le \mathrm{area\_use\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{area\_use\_min}_{n,i} > 0 \vee \mathrm{area\_use\_max}_{n,i} \text{ is defined} \vee \mathrm{area\_use\_per\_flow\_cap}_{n,i} \text{ is defined} \vee \mathrm{sink\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} \vee \mathrm{source\_unit}_{n,i} = \text{'}\mathrm{per\_area}\text{'} +``` + +**`source_cap`** + +```math +0 \le \mathit{source\_cap}_{n,i} \le \mathrm{source\_cap\_max}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{supply}\text{'} +``` + +**`storage_cap`** + +```math +0 \le \mathit{storage}^{\mathrm{cap}}_{n,i} \le \mathrm{storage}^{\mathrm{cap,max}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`flow_out_max`** + +```math +\mathit{flow\_out}_{n,i,c,t} \le \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_out\_parasitic\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_out}_{n,i,c} \wedge \neg \left( \mathit{operating\_units}_{n,i,t} \text{ exists} \right) +``` + +**`flow_out_min`** + +```math +\mathit{flow\_out}_{n,i,c,t} \ge \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \cdot \mathrm{flow\_out\_min\_relative}_{n,i,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \wedge \mathrm{flow\_out\_min\_relative}_{n,i,t} \text{ is defined} \wedge \neg \left( \mathit{operating\_units}_{n,i,t} \text{ exists} \right) +``` + +**`flow_in_max`** + +```math +\mathit{flow\_in}_{n,i,c,t} \le \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{timestep\_resolution}_{t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C},\ t \in \mathcal{T} \,:\, \mathrm{carrier\_in}_{n,i,c} \wedge \neg \left( \mathit{operating\_units}_{n,i,t} \text{ exists} \right) +``` + +**`flow_capacity_systemwide_min`** + +```math +\sum_{n \in \mathcal{N}} \mathit{flow\_cap}_{n,i,c} \ge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \qquad \forall\, i \in \mathcal{I},\ c \in \mathcal{C} \,:\, \lvert \{ n \in \mathcal{N} \,:\, \mathit{flow\_cap}_{n,i,c} \text{ exists} \} \rvert \ge 1 \wedge \mathrm{flow\_cap\_min\_systemwide}_{i,c} \text{ is defined} \wedge \neg \left( \lvert \{ n \in \mathcal{N} \,:\, \mathit{purchased\_units}_{n,i} \text{ exists} \} \rvert \ge 1 \right) +``` + diff --git a/docs/examples/calliope/variants/operate.md b/docs/examples/calliope/variants/operate.md new file mode 100644 index 00000000..45573921 --- /dev/null +++ b/docs/examples/calliope/variants/operate.md @@ -0,0 +1,88 @@ + + +# Operate mode + +A patch of [Calliope in fragments](../index.md). Calliope's `operate.yaml`: every capacity is data, not a decision. The patch turns each capacity variable into a parameter of the same name, removes the rows and the costs that only a capacity decision has, and leaves the operating cost. Calliope's rolling horizon is a loop of solves, and is not in the math. A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base), + ['variants/operate.yaml'], +) +``` + +```yaml title="variants/operate.yaml" +parameters: + flow_cap: + description: "`flow_cap` — the flow capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs, carriers] + area_use: + description: "`area_use` — the area used, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + source_cap: + description: "`source_cap` — the source capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + storage_cap: + description: "`storage_cap` — the storage capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + +variables: + flow_cap: null + area_use: null + source_cap: null + storage_cap: null + +constraints: + flow_capacity_per_storage_capacity_min: null + flow_capacity_per_storage_capacity_max: null + source_capacity_equals_flow_capacity: null + force_zero_area_use: null + area_use_per_flow_capacity: null + area_use_capacity_per_loc: null + flow_capacity_systemwide_max: null + flow_capacity_systemwide_min: null + symmetric_transmission: null + +expressions: + cost_investment: null + cost_investment_annualised: null + cost_investment_flow_cap: null + cost_investment_storage_cap: null + cost_investment_source_cap: null + cost_investment_area_use: null + cost_operation_fixed: null + cost_flow_cap_sum: null + depreciation_rate: null + flows_cost_investment: null + flows_cost_operation_fixed: null + flow_cap_from: null + flow_cap_to: null + cost: + description: "`cost` — the operating cost of a technology, over every time step" + expression: sum(cost_operation_variable, over=timesteps) + +assumptions: + operate_mode_cyclic_storage: + description: Calliope's `operate_mode_cyclic_storage` — a store in operate mode is not cyclic + holds: NOT (cyclic_storage AND (base_tech == 'storage' OR include_storage)) +``` + +**`cost`** + +```math +\mathit{cost}_{n,i,k} = \sum_{t \in \mathcal{T}} \mathit{cost}^{\mathrm{operation,variable}}_{n,i,k,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ k \in \mathcal{K} +``` + +**`operate_mode_cyclic_storage`** + +```math +\neg \left( \mathrm{cyclic\_storage}_{n,i} \wedge \left( \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} \vee \mathrm{include\_storage}_{n,i} \right) \right) \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I} +``` + +Removed: `flow_cap`, `area_use`, `source_cap`, `storage_cap`, `cost_investment`, `cost_investment_annualised`, `cost_investment_flow_cap`, `cost_investment_storage_cap`, `cost_investment_source_cap`, `cost_investment_area_use`, `cost_operation_fixed`, `cost_flow_cap_sum`, `depreciation_rate`, `flows_cost_investment`, `flows_cost_operation_fixed`, `flow_cap_from`, `flow_cap_to`, `flow_capacity_per_storage_capacity_min`, `flow_capacity_per_storage_capacity_max`, `source_capacity_equals_flow_capacity`, `force_zero_area_use`, `area_use_per_flow_capacity`, `area_use_capacity_per_loc`, `flow_capacity_systemwide_max`, `flow_capacity_systemwide_min`, `symmetric_transmission`. + diff --git a/docs/examples/calliope/variants/operate_milp.md b/docs/examples/calliope/variants/operate_milp.md new file mode 100644 index 00000000..db2cb888 --- /dev/null +++ b/docs/examples/calliope/variants/operate_milp.md @@ -0,0 +1,51 @@ + + +# Operate mode, with MILP + +A patch of [Calliope in fragments](../index.md). The half of Calliope's `operate.yaml` that lands on the MILP fragment: the units bought are data too. It is laid after the MILP patch and operate mode. A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base + ['extensions/milp.yaml']), + ['variants/milp.yaml', 'variants/operate.yaml', 'variants/operate_milp.yaml'], +) +``` + +```yaml title="variants/operate_milp.yaml" +parameters: + purchased_units: + description: "`purchased_units` — the units bought, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + +variables: + purchased_units: null + +constraints: + storage_capacity_units_milp: null + flow_capacity_units_milp: null + unit_capacity_max_systemwide_milp: null + unit_capacity_min_systemwide_milp: null + flow_capacity_max_purchase_milp: null + flow_capacity_max_purchase_milp_big_m: null + storage_capacity_max_purchase_milp: null + flow_capacity_minimum: null + flow_capacity_minimum_purchased: null + storage_capacity_minimum: null + storage_capacity_minimum_purchased: null + area_use_minimum: null + area_use_minimum_purchased: null + source_capacity_minimum: null + source_capacity_minimum_purchased: null + flow_capacity_systemwide_min_purchased: null + +expressions: + cost_investment_purchase: null +``` + +Removed: `purchased_units`, `cost_investment_purchase`, `storage_capacity_units_milp`, `flow_capacity_units_milp`, `unit_capacity_max_systemwide_milp`, `unit_capacity_min_systemwide_milp`, `flow_capacity_max_purchase_milp`, `flow_capacity_max_purchase_milp_big_m`, `storage_capacity_max_purchase_milp`, `flow_capacity_minimum`, `flow_capacity_minimum_purchased`, `storage_capacity_minimum`, `storage_capacity_minimum_purchased`, `area_use_minimum`, `area_use_minimum_purchased`, `source_capacity_minimum`, `source_capacity_minimum_purchased`, `flow_capacity_systemwide_min_purchased`. + diff --git a/docs/examples/calliope/variants/spores.md b/docs/examples/calliope/variants/spores.md new file mode 100644 index 00000000..c17d1c5f --- /dev/null +++ b/docs/examples/calliope/variants/spores.md @@ -0,0 +1,80 @@ + + +# SPORES + +A patch of [Calliope in fragments](../index.md). Calliope's `spores.yaml`: the objective is the SPORES score of the capacity built, and the least-cost objective becomes a row capped at the least cost plus a slack. The row reads the sums the objective read, so a file that adds a cost is capped too. The iteration that updates the scores is a loop of solves, and is not in the math. A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base), + ['variants/spores.yaml'], +) +``` + +```yaml title="variants/spores.yaml" +parameters: + spores_baseline_cost: + description: >- + `spores_baseline_cost` — the least cost of the system, which a SPORES + iteration may exceed by its slack. Calliope's default is `.inf` + dims: [] + spores_slack: + description: "`spores_slack` — the share by which a SPORES iteration may exceed the least cost" + dims: [] + spores_score: + description: "`spores_score` — the score a technology at a node carries from the SPORES iterations before" + dims: [nodes, techs, carriers] + +constraints: + total_system_cost_max: + description: >- + `total_system_cost_max` — the cost the least-cost objective reads is at + most the least cost plus the slack. It reads the same sums the + objective did, so a file that adds a cost adds it here too + dims: [] + expression: system_cost + penalty <= spores_baseline_cost * (1 + spores_slack) + +expressions: + spores_score_cumulative: + description: "`spores_score_cumulative` — the SPORES score, reported with the results" + expression: spores_score + spores_baseline_cost_tracked: + description: "`spores_baseline_cost_tracked` — the SPORES baseline cost, reported with the results" + expression: spores_baseline_cost + +objective: + description: >- + `min_spores` — the SPORES score of the flow capacity built, plus the + penalty on unmet demand + expression: sum(flow_cap * spores_score) + penalty +``` + +**`spores_score_cumulative`** + +```math +\mathrm{spores\_score\_cumulative}_{n,i,c} = \mathrm{spores\_score}_{n,i,c} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C} +``` + +**`spores_baseline_cost_tracked`** + +```math +\mathrm{spores\_baseline\_cost\_tracked} = \mathrm{spores\_baseline\_cost} +``` + +**`total_system_cost_max`** + +```math +\mathit{system\_cost} + \mathit{penalty} \le \mathrm{spores\_baseline\_cost} \cdot \left( 1 + \mathrm{spores\_slack} \right) +``` + +**The objective** + +```math +\min \sum_{n \in \mathcal{N},\ i \in \mathcal{I},\ c \in \mathcal{C}} \mathit{flow\_cap}_{n,i,c} \cdot \mathrm{spores\_score}_{n,i,c} + \mathit{penalty} +``` + diff --git a/docs/examples/calliope/variants/storage_inter_cluster.md b/docs/examples/calliope/variants/storage_inter_cluster.md new file mode 100644 index 00000000..8e78b0e6 --- /dev/null +++ b/docs/examples/calliope/variants/storage_inter_cluster.md @@ -0,0 +1,238 @@ + + +# Inter-cluster storage + +A patch of [Calliope in fragments](../index.md). Calliope's `storage_inter_cluster.yaml`: with clustered days, a store carries a fill between the days of the whole time series and swings within its clustered day. A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base), + ['variants/storage_inter_cluster.yaml'], +) +``` + +```yaml title="variants/storage_inter_cluster.yaml" +dimensions: + clusters: + description: Calliope's `clusters` — the representative days a clustered time series is made of + dtype: int + datesteps: + description: Calliope's `datesteps` — the days of the whole time series, in order + dtype: datetime + +relations: + timestep_cluster: + description: "`timestep_cluster` — the cluster a time step belongs to" + key: timesteps + values: clusters + lookup_datestep_cluster: + description: "`lookup_datestep_cluster` — the cluster a day stands for" + key: datesteps + values: clusters + lookup_datestep_last_cluster_timestep: + description: "`lookup_datestep_last_cluster_timestep` — the last time step of the cluster a day stands for" + key: datesteps + values: timesteps + +parameters: + storage_loss: + description: >- + `storage_loss` — the share of what a store holds that it loses in an + hour. Between days it is raised to 24, so it does not vary over time + steps here + dims: [nodes, techs] + +variables: + storage: + description: >- + `storage` — what a store holds within a clustered day, relative to what + it carries between days. It may go below zero, as long as the sum does + not + bounds: { lower: null } + storage_inter_cluster: + description: "`storage_inter_cluster` — what a store carries from one day of the whole time series to the next" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + bounds: { lower: 0 } + absence: zero + storage_intra_cluster_max: + description: "`storage_intra_cluster_max` — the most a store holds within a clustered day" + dims: [nodes, techs, clusters] + where: include_storage OR base_tech == 'storage' + storage_intra_cluster_min: + description: "`storage_intra_cluster_min` — the least a store holds within a clustered day" + dims: [nodes, techs, clusters] + where: include_storage OR base_tech == 'storage' + +expressions: + storage_previous_step: + description: >- + `$storage_previous_step` under inter-cluster storage — what a store + carries into a time step: its initial fill at the first step of a store + that is not cyclic, nothing at the first step of a clustered day, and + what is left of the step before everywhere else + cases: + initial: + when: position(timesteps) == 0 AND NOT cyclic_storage + expression: storage_initial * storage_cap + cluster_start: + when: lookup_cluster_last_timestep AND NOT (position(timesteps) == 0 AND NOT cyclic_storage) + expression: "0" + storage_inter_previous_step: + description: >- + `$storage_previous_step` of `balance_storage_inter` — what a store + carries into a day: its initial fill on the first day of a store that + is not cyclic, and what is left of the day before everywhere else. + Calliope reads the initial fill as a share, not times the capacity, as + here + dims: [nodes, techs, datesteps] + cases: + initial: + when: position(datesteps) == 0 AND NOT cyclic_storage + expression: storage_initial + otherwise: (1 - storage_loss) ** 24 * shift(storage_inter_cluster, along=datesteps, offset=1, edge='wrap') + storage_intra: + description: >- + `$storage_intra` of `balance_storage_inter` — what the clustered day of + the day before left at its last step, and nothing on the first day of + a store that is not cyclic + dims: [nodes, techs, datesteps] + cases: + initial: + when: position(datesteps) == 0 AND NOT cyclic_storage + expression: "0" + otherwise: >- + shift(at(storage, by=lookup_datestep_last_cluster_timestep, over=timesteps, into=datesteps), + along=datesteps, offset=1, edge='wrap') + +constraints: + storage_max: null + set_storage_initial: + description: >- + `set_storage_initial` under inter-cluster storage — a cyclic store with + an initial fill carries it between days at the end, after a day's loss + dims: [nodes, techs, datesteps] + where: position(datesteps) == -1 AND storage_inter_cluster AND storage_initial AND cyclic_storage + expression: storage_inter_cluster * (1 - storage_loss) ** 24 == storage_initial * storage_cap + storage_intra_max: + description: "`storage_intra_max` — a store holds at most its most within its clustered day" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + expression: storage <= at(storage_intra_cluster_max, by=timestep_cluster, over=clusters, into=timesteps) + storage_intra_min: + description: "`storage_intra_min` — a store holds at least its least within its clustered day" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + expression: storage >= at(storage_intra_cluster_min, by=timestep_cluster, over=clusters, into=timesteps) + storage_inter_max: + description: "`storage_inter_max` — what a store carries between days plus the most of its day is at most its capacity" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: >- + storage_inter_cluster + at(storage_intra_cluster_max, by=lookup_datestep_cluster, over=clusters, into=datesteps) + <= storage_cap + storage_inter_min: + description: "`storage_inter_min` — what a store carries between days, after a day's loss, plus the least of its day is not below zero" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: >- + storage_inter_cluster * (1 - storage_loss) ** 24 + + at(storage_intra_cluster_min, by=lookup_datestep_cluster, over=clusters, into=datesteps) >= 0 + balance_storage_inter: + description: >- + `balance_storage_inter` — what a store carries into a day is what it + carried into the day before, after a day's loss, plus what that day's + cluster left + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: storage_inter_cluster == storage_inter_previous_step + storage_intra + +assumptions: + cyclic_storage_needs_inter_cluster: null +``` + +**`storage`** + +```math +\mathit{storage}_{n,i,t} \in \mathbb{R} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_inter_cluster`** + +```math +\mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_intra_cluster_max`** + +```math +\mathit{storage}^{\mathrm{intra,cluster,max}}_{n,i,l} \in \mathbb{R} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ l \in \mathcal{L} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_intra_cluster_min`** + +```math +\mathit{storage}^{\mathrm{intra,cluster,min}}_{n,i,l} \in \mathbb{R} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ l \in \mathcal{L} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_previous_step`** + +```math +\mathit{storage}^{\mathrm{previous,step}}_{n,i,t} = \begin{cases} \mathrm{storage}^{\mathrm{initial}}_{n,i} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} & \text{if } \mathrm{pos}(t) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \\ 0 & \text{if } \mathrm{lookup\_cluster\_last\_timestep}(t) \text{ is defined} \wedge \neg \left( \mathrm{pos}(t) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \right) \\ \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i} \right)^{\mathrm{timestep\_resolution}_{t \ominus 1}} \cdot \mathit{storage}_{n,i,t \ominus 1} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} +``` + +**`storage_inter_previous_step`** + +```math +\mathit{storage}^{\mathrm{inter,previous,step}}_{n,i,d} = \begin{cases} \mathrm{storage}^{\mathrm{initial}}_{n,i} & \text{if } \mathrm{pos}(d) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \\ \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i} \right)^{24} \cdot \mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d \ominus 1} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} +``` + +**`storage_intra`** + +```math +\mathit{storage}^{\mathrm{intra}}_{n,i,d} = \begin{cases} 0 & \text{if } \mathrm{pos}(d) = 0 \wedge \neg \mathrm{cyclic\_storage}_{n,i} \\ \mathit{storage}_{n,i,\mathrm{lookup\_datestep\_last\_cluster\_timestep}(d \ominus 1)} & \text{otherwise} \end{cases} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} +``` + +**`set_storage_initial`** + +```math +\mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} \cdot \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i} \right)^{24} = \mathrm{storage}^{\mathrm{initial}}_{n,i} \cdot \mathit{storage}^{\mathrm{cap}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} \,:\, \mathrm{pos}(d) = \lvert \mathcal{D} \rvert - 1 \wedge \mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} \text{ exists} \wedge \mathrm{storage}^{\mathrm{initial}}_{n,i} \text{ is defined} \wedge \mathrm{cyclic\_storage}_{n,i} +``` + +**`storage_intra_max`** + +```math +\mathit{storage}_{n,i,t} \le \mathit{storage}^{\mathrm{intra,cluster,max}}_{n,i,\mathrm{timestep\_cluster}(t)} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_intra_min`** + +```math +\mathit{storage}_{n,i,t} \ge \mathit{storage}^{\mathrm{intra,cluster,min}}_{n,i,\mathrm{timestep\_cluster}(t)} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_inter_max`** + +```math +\mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} + \mathit{storage}^{\mathrm{intra,cluster,max}}_{n,i,\mathrm{lookup\_datestep\_cluster}(d)} \le \mathit{storage}^{\mathrm{cap}}_{n,i} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`storage_inter_min`** + +```math +\mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} \cdot \left( 1 - \mathrm{storage}^{\mathrm{loss}}_{n,i} \right)^{24} + \mathit{storage}^{\mathrm{intra,cluster,min}}_{n,i,\mathrm{lookup\_datestep\_cluster}(d)} \ge 0 \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +**`balance_storage_inter`** + +```math +\mathit{storage}^{\mathrm{inter,cluster}}_{n,i,d} = \mathit{storage}^{\mathrm{inter,previous,step}}_{n,i,d} + \mathit{storage}^{\mathrm{intra}}_{n,i,d} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ d \in \mathcal{D} \,:\, \mathrm{include\_storage}_{n,i} \vee \mathrm{base\_tech}_{i} = \text{'}\mathrm{storage}\text{'} +``` + +Removed: `storage_max`, `cyclic_storage_needs_inter_cluster`. + diff --git a/docs/examples/calliope/variants/urban_scale_chp.md b/docs/examples/calliope/variants/urban_scale_chp.md new file mode 100644 index 00000000..b33b0db4 --- /dev/null +++ b/docs/examples/calliope/variants/urban_scale_chp.md @@ -0,0 +1,33 @@ + + +# The urban-scale CHP patch + +A patch of [Calliope in fragments](../index.md). What the urban-scale example changes in the base: `balance_conversion` holds for every conversion technology but `chp`. Its rows are [the urban-scale fragment](../extensions/urban_scale_chp.md). A patch is not a spec, so it prints as the declarations it writes, in the spec it lands on. + + +```python +ms.override( + ms.merge(base + ['extensions/urban_scale_chp.yaml']), + ['variants/urban_scale_chp.yaml'], +) +``` + +```yaml title="variants/urban_scale_chp.yaml" +constraints: + balance_conversion: + description: >- + `balance_conversion` for every conversion technology but `chp` — it + puts out, before its losses, what it takes in after them + where: base_tech == 'conversion' AND NOT include_storage AND NOT techs == chp +``` + +**`balance_conversion`** + +```math +\sum_{c \in \mathcal{C}} \mathit{flow\_out\_inc\_eff}_{n,i,c,t} = \sum_{c \in \mathcal{C}} \mathit{flow\_in\_inc\_eff}_{n,i,c,t} \qquad \forall\, n \in \mathcal{N},\ i \in \mathcal{I},\ t \in \mathcal{T} \,:\, \mathrm{base\_tech}_{i} = \text{'}\mathrm{conversion}\text{'} \wedge \neg \mathrm{include\_storage}_{n,i} \wedge \neg \left( i = \text{'}\mathrm{chp}\text{'} \right) +``` + diff --git a/docs/examples/index.md b/docs/examples/index.md index 7a53f66d..9b2c8f4a 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -21,5 +21,7 @@ Every spec is a file under `examples/` in the repository. The PyPSA parity pages, from [PyPSA in one file](pypsa.md) on, are a proof of concept. They sit in the Development section, and [PyPSA in 24 files](pypsa/index.md) is the same spec composed from fragments. +[Calliope in fragments](calliope/index.md) is the second: all of Calliope's +math, its base merged from fragments, and its modes laid over as patches. [Typeset the math](../reference/typeset.md) prints your own. diff --git a/examples/calliope/area.yaml b/examples/calliope/area.yaml new file mode 100644 index 00000000..a89f5b7e --- /dev/null +++ b/examples/calliope/area.yaml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + area_use_min: + description: "`area_use_min` — least area use. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + area_use_max: + description: "`area_use_max` — most area use. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + area_use_per_flow_cap: + description: "`area_use_per_flow_cap` — area use per unit of flow capacity; given only where set" + dims: [nodes, techs] + available_area: + description: "`available_area` — the area every technology at a node may use; given only where set" + dims: [nodes] + cost_area_use: + description: "`cost_area_use` — the cost of one unit of area use" + dims: [nodes, techs, costs] + +variables: + area_use: + description: >- + `area_use` — the area a technology uses. Calliope builds it where + `area_use_min` is given at all; the least area use is data here, so + it is built where that is above zero + dims: [nodes, techs] + where: area_use_min > 0 OR area_use_max OR area_use_per_flow_cap OR sink_unit == per_area OR source_unit == per_area + bounds: { lower: area_use_min, upper: area_use_max } + absence: zero + +expressions: + cost_investment_area_use: + description: "`cost_investment_area_use` — the investment cost of area use" + expression: cost_area_use * area_use + +given: + parameters: + flow_cap_max: { dims: [nodes, techs] } + sink_unit: { dims: [nodes, techs], dtype: str } + source_unit: { dims: [nodes, techs], dtype: str } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_area_use } + +constraints: + force_zero_area_use: + description: "`force_zero_area_use` — a technology with no flow capacity uses no area" + dims: [nodes, techs] + where: area_use AND flow_cap_max == 0 + expression: area_use == 0 + area_use_per_flow_capacity: + description: "`area_use_per_flow_capacity` — area use follows flow capacity, where set" + dims: [nodes, techs, carriers] + where: flow_cap AND area_use AND area_use_per_flow_cap + expression: area_use == flow_cap * area_use_per_flow_cap + area_use_capacity_per_loc: + description: >- + `area_use_capacity_per_loc` — the technologies at a node use at most + its available area. Calliope's `where: area_use` over a node reads as + any technology there using area + dims: [nodes] + where: count(area_use, over=techs) >= 1 AND available_area + expression: sum(area_use, over=techs) <= available_area + +assumptions: + unbounded_area_use_cost: + description: Calliope's `unbounded_area_use_cost` — a negative area cost needs a finite maximum + holds: NOT cost_area_use < 0 OR area_use_max diff --git a/examples/calliope/balance.yaml b/examples/calliope/balance.yaml new file mode 100644 index 00000000..3e6bffd8 --- /dev/null +++ b/examples/calliope/balance.yaml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + carrier_in: + description: whether a technology consumes a carrier at a node + dims: [nodes, techs, carriers] + dtype: bool + carrier_out: + description: whether a technology produces a carrier at a node + dims: [nodes, techs, carriers] + dtype: bool + +expressions: + carrier_flow: + description: >- + what every technology and every other file puts into a node's carrier, + less what it takes out + dims: [nodes, carriers, timesteps] + empty: true + +constraints: + system_balance: + description: >- + `system_balance` — at every node, in every time step, a carrier's + production equals its consumption. Built where a technology at the + node produces or consumes the carrier + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + expression: carrier_flow == 0 diff --git a/examples/calliope/conversion.yaml b/examples/calliope/conversion.yaml new file mode 100644 index 00000000..c73050e3 --- /dev/null +++ b/examples/calliope/conversion.yaml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_conversion: + description: "`balance_conversion` — a conversion technology puts out, before its losses, what it takes in after them" + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage + expression: sum(flow_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) diff --git a/examples/calliope/cost.yaml b/examples/calliope/cost.yaml new file mode 100644 index 00000000..a0e06a32 --- /dev/null +++ b/examples/calliope/cost.yaml @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + cost_om_annual_investment_fraction: + description: "`cost_om_annual_investment_fraction` — the annual cost of operation, as a share of the investment cost" + dims: [nodes, techs, costs] + cost_depreciation_rate: + description: >- + `cost_depreciation_rate` — the share of the investment cost a year + carries; given only where set, and derived from the lifetime and the + interest rate elsewhere + dims: [nodes, techs, costs] + cost_interest_rate: + description: "`cost_interest_rate` — the interest rate an investment is annualised at" + dims: [nodes, techs, costs] + lifetime: + description: >- + `lifetime` — the years a technology lasts. Calliope's default is + `.inf`, and data prep fills it + dims: [nodes, techs] + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + objective_cost_weights: { dims: [costs] } + expressions: + system_cost: { dims: [], term: cost_of_techs } + +expressions: + cost_investment: + description: >- + `cost_investment` — the investment cost of a technology: flow, storage + and source capacity, and area use. Each file that builds a capacity + adds its own cost + dims: [nodes, techs, costs] + empty: true + cost_operation_variable: + description: >- + `cost_operation_variable` — the operating cost of a technology in a + time step. Each file that builds a flow adds its own cost + dims: [nodes, techs, costs, timesteps] + empty: true + cost_operation_fixed: + description: >- + `cost_operation_fixed` — the fixed annual operating cost of a + technology: its share of the investment cost here, and what each file + adds per unit of capacity + dims: [nodes, techs, costs] + expression: annualisation_weight * cost_investment * cost_om_annual_investment_fraction + annualisation_weight: + description: "`$annualisation_weight` — the share of a year the modelled time steps stand for" + expression: sum(timestep_resolution * timestep_weights, over=timesteps) / 8760 + depreciation_rate: + description: >- + `$depreciation_rate` of `cost_investment_annualised` — the share of the + investment cost a year carries: as given, one over the lifetime with + no interest, and the annuity factor with some + dims: [nodes, techs, costs] + cases: + given: + when: cost_depreciation_rate + expression: cost_depreciation_rate + no_interest: + when: NOT cost_depreciation_rate AND (NOT cost_interest_rate OR cost_interest_rate == 0) + expression: 1 / lifetime + otherwise: >- + cost_interest_rate * (1 + cost_interest_rate) ** lifetime + / ((1 + cost_interest_rate) ** lifetime - 1) + cost_investment_annualised: + description: "`cost_investment_annualised` — the investment cost, as a year's share scaled to the modelled time" + expression: annualisation_weight * depreciation_rate * cost_investment + cost: + description: "`cost` — the total cost of a technology: investment, variable and fixed operation" + expression: cost_investment_annualised + sum(cost_operation_variable, over=timesteps) + cost_operation_fixed + cost_of_techs: + description: "`sum(sum(cost, over=[nodes, techs]) * objective_cost_weights, over=costs)` of `min_cost_optimisation`" + expression: sum(sum(cost, over=[nodes, techs]) * objective_cost_weights) diff --git a/examples/calliope/demand.yaml b/examples/calliope/demand.yaml new file mode 100644 index 00000000..ab8994bc --- /dev/null +++ b/examples/calliope/demand.yaml @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + sink_use_min: + description: "`sink_use_min` — least sink use in a time step, per unit of `sink_unit`" + dims: [nodes, techs, timesteps] + sink_use_max: + description: "`sink_use_max` — most sink use in a time step, per unit of `sink_unit`; given only where set" + dims: [nodes, techs, timesteps] + sink_use_equals: + description: "`sink_use_equals` — the sink use required in a time step, such as a demand profile; given only where set" + dims: [nodes, techs, timesteps] + sink_unit: + description: >- + `sink_unit` — what the sink is per: `absolute`, `per_area` of area + use, or `per_cap` of flow capacity. Calliope's default is + `absolute`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + +expressions: + flow_cap_in: + description: "`where(flow_cap, carrier_in)` — the flow capacity of the carriers a technology consumes" + dims: [nodes, techs, carriers] + cases: + consumed: + when: carrier_in + expression: flow_cap + otherwise: 0 + sink_scaler: + description: "`$sink_scaler` — what the sink parameters are per: area use, flow capacity, or one" + dims: [nodes, techs] + cases: + per_area: + when: sink_unit == per_area + expression: area_use + per_cap: + when: sink_unit == per_cap + expression: sum(flow_cap_in, over=carriers) + otherwise: 1 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_in: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + area_use: { dims: [nodes, techs] } + expressions: + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_demand_equals: + description: "`balance_demand` where `sink_use_equals` is set — a demand technology takes in what its sink requires" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND base_tech == 'demand' AND sink_use_equals + expression: flow_in_inc_eff == sink_use_equals * sink_scaler + balance_demand_max: + description: "`balance_demand` where only `sink_use_max` is set — a demand technology takes in at most what its sink allows" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND base_tech == 'demand' AND NOT sink_use_equals AND sink_use_max + expression: flow_in_inc_eff <= sink_use_max * sink_scaler + balance_demand_min_use: + description: "`balance_demand_min_use` — a demand technology takes in at least its least sink use" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND sink_use_min AND NOT sink_use_equals AND base_tech == 'demand' + expression: flow_in_inc_eff >= sink_use_min * sink_scaler + +assumptions: + finite_sink_use: + description: Calliope's `finite_source_use`, for the sink — a required use is finite + holds: NOT sink_use_equals == inf + sink_unit_one_of: + description: Calliope's `one_of` on `sink_unit` + holds: sink_unit == absolute OR sink_unit == per_area OR sink_unit == per_cap + where: sink_unit diff --git a/examples/calliope/export.yaml b/examples/calliope/export.yaml new file mode 100644 index 00000000..792d26d7 --- /dev/null +++ b/examples/calliope/export.yaml @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + carrier_export: + description: "`carrier_export` — whether a technology may export a carrier it produces out of the system" + dims: [nodes, techs, carriers] + dtype: bool + export_min: + description: "`export_min` — least export. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs, carriers] + export_max: + description: "`export_max` — most export. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs, carriers] + cost_export: + description: "`cost_export` — the cost of one unit of export, usually negative" + dims: [nodes, techs, costs, timesteps] + +variables: + flow_export: + description: "`flow_export` — what a technology exports out of the system in a time step" + dims: [nodes, techs, carriers, timesteps] + where: carrier_export + bounds: { lower: export_min, upper: export_max } + absence: zero + +expressions: + export_carrier_flow: -sum(flow_export, over=techs) + export_cost_operation_variable: timestep_weights * sum(cost_export * flow_export, over=carriers) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_weights: { dims: [timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: export_carrier_flow } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: export_cost_operation_variable } + +constraints: + export_balance: + description: "`export_balance` — a technology exports at most what it puts out" + dims: [nodes, techs, carriers, timesteps] + where: flow_export + expression: flow_out >= flow_export + +assumptions: + export_only_for_outflows: + description: Calliope's `export_only_for_outflows` — an exported carrier is one the technology produces + holds: NOT carrier_export OR count(carrier_out, over=nodes) >= 1 diff --git a/examples/calliope/extensions/annual_energy_balance.yaml b/examples/calliope/extensions/annual_energy_balance.yaml new file mode 100644 index 00000000..7c631df9 --- /dev/null +++ b/examples/calliope/extensions/annual_energy_balance.yaml @@ -0,0 +1,89 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + annual_flow_max: + description: "`annual_flow_max` — the most a technology puts out over the whole time; given only where set" + dims: [techs] + annual_flow_max_group: + description: >- + `annual_flow_max` as the group row reads it — the most the group puts + out over the whole time. Calliope reads one parameter at three shapes + and lets the data choose; a parameter here has one, so the group's + limit is a number of its own + dims: [] + annual_source_max: + description: "`annual_source_max` — the most a technology takes from its source over the whole time; given only where set" + dims: [techs] + annual_sink_max: + description: "`annual_sink_max` — the most a technology puts into its sink over the whole time; given only where set" + dims: [techs] + flow_max_group: + description: "`flow_max_group` — whether a technology is in the group the group limit holds for" + dims: [techs] + dtype: bool + +expressions: + flow_out_of_group: + description: "`flow_out[techs=$techs]` — outflow of the technologies in the group" + dims: [nodes, techs, carriers, timesteps] + cases: + in_group: + when: flow_max_group + expression: flow_out + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + source_use: { dims: [nodes, techs, timesteps] } + +constraints: + annual_energy_balance_per_tech_and_node: + description: "`annual_energy_balance_per_tech_and_node` — a technology at a node puts out at most its annual limit" + dims: [nodes, techs] + where: annual_flow_max + expression: sum(flow_out, over=[carriers, timesteps]) <= annual_flow_max + annual_energy_balance_global_per_tech: + description: "`annual_energy_balance_global_per_tech` — a technology puts out at most its annual limit over every node" + dims: [techs] + where: annual_flow_max + expression: sum(flow_out, over=[nodes, carriers, timesteps]) <= annual_flow_max + annual_energy_balance_global_multi_tech: + description: "`annual_energy_balance_global_multi_tech` — the group of technologies puts out at most its annual limit over every node" + dims: [] + where: annual_flow_max_group + expression: sum(flow_out_of_group) <= annual_flow_max_group + annual_energy_balance_total_source_availability: + description: >- + `annual_energy_balance_total_source_availability` — a technology takes + at most its annual limit from its source. Calliope's `where: + source_use` over a technology reads as the technology being a supply + one, which is where `source_use` is built + dims: [techs] + where: base_tech == 'supply' AND annual_source_max + expression: sum(source_use, over=[nodes, timesteps]) <= annual_source_max + annual_energy_balance_total_sink_availability: + description: "`annual_energy_balance_total_sink_availability` — a demand technology takes in at most its annual limit" + dims: [techs] + where: base_tech == 'demand' AND annual_sink_max + expression: sum(flow_in, over=[nodes, carriers, timesteps]) <= annual_sink_max diff --git a/examples/calliope/extensions/chp_htp.yaml b/examples/calliope/extensions/chp_htp.yaml new file mode 100644 index 00000000..d174b61f --- /dev/null +++ b/examples/calliope/extensions/chp_htp.yaml @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + turbine_type: + description: "`turbine_type` — `extraction` or `backpressure`: the kind of turbine a combined heat and power plant has" + dims: [nodes, techs] + dtype: str + power_loss_factor: + description: "`power_loss_factor` — `cv`, the power an extraction turbine loses per unit of heat. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + power_to_heat_ratio: + description: "`power_to_heat_ratio` — `cb`, the backpressure ratio. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + boiler_eff: + description: "`boiler_eff` — the efficiency of the boiler fuel may be diverted to; given only where set" + dims: [nodes, techs] + +expressions: + chp_electricity_out: + description: "`flow_out[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out + otherwise: 0 + chp_heat_out: + description: "`flow_out[carriers=heat]`" + dims: [nodes, techs, carriers, timesteps] + cases: + heat: + when: carriers == heat + expression: flow_out + otherwise: 0 + chp_electricity_out_eff: + description: "`flow_out_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_eff + otherwise: 0 + chp_electricity_out_inc_eff: + description: "`flow_out_inc_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_inc_eff + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + flow_out_eff: { dims: [nodes, techs, carriers, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_conversion_backpressure: + description: >- + `balance_conversion` for a backpressure plant with no boiler — the + plant puts out, before losses, as much electricity as it takes in fuel + after them. The patch keeps the base row off these plants + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage AND turbine_type == backpressure AND NOT boiler_eff + expression: sum(chp_electricity_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) + chp_extraction_line: + description: "`chp_extraction_line` — an extraction plant puts out at most the electricity its fuel gives, less what the heat costs" + dims: [nodes, techs, timesteps] + where: turbine_type == extraction + expression: >- + sum(chp_electricity_out, over=carriers) + <= sum(flow_in_inc_eff, over=carriers) * sum(chp_electricity_out_eff, over=carriers) + - sum(chp_heat_out, over=carriers) * power_loss_factor + chp_backpressure_line_min: + description: "`chp_backpressure_line_min` — an extraction plant puts out at least the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == extraction + expression: sum(chp_electricity_out, over=carriers) >= sum(chp_heat_out, over=carriers) * power_to_heat_ratio + chp_backpressure_line_max: + description: "`chp_backpressure_line_max` — a backpressure plant with a boiler puts out at most the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND boiler_eff + expression: sum(chp_electricity_out, over=carriers) <= sum(chp_heat_out, over=carriers) * power_to_heat_ratio + chp_divert_fuel_to_boiler: + description: "`chp_divert_fuel_to_boiler` — a backpressure plant with a boiler puts out at most the heat its fuel gives through turbine and boiler" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND boiler_eff + expression: >- + sum(chp_heat_out, over=carriers) + <= sum(flow_in_inc_eff, over=carriers) * boiler_eff + - sum(chp_electricity_out, over=carriers) + * (boiler_eff / sum(chp_electricity_out_eff, over=carriers) - 1 / power_to_heat_ratio) + chp_backpressure_line_equals: + description: "`chp_backpressure_line_equals` — a backpressure plant with no boiler puts out the backpressure ratio of electricity per unit of heat" + dims: [nodes, techs, timesteps] + where: turbine_type == backpressure AND NOT boiler_eff + expression: sum(chp_electricity_out, over=carriers) == sum(chp_heat_out, over=carriers) * power_to_heat_ratio + +assumptions: + turbine_type_one_of: + description: Calliope's `one_of` on `turbine_type` + holds: turbine_type == extraction OR turbine_type == backpressure + where: turbine_type diff --git a/examples/calliope/extensions/demand_share_per_timestep_decision.yaml b/examples/calliope/extensions/demand_share_per_timestep_decision.yaml new file mode 100644 index 00000000..fbcd2637 --- /dev/null +++ b/examples/calliope/extensions/demand_share_per_timestep_decision.yaml @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + decide_demand_share: + description: >- + `decide_demand_share` — the demand technology whose inflow a technology + meets a share of. Calliope reads it with `select_from_lookup_arrays`, + which is a read through the relation + key: techs + values: { demand: techs } + demand_share_carrier: + description: >- + `demand_share_carrier` — the carrier a share of demand is counted in. + Calliope slices `flow_out` by it, which is a test of the pair + key: [techs, carriers] + +parameters: + demand_share_relaxation: + description: "`demand_share_relaxation` — how far the share may stray from the one decided, as a fraction" + dims: [nodes, techs] + demand_share_limit: + description: "`demand_share_limit` — the share of demand the technologies meet together; given only where set" + dims: [nodes] + +variables: + demand_share_per_timestep_decision: + description: "`demand_share_per_timestep_decision` — the share of demand a technology meets, the same in every time step" + dims: [nodes, techs] + where: decide_demand_share + bounds: { lower: 0 } + absence: zero + +expressions: + demand_share_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of the carrier its share is counted in" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: demand_share_carrier + expression: flow_out + otherwise: 0 + demand_share_sink: + description: "`select_from_lookup_arrays(sink_use_equals, techs=decide_demand_share)` — the demand a technology meets a share of" + expression: at(sink_use_equals, by=decide_demand_share, over=demand, into=techs) + +given: + parameters: + sink_use_equals: { dims: [nodes, techs, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_per_timestep_decision_main_min: + description: "`demand_share_per_timestep_decision_main_min` — a technology puts out at least its decided share of demand, less the relaxation" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_decision + expression: >- + sum(demand_share_flow_out, over=carriers) + >= (1 - demand_share_relaxation) * demand_share_sink * demand_share_per_timestep_decision + demand_share_per_timestep_decision_main_max: + description: "`demand_share_per_timestep_decision_main_max` — a technology puts out at most its decided share of demand, plus the relaxation" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_decision + expression: >- + sum(demand_share_flow_out, over=carriers) + <= (1 + demand_share_relaxation) * demand_share_sink * demand_share_per_timestep_decision + demand_share_per_timestep_decision_sum: + description: >- + `demand_share_per_timestep_decision_sum` — the decided shares at a node + add up to the limit. Calliope's `where: demand_share_per_timestep_decision` + over a node reads as any technology there deciding a share. Calliope + builds the row in every time step, and it is the same in each; a row + repeated along a dimension it does not read is refused, so it is one + row per node + dims: [nodes] + where: count(demand_share_per_timestep_decision, over=techs) >= 1 AND demand_share_limit + expression: sum(demand_share_per_timestep_decision, over=techs) == demand_share_limit + +assumptions: + demand_share_is_fraction: + description: Calliope's `demand_share_is_fraction` — the demand share limit is a fraction + holds: demand_share_limit >= 0 AND demand_share_limit <= 1 + where: demand_share_limit diff --git a/examples/calliope/extensions/fuel_dist.yaml b/examples/calliope/extensions/fuel_dist.yaml new file mode 100644 index 00000000..a9fca0e0 --- /dev/null +++ b/examples/calliope/extensions/fuel_dist.yaml @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + allow_fuel_distribution: + description: "`allow_fuel_distribution` — whether a node takes part in distributing a carrier" + dims: [nodes, carriers] + dtype: bool + fuel_import_max: + description: "`fuel_import_max` — the most of a carrier a node imports in a time step; given only where set" + dims: [nodes, carriers] + fuel_export_max: + description: "`fuel_export_max` — the most of a carrier a node exports in a time step; given only where set" + dims: [nodes, carriers] + cost_fuel_distribution: + description: "`cost_fuel_distribution` — the cost of importing one unit of a carrier, and the revenue of exporting it" + dims: [nodes, carriers, costs] + +variables: + fuel_distributor: + description: >- + `fuel_distributor` — what a node imports of a carrier, with no network + behind it; an export is negative + dims: [nodes, carriers, timesteps] + where: allow_fuel_distribution + absence: zero + +expressions: + fuel_dist_carrier_flow: + description: "`+ fuel_distributor` — the term Calliope writes into `system_balance`, by restating it whole" + expression: fuel_distributor + cost_var_fuel_distribution: + description: "`cost_var_fuel_distribution` — the cost of importing, and the revenue of exporting, a carrier" + expression: timestep_weights * fuel_distributor * cost_fuel_distribution + fuel_dist_system_cost: + description: >- + `sum(cost_var_fuel_distribution, …) * objective_cost_weights` — the term + Calliope writes into the objective, by restating it whole + expression: sum(cost_var_fuel_distribution * objective_cost_weights) + +given: + parameters: + timestep_weights: { dims: [timesteps] } + objective_cost_weights: { dims: [costs] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: fuel_dist_carrier_flow } + system_cost: { dims: [], term: fuel_dist_system_cost } + +constraints: + restrict_total_imports_and_exports: + description: >- + `restrict_total_imports_and_exports` — what the nodes import of a + carrier is what they export. Calliope's `where: fuel_distributor` over + a carrier reads as any node distributing it + dims: [carriers, timesteps] + where: count(fuel_distributor, over=nodes) >= 1 + expression: sum(fuel_distributor, over=nodes) == 0 + restrict_nodal_imports: + description: "`restrict_nodal_imports` — a node imports at most its limit" + dims: [nodes, carriers, timesteps] + where: fuel_distributor AND fuel_import_max + expression: fuel_distributor <= fuel_import_max + restrict_nodal_exports: + description: "`restrict_nodal_exports` — a node exports at most its limit" + dims: [nodes, carriers, timesteps] + where: fuel_distributor AND fuel_export_max + expression: -1 * fuel_distributor <= fuel_export_max diff --git a/examples/calliope/extensions/max_time_varying.yaml b/examples/calliope/extensions/max_time_varying.yaml new file mode 100644 index 00000000..f4fa6150 --- /dev/null +++ b/examples/calliope/extensions/max_time_varying.yaml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + flow_cap_max_relative_per_ts: + description: "`flow_cap_max_relative_per_ts` — the share of its flow capacity a technology may put out in a time step; given only where set" + dims: [nodes, techs, timesteps] + +given: + parameters: + flow_out_parasitic_eff: { dims: [nodes, techs, carriers, timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_cap: { dims: [nodes, techs, carriers] } + +constraints: + max_time_varying_flow_cap: + description: "`max_time_varying_flow_cap` — outflow is at most a share of the flow capacity that varies in time" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND flow_cap_max_relative_per_ts + expression: flow_out <= flow_cap_max_relative_per_ts * flow_cap * flow_out_parasitic_eff diff --git a/examples/calliope/extensions/milp.yaml b/examples/calliope/extensions/milp.yaml new file mode 100644 index 00000000..79ec713a --- /dev/null +++ b/examples/calliope/extensions/milp.yaml @@ -0,0 +1,282 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + cap_method: + description: >- + `cap_method` — `continuous` or `integer`: whether a technology's + capacity is bought in whole units. Calliope's default is + `continuous`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + integer_dispatch: + description: "`integer_dispatch` — whether a unit-bought technology runs in whole units" + dims: [nodes, techs] + dtype: bool + force_async_flow: + description: "`force_async_flow` — whether a technology may not take in and put out in one time step" + dims: [nodes, techs] + dtype: bool + flow_cap_per_unit: + description: "`flow_cap_per_unit` — the flow capacity of one unit; given only where set" + dims: [nodes, techs] + storage_cap_per_unit: + description: "`storage_cap_per_unit` — the storage capacity of one unit; given only where set" + dims: [nodes, techs] + purchased_units_min: + description: "`purchased_units_min` — least units bought. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + purchased_units_max: + description: "`purchased_units_max` — most units bought. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + purchased_units_min_systemwide: + description: "`purchased_units_min_systemwide` — least units of a technology bought over every node" + dims: [techs] + purchased_units_max_systemwide: + description: "`purchased_units_max_systemwide` — most units of a technology bought over every node; given only where set" + dims: [techs] + cost_purchase: + description: "`cost_purchase` — the cost of one unit bought" + dims: [nodes, techs, costs] + cost_purchase_per_distance: + description: "`cost_purchase_per_distance` — the cost of one unit of a link bought, per unit of distance" + dims: [nodes, techs, costs] + +variables: + purchased_units: + description: "`purchased_units` — how many units of a technology are bought" + dims: [nodes, techs] + where: cap_method == integer + domain: integer + bounds: { lower: purchased_units_min, upper: purchased_units_max } + absence: zero + operating_units: + description: "`operating_units` — how many bought units run in a time step" + dims: [nodes, techs, timesteps] + where: integer_dispatch AND cap_method == integer + domain: integer + bounds: { lower: 0 } + absence: zero + async_flow_switch: + description: "`async_flow_switch` — whether a technology puts out, rather than takes in, in a time step" + dims: [nodes, techs, timesteps] + where: force_async_flow + domain: binary + absence: zero + available_flow_cap: + description: "`available_flow_cap` — the flow capacity in a time step: the whole of it where the technology runs, none where it does not" + dims: [nodes, techs, carriers, timesteps] + where: flow_cap AND integer_dispatch AND flow_cap_max AND NOT flow_cap_per_unit + bounds: { lower: 0 } + absence: zero + +expressions: + cost_investment_purchase: + description: "`cost_investment_purchase` — the investment cost of the units bought; a link's cost is split between its two ends" + dims: [nodes, techs, costs] + cases: + transmission: + when: base_tech == 'transmission' + expression: (cost_purchase + cost_purchase_per_distance * distance) * purchased_units * 0.5 + otherwise: cost_purchase * purchased_units + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + distance: { dims: [techs] } + bigM: { dims: [] } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + flow_cap_min: { dims: [nodes, techs] } + flow_cap_max: { dims: [nodes, techs] } + flow_cap_min_systemwide: { dims: [techs, carriers] } + flow_out_min_relative: { dims: [nodes, techs, timesteps] } + flow_out_parasitic_eff: { dims: [nodes, techs, carriers, timesteps] } + storage_cap_min: { dims: [nodes, techs] } + storage_cap_max: { dims: [nodes, techs] } + area_use_min: { dims: [nodes, techs] } + source_cap_min: { dims: [nodes, techs] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + storage: { dims: [nodes, techs, timesteps] } + storage_cap: { dims: [nodes, techs] } + area_use: { dims: [nodes, techs] } + source_cap: { dims: [nodes, techs] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_purchase } + +constraints: + unit_commitment_milp: + description: "`unit_commitment_milp` — at most the units bought run" + dims: [nodes, techs, timesteps] + where: operating_units AND purchased_units + expression: operating_units <= purchased_units + flow_out_max_milp: + description: "`flow_out_max_milp` — outflow is at most what the running units can put out" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_cap_per_unit + expression: flow_out <= operating_units * timestep_resolution * flow_cap_per_unit * flow_out_parasitic_eff + flow_in_max_milp: + description: "`flow_in_max_milp` — inflow is at most what the running units can take in" + dims: [nodes, techs, carriers, timesteps] + where: flow_in AND operating_units AND flow_cap_per_unit + expression: flow_in <= operating_units * timestep_resolution * flow_cap_per_unit + flow_out_min_milp_per_unit: + description: "`flow_out_min_milp` where `flow_cap_per_unit` is set — outflow is at least the running units' least share" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_out_min_relative AND flow_cap_per_unit + expression: flow_out >= operating_units * timestep_resolution * flow_cap_per_unit * flow_out_min_relative + flow_out_min_milp_available: + description: "`flow_out_min_milp` where the available flow capacity is built — outflow is at least its least share of it" + dims: [nodes, techs, carriers, timesteps] + where: flow_out AND operating_units AND flow_out_min_relative AND available_flow_cap + expression: flow_out >= available_flow_cap * timestep_resolution * flow_out_min_relative + storage_capacity_units_milp: + description: "`storage_capacity_units_milp` — storage capacity is the units bought times the capacity of one" + dims: [nodes, techs] + where: storage_cap AND purchased_units AND storage_cap_per_unit + expression: storage_cap == purchased_units * storage_cap_per_unit + flow_capacity_units_milp: + description: "`flow_capacity_units_milp` — flow capacity is the units bought times the capacity of one" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND flow_cap_per_unit + expression: flow_cap == purchased_units * flow_cap_per_unit + flow_capacity_max_purchase_milp: + description: "`flow_capacity_max_purchase_milp` where `flow_cap_max` is set — no flow capacity unless a unit is bought" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND flow_cap_max + expression: flow_cap <= flow_cap_max * purchased_units + flow_capacity_max_purchase_milp_big_m: + description: "`flow_capacity_max_purchase_milp` where `flow_cap_max` is not set — the same, with `bigM` for the maximum" + dims: [nodes, techs, carriers] + where: flow_cap AND purchased_units AND NOT flow_cap_max + expression: flow_cap <= bigM * purchased_units + storage_capacity_max_purchase_milp: + description: "`storage_capacity_max_purchase_milp` — no storage capacity unless a unit is bought" + dims: [nodes, techs] + where: purchased_units AND storage_cap_max + expression: storage_cap <= storage_cap_max * purchased_units + unit_capacity_max_systemwide_milp: + description: "`unit_capacity_max_systemwide_milp` — the units of a technology bought over every node are at most its system-wide maximum" + dims: [techs] + where: count(purchased_units, over=nodes) >= 1 AND purchased_units_max_systemwide + expression: sum(purchased_units, over=nodes) <= purchased_units_max_systemwide + unit_capacity_min_systemwide_milp: + description: >- + `unit_capacity_min_systemwide_milp` — the units of a technology bought + over every node are at least its system-wide minimum. Calliope builds + it where the system-wide maximum is set, as here + dims: [techs] + where: count(purchased_units, over=nodes) >= 1 AND purchased_units_max_systemwide + expression: sum(purchased_units, over=nodes) >= purchased_units_min_systemwide + async_flow_in_milp: + description: "`async_flow_in_milp` — no inflow in a time step the switch gives to outflow" + dims: [nodes, techs, timesteps] + where: async_flow_switch + expression: sum(flow_in, over=carriers) <= (1 - async_flow_switch) * bigM + async_flow_out_milp: + description: "`async_flow_out_milp` — no outflow in a time step the switch gives to inflow" + dims: [nodes, techs, timesteps] + where: async_flow_switch + expression: sum(flow_out, over=carriers) <= async_flow_switch * bigM + available_flow_cap_continuous: + description: "`available_flow_cap_continuous` — the available flow capacity is at most the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap <= flow_cap + available_flow_cap_binary: + description: "`available_flow_cap_binary` — the available flow capacity is zero where no unit runs" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap <= flow_cap_max * operating_units + available_flow_cap_max_binary_continuous_switch: + description: "`available_flow_cap_max_binary_continuous_switch` — the available flow capacity is the whole flow capacity where the units run" + dims: [nodes, techs, carriers, timesteps] + where: available_flow_cap + expression: available_flow_cap >= flow_cap + (operating_units - purchased_units) * flow_cap_max + flow_capacity_minimum: + description: "`flow_capacity_minimum` where no unit is bought — flow capacity is at least its least" + dims: [nodes, techs, carriers] + where: flow_cap AND flow_cap_min AND NOT purchased_units + expression: flow_cap >= flow_cap_min + flow_capacity_minimum_purchased: + description: "`flow_capacity_minimum` where units are bought — flow capacity is at least its least, if a unit is bought" + dims: [nodes, techs, carriers] + where: flow_cap AND flow_cap_min AND purchased_units + expression: flow_cap >= flow_cap_min * purchased_units + storage_capacity_minimum: + description: "`storage_capacity_minimum` where no unit is bought — storage capacity is at least its least" + dims: [nodes, techs] + where: storage_cap_min AND NOT purchased_units + expression: storage_cap >= storage_cap_min + storage_capacity_minimum_purchased: + description: "`storage_capacity_minimum` where units are bought — storage capacity is at least its least, if a unit is bought" + dims: [nodes, techs] + where: storage_cap_min AND purchased_units + expression: storage_cap >= storage_cap_min * purchased_units + area_use_minimum: + description: "`area_use_minimum` where no unit is bought — area use is at least its least" + dims: [nodes, techs] + where: area_use_min AND NOT purchased_units + expression: area_use >= area_use_min + area_use_minimum_purchased: + description: "`area_use_minimum` where units are bought — area use is at least its least, if a unit is bought" + dims: [nodes, techs] + where: area_use_min AND purchased_units + expression: area_use >= area_use_min * purchased_units + source_capacity_minimum: + description: "`source_capacity_minimum` where no unit is bought — source capacity is at least its least" + dims: [nodes, techs] + where: base_tech == 'supply' AND source_cap_min AND NOT purchased_units + expression: source_cap >= source_cap_min + source_capacity_minimum_purchased: + description: "`source_capacity_minimum` where units are bought — source capacity is at least its least, if a unit is bought" + dims: [nodes, techs] + where: base_tech == 'supply' AND source_cap_min AND purchased_units + expression: source_cap >= source_cap_min * purchased_units + flow_capacity_systemwide_min_purchased: + description: >- + `flow_capacity_systemwide_min` where units are bought — the flow + capacity over every node is at least the system-wide minimum times the + units bought. The patch narrows the base row to where none are + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide AND count(purchased_units, over=nodes) >= 1 + expression: sum(flow_cap, over=nodes) >= flow_cap_min_systemwide * sum(purchased_units, over=nodes) + +assumptions: + distance_only_for_transmission_milp: + description: Calliope's `distance_only_for_transmission_milp` — only a link sets a per-distance purchase cost + holds: base_tech == 'transmission' OR NOT cost_purchase_per_distance + conflicting_flow_caps: + description: Calliope's `conflicting_flow_caps` — a technology sets a capacity per unit or a capacity range, not both + holds: NOT ((flow_cap_max OR flow_cap_min) AND flow_cap_per_unit) + unit_commitment_only_for_units: + description: Calliope's `unit_commitment_only_for_units` — integer dispatch needs integer units + holds: NOT integer_dispatch OR cap_method == integer + conflicting_storage_caps: + description: Calliope's `conflicting_storage_caps` — a technology sets a storage capacity per unit or a range, not both + holds: NOT ((storage_cap_max OR storage_cap_min) AND storage_cap_per_unit) + cap_method_one_of: + description: Calliope's `one_of` on `cap_method` + holds: cap_method == continuous OR cap_method == integer + where: cap_method diff --git a/examples/calliope/extensions/monthly_peak_flow_charge.yaml b/examples/calliope/extensions/monthly_peak_flow_charge.yaml new file mode 100644 index 00000000..33ee5c6f --- /dev/null +++ b/examples/calliope/extensions/monthly_peak_flow_charge.yaml @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + months: + description: Calliope's `months` — the months of the year + dtype: int + +relations: + lookup_month: + description: >- + `lookup_month` — the month a time step falls in. Calliope ships it as a + boolean table over time step and month, and builds its row over both; + as a relation the row is one per time step + key: timesteps + values: months + +parameters: + monthly_peak_mode: + description: "`monthly_peak_mode` — whether a technology's peak outflow in a month is priced" + dims: [nodes, techs, carriers] + dtype: bool + cost_month_peak: + description: "`cost_month_peak` — the cost of one unit of peak outflow in a month" + dims: [nodes, techs, costs] + +variables: + flow_peak_month: + description: "`flow_peak_month` — a technology's peak outflow in a month" + dims: [nodes, techs, carriers, months] + where: carrier_out AND monthly_peak_mode + bounds: { lower: 0, upper: flow_cap_max } + absence: zero + +expressions: + cost_month_peak_charge: + description: "`sum(cost_month_peak * flow_peak_month, over=[carriers, months])` — the term Calliope writes into `cost_operation_fixed`, by restating it whole" + expression: sum(cost_month_peak * flow_peak_month, over=[carriers, months]) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + flow_cap_max: { dims: [nodes, techs] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + cost_operation_fixed: { dims: [nodes, techs, costs], term: cost_month_peak_charge } + +constraints: + set_peak_month_flow: + description: "`set_peak_month_flow` — the peak outflow in a month is at least the outflow in each of its time steps" + dims: [nodes, techs, carriers, timesteps] + where: at(flow_peak_month, by=lookup_month, over=months, into=timesteps) + expression: flow_out <= at(flow_peak_month, by=lookup_month, over=months, into=timesteps) diff --git a/examples/calliope/extensions/net_import_share.yaml b/examples/calliope/extensions/net_import_share.yaml new file mode 100644 index 00000000..9b193f92 --- /dev/null +++ b/examples/calliope/extensions/net_import_share.yaml @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + net_import_share: + description: >- + `net_import_share` — the share of a node's flows that imports may make + up. Calliope's default is 1, and data prep fills it. Calliope reads it + per node and in a row over a group of nodes; a parameter here has one + shape, so it is one number + dims: [] + +expressions: + flow_out_transmission_techs: + description: "`flow_out_transmission_techs` — the outflow of transmission technologies, that is, imports" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: carrier_out AND base_tech == 'transmission' + expression: flow_out + otherwise: 0 + electricity_imports: + description: "`flow_out_transmission_techs[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_transmission_techs + otherwise: 0 + electricity_balance: + description: "`$total_energy_balance` — the outflow of electricity at a node, less its inflow" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out - flow_in + otherwise: 0 + node_group_heat_imports: + description: "`flow_out_transmission_techs[nodes=$node_group, carriers=$carrier]` — heat imports at nodes `a` and `c`" + dims: [nodes, techs, carriers, timesteps] + cases: + group: + when: (nodes == 'a' OR nodes == 'c') AND carriers == heat + expression: flow_out_transmission_techs + otherwise: 0 + node_group_heat_balance: + description: "`$total_energy_balance` of the node group — the outflow of heat at nodes `a` and `c`, less its inflow" + dims: [nodes, techs, carriers, timesteps] + cases: + group: + when: (nodes == 'a' OR nodes == 'c') AND carriers == heat + expression: flow_out - flow_in + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + net_import_share_max: + description: >- + `net_import_share_max` — electricity imports at a node are at most + their share of its electricity balance in each time step. Calliope's + `where: any(flow_out_transmission_techs, over=techs)` reads as a link + at the node putting out any carrier + dims: [nodes, timesteps] + where: count(count(carrier_out, over=carriers) >= 1 AND base_tech == 'transmission', over=techs) >= 1 + expression: >- + net_import_share * sum(electricity_imports, over=[techs, carriers]) + <= sum(electricity_balance, over=[techs, carriers]) + net_annual_import_share_max: + description: "`net_annual_import_share_max` — electricity imports at a node are at most their share of its electricity balance over the year" + dims: [nodes] + where: count(count(carrier_out, over=carriers) >= 1 AND base_tech == 'transmission', over=techs) >= 1 + expression: >- + net_import_share * sum(electricity_imports, over=[techs, carriers, timesteps]) + <= sum(electricity_balance, over=[techs, carriers, timesteps]) + net_annual_import_share_max_node_group: + description: "`net_annual_import_share_max_node_group` — heat imports at nodes `a` and `c` are at most their share of the group's heat balance over the year" + dims: [] + expression: net_import_share * sum(node_group_heat_imports) <= sum(node_group_heat_balance) diff --git a/examples/calliope/extensions/piecewise_linear_costs.yaml b/examples/calliope/extensions/piecewise_linear_costs.yaml new file mode 100644 index 00000000..63215fef --- /dev/null +++ b/examples/calliope/extensions/piecewise_linear_costs.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + pieces: + description: Calliope's `pieces` — the lines a piecewise curve is the upper envelope of + dtype: int + +parameters: + cost_flow_cap_piecewise_slopes: + description: "`cost_flow_cap_piecewise_slopes` — the slope of each line of a convex investment cost curve" + dims: [nodes, techs, costs, pieces] + cost_flow_cap_piecewise_intercept: + description: "`cost_flow_cap_piecewise_intercept` — the intercept of each line of a convex investment cost curve" + dims: [nodes, techs, costs, pieces] + +variables: + piecewise_cost_investment: + description: "`piecewise_cost_investment` — an investment cost that grows faster the more capacity is built" + dims: [nodes, techs, costs] + where: >- + count(cost_flow_cap_piecewise_slopes, over=pieces) >= 1 + AND count(cost_flow_cap_piecewise_intercept, over=pieces) >= 1 AND purchased_units + bounds: { lower: 0 } + absence: zero + +given: + variables: + flow_cap: { dims: [nodes, techs, carriers] } + purchased_units: { dims: [nodes, techs] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: piecewise_cost_investment_term } + +expressions: + piecewise_cost_investment_term: + description: "`+ piecewise_cost_investment` — the term Calliope writes into `cost_investment`, by restating it whole" + expression: piecewise_cost_investment + +constraints: + piecewise_costs: + description: "`piecewise_costs` — the investment cost is at least every line of the curve, so at least the curve" + dims: [nodes, techs, costs, pieces] + where: piecewise_cost_investment + expression: >- + piecewise_cost_investment >= sum(cost_flow_cap_piecewise_slopes * flow_cap, over=carriers) + + cost_flow_cap_piecewise_intercept * purchased_units diff --git a/examples/calliope/extensions/piecewise_linear_efficiency.yaml b/examples/calliope/extensions/piecewise_linear_efficiency.yaml new file mode 100644 index 00000000..261ef5b1 --- /dev/null +++ b/examples/calliope/extensions/piecewise_linear_efficiency.yaml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + pieces: + description: Calliope's `pieces` — the lines a piecewise curve is the upper envelope of + dtype: int + +parameters: + flow_eff_piecewise_slopes: + description: "`flow_eff_piecewise_slopes` — the slope of each line of a convex inflow curve" + dims: [nodes, techs, pieces] + flow_eff_piecewise_intercept: + description: "`flow_eff_piecewise_intercept` — the intercept of each line of a convex inflow curve" + dims: [nodes, techs, pieces] + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + available_flow_cap: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + piecewise_efficiency: + description: >- + `piecewise_efficiency` — inflow is at least every line of the curve of + outflow, so at least the curve. Calliope's `where: available_flow_cap` + over a technology reads as the technology having it for some carrier + dims: [nodes, techs, timesteps, pieces] + where: flow_eff_piecewise_slopes AND flow_eff_piecewise_intercept AND count(available_flow_cap, over=carriers) >= 1 + expression: >- + sum(flow_in, over=carriers) >= flow_eff_piecewise_slopes * sum(flow_out, over=carriers) + + flow_eff_piecewise_intercept * sum(available_flow_cap, over=carriers) diff --git a/examples/calliope/extensions/share_all_timesteps.yaml b/examples/calliope/extensions/share_all_timesteps.yaml new file mode 100644 index 00000000..56786060 --- /dev/null +++ b/examples/calliope/extensions/share_all_timesteps.yaml @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + demand_share_tech: + description: >- + `demand_share_tech` — the demand technology whose inflow a technology + meets a share of. Calliope slices `flow_in` by it; a read through the + relation is that slice + key: techs + values: { demand: techs } + supply_share_carrier: + description: >- + `supply_share_carrier` — the carrier a technology's share of outflow is + counted in. Calliope slices `flow_out` by it, which is a test of the + pair + key: [techs, carriers] + +parameters: + demand_share_equals: + description: "`demand_share_equals` — the share of a demand technology's inflow a technology meets; given only where set" + dims: [nodes, techs] + supply_share_equals: + description: "`supply_share_equals` — the share of a node's outflow of a carrier a technology puts out; given only where set" + dims: [nodes, techs] + +expressions: + supply_share_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of its share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: flow_out + otherwise: 0 + supply_share_all_flow_out: + description: "`sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: sum(flow_out, over=techs) + otherwise: 0 + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_equals_per_tech: + description: "`demand_share_equals_per_tech` — a technology puts out its share of a demand technology's inflow over the whole time" + dims: [nodes, techs] + where: demand_share_equals + expression: >- + sum(flow_out, over=[timesteps, carriers]) + == sum(at(flow_in, by=demand_share_tech, over=demand, into=techs), over=[timesteps, carriers]) + * demand_share_equals + supply_share_equals_per_tech: + description: "`supply_share_equals_per_tech` — a technology puts out its share of a node's outflow of a carrier over the whole time" + dims: [nodes, techs] + where: supply_share_equals + expression: >- + sum(supply_share_flow_out, over=[carriers, timesteps]) + == sum(supply_share_all_flow_out, over=[carriers, timesteps]) * supply_share_equals diff --git a/examples/calliope/extensions/share_per_timestep.yaml b/examples/calliope/extensions/share_per_timestep.yaml new file mode 100644 index 00000000..b916826f --- /dev/null +++ b/examples/calliope/extensions/share_per_timestep.yaml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + demand_share_tech: + description: >- + `demand_share_tech` — the demand technology whose inflow a technology + meets a share of. Calliope slices `flow_in` by it; a read through the + relation is that slice + key: techs + values: { demand: techs } + supply_share_carrier: + description: >- + `supply_share_carrier` — the carrier a technology's share of outflow is + counted in. Calliope slices `flow_out` by it, which is a test of the + pair + key: [techs, carriers] + +parameters: + demand_share_per_timestep_equals: + description: "`demand_share_per_timestep_equals` — the share of a demand technology's inflow a technology meets in each time step; given only where set" + dims: [nodes, techs, timesteps] + supply_share_per_timestep_equals: + description: "`supply_share_per_timestep_equals` — the share of a node's outflow of a carrier a technology puts out in each time step; given only where set" + dims: [nodes, techs, timesteps] + +expressions: + supply_share_timestep_flow_out: + description: "`flow_out[carriers=$carrier]` — a technology's outflow of its share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: flow_out + otherwise: 0 + supply_share_timestep_all_flow_out: + description: "`sum(flow_out[carriers=$carrier], over=techs)` — every technology's outflow of a technology's share carrier" + dims: [nodes, techs, carriers, timesteps] + cases: + share_carrier: + when: supply_share_carrier + expression: sum(flow_out, over=techs) + otherwise: 0 + +given: + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_in: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + demand_share_per_timestep_equals_per_tech: + description: "`demand_share_per_timestep_equals_per_tech` — a technology puts out its share of a demand technology's inflow in each time step" + dims: [nodes, techs, timesteps] + where: demand_share_per_timestep_equals + expression: >- + sum(flow_out, over=carriers) + == sum(at(flow_in, by=demand_share_tech, over=demand, into=techs), over=carriers) + * demand_share_per_timestep_equals + supply_share_per_timestep_equals_per_tech: + description: >- + `supply_share_per_timestep_equals_per_tech` — a technology puts out its + share of a node's outflow of a carrier in each time step. Calliope's + row keeps the carrier dimension of the slice; the slice here is summed + over the one carrier it keeps + dims: [nodes, techs, timesteps] + where: supply_share_per_timestep_equals + expression: >- + sum(supply_share_timestep_flow_out, over=carriers) + == sum(supply_share_timestep_all_flow_out, over=carriers) * supply_share_per_timestep_equals diff --git a/examples/calliope/extensions/sos2_piecewise_linear_costs.yaml b/examples/calliope/extensions/sos2_piecewise_linear_costs.yaml new file mode 100644 index 00000000..65bae56d --- /dev/null +++ b/examples/calliope/extensions/sos2_piecewise_linear_costs.yaml @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + breakpoints: + description: Calliope's `breakpoints` — the corners of a piecewise-linear curve, in order + dtype: int + +parameters: + piecewise_cost_investment_x: + description: "`piecewise_cost_investment_x` — the flow capacity at each breakpoint" + dims: [techs, breakpoints] + piecewise_cost_investment_y: + description: "`piecewise_cost_investment_y` — the investment cost at each breakpoint" + dims: [techs, costs, breakpoints] + +variables: + piecewise_cost_investment: + description: "`piecewise_cost_investment` — an investment cost that grows more slowly the more capacity is built" + dims: [nodes, techs, carriers, costs] + where: count(piecewise_cost_investment_x, over=breakpoints) >= 1 AND count(piecewise_cost_investment_y, over=breakpoints) >= 1 + bounds: { lower: 0 } + absence: zero + piecewise_flow_cap: + description: >- + the flow capacity, where the technology has a cost curve. A + `piecewise:` block takes no `where:`, so its link rows would pin + `flow_cap` to the curve at every technology; a link over this copy is + built only where the copy is + dims: [nodes, techs, carriers, costs] + where: count(piecewise_cost_investment_x, over=breakpoints) >= 1 AND count(piecewise_cost_investment_y, over=breakpoints) >= 1 + +constraints: + piecewise_flow_cap_is_flow_cap: + description: the copy of the flow capacity the curve reads is the flow capacity + dims: [nodes, techs, carriers, costs] + where: piecewise_flow_cap + expression: piecewise_flow_cap == flow_cap + +piecewise: + sos2_piecewise_costs: + description: "`sos2_piecewise_costs` — the investment cost lies on the curve through the breakpoints, stated as an SOS2 set" + over: breakpoints + method: sos2 + points: piecewise_cost_investment_x + links: + - [piecewise_flow_cap, piecewise_cost_investment_x] + - [piecewise_cost_investment, piecewise_cost_investment_y] + +expressions: + cost_investment_piecewise: + description: "`sum(piecewise_cost_investment, over=carriers)` — the term Calliope writes into `cost_investment`" + expression: sum(piecewise_cost_investment, over=carriers) + +given: + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_piecewise } diff --git a/examples/calliope/extensions/uptime_downtime_limits.yaml b/examples/calliope/extensions/uptime_downtime_limits.yaml new file mode 100644 index 00000000..c7f9eee4 --- /dev/null +++ b/examples/calliope/extensions/uptime_downtime_limits.yaml @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + capacity_factor_min: + description: "`capacity_factor_min` — the least capacity factor a technology reaches over the whole time" + dims: [nodes, techs] + capacity_factor_max: + description: "`capacity_factor_max` — the most capacity factor a technology reaches over the whole time; given only where set" + dims: [nodes, techs] + uptime_limit: + description: "`uptime_limit` — the most time steps a technology runs in, weighted; given only where set" + dims: [nodes, techs] + downtime_periods: + description: "`downtime_periods` — whether a technology is down for maintenance in a time step" + dims: [nodes, techs, timesteps] + dtype: bool + +expressions: + total_time: + description: "`$total_time` — the hours the modelled time steps stand for" + expression: sum(timestep_resolution * timestep_weights, over=timesteps) + +given: + parameters: + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_cap: { dims: [nodes, techs, carriers] } + operating_units: { dims: [nodes, techs, timesteps] } + +constraints: + annual_capacity_factor_min: + description: "`annual_capacity_factor_min` — a technology's outflow over the whole time is at least its least capacity factor" + dims: [nodes, techs, carriers] + where: carrier_out AND capacity_factor_min + expression: sum(flow_out * timestep_weights, over=timesteps) >= flow_cap * capacity_factor_min * total_time + annual_capacity_factor_max: + description: "`annual_capacity_factor_max` — a technology's outflow over the whole time is at most its most capacity factor" + dims: [nodes, techs, carriers] + where: carrier_out AND capacity_factor_max + expression: sum(flow_out * timestep_weights, over=timesteps) <= flow_cap * capacity_factor_max * total_time + downtime_period: + description: "`downtime_period` — a technology puts out nothing in a time step it is down" + dims: [nodes, techs, timesteps] + where: downtime_periods + expression: sum(flow_out, over=carriers) == 0 + downtime_period_decision: + description: >- + `downtime_period_decision` — a unit-bought technology runs in at most + its limit of time steps. Calliope's `where: operating_units` over a + technology reads as the technology running in whole units at all + dims: [nodes, techs] + where: count(operating_units, over=timesteps) >= 1 AND uptime_limit + expression: sum(operating_units * timestep_weights, over=timesteps) <= uptime_limit diff --git a/examples/calliope/extensions/urban_scale_chp.yaml b/examples/calliope/extensions/urban_scale_chp.yaml new file mode 100644 index 00000000..49a4a769 --- /dev/null +++ b/examples/calliope/extensions/urban_scale_chp.yaml @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +parameters: + heat_to_power_ratio: + description: "`heat_to_power_ratio` — the heat a combined heat and power plant puts out per unit of electricity. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs] + +expressions: + urban_electricity_out: + description: "`flow_out[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out + otherwise: 0 + urban_heat_out: + description: "`flow_out[carriers=heat]`" + dims: [nodes, techs, carriers, timesteps] + cases: + heat: + when: carriers == heat + expression: flow_out + otherwise: 0 + urban_electricity_out_inc_eff: + description: "`flow_out_inc_eff[carriers=electricity]`" + dims: [nodes, techs, carriers, timesteps] + cases: + electricity: + when: carriers == electricity + expression: flow_out_inc_eff + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + variables: + flow_out: { dims: [nodes, techs, carriers, timesteps] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + link_chp_outputs: + description: "`link_chp_outputs` — the technology `chp` puts out heat in a fixed ratio to its electricity" + dims: [nodes, techs, timesteps] + where: techs == chp + expression: sum(urban_electricity_out, over=carriers) * heat_to_power_ratio == sum(urban_heat_out, over=carriers) + balance_conversion_chp: + description: >- + `balance_conversion` for the technology `chp` — it puts out, before + losses, as much electricity as it takes in fuel after them. The patch + keeps the base row off it + dims: [nodes, techs, timesteps] + where: base_tech == 'conversion' AND NOT include_storage AND techs == chp + expression: sum(urban_electricity_out_inc_eff, over=carriers) == sum(flow_in_inc_eff, over=carriers) diff --git a/examples/calliope/feasibility.yaml b/examples/calliope/feasibility.yaml new file mode 100644 index 00000000..2f10a90c --- /dev/null +++ b/examples/calliope/feasibility.yaml @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +variables: + unmet_demand: + description: >- + `unmet_demand` — a source of any carrier at any node, at a high price, + so a model that cannot meet its demand still solves. Calliope builds + it under `config.ensure_feasibility`; here it is this file + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + bounds: { lower: 0 } + absence: zero + unused_supply: + description: "`unused_supply` — a sink of any carrier at any node, at a high price, the counterpart of `unmet_demand`" + dims: [nodes, carriers, timesteps] + where: count(carrier_in, over=techs) >= 1 OR count(carrier_out, over=techs) >= 1 + bounds: { upper: 0 } + absence: zero + +expressions: + feasibility_carrier_flow: unmet_demand + unused_supply + unmet_demand_penalty: + description: "`$unmet_demand` of `min_cost_optimisation` — what unmet demand and unused supply cost" + expression: sum(sum(unmet_demand - unused_supply, over=[carriers, nodes]) * timestep_weights) * bigM + unmet_sum: + description: "`unmet_sum` — net unmet demand; reported" + expression: unmet_demand + unused_supply + +given: + parameters: + carrier_in: { dims: [nodes, techs, carriers], dtype: bool } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + timestep_weights: { dims: [timesteps] } + bigM: { dims: [] } + expressions: + carrier_flow: { dims: [nodes, carriers, timesteps], term: feasibility_carrier_flow } + penalty: { dims: [], term: unmet_demand_penalty } diff --git a/examples/calliope/flows.yaml b/examples/calliope/flows.yaml new file mode 100644 index 00000000..6a7b0456 --- /dev/null +++ b/examples/calliope/flows.yaml @@ -0,0 +1,263 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + link_from: + description: >- + `link_from` — the node a transmission technology links from. Calliope + reads it as `map_dim(nodes, link_from)`, a mask over technology and + node, which is the relation's own row test + key: [techs, nodes] + link_to: + description: >- + `link_to` — the node a transmission technology links to, read as + `link_from` is + key: [techs, nodes] + +parameters: + base_tech: + description: >- + `base_tech` — the abstract class a technology derives from: demand, + supply, conversion, storage or transmission + dims: [techs] + dtype: str + carrier_in: + description: "`carrier_in` — whether a technology consumes a carrier at a node" + dims: [nodes, techs, carriers] + dtype: bool + carrier_out: + description: "`carrier_out` — whether a technology produces a carrier at a node" + dims: [nodes, techs, carriers] + dtype: bool + include_storage: + description: >- + `include_storage` — whether a technology that is not a storage one + carries a store all the same + dims: [nodes, techs] + dtype: bool + one_way: + description: "`one_way` — whether a transmission technology carries flow only from `link_from` to `link_to`" + dims: [techs] + dtype: bool + flow_cap_min: + description: >- + `flow_cap_min` — least flow capacity. Calliope's default is 0; a bound + has a row wherever the variable has one, so data prep fills it + dims: [nodes, techs] + flow_cap_max: + description: >- + `flow_cap_max` — most flow capacity. Calliope's default is `.inf`, + which data prep fills, and a `where` reads as not given + dims: [nodes, techs] + flow_cap_min_systemwide: + description: "`flow_cap_min_systemwide` — least flow capacity of a technology over every node; given only where set" + dims: [techs, carriers] + flow_cap_max_systemwide: + description: "`flow_cap_max_systemwide` — most flow capacity of a technology over every node; given only where set" + dims: [techs, carriers] + flow_out_min_relative: + description: "`flow_out_min_relative` — least outflow, per unit of flow capacity; given only where set" + dims: [nodes, techs, timesteps] + flow_out_eff: + description: "`flow_out_eff` — the share of flow that leaves a technology as outflow. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_in_eff: + description: "`flow_in_eff` — the share of inflow that enters a technology. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_out_parasitic_eff: + description: "`flow_out_parasitic_eff` — what is left after the plant's own use. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_out_eff_per_distance: + description: "`flow_out_eff_per_distance` — the outflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + flow_in_eff_per_distance: + description: "`flow_in_eff_per_distance` — the inflow efficiency of a link per unit of distance. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, carriers, timesteps] + distance: + description: >- + `distance` — the length of a transmission link. Calliope's default is + 1, which data prep fills, where it does not derive one from the + coordinates of the nodes + dims: [techs] + flow_ramping: + description: "`flow_ramping` — the most flow may change in an hour, per unit of flow capacity; given only where set" + dims: [nodes, techs] + cost_flow_cap: + description: "`cost_flow_cap` — the cost of one unit of flow capacity" + dims: [nodes, techs, costs] + cost_flow_cap_per_distance: + description: "`cost_flow_cap_per_distance` — the cost of one unit of flow capacity per unit of link distance" + dims: [nodes, techs, costs] + cost_flow_out: + description: "`cost_flow_out` — the cost of one unit of outflow" + dims: [nodes, techs, costs, timesteps] + cost_flow_in: + description: "`cost_flow_in` — the cost of one unit of inflow" + dims: [nodes, techs, costs, timesteps] + cost_om_annual: + description: "`cost_om_annual` — the annual cost of one unit of flow capacity" + dims: [nodes, techs, costs] + +variables: + flow_cap: + description: "`flow_cap` — the flow capacity of a technology, its nominal or nameplate capacity" + dims: [nodes, techs, carriers] + where: carrier_in OR carrier_out + bounds: { lower: flow_cap_min, upper: flow_cap_max } + absence: zero + flow_out: + description: >- + `flow_out` — the outflow of a technology in a time step. A one-way link + has none at the node it links from + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND NOT (one_way AND link_from) + bounds: { lower: 0 } + absence: zero + flow_in: + description: >- + `flow_in` — the inflow to a technology in a time step. A one-way link + has none at the node it links to + dims: [nodes, techs, carriers, timesteps] + where: carrier_in AND NOT (one_way AND link_to) + bounds: { lower: 0 } + absence: zero + +expressions: + flow_out_inc_eff: + description: "`flow_out_inc_eff` — outflow before the losses on the way out" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: base_tech == 'transmission' + expression: flow_out / (flow_out_eff * flow_out_parasitic_eff * flow_out_eff_per_distance ** distance) + otherwise: flow_out / (flow_out_eff * flow_out_parasitic_eff) + flow_in_inc_eff: + description: "`flow_in_inc_eff` — inflow after the losses on the way in" + dims: [nodes, techs, carriers, timesteps] + cases: + transmission: + when: base_tech == 'transmission' + expression: flow_in * flow_in_eff * flow_in_eff_per_distance ** distance + otherwise: flow_in * flow_in_eff + ramping_flow: + description: >- + `$flow` of `ramping_up` and `ramping_down` — the flow a ramping limit + holds, per hour: outflow, inflow, or their difference where a + technology has both + dims: [nodes, techs, carriers, timesteps] + cases: + out: + when: carrier_out AND NOT carrier_in + expression: flow_out / timestep_resolution + in: + when: carrier_in AND NOT carrier_out + expression: flow_in / timestep_resolution + otherwise: (flow_out - flow_in) / timestep_resolution + cost_flow_cap_sum: + description: >- + `$cost_sum` of `cost_investment_flow_cap` — what one unit of flow + capacity costs; a link's cost is split between its two ends + dims: [nodes, techs, costs] + cases: + transmission: + when: base_tech == 'transmission' + expression: (cost_flow_cap + cost_flow_cap_per_distance * distance) * 0.5 + otherwise: cost_flow_cap + cost_investment_flow_cap: + description: "`cost_investment_flow_cap` — the investment cost of flow capacity" + expression: cost_flow_cap_sum * flow_cap + flows_carrier_flow: sum(flow_out, over=techs) - sum(flow_in, over=techs) + flows_cost_investment: sum(cost_investment_flow_cap, over=carriers) + flows_cost_operation_variable: >- + timestep_weights * (sum(cost_flow_out * flow_out, over=carriers) + sum(cost_flow_in * flow_in, over=carriers)) + flows_cost_operation_fixed: annualisation_weight * sum(cost_om_annual * flow_cap, over=carriers) + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + expressions: + annualisation_weight: + description: the share of a year the modelled time steps stand for + dims: [] + carrier_flow: { dims: [nodes, carriers, timesteps], term: flows_carrier_flow } + cost_investment: { dims: [nodes, techs, costs], term: flows_cost_investment } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: flows_cost_operation_variable } + cost_operation_fixed: { dims: [nodes, techs, costs], term: flows_cost_operation_fixed } + +constraints: + flow_out_max: + description: "`flow_out_max` — outflow is at most the flow capacity over the time step, less the plant's own use" + dims: [nodes, techs, carriers, timesteps] + where: carrier_out + expression: flow_out <= flow_cap * timestep_resolution * flow_out_parasitic_eff + flow_out_min: + description: "`flow_out_min` — outflow is at least its least share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: flow_cap AND flow_out_min_relative + expression: flow_out >= flow_cap * timestep_resolution * flow_out_min_relative + flow_in_max: + description: "`flow_in_max` — inflow is at most the flow capacity over the time step" + dims: [nodes, techs, carriers, timesteps] + where: carrier_in + expression: flow_in <= flow_cap * timestep_resolution + flow_capacity_systemwide_max: + description: "`flow_capacity_systemwide_max` — the flow capacity of a technology over every node is at most its system-wide maximum" + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_max_systemwide + expression: sum(flow_cap, over=nodes) <= flow_cap_max_systemwide + flow_capacity_systemwide_min: + description: "`flow_capacity_systemwide_min` — the flow capacity of a technology over every node is at least its system-wide minimum" + dims: [techs, carriers] + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide + expression: sum(flow_cap, over=nodes) >= flow_cap_min_systemwide + ramping_up: + description: "`ramping_up` — flow rises from one time step to the next by at most its ramping share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: (carrier_in OR carrier_out) AND flow_ramping AND position(timesteps) > 0 + expression: ramping_flow - shift(ramping_flow, along=timesteps, offset=1) <= flow_ramping * flow_cap + ramping_down: + description: "`ramping_down` — flow falls from one time step to the next by at most its ramping share of the flow capacity" + dims: [nodes, techs, carriers, timesteps] + where: (carrier_in OR carrier_out) AND flow_ramping AND position(timesteps) > 0 + expression: -1 * flow_ramping * flow_cap <= ramping_flow - shift(ramping_flow, along=timesteps, offset=1) + +assumptions: + must_have_base: + description: Calliope's `must_have_base` — every technology derives from an abstract class + holds: base_tech + base_tech_one_of: + description: Calliope's `one_of` on `base_tech` + holds: >- + base_tech == 'demand' OR base_tech == 'supply' OR base_tech == 'conversion' + OR base_tech == 'storage' OR base_tech == 'transmission' + distance_only_for_transmission: + description: >- + Calliope's `distance_only_for_transmission` — only a link sets a + distance or a per-distance value. Data prep fills the defaults, so a + technology that is not a link keeps them + holds: >- + distance == 1 AND flow_in_eff_per_distance == 1 + AND flow_out_eff_per_distance == 1 AND NOT cost_flow_cap_per_distance + where: NOT base_tech == 'transmission' + unbounded_flow_cap_cost: + description: Calliope's `unbounded_flow_cap_cost` — a negative flow capacity cost needs a finite maximum + holds: NOT cost_flow_cap < 0 OR flow_cap_max diff --git a/examples/calliope/reporting.yaml b/examples/calliope/reporting.yaml new file mode 100644 index 00000000..a559761f --- /dev/null +++ b/examples/calliope/reporting.yaml @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +given: + parameters: + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + flow_out: { dims: [nodes, techs, carriers, timesteps] } + flow_export: { dims: [nodes, techs, carriers, timesteps] } + expressions: + cost: { dims: [nodes, techs, costs] } + +expressions: + capacity_factor: + description: "`capacity_factor` — the share of its flow capacity a technology puts out in a time step" + expression: flow_out / (flow_cap * timestep_resolution) + systemwide_capacity_factor: + description: "`systemwide_capacity_factor` — the share of its flow capacity a technology puts out over every node and time step" + expression: >- + sum(flow_out * timestep_weights, over=[nodes, timesteps]) + / (sum(flow_cap, over=nodes) * sum(timestep_resolution * timestep_weights, over=timesteps)) + total_generation: + description: >- + `total_generation` — outflow over every node and time step. Calliope + weights only the export, as written here + expression: sum(flow_out + flow_export * timestep_weights, over=[nodes, timesteps]) + systemwide_levelised_cost: + description: "`systemwide_levelised_cost` — a technology's cost per unit of what it generates, over every node" + expression: sum(cost, over=nodes) / total_generation + total_levelised_cost: + description: "`total_levelised_cost` — the system's cost per unit of a carrier generated" + expression: sum(cost, over=[nodes, techs]) / sum(total_generation, over=techs) diff --git a/examples/calliope/settings.yaml b/examples/calliope/settings.yaml new file mode 100644 index 00000000..8da8adde --- /dev/null +++ b/examples/calliope/settings.yaml @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + timestep_resolution: + description: >- + `timestep_resolution` — hours a time step lasts. Calliope's default is + 1, and data prep fills it + dims: [timesteps] + timestep_weights: + description: >- + `timestep_weights` — how many times a time step counts, as after + clustering. Calliope's default is 1, and data prep fills it + dims: [timesteps] + objective_cost_weights: + description: >- + `objective_cost_weights` — what one unit of a cost class weighs in the + objective. Calliope's default is 1, and data prep fills it + dims: [costs] + bigM: + description: >- + `bigM` — a number larger than any decision can take. Calliope's + default is 1e6, and data prep fills it + dims: [] + +expressions: + system_cost: + description: >- + the weighted cost of the system, over every cost class — Calliope's + `min_cost_optimisation` less its unmet-demand penalty. The cost file and + every file that prices something outside a technology add to it + dims: [] + empty: true + penalty: + description: >- + what the objective adds to the system cost to keep a model feasible — + Calliope's `$unmet_demand` sub-expression. It is zero, and a file that + keeps a model feasible adds to it + dims: [] + expression: "0" + +objective: + description: >- + `min_cost_optimisation` — the weighted cost of installing and operating + every technology, plus the penalty on unmet demand + sense: minimize + expression: system_cost + penalty diff --git a/examples/calliope/storage.yaml b/examples/calliope/storage.yaml new file mode 100644 index 00000000..4c45271d --- /dev/null +++ b/examples/calliope/storage.yaml @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +relations: + lookup_cluster_last_timestep: + description: >- + `lookup_cluster_last_timestep` — the last time step of the cluster a + time step stands for, at the first time step of each clustered day + key: timesteps + values: { last: timesteps } + +parameters: + storage_cap_min: + description: "`storage_cap_min` — least storage capacity. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + storage_cap_max: + description: "`storage_cap_max` — most storage capacity. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + storage_discharge_depth: + description: "`storage_discharge_depth` — the least a store holds, as a share of its capacity" + dims: [nodes, techs, timesteps] + storage_initial: + description: "`storage_initial` — what a store holds at the start, as a share of its capacity; given only where set" + dims: [nodes, techs] + storage_loss: + description: "`storage_loss` — the share of what a store holds that it loses in an hour" + dims: [nodes, techs, timesteps] + cyclic_storage: + description: >- + `cyclic_storage` — whether a store ends where it starts. Calliope's + default is true, and data prep fills it + dims: [nodes, techs] + dtype: bool + cluster_first_timestep: + description: "`cluster_first_timestep` — whether a time step is the first of its clustered day" + dims: [timesteps] + dtype: bool + flow_cap_per_storage_cap_min: + description: "`flow_cap_per_storage_cap_min` — least flow capacity per unit of storage capacity; given only where set" + dims: [nodes, techs] + flow_cap_per_storage_cap_max: + description: "`flow_cap_per_storage_cap_max` — most flow capacity per unit of storage capacity; given only where set" + dims: [nodes, techs] + cost_storage_cap: + description: "`cost_storage_cap` — the cost of one unit of storage capacity" + dims: [nodes, techs, costs] + +variables: + storage_cap: + description: "`storage_cap` — the most a technology can store" + dims: [nodes, techs] + where: include_storage OR base_tech == 'storage' + bounds: { lower: storage_cap_min, upper: storage_cap_max } + absence: zero + storage: + description: "`storage` — what a technology holds at the end of a time step" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + bounds: { lower: 0 } + absence: zero + +expressions: + storage_previous_step: + description: >- + `$storage_previous_step` — what a store carries into a time step: + its initial fill at the first step of a store that is not cyclic, what + is left of the last step of its clustered day at the first step of a + cluster, and what is left of the step before everywhere else + dims: [nodes, techs, timesteps] + cases: + initial: + when: position(timesteps) == 0 AND NOT cyclic_storage + expression: storage_initial * storage_cap + cluster_start: + when: cluster_first_timestep AND NOT (position(timesteps) == 0 AND NOT cyclic_storage) + expression: >- + (1 - storage_loss) ** at(timestep_resolution, by=lookup_cluster_last_timestep, over=last, into=timesteps) + * at(storage, by=lookup_cluster_last_timestep, over=last, into=timesteps) + otherwise: >- + (1 - storage_loss) ** shift(timestep_resolution, along=timesteps, offset=1, edge='wrap') + * shift(storage, along=timesteps, offset=1, edge='wrap') + cost_investment_storage_cap: + description: "`cost_investment_storage_cap` — the investment cost of storage capacity" + expression: cost_storage_cap * storage_cap + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + include_storage: { dims: [nodes, techs], dtype: bool } + timestep_resolution: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_storage_cap } + +constraints: + flow_capacity_per_storage_capacity_min: + description: "`flow_capacity_per_storage_capacity_min` — flow capacity is at least its least share of storage capacity" + dims: [nodes, techs, carriers] + where: flow_cap AND storage_cap AND flow_cap_per_storage_cap_min + expression: flow_cap >= storage_cap * flow_cap_per_storage_cap_min + flow_capacity_per_storage_capacity_max: + description: "`flow_capacity_per_storage_capacity_max` — flow capacity is at most its most share of storage capacity" + dims: [nodes, techs, carriers] + where: flow_cap AND storage_cap AND flow_cap_per_storage_cap_max + expression: flow_cap <= storage_cap * flow_cap_per_storage_cap_max + storage_max: + description: "`storage_max` — a store holds at most its capacity" + dims: [nodes, techs, timesteps] + where: storage + expression: storage <= storage_cap + storage_discharge_depth_limit: + description: "`storage_discharge_depth_limit` — a store holds at least its depth of discharge" + dims: [nodes, techs, timesteps] + where: storage AND storage_discharge_depth + expression: storage - storage_discharge_depth * storage_cap >= 0 + balance_storage: + description: >- + `balance_storage` — what a store holds at the end of a time step is + what it carried in, less what it put out before losses, plus what it + took in after them + dims: [nodes, techs, timesteps] + where: (include_storage OR base_tech == 'storage') AND NOT (base_tech == 'supply' OR base_tech == 'demand') + expression: >- + storage == storage_previous_step + - sum(flow_out_inc_eff, over=carriers) + sum(flow_in_inc_eff, over=carriers) + set_storage_initial: + description: >- + `set_storage_initial` — a cyclic store with an initial fill holds it + at the end, after the last step's loss. Calliope builds one row per + store and reads the last step; this builds that row at the last step + dims: [nodes, techs, timesteps] + where: position(timesteps) == -1 AND storage AND storage_initial AND cyclic_storage + expression: storage * (1 - storage_loss) ** timestep_resolution == storage_initial * storage_cap + +assumptions: + unbounded_storage_cap_cost: + description: Calliope's `unbounded_storage_cap_cost` — a negative storage capacity cost needs a finite maximum + holds: NOT cost_storage_cap < 0 OR storage_cap_max + storage_initial_max: + description: Calliope's `storage_initial_max` — the initial fill is a share + holds: storage_initial >= 0 AND storage_initial <= 1 + where: storage_initial + cyclic_storage_needs_inter_cluster: + description: >- + Calliope's `cyclic_storage_needs_inter_cluster` — a cyclic store under + clustering needs the inter-cluster patch + holds: NOT (cyclic_storage AND lookup_cluster_last_timestep) diff --git a/examples/calliope/supply.yaml b/examples/calliope/supply.yaml new file mode 100644 index 00000000..2b4fe912 --- /dev/null +++ b/examples/calliope/supply.yaml @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + costs: + description: Calliope's `costs` — cost classes, such as monetary and CO2 + +parameters: + source_eff: + description: "`source_eff` — the share of the source a supply technology takes in. Calliope's default is 1, and data prep fills it" + dims: [nodes, techs, timesteps] + source_use_min: + description: "`source_use_min` — least source use in a time step, per unit of `source_unit`" + dims: [nodes, techs, timesteps] + source_use_max: + description: "`source_use_max` — most source use in a time step, per unit of `source_unit`; given only where set" + dims: [nodes, techs, timesteps] + source_use_equals: + description: "`source_use_equals` — the source use required in a time step, per unit of `source_unit`; given only where set" + dims: [nodes, techs, timesteps] + source_unit: + description: >- + `source_unit` — what the source is per: `absolute`, `per_area` of + area use, or `per_cap` of flow capacity. Calliope's default is + `absolute`, which is what a technology with no row reads as + dims: [nodes, techs] + dtype: str + source_cap_min: + description: "`source_cap_min` — least source capacity. Calliope's default is 0, and data prep fills it" + dims: [nodes, techs] + source_cap_max: + description: "`source_cap_max` — most source capacity. Calliope's default is `.inf`, and data prep fills it" + dims: [nodes, techs] + source_cap_equals_flow_cap: + description: "`source_cap_equals_flow_cap` — whether the source capacity equals the flow capacity" + dims: [nodes, techs] + dtype: bool + cost_source_use: + description: "`cost_source_use` — the cost of one unit of source use" + dims: [nodes, techs, costs, timesteps] + cost_source_cap: + description: "`cost_source_cap` — the cost of one unit of source capacity" + dims: [nodes, techs, costs] + +variables: + source_use: + description: "`source_use` — what a supply technology takes in from outside the system in a time step" + dims: [nodes, techs, timesteps] + where: base_tech == 'supply' + bounds: { lower: 0 } + absence: zero + source_cap: + description: "`source_cap` — the most a supply technology can take in from outside the system" + dims: [nodes, techs] + where: base_tech == 'supply' + bounds: { lower: source_cap_min, upper: source_cap_max } + absence: zero + +expressions: + flow_cap_out: + description: "`where(flow_cap, carrier_out)` — the flow capacity of the carriers a technology produces" + dims: [nodes, techs, carriers] + cases: + produced: + when: carrier_out + expression: flow_cap + otherwise: 0 + source_scaler: + description: "`$source_scaler` — what the source parameters are per: area use, flow capacity, or one" + dims: [nodes, techs] + cases: + per_area: + when: source_unit == per_area + expression: area_use + per_cap: + when: source_unit == per_cap + expression: sum(flow_cap_out, over=carriers) + otherwise: 1 + cost_investment_source_cap: + description: "`cost_investment_source_cap` — the investment cost of source capacity" + expression: cost_source_cap * source_cap + supply_cost_operation_variable: timestep_weights * cost_source_use * source_use + curtailment: + description: >- + `curtailment` — the share of the available source a supply technology + leaves unused in a time step; reported + expression: 1 - source_use / (source_use_max * source_scaler) + total_curtailment: + description: "`total_curtailment` — the share of the available source left unused over the whole time; reported" + expression: 1 - sum(source_use, over=timesteps) / sum(source_use_max * source_scaler, over=timesteps) + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + include_storage: { dims: [nodes, techs], dtype: bool } + timestep_resolution: { dims: [timesteps] } + timestep_weights: { dims: [timesteps] } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + area_use: { dims: [nodes, techs] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + cost_investment: { dims: [nodes, techs, costs], term: cost_investment_source_cap } + cost_operation_variable: { dims: [nodes, techs, costs, timesteps], term: supply_cost_operation_variable } + +constraints: + source_max: + description: "`source_max` — source use is at most the source capacity over the time step" + dims: [nodes, techs, timesteps] + where: source_cap + expression: source_use <= timestep_resolution * source_cap + source_capacity_equals_flow_capacity: + description: "`source_capacity_equals_flow_capacity` — a supply technology's source capacity equals its flow capacity, where set" + dims: [nodes, techs, carriers] + where: flow_cap AND source_cap AND source_cap_equals_flow_cap + expression: source_cap == flow_cap + balance_supply_no_storage: + description: "`balance_supply_no_storage` — a supply technology with no store puts out what it takes from its source" + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND base_tech == 'supply' AND NOT include_storage + expression: flow_out_inc_eff == source_use * source_eff + source_availability_supply_equals: + description: "`source_availability_supply` where `source_use_equals` is set — source use is what is available" + dims: [nodes, techs, timesteps] + where: source_use AND source_use_equals + expression: source_use == source_use_equals * source_scaler + source_availability_supply_max: + description: "`source_availability_supply` where only `source_use_max` is set — source use is at most what is available" + dims: [nodes, techs, timesteps] + where: source_use AND NOT source_use_equals AND source_use_max + expression: source_use <= source_use_max * source_scaler + balance_supply_min_use: + description: "`balance_supply_min_use` — source use is at least its least use" + dims: [nodes, techs, timesteps] + where: source_use_min AND NOT source_use_equals AND base_tech == 'supply' + expression: source_use >= source_use_min * source_scaler + +assumptions: + unbounded_source_use_cost: + description: Calliope's `unbounded_source_use_cost` — a negative source capacity cost needs a finite maximum + holds: NOT cost_source_cap < 0 OR source_cap_max + finite_source_use: + description: Calliope's `finite_source_use`, for the source — a required use is finite + holds: NOT source_use_equals == inf + source_unit_one_of: + description: Calliope's `one_of` on `source_unit` + holds: source_unit == absolute OR source_unit == per_area OR source_unit == per_cap + where: source_unit diff --git a/examples/calliope/supply_storage.yaml b/examples/calliope/supply_storage.yaml new file mode 100644 index 00000000..71972b24 --- /dev/null +++ b/examples/calliope/supply_storage.yaml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + source_eff: { dims: [nodes, techs, timesteps] } + variables: + storage: { dims: [nodes, techs, timesteps] } + source_use: { dims: [nodes, techs, timesteps] } + expressions: + storage_previous_step: { dims: [nodes, techs, timesteps] } + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_supply_with_storage: + description: >- + `balance_supply_with_storage` — a supply technology with a store puts + in what it takes from its source and draws out what it puts out + dims: [nodes, techs, carriers, timesteps] + where: carrier_out AND storage AND base_tech == 'supply' + expression: storage == storage_previous_step + source_use * source_eff - flow_out_inc_eff diff --git a/examples/calliope/transmission.yaml b/examples/calliope/transmission.yaml new file mode 100644 index 00000000..d132cffe --- /dev/null +++ b/examples/calliope/transmission.yaml @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + nodes: + description: Calliope's `nodes` — the places technologies stand at + techs: + description: Calliope's `techs` — technologies + carriers: + description: Calliope's `carriers` — energy and commodity carriers + timesteps: + description: Calliope's `timesteps` — time steps, in order + dtype: datetime + +relations: + link_from: + description: >- + `link_from` — the node a transmission technology links from. Calliope + reads it as `map_dim(nodes, link_from)`, a mask over technology and + node, which is the relation's own row test + key: [techs, nodes] + link_to: + description: >- + `link_to` — the node a transmission technology links to, read as + `link_from` is + key: [techs, nodes] + +expressions: + flow_cap_from: + description: "`where(flow_cap, map_dim(nodes, link_from))` — a link's flow capacity at the node it links from" + dims: [nodes, techs, carriers] + cases: + from: + when: link_from + expression: flow_cap + otherwise: 0 + flow_cap_to: + description: "`where(flow_cap, map_dim(nodes, link_to))` — a link's flow capacity at the node it links to" + dims: [nodes, techs, carriers] + cases: + to: + when: link_to + expression: flow_cap + otherwise: 0 + +given: + parameters: + base_tech: { dims: [techs], dtype: str } + carrier_out: { dims: [nodes, techs, carriers], dtype: bool } + variables: + flow_cap: { dims: [nodes, techs, carriers] } + expressions: + flow_out_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + flow_in_inc_eff: { dims: [nodes, techs, carriers, timesteps] } + +constraints: + balance_transmission: + description: "`balance_transmission` — a link puts out at one end, before losses, what it takes in at the other after them" + dims: [techs, timesteps] + where: base_tech == 'transmission' + expression: >- + sum(flow_out_inc_eff, over=[nodes, carriers]) + == sum(flow_in_inc_eff, over=[nodes, carriers]) + symmetric_transmission: + description: "`symmetric_transmission` — a link has the same flow capacity at both ends" + dims: [techs, carriers] + where: count(carrier_out, over=nodes) >= 1 AND base_tech == 'transmission' + expression: sum(flow_cap_from, over=nodes) == sum(flow_cap_to, over=nodes) diff --git a/examples/calliope/variants/chp_htp.yaml b/examples/calliope/variants/chp_htp.yaml new file mode 100644 index 00000000..c81b13d8 --- /dev/null +++ b/examples/calliope/variants/chp_htp.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +constraints: + balance_conversion: + description: >- + `balance_conversion` for a plant with no turbine type — a conversion + technology puts out, before its losses, what it takes in after them. + Extraction and backpressure plants have rows of their own + where: base_tech == 'conversion' AND NOT include_storage AND NOT turbine_type diff --git a/examples/calliope/variants/milp.yaml b/examples/calliope/variants/milp.yaml new file mode 100644 index 00000000..7c806b93 --- /dev/null +++ b/examples/calliope/variants/milp.yaml @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +parameters: + flow_cap_min: + description: >- + `flow_cap_min` — least flow capacity, scaled by the units bought where + a technology buys units; given only where set, as no bound reads it + flow_cap_min_systemwide: + description: >- + `flow_cap_min_systemwide` — least flow capacity of a technology over + every node, scaled by the units bought where it buys units; given only + where set + flow_out_min_relative: + description: >- + `flow_out_min_relative` — least outflow, per unit of flow capacity. For + a continuous technology it holds in every time step; given only where + set + storage_cap_min: + description: "`storage_cap_min` — least storage capacity; given only where set, as no bound reads it" + area_use_min: + description: "`area_use_min` — least area use; given only where set, as no bound reads it" + source_cap_min: + description: "`source_cap_min` — least source capacity; given only where set, as no bound reads it" + +variables: + flow_cap: { bounds: { lower: 0 } } + area_use: { bounds: { lower: 0 } } + source_cap: { bounds: { lower: 0 } } + storage_cap: { bounds: { lower: 0 } } + +constraints: + flow_out_max: + description: "`flow_out_max` — a continuous technology's outflow is at most its flow capacity over the time step" + where: carrier_out AND NOT operating_units + flow_out_min: + description: "`flow_out_min` — a continuous technology's outflow is at least its least share of the flow capacity" + where: flow_cap AND flow_out_min_relative AND NOT operating_units + flow_in_max: + description: "`flow_in_max` — a continuous technology's inflow is at most its flow capacity over the time step" + where: carrier_in AND NOT operating_units + flow_capacity_systemwide_min: + description: >- + `flow_capacity_systemwide_min` where no unit is bought — the flow + capacity over every node is at least the system-wide minimum + where: count(flow_cap, over=nodes) >= 1 AND flow_cap_min_systemwide AND NOT count(purchased_units, over=nodes) >= 1 diff --git a/examples/calliope/variants/operate.yaml b/examples/calliope/variants/operate.yaml new file mode 100644 index 00000000..52726725 --- /dev/null +++ b/examples/calliope/variants/operate.yaml @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +parameters: + flow_cap: + description: "`flow_cap` — the flow capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs, carriers] + area_use: + description: "`area_use` — the area used, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + source_cap: + description: "`source_cap` — the source capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + storage_cap: + description: "`storage_cap` — the storage capacity, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + +variables: + flow_cap: null + area_use: null + source_cap: null + storage_cap: null + +constraints: + flow_capacity_per_storage_capacity_min: null + flow_capacity_per_storage_capacity_max: null + source_capacity_equals_flow_capacity: null + force_zero_area_use: null + area_use_per_flow_capacity: null + area_use_capacity_per_loc: null + flow_capacity_systemwide_max: null + flow_capacity_systemwide_min: null + symmetric_transmission: null + +expressions: + cost_investment: null + cost_investment_annualised: null + cost_investment_flow_cap: null + cost_investment_storage_cap: null + cost_investment_source_cap: null + cost_investment_area_use: null + cost_operation_fixed: null + cost_flow_cap_sum: null + depreciation_rate: null + flows_cost_investment: null + flows_cost_operation_fixed: null + flow_cap_from: null + flow_cap_to: null + cost: + description: "`cost` — the operating cost of a technology, over every time step" + expression: sum(cost_operation_variable, over=timesteps) + +assumptions: + operate_mode_cyclic_storage: + description: Calliope's `operate_mode_cyclic_storage` — a store in operate mode is not cyclic + holds: NOT (cyclic_storage AND (base_tech == 'storage' OR include_storage)) diff --git a/examples/calliope/variants/operate_milp.yaml b/examples/calliope/variants/operate_milp.yaml new file mode 100644 index 00000000..78cc8bae --- /dev/null +++ b/examples/calliope/variants/operate_milp.yaml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +parameters: + purchased_units: + description: "`purchased_units` — the units bought, fixed in operate mode. Calliope's default is `.inf`" + dims: [nodes, techs] + +variables: + purchased_units: null + +constraints: + storage_capacity_units_milp: null + flow_capacity_units_milp: null + unit_capacity_max_systemwide_milp: null + unit_capacity_min_systemwide_milp: null + flow_capacity_max_purchase_milp: null + flow_capacity_max_purchase_milp_big_m: null + storage_capacity_max_purchase_milp: null + flow_capacity_minimum: null + flow_capacity_minimum_purchased: null + storage_capacity_minimum: null + storage_capacity_minimum_purchased: null + area_use_minimum: null + area_use_minimum_purchased: null + source_capacity_minimum: null + source_capacity_minimum_purchased: null + flow_capacity_systemwide_min_purchased: null + +expressions: + cost_investment_purchase: null diff --git a/examples/calliope/variants/spores.yaml b/examples/calliope/variants/spores.yaml new file mode 100644 index 00000000..82a19ce1 --- /dev/null +++ b/examples/calliope/variants/spores.yaml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +parameters: + spores_baseline_cost: + description: >- + `spores_baseline_cost` — the least cost of the system, which a SPORES + iteration may exceed by its slack. Calliope's default is `.inf` + dims: [] + spores_slack: + description: "`spores_slack` — the share by which a SPORES iteration may exceed the least cost" + dims: [] + spores_score: + description: "`spores_score` — the score a technology at a node carries from the SPORES iterations before" + dims: [nodes, techs, carriers] + +constraints: + total_system_cost_max: + description: >- + `total_system_cost_max` — the cost the least-cost objective reads is at + most the least cost plus the slack. It reads the same sums the + objective did, so a file that adds a cost adds it here too + dims: [] + expression: system_cost + penalty <= spores_baseline_cost * (1 + spores_slack) + +expressions: + spores_score_cumulative: + description: "`spores_score_cumulative` — the SPORES score, reported with the results" + expression: spores_score + spores_baseline_cost_tracked: + description: "`spores_baseline_cost_tracked` — the SPORES baseline cost, reported with the results" + expression: spores_baseline_cost + +objective: + description: >- + `min_spores` — the SPORES score of the flow capacity built, plus the + penalty on unmet demand + expression: sum(flow_cap * spores_score) + penalty diff --git a/examples/calliope/variants/storage_inter_cluster.yaml b/examples/calliope/variants/storage_inter_cluster.yaml new file mode 100644 index 00000000..e63e1314 --- /dev/null +++ b/examples/calliope/variants/storage_inter_cluster.yaml @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +dimensions: + clusters: + description: Calliope's `clusters` — the representative days a clustered time series is made of + dtype: int + datesteps: + description: Calliope's `datesteps` — the days of the whole time series, in order + dtype: datetime + +relations: + timestep_cluster: + description: "`timestep_cluster` — the cluster a time step belongs to" + key: timesteps + values: clusters + lookup_datestep_cluster: + description: "`lookup_datestep_cluster` — the cluster a day stands for" + key: datesteps + values: clusters + lookup_datestep_last_cluster_timestep: + description: "`lookup_datestep_last_cluster_timestep` — the last time step of the cluster a day stands for" + key: datesteps + values: timesteps + +parameters: + storage_loss: + description: >- + `storage_loss` — the share of what a store holds that it loses in an + hour. Between days it is raised to 24, so it does not vary over time + steps here + dims: [nodes, techs] + +variables: + storage: + description: >- + `storage` — what a store holds within a clustered day, relative to what + it carries between days. It may go below zero, as long as the sum does + not + bounds: { lower: null } + storage_inter_cluster: + description: "`storage_inter_cluster` — what a store carries from one day of the whole time series to the next" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + bounds: { lower: 0 } + absence: zero + storage_intra_cluster_max: + description: "`storage_intra_cluster_max` — the most a store holds within a clustered day" + dims: [nodes, techs, clusters] + where: include_storage OR base_tech == 'storage' + storage_intra_cluster_min: + description: "`storage_intra_cluster_min` — the least a store holds within a clustered day" + dims: [nodes, techs, clusters] + where: include_storage OR base_tech == 'storage' + +expressions: + storage_previous_step: + description: >- + `$storage_previous_step` under inter-cluster storage — what a store + carries into a time step: its initial fill at the first step of a store + that is not cyclic, nothing at the first step of a clustered day, and + what is left of the step before everywhere else + cases: + initial: + when: position(timesteps) == 0 AND NOT cyclic_storage + expression: storage_initial * storage_cap + cluster_start: + when: lookup_cluster_last_timestep AND NOT (position(timesteps) == 0 AND NOT cyclic_storage) + expression: "0" + storage_inter_previous_step: + description: >- + `$storage_previous_step` of `balance_storage_inter` — what a store + carries into a day: its initial fill on the first day of a store that + is not cyclic, and what is left of the day before everywhere else. + Calliope reads the initial fill as a share, not times the capacity, as + here + dims: [nodes, techs, datesteps] + cases: + initial: + when: position(datesteps) == 0 AND NOT cyclic_storage + expression: storage_initial + otherwise: (1 - storage_loss) ** 24 * shift(storage_inter_cluster, along=datesteps, offset=1, edge='wrap') + storage_intra: + description: >- + `$storage_intra` of `balance_storage_inter` — what the clustered day of + the day before left at its last step, and nothing on the first day of + a store that is not cyclic + dims: [nodes, techs, datesteps] + cases: + initial: + when: position(datesteps) == 0 AND NOT cyclic_storage + expression: "0" + otherwise: >- + shift(at(storage, by=lookup_datestep_last_cluster_timestep, over=timesteps, into=datesteps), + along=datesteps, offset=1, edge='wrap') + +constraints: + storage_max: null + set_storage_initial: + description: >- + `set_storage_initial` under inter-cluster storage — a cyclic store with + an initial fill carries it between days at the end, after a day's loss + dims: [nodes, techs, datesteps] + where: position(datesteps) == -1 AND storage_inter_cluster AND storage_initial AND cyclic_storage + expression: storage_inter_cluster * (1 - storage_loss) ** 24 == storage_initial * storage_cap + storage_intra_max: + description: "`storage_intra_max` — a store holds at most its most within its clustered day" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + expression: storage <= at(storage_intra_cluster_max, by=timestep_cluster, over=clusters, into=timesteps) + storage_intra_min: + description: "`storage_intra_min` — a store holds at least its least within its clustered day" + dims: [nodes, techs, timesteps] + where: include_storage OR base_tech == 'storage' + expression: storage >= at(storage_intra_cluster_min, by=timestep_cluster, over=clusters, into=timesteps) + storage_inter_max: + description: "`storage_inter_max` — what a store carries between days plus the most of its day is at most its capacity" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: >- + storage_inter_cluster + at(storage_intra_cluster_max, by=lookup_datestep_cluster, over=clusters, into=datesteps) + <= storage_cap + storage_inter_min: + description: "`storage_inter_min` — what a store carries between days, after a day's loss, plus the least of its day is not below zero" + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: >- + storage_inter_cluster * (1 - storage_loss) ** 24 + + at(storage_intra_cluster_min, by=lookup_datestep_cluster, over=clusters, into=datesteps) >= 0 + balance_storage_inter: + description: >- + `balance_storage_inter` — what a store carries into a day is what it + carried into the day before, after a day's loss, plus what that day's + cluster left + dims: [nodes, techs, datesteps] + where: include_storage OR base_tech == 'storage' + expression: storage_inter_cluster == storage_inter_previous_step + storage_intra + +assumptions: + cyclic_storage_needs_inter_cluster: null diff --git a/examples/calliope/variants/urban_scale_chp.yaml b/examples/calliope/variants/urban_scale_chp.yaml new file mode 100644 index 00000000..b96bcafd --- /dev/null +++ b/examples/calliope/variants/urban_scale_chp.yaml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Calliope contributors +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Ported from the math of Calliope v0.7.0, https://github.com/calliope-project/calliope, +# and changed to mathspec's language. docs/examples/calliope/port.md records each change. + +constraints: + balance_conversion: + description: >- + `balance_conversion` for every conversion technology but `chp` — it + puts out, before its losses, what it takes in after them + where: base_tech == 'conversion' AND NOT include_storage AND NOT techs == chp diff --git a/examples/symbols/calliope.yaml b/examples/symbols/calliope.yaml new file mode 100644 index 00000000..319bb3e5 --- /dev/null +++ b/examples/symbols/calliope.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +# How `examples/calliope/` prints. The names are Calliope's and print as +# themselves, so a row reads beside Calliope's own math; only the dimensions +# take a letter, as Calliope's iterators take a word. +notation: latex +dimensions: + nodes: { index: n, set: '\mathcal{N}' } + techs: { index: i, set: '\mathcal{I}' } + carriers: { index: c, set: '\mathcal{C}' } + costs: { index: k, set: '\mathcal{K}' } + timesteps: { index: t, set: '\mathcal{T}' } + clusters: { index: l, set: '\mathcal{L}' } + datesteps: { index: d, set: '\mathcal{D}' } + months: { index: m, set: '\mathcal{M}' } + pieces: { index: p, set: '\mathcal{P}' } + breakpoints: { index: b, set: '\mathcal{B}' } +names: {} diff --git a/mkdocs.yml b/mkdocs.yml index 253a94a4..7d442464 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,7 +81,7 @@ nav: # writes a tool against `Spec` and `Program` (an engine such as specsolve, a # renderer, a checker), whoever changes mathspec itself, and the proofs of # concept: the notation page, which renders the typesetting test spec, and - # the PyPSA pages. The PyPSA pages stay in `docs/examples/`, where + # the PyPSA and Calliope pages. Those stay in `docs/examples/`, where # `tools/gallery.py` writes them. - Development: - Building on mathspec: @@ -121,6 +121,47 @@ nav: - Lines: examples/pypsa/line.md - Transformers: examples/pypsa/transformer.md - Loads: examples/pypsa/load.md + - Calliope in fragments: + - examples/calliope/index.md + - The port record: examples/calliope/port.md + - Settings: examples/calliope/settings.md + - The balance: examples/calliope/balance.md + - Flows: examples/calliope/flows.md + - The cost: examples/calliope/cost.md + - Supply: examples/calliope/supply.md + - Demand: examples/calliope/demand.md + - Conversion: examples/calliope/conversion.md + - Storage: examples/calliope/storage.md + - Supply with storage: examples/calliope/supply_storage.md + - Area: examples/calliope/area.md + - Transmission: examples/calliope/transmission.md + - Export: examples/calliope/export.md + - Feasibility: examples/calliope/feasibility.md + - Reporting: examples/calliope/reporting.md + - Extensions: + - MILP: examples/calliope/extensions/milp.md + - Annual energy balance: examples/calliope/extensions/annual_energy_balance.md + - CHP plants: examples/calliope/extensions/chp_htp.md + - Demand share as a decision: examples/calliope/extensions/demand_share_per_timestep_decision.md + - Fuel distribution: examples/calliope/extensions/fuel_dist.md + - Time-varying flow limit: examples/calliope/extensions/max_time_varying.md + - Monthly peak flow charge: examples/calliope/extensions/monthly_peak_flow_charge.md + - Net import share: examples/calliope/extensions/net_import_share.md + - Piecewise linear costs: examples/calliope/extensions/piecewise_linear_costs.md + - Piecewise linear efficiency: examples/calliope/extensions/piecewise_linear_efficiency.md + - Flow share over the whole time: examples/calliope/extensions/share_all_timesteps.md + - Flow share per time step: examples/calliope/extensions/share_per_timestep.md + - Piecewise linear costs with SOS2: examples/calliope/extensions/sos2_piecewise_linear_costs.md + - Uptime and downtime limits: examples/calliope/extensions/uptime_downtime_limits.md + - Urban-scale CHP: examples/calliope/extensions/urban_scale_chp.md + - Variants: + - The MILP patch: examples/calliope/variants/milp.md + - Operate mode: examples/calliope/variants/operate.md + - Operate mode, with MILP: examples/calliope/variants/operate_milp.md + - SPORES: examples/calliope/variants/spores.md + - Inter-cluster storage: examples/calliope/variants/storage_inter_cluster.md + - The CHP patch: examples/calliope/variants/chp_htp.md + - The urban-scale CHP patch: examples/calliope/variants/urban_scale_chp.md theme: palette: diff --git a/tests/test_calliope_example.py b/tests/test_calliope_example.py new file mode 100644 index 00000000..db851106 --- /dev/null +++ b/tests/test_calliope_example.py @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: mathspec Contributors +# +# SPDX-License-Identifier: MIT + +"""The Calliope port under `examples/calliope/`, held to what its pages claim. + +The gallery test holds each page to its generator. What is left for here is +what the index and the port record state: every fragment stands alone, the +base and every extension compose with nothing left to provide, every variant +lands on the composition its page names, and a sum Calliope restates to add a +term gains the term instead. +""" + +from __future__ import annotations + +import re + +import pytest + +from mathspec import LanguageError, advice, merge, override, to_markdown, to_spec +from tests.fixtures import EXAMPLES +from tools import gallery + +CALLIOPE = EXAMPLES / 'calliope' +BASE = sorted(CALLIOPE.glob('*.yaml')) +EXTENSIONS = {path.stem: path for path in sorted((CALLIOPE / 'extensions').glob('*.yaml'))} +VARIANTS = {path.stem: path for path in sorted((CALLIOPE / 'variants').glob('*.yaml'))} + +#: The extensions that read the units the MILP fragment builds. +NEEDS_MILP = {'piecewise_linear_costs', 'piecewise_linear_efficiency', 'uptime_downtime_limits'} + +#: The extensions whose new rows replace a base row a variant of the same name narrows. +REWRITES_A_BASE_ROW = {'chp_htp', 'urban_scale_chp'} + + +def _composed(*extensions: str): + """The base, the MILP pair where an extension needs it, the extensions, and the variants of their rewrites.""" + milp = 'milp' in extensions or NEEDS_MILP & set(extensions) + names = [*(['milp'] if milp and 'milp' not in extensions else []), *extensions] + patches = [VARIANTS['milp']] if milp else [] + patches += [VARIANTS[name] for name in names if name in REWRITES_A_BASE_ROW] + merged = merge([*BASE, *(EXTENSIONS[name] for name in names)]) + return override(merged, patches) if patches else merged + + +@pytest.mark.parametrize('path', [*BASE, *EXTENSIONS.values()], ids=lambda path: path.stem) +def test_every_fragment_loads_and_prints_on_its_own(path): + assert to_markdown(to_spec(path)), f'{path.stem} rendered nothing' + + +def test_the_base_composes_with_nothing_left_to_provide(): + spec = merge(BASE) + assert not spec.given, 'every name a base fragment reads is one another base fragment declares' + assert not advice(spec), 'the composed base has no dimension out of use, no column to provide, no open variable' + + +@pytest.mark.parametrize('name', sorted(EXTENSIONS)) +def test_every_extension_composes_onto_the_base(name): + spec = _composed(name) + assert not spec.given, f'{name} reads only what the base, and the MILP fragment where it needs it, declare' + assert not advice(spec), f'{name} composed leaves nothing to advise on' + + +@pytest.mark.parametrize('name', sorted(VARIANTS)) +def test_every_variant_is_a_patch_rather_than_a_spec(name): + with pytest.raises(LanguageError): + to_spec(VARIANTS[name]) + + +@pytest.mark.parametrize('page', sorted(gallery.CALLIOPE_VARIANTS)) +def test_every_variant_lands_on_the_composition_its_page_names(page): + fragments, before = gallery.CALLIOPE_VARIANTS[page] + name = page.removeprefix('calliope/variants/').removesuffix('.md') + spec = override(merge(fragments), [*(VARIANTS[earlier] for earlier in before), VARIANTS[name]]) + assert not spec.given and not advice(spec), f'{name} leaves a whole spec with nothing to advise on' + + +def test_every_variant_has_a_page(): + paged = {page.removeprefix('calliope/variants/').removesuffix('.md') for page in gallery.CALLIOPE_VARIANTS} + assert paged == set(VARIANTS), 'a variant with no page is a patch nobody can read as math' + + +def test_the_whole_port_composes_in_one_spec(): + """Every extension but the two that Calliope also offers as alternatives, with every mode but operate.""" + alternatives = {'sos2_piecewise_linear_costs', 'urban_scale_chp'} + merged = merge([*BASE, *(path for name, path in EXTENSIONS.items() if name not in alternatives)]) + patches = ['milp', 'chp_htp', 'spores', 'storage_inter_cluster'] + spec = override(merged, [VARIANTS[name] for name in patches]) + assert not spec.given and not advice(spec), 'the port is one spec once composed' + + +def test_the_two_piecewise_costs_are_alternatives(): + with pytest.raises(LanguageError, match=r"both declare the variable 'piecewise_cost_investment'"): + merge( + [*BASE, EXTENSIONS['milp'], EXTENSIONS['piecewise_linear_costs'], EXTENSIONS['sos2_piecewise_linear_costs']] + ) + + +@pytest.mark.parametrize( + ('name', 'hub', 'term'), + [ + pytest.param('fuel_dist', 'carrier_flow', 'fuel_dist_carrier_flow', id='fuel-in-the-balance'), + pytest.param('fuel_dist', 'system_cost', 'fuel_dist_system_cost', id='fuel-in-the-objective'), + pytest.param('monthly_peak_flow_charge', 'cost_operation_fixed', 'cost_month_peak_charge', id='peak-charge'), + pytest.param('milp', 'cost_investment', 'cost_investment_purchase', id='purchase-cost'), + pytest.param( + 'piecewise_linear_costs', 'cost_investment', 'piecewise_cost_investment_term', id='piecewise-cost' + ), + ], +) +def test_a_sum_calliope_restates_gains_a_term_instead(name, hub, term): + """Calliope restates the whole block to add one term; here the base file stays as it is.""" + spec = _composed(name) + assert re.search(rf'\b{term}\b', spec.expressions[hub].expression), f'{hub} carries the term {name} adds' + + +def test_spores_caps_the_sums_the_objective_read(): + """Calliope restates its objective in the cap, and misses a cost an example adds to it; the cap reads the sums.""" + spec = override(_composed('fuel_dist'), [VARIANTS['spores']]) + cap = spec.constraints['total_system_cost_max'].expression + assert 'system_cost' in cap and 'penalty' in cap, 'the cap reads the two sums the least-cost objective read' + assert 'fuel_dist_system_cost' in spec.expressions['system_cost'].expression, 'so fuel distribution is capped too' + + +def test_operate_mode_turns_every_capacity_into_data(): + spec = override(_composed('milp'), [VARIANTS[name] for name in ('milp', 'operate', 'operate_milp')]) + capacities = {'flow_cap', 'area_use', 'source_cap', 'storage_cap', 'purchased_units'} + assert capacities <= set(spec.parameters), 'each capacity is a parameter of the same name' + assert not capacities & set(spec.variables), 'and no longer a decision' + + +def test_the_sos2_curve_links_a_copy_of_the_flow_capacity_masked_to_the_curve(): + """A `piecewise:` block has no `where:`, and a link over `flow_cap` pinned every technology with no curve to zero. + + The expanded link row `flow_cap == sum(lam * x)` is built at every + coordinate of the frame. Where a technology has no breakpoints its weights + do not exist, so the row reads `flow_cap == 0`. The link reads a copy of + `flow_cap` that exists only where the curve does, and the row goes with it. + """ + spec = to_spec(EXTENSIONS['sos2_piecewise_linear_costs']).expand() + link = spec.constraints['sos2_piecewise_costs_link0'].expression + assert link.startswith('(piecewise_flow_cap)'), 'the curve pins the masked copy, not the flow capacity itself' + assert 'piecewise_cost_investment_x' in spec.variables['piecewise_flow_cap'].where, ( + 'the copy exists only where the technology has breakpoints' + ) diff --git a/tools/gallery.py b/tools/gallery.py index 67d2df05..84e35309 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -17,6 +17,7 @@ import re import textwrap from functools import partial +from pathlib import Path from typing import TYPE_CHECKING, Any import yaml @@ -30,8 +31,6 @@ from tools.spec_math import OPERATORS, PROBES, _section, rendered_probe if TYPE_CHECKING: - from pathlib import Path - from mathspec.spec import Spec PAGES = ROOT / 'docs' / 'examples' @@ -43,6 +42,13 @@ #: PyPSA split prints in the one file's table, cut the same way. LIBRARY_SYMBOLS = ROOT / 'examples' / 'symbols' / 'library.yaml' PYPSA_SYMBOLS = ROOT / 'examples' / 'symbols' / 'pypsa.yaml' +#: Calliope's math: its base as one fragment per topic, each file beyond the +#: base that adds to it, and the patches its modes and its rewrites of a base +#: row are written as. Every page prints in one table, cut as above. +CALLIOPE = ROOT / 'examples' / 'calliope' +CALLIOPE_BASE = sorted(CALLIOPE.glob('*.yaml')) +CALLIOPE_EXTENSIONS = sorted((CALLIOPE / 'extensions').glob('*.yaml')) +CALLIOPE_SYMBOLS = ROOT / 'examples' / 'symbols' / 'calliope.yaml' BEGIN, END = '', '' #: Page -> the spec it shows. One spec per page, because a gallery of @@ -54,10 +60,33 @@ 'library/generator.md': LIBRARY / 'generator.yaml', 'library/load.md': LIBRARY / 'load.yaml', **{f'pypsa/{path.stem}.md': path for path in sorted(PYPSA.glob('*.yaml'))}, + **{f'calliope/{path.stem}.md': path for path in CALLIOPE_BASE}, + **{f'calliope/extensions/{path.stem}.md': path for path in CALLIOPE_EXTENSIONS}, } #: The index of the PyPSA split: its two tables are read off the fragments. SPLIT_INDEX = 'pypsa/index.md' +#: The index of the Calliope port: the same two tables, over the base and every extension. +CALLIOPE_INDEX = 'calliope/index.md' + + +def _calliope(*extensions: str) -> list[Path]: + """The base fragments, then the named extensions, in the order `merge` takes them.""" + return [*CALLIOPE_BASE, *(CALLIOPE / 'extensions' / f'{name}.yaml' for name in extensions)] + + +#: Page -> the fragments a Calliope patch is laid over, and the patches laid +#: before it. A patch is not a spec, so its page prints what it writes, as the +#: spec it lands on prints it. +CALLIOPE_VARIANTS: dict[str, tuple[list[Path], list[str]]] = { + 'calliope/variants/milp.md': (_calliope('milp'), []), + 'calliope/variants/operate.md': (_calliope(), []), + 'calliope/variants/operate_milp.md': (_calliope('milp'), ['milp', 'operate']), + 'calliope/variants/spores.md': (_calliope(), []), + 'calliope/variants/storage_inter_cluster.md': (_calliope(), []), + 'calliope/variants/chp_htp.md': (_calliope('chp_htp'), []), + 'calliope/variants/urban_scale_chp.md': (_calliope('urban_scale_chp'), []), +} #: Page -> the fragments whose composition it shows, and the patches laid over #: it. The spec is what `merge` returns, which no file in the tree holds, so @@ -119,26 +148,46 @@ def fragment_block(path: Path, table_path: Path) -> str: def split_index_block() -> str: - """The PyPSA split's two tables: each sum with the fragment that declares it and the terms, and each fragment. + """The PyPSA split's two tables, read off its fragments.""" + return sum_tables({path.stem: (path, f'{path.stem}.md') for path in sorted(PYPSA.glob('*.yaml'))}) + + +def calliope_index_block() -> str: + """The Calliope port's two tables, over the base fragments and then every extension.""" + return sum_tables( + { + **{path.stem: (path, f'{path.stem}.md') for path in CALLIOPE_BASE}, + **{path.stem: (path, f'extensions/{path.stem}.md') for path in CALLIOPE_EXTENSIONS}, + } + ) + + +def sum_tables(fragments: dict[str, tuple[Path, str]]) -> str: + """Each sum with the fragment that declares it and the terms, then each fragment. - Both are read off the fragments, so the index cannot name a term or an - owner the files no longer have. + Both are read off the fragments, so an index cannot name a term or an + owner the files no longer have. The owner of a sum is the fragment that + declares it under ``expressions:``, empty or with a body of its own. """ - specs = {path.stem: to_spec(path) for path in sorted(PYPSA.glob('*.yaml'))} - owners: dict[str, tuple[str, tuple[str, ...]]] = {} + specs = {name: to_spec(path) for name, (path, _) in fragments.items()} + href = {name: link for name, (_, link) in fragments.items()} terms: dict[str, dict[str, str]] = {} for name, spec in specs.items(): for hub, entry in spec.given.expressions.items(): if entry.term is not None: terms.setdefault(hub, {})[name] = entry.term - for hub, block in spec.expressions.items(): - if block.expression is None and not block.cases: - owners[hub] = (name, tuple(block.dims or ())) + owners = { + hub: (name, tuple(spec.expressions[hub].dims or ())) + for name, spec in specs.items() + for hub in terms + if hub in spec.expressions + } sums = ['| Sum | Over | Declared in | The terms, by the fragment that adds each |', '| --- | --- | --- | --- |'] for hub, by_fragment in sorted(terms.items(), key=lambda item: -len(item[1])): reader, dims = owners[hub] - cells = ', '.join(f'[`{term}`]({fragment}.md)' for fragment, term in sorted(by_fragment.items())) - sums.append(f'| `{hub}` | `{", ".join(dims)}` | [{reader}]({reader}.md) | {cells} |') + cells = ', '.join(f'[`{term}`]({href[fragment]})' for fragment, term in sorted(by_fragment.items())) + over = f'`{", ".join(dims)}`' if dims else 'nothing: one number' + sums.append(f'| `{hub}` | {over} | [{reader}]({href[reader]}) | {cells} |') files = [ '| Fragment | Parameters | Variables | Constraints | Reads | Adds to |', '| --- | --- | --- | --- | --- | --- |', @@ -148,12 +197,46 @@ def split_index_block() -> str: reads = len(given.parameters) + len(given.variables) + len(given.expressions) + len(given.constraints) adds = ', '.join(f'`{hub}`' for hub, entry in given.expressions.items() if entry.term is not None) files.append( - f'| [{name}]({name}.md) | {len(spec.parameters)} | {len(spec.variables)} | {len(spec.constraints)} ' + f'| [{name}]({href[name]}) | {len(spec.parameters)} | {len(spec.variables)} | {len(spec.constraints)} ' f'| {reads} | {adds} |' ) return '### The sums\n\n' + '\n'.join(sums) + '\n\n### The fragments\n\n' + '\n'.join(files) +def variant_block(page: str) -> str: + """A Calliope patch: the call that lays it, the file, then each declaration it writes as the patched spec prints it. + + A patch that removes a declaration names it in a list, since what is gone + has no line to print. + """ + fragments, before = CALLIOPE_VARIANTS[page] + path = CALLIOPE / 'variants' / f'{Path(page).stem}.yaml' + patches = [CALLIOPE / 'variants' / f'{name}.yaml' for name in before] + [path] + patched = override(merge(fragments), patches) + table = symbols_for(patched, CALLIOPE_SYMBOLS) + extensions = [f.stem for f in fragments if f.parent.name == 'extensions'] + listed = ', '.join(repr(f'extensions/{name}.yaml') for name in extensions) + merged = f'ms.merge(base + [{listed}])' if extensions else 'ms.merge(base)' + laid = ', '.join(repr(f'variants/{patch.name}') for patch in patches) + call = f'ms.override(\n {merged},\n [{laid}],\n)' + written = yaml.safe_load(path.read_text()) + parts = [f'```python\n{call}\n```', f'```yaml title="variants/{path.name}"\n{without_header(path)}\n```'] + removed = [] + for section in ('variables', 'expressions', 'constraints', 'assumptions'): + for name, entry in (written.get(section) or {}).items(): + if entry is None: + removed.append(f'`{name}`') + continue + line = typeset_declaration(patched, name, 'markdown', symbols=table, inline_expressions=False) + parts.append(f'**`{name}`**\n\n```math\n{line}\n```') + if written.get('objective'): + page_math = to_markdown(patched, symbols=table, numbered=False) + parts.append('**The objective**\n\n' + _section(page_math, 'Objective').removeprefix('#### Objective').strip()) + if removed: + parts.append(f'Removed: {", ".join(removed)}.') + return '\n\n'.join(parts) + + def composed_block(fragments: list[Path], patches: dict[str, Path]) -> str: """The spec `merge` returns for *fragments* as YAML, then its document as composed and under each patch. @@ -358,6 +441,12 @@ def block(page: str) -> str: return composed_block(*COMPOSED[page]) if page == SPLIT_INDEX: return split_index_block() + if page == CALLIOPE_INDEX: + return calliope_index_block() + if page in CALLIOPE_VARIANTS: + return variant_block(page) + if CALLIOPE in MODELS[page].parents: + return fragment_block(MODELS[page], CALLIOPE_SYMBOLS) if MODELS[page].parent == LIBRARY: return fragment_block(MODELS[page], LIBRARY_SYMBOLS) if MODELS[page].parent == PYPSA: @@ -373,7 +462,7 @@ def rendered(page: str, text: str) -> str: def pages() -> list[str]: - return [*MODELS, *COMPOSED, *DECLARED, 'operators.md', SPLIT_INDEX] + return [*MODELS, *COMPOSED, *DECLARED, 'operators.md', SPLIT_INDEX, CALLIOPE_INDEX, *CALLIOPE_VARIANTS] def main(argv: list[str] | None = None) -> int: diff --git a/tools/render_tex.py b/tools/render_tex.py index c882cea3..3cfa6032 100644 --- a/tools/render_tex.py +++ b/tools/render_tex.py @@ -23,9 +23,9 @@ CORPUS = ('examples/**/*.yaml', 'tests/typesetting/golden/*.yaml') #: Inside that glob and not specs: the symbol tables `sidecar_for` looks up, -#: and the patches a library's variants are written as, which `override` lays +#: and the patches the variants of a library and of Calliope are written as, which `override` lays #: over a spec rather than anything loading them on their own. -NOT_MODELS = ('examples/symbols', 'examples/library/variants') +NOT_MODELS = ('examples/symbols', 'examples/library/variants', 'examples/calliope/variants') def models() -> list[Path]: