Skip to content

Capability matrix

Property-by-property support (every input, node, tech, transmission/trade and output property × target, plus each framework's native attach points) is catalogued in the capability reference; this page summarizes the feature-level matrix that validation enforces.

Each feature maps to a first-class schema field or a run mode. ValidateFor rejects (422) any payload that uses a feature the selected target doesn't support. Support is a sparse map — absence means unsupported.

Feature key Schema field / concept PyPSA Calliope 0.7 Calliope 0.6 AdOpT Enforced
mode:plan experiment.mode ✓ ✓ ✓ ✓ yes
mode:operate experiment.mode (fixed caps; PyPSA = rolling horizon) ✓ ✓ ✓ yes
mode:alternatives experiment.mode (PyPSA = MGA, Calliope = SPORES) ✓ ✓ ✓ yes
mode:pareto experiment.mode ✓ yes
mode:monte_carlo experiment.mode ✓ yes
mode:stochastic experiment.mode + experiment.scenarios (two-stage, n.set_scenarios) ✓ yes
time_aggregation experiment.time_aggregation (Calliope 0.7: resample only; 0.6: resample + typical_days k-means; AdOpT: typical_days only) ✓ ✓ ✓ yes (per method)
multi_carrier_conversion technology.flows (rigid ratios on every target; 0.6 = conversion_plus tiers, one input) ✓ ✓ ✓ yes
emission_limit model.emission_limits ✓ ✓ ✓ ✓ yes
import_export model.trade ✓ ✓ ✓ ✓ yes
committable operation.committable (Calliope = MILP integer-unit subset) ✓ ✓ ✓ yes
node_override technology.node_overrides ✓ ✓ ✓ ✓ yes
linear_constraint model.constraints (PyPSA: GlobalConstraint or run.py sidecar; 0.6: single-term group_constraints) ✓ ✓ ✓ yes
area_constraints technology.area ✓ ✓ yes
source_constraints technology.source ✓ ✓ yes
indexed_params multi-valued Value ✓ yes (multi-valued only)
piecewise_performance performance.type: piecewise (Calliope: fixed-capacity conversion) ✓ yes
physics_performance performance.type: physics ✓ yes (needs precomputed off AdOpT)
power_flow experiment.mode: power_flow + node.electrical (v_nom, shunt), transmission.electrical / transmission.dispatch, technology.dispatch, technology.electrical (reactive power, voltage control) ✓ yes (mode gate; the inputs are ignored with a warning outside the mode)
linear_power_flow experiment.network.model: linear_power_flow (+ security.outages) ✓ yes (rejected like a mode elsewhere)
soc_profile storage.soc_profile ✓ ignored with a warning elsewhere
unit_commitment_extras operation.ramp_start_up/ramp_shut_down/standby_cost/initial_uptime/initial_downtime ✓ ignored with a warning elsewhere
quadratic_cost costs.<class>.variable_om_quadratic ✓ ignored with a warning elsewhere; MIQP needs gurobi
multi_period model.periods with years ✓ ignored with a warning elsewhere (single horizon)
custom_math experiment.custom_math → additional_math.yaml ✓ rendered (linear IR + emission limits)
objective:min_emissions experiment.objective ✓ ✓ yes

Enforcement nuances. Modes route through modeFeature(mode). Every claimed cell is backed by a runnable scenario (TestCapabilityCoverage fails the build otherwise; calliope/indexed_params is the one named exception, covered by a unit test). PyPSA operate requires fixed capacities and rejects horizon-wide constraints; PyPSA stochastic validates that every scenario override translates to a component attribute. Calliope committable is the MILP subset (integer units + min stable load) — start/stop costs and min up/down times are rejected with a pointer to native.calliope. AdOpT accepts only techs that map onto its technology database (pv/wind/heat_pump physics, storage); anything else is rejected with a pointer to node-level native.adopt-net0. power_flow is claimed by PyPSA alone and is both a mode and a field family: in mode power_flow the electrical fields become buses with v_nom and set-points, Lines, Transformers, Shunts and controlled Links solved by lpf()/pf(); wherever no power flow runs — Calliope, AdOpT, PyPSA's optimization modes — every power-flow input (dispatch, the electrical blocks) is ignored and named in one warning while the arcs stay transport Links, so one payload serves the planning call and the power-flow call (ADR-0008, ADR-0009). Two kinds of PyPSA-only claims follow: a run specification a transport target cannot honor (linear_power_flow) is rejected there like a mode, because solving the same payload as a transport model would be a different problem; an ignorable field (soc_profile, unit_commitment_extras, quadratic_cost, multi_period) is kept and named in a warning, because the optimizer solves the same problem without it (ADR-0010, ADR-0011). Fields other targets honor and PyPSA used to reject — independent storage rates, depth of discharge, continuous retirement, unmet-demand slack, copperplate — are honored now through the Store representation, a retirable split component, slack generators and a copperplate hub. physics_performance off AdOpT requires a performance.precomputed series (else 422). custom_math is a Calliope 0.7 render toggle (experiment.custom_math, default on) for the portable constraints/emission_limits IR; ratio-pinning and piecewise math render regardless (they are model physics, not user math). Calliope 0.6 has no custom math: constraints and emission limits become group_constraints, which bound one quantity (capacity, production, consumption, storage capacity or a cost class) over a tech/node set — so only single-term constraints are accepted there (the coefficient folds into the bound); multi-term constraints, multi-input flows, time.weights, an export without a limit, indexed parameters and piecewise performance are rejected with a pointer to the 0.7 target.

Canonical → framework mapping (selected)

Canonical PyPSA Calliope 0.7 (calliope) Calliope 0.6 (calliope-0.6) AdOpT-NET0
node + carrier one Bus per (node, carrier) orthogonal nodes / carriers locations / carriers orthogonal nodes / carriers
role: supply Generator base_tech: supply parent: supply (supply_plus when source.cap is set) Res / physics tech
role: conversion + flows multi-output Link (bus2/efficiency2) carrier_out list + ratio-pinning math parent: conversion_plus, carrier_out_2/_3 tiers, carrier_ratios relative to the primary output ✗ rejected (multi_carrier_conversion unclaimed)
role: storage StorageUnit base_tech: storage parent: storage (one energy_eff for both directions, model-wide run.cyclic_storage) Storage
transmission Link across buses transmission tech (link_from/link_to) transmission tech placed in links.<from>,<to> (+ distance) ✗ rejected (Networks unmapped)
time series per-attribute CSVs data_tables (table, add_dims.inputs) + CSVs file=<file>.csv:<node> references into timeseries_data/ (demand negative) per-node CSV columns
overnight cost + lifetime/interest_rate annualized capital_cost (CRF) cost_flow_cap + cost_interest_rate (annualized internally) costs.monetary.energy_cap + interest_rate + constraints.lifetime (both required by 0.6; lifetime 1 / rate 0 when absent or already annualized) economics block
carrier co2_intensity carrier co2_emissions per-tech cost_flow_in[costs=co2] on consumed fuels + objective_cost_weights[co2]=0 costs.emissions.om_con on consuming techs + run.objective_options.cost_class.emissions: 0 emission factors
emission_limits global_constraints.csv (primary_energy) additional_math.yaml constraint on cost[costs=co2] group_constraints.<name>.cost_max.emissions emission targets
constraints (linear IR) GlobalConstraint or _constraints.json additional_math.yaml, registered via config.init.math_paths/extra_math (sum(flow_cap[techs=pv]) <= …, one member per dim; multi-member sets expand into a sum) single-term → group_constraints (energy_cap_*, carrier_prod_*, carrier_con_*, storage_cap_*, cost_*) ✗ rejected (linear_constraint unclaimed)
experiment.mode run.py branches: plan → optimize, operate → rolling horizon, alternatives → MGA, stochastic → scenarios, power_flow → pf/lpf config.init.mode: plan→base, operate→operate, alternatives→spores (+ config.solve.spores) run.mode: plan / operate (+ run.operation window/horizon hours) / spores (+ run.spores_options, systemwide_cost_max group, spores_score costs) ConfigModel.optimization: pareto→objective: pareto, monte_carlo→monte_carlo.N > 0; time_aggregation: typical_days→typicaldays.N. tuned by experiment.monte_carlo.samples / experiment.pareto.points (the solver.options.monte_carlo/.pareto namespaces are deprecated and warn)
experiment.time_aggregation — config.init.resample model.time: resample, or apply_clustering (k-means, k = periods, fixed random_seed) typicaldays.N
experiment.objective: min_emissions — — objective_options.cost_class: {monetary: 0, emissions: 1} costs_emissionlimit/emissions objective
experiment.solver passed through (e.g. highs) highs→cbc (HiGHS can't drive Calliope's pyomo backend) highs→cbc forced glpk (adopt supports only gurobi/glpk)
technology (mapped) Generator/Link/StorageUnit techs entry techs entry (essentials/constraints/costs) named database tech (Storage_Battery, Photovoltaic, …) copied + economics-overridden
trade market generators (import/export) supply/demand + flow_export supply (om_prod = price) / demand (resource: -limit, force_resource: false, om_con = −price) carrier_data import/export columns
performance: physics precomputed → p_max_pu + _performance.json precomputed series precomputed series (resource, energy_per_cap) native tech type (pvlib, hydro, COP)
native native.pypsa native.calliope (flat merge) native["calliope-0.6"] (deep merge into the nested mapping) native.adopt-net0

Parameter support: shared vs framework-specific

Design rule: a concept that more than one framework supports is a first-class canonical field emitted for every applicable target; a concept only one framework has is supplied through that framework's native block on the tech or node (native.pypsa / native.calliope / native["calliope-0.6"] / native.adopt-net0), which is merged into the emitted output. A framework version with its own input dialect has its own key: native.calliope is 0.7 syntax and is never applied by the calliope-0.6 target.

All emitters honor their native block at tech and node level (PyPSA also merges it into buses/carriers/links; Calliope 0.6 deep-merges it into the nested essentials/constraints/costs mapping and applies model- and experiment-level blocks to the whole document; AdOpT deep-merges it into the tech economics and node Technologies.json, and the model-level block into ConfigModel.json). PyPSA additionally writes a whole-model native.pypsa block into a _native.pypsa.json sidecar that documents it; run.py does not read it.

Shared (canonical field → per-framework param)

Canonical field PyPSA Calliope 0.7 AdOpT
capacity.max / .min p_nom_max / (bound) flow_cap_max / flow_cap_min capacity bounds
efficiency / performance efficiency flow_out_eff efficiency
costs.investment_per_capacity capital_cost (CRF) cost_flow_cap (+cost_interest_rate) capex
costs.variable_om marginal_cost cost_flow_out variable O&M
costs.fixed_om folded into capital_cost cost_om_annual fixed O&M
costs.investment_per_energy_capacity Store capital_cost per MWh (needs storage.energy_capacity; dropped on a StorageUnit) cost_storage_cap energy capex
costs.purchase — cost_purchase purchase cost
lifetime / interest_rate annualization inputs lifetime / cost_interest_rate economics
operation.min_pu p_min_pu flow_out_min_relative min load
operation.ramp_up / ramp_down ramp_limit_up / _down flow_ramping ramping
storage.charge_eff / discharge_eff efficiency_store / _dispatch flow_in_eff / flow_out_eff storage eff
storage.self_discharge standing_loss storage_loss self-discharge
storage.initial_soc state_of_charge_initial storage_initial initial SOC
storage.cyclic cyclic_state_of_charge cyclic_storage cyclic
storage.energy_capacity / max_hours Store e_nom* with charger/discharger Links / StorageUnit max_hours storage_cap_max / _min energy capacity
carrier co2_intensity + emission_limits carrier co2_emissions + global constraint cost_flow_in[costs=co2] + objective_cost_weights[co2]=0 emission factor/target
constraints (linear IR) GlobalConstraint (unscoped) / run.py linopy constraints (tech- or node-scoped) additional_math.yaml constraints

Shared concept, partially mapped

Concept PyPSA Calliope AdOpT
Unit commitment committable, start_up_cost, min_up_time, … MILP subset: integer units + flow_out_min_relative (start/stop cost + up/down times rejected, use native.calliope) rejected (map a CONV database tech via node native.adopt-net0)
Availability / capacity factor (operation.max_pu, performance.precomputed) p_max_pu (materialized series) source_use_max data table + source_unit: per_cap RES physics techs compute from node.climate
Hydro inflow / spill_cost inflow CSV / spill_cost rejected (use native.calliope) rejected
Series prices (trade.*.price) generators-marginal_cost.csv rejected (scalar only) per-timestep price columns

Framework-specific (only that framework; use its native block)

Framework Params Supply via
PyPSA component columns without a canonical slot (terrain_factor, v_ang_min/v_ang_max, e_cyclic_per_period, …) and whole-model options; the electrical network, reactive power, Stores and the unit-commitment extras are first-class native.pypsa
Calliope flow_out_parasitic_eff, flow_in_eff_per_distance, storage_cap_per_unit, sink_use_min, area_use_per_flow_cap (0.7), source_use_equals/source_use_min (must-run) native.calliope
AdOpT unmapped techs (generic supply/conversion, GasTurbine, CHP, …), per-tech physics params, climate data native.adopt-net0 (name a real database tech + override)

native blocks are merged last, so they can also override an emitted attribute. First-class Calliope-only fields in the schema are emitted directly: technology.area.max → area_use_max, technology.source → source_use_max/source_cap_max/source_unit, experiment.custom_math; everything else Calliope-specific rides in native.calliope.

Calliope source model / forcing resource consumption. Calliope 0.7 has no force_resource flag (that was 0.6). Resource use is governed by which parameter is set: source_use_max allows curtailment; source_use_equals forces exact consumption (must-run, no curtailment); source_use_min is a lower bound. The canonical technology.source field maps to the curtailable bound:

"technologies": { "pv": {
  "role": "supply", "node": "n1", "carrier_out": "electricity",
  "source": {
    "max": 0.25,          // -> source_use_max (curtailable resource bound)
    "unit": "per_area"    // -> source_unit: per_area | per_cap | absolute
  }
}}

Native values are merged verbatim: a string naming a time series (e.g. "source_use_equals": "pv_resource") is NOT resolved against model.timeseries — Calliope would try to parse it as a number and abort. Use scalars (broadcast over timesteps via config.init.broadcast_input_data), or supply a full data_tables entry plus the CSV through the native block. Also beware source_use_equals pins consumption per timestep: combined with a time-varying demand and no storage/export it renders the model infeasible — prefer source_use_max unless must-run behavior is intended.