Skip to content

Capacity Limits

Process formulation

Unless stated otherwise, all formulations shown for Link apply analogously to Process by replacing \((f,\hat f,F,l)\) with \((r,\hat r,R,m)\) and Link-* with Process-* in the constraint names.

Upper and Lower Bounds

If the nominal capacity of a components is also the subject of optimisation (e.g. with decision variable \(G_{n,s}\) for generators), limits on the installable capacity may also be introduced (e.g. \(\underline{G}_{n,s}\) and \(\bar{G}_{n,s}\)):

Constraint Dual Variable Name
\(G_{n,s} \geq \underline{G}_{n,s}\) n.generators.mu_lower Generator-ext-p_nom-lower
\(G_{n,s} \leq \bar{G}_{n,s}\) n.generators.mu_upper Generator-ext-p_nom-upper
\(F_{l} \geq \underline{F}_{l}\) n.links.mu_lower Link-ext-p_nom-lower
\(F_{l} \leq \bar{F}_{l}\) n.links.mu_upper Link-ext-p_nom-upper
\(R_{m} \geq \underline{R}_{m}\) n.processes.mu_lower Process-ext-p_nom-lower
\(R_{m} \leq \bar{R}_{m}\) n.processes.mu_upper Process-ext-p_nom-upper
\(P_{l} \geq \underline{P}_{l}\) n.{lines,transformers}.mu_lower {Line,Transformer}-ext-s_nom-lower
\(P_{l} \leq \bar{P}_{l}\) n.{lines, transformers}.mu_upper {Line,Transformer}-ext-s_nom-upper
\(E_{n,s} \geq \underline{E}_{n,s}\) n.stores.mu_lower Store-ext-e_nom-lower
\(E_{n,s} \leq \bar{E}_{n,s}\) n.stores.mu_upper Store-ext-e_nom-upper
\(H_{n,s} \geq \underline{H}_{n,s}\) n.storage_units.mu_lower StorageUnit-ext-p_nom-lower

These constraints are set in the function define_nominal_constraints_for_extendables.

Committable and Extendable Components

When components are both committable (committable=True) and extendable (e.g. p_nom_extendable=True), the optimizer co-optimizes both capacity expansion and operational unit commitment. The challenge is that the upper bound on dispatch becomes nonlinear: \(g_{n,s,t} \leq u_{n,s,t} \cdot G_{n,s}\) (the product of binary status \(u\) and continuous capacity \(G\)). To maintain a Mixed-Integer Linear Programme (MILP), PyPSA uses a big-M formulation that replaces this nonlinear constraint with two linear constraints:

Constraint Name
\(g_{n,s,t} \leq u_{n,s,t} \cdot M\) Generator-com-ext-p-upper-bigM
\(g_{n,s,t} \leq \bar{g}_{n,s,t} \cdot G_{n,s}\) Generator-com-ext-p-upper-cap
\(g_{n,s,t} \geq \underline{g}_{n,s,t} \cdot G_{n,s} - M \cdot (1 - u_{n,s,t})\) Generator-com-ext-p-lower
\(g_{n,s,t} \geq 0\) Generator-com-ext-p-lower-nonneg
Constraint Name
\(f_{l,t} \leq u_{l,t} \cdot M\) Link-com-ext-p-upper-bigM
\(f_{l,t} \leq \bar{f}_{l,t} \cdot F_{l}\) Link-com-ext-p-upper-cap
\(f_{l,t} \geq \underline{f}_{l,t} \cdot F_{l} - M \cdot (1 - u_{l,t})\) Link-com-ext-p-lower
\(f_{l,t} \geq 0\) Link-com-ext-p-lower-nonneg
Constraint Name
\(r_{m,t} \leq u_{m,t} \cdot M\) Process-com-ext-p-upper-bigM
\(r_{m,t} \leq \bar{r}_{m,t} \cdot R_{m}\) Process-com-ext-p-upper-cap
\(r_{m,t} \geq \underline{r}_{m,t} \cdot R_{m} - M \cdot (1 - u_{m,t})\) Process-com-ext-p-lower
\(r_{m,t} \geq 0\) Process-com-ext-p-lower-nonneg

where \(M\) is a sufficiently large constant (the "big-M") and \(\bar{g}\), \(\bar{f}\), \(\bar{r}\) are the maximum per-unit dispatch limits (p_max_pu, default 1). When the status \(u = 0\), the big-M constraint forces dispatch to zero. When \(u = 1\), the lower bound becomes \(g \geq \underline{g} \cdot G\) (minimum part-load) and the upper bound is \(g \leq \bar{g} \cdot G\) (capacity limit).

