Model outputs
After every successful solve, the HiGHS solution is handed back to FlexTool in-memory together with the polars FlexData bundle that produced the LP. flextool.process_outputs.write_outputs then turns that pair into the result tables documented on this page.
The result tables are stored as parquet (the canonical store) and a configurable mix of derived artefacts:
| Location | Format | When written | Notes |
|---|---|---|---|
output_parquet/<scenario>/*.parquet |
parquet | always (canonical) | one file per result table; read by the GUI, the scenario-comparison tools, and read_parquet_dir=True re-runs |
output_csv/<scenario>/*.csv |
CSV | when output-csv is enabled |
one CSV per parquet, plus summary_solve.csv (diagnostic overview), units.csv and datapackage.json (see Units in the files) |
output_excel/output_<scenario>.xlsx |
Excel | when output-excel is enabled |
one workbook per result folder (in the FlexTool window output_<scenario>_<source number>.xlsx): the Units and _output_metadata sheets, then the result sheets in alphabetical order |
output_plots/<scenario>/*.png (or svg/pdf) |
image | when output-plot is enabled |
driven by the bundled flextool/schemas/default_plots.yaml |
<output-location>/results.sqlite (or --results-db-url) |
SpineDB | when output-spinedb is enabled (--write-methods spinedb) |
the processed result tables written into a SpineDB using the FlexTool results schema (flextool/schemas/spinedb_results_schema.json); one Spine alternative per run (named after the scenario) so multiple runs coexist in one file. Time series land as nested Spine Map values on the model entity classes (node / unit / connection / group / model). |
output_parquet/<scenario>/timeline_breaks.parquet (columns period, time) lists the realized steps at which time-series plots start a new line, so a plot leaves a gap there instead of drawing a straight line across it: the step after a gap in the timeline, the start of every representative period (also when two representative periods are adjacent on the timeline, since they represent different base periods), and the first step of a period that does not continue the previous one (also a later year that reuses the same timeset). Rolling windows that follow each other on the timeline, and periods that continue the previous one, are drawn as one line. The file is written also when there are no breaks. Results saved before this file had the period and time columns are drawn without gaps until the scenario is run again. In scenario comparison plots the scenarios share one time axis, so a break of any selected scenario cuts every scenario's line at that step.
In the per-period tables (…__d) the solve column names the last complete solve that realized the period's dispatch; a rolling solve is named without its roll number (dispatch_fullYear_roll, not dispatch_fullYear_roll_roll_17). The capacity tables (unit_capacity__d, connection_capacity__d, node_capacity__d) the investment marginal values (…_invest_marginal__d) and the capacity-margin shortfall (group slack_capacity_margin) name the solve that realized the period's investments instead, for example the investment solve of an investment → dispatch chain. The per-step tables (…__dt) name the solve or roll that realized each step. Results saved before this version label the capacity tables with the dispatch solve until the scenario is run again.
Excel limits a sheet name to 31 characters. The longer table names get short sheet names (for example unit_curtailment__outputNode__dt is the sheet unit_VRE_curtailment__dt and process__reserve__upDown__node__dt is process__reserve__node__dt; the full list is SHEET_NAMES in flextool/process_outputs/_sheet_names.py); a longer name of your own is shortened in front of its __d / __dt suffix. The CSV and parquet file names keep the table names.
Every result table, with its parquet and CSV file names, its index and column levels, the unit of each column and its name in the results database, is listed in the Results catalogue, generated from the code. Below, a result is named by its results-database parameter (class entity parameter) where it is written to the results database, else by its table name.
The Excel workbooks hold names (entities, scenarios, alternatives) as text: a name that starts with = stays that text and never becomes a formula or a link. The CSV files hold the names exactly as they are, for the programs that read them. A spreadsheet program that opens a CSV file can take a name that starts with =, +, - or @ for a formula; open the CSV files of a model from someone else as data (an import that sets the columns to text), or use the Excel workbook.
The CSV / Excel / plot files are derived from the parquets, so deleting or regenerating them is safe. summary_solve.csv is a diagnostic file aimed at a quick overview of a solve — it lists objective decomposition, period weighting, CO2 total, and any non-zero slack uses.
All annualized numbers are scaled to a full year of operation by dividing through by complete_period_share_of_year. All values are returned in user units — auto-scaling (see LP scaling pipeline) and time-block aggregation under flex-temporal decomposition are unwound before they reach the parquets, so users always read MW / MWh / [CUR] in the original problem space.
For stochastic models the parquets carry only the realised / committed results of each solve by default — the investment decisions, the operations of the periods before the reveal and, where a branch is designated realized, the realization dispatch of the fan — not the individual forecast branches. A forecasts-only fan has no realized operation, so its operational results are empty (the period label of the fan has no dispatch) and its operating costs and CO2 total are zero; its results hold the investment decisions and the investment and fixed costs (see Costs of a run of several solves). The same holds for a rolling solve: only the realized steps of each roll (up to rolling_solve_jump) are reported, never its look-ahead. This does not depend on the model parameter output_horizon — the unrealized rows are only in its separate raw dump, below (see also How to use stochastics).
With output_horizon = yes (a debug option), output_parquet/<scenario>/horizon/ holds a raw LP dump: one parquet per LP variable and constraint, named as in the model (v_flow.parquet, v_invest_p.parquet, nodeBalance_eq.parquet, …), with every row of every solve — realized and unrealized (forecast branches, rolling look-ahead) alike — appended over the solves and rolls. Columns: the LP index columns (p, n, d, t, …), solve, roll, realized (true for the rows that also reach the normal results; empty for a variable or constraint without a period index), and value + reduced_cost for a variable or dual for a constraint. The values are raw solver values: primal values per unitsize as in the LP, duals and reduced costs only divided by scale_the_objective (HiGHS sign convention, probability and time weights included, empty for a MIP solve) — no unitsize, price or cost conversion. A solve decomposed with Benders is not dumped. The dump keeps every constraint's index until the solve's outputs are written, so it raises peak memory on large models.
- Units in the files
- Costs
- Cost by entity
- Prices
- Energy flows
- Capacity factors
- Energy balance in nodes
- Unit online and startup
- Connection online and startup
- Unit curtailment and VRE potential
- Node group results
- Flow group results
- Capacity and investment results
- CO2 emissions
- Reserves
- Inertia and non-synchronous generation
- Ramps
- DC power flow
- Slack and penalty values
- Plotted data as CSV
Units in the files
The unit of measure of every value column is written with the results; it follows the convention of Units of measure (a column without a unit, such as a share, says per unit).
- Excel, a sheet with several header rows (a table with two or more column levels, such as
unit__outputNode__dtwith the levelsunitandnode; a scenario comparison adds thescenariolevel, so most of its sheets are such sheets): the last header row isunit of measureand gives the unit of each column right above its data. A sheet of a table without declared units gets nounit of measurerow.pd.read_excel(path, sheet_name=..., header=[0, 1, 2], index_col=[0, 1])reads such a sheet back, with one more header row than the table has column levels. - Excel, a sheet with one header row (for example
annualized_costs__d,node_state__dt, and in a scenario comparison a table such ascosts_discountedwhose only columns are the scenarios): the header row atA1holds the column names, and the data is an Excel Table namedtbl_<sheet name>(characters other than letters, digits,_and.become_). The row index values are repeated on every row, not merged. To the right of the Table, one empty column apart, a small block gives the units:all value columnsand the unit when every value column has the same unit, else one line per value column. Power Query reads the Table (Excel.Workbook(...){[Item="tbl_annualized_costs__d", Kind="Table"]}[Data]), so the unit block does not enter the query.pd.read_excelreads the whole sheet, the unit block included: passusecolswith the Table's columns, or drop the columns after the first empty one. - Excel, the first sheet
Units: one row per result sheet with the sheet name (a link to the sheet), the table name, the unit(s) (a table whose units differ by column lists them with(by column)), the semantics (instantaneous, annualized, discounted, …) and a description._output_metadatafollows with one row per value column (unit, semantics, tooltip, formula; in a scenario comparison the scenario of the column in its ownscenariocolumn). The header cells carry no comments: LibreOffice cannot save a workbook with them. - CSV: the CSV files are written as before;
units.csvnext to them has one row per value column:file,position(the 0-based column of the file),header_1,header_2, … (the labels of the column in the file's header rows, empty below the file's last header row),unitandsemantics.summary_solve.csvhas one row (M CUR) for its costs.datapackage.jsonis a Frictionless descriptor of every CSV file: the field names are the labels of a column joined with|as a Frictionless reader joins them (coal|east; a label equal to the one above it is a merged cell and is not repeated, so the index columnperiodof a file with three header rows isperiod|), a file with several header rows says so in itsdialect(headerRows,headerJoin; header rows that are blank in every column, as in a wide file without value columns, arecommentRows), and each value field has itsunit.frictionless validate datapackage.jsonpasses. The namesunits.csvanddatapackage.jsoncannot be given to a result in therenamesection of the output settings.
Nodes with their own unit of measure. When a node sets unit_of_measure (see
Units of measure), the units of its results follow it in every
file: a flow into a water node in m3 is m3/h, its storage m3, its annual flow m3/a, its price CUR/m3. A
sum over nodes of different units (a nodeGroup's flows over an electricity and a water node, a connection's net flow
and losses between them) is labelled Mixed units of measure!. Reserves, inertia, the non-synchronous share and
the capacity margin stay in MW. A table whose rows are of different units (unit_capacity__d,
connection_capacity__d, node_capacity__d, the nodeGroup and flowGroup tables) gets the units of each row: the
Excel Table ends in a column unit of measure with the row's units (for the nodeGroup tables, whose columns differ,
the units of the row's columns joined, for example m3; m3/a), units.csv gets a row column and one row per
column and row entity, and the parquet footer holds them (flextool_row_units). With every node in MWh, none of
these is written and the files are as before. The scenario's parquet folder then also holds _unit_context.json
(the node units and which nodes each result follows), which the plots, the GUI and the scenario comparison read; a
run whose nodes are all MWh, or a run that writes no parquets, deletes it. A replay (flextool-write-outputs) takes
the units from the parquet footers. In a scenario comparison each scenario's columns keep that scenario's units,
and the combined comparison parquets carry them in their footer; a column whose unit differs between the scenarios
gets a note.
In the results database, a run with such a node defines the parameter node unit_of_measure (default MWh) and
writes its value for the nodes that are not MWh; the descriptions of the other parameters give the units of an
MWh node. A results database without this definition comes from runs whose nodes were all MWh.
The plot labels follow the nodes too. The labels of default_plots.yaml are unit templates: {A} is the node's
amount unit, {R} its rate, {kR} and {kA} the per-kW and per-kWh forms ({A}/h, {A} (annualized),
Currency/{A}, CUR/{kR}). For MWh nodes they read as before (MWh/h, MWh (annualized), CUR/kW). With a node
in another unit each subplot gets the unit of its nodes; a plot that sums, stacks or averages nodes of different
units says Mixed units of measure!, and lines of several units list them (MWh/h; m3/h). The dispatch plots'
y-axis is m3/h for a water node and MWh/h by default. A label without tokens in your own plot_settings.yaml
(or in a copy of the old default settings) is shown as written and does not follow the nodes.
Costs
-
modelentitycost_annualizedparameter — M[CUR] (millions of user-chosen currency); annualized total cost broken down by category:- unit investment & retirement — M[CUR] cost of investing in unit capacity or salvage from retiring it
- connection investment & retirement — M[CUR] same for connections
- storage investment & retirement — M[CUR] same for node states (storages)
- fixed cost pre-existing — M[CUR] fixed O&M for pre-existing capacity
- fixed cost invested — M[CUR] fixed O&M for newly invested capacity
- fixed cost reduction of divestments — M[CUR] fixed O&M removed by divestments
- commodity cost / commodity sales — M[CUR] cost of buying / revenue from selling commodities
- co2 — M[CUR] cost of CO2 emissions caused by commodities with CO2 content, net of the removal credit (negative cost) for flows into the same priced group's node(s)
- other operational — M[CUR] other variable O&M costs
- starts — M[CUR] start-up costs
- ramps — M[CUR] ramp costs: the cost of changing the flow of units with a
ramp_cost - upward / downward slack penalty — M[CUR] cost of involuntary demand reduction / increase
- inertia slack penalty — M[CUR] cost of not meeting the inertia constraint
- non-synchronous slack penalty — M[CUR] cost of not meeting the non-synchronous constraint
- capacity margin penalty — M[CUR] cost of not meeting the capacity margin constraint
- upward / downward reserve slack penalty — M[CUR] cost of not meeting the reserve constraint
The annualized investment & retirement, fixed cost invested and fixed cost reduction of divestments are the yearly amounts of every vintage and retirement in effect in the period: an investment adds its annuity (the
invest_costannuity of the period the capacity starts to operate) and its yearly fixed cost in every period it operates within the run (within itslifetime, or every later period withreinvest_automatic); a retirement subtracts its salvage annuity in every period from the retirement untillifetimelater, and the yearly fixed cost of the retired capacity in every period the retirement counts in (until the end of life of the existing capacity). They are reported only for capacity whose investment cost is reported (see the next section). For these columns the annualized value is therefore not the discounted value divided by the period's discount factor, which holds for the other columns. -
modelentitycost_tparameter — [CUR] (currency, not millions) same categories as above but per timestep: the cost incurred during the step, with the step duration and timestep weight applied but not scaled to a full year (no investment / fixed terms). The CSV / parquet outputcosts__dthas the same values and unit. modelentitycost_discounted_solveparameter — M[CUR] costs for the solve considering discounting and years represented (NPV currency)modelentitycost_discounted_totalparameter — M[CUR] same, totalled over all realised periods- table
years_represented__d(not in the results database) — [a] the years each period represents; an annualized per-period value times these years gives the period's total over the horizon - table
period_share_of_year__d(not in the results database) — [per unit] for each realised period, the share of a year it covers (its complete hours / 8760, in the solve that realises it): the annualized per-period (…__d) results (flows, energy, annualized costs, emissions, start-ups) are the period's weighted sums over its time steps divided by this share; capacities, prices, averages, ratios and discounted costs are not scaled. A 48-hour period has 0.005479, so its annualized per-period values are 182.5 times its 48-hour sums; a full-year period has 1. Results saved before this version have no such table.
Costs of a run of several solves
When model.solves holds several solves (a chain of investment solves, an investment solve and its dispatch
solves, a stochastic solve and its realization solve), the reported costs are those of the run as a whole, the same
as one solve over the same periods with the same decisions would report:
- One discount base, the start of the run. Every discounted cost counts its years from the first period of the run, not from the first period of the solve that reports it.
- Each realized period is reported once, by the solve that realizes it: its operating and penalty costs come from that solve's dispatch, except the capacity margin penalty.
- The capacity margin penalty (and its slack,
slack__capacity_margin__d) of a period is that of the solve that decided the period's investments, also when a later dispatch solve realizes the period (a dispatch solve has no capacity margin). The forecast branch periods of a stochastic fan are not realized, so their shortfalls and penalties stay out of the costs;capacity_margin__dhas them. - New capacity is reported in the period where it starts to operate (the decision period without a
construction_lead_time; during the lead time it is onlyin_construction, at no cost). Its investment cost (the annuity over the years it operates within the run, at theinvest_cost,lifetimeanddiscount_rateof that period) and the fixed cost over the same years come once, from the solve that decided it, and are reported only once a solve realizes the period it starts to operate in. Capacity carried from an earlier solve is therefore not charged again as existing capacity. - Retirements are reported in the retirement period: the salvage annuity over the years from the retirement
until
lifetimelater, and the fixed cost saved in the periods the retirement counts in. The fixed cost of existing capacity (fixed cost pre-existing) is that of the capacity given inexisting, aged over its lifetime, in every period of the run, also in the last solve's look-ahead periods that no solve realizes (the fixed cost of new capacity covers them too). - Inside a stochastic fan the investment cost and the fixed cost of the new capacity that operates in the fan's
branch periods are reported, weighted by each branch's probability weight, at the fan's period label of the
period the capacity starts to operate in (with or without a
construction_lead_time, and also without a realization solve). A shared investment (stochastic_invest_methodnone) is one decision whose branch weights sum to 1, so it is reported at its full amount; underrecourseeach forecast's own investment is reported times its branch weight (the expected investment, as in the objective). Retirements in the branch periods of a recourse fan are not reported. - In a stochastic solve with a realization solve, the investments come from the stochastic solve and the operations and the fixed cost of existing capacity from the realization solve. A fan without a realization solve reports no operating or penalty costs; the fixed cost of existing capacity is reported in the fan's period labels.
Every solve also discounts its own objective from the start of the run (as one solve over the same periods
does), so the printed objective of each solve, the cumulative CO2 cap price and the investment duals
(invest_marginal, the value of one more MW of capacity) of every solve are on the run's discount base. The
printed objectives still cover each solve's own horizon (its look-ahead periods too), so cost_discounted_total is
not the sum of the solves' objectives. The CO2 price of a period is its dual divided by the discount factor of the
solve that realizes the period. A CSV-dump reload has no run-level cost data: its reported costs are discounted
from the start of the run with the solve's own factors over the solve's own periods, and its annualized
investment, retirement and fixed costs of new and retired capacity are 0 (a warning is printed).
Cost by entity
The system Costs above are also broken down per entity at the period level, so you can see which unit, connection or node drives each cost category. Six result tables are produced — one for each entity type (unit, connection, node) in each of two flavours:
- annualized — M[CUR/a], scaled to a full year of operation (the same basis as
cost_annualized) - discounted — M[CUR], net present value over the horizon (the same basis as
cost_discounted_total)
Each table is indexed by (period, entity) — the entity level is named after its type (unit, connection or node) — and carries one column per cost category:
unitentitycost_annualized/cost_discountedparameters — per-unit cost break-downconnectionentitycost_annualized/cost_discountedparameters — per-connection cost break-downnodeentitycost_annualized/cost_discountedparameters — per-node cost break-down
The categories mirror the system Costs decomposition, restricted to those that apply to the entity type:
- commodity cost / commodity sales — M[CUR] cost of buying / revenue from selling commodities
- co2 — M[CUR] cost of CO2 emissions caused by the entity's flows
- other operational — M[CUR] other variable O&M costs
- starts — M[CUR] start-up costs (units)
- ramps — M[CUR] ramp costs (units with a
ramp_cost) - investment / retirement — M[CUR] cost of investing in unit / connection capacity or salvage from retiring it (named storage investment / storage retirement in the
nodetable, where the capacity is node-state storage energy) - fixed cost pre-existing / fixed cost invested / fixed cost reduction of divestments — M[CUR] fixed O&M for pre-existing, newly invested and divested capacity
- in the annualized tables, investment, retirement, fixed cost invested and fixed cost reduction of divestments are the yearly amounts in every period the capacity operates or the retirement applies, as in the system Costs
- upward slack penalty / downward slack penalty (
nodetable only) — M[CUR] cost of involuntary demand reduction / increase at the node
Attribution rules (matching the LP objective):
- Fuel / commodity cost is attributed to the consuming process — the unit or connection that draws the commodity — not to the source node.
- CO2 cost for a process is the sum over every priced group the process touches, so a flow that contributes emissions to several priced groups carries the full cost of each. A flow into a priced group's node (removal) carries a negative cost (credit) the same way.
By construction these tables are purely additive collapses of the same per-entity intermediates the system summary uses, so summing any category over all entities reproduces the system summary: the unit + connection + node contributions for a given category and period add up to the matching cost_annualized / cost_discounted_total category value (annualized_costs_d_p / costs_discounted_d_p in the parquet store).
These are period-level tables only — there are no per-timestep per-entity cost tables.
Prices
nodeentityprice_tparameter — [CUR/MWh] the cost of one more MWh of demand at that node and time step (the dual of the node's energy balance; positive when more demand would cost more), for every node that maintains an energy balance. It is in the price base of the cost inputs: the inflation / discounting of the period and the years it represents are removed, and it is not weighted by the timestep weight (timeset_weights, representative-period weights) or by the branch probability — in a stochastic solve each forecast branch reports its own price, not its probability-weighted contribution (the cost outputs per step are probability-weighted, so price × flow is not the branch's cost row). A step that the objective does not weigh (e.g. a step thattimeset_weightsleaves out) has no price (empty). A node of a group with a coarsernew_stepduration(and abind_intraperiod_blocksstorage node) has one balance per coarse step (per timeset block); its price is that balance's dual, the same at every time step of the coarse step when the steps have the same weight. Within a coarse step the energy may be moved freely between time steps, so for such a node the hour at which a slack (or any flow that only the coarse balance limits) is reported is one of several equally good choices; with a time-varying penalty the solver picks the cheapest hour. For a storage node the price is the value of stored energy.- table
reserve_prices_dt_ppg(group__reserve__upDown, not in the results database) — [CUR/MWh] the cost of one more MW of reserve requirement held for one hour (in the same price base and with the same weights removed asprice_t): the sum of the duals of the group's requirement constraints at the step (the timeseries requirement, the dynamic requirement and the large-failure requirement of every failing unit or connection), i.e. the cost of one more MW of every requirement of the group at once. Reported for every group with a requirement. When the requirement is not met it equalspenalty_reserve. - table
co2_price_period_d_g(group, not in the results database) — [CUR/tCO2] shadow price of the per-period CO2 cap (nominal currency, sign-flipped so a binding cap shows positive). In a rolling solve where a period's hours are split across more than one roll, the reported price is the hour-weighted average of the rolls that realize it (weighted by the hours each roll keeps of the period). In a stochastic solve each scenario path reports its own price, not its price times the path's branch probability. - table
co2_price_total_d_g(group, not in the results database) — [CUR/tCO2] shadow price of the cumulative CO2 cap, per tonne over the represented years (NPV currency, broadcast across periods)
Energy flows
unit__nodeentityflow_annualizedparameter — [MWh] cumulative flow from the node (if node is input) or to the node (if node is output), annualized to a full yearunit__nodeentityflow_tparameter — [MW] flow at each timestepconnection__node__nodeentityflow_annualizedparameter — [MWh] annualized cumulative flow through the connection (left-to-right is positive)connection__node__nodeentityflow_tparameter — [MW] flow through the connection at each timestep- table
connection_losses_d_eee(not in the results database) — [MWh] annualized losses on the connection - table
connection_losses_dt_eee(not in the results database) — [MW] losses on the connection at each timestep
Optional output: output_connection_flows_separate
connection__node__nodeentityflow_to_first_node_annualizedparameter — [MWh] annualized cumulative flow only to the left (first) nodeconnection__node__nodeentityflow_to_second_node_annualizedparameter — [MWh] annualized cumulative flow only to the right (second) nodeconnection__node__nodeentityflow_to_first_node_tparameter — [MW] flow to the left (first) node at each timestepconnection__node__nodeentityflow_to_second_node_tparameter — [MW] flow to the right (second) node at each timestep
Capacity factors
unit__nodeentitycfparameter — [per unit] average capacity factor of the flow: time-average of flow [MWh/h] divided by the unit's capacity [MW], which is on its output side; so the input cf of a unit at full load is 1/efficiency. One table per direction (unit_cf__inputNode,unit_cf__outputNode).connection__node__nodeentitycfparameter — [per unit] average capacity factor of the absolute flow (flows in either direction count as utilization), divided by connection capacity
Energy balance in nodes
nodeentitybalanceparameter — [MWh] annualized period sum of all balance contributions: From units, From connections, To units, To connections, Self discharge, Loss of load (upward slack), Excess load (downward slack), Inflownodeentitybalance_tparameter — [MW] same categories per timestep. Self discharge is the energy the storage lost in the step (the loss the optimisation used) divided by the step length; the loss of a coarse step (new_stepdurationgroup) or of abind_intraperiod_blocksblock is spread evenly over its time steps.nodeentitystate_tparameter — [MWh] storage state of the node at each timestep (state nodes only; multiplied byentity_unitsizeso the value is in storage MWh, not the scaled LP variable)
Unit online and startup
unitentityonline_tparameter — [count] number of units online at each timestepunitentityonline_averageparameter — [count] average online status over the period (weighted by step duration)unitentitystartup_cumulativeparameter — [count] startups during the period scaled to a full year (weighted byrp_cost_weightto handle representative periods correctly)unit__nodeentitystartup_energy_tparameter — [MWh] energy that the unit's start-ups take from the node in each timestep (unit__inputNodestartup_input_methodstartup_and_input or startup_only:startup_energy× MW started)unit__nodeentitystartup_energy_annualizedparameter — [MWh] the same summed over the period and scaled to a full year. The start-up input is also in the unit's input flow from the node (flow_t,flow_annualized), the commodity cost, the CO2 emissions and the node balance; a startup_only arc has no other flow
Connection online and startup
Only for connections with a startup_method:
connectionentityonline_tparameter — [count] units of the connection online at each timestepconnectionentityonline_averageparameter — [count] average online status over the period (weighted by step duration)connectionentitystartup_cumulativeparameter — [count] startups during the period scaled to a full year (as for units)
Unit curtailment and VRE potential
For VRE units (units that use an upper_limit profile) the model also reports curtailment and potential generation against each output node:
- table
unit_VRE_potential_outputNode_dt_ee(not in the results database) — [MW] potential VRE generation at the timestep (capacity × profile) - table
unit_VRE_potential_outputNode_d_ee(not in the results database) — [MWh/a] potential VRE generation summed over the period's time steps (with the timestep weights) and scaled to a full year, like the flows unit__nodeentitycurtailment_tparameter — [MW] potential minus actual generation at the timestep- table
unit_curtailment_outputNode_d_ee(not in the results database) — [MWh/a] curtailed energy summed over the period's time steps (with the timestep weights) and scaled to a full year, like the flows; the total plot multiplies each period by its years represented - table
unit_curtailment_share_outputNode_dt_ee(not in the results database) — [0–1] timestep curtailment / potential unit__nodeentitycurtailment_shareparameter — [0–1] period curtailment / period potential
Node group results
group entities with print_indicators = yes produce indicator tables aggregating flows / inflows / slacks over their member nodes:
groupentityindicator(period table) — gives a set of indicators for allnodemembers. Each share is a ratio of two energies summed over the period, weighted by the time-step weights:share = Σ_t w·X / Σ_t w·Y(never a sum or an average of the time-step shares):- Loss of load share — [0–1] upward slack ÷ inflow over the period
- VRE share of demand — [≥ 0] VRE generation into the member nodes ÷ inflow over the period (can exceed 1 when VRE is exported out of the group)
- Excess load share — [0–1] downward slack ÷ inflow over the period
- Curtailed VRE of demand — [≥ 0] curtailed VRE ÷ inflow over the period (can exceed 1 when the curtailment is larger than the demand)
- Annualized inflow — [MWh] sum of
inflowto member nodes scaled to a year - Curtailed VRE of potential VRE — [0–1] curtailed VRE ÷ potential VRE generation over the period, both on the arcs of the VRE units with an
upper_limitprofile into the member nodes (the group sum ofunit_curtailment_outputNodeandunit_VRE_potential_outputNode)
- table
nodeGroup_g_p(group_node__horizon, not in the results database) — the same indicators over the whole horizon: each share is the ratio of the energies over all periods, each period's weighted sum scaled to a year and multiplied by its years represented (Σ_d y·Σ_t w·X / share_d ÷ Σ_d y·Σ_t w·Y / share_d), not an average of the period shares; Inflow (horizon total) [MWh] replaces the annualized inflow. In a stochastic fan each branch period counts with its years, so the horizon total adds up all the paths - table
nodeGroup_gdt_p(timestep table, not in the results database) — exposes the timestep-level building blocks of the above: Loss of load [MWh/step], VRE generation [MWh/step], Excess load [MWh/step], Curtailed VRE [MWh/step], Timestep inflow [MWh/step], Curtailed VRE of potential VRE [0–1], Annualized inflow [MWh], VRE share of demand [0–1] groupentityVRE_share_tparameter — [≥ 0] share of inflow served by VRE sources at each timestep (one column per node group; 0 at a time step without inflow)- table
nodeGroup_VRE_share_d_g(not in the results database) — [≥ 0] VRE share of the period's inflow: VRE generation ÷ inflow, both summed over the period with the time-step weights (equals VRE share of demand above) - table
nodeGroup_VRE_share_g(group_node_VRE_share__horizon, not in the results database) — [≥ 0] VRE share over the whole horizon: the total VRE energy ÷ the total inflow, each period scaled to a year and multiplied by its years represented groupentityflow_annualizedparameter — [MWh] annualized sum ofinflowto member nodesgroupentityflow_tparameter — [MWh/step] timestep inflow to member nodes
group entities with print_dispatch = yes produce a multi-column dispatch table over the group:
groupentitysum_flow_tparameter — [MWh/step] dispatch decomposed by(type, item)wheretypeis one of: slack (upward/downward), from_unit, from_unitGroup, to_unit, to_unitGroup, from_connection, from_connectionGroup, to_connection, to_connectionGroup, inflow, internal_losses (units/connections/storages)groupentitysum_flow_annualizedparameter — [MWh] same decomposition, annualized period totals
Flow group results
flowGroup entities whose flow_aggregator is standalone_aggregator_only or both produce aggregate flow statistics over their member (process, node) legs (listed via flowGroup__unit__node / flowGroup__connection__node):
group_flow__d.csv— per-(flowGroup, period) totals:cumulative_flowparameter — [MWh/a] sum of |flow| over the period for all member legs, with the time-step weights, scaled to a full yearaverage_flowparameter — [MW] average power (cumulative_flow/ 8760 h)
group_flow__dt.csv— per-(flowGroup, period, time) signed net flow [MW] (sign convention: flow into the group's nodes is positive, flow out of them is negative). Intended for spreadsheet post-processing; because the values are signed they do not stack, so this series is not meant for stacked dispatch plots.
(For member flows shown as aggregated bands inside a node group's dispatch table, set flow_aggregator to dispatch_plots_only or both instead — see Node group results and the reference.)
Capacity and investment results
-
unit,connection,nodeentitiescapacityparameter — [MW or MWh] decomposed into:- existing — capacity assumed at the start of the period
- invested — capacity that comes online in the period (without a construction lead time, the capacity invested in that period)
- in_construction — capacity decided but not yet operating in the period; always present, empty without a construction lead time
- divested — capacity the model decided to retire at the start of the period
- total — capacity in service:
existing + invested − divested, cumulated over the periods; capacity in construction is not included
In combined solves (e.g. a chain of solves, or invest then dispatch), each realized investment is counted once, in the period it comes online.
Period axis covers every period in which an investment or a divestment decision occurs (both the invest-eligible and divest-eligible periods feed into
d_realize_invest), so divest-only periods are visible even when no new capacity is added. -
unit,connection,nodeentitiesinvest_marginalparameter — [CUR/kW, CUR/kWh for storage nodes, asinvest_cost] effective dual of the investment decision: zero means the model is at the unconstrained optimum; positive means an active upper bound (per-entity, per-group, period, total or cumulative caps are summed automatically); negative means the entity is not built and is unprofitable to build (the v_invest column's own reduced cost) — this is the ordinary reading for an unbuilt entity, not an artefact; a numerical artefact instead looks like a non-zero value on a built (or at-bound) entity, or an inconsistent sign across these regimes, and is worth investigating
CO2 emissions
- table
CO2__(not in the results database) — [Mt] horizon-total CO2 across all groups and units (usesyears_represented_dto convert annualized emissions to horizon totals). In a stochastic solve with more than one scenario path this is reported per path (one column per path) instead of pooling every path's emissions into one number — the anchor's shared pre-reveal history and each forecast branch's own periods are alternative futures, not consecutive years to add together. groupentityCO2_annualizedparameter — [t/yr] annualized CO2 emissions caused (or removed) by units and connections in the groupunitentityCO2_annualizedparameter — [t/yr] annualized CO2 emissions of the unit's flows, by commodity and node
Reserves
unit__reserve__upDown__nodeandconnection__reserve__upDown__nodereservation_tparameter — [MW] reserve provision at each timestepunit__reserve__upDown__nodeandconnection__reserve__upDown__nodereservation_averageparameter — [MW] the time-step-weighted average MW over the period's realized steps: Σ_t w·Δt·MW / Σ_t w·Δt withwthe time-step weight (representative periods weigh by the base periods they stand for). The steps of a solve that left reserves out (reduce_system_detail_method) are left out of both sums, not counted as 0 MW. This table does not multiply byreliability; the group tables below do.group__reserve__upDownentityrequirement_tparameter — the reserve requirement of each reserve, direction and group at each time step (reserve__upDown__group__dt, tablenodeGroup_reserve_dt_eegp), from the group's requirement constraints evaluated at the solution, by quantity [MW]:- requirement_timeseries, requirement_dynamic, requirement_n_1 — the need of each method the group has (
reserve_method; a column of a method the group does not have is left out): thereservation; the flows into and out of the group's nodes ×increase_reserve_ratioplus the group's demand × itsincrease_reserve_ratio; the largest failure, the flow of a failing unit or connection ×large_failure_ratio(one requirement per failing process; the raw parquetoutput_raw/reserve_group__<solve>.parquetnames the largest one inn_1_process) - requirement — the need of the binding requirement, the one with the largest deficit (need − counted reserve; among equal ones timeseries, then dynamic, then the failing processes by name; the raw parquet names it in
binding_methodand, for a failure,binding_process). This is not always the largest need: when no requirement is short, the binding one is the one closest to being short, and its need can be below another method's. For example, a timeseries need of 90 MW with 100 MW counted (10 MW to spare) and a large-failure need of 80 MW with 85 MW counted (5 MW to spare) give requirement = 80 MW. The largest need of the group is the largest of the requirement_timeseries, requirement_dynamic and requirement_n_1 columns - provided — the reserve of the group's providers: Σ reserve × unitsize ×
reliabilityover the units and connections that hold the reserve at a node of the group (a provider at a node counts in every group that has the node) - counted — the reserve the binding requirement counts: provided, but for a large-failure requirement without the failing process's own reserve (it cannot cover its own failure). So shortfall = requirement − counted when there is a shortfall
- shortfall — [MW] the requirement not met (the same as
slack_reserve_t)
- requirement_timeseries, requirement_dynamic, requirement_n_1 — the need of each method the group has (
group__reserve__upDownentityrequirementparameter — the same per period (reserve__upDown__group__d, tablenodeGroup_reserve_d_eegp): requirement_avg, provided_avg, counted_avg and shortfall_avg are time-step-weighted averages over the period's realized steps (Σ_t w·Δt·X / Σ_t w·Δt, the steps of a solve that left reserves out not counted), requirement_max and shortfall_max the largest step values (not weighted). Compare requirement_avg with counted_avg, not with provided_avg: with a large-failure requirement the failing unit's own reserve is in provided but not in counted. requirement_avg averages the binding requirement, which can come from a different method at different steps.- The reserve price of the group (the sum of the duals of its requirements) is
reserve_price__upDown__group__dt(tablereserve_prices_dt_ppg, see Prices). - table
nodeGroup_reserve_provider_dt_eegee(reserve__group__provider__dt, not in the results database) — [MW] the counted reserve (×reliability) of each unit or connection at each node for each reserve, direction and group at each time step; per group they add up to provided. Withreliabilitybelow 1 it is less than the reserve the unit holds (reservation_t).
A step whose solve left reserves out has empty cells in these tables. A reserve held by storage with a reserve_duration adds no requirement; it shows as a lower provided. After each solve the run log prints reserve requirement check: solve ..., reserve ..., group ..., step (...): deficit ... MW, shortfall ... MW for a step where the evaluated requirement and the solver's shortfall disagree (that should not happen; on an accepted near-optimal solve it can be solver tolerance, and the line says so).
Inertia and non-synchronous generation
groupentityinertia_tparameter — [MWs] total inertia in the group of nodes at each timestepgroupentityinertia_largest_flow_tparameter — [MW] largest individual flow coming into the group of nodes withhas_inertiagroupentityinertia_unit_node_tparameter — [MWs] inertia contribution per (unit, node) at each timestep, one column per contributing(unit, node)
Ramps
A ramp is how much a unit's flow changed from the previous time step: the flow at this step minus the flow at the previous step. It is positive when the flow rose and negative when it fell. FlexTool writes it for every input and output of every unit, whether or not the unit has ramp limits.
Table (results database: unit__node ramp_t) |
Unit | Sign | Empty when |
|---|---|---|---|
unit_ramp__outputNode__dt: each unit output |
MW per time step (or the node's unit) | + the output rose, − it fell | the step has no previous step: the first step of a solve, the step after a gap in the timeline, the first step of a representative period, or a period that does not continue the previous one |
unit_ramp__inputNode__dt: each unit input |
MW per time step (or the node's unit) | + the unit took more from the node, − less | as above |
In a rolling solve, the first step of a window is compared with the last realized step of the previous window. No
column is a sum of the others. Earlier versions also had a node-level "ramp room" table, node ramp_t, with
stacked headroom levels. It is no longer produced.
From MW per step to the ramp speed. ramp_speed_up and ramp_speed_down are a share of capacity per minute
(see Units). To compare, divide the ramp by the capacity of that input or output and by the
minutes between the two step centres (60 × the average of the two step durations in hours). Example: a 500 MW
unit on hourly steps has ramp_t = −30 MW. That is 30 / 500 / (60 × 1) = 0.001, so it ramped down at 0.1 % of
its capacity per minute, or 30 / 60 = 0.5 MW per minute. With 3-hour steps the same −30 MW is
30 / 500 / 180 ≈ 0.00033. The ramp limit also counts the reserve the unit holds in that direction, so a unit can be
at its limit with a ramp_t smaller than its ramp speed allows.
DC power flow
When the network contains DC-power-flow connections / nodes (is_dc_power_flow = yes):
- table
dc_angle_dt_e(not in the results database) — [rad] voltage angle at each timestep - table
dc_angle_diff_dt_e(not in the results database) — [rad] angle difference across the connection at each timestep
Slack and penalty values
Slack uses are listed in the Energy balance in nodes section (Loss of load, Excess load and per-node slack_up / slack_down parameters with _t and annualized variants) and aggregated in Costs. Group-level slack tables:
groupentityslack_capacity_marginparameter — [MW] capacity-margin shortfall in investment periods. It is the shortfall of the solve that decided the investments: in an investment → dispatch chain it is the investment solve's, shown under the investment solve, and its penalty is in the costs (capacity margin penalty). The forecast branch periods of a stochastic fan are not realized, so this table and the costs leave out their shortfalls;capacity_margin__dhas them.groupentitycapacity_marginparameter — the capacity margin of every solve with investment periods (capacity_margin__d, tablenodeGroup_capacity_margin_d_g), per solve, group and investment period (look-ahead and forecast branch periods included, not only realized ones) at the binding stepbinding_time, the time step with the largest deficit (requirement − counted; on a group whose row is averaged over coarse steps, the first time step of the coarse step): margin [MW] (thecapacity_margin, orcapacity_margin_share× the peak), peak_demand [MW] (the peak demand of the group's nodes over the period's original timeline), demand [MW] (at the binding step), requirement [MW] (margin + demand), firm and vre [MW] (the counted capacity of firm units and of VRE units at their profile, with availability), storage_flow, unit_flow, connections_net and consumption [MW] (the flows of storage-backed units minus the slack energy of the storage nodes feeding the group, of the other units counted by their flow, of connections and of the group's consumption, signed as in the node balance), dumped [MW] (minus the downward slack), counted [MW] (their sum), shortfall [MW] (max(0, requirement − counted) at the binding step: 0 when the margin holds), penalty_cost [M CUR] (shortfall ×penalty_capacity_margin× 1000 × inflation, as in the costs) and capacity_price [CUR/kW/a] (the shadow price of the margin: what one more kW of margin would cost; it equalspenalty_capacity_marginwhile there is a shortfall, and is empty for a MIP). In the results database the binding step is not carried (the value is category → solve → period); it is in the CSV and parquet tables.groupentitycapacity_margin_contributionparameter — [MW] what each unit, connection and node counts in the group's capacity margin at its binding step (capacity_margin_entity__d, tablenodeGroup_capacity_margin_entity_d_g), by class: firm, vre, storage, storage_slack (minus the slack energy of a storage node feeding the group, per hour of the step), unit_by_flow, connection, consumption and dumped (a node's downward slack; a node that dumped nothing is left out). They add up to the columns ofcapacity_margin__d: firm → firm, vre → vre, storage and storage_slack → storage_flow, unit_by_flow → unit_flow, connection → connections_net, consumption → consumption, dumped → dumped. A row sums to counted. A unit that you expect to count but that is missing, or counts by its flow, shows which rule of the capacity margin applies to it.groupentityslack_inertia_t/slack_inertia— [MWs] inertia shortfall, timestep / annualizedgroupentityslack_nonsync_t/slack_nonsync— [MW] non-synchronous-share shortfall, timestep / annualizedgroup__reserve__upDownentityslack_reserve_t/slack_reserve— [MW] reserve shortfall, timestep / annualized; the requirement it falls short of, and what each provider counts, is in Reserves (reserve__upDown__group__dt)
For the sign / direction conventions of slacks, see Slack convention. For how block-aware solves (flex-temporal decomposition) and spatial Benders decomposition feed into these tables, see Decomposition — the result writers always operate on the fine-timeline, full-spatial solution, so the parquet tables look identical to a monolithic run.
Plotted data as CSV
With Write plotted data (CSV) in the GUI's Png settings, --write-plot-data on flextool-run,
flextool-write-outputs or flextool-scenario-results (Spine Toolbox users can add it to the tool's command-line
arguments), or a top-level write_plot_data: true in the project's plot_settings.yaml, each plot file gets a CSV
file next to it with the data drawn in it: output_plots/<scenario>/<plot>.png comes with <plot>.csv, a plot
split into several files writes <plot>_01.csv, <plot>_02.csv, … with the data of each file, and the comparison
plots and dispatch plots in output_plot_comparisons/ get theirs too. Any one of these turns it on. It is off by
default and then costs nothing.
- Rows are the x axis of the plot (the time steps, or the bars), with the row index levels (for example
period,time) as the first columns. Only the plotted time window is written, and the gap rows that leave a gap in a line at a timeline break are not. - Columns are the plotted series (lines, stacks, bars), with one header row. When a series is named by several
dimensions, its header is their values joined with
|, in the order of the result's dimensions; the dimension names themselves are not written. For example, in a comparison plot of unit output with the scenario first and the unit second, the columnbase | coal_plantis unitcoal_plantin scenariobase. A series named like one of the first columns (for example an entity calledtime) gets(series)added to its name. - A leading
subplotcolumn names the subplot of each row when the file has more than one subplot. - Values are as plotted, after
multiply_by(the unit is in the plot's axis label). For a stacked plot, each column is that series' own value, not the stacked total, so do not add the columns up again. Where a line is drawn as the sum over a dimension the plot does not show, the column is that sum. A cell that is empty in the results stays empty, also where a bar plot draws it as zero. - A dispatch plot's CSV has a
periodand atimecolumn for the steps of the plotted time window (the periods one after another), the flows, and the demand line as aDemandcolumn, as in the legend (Demand (line)if a flow is already calledDemand).dispatch_data.xlsx(scenario comparison with--write-dispatch-xlsx) has one sheet per dispatch plot with the sameperiodandtimecolumns, a value on every row. - If a CSV cannot be written, the log has a warning naming the file, and the plots are still made.
- In
plot_settings.yaml, onlywrite_plot_data: true(no quotes) turns this on; a quoted"true"or any value that is nottrueorfalseleaves it off with a warning.
The CSVs are removed with the plots when the plots are made again.