Repository navigation
Add CVAR and NCVAR resource adequacy reporting #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
45b2ffd
3b05710
8e761fc
7e560cd
3a7626c
48f41eb
c5995a9
17089a7
3735a61
91730d2
081086f
fad7283
6a95ef2
aed6bb4
393be97
72d5e9a
9ec3c00
24ddcd7
9a14001
aa67d4d
c516865
e86cc9e
905c90d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ def run_pras( | |
| write_surplus=False, | ||
| write_energy=False, | ||
| write_shortfall_samples=False, | ||
| write_shortfall_samples_totals=True, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like that PRAS PR has been merged, but I'd still be interested in a check on the memory usage before merging.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CVAR no longer uses |
||
| write_availability_samples=False, | ||
| **kwargs, | ||
| ): | ||
|
|
@@ -68,6 +69,7 @@ def run_pras( | |
| f"--write_surplus={int(write_surplus)}", | ||
| f"--write_energy={int(write_energy)}", | ||
| f"--write_shortfall_samples={int(write_shortfall_samples)}", | ||
| f"--write_shortfall_samples_totals={int(write_shortfall_samples_totals)}", | ||
| f"--write_availability_samples={int(write_availability_samples)}", | ||
| f"--iteration={iteration}", | ||
| f"--samples={sw['pras_samples']}", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -70,7 +70,12 @@ function parse_commandline() | |||||
| default = 0 | ||||||
| required = false | ||||||
| "--write_shortfall_samples" | ||||||
| help = "Write the sample-level shortfall" | ||||||
| help = "Write per-sample hourly shortfall by region" | ||||||
| arg_type = Int | ||||||
| default = 0 | ||||||
| required = false | ||||||
| "--write_shortfall_samples_totals" | ||||||
| help = "Write per-sample total shortfall by region over the full PRAS time period" | ||||||
| arg_type = Int | ||||||
| default = 0 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the default in
Suggested change
|
||||||
| required = false | ||||||
|
|
@@ -182,6 +187,9 @@ function run_pras(pras_system_path::String, args::Dict) | |||||
| if args["write_energy"] == 1 | ||||||
| resultspec["energy"] = PRAS.StorageEnergy() | ||||||
| end | ||||||
| ## Hourly per-sample shortfall (region x hour x sample) is memory-heavy, so only | ||||||
| ## request it when the hourly samples are written. Per-sample totals (used for CVaR) | ||||||
| ## come from the regular Shortfall result (PRAS >= 0.9). | ||||||
| if args["write_shortfall_samples"] == 1 | ||||||
| resultspec["short_samples"] = PRAS.ShortfallSamples() | ||||||
| end | ||||||
|
|
@@ -327,6 +335,24 @@ function run_pras(pras_system_path::String, args::Dict) | |||||
| @info("Wrote PRAS shortfall by sample to $(shortfile)") | ||||||
| end | ||||||
|
|
||||||
| ### Per-sample total shortfall by region over the full PRAS time period, needed for CVaR | ||||||
| if args["write_shortfall_samples_totals"] == 1 | ||||||
| ## Read the per-sample totals (MWh) from the regular Shortfall result (PRAS >= 0.9). | ||||||
| sf = results["short"] | ||||||
| ## Use filtered system regions to avoid DC converter pseudo-regions without load. | ||||||
| region_names = sf.regions.names | ||||||
| totalsfile = replace(outfile, ".h5"=>"-shortfall_totals_by_sample.h5") | ||||||
| HDF5.h5open(totalsfile, "w") do f | ||||||
| f["sample", compress=4] = collect(1:args["samples"]) | ||||||
| f["USA", compress=4] = Float64.(sf.shortfall_samples) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You drop the USA column below, and it would make more sense to just drop it from the EUE and LOLP profiles too, so I'd suggest just not writing it here and then dropping the
Suggested change
|
||||||
| for r in regions | ||||||
| i_r = findfirst(==(r), region_names) | ||||||
| f["$r", compress=4] = Float64.(sf.shortfall_region_samples[i_r, :]) | ||||||
| end | ||||||
| end | ||||||
| @info("Wrote PRAS shortfall totals by sample to $(totalsfile)") | ||||||
| end | ||||||
|
|
||||||
| ### Sample-level generator and storage availability | ||||||
| if args["write_availability_samples"] == 1 | ||||||
| dictavail = Dict(s => DF.DataFrame() for s = 1:args["samples"]) | ||||||
|
|
@@ -458,6 +484,7 @@ if abspath(PROGRAM_FILE) == @__FILE__ | |||||
| # "write_surplus" => 0, | ||||||
| # "write_energy" => 0, | ||||||
| # "write_shortfall_samples" => 1, | ||||||
| # "write_shortfall_samples_totals" => 1, | ||||||
| # "write_availability_samples" => 0, | ||||||
| # "overwrite" => 1, | ||||||
| # "debug" => 0, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -143,6 +143,51 @@ def calc_neue(dfeue_agg, dfload_agg): | |||||||||||
| return neue | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def get_shortfall_totals_by_sample(case, t, iteration=0): | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry but just for consistent organization, can you move this up after the other |
||||||||||||
| filepath = os.path.join(case, 'handoff', 'PRAS', f'PRAS_{t}i{iteration}-shortfall_totals_by_sample.h5') | ||||||||||||
| if not os.path.isfile(filepath): | ||||||||||||
| raise FileNotFoundError(f"{filepath} not found. Re-run PRAS with --write_shortfall_samples_totals 1.") | ||||||||||||
| df = reeds.io.read_pras_results(filepath) | ||||||||||||
| df.columns = df.columns.astype(str) | ||||||||||||
| if 'sample' in df.columns: | ||||||||||||
| df = df.set_index('sample') | ||||||||||||
| elif df.index.name != 'sample': | ||||||||||||
| df.index = pd.RangeIndex(1, len(df) + 1, name='sample') | ||||||||||||
| return df.apply(pd.to_numeric, errors='coerce').clip(lower=0) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is
Suggested change
|
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _sample_cvar(samples, alpha=0.95): | ||||||||||||
| x = pd.Series(samples).dropna().astype(float) | ||||||||||||
| if x.empty: | ||||||||||||
| return np.nan | ||||||||||||
|
|
||||||||||||
| # Round before applying ceil to remove floating-point noise. | ||||||||||||
| # For example, a mathematically exact tail size of 50 may be | ||||||||||||
| # represented as 50.00000000000004, which would otherwise select 51 samples. | ||||||||||||
| tail_size = round((1 - alpha) * len(x), 12) | ||||||||||||
| n_tail = max(1, int(np.ceil(tail_size))) | ||||||||||||
|
|
||||||||||||
| return x.nlargest(n_tail).mean() | ||||||||||||
|
|
||||||||||||
| def calc_cvar(shortfall_samples_agg, alpha=0.95): | ||||||||||||
| """ | ||||||||||||
| CVAR from total shortfall by PRAS sample. | ||||||||||||
| """ | ||||||||||||
| cvar = shortfall_samples_agg.apply( | ||||||||||||
| lambda s: _sample_cvar(s, alpha=alpha), | ||||||||||||
| axis=0, | ||||||||||||
| ) | ||||||||||||
| return cvar | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def calc_ncvar(cvar, dfload_agg): | ||||||||||||
| """ | ||||||||||||
| NCVAR = CVAR / total load, in ppm. | ||||||||||||
| """ | ||||||||||||
| ncvar = cvar / dfload_agg.sum().reindex(cvar.index) * 1e6 | ||||||||||||
| return ncvar | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def calc_peak_eue(dfeue_agg, dfload_agg, norm:Literal['peak','hourly','absolute']='peak'): | ||||||||||||
| """ | ||||||||||||
| Get the peak hourly outage magnitude | ||||||||||||
|
|
@@ -192,6 +237,12 @@ def calc_ra_metrics( | |||||||||||
| sw = reeds.io.get_switches(case) | ||||||||||||
| numyears = len(sw.resource_adequacy_years_list) | ||||||||||||
|
|
||||||||||||
| ### Get total shortfall samples for CVAR calculation | ||||||||||||
| shortfall_samples = ( | ||||||||||||
| get_shortfall_totals_by_sample(case=case, t=t, iteration=iteration) | ||||||||||||
| .drop(columns=['USA'], errors='ignore') | ||||||||||||
| ) | ||||||||||||
|
Comment on lines
+241
to
+244
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above, if you don't write it you can drop it from here too
Suggested change
|
||||||||||||
|
|
||||||||||||
| ### Loop over aggregation levels and calculate all metrics | ||||||||||||
| ra_metrics = {} | ||||||||||||
| for level in levels: | ||||||||||||
|
|
@@ -212,6 +263,18 @@ def calc_ra_metrics( | |||||||||||
| 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') | ||||||||||||
| ## Calculate tail-based metrics (CVAR and NCVAR) | ||||||||||||
| regions = [c for c in shortfall_samples.columns if c in rmap.index] | ||||||||||||
| if len(regions): | ||||||||||||
| shortfall_samples_agg = ( | ||||||||||||
| shortfall_samples[regions] | ||||||||||||
| .rename(columns=rmap) | ||||||||||||
| .T.groupby(level=0) | ||||||||||||
| .sum().T | ||||||||||||
| ) | ||||||||||||
| cvar = calc_cvar(shortfall_samples_agg, alpha=float(sw.GSw_PRM_CVARalpha)) | ||||||||||||
| ra_metrics[level, 'cvar_mwh_peryear'] = cvar / numyears | ||||||||||||
| ra_metrics[level, 'ncvar_ppm'] = calc_ncvar(cvar, dfload_agg) | ||||||||||||
|
|
||||||||||||
| ### Combine it | ||||||||||||
| dfout = pd.concat(ra_metrics, names=['level','metric','region']).rename('value') | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.