Big-M Parameter Configuration

The big-M constant must be large enough to not constrain the optimization, but not so large as to cause numerical issues. PyPSA automatically infers an appropriate value based on the network's peak load:

\[M = 10 \times \max_t \left( \sum_n L_{n,t} \right)\]

where \(L_{n,t}\) is the load at bus \(n\) and time \(t\). The factor of 10 provides a safety margin.

The big-M value can be manually overridden using the committable_big_m parameter:

n.optimize(committable_big_m=value)

Big-M Size Warning

If the optimized capacity \(G_{n,s}\), \(F_{l}\) or \(R_{m}\) exceeds the big-M value, PyPSA will issue a warning. In this case, increase the big-M value manually to ensure the formulation remains valid.

Ramp Constraints for Extendable Committable Components

For components that are both committable and extendable with ramp limits, a big-M formulation is used to handle the interaction between ramp limits and the commitment status. Four constraints are added — two for ramping up (during normal operation and during start-up) and two for ramping down (during normal operation and during shut-down):

Constraint Name
\((g_{n,s,t} - g_{n,s,t-1}) \leq ru_{n,s} \cdot G_{n,s} + M \cdot (1 - u_{n,s,t-1})\) Generator-p-ramp_limit_up-run-bigM
\((g_{n,s,t} - g_{n,s,t-1}) \leq rs_{n,s} \cdot G_{n,s} + M \cdot (1 - su_{n,s,t})\) Generator-p-ramp_limit_up-start-bigM
\((g_{n,s,t} - g_{n,s,t-1}) \geq -rd_{n,s} \cdot G_{n,s} - M \cdot (1 - u_{n,s,t})\) Generator-p-ramp_limit_down-run-bigM
\((g_{n,s,t} - g_{n,s,t-1}) \geq -rsd_{n,s} \cdot G_{n,s} - M \cdot (1 - sd_{n,s,t})\) Generator-p-ramp_limit_down-shut-bigM
Constraint Name
\((f_{l,t} - f_{l,t-1}) \leq ru_{l} \cdot F_{l} + M \cdot (1 - u_{l,t-1})\) Link-p-ramp_limit_up-run-bigM
\((f_{l,t} - f_{l,t-1}) \leq rs_{l} \cdot F_{l} + M \cdot (1 - su_{l,t})\) Link-p-ramp_limit_up-start-bigM
\((f_{l,t} - f_{l,t-1}) \geq -rd_{l} \cdot F_{l} - M \cdot (1 - u_{l,t})\) Link-p-ramp_limit_down-run-bigM
\((f_{l,t} - f_{l,t-1}) \geq -rsd_{l} \cdot F_{l} - M \cdot (1 - sd_{l,t})\) Link-p-ramp_limit_down-shut-bigM
Constraint Name
\((r_{m,t} - r_{m,t-1}) \leq ru_{m} \cdot R_{m} + M \cdot (1 - u_{m,t-1})\) Process-p-ramp_limit_up-run-bigM
\((r_{m,t} - r_{m,t-1}) \leq rs_{m} \cdot R_{m} + M \cdot (1 - su_{m,t})\) Process-p-ramp_limit_up-start-bigM
\((r_{m,t} - r_{m,t-1}) \geq -rd_{m} \cdot R_{m} - M \cdot (1 - u_{m,t})\) Process-p-ramp_limit_down-run-bigM
\((r_{m,t} - r_{m,t-1}) \geq -rsd_{m} \cdot R_{m} - M \cdot (1 - sd_{m,t})\) Process-p-ramp_limit_down-shut-bigM

When the unit was running in the previous timestep (\(u_{t-1} = 1\)), the big-M term vanishes and the normal ramp-up limit \(ru\) applies. When \(u_{t-1} = 0\), the big-M relaxes the constraint, allowing unrestricted ramp-up. The start-up ramp limit \(rs\) is enforced only during start-up events (\(su_t = 1\)). The same logic applies symmetrically to ramp-down and shut-down constraints.

These constraints are defined in the function define_ramp_limit_constraints().

