Skip to content

Stores#

One of the 24 fragments of examples/pypsa.yaml: PyPSA's Store. It adds a term to primary_energy, operational_limit, tech_capacity_expansion, scenario_opex, Carrier_additions, Bus_injection. It reads CVaR_omega, GlobalConstraint_counts_snapshot, GlobalConstraint_snapshot_closes, period_weight_objective, period_weight_years, scenario_weight and 2 more under given.

dimensions:
  scenario:
    description: the futures dispatch is chosen in, each with a weight
  snapshot:
    description: dispatch periods
    dtype: datetime
  bus:
    description: network nodes
  store:
    description: pure energy stores, each on one bus
  global_constraint:
    description: PyPSA's `GlobalConstraint` rows, one label per declared limit
  period:
    description: investment periods — PyPSA's `investment_periods`
    dtype: int
  carrier:
    description: energy carriers, what a growth limit is set per

relations:
  snapshot_period:
    description: the investment period a snapshot falls in
    key: snapshot
    values: period
  Store_carrier:
    description: the carrier a store holds
    key: store
    values: carrier
  Store_bus:
    description: the bus a store sits on
    key: store
    values: bus

parameters:
  Store_active:
    description: whether a store stands in a snapshot's period — PyPSA's `active`, data prep
    dims: [snapshot, store]
    dtype: bool
  Store_capital_weight:
    description: the sum of period weights a store stands in — PyPSA's `active * period_weighting`, summed, data prep
    dims: [store]
  Store_first_active:
    description: >-
      one in the first period a store stands in, zero elsewhere, data prep.
      PyPSA `1.3.0` takes `active.cumsum() == 1`, which also counts a store
      that has retired in every later period (`global_constraints.py:276`,
      PyPSA/PyPSA#1938)
    dims: [period, store]
  Store_e_nom_min:
    description: least nominal capacity an extendable store may be built at
    dims: [scenario, store]
  Store_e_nom_max:
    description: most nominal capacity an extendable store may be built at
    dims: [scenario, store]
  Store_capital_cost:
    description: cost of one unit of nominal capacity — PyPSA's `capital_cost`, periodized as an annuity in data prep
    dims: [scenario, store]
  Store_e_nom_set:
    description: a given nominal capacity for an extendable store; one without a value has no row here
    dims: [scenario, store]
  Store_e_nom:
    description: nominal energy capacity
    dims: [scenario, store]
  Store_e_nom_extendable:
    description: whether the nominal energy capacity is a decision
    dims: [store]
    dtype: bool
  Store_e_min_pu:
    description: least energy held, per unit of nominal capacity — negative for a store that may go short
    dims: [scenario, snapshot, store]
  Store_e_max_pu:
    description: most energy held, per unit of nominal capacity
    dims: [scenario, snapshot, store]
  Store_sign:
    description: >-
      the sign the power a store delivers enters its bus's balance with —
      PyPSA's `sign`, `1` unless given. PyPSA refuses one that differs by
      scenario (`consistency.py:1187`)
    dims: [store]
  Store_retention:
    description: share of energy kept over a snapshot — PyPSA's `(1 - standing_loss) ** elapsed hours`, data prep
    dims: [scenario, snapshot, store]
  Store_e_initial:
    description: energy held before the first snapshot
    dims: [scenario, store]
  Store_e_cyclic:
    description: whether the horizon closes on itself instead of opening on the initial energy
    dims: [scenario, store]
    dtype: bool
  Store_e_cyclic_per_period:
    description: >-
      whether each investment period closes on itself instead of carrying its
      energy on to the next; it overrides `e_cyclic` and `e_initial_per_period`.
      PyPSA reads it only under `multi_investment_periods`, so data prep feeds
      false otherwise
    dims: [scenario, store]
    dtype: bool
  Store_e_initial_per_period:
    description: >-
      whether each investment period opens on the initial energy instead of
      carrying the previous period's; PyPSA reads it only under
      `multi_investment_periods`, so data prep feeds false otherwise
    dims: [scenario, store]
    dtype: bool
  Store_opens_late:
    description: >-
      whether a snapshot is the first a store stands in, where that is not the
      first of the horizon — PyPSA's `active.cumsum() == 1` over the snapshots
      it stands in, past the first snapshot, data prep; false in a run where
      every store stands throughout
    dims: [snapshot, store]
    dtype: bool
  Store_inactive_snapshots:
    description: >-
      how many snapshots a store does not stand in — PyPSA's `(~active).sum()`,
      data prep. A cyclic store reaches back this many snapshots further, so it
      closes on the last snapshot it stands in
    dims: [store]
    dtype: int
  Store_marginal_cost:
    description: cost of one unit of power delivered
    dims: [scenario, snapshot, store]
  Store_marginal_cost_quadratic:
    description: cost of the square of the net power delivered, so charging costs as much as delivering
    dims: [scenario, snapshot, store]
  Store_marginal_cost_storage:
    description: cost of one unit of energy held over one snapshot
    dims: [scenario, snapshot, store]
  Store_e_set:
    description: a given energy schedule; a store without one has no row here
    dims: [scenario, snapshot, store]
  Store_p_set:
    description: a given schedule of power delivered; a store without one has no row here
    dims: [scenario, snapshot, store]
  Store_primary_energy_weight:
    description: the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row
    dims: [scenario, global_constraint, store]
  Store_operational_limit_weight:
    description: one where the store is in the row's set — data prep; one outside it has no row
    dims: [scenario, global_constraint, store]
  Store_tech_capacity_weight:
    description: >-
      one where the store is in the row's carrier-and-bus set — data prep; one
      outside it, or one that does not stand in the row's `investment_period`,
      has no row
    dims: [global_constraint, store]

variables:
  Store_e:
    description: "`Store-e` — energy held at the end of a snapshot"
    dims: [scenario, snapshot, store]
    where: Store_active
  Store_p:
    description: "`Store-p` — power delivered to the bus; charging is negative"
    dims: [scenario, snapshot, store]
    where: Store_active
  Store_e_nom_ext:
    description: >-
      `Store-e_nom` — nominal capacity where it is a decision; the parameter
      of the same PyPSA name carries the fixed regime
    dims: [store]
    where: Store_e_nom_extendable

given:
  parameters:
    snapshot_weightings_objective: { dims: [snapshot] }
    scenario_weight: { dims: [scenario] }
    CVaR_omega: { dims: [] }
    period_weight_objective: { dims: [period] }
    period_weight_years: { dims: [period] }
    snapshot_weightings_stores: { dims: [snapshot] }
    GlobalConstraint_counts_snapshot: { dims: [scenario, global_constraint, snapshot], dtype: bool }
  expressions:
    GlobalConstraint_snapshot_closes: { dims: [scenario, global_constraint, snapshot] }
    primary_energy: { dims: [scenario, global_constraint], term: Store_primary_energy }
    operational_limit: { dims: [scenario, global_constraint], term: Store_operational_limit }
    tech_capacity_expansion: { dims: [global_constraint], term: Store_tech_capacity_expansion }
    scenario_opex: { dims: [scenario], term: Store_opex }
    Carrier_additions: { dims: [period, carrier], term: Store_additions }
    Bus_injection: { dims: [scenario, snapshot, bus], term: Store_injection }

expressions:
  Store_energy_carried_in:
    description: >-
      the energy a store opens a snapshot with — at the first snapshot it
      stands in, its last such snapshot's less standing loss where it is
      cyclic and the given initial energy, which no standing loss has touched
      yet, where it is not; the previous snapshot's less standing loss
      otherwise. A store built in a later period opens in that period, and a
      cyclic one that retires closes on its own last snapshot. Per period, the
      same holds with each investment period as the horizon
    dims: [scenario, snapshot, store]
    cases:
      cyclic:
        when: >-
          Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period
          AND (position(snapshot) == 0 OR Store_opens_late)
        expression: >-
          Store_retention
          * shift(shift(Store_e, along=snapshot, offset=1, edge='wrap'), along=snapshot, offset=Store_inactive_snapshots, edge='wrap')
      opening:
        when: >-
          NOT Store_e_cyclic AND NOT Store_e_cyclic_per_period AND NOT Store_e_initial_per_period
          AND (position(snapshot) == 0 OR Store_opens_late)
        expression: Store_e_initial
      period_cyclic:
        when: Store_e_cyclic_per_period
        expression: Store_retention * shift(Store_e, along=snapshot, offset=1, edge='wrap', by=snapshot_period, within=period)
      period_opening:
        when: Store_e_initial_per_period AND NOT Store_e_cyclic_per_period AND position(snapshot, by=snapshot_period, within=period) == 0
        expression: Store_e_initial
    otherwise: Store_retention * shift(Store_e, along=snapshot, offset=1)
  Store_closing_weight:
    description: >-
      what the energy a store holds at a snapshot counts for in a row as its
      closing level — the years of the period at the last snapshot of each
      counted period where the store reopens per period, one at the last
      counted snapshot where it does not, and nothing elsewhere
    dims: [scenario, global_constraint, snapshot, store]
    cases:
      per_period:
        when: Store_e_initial_per_period AND GlobalConstraint_counts_snapshot AND position(snapshot, by=snapshot_period, within=period) == -1
        expression: at(period_weight_years, by=snapshot_period, over=period, into=snapshot)
      carried_over:
        when: NOT Store_e_initial_per_period
        expression: GlobalConstraint_snapshot_closes
    otherwise: 0
  Store_primary_energy:
    expression: >-
      -sum(sum((Store_e * Store_closing_weight) * Store_primary_energy_weight, over=snapshot), over=store)
  Store_operational_limit:
    expression: >-
      -sum(sum((Store_e * Store_closing_weight) * Store_operational_limit_weight, over=snapshot), over=store)
  Store_tech_capacity_expansion:
    expression: sum(Store_e_nom_ext * Store_tech_capacity_weight, over=store)
  Store_opex:
    expression: >-
      sum(sum(((Store_p * Store_marginal_cost) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot)
      + sum(sum((((Store_p * Store_p) * Store_marginal_cost_quadratic) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot)
      + sum(sum(((Store_e * Store_marginal_cost_storage) * snapshot_weightings_objective) * at(period_weight_objective, by=snapshot_period, over=period, into=snapshot), over=store), over=snapshot)
  Store_additions:
    expression: >-
      sum(Store_e_nom_ext * Store_first_active, by=Store_carrier, over=store, into=carrier)
  Store_injection: sum(Store_sign * Store_p, by=Store_bus, over=store, into=bus)

constraints:
  Store_fix_e_lower:
    description: "`Store-fix-e-lower` — a fixed store holds at least its floor"
    dims: [scenario, snapshot, store]
    where: not Store_e_nom_extendable AND Store_active
    expression: Store_e >= Store_e_min_pu * Store_e_nom
  Store_fix_e_upper:
    description: "`Store-fix-e-upper` — a fixed store holds at most its nominal capacity"
    dims: [scenario, snapshot, store]
    where: not Store_e_nom_extendable AND Store_active
    expression: Store_e <= Store_e_max_pu * Store_e_nom
  Store_ext_e_lower:
    description: "`Store-ext-e-lower` — an extendable store holds at least its floor of the chosen build"
    dims: [scenario, snapshot, store]
    where: Store_e_nom_extendable AND Store_active
    expression: Store_e >= Store_e_min_pu * Store_e_nom_ext
  Store_ext_e_upper:
    description: "`Store-ext-e-upper` — an extendable store holds at most the chosen build"
    dims: [scenario, snapshot, store]
    where: Store_e_nom_extendable AND Store_active
    expression: Store_e <= Store_e_max_pu * Store_e_nom_ext
  Store_ext_e_nom_lower:
    description: "`Store-ext-e_nom-lower` — the chosen build is at least its floor in every scenario"
    dims: [scenario, store]
    where: Store_e_nom_extendable
    expression: Store_e_nom_ext >= Store_e_nom_min
  Store_ext_e_nom_upper:
    description: "`Store-ext-e_nom-upper` — the chosen build is at most its cap in every scenario; a cap of infinity is no row"
    dims: [scenario, store]
    where: Store_e_nom_extendable AND Store_e_nom_max
    expression: Store_e_nom_ext <= Store_e_nom_max
  Store_e_nom_set:
    description: "`Store-e_nom_set` — the chosen build pinned, wherever a value is given"
    dims: [scenario, store]
    where: Store_e_nom_extendable AND Store_e_nom_set
    expression: Store_e_nom_ext == Store_e_nom_set
  Store_energy_balance:
    description: "`Store-energy_balance` — the energy carried in, less what is delivered to the bus"
    dims: [scenario, snapshot, store]
    where: Store_active
    expression: >-
      Store_e ==
      Store_energy_carried_in
      - Store_p * snapshot_weightings_stores
  Store_e_set:
    description: "`Store-e_set` — energy pinned to the given schedule, wherever one is given"
    dims: [scenario, snapshot, store]
    where: Store_e_set AND Store_active
    expression: Store_e == Store_e_set
  Store_p_set:
    description: "`Store-p_set` — power delivered pinned to the given schedule, wherever one is given"
    dims: [scenario, snapshot, store]
    where: Store_p_set AND Store_active
    expression: Store_p == Store_p_set

assumptions:
  Store_stands_in_one_run:
    holds: "count(Store_active AND NOT shift(Store_active, along=snapshot, offset=1), over=snapshot) <= 1"
    description: >-
      the snapshots a store stands in are one unbroken run, as a build
      year and a lifetime make them. The opening row holds at the first
      of them only, and a cyclic store reaches back
      `Store_inactive_snapshots` further to the last of them
  Store_opens_late_only_where_it_opens:
    holds: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0"
    where: "Store_opens_late"
    description: >-
      `Store_opens_late` marks the first snapshot the store stands in, past
      the first of the horizon, and no other
  Store_opens_late_where_it_opens:
    holds: "Store_opens_late"
    where: "Store_active AND NOT shift(Store_active, along=snapshot, offset=1) AND position(snapshot) > 0"
    description: >-
      a store that opens past the first snapshot of the horizon opens on
      its initial level, or its last level where it is cyclic, only where
      `Store_opens_late` marks the snapshot
  Store_primary_energy_per_period_closes_over_the_horizon:
    holds: "count(NOT GlobalConstraint_counts_snapshot, over=snapshot) == 0"
    where: "Store_primary_energy_weight AND Store_e_initial_per_period"
    description: >-
      PyPSA reads the closing energy of a store that reopens per period at
      the last snapshot of every period, and fails on a `primary_energy` row
      that names an `investment_period` (`global_constraints.py:526`)
  Store_primary_energy_carried_over_has_unit_years:
    holds: "period_weight_years == 1"
    where: "Store_primary_energy_weight AND NOT Store_e_initial_per_period"
    description: >-
      a store that carries its energy from one period to the next closes
      once, at the last counted snapshot, and no period's years weighs that
      level — PyPSA refuses it where any period's years is not one
      (`global_constraints.py:500`)
  Store_operational_limit_carried_over_has_unit_years:
    holds: "at(period_weight_years == 1, by=snapshot_period, over=period, into=snapshot)"
    where: "Store_operational_limit_weight AND NOT Store_e_initial_per_period AND GlobalConstraint_counts_snapshot"
    description: >-
      the same for an `operational_limit` row, over the periods it counts —
      PyPSA refuses it (`global_constraints.py:695`)
  Store_marginal_cost_quadratic_without_risk_preference:
    holds: "Store_marginal_cost_quadratic == 0"
    where: "CVaR_omega > 0"
    description: >-
      a quadratic cost puts a square into every `CVaR-excess` row, and PyPSA
      refuses quadratic costs under any risk preference
      (`optimize.py:467-474`). The spec cannot tell no risk preference from
      one with `omega = 0`, so it refuses only where `omega` is positive

objective:
  sense: minimize
  expression: >-
    sum(((scenario_weight * Store_e_nom_ext) * Store_capital_cost) * Store_capital_weight)

Sets#

Symbol Meaning
\(\Xi\) index \(\xi\) — scenario — the futures dispatch is chosen in, each with a weight
\(\mathcal{T}\) index \(t\) — snapshot with \(\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}\) — dispatch periods
\(\mathcal{N}\) index \(n\) — bus with \(\mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}\) — network nodes
\(\mathcal{V}\) index \(v\) — store with \(\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I},\ \mathrm{Store\_bus}: \mathcal{V} \to \mathcal{N}\) — pure energy stores, each on one bus
\(\mathcal{G}\) index \(g\) — global_constraint — PyPSA's GlobalConstraint rows, one label per declared limit
\(\mathcal{Y}\) index \(y\) — period with \(\mathrm{snapshot\_period}: \mathcal{T} \to \mathcal{Y}\) — investment periods — PyPSA's investment_periods
\(\mathcal{I}\) index \(i\) — carrier with \(\mathrm{Store\_carrier}: \mathcal{V} \to \mathcal{I}\) — energy carriers, what a growth limit is set per

Parameters#

Symbol Meaning
\(\mathrm{on}^{e}\) Store_active over \(\mathcal{T} \times \mathcal{V}\) — whether a store stands in a snapshot's period — PyPSA's active, data prep
\(\mathrm{W}^{e}\) Store_capital_weight over \(\mathcal{V}\) — the sum of period weights a store stands in — PyPSA's active * period_weighting, summed, data prep
\(\mathrm{new}^{e}\) Store_first_active over \(\mathcal{Y} \times \mathcal{V}\) — one in the first period a store stands in, zero elsewhere, data prep. PyPSA 1.3.0 takes active.cumsum() == 1, which also counts a store that has retired in every later period (global_constraints.py:276, PyPSA/PyPSA#1938)
\(\underline{\mathrm{e}}^{\mathrm{nom}}\) Store_e_nom_min over \(\Xi \times \mathcal{V}\) — least nominal capacity an extendable store may be built at
\(\overline{\mathrm{e}}^{\mathrm{nom}}\) Store_e_nom_max over \(\Xi \times \mathcal{V}\) — most nominal capacity an extendable store may be built at
\(\mathrm{c}^{\mathrm{cap},e}\) Store_capital_cost over \(\Xi \times \mathcal{V}\) — cost of one unit of nominal capacity — PyPSA's capital_cost, periodized as an annuity in data prep
\(\mathrm{e}^{\mathrm{nom,set}}\) Store_e_nom_set over \(\Xi \times \mathcal{V}\) — a given nominal capacity for an extendable store; one without a value has no row here
\(\mathrm{e}^{\mathrm{nom}}\) Store_e_nom over \(\Xi \times \mathcal{V}\) — nominal energy capacity
\(\mathrm{ext}^{e}\) Store_e_nom_extendable over \(\mathcal{V}\) — whether the nominal energy capacity is a decision
\(\underline{\mathrm{e}}\) Store_e_min_pu over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — least energy held, per unit of nominal capacity — negative for a store that may go short
\(\overline{\mathrm{e}}\) Store_e_max_pu over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — most energy held, per unit of nominal capacity
\(\mathrm{sgn}^{q}\) Store_sign over \(\mathcal{V}\) — the sign the power a store delivers enters its bus's balance with — PyPSA's sign, 1 unless given. PyPSA refuses one that differs by scenario (consistency.py:1187)
\(\rho^{e}\) Store_retention over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — share of energy kept over a snapshot — PyPSA's (1 - standing_loss) ** elapsed hours, data prep
\(\mathrm{e}^{0}\) Store_e_initial over \(\Xi \times \mathcal{V}\) — energy held before the first snapshot
\(\mathrm{cyc}^{e}\) Store_e_cyclic over \(\Xi \times \mathcal{V}\) — whether the horizon closes on itself instead of opening on the initial energy
\(\mathrm{cyc}^{e,y}\) Store_e_cyclic_per_period over \(\Xi \times \mathcal{V}\) — whether each investment period closes on itself instead of carrying its energy on to the next; it overrides e_cyclic and e_initial_per_period. PyPSA reads it only under multi_investment_periods, so data prep feeds false otherwise
\(\mathrm{reset}^{e}\) Store_e_initial_per_period over \(\Xi \times \mathcal{V}\) — whether each investment period opens on the initial energy instead of carrying the previous period's; PyPSA reads it only under multi_investment_periods, so data prep feeds false otherwise
\(\mathrm{open}^{e}\) Store_opens_late over \(\mathcal{T} \times \mathcal{V}\) — whether a snapshot is the first a store stands in, where that is not the first of the horizon — PyPSA's active.cumsum() == 1 over the snapshots it stands in, past the first snapshot, data prep; false in a run where every store stands throughout
\(\mathrm{idle}^{e}\) Store_inactive_snapshots over \(\mathcal{V}\) — how many snapshots a store does not stand in — PyPSA's (~active).sum(), data prep. A cyclic store reaches back this many snapshots further, so it closes on the last snapshot it stands in
\(\mathrm{c}^{q}\) Store_marginal_cost over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — cost of one unit of power delivered
\(\mathrm{c}^{q,(2)}\) Store_marginal_cost_quadratic over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — cost of the square of the net power delivered, so charging costs as much as delivering
\(\mathrm{c}^{e}\) Store_marginal_cost_storage over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — cost of one unit of energy held over one snapshot
\(\mathrm{e}^{\mathrm{set}}\) Store_e_set over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — a given energy schedule; a store without one has no row here
\(\mathrm{q}^{\mathrm{set}}\) Store_p_set over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — a given schedule of power delivered; a store without one has no row here
\(\mathrm{a}^{e}\) Store_primary_energy_weight over \(\Xi \times \mathcal{G} \times \mathcal{V}\) — the constrained attribute per unit of energy depleted — data prep; an unweighted store has no row
\(\mathrm{b}^{e}\) Store_operational_limit_weight over \(\Xi \times \mathcal{G} \times \mathcal{V}\) — one where the store is in the row's set — data prep; one outside it has no row
\(\mathrm{m}^{e}\) Store_tech_capacity_weight over \(\mathcal{G} \times \mathcal{V}\) — one where the store is in the row's carrier-and-bus set — data prep; one outside it, or one that does not stand in the row's investment_period, has no row

Variables#

Symbol Meaning
\(e\) Store_e over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — Store-e — energy held at the end of a snapshot
\(q\) Store_p over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — Store-p — power delivered to the bus; charging is negative
\(E\) Store_e_nom_ext over \(\mathcal{V}\) — Store-e_nom — nominal capacity where it is a decision; the parameter of the same PyPSA name carries the fixed regime

Given#

Symbol Meaning
\(\mathrm{w}\) snapshot_weightings_objective over \(\mathcal{T}\), data another file declares
\(\pi\) scenario_weight over \(\Xi\), data another file declares
\(\omega\) CVaR_omega (scalar), data another file declares
\(\mathrm{w}^{y}\) period_weight_objective over \(\mathcal{Y}\), data another file declares
\(\mathrm{w}^{\mathrm{yr}}\) period_weight_years over \(\mathcal{Y}\), data another file declares
\(\mathrm{w}^{\mathrm{sto}}\) snapshot_weightings_stores over \(\mathcal{T}\), data another file declares
\(\mathrm{in}\) GlobalConstraint_counts_snapshot over \(\Xi \times \mathcal{G} \times \mathcal{T}\), data another file declares
\(\mathit{last}\) GlobalConstraint_snapshot_closes over \(\Xi \times \mathcal{G} \times \mathcal{T}\), an expression another file defines
\(\mathit{primary\_energy}\) primary_energy over \(\Xi \times \mathcal{G}\), an expression this file adds Store_primary_energy to
\(\mathit{operational\_limit}\) operational_limit over \(\Xi \times \mathcal{G}\), an expression this file adds Store_operational_limit to
\(\mathit{tech\_capacity\_expansion}\) tech_capacity_expansion over \(\mathcal{G}\), an expression this file adds Store_tech_capacity_expansion to
\(\mathit{scenario\_opex}\) scenario_opex over \(\Xi\), an expression this file adds Store_opex to
\(\mathit{Carrier\_additions}\) Carrier_additions over \(\mathcal{Y} \times \mathcal{I}\), an expression this file adds Store_additions to
\(\mathit{Bus\_injection}\) Bus_injection over \(\Xi \times \mathcal{T} \times \mathcal{N}\), an expression this file adds Store_injection to

Definitions#

Symbol Meaning
\(\overleftarrow{e}\) Store_energy_carried_in over \(\Xi \times \mathcal{T} \times \mathcal{V}\) — the energy a store opens a snapshot with — at the first snapshot it stands in, its last such snapshot's less standing loss where it is cyclic and the given initial energy, which no standing loss has touched yet, where it is not; the previous snapshot's less standing loss otherwise. A store built in a later period opens in that period, and a cyclic one that retires closes on its own last snapshot. Per period, the same holds with each investment period as the horizon
\(\mathit{w}^{e}\) Store_closing_weight over \(\Xi \times \mathcal{G} \times \mathcal{T} \times \mathcal{V}\) — what the energy a store holds at a snapshot counts for in a row as its closing level — the years of the period at the last snapshot of each counted period where the store reopens per period, one at the last counted snapshot where it does not, and nothing elsewhere
\(\mathit{Store\_primary\_energy}\) Store_primary_energy over \(\Xi \times \mathcal{G}\)
\(\mathit{Store\_operational\_limit}\) Store_operational_limit over \(\Xi \times \mathcal{G}\)
\(\mathit{Store\_tech\_capacity\_expansion}\) Store_tech_capacity_expansion over \(\mathcal{G}\)
\(\mathit{Store\_opex}\) Store_opex over \(\Xi\)
\(\mathit{Store\_additions}\) Store_additions over \(\mathcal{Y} \times \mathcal{I}\)
\(\mathit{Store\_injection}\) Store_injection over \(\Xi \times \mathcal{T} \times \mathcal{N}\)

\(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.

\(t \ominus^{\mathrm{relation}(t)} k\) denotes a translation counted inside the group a relation puts \(t\) in (shift(by=relation)), so a term never crosses out of its own group.

\(\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.

\(\mathrm{pos}_{\mathrm{relation}(t)}(t)\) counts within the group a relation puts \(t\) in: the subscript names the map, \(\mathcal{T}_{\mathrm{relation}(t)}\) is the group it lands in, and that group has a first position of its own.

\(\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\).

Objective#

\[ \min \sum_{\xi \in \Xi,\ v \in \mathcal{V}} \pi_{\xi} \cdot E_{v} \cdot \mathrm{c}^{\mathrm{cap},e}_{\xi,v} \cdot \mathrm{W}^{e}_{v} \]

Subject to#

Store_fix_e_lower

\[ e_{\xi,t,v} \ge \underline{\mathrm{e}}_{\xi,t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} \]

Store_fix_e_upper

\[ e_{\xi,t,v} \le \overline{\mathrm{e}}_{\xi,t,v} \cdot \mathrm{e}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \neg \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} \]

Store_ext_e_lower

\[ e_{\xi,t,v} \ge \underline{\mathrm{e}}_{\xi,t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} \]

Store_ext_e_upper

\[ e_{\xi,t,v} \le \overline{\mathrm{e}}_{\xi,t,v} \cdot E_{v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{on}^{e}_{t,v} \]

Store_ext_e_nom_lower

\[ E_{v} \ge \underline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \]

Store_ext_e_nom_upper

\[ E_{v} \le \overline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \overline{\mathrm{e}}^{\mathrm{nom}}_{\xi,v} \text{ is defined} \]

Store_e_nom_set

\[ E_{v} = \mathrm{e}^{\mathrm{nom,set}}_{\xi,v} \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \wedge \mathrm{e}^{\mathrm{nom,set}}_{\xi,v} \text{ is defined} \]

Store_energy_balance

\[ e_{\xi,t,v} = \overleftarrow{e}_{\xi,t,v} - q_{\xi,t,v} \cdot \mathrm{w}^{\mathrm{sto}}_{t} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \]

Store_e_set

\[ e_{\xi,t,v} = \mathrm{e}^{\mathrm{set}}_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{e}^{\mathrm{set}}_{\xi,t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} \]

Store_p_set

\[ q_{\xi,t,v} = \mathrm{q}^{\mathrm{set}}_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{q}^{\mathrm{set}}_{\xi,t,v} \text{ is defined} \wedge \mathrm{on}^{e}_{t,v} \]

Definitions#

Store_energy_carried_in

\[ \overleftarrow{e}_{\xi,t,v} = \begin{cases} \rho^{e}_{\xi,t,v} \cdot e_{\xi,\left( t \ominus \mathrm{idle}^{e} \right) \ominus 1,v} & \text{if } \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \neg \mathrm{cyc}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \left( \mathrm{pos}(t) = 0 \vee \mathrm{open}^{e}_{t,v} \right) \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t \ominus^{\mathrm{snapshot\_period}(t)} 1,v} & \text{if } \mathrm{cyc}^{e,y}_{\xi,v} \\ \mathrm{e}^{0}_{\xi,v} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \neg \mathrm{cyc}^{e,y}_{\xi,v} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = 0 \\ \rho^{e}_{\xi,t,v} \cdot e_{\xi,t - 1,v} & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \]

Store_closing_weight

\[ \mathit{w}^{e}_{\xi,g,t,v} = \begin{cases} \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} & \text{if } \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} \wedge \mathrm{pos}_{\mathrm{snapshot\_period}(t)}(t) = \lvert \mathcal{T}_{\mathrm{snapshot\_period}(t)} \rvert - 1 \\ \mathit{last}_{\xi,g,t} & \text{if } \neg \mathrm{reset}^{e}_{\xi,v} \\ 0 & \text{otherwise} \end{cases} \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G},\ t \in \mathcal{T},\ v \in \mathcal{V} \]

Store_primary_energy

\[ \mathit{Store\_primary\_energy}_{\xi,g} = -\left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{\xi,g,t,v} \cdot \mathrm{a}^{e}_{\xi,g,v} \right) \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \]

