Full-year seasonal-storage LP: residual-load duration curves & surplus utilisation across FES weather years#48
Draft
yangpu2022 wants to merge 18 commits into
Draft
Conversation
Standalone analysis that builds a 1-bus national copperplate from a solved scenario (generators aggregated by carrier with available VRE, the storage fleet, and the H2 store) and solves the whole year in one LP. Full-year foresight, tiny/fast, so it reflects seasonal H2 storage behaviour that the 24h rolling market cannot and that the full-network single solve OOMs on. Writes SoC trajectory + gas/curtailment sizing sweep to resources/analysis/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend seasonal_storage_lp.py with a full-year load-duration / residual-load-duration view across the FES 2030 & 2040 scenarios (weather 2010, low wind), showing the long-duration storage need and how the VRE surplus is utilised by storage. - vre_availability(): per-hour available VRE from the solved network (no LP solve), reusing agg_generators(). - compute_ldc(): demand / available-VRE / net-load pre & post storage duration curves + a surplus/deficit energy decomposition, persisted to seasonal_storage_<scn>_ldc.csv and _surplus.csv. - build_report(): consolidates into ONE Excel workbook (README first) and ONE tabbed, theme-aware vanilla-SVG HTML report, superseding the orphan plotly full_year_generation_storage.html. - Fast default (report from existing solved outputs, no solve) plus a --recompute flag that re-solves the full-year LP + sizing sweep first. Surplus split reconciles to total available VRE (err 0.000); firm peak falls 50.4->44.1 GW (2030) and 75.5->68.0 GW (2040) after storage; H2 seasonal swing grows 24 -> 982 GWh from 2030 to 2040. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single-node LP is cheap, so run every weather year with a solved
wholesale network: a 2x2 grid of 2030 & 2040 x weather 2010 (low wind)
and 2013. SCEN now carries {year, weather} per scenario; the Excel and
tabbed HTML report label and colour by horizon x weather year.
Weather sensitivity: the H2 store size is fixed by FES and binds in every
case (SoC swing = full store, 24 GWh in 2030, 982 GWh in 2040). In 2030
the store is too small to matter - the windier 2013 just curtails more
(31 vs 10 TWh). In 2040 the large store does more work in the windier
year (37.8 vs 20.7 TWh electrolysis in) and curtails less (23 vs 30 TWh)
despite more VRE - long-duration storage value scales with both fleet and
weather. Broader weather-year coverage (the 41-year ambition) needs the
offline scripts/gap/build_vre_profiles.py path, not more LP solves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
seasonal_storage_weatheryears.py runs the single-node LP (dispatch + seasonal storage) across all 41 weather years (1985-2025) on disk for FES 2030 & 2040, by swapping in weather-year VRE (gap-layer 2084-site fleet-CF x FES capacity) and temperature-driven project_demand while holding the firm + storage + H2 fleet fixed from a reference solved network. Unlike the offline gap layer this is the optimisation, so it yields a full hourly generation profile per (year, weather year). Validated against the solved-network 2013 case (VRE 271.7 vs 271.2 TWh in 2030, 478.8 vs 477.9 in 2040; electrolysis 0.86/37.76 vs 0.80/37.79). Key result: unserved energy is zero in all 41 weather years for both horizons; gas (2030 1.8-11, 2040 0-4.2 TWh) and curtailment (2040 14-83 TWh) swing widely with weather, worst in low-wind 2010. ~55s/weather-year; CF cached once per year and reused across both FES horizons. Outputs (git-ignored): seasonal_storage_wy_summary.csv (82 rows) and weatheryears/<ty>_wy<wy>_genmix.parquet (hourly per run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-tech plot_rdc_combined.R produces a 2x2 residual-demand duration-curve figure: top row is the gap-style residual (demand - VRE - must-run firm) across 41 weather years with a P10-P90 band; bottom row is the whole-network full-year optimisation (FES 2025 HT, weather 2010) with storage charge/discharge broken down by technology (Battery / Pumped hydro / LAES / Hydrogen). Bottom-row bands are binned into percentile bins so per-tech storage behaviour is readable. Also commits plot_rdc_reference.R and plot_hourly_dispatch_2030_2040.R. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…egend Applies scientific-figure best practice: replace the verbose bottom-row legend (4 techs x "dis X / cha Y TWh" + 2 line entries) with a compact colour-keyed table carrying the charge/discharge numbers, direct-label every series next to its curve/area, and move annotations adjacent to the feature they describe. Drop the parenthetical qualifiers from the panel titles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enlarge all text (base 13pt), and move the peak / residual-demand / surplus numbers out of the plot area into a consistent per-panel subtitle so no text floats over the bands. Inside each panel only the direct series labels and the storage key-table remain. Widen output to 15x9.5in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…istent Fix a credibility defect: the peak (31 / 57 GW) and after-storage numbers were computed from the data but the plotted curve was binned into 120 percentile bins, so the curve read ~25 GW at the left edge and the annotated peak could not be seen. Now the no-storage residual duration curve is plotted full-resolution (leftmost point = true peak, with a marker), and only the per-tech band nets are smoothed (moving mean, edge-carried) for readability. Remove the after-storage line, the "mean"/"after storage" labels and the subtitle; annotate the peak, residual-demand and surplus numbers next to their areas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l overlap Drop marine/tidal/wave from the VRE definition (both rows) so residual = demand - wind - solar - firm. Remove dead code (unused after_full/pkp, CAES pattern, subtitle theme) and stale comments. Move the bottom-row "Residual demand" label into clear space above the curve so no text overlaps the bands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th rows Delete the superseded residual_demand_storage.* and residual_demand_and_energy.* figures and remove their generating code from plot_hourly_dispatch (they used a different residual = demand - wind - solar with no firm subtracted, giving an inconsistent second residual-demand number). Standardise on rdc_combined. Coordinate the residual accounting: make the whole-network "firm" regex identical to the top-row 41-year FIRM set (drop small_hydro/oil; oil is a dispatchable peaker, not must-run), so both rows use residual = demand - wind - solar - firm. Label the top-row residual demand as the 41-year mean and the bottom as weather 2010, and expand the caption to explain the mean-vs-single-year difference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n too Both rows are Holistic Transition (top: 41 weather years single-node; bottom: weather 2010 whole-network); the caption previously named the scenario only for the bottom row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…4 panels Same residual definition in both rows, so label them identically: "Residual demand" / "Residual surplus" + value + (41-yr mean | weather 2010), all bold, same size, at the same relative position in every panel (x=24 y=0.50*ymax for demand, x=56 y=0.45*ymin for surplus). This also clears the GB2040 top-panel curve overlap and lifts the bottom surplus label up near the charging region. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elivered links_t.p0 records the bus0 side of each link. For H2_turbine bus0 is hydrogen, so p0 is H2 consumed; the electricity delivered to the grid is p0 x turbine efficiency (0.50). The figures had plotted p0 directly, overstating hydrogen discharge 2x and implying a false ~68% round-trip when the model correctly uses 70% electrolysis x 50% turbine = 35%. Corrected the Hydrogen band and the demand reconstruction in both plot_rdc_combined.R and plot_hourly_dispatch. Knock-on: 2040 residual demand 80.7 -> 74.5 TWh, peak 57 -> 54 GW; hydrogen now 19/6.5 charge/discharge (34% round-trip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion Show storage charge/discharge to one decimal so sub-1-TWh values are not misleadingly rounded to integers (0.9 was shown as "1", 0.3 as "0"/"1"). Verified from the solved wholesale.nc that turbine efficiency is uniform 0.50 across all 111 H2_turbine links and electrolysis 0.70. The wholesale.nc link time-series are empty (only links_t.p0 was exported to CSV), so the turbine's electrical output p1 is not directly stored; it is derived exactly as 0.50 x p0 (the LP identity), not assumed. Electricity used to make H2 = 19.0 TWh (electrolysis p0, measured); electricity generated from H2 = 6.5 TWh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…torage by technology) as standalone figures for the policy brief. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export two publication variants of the 2x2 (no caption row): rdc_combined_labelled (with in-plot annotations) and rdc_combined_notext (all in-plot text stripped via a new annot=FALSE toggle on the panel functions), both with (a)/(b) top and (c)/(d) bottom panel tags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(a)-(c) tags) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ee versions rdc_combined_notext and dispatch_A4_2030_2040_notext now strip all text (tags, titles, axis labels, legend), leaving only marks/axes. The (a)-(d) tags remain only on rdc_combined_labelled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A lightweight full-year, single-node (copperplate) LP for PyPSA-GB that solves
the whole year at once with foresight, so the hydrogen store can shift energy
seasonally (fill in windy/summer months, discharge in winter droughts) - which
the 24 h rolling market model cannot represent. On top of the solve it builds a
load-duration / residual-load-duration curve (RLDC) view that quantifies the
long-duration-storage need and how the VRE surplus is utilised by storage, across
FES 2025 Holistic Transition scenarios.
scripts/analysis/seasonal_storage_lp.py:on available VRE profiles, the storage fleet, the H2 store) and solves the full
year as one small LP (~10^5 vars).
(
_ldc) and surplus/deficit decomposition (_surplus) tables.report (per the repo's analysis convention), superseding an earlier one-off
Plotly artifact.
Fast default rebuilds the report from existing solved outputs;
--recomputere-solves the LPs first.
Why single-node?
Deliberate, not a shortcut: (1) seasonal storage valuation needs a full-year
single LP with foresight; (2) a full-network full-year single solve exhausts memory
on a typical laptop, while single-node solves in seconds; (3) the national
energy-balance / wholesale price is ~unchanged by transmission. Caveat (documented):
copperplate ignores grid bottlenecks, so it understates curtailment and is an
optimistic view of storage value; a ~40-zone GSP-clustered version is the natural
transmission-aware extension.
Selected results
The H2 store size is fixed by FES and binds in every case (SoC swing = full store,
24 GWh in 2030, 982 GWh in 2040). In 2030 it is too small to matter; by 2040 the
large store does more work in a windier year and curtails less despite more VRE.
Analysis outputs under
resources/analysis/are git-ignored.🤖 Generated with Claude Code