Mapping of symbols to component attributes
Symbol Attribute Type
\(g_{n,s,t}\) n.generators_t.p Decision variable
\(G_{n,s}\) n.generators.p_nom_opt Decision variable
\(u_{n,s,t}\) n.generators_t.status Decision variable
\(su_{n,s,t}\) n.generators_t.start_up Decision variable
\(sd_{n,s,t}\) n.generators_t.shut_down Decision variable
\(M\) auto-inferred or committable_big_m parameter Parameter
\(ru_{n,s}\) n.generators.ramp_limit_up Parameter
\(rd_{n,s}\) n.generators.ramp_limit_down Parameter
\(rs_{n,s}\) n.generators.ramp_limit_start_up Parameter
\(rsd_{n,s}\) n.generators.ramp_limit_shut_down Parameter
Symbol Attribute Type
\(f_{l,t}\) n.links_t.p Decision variable
\(F_{l}\) n.links.p_nom_opt Decision variable
\(u_{l,t}\) n.links_t.status Decision variable
\(su_{l,t}\) n.links_t.start_up Decision variable
\(sd_{l,t}\) n.links_t.shut_down Decision variable
\(M\) auto-inferred or committable_big_m parameter Parameter
\(ru_{l}\) n.links.ramp_limit_up Parameter
\(rd_{l}\) n.links.ramp_limit_down Parameter
\(rs_{l}\) n.links.ramp_limit_start_up Parameter
\(rsd_{l}\) n.links.ramp_limit_shut_down Parameter

Modularity Constraints

The capacity expansion can be further constrained to be a multiple (e.g. \(G^{\textrm{mod}}_{n,s} \in \mathbb{N}\)) of a modular capacity (e.g. \(\tilde{G}_{n,s}\)) to represent fixed block sizes of added components (e.g. fixed block size of a nuclear power plant or a fixed capacity of a new circuit).

If {p,s,e}_nom_mod>0, the nominal capacity is given by:

Constraint Dual Variable Name
\(G_{n,s} = G^{\textrm{mod}}_{n,s} \cdot \tilde{G}_{n,s}\) N/A Generator-p_nom_modularity
\(F_{l} = F^{\textrm{mod}}_{l} \cdot \tilde{F}_{l}\) N/A Link-p_nom_modularity
\(R_{m} = R^{\textrm{mod}}_{m} \cdot \tilde{R}_{m}\) N/A Process-p_nom_modularity
\(P_{l} = P^{\textrm{mod}}_{l} \cdot \tilde{P}_{l}\) N/A {Line,Transformer}-s_nom_modularity
\(E_{n,s} = E^{\textrm{mod}}_{n,s} \cdot \tilde{E}_{n,s}\) N/A Store-e_nom_modularity
\(H_{n,s} = H^{\textrm{mod}}_{n,s} \cdot \tilde{H}_{n,s}\) N/A StorageUnit-p_nom_modularity

These constraints are set in the function define_modular_constraints().

Modular and Committable Components

When extendable components additionally have modular capacities activated (p_nom_mod > 0) and are committable (committable=True), the formulation differs from the big-M approach above. Instead of a binary on/off status variable, the status variable becomes an integer representing the number of committed modules inside the component.

Module-Level Commitment Formulation

For modular committable components, two integer variables are introduced:

  • \(n^{\textrm{mod}}_{*}\): Number of modules built (determines total capacity)
  • \(u_{*,t}\): Number of modules committed at time \(t\) (determines operational state)

The capacity is constrained to be a multiple of the module size:

Constraint Name
\(G_{n,s} = n^{\textrm{mod}}_{n,s} \cdot \tilde{G}_{n,s}\) Generator-p_nom_modularity
\(0 \leq u_{n,s,t} \leq n^{\textrm{mod}}_{n,s}\) Generator-status-p_nom-variable-upper
Constraint Name
\(F_{l} = n^{\textrm{mod}}_{l} \cdot \tilde{F}_{l}\) Link-p_nom_modularity
\(0 \leq u_{l,t} \leq n^{\textrm{mod}}_{l}\) Link-status-p_nom-variable-upper

The dispatch constraints enforce that power output respects the number of committed modules:

Constraint Name
\(g_{n,s,t} \geq u_{n,s,t} \cdot \underline{g}_{n,s,t} \cdot \tilde{G}_{n,s}\) Generator-com-mod-p-lower
\(g_{n,s,t} \leq u_{n,s,t} \cdot \bar{g}_{n,s,t} \cdot \tilde{G}_{n,s}\) Generator-com-mod-p-upper
Constraint Name
\(f_{l,t} \geq u_{l,t} \cdot \underline{f}_{l,t} \cdot \tilde{F}_{l}\) Link-com-mod-p-lower
\(f_{l,t} \leq u_{l,t} \cdot \bar{f}_{l,t} \cdot \tilde{F}_{l}\) Link-com-mod-p-upper

Note that the minimum and maximum part-load parameters (\(\underline{g}\) and \(\bar{g}\)) apply per committed module, not to the total capacity.

Start-up and Shut-down for Modular Components

Start-up and shut-down variables track changes in the number of committed modules:

