Groups
Groups are used to make constraints that apply to a group of nodes, units and/or connections. A group is defined by creating a group entity and then creating an entity between the group and its members. The membership entity classes are group__node, group__unit, group__connection and reserve__upDown__group. The choice of group members depends on what the group is trying to achieve. For instance a group that limits investments could have a set of units included in the group.
Flow aggregation (flow limits and aggregated flow outputs) lives on a separate flowGroup class with its own 3-dim membership classes flowGroup__unit__node and flowGroup__connection__node; see that section below.
Capacity limits for nodes, units and connections
invest_method- How the group limits investment and retirement of its members (a value list of its own,group_invest_methods). not_active (default): the group sets no limits. invest / retire / invest_retire: which decisions the limits apply to; _period uses the…_periodlimits, _total the…_totallimits, _period_total both; no_limit: no limits even if values are set; cumulative_limits uses the cumulative capacity limits. Before database version 77 not_active was called not_allowed; the upgrade renames stored values, and a database that still holds not_allowed for a group stops the run with an error.invest_max_total/invest_min_total- [MW or MWh] Largest allowed / smallest required total investment of the group over all solves, summed over its units and connections (MW) and storage nodes (MWh). Every investment decision counts; retirements do not reduce it. Constant.invest_max_period/invest_min_period- [MW or MWh] Largest allowed / smallest required capacity coming online in each period, summed over the group's members. With a construction lead time, the investment is decided earlier. Constant or period. A period map must give a value for every period in which capacity of the solve comes online: its investment periods, or with a construction lead time the periods the investments start to operate in (a missing period is an error).retire_max_period/retire_min_period- [MW or MWh] Largest allowed / smallest required retirement in each period, summed over the group's members. Needs a retire …_period option in the group'sinvest_method. Constant or period. A period map must give a value for every investment period of the solve (a missing period is an error).retire_max_total/retire_min_total- [MW or MWh] Largest allowed / smallest required total retirement of the group over all solves. Needs a retire …_total option in the group'sinvest_method. Constant.cumulative_max_capacity- [MW or MWh] Maximum cumulative capacity for a group of entities (considers existing, invested and retired capacity). Constant or period. A period map must give a value for every investment period of the solve (a missing period is an error). Only applies wheninvest_methodiscumulative_limits.cumulative_min_capacity- [MW or MWh] Minimum cumulative capacity for a group of entities (considers existing, invested and retired capacity). Constant or period. A period map must give a value for every investment period of the solve (a missing period is an error). Only applies wheninvest_methodiscumulative_limits.
Transfer method override for connections in a group
transfer_method- Overrides thetransfer_methodchosen on each individualconnectionwhose both endpoints belong to this group (group__node). Options: use_connection_transfer_methods (default, no override), no_losses_no_variable_cost, regular, exact, variable_cost_only, unidirectional, dc_power_flow_with_angles. The connection-level options work exactly as in the connection reference; dc_power_flow_with_angles activates B-theta DC power flow on the subnet and requires each member connection to havereactance;is_DCconnections are not overridden and need noreactance. A connection with both endpoints in several groups that set different overrides is an error.base_MVA- [MVA] Base power for the per-unit system used in DC power flow. Default 100. Only relevant when this group'stransfer_method = dc_power_flow_with_angles.reference_node- Name of the reference bus node (angle fixed to zero) for DC power flow. Optional; if not set the engine picks one deterministically.candidate_precapacity_to_avoid_big_m- [MW] Small pre-existing capacity assigned to investment-candidate connections in DC power-flow subnets to avoid big-M angle constraints. Tuning parameter for numerical stability of the angle formulation.
Limits for nodes
has_inertia- A flag whether the group of nodes has an inertia constraint active. Default no. Without yes the group has no inertia constraint, even ifinertia_limitis given.inertia_limit- [MWs] Minimum for synchronous inertia in the group of nodes. Used only whenhas_inertiais yes; no value (or 0) means no requirement. Constant, period, time or period+time. A time map (or the time level of a period+time map) of a group withhas_inertiayes must give a value for every time step of the solve, look-ahead included (a missing time step is an error, reported before the first solve; give 0 for an hour without a requirement). A solve whosereduce_system_detail_methodleaves inertia out is not checked. A period map (or the period level of a period+time map) must give a value for every period of the solve, look-ahead periods included (a missing period is an error).penalty_inertia- [CUR/MWs per hour] Penalty for violating the inertia constraint. The cost scales with the duration of the violation. Default 5000, applied to every group without a value of its own (and to the periods a map leaves out); 0 makes the violation free. Constant or period.has_non_synchronous- A flag whether the group of nodes has the non-synchronous share constraint active. Default no. Without yes the group has no non-synchronous constraint, even ifnon_synchronous_limitis given.non_synchronous_limit- [share, e.g. 0.8 means 80%] The maximum share of non-synchronous generation in the node group, of the group's supply measured with the real demand (without an energy margin). Flows between the group's nodes are excluded. Used only whenhas_non_synchronousis yes; with the flag and no value (or 0), no non-synchronous infeed is allowed (violations are penalised). Constant, period, time or period+time. A time map (or the time level of a period+time map) of a group withhas_non_synchronousyes must give a value for every time step of the solve, look-ahead included (a missing time step is an error, reported before the first solve; an authored 0 is a limit of 0, give 1 for an hour without a limit). A solve whosereduce_system_detail_methodleaves the non-synchronous limit out is not checked. A period map (or the period level of a period+time map) must give a value for every period of the solve, look-ahead periods included (a missing period is an error).penalty_non_synchronous- [CUR/MWh] Penalty for violating the non synchronous constraint. Default 5000, applied to every group without a value of its own (and to the periods a map leaves out); 0 makes the violation free. Constant or period.capacity_margin_method- Whether the group of nodes has a capacity margin in investment solves, and how the margin is given. Options: none (default, no margin), manual (the margin iscapacity_marginMW) and share_of_peak (the margin iscapacity_margin_sharetimes the group's peak demand). The margin applies in the investment periods of each solve (and their stochastic branch periods); a solve withoutinvest_periods(dispatch, rolling dispatch) has none. See The capacity margin below for what counts and how.capacity_margin- [MW] The margin of a group withcapacity_margin_methodmanual: the capacity the group needs above its demand at every time step of an investment period. Without that method the value is ignored (a warning says so). No value (or 0) means the capacity must still cover the demand. Constant or period. A period map must give a value for every investment period of the solve (a missing period is an error).capacity_margin_share- [share, e.g. 0.15 means 15%] The margin of a group withcapacity_margin_methodshare_of_peak, as a share of the group's peak demand in the period. Without that method the value is ignored (a warning says so). No value (or 0) means no margin. Constant or period. A period map must give a value for every investment period of the solve.penalty_capacity_margin- [CUR/kW/a] Penalty for violating the capacity margin constraint. Uses operational discounting (not annualized over lifetime like investment costs), so a penalty of e.g. 1000 CUR/kW is not comparable to an investment cost of 1000 CUR/kW which would be annualized to a much lower annual cost. Default 5000, applied to every group without a value of its own (and to the periods a map leaves out); 0 makes the violation free. In a stochastic fan each forecast branch period has its own shortfall, and its penalty is weighted by the forecast's probability. Constant or period.share_loss_of_load- Force the upward slack of the nodes in this group to be equal or inflow (demand) weighted
The capacity margin
At every time step of an investment period, the capacity margin row of a group asks:
firm capacity + VRE potential + what the group's other entities deliver or take at that step ≥ the group's demand + the margin.
The demand is the real demand of the group's nodes (the negative inflow, without an energy margin). Storage nodes have no demand in the row: a storage node of the group with demand gets a warning (model that demand on a balance node of the group), a group of storage nodes only is an error, and so is a balance_within_period node in the group (its balance is per period, the margin per time step). The entities count as follows:
| Entity | Counts as |
|---|---|
| Firm unit: a unit with an output into the group that is not one of the kinds below. Units fed from fuel or commodity nodes, from balance nodes outside the group, and units without inputs (e.g. demand response) | its capacity × availability × capacity_max_coeff of the output. A unit with several outputs into the group (a CHP) counts its capacity once: capacity × availability × min(1, the sum of capacity_max_coeff of those outputs), the limit the sum of its outputs has |
| VRE unit: a unit whose output into the group has an upper_limit or fixed profile | its capacity × min(profile, capacity_max_coeff) × availability |
Storage-backed unit: a unit with an input from a storage node, unless its other inputs can run it at full output alone (their input_share_max adds up to at least 1; the default is 1) |
its actual flow, minus the energy the storage node's balance slack makes in the period (see below) |
| Unit inside the group with efficiency ≥ 1: a unit with an input from one of the group's nodes (a heat pump in an electricity + heat group, an inverter fed from a DC bus of the group) | its actual flow, and its input as consumption |
| Lossy unit inside the group: a unit whose inputs all come from the group's nodes and whose outputs all go to them, with an efficiency (at full load) below 1 at some step (an electric boiler, an electrolyser into a hydrogen node of the group) | left out: the demand it serves counts one to one |
| Connection inside the group: both ends in the group | left out: the demand at its far end counts one to one, without its losses |
| Other connection (imports, exports, a battery behind an inverter connection) | its actual flow, into the group positive, out of it negative |
| Consumption by the units fed from the group's nodes that are not inside the group (electrolysers into a node outside the group, pumps, chargers) | its actual flow, negative |
Inputs and outputs with conversion_flow_coeff 0 carry no energy limit: such an output counts by its flow, and such an input is left out of the storage and inside-the-group rules.
What this means in practice:
- Only firm and VRE capacity can hold the margin. Imports, storage and the group's own consumption help only by what they deliver at that step, which lets the firm units run less. A group fed only by imports cannot hold any margin. Energy dumped through the node's downward slack does not count.
- Fuel is assumed to last. A unit fed from a fuel or commodity node, or from a balance node, counts at its capacity, even if the fuel is limited (a fuel node with a storage state makes its units storage-backed). To have a fuel-limited unit count by its flow, model the fuel as a storage node.
- Storage counts by what it delivers. A battery, a hydro reservoir or a CSP heat tank counts with its discharge at that step, which is conservative when the storage is full. Pumped hydro: the turbine counts by its flow, the pump as consumption. Run-of-river counts at capacity when the river node (a balance node with inflow, no storage state) is outside the group; with the river node in the group, the turbine is a unit inside the group and counts by its flow, and the river's inflow lowers the group's demand.
- Slack energy is not storage delivery. Energy that a storage node's upward balance slack makes could be stored and sent into the group at the binding step. For a storage node that feeds the group through a storage-backed unit or a connection, the row takes back all slack energy the node makes in the period, divided by the step's length. A storage node with
penalty_methodoff has no slack. The same can happen through a node outside the group whose slack makes the energy it exports into the group; that is not taken back, so give such nodes a highpenalty_up(orpenalty_methodoff). - Exports add to the requirement. A connection out of the group counts negative, so the capacity behind an export cannot also hold the margin.
- Losses inside the group are not part of the requirement. Connections with both ends in the group and lossy units inside the group are left out of the row: their far end's demand counts one to one. If the losses at the peak matter, add an estimate of them to the margin. (Had the row kept them, it would require the losses of the dispatch at each step, and shedding the load behind a lossy line would lower that requirement by more than the unserved energy raises the shortfall: a margin MW is priced for the whole year, a shed MWh only for its hour, so the model would shed load to hold the margin.) A heat pump (efficiency ≥ 1) stays in the row by its flow: its input is the power the heat demand needs.
- Unserved energy is priced once. Load the group does not serve leaves the row unchanged where firm capacity would have served it, and is priced by
penalty_uponly. - The energy margin does not change the capacity margin. The row uses the real demand. Storage and imports count with the power they deliver at the step, also when it serves the energy margin's added demand: that power is real capability at that step. Firm output that serves the added demand leaves the row unchanged.
The margin of a share_of_peak group is capacity_margin_share × the group's peak demand: the largest sum of the demand of the group's nodes over the period's full time series, on the period's original timeline (before representative periods and new_stepduration, and the same in every roll of a solve). If the representative periods leave the peak out, the margin still uses the real peak. When several investment periods of a solve use different parts of one timeline (a multi-year timeline, one block per period), each period's peak is over its own part: the time steps of its timeset blocks, or, with representative periods, of the base periods they stand for. A stochastic branch period has its own peak, from its own branch data.
Reading the result. After each investment solve that leaves a shortfall, the run log prints capacity margin shortfall: solve …, group …, period …: … MW at … (penalty … M CUR). The table capacity_margin__d has, per solve, group and investment period, the binding time step, the requirement, what each class above counts there and the shortfall; capacity_margin_entity__d has what each unit, connection and node counts, which shows which rule applies to it. slack__capacity_margin__d and the capacity margin penalty of the costs are those of the solve that decided the investments.
With representative periods, the row holds at the sampled time steps only, and the storage that counts is the storage of the sampled days: force-include the peak days. Over a coarse time step (new_stepduration) the row may be averaged (see below).
Under spatial Benders decomposition (decomposition_method benders_regional), a capacity margin group (and an inertia or non-synchronous group) with nodes in more than one region is an error: each region would have to hold the whole margin. Use one group per region.
CO2 costs and limits
co2_method- Choice of the CO2 method or a combination of methods: no_method, price, period, total, price_period, price_total, period_total, price_period_total. A method with period in it needsco2_max_periodand a method with total in it needsco2_max_total; a group without that value is an error.co2_price[CUR/tCO2] CO2 price for a group of nodes. Constant or period. A period-indexed Map may cover more periods than the current solve realises — extra entries are silently ignored, so the same shared CO2-price dataset can drive solves with different period subsets. If the active solve activatesco2_pricetopology (a priced commodity is wired up) but noco2_priceorco2_contentvalue is supplied for a needed period, the engine logs a warning and skips the CO2 cost term for that period rather than raising an error.co2_max_period[tCO2/a] Annualized maximum limit for emitted CO2 in each period (tonnes per year of the period). Constant or period. A period map must give a value for every period of the solve, look-ahead periods included (a missing period is an error). In a rolling solve each roll applies the limit to its own window (realized part and look-ahead): its emissions are annualised over the window's hours of each period. The limit is carried across the rolls: each roll may emit the share of the period's limit reached by the end of its horizon, minus the emissions earlier rolls already realized in the period, so the realized emissions of a period stay within the limit. In a rolling solve the reported CO2 price of a period is the shadow price of the last roll that covers the period.co2_max_total[tCO2] Cumulative limit for emitted CO2 over the whole solve horizon: each period's annualized emissions times the years it represents (years_represented), summed over the periods, so the model chooses when to emit. In a rolling solve or a chain of solves the limit is carried: each roll or solve may emit the part of the limit allotted to the periods it reaches (in a rolling solve, theyears_represented-weighted share of the solve's periods reached by the end of the roll's horizon; a solve of a chain reaches all of its own periods) minus what earlier rolls and solves already realized, counted the same way (annualized and timesyears_represented). The realized emissions of the whole chain therefore stay within the limit, but an early solve of a chain may use up what later solves would have needed. In a stochastic solve each forecast path gets the whole remaining limit.
The emissions that the CO2 price and the limits apply to are the fuel drawn from the group's nodes times the commodity's co2_content, including the extra fuel that min_load_efficiency units burn at part load.
Stochastics
include_stochasticsFlag to choose if stochastic timeseries are to be used for the units/nodes/connections of this group
Decomposition (spatial Benders)
decomposition_method(ongroup) - Decomposition strategy to apply to this group. Currently supported:none(no decomposition; default) andbenders_regional(the group becomes a spatial region when a solve setsdecomposition = benders— the_regionsuffix emphasises that this is the geographic flavour, distinct from any future temporal variant). See dev/decomposition.md for the algorithm, gap tolerances, and the membership classes the decomposer expects.decomposition(onsolve) - Whether this solve decomposes.none(default) solves monolithically;bendersroutes the solve through the spatial Benders coordinator (master trade flows + trade invest + per-region recourse, optimality cuts) over thebenders_regionalgroups. Chosen per solve, so one chain can mix schemes. The optional knobsbenders_max_iter(default50) andbenders_tolerance(default1e-3) cap the cut loop and set the relative optimality-gap threshold.
Flex-temporal decomposition (per-group step duration)
-
new_stepduration- Hours. Balances the nodes of this group once per coarse step of this length, while the rest of the model keeps the solve's time step.What it does. The balance and storage nodes of this group are balanced once per coarse step when the step is longer than the solve's time step. An example is hourly electricity nodes next to daily hydrogen nodes. The energy over each coarse step balances, so the node may shift energy freely within the step. A storage node's state is constant within the step.
Which units and connections go coarse. A unit or connection with a node outside the coarse groups keeps the solve's time step. A coarse node sums its flows over the step, so an arc between an hourly and a daily node conserves energy at both ends. A unit or connection whose balance nodes are all coarse uses the coarse step: it has one flow per coarse step, a rate that stays the same over the step. If its nodes have different coarse steps, it takes the shortest one when that step divides the others; otherwise it keeps the solve's step. Listing a unit or connection in the group has no effect (a warning names those that touch a node outside the group).
Nodes without a balance. Commodity and price nodes and
balance_within_periodnodes do not count. A unit that also buys fuel can therefore be coarse; it pays the average price of the step.Averaged limits. The capacity, availability, profile, efficiency and input-share limits of a coarse unit or connection apply to the average over each coarse step. This gives the same result as hourly flows when each arc has at most one limit that varies within the step, and its cost, efficiency and prices do not vary within the step. Otherwise the result can differ. With a cost or efficiency that varies, hourly flows could use the cheapest or most efficient hours. With two or more varying limits, the averaged limits are looser.
Ramps. Ramp limits apply between consecutive coarse steps, with the average duration of the two steps (see
ramp_speed_up). Inside a coarse step the flow does not change.Online units. A unit or connection with online variables uses the coarse step too. Its units online, start-ups and shut-downs are decided once per coarse step; it starts or stops at the start of the step. Its minimum load and online capacity apply to the average over the step. A minimum up or down time counts whole coarse steps: a start-up counts while the steps since its start last less than
min_uptime. On daily steps amin_uptimeof at most 24 h adds nothing.Online units with ramp limits. The start-up and shut-down capacity reduction uses the coarse step. While the ramp does not reach full output within the step, a unit started at the start of a step reaches on average
min_load + ½ × ramp_speed_up × 60 × step durationof its capacity over the step. When the ramp does reach full output (full output for the rest of the step), the average is1 − (1 − min_load)² / (2 × ramp_speed_up × 60 × step duration). The same holds withramp_speed_downbefore a shut-down. Its units online ramp between coarse steps.Reserves. A unit or connection whose balance nodes are all coarse and that provides reserves holds one reserve per coarse step. Its reserve limits apply to the average over the step, with the average availability or profile. These limits are the reserve share of its capacity, a connection's net position, the floor of its flow, and the storage energy for the reserve duration when the storage node and every provider are coarse. The room its ramp limit leaves for reserve is the smallest of the step's time steps, each with its own capacity factor. The reserve requirement is still checked at every time step of the solve, and nodes of a coarse group may belong to reserve groups.
Exact connections and DC power flow. An
exactconnection whose nodes are all coarse chooses its direction once per coarse step; its direction limits apply to the average over the step. A DC power-flow line whose nodes are all coarse has one flow per coarse step. When every line of a DC network uses the coarse step (all its nodes with the same coarse step), its nodes have one voltage angle per coarse step. In a DC network with a line at the solve's step, every angle stays at the solve's step, so that line keeps its per-step physics.Rows that combine several entities. These rows follow one rule: the capacity-margin, inertia and non-synchronous rows of a group, the instant flow limits of a flow group, a user
constraintwith flow, storage or online terms (constraint_flow_coeff,constraint_state_coeff,constraint_online_coeff), the DC power-flow row of a line, and the stochastic storage-use tie of a node. Such a row becomes one row per coarse step, the average of the row over the step, when every unit and connection in it uses the coarse step. In a user constraint, every storage node in it must also be coarse. In a capacity-margin row only the entities counted by their flow (storage-backed units, units inside the group, connections, consumption) are in it: firm and VRE units count by their capacity, and the units and connections left out are not in the row.Averaged rows. An averaged row uses the step mean of the demand, inflow, profile, limit or constant in it. An averaged capacity margin therefore covers the mean net demand of the step, not its peak hour. A row given on only part of a coarse step (e.g. a
max_instant_flowmap without values for some hours) stays at the time steps it covers. Aninertia_limitornon_synchronous_limittime map must give a value for every original time step (hour) of a coarse step; its row uses the step mean. The inertia and non-synchronous shortfalls are then one value per coarse step.Mixed rows. A row that mixes units or connections on the coarse step with others on the solve's step is checked at every time step of the solve. It sees the coarse flows as constant through their step. A coarse DC line in a DC network with a line at the solve's step keeps the angle difference across it constant through the coarse step, while the angles stay per time step.
Exceptions. Units and connections with a delay keep the solve's time step. In a Benders decomposition, flows that cross regions and every flow in the master keep the solve's step. In a stochastic solve, a unit or connection at a stochastic node, with online variables or providing reserves, keeps the solve's step when the forecasts are tied over only part of one of its coarse steps. In a realization solve, the same applies to one whose committed units online or reserve change within a coarse step or are pinned on only part of it.
Messages. An info line lists the units and connections that use a coarse step, and those that keep the solve's step with the reason. A database upgraded from an earlier FlexTool version prints a notice naming the coarse units and connections whose results can differ from flows at the solve's step, and why.
Outputs. Flows, units online, reserves, voltage angles and the shortfalls of averaged rows are written at every time step of the solve, with the same value through each coarse step. Start-ups and shut-downs are written whole at the first time step of their coarse step and as 0 at the others. A coarse node's price is the dual of its coarse-step balance, the same at every time step of the step.
Rules for the step. The group step must be a multiple of the solve's time step; a shorter group step is ignored with a warning. Coarse steps start from the start of each timeset block. They restart after a gap in the timeline, at each representative period and at the end of the realized part of a roll. A block that is not a multiple of the step ends with a shorter step; with representative periods the step must divide the representative period.
Storage. Storage binding methods,
fix_start,storage_solve_horizon_method, nested fix methods and the roll carry apply to coarse nodes as to others.bind_intraperiod_blockscannot be combined with a coarse group.
Controlling outputs
Two group-level flags drive the extra outputs computed from a node group, and the flow_aggregator method (on the flowGroup class) drives the flow-aggregation outputs. Each has a specific intent and requires a matching membership class; setting it without the right members yields an empty result (the runner will log a warning).
| You want … | Set this on … | Must contain |
|---|---|---|
| Summary metrics (loss of load, VRE share, curtailment, excess load, annualised inflow) for a collection of nodes | print_indicators: yes on group |
group__node rows |
A per-timestep in/out decomposition (group_flow_t) of a node collection, one column per contributing process/connection |
print_dispatch: yes on group |
group__node rows |
| Member flows shown as aggregated bands inside a node group's dispatch table | flow_aggregator: dispatch_plots_only on flowGroup |
flowGroup__unit__node or flowGroup__connection__node rows |
The flowGroup's own aggregated outputs (per-period cumulative_flow in MWh/a and average_flow in MW, plus a signed net-flow timewise series) |
flow_aggregator: standalone_aggregator_only on flowGroup |
flowGroup__unit__node or flowGroup__connection__node rows |
| Both of the above | flow_aggregator: both on flowGroup |
flowGroup__unit__node or flowGroup__connection__node rows |
print_indicators(ongroup) — emits aggregate indicator metrics (loss-of-load share, VRE share, curtailment share, excess-load share and annualised inflow) over the node members. Acts as a summary of the node group. Requiresgroup__nodemembers.print_dispatch(ongroup) — emits the per-timestep in/out decomposition of the node group, with one column per contributing process or connection (units feeding the group, connections crossing its boundary, storage, slacks). Requiresgroup__nodemembers. If any of those contributing flows belong to aflowGroupwhoseflow_aggregatorisdispatch_plots_onlyorboth, their columns are replaced by a single aggregated band named after the flowGroup.
See Flow groups (flowGroup) for the flow_aggregator method values and the flow-limit parameters.