Store_operational_limit

\[ \mathit{Store\_operational\_limit}_{\xi,g} = -\left( \sum_{v \in \mathcal{V}} \sum_{t \in \mathcal{T}} e_{\xi,t,v} \cdot \mathit{w}^{e}_{\xi,g,t,v} \cdot \mathrm{b}^{e}_{\xi,g,v} \right) \qquad \forall\, \xi \in \Xi,\ g \in \mathcal{G} \]

Store_tech_capacity_expansion

\[ \mathit{Store\_tech\_capacity\_expansion}_{g} = \sum_{v \in \mathcal{V}} E_{v} \cdot \mathrm{m}^{e}_{g,v} \qquad \forall\, g \in \mathcal{G} \]

Store_opex

\[ \mathit{Store\_opex}_{\xi} = \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot \mathrm{c}^{q}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} q_{\xi,t,v} \cdot q_{\xi,t,v} \cdot \mathrm{c}^{q,(2)}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} + \sum_{t \in \mathcal{T}} \sum_{v \in \mathcal{V}} e_{\xi,t,v} \cdot \mathrm{c}^{e}_{\xi,t,v} \cdot \mathrm{w}_{t} \cdot \mathrm{w}^{y}_{\mathrm{snapshot\_period}(t)} \qquad \forall\, \xi \in \Xi \]

