Stress metrics extension#124
Conversation
This reverts commit e248912.
| GSw_PRM_StressThreshold,/-delimited list of annual NEUE level [ppm] above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_NEUEppm_StressMetric_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; NEUEppm is normalized expected unserved energy in parts per million; StressMetric is EUE or NEUE (only used in period selection); PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_1_EUE_sum, | ||
| GSw_PRM_StressThresholdMetrics,"/-delimited list of metrics for identifying stress periods (supported options are: NEUE, LOLH)",N/A,NEUE, | ||
| GSw_PRM_StressThresholdLOLH,LOLH threshold [hours/year] above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_LOLH_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; LOLH is loss of load event-hours per year [event-h/year]; PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_2.4_sum, | ||
| GSw_PRM_StressThresholdLOLE,LOLE threshold [event-days/year] above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_LOLEdays_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; LOLEdays is loss of load event-days per year (a day is counted if at least one hour has a shortfall) [event-day/year]; PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_0.1_max, |
There was a problem hiding this comment.
I've seen different definitions used in different sources, but at least in some places, "LOLE" is taken to mean "events" and LOLD as "event-days". So if the units used here are "event-days", then it might be clearer to label it as LOLD.
Here's the figure I'm thinking of, from https://doi.org/10.1109/PMAPS53380.2022.9810615:
| GSw_PRM_StressThresholdLOLH,LOLH threshold [hours/year] above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_LOLH_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; LOLH is loss of load event-hours per year [event-h/year]; PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_2.4_sum, | ||
| GSw_PRM_StressThresholdLOLE,LOLE threshold [event-days/year] above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_LOLEdays_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; LOLEdays is loss of load event-days per year (a day is counted if at least one hour has a shortfall) [event-day/year]; PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_0.1_max, | ||
| GSw_PRM_StressThresholdNEUE,Annual NEUE level [ppm] threshold above which to re-solve the latest model year with new stress periods; formulated as HierarchyLevel_NEUEppm_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; NEUEppm is normalized expected unserved energy in parts per million [ppm]; PeriodAggMethod is 'sum' or 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_1_sum, | ||
| GSw_PRM_StressThresholdOutageDuration,Outage duration; formulated as HierarchyLevel_OutageDurationHours_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; OutageDurationHours is the max outage duration in hours; PeriodAggMethod is 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_10000_max, |
There was a problem hiding this comment.
- 10000 hours is an extremely long event. I think it'd be most convenient to set the defaults to values in the range of what's come up in the literature review. If I'm interpreting correctly, the values include 8 hours for NWPCC, 12 hours for ERCOT, and 18 hours for ISONE. So how about 12 hours?
- Just to keep the switch name shorter, you could change it to
GSw_PRM_StressThresholdDuration.
| GSw_PRM_StressThresholdOutageDuration,Outage duration; formulated as HierarchyLevel_OutageDurationHours_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; OutageDurationHours is the max outage duration in hours; PeriodAggMethod is 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_10000_max, | |
| GSw_PRM_StressThresholdDuration,Outage duration; formulated as HierarchyLevel_OutageDurationHours_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; OutageDurationHours is the max outage duration in hours; PeriodAggMethod is 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_12_max, |
| GSw_PRM_StressThresholdOutageMagnitude,Outage magnitude; formulated as HierarchyLevel_OutageMagnitudeMW_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; OutageMagnitudeMW is the max outage magnitude in MW; PeriodAggMethod 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_0.1_max, | ||
| GSw_PRM_StressThresholdNormalizedOutageMagnitude,Normalized outage magnitude; formulated as HierarchyLevel_NormalizedOutageMagnitude_PeriodAggMethod where HierarchyLevel is a column in hierarchy.csv; NormalizedOutageMagnitude is the max outage normalized magnitude (ratio of the maxmimum load) ; PeriodAggMethod is 'max' over the hours in each period (only used in period selection) (see README.md for detailed notes),N/A,transgrp_10_max, |
There was a problem hiding this comment.
- I can't remember how much we discussed this, but in the same vein as the earlier discussion on the EUE metric, it'd be hard to apply a single magnitude threshold in units of MW across different transgrps (with very different peak loads) and over time. In the same way that we supply and enforce the PRM as a percent (even if some reasons think of it as MW), it seems clearest to stick with the normalized approach here. So I think we could drop
GSw_PRM_StressThresholdOutageMagnitudeand only keeping the normalized version. - Just to keep the switch name shorter, you could drop 'Normalized' from the switch name. So just keep a single switch,
GSw_PRM_StressThresholdMagnitude, with the functionality of the currentGSw_PRM_StressThresholdNormalizedOutageMagnitudeswitch. - It's unclear what units are being used for
GSw_PRM_StressThresholdNormalizedOutageMagnitudeif the value is 10 (the description says it's a ratio). From your review, it looks like thresholds range from 3% in ISONE and NWPCC to 20% in Tri-state and 25% in ERCOT. It will be good to do a sweep of the values, but for now, I would use something between 3% and 25%. - But I would use fractional units in the switch (in keeping with the related switches), so
transgrp_0.03_maxif you go with the ISONE/NWPCC number.
| dfmetric /= len(sw['resource_adequacy_years']) | ||
|
|
||
| dfmetric.round(2).to_csv( | ||
| os.path.join(sw.casedir, 'outputs', f"{stress_metric.lower()}_{t}i{iteration}.csv") |
There was a problem hiding this comment.
It's kind of a lot of csv's if we're saving each metric, year, and iteration to its own file; on the order of 100 files for a full-US run.
Could we instead write a single file for each year/iteration with all the metrics?
- You could change the existing
metriccolumn toaggmethodsince it only recordssumormax - Then you could either add a
metriccolumn for NEUE/LOLH/etc., or put the metrics in wide format (so the columns would belevel,aggmethod,region,NEUE,LOLH, etc.). Long is clearer organizationally but wide is ok if the files get too large.
| def get_max_magnitude_outage(dfmetric): | ||
| """Return the peak single-hour EUE per region (MW). | ||
|
|
||
| Finds the highest magnitude of shortfall for each region. Hours at or | ||
| below ``eue_threshold`` are masked before taking the maximum, so they cannot | ||
| inflate the result. Regions with no outage hours return 0. | ||
|
|
||
| Args: | ||
| dfmetric (pd.DataFrame): Hourly EUE values (MW) indexed by timestamp, one | ||
| column per region. Typically obtained from :func:`get_pras_stress_metric`. | ||
| eue_threshold (float, optional): Minimum EUE (MW) to qualify as an outage | ||
| hour. Hours at or below this value are excluded. Defaults to 0. | ||
|
|
||
| Returns: | ||
| pd.Series: Peak hourly EUE (MW) over the full timeseries, indexed by region. | ||
| Regions with no outage hours return 0. | ||
| """ | ||
| return dfmetric.max() |
There was a problem hiding this comment.
I'm in favor of docs but this seems like a lot for a single .max() call. I think it'd be clearer and more readable to just use .max() when you need it. (Maybe same for the above.)
|
|
||
| # Metric thresholds are defined on a per-year basis, but PRAS reports the total over all resource adequacy years, | ||
| # For all metrics except NEUE, divide by the number of resource adequacy years to get the average per year | ||
| if stress_metric != 'NEUE': |
There was a problem hiding this comment.
Outage magnitude is an hourly metric, and duration is a per-event metric, so neither of them should be divided by the number of RA years.
I think only EUE (which was dropped) and the LOLx metrics would be divided by the number of weather years.
| event_days = (daily_max > 0).sum() | ||
| _metric[hierarchy_level, 'sum'] = event_days | ||
| _metric[hierarchy_level, 'max'] = event_days | ||
| continue |
There was a problem hiding this comment.
The continues here are kind of hard to follow. It looks like they're being used to control which metrics get summed, but those metrics are implicit rather than explicit. I think it'd be clearer to drop the continues and name the metrics that need to be summed.
That being said, since you will always run this function for each of the metrics (since we always save all of the values), I think it might be clearer to reorganize the function:
- Drop the
stress_metrickwarg. Instead of theif stress_metric ==blocks, just explicitly calculate each metric.- So above, you'd run
get_pras_stress_metric()twice, once for EUE and once for LOLE (since those are the only two things you can get from it), and use the appropriate one for the calculations below. - Add the metric name as a third key in the
_metricdataframe - Calculate each metric explicitly
- So above, you'd run
- Then at the end you'd have a single dataframe you could write, which addresses the suggestion below to put them all in the same file.
There was a problem hiding this comment.
Here's my take on the structure: 475a94a. It's not complete because it doesn't yet adapt the rest of the processing for the single ra_metrics_{year}i{iteration}.csv file, but it makes the individual aggregate RA metric calculations more explicit (at least in my mind).
I might try a few more tweaks on the pb/multimetric branch to get some more test cases running, so let's check in about integration when you get back.
Summary
OutageDuration,OutageMagnitude,NormalizedOutageMagnitude, andLOLE.GSw_PRM_StressThresholdMetricsalongside the existing metrics.of the LOLE Resource Adequacy Metric
Technical details
Implementation notes
get_annual_stress_metric()after reading PRAS outputs.OutageDuration,OutageMagnitude, andNormalizedOutageMagnitudederive from the hourly EUE timeseries via three new helper functions:get_max_duration_outage(dfmetric), length of the longest consecutive run of hours with EUE >eue_threshold (MW)get_max_magnitude_outage(dfmetric), largest shortfall magnitude (MW)LOLEuses the PRAS_LOLEhourly columns (same source asLOLH). A day is counted as an event-day if at least one hour has LOLE > 0New stress metric switches
OutageDurationGSw_PRM_StressThresholdOutageDurationtransgrp_10000_max: threshold at 10000 h (effectively off), usingmaxagg overtransgrpOutageMagnitudeGSw_PRM_StressThresholdOutageMagnitudetransgrp_0.1_max: threshold at 0.1 MW, usingmaxagg overtransgrpNormalizedOutageMagnitudeGSw_PRM_StressThresholdNormalizedOutageMagnitudetransgrp_10_max: threshold at 10 p.u. (effectively off), usingmaxagg overtransgrpLOLEGSw_PRM_StressThresholdLOLEtransgrp_0.1_max: threshold at 0.1 event-days/year, usingmaxagg overtransgrpValidation, testing, and comparison report(s)
Checklist for author
Details to double-check
General information to guide review
Did you use LLM tools (chatbot or copilot) in the preparation of this PR? If so, describe how