Cost-Benefit Analysis (CBA)¶
The Cost-Benefit Analysis (CBA) evaluates transmission and storage projects by comparing two dispatch-only simulations: a reference network (the baseline grid) and a project network (the grid with a specific project added or removed). Unlike the Scenario Building (SB) phase, the CBA does not re-optimize capacities; it reuses the SB's solved network, fixes capacities, and runs dispatch-only optimizations.

CBA Workflow Methodology¶
The workflow evaluates projects using a rolling horizon approach where the full year is divided into sequential weekly windows (168 hourly snapshots each, with an overlap of 1 snapshot). An overview of the CBA rules is documented at CBA rules.
To resolve myopia—where the optimizer cannot see beyond the current week and makes suboptimal decisions for seasonal storage (H2, gas, large hydro)—the workflow uses Marginal Storage Values (MSV) derived from a full-year optimization.

Network Simplification¶
The SB network is transformed into a dispatch-ready CBA network:
- Fixed Capacities: Capacities are fixed via
n.optimize.fix_optimal_capacities(). - Hurdle Costs: A cost of 0.01 €/MWh is applied to all DC links.
- Fuel Capacities: Primary fuel generator capacities (coal, gas, oil, nuclear) are set to infinity to prevent dispatch from being artificially restricted by fuel-supply limits during peak hours.
Reference Network¶
The simplified network is extended to form the CBA reference baseline by adding all TOOT project capacities. This ensures the reference and MSV extraction operate on the same topology.
MSV Extraction¶
The reference network is solved with perfect foresight (entire year, single LP). This exposes the shadow prices (mu_energy_balance) of energy balance constraints, representing the Marginal Storage Value (MSV)—the opportunity cost of stored energy at that moment.
Rolling Horizon Preparation¶
The reference network and MSV results are combined through five transformations:
- (a) Initial storage state: Seasonal components have their initial state set to the perfect foresight solution's last-snapshot value.
- (b) Disable cyclicity: Short-term storage (battery) keeps cyclicity, while seasonal units (H2, gas, hydro) have it disabled, guided instead by MSVs.
- (c) Remove global constraints: Annual CO2 and biomass limits are removed as they cannot be enforced consistently in weekly windows.
- (d) Disable annual volume limits: Annual budgets for biomass/biogas are distributed proportionally to the perfect foresight dispatch.
- (e) Apply MSV: The
mu_energy_balancetime series is written into themarginal_costof storage components. - (f) Hydro Pinning: Large hydro reservoirs are pinned to their perfect-foresight state-of-charge values at window boundaries to guide dispatch where duals are near-zero.
Solve¶
The prepared network is solved for the Reference baseline and then for each Project. Projects are evaluated using either TOOT (Take Out One at a Time) or PINT (Put IN at a Time) methods.
CBA Indicators¶
Indicators are computed as the difference in system costs and emissions between the reference and project dispatch solutions.
- B1: Social Economic Welfare (SEW): Quantifies the change in operational system costs (socio-economic welfare).
- B2: Social costs of CO2 emissions: Calculates the impact using societal cost assumptions (low, central, high).
- B3: RES integration costs: Tracks changes in renewable capacity, generation, and avoided curtailment.
- B4: Non-direct greenhouse emissions: Quantifies pollutants (NOx, SO2, PM, etc.) using fuel consumption multipliers.
Configuration¶
CBA settings are defined in the cba section of the configuration file. You can refer to
the configuration page for a more comprehensive list of available PyPSA-Eur
and Open-TYNDP configuration options.
Project Selection¶
planning_horizons: Selects horizons (e.g., 2030, 2040).projects: Defines project identifiers (e.g.,t1-t35).cba_scenario_input: Ifuse_presolvedis true, the workflow retrieves pre-solved SB networks from an archive.
Warning
We recommend always running the CBA workflow with the latest pre-solved SB networks, which is what the
default cba_scenario_input:sb_version: latest selects. Earlier versions stay retrievable
but may no longer match what a newer workflow expects, and nothing checks for that
mismatch. If you encounter errors, please
report them and fall back to
the code of the latest release, which is what the networks were produced with (e.g.
pre-solve 0.9 with the v0.9 tag). For doing so, open a terminal in your open-tyndp folder and run e.g.:
git fetch --tags
git checkout v0.9
The pre-solved networks also assume the default settings of config.tyndp.yaml and config.hpc.yaml,
so a deviating config can leave the SB and CBA assumptions out of alignment.
Rolling Horizon Settings¶
storage.cyclic_carriers: Carriers that remain cyclic within each weekly window.storage.soc_boundary_carriers: Carriers pinned at window boundaries.msv_extraction.resolution: Controls temporal resolution for the MSV solve (e.g.,24H).
Running Single vs Multiple Climate Years¶
Climate-year collections allow project benefits to be assessed across multiple weather years, consistent with the 2024 TYNDP implementation.
The CBA entry point pixi run tyndp-cba can run a single scenario that is one single climate year or a collection scenario that defines a list of child (climate years) scenarios under cba.scenarios.
Example Collection (config/scenarios.tyndp.yaml):
NT-cy1995:
# <<: *cba-common
snapshots:
start: "1995-01-01"
end: "1996-01-01"
atlite:
default_cutout: europe-1995-sarah3-era5
cba:
sb_scenario: NT
NT-cy2008:
# <<: *cba-common
snapshots:
start: "2008-01-01"
end: "2009-01-01"
atlite:
default_cutout: europe-2008-sarah3-era5
cba:
sb_scenario: NT
NT-cy2009:
# <<: *cba-common
snapshots:
start: "2009-01-01"
end: "2010-01-01"
atlite:
default_cutout: europe-2009-sarah3-era5
cba:
sb_scenario: NT
NT-cyears:
cba:
scenarios: [NT-cy2009, NT-cy2008, NT-cy1995]
Individual child scenarios (e.g., NT-cy2009) must define their specific snapshots, atlite.default_cutout, and the cba.sb_scenario used as input.
Tip
If too many parallel jobs cause out-of-memory issues, you can specify your machine's
physical RAM limit in profiles/default/config.yaml for Snakemake to use
when scheduling jobs:
resources:
mem_mb: 16000
Running Multiple Years¶
To run a collection like NT-cyears, modify run.name in config/config.tyndp.yaml or override it via command line:
$ pixi run tyndp-cba --config run='{"name":"NT-cyears"}'
Running a Single Climate Year¶
Similarly, a single climate year can be run by modifying run.name in config/config.tyndp.yaml to the desired scenario (e.g., NT-cy2009) or overriding it via command line:
$ pixi run tyndp-cba --config run='{"name":"NT-cy2009"}'
Evaluation of custom projects¶
Custom projects can be evaluated with the CBA workflow, either as transmission or generator projects.
Custom transmission projects¶
Custom PINT transmission projects can be evaluated with the CBA workflow. Each project is defined in data/cba/custom_projects/transmission_projects.csv and selected in the configuration. Two types of custom project are supported, depending on whether project_id refers to an existing PINT project:
-
Modified projects: if the combination (
project_id,bus0,bus1) matches an existing PINT project, the specified fields overwrite those of that project. Fields left empty retain their original values. -
New projects: if
project_idis not yet used, the entry is added as a new PINT project, modeled as a link.
Every entry must define project_id, bus0, bus1 and at least one capacity (p_nom 0->1 or p_nom 1->0), and the resulting combinations must be unique. Entries referring to TOOT projects are not supported and are ignored with a warning. Transmission capacities are in MW.
Custom generators¶
Custom generators can also be evaluated with the CBA workflow.
Custom generators are never assessed on their own: each one is grouped with either a transmission or a storage project, which it is added alongside in the same project network. The grouping is expressed by two columns, project_type (t for transmission, s for storage) and project_id (the ID of that project), and the generator inherits the assessment method of the project it is grouped with.
Each project is defined across two files:
-
data/cba/custom_projects/generators_static.csv: one row per generator, withproject_name,project_type,project_id,generator_name,carrier,bus,p_nom,marginal_cost,capital_costandefficiency.project_typeandproject_ididentify the project the generator is grouped with:project_typeistfor a transmission project orsfor a storage project, andproject_idis that project's ID, without the project_type (e.g.project_type = t,project_id = 1500for transmission projectt1500). Entries whoseproject_typeis neithersnortare dropped with a warning, as are entries without aproject_idorgenerator_name, or whosebusdoes not already exist in the network. Duplicate (project_type,project_id,generator_name) combinations are also dropped. Missingmarginal_cost,capital_costandefficiencydefault to0,0and1respectively. -
data/cba/custom_projects/generators_dynamic.csv: time series for the same projects, in wide format with a two-row header and snapshots as the index:- Row 1 identifies the generator: replace the placeholder
<project_type><project_id>_<generator_name>with theproject_type,project_idandgenerator_nameof the corresponding row in the static file, the project_type written directly against the ID and the generator name joined with an underscore (e.g.t1500_BEI wind). A project with multiple generators needs one such column group per generator. - Row 2 names the PyPSA
Generatorattribute the column provides, e.g.p_max_pu,p_min_pu,efficiency,marginal_cost,p_set.
Only columns whose subset of project_type, project_id and generator_name matches an entry in the static file, and whose attribute is a valid time-varying PyPSA
Generatorinput, are kept; everything else (unmatched projects, non-input attributes, fully empty columns) is dropped. - Row 1 identifies the generator: replace the placeholder
When applied, the project network is searched for a generator with the same bus and carrier as the entry, and the outcome depends on whether such a generator already exists:
-
The generator already exists: only its
p_nomis updated. PINT adds thep_nomof the entry to the existing capacity, TOOT subtracts it, and the generator is removed from the network when its capacity reaches zero. Removing more capacity than exists is governed bycba.negative_toot_capacity, which either clamps the capacity to zero or raises an error. Every other attribute keeps the value of the existing generator, somarginal_cost,capital_cost,efficiencyand the time series of the custom files are ignored for that entry. -
No such generator exists: PINT adds a new
Generatorcomponent named<project_type><project_id>_<generator_name>at the specifiedbus, using the staticp_nomandcapital_cost, with the remaining time-varying attributes taken from the dynamic file where available and falling back to the static value where given, and to the PyPSA default otherwise. If the generator'scarrierdoes not yet exist in the network, it is added to the PyPSA network. TOOT has no capacity to remove in this case, so the entry is skipped with a warning and nothing is added.
If several generators share the same bus and carrier, the first one is updated and a warning is logged.
A filled-in template with a few examples for custom transmission projects and generators are available at data/cba/custom_projects/examples/.
A user can optionally provide the same files as .xlsx format for each input file; if detected this will take precedence over given csv files.
Selecting custom projects¶
Custom projects are injected into the workflow in clean_projects, once the project list has been extracted. The set of projects evaluated in the CBA workflow is configured by cba.projects.
Checkpoint¶
If you run the CBA workflow for the first time, you might not see a full DAG and instead only see a small number of steps listed in your DAG (including a step called clean_projects).
This rule represents a checkpoint in the workflow that first checks how many projects are being asked to run before building out the full DAG. Specifically, the checkpoint tells the workflow which CBA projects exist, which project IDs to run, and which method applies (TOOT/PINT).
Thus, if you see only a few steps in your DAG, it is because Snakemake has not yet reached the checkpoint to determine the full list of projects to evaluate. This short DAG is therefore not reflective of the actual number of steps that will run once the checkpoint is passed.
After clean_projects finishes and the checkpoint passes, Snakemake can read the cleaned CSV and expand the DAG into concrete jobs, at which point you will see the full set of steps.
To run the workflow only up to the checkpoint, one can use the pixi run tyndp-checkpoint command, which executes all steps up to and including the checkpoint.
$ pixi run tyndp-checkpoint
After this, you can run the full workflow with pixi run tyndp-cba to execute all remaining steps, including those that follow the checkpoint.