Store_additions

\[ \mathit{Store\_additions}_{y,i} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_carrier}(v) = i} E_{v} \cdot \mathrm{new}^{e}_{y,v} \qquad \forall\, y \in \mathcal{Y},\ i \in \mathcal{I} \]

Store_injection

\[ \mathit{Store\_injection}_{\xi,t,n} = \sum_{v \in \mathcal{V} \,:\, \mathrm{Store\_bus}(v) = n} \mathrm{sgn}^{q}_{v} \cdot q_{\xi,t,v} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ n \in \mathcal{N} \]

Variable domains#

Store_e

\[ e_{\xi,t,v} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \]

Store_p

\[ q_{\xi,t,v} \in \mathbb{R} \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \]

Store_e_nom_ext

\[ E_{v} \in \mathbb{R} \qquad \forall\, v \in \mathcal{V} \,:\, \mathrm{ext}^{e}_{v} \]

Assumptions#

Store_stands_in_one_run

\[ \lvert \{ t \in \mathcal{T} \,:\, \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \} \rvert \le 1 \qquad \forall\, v \in \mathcal{V} \]

Store_opens_late_only_where_it_opens

\[ \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \wedge \mathrm{pos}(t) > 0 \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{open}^{e}_{t,v} \]

Store_opens_late_where_it_opens

