feat(cba): add MSV extraction for seasonal storage dispatch#441
feat(cba): add MSV extraction for seasonal storage dispatch#441
Conversation
Implement Marginal Storage Value (MSV) extraction to guide seasonal storage dispatch in rolling horizon CBA optimization. Changes: - Add prepare_cba_base rule to fix capacities and remove co2_sequestration_limit - Add solve_cba_msv_extraction rule for perfect foresight MSV extraction - Apply MSV as marginal_cost to seasonal stores (H2, gas, co2 sequestered) - Support temporal resampling with configurable resolution and method (ffill/interpolate) - Refactor pipeline: base → reference → MSV extraction → simplify → dispatch Config options: - cba.storage.seasonal_carriers: carriers receiving MSV - cba.msv_extraction.resolution: extraction resolution (false or 24H, etc.) - cba.msv_extraction.resample_method: ffill or interpolate
for more information, see https://pre-commit.ci
lisazeyen
left a comment
There was a problem hiding this comment.
thanks @cdgaete for the PR! I have a few comments concerning the workflow structure and naming.
Before running the solve_cba_msv_extraction we should
- keep all global constraints
- fix all capacities first but then set the primary fuel generators to extendable
Then run the solving with the full year optional with a lower temporal resolution
In the next rule, set the shadow prices as marginal cost, remove all global constraints, set non cyclic for seasonal storage and then prepare pint/toot run the solving.
I have some more detailed comments below, feel free to write me a message if something is unclear.
rules/cba.smk
Outdated
|
|
||
| # Simplify reference network for rolling horizon dispatch | ||
| # Disables cyclicity for seasonal stores (they will receive MSV instead) | ||
| rule simplify_sb_network: |
There was a problem hiding this comment.
the naming of the rules is now confusing, since you have as an input a CBA network but it is called simplify SB network. Simplifications of the SB network (e.g. merging the two H2 zones) should already happen in the rule which you renamed to prepare_cba_base.
I would suggest to rename rule + script and where ever else used the rule which is called in this implementation simplify_sb_network to prepare_rolling_horizon
In this rule, we
- add the MSV to the network
- set the storage non-cyclic if seasonal
- drop all global constraints
scripts/cba/simplify_sb_network.py
Outdated
| tyndp_conventional_carriers = snakemake.params.tyndp_conventional_carriers | ||
| n = extend_primary_fuel_sources(n, tyndp_conventional_carriers) |
There was a problem hiding this comment.
This should already happening before solve_cba_msv_extraction you can move fixing the optimal capacities and extending primary fuel source capacities to prepare_cba_base
rules/cba.smk
Outdated
| rule simplify_sb_network: | ||
| # Prepare base network for CBA (common for both MSV extraction and reference) | ||
| # Fixes optimal capacities and adds hurdle costs | ||
| rule prepare_cba_base: |
There was a problem hiding this comment.
I think we should keep the old name for the rule and the script here so replace here and in the following prepare_cba_base which simplify_sb_network
Because this is exactly the script which reads in the SB network and does modifications to it
What it should do here:
- hurdle costs
- fix optimal capacities
- make primary fuel carriers extendable
- keep ALL global constraints
In the future: - modifications concerning EV demand/merging H2 zones
rules/cba.smk
Outdated
| seasonal_carriers=config_provider("cba", "storage", "seasonal_carriers"), | ||
| msv_resample_method=config_provider("cba", "msv_extraction", "resample_method"), |
There was a problem hiding this comment.
If you add the MSV already in currently called simplify_sb_network (my suggestion: prepare_rolling_horizon) you can remove the params here
scripts/cba/simplify_sb_network.py
Outdated
| cyclic_carriers = snakemake.params.get("cyclic_carriers", []) | ||
| disable_store_cyclicity(n, cyclic_carriers=cyclic_carriers) | ||
| seasonal_carriers = snakemake.params.get("seasonal_carriers", []) | ||
| disable_store_cyclicity( |
There was a problem hiding this comment.
I would add here the setting of the MSV as marginal costs
There was a problem hiding this comment.
Done, thank you very much Lisa for all the comments. Now the code looks much more clean. I will provide a detailed comment that summarize all changes.
Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com>
Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com>
Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com>
Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com>
Co-authored-by: lisazeyen <35347358+lisazeyen@users.noreply.github.com>
for more information, see https://pre-commit.ci
…egation Address review feedback on workflow structure and naming: - Rename prepare_cba_base -> simplify_sb_network (fixes capacities, hurdle costs, extends fuel sources, disables volume limits) - Create prepare_rolling_horizon rule (disables cyclicity, applies MSV) - Move MSV application from solve-time to network preparation phase - Add build_msv_snapshot_weightings rule for temporal aggregation support The set_temporal_aggregation function requires a CSV file with pre-computed snapshot weightings, so a new rule generates this file when hourly resolution is configured (e.g., "24H", "48H"). This follows existing snakemake patterns. Additional changes: - Add seasonal_carriers and msv_extraction to config schema - Add _CbaMsvExtractionConfig to validation config - Improve docstrings and comments for clarity - Fix typo: "unustainable" -> "unsustainable"
f4d4478 to
a63723f
Compare
for more information, see https://pre-commit.ci
|
Thanks Lisa for the thorough review! I've addressed all your feedback: ✅ Workflow restructuring:
✅ Naming consistency:
✅ Schema updates:
📝 Note on Tested with |
lisazeyen
left a comment
There was a problem hiding this comment.
@cdgaete thanks a lot for the implementations.
I have two more comments concerning the temporal aggregation and a remaining global constraint in the simplify_sb_network rule.
I also made some further changes (add MSV to storage units, removed some of the logging because it was quite extensive).
Can you check my changes, implement the remaining change concerning the temporal aggregation and do some test runs?
| "cba/msv_snapshot_weightings_{planning_horizons}.csv" | ||
| ), | ||
| script: | ||
| "../scripts/cba/build_msv_snapshot_weightings.py" |
There was a problem hiding this comment.
It would be better to reuse the existing script time_aggregation.py
There was a problem hiding this comment.
Hi @lisazeyen, thanks for the review and commits. time_aggregation.py can't be reused as-is since it's coupled to SB workflow configs (resolution_elec/resolution_sector dict, drop_leap_day, solver_name, optional heat/solar inputs for segmentation). Refactoring it into importable functions would be the clean solution but seems out of scope for this PR.
Instead, build_msv_snapshot_weightings.py now would replicates the relevant logic from time_aggregation.py: per-year resampling, zero-weight filtering, and leap day handling for the false "Nsn" "Nh" formats. The aggregation itself reuses set_temporal_aggregation from prepare_sector_network.py. Did I understand well the task?
# build_msv_snapshot_weightings.py
# No aggregation or representative snapshots — output empty CSV
# (set_temporal_aggregation handles "Nsn" directly without a file)
if not resolution or (isinstance(resolution, str) and "sn" in resolution.lower()):
logger.info("No hourly resampling needed, creating empty weightings file")
pd.DataFrame().to_csv(snakemake.output.snapshot_weightings)
# Hourly resampling (e.g., "24H", "48H")
elif isinstance(resolution, str) and "h" in resolution.lower():
offset = resolution.lower()
logger.info(f"Resampling snapshot weightings every {offset}")
# Resample years separately to handle non-contiguous years
years = pd.DatetimeIndex(n.snapshots).year.unique()
snapshot_weightings = []
for year in years:
sws_year = n.snapshot_weightings[n.snapshots.year == year]
sws_year = sws_year.resample(offset).sum()
snapshot_weightings.append(sws_year)
snapshot_weightings = pd.concat(snapshot_weightings)
# Drop rows with zero weight (gaps in original snapshots)
zeros_i = snapshot_weightings.query("objective == 0").index
snapshot_weightings.drop(zeros_i, inplace=True)
# Handle leap days: redistribute weights to March 1st
swi = snapshot_weightings.index
leap_days = swi[(swi.month == 2) & (swi.day == 29)]
if drop_leap_day and not leap_days.empty:
for year in leap_days.year.unique():
year_leap_days = leap_days[leap_days.year == year]
leap_weights = snapshot_weightings.loc[year_leap_days].sum()
march_first = pd.Timestamp(year, 3, 1, 0, 0, 0)
snapshot_weightings.loc[march_first] = leap_weights
snapshot_weightings = snapshot_weightings.drop(leap_days).sort_index()
logger.info("Dropped leap day(s), redistributed weights to March 1st")
logger.info(
f"Generated weightings: {len(n.snapshots)} -> {len(snapshot_weightings)} snapshots"
)
snapshot_weightings.to_csv(snakemake.output.snapshot_weightings)
else:
raise ValueError(
f"Unsupported MSV extraction resolution: {resolution!r}. "
"Use false, 'Nsn' (e.g., '2sn'), or 'Nh' (e.g., '24H', '48H')."
)
scripts/cba/simplify_sb_network.py
Outdated
| Disable volume limits (e_sum_min) for generators and links. | ||
|
|
||
| Volume limits constrain minimum energy production over the optimization period. | ||
| Disable minimum energy production limits (e_sum_min) for generators and links. |
There was a problem hiding this comment.
this should not happen here but in prepare_rolling_horizon I moved it in this commit
|
@cdgaete the last commit is not quite what I meant concerning the temporal aggregation. Can we just reuse the same script |
Split storage carriers into three categories for rolling horizon dispatch: - seasonal_carriers: MSV + initial state from perfect foresight (hydro, H2, gas) - accumulator_carriers: MSV only, start empty (co2 sequestered) - cyclic_carriers: unchanged (batteries) Add set_initial_state_from_pf() to initialize seasonal stores from PF e(t=-1), addressing hydro reservoir under-dispatch in rolling horizon. Extend MSV extraction and application to StorageUnit components (needed for hydro modeled as StorageUnit in PyPSA). Separate MSV extraction solving config (load_shedding disabled to preserve duals) from rolling horizon solving config. Add fallback solver mechanism for rolling horizon window failures.
Fix state_of_charge_set on hydro-reservoir StorageUnits to the PF trajectory, giving the RH optimizer SOC guidance while retaining dispatch freedom for all other components. Results (2030 reference network): - Load shedding: 12.2 TWh → 0.0001 TWh (eliminated) - Bus prices match PF: mean 71.9 vs 71.4 EUR/MWh, no VOLL hours - Reservoir spill: 140 TWh → 0.25 TWh (water no longer wasted) - Generation mix within 3% of PF across all carriers Also documents failed Approach 1 (p_dispatch_set) which caused infeasibility because spill remained free, draining reservoirs between RH windows.
Closes #376 (Phase 1: Implementation for Stores)
Changes proposed in this Pull Request
This PR implements Marginal Storage Value (MSV) extraction for CBA seasonal storage dispatch. MSV addresses the rolling horizon myopia problem where the optimizer, seeing only a limited time window, cannot properly value seasonal storage for future scarcity periods.
New scripts:
scripts/cba/prepare_cba_base.py: Prepares the base CBA network with fixed capacities, hurdle costs, and removesco2_sequestration_limitscripts/cba/solve_cba_msv_extraction.py: Solves perfect foresight optimization and extracts MSV from bus marginal pricesscripts/cba/solve_cba_network.py: Applies MSV as marginal costs and runs rolling horizon dispatchModified files:
rules/cba.smk: New rules for MSV extraction workflowscripts/cba/prepare_reference.py: Integration with MSV workflowscripts/cba/simplify_sb_network.py: Refactored constraint handling and cyclicity logicconfig/config.tyndp.yaml: New MSV configuration optionsWorkflow
New config options:
Notes
co2_sequestration_limitis removed inprepare_cba_base.pyso both MSV extraction and rolling horizon see the same constraint structure.unsustainable biomass limitis removed later insimplify_sb_network.py- for full consistency this could also be moved toprepare_cba_base.pyin a follow-up.Checklist
pixi.toml(usingpixi add).config/config.default.yaml.doc/configtables/*.csv.config/test/*.yaml.doc/*.rstfiles.doc/release_notes.rstis added.READMEanddoc/index.rst.