Add CVAR and NCVAR resource adequacy reporting - #160
Conversation
patrickbrown4
left a comment
There was a problem hiding this comment.
Thanks Burcin. Would you mind moving this fork to a branch on the ReEDS repo so the github runner can run the tests? (There might be a way to do it for forks but I'm not sure how.)
| def get_cvar_alpha(sw): | ||
| alpha = float(sw.GSw_PRM_CVARAlpha) | ||
| if not (0 <= alpha < 1): | ||
| raise ValueError(f"GSw_PRM_CVARAlpha must be in [0, 1). Got {alpha}") | ||
| return alpha | ||
|
|
||
|
|
There was a problem hiding this comment.
The rest of the switch testing is in runreeds.check_compatibility(), so can you move this check there, and then just use float(sw.GSw_PRM_CVARalpha) here?
| x = pd.Series(samples).dropna().astype(float) | ||
| if x.empty: | ||
| return np.nan | ||
| n_tail = max(1, int(np.ceil(round((1 - alpha) * len(x), 12))),) |
There was a problem hiding this comment.
What is the 12 here?
| dflole_agg = dflole.rename(columns=rmap).groupby(axis=1, level=0).max() | ||
| dfeue_agg = dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum() | ||
| dfload_agg = dfload.rename(columns=rmap).groupby(axis=1, level=0).sum() |
There was a problem hiding this comment.
Revert so it works with the current environment
| dflole_agg = dflole.rename(columns=rmap).groupby(axis=1, level=0).max() | |
| dfeue_agg = dfeue.rename(columns=rmap).groupby(axis=1, level=0).sum() | |
| dfload_agg = dfload.rename(columns=rmap).groupby(axis=1, level=0).sum() | |
| dflole_agg = dflole.rename(columns=rmap).T.groupby(level=0).max().T | |
| dfeue_agg = dfeue.rename(columns=rmap).T.groupby(level=0).sum().T | |
| dfload_agg = dfload.rename(columns=rmap).T.groupby(level=0).sum().T |
| .groupby(axis=1, level=0) | ||
| .sum() |
There was a problem hiding this comment.
For compatibility with pandas 3; see #103
| .groupby(axis=1, level=0) | |
| .sum() | |
| .T.groupby(level=0).T.sum() |
| ra_metrics[level, 'euemax_peakloadfrac'] = calc_peak_eue(dfeue_agg, dfload_agg, 'peak') | ||
| ra_metrics[level, 'euemax_hourlyloadfrac'] = calc_peak_eue(dfeue_agg, dfload_agg, 'hourly') | ||
| ra_metrics[level, 'euemax_mw'] = calc_peak_eue(dfeue_agg, dfload_agg, 'absolute') | ||
| if len(cvar_metrics): |
There was a problem hiding this comment.
Does calculating the new metrics add much runtime? If not, I would instead just skip the if block and always calculate them.
For a bit cleaner organization, I would move shortfall_samples_agg up and then just put the ra_metrics[level, 'cvar_mwh_peryear'] = cvar / numyears and ra_metrics[level, 'ncvar_ppm'] = calc_ncvar(cvar, dfload_agg) one-liners after the rest of the ra_metrics one-liners above.
| GSw_PRM_StressSeedMinRElevel,Region hierarchy level at which to include minimum wind and solar capacity factor days as seeded stress periods (or False to ignore min-wind/solar CF days),false; False; FALSE; r; nercr; transreg; transgrp; cendiv; st; interconnect; country; usda_region; ccreg,interconnect, | ||
| GSw_PRM_StressStorageCutoff,How to select shoulder stress periods giving storage time to recharge before/after high-unserved-energy periods. Two-part switch separated by _. The first argument is 'EUE' or 'capacity' or 'absolute'. If 'EUE' the second argument specifies a PRAS storage headspace / EUE threshold [fraction]; if 'cap' it specifies a headspace / storage capacity threshold [fraction]; if 'abs' it specifies absolute number of periods before/after [integer]. Turned off if set to 'off'.,N/A,EUE_0.1, | ||
| GSw_PRM_StressThresholdMetrics,/-delimited list of metrics for identifying stress periods; supported metrics (case-insensitive) are: LOLD | LOLE | LOLH | NEUE | Depth | Duration,N/A,NEUE, | ||
| GSw_PRM_StressThresholdMetrics,/-delimited list of resource adequacy metrics. LOLD | LOLE | LOLH | NEUE | Depth | Duration identify stress periods; CVAR and NCVAR are report-only metrics and do not add stress periods or update PRM.,N/A,NEUE, |
There was a problem hiding this comment.
All the metrics are always calculated even if they are not compared against the threshold or used to pick stress periods (sorry, I know this changed since the version of the branch you started from), so since CVAR and NCVAR are not yet used to select stress periods, I think this switch can remain unchanged
| GSw_PRM_StressThresholdMetrics,/-delimited list of resource adequacy metrics. LOLD | LOLE | LOLH | NEUE | Depth | Duration identify stress periods; CVAR and NCVAR are report-only metrics and do not add stress periods or update PRM.,N/A,NEUE, | |
| GSw_PRM_StressThresholdMetrics,/-delimited list of metrics for identifying stress periods; supported metrics (case-insensitive) are: LOLD | LOLE | LOLH | NEUE | Depth | Duration,N/A,NEUE, |
| GSw_PRM_StressThresholdLOLE,LOLE threshold [events/year]; formulated as HierarchyLevel_LOLE where HierarchyLevel is a column in hierarchy.csv; events is loss-of-load events per year,N/A,transgrp_0.1, | ||
| GSw_PRM_StressThresholdLOLH,LOLH threshold [event-hours/year]; formulated as HierarchyLevel_LOLH where HierarchyLevel is a column in hierarchy.csv; LOLH is loss-of-load event-hours per year,N/A,transgrp_2.4, | ||
| GSw_PRM_StressThresholdNEUE,NEUE threshold [ppm]; formulated as HierarchyLevel_NEUE where HierarchyLevel is a column in hierarchy.csv; NEUEppm is normalized expected unserved energy in parts per million,N/A,transgrp_1, | ||
| GSw_PRM_CVARAlpha,Alpha value used for CVAR/NCVAR calculations,float,0.95, |
There was a problem hiding this comment.
(minor nitpick) personally I think it's a bit more readable to switch to lower case after an acronym, so GSw_PRM_CVARalpha instead of GSw_PRM_CVARAlpha, but not a big deal and probably debatable
| endyear,2032,,2050,2050,2050,2029,2060,2026,,,,,,,,,,,2035,2030,2030,2060,2035,2050,2050,2050,,,,, | ||
| yearset,,,,,,,2010..2060..10,,,,,,,,,,,,,2010..2050..5,2010..2050..5,2010..2060..10,,,2010_2025_2050,2010..2050..5,,,,, | ||
| GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,,,,,, | ||
| GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,z132,,,,, |
There was a problem hiding this comment.
Can this be reverted?
| GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,z132,,,,, | |
| GSw_ZoneSet,,,,,,,z54,z3109,,,,,,,z3109,z3109,z3109,PJMcounty,,,,z54,z134,z54,z48,,,,,, |
There was a problem hiding this comment.
I think these can all be reverted
| GSw_H2_PTC,,,,,,,,,,,,,,,,,,,,,,,,0,0,,,,,, | ||
| GSw_H2Combustion,,,,,,,,,,,,,,,,,,,,,,,,,0,,,,,, | ||
| GSw_PRM_StressThresholdMetrics,,,,,,,,,,,,,,,,,,,,,,,,,,NEUE/LOLH/LOLE/LOLD/duration/depth,,,,, | ||
| GSw_PRM_StressThresholdMetrics,,,,,,,,,,,,,,,,,,,,,,,,,,NEUE/LOLH/LOLE/LOLD/duration/depth/CVAR/NCVAR,,,,, |
There was a problem hiding this comment.
As noted above, this switch should only indicate metrics that are compared against thresholds and used to select stress periods. As long as the CVAR/NCVAR metrics don't add much runtime/filesize, I think we should always calculate them. We can do a followup PR to add the thresholds and stress-period selection logic.
| GSw_PRM_StressThresholdMetrics,,,,,,,,,,,,,,,,,,,,,,,,,,NEUE/LOLH/LOLE/LOLD/duration/depth/CVAR/NCVAR,,,,, | |
| GSw_PRM_StressThresholdMetrics,,,,,,,,,,,,,,,,,,,,,,,,,,NEUE/LOLH/LOLE/LOLD/duration/depth,,,,, |
Summary
This PR adds Conditional Value at Risk (CVAR) and Normalized Conditional Value at Risk (NCVAR) reporting to the ReEDS–PRAS resource adequacy workflow.
CVAR and NCVAR quantify the severity of the highest-shortfall Monte Carlo samples and complement the existing resource adequacy metrics, including NEUE, LOLH, LOLE, LOLD, Duration, and Depth.
The implementation:
ra_metrics_{t}i{iteration}.csvoutput,CVAR and NCVAR do not select stress periods and do not modify the existing PRM update workflow.
Technical details
run_pras.jlAdded the following command-line arguments:
--write_shortfall_samples_totals--cvar_alphaWhen sample-total output is requested, PRAS retains
PRAS.ShortfallSamples()and writes:The H5 file contains:
Each value represents cumulative shortfall over all hours and resource adequacy weather years in one Monte Carlo sample.
For consistency with the existing LOLE, EUE, and NEUE diagnostics,
run_pras.jlalso logs whole-system:The CVAR value printed by Julia is the cumulative MWh value over the complete PRAS simulation horizon.
No separate CVAR/NCVAR summary CSV is written by
run_pras.jl.ra_calcs.pyAdded support for requesting the new sample-total PRAS output.
When
CVARorNCVARis included inGSw_PRM_StressThresholdMetrics,ra_calcs.py:write_shortfall_samples_totals,GSw_PRM_CVARAlphatorun_pras.jl.The existing hourly sample output remains unchanged:
write_shortfall_samplescontinues to support the existing PRAS-informed PRM update workflow,write_shortfall_samples_totalssupplies the data needed for CVAR/NCVAR reporting.stress_periods.pyAdded the following CVAR/NCVAR reporting functions:
get_cvar_alpha()get_shortfall_totals_by_sample()_sample_cvar()calc_cvar()calc_ncvar()CVAR and NCVAR are calculated at the same hierarchy levels used for the other resource adequacy metrics:
countryinterconnectnercrtransregtransgrpstrFor each hierarchy level, shortfall is first aggregated by Monte Carlo sample. CVAR is then calculated from the highest-shortfall tail of the resulting sample distribution.
The following metrics are added to the standard resource adequacy output:
They are written to:
using the existing long-format structure:
CVAR and NCVAR are excluded from the stress-period selection loop. The existing
prm_increment_pras()andupdate_prm()implementations are unchanged.runreeds.pyUpdated the resource adequacy metric compatibility check so that
CVARandNCVARare accepted in:They are classified as report-only metrics and are skipped by the threshold parser.
Compatibility checks for the standard stress-period metrics remain unchanged. The existing requirement that PRM updates use NEUE-based failed regions is also preserved.
Implementation notes
CVAR calculation
CVAR is calculated from sample-level total shortfall, not from hourly EUE.
For each Monte Carlo sample, PRAS produces an hourly shortfall trajectory. The hourly values are summed over the complete PRAS horizon:
The sample totals are then sorted from highest to lowest. CVAR is the average shortfall of the worst
(1 - alpha)fraction of samples.For example:
The tail-size calculation rounds the floating-point result before applying
ceil. This prevents a value such as50.00000000000004from incorrectly selecting 51 samples.Annualization
Each PRAS sample spans all configured resource adequacy weather years.
CVAR is first calculated from cumulative shortfall over the complete PRAS horizon. It is then divided by the number of RA weather years and reported as:
Therefore, the reported value is the annualized CVAR of cumulative shortfall over the full RA horizon. It is not the average of independently calculated weather-year CVAR values.
NCVAR calculation
NCVAR is calculated as:
and is reported in ppm.
No additional annualization is required because both the CVAR numerator and load denominator cover the same simulation horizon.
Regional aggregation
CVAR is calculated after shortfall is aggregated by sample to the requested hierarchy level.
Regional CVAR values should not generally be added together to reproduce system-level CVAR because different regions may have different samples in their highest-shortfall tails.
Additional changes
MultiMetricRAtest case to request CVAR and NCVAR reporting.MultiMetricRAtest configuration to usez132.ra_metricsoutput rather than separate reporting files.Switches added/changed
Added:
GSw_PRM_CVARAlphaAlpha value used for CVAR and NCVAR calculations.
The valid range is:
Changed:
GSw_PRM_StressThresholdMetricsThe metric list can now include:
Standard stress-period metrics:
LOLDLOLELOLHNEUEDepthDurationReport-only metrics:
CVARNCVARExample:
Despite the existing switch name, CVAR and NCVAR are not interpreted as threshold-based stress-period metrics.
No CVAR- or NCVAR-specific threshold switches are added in this PR.
Known incompatibilities and limitations
PRAS.ShortfallSamples(), which increases PRAS memory use when these metrics are enabled.cvar_mwh_peryearinra_metricsis annualized.Validation, testing, and comparison report(s)
The implementation was tested using an end-to-end NY MultiMetricRA case with:
z132,GSw_PRM_CVARAlpha = 0.95,CVAR/NCVAR.The test completed all modeled solve years and resource adequacy iterations.
Validation confirmed that:
PRAS_{t}i{iteration}-shortfall_totals_by_sample.h5contains one cumulative shortfall value per sample and PRAS region.ra_metrics_{t}i{iteration}.csv.alpha = 0.95, the corrected tail contains exactly 50 samples.Python syntax checks passed for:
runreeds.pyreeds/resource_adequacy/ra_calcs.pyreeds/resource_adequacy/stress_periods.pyThe Julia environment and
run_pras.jlimports were also tested successfully.Additional diagnostic analysis included:
A formal
compare_cases.pyreport is not included because this PR does not modify ReEDS equations, model input data, or default-case results.Checklist for author
Details to double-check
cases.csvGeneral information to guide review
runreeds.pychange is limited to metric compatibility validationenvironment.ymlorProject.toml)Did you use LLM tools (chatbot or copilot) in the preparation of this PR? If so, describe how
ChatGPT was used to help draft the PR description and troubleshoot implementation, testing, and Git workflow issues. The code changes, final diffs, calculations, and test results were reviewed by the author.
Suggested reviewers