From 3b004e105e1eaa4182213a3606574176a38ca486 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 28 May 2026 09:09:33 +0200 Subject: [PATCH 01/29] feat: set marginal costs for biomass and biogas generators --- scripts/cba/prepare_rolling_horizon.py | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 8e080f28e6..87bac6530e 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -191,6 +191,79 @@ def apply_msv_to_network( n.c[c].dynamic["marginal_cost"].loc[:, s_i] = msv +def apply_biomass_biogas_bus_marginal_prices( + n, + n_msv, + carriers=("solid biomass", "biogas"), + resample_method="ffill", +): + """ + Add marginal prices to biomass/biogas generator marginal costs. + + For each biomass/biogas generator in the rolling-horizon network, use the + marginal price time series of its attached bus from the MSV network and add it to the generator's base marginal cost. + + effective_marginal_cost_t = static_marginal_cost + bus_marginal_price_t + + Parameters + ---------- + n : pypsa.Network + Target network (will be modified in place). + n_msv : pypsa.Network + Solved MSV network containing bus marginal prices. + carriers : tuple[str], optional + Generator carriers to apply bus marginal prices to. Defaults to ("solid biomass", "biogas"). + resample_method : str, optional + Method for resampling MSV bus marginal prices to target resolution. Default "ffill". + """ + import pandas as pd + + if n_msv is None or n_msv.buses_t.marginal_price.empty: + logger.warning( + "No MSV bus marginal prices available; skipping biomass/biogas cost update." + ) + return + if isinstance(carriers, str): + carriers = (carriers,) + + # Get bus marginal prices from MSV network, resampling if needed + msv_mp = n_msv.buses_t.marginal_price + if msv_mp.index.equals(n.snapshots): + bus_mp = msv_mp + else: + bus_mp = resample_msv_to_target(msv_mp, n.snapshots, method=resample_method) + + # Get biomass/biogas generators + g_idx = n.generators.index[n.generators.carrier.isin(carriers)] + if g_idx.empty: + logger.info("No biomass/biogas generators found for marginal-price update.") + return + + # Ensure marginal cost frame exists on correct index + if ( + n.generators_t.marginal_cost.empty + or not n.generators_t.marginal_cost.index.equals(n.snapshots) + ): + n.generators_t.marginal_cost = pd.DataFrame(index=n.snapshots) + + updated = 0 + for g in g_idx: + bus = n.generators.at[g, "bus"] + if bus not in bus_mp.columns: + logger.warning( + "Skipping %s: bus %s not found in MSV marginal prices.", g, bus + ) + continue + + base_cost = float(n.generators.at[g, "marginal_cost"]) + n.generators_t.marginal_cost[g] = base_cost + bus_mp[bus].reindex(n.snapshots) + updated += 1 + + logger.info( + "Applied bus-marginal-price costs to %d biomass/biogas generators.", updated + ) + + def set_initial_state_from_pf( n: pypsa.Network, n_msv: pypsa.Network, @@ -354,6 +427,9 @@ def fix_reservoir_soc_at_boundaries( # Apply marginal storage value to all non-cyclic carriers apply_msv_to_network(n, n_msv, cyclic_carriers, resample_method) + # Add bus marginal prices to biomass/biogas generators + apply_biomass_biogas_bus_marginal_prices(n, n_msv, resample_method=resample_method) + # Fix reservoir state of charge at window boundaries from perfect foresight soc_boundary_carriers = snakemake.params.get("soc_boundary_carriers", []) cba_solving = snakemake.config.get("cba", {}).get("solving", {}).get("options", {}) From 20a86c1eec15f56ead8f350d158dc53c17988094 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 28 May 2026 09:13:40 +0200 Subject: [PATCH 02/29] feat: remove volume limits on biomass and biogas dispatch --- scripts/cba/solve_cba_network.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 78b7d713f5..74f7b114fe 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -158,24 +158,6 @@ def optimize_with_rolling_horizon( n.storage_units_t.state_of_charge.loc[snapshots[start - 1]] ) - # Set per-window energy budgets for volume-limited components - # (biomass, biogas) based on PF dispatch stored in generators_t.p - for c_name in ["Generator", "Link"]: - c = n.c[c_name] - if "has_volume_limit" not in c.static.columns: - continue - vol_idx = c.static.index[c.static["has_volume_limit"] == 1] - if vol_idx.empty: - continue - p_col = "p" if c_name == "Generator" else "p0" - pf_p = c.dynamic[p_col] - for comp in vol_idx: - if comp not in pf_p.columns: - continue - window_energy = pf_p.loc[sns, comp].sum() - c.static.loc[comp, "e_sum_min"] = window_energy - c.static.loc[comp, "e_sum_max"] = window_energy - status, condition = n.optimize(sns, **kwargs) # type: ignore if status != "ok": logger.warning( From 7f2a678df25ec697b1c20469cfc4a4ca53a55716 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 28 May 2026 10:29:39 +0200 Subject: [PATCH 03/29] chore: remove checks and logs --- scripts/cba/prepare_rolling_horizon.py | 27 ++++++-------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 87bac6530e..e0b2695922 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -216,13 +216,6 @@ def apply_biomass_biogas_bus_marginal_prices( resample_method : str, optional Method for resampling MSV bus marginal prices to target resolution. Default "ffill". """ - import pandas as pd - - if n_msv is None or n_msv.buses_t.marginal_price.empty: - logger.warning( - "No MSV bus marginal prices available; skipping biomass/biogas cost update." - ) - return if isinstance(carriers, str): carriers = (carriers,) @@ -233,34 +226,26 @@ def apply_biomass_biogas_bus_marginal_prices( else: bus_mp = resample_msv_to_target(msv_mp, n.snapshots, method=resample_method) - # Get biomass/biogas generators + # Get index of generators with target carriers g_idx = n.generators.index[n.generators.carrier.isin(carriers)] - if g_idx.empty: - logger.info("No biomass/biogas generators found for marginal-price update.") - return - # Ensure marginal cost frame exists on correct index + # Make sure marginal cost frame exists on correct index if ( n.generators_t.marginal_cost.empty or not n.generators_t.marginal_cost.index.equals(n.snapshots) ): n.generators_t.marginal_cost = pd.DataFrame(index=n.snapshots) - updated = 0 + number_updated = 0 for g in g_idx: bus = n.generators.at[g, "bus"] - if bus not in bus_mp.columns: - logger.warning( - "Skipping %s: bus %s not found in MSV marginal prices.", g, bus - ) - continue - base_cost = float(n.generators.at[g, "marginal_cost"]) n.generators_t.marginal_cost[g] = base_cost + bus_mp[bus].reindex(n.snapshots) - updated += 1 + number_updated += 1 logger.info( - "Applied bus-marginal-price costs to %d biomass/biogas generators.", updated + "Applied bus-marginal-price costs to %d biomass/biogas generators.", + number_updated, ) From 68634be8ebb99440e510bd0c772799b8c6126b97 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Tue, 16 Jun 2026 15:13:26 +0200 Subject: [PATCH 04/29] chore: remove logger and update docs --- scripts/cba/prepare_rolling_horizon.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 18552ba4c4..c67d31e0c8 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -200,8 +200,9 @@ def apply_biomass_biogas_bus_marginal_prices( """ Add marginal prices to biomass/biogas generator marginal costs. - For each biomass/biogas generator in the rolling-horizon network, use the - marginal price time series of its attached bus from the MSV network and add it to the generator's base marginal cost. + For each biomass/biogas generator in the rolling-horizon network: + use the marginal price time series of its attached bus from the MSV network and + add the marginal price to the generator's base marginal cost. effective_marginal_cost_t = static_marginal_cost + bus_marginal_price_t @@ -236,17 +237,10 @@ def apply_biomass_biogas_bus_marginal_prices( ): n.generators_t.marginal_cost = pd.DataFrame(index=n.snapshots) - number_updated = 0 for g in g_idx: bus = n.generators.at[g, "bus"] base_cost = float(n.generators.at[g, "marginal_cost"]) n.generators_t.marginal_cost[g] = base_cost + bus_mp[bus].reindex(n.snapshots) - number_updated += 1 - - logger.info( - "Applied bus-marginal-price costs to %d biomass/biogas generators.", - number_updated, - ) def set_initial_state_from_pf( From d6d711403f48e4872e117d320fea137197b31ccd Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Tue, 16 Jun 2026 15:35:30 +0200 Subject: [PATCH 05/29] refac: make simple refactoring and add comments --- scripts/cba/prepare_rolling_horizon.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index c67d31e0c8..e89d65ee7a 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -220,15 +220,18 @@ def apply_biomass_biogas_bus_marginal_prices( if isinstance(carriers, str): carriers = (carriers,) - # Get bus marginal prices from MSV network, resampling if needed + # Get bus marginal prices from MSV network msv_mp = n_msv.buses_t.marginal_price - if msv_mp.index.equals(n.snapshots): - bus_mp = msv_mp - else: - bus_mp = resample_msv_to_target(msv_mp, n.snapshots, method=resample_method) + + # Resample marginal prices if needed + bus_mp = ( + msv_mp + if msv_mp.index.equals(n.snapshots) + else resample_msv_to_target(msv_mp, n.snapshots, method=resample_method) + ) # Get index of generators with target carriers - g_idx = n.generators.index[n.generators.carrier.isin(carriers)] + gen_index = n.generators.index[n.generators.carrier.isin(carriers)] # Make sure marginal cost frame exists on correct index if ( @@ -237,7 +240,8 @@ def apply_biomass_biogas_bus_marginal_prices( ): n.generators_t.marginal_cost = pd.DataFrame(index=n.snapshots) - for g in g_idx: + # Add bus marginal price to generator marginal cost for each generator + for g in gen_index: bus = n.generators.at[g, "bus"] base_cost = float(n.generators.at[g, "marginal_cost"]) n.generators_t.marginal_cost[g] = base_cost + bus_mp[bus].reindex(n.snapshots) From 9f233ac18d9133015da744902a31677d4f49eb95 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Tue, 16 Jun 2026 15:36:21 +0200 Subject: [PATCH 06/29] feat: revert `scripts/cba/solve_cba_network.py` back to `master` --- scripts/cba/solve_cba_network.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 8d334f98b3..eb21fb57d0 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -158,6 +158,24 @@ def optimize_with_rolling_horizon( n.storage_units_t.state_of_charge.loc[snapshots[start - 1]] ) + # Set per-window energy budgets for volume-limited components + # (biomass, biogas) based on PF dispatch stored in generators_t.p + for c_name in ["Generator", "Link"]: + c = n.c[c_name] + if "has_volume_limit" not in c.static.columns: + continue + vol_idx = c.static.index[c.static["has_volume_limit"] == 1] + if vol_idx.empty: + continue + p_col = "p" if c_name == "Generator" else "p0" + pf_p = c.dynamic[p_col] + for comp in vol_idx: + if comp not in pf_p.columns: + continue + window_energy = pf_p.loc[sns, comp].sum() + c.static.loc[comp, "e_sum_min"] = window_energy + c.static.loc[comp, "e_sum_max"] = window_energy + status, condition = n.optimize(sns, **kwargs) # type: ignore if status != "ok": logger.warning( From d7c28542279c1ad99bdbeab48d45e164b105042f Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Wed, 17 Jun 2026 11:48:34 +0200 Subject: [PATCH 07/29] feat: add slack to biomass/biogas dispatch `e_sum_min` --- scripts/cba/solve_cba_network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index eb21fb57d0..e217ed4002 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -173,7 +173,8 @@ def optimize_with_rolling_horizon( if comp not in pf_p.columns: continue window_energy = pf_p.loc[sns, comp].sum() - c.static.loc[comp, "e_sum_min"] = window_energy + slack = 0.3 + c.static.loc[comp, "e_sum_min"] = (1 - slack) * window_energy c.static.loc[comp, "e_sum_max"] = window_energy status, condition = n.optimize(sns, **kwargs) # type: ignore From ea075e4f18ba804af0fcaa416649b31e3e9e624e Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Wed, 17 Jun 2026 12:52:28 +0200 Subject: [PATCH 08/29] feat: increase slack to 40% --- scripts/cba/solve_cba_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index e217ed4002..dbffa0102a 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -173,7 +173,7 @@ def optimize_with_rolling_horizon( if comp not in pf_p.columns: continue window_energy = pf_p.loc[sns, comp].sum() - slack = 0.3 + slack = 0.4 c.static.loc[comp, "e_sum_min"] = (1 - slack) * window_energy c.static.loc[comp, "e_sum_max"] = window_energy From 1977d07d10eae756dbbea8ae09f0164b20935bec Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Wed, 17 Jun 2026 14:33:05 +0200 Subject: [PATCH 09/29] docs: add #719 to doc/release_notes --- doc/release_notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 05c2ce4933..83ed7af987 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -33,6 +33,8 @@ Upcoming Open-TYNDP Release - Add ``include_objective_constant`` and ``assign_all_duals`` to solving config validator. - Add ``gurobi-simplex`` as solver option. +* Change dispatch of biomass and biogas generators in CBA by (a) adding marginal prices of buses to the generators' marginal costs and (b) including a slack of 40% in the `e_sum_min` (https://github.com/open-energy-transition/open-tyndp/pull/719). + **Bugfixes and Compatibility** **Documentation** From 0698bd424e092af2b8eb15f13d0fe0b8b7928cf2 Mon Sep 17 00:00:00 2001 From: meas Date: Thu, 2 Jul 2026 17:22:52 +0200 Subject: [PATCH 10/29] refac: use `n_msv.snapshots` instead of `msv_mp.index` Co-authored-by: Thomas Gilon --- scripts/cba/prepare_rolling_horizon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index e89d65ee7a..6e2ae39914 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -226,7 +226,7 @@ def apply_biomass_biogas_bus_marginal_prices( # Resample marginal prices if needed bus_mp = ( msv_mp - if msv_mp.index.equals(n.snapshots) + if n_msv.snapshots.equals(n.snapshots) else resample_msv_to_target(msv_mp, n.snapshots, method=resample_method) ) From 9dc4fee6b3d195b9b0c15860e6042bfac559bff2 Mon Sep 17 00:00:00 2001 From: meas Date: Thu, 2 Jul 2026 17:23:47 +0200 Subject: [PATCH 11/29] feat: directly set marginal price as marginal cost, instead of adding Co-authored-by: Thomas Gilon --- scripts/cba/prepare_rolling_horizon.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 6e2ae39914..ebac8558a3 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -243,8 +243,7 @@ def apply_biomass_biogas_bus_marginal_prices( # Add bus marginal price to generator marginal cost for each generator for g in gen_index: bus = n.generators.at[g, "bus"] - base_cost = float(n.generators.at[g, "marginal_cost"]) - n.generators_t.marginal_cost[g] = base_cost + bus_mp[bus].reindex(n.snapshots) + n.generators_t.marginal_cost[g] = bus_mp[bus].reindex(n.snapshots) def set_initial_state_from_pf( From be81501adb7c21d96ae697424e4a2344c2bc1846 Mon Sep 17 00:00:00 2001 From: meas Date: Thu, 2 Jul 2026 17:24:04 +0200 Subject: [PATCH 12/29] chore: edit comment Co-authored-by: Thomas Gilon --- scripts/cba/prepare_rolling_horizon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index ebac8558a3..95ea21f135 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -409,7 +409,7 @@ def fix_reservoir_soc_at_boundaries( # Apply marginal storage value to all non-cyclic carriers apply_msv_to_network(n, n_msv, cyclic_carriers, resample_method) - # Add bus marginal prices to biomass/biogas generators + # Add bus marginal prices to the marginal costs of the biomass/biogas generators apply_biomass_biogas_bus_marginal_prices(n, n_msv, resample_method=resample_method) # Fix reservoir state of charge at window boundaries from perfect foresight From ee16ef4d2c107bfd12516edc578e0d797fdf28ff Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 2 Jul 2026 22:18:51 +0200 Subject: [PATCH 13/29] feat: create config option `cba.biomass_biogas_slack` for slack instead of hardcoding --- config/config.default.yaml | 1 + config/config.tyndp.yaml | 1 + config/schema.default.json | 14 ++++++++++++++ config/test/config.cyears.yaml | 1 + config/test/config.tyndp.yaml | 1 + scripts/cba/solve_cba_network.py | 6 ++++-- scripts/lib/validation/config/cba.py | 6 ++++++ 7 files changed, 28 insertions(+), 2 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 3ef15b3843..5f6bbaa54f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1604,6 +1604,7 @@ cba: area: tyndp remove_noisy_costs: true negative_toot_capacity: zero + biomass_biogas_slack: 0.4 storage: cyclic_carriers: - battery diff --git a/config/config.tyndp.yaml b/config/config.tyndp.yaml index 4f0b59a88c..f61206ad05 100644 --- a/config/config.tyndp.yaml +++ b/config/config.tyndp.yaml @@ -433,6 +433,7 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero + biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits # Storage settings for dispatch storage: diff --git a/config/schema.default.json b/config/schema.default.json index 82119850e0..bc980d3039 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -533,6 +533,13 @@ ], "type": "string" }, + "biomass_biogas_slack": { + "default": 0.4, + "description": "Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, "storage": { "description": "Configuration for `cba.storage` settings.", "properties": { @@ -15468,6 +15475,13 @@ ], "type": "string" }, + "biomass_biogas_slack": { + "default": 0.4, + "description": "Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + "maximum": 1.0, + "minimum": 0.0, + "type": "number" + }, "storage": { "description": "Configuration for `cba.storage` settings.", "properties": { diff --git a/config/test/config.cyears.yaml b/config/test/config.cyears.yaml index 25d80bd3ed..e33d6484ea 100644 --- a/config/test/config.cyears.yaml +++ b/config/test/config.cyears.yaml @@ -443,6 +443,7 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero + biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits # Storage settings for dispatch storage: diff --git a/config/test/config.tyndp.yaml b/config/test/config.tyndp.yaml index bbd7f94531..eaa0d0d186 100644 --- a/config/test/config.tyndp.yaml +++ b/config/test/config.tyndp.yaml @@ -441,6 +441,7 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero + biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits # Storage settings for dispatch storage: diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index dbffa0102a..057d611db5 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -136,6 +136,7 @@ def optimize_with_rolling_horizon( assert len(snapshots), "Need at least one snapshot to optimize" fallback_solver = kwargs.pop("fallback_solver", None) + biomass_biogas_slack = n.config["cba"].get("biomass_biogas_slack", 0.4) starting_points = range(0, len(snapshots), horizon - overlap) for i, start in tqdm(enumerate(starting_points), total=len(starting_points)): @@ -173,8 +174,9 @@ def optimize_with_rolling_horizon( if comp not in pf_p.columns: continue window_energy = pf_p.loc[sns, comp].sum() - slack = 0.4 - c.static.loc[comp, "e_sum_min"] = (1 - slack) * window_energy + c.static.loc[comp, "e_sum_min"] = ( + 1 - biomass_biogas_slack + ) * window_energy c.static.loc[comp, "e_sum_max"] = window_energy status, condition = n.optimize(sns, **kwargs) # type: ignore diff --git a/scripts/lib/validation/config/cba.py b/scripts/lib/validation/config/cba.py index 5ee591f876..7fac2a88cb 100644 --- a/scripts/lib/validation/config/cba.py +++ b/scripts/lib/validation/config/cba.py @@ -177,6 +177,12 @@ class CbaConfig(BaseModel): default="zero", description="How to handle TOOT project removal when removing project capacity would make an existing interconnector capacity negative. 'zero' clamps the resulting capacity to zero and continues; 'break' raises an error.", ) + biomass_biogas_slack: float = Field( + 0.4, + ge=0.0, + le=1.0, + description="Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + ) storage: _CbaStorageConfig = Field( default_factory=_CbaStorageConfig, description="Storage configuration for the cost-benefit analysis workflow.", From ab0f708df3b4a1e34c3b9977d63d34f0664843fe Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 3 Jul 2026 08:38:32 +0200 Subject: [PATCH 14/29] refac: filter by both carriers and volume limits --- scripts/cba/solve_cba_network.py | 45 +++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 057d611db5..fbecda2941 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -51,6 +51,37 @@ logger = logging.getLogger(__name__) +def get_components_with_volume_limits( + n: pypsa.Network, + type: str, + carriers: list[str], +) -> pd.Index: + """ + Return components that have volume limits, given a list of carriers. + + Parameters + ---------- + n : pypsa.Network + PyPSA network + type : str + Component type, e.g. "Generator" or "Link" + carriers : list of str + List of carriers to filter components by + + Returns + ------- + pd.Index + Index of components that have volume limits and match the given carriers + """ + static = n.c[type].static + if "has_volume_limit" not in static.columns: + return pd.Index([]) + + return static.index[ + static["carrier"].isin(carriers) & static["has_volume_limit"].eq(1) + ] + + def extra_functionality( n: pypsa.Network, snapshots: pd.DatetimeIndex, @@ -159,13 +190,13 @@ def optimize_with_rolling_horizon( n.storage_units_t.state_of_charge.loc[snapshots[start - 1]] ) - # Set per-window energy budgets for volume-limited components - # (biomass, biogas) based on PF dispatch stored in generators_t.p - for c_name in ["Generator", "Link"]: + # Set per-window energy budgets for biomass/biogas components + # based on PF dispatch stored in generators_t.p + for c_name in ["Link"]: c = n.c[c_name] - if "has_volume_limit" not in c.static.columns: - continue - vol_idx = c.static.index[c.static["has_volume_limit"] == 1] + vol_idx = get_components_with_volume_limits( + n, c_name, ["solid biomass", "biogas"] + ) if vol_idx.empty: continue p_col = "p" if c_name == "Generator" else "p0" @@ -305,7 +336,7 @@ def solve_network( snakemake = mock_snakemake( "solve_cba_network", run="NT", - cba_project="t4", + cba_project="t16", planning_horizons="2030", configfiles=["config/config.tyndp.yaml"], ) From 622132e9e69523bc79442349bfd9e710f364f017 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 3 Jul 2026 09:00:46 +0200 Subject: [PATCH 15/29] refac: remove setting marginal cost dataframe --- scripts/cba/prepare_rolling_horizon.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 95ea21f135..231d6bd84a 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -233,14 +233,6 @@ def apply_biomass_biogas_bus_marginal_prices( # Get index of generators with target carriers gen_index = n.generators.index[n.generators.carrier.isin(carriers)] - # Make sure marginal cost frame exists on correct index - if ( - n.generators_t.marginal_cost.empty - or not n.generators_t.marginal_cost.index.equals(n.snapshots) - ): - n.generators_t.marginal_cost = pd.DataFrame(index=n.snapshots) - - # Add bus marginal price to generator marginal cost for each generator for g in gen_index: bus = n.generators.at[g, "bus"] n.generators_t.marginal_cost[g] = bus_mp[bus].reindex(n.snapshots) From dde4f58974430b805b25657dd981041079b9f141 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 3 Jul 2026 09:04:34 +0200 Subject: [PATCH 16/29] chore: add type hints --- scripts/cba/prepare_rolling_horizon.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 231d6bd84a..1dd3167622 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -192,10 +192,10 @@ def apply_msv_to_network( def apply_biomass_biogas_bus_marginal_prices( - n, - n_msv, - carriers=("solid biomass", "biogas"), - resample_method="ffill", + n: pypsa.Network, + n_msv: pypsa.Network, + carriers: list[str] = ["solid biomass", "biogas"], + resample_method: str = "ffill", ): """ Add marginal prices to biomass/biogas generator marginal costs. @@ -212,13 +212,13 @@ def apply_biomass_biogas_bus_marginal_prices( Target network (will be modified in place). n_msv : pypsa.Network Solved MSV network containing bus marginal prices. - carriers : tuple[str], optional - Generator carriers to apply bus marginal prices to. Defaults to ("solid biomass", "biogas"). + carriers : list[str], optional + Generator carriers to apply bus marginal prices to. Defaults to ["solid biomass", "biogas"]. resample_method : str, optional Method for resampling MSV bus marginal prices to target resolution. Default "ffill". """ if isinstance(carriers, str): - carriers = (carriers,) + carriers = [carriers] # Get bus marginal prices from MSV network msv_mp = n_msv.buses_t.marginal_price @@ -233,6 +233,7 @@ def apply_biomass_biogas_bus_marginal_prices( # Get index of generators with target carriers gen_index = n.generators.index[n.generators.carrier.isin(carriers)] + # Set marginal cost for each generator based on the marginal price of its bus for g in gen_index: bus = n.generators.at[g, "bus"] n.generators_t.marginal_cost[g] = bus_mp[bus].reindex(n.snapshots) From ca262db50bdcd27da8b09f9da9a90e662dc60b3c Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 3 Jul 2026 12:15:00 +0200 Subject: [PATCH 17/29] feat: add `cba.biomass_biogas_slack` config option as rule input --- rules/cba.smk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rules/cba.smk b/rules/cba.smk index 7baf9ab965..578789cce6 100644 --- a/rules/cba.smk +++ b/rules/cba.smk @@ -387,6 +387,7 @@ rule solve_cba_reference_network: params: solving=config_provider("solving"), cba_solving=config_provider("cba", "solving"), + biomass_biogas_slack=config_provider("cba", "biomass_biogas_slack"), foresight=config_provider("foresight"), time_resolution=config_provider("clustering", "temporal", "resolution_sector"), custom_extra_functionality=None, @@ -408,6 +409,7 @@ rule solve_cba_network: params: solving=config_provider("solving"), cba_solving=config_provider("cba", "solving"), + biomass_biogas_slack=config_provider("cba", "biomass_biogas_slack"), foresight=config_provider("foresight"), time_resolution=config_provider("clustering", "temporal", "resolution_sector"), custom_extra_functionality=None, From e5195a787f0a4705186cb8f35da9e2fbf55efd91 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Fri, 3 Jul 2026 17:26:34 +0200 Subject: [PATCH 18/29] fix: add back Generator type --- scripts/cba/solve_cba_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index fbecda2941..5d712f5c36 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -192,7 +192,7 @@ def optimize_with_rolling_horizon( # Set per-window energy budgets for biomass/biogas components # based on PF dispatch stored in generators_t.p - for c_name in ["Link"]: + for c_name in ["Generator", "Link"]: c = n.c[c_name] vol_idx = get_components_with_volume_limits( n, c_name, ["solid biomass", "biogas"] From ea3aa4ee7db6ee0bcd81fcf8bbb88e9c49625daf Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Mon, 6 Jul 2026 15:27:45 +0200 Subject: [PATCH 19/29] docs: add #719 to doc/release_notes --- doc/release_notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/release_notes.md b/doc/release_notes.md index 79c6bf77cc..574ffcb53a 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -29,6 +29,8 @@ - Add `include_objective_constant` and `assign_all_duals` to solving config validator. - Add `gurobi-simplex` as solver option. +* Change dispatch of biomass and biogas generators in CBA by (a) setting the buses' marginal prices as the generators' marginal costs and (b) including a slack of 40% in the `e_sum_min` (https://github.com/open-energy-transition/open-tyndp/pull/719). + **Bugfixes and Compatibility** **Documentation** From ecbd8032f1ebbfd74f5777a173ac126f6b70c139 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:28:11 +0000 Subject: [PATCH 20/29] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/cba.smk | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rules/cba.smk b/rules/cba.smk index 4cbf7c263b..7b4039d0a7 100644 --- a/rules/cba.smk +++ b/rules/cba.smk @@ -384,13 +384,6 @@ rule prepare_project: # Solve reference network with rolling horizon (MSV already applied) rule solve_cba_reference_network: - params: - solving=config_provider("solving"), - cba_solving=config_provider("cba", "solving"), - biomass_biogas_slack=config_provider("cba", "biomass_biogas_slack"), - foresight=config_provider("foresight"), - time_resolution=config_provider("clustering", "temporal", "resolution_sector"), - custom_extra_functionality=None, input: network=rules.prepare_rolling_horizon.output.network, output: From 5532a4facbdb209bdfe03a0f73e9a081e9c99df2 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Mon, 6 Jul 2026 15:39:00 +0200 Subject: [PATCH 21/29] chore: add details in comments of configs --- config/config.tyndp.yaml | 4 +++- config/schema.default.json | 4 ++-- config/test/config.cyears.yaml | 4 +++- scripts/lib/validation/config/cba.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config/config.tyndp.yaml b/config/config.tyndp.yaml index 07507cc960..9aa0d42d13 100644 --- a/config/config.tyndp.yaml +++ b/config/config.tyndp.yaml @@ -433,7 +433,9 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Set to 40% to allow for all projects to successfully solve in 1H resolution + biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: diff --git a/config/schema.default.json b/config/schema.default.json index 74e57a1b93..8dd2484fe7 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -540,7 +540,7 @@ }, "biomass_biogas_slack": { "default": 0.4, - "description": "Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + "description": "Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", "maximum": 1.0, "minimum": 0.0, "type": "number" @@ -15700,7 +15700,7 @@ }, "biomass_biogas_slack": { "default": 0.4, - "description": "Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + "description": "Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", "maximum": 1.0, "minimum": 0.0, "type": "number" diff --git a/config/test/config.cyears.yaml b/config/test/config.cyears.yaml index 84ce71690c..1ac92b54da 100644 --- a/config/test/config.cyears.yaml +++ b/config/test/config.cyears.yaml @@ -443,7 +443,9 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Set to 40% to allow for all projects to successfully solve in 1H resolution + biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: diff --git a/scripts/lib/validation/config/cba.py b/scripts/lib/validation/config/cba.py index 7fac2a88cb..ec8b8dab14 100644 --- a/scripts/lib/validation/config/cba.py +++ b/scripts/lib/validation/config/cba.py @@ -181,7 +181,7 @@ class CbaConfig(BaseModel): 0.4, ge=0.0, le=1.0, - description="Slack applied to rolling-horizon biomass and biogas `e_sum_min` limits as a fraction of the perfect-foresight window dispatch.", + description="Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", ) storage: _CbaStorageConfig = Field( default_factory=_CbaStorageConfig, From 34f15ec2a73aed74eb4d747fd45775d94ad6d695 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Mon, 6 Jul 2026 15:41:37 +0200 Subject: [PATCH 22/29] docs: change function description --- scripts/cba/prepare_rolling_horizon.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index 1dd3167622..d458f53698 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -198,18 +198,16 @@ def apply_biomass_biogas_bus_marginal_prices( resample_method: str = "ffill", ): """ - Add marginal prices to biomass/biogas generator marginal costs. + Set biomass/biogas buses' marginal prices as the generators' marginal costs. - For each biomass/biogas generator in the rolling-horizon network: + For each biomass/biogas generator in the rolling horizon network: use the marginal price time series of its attached bus from the MSV network and - add the marginal price to the generator's base marginal cost. - - effective_marginal_cost_t = static_marginal_cost + bus_marginal_price_t + set the marginal price as the generator's marginal cost. Parameters ---------- n : pypsa.Network - Target network (will be modified in place). + Target network. n_msv : pypsa.Network Solved MSV network containing bus marginal prices. carriers : list[str], optional From 4e0abee5937f50e7650404c3e81e90177150362f Mon Sep 17 00:00:00 2001 From: meas Date: Wed, 8 Jul 2026 10:02:43 +0200 Subject: [PATCH 23/29] chore: edit comment for slack in config/test/config.tyndp.yaml Co-authored-by: Thomas Gilon --- config/test/config.tyndp.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/test/config.tyndp.yaml b/config/test/config.tyndp.yaml index e664ba2c5e..3819407a9d 100644 --- a/config/test/config.tyndp.yaml +++ b/config/test/config.tyndp.yaml @@ -441,7 +441,9 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - biomass_biogas_slack: 0.4 # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Slack on rolling-horizon biomass and biogas e_sum_min volume limits + # Set to 40% to allow for all projects to successfully solve in 1H resolution + biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: From 41b387e375be02f88a3172293720bc0075f12289 Mon Sep 17 00:00:00 2001 From: meas Date: Wed, 8 Jul 2026 10:03:04 +0200 Subject: [PATCH 24/29] doc: edit link in PR description Co-authored-by: Thomas Gilon --- doc/release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.md b/doc/release_notes.md index 574ffcb53a..3db6ddd202 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -29,7 +29,7 @@ - Add `include_objective_constant` and `assign_all_duals` to solving config validator. - Add `gurobi-simplex` as solver option. -* Change dispatch of biomass and biogas generators in CBA by (a) setting the buses' marginal prices as the generators' marginal costs and (b) including a slack of 40% in the `e_sum_min` (https://github.com/open-energy-transition/open-tyndp/pull/719). +* Change dispatch of biomass and biogas generators in CBA by (a) setting the buses' marginal prices as the generators' marginal costs and (b) including a slack of 40% in the `e_sum_min` ([#719](https://github.com/open-energy-transition/open-tyndp/pull/719)). **Bugfixes and Compatibility** From cb0a84f43d49cb98c21cfba2bd72cfad9f2fcceb Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Wed, 8 Jul 2026 10:35:45 +0200 Subject: [PATCH 25/29] refac: change `has_volume_limit` to bool --- scripts/cba/prepare_rolling_horizon.py | 4 ++-- scripts/cba/solve_cba_network.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/cba/prepare_rolling_horizon.py b/scripts/cba/prepare_rolling_horizon.py index d458f53698..48659efa1c 100644 --- a/scripts/cba/prepare_rolling_horizon.py +++ b/scripts/cba/prepare_rolling_horizon.py @@ -130,8 +130,8 @@ def disable_volume_limits(n: pypsa.Network): for c in n.components[{"Generator", "Link"}]: has_e_sum_min = isfinite(c.static.get("e_sum_min", [])) if has_e_sum_min.any(): - c.static["has_volume_limit"] = 0 - c.static.loc[has_e_sum_min, "has_volume_limit"] = 1 + c.static["has_volume_limit"] = False + c.static.loc[has_e_sum_min, "has_volume_limit"] = True c.static.loc[has_e_sum_min, "e_sum_min"] = -inf c.static.loc[has_e_sum_min, "e_sum_max"] = inf diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 5d712f5c36..c28433910d 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -77,9 +77,7 @@ def get_components_with_volume_limits( if "has_volume_limit" not in static.columns: return pd.Index([]) - return static.index[ - static["carrier"].isin(carriers) & static["has_volume_limit"].eq(1) - ] + return static.index[static["carrier"].isin(carriers) & static["has_volume_limit"]] def extra_functionality( From 7a6d7af2d2672f01b4f280af18df8c10490d454e Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 9 Jul 2026 19:43:55 +0200 Subject: [PATCH 26/29] feat: remove `e_sum` energy budget constraint on biomass/biogas slack --- scripts/cba/solve_cba_network.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index dc93c60f29..5469826dab 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -221,26 +221,6 @@ def optimize_with_rolling_horizon( n.storage_units_t.state_of_charge.loc[snapshots[start - 1]] ) - # Set per-window energy budgets for biomass/biogas components - # based on PF dispatch stored in generators_t.p - for c_name in ["Generator", "Link"]: - c = n.c[c_name] - vol_idx = get_components_with_volume_limits( - n, c_name, ["solid biomass", "biogas"] - ) - if vol_idx.empty: - continue - p_col = "p" if c_name == "Generator" else "p0" - pf_p = c.dynamic[p_col] - for comp in vol_idx: - if comp not in pf_p.columns: - continue - window_energy = pf_p.loc[sns, comp].sum() - c.static.loc[comp, "e_sum_min"] = ( - 1 - biomass_biogas_slack - ) * window_energy - c.static.loc[comp, "e_sum_max"] = window_energy - status, condition = n.optimize(sns, **kwargs) # type: ignore # If solve is successful, dispose of Model object to release license before next rolling horizon From 29e7e87dced5a234ffa34b575c1e426cda2a6547 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 9 Jul 2026 19:52:23 +0200 Subject: [PATCH 27/29] feat: remove `cba.biomass_biogas_slack` config option --- config/config.default.yaml | 1 - config/config.tyndp.yaml | 3 --- config/schema.default.json | 14 -------------- config/test/config.cyears.yaml | 3 --- config/test/config.tyndp.yaml | 3 --- rules/cba.smk | 1 - scripts/cba/solve_cba_network.py | 1 - scripts/lib/validation/config/cba.py | 6 ------ 8 files changed, 32 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index aa6b221ffd..b616b54f8c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -1617,7 +1617,6 @@ cba: area: tyndp remove_noisy_costs: true negative_toot_capacity: zero - biomass_biogas_slack: 0.4 storage: cyclic_carriers: - battery diff --git a/config/config.tyndp.yaml b/config/config.tyndp.yaml index 9aa0d42d13..f6e1351f45 100644 --- a/config/config.tyndp.yaml +++ b/config/config.tyndp.yaml @@ -433,9 +433,6 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - # Slack on rolling-horizon biomass and biogas e_sum_min volume limits - # Set to 40% to allow for all projects to successfully solve in 1H resolution - biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: diff --git a/config/schema.default.json b/config/schema.default.json index 8dd2484fe7..fd78387fa1 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -538,13 +538,6 @@ ], "type": "string" }, - "biomass_biogas_slack": { - "default": 0.4, - "description": "Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", - "maximum": 1.0, - "minimum": 0.0, - "type": "number" - }, "storage": { "description": "Configuration for `cba.storage` settings.", "properties": { @@ -15698,13 +15691,6 @@ ], "type": "string" }, - "biomass_biogas_slack": { - "default": 0.4, - "description": "Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", - "maximum": 1.0, - "minimum": 0.0, - "type": "number" - }, "storage": { "description": "Configuration for `cba.storage` settings.", "properties": { diff --git a/config/test/config.cyears.yaml b/config/test/config.cyears.yaml index 1ac92b54da..2981f59e1e 100644 --- a/config/test/config.cyears.yaml +++ b/config/test/config.cyears.yaml @@ -443,9 +443,6 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - # Slack on rolling-horizon biomass and biogas e_sum_min volume limits - # Set to 40% to allow for all projects to successfully solve in 1H resolution - biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: diff --git a/config/test/config.tyndp.yaml b/config/test/config.tyndp.yaml index 3819407a9d..55d6383728 100644 --- a/config/test/config.tyndp.yaml +++ b/config/test/config.tyndp.yaml @@ -441,9 +441,6 @@ cba: area: tyndp # options: tyndp (TODO: entso-e, eu27) remove_noisy_costs: true # Use pre-noise costs for indicators while keeping noisy costs in the solve negative_toot_capacity: zero - # Slack on rolling-horizon biomass and biogas e_sum_min volume limits - # Set to 40% to allow for all projects to successfully solve in 1H resolution - biomass_biogas_slack: 0.4 # Storage settings for dispatch storage: diff --git a/rules/cba.smk b/rules/cba.smk index 7b4039d0a7..c90db6d1f6 100644 --- a/rules/cba.smk +++ b/rules/cba.smk @@ -396,7 +396,6 @@ rule solve_cba_reference_network: params: solving=config_provider("solving"), cba_solving=config_provider("cba", "solving"), - biomass_biogas_slack=config_provider("cba", "biomass_biogas_slack"), foresight=config_provider("foresight"), time_resolution=config_provider("clustering", "temporal", "resolution_sector"), custom_extra_functionality=None, diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index 5469826dab..e252680503 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -198,7 +198,6 @@ def optimize_with_rolling_horizon( assert len(snapshots), "Need at least one snapshot to optimize" fallback_solver = kwargs.pop("fallback_solver", None) - biomass_biogas_slack = n.config["cba"].get("biomass_biogas_slack", 0.4) starting_points = range(0, len(snapshots), horizon - overlap) for i, start in tqdm(enumerate(starting_points), total=len(starting_points)): diff --git a/scripts/lib/validation/config/cba.py b/scripts/lib/validation/config/cba.py index ec8b8dab14..5ee591f876 100644 --- a/scripts/lib/validation/config/cba.py +++ b/scripts/lib/validation/config/cba.py @@ -177,12 +177,6 @@ class CbaConfig(BaseModel): default="zero", description="How to handle TOOT project removal when removing project capacity would make an existing interconnector capacity negative. 'zero' clamps the resulting capacity to zero and continues; 'break' raises an error.", ) - biomass_biogas_slack: float = Field( - 0.4, - ge=0.0, - le=1.0, - description="Slack applied to CBA rolling horizon biomass and biogas `e_sum_min` limits. The slack is applied as a fraction of the original value, allowing for more flexible dispatch of biomass and biogas units in the CBA workflow. This is only applied to the `e_sum_min`, while the `e_sum_max` remains unchanged.", - ) storage: _CbaStorageConfig = Field( default_factory=_CbaStorageConfig, description="Storage configuration for the cost-benefit analysis workflow.", From 4930a70f0a71faefa7d77e4016c4da6502412673 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 9 Jul 2026 19:54:35 +0200 Subject: [PATCH 28/29] refac: remove `get_components_with_volume_limits()` function --- scripts/cba/solve_cba_network.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/scripts/cba/solve_cba_network.py b/scripts/cba/solve_cba_network.py index e252680503..781dcefe42 100644 --- a/scripts/cba/solve_cba_network.py +++ b/scripts/cba/solve_cba_network.py @@ -51,35 +51,6 @@ logger = logging.getLogger(__name__) -def get_components_with_volume_limits( - n: pypsa.Network, - type: str, - carriers: list[str], -) -> pd.Index: - """ - Return components that have volume limits, given a list of carriers. - - Parameters - ---------- - n : pypsa.Network - PyPSA network - type : str - Component type, e.g. "Generator" or "Link" - carriers : list of str - List of carriers to filter components by - - Returns - ------- - pd.Index - Index of components that have volume limits and match the given carriers - """ - static = n.c[type].static - if "has_volume_limit" not in static.columns: - return pd.Index([]) - - return static.index[static["carrier"].isin(carriers) & static["has_volume_limit"]] - - def dispose_model(n: pypsa.Network) -> None: """ Explicitly dispose of Model object. From 7cf340dade0d2cb0a3c5a7936c3a64d5bcb41c79 Mon Sep 17 00:00:00 2001 From: Measrainsey Meng Date: Thu, 9 Jul 2026 19:56:40 +0200 Subject: [PATCH 29/29] docs: edit PR description in doc/release_notes --- doc/release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.md b/doc/release_notes.md index b8bfb4c97e..00cc3c3131 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -29,7 +29,7 @@ - Add `include_objective_constant` and `assign_all_duals` to solving config validator. - Add `gurobi-simplex` as solver option. -* Change dispatch of biomass and biogas generators in CBA by (a) setting the buses' marginal prices as the generators' marginal costs and (b) including a slack of 40% in the `e_sum_min` ([#719](https://github.com/open-energy-transition/open-tyndp/pull/719)). +* Change dispatch of biomass and biogas generators in CBA by (a) setting the buses' marginal prices as the generators' marginal costs and (b) removing energy budget constraints ([#719](https://github.com/open-energy-transition/open-tyndp/pull/719)). **Bugfixes and Compatibility**