Constraint Name
\(su_{n,s,t} \geq u_{n,s,t} - u_{n,s,t-1}\) Generator-com-transition-start-up
\(sd_{n,s,t} \geq u_{n,s,t-1} - u_{n,s,t}\) Generator-com-transition-shut-down
Constraint Name
\(su_{l,t} \geq u_{l,t} - u_{l,t-1}\) Link-com-transition-start-up
\(sd_{l,t} \geq u_{l,t-1} - u_{l,t}\) Link-com-transition-shut-down

The start-up and shut-down cost terms in the objective function are multiplied by the number of modules being started or stopped.

Initial status affects start-up costs

The status attribute defaults to 1, which is used as \(u_{t-1}\) for the first snapshot. For modular committable components, this means one module is assumed to be already committed at the start of the optimization. For example, if the optimizer invests in 5 modules and commits all 5 in the first timestep, only 4 start-up events are counted (\(su_0 \geq 5 - 1 = 4\)), and start-up costs are only charged for those 4 transitions. To charge start-up costs for all modules, set the initial status to 0 via n.generators.loc[name, "status"] = 0. This also applies to the non-modular (big-M) committable formulation.

These constraints are defined in the function define_operational_constraints_for_committables().

Mapping of symbols to component attributes
Symbol Attribute Type
\(g_{n,s,t}\) n.generators_t.p Decision variable
\(G_{n,s}\) n.generators.p_nom_opt Decision variable
\(n^{\textrm{mod}}_{n,s}\) n.model.variables['Generator-n_mod'] Decision variable
\(u_{n,s,t}\) n.generators_t.status Decision variable (integer)
\(su_{n,s,t}\) n.generators_t.start_up Decision variable (integer)
\(sd_{n,s,t}\) n.generators_t.shut_down Decision variable (integer)
\(\tilde{G}_{n,s}\) n.generators.p_nom_mod Parameter
\(\underline{g}_{n,s,t}\) n.generators_t.p_min_pu Parameter
\(\bar{g}_{n,s,t}\) n.generators_t.p_max_pu Parameter
Symbol Attribute Type
\(f_{l,t}\) n.links_t.p Decision variable
\(F_{l}\) n.links.p_nom_opt Decision variable
\(n^{\textrm{mod}}_{l}\) n.model.variables['Link-n_mod'] Decision variable
\(u_{l,t}\) n.links_t.status Decision variable (integer)
\(su_{l,t}\) n.links_t.start_up Decision variable (integer)
\(sd_{l,t}\) n.links_t.shut_down Decision variable (integer)
\(\tilde{F}_{l}\) n.links.p_nom_mod Parameter
\(\underline{f}_{l,t}\) n.links_t.p_min_pu Parameter
\(\bar{f}_{l,t}\) n.links_t.p_max_pu Parameter

Compatibility of Capacity Expansion with Unit Commitment Features

The following table summarizes which unit commitment features are compatible with the two formulations:

Feature Committable + Extendable (Big-M) Modular + Committable
Start-up costs
Shut-down costs
Minimum part-load (p_min_pu) ✓ (per module)¹
Ramp limits (ramp_limit_up/down) ✓²
Stand-by costs
Minimum up time (min_up_time)
Minimum down time (min_down_time)
Up time before (up_time_before)
Down time before (down_time_before)

¹ For modular components, p_min_pu and p_max_pu apply to each committed module. The minimum/maximum power is calculated as p_min_pu × p_nom_mod × status where status is the number of committed modules.

² For modular + committable components, ramp limits are applied to the total installed capacity (similar to standard committable components), not per module. The ramp constraint is p_t - p_{t-1} ≤ ramp_limit_up × p_nom_opt, where p_nom_opt is the total capacity of all installed modules. This differs from the per-module behavior of minimum/maximum part-load constraints.

Fixed Capacity

Additionally, the nominal capacity can be fixed to a certain value \(\tilde{G}_{n,s}\) for generators, \(\tilde{F}_{l}\) for links, \(\tilde{R}_{m}\) for processes, \(\tilde{P}_{l}\) for lines and transformers, and \(\tilde{E}_{n,s}\) for stores, and \(\tilde{H}_{n,s}\) for storage units. In this case, the nominal capacity is given by:

Constraint Dual Variable Name
\(G_{n,s} = \tilde{G}_{n,s}\) only in n.model Generator-p_nom_set
\(F_{l} = \tilde{F}_{l}\) only in n.model Link-p_nom_set
\(R_{m} = \tilde{R}_{m}\) only in n.model Process-p_nom_set
\(P_{l} = \tilde{P}_{l}\) only in n.model {Line,Transformer}-s_nom_set
\(E_{n,s} = \tilde{E}_{n,s}\) only in n.model Store-e_nom_set
\(H_{n,s} = \tilde{H}_{n,s}\) only in n.model StorageUnit-p_nom_set

