From f4b5f13603431d3bd416c1245dab0b4c1b4ec8a0 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 1 May 2026 11:29:00 -0600 Subject: [PATCH 01/12] add e_report_calcs.py and move a couple parameter calculations to it from e_report.gms --- e_report.gms | 12 +++------ e_report_calcs.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++ e_report_data.csv | 7 +++++ e_report_dump.py | 9 ++++--- runfiles.csv | 2 ++ 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 e_report_calcs.py create mode 100644 e_report_data.csv diff --git a/e_report.gms b/e_report.gms index ebd14d10e..aa30af515 100644 --- a/e_report.gms +++ b/e_report.gms @@ -122,18 +122,10 @@ $include e_report_params.gms h(h)$[not h_rep(h)] = no ; szn(szn)$[not szn_rep(szn)] = no ; -*================================================= -* -- CAPACITY ABOVE INTERCONNECTION QUEUE LIMIT -- -*================================================= - -cap_above_limit(tg,r,t)$tmodel_new(t) = CAP_ABOVE_LIM.l(tg,r,t) ; - *===================== * -- CO2 Reporting -- *===================== -CO2_CAPTURED_out(r,h,t)$tmodel_new(t) = CO2_CAPTURED.l(r,h,t) ; -CO2_CAPTURED_out_ann(r,t)$tmodel_new(t) = sum(h,hours(h) * CO2_CAPTURED.l(r,h,t) ); CO2_STORED_out(r,cs,h,t)$[tmodel_new(t)$csfeas(cs)] = CO2_STORED.l(r,cs,h,t) ; CO2_STORED_out_ann(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = sum(h,hours(h) * CO2_STORED.l(r,cs,h,t) ); CO2_TRANSPORT_INV_out(r,rr,t)$tmodel_new(t) = CO2_TRANSPORT_INV.l(r,rr,t) ; @@ -2084,6 +2076,10 @@ $endif.powerfrac * Dump results *======================================== +execute_unload "outputs%ds%results.gdx" +$include e_report_data.csv +; + * The parameter list in the following file is read from e_report_params.csv * and parsed in copy_files.py execute_unload "outputs%ds%rep_%fname%.gdx" diff --git a/e_report_calcs.py b/e_report_calcs.py new file mode 100644 index 000000000..1bb2e0033 --- /dev/null +++ b/e_report_calcs.py @@ -0,0 +1,65 @@ +#%% Imports +import sys +import gdxpds +import pandas as pd +from pathlib import Path +import reeds + +#%% Functions +def get_gams_results(case): + print('Loading resultsgdx') + dictin = gdxpds.to_dataframes(Path(case, 'outputs', 'results.gdx')) + ## Set indices as multiindex + valcols = ['Value','Level','Marginal','Lower','Upper','Scale'] + for key, df in dictin.items(): + indices = [i for i in df if i not in valcols] + dictin[key] = df.set_index(indices).squeeze(1) + print('Finished loading results.gdx') + return dictin + + +def calc_iq(g): + """Capacity above interconnection queue limit""" + dfs = {} + dfs['cap_above_limit'] = g['CAP_ABOVE_LIM'].Level + return dfs + + +def calc_co2_stor(g): + """CO2 capture, transport, and storage""" + dfs = {} + dfs['CO2_CAPTURED_out'] = g['CO2_CAPTURED'].Level + dfs['CO2_CAPTURED_out_ann'] = (g['CO2_STORED'].Level * g['hours']).groupby(['r','t']).sum() + return dfs + + +# def calc_transmission(g): +# """Transmission capacity and flow""" +# dfs = {} +# ## Combine forward and reverse into one +/- series +# # forward = list(zip(g['FLOW'].index.get_level_values('r'), g['FLOW'].index.get_level_values('rr'))) +# # reverse = list(zip(g['FLOW'].index.get_level_values('rr'), g['FLOW'].index.get_level_values('r'))) +# # g['FLOW'].reset_index(['allh','t','trtype']).loc[forward] +# g['FLOW'].loc[g['FLOW'].index.get_level_values('r') > g['FLOW'].index.get_level_values('rr')] +# g['FLOW'].loc[g['FLOW'].index.get_level_values('r') < g['FLOW'].index.get_level_values('rr')] +# # dfs['tran_flow_rep'] = g['FLOW'].Level + +# # g['FLOW'] +# # g['FLOW'].loc[ +# # g['FLOW'].index.get_level_values('rr'), +# # g['FLOW'].index.get_level_values('r'), +# # ] +# return dfs + + +def main(case): + dictin = get_gams_results(case) + dictout = { + **calc_iq(dictin), + **calc_co2_stor(dictin), + } + ## Drop zeros to reduce file size and match GAMS convention + for key, df in dictout.items(): + _df = df.rename('Value').reset_index() + dictout[key] = _df.loc[_df.Value != 0].copy() + return dictout diff --git a/e_report_data.csv b/e_report_data.csv new file mode 100644 index 000000000..9b504ad69 --- /dev/null +++ b/e_report_data.csv @@ -0,0 +1,7 @@ +CAP_ABOVE_LIM +CO2_CAPTURED +CO2_STORED +FLOW +hours +r +routes diff --git a/e_report_dump.py b/e_report_dump.py index faa3a20ab..242673d3c 100644 --- a/e_report_dump.py +++ b/e_report_dump.py @@ -7,12 +7,14 @@ import os import traceback import sys +from pathlib import Path # Third-party packages import pandas as pd import gdxpds import reeds +import e_report_calcs #%% Generic functions @@ -271,9 +273,10 @@ def postprocess_outputs(case, outputs_path=None, verbose=0): # %%### Write results for each gdx file ### outputs gdx print("Loading outputs gdx") - dict_out = gdxpds.to_dataframes( - os.path.join(outputs_path, f"rep_{os.path.basename(case)}.gdx") - ) + dict_out = { + **gdxpds.to_dataframes(Path(outputs_path, f"rep_{os.path.basename(case)}.gdx")), + **e_report_calcs.main(case), + } print("Finished loading outputs gdx") write_dfdict( diff --git a/runfiles.csv b/runfiles.csv index 1a60f823d..f1f65df0f 100644 --- a/runfiles.csv +++ b/runfiles.csv @@ -513,7 +513,9 @@ d3_data_dump.gms,d3_data_dump.gms,1,ignore,ignore,,,,,,,,,,, dump_alldata.gms,dump_alldata.gms,1,ignore,ignore,,,,,,,,,,, e_powfrac_calc.gms,e_powfrac_calc.gms,1,ignore,ignore,,,,,,,,,,, e_report.gms,e_report.gms,1,ignore,ignore,,,,,,,,,,, +e_report_calcs.py,e_report_calcs.py,1,ignore,ignore,,,,,,,,,,, e_report_dump.py,e_report_dump.py,1,ignore,ignore,,,,,,,,,,, +e_report_data.csv,e_report_data.csv,1,ignore,ignore,,,,,,,,,,, e_report_params.csv,e_report_params.csv,1,ignore,ignore,,,,,,,,,,, gamslice.txt,gamslice.txt,0,ignore,ignore,,,,,,,,,,, gurobi.opt,gurobi.opt,1,ignore,ignore,,,,,,,,,,, From b48288bc1651242e0d9b1f15632885874250590b Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Wed, 6 May 2026 16:26:52 -0600 Subject: [PATCH 02/12] add reeds.output_calc.diff_outputs() --- e_report_dump.py | 2 +- reeds/io.py | 2 +- reeds/output_calc.py | 96 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/e_report_dump.py b/e_report_dump.py index 242673d3c..9c9bc5c80 100644 --- a/e_report_dump.py +++ b/e_report_dump.py @@ -73,7 +73,7 @@ def dfdict_to_h5( _symbol_list = dfdict.keys() if symbol_list is None else symbol_list ### Check for existing file - _filepath = filepath if filepath.endswith(".h5") else filepath + ".h5" + _filepath = filepath if Path(filepath).suffix == '.h5' else str(filepath) + '.h5' if os.path.exists(_filepath): if overwrite: os.remove(_filepath) diff --git a/reeds/io.py b/reeds/io.py index 57d7dec6c..074c4e3b2 100644 --- a/reeds/io.py +++ b/reeds/io.py @@ -475,7 +475,7 @@ def read_output( Returns: pd.DataFrame """ - if case.endswith('.h5'): + if Path(case).suffix == '.h5': h5path = case else: h5path = os.path.join(case, 'outputs', 'outputs.h5') diff --git a/reeds/output_calc.py b/reeds/output_calc.py index a641bfe53..1ee0271bd 100644 --- a/reeds/output_calc.py +++ b/reeds/output_calc.py @@ -9,8 +9,10 @@ ### =========================================================================== import os import sys -import pandas as pd +import h5py import numpy as np +import pandas as pd +from pathlib import Path from itertools import product sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) @@ -730,4 +732,94 @@ def get_level_map(case): return level_map -# %% + +def diff_outputs( + casebase:str|Path, + casecomp:str|Path, + outpath:str|Path|None|bool=None, + threshold_abs=0, + threshold_rel=0, + verbose=0, +): + """ + Diff two {case}/outputs/outputs.h5 files and save the result to outpath. + + Args: + casebase: Absolute path to base ReEDS case + casecomp: Absolute path to comparison ReEDS case + outpath: Absolute path to resulting difference .h5 file + If None, difference file is saved to + {casebase}/comparisons/diff_{casebase.stem}_{casecomp.stem}.h5 + threshold_abs: Absolute cutoff for differences to include + (i.e., to ignore differences of 0.1 MW in an output parameter in units of MW, + set to 0.1) + threshold_rel: [fraction] Relative cutoff for differences to include, relative to base + (i.e., to ignore differences <1%, set to 0.01) + verbose (int): If nonzero, print descriptive logs + + Returns: + dict of pd.DataFrames (keys = entries in outputs.h5 with nonzero diff) + + Inputs for testing: + casebase = Path(reeds.io.reeds_path, 'runs', 'v20260306_itlM0_Pacific') + casecomp = Path(reeds.io.reeds_path, 'runs', 'v20260306_itlM0_Pacific_CC') + outpath = None + """ + ## Check inputs + casebase = Path(casebase) + casecomp = Path(casecomp) + fpaths = { + 'base': Path(casebase, 'outputs', 'outputs.h5'), + 'comp': Path(casecomp, 'outputs', 'outputs.h5'), + } + for key, fpath in fpaths.items(): + if not fpath.is_file(): + raise FileNotFoundError(fpath) + ## Make output directory + if outpath in [None, True]: + outpath = Path( + casebase, 'outputs', 'comparisons', f'diff_{casebase.stem}_{casecomp.stem}.h5' + ) + elif outpath is False: + pass + else: + outpath = Path(outpath) + if outpath: + outpath.parent.mkdir(exist_ok=True, parents=True) + ## Get results + keys = {} + dictin = {} + for case, fpath in fpaths.items(): + _dictin = {} + with h5py.File(fpath, 'r') as f: + keys[case] = list(f) + for key in keys[case]: + df = reeds.io.read_output(fpath, key) + indices = [i for i in df if i.lower() != 'value'] + if len(indices): + df = df.set_index(indices) + _dictin[key] = df.squeeze(1).astype(np.float32) + dictin[case] = _dictin + ## Take diff + allkeys = sorted(set(keys['base'] + keys['comp'])) + dictout = {} + for key in allkeys: + df = pd.concat({case:dictin[case].get(key, None) for case in dictin}, axis=1) + ## Sets have no value column; ignore them + if df.shape[1] == 0: + continue + df['diff'] = df.get('comp', 0) - df.get('base', 0) + df['frac'] = df.get('comp', 0) / df.get('base', 0) - 1 + df = df.loc[(df['diff'].abs() > threshold_abs) & (df['frac'].abs() > threshold_rel)].copy() + if len(df): + dictout[key] = df + if (verbose > 1) or (((verbose > 0) and len(df))): + print(f'{key}: {len(df)} differences') + ## Write it + if outpath: + if outpath.is_file(): + outpath.unlink() + for key, df in dictout.items(): + reeds.io.write_to_h5(df.reset_index(), key, outpath) + print(f'Difference written to {outpath}') + return dictout From 22d1d99dc442ab5e1ee2d43c7a0ed3ecb554a845 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 7 May 2026 08:43:39 -0600 Subject: [PATCH 03/12] e_report_calcs.py: add tran_flow_rep and tran_flow_rep_ann --- e_report.gms | 10 ---------- e_report_calcs.py | 40 ++++++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/e_report.gms b/e_report.gms index aa30af515..735849fe5 100644 --- a/e_report.gms +++ b/e_report.gms @@ -1786,21 +1786,11 @@ tran_flow_all_rep(r,rr,h,trtype,t) tran_flow_all_stress(r,rr,allh,trtype,t) $[tmodel_new(t)$routes(r,rr,trtype,t)$h_stress_t(allh,t)] = FLOW.l(r,rr,allh,t,trtype) ; -tran_flow_rep(r,rr,h,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$(ord(r) < ord(rr))] = - FLOW.l(r,rr,h,t,trtype) - FLOW.l(rr,r,h,t,trtype) -; - tran_flow_stress(r,rr,allh,trtype,t) $[tmodel_new(t)$routes(r,rr,trtype,t)$(ord(r) < ord(rr))$h_stress_t(allh,t)] = FLOW.l(r,rr,allh,t,trtype) - FLOW.l(rr,r,allh,t,trtype) ; -tran_flow_rep_ann(r,rr,trtype,t) - $[sum{h, tran_flow_rep(r,rr,h,trtype,t)}] = - sum{h, hours(h) * tran_flow_rep(r,rr,h,trtype,t) } -; - tran_util_h_rep(r,rr,h,trtype,t) $[tmodel_new(t)$routes(r,rr,trtype,t)$tran_cap_energy(r,rr,trtype,t)] = FLOW.l(r,rr,h,t,trtype) / tran_cap_energy(r,rr,trtype,t) diff --git a/e_report_calcs.py b/e_report_calcs.py index 1bb2e0033..c934eaade 100644 --- a/e_report_calcs.py +++ b/e_report_calcs.py @@ -1,9 +1,8 @@ #%% Imports -import sys import gdxpds import pandas as pd from pathlib import Path -import reeds + #%% Functions def get_gams_results(case): @@ -33,23 +32,27 @@ def calc_co2_stor(g): return dfs -# def calc_transmission(g): -# """Transmission capacity and flow""" -# dfs = {} -# ## Combine forward and reverse into one +/- series -# # forward = list(zip(g['FLOW'].index.get_level_values('r'), g['FLOW'].index.get_level_values('rr'))) -# # reverse = list(zip(g['FLOW'].index.get_level_values('rr'), g['FLOW'].index.get_level_values('r'))) -# # g['FLOW'].reset_index(['allh','t','trtype']).loc[forward] -# g['FLOW'].loc[g['FLOW'].index.get_level_values('r') > g['FLOW'].index.get_level_values('rr')] -# g['FLOW'].loc[g['FLOW'].index.get_level_values('r') < g['FLOW'].index.get_level_values('rr')] -# # dfs['tran_flow_rep'] = g['FLOW'].Level +def combine_forward_reverse(df, value='Level'): + """Combine forward (r < rr) and reverse (r > rr) into one +/- series""" + r_indices = ['r', 'rr'] + other_indices = [i for i in df.index.names if i not in r_indices] + forward = df.loc[ + df.index.get_level_values('r') < df.index.get_level_values('rr'), + value, + ] + reverse = -df.loc[ + df.index.get_level_values('r') > df.index.get_level_values('rr'), + value, + ].rename_axis(['rr', 'r'] + other_indices).reorder_levels(['r', 'rr'] + other_indices) + return pd.concat([forward, reverse]).groupby(['r', 'rr'] + other_indices).sum() + -# # g['FLOW'] -# # g['FLOW'].loc[ -# # g['FLOW'].index.get_level_values('rr'), -# # g['FLOW'].index.get_level_values('r'), -# # ] -# return dfs +def calc_transmission(g): + """Transmission capacity and flow""" + dfs = {} + dfs['tran_flow_rep'] = combine_forward_reverse(g['FLOW']) + dfs['tran_flow_rep_ann'] = (dfs['tran_flow_rep'] * g['hours']).groupby(['r','rr','trtype','t']).sum() + return dfs def main(case): @@ -57,6 +60,7 @@ def main(case): dictout = { **calc_iq(dictin), **calc_co2_stor(dictin), + **calc_transmission(dictin), } ## Drop zeros to reduce file size and match GAMS convention for key, df in dictout.items(): From d66997b07e029a8f1e490aefe8efbde929f4cca3 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:58:18 -0600 Subject: [PATCH 04/12] move rest of CO2 outputs to report_calcs.py --- reeds/core/terminus/report.gms | 47 +++++++++++++++++------------ reeds/core/terminus/report_calcs.py | 45 ++++++++++++++++++++------- reeds/core/terminus/report_data.csv | 15 +++++++-- 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index cf83ea645..5e81abd39 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -122,26 +122,33 @@ $include autocode%ds%report_params.gms h(h)$[not h_rep(h)] = no ; szn(szn)$[not szn_rep(szn)] = no ; -*===================== -* -- CO2 Reporting -- -*===================== - -CO2_STORED_out(r,cs,h,t)$[tmodel_new(t)$csfeas(cs)] = CO2_STORED.l(r,cs,h,t) ; -CO2_STORED_out_ann(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = sum(h,hours(h) * CO2_STORED.l(r,cs,h,t) ); -CO2_TRANSPORT_INV_out(r,rr,t)$tmodel_new(t) = CO2_TRANSPORT_INV.l(r,rr,t) ; -CO2_SPURLINE_INV_out(r,cs,t)$[tmodel_new(t)$csfeas(cs)] = CO2_SPURLINE_INV.l(r,cs,t) ; - -CO2_FLOW_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) + CO2_FLOW.l(rr,r,h,t)) } ; - -CO2_FLOW_pos_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) ; -CO2_FLOW_pos_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * CO2_FLOW.l(r,rr,h,t) } ; - -CO2_FLOW_neg_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_neg_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = -1 * sum{h, hours(h) * CO2_FLOW.l(rr,r,h,t) } ; - -CO2_FLOW_net_out(r,rr,h,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t) ; -CO2_FLOW_net_out_ann(r,rr,t)$[(ord(r) < ord(rr))$tmodel_new(t)] = sum{h, hours(h) * (CO2_FLOW.l(r,rr,h,t) - CO2_FLOW.l(rr,r,h,t)) } ; +*===================================== +* -- Parameters moved to Python -- +*===================================== + +$ontext +The calculation of the following output parameters has been moved to report_calcs.py: + +cap_above_limit + +CO2_CAPTURED_out +CO2_CAPTURED_out_ann +CO2_STORED_out +CO2_STORED_out_ann +CO2_TRANSPORT_INV_out +CO2_SPURLINE_INV_out +CO2_FLOW_out +CO2_FLOW_out_ann +CO2_FLOW_pos_out +CO2_FLOW_pos_out_ann +CO2_FLOW_neg_out +CO2_FLOW_neg_out_ann +CO2_FLOW_net_out +CO2_FLOW_net_out_ann + +tran_flow_rep +tran_flow_rep_ann +$offtext *========================= * LCOE diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index d8c41d14f..96f75fecd 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -2,6 +2,7 @@ import gdxpds import pandas as pd from pathlib import Path +from typing import Literal #%% Helper functions @@ -17,19 +18,29 @@ def get_gams_results(case): return dictin -def combine_forward_reverse(df, value='Level'): +def get_flow(df, direction:Literal['forward','reverse']='forward', value='Level'): + r_indices = ['r', 'rr'] + other_indices = [i for i in df.index.names if i not in r_indices] + if direction == 'forward': + out = df.loc[ + df.index.get_level_values('r') < df.index.get_level_values('rr'), + value, + ] + elif direction == 'reverse': + out = df.loc[ + df.index.get_level_values('r') > df.index.get_level_values('rr'), + value, + ].rename_axis(['rr', 'r'] + other_indices).reorder_levels(r_indices + other_indices) + return out + + +def combine_forward_reverse(df, agg:Literal['net','simult']='net', value='Level'): """Combine forward (r < rr) and reverse (r > rr) into one +/- series""" r_indices = ['r', 'rr'] other_indices = [i for i in df.index.names if i not in r_indices] - forward = df.loc[ - df.index.get_level_values('r') < df.index.get_level_values('rr'), - value, - ] - reverse = -df.loc[ - df.index.get_level_values('r') > df.index.get_level_values('rr'), - value, - ].rename_axis(['rr', 'r'] + other_indices).reorder_levels(['r', 'rr'] + other_indices) - return pd.concat([forward, reverse]).groupby(['r', 'rr'] + other_indices).sum() + forward = get_flow(df, 'forward') + reverse = (-1 if agg == 'net' else 1) * get_flow(df, 'reverse') + return pd.concat([forward, reverse]).groupby(r_indices + other_indices).sum() #%% Results calculations @@ -44,7 +55,19 @@ def calc_co2_stor(g): """CO2 capture, transport, and storage""" dfs = {} dfs['CO2_CAPTURED_out'] = g['CO2_CAPTURED'].Level - dfs['CO2_CAPTURED_out_ann'] = (g['CO2_STORED'].Level * g['hours']).groupby(['r','t']).sum() + dfs['CO2_CAPTURED_out_ann'] = (g['CO2_CAPTURED'].Level * g['hours']).groupby(['r','t']).sum() + dfs['CO2_STORED_out'] = g['CO2_STORED'].Level + dfs['CO2_STORED_out_ann'] = (g['CO2_STORED'].Level * g['hours']).groupby(['r','cs','t']).sum() + dfs['CO2_TRANSPORT_INV_out'] = g['CO2_TRANSPORT_INV'].Level + dfs['CO2_SPURLINE_INV_out'] = g['CO2_SPURLINE_INV'].Level + dfs['CO2_FLOW_out'] = combine_forward_reverse(g['CO2_FLOW'], agg='simult') + dfs['CO2_FLOW_out_ann'] = (dfs['CO2_FLOW_out'] * g['hours']).groupby(['r','rr','t']).sum() + dfs['CO2_FLOW_pos_out'] = get_flow(g['CO2_FLOW'], 'forward') + dfs['CO2_FLOW_pos_out_ann'] = (dfs['CO2_FLOW_pos_out'] * g['hours']).groupby(['r','rr','t']).sum() + dfs['CO2_FLOW_neg_out'] = -get_flow(g['CO2_FLOW'], 'reverse') + dfs['CO2_FLOW_neg_out_ann'] = (dfs['CO2_FLOW_neg_out'] * g['hours']).groupby(['r','rr','t']).sum() + dfs['CO2_FLOW_net_out'] = combine_forward_reverse(g['CO2_FLOW'], agg='net') + dfs['CO2_FLOW_net_out_ann'] = (dfs['CO2_FLOW_net_out'] * g['hours']).groupby(['r','rr','t']).sum() return dfs diff --git a/reeds/core/terminus/report_data.csv b/reeds/core/terminus/report_data.csv index 9b504ad69..b9a8ae931 100644 --- a/reeds/core/terminus/report_data.csv +++ b/reeds/core/terminus/report_data.csv @@ -1,7 +1,16 @@ +*** +*** Sets and parameters +*** +hours +r +routes +*** +*** Variables +*** CAP_ABOVE_LIM CO2_CAPTURED CO2_STORED +CO2_TRANSPORT_INV +CO2_SPURLINE_INV +CO2_FLOW FLOW -hours -r -routes From ab7ee9b8a5db6cdf8ae21ca5af62b071bc24e1dc Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:14:01 -0600 Subject: [PATCH 05/12] move more transmission outputs to report_calcs.py --- reeds/core/terminus/report.gms | 78 ++++++++++++----------------- reeds/core/terminus/report_calcs.py | 13 ++++- reeds/core/terminus/report_data.csv | 10 +++- 3 files changed, 53 insertions(+), 48 deletions(-) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index 5e81abd39..5d2076510 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -129,25 +129,39 @@ szn(szn)$[not szn_rep(szn)] = no ; $ontext The calculation of the following output parameters has been moved to report_calcs.py: -cap_above_limit - -CO2_CAPTURED_out -CO2_CAPTURED_out_ann -CO2_STORED_out -CO2_STORED_out_ann -CO2_TRANSPORT_INV_out -CO2_SPURLINE_INV_out -CO2_FLOW_out -CO2_FLOW_out_ann -CO2_FLOW_pos_out -CO2_FLOW_pos_out_ann -CO2_FLOW_neg_out -CO2_FLOW_neg_out_ann -CO2_FLOW_net_out -CO2_FLOW_net_out_ann - -tran_flow_rep -tran_flow_rep_ann +Interconnection queue + cap_above_limit + +CO2 storage and flows + CO2_CAPTURED_out + CO2_CAPTURED_out_ann + CO2_STORED_out + CO2_STORED_out_ann + CO2_TRANSPORT_INV_out + CO2_SPURLINE_INV_out + CO2_FLOW_out + CO2_FLOW_out_ann + CO2_FLOW_pos_out + CO2_FLOW_pos_out_ann + CO2_FLOW_neg_out + CO2_FLOW_neg_out_ann + CO2_FLOW_net_out + CO2_FLOW_net_out_ann + +Transmission + invtran_out + tran_cap_energy + tran_cap_prm + tran_cap_grp + tran_out + tran_prm_out + tran_mi_out_detail + tran_mi_out + tran_prm_mi_out + cap_converter_out + tran_flow_all_rep + tran_flow_rep + tran_flow_rep_ann $offtext *========================= @@ -1767,32 +1781,6 @@ excess_load(r,h,t) = EXCESS.l(r,h,t) ; *====================== * Transmission *====================== - -invtran_out(r,rr,trtype,t)$routes_inv(r,rr,trtype,t) = INVTRAN.l(r,rr,trtype,t) ; - -tran_cap_energy(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_ENERGY.l(r,rr,trtype,t) ; -tran_cap_prm(r,rr,trtype,t)$routes(r,rr,trtype,t) = CAPTRAN_PRM.l(r,rr,trtype,t) ; -tran_cap_grp(transgrp,transgrpp,t)$trancap_init_transgroup(transgrp,transgrpp,"AC") - = CAPTRAN_GRP.l(transgrp,transgrpp,t) ; - -tran_out(r,rr,trtype,t)$[(ord(r) Date: Thu, 9 Jul 2026 16:19:15 -0600 Subject: [PATCH 06/12] report_calcs.py: add comments with indices --- reeds/core/terminus/report_calcs.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index 4968ca1b3..66def57ac 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -47,6 +47,7 @@ def combine_forward_reverse(df, agg:Literal['net','simult']='net', value='Level' def calc_iq(g): """Capacity above interconnection queue limit""" dfs = {} + ## (tg,r,t) dfs['cap_above_limit'] = g['CAP_ABOVE_LIM'].Level return dfs @@ -54,19 +55,33 @@ def calc_iq(g): def calc_co2_stor(g): """CO2 capture, transport, and storage""" dfs = {} + ## (r,h,t) dfs['CO2_CAPTURED_out'] = g['CO2_CAPTURED'].Level + ## (r,t) dfs['CO2_CAPTURED_out_ann'] = (g['CO2_CAPTURED'].Level * g['hours']).groupby(['r','t']).sum() + ## (r,cs,h,t) dfs['CO2_STORED_out'] = g['CO2_STORED'].Level + ## (r,cs,t) dfs['CO2_STORED_out_ann'] = (g['CO2_STORED'].Level * g['hours']).groupby(['r','cs','t']).sum() + ## (r,rr,t) dfs['CO2_TRANSPORT_INV_out'] = g['CO2_TRANSPORT_INV'].Level + ## (r,cs,t) dfs['CO2_SPURLINE_INV_out'] = g['CO2_SPURLINE_INV'].Level + ## (r,rr,h,t) dfs['CO2_FLOW_out'] = combine_forward_reverse(g['CO2_FLOW'], agg='simult') + ## (r,rr,t) dfs['CO2_FLOW_out_ann'] = (dfs['CO2_FLOW_out'] * g['hours']).groupby(['r','rr','t']).sum() + ## (r,rr,h,t) dfs['CO2_FLOW_pos_out'] = get_flow(g['CO2_FLOW'], 'forward') + ## (r,rr,t) dfs['CO2_FLOW_pos_out_ann'] = (dfs['CO2_FLOW_pos_out'] * g['hours']).groupby(['r','rr','t']).sum() + ## (r,rr,h,t) dfs['CO2_FLOW_neg_out'] = -get_flow(g['CO2_FLOW'], 'reverse') + ## (r,rr,t) dfs['CO2_FLOW_neg_out_ann'] = (dfs['CO2_FLOW_neg_out'] * g['hours']).groupby(['r','rr','t']).sum() + ## (r,rr,h,t) dfs['CO2_FLOW_net_out'] = combine_forward_reverse(g['CO2_FLOW'], agg='net') + ## (r,rr,t) dfs['CO2_FLOW_net_out_ann'] = (dfs['CO2_FLOW_net_out'] * g['hours']).groupby(['r','rr','t']).sum() return dfs @@ -74,18 +89,31 @@ def calc_co2_stor(g): def calc_transmission(g): """Transmission capacity and flow""" dfs = {} + ## (r,rr,trtype,t) dfs['invtran_out'] = g['INVTRAN'].Level + ## (r,rr,trtype,t) dfs['tran_cap_energy'] = g['CAPTRAN_ENERGY'].Level + ## (r,rr,trtype,t) dfs['tran_cap_prm'] = g['CAPTRAN_PRM'].Level + ## (transgrp,transgrpp,t) dfs['tran_cap_grp'] = g['CAPTRAN_GRP'].Level + ## (r,rr,trtype,t) dfs['tran_out'] = combine_forward_reverse(dfs['tran_cap_energy'], agg='simult') / 2 + ## (r,rr,trtype,t) dfs['tran_prm_out'] = combine_forward_reverse(dfs['tran_cap_prm'], agg='simult') / 2 + ## (r,rr,trtype,t) dfs['tran_mi_out_detail'] = dfs['tran_out'] * g['distance'] + ## (trtype,t) dfs['tran_mi_out'] = dfs['tran_mi_out_detail'].groupby(['trtype','t']).sum() + ## (trtype,t) dfs['tran_prm_mi_out'] = (dfs['tran_prm_out'] * g['distance']).groupby(['trtype','t']).sum() + ## (r,t) dfs['cap_converter_out'] = g['CAP_CONVERTER'].Level + ## (r,rr,h,trtype,t) dfs['tran_flow_all_rep'] = g['FLOW'].Level.xs(g['h_rep'], 0, 'h') + ## (r,rr,h,trtype,t) dfs['tran_flow_rep'] = combine_forward_reverse(g['FLOW'], agg='net') + ## (r,rr,trtype,t) dfs['tran_flow_rep_ann'] = (dfs['tran_flow_rep'] * g['hours']).groupby(['r','rr','trtype','t']).sum() return dfs From 81febfa61708482c6a0b4c67ccf7f8bb2dd90fd6 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:43:50 -0600 Subject: [PATCH 07/12] more transmission outputs --- reeds/core/terminus/report.gms | 33 ----------------- reeds/core/terminus/report_calcs.py | 55 ++++++++++++++++++++++------- reeds/core/terminus/report_data.csv | 2 ++ 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index 5d2076510..8ddf2a057 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -1781,39 +1781,6 @@ excess_load(r,h,t) = EXCESS.l(r,h,t) ; *====================== * Transmission *====================== -tran_flow_all_stress(r,rr,allh,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$h_stress_t(allh,t)] = FLOW.l(r,rr,allh,t,trtype) ; - -tran_flow_stress(r,rr,allh,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$(ord(r) < ord(rr))$h_stress_t(allh,t)] = - FLOW.l(r,rr,allh,t,trtype) - FLOW.l(rr,r,allh,t,trtype) -; - -tran_util_h_rep(r,rr,h,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$tran_cap_energy(r,rr,trtype,t)] = - FLOW.l(r,rr,h,t,trtype) / tran_cap_energy(r,rr,trtype,t) -; - -tran_util_h_stress(r,rr,allh,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$tran_cap_prm(r,rr,trtype,t)$h_stress_t(allh,t)] = - FLOW.l(r,rr,allh,t,trtype) / tran_cap_prm(r,rr,trtype,t) -; - -tran_util_ann_rep(r,rr,trtype,t) - $[tmodel_new(t)$routes(r,rr,trtype,t)$tran_cap_energy(r,rr,trtype,t)] = - sum{h, FLOW.l(r,rr,h,t,trtype) * hours(h) / tran_cap_energy(r,rr,trtype,t) } - / sum{h, hours(h) } -; - -tran_util_ann_stress(r,rr,trtype,t) - $[tmodel_new(t) - $routes(r,rr,trtype,t)$tran_cap_prm(r,rr,trtype,t) - $sum{allh$h_stress_t(allh,t), hours(allh)}] = - sum{allh$h_stress_t(allh,t), - FLOW.l(r,rr,allh,t,trtype) * hours(allh) / tran_cap_prm(r,rr,trtype,t) } - / sum{allh$h_stress_t(allh,t), hours(allh) } -; - import_h_rep(r,h,t) $[tmodel_new(t)] = * Imports with losses diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index 66def57ac..6b776edd5 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -22,15 +22,18 @@ def get_flow(df, direction:Literal['forward','reverse']='forward', value='Level' r_indices = ['r', 'rr'] other_indices = [i for i in df.index.names if i not in r_indices] if direction == 'forward': - out = df.loc[ - df.index.get_level_values('r') < df.index.get_level_values('rr'), - value, - ] + mask = df.index.get_level_values('r') < df.index.get_level_values('rr') + if isinstance(df, pd.Series): + out = df.loc[mask] + elif isinstance(df, pd.DataFrame): + out = df.loc[mask, value] elif direction == 'reverse': - out = df.loc[ - df.index.get_level_values('r') > df.index.get_level_values('rr'), - value, - ].rename_axis(['rr', 'r'] + other_indices).reorder_levels(r_indices + other_indices) + mask = df.index.get_level_values('r') > df.index.get_level_values('rr') + if isinstance(df, pd.Series): + _out = df.loc[mask] + elif isinstance(df, pd.DataFrame): + _out = df.loc[mask, value] + out = _out.rename_axis(['rr', 'r'] + other_indices).reorder_levels(r_indices + other_indices) return out @@ -109,12 +112,38 @@ def calc_transmission(g): dfs['tran_prm_mi_out'] = (dfs['tran_prm_out'] * g['distance']).groupby(['trtype','t']).sum() ## (r,t) dfs['cap_converter_out'] = g['CAP_CONVERTER'].Level - ## (r,rr,h,trtype,t) - dfs['tran_flow_all_rep'] = g['FLOW'].Level.xs(g['h_rep'], 0, 'h') - ## (r,rr,h,trtype,t) - dfs['tran_flow_rep'] = combine_forward_reverse(g['FLOW'], agg='net') + ## (r,rr,h,trtype,t) (r,rr,allh,t,trtype) + dfs['tran_flow_all_rep'] = g['FLOW'].Level.loc[:,:,g['h_rep'].index] + ## (r,rr,allh,trtype,t) + dfs['tran_flow_all_stress'] = ( + g['FLOW'].Level.reset_index() + .merge(g['h_stress_t'], left_on=['allh','t'], right_on=['allh','allt']) + .set_index(['r','rr','allh','trtype','t']).Level + ) + ## (r,rr,h,trtype,t) (r,rr,allh,t,trtype) + dfs['tran_flow_rep'] = combine_forward_reverse(g['FLOW'], agg='net').loc[:,:,g['h_rep'].index] + ## (r,rr,allh,trtype,t) + dfs['tran_flow_stress'] = ( + combine_forward_reverse(g['FLOW']).reset_index() + .merge(g['h_stress_t'], left_on=['allh','t'], right_on=['allh','allt']) + .set_index(['r','rr','allh','trtype','t']).Level + ) ## (r,rr,trtype,t) dfs['tran_flow_rep_ann'] = (dfs['tran_flow_rep'] * g['hours']).groupby(['r','rr','trtype','t']).sum() + ## (r,rr,h,trtype,t) + dfs['tran_util_h_rep'] = dfs['tran_flow_all_rep'] / dfs['tran_cap_energy'] + ## (r,rr,allh,trtype,t) + dfs['tran_util_h_stress'] = dfs['tran_flow_all_stress'] / dfs['tran_cap_prm'] + ## (r,rr,trtype,t) + dfs['tran_util_ann_rep'] = ( + (dfs['tran_flow_all_rep'] * g['hours'] / dfs['tran_cap_energy']).groupby(['r','rr','trtype','t']).sum() + / g['hours'].sum() + ) + ## (r,rr,trtype,t) + dfs['tran_util_ann_stress'] = ( + (dfs['tran_flow_all_stress'] * g['hours'] / dfs['tran_cap_prm']).groupby(['r','rr','trtype','t']).sum() + / g['hours'].drop(g['h_rep'].index).sum() + ) return dfs @@ -129,5 +158,5 @@ def main(case): ## Drop zeros to reduce file size and match GAMS convention for key, df in dictout.items(): _df = df.rename('Value').reset_index() - dictout[key] = _df.loc[_df.Value != 0].copy() + dictout[key] = _df.loc[_df.Value != 0].dropna().copy() return dictout diff --git a/reeds/core/terminus/report_data.csv b/reeds/core/terminus/report_data.csv index 3cd216479..b546c3225 100644 --- a/reeds/core/terminus/report_data.csv +++ b/reeds/core/terminus/report_data.csv @@ -3,6 +3,7 @@ *** distance h_rep +h_stress_t hours r routes @@ -12,6 +13,7 @@ routes CAP_ABOVE_LIM CAP_CONVERTER CAPTRAN_ENERGY +CAPTRAN_GRP CAPTRAN_PRM CO2_CAPTURED CO2_FLOW From 001bd1f82cf0633637432640714bd529cabba65b Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:08:58 -0600 Subject: [PATCH 08/12] clean up comments --- reeds/core/terminus/report.gms | 36 +++++++++++++++++------------ reeds/core/terminus/report_calcs.py | 2 ++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/reeds/core/terminus/report.gms b/reeds/core/terminus/report.gms index 8ddf2a057..c14dfe81f 100644 --- a/reeds/core/terminus/report.gms +++ b/reeds/core/terminus/report.gms @@ -135,33 +135,39 @@ Interconnection queue CO2 storage and flows CO2_CAPTURED_out CO2_CAPTURED_out_ann - CO2_STORED_out - CO2_STORED_out_ann - CO2_TRANSPORT_INV_out - CO2_SPURLINE_INV_out - CO2_FLOW_out - CO2_FLOW_out_ann - CO2_FLOW_pos_out - CO2_FLOW_pos_out_ann CO2_FLOW_neg_out CO2_FLOW_neg_out_ann CO2_FLOW_net_out CO2_FLOW_net_out_ann + CO2_FLOW_out + CO2_FLOW_out_ann + CO2_FLOW_pos_out + CO2_FLOW_pos_out_ann + CO2_SPURLINE_INV_out + CO2_STORED_out + CO2_STORED_out_ann + CO2_TRANSPORT_INV_out Transmission + cap_converter_out invtran_out tran_cap_energy - tran_cap_prm tran_cap_grp - tran_out - tran_prm_out - tran_mi_out_detail - tran_mi_out - tran_prm_mi_out - cap_converter_out + tran_cap_prm tran_flow_all_rep + tran_flow_all_stress tran_flow_rep tran_flow_rep_ann + tran_flow_stress + tran_mi_out + tran_mi_out_detail + tran_out + tran_prm_mi_out + tran_prm_out + tran_util_ann_rep + tran_util_ann_stress + tran_util_h_rep + tran_util_h_stress $offtext *========================= diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index 6b776edd5..e365b8690 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -149,6 +149,8 @@ def calc_transmission(g): #%% Procedure def main(case): + ## NOTE: If calculations slow down for large runs, consider dropping zeros upfront + ## in get_gams_results() to speed up processing dictin = get_gams_results(case) dictout = { **calc_iq(dictin), From b30a9c7d6ad31bd912b51f5e063bce3ed1787eb5 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:29:29 -0600 Subject: [PATCH 09/12] report_calcs.py: tran_util_ann_stress: drop hour weighting since it's wrong for stress periods before the final year --- reeds/core/terminus/report_calcs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index e365b8690..ed4aaefbc 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -139,11 +139,12 @@ def calc_transmission(g): (dfs['tran_flow_all_rep'] * g['hours'] / dfs['tran_cap_energy']).groupby(['r','rr','trtype','t']).sum() / g['hours'].sum() ) + ## NOTE: We here assume that all solve years use the same total weighting for stress + ## periods and weight all stress hours the same, so we don't weight by the number of hours. + ## If we switch to different weightings for different stress periods, + ## should add an hours(allh,t) parameter. ## (r,rr,trtype,t) - dfs['tran_util_ann_stress'] = ( - (dfs['tran_flow_all_stress'] * g['hours'] / dfs['tran_cap_prm']).groupby(['r','rr','trtype','t']).sum() - / g['hours'].drop(g['h_rep'].index).sum() - ) + dfs['tran_util_ann_stress'] = dfs['tran_util_h_stress'].groupby(['r','rr','trtype','t']).mean() return dfs From 28b3af6498348f63a41d491faca633b4e0069e5c Mon Sep 17 00:00:00 2001 From: bsergi Date: Thu, 23 Jul 2026 11:23:37 -0400 Subject: [PATCH 10/12] small logging fix --- reeds/core/terminus/report_dump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/core/terminus/report_dump.py b/reeds/core/terminus/report_dump.py index e0f62168a..5169e5399 100644 --- a/reeds/core/terminus/report_dump.py +++ b/reeds/core/terminus/report_dump.py @@ -307,6 +307,6 @@ def postprocess_outputs(case, outputs_path=None, verbose=0): #%% All done print("Completed report_dump.py") try: - toc(tic=tic, year=0, path=case, process="report_dump.py") + reeds.log.toc(tic=tic, year=0, path=case, process="report_dump.py") except NameError: print("reeds/log.py not found, so not logging output") From b983c5d9ba60a7adc5f354f53080176b6b013223 Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:08:33 -0600 Subject: [PATCH 11/12] report_calcs.py: Fix typo Co-authored-by: Brian Sergi --- reeds/core/terminus/report_calcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/core/terminus/report_calcs.py b/reeds/core/terminus/report_calcs.py index ed4aaefbc..d4f5820da 100644 --- a/reeds/core/terminus/report_calcs.py +++ b/reeds/core/terminus/report_calcs.py @@ -7,7 +7,7 @@ #%% Helper functions def get_gams_results(case): - print('Loading resultsgdx') + print('Loading results.gdx') dictin = gdxpds.to_dataframes(Path(case, 'outputs', 'results.gdx')) ## Set indices as multiindex valcols = ['Value','Level','Marginal','Lower','Upper','Scale'] From 9d4dbbbbbfca1d73ae785900b06c8d17750b4c2c Mon Sep 17 00:00:00 2001 From: Patrick Brown <25125211+patrickbrown4@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:21:06 -0600 Subject: [PATCH 12/12] results.diff_outputs(): expand docstring --- reeds/results.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/reeds/results.py b/reeds/results.py index dcf76cd57..496720376 100644 --- a/reeds/results.py +++ b/reeds/results.py @@ -718,10 +718,10 @@ def diff_outputs( casebase:str|Path, casecomp:str|Path, outpath:str|Path|None|bool=None, - threshold_abs=0, - threshold_rel=0, - verbose=0, -): + threshold_abs:float=0, + threshold_rel:float=0, + verbose:int=0, +) -> dict: """ Diff two {case}/outputs/outputs.h5 files and save the result to outpath. @@ -729,8 +729,9 @@ def diff_outputs( casebase: Absolute path to base ReEDS case casecomp: Absolute path to comparison ReEDS case outpath: Absolute path to resulting difference .h5 file - If None, difference file is saved to - {casebase}/comparisons/diff_{casebase.stem}_{casecomp.stem}.h5 + If None or True, difference file is saved to + {casebase}/comparisons/diff_{casebase.stem}_{casecomp.stem}.h5. + If False, no difference file is written. threshold_abs: Absolute cutoff for differences to include (i.e., to ignore differences of 0.1 MW in an output parameter in units of MW, set to 0.1)