Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3b004e1
feat: set marginal costs for biomass and biogas generators
measrainsey May 28, 2026
20a86c1
feat: remove volume limits on biomass and biogas dispatch
measrainsey May 28, 2026
7f2a678
chore: remove checks and logs
measrainsey May 28, 2026
bcc5199
Merge branch 'master' into feat/bio-dispatch
measrainsey Jun 15, 2026
3c3be42
Merge branch 'master' into feat/bio-dispatch
measrainsey Jun 16, 2026
68634be
chore: remove logger and update docs
measrainsey Jun 16, 2026
d6d7114
refac: make simple refactoring and add comments
measrainsey Jun 16, 2026
9f233ac
feat: revert `scripts/cba/solve_cba_network.py` back to `master`
measrainsey Jun 16, 2026
d7c2854
feat: add slack to biomass/biogas dispatch `e_sum_min`
measrainsey Jun 17, 2026
ea075e4
feat: increase slack to 40%
measrainsey Jun 17, 2026
1977d07
docs: add #719 to doc/release_notes
measrainsey Jun 17, 2026
0698bd4
refac: use `n_msv.snapshots` instead of `msv_mp.index`
measrainsey Jul 2, 2026
9dc4fee
feat: directly set marginal price as marginal cost, instead of adding
measrainsey Jul 2, 2026
be81501
chore: edit comment
measrainsey Jul 2, 2026
ee16ef4
feat: create config option `cba.biomass_biogas_slack` for slack inste…
measrainsey Jul 2, 2026
ab0f708
refac: filter by both carriers and volume limits
measrainsey Jul 3, 2026
622132e
refac: remove setting marginal cost dataframe
measrainsey Jul 3, 2026
dde4f58
chore: add type hints
measrainsey Jul 3, 2026
ca262db
feat: add `cba.biomass_biogas_slack` config option as rule input
measrainsey Jul 3, 2026
9413b84
Merge branch 'master' into feat/bio-dispatch
measrainsey Jul 3, 2026
e5195a7
fix: add back Generator type
measrainsey Jul 3, 2026
7dbda4a
Merge branch 'master' into feat/bio-dispatch
measrainsey Jul 6, 2026
ea3aa4e
docs: add #719 to doc/release_notes
measrainsey Jul 6, 2026
ecbd803
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 6, 2026
5532a4f
chore: add details in comments of configs
measrainsey Jul 6, 2026
34f15ec
docs: change function description
measrainsey Jul 6, 2026
4e0abee
chore: edit comment for slack in config/test/config.tyndp.yaml
measrainsey Jul 8, 2026
41b387e
doc: edit link in PR description
measrainsey Jul 8, 2026
cb0a84f
refac: change `has_volume_limit` to bool
measrainsey Jul 8, 2026
d973cdb
Merge branch 'master' into feat/bio-dispatch
measrainsey Jul 9, 2026
7a6d7af
feat: remove `e_sum` energy budget constraint on biomass/biogas slack
measrainsey Jul 9, 2026
29e7e87
feat: remove `cba.biomass_biogas_slack` config option
measrainsey Jul 9, 2026
4930a70
refac: remove `get_components_with_volume_limits()` function
measrainsey Jul 9, 2026
7cf340d
docs: edit PR description in doc/release_notes
measrainsey Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1617,6 +1617,7 @@ cba:
area: tyndp
remove_noisy_costs: true
negative_toot_capacity: zero
biomass_biogas_slack: 0.4
storage:
cyclic_carriers:
- battery
Expand Down
3 changes: 3 additions & 0 deletions config/config.tyndp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +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
# 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:
Expand Down
14 changes: 14 additions & 0 deletions config/schema.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,13 @@
],
"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": {
Expand Down Expand Up @@ -15691,6 +15698,13 @@
],
"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": {
Expand Down
3 changes: 3 additions & 0 deletions config/test/config.cyears.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +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
# Slack on rolling-horizon biomass and biogas e_sum_min volume limits

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my understanding of the water value approach we should remove the limits on e_sum_min and e_sum_max, this should be covered by the introduced marginal prices

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, you're probably right. These are now redundant.

# Set to 40% to allow for all projects to successfully solve in 1H resolution
biomass_biogas_slack: 0.4

# Storage settings for dispatch
storage:
Expand Down
3 changes: 3 additions & 0 deletions config/test/config.tyndp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +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
# 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:
Expand Down
2 changes: 2 additions & 0 deletions doc/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` ([#719](https://github.com/open-energy-transition/open-tyndp/pull/719)).

**Bugfixes and Compatibility**

**Documentation**
Expand Down
1 change: 1 addition & 0 deletions rules/cba.smk
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,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,
Expand Down
53 changes: 51 additions & 2 deletions scripts/cba/prepare_rolling_horizon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -191,6 +191,52 @@ def apply_msv_to_network(
n.c[c].dynamic["marginal_cost"].loc[:, s_i] = msv


def apply_biomass_biogas_bus_marginal_prices(
Comment thread
tgilon marked this conversation as resolved.
n: pypsa.Network,
n_msv: pypsa.Network,
carriers: list[str] = ["solid biomass", "biogas"],
resample_method: str = "ffill",
):
"""
Set biomass/biogas buses' marginal prices as the generators' 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
set the marginal price as the generator's marginal cost.

Parameters
----------
n : pypsa.Network
Target network.
n_msv : pypsa.Network
Solved MSV network containing bus marginal prices.
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]

# Get bus marginal prices from MSV network
msv_mp = n_msv.buses_t.marginal_price

# Resample marginal prices if needed
bus_mp = (
msv_mp
if n_msv.snapshots.equals(n.snapshots)
else resample_msv_to_target(msv_mp, n.snapshots, method=resample_method)
)

# 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)


def set_initial_state_from_pf(
n: pypsa.Network,
n_msv: pypsa.Network,
Expand Down Expand Up @@ -354,6 +400,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 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
soc_boundary_carriers = snakemake.params.get("soc_boundary_carriers", [])
cba_solving = snakemake.config.get("cba", {}).get("solving", {})
Expand Down
46 changes: 39 additions & 7 deletions scripts/cba/solve_cba_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@
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 extra_functionality(
n: pypsa.Network,
snapshots: pd.DatetimeIndex,
Expand Down Expand Up @@ -136,6 +165,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)):
Expand All @@ -158,13 +188,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
# 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]
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"
Expand All @@ -173,7 +203,9 @@ 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
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
Expand Down Expand Up @@ -302,7 +334,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"],
)
Expand Down
6 changes: 6 additions & 0 deletions scripts/lib/validation/config/cba.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.",
Expand Down