These constraints are set in the function define_fixed_nominal_constraints().

Why not just set p_nom_extendable=False?

Using p_nom_extendable=False means the capacity is fixed and not optimized. However, sometimes we need to fix the capacity to a specific value while still keeping track of the dual variables associated with capacity constraints. Setting {p,s,e}_nom_set allows for this while maintaining p_nom_extendable=True.

Mapping of symbols to component attributes
Symbol Attribute Type
\(G_{n,s}\) n.generators.p_nom_opt Decision variable
\(G^{\textrm{mod}}_{n,s}\) not stored Decision variable
\(\underline{G}_{n,s}\) n.generators.p_nom_min Parameter
\(\bar{G}_{n,s}\) n.generators.p_nom_max Parameter
\(\tilde{G}_{n,s}\) n.generators.p_nom_mod Parameter
\(\hat{G}_{n,s}\) n.generators.p_nom_set Parameter
Symbol Attribute Type
\(F_{l}\) n.links.p_nom_opt Decision variable
\(F^{\textrm{mod}}_{l}\) not stored Decision variable
\(\underline{F}_{l}\) n.links.p_nom_min Parameter
\(\bar{F}_{l}\) n.links.p_nom_max Parameter
\(\tilde{F}_{l}\) n.links.p_nom_mod Parameter
\(\hat{F}_{l}\) n.links.p_nom_set Parameter
Symbol Attribute Type
\(R_{m}\) n.processes.p_nom_opt Decision variable
\(R^{\textrm{mod}}_{m}\) not stored Decision variable
\(\underline{R}_{m}\) n.processes.p_nom_min Parameter
\(\bar{R}_{m}\) n.processes.p_nom_max Parameter
\(\tilde{R}_{m}\) n.processes.p_nom_mod Parameter
\(\hat{R}_{m}\) n.processes.p_nom_set Parameter
Symbol Attribute Type
\(P_{l}\) n.lines.s_nom_opt Decision variable
\(P^{\textrm{mod}}_{l}\) not stored Decision variable
\(\underline{P}_{l}\) n.lines.s_nom_min Parameter
\(\bar{P}_{l}\) n.lines.s_nom_max Parameter
\(\tilde{P}_{l}\) n.lines.s_nom_mod Parameter
\(\hat{P}_{l}\) n.lines.s_nom_set Parameter
Symbol Attribute Type
\(P_{l}\) n.transformers.s_nom_opt Decision variable
\(P^{\textrm{mod}}_{l}\) not stored Decision variable
\(\underline{P}_{l}\) n.transformers.s_nom_min Parameter
\(\bar{P}_{l}\) n.transformers.s_nom_max Parameter
\(\tilde{P}_{l}\) n.transformers.s_nom_mod Parameter
\(\hat{P}_{l}\) n.transformers.s_nom_set Parameter
Symbol Attribute Type
\(E_{n,s}\) n.stores.e_nom_opt Decision variable
\(E^{\textrm{mod}}_{n,s}\) not stored Decision variable
\(\underline{E}_{n,s}\) n.stores.e_nom_min Parameter
\(\bar{E}_{n,s}\) n.stores.e_nom_max Parameter
\(\tilde{E}_{n,s}\) n.stores.e_nom_mod Parameter
\(\hat{E}_{n,s}\) n.stores.e_nom_set Parameter
Symbol Attribute Type
\(H_{n,s}\) n.storage_units.p_nom_opt Decision variable
\(H^{\textrm{mod}}_{n,s}\) not stored Decision variable
\(\underline{H}_{n,s}\) n.storage_units.p_nom_min Parameter
\(\bar{H}_{n,s}\) n.storage_units.p_nom_max Parameter
\(\tilde{H}_{n,s}\) n.storage_units.p_nom_mod Parameter
\(\hat{H}_{n,s}\) n.storage_units.p_nom_set Parameter

Examples

  • Modular Capacity Expansion


    Models discrete capacity additions with integer constraints on investment decisions considering predefined unit sizes.

    Go to example

  • Committable and Extendable Components


    Co-optimize capacity expansion and unit commitment using big-M linearization. Demonstrates continuous capacity decisions with start-up/shut-down costs, ramp limits, and minimum load constraints.

    Go to example

  • Modular and Committable Components


    Model discrete capacity blocks with unit commitment where status represents the number of committed modules. Shows modular gas turbines, HVDC links, and multi-module operational dynamics.

    Go to example