\[ \mathrm{open}^{e}_{t,v} \qquad \forall\, t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \mathrm{on}^{e}_{t,v} \wedge \neg \mathrm{on}^{e}_{t - 1,v} \wedge \mathrm{pos}(t) > 0 \]

Store_primary_energy_per_period_closes_over_the_horizon

\[ \lvert \{ t \in \mathcal{T} \,:\, \neg \mathrm{in}_{\xi,g,t} \} \rvert = 0 \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V},\ g \in \mathcal{G} \,:\, \mathrm{a}^{e}_{\xi,g,v} \text{ is defined} \wedge \mathrm{reset}^{e}_{\xi,v} \]

Store_primary_energy_carried_over_has_unit_years

\[ \mathrm{w}^{\mathrm{yr}}_{y} = 1 \qquad \forall\, \xi \in \Xi,\ v \in \mathcal{V},\ g \in \mathcal{G},\ y \in \mathcal{Y} \,:\, \mathrm{a}^{e}_{\xi,g,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \]

Store_operational_limit_carried_over_has_unit_years

\[ \mathrm{w}^{\mathrm{yr}}_{\mathrm{snapshot\_period}(t)} = 1 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V},\ g \in \mathcal{G} \,:\, \mathrm{b}^{e}_{\xi,g,v} \text{ is defined} \wedge \neg \mathrm{reset}^{e}_{\xi,v} \wedge \mathrm{in}_{\xi,g,t} \]

Store_marginal_cost_quadratic_without_risk_preference

\[ \mathrm{c}^{q,(2)}_{\xi,t,v} = 0 \qquad \forall\, \xi \in \Xi,\ t \in \mathcal{T},\ v \in \mathcal{V} \,:\, \omega > 0 \]