Capability Reference¶
What the canonical JSON schema can express, how each framework's native
passthrough works, and — property by property — what each target actually
honors. This document mirrors the enforced behavior:
- ✓ emitted — the property lands in the target's native input and affects
the solve. Every ✓ is backed by a runnable scenario
(
internal/scenarios/testdata/scenarios/); the build fails if a claimed capability loses its scenario (TestCapabilityCoverage;indexed_paramson Calliope is the one claim covered by a unit test instead). - ✗ rejected —
ValidateForreturns 422 with a pointed message. A payload is never accepted and then partially ignored. - ⚠ warning — accepted, but the property is dropped and a warning names it
in the response (
warnings: [...]). - · informational — accepted metadata with no solver effect on any target.
Targets: P = PyPSA 1.2.4 · C = Calliope 0.7.0 (target calliope, alias
calliope-0.7) · A = AdOpT-NET0 0.1.10 (pinned in
environment/requirements*.txt). Calliope 0.6.8 is a fourth,
explicitly selected target (calliope-0.6); it honors the C column except
where §3.7 says otherwise.
1. Shared canonical schema¶
One Job = model (the physical system) + experiment (how to run it).
{
"model": {
"metadata": { "name": "…", "version": "…", "description": "…", "currency": "EUR", "currency_year": 2025 },
"time": { "start": "2025-01-01", "end": "2025-01-02", "resolution": "1H", "subset": ["…", "…"],
"timesteps": ["…", …], // explicit labels (P/C; A needs a regular range)
"weights": 1 | "ts-id" }, // snapshot weightings (P; C scalar; A rejects)
"periods": ["…" | {"name":"…","year":2030,"length_years":5,"objective_weight":1}], // with years: PyPSA investment periods (§3.2)
"discount_rate": 0.07, // global; overrides per-tech interest_rate
"carriers": { "<id>": { "unit": "MWh", "co2_intensity": 0.2, "color": "…", "native": { … } } },
"nodes": { "<id>": { "coords": {"lat":48,"lon":11,"alt":0}, "available_area": 1e4,
"climate": { "ghi": "ts-id", "temp_air": 8.5, … }, "native": { … } } },
"electrical": { "v_nom": 20, "shunt": {"g": 0, "b": 0.001} }, // kV + shunt (S); the node's AC bus for a power flow (P, mode power_flow)
"timeseries":{ "<id>": { "source": "inline", "values": [ … ], "unit": "MW" } // or
{ "source": "file", "path": "…", "column": "…" } }, // file: schema-valid, but no target reads files yet -> 422
"technologies": {
"<id>": {
"role": "supply" | "demand" | "conversion" | "storage",
"node": "n1" | ["n1","n2"],
"carrier_in": "…" | ["…"], "carrier_out": "…" | ["…"],
"efficiency": 0.9, // simple 1-in/1-out factor
"flows": [ // multi-port conversion (takes precedence)
{ "carrier": "gas", "direction": "in", "reference": true },
{ "carrier": "heat", "direction": "out", "ratio": 0.45 } // per unit of reference input
],
"performance": { // beyond a single efficiency
"type": "constant" | "piecewise" | "physics",
"efficiency": 0.6, // constant
"breakpoints": [{"load":0.3,"efficiency":0.4}, …], "min_load": 0.3, // piecewise
"model": "pv" | "wind" | "heat_pump" | "open_hydro", // physics
"params": { … }, "precomputed": "ts-id" // profile for non-AdOpT targets
},
"capacity": { "existing": 100, "expandable": true, "min": 10, "max": 500,
"per_unit": 100, "units_min": 0, "units_max": 5, "unit": "MW",
"systemwide_min": 0, "systemwide_max": 700,
"decommission": { "mode": "impossible"|"continuous"|"only_complete", "cost": 12 } },
"operation": { "max_pu": 0.9 | "ts-id", "min_pu": 0.3, "equals_pu": 0.4 | "ts-id",
"ramp_up": 0.6, "ramp_down": 0.6,
"committable": true, "start_up_cost": 5000, "shut_down_cost": 0,
"min_uptime": 3, "min_downtime": 2, "max_startups": 50, "standby_power": 0.02 },
"ramp_start_up": 0.6, "ramp_shut_down": 0.6, "standby_cost": 5, "initial_uptime": 2 }, // UC extras (P; ignored elsewhere)
"storage": { "energy_capacity": {…}, "max_hours": 4, "charge_eff": 0.95, "discharge_eff": 0.95,
"max_charge_rate": 0.25, "max_discharge_rate": 0.25, // instead of max_hours
"self_discharge": 0.001, "depth_of_discharge": 0.1, "inflow": "ts-id",
"spill_cost": 5, "initial_soc": 0.5, "cyclic": true,
"soc_profile": 12 | "ts-id" }, // pinned state of charge (P StorageUnit; ignored elsewhere)
"no_simultaneous_charge_discharge": true },
"demand_profile": 100 | "ts-id", "demand_curtailable": false,
"dispatch": 3 | "ts-id", // fixed MW per timestep a power flow injects (P, mode power_flow; ignored elsewhere)
"electrical": { "reactive_dispatch": 1 | "ts-id", "power_factor": 0.95, "leading": false, // Q one way or the other
"control": "pq" | "pv", "voltage_setpoint": 1.0 }, // pv holds its bus (supply)
"area": { "max": 5000, "per_capacity": 8 },
"source": { "max": 0.25 | "ts-id", "unit": "per_area" | "per_cap" | "absolute", "cap": 900 },
"costs": { "monetary": { // only the "monetary" class is priced
"investment_per_capacity": 6e5, "investment_per_energy_capacity": 1.5e5,
"investment_per_capacity_distance": 900, // transmission only
"fixed_om": 2000, "fixed_om_fraction": 0.04, // mutually exclusive
"variable_om": 45 | "ts-id", "fuel_cost": 20, "purchase": 1000 } },
"lifetime": 25, "interest_rate": 0.05, "cost_basis": "overnight" | "annualized",
"emission_factor": 0.2, "annual_output_min": 0, "annual_output_max": 5e4,
"build_year": 2024, "active": true,
"node_overrides": { "<node>": { /* any subset of the fields above */ } },
"native": { "<target>": { … } }
}
},
"transmission": { "<id>": { "carrier": "…", "from": "n1", "to": "n2", "bidirectional": true,
"electrical": { "kind": "line" | "transformer", "r": 1.2, "x": 3.5, "r_per_length": 0.12, "x_per_length": 0.35,
"s_nom": 20, "num_parallel": 1, "b": 0.0001, "g": 0, "tap_ratio": 1, "phase_shift": 0, "max_loading": 0.9 }, // AC branch (power flow / voltage law)
"dispatch": 3 | "ts-id", // fixed transfer of a controlled link (HVDC) instead of an impedance
"lifetime": 40, "interest_rate": 0.05, "cost_basis": "overnight", // annualizes the arc's investment (P)
"capacity": {…}, "efficiency": 0.97, "distance": 50,
"loss_per_distance": 1e-4, "min_flow": 0.05, "emission_factor": 0.001,
"energy_consumption": { "carrier": "…", "per_flow": 0.01, "per_flow_distance": 1e-4 },
"costs": {…}, "active": true, "native": { … } } },
"trade": { "<id>": { "node": "n1", "carrier": "…",
"import": { "limit": 500 | "ts-id", "price": 60 | "ts-id", "emission_factor": 0.35 },
"export": { "limit": 200 | "ts-id", "price": 30 | "ts-id", "emission_factor": 0.3 },
"native": { … } } },
"emission_limits": [ { "name": "co2_cap", "carrier": "co2", "sense": "<=", "limit": 1e6, "period": "…" } ],
"constraints": [ { // portable linear IR: Σ coeff·var[scope] sense bound
"name": "…", "sense": "<=" , "bound": 800, "period": "…",
"terms": [ { "coefficient": 1,
"variable": "capacity" | "flow_out" | "flow_in" | "storage_cap" | "emissions",
"techs": ["…"], "carriers": ["…"], "nodes": ["…"] } ] } ],
"native": { "<target>": { … } } // system-wide target-specific config
},
"experiment": {
"mode": "plan" | "operate" | "alternatives" | "pareto" | "monte_carlo" | "stochastic" | "power_flow",
"network": { "model": "transport" | "linear_power_flow", "security": { "outages": ["ab"] } }, // the voltage law inside optimize (P)
"objective": "min_cost" | "min_emissions",
"emission_accounting": "net" | "positive_only", // A: emissions_net vs emissions_pos
"foresight": "perfect" | "myopic",
"time_aggregation": { "method": "typical_days" | "resample" | "cluster", "periods": 7,
"resolution": "3H", "cluster_series": "ts-id",
"keep_full_resolution_for": ["STOR", …] },
"operate": { "window": "12h", "horizon": "24h" }, // typed mode options; the legacy
"alternatives": { "number": 5, "slack": 0.1, // solver.options namespaces still
"scoring_algorithm": "integer" }, // work but warn as deprecated
"pareto": { "points": 5 },
"monte_carlo": { "samples": 25, "standard_deviation": 0.3, "on": ["technology_capex", …] },
"allow_unmet_demand": { "enabled": true, "penalty_price": 3000 },
"copperplate": false,
"reporting": { "case_name": "…", "save_logs": "…", "shadow_prices": ["…"] },
"scenarios": [ { "name": "…", "weight": 0.5, "overrides": { "<dotted.path>": 90 } } ],
"sweep": [ { "parameter": "technologies.pv.capacity.max", "values": [200, 400] } ],
"solver": { "name": "highs" | "cbc" | "glpk" | "gurobi",
"time_limit": 3600, "mip_gap": 0.01, "threads": 8, "options": { … } },
"custom_math": true, // Calliope: render constraint IR into extra math
"native": { "<target>": { … } }
}
}
Value union. Any field typed Value accepts a number (scalar), a string
(time-series id from model.timeseries, optionally "ts:"-prefixed), an
inline per-timestep array [0.1, 0.4, …] (interned into model.timeseries
under a reserved _inline: id before validation), or an indexed object
{ "data": […], "index": […], "dims": […] } (Calliope-style; multi-valued
indexed parameters are Calliope-only).
Dotted parameter paths (sweep axes and stochastic overrides):
technologies.<id>.capacity.{max,min,existing}, technologies.<id>.efficiency,
technologies.<id>.demand_profile (stochastic),
technologies.<id>.costs.monetary.{investment_per_capacity,variable_om} (sweep) /
costs.monetary.variable_om (stochastic), carriers.<id>.co2_intensity.
2. Native passthrough per framework¶
Every native block is scoped to one target ("native": {"pypsa": …} is
invisible to the others; a warning lists blocks the selected target ignores).
Because selection is by key, blocks for every target can ride in one payload
side by side — examples/payload_full.json
demonstrates this at every scope. Framework versions with different input
dialects have separate keys: native.calliope is Calliope 0.7 syntax and is
never applied by the calliope-0.6 target, which reads
native["calliope-0.6"].
Native values are merged last — verbatim, unvalidated — so they can extend
or override emitted attributes. A string naming a time series is not
resolved; supply real literals or full data-table definitions.
PyPSA (native.pypsa)¶
| Attach point | Lands in |
|---|---|
carriers.<id>.native |
extra columns on the carriers.csv row |
nodes.<id>.native |
extra columns on every buses.csv row of that node |
technologies.<id>.native |
extra columns on the component's CSV row (generators/loads/links/storage_units) |
transmission.<id>.native |
extra columns on the link row, or on the line/transformer row in mode power_flow (e.g. {"length": 12}) |
model.native |
_native.pypsa.json sidecar (not consumed by run.py — documentation payload) |
Typical native-only uses: component columns without a canonical slot —
terrain_factor on lines, v_ang_min/v_ang_max on buses,
e_cyclic_per_period on Stores — and the whole-model block. The electrical
network, reactive set-points and voltage control, Stores, the unit-commitment
extras and the quadratic cost are first-class (ADR-0008, ADR-0009, ADR-0011)
and have their rows in §3.4.
Calliope (native.calliope)¶
| Attach point | Lands in |
|---|---|
technologies.<id>.native |
keys merged into the tech's model.yaml entry |
nodes.<id>.native |
keys merged into the node's entry |
carriers.<id>.native / model.native |
merged into the respective YAML scopes |
Typical native-only uses: source_use_min (resource floor),
flow_out_parasitic_eff, sink_use_min, area_use_per_flow_cap, custom math
files. Scalars broadcast over missing dims
(config.init.broadcast_input_data: true is always emitted).
Calliope 0.6 (native["calliope-0.6"])¶
| Attach point | Lands in |
|---|---|
technologies.<id>.native |
deep-merged into the tech's techs.<id> mapping (essentials / constraints / costs), so {"constraints": {"parasitic_eff": 0.98}} extends the emitted constraints instead of replacing them |
transmission.<id>.native / trade.<id>.native |
deep-merged into the generated transmission / market tech |
nodes.<id>.native |
deep-merged into locations.<id> |
model.native |
deep-merged into the whole document (run.bigM, model.random_seed, extra group_constraints, …); experiment.native is reserved and not read |
Typical native-only uses: resource_min_use, parasitic_eff, export_cap,
energy_cap_scale, run.bigM, run.zero_threshold, additional
group_constraints (demand_share_min, net_import_share_max, …). Use 0.6
parameter names (energy_cap_max, om_prod, storage_cap), not 0.7's.
AdOpT-NET0 (native.adopt-net0)¶
| Attach point | Lands in |
|---|---|
technologies.<id>.native |
patched onto the copied database-tech JSON (via the node-keyed _meme_overrides.json) |
nodes.<id>.native |
deep-merged into the node's Technologies.json — the way to add unmapped techs ({"new": ["GasTurbine_simple"]}) |
model.native |
merged into ConfigModel.json (solver/MILP gap/scaling options) |
adopt_net0 is database-driven: only physics techs (pv → Photovoltaic,
wind → WindTurbine_Onshore_1500, heat_pump → HeatPump_AirSourced) and
storage (Storage_Battery) map canonically. Any other non-demand tech is
rejected unless introduced through node-level native.
3. Support tables¶
3.1 Experiment (run spec)¶
| Property | P | C | A | Notes |
|---|---|---|---|---|
mode: plan |
✓ | ✓ | ✓ | default |
mode: operate |
✓ rolling horizon (experiment.operate.horizon; fixed capacities enforced; no horizon-wide constraints) |
✓ receding horizon (experiment.operate.{window,horizon}; every size becomes an input: techs, lines and imports are installed at existing, else max ⚠, imports at their limit; source.cap → source_cap, area.max → area_use ⚠; ✗ when nothing defines a capacity) |
✗ | typed block wins over legacy solver.options.operate (deprecation warning) |
mode: alternatives |
✓ MGA (experiment.alternatives.slack; baseline + alternative exported) |
✓ SPORES (experiment.alternatives.{number,slack,scoring_algorithm}) |
✗ | legacy options.spores/options.mga warn |
mode: pareto |
✗ | ✗ | ✓ (experiment.pareto.points) |
|
mode: monte_carlo |
✗ | ✗ | ✓ (experiment.monte_carlo.{samples,standard_deviation,on}) |
|
mode: stochastic |
✓ two-stage set_scenarios |
✗ | ✗ | overrides must be translatable and numeric (validated) |
network.model: linear_power_flow |
✓ Lines and Transformers under Kirchhoff's voltage law inside optimize (arcs without electrical stay Links; line expansion with the impedance held fixed; max_loading → s_max_pu) |
✗ rejected like a mode | ✗ rejected | default transport keeps every arc a Link (ADR-0010) |
network.security.outages |
✓ optimize_security_constrained over the named branches (mode plan; no portable constraints) |
✗ | ✗ | the outages must be electrical arcs |
mode: power_flow |
✓ lpf() then pf() (or lpf() alone) over the electrical fields; experiment.power_flow.{method,slack,distribute_slack,tolerance}; results under output/power_flow/ |
✗ | ✗ | no optimization: fixed capacities, dispatch on every injection but the slack, no trade/constraints/emission limits/scenarios (ADR-0008) |
objective: min_cost |
✓ | ✓ | ✓ | default |
objective: min_emissions |
✗ | ✗ | ✓ | |
emission_accounting: positive_only |
✗ | ✗ | ✓ emissions_pos |
default net everywhere |
foresight |
· | · | · | accepted, not yet mapped |
time_aggregation: resample |
✗ | ✓ config.init.resample |
✗ | |
time_aggregation: typical_days |
✗ | ✗ | ✓ k-means typicaldays.N (+ keep_full_resolution_for → technologies_with_full_res) |
|
time_aggregation: cluster |
✗ | ✗ | ✗ | accepted by the model; no target claims it yet |
allow_unmet_demand |
✓ slack generator per load bus under carrier unmet, priced at penalty_price (required) |
✓ ensure_feasibility (⚠ penalty approximated by bigM) |
✓ energybalance.violation (needs penalty_price) |
|
copperplate |
✓ one free hub per carrier joins every node's bus, arcs dropped (⚠ named); not with a power flow or the voltage law | ✗ | ✓ energybalance.copperplate |
|
reporting |
⚠ output_dir service-managed |
✓ save_logs, shadow_prices |
✓ case_name |
|
scenarios |
✓ (stochastic mode) | ✗ | ✗ | |
sweep |
✓ | ✓ | ✓ | orchestrator-level: N independent runs, any target |
solver.name |
✓ highs (default) / cbc / glpk / gurobi | ✓ cbc (default; highs → cbc with warning) |
✓ glpk (fixed) | |
solver.time_limit/mip_gap/threads |
✓ mapped per solver (highs/gurobi/cbc; else ⚠) | ✓ mapped (gurobi/cbc; else ⚠) | ✓ mipgap/timelim (s→h)/threads |
|
solver.options |
✓ legacy mode options (deprecated) | ✓ legacy mode options (deprecated) | ✓ legacy mode options (deprecated) | |
custom_math |
— | ✓ render toggle for constraint IR | — | ratio/piecewise math renders regardless |
experiment.native |
· | · | · | reserved |
3.2 Model-level inputs¶
| Property | P | C | A | Notes |
|---|---|---|---|---|
metadata.* |
· | · | · | informational; name is required |
time.start/end/resolution |
✓ snapshots | ✓ time index | ✓ Topology dates | required + ISO-parseable; the snapshot/time index spans the longest inline series, else the range start..end at resolution, on every target |
time.subset |
✗ rejected | ✓ init.subset.timesteps (0.6: subset_time) |
✗ rejected | P/A: narrow time.start/end instead |
time.timesteps (explicit labels) |
✓ snapshot labels | ✓ data-table index | ✗ rejected (needs a regular date_range) | |
time.weights |
✓ snapshot_weightings (scalar or series) |
✓ timestep_weights (scalar; series ✗) |
✗ rejected | |
periods (with year) |
✓ investment_periods (years from length_years, weights from objective_weight), snapshots (period, timestep) with every series repeated per period, build_year + lifetime, new builds per period as <name>-<year>, multi_investment_periods |
⚠ ignored (single horizon) | ⚠ ignored (single horizon) | mode plan; an expandable technology with existing capacity needs continuous retirement; expandable stores and arcs not yet (ADR-0011) |
discount_rate (global) |
✓ overrides annuity rate | ✓ overrides cost_interest_rate |
✓ global_discountrate |
|
carriers.<id>.co2_intensity |
✓ carriers.co2_emissions |
✓ co2 cost class on fuel inflow | ✓ emission_factor |
|
carriers.<id>.unit/color |
· | · | · | |
timeseries (inline) |
✓ | ✓ | ✓ | source: file is rejected on every target (inline the values) |
transmission |
✓ Link; Line/Transformer under network.model: linear_power_flow |
✓ transmission tech | ✗ rejected (Networks unmapped) | |
trade |
✓ market generators | ✓ market supply/demand techs | ✓ carrier CSV columns | |
emission_limits |
✓ GlobalConstraint primary_energy |
✓ custom-math cap on cost[costs=co2] |
✓ costs_emissionlimit objective |
|
constraints (unscoped, single-variable) |
✓ GlobalConstraint | ✓ custom math | ✗ rejected | |
constraints (tech/node-scoped or multi-term) |
✓ run.py linopy constraints (scope-exact; no scoped emissions terms) |
✓ custom math | ✗ rejected | |
model.native |
· _native.pypsa.json sidecar (documentation only) |
✓ YAML merge | ✓ ConfigModel merge |
3.3 Node properties¶
| Property | P | C | A | Notes |
|---|---|---|---|---|
coords |
✓ bus x/y |
✓ latitude/longitude |
✓ NodeLocations.csv |
|
electrical.v_nom |
✓ bus v_nom under carrier AC (mode power_flow; ⚠ ignored otherwise) |
⚠ ignored | ⚠ ignored | required on every bus of the power-flow network |
electrical.shunt (g, b) |
✓ ShuntImpedance at the bus (mode power_flow) |
⚠ ignored | ⚠ ignored | siemens; b positive = capacitive |
available_area |
✗ (area is Calliope-only) | ✓ available_area |
✗ | gated with technology.area |
climate (ghi, dni, dhi, temp_air, rh, ws10, hydro_inflow) |
— | — | ✓ ClimateData.csv (scalar or series) |
feeds physics techs |
allowed_techs |
· | · | · | informational (tech placement comes from technology.node) |
native |
✓ bus columns | ✓ node YAML | ✓ Technologies.json merge |
3.4 Technology properties¶
| Property | P | C | A | Notes |
|---|---|---|---|---|
role: supply |
✓ Generator | ✓ base_tech: supply |
✓ physics DB tech, else ✗ | A: needs AdoptDBTech mapping or node-native |
role: demand |
✓ Load | ✓ demand tech + data table | ✓ Demand column |
scalar demand expands to a series everywhere it must |
role: conversion |
✓ Link | ✓ conversion tech | ✗ (no DB mapping) | |
role: storage |
✓ StorageUnit | ✓ storage tech | ✓ Storage_Battery |
|
carrier_in / carrier_out |
✓ buses per (node, carrier) | ✓ | ✓ | |
efficiency (scalar) |
✓ efficiency |
✓ flow_out_eff |
✓ via DB tech performance | series → ✗ everywhere |
flows (multi-port, rigid ratios) |
✓ multi-output Link (bus2/efficiency2, negative eff for extra inputs) |
✓ carrier lists + ratio-pinning math (fuel single-counted) | ✗ | ratios are rigid on both: demands must be consistent |
performance: constant |
✓ | ✓ | ✓ | |
performance: piecewise (+min_load) |
✗ | ✓ piecewise_constraints math — conversion role, fixed capacity, no flows |
✗ | C: flow_cap also caps inflow — keep curve x ≤ capacity |
performance: physics (pv, wind, heat_pump) |
✓ with precomputed profile |
✓ with precomputed → source_use_max table |
✓ native (computes from climate) | open_hydro: A has no mapping → ✗ |
capacity.existing |
✓ p_nom |
✓ flow_cap_min = flow_cap_max (fixed) |
✓ existing size |
|
capacity.expandable |
✓ p_nom_extendable |
✓ bounds only | ✓ new tech |
|
capacity.min / max |
✓ p_nom_min/p_nom_max |
✓ flow_cap_min/flow_cap_max |
✓ size_min/size_max overrides |
|
capacity.per_unit + units_min/max |
✓ integer module p_nom_mod / e_nom_mod / s_nom_mod on expandable components + implied p_nom_min/_max = units·per_unit |
✓ flow_cap_per_unit + purchased_units_min/max (committable); implied bounds otherwise |
✓ implied size_min/size_max |
explicit min/max win; a module with a quadratic cost is a MIQP (gurobi) |
capacity.systemwide_min/max |
✓ synthesized sidecar constraint (✗ in operate/stochastic) | ✓ flow_cap_min/max_systemwide |
✗ | |
capacity.decommission |
✓ continuous: the existing part extendable in [0, existing] at its fixed O&M, a new-build part above it when expandable (<name>_new); only_complete ✗; cost ⚠ not priced; not for Stores or arcs |
✓ impossible/continuous (only_complete ✗) |
✓ full enum + decommission_cost |
default: impossible |
operation.max_pu (scalar) |
✓ p_max_pu |
✓ source_use_max + per_cap (supply only) |
✗ | |
operation.max_pu (series) |
✓ generators-p_max_pu.csv |
✓ data table (supply only) | ✗ | |
operation.min_pu (scalar) |
✓ p_min_pu |
✓ flow_out_min_relative |
✗ | series → ✗ everywhere |
operation.equals_pu (scalar / series) |
✓ pins p_min_pu = p_max_pu (+ both CSVs) |
✓ source_use_equals (supply) |
✗ | fixed, non-curtailable dispatch |
operation.ramp_up / ramp_down |
✓ ramp_limit_up/_down |
✓ flow_ramping (symmetric, first non-nil) |
✗ | series → ✗ everywhere |
operation.committable |
✓ MILP UC | ✓ MILP: cap_method: integer + integer_dispatch + purchased_units_max (needs unit size) |
✗ | |
operation.start_up_cost / shut_down_cost |
✓ | ✗ (no 0.7 math) | ✗ | |
operation.min_uptime / min_downtime |
✓ min_up_time/min_down_time (snapshot counts) |
✗ | ✗ | |
operation.ramp_start_up / ramp_shut_down |
✓ ramp_limit_start_up/ramp_limit_shut_down |
⚠ ignored | ⚠ ignored | fraction of capacity in the start/stop hour |
operation.standby_cost |
✓ stand_by_cost |
⚠ ignored | ⚠ ignored | per committed hour |
operation.initial_uptime / initial_downtime |
✓ up_time_before/down_time_before |
⚠ ignored | ⚠ ignored | one of the two |
operation.max_startups / standby_power |
✗ | ✗ | ✓ Performance.max_startups/standby_power |
|
storage.energy_capacity |
✓ Store on its own bus with charger/discharger Links (e_nom*, equal link power when expandable, rate bounds via _storage.json) |
✓ storage_cap_max/_min |
(fixed E/P ratio of DB tech) | without it a max_hours storage is a StorageUnit (ADR-0011) |
storage.max_hours |
✓ max_hours |
✓ storage_cap_max = max_hours·cap_max |
— | mutually exclusive with the rate bounds |
storage.max_charge_rate / max_discharge_rate |
✓ link power ≤ rate × store energy (needs energy_capacity; ✗ on a StorageUnit) |
✓ flow_cap_per_storage_cap_max (rates must be equal) |
✓ Flexibility.charge_rate/discharge_rate |
independent power/energy sizing |
storage.charge_eff / discharge_eff |
✓ efficiency_store/_dispatch |
✓ flow_in_eff/flow_out_eff |
✓ DB performance | |
storage.self_discharge |
✓ standing_loss |
✓ storage_loss |
— | |
storage.depth_of_discharge |
✓ Store e_min_pu (needs energy_capacity; ✗ on a StorageUnit) |
✓ storage_discharge_depth |
✗ | |
storage.no_simultaneous_charge_discharge |
✗ | ✓ force_async_flow |
✓ allow_only_one_direction |
|
storage.inflow |
✓ scalar or storage_units-inflow.csv (StorageUnit only; ✗ on a Store) |
✗ | ✗ | |
storage.spill_cost |
✓ | ✗ | ✗ | |
storage.initial_soc |
✓ state_of_charge_initial (absolute: fraction × p_nom × max_hours) / Store e_initial of a fixed energy capacity (⚠ expandable: starts from existing) |
✓ storage_initial |
— | |
storage.soc_profile (scalar / series) |
✓ state_of_charge_set / storage_units-state_of_charge_set.csv (StorageUnit only) |
⚠ ignored | ⚠ ignored | pins the state of charge |
storage.cyclic |
✓ cyclic_state_of_charge |
✓ cyclic_storage (operate: forced false) |
— | |
demand_profile (scalar / series) |
✓ p_set / loads-p_set.csv |
✓ data table (scalar expanded) | ✓ Demand column |
|
demand_curtailable |
✗ | ✓ sink_use_max instead of sink_use_equals |
✗ | |
dispatch (scalar / series) |
✓ p_set / <component>-p_set.csv on generators, storage units and links (mode power_flow) |
⚠ ignored | ⚠ ignored | a power-flow input: ignored with one warning wherever no power flow runs, PyPSA optimization modes included (pin dispatch in an optimization with operation.equals_pu); overridable per node |
electrical.reactive_dispatch (scalar / series) or electrical.power_factor (+ leading) |
✓ q_set / <component>-q_set.csv on generators, loads and storage units (mode power_flow; Q derived per snapshot from the active set-point at the power factor, in the component's own direction) |
⚠ ignored | ⚠ ignored | mutually exclusive; a conversion carries no reactive fields; method: linear ignores them (warned) |
electrical.control: pv + electrical.voltage_setpoint |
✓ generator control: PV, bus v_mag_pu_set (the slack may carry a set-point too) |
⚠ ignored | ⚠ ignored | supply only; a set-point needs pv or the slack role; set-points at one bus must agree |
area.max |
✗ gated | ✓ area_use_max (+node available_area) |
✗ gated | |
area.per_capacity |
✗ gated | ⚠ warned, not emitted on 0.7 (use native area_use_per_flow_cap); ✓ resource_area_per_energy_cap on 0.6 |
✗ gated | |
source.max (scalar / series) |
✗ gated | ✓ source_use_max (+ data table) |
✗ gated | |
source.unit / source.cap |
— | ✓ source_unit / source_cap_max |
— | |
costs.monetary.investment_per_capacity |
✓ annualized → capital_cost |
✓ cost_flow_cap + cost_interest_rate |
✓ unit_CAPEX |
overnight basis needs lifetime+interest_rate |
costs.monetary.investment_per_energy_capacity |
✓ Store capital_cost per MWh (needs energy_capacity; ⚠ dropped on a StorageUnit) |
✓ cost_storage_cap |
⚠ dropped | |
costs.monetary.variable_om_quadratic |
✓ marginal_cost_quadratic (QP; with integer variables a MIQP that needs gurobi) |
⚠ ignored | ⚠ ignored | |
costs.monetary.fixed_om |
✓ folded into capital_cost |
✓ cost_om_annual |
✓ OPEX_fixed |
mutually exclusive with fixed_om_fraction |
costs.monetary.fixed_om_fraction |
✓ fraction·investment folded into capital_cost |
✓ cost_om_annual_investment_fraction |
✓ OPEX_fixed (adopt's native convention) |
needs overnight basis |
costs.monetary.variable_om (scalar) |
✓ marginal_cost |
✓ cost_flow_out (merged multi-class) |
✓ OPEX_variable |
|
costs.monetary.variable_om (series) |
✓ *-marginal_cost.csv |
✗ | ✗ | |
costs.monetary.fuel_cost (scalar) |
✓ fuel/efficiency folded into marginal_cost |
✓ cost_flow_in (merged with co2 class) |
✗ (price the input via trade) | series → ✗ |
costs.monetary.purchase |
✗ rejected (no fixed purchase cost; fold it into investment_per_capacity) |
✓ cost_purchase (committable) |
✗ rejected | |
costs.<other class> |
⚠ warned & dropped | ⚠ (except co2 accounting) | ⚠ | only monetary is priced |
lifetime / interest_rate / cost_basis |
✓ CRF annualization | ✓ lifetime + cost_interest_rate |
✓ economics | global discount_rate overrides the rate |
emission_factor |
✗ (carrier-wide accounting; set carrier intensity) | ✓ co2 cost class on cost_flow_out |
✓ Performance.emission_factor |
|
annual_output_min / max |
✓ e_sum_min/e_sum_max (supply only) |
✗ | ✗ | |
build_year |
✓ build_year |
⚠ informational | ⚠ informational | |
active: false |
✓ active column |
✓ active: false |
✓ omitted from Technologies.json |
|
node_overrides |
✓ one component per node | ✓ nodes.<n>.techs overrides (capacity + costs projected; a per-node demand_profile / availability lands in that node's data-table column) |
✓ per-node Technologies.json + node-keyed overrides |
0.6: per-location constraints/costs + per-node file= columns |
native |
✓ CSV columns | ✓ YAML merge | ✓ tech-JSON patch | merged last, verbatim |
3.5 Transmission & trade properties¶
| Property | P | C | A | Notes |
|---|---|---|---|---|
transmission.capacity |
✓ p_nom* |
✓ flow_cap_max |
✗ | systemwide bounds as for technologies |
transmission.efficiency |
✓ (scalar; series ✗) | ✓ flow_out_eff |
✗ | |
transmission.bidirectional |
✓ p_min_pu: -1 |
✓ one_way: true when false |
✗ | |
transmission.distance |
· folded into the link efficiency (loss_per_distance) and the per-distance investment; length on a Line/Transformer |
✓ distance |
✗ | |
transmission.loss_per_distance |
✓ folded into efficiency (needs distance) |
✓ flow_out_eff_per_distance |
✗ | |
transmission.min_flow |
✓ p_min_pu (one-way arcs only) |
✓ flow_out_min_relative |
✗ | |
transmission.emission_factor |
✗ (carrier-wide accounting) | ✓ co2 cost class on the link flow | ✗ | |
transmission.energy_consumption |
✓ multi-port link (bus2, negative efficiency2; one-way arcs only) |
✗ | ✗ | |
transmission.active: false |
✓ active column |
✓ active: false |
✗ | |
transmission.electrical (kind, r/x or r_per_length/x_per_length, s_nom, num_parallel, b, g, tap_ratio, phase_shift) |
✓ lines.csv / transformers.csv row with the impedance, admittance, rating and tap (mode power_flow; ⚠ ignored otherwise, arc stays a Link) |
⚠ ignored (transmission tech) | ✗ | electrical arcs are bidirectional; a line joins equal v_nom; s_nom defaults to capacity.existing; per-length impedance × distance; b/g lines only, phase_shift transformers only |
transmission.dispatch (scalar / series) |
✓ Link p_set / links-p_set.csv: a controlled HVDC transfer (mode power_flow) |
⚠ ignored | ✗ | exclusive with electrical; an electricity arc in a power flow needs one of the two; the island behind a link needs its own generator (PyPSA balances it, warned) |
transmission.costs.investment_per_capacity_distance |
✓ × distance into capital_cost (annualized) |
✓ cost_flow_cap_per_distance |
✗ | |
transmission.costs.investment_per_capacity, fixed_om, fixed_om_fraction |
✓ annualized into capital_cost of the Link / Line (⚠ not priced without lifetime + interest_rate or cost_basis: annualized) |
⚠ warned & dropped | ✗ | |
transmission.lifetime / interest_rate / cost_basis |
✓ annuity of the arc's investment (global discount_rate overrides) |
⚠ not used | ✗ | |
transmission.electrical.max_loading |
✓ s_max_pu (voltage-law optimization) |
⚠ ignored | ✗ | |
trade.import.limit (scalar / series) |
✓ p_nom_max / ✓ fixed p_nom=1 + p_max_pu series |
✓ flow_cap_max / ✗ |
✓ / ✓ per-timestep column | |
trade.import.price (scalar / series) |
✓ / ✓ generators-marginal_cost.csv |
✓ / ✗ | ✓ / ✓ per-timestep column | |
trade.import.emission_factor |
⚠ not emitted (set carrier intensity) | ✓ co2 cost class | ✓ Import emission factor |
|
trade.export.limit (scalar / series) |
✓ p_nom_max / ✓ fixed p_nom=1 + negated p_min_pu series |
✓ sink_use_max / ✗ |
✓ / ✓ | |
trade.export.price (scalar / series) |
✓ (+mc, revenue via p<0) / ✓ | ✓ (−cost_flow_in) / ✗ |
✓ / ✓ | |
trade.export.emission_factor |
⚠ not emitted | ✓ co2 cost class | ✓ Export emission factor |
|
trade.native |
✓ columns on the market generators | ✓ merged into the market techs | ⚠ no merge anchor (warned) |
3.6 Outputs¶
| Output | P | C | A |
|---|---|---|---|
| Native inputs on disk | run_i/input/*.csv + sidecars |
run_i/input/model.yaml + data tables + additional_math.yaml |
run_i/input/input_data/ tree |
| Executed driver | run_i/run.py |
Calliope CLI (command in results.json) |
run_i/run.py |
| Solved model | run_i/output/network.nc (+ network_baseline.nc for MGA) and run_i/output/results/: generators-p.csv, loads-p.csv, links-p0/p1.csv, lines-p0/p1.csv, transformers-p0/p1.csv, storage_units-p/state_of_charge.csv, stores-e/p.csv, buses-marginal_price.csv, capacities.csv (nominal and optimal per component), statistics.csv (PyPSA statistics), summary.json (objective, status, mode) |
run_i/output/results.nc + output/csv/* (per-variable CSVs incl. flow_out, storage, purchased_units, costs) |
run_i/input/results/ (HDF5 per solve + Summary.xlsx + solver_log.txt) |
| Objective / status in log | status ok optimal, objective <v> (non-optimal ⇒ non-zero exit ⇒ job failed); power flow: power flow converged k/n snapshots, status ok power flow converged (a non-converged snapshot ⇒ exit 3 ⇒ job failed) |
Optimal - objective value <v>, Calliope run complete |
Pyomo block (Termination condition: optimal, bounds) |
| Power-flow results (mode power_flow) | run_i/output/power_flow/: convergence.csv, buses-v_mag_pu/v_ang/p/q.csv, lines-p0/p1/q0/q1/loading.csv, transformers-…, generators-p/q.csv, loads-p/q.csv, storage_units-p/q.csv, links-p0/p1.csv, shunt_impedances-p/q.csv, summary.json (method, slack bus, converged snapshots, voltage band, peak loadings) |
— | — |
| Job artifacts (all targets) | GET /jobs/{id} zip: metadata.json, log.txt, results.json (target-labeled per-run exit/stdout/plan), config.json (byte-identical initial config), files/ (everything above; multi-target jobs nest as files/<target>/run_i/…) |
— | — |
| Status endpoint | GET /jobs/{id}/status: state (queued/running/succeeded/failed), live log, per-run results, timings |
— | — |
| Multi-target submit | ?target=pypsa,calliope,adopt-net0 or target=all on validate/convert/simulate: per-target verdicts/entrypoints, one job running every framework (422 up front if ANY requested target rejects the payload; see examples/shared_full.json) |
— | — |
3.7 Calliope 0.6 (calliope-0.6)¶
The 0.6 target reads the same canonical payload and honors the C column
above, with these differences (each enforced by Calliope06.ValidateJob, frozen
in the validation-matrix golden, and exercised by the calliope06_* scenarios):
| Property | 0.6 | Notes |
|---|---|---|
experiment.time_aggregation.method: typical_days |
✓ | k-means representative days (model.time.apply_clustering, k = periods, fixed random_seed); keep_full_resolution_for ⚠ ignored. 0.7 rejects this method. |
experiment.objective: min_emissions |
✓ | run.objective_options.cost_class: {monetary: 0, emissions: 1}; ✗ when the model has no emission source to weight |
model.constraints |
✓ single-term only | one variable over a tech/node set → group_constraints (energy_cap_*, carrier_prod_*, carrier_con_* (negative), storage_cap_*, cost_*); coefficient folded into the bound; multi-term ✗ (use calliope) |
experiment.custom_math |
⚠ ignored | no custom math in 0.6; constraints always render as group constraints |
multi-valued indexed Value |
✗ | no indexed parameters in 0.6 |
performance.type: piecewise |
✗ | no native piecewise efficiency |
technology.flows |
✓ one input, ≤ 3 outputs | conversion_plus tiers; carrier_ratios relative to the primary output |
time.weights |
✗ | timestep weights derive from the time resolution |
trade.export without limit |
✗ | the market demand tech needs a resource bound |
storage.charge_eff ≠ discharge_eff |
⚠ | one energy_eff for both directions: emits the geometric mean (round trip preserved) |
storage.cyclic (mixed across techs) |
⚠ | run.cyclic_storage is model-wide; mixed settings disable it. Operate mode is never cyclic. |
lifetime / interest_rate with fixed costs |
✓ | both are mandatory in 0.6; absent values (or cost_basis: annualized) become lifetime 1 / rate 0, i.e. the figure is charged in full every year |
reporting.shadow_prices |
⚠ ignored | not available in 0.6 |
alternatives.scoring_algorithm |
⚠ ignored | 0.6 SPORES uses integer scoring |
capacity.systemwide_min, annual_output_min/max |
✓ | per-tech group_constraints (energy_cap_min, carrier_prod_min/max) |
native.calliope |
⚠ ignored | 0.7 syntax; use native["calliope-0.6"] |
4. Cross-target guarantees¶
- A job only reaches the runner if every property above resolves to ✓ or ⚠
for the selected target (⚠ always named in
warnings). For a multi-target request (?target=all) this holds per requested target — one rejection fails the whole submit up front. - The portable maximum is a real, runnable artifact:
examples/payload_full.jsonuses the largest property set all three frameworks accept simultaneously (with each framework's extras in its ownnativenamespace) and solves to optimal on all of them from a single?target=allsubmit. - The identical dispatch model produces the same objective on all three
frameworks (
TestE2ECrossTargetConsistency, ±1%), and the same plan model reaches the same pinned objective on both Calliope releases (TestE2ECalliopeLifecycle/TestE2ECalliope06Lifecycle). - Multi-output conversion ratios are rigid on PyPSA and Calliope alike (heat/power fixed per unit of fuel); over-determined demand patterns go infeasible rather than silently rebalancing, and fuel is single-counted.
- Trade economics are sign-verified: profitable exports are exploited
(
TestE2ETradeExportRevenue), time-varying prices steer dispatch (TestE2ETradePriceSeries), scoped constraints bind (TestE2ESidecarConstraintBinds).