From 278f7a9e6f1a816c3a477f777ab2ae834dc90193 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 5 Aug 2026 14:33:30 -0600 Subject: [PATCH 01/58] add adjustText to environment.yml --- environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 8df757e15..0a1e1a0a7 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: reeds +name: reeds-at channels: - conda-forge - nodefaults @@ -25,6 +25,7 @@ dependencies: - scipy=1.18 - shapely=2.1 - tqdm=4.68 + - adjustText ## vvv The next packages are optional for default settings and may ## vvv be excluded if you're having trouble building the environment. - fiona=1.10 # for interactive maps From bd8f7031fc64f8440e9493b476e0775abd817a6c Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 6 Aug 2026 17:51:09 -0600 Subject: [PATCH 02/58] Fix map-diff tech aggregation bug --- reeds/reedsplots.py | 55 +++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index b184fd4ce..7b49b8bfa 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -981,6 +981,31 @@ def plot_diff_maps( dfbase[valcol] *= unitscaler dfcomp[valcol] *= unitscaler + ### Aggregate selected technologies to one value per region for the target year + dfbase_selected = ( + dfbase.loc[(dfbase.i.isin(titles)) & (dfbase.t == year), ['r', valcol]] + .groupby('r', as_index=False)[valcol].sum() + .rename(columns={valcol: f'{valcol}_base'}) + ) + dfcomp_selected = ( + dfcomp.loc[(dfcomp.i.isin(titles)) & (dfcomp.t == year), ['r', valcol]] + .groupby('r', as_index=False)[valcol].sum() + .rename(columns={valcol: f'{valcol}_comp'}) + ) + dfdiff_selected = ( + dfbase_selected + .merge(dfcomp_selected, on='r', how='outer') + .fillna(0) + ) + dfdiff_selected[f'{valcol}_diff'] = ( + dfdiff_selected[f'{valcol}_comp'] - dfdiff_selected[f'{valcol}_base'] + ) + dfdiff_selected[f'{valcol}_pctdiff'] = np.where( + dfdiff_selected[f'{valcol}_base'] != 0, + dfdiff_selected[f'{valcol}_diff'] / dfdiff_selected[f'{valcol}_base'] * 100, + np.nan, + ) + ### Start the plot # plt.close() if (f is None) and (ax is None): @@ -988,32 +1013,18 @@ def plot_diff_maps( else: pass - ###### Calculate the diff - dfdiff = dfbase.merge( - dfcomp, on=['i','r','t'], how='outer', suffixes=('_base','_comp')).fillna(0) - if plot in ['diff','pctdiff','pct_diff','diffpct','diff_pct','pct']: - ### Percent difference - dfdiff['{}_diff'.format(valcol)] = ( - (dfdiff['{}_comp'.format(valcol)] - dfdiff['{}_base'.format(valcol)]) - / dfdiff['{}_base'.format(valcol)] * 100 - ).replace(np.inf,np.nan) - elif plot in ['absdiff', 'abs_diff', 'diffabs', 'diff_abs']: - ### Difference - dfdiff['{}_diff'.format(valcol)] = ( - dfdiff['{}_comp'.format(valcol)] - dfdiff['{}_base'.format(valcol)]) - if zmax is None: zmax = max( - dfdiff.loc[(dfdiff.i.isin(titles))&(dfdiff.t==year),valcol+'_base'].max(), - dfdiff.loc[(dfdiff.i.isin(titles))&(dfdiff.t==year),valcol+'_comp'].max(), + dfdiff_selected[f'{valcol}_base'].max(), + dfdiff_selected[f'{valcol}_comp'].max(), ) ###### Plot the base if plot == 'base': dfplot = dfba.merge( - dfbase.loc[(dfbase.i.isin(titles))&(dfbase.t==year),['r',valcol]], + dfdiff_selected[['r', f'{valcol}_base']], left_index=True, right_on='r', how='left' - ).fillna(0).reset_index(drop=True) + ).fillna(0).reset_index(drop=True).rename(columns={f'{valcol}_base': valcol}) dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) @@ -1021,9 +1032,9 @@ def plot_diff_maps( ###### Plot the comp elif plot == 'comp': dfplot = dfba.merge( - dfcomp.loc[(dfcomp.i.isin(titles))&(dfcomp.t==year),['r',valcol]], + dfdiff_selected[['r', f'{valcol}_comp']], left_index=True, right_on='r', how='left' - ).fillna(0).reset_index(drop=True) + ).fillna(0).reset_index(drop=True).rename(columns={f'{valcol}_comp': valcol}) dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) @@ -1033,7 +1044,7 @@ def plot_diff_maps( legend_kwds['label'] = '{} {} {}\n[% diff]'.format(valcol,i_plot,year) dfplot = dfba.merge( - dfdiff.loc[(dfdiff.i.isin(titles))&(dfdiff.t==year),['r',valcol+'_diff']], + dfdiff_selected[['r', f'{valcol}_pctdiff']], left_index=True, right_on='r', how='left' ).reset_index(drop=True) @@ -1052,7 +1063,7 @@ def plot_diff_maps( legend_kwds['label'] = '{} diff {} [{}]'.format(valcol,i_plot,units) dfplot = dfba.merge( - dfdiff.loc[(dfdiff.i.isin(titles))&(dfdiff.t==year),['r',valcol+'_diff']], + dfdiff_selected[['r', f'{valcol}_diff']], left_index=True, right_on='r', how='left' ).reset_index(drop=True) From 658f5163f34511f27489aa356befa92c3000d677 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 6 Aug 2026 18:33:10 -0600 Subject: [PATCH 03/58] abs cmap rain->tempo in compare_cases.py --- postprocessing/compare_cases.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/postprocessing/compare_cases.py b/postprocessing/compare_cases.py index 7972b6547..e7d046051 100644 --- a/postprocessing/compare_cases.py +++ b/postprocessing/compare_cases.py @@ -130,7 +130,7 @@ #%%### Fixed inputs -cmap = cmocean.cm.rain +cmap = cmocean.cm.tempo cmap_diff = plt.cm.RdBu_r ## https://www.whitehouse.gov/wp-content/uploads/2023/11/CircularA-4.pdf discountrate_social = DEFAULT_DISCOUNT_RATE @@ -2575,7 +2575,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): year=lastyear, casebase=casebase, casecomp=casecomp, level=level, plot='base', f=f, ax=ax[0], - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, ) ax[0].annotate( casebase_name, @@ -2586,7 +2586,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): year=lastyear, casebase=casebase, casecomp=casecomp, level=level, plot='comp', f=f, ax=ax[1], - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, ) ax[1].annotate( casecomp_name, From bae00b2ae31f6d3b75025edb3b6e5c57dfe9dc60 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 6 Aug 2026 18:37:03 -0600 Subject: [PATCH 04/58] Add region labels in plot_seed_stressperiods --- reeds/reedsplots.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 7b49b8bfa..6bd717832 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -15,6 +15,7 @@ import geopandas as gpd import shapely import cmocean +from adjustText import adjust_text sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import reeds from reeds import plots @@ -5116,12 +5117,18 @@ def plot_seed_stressperiods( ax=ax[row,col], column='val', edgecolor='none', lw=0, cmap=cmap, alpha=alpha, vmin=0, vmax=1, ) + text_artists = [] for i, _row in df.iterrows(): - ax[row,col].annotate( - _row.date, (_row.centroid_x, _row.centroid_y), - ha='center', va='center', color='k', fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], + text_artists.append( + ax[row,col].annotate( + _row.date, (_row.centroid_x, _row.centroid_y), + ha='center', va='center', color='k', fontsize=fontsize, + path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], + ) ) + adjust_text(text_artists, ax=ax[row,col], + avoid_self=False, ensure_inside_axes=True, + ) ### Max load @@ -5152,12 +5159,18 @@ def plot_seed_stressperiods( ax=ax[row,col], column='val', edgecolor='none', lw=0, cmap=cmap, alpha=alpha, vmin=0, vmax=1, ) + text_artists = [] for i, _row in df.iterrows(): - ax[row,col].annotate( - _row.date, (_row.centroid_x, _row.centroid_y), - ha='center', va='center', color='k', fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], + text_artists.append( + ax[row,col].annotate( + _row.date, (_row.centroid_x, _row.centroid_y), + ha='center', va='center', color='k', fontsize=fontsize, + path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], + ) ) + adjust_text(text_artists, ax=ax[row,col], + avoid_self=False, ensure_inside_axes=True, + ) ### Colorbar row, col = 0, 0 From 587a9767f165ea8126973b96786ec70e3d945fcb Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 6 Aug 2026 18:42:44 -0600 Subject: [PATCH 05/58] Add region labels in capacity maps --- postprocessing/compare_cases.py | 2 ++ reeds/reedsplots.py | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/postprocessing/compare_cases.py b/postprocessing/compare_cases.py index e7d046051..cab0cdab8 100644 --- a/postprocessing/compare_cases.py +++ b/postprocessing/compare_cases.py @@ -2662,6 +2662,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): ax=ax[coords[case]], column='GW', cmap=cmap, vmin=vmin, vmax=vmax, legend=False, ) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fmt = '{:.0f}', fontsize=6) ## Legend if coords[case] == legendcoords: plots.addcolorbarhist( @@ -2733,6 +2734,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): vmax=(absmax if case == basecase else diffmax), legend=False, ) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fmt = ('{:.0f}' if case == basecase else '{:+.0f}'), fontsize=6) ## Difference legend if coords[case] == legendcoords: plots.addcolorbarhist( diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 6bd717832..4af4efc6f 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1029,6 +1029,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) + label_region_value(dfplot, ax=ax, column=valcol, fmt = '{:.1f}', fontsize=6) ###### Plot the comp elif plot == 'comp': @@ -1040,6 +1041,8 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) + label_region_value(dfplot, ax=ax, column=valcol, fmt = '{:.1f}', fontsize=6) + ###### Plot the pct diff elif plot in ['diff','pctdiff','pct_diff','diffpct','diff_pct','pct']: legend_kwds['label'] = '{} {} {}\n[% diff]'.format(valcol,i_plot,year) @@ -1058,6 +1061,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_pctdiff', cmap=cmap, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) + label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fmt = '{:.1f}', fontsize=6) ###### Plot the absolute diff elif plot in ['absdiff', 'abs_diff', 'diffabs', 'diff_abs']: @@ -1077,6 +1081,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_diff', cmap=plt.cm.RdBu_r, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) + label_region_value(dfplot, ax=ax, column=valcol+'_diff', fmt = '{:.1f}', fontsize=6) ### Finish and return # ax.set_title(title, y=0.95) @@ -6153,15 +6158,24 @@ def get_cf_map(case, tech='wind-ons', timestamp=None, recf=None, crs='EPSG:5070' def label_region_value(df, ax, column, fmt='{:.2f}', color='k', fontsize=8, **kwargs): """kwargs are passed to patheffects.withStroke()""" pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':1}, **kwargs} + text_artists = [] for r, row in df.iterrows(): - ax.annotate( - fmt.format(row[column]), - (row.geometry.centroid.x, row.geometry.centroid.y), - ha='center', va='center', fontsize=fontsize, - color=color, - path_effects=[pe.withStroke(**pe_kwargs)], - zorder=1e9, + value = row.get(column, np.nan) + if not np.isfinite(value): + continue + text_artists.append( + ax.annotate( + fmt.format(value), + (row.geometry.centroid.x, row.geometry.centroid.y), + ha='center', va='center', fontsize=fontsize, + color=color, + path_effects=[pe.withStroke(**pe_kwargs)], + zorder=1e9, + ) ) + adjust_text(text_artists, ax=ax, + avoid_self=False, ensure_inside_axes=True, + ) def map_stressors( From 1218a8a4a9e418653facc7bffa3366d2dda1a6cc Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 09:06:20 -0600 Subject: [PATCH 06/58] Adjust labels in map_neue --- reeds/reedsplots.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 94accdc79..a9ed9f4dc 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -4390,25 +4390,31 @@ def map_neue( ## Labels # decimals = (0 if df.NEUE_ppm.max() >= 10 else 1) decimals = (0 if level in ['st','r'] else 1) + text_artists = [] for r, row in df.sort_values('NEUE_ppm').iterrows(): if highlight_over_threshold: over_threshold = row.NEUE_ppm > neue_threshold else: over_threshold = False - ax[coords[level]].annotate( - f"{row.NEUE_ppm:.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', - c=(over_threshold_textcolor if over_threshold else 'k'), - weight=('bold' if over_threshold else 'normal'), - fontsize={'r':5}.get(level,7), - zorder=1e9, - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], + text_artists.append( + ax[coords[level]].annotate( + f"{row.NEUE_ppm:.{decimals}f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', + c=(over_threshold_textcolor if over_threshold else 'k'), + weight=('bold' if over_threshold else 'normal'), + fontsize={'r':5}.get(level,7), + zorder=1e9, + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], + ) ) if over_threshold and (level == neue_threshold_level): ax[coords[level]].set_title( level, y=0.9, weight='bold', color=over_threshold_textcolor, ) + adjust_text(text_artists, ax=ax[coords[level]], + avoid_self=False, ensure_inside_axes=True, + ) ### Formatting plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=df.NEUE_ppm, From 07572a3022598b08a575da92cbabb7c2cd52d021 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 09:07:20 -0600 Subject: [PATCH 07/58] Adjust labels in hourly_plots.py --- reeds/input_processing/hourly_plots.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py index 67934a3ca..c9e87d742 100644 --- a/reeds/input_processing/hourly_plots.py +++ b/reeds/input_processing/hourly_plots.py @@ -12,6 +12,7 @@ from matplotlib import patheffects as pe import cmocean from pathlib import Path +from adjustText import adjust_text sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds from reeds import plots @@ -355,14 +356,20 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) ## Text differences + text_artists = [] for r, row in (dfdiffs[level].assign(val=dfdiffs[level].cf_diff.abs()).sort_values('val')).iterrows(): decimals = 0 if abs(row.cf_diff) >= 1 else 1 - ax[coords[level]].annotate( + text_artists.append( + ax[coords[level]].annotate( f"{row.cf_diff*100:+.{decimals}f}", [row.centroid_x, row.centroid_y], ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], + ) ) + adjust_text(text_artists, ax=ax[coords[level]], + avoid_self=False,ensure_inside_axes=True, + ) ## Colorbar plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=dfdiffs[level].cf_diff*100, nbins=51, @@ -489,14 +496,20 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) ## Text differences + text_artists = [] for r, row in (dfdiffs[level].assign(val=dfdiffs[level][val].abs()).sort_values('val')).iterrows(): decimals = 0 if abs(row[val]) >= 1 else 1 - ax[coords[level]].annotate( - f"{row[val]:+.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], + text_artists.append( + ax[coords[level]].annotate( + f"{row[val]:+.{decimals}f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], + ) ) + adjust_text(text_artists, ax=ax[coords[level]], + avoid_self=False,ensure_inside_axes=True, + ) ## Colorbar plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=dfdiffs[level][val], nbins=51, From b8abd24b03030ee79a547d7732f4addaf963f6ef Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 09:27:37 -0600 Subject: [PATCH 08/58] Update hourly_plots abs cbar --- reeds/input_processing/hourly_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py index c9e87d742..f7c45d794 100644 --- a/reeds/input_processing/hourly_plots.py +++ b/reeds/input_processing/hourly_plots.py @@ -231,7 +231,7 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: ### Settings cmaps = { 'cf_actual':plt.cm.turbo, 'cf_rep':plt.cm.turbo, 'cf_diff':plt.cm.RdBu_r, - 'GW_actual':cmocean.cm.rain, 'GW_rep':cmocean.cm.rain, + 'GW_actual':cmocean.cm.tempo, 'GW_rep':cmocean.cm.tempo, 'GW_diff':plt.cm.RdBu_r, 'GW_frac':plt.cm.RdBu_r, 'pct_diff':plt.cm.RdBu_r, } vm = { From d2a686f7d1b8b274f7b5505f8ba065925d9c93bd Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 13:36:58 -0600 Subject: [PATCH 09/58] Label capacity, net_imports, h2_capacity maps --- reeds/reedsplots.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index a9ed9f4dc..a21b4f723 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1375,6 +1375,7 @@ def map_net_imports( legend=False, vmin=-vmax[year], vmax=vmax[year], ) + label_region_value(df, ax=ax[coords[year]], column='net_import', fmt = '{:+.0f}', fontsize=6) ## Formatting ax[coords[year]].set_title(year, y=0.9) if vlim != 'shared': @@ -2768,6 +2769,7 @@ def map_capacity_techs( 'label': '{} [GW]'.format(tech), } ) + label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fmt = '{:.1f}', fontsize=6) ax[coords[tech]].axis('off') ax[0,0].set_title( '{} ({})'.format(os.path.basename(case), year), @@ -4517,16 +4519,19 @@ def map_h2_capacity( cap_h2turbine.plot( ax=ax[0,0], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Turbines [kT/day]'}}) + label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fmt = '{:.1f}', fontsize=6) ### Electrolyzers if not cap_h2prod.empty: cap_h2prod.plot( ax=ax[0,1], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Production [kT/day]'}}) + label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fmt = '{:.1f}', fontsize=6) ### Storage if not cap_h2prod.empty: cap_storage.plot( ax=ax[1,0], column='h2_storage', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Storage [kT]'}}) + label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fmt = '{:.1f}', fontsize=6) ### Pipelines if not h2_trans_cap.empty: for i,row in h2_trans_cap.iterrows(): From 4c64cdcd554afda3d0042636dc75f12842359d80 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 13:40:50 -0600 Subject: [PATCH 10/58] rain -> tempo in reedsplots --- reeds/reedsplots.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index a21b4f723..2cc61f72d 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1093,7 +1093,7 @@ def plot_diff_maps( def plot_trans_vsc( case, year=2050, wscale=0.4, alpha=1.0, miles=300, - cmap=cmocean.cm.rain, scale=True, title=True, + cmap=cmocean.cm.tempo, scale=True, title=True, convertermax=None, f=None, ax=None, ): @@ -1188,7 +1188,7 @@ def plot_trans_vsc( def plot_transmission_utilization( case, year=2050, plottype='mean', network='trans', - wscale=0.0004, alpha=1.0, cmap=cmocean.cm.rain, + wscale=0.0004, alpha=1.0, cmap=cmocean.cm.tempo, extent='modeled', f=None, ax=None, thicklevel='transreg', @@ -2715,7 +2715,7 @@ def map_capacity_techs( techs='aggregation', ncols=4, vmax='shared', - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, ): """ techs: list of technologies to plot, or 'aggregation' to plot all aggregated technologies @@ -3456,7 +3456,7 @@ def map_hybrid_pv_wind( case, val='site_cap', year=2050, tech=None, vmax=None, markersize=10.75, #stretch=1.2, - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, f=None, ax=None, figsize=(6,6), dpi=None, ): """ @@ -3464,7 +3464,7 @@ def map_hybrid_pv_wind( cmap: Suggestions: val=site_cap, tech=wind-ons: plt.cm.Blues val=site_cap, tech=upv: plt.cm.Oranges - val=(site_hybridization,site_spurcap), tech=None: cmocean.cm.rain + val=(site_hybridization,site_spurcap), tech=None: cmocean.cm.tempo val=(site_pv_fraction,site_gir), tech=either: plt.cm.turbo or mpl.colors.LinearSegmentedColormap.from_list 'turboclip', [plt.cm.turbo(c) for c in np.linspace(0.1,0.91,101)]) @@ -4343,7 +4343,7 @@ def plot_ra_metrics_bylevel( def map_neue( case, year=2050, iteration='last', samples=None, - vmax=10., cmap=cmocean.cm.rain, label=True, + vmax=10., cmap=cmocean.cm.tempo, label=True, over_vmax_mapcolor=None, over_threshold_textcolor='C3', highlight_over_threshold=True, @@ -4433,7 +4433,7 @@ def map_neue( def map_h2_capacity( case, year=2050, wscale_h2=10, figheight=6, pipescale=0.1, legend_kwds={'shrink':0.6, 'pad':0, 'orientation':'horizontal', 'aspect':12}, - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, ): """ H2 turbines, production (Electrolyzer/SMR), pipelines, and storage @@ -6124,7 +6124,7 @@ def map_outage_days( techs=['gas-cc', 'gas-ct', 'coaloldscr', 'nuclear', 'hyded'], outage_type='forced', aggfunc='max', - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, vmin=0, vmax='auto', fontsize=4, @@ -6325,7 +6325,7 @@ def map_stressors( """ ### Plot setup cmaps = { - 'load': cmocean.cm.rain, + 'load': cmocean.cm.tempo, # 'load': plt.cm.turbo, # 'wind-ons': cmocean.cm.ice_r, 'wind-ons': cmocean.cm.ice, @@ -6899,7 +6899,7 @@ def map_output_byyear( return f, ax, dictplot -def map_prm(case, tmin=2023, cmap=cmocean.cm.rain, scale=3, fontsize=7, vmax=None): +def map_prm(case, tmin=2023, cmap=cmocean.cm.tempo, scale=3, fontsize=7, vmax=None): dfmap = reeds.io.get_dfmap(case) sw = reeds.io.get_switches(case) ### Get final iterations From 4be54cac296f208e5c1cdbd42a63f372d66197d0 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 17:55:06 -0600 Subject: [PATCH 11/58] Re-attach dropped CRS --- reeds/reedsplots.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 2cc61f72d..240f7024e 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -6256,7 +6256,8 @@ def get_cf_map(case, tech='wind-ons', timestamp=None, recf=None, crs='EPSG:5070' dfsc['i'] = tech + '_' + dfsc['class'].astype(str) sitemap = reeds.io.get_sitemap(offshore=(True if tech == 'wind-ofs' else False)) dfsc['geometry'] = dfsc.index.map(sitemap.geometry) - dfsc = gpd.GeoDataFrame(dfsc).to_crs(crs) + dfsc = gpd.GeoDataFrame(dfsc, geometry='geometry', crs=sitemap.crs) + dfsc = dfsc.to_crs(crs) dfsc['cf'] = dfsc[['i','r']].merge(cf.rename('cf'), on=['i','r'], how='left').cf.values ## Convert to polygons From 8db69e6db012c7d2971dd2b7331006a2b0069ef8 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 17:56:09 -0600 Subject: [PATCH 12/58] rain -> tempo in plots.py --- reeds/plots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reeds/plots.py b/reeds/plots.py index 37b9654e3..e216e8ee2 100644 --- a/reeds/plots.py +++ b/reeds/plots.py @@ -1040,7 +1040,7 @@ def draw_screen_poly(poly, m, def plotusascattermap( dfplot, colorcolumn=None, sizecolumn=None, filterdict=None, sort=True, - basemap=None, cmap=cmocean.cm.rain, mappath=None, + basemap=None, cmap=cmocean.cm.tempo, mappath=None, facecolor='w', edgecolor='w', latlonlabels=None, markersize=None, marker='o', figsize=(10,7.5), dpi=None, zrange=None, colors=None, maptype='scatter', contourlevels=100, @@ -2113,7 +2113,7 @@ def plot_region_bars( def map_years_months( - dfzones, dfdata, cmap=cmocean.cm.rain, aggfunc='mean', + dfzones, dfdata, cmap=cmocean.cm.tempo, aggfunc='mean', colwidth=1.2, rowheight=1.0, simplify=10000, mapbuffer=1.08, vmin=0., vmax=None, outline=0.25, labels=True, **colorbarkwargs, From 8bb8c1292021ef3521f03b6051cf2f7e0ee142df Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 17:56:33 -0600 Subject: [PATCH 13/58] rain -> tempo in single_case_plots.py --- postprocessing/single_case_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postprocessing/single_case_plots.py b/postprocessing/single_case_plots.py index 63c73ff98..3e5015aaa 100644 --- a/postprocessing/single_case_plots.py +++ b/postprocessing/single_case_plots.py @@ -29,7 +29,7 @@ ## the position of the annotations crs = 'ESRI:102008' ### General purpose -cmap = cmocean.cm.rain +cmap = cmocean.cm.tempo ### For VRE siting & transmission maps transalpha = 0.25 transcolor = 'k' From b304d0b8d634283b80286e14cc355ecf60f46634 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 18:04:41 -0600 Subject: [PATCH 14/58] Adjust labels in map_outage_days --- reeds/reedsplots.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 240f7024e..91f4e8daa 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -6182,14 +6182,20 @@ def map_outage_days( dfplot.plot(ax=_ax, column='outage_pct', cmap=cmap, vmin=vmin, vmax=vmax) ## Data values if fontsize: + text_artists = [] for r, row in dfplot.sort_values('outage_pct').iterrows(): - _ax.annotate( - f"{row.outage_pct:.0f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', - fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=1.4, foreground='w', alpha=0.7)], + text_artists.append( + _ax.annotate( + f"{row.outage_pct:.0f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', c='k', + fontsize=fontsize, + path_effects=[pe.withStroke(linewidth=1.4, foreground='w', alpha=0.7)], + ) ) + adjust_text(text_artists, ax=_ax, + avoid_self=False, ensure_inside_axes=True, + ) _ax.axis('off') ## Formatting if date == dates[0]: From 9e286b85b8ef0d7517ebbcbaffcfade015f5aea2 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 18:05:06 -0600 Subject: [PATCH 15/58] rain -> tempo in map_output_byyear --- reeds/reedsplots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 91f4e8daa..391d6d6af 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -6834,7 +6834,7 @@ def map_output_byyear( oneaxis='columns', yearaxis='rows', mapscale=4, - cmap=cmocean.cm.rain, + cmap=cmocean.cm.tempo, vscale=1, vmin=None, vmax=None, From 58293f8e2e87bda31066225f6ab1400fadb61107 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 18:06:02 -0600 Subject: [PATCH 16/58] Remove slashes from savenames --- reeds/resource_adequacy/diagnostic_plots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index 3e8f87e19..b47e6fbec 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -987,7 +987,7 @@ def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): plottechs = failsum.loc[failsum != 0].index.get_level_values('i').unique() for tech in plottechs: - savename = f"hourly_failure_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" + savename = f"hourly_failure_rate-year,month-{aggfunc}-{tech.replace('-','').replace('/','')}-{sw['t']}" plt.close() f, ax = plots.map_years_months( dfzones=dfzones, dfdata=failrate[tech], @@ -1008,7 +1008,7 @@ def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): ) repairrate.index = dfs['pras_system']['genrepairrate'].index for tech in plottechs: - savename = f"hourly_repair_rate-year,month-{aggfunc}-{tech.replace('-','')}-{sw['t']}" + savename = f"hourly_repair_rate-year,month-{aggfunc}-{tech.replace('-','').replace('/','')}-{sw['t']}" plt.close() f, ax = plots.map_years_months( dfzones=dfzones, dfdata=repairrate[tech], From a8a1777f570a4a2ad7aac74df09c52f857ccb223 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 20 Aug 2026 18:06:26 -0600 Subject: [PATCH 17/58] rain -> tempo in diagnostic_plots.py --- reeds/resource_adequacy/diagnostic_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index b47e6fbec..5f04b93cc 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -426,7 +426,7 @@ def map_dropped_load(sw, dfs, level='r'): ### Background dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) ### Data - dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.rain) + dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.tempo) for r, row in dfplot.iterrows(): if row.val > 0: ax.annotate( From 2860563e5062f57bfcd6becbff4c80ec5f4fedad Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 12:06:22 -0600 Subject: [PATCH 18/58] Add opt_single_decimal to label_region_value --- reeds/reedsplots.py | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 391d6d6af..48bf24792 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1029,7 +1029,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fmt = '{:.1f}', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol, fontsize=6) ###### Plot the comp elif plot == 'comp': @@ -1041,7 +1041,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fmt = '{:.1f}', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol, fontsize=6) ###### Plot the pct diff elif plot in ['diff','pctdiff','pct_diff','diffpct','diff_pct','pct']: @@ -1061,7 +1061,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_pctdiff', cmap=cmap, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fmt = '{:.1f}', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fontsize=6) ###### Plot the absolute diff elif plot in ['absdiff', 'abs_diff', 'diffabs', 'diff_abs']: @@ -1081,7 +1081,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_diff', cmap=plt.cm.RdBu_r, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_diff', fmt = '{:.1f}', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol+'_diff', fontsize=6) ### Finish and return # ax.set_title(title, y=0.95) @@ -1375,7 +1375,8 @@ def map_net_imports( legend=False, vmin=-vmax[year], vmax=vmax[year], ) - label_region_value(df, ax=ax[coords[year]], column='net_import', fmt = '{:+.0f}', fontsize=6) + label_region_value(df, ax=ax[coords[year]], column='net_import', + opt_single_decimal=False, fmt = '{:+.0f}', fontsize=6) ## Formatting ax[coords[year]].set_title(year, y=0.9) if vlim != 'shared': @@ -2769,7 +2770,7 @@ def map_capacity_techs( 'label': '{} [GW]'.format(tech), } ) - label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fmt = '{:.1f}', fontsize=6) + label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fontsize=6) ax[coords[tech]].axis('off') ax[0,0].set_title( '{} ({})'.format(os.path.basename(case), year), @@ -4519,19 +4520,19 @@ def map_h2_capacity( cap_h2turbine.plot( ax=ax[0,0], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Turbines [kT/day]'}}) - label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fmt = '{:.1f}', fontsize=6) + label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fontsize=6) ### Electrolyzers if not cap_h2prod.empty: cap_h2prod.plot( ax=ax[0,1], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Production [kT/day]'}}) - label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fmt = '{:.1f}', fontsize=6) + label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fontsize=6) ### Storage if not cap_h2prod.empty: cap_storage.plot( ax=ax[1,0], column='h2_storage', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Storage [kT]'}}) - label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fmt = '{:.1f}', fontsize=6) + label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fontsize=6) ### Pipelines if not h2_trans_cap.empty: for i,row in h2_trans_cap.iterrows(): @@ -6272,7 +6273,16 @@ def get_cf_map(case, tech='wind-ons', timestamp=None, recf=None, crs='EPSG:5070' return dfsc -def label_region_value(df, ax, column, fmt='{:.2f}', color='k', fontsize=8, **kwargs): +def label_region_value( + df, + ax, + column, + opt_single_decimal=True, + fmt='{:.0f}', + color='k', + fontsize=8, + **kwargs +): """kwargs are passed to patheffects.withStroke()""" pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':1}, **kwargs} text_artists = [] @@ -6280,6 +6290,9 @@ def label_region_value(df, ax, column, fmt='{:.2f}', color='k', fontsize=8, **kw value = row.get(column, np.nan) if not np.isfinite(value): continue + if opt_single_decimal: + decimals = 0 if abs(value) >= 1 else 1 + fmt = f"{{:.{decimals}f}}" text_artists.append( ax.annotate( fmt.format(value), @@ -6611,7 +6624,8 @@ def map_stressors( ax=ax[1,0], column='load_rank', cmap=cmaps['rank'], vmin=0, vmax=100, ) label_region_value( - df=dflevel, ax=ax[1,0], column='load_rank', fmt='{:.0f}%', + df=dflevel, ax=ax[1,0], column='load_rank', + opt_single_decimal=False, fmt='{:.0f}%', linewidth=2.0, alpha=0.8, ) ax[1,0].set_title('Demand', y=0.9) @@ -6626,7 +6640,8 @@ def map_stressors( ax=ax[1,col], column=f'{tech}_rank', cmap=cmaps['rank'], vmin=0, vmax=100, ) label_region_value( - df=dflevel, ax=ax[1,col], column=f'{tech}_rank', fmt='{:.0f}%', + df=dflevel, ax=ax[1,col], column=f'{tech}_rank', + opt_single_decimal=False, fmt='{:.0f}%', linewidth=2.0, alpha=0.8, ) ax[1,col].set_title(labels.get(tech,tech), y=0.9) @@ -6640,7 +6655,8 @@ def map_stressors( ax=ax[1,3], column='temperature_rank', cmap=cmaps['rank'], vmin=0, vmax=100, ) label_region_value( - df=dflevel, ax=ax[1,3], column='temperature_rank', fmt='{:.0f}%', + df=dflevel, ax=ax[1,3], column='temperature_rank', + opt_single_decimal=False, fmt='{:.0f}%', linewidth=2.0, alpha=0.8, ) ax[1,3].set_title('Temperature', y=0.9) @@ -6658,7 +6674,8 @@ def map_stressors( ) dflevel[f'outage_{tech}'] = outage_region.loc[(y,m,d), tech] label_region_value( - df=dflevel, ax=ax[2,col], column=f'outage_{tech}', fmt='{:.0f}%', + df=dflevel, ax=ax[2,col], column=f'outage_{tech}', + opt_single_decimal=False, fmt='{:.0f}%', linewidth=2.0, alpha=0.8, ) ax[2,col].set_title(label, y=0.9) From a54b9740d18edb4db751fd22fc362915e9abfc08 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 13:48:33 -0600 Subject: [PATCH 19/58] Use opt_single_decimal in compare_cases.py capacity maps --- postprocessing/compare_cases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postprocessing/compare_cases.py b/postprocessing/compare_cases.py index cac0d1efa..e2e641a96 100644 --- a/postprocessing/compare_cases.py +++ b/postprocessing/compare_cases.py @@ -2683,7 +2683,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): ax=ax[coords[case]], column='GW', cmap=cmap, vmin=vmin, vmax=vmax, legend=False, ) - reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fmt = '{:.0f}', fontsize=6) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=6) ## Legend if coords[case] == legendcoords: plots.addcolorbarhist( @@ -2755,7 +2755,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): vmax=(absmax if case == basecase else diffmax), legend=False, ) - reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fmt = ('{:.0f}' if case == basecase else '{:+.0f}'), fontsize=6) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=6) ## Difference legend if coords[case] == legendcoords: plots.addcolorbarhist( From e495a886936101f5b5b624a2565ecda52b2df1b8 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 13:49:01 -0600 Subject: [PATCH 20/58] Add sub-state regional outlines to plot_diff_maps --- reeds/reedsplots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 48bf24792..fac917495 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1085,6 +1085,7 @@ def plot_diff_maps( ### Finish and return # ax.set_title(title, y=0.95) + dfba.plot(ax=ax, edgecolor='k', facecolor='none', lw=0.1) dfstates.plot(ax=ax, edgecolor='k', facecolor='none', lw=0.25) ax.axis('off') From d8c31e7edec28a4ec9814e6e6f1d0dfe56fe5377 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 13:49:26 -0600 Subject: [PATCH 21/58] Decrease map_capacity_techs label fontsize --- reeds/reedsplots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index fac917495..273462085 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -2771,7 +2771,7 @@ def map_capacity_techs( 'label': '{} [GW]'.format(tech), } ) - label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fontsize=6) + label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fontsize=5) ax[coords[tech]].axis('off') ax[0,0].set_title( '{} ({})'.format(os.path.basename(case), year), From 8e2475bff0f1818c014a488475f2474000f41be4 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 14:21:09 -0600 Subject: [PATCH 22/58] Decrease multi-case map capacity tech label fontsize --- postprocessing/compare_cases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postprocessing/compare_cases.py b/postprocessing/compare_cases.py index e2e641a96..5efd7dd0d 100644 --- a/postprocessing/compare_cases.py +++ b/postprocessing/compare_cases.py @@ -2683,7 +2683,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): ax=ax[coords[case]], column='GW', cmap=cmap, vmin=vmin, vmax=vmax, legend=False, ) - reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=6) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=5) ## Legend if coords[case] == legendcoords: plots.addcolorbarhist( @@ -2755,7 +2755,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): vmax=(absmax if case == basecase else diffmax), legend=False, ) - reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=6) + reedsplots.label_region_value(dfplot, ax=ax[coords[case]], column="GW", fontsize=5) ## Difference legend if coords[case] == legendcoords: plots.addcolorbarhist( From 5078bd4702549df30be3aa8b989230c6686422cb Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 16:18:42 -0600 Subject: [PATCH 23/58] Update reedsplot.py label format --- reeds/reedsplots.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 273462085..7ce8a4109 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1029,7 +1029,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol, fontsize=5) ###### Plot the comp elif plot == 'comp': @@ -1041,7 +1041,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol, fontsize=5) ###### Plot the pct diff elif plot in ['diff','pctdiff','pct_diff','diffpct','diff_pct','pct']: @@ -1061,7 +1061,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_pctdiff', cmap=cmap, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fontsize=5) ###### Plot the absolute diff elif plot in ['absdiff', 'abs_diff', 'diffabs', 'diff_abs']: @@ -1081,7 +1081,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_diff', cmap=plt.cm.RdBu_r, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_diff', fontsize=6) + label_region_value(dfplot, ax=ax, column=valcol+'_diff', fontsize=5) ### Finish and return # ax.set_title(title, y=0.95) @@ -1377,7 +1377,7 @@ def map_net_imports( vmin=-vmax[year], vmax=vmax[year], ) label_region_value(df, ax=ax[coords[year]], column='net_import', - opt_single_decimal=False, fmt = '{:+.0f}', fontsize=6) + opt_single_decimal=False, fmt = '{:.0f}', fontsize=5) ## Formatting ax[coords[year]].set_title(year, y=0.9) if vlim != 'shared': @@ -4521,19 +4521,19 @@ def map_h2_capacity( cap_h2turbine.plot( ax=ax[0,0], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Turbines [kT/day]'}}) - label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fontsize=6) + label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fontsize=5) ### Electrolyzers if not cap_h2prod.empty: cap_h2prod.plot( ax=ax[0,1], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Production [kT/day]'}}) - label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fontsize=6) + label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fontsize=5) ### Storage if not cap_h2prod.empty: cap_storage.plot( ax=ax[1,0], column='h2_storage', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Storage [kT]'}}) - label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fontsize=6) + label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fontsize=5) ### Pipelines if not h2_trans_cap.empty: for i,row in h2_trans_cap.iterrows(): @@ -6285,14 +6285,14 @@ def label_region_value( **kwargs ): """kwargs are passed to patheffects.withStroke()""" - pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':1}, **kwargs} + pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':0.7}, **kwargs} text_artists = [] for r, row in df.iterrows(): value = row.get(column, np.nan) if not np.isfinite(value): continue if opt_single_decimal: - decimals = 0 if abs(value) >= 1 else 1 + decimals = 0 if ((abs(value) >= 1) or abs(value) < 0.05) else 1 fmt = f"{{:.{decimals}f}}" text_artists.append( ax.annotate( From 65252eee434db39885ecbe1d59b28a426b58f841 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 24 Aug 2026 16:19:23 -0600 Subject: [PATCH 24/58] Adjust labels in map_dropped_load --- reeds/resource_adequacy/diagnostic_plots.py | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index 5f04b93cc..bfa332694 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -10,6 +10,7 @@ from glob import glob import traceback import cmocean +from adjustText import adjust_text ### Local imports from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) @@ -427,17 +428,29 @@ def map_dropped_load(sw, dfs, level='r'): dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) ### Data dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.tempo) + text_artists = [] for r, row in dfplot.iterrows(): if row.val > 0: - ax.annotate( - f'{row.val:,.0f} {units[metric,agg][0]}', - (row.centroid_x, row.centroid_y), - color='r', ha='center', va='top', fontsize=6, weight='bold') + text_artists.append( + ax.annotate( + f'{row.val:,.0f} {units[metric,agg][0]}', + (row.centroid_x, row.centroid_y), + color='r', ha='center', va='top', fontsize=6, weight='bold') + ) + adjust_text(text_artists, ax=ax, + avoid_self=False, ensure_inside_axes=True, + ) ### Formatting if level in ['r','rb','ba']: + text_artists = [] for r, row in dfba.iterrows(): - ax.annotate(r, (row.centroid_x, row.centroid_y), - ha='center', va='bottom', fontsize=6, color='C7') + text_artists.append( + ax.annotate(r, (row.centroid_x, row.centroid_y), + ha='center', va='bottom', fontsize=6, color='C7') + ) + adjust_text(text_artists, ax=ax, + avoid_self=False, ensure_inside_axes=True, + ) ax.axis('off') if savefig: plt.savefig(os.path.join(sw['savepath'],savename)) From 2bdf21503516c04d615a3d5c8afb12785f929557 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 25 Aug 2026 11:17:06 -0600 Subject: [PATCH 25/58] Avoid label overlap in maps.py --- docs/source/plotting_scripts/maps.py | 42 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/source/plotting_scripts/maps.py b/docs/source/plotting_scripts/maps.py index 7a8f0d1c3..db2d511dd 100644 --- a/docs/source/plotting_scripts/maps.py +++ b/docs/source/plotting_scripts/maps.py @@ -3,6 +3,7 @@ #%% Imports import os import sys +from adjustText import adjust_text import shapely import datetime import numpy as np @@ -206,7 +207,10 @@ } draw_lakes = True draw_counties = False -label_regions = {'hurdlereg': False} +label_regions = { + 'hurdlereg': False + 'r': False, + } for level in dfmap: dfregion = dfmap[level].copy() @@ -230,6 +234,7 @@ dfcounty.plot(ax=ax, facecolor='none', edgecolor='C7', lw=0.02, zorder=1e6) if draw_lakes: greatlakes.plot(ax=ax, edgecolor='#2CA8E7', facecolor='#D3EFFA', lw=0.2, zorder=-1) + text_artists = [] for r, row in dfregion.iterrows(): dfregion.loc[[r]].plot(ax=ax, color=colors[r], alpha=alpha_region, lw=0, zorder=1) if label_regions.get(level, True): @@ -240,15 +245,17 @@ for i, (c, a) in enumerate([('k',1), (colors[r], 0.6)]): if i == 1 and level != 'r': continue - ax.annotate( - (r if level == 'r' else r.replace('_','\n')), - (x, y), - ha='center', va='center', weight='bold', - size={'r':7, 'hurdlereg':7, 'st':10}.get(level,11), - color=c, zorder=1e11+i, alpha=a, - path_effects=( - [pe.withStroke(linewidth=1.5, foreground='w', alpha=(1 if i == 0 else 0))] - ), + text_artists.append( + ax.annotate( + (r if level == 'r' else r.replace('_','\n')), + (x, y), + ha='center', va='center', weight='bold', + size={'r':7, 'hurdlereg':7, 'st':10}.get(level,11), + color=c, zorder=1e11+i, alpha=a, + path_effects=( + [pe.withStroke(linewidth=1.5, foreground='w', alpha=(1 if i == 0 else 0))] + ), + ) ) if label_zones.get(level, True): for r, row in dfmap['r'].iterrows(): @@ -256,13 +263,16 @@ np.array([row.geometry.centroid.x, row.geometry.centroid.y]) + np.array(offset.get('r', {}).get(r, (0,0))) ) - ax.annotate( - r, - (x, y), - ha='center', va='center', size=6, weight='normal', - color='C7', zorder=1e10, - path_effects=[pe.withStroke(linewidth=0.7, foreground='w', alpha=1)] + text_artists.append( + ax.annotate( + r, + (x, y), + ha='center', va='center', size=6, weight='normal', + color='C7', zorder=1e10, + path_effects=[pe.withStroke(linewidth=0.7, foreground='w', alpha=1)] + ) ) + adjust_text(text_artists, ax=ax, avoid_self=False, ensure_inside_axes=True) ax.axis('off') savename = ( From 330e5b807c688ff147c34678347522de4dae6551 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 25 Aug 2026 11:27:10 -0600 Subject: [PATCH 26/58] Make cf map NaNs gray and unlabeled --- reeds/input_processing/hourly_plots.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py index f7c45d794..ffc87e94a 100644 --- a/reeds/input_processing/hourly_plots.py +++ b/reeds/input_processing/hourly_plots.py @@ -352,12 +352,14 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: dfdiffs[level].plot( ax=ax[coords[level]], column='cf_diff', cmap=cmaps['cf_diff'], vmin=vm[tech]['cf_diff'][0], vmax=vm[tech]['cf_diff'][1], - lw=0, legend=False, + lw=0, legend=False, missing_kwds={"color": "lightgrey"}, ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) ## Text differences text_artists = [] for r, row in (dfdiffs[level].assign(val=dfdiffs[level].cf_diff.abs()).sort_values('val')).iterrows(): + if np.isnan(row.cf_diff): + continue decimals = 0 if abs(row.cf_diff) >= 1 else 1 text_artists.append( ax[coords[level]].annotate( From 40d852823566c4ae621a4ff0b69e1ceef34e2613 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 25 Aug 2026 13:54:01 -0600 Subject: [PATCH 27/58] Seperate region-colored and black labels for different levels in docs maps --- docs/source/plotting_scripts/maps.py | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/source/plotting_scripts/maps.py b/docs/source/plotting_scripts/maps.py index db2d511dd..bc1a68e6e 100644 --- a/docs/source/plotting_scripts/maps.py +++ b/docs/source/plotting_scripts/maps.py @@ -207,10 +207,7 @@ } draw_lakes = True draw_counties = False -label_regions = { - 'hurdlereg': False - 'r': False, - } +label_regions = {'hurdlereg': False} for level in dfmap: dfregion = dfmap[level].copy() @@ -242,21 +239,37 @@ np.array([row.geometry.centroid.x, row.geometry.centroid.y]) + np.array(offset.get(level, {}).get(r, (0,0))) ) - for i, (c, a) in enumerate([('k',1), (colors[r], 0.6)]): - if i == 1 and level != 'r': - continue + if level == 'r': + # adds region-colored label for r + text_artists.append( + ax.annotate( + r, + (x, y), + ha='center', va='center', weight='bold', + size={'r':7, 'hurdlereg':7, 'st':10}.get(level,11), + color=colors[r], zorder=1e11, alpha=1, + path_effects=( + [pe.withStroke(linewidth=1.5, foreground='w', alpha=1)] + ), + ) + ) + else: + # adds black label for other levels text_artists.append( ax.annotate( (r if level == 'r' else r.replace('_','\n')), (x, y), ha='center', va='center', weight='bold', size={'r':7, 'hurdlereg':7, 'st':10}.get(level,11), - color=c, zorder=1e11+i, alpha=a, + color='k', zorder=1e11, alpha=1, path_effects=( - [pe.withStroke(linewidth=1.5, foreground='w', alpha=(1 if i == 0 else 0))] + [pe.withStroke(linewidth=1.5, foreground='w', alpha=1)] ), ) ) + adjust_text(text_artists, ax=ax, avoid_self=False, ensure_inside_axes=True) + + text_artists = [] if label_zones.get(level, True): for r, row in dfmap['r'].iterrows(): x, y = ( @@ -272,7 +285,7 @@ path_effects=[pe.withStroke(linewidth=0.7, foreground='w', alpha=1)] ) ) - adjust_text(text_artists, ax=ax, avoid_self=False, ensure_inside_axes=True) + adjust_text(text_artists, ax=ax, avoid_self=False, ensure_inside_axes=True) ax.axis('off') savename = ( From 2507aa0f7255478c50785cb5d1b3892465e7811d Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 10 Sep 2026 16:45:19 -0600 Subject: [PATCH 28/58] cmap added as global in outage_rates.py and diagnostic_plots.py --- reeds/input_processing/outage_rates.py | 3 ++- reeds/plots.py | 4 ++-- reeds/resource_adequacy/diagnostic_plots.py | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/reeds/input_processing/outage_rates.py b/reeds/input_processing/outage_rates.py index 6d36110a7..d4919ebbf 100644 --- a/reeds/input_processing/outage_rates.py +++ b/reeds/input_processing/outage_rates.py @@ -19,6 +19,7 @@ tz_out = 'Etc/GMT+6' temp_min = -50 temp_max = 60 +cmap = cmocean.cm.tempo ## Only use during_quarters for techs without a monthly scheduled outage rate during_quarters = ['spring', 'fall'] ## Cap the extrapolation of forced outage rates at high/low temperatures to 0.4 because @@ -383,7 +384,7 @@ def plot_outage_forced( plt.close() f, ax = reeds.plots.map_years_months( - dfzones=dfzones, dfdata=dfdata, aggfunc=aggfunc, + dfzones=dfzones, dfdata=dfdata, cmap=cmap, aggfunc=aggfunc, title=f"Monthly {aggfunc}\nforced outage rate,\n{nicelabels.get(pm,pm)} [%]", ) plt.savefig(os.path.join(figpath, f'FOR_monthly-{aggfunc}-{pm}.png')) diff --git a/reeds/plots.py b/reeds/plots.py index e216e8ee2..37b9654e3 100644 --- a/reeds/plots.py +++ b/reeds/plots.py @@ -1040,7 +1040,7 @@ def draw_screen_poly(poly, m, def plotusascattermap( dfplot, colorcolumn=None, sizecolumn=None, filterdict=None, sort=True, - basemap=None, cmap=cmocean.cm.tempo, mappath=None, + basemap=None, cmap=cmocean.cm.rain, mappath=None, facecolor='w', edgecolor='w', latlonlabels=None, markersize=None, marker='o', figsize=(10,7.5), dpi=None, zrange=None, colors=None, maptype='scatter', contourlevels=100, @@ -2113,7 +2113,7 @@ def plot_region_bars( def map_years_months( - dfzones, dfdata, cmap=cmocean.cm.tempo, aggfunc='mean', + dfzones, dfdata, cmap=cmocean.cm.rain, aggfunc='mean', colwidth=1.2, rowheight=1.0, simplify=10000, mapbuffer=1.08, vmin=0., vmax=None, outline=0.25, labels=True, **colorbarkwargs, diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index bfa332694..7f45f4ab4 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -24,7 +24,7 @@ dpi = None interactive = False savefig = True - +cmap = cmocean.cm.tempo #%%### Functions def delete_temporary_files(sw): @@ -427,7 +427,7 @@ def map_dropped_load(sw, dfs, level='r'): ### Background dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) ### Data - dfplot.plot(ax=ax, column='val', cmap=cmocean.cm.tempo) + dfplot.plot(ax=ax, column='val', cmap=cmap) text_artists = [] for r, row in dfplot.iterrows(): if row.val > 0: @@ -1003,7 +1003,7 @@ def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): savename = f"hourly_failure_rate-year,month-{aggfunc}-{tech.replace('-','').replace('/','')}-{sw['t']}" plt.close() f, ax = plots.map_years_months( - dfzones=dfzones, dfdata=failrate[tech], + dfzones=dfzones, dfdata=failrate[tech], cmap=cmap, title=f"Monthly {aggfunc}\nhourly failure rate,\n{tech} [%]", ) ## Save it @@ -1024,7 +1024,7 @@ def map_pras_failure_rate(sw, dfs, aggfunc='mean', repair=False): savename = f"hourly_repair_rate-year,month-{aggfunc}-{tech.replace('-','').replace('/','')}-{sw['t']}" plt.close() f, ax = plots.map_years_months( - dfzones=dfzones, dfdata=repairrate[tech], + dfzones=dfzones, dfdata=repairrate[tech], cmap=cmap, title=f"Monthly {aggfunc}\nhourly repair rate,\n{tech} [%]", ) ## Save it @@ -1367,12 +1367,12 @@ def main(sw, debug=False): debug = args.debug # #%%### Inputs for debugging - # reeds_path = reeds.io.reeds_path - # casedir = os.path.join(reeds_path, 'runs', 'v20260715_stressM3_MultiMetricRA') - # t = 2050 - # interactive = True - # iteration = 0 - # debug = True + reeds_path = reeds.io.reeds_path + casedir = os.path.join(reeds_path, 'runs', 'v20260820_USA_defaults') + t = 2035 + interactive = True + iteration = 0 + debug = True #%%### INPUTS ### Switches From f72ca9af9d7f92c83863fe0fa64c5b8e351c733c Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 10 Sep 2026 16:53:33 -0600 Subject: [PATCH 29/58] cmap updated with global in single_case_plots.py and diagnostic_plots.py --- postprocessing/single_case_plots.py | 9 +++++---- reeds/reedsplots.py | 20 ++++++++++---------- reeds/resource_adequacy/diagnostic_plots.py | 1 + 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/postprocessing/single_case_plots.py b/postprocessing/single_case_plots.py index 3e5015aaa..fa059cd37 100644 --- a/postprocessing/single_case_plots.py +++ b/postprocessing/single_case_plots.py @@ -255,7 +255,7 @@ plt.close() f,ax = reedsplots.plot_trans_vsc( case=case, year=year, wscale=wscale_straight*1e3, - alpha=1.0, miles=300, + alpha=1.0, miles=300, cmap=cmap, ) savename = f'map_translines_vsc-{year}.png' if write and (f is not None): @@ -276,7 +276,7 @@ for vmax in ['each', 'shared']: try: f,ax = reedsplots.map_capacity_techs( - case, year=year, ncols=ncols, vmax=vmax, + case, year=year, ncols=ncols, vmax=vmax, cmap=cmap, ) savename = f'map_capacity-{year}-{vmax}.png' if write: @@ -634,7 +634,7 @@ try: for y in [y for y in years if y >= 2025]: plt.close() - f, ax, neue, _iteration = reedsplots.map_neue(case=case, year=y) + f, ax, neue, _iteration = reedsplots.map_neue(case=case, year=y, cmap=cmap) savename = f"map_PRAS_neue-{y}i{_iteration}.png" if write: plt.savefig(os.path.join(savepath, savename)) @@ -848,7 +848,7 @@ #%% PRM if iterating if int(sw.GSw_PRM_StressIterateMax) and int(sw.GSw_PRM_UpdateMethod): try: - f, ax, prm_final = reedsplots.map_prm(case) + f, ax, prm_final = reedsplots.map_prm(case, cmap=cmap) savename = 'map_prm.png' if write: plt.savefig(os.path.join(savepath, savename)) @@ -916,6 +916,7 @@ years=[year], vscale=1e-3, vmin=0, + cmap=cmap, title='Sited demand [GW]', ) savename = f'map_loadsite-{year}.png' diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 7ce8a4109..ae8d4d967 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -1094,7 +1094,7 @@ def plot_diff_maps( def plot_trans_vsc( case, year=2050, wscale=0.4, alpha=1.0, miles=300, - cmap=cmocean.cm.tempo, scale=True, title=True, + cmap=cmocean.cm.rain, scale=True, title=True, convertermax=None, f=None, ax=None, ): @@ -1189,7 +1189,7 @@ def plot_trans_vsc( def plot_transmission_utilization( case, year=2050, plottype='mean', network='trans', - wscale=0.0004, alpha=1.0, cmap=cmocean.cm.tempo, + wscale=0.0004, alpha=1.0, cmap=cmocean.cm.rain, extent='modeled', f=None, ax=None, thicklevel='transreg', @@ -2717,7 +2717,7 @@ def map_capacity_techs( techs='aggregation', ncols=4, vmax='shared', - cmap=cmocean.cm.tempo, + cmap=cmocean.cm.rain, ): """ techs: list of technologies to plot, or 'aggregation' to plot all aggregated technologies @@ -3458,7 +3458,7 @@ def map_hybrid_pv_wind( case, val='site_cap', year=2050, tech=None, vmax=None, markersize=10.75, #stretch=1.2, - cmap=cmocean.cm.tempo, + cmap=cmocean.cm.rain, f=None, ax=None, figsize=(6,6), dpi=None, ): """ @@ -3466,7 +3466,7 @@ def map_hybrid_pv_wind( cmap: Suggestions: val=site_cap, tech=wind-ons: plt.cm.Blues val=site_cap, tech=upv: plt.cm.Oranges - val=(site_hybridization,site_spurcap), tech=None: cmocean.cm.tempo + val=(site_hybridization,site_spurcap), tech=None: cmocean.cm.rain val=(site_pv_fraction,site_gir), tech=either: plt.cm.turbo or mpl.colors.LinearSegmentedColormap.from_list 'turboclip', [plt.cm.turbo(c) for c in np.linspace(0.1,0.91,101)]) @@ -4345,7 +4345,7 @@ def plot_ra_metrics_bylevel( def map_neue( case, year=2050, iteration='last', samples=None, - vmax=10., cmap=cmocean.cm.tempo, label=True, + vmax=10., cmap=cmocean.cm.rain, label=True, over_vmax_mapcolor=None, over_threshold_textcolor='C3', highlight_over_threshold=True, @@ -4435,7 +4435,7 @@ def map_neue( def map_h2_capacity( case, year=2050, wscale_h2=10, figheight=6, pipescale=0.1, legend_kwds={'shrink':0.6, 'pad':0, 'orientation':'horizontal', 'aspect':12}, - cmap=cmocean.cm.tempo, + cmap=cmocean.cm.rain, ): """ H2 turbines, production (Electrolyzer/SMR), pipelines, and storage @@ -6126,7 +6126,7 @@ def map_outage_days( techs=['gas-cc', 'gas-ct', 'coaloldscr', 'nuclear', 'hyded'], outage_type='forced', aggfunc='max', - cmap=cmocean.cm.tempo, + cmap=cmocean.cm.rain, vmin=0, vmax='auto', fontsize=4, @@ -6852,7 +6852,7 @@ def map_output_byyear( oneaxis='columns', yearaxis='rows', mapscale=4, - cmap=cmocean.cm.tempo, + cmap=cmocean.cm.rain, vscale=1, vmin=None, vmax=None, @@ -6924,7 +6924,7 @@ def map_output_byyear( return f, ax, dictplot -def map_prm(case, tmin=2023, cmap=cmocean.cm.tempo, scale=3, fontsize=7, vmax=None): +def map_prm(case, tmin=2023, cmap=cmocean.cm.rain, scale=3, fontsize=7, vmax=None): dfmap = reeds.io.get_dfmap(case) sw = reeds.io.get_switches(case) ### Get final iterations diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index 7f45f4ab4..55f1ee8ca 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -1056,6 +1056,7 @@ def map_outagerate_new_stressperiods(sw, dfs): dates=dates, outage_type=outage_type, aggfunc=aggfunc, + cmap=cmap, vmax=vmax[outage_type], ) ## Save it From e3a9e349c38bf4869dca9effbdb34d271d1e6f8a Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Thu, 10 Sep 2026 16:55:33 -0600 Subject: [PATCH 30/58] Use global cmap in compare_cases.py --- postprocessing/compare_cases.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postprocessing/compare_cases.py b/postprocessing/compare_cases.py index 5efd7dd0d..b77faf09d 100644 --- a/postprocessing/compare_cases.py +++ b/postprocessing/compare_cases.py @@ -2596,7 +2596,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): year=lastyear, casebase=casebase, casecomp=casecomp, level=level, plot='base', f=f, ax=ax[0], - cmap=cmocean.cm.tempo, + cmap=cmap, ) ax[0].annotate( casebase_name, @@ -2607,7 +2607,7 @@ def two_bars(dfplot, basecase, colors, ax, col=0, ypad=0.02): year=lastyear, casebase=casebase, casecomp=casecomp, level=level, plot='comp', f=f, ax=ax[1], - cmap=cmocean.cm.tempo, + cmap=cmap, ) ax[1].annotate( casecomp_name, From 7efb320541773defa4b3d99f0c88e536bf26a204 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Fri, 11 Sep 2026 17:43:44 -0600 Subject: [PATCH 31/58] adjustText optional for core functionality --- environment.yml | 4 +- reeds/input_processing/hourly_plots.py | 45 ++---- reeds/reedsplots.py | 145 +++++++++----------- reeds/resource_adequacy/diagnostic_plots.py | 36 ++--- 4 files changed, 95 insertions(+), 135 deletions(-) diff --git a/environment.yml b/environment.yml index 0a1e1a0a7..003640058 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: reeds-at +name: reeds channels: - conda-forge - nodefaults @@ -25,9 +25,9 @@ dependencies: - scipy=1.18 - shapely=2.1 - tqdm=4.68 - - adjustText ## vvv The next packages are optional for default settings and may ## vvv be excluded if you're having trouble building the environment. + - adjustText=1.14 # for avoiding map label overlaps - fiona=1.10 # for interactive maps - folium=0.20 # for interactive maps - ipykernel=7.3 # for interactive python in VS Code diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py index ffc87e94a..5654c8b9a 100644 --- a/reeds/input_processing/hourly_plots.py +++ b/reeds/input_processing/hourly_plots.py @@ -12,9 +12,9 @@ from matplotlib import patheffects as pe import cmocean from pathlib import Path -from adjustText import adjust_text sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds +from reeds import reedsplots from reeds import plots plots.plotparams() @@ -355,23 +355,14 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: lw=0, legend=False, missing_kwds={"color": "lightgrey"}, ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) - ## Text differences - text_artists = [] - for r, row in (dfdiffs[level].assign(val=dfdiffs[level].cf_diff.abs()).sort_values('val')).iterrows(): - if np.isnan(row.cf_diff): - continue - decimals = 0 if abs(row.cf_diff) >= 1 else 1 - text_artists.append( - ax[coords[level]].annotate( - f"{row.cf_diff*100:+.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], - ) + dfmap[level]['cf_diff_pct'] = (dfdiffs[level].cf_diff * 100).values + reedsplots.label_region_value( + dfmap[level], + ax[coords[level]], + column='cf_diff_pct', + text_kwargs={'fontsize': {'r':5}.get(level,7)}, + pe_kwargs={'linewidth': 1.5, 'foreground': 'w', 'alpha': 0.5}, ) - adjust_text(text_artists, ax=ax[coords[level]], - avoid_self=False,ensure_inside_axes=True, - ) ## Colorbar plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=dfdiffs[level].cf_diff*100, nbins=51, @@ -497,21 +488,13 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: lw=0, legend=False, ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) - ## Text differences - text_artists = [] - for r, row in (dfdiffs[level].assign(val=dfdiffs[level][val].abs()).sort_values('val')).iterrows(): - decimals = 0 if abs(row[val]) >= 1 else 1 - text_artists.append( - ax[coords[level]].annotate( - f"{row[val]:+.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', fontsize={'r':5}.get(level,7), - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.5)], - ) + reedsplots.label_region_value( + dfmap[level], + ax[coords[level]], + column=val, + text_kwargs={'fontsize': {'r':5}.get(level,7)}, + pe_kwargs={'linewidth': 1.5, 'foreground': 'w', 'alpha': 0.5}, ) - adjust_text(text_artists, ax=ax[coords[level]], - avoid_self=False,ensure_inside_axes=True, - ) ## Colorbar plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=dfdiffs[level][val], nbins=51, diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index ae8d4d967..148cd8e5a 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -15,7 +15,6 @@ import geopandas as gpd import shapely import cmocean -from adjustText import adjust_text sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import reeds from reeds import plots @@ -1029,7 +1028,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fontsize=5) + label_region_value(dfplot, ax=ax, column=valcol, text_kwargs={'fontsize':5}) ###### Plot the comp elif plot == 'comp': @@ -1041,7 +1040,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol, cmap=cmap, legend=True, legend_kwds=legend_kwds, vmax=zmax) - label_region_value(dfplot, ax=ax, column=valcol, fontsize=5) + label_region_value(dfplot, ax=ax, column=valcol, text_kwargs={'fontsize':5}) ###### Plot the pct diff elif plot in ['diff','pctdiff','pct_diff','diffpct','diff_pct','pct']: @@ -1061,7 +1060,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_pctdiff', cmap=cmap, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', fontsize=5) + label_region_value(dfplot, ax=ax, column=valcol+'_pctdiff', text_kwargs={'fontsize':5}) ###### Plot the absolute diff elif plot in ['absdiff', 'abs_diff', 'diffabs', 'diff_abs']: @@ -1081,7 +1080,7 @@ def plot_diff_maps( dfplot.plot(ax=ax, column=valcol+'_diff', cmap=plt.cm.RdBu_r, legend=True, vmin=-zlim, vmax=+zlim, legend_kwds=legend_kwds) - label_region_value(dfplot, ax=ax, column=valcol+'_diff', fontsize=5) + label_region_value(dfplot, ax=ax, column=valcol+'_diff', text_kwargs={'fontsize':5}) ### Finish and return # ax.set_title(title, y=0.95) @@ -1377,7 +1376,7 @@ def map_net_imports( vmin=-vmax[year], vmax=vmax[year], ) label_region_value(df, ax=ax[coords[year]], column='net_import', - opt_single_decimal=False, fmt = '{:.0f}', fontsize=5) + opt_single_decimal=False, fmt = '{:.0f}', text_kwargs={'fontsize':5}) ## Formatting ax[coords[year]].set_title(year, y=0.9) if vlim != 'shared': @@ -2771,7 +2770,7 @@ def map_capacity_techs( 'label': '{} [GW]'.format(tech), } ) - label_region_value(dfplot, ax=ax[coords[tech]], column='GW', fontsize=5) + label_region_value(dfplot, ax=ax[coords[tech]], column='GW', text_kwargs={'fontsize':5}) ax[coords[tech]].axis('off') ax[0,0].set_title( '{} ({})'.format(os.path.basename(case), year), @@ -4394,31 +4393,28 @@ def map_neue( ## Labels # decimals = (0 if df.NEUE_ppm.max() >= 10 else 1) decimals = (0 if level in ['st','r'] else 1) - text_artists = [] + # label_region_value can't handle conditional formating, so keep as-is for r, row in df.sort_values('NEUE_ppm').iterrows(): if highlight_over_threshold: over_threshold = row.NEUE_ppm > neue_threshold else: over_threshold = False - text_artists.append( - ax[coords[level]].annotate( - f"{row.NEUE_ppm:.{decimals}f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', - c=(over_threshold_textcolor if over_threshold else 'k'), - weight=('bold' if over_threshold else 'normal'), - fontsize={'r':5}.get(level,7), - zorder=1e9, - path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], - ) + ax[coords[level]].annotate( + f"{row.NEUE_ppm:.{decimals}f}", + [row.centroid_x, row.centroid_y], + ha='center', va='center', + c=(over_threshold_textcolor if over_threshold else 'k'), + weight=('bold' if over_threshold else 'normal'), + fontsize={'r':5}.get(level,7), + zorder=1e9, + path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], ) + if over_threshold and (level == neue_threshold_level): ax[coords[level]].set_title( level, y=0.9, weight='bold', color=over_threshold_textcolor, ) - adjust_text(text_artists, ax=ax[coords[level]], - avoid_self=False, ensure_inside_axes=True, - ) + ### Formatting plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=df.NEUE_ppm, @@ -4521,19 +4517,19 @@ def map_h2_capacity( cap_h2turbine.plot( ax=ax[0,0], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Turbines [kT/day]'}}) - label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', fontsize=5) + label_region_value(cap_h2turbine, ax=ax[0,0], column='kTperday', text_kwargs={'fontsize':5}) ### Electrolyzers if not cap_h2prod.empty: cap_h2prod.plot( ax=ax[0,1], column='kTperday', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Production [kT/day]'}}) - label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', fontsize=5) + label_region_value(cap_h2prod, ax=ax[0,1], column='kTperday', text_kwargs={'fontsize':5}) ### Storage if not cap_h2prod.empty: cap_storage.plot( ax=ax[1,0], column='h2_storage', cmap=cmap, lw=0, vmin=0, legend=True, legend_kwds={**legend_kwds, **{'label':'Storage [kT]'}}) - label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', fontsize=5) + label_region_value(cap_storage, ax=ax[1,0], column='h2_storage', text_kwargs={'fontsize':5}) ### Pipelines if not h2_trans_cap.empty: for i,row in h2_trans_cap.iterrows(): @@ -5135,18 +5131,13 @@ def plot_seed_stressperiods( ax=ax[row,col], column='val', edgecolor='none', lw=0, cmap=cmap, alpha=alpha, vmin=0, vmax=1, ) - text_artists = [] - for i, _row in df.iterrows(): - text_artists.append( - ax[row,col].annotate( - _row.date, (_row.centroid_x, _row.centroid_y), - ha='center', va='center', color='k', fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], - ) - ) - adjust_text(text_artists, ax=ax[row,col], - avoid_self=False, ensure_inside_axes=True, - ) + label_region_value( + df, + ax=ax[row,col], + column='val', + text_kwargs={'fontsize': fontsize}, + pe_kwargs={'linewidth': pelinewidth, 'foreground': 'w', 'alpha': pealpha}, + ) ### Max load @@ -5177,18 +5168,13 @@ def plot_seed_stressperiods( ax=ax[row,col], column='val', edgecolor='none', lw=0, cmap=cmap, alpha=alpha, vmin=0, vmax=1, ) - text_artists = [] - for i, _row in df.iterrows(): - text_artists.append( - ax[row,col].annotate( - _row.date, (_row.centroid_x, _row.centroid_y), - ha='center', va='center', color='k', fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=pelinewidth, foreground='w', alpha=pealpha)], - ) - ) - adjust_text(text_artists, ax=ax[row,col], - avoid_self=False, ensure_inside_axes=True, - ) + label_region_value( + df, + ax=ax[row,col], + column='date', + text_kwargs={'fontsize': fontsize}, + pe_kwargs={'linewidth': pelinewidth, 'foreground': 'w', 'alpha': pealpha}, + ) ### Colorbar row, col = 0, 0 @@ -6184,20 +6170,14 @@ def map_outage_days( dfplot.plot(ax=_ax, column='outage_pct', cmap=cmap, vmin=vmin, vmax=vmax) ## Data values if fontsize: - text_artists = [] - for r, row in dfplot.sort_values('outage_pct').iterrows(): - text_artists.append( - _ax.annotate( - f"{row.outage_pct:.0f}", - [row.centroid_x, row.centroid_y], - ha='center', va='center', c='k', - fontsize=fontsize, - path_effects=[pe.withStroke(linewidth=1.4, foreground='w', alpha=0.7)], - ) - ) - adjust_text(text_artists, ax=_ax, - avoid_self=False, ensure_inside_axes=True, - ) + label_region_value( + df=dfplot, + ax=_ax, + column='outage_pct', + fmt="{:.0f}", + text_kwargs={'fontsize': fontsize}, + pe_kwargs={'linewidth': 1.4, 'foreground': 'w', 'alpha': 0.7}, + ) _ax.axis('off') ## Formatting if date == dates[0]: @@ -6280,30 +6260,35 @@ def label_region_value( column, opt_single_decimal=True, fmt='{:.0f}', - color='k', - fontsize=8, - **kwargs + text_kwargs={'ha':'center', 'va':'center', 'color':'k', 'fontsize':8}, + pe_kwargs={'linewidth':1.5, 'foreground':'w', 'alpha':0.7} ): - """kwargs are passed to patheffects.withStroke()""" - pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':0.7}, **kwargs} + """pe_kwargs are passed to patheffects.withStroke()""" + text_kwargs = {**{'ha':'center', 'va':'center', 'color':'k', 'fontsize':8}, **text_kwargs} + pe_kwargs = {**{'linewidth':1.5, 'foreground':'w', 'alpha':0.7}, **pe_kwargs} text_artists = [] for r, row in df.iterrows(): value = row.get(column, np.nan) - if not np.isfinite(value): - continue - if opt_single_decimal: - decimals = 0 if ((abs(value) >= 1) or abs(value) < 0.05) else 1 - fmt = f"{{:.{decimals}f}}" + if isinstance(value, (int, float, complex)): + if not np.isfinite(value): + continue + if opt_single_decimal: + decimals = 0 if ((abs(value) >= 1) or abs(value) < 0.05) else 1 + fmt = f"{{:.{decimals}f}}" text_artists.append( ax.annotate( - fmt.format(value), + (fmt.format(value) if not isinstance(value, str) else value), (row.geometry.centroid.x, row.geometry.centroid.y), - ha='center', va='center', fontsize=fontsize, - color=color, + **text_kwargs, path_effects=[pe.withStroke(**pe_kwargs)], zorder=1e9, ) ) + try: + from adjustText import adjust_text + except ImportError: + return + adjust_text(text_artists, ax=ax, avoid_self=False, ensure_inside_axes=True, ) @@ -6627,7 +6612,7 @@ def map_stressors( label_region_value( df=dflevel, ax=ax[1,0], column='load_rank', opt_single_decimal=False, fmt='{:.0f}%', - linewidth=2.0, alpha=0.8, + pe_kwargs={'linewidth':2.0, 'alpha':0.8}, ) ax[1,0].set_title('Demand', y=0.9) plots.addcolorbarhist( @@ -6643,7 +6628,7 @@ def map_stressors( label_region_value( df=dflevel, ax=ax[1,col], column=f'{tech}_rank', opt_single_decimal=False, fmt='{:.0f}%', - linewidth=2.0, alpha=0.8, + pe_kwargs={'linewidth':2.0, 'alpha':0.8}, ) ax[1,col].set_title(labels.get(tech,tech), y=0.9) plots.addcolorbarhist( @@ -6658,7 +6643,7 @@ def map_stressors( label_region_value( df=dflevel, ax=ax[1,3], column='temperature_rank', opt_single_decimal=False, fmt='{:.0f}%', - linewidth=2.0, alpha=0.8, + pe_kwargs={'linewidth':2.0, 'alpha':0.8}, ) ax[1,3].set_title('Temperature', y=0.9) plots.addcolorbarhist( @@ -6677,7 +6662,7 @@ def map_stressors( label_region_value( df=dflevel, ax=ax[2,col], column=f'outage_{tech}', opt_single_decimal=False, fmt='{:.0f}%', - linewidth=2.0, alpha=0.8, + pe_kwargs={'linewidth':2.0, 'alpha':0.8}, ) ax[2,col].set_title(label, y=0.9) plots.addcolorbarhist( diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index 55f1ee8ca..d0711f87e 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -10,11 +10,11 @@ from glob import glob import traceback import cmocean -from adjustText import adjust_text ### Local imports from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds +from reeds import reedsplots from reeds import plots plots.plotparams() @@ -428,28 +428,20 @@ def map_dropped_load(sw, dfs, level='r'): dfba.plot(ax=ax, facecolor='none', edgecolor='k', lw=0.2) ### Data dfplot.plot(ax=ax, column='val', cmap=cmap) - text_artists = [] - for r, row in dfplot.iterrows(): - if row.val > 0: - text_artists.append( - ax.annotate( - f'{row.val:,.0f} {units[metric,agg][0]}', - (row.centroid_x, row.centroid_y), - color='r', ha='center', va='top', fontsize=6, weight='bold') - ) - adjust_text(text_artists, ax=ax, - avoid_self=False, ensure_inside_axes=True, - ) + reedsplots.label_region_value( + dfplot, + ax=ax, + column='val', + fmt=f'{{:,.0f}} {units[metric,agg][0]}', + text_kwargs={'color':'r', 'ha':'center', 'va':'top', 'fontsize':6, 'weight':'bold'} + ) ### Formatting if level in ['r','rb','ba']: - text_artists = [] - for r, row in dfba.iterrows(): - text_artists.append( - ax.annotate(r, (row.centroid_x, row.centroid_y), - ha='center', va='bottom', fontsize=6, color='C7') - ) - adjust_text(text_artists, ax=ax, - avoid_self=False, ensure_inside_axes=True, + reedsplots.label_region_value( + dfba, + ax=ax, + column=dfba.index, + text_kwargs={'ha':'center', 'va':'bottom', 'fontsize':6, 'color':'C7'} ) ax.axis('off') if savefig: @@ -1370,7 +1362,7 @@ def main(sw, debug=False): # #%%### Inputs for debugging reeds_path = reeds.io.reeds_path casedir = os.path.join(reeds_path, 'runs', 'v20260820_USA_defaults') - t = 2035 + t = 2041 interactive = True iteration = 0 debug = True From db5a9fbd87c8b195d56d8ed904aa8c4a6b88483e Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 14 Sep 2026 10:56:02 -0600 Subject: [PATCH 32/58] Import cmocean in outage_rate.py --- reeds/input_processing/outage_rates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reeds/input_processing/outage_rates.py b/reeds/input_processing/outage_rates.py index d4919ebbf..ae52ff758 100644 --- a/reeds/input_processing/outage_rates.py +++ b/reeds/input_processing/outage_rates.py @@ -7,6 +7,7 @@ import argparse import h5py from pathlib import Path +import cmocean sys.path.append(str(Path(__file__).parent.parent.parent)) import reeds reeds_path = reeds.io.reeds_path From d54a6213626d4184c5f1127e554d6874c1669c48 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 14 Sep 2026 11:00:47 -0600 Subject: [PATCH 33/58] Correct adjustText version --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 003640058..bbe8d8b05 100644 --- a/environment.yml +++ b/environment.yml @@ -27,7 +27,7 @@ dependencies: - tqdm=4.68 ## vvv The next packages are optional for default settings and may ## vvv be excluded if you're having trouble building the environment. - - adjustText=1.14 # for avoiding map label overlaps + - adjustText=1.4 # for avoiding map label overlaps - fiona=1.10 # for interactive maps - folium=0.20 # for interactive maps - ipykernel=7.3 # for interactive python in VS Code From 155671ec572c44dca82d79b381437156da241640 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 14 Sep 2026 11:13:33 -0600 Subject: [PATCH 34/58] Remove added lines in reedsplots.py --- reeds/reedsplots.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reeds/reedsplots.py b/reeds/reedsplots.py index 148cd8e5a..90fff7834 100644 --- a/reeds/reedsplots.py +++ b/reeds/reedsplots.py @@ -4409,12 +4409,10 @@ def map_neue( zorder=1e9, path_effects=[pe.withStroke(linewidth=1.5, foreground='w', alpha=0.7)], ) - if over_threshold and (level == neue_threshold_level): ax[coords[level]].set_title( level, y=0.9, weight='bold', color=over_threshold_textcolor, ) - ### Formatting plots.addcolorbarhist( f=f, ax0=ax[coords[level]], data=df.NEUE_ppm, From 5c703eb9db30bfaef8e825cbf8815fcfe676b871 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 14 Sep 2026 14:28:52 -0600 Subject: [PATCH 35/58] Fix load map annotation --- reeds/input_processing/hourly_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reeds/input_processing/hourly_plots.py b/reeds/input_processing/hourly_plots.py index 5654c8b9a..d76adaa50 100644 --- a/reeds/input_processing/hourly_plots.py +++ b/reeds/input_processing/hourly_plots.py @@ -489,7 +489,7 @@ def plot_maps(sw, inputs_case, reeds_path, figpath, periodtype='rep', crs='EPSG: ) dfmap[level].plot(ax=ax[coords[level]], facecolor='none', edgecolor='k', lw=0.2) reedsplots.label_region_value( - dfmap[level], + dfdiffs[level], ax[coords[level]], column=val, text_kwargs={'fontsize': {'r':5}.get(level,7)}, From 5909ec31fa9993abe27d2bd2b371d1c26595d8a7 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 14 Sep 2026 15:05:29 -0600 Subject: [PATCH 36/58] Reset diagnostic plots debug inputs --- reeds/resource_adequacy/diagnostic_plots.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/reeds/resource_adequacy/diagnostic_plots.py b/reeds/resource_adequacy/diagnostic_plots.py index d0711f87e..a7eb691ac 100644 --- a/reeds/resource_adequacy/diagnostic_plots.py +++ b/reeds/resource_adequacy/diagnostic_plots.py @@ -1360,12 +1360,12 @@ def main(sw, debug=False): debug = args.debug # #%%### Inputs for debugging - reeds_path = reeds.io.reeds_path - casedir = os.path.join(reeds_path, 'runs', 'v20260820_USA_defaults') - t = 2041 - interactive = True - iteration = 0 - debug = True + # reeds_path = reeds.io.reeds_path + # casedir = os.path.join(reeds_path, 'runs', 'v20260715_stressM3_MultiMetricRA') + # t = 2050 + # interactive = True + # iteration = 0 + # debug = True #%%### INPUTS ### Switches From 28e76f2fa18296ce57486c653dc5ff3fdca37adb Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Fri, 31 Jul 2026 16:50:43 -0600 Subject: [PATCH 37/58] Add compare_inputs.py --- postprocessing/compare_inputs.py | 790 +++++++++++++++++++++++++++++++ 1 file changed, 790 insertions(+) create mode 100644 postprocessing/compare_inputs.py diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py new file mode 100644 index 000000000..432a51797 --- /dev/null +++ b/postprocessing/compare_inputs.py @@ -0,0 +1,790 @@ +#%% Imports +import argparse +import os +import re +import sys +import textwrap +import traceback +import cmocean + +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib import patheffects as pe +import pandas as pd +# from adjustText import adjust_text + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +import reeds + +reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +reeds.plots.plotparams() + +#%% Plotting + +def _parse_weatheryears(weatheryear): + """Parse a weather-year input into a sorted list of unique integer years.""" + if isinstance(weatheryear, int): + return [weatheryear] + + if isinstance(weatheryear, str): + tokens = [tok.strip() for tok in re.split(r'[_\s]+', weatheryear) if tok.strip()] + else: + try: + tokens = list(weatheryear) + except TypeError as exc: + raise ValueError( + f'Invalid weather year input: {weatheryear}. Provide an integer year ' + 'or comma/space-delimited list of years.' + ) from exc + + if not tokens: + raise ValueError('No weather years provided.') + + try: + years = sorted(set(int(token) for token in tokens)) + except (TypeError, ValueError) as exc: + raise ValueError( + f'Invalid weather year input: {weatheryear}. All weather years must be integers.' + ) from exc + + return years + + +def _weatheryears_label(weatheryears): + """Create a compact weather-year label for titles and output names.""" + if len(weatheryears) == 1: + return str(weatheryears[0]) + return ','.join("'"+str(y)[2:] for y in weatheryears) + + +def _validate_selected_weatheryears(selected_years, available_years, case_label, modelyear=None): + """Ensure selected weather years exist in available years for a given case/model year.""" + missing = sorted(set(selected_years) - set(available_years)) + if missing: + where = f' for case {case_label}' + if modelyear is not None: + where += f', model year {modelyear}' + raise ValueError( + f'Weather year(s) {missing} not found{where}. ' + f'Available weather years: {sorted(available_years)}' + ) + +def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): + """ + Compare mean daily demand profiles across cases for one or more weather years. + Overlays the selected weather year profile(s) and fills envelope of min/max across all weather years. + """ + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') + + selected_weatheryears = _parse_weatheryears(weatheryear) + selected_label = _weatheryears_label(selected_weatheryears) + + plt.close() + f, ax = plt.subplots(figsize=(12, 4.5)) + x_start, x_end = None, None + # use plots.differentiate_lines instead + + for idx, (casename, casepath) in enumerate(cases.items()): + print(f' {casename}: loading demand...') + color = colors.get(casename, f'C{idx}') + + # Parse inputs + sw = reeds.io.get_switches(casepath) + t = reeds.io.get_years(casepath)[-1] if year in [0, None, 'last'] else year + rs = reeds.inputs.parse_regions(casepath) + + case_weatheryears = sw.resource_adequacy_years_list + + # Load demand profile + dfprofile = ( + reeds.io.read_file( + os.path.join(casepath, 'inputs_case', 'load.h5'), + parse_timestamps=True, + ) + / 1e3 + ) + dfprofile = ( + dfprofile + .loc[t, [r for r in dfprofile if r in rs]] + .sum(axis=1) + ) + + # Slice to weather years range + dfprofile = dfprofile.loc[ + str(min(case_weatheryears)) : str(max(case_weatheryears)) + ].copy() + dfprofile.index = pd.to_datetime(dfprofile.index) + + # Group by day of year (1-365/366) across all weather years + doy = dfprofile.index.dayofyear + hourly_stats = dfprofile.groupby(doy).agg(['min', 'max']) + hourly_stats.columns = ['min', 'max'] + + available_years = sorted(dfprofile.index.year.unique().tolist()) + _validate_selected_weatheryears(selected_weatheryears, available_years, casename) + selected_daily_profiles = {} + for wy in selected_weatheryears: + selected_profile = dfprofile.loc[dfprofile.index.year == wy] + daily_profile = selected_profile.groupby(selected_profile.index.dayofyear).mean() + selected_daily_profiles[wy] = daily_profile.reindex(hourly_stats.index) + + # Create date x-axis for a standard year + date_range = pd.date_range('2025-01-01', periods=len(hourly_stats), freq='D') + if x_start is None: + x_start, x_end = date_range[0], date_range[-1] + + # Plot average daily profile across selected weather year(s) + avg_daily_profile = pd.concat( + [selected_daily_profiles[wy] for wy in selected_weatheryears], axis=1 + ).mean(axis=1) + ax.plot( + date_range, + avg_daily_profile.values, + lw=1.0, + color=color, + label=casename, + ) + + # Fill envelope (min/max across weather years) + ax.fill_between( + date_range, hourly_stats['max'], hourly_stats['min'], + lw=0, alpha=0.15, color=color, + ) + + # Plot min/max dotted lines + ax.plot(date_range, hourly_stats['max'], lw=0.5, linestyle=':', color=color, alpha=0.6) + ax.plot(date_range, hourly_stats['min'], lw=0.5, linestyle=':', color=color, alpha=0.6) + + ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) + ax.set_ylabel('Daily mean electricity demand [GW]') + ax.set_ylim(0) + + # Format x-axis dates + ax.xaxis.set_major_locator(mpl.dates.MonthLocator()) + ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%b-1')) + ax.tick_params(axis='x', rotation=45) + if x_start is not None and x_end is not None: + ax.set_xlim(x_start, x_end) + ax.margins(x=0) + ax.grid(True, which='major', axis='y', alpha=0.3) + + yearlabel = year if year not in [0, None, 'last'] else 'last model year' + wy_title_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' + ax.set_title( + f'Daily mean demand in {yearlabel} for {wy_title_label} {selected_label} with min/max envelope across all weather years', + x=0, + ha='left', + ) + ax.legend(frameon=False, loc='lower center', ncol=len(cases)) + reeds.plots.despine(ax) + + return f, ax + + +def plot_peak_and_total_load(cases, colors, weatheryear=2012): + """ + Compare annual total load and annual peak load by model year. + For each model year and case, compute min/max across weather years and + the value for each selected weather year. + """ + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') + + selected_weatheryears = _parse_weatheryears(weatheryear) + selected_label = _weatheryears_label(selected_weatheryears) + + f, (ax_total, ax_peak) = plt.subplots( + 1, 2, figsize=(12, 4.5), constrained_layout=True + ) + stats_by_case = {} + + for idx, (casename, casepath) in enumerate(cases.items()): + print(f' {casename}: loading annual and peak stats by model year...') + color = colors.get(casename, f'C{idx}') + + sw = reeds.io.get_switches(casepath) + modelyears = reeds.io.get_years(casepath) + rs = reeds.inputs.parse_regions(casepath) + case_weatheryears = sw.resource_adequacy_years_list + + dfload = ( + reeds.io.read_file( + os.path.join(casepath, 'inputs_case', 'load.h5'), + parse_timestamps=True, + ) + / 1e3 + ) + region_cols = [r for r in dfload if r in rs] + + rows = [] + for t in modelyears: + profile = dfload.loc[t, region_cols].sum(axis=1) + profile = profile.loc[ + str(min(case_weatheryears)) : str(max(case_weatheryears)) + ].copy() + profile.index = pd.to_datetime(profile.index) + + # profile is in GW and timestamps are hourly, so annual sum is GWh. + # Convert to TWh for plotting. + annual_total_by_wy = profile.groupby(profile.index.year).sum() / 1e3 + annual_peak_by_wy = profile.groupby(profile.index.year).max() + + _validate_selected_weatheryears( + selected_weatheryears, + annual_total_by_wy.index.tolist(), + casename, + modelyear=t, + ) + + rows.append( + { + 't': t, + 'total_min': annual_total_by_wy.min(), + 'total_max': annual_total_by_wy.max(), + 'peak_min': annual_peak_by_wy.min(), + 'peak_max': annual_peak_by_wy.max(), + **{ + f'total_selected_{wy}': annual_total_by_wy.loc[wy] + for wy in selected_weatheryears + }, + **{ + f'peak_selected_{wy}': annual_peak_by_wy.loc[wy] + for wy in selected_weatheryears + }, + } + ) + + dfstats = pd.DataFrame(rows).set_index('t').sort_index() + stats_by_case[casename] = dfstats + + x = dfstats.index.values + + avg_total = dfstats[[f'total_selected_{wy}' for wy in selected_weatheryears]].mean(axis=1) + ax_total.plot( + x, avg_total.values, lw=1.2, color=color, label=casename, + ) + ax_total.fill_between(x, dfstats['total_max'].values, dfstats['total_min'].values, lw=0, alpha=0.15, color=color) + ax_total.plot(x, dfstats['total_max'].values, lw=0.8, linestyle=':', color=color, alpha=0.7) + ax_total.plot(x, dfstats['total_min'].values, lw=0.8, linestyle=':', color=color, alpha=0.7) + + avg_peak = dfstats[[f'peak_selected_{wy}' for wy in selected_weatheryears]].mean(axis=1) + ax_peak.plot( + x, avg_peak.values, lw=1.2, color=color, label=casename, + ) + ax_peak.fill_between(x, dfstats['peak_max'].values, dfstats['peak_min'].values, lw=0, alpha=0.15, color=color) + ax_peak.plot(x, dfstats['peak_max'].values, lw=0.8, linestyle=':', color=color, alpha=0.7) + ax_peak.plot(x, dfstats['peak_min'].values, lw=0.8, linestyle=':', color=color, alpha=0.7) + + ax_total.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) + ax_total.set_ylabel('Annual total load [TWh]') + wy_total_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' + ax_total.set_title( + textwrap.fill( + f'Annual total load by model year ({wy_total_label} {selected_label}, min/max envelope)', + width=55, + ), + x=0, ha='left', + ) + ax_total.legend(frameon=False, loc='best') + ax_total.margins(x=0) + ax_total.set_xlim(left=2020) + ax_total.grid(True, which='major', axis='y', alpha=0.3) + reeds.plots.despine(ax_total) + + ax_peak.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) + ax_peak.set_xlabel('Model year') + ax_peak.set_ylabel('Annual peak load [GW]') + ax_peak.set_title( + textwrap.fill( + f'Annual peak load by model year ({wy_total_label} {selected_label}, min/max envelope)', + width=55, + ), + x=0, ha='left', + ) + ax_peak.legend(frameon=False, loc='best') + ax_peak.margins(x=0) + ax_peak.set_xlim(left=2020) + ax_peak.grid(True, which='major', axis='y', alpha=0.3) + reeds.plots.despine(ax_peak) + + plt.draw() + + + return f, (ax_total, ax_peak), stats_by_case + +def plot_regional_peak_demand_maps(cases, year='last'): + """ + Map the max peak demand per state, comparing across cases. + + For each state, peak demand is the highest simultaneous (summed across + BAs in that state) hourly value across ALL available weather years for + the given model year. + + Layout: 1 row x N-cases columns. + Base case (first case): absolute peak demand [GW] with a sequential colormap. + All other cases: percent difference vs. the base case with a diverging colormap. + + Parameters + ---------- + cases : dict {casename: casepath} + year : int or 'last' + Model year to plot. Defaults to the last solved year of the base case. + + Returns + ------- + f, ax, df_peak + df_peak has one column per case, indexed by state, values in GW. + """ + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') + + basecasename = list(cases.keys())[0] + basecasepath = list(cases.values())[0] + + if year in [0, None, 'last']: + t = reeds.io.get_years(basecasepath)[-1] + else: + t = int(year) + + dfmap = reeds.io.get_dfmap(basecasepath) + dfstates = dfmap['st'] + + peak_st = {} + for casename, casepath in cases.items(): + print(f' {casename}: loading state-level peak demand...') + sw = reeds.io.get_switches(casepath) + rs = reeds.inputs.parse_regions(casepath) + case_weatheryears = sw.resource_adequacy_years_list + region_to_state = reeds.io.get_hierarchy(casepath)['st'] + + dfprofile = ( + reeds.io.read_file( + os.path.join(casepath, 'inputs_case', 'load.h5'), + parse_timestamps=True, + ) + / 1e3 # MW -> GW + ) + valid_cols = [r for r in dfprofile.columns if r in rs] + dfprofile = dfprofile[valid_cols] + + try: + df_t = dfprofile.loc[t].copy() + except KeyError: + raise KeyError(f'Model year {t} not found in load.h5 for case {casename}.') + + df_t.index = pd.to_datetime(df_t.index) + # Use the full weather-year range — max across ALL years + df_t = df_t.loc[ + str(min(case_weatheryears)) : str(max(case_weatheryears)) + ] + # Aggregate BAs to states, then take the max simultaneous hour + df_t_st = df_t.T.groupby(df_t.columns.map(region_to_state)).sum().T + peak_st[casename] = df_t_st.max() + + ncols = len(cases) + cmap_abs = cmocean.cm.tempo + cmap_diff = plt.cm.RdBu_r + + plt.close() + f, ax = plt.subplots( + 1, ncols, + figsize=(max(5 * ncols, 10), 5), + gridspec_kw={'wspace': 0.0}, + ) + if ncols == 1: + ax = [ax] + + base_vals = peak_st[basecasename] + abs_vmax = max( + (peak_st[c].reindex(dfstates.index).fillna(0).max() for c in cases), + default=0.0, + ) + pct_diffs = { + c: ((peak_st[c] - base_vals) / base_vals * 100) + .replace([float('inf'), float('-inf')], float('nan')) + for c in list(cases.keys())[1:] + } + diff_absmax = max( + (pct_diffs[c].abs().max() + for c in pct_diffs if not pct_diffs[c].dropna().empty), + default=1.0, + ) + + for col, casename in enumerate(cases): + _ax = ax[col] + dfplot = dfstates.copy() + + if casename == basecasename: + dfplot['val'] = base_vals.reindex(dfstates.index).fillna(0) + _cmap, vmin, vmax = cmap_abs, 0, abs_vmax + cbar_label = 'Peak demand [GW]' + else: + dfplot['val'] = pct_diffs[casename].reindex(dfstates.index) + _cmap, vmin, vmax = cmap_diff, -diff_absmax, diff_absmax + cbar_label = f'Change in peak demand [%]' + + dfstates.plot(ax=_ax, facecolor='none', edgecolor='k', lw=0.2, zorder=10000) + dfplot.plot(ax=_ax, column='val', cmap=_cmap, vmin=vmin, vmax=vmax, legend=False) + + # Value labels for states + texts = [] + for st, row in dfplot.iterrows(): + if pd.isna(row['val']): + continue + centroid = dfstates.loc[st, 'geometry'].representative_point() + label = f'{row["val"]:.0f}' if casename == basecasename else f'{row["val"]:+.0f}' + texts.append(_ax.text( + centroid.x, centroid.y, label, + fontsize=7, ha='center', va='center', color='k', + path_effects=[pe.withStroke(linewidth=1.5, foreground='white')], + zorder=20000, + )) + # adjust_text( + # texts, + # ax=_ax, + # avoid_self=False, + # ) + + if col == 0 or col == 1: + reeds.plots.addcolorbarhist( + f=f, ax0=_ax, data=dfplot['val'].dropna().values, + title=cbar_label, cmap=_cmap, vmin=vmin, vmax=vmax, + orientation='horizontal', labelpad=2.25, histratio=0., + cbarwidth=0.05, cbarheight=0.85, + cbarbottom=-0.05, cbarhoffset=0., + ) + + _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=14) + _ax.axis('off') + + f.suptitle( + f'Peak demand in {t} (max across all weather years)', + fontsize=18, + y=0.9, + ) + + df_peak = pd.DataFrame(peak_st).reindex(dfstates.index) + return f, ax, df_peak + + +def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012): + """ + Map total annual demand per state, comparing across cases. + + For each state, total demand is summed across all BAs in that state over + the selected weather year(s) and averaged if more than one year is given. + + Layout: 1 row x N-cases columns. + Base case (first case): absolute total demand [TWh] with a sequential colormap. + All other cases: percent difference vs. the base case with a diverging colormap. + + Parameters + ---------- + cases : dict {casename: casepath} + colors : dict {casename: color} (unused in maps but kept for API parity) + year : int or 'last' + Model year to plot. Defaults to the last solved year of the base case. + weatheryear : int, str, or iterable + Weather year(s) to use. Values are averaged when multiple are given. + + Returns + ------- + f, ax, df_total + df_total has one column per case, indexed by state, values in TWh. + df_total has one column per case, indexed by region, values in TWh. + """ + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') + + selected_weatheryears = _parse_weatheryears(weatheryear) + wy_label = _weatheryears_label(selected_weatheryears) + basecasename = list(cases.keys())[0] + basecasepath = list(cases.values())[0] + + if year in [0, None, 'last']: + t = reeds.io.get_years(basecasepath)[-1] + else: + t = int(year) + + dfmap = reeds.io.get_dfmap(basecasepath) + dfstates = dfmap['st'] + + total_st = {} + for casename, casepath in cases.items(): + print(f' {casename}: loading state-level total demand...') + sw = reeds.io.get_switches(casepath) + rs = reeds.inputs.parse_regions(casepath) + case_weatheryears = sw.resource_adequacy_years_list + region_to_state = reeds.io.get_hierarchy(casepath)['st'] + + dfprofile = ( + reeds.io.read_file( + os.path.join(casepath, 'inputs_case', 'load.h5'), + parse_timestamps=True, + ) + / 1e3 # MW -> GW + ) + valid_cols = [r for r in dfprofile.columns if r in rs] + dfprofile = dfprofile[valid_cols] + + try: + df_t = dfprofile.loc[t].copy() + except KeyError: + raise KeyError(f'Model year {t} not found in load.h5 for case {casename}.') + + df_t.index = pd.to_datetime(df_t.index) + df_t = df_t.loc[ + str(min(case_weatheryears)) : str(max(case_weatheryears)) + ] + available_wy = sorted(df_t.index.year.unique().tolist()) + _validate_selected_weatheryears(selected_weatheryears, available_wy, casename, t) + + # Aggregate BAs to states, then average annual total across ALL available weather years + df_t_st = df_t.T.groupby(df_t.columns.map(region_to_state)).sum().T + wy_totals = pd.concat( + [df_t_st.loc[df_t_st.index.year == wy].sum() / 1e3 # GWh -> TWh + for wy in available_wy], + axis=1, + ).mean(axis=1) + total_st[casename] = wy_totals + + ncols = len(cases) + cmap_abs = cmocean.cm.tempo + cmap_diff = plt.cm.RdBu_r + + plt.close() + f, ax = plt.subplots( + 1, ncols, + figsize=(max(5 * ncols, 10), 5), + gridspec_kw={'wspace': 0.0}, + ) + if ncols == 1: + ax = [ax] + + base_vals = total_st[basecasename] + abs_vmax = max( + (total_st[c].reindex(dfstates.index).fillna(0).max() for c in cases), + default=0.0, + ) + pct_diffs = { + c: ((total_st[c] - base_vals) / base_vals * 100) + .replace([float('inf'), float('-inf')], float('nan')) + for c in list(cases.keys())[1:] + } + diff_absmax = max( + (pct_diffs[c].abs().max() + for c in pct_diffs if not pct_diffs[c].dropna().empty), + default=1.0, + ) + + for col, casename in enumerate(cases): + _ax = ax[col] + dfplot = dfstates.copy() + + if casename == basecasename: + dfplot['val'] = base_vals.reindex(dfstates.index).fillna(0) + _cmap, vmin, vmax = cmap_abs, 0, abs_vmax + cbar_label = 'Annual demand [TWh]' + else: + dfplot['val'] = pct_diffs[casename].reindex(dfstates.index) + _cmap, vmin, vmax = cmap_diff, -diff_absmax, diff_absmax + cbar_label = f'Change in annual demand [%]' + + dfstates.plot(ax=_ax, facecolor='none', edgecolor='k', lw=0.2, zorder=10000) + dfplot.plot(ax=_ax, column='val', cmap=_cmap, vmin=vmin, vmax=vmax, legend=False) + + # Value labels for states + texts = [] + for st, row in dfplot.iterrows(): + if pd.isna(row['val']): + continue + centroid = dfstates.loc[st, 'geometry'].representative_point() + label = f'{row["val"]:.0f}' if casename == basecasename else f'{row["val"]:+.0f}' + texts.append(_ax.text( + centroid.x, centroid.y, label, + fontsize=7, ha='center', va='center', color='k', + path_effects=[pe.withStroke(linewidth=1.5, foreground='white')], + zorder=20000, + )) + # adjust_text( + # texts, + # ax=_ax, + # avoid_self=False, + # ) + + if col == 0 or col == 1: + reeds.plots.addcolorbarhist( + f=f, ax0=_ax, data=dfplot['val'].dropna().values, + title=cbar_label, cmap=_cmap, vmin=vmin, vmax=vmax, + orientation='horizontal', labelpad=2.25, histratio=0., + cbarwidth=0.05, cbarheight=0.85, + cbarbottom=-0.05, cbarhoffset=0., + ) + + _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=14) + _ax.axis('off') + + wy_title = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' + f.suptitle( + f'Annual demand in {t}, {wy_title} {wy_label}', + fontsize=18, + y=0.9, + ) + + df_total = pd.DataFrame(total_st).reindex(dfstates.index) + return f, ax, df_total + + +def plot_peak_and_total_load_maps(cases, colors, year='last', weatheryear=2012): + """ + Convenience wrapper: calls plot_regional_peak_demand_maps and + plot_regional_total_demand_maps and returns their results as a tuple. + """ + f_peak, ax_peak, df_peak = plot_regional_peak_demand_maps(cases, year=year) + f_total, ax_total, df_total = plot_regional_total_demand_maps( + cases, colors, year=year, weatheryear=weatheryear + ) + return (f_peak, f_total), (ax_peak, ax_total), (df_peak, df_total) + +#%% Main + + + +if __name__ == '__main__': + #%% Argument inputs + parser = argparse.ArgumentParser( + description='Compare ReEDS input data for multiple cases', + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + 'caselist', + type=str, + nargs='+', + help=( + 'Space-delimited list of case paths, OR shared case prefix, OR CSV ' + 'with casepath/casename/color columns (e.g., postprocessing/example.csv).' + ), + ) + parser.add_argument( + '--casenames', '-n', type=str, default='', + help='Comma-delimited case names when not provided in CSV.', + ) + parser.add_argument( + '--titleshorten', '-s', type=str, default='', + help='Characters to cut from start of case name (if not using CSV casenames).', + ) + parser.add_argument( + '--basecase', '-b', type=str, default='', + help='Case-path suffix to force as base case.', + ) + parser.add_argument( + '--output', '-o', choices=['png', 'ppt', 'pptx'], default='png', + help='Output format (png or pptx)') + parser.add_argument( + '--year', '-y', type=str, default='2050', + help='Model year for hourly demand plot (integer year or "last").', + ) + parser.add_argument( + '--weatheryear', '-w', type=str, default=None, + help=( + 'Weather year(s) to plot (single year or comma/space-delimited list, e.g., ' + '"2012" or "2007,2012"). Defaults to GSw_HourlyWeatherYears from the first case.' + ), + ) + args = parser.parse_args() + + try: + titleshorten = int(args.titleshorten) + except ValueError: + titleshorten = len(args.titleshorten) + + cases, colors, basecase, basemap = reeds.report_utils.parse_caselist( + args.caselist, + args.casenames, + args.basecase, + titleshorten, + ) + + firstcasepath = list(cases.values())[0] + outpath = os.path.join(firstcasepath, 'outputs', 'comparisons','inputs') + os.makedirs(outpath, exist_ok=True) + + #%% Create output container + suffix = args.output.strip('.') + if suffix in ['pdf', 'png']: + savepath = os.path.join(firstcasepath, 'outputs', 'comparisons','inputs') + os.makedirs(savepath, exist_ok=True) + + def saveit(savename): + outpath = os.path.join(savepath, savename.lower().replace(' ', '-') + f'.{suffix}') + plt.savefig(outpath) + print(os.path.basename(outpath)) + if mpl.interactive: + plt.show() + + elif suffix in ['ppt', 'pptx']: + savepath = os.path.join(firstcasepath, 'outputs', 'comparisons', 'inputs', 'inputs.pptx') + prs = reeds.report_utils.init_pptx() + def saveit(savename, **kwargs): + reeds.report_utils.add_to_pptx(savename, prs=prs, **kwargs) + if mpl.interactive: + plt.show() + + try: + year = int(args.year) + except ValueError: + year = args.year + + if args.weatheryear is None: + sw = reeds.io.get_switches(firstcasepath) + selected_weatheryears = sw.GSw_HourlyWeatherYears + else: + selected_weatheryears = _parse_weatheryears(args.weatheryear) + weatheryear_label = _weatheryears_label(selected_weatheryears) + + try: + f, ax = plot_daily_demand_profiles( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + ) + saveit(f'Demand daily profile {year}') + except Exception as e: + print(traceback.format_exc()) + + try: + f_modelyear, _, _ = plot_peak_and_total_load( + cases, + colors, + weatheryear=selected_weatheryears, + ) + saveit(f'Demand by year') + except Exception as e: + print(traceback.format_exc()) + + try: + f_peak, _, _ = plot_regional_peak_demand_maps( + cases, + year=year, + ) + saveit(f'Demand peak by region {year}') + except Exception as e: + print(traceback.format_exc()) + + try: + f_total, _, _ = plot_regional_total_demand_maps( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + ) + saveit(f'Demand total by region {year}') + except Exception as e: + print(traceback.format_exc()) + + #%% Save the powerpoint file if necessary + if suffix in ['ppt', 'pptx']: + print(f'\ninput_plots.py results saved to:\n{savepath}') + prs.save(savepath) \ No newline at end of file From e5a4f090ab0af7fb0017c44ba5c4029d87d179b5 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 15 Sep 2026 14:26:51 -0600 Subject: [PATCH 38/58] Use reedsplots.label_region_value() --- postprocessing/compare_inputs.py | 51 ++++++++++---------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 432a51797..a1efe6126 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -11,7 +11,6 @@ import matplotlib.pyplot as plt from matplotlib import patheffects as pe import pandas as pd -# from adjustText import adjust_text sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import reeds @@ -428,23 +427,14 @@ def plot_regional_peak_demand_maps(cases, year='last'): dfplot.plot(ax=_ax, column='val', cmap=_cmap, vmin=vmin, vmax=vmax, legend=False) # Value labels for states - texts = [] - for st, row in dfplot.iterrows(): - if pd.isna(row['val']): - continue - centroid = dfstates.loc[st, 'geometry'].representative_point() - label = f'{row["val"]:.0f}' if casename == basecasename else f'{row["val"]:+.0f}' - texts.append(_ax.text( - centroid.x, centroid.y, label, - fontsize=7, ha='center', va='center', color='k', - path_effects=[pe.withStroke(linewidth=1.5, foreground='white')], - zorder=20000, - )) - # adjust_text( - # texts, - # ax=_ax, - # avoid_self=False, - # ) + reeds.reedsplots.label_region_value( + dfplot, + ax=_ax, + column='val', + fmt='{:.0f}' if casename == basecasename else '{:+.0f}', + text_kwargs={'fontsize': 7, 'ha': 'center', 'va': 'center', 'color': 'k'}, + pe_kwargs={'linewidth': 1.5, 'foreground': 'white'} + ) if col == 0 or col == 1: reeds.plots.addcolorbarhist( @@ -595,23 +585,14 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 dfplot.plot(ax=_ax, column='val', cmap=_cmap, vmin=vmin, vmax=vmax, legend=False) # Value labels for states - texts = [] - for st, row in dfplot.iterrows(): - if pd.isna(row['val']): - continue - centroid = dfstates.loc[st, 'geometry'].representative_point() - label = f'{row["val"]:.0f}' if casename == basecasename else f'{row["val"]:+.0f}' - texts.append(_ax.text( - centroid.x, centroid.y, label, - fontsize=7, ha='center', va='center', color='k', - path_effects=[pe.withStroke(linewidth=1.5, foreground='white')], - zorder=20000, - )) - # adjust_text( - # texts, - # ax=_ax, - # avoid_self=False, - # ) + reeds.reedsplots.label_region_value( + dfplot, + ax=_ax, + column='val', + fmt='{:.0f}' if casename == basecasename else '{:+.0f}', + text_kwargs={'fontsize': 7, 'ha': 'center', 'va': 'center', 'color': 'k'}, + pe_kwargs={'linewidth': 1.5, 'foreground': 'white'} + ) if col == 0 or col == 1: reeds.plots.addcolorbarhist( From 3daa4c0ca28c6047f8254d867bfcc6214416a9ba Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 15 Sep 2026 14:27:11 -0600 Subject: [PATCH 39/58] Add plot_hourly_demand_profiles and plot_demand_yearbymonth --- postprocessing/compare_inputs.py | 91 ++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index a1efe6126..44f018cb4 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -14,6 +14,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import reeds +from postprocessing import input_plots reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) reeds.plots.plotparams() @@ -68,6 +69,7 @@ def _validate_selected_weatheryears(selected_years, available_years, case_label, f'Available weather years: {sorted(available_years)}' ) +# should this be replaced with Patrick's script? def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): """ Compare mean daily demand profiles across cases for one or more weather years. @@ -161,8 +163,7 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): # Format x-axis dates ax.xaxis.set_major_locator(mpl.dates.MonthLocator()) - ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%b-1')) - ax.tick_params(axis='x', rotation=45) + ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%b')) if x_start is not None and x_end is not None: ax.set_xlim(x_start, x_end) ax.margins(x=0) @@ -180,6 +181,68 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): return f, ax +def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012): + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') + + selected_weatheryears = _parse_weatheryears(weatheryear) + selected_label = _weatheryears_label(selected_weatheryears) + yearlabel = year if year not in [0, None, 'last'] else 'last model year' + wy_title_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' + + plt.close() + f, ax = plt.subplots(figsize=(13.33, 4)) + + for idx, (casename, casepath) in enumerate(cases.items()): + color = colors.get(casename, f'C{idx}') + + input_plots.plot_profile( + casepath, datum='demand', year=year, weatheryears=selected_weatheryears, + color=color, label=casename, f=f, ax=ax, hourly=True, + ) + ax.margins(x=0) + ax.grid(True, which='major', axis='y', alpha=0.3) + + ax.set_title( + f'Hourly demand in {yearlabel} for {wy_title_label} {selected_label} with min/max daily envelope', + x=0, + ha='left', + ) + ax.legend(frameon=False, loc='lower center', ncol=len(cases)) + + return f, ax + +def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): + yearlabel = year if year not in [0, None, 'last'] else 'last model year' + + plt.close() + for idx, (casename, casepath) in enumerate(cases.items()): + color = colors.get(casename, f'C{idx}') + # replace with reeds.results.summarize_load_data? + dfprofile = ( + reeds.io.read_file(os.path.join(casepath, 'inputs_case', 'load.h5'), + parse_timestamps=True) + .loc[year].loc[str(weatheryear)].sum(axis=1) + ) + if idx == 0: + f,ax = reeds.plots.plotyearbymonth( + dfprofile.rename(casename).to_frame(), + style='line', colors=[color] + ) + else: + reeds.plots.plotyearbymonth( + dfprofile.rename(casename).to_frame(), + style='line', colors=[color], f=f, ax=ax + ) + + ax[0].set_title( + f'Hourly demand in {yearlabel} for weather year {weatheryear}', + x=0, + ha='left', + ) + ax[0].legend(loc='upper left', bbox_to_anchor=(1,1), frameon=False, fontsize='x-large') + + return f, ax def plot_peak_and_total_load(cases, colors, weatheryear=2012): """ @@ -197,7 +260,7 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): 1, 2, figsize=(12, 4.5), constrained_layout=True ) stats_by_case = {} - + # replace most of this with reeds.results.summarize_load_data? for idx, (casename, casepath) in enumerate(cases.items()): print(f' {casename}: loading annual and peak stats by model year...') color = colors.get(casename, f'C{idx}') @@ -735,6 +798,28 @@ def saveit(savename, **kwargs): except Exception as e: print(traceback.format_exc()) + try: + f, ax = plot_hourly_demand_profiles( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + ) + saveit(f'Demand hourly profile {selected_weatheryears}') + except Exception as e: + print(traceback.format_exc()) + + try: + f, ax = plot_demand_yearbymonth( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + ) + saveit(f'Demand by year and month {year}') + except Exception as e: + print(traceback.format_exc()) + try: f_modelyear, _, _ = plot_peak_and_total_load( cases, From 7a8d4d316e02c06ad70b5e431d85f23d03d0c807 Mon Sep 17 00:00:00 2001 From: Brian Sergi Date: Thu, 13 Aug 2026 12:18:27 -0600 Subject: [PATCH 40/58] function for looking at demand --- reeds/results.py | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/reeds/results.py b/reeds/results.py index 22743d651..a2d785d62 100644 --- a/reeds/results.py +++ b/reeds/results.py @@ -12,6 +12,7 @@ import h5py import numpy as np import pandas as pd +import time from pathlib import Path from itertools import product @@ -808,3 +809,147 @@ def diff_outputs( reeds.io.write_to_h5(df.reset_index(), key, outpath) print(f'Difference written to {outpath}') return dictout + +def summarize_load_data( + scenario, + use_run=False, + run_path=None, + reg_sub=None, + agg_reg_lvl=None, + model_year_sub=None, + weather_year_sub=None, + agg_by_year=False +): + """helper function to read and summarize load data + Parameters + ---------- + scenario + Either the name of a ReEDS run (if use_run=True) or the name of a load file in the repo + use_run, optional + If True looks for load data from a ReEDS run, by default False + run_path, optional + Folder to look for the runs (if none then uses the run folder in the ReEDS repo) + If use use_run=False and run_path is specified, use this as the ReEDS repo. + reg_sub, optional + List of 'r' regions to subset on + agg_reg_lvl, optional + Regionality to aggregate to using a column from hierarchy.csv; if 'all' then aggregates all + regions, by default None + model_year_sub, optional + Model years to subset on, specified either as a list of years or 'last' for the final year + weather_year_sub + Weather years to subset on, specified as a list of years, by default None + Returns + ------- + pd.DataFrame: load data summarized as specified + """ + + print(f"\nprocessing {scenario}") + start_time = time.perf_counter() + + ## load data + if use_run: + # set path to load file in inputs case folder + if run_path is None: + run_path = os.path.join(reeds_path, 'runs', scenario) + else: + run_path = os.path.join(run_path, scenario) + load_file_path = os.path.join(run_path, 'inputs_case', 'load.h5') + else: + # use input load in repo + if run_path is None: + load_file_path = os.path.join(reeds_path,'inputs','load', scenario + "_load_hourly.h5") + else: + load_file_path = os.path.join(run_path,'inputs','load', scenario + "_load_hourly.h5") + + # check that file exists; if not, return empty data frame + if not os.path.exists(load_file_path): + print(f"'{load_file_path}' does not exist.") + return pd.DataFrame() + print(f"...loading {load_file_path}") + dfin = reeds.io.read_file(os.path.join(load_file_path), parse_timestamps=True) + + ## subset to desired regions (defined in terms of r regions) + if reg_sub is None: + dfout = dfin.copy() + else: + print(f"...subsettting to {reg_sub}") + dfout = dfin.loc[:, reg_sub].copy() + # TODO: add error checking for misspecified region list + + ## aggregate to desired regionality + if agg_reg_lvl is None: + # no aggregation + pass + elif agg_reg_lvl == 'all': + # sum all regions + print(f"...aggregating all regions") + dfout = dfout.sum(axis=1) + dfout.name = 'load_MWh' + else: + # sum to new mapping + print(f"...aggregating regionality to {agg_reg_lvl}") + # load hierarchy + if use_run: + hierarchy_file_path = os.path.join(run_path, 'inputs_case', 'hierarchy.csv') + reg_col = "*r" + else: + hierarchy_file_path = os.path.join(reeds_path, 'inputs', 'hierarchy.csv') + reg_col = "ba" + hierarchy = pd.read_csv(hierarchy_file_path) + # set up region mapping + if agg_reg_lvl not in hierarchy.columns: + print(f"ERROR: {agg_reg_lvl} is not a column in hierarchy file. Skipping aggregation.") + else: + region_map = hierarchy[[reg_col, agg_reg_lvl]].set_index(reg_col).squeeze() + + #TODO: add option to aggregate without region mapping + # map load to regionality in relevant hierarchy column + dfout.columns = dfout.columns.map(region_map) + # aggregate + dfout = dfout.groupby(level=0, axis=1).sum() + + # subset to desired model years + if model_year_sub is not None: + print(f"...subsetting to {model_year_sub} model years") + # TODO: implement 'last' option, convert int to list of last value + # TODO: warning on missing values? + my_vals = dfout.index.get_level_values('year') + dfout = dfout[my_vals.isin(model_year_sub)] + + ## subset to desired weather years + if weather_year_sub is not None: + print(f"...subsetting to {weather_year_sub} weather years") + # temporarily reset index to slice + idx_names = dfout.index.names + dfout = dfout.reset_index() + # isolate weather year from datetime + dfout['weather_year'] = dfout['datetime'].dt.year + if isinstance(weather_year_sub, int): + weather_year_sub = [weather_year_sub] + elif not isinstance(weather_year_sub, list): + print("'weather_year_sub' only accepts int or list.") + raise Exception + dfout = dfout.loc[dfout.weather_year.isin(weather_year_sub)] + # reset index + dfout = dfout.drop('weather_year', axis=1).set_index(idx_names) + + ## aggregate data + if agg_by_year: + print(f"...aggregating hourly data to annual") + # temporarily reset index to slice + idx_names = dfout.index.names + dfout = dfout.reset_index() + # isolate weather year from datetime + dfout['weather_year'] = dfout['datetime'].dt.year + dfout = dfout.drop('datetime', axis=1).groupby(['year','weather_year']).sum() + + # ensure output is pd.DataFrame and not a Series + if isinstance(dfout, pd.Series): + dfout = dfout.to_frame() + + end_time = time.perf_counter() + elapsed_time = end_time - start_time + print(f"\ndone processing {scenario} ({elapsed_time:.0f} seconds)") + + return dfout \ No newline at end of file From 0136691037d4a9ad5ae55bd0988c163f21861a2c Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 16:00:36 -0600 Subject: [PATCH 41/58] Use reeds.results.summarize_load_data() --- postprocessing/compare_inputs.py | 78 +++++++++++++------------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 44f018cb4..96820d900 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -99,22 +99,17 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): # Load demand profile dfprofile = ( - reeds.io.read_file( - os.path.join(casepath, 'inputs_case', 'load.h5'), - parse_timestamps=True, + reeds.results.summarize_load_data( + casepath, + use_run=True, + reg_sub=rs, + model_year_sub=[year], + weather_year_sub=[weatheryear] ) / 1e3 ) - dfprofile = ( - dfprofile - .loc[t, [r for r in dfprofile if r in rs]] - .sum(axis=1) - ) # Slice to weather years range - dfprofile = dfprofile.loc[ - str(min(case_weatheryears)) : str(max(case_weatheryears)) - ].copy() dfprofile.index = pd.to_datetime(dfprofile.index) # Group by day of year (1-365/366) across all weather years @@ -270,21 +265,19 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): rs = reeds.inputs.parse_regions(casepath) case_weatheryears = sw.resource_adequacy_years_list - dfload = ( - reeds.io.read_file( - os.path.join(casepath, 'inputs_case', 'load.h5'), - parse_timestamps=True, + dfprofile = ( + reeds.results.summarize_load_data( + casepath, + use_run=True, + reg_sub=rs, + weather_year_sub=selected_weatheryears, ) / 1e3 ) - region_cols = [r for r in dfload if r in rs] rows = [] for t in modelyears: - profile = dfload.loc[t, region_cols].sum(axis=1) - profile = profile.loc[ - str(min(case_weatheryears)) : str(max(case_weatheryears)) - ].copy() + profile = dfprofile.loc[t].sum(axis=1) profile.index = pd.to_datetime(profile.index) # profile is in GW and timestamps are hourly, so annual sum is GWh. @@ -421,14 +414,14 @@ def plot_regional_peak_demand_maps(cases, year='last'): region_to_state = reeds.io.get_hierarchy(casepath)['st'] dfprofile = ( - reeds.io.read_file( - os.path.join(casepath, 'inputs_case', 'load.h5'), - parse_timestamps=True, + reeds.results.summarize_load_data( + casepath, + use_run=True, + reg_sub=rs, + model_year_sub=[t], ) - / 1e3 # MW -> GW + / 1e3 ) - valid_cols = [r for r in dfprofile.columns if r in rs] - dfprofile = dfprofile[valid_cols] try: df_t = dfprofile.loc[t].copy() @@ -436,10 +429,6 @@ def plot_regional_peak_demand_maps(cases, year='last'): raise KeyError(f'Model year {t} not found in load.h5 for case {casename}.') df_t.index = pd.to_datetime(df_t.index) - # Use the full weather-year range — max across ALL years - df_t = df_t.loc[ - str(min(case_weatheryears)) : str(max(case_weatheryears)) - ] # Aggregate BAs to states, then take the max simultaneous hour df_t_st = df_t.T.groupby(df_t.columns.map(region_to_state)).sum().T peak_st[casename] = df_t_st.max() @@ -572,31 +561,26 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 region_to_state = reeds.io.get_hierarchy(casepath)['st'] dfprofile = ( - reeds.io.read_file( - os.path.join(casepath, 'inputs_case', 'load.h5'), - parse_timestamps=True, + reeds.results.summarize_load_data( + casepath, + use_run=True, + reg_sub=rs, + model_year_sub=[t], + weather_year_sub=selected_weatheryears, ) - / 1e3 # MW -> GW + / 1e3 ) - valid_cols = [r for r in dfprofile.columns if r in rs] - dfprofile = dfprofile[valid_cols] - - try: - df_t = dfprofile.loc[t].copy() - except KeyError: - raise KeyError(f'Model year {t} not found in load.h5 for case {casename}.') - - df_t.index = pd.to_datetime(df_t.index) - df_t = df_t.loc[ + dfprofile.index = pd.to_datetime(dfprofile.index) + dfprofile = dfprofile.loc[ str(min(case_weatheryears)) : str(max(case_weatheryears)) ] - available_wy = sorted(df_t.index.year.unique().tolist()) + available_wy = sorted(dfprofile.index.year.unique().tolist()) _validate_selected_weatheryears(selected_weatheryears, available_wy, casename, t) # Aggregate BAs to states, then average annual total across ALL available weather years - df_t_st = df_t.T.groupby(df_t.columns.map(region_to_state)).sum().T + dfprofile_st = dfprofile.T.groupby(dfprofile.columns.map(region_to_state)).sum().T wy_totals = pd.concat( - [df_t_st.loc[df_t_st.index.year == wy].sum() / 1e3 # GWh -> TWh + [dfprofile_st.loc[dfprofile_st.index.year == wy].sum() / 1e3 # GWh -> TWh for wy in available_wy], axis=1, ).mean(axis=1) From 44fbf51c373d88ce22f274a4481be20a0f784e46 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 16:01:04 -0600 Subject: [PATCH 42/58] Fix hourly demand profile y-lim --- postprocessing/compare_inputs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 96820d900..a2df14283 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -204,6 +204,8 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012): ha='left', ) ax.legend(frameon=False, loc='lower center', ncol=len(cases)) + plt.autoscale(enable=True, axis='y') + ax.set_ylim(0) return f, ax From a8a72389a59dd50f0fabe65ea94c9a27cf2529d4 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 16:01:30 -0600 Subject: [PATCH 43/58] Add regional subsetting to hourly demand profiles --- postprocessing/compare_inputs.py | 34 +++++++++++++++++++++++++++++--- postprocessing/input_plots.py | 18 ++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index a2df14283..024b782af 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -176,7 +176,8 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): return f, ax -def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012): +def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, + region=None, region_title_label=None): if len(cases) < 2: raise ValueError('Need at least 2 cases to compare inputs.') @@ -184,6 +185,7 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012): selected_label = _weatheryears_label(selected_weatheryears) yearlabel = year if year not in [0, None, 'last'] else 'last model year' wy_title_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' + region_title_label = 'CONUS' if region_title_label is None else region_title_label plt.close() f, ax = plt.subplots(figsize=(13.33, 4)) @@ -193,13 +195,13 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012): input_plots.plot_profile( casepath, datum='demand', year=year, weatheryears=selected_weatheryears, - color=color, label=casename, f=f, ax=ax, hourly=True, + reg_sub=region, color=color, label=casename, f=f, ax=ax, hourly=True, ) ax.margins(x=0) ax.grid(True, which='major', axis='y', alpha=0.3) ax.set_title( - f'Hourly demand in {yearlabel} for {wy_title_label} {selected_label} with min/max daily envelope', + f'Hourly demand in {region_title_label} in {yearlabel} for {wy_title_label} {selected_label} with min/max daily envelope', x=0, ha='left', ) @@ -795,6 +797,32 @@ def saveit(savename, **kwargs): except Exception as e: print(traceback.format_exc()) + try: + f, ax = plot_hourly_demand_profiles( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + region=['IL_MISO','IL_PJM'], + region_title_label='Illinois', + ) + saveit(f'Demand hourly profile {selected_weatheryears} IL') + except Exception as e: + print(traceback.format_exc()) + + try: + f, ax = plot_hourly_demand_profiles( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + region=['TX_ERCOT_E', 'TX_ERCOT_W', 'TX_MISO', 'TX_SPP'], + region_title_label='Texas', + ) + saveit(f'Demand hourly profile {selected_weatheryears} TX') + except Exception as e: + print(traceback.format_exc()) + try: f, ax = plot_demand_yearbymonth( cases, diff --git a/postprocessing/input_plots.py b/postprocessing/input_plots.py index 0019d7829..4a7af7a75 100644 --- a/postprocessing/input_plots.py +++ b/postprocessing/input_plots.py @@ -100,6 +100,7 @@ def plot_profile( datum='demand', year=0, weatheryears=None, + reg_sub=None, color='k', hourly=False, f=None, @@ -131,11 +132,18 @@ def plot_profile( os.path.join(case, 'inputs_case', 'load.h5'), ## Convert to GW ) / 1e3 - dfprofile = ( - dfprofile - .loc[t, [r for r in dfprofile if r in rs]] - .sum(axis=1) - ) + if reg_sub is not None: + dfprofile = ( + dfprofile + .loc[t, reg_sub] + .sum(axis=1) + ) + else: + dfprofile = ( + dfprofile + .loc[t, [r for r in dfprofile if r in rs]] + .sum(axis=1) + ) elif datum in ['temperature']: ylabel = 'Temperature [°C]' ... From a30877cd4cefc71a3523297639f67188ba3ff226 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 17:26:52 -0600 Subject: [PATCH 44/58] Fix datetime and weatheryear bugs --- postprocessing/compare_inputs.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 024b782af..8d1b73e46 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -101,16 +101,15 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): dfprofile = ( reeds.results.summarize_load_data( casepath, - use_run=True, - reg_sub=rs, + use_run=True, + agg_reg_lvl='all', model_year_sub=[year], - weather_year_sub=[weatheryear] ) / 1e3 ) # Slice to weather years range - dfprofile.index = pd.to_datetime(dfprofile.index) + dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) # Group by day of year (1-365/366) across all weather years doy = dfprofile.index.dayofyear @@ -259,7 +258,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): 1, 2, figsize=(12, 4.5), constrained_layout=True ) stats_by_case = {} - # replace most of this with reeds.results.summarize_load_data? for idx, (casename, casepath) in enumerate(cases.items()): print(f' {casename}: loading annual and peak stats by model year...') color = colors.get(casename, f'C{idx}') @@ -274,7 +272,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): casepath, use_run=True, reg_sub=rs, - weather_year_sub=selected_weatheryears, ) / 1e3 ) @@ -574,7 +571,7 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 ) / 1e3 ) - dfprofile.index = pd.to_datetime(dfprofile.index) + dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) dfprofile = dfprofile.loc[ str(min(case_weatheryears)) : str(max(case_weatheryears)) ] From 5b1948bc70659882d81b09974b09c514cd2fd0fc Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 17:38:30 -0600 Subject: [PATCH 45/58] Use summarize_load_data in plot_demand_yearbymonth --- postprocessing/compare_inputs.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 8d1b73e46..d04821ad4 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -211,25 +211,32 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, return f, ax def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): + selected_weatheryears = _parse_weatheryears(weatheryear) yearlabel = year if year not in [0, None, 'last'] else 'last model year' plt.close() for idx, (casename, casepath) in enumerate(cases.items()): color = colors.get(casename, f'C{idx}') - # replace with reeds.results.summarize_load_data? + dfprofile = ( - reeds.io.read_file(os.path.join(casepath, 'inputs_case', 'load.h5'), - parse_timestamps=True) - .loc[year].loc[str(weatheryear)].sum(axis=1) + reeds.results.summarize_load_data( + casepath, + use_run=True, + agg_reg_lvl='all', + model_year_sub=[year], + weather_year_sub=selected_weatheryears, + ) + / 1e3 ) + dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) if idx == 0: f,ax = reeds.plots.plotyearbymonth( - dfprofile.rename(casename).to_frame(), + dfprofile.rename({'load_MWh': casename}), style='line', colors=[color] ) else: reeds.plots.plotyearbymonth( - dfprofile.rename(casename).to_frame(), + dfprofile.rename({'load_MWh': casename}), style='line', colors=[color], f=f, ax=ax ) From c7a9740a465fd3a871fa5d31b93599c565a5b021 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 17:49:02 -0600 Subject: [PATCH 46/58] Clean up local variables --- postprocessing/compare_inputs.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index d04821ad4..4049f48a8 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -84,18 +84,11 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): plt.close() f, ax = plt.subplots(figsize=(12, 4.5)) x_start, x_end = None, None - # use plots.differentiate_lines instead for idx, (casename, casepath) in enumerate(cases.items()): print(f' {casename}: loading demand...') color = colors.get(casename, f'C{idx}') - - # Parse inputs - sw = reeds.io.get_switches(casepath) t = reeds.io.get_years(casepath)[-1] if year in [0, None, 'last'] else year - rs = reeds.inputs.parse_regions(casepath) - - case_weatheryears = sw.resource_adequacy_years_list # Load demand profile dfprofile = ( @@ -103,7 +96,7 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): casepath, use_run=True, agg_reg_lvl='all', - model_year_sub=[year], + model_year_sub=[t], ) / 1e3 ) @@ -216,6 +209,7 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): plt.close() for idx, (casename, casepath) in enumerate(cases.items()): + t = reeds.io.get_years(casepath)[-1] if year in [0, None, 'last'] else year color = colors.get(casename, f'C{idx}') dfprofile = ( @@ -223,7 +217,7 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): casepath, use_run=True, agg_reg_lvl='all', - model_year_sub=[year], + model_year_sub=[t], weather_year_sub=selected_weatheryears, ) / 1e3 @@ -271,14 +265,12 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): sw = reeds.io.get_switches(casepath) modelyears = reeds.io.get_years(casepath) - rs = reeds.inputs.parse_regions(casepath) case_weatheryears = sw.resource_adequacy_years_list dfprofile = ( reeds.results.summarize_load_data( casepath, use_run=True, - reg_sub=rs, ) / 1e3 ) @@ -417,7 +409,6 @@ def plot_regional_peak_demand_maps(cases, year='last'): for casename, casepath in cases.items(): print(f' {casename}: loading state-level peak demand...') sw = reeds.io.get_switches(casepath) - rs = reeds.inputs.parse_regions(casepath) case_weatheryears = sw.resource_adequacy_years_list region_to_state = reeds.io.get_hierarchy(casepath)['st'] @@ -425,7 +416,6 @@ def plot_regional_peak_demand_maps(cases, year='last'): reeds.results.summarize_load_data( casepath, use_run=True, - reg_sub=rs, model_year_sub=[t], ) / 1e3 @@ -564,7 +554,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 for casename, casepath in cases.items(): print(f' {casename}: loading state-level total demand...') sw = reeds.io.get_switches(casepath) - rs = reeds.inputs.parse_regions(casepath) case_weatheryears = sw.resource_adequacy_years_list region_to_state = reeds.io.get_hierarchy(casepath)['st'] @@ -572,7 +561,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 reeds.results.summarize_load_data( casepath, use_run=True, - reg_sub=rs, model_year_sub=[t], weather_year_sub=selected_weatheryears, ) From f2b6f1b0b1409eab75a7fdbc755e4a96895ae119 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 16 Sep 2026 17:50:27 -0600 Subject: [PATCH 47/58] cmap globals --- postprocessing/compare_inputs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 4049f48a8..81265984f 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -19,6 +19,9 @@ reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) reeds.plots.plotparams() +cmap = cmocean.cm.tempo +cmap_diff = plt.cm.RdBu_r + #%% Plotting def _parse_weatheryears(weatheryear): @@ -432,8 +435,8 @@ def plot_regional_peak_demand_maps(cases, year='last'): peak_st[casename] = df_t_st.max() ncols = len(cases) - cmap_abs = cmocean.cm.tempo - cmap_diff = plt.cm.RdBu_r + cmap_abs = cmap + cmap_diff = cmap_diff plt.close() f, ax = plt.subplots( @@ -583,8 +586,8 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 total_st[casename] = wy_totals ncols = len(cases) - cmap_abs = cmocean.cm.tempo - cmap_diff = plt.cm.RdBu_r + cmap_abs = cmap + cmap_diff = cmap_diff plt.close() f, ax = plt.subplots( From a51ca645d3b439a183c62119833f4bcdab895edd Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 10:39:02 -0600 Subject: [PATCH 48/58] Remove titles, increase fontsize --- postprocessing/compare_inputs.py | 60 +++----------------------------- 1 file changed, 5 insertions(+), 55 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 81265984f..ab6abd5ba 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -160,27 +160,16 @@ def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): ax.grid(True, which='major', axis='y', alpha=0.3) yearlabel = year if year not in [0, None, 'last'] else 'last model year' - wy_title_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' - ax.set_title( - f'Daily mean demand in {yearlabel} for {wy_title_label} {selected_label} with min/max envelope across all weather years', - x=0, - ha='left', - ) ax.legend(frameon=False, loc='lower center', ncol=len(cases)) reeds.plots.despine(ax) return f, ax -def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, - region=None, region_title_label=None): +def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, region=None,): if len(cases) < 2: raise ValueError('Need at least 2 cases to compare inputs.') selected_weatheryears = _parse_weatheryears(weatheryear) - selected_label = _weatheryears_label(selected_weatheryears) - yearlabel = year if year not in [0, None, 'last'] else 'last model year' - wy_title_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' - region_title_label = 'CONUS' if region_title_label is None else region_title_label plt.close() f, ax = plt.subplots(figsize=(13.33, 4)) @@ -195,11 +184,6 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, ax.margins(x=0) ax.grid(True, which='major', axis='y', alpha=0.3) - ax.set_title( - f'Hourly demand in {region_title_label} in {yearlabel} for {wy_title_label} {selected_label} with min/max daily envelope', - x=0, - ha='left', - ) ax.legend(frameon=False, loc='lower center', ncol=len(cases)) plt.autoscale(enable=True, axis='y') ax.set_ylim(0) @@ -237,11 +221,6 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): style='line', colors=[color], f=f, ax=ax ) - ax[0].set_title( - f'Hourly demand in {yearlabel} for weather year {weatheryear}', - x=0, - ha='left', - ) ax[0].legend(loc='upper left', bbox_to_anchor=(1,1), frameon=False, fontsize='x-large') return f, ax @@ -336,14 +315,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): ax_total.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) ax_total.set_ylabel('Annual total load [TWh]') - wy_total_label = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' - ax_total.set_title( - textwrap.fill( - f'Annual total load by model year ({wy_total_label} {selected_label}, min/max envelope)', - width=55, - ), - x=0, ha='left', - ) ax_total.legend(frameon=False, loc='best') ax_total.margins(x=0) ax_total.set_xlim(left=2020) @@ -353,13 +324,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): ax_peak.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) ax_peak.set_xlabel('Model year') ax_peak.set_ylabel('Annual peak load [GW]') - ax_peak.set_title( - textwrap.fill( - f'Annual peak load by model year ({wy_total_label} {selected_label}, min/max envelope)', - width=55, - ), - x=0, ha='left', - ) ax_peak.legend(frameon=False, loc='best') ax_peak.margins(x=0) ax_peak.set_xlim(left=2020) @@ -436,7 +400,6 @@ def plot_regional_peak_demand_maps(cases, year='last'): ncols = len(cases) cmap_abs = cmap - cmap_diff = cmap_diff plt.close() f, ax = plt.subplots( @@ -496,17 +459,12 @@ def plot_regional_peak_demand_maps(cases, year='last'): orientation='horizontal', labelpad=2.25, histratio=0., cbarwidth=0.05, cbarheight=0.85, cbarbottom=-0.05, cbarhoffset=0., + title_fontsize=18, ticklabel_fontsize=16 ) - _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=14) + _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=18) _ax.axis('off') - f.suptitle( - f'Peak demand in {t} (max across all weather years)', - fontsize=18, - y=0.9, - ) - df_peak = pd.DataFrame(peak_st).reindex(dfstates.index) return f, ax, df_peak @@ -587,7 +545,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 ncols = len(cases) cmap_abs = cmap - cmap_diff = cmap_diff plt.close() f, ax = plt.subplots( @@ -647,17 +604,12 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 orientation='horizontal', labelpad=2.25, histratio=0., cbarwidth=0.05, cbarheight=0.85, cbarbottom=-0.05, cbarhoffset=0., + title_fontsize=18, ticklabel_fontsize=16 ) - _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=14) + _ax.annotate(casename, (0.1, 1), xycoords='axes fraction', fontsize=18) _ax.axis('off') - wy_title = 'weather year' if len(selected_weatheryears) == 1 else 'weather years' - f.suptitle( - f'Annual demand in {t}, {wy_title} {wy_label}', - fontsize=18, - y=0.9, - ) df_total = pd.DataFrame(total_st).reindex(dfstates.index) return f, ax, df_total @@ -799,7 +751,6 @@ def saveit(savename, **kwargs): year=year, weatheryear=selected_weatheryears, region=['IL_MISO','IL_PJM'], - region_title_label='Illinois', ) saveit(f'Demand hourly profile {selected_weatheryears} IL') except Exception as e: @@ -812,7 +763,6 @@ def saveit(savename, **kwargs): year=year, weatheryear=selected_weatheryears, region=['TX_ERCOT_E', 'TX_ERCOT_W', 'TX_MISO', 'TX_SPP'], - region_title_label='Texas', ) saveit(f'Demand hourly profile {selected_weatheryears} TX') except Exception as e: From 4ee3aa42b824723a775c03fed919241bcb88e68f Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 10:41:35 -0600 Subject: [PATCH 49/58] Remove unused label logic --- postprocessing/compare_inputs.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index ab6abd5ba..2d1c1fe52 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -52,14 +52,6 @@ def _parse_weatheryears(weatheryear): return years - -def _weatheryears_label(weatheryears): - """Create a compact weather-year label for titles and output names.""" - if len(weatheryears) == 1: - return str(weatheryears[0]) - return ','.join("'"+str(y)[2:] for y in weatheryears) - - def _validate_selected_weatheryears(selected_years, available_years, case_label, modelyear=None): """Ensure selected weather years exist in available years for a given case/model year.""" missing = sorted(set(selected_years) - set(available_years)) @@ -192,7 +184,6 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, re def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): selected_weatheryears = _parse_weatheryears(weatheryear) - yearlabel = year if year not in [0, None, 'last'] else 'last model year' plt.close() for idx, (casename, casepath) in enumerate(cases.items()): From 7a51ecd39e055872077707a703147d5b9410436e Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 10:42:39 -0600 Subject: [PATCH 50/58] Remove plot_daily_demand_profiles --- postprocessing/compare_inputs.py | 104 ------------------------------- 1 file changed, 104 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 2d1c1fe52..9d3131787 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -64,99 +64,6 @@ def _validate_selected_weatheryears(selected_years, available_years, case_label, f'Available weather years: {sorted(available_years)}' ) -# should this be replaced with Patrick's script? -def plot_daily_demand_profiles(cases, colors, year='last', weatheryear=2012): - """ - Compare mean daily demand profiles across cases for one or more weather years. - Overlays the selected weather year profile(s) and fills envelope of min/max across all weather years. - """ - if len(cases) < 2: - raise ValueError('Need at least 2 cases to compare inputs.') - - selected_weatheryears = _parse_weatheryears(weatheryear) - selected_label = _weatheryears_label(selected_weatheryears) - - plt.close() - f, ax = plt.subplots(figsize=(12, 4.5)) - x_start, x_end = None, None - - for idx, (casename, casepath) in enumerate(cases.items()): - print(f' {casename}: loading demand...') - color = colors.get(casename, f'C{idx}') - t = reeds.io.get_years(casepath)[-1] if year in [0, None, 'last'] else year - - # Load demand profile - dfprofile = ( - reeds.results.summarize_load_data( - casepath, - use_run=True, - agg_reg_lvl='all', - model_year_sub=[t], - ) - / 1e3 - ) - - # Slice to weather years range - dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) - - # Group by day of year (1-365/366) across all weather years - doy = dfprofile.index.dayofyear - hourly_stats = dfprofile.groupby(doy).agg(['min', 'max']) - hourly_stats.columns = ['min', 'max'] - - available_years = sorted(dfprofile.index.year.unique().tolist()) - _validate_selected_weatheryears(selected_weatheryears, available_years, casename) - selected_daily_profiles = {} - for wy in selected_weatheryears: - selected_profile = dfprofile.loc[dfprofile.index.year == wy] - daily_profile = selected_profile.groupby(selected_profile.index.dayofyear).mean() - selected_daily_profiles[wy] = daily_profile.reindex(hourly_stats.index) - - # Create date x-axis for a standard year - date_range = pd.date_range('2025-01-01', periods=len(hourly_stats), freq='D') - if x_start is None: - x_start, x_end = date_range[0], date_range[-1] - - # Plot average daily profile across selected weather year(s) - avg_daily_profile = pd.concat( - [selected_daily_profiles[wy] for wy in selected_weatheryears], axis=1 - ).mean(axis=1) - ax.plot( - date_range, - avg_daily_profile.values, - lw=1.0, - color=color, - label=casename, - ) - - # Fill envelope (min/max across weather years) - ax.fill_between( - date_range, hourly_stats['max'], hourly_stats['min'], - lw=0, alpha=0.15, color=color, - ) - - # Plot min/max dotted lines - ax.plot(date_range, hourly_stats['max'], lw=0.5, linestyle=':', color=color, alpha=0.6) - ax.plot(date_range, hourly_stats['min'], lw=0.5, linestyle=':', color=color, alpha=0.6) - - ax.yaxis.set_minor_locator(mpl.ticker.AutoMinorLocator(2)) - ax.set_ylabel('Daily mean electricity demand [GW]') - ax.set_ylim(0) - - # Format x-axis dates - ax.xaxis.set_major_locator(mpl.dates.MonthLocator()) - ax.xaxis.set_major_formatter(mpl.dates.DateFormatter('%b')) - if x_start is not None and x_end is not None: - ax.set_xlim(x_start, x_end) - ax.margins(x=0) - ax.grid(True, which='major', axis='y', alpha=0.3) - - yearlabel = year if year not in [0, None, 'last'] else 'last model year' - ax.legend(frameon=False, loc='lower center', ncol=len(cases)) - reeds.plots.despine(ax) - - return f, ax - def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, region=None,): if len(cases) < 2: raise ValueError('Need at least 2 cases to compare inputs.') @@ -713,17 +620,6 @@ def saveit(savename, **kwargs): selected_weatheryears = _parse_weatheryears(args.weatheryear) weatheryear_label = _weatheryears_label(selected_weatheryears) - try: - f, ax = plot_daily_demand_profiles( - cases, - colors, - year=year, - weatheryear=selected_weatheryears, - ) - saveit(f'Demand daily profile {year}') - except Exception as e: - print(traceback.format_exc()) - try: f, ax = plot_hourly_demand_profiles( cases, From 37c224effc413e8f16104161762269a287906126 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 10:44:35 -0600 Subject: [PATCH 51/58] Remove more unused label logic --- postprocessing/compare_inputs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 9d3131787..fd7f4521b 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -133,7 +133,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): raise ValueError('Need at least 2 cases to compare inputs.') selected_weatheryears = _parse_weatheryears(weatheryear) - selected_label = _weatheryears_label(selected_weatheryears) f, (ax_total, ax_peak) = plt.subplots( 1, 2, figsize=(12, 4.5), constrained_layout=True @@ -397,7 +396,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 raise ValueError('Need at least 2 cases to compare inputs.') selected_weatheryears = _parse_weatheryears(weatheryear) - wy_label = _weatheryears_label(selected_weatheryears) basecasename = list(cases.keys())[0] basecasepath = list(cases.values())[0] @@ -618,7 +616,6 @@ def saveit(savename, **kwargs): selected_weatheryears = sw.GSw_HourlyWeatherYears else: selected_weatheryears = _parse_weatheryears(args.weatheryear) - weatheryear_label = _weatheryears_label(selected_weatheryears) try: f, ax = plot_hourly_demand_profiles( From df542f1f183ae419cc85a8e2d05a1c4741e11550 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 10:45:28 -0600 Subject: [PATCH 52/58] Remove plot_peak_and_total_load_maps helper --- postprocessing/compare_inputs.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index fd7f4521b..038e61d63 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -510,18 +510,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 df_total = pd.DataFrame(total_st).reindex(dfstates.index) return f, ax, df_total - -def plot_peak_and_total_load_maps(cases, colors, year='last', weatheryear=2012): - """ - Convenience wrapper: calls plot_regional_peak_demand_maps and - plot_regional_total_demand_maps and returns their results as a tuple. - """ - f_peak, ax_peak, df_peak = plot_regional_peak_demand_maps(cases, year=year) - f_total, ax_total, df_total = plot_regional_total_demand_maps( - cases, colors, year=year, weatheryear=weatheryear - ) - return (f_peak, f_total), (ax_peak, ax_total), (df_peak, df_total) - #%% Main From 6e5b94d971f0dac1c175358bedcac688bbfb9e12 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 11:17:22 -0600 Subject: [PATCH 53/58] Add hourly_regions dict --- postprocessing/compare_inputs.py | 41 ++++++++++++++------------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 038e61d63..a11a2ada9 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -19,6 +19,12 @@ reeds_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) reeds.plots.plotparams() +#%% Global Variables +# Define regions for which hourly demand profiles will be plotted +hourly_regions = { + 'IL':['IL_MISO','IL_PJM'], + 'TX':['TX_ERCOT_E', 'TX_ERCOT_W', 'TX_MISO', 'TX_SPP'], +} cmap = cmocean.cm.tempo cmap_diff = plt.cm.RdBu_r @@ -616,29 +622,18 @@ def saveit(savename, **kwargs): except Exception as e: print(traceback.format_exc()) - try: - f, ax = plot_hourly_demand_profiles( - cases, - colors, - year=year, - weatheryear=selected_weatheryears, - region=['IL_MISO','IL_PJM'], - ) - saveit(f'Demand hourly profile {selected_weatheryears} IL') - except Exception as e: - print(traceback.format_exc()) - - try: - f, ax = plot_hourly_demand_profiles( - cases, - colors, - year=year, - weatheryear=selected_weatheryears, - region=['TX_ERCOT_E', 'TX_ERCOT_W', 'TX_MISO', 'TX_SPP'], - ) - saveit(f'Demand hourly profile {selected_weatheryears} TX') - except Exception as e: - print(traceback.format_exc()) + for region_label, region_list in hourly_regions.items(): + try: + f, ax = plot_hourly_demand_profiles( + cases, + colors, + year=year, + weatheryear=selected_weatheryears, + region=region_list, + ) + saveit(f'Demand hourly profile {selected_weatheryears} {region_label}') + except Exception as e: + print(traceback.format_exc()) try: f, ax = plot_demand_yearbymonth( From 85d1c64aaa1cfdf9d6f251d56541547f6f44e314 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 11:21:04 -0600 Subject: [PATCH 54/58] Move <2 case check to main --- postprocessing/compare_inputs.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index a11a2ada9..15311e832 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -71,8 +71,6 @@ def _validate_selected_weatheryears(selected_years, available_years, case_label, ) def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, region=None,): - if len(cases) < 2: - raise ValueError('Need at least 2 cases to compare inputs.') selected_weatheryears = _parse_weatheryears(weatheryear) @@ -135,9 +133,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): For each model year and case, compute min/max across weather years and the value for each selected weather year. """ - if len(cases) < 2: - raise ValueError('Need at least 2 cases to compare inputs.') - selected_weatheryears = _parse_weatheryears(weatheryear) f, (ax_total, ax_peak) = plt.subplots( @@ -261,9 +256,6 @@ def plot_regional_peak_demand_maps(cases, year='last'): f, ax, df_peak df_peak has one column per case, indexed by state, values in GW. """ - if len(cases) < 2: - raise ValueError('Need at least 2 cases to compare inputs.') - basecasename = list(cases.keys())[0] basecasepath = list(cases.values())[0] @@ -398,9 +390,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 df_total has one column per case, indexed by state, values in TWh. df_total has one column per case, indexed by region, values in TWh. """ - if len(cases) < 2: - raise ValueError('Need at least 2 cases to compare inputs.') - selected_weatheryears = _parse_weatheryears(weatheryear) basecasename = list(cases.keys())[0] basecasepath = list(cases.values())[0] @@ -574,6 +563,8 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 args.basecase, titleshorten, ) + if len(cases) < 2: + raise ValueError('Need at least 2 cases to compare inputs.') firstcasepath = list(cases.values())[0] outpath = os.path.join(firstcasepath, 'outputs', 'comparisons','inputs') From c03db2d6c129e211e63d1fb6969a75ce65c98ba3 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 11:58:29 -0600 Subject: [PATCH 55/58] Fix plotyearbymonth labels --- postprocessing/compare_inputs.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 15311e832..95b0c5a5b 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -112,15 +112,14 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): / 1e3 ) dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) + dfprofile.rename(columns={'load_MWh': casename}, inplace=True) if idx == 0: f,ax = reeds.plots.plotyearbymonth( - dfprofile.rename({'load_MWh': casename}), - style='line', colors=[color] + dfprofile, style='line', colors=[color], ) else: reeds.plots.plotyearbymonth( - dfprofile.rename({'load_MWh': casename}), - style='line', colors=[color], f=f, ax=ax + dfprofile, style='line', colors=[color], f=f, ax=ax, ) ax[0].legend(loc='upper left', bbox_to_anchor=(1,1), frameon=False, fontsize='x-large') From 500bb529cd02508feba72b67ece0a421f64a4dff Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Mon, 21 Sep 2026 17:05:08 -0600 Subject: [PATCH 56/58] Errors for out-of-range model years, weather years, regions --- postprocessing/compare_inputs.py | 54 +++++++++----------------------- postprocessing/input_plots.py | 8 +++++ reeds/results.py | 25 ++++++++++----- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 95b0c5a5b..e5d5e9e77 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -58,18 +58,6 @@ def _parse_weatheryears(weatheryear): return years -def _validate_selected_weatheryears(selected_years, available_years, case_label, modelyear=None): - """Ensure selected weather years exist in available years for a given case/model year.""" - missing = sorted(set(selected_years) - set(available_years)) - if missing: - where = f' for case {case_label}' - if modelyear is not None: - where += f', model year {modelyear}' - raise ValueError( - f'Weather year(s) {missing} not found{where}. ' - f'Available weather years: {sorted(available_years)}' - ) - def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, region=None,): selected_weatheryears = _parse_weatheryears(weatheryear) @@ -98,7 +86,6 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): plt.close() for idx, (casename, casepath) in enumerate(cases.items()): - t = reeds.io.get_years(casepath)[-1] if year in [0, None, 'last'] else year color = colors.get(casename, f'C{idx}') dfprofile = ( @@ -106,7 +93,7 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): casepath, use_run=True, agg_reg_lvl='all', - model_year_sub=[t], + model_year_sub=year if year in [0, 'last'] else [year], weather_year_sub=selected_weatheryears, ) / 1e3 @@ -126,7 +113,7 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): return f, ax -def plot_peak_and_total_load(cases, colors, weatheryear=2012): +def plot_peak_and_total_load(cases, colors, weatheryear=None): """ Compare annual total load and annual peak load by model year. For each model year and case, compute min/max across weather years and @@ -149,7 +136,8 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): dfprofile = ( reeds.results.summarize_load_data( casepath, - use_run=True, + use_run=True, + weather_year_sub=selected_weatheryears, ) / 1e3 ) @@ -164,13 +152,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): annual_total_by_wy = profile.groupby(profile.index.year).sum() / 1e3 annual_peak_by_wy = profile.groupby(profile.index.year).max() - _validate_selected_weatheryears( - selected_weatheryears, - annual_total_by_wy.index.tolist(), - casename, - modelyear=t, - ) - rows.append( { 't': t, @@ -232,7 +213,7 @@ def plot_peak_and_total_load(cases, colors, weatheryear=2012): return f, (ax_total, ax_peak), stats_by_case -def plot_regional_peak_demand_maps(cases, year='last'): +def plot_regional_peak_demand_maps(cases, year='last', weatheryear=None): """ Map the max peak demand per state, comparing across cases. @@ -249,7 +230,8 @@ def plot_regional_peak_demand_maps(cases, year='last'): cases : dict {casename: casepath} year : int or 'last' Model year to plot. Defaults to the last solved year of the base case. - + weatheryear : int, list, or None + Specific weather year(s) to use. Defaults to None, which uses all available weather years. Returns ------- f, ax, df_peak @@ -258,10 +240,7 @@ def plot_regional_peak_demand_maps(cases, year='last'): basecasename = list(cases.keys())[0] basecasepath = list(cases.values())[0] - if year in [0, None, 'last']: - t = reeds.io.get_years(basecasepath)[-1] - else: - t = int(year) + selected_weatheryears = _parse_weatheryears(weatheryear) if weatheryear is not None else None dfmap = reeds.io.get_dfmap(basecasepath) dfstates = dfmap['st'] @@ -277,19 +256,15 @@ def plot_regional_peak_demand_maps(cases, year='last'): reeds.results.summarize_load_data( casepath, use_run=True, - model_year_sub=[t], + model_year_sub=year if year in [0, None, 'last'] else [year], + weather_year_sub=selected_weatheryears, ) / 1e3 ) - try: - df_t = dfprofile.loc[t].copy() - except KeyError: - raise KeyError(f'Model year {t} not found in load.h5 for case {casename}.') - - df_t.index = pd.to_datetime(df_t.index) + dfprofile.index = pd.to_datetime(dfprofile.index) # Aggregate BAs to states, then take the max simultaneous hour - df_t_st = df_t.T.groupby(df_t.columns.map(region_to_state)).sum().T + df_t_st = dfprofile.T.groupby(dfprofile.columns.map(region_to_state)).sum().T peak_st[casename] = df_t_st.max() ncols = len(cases) @@ -421,14 +396,12 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 dfprofile = dfprofile.loc[ str(min(case_weatheryears)) : str(max(case_weatheryears)) ] - available_wy = sorted(dfprofile.index.year.unique().tolist()) - _validate_selected_weatheryears(selected_weatheryears, available_wy, casename, t) # Aggregate BAs to states, then average annual total across ALL available weather years dfprofile_st = dfprofile.T.groupby(dfprofile.columns.map(region_to_state)).sum().T wy_totals = pd.concat( [dfprofile_st.loc[dfprofile_st.index.year == wy].sum() / 1e3 # GWh -> TWh - for wy in available_wy], + for wy in selected_weatheryears], axis=1, ).mean(axis=1) total_st[casename] = wy_totals @@ -650,6 +623,7 @@ def saveit(savename, **kwargs): f_peak, _, _ = plot_regional_peak_demand_maps( cases, year=year, + weatheryear=selected_weatheryears, ) saveit(f'Demand peak by region {year}') except Exception as e: diff --git a/postprocessing/input_plots.py b/postprocessing/input_plots.py index 4a7af7a75..80b819dfa 100644 --- a/postprocessing/input_plots.py +++ b/postprocessing/input_plots.py @@ -119,6 +119,8 @@ def plot_profile( ## Parse inputs sw = reeds.io.get_switches(case) t = reeds.io.get_years(case)[-1] if year in [0, None, 'last'] else year + if t not in reeds.io.get_years(case): + raise KeyError("Year {} not in the data.".format(t)) rs = reeds.inputs.parse_regions(case) if weatheryears is None: weatheryears = sw.resource_adequacy_years_list @@ -133,6 +135,9 @@ def plot_profile( ## Convert to GW ) / 1e3 if reg_sub is not None: + if not all(reg in dfprofile.columns for reg in reg_sub): + raise KeyError("Region(s) {} not in the data.".format( + " ".join([reg for reg in reg_sub if reg not in dfprofile.columns]))) dfprofile = ( dfprofile .loc[t, reg_sub] @@ -156,6 +161,9 @@ def plot_profile( ylabel = 'PV CF [%]' dfprofile = recf['upv'].squeeze(1) + if not all(wy in dfprofile.index.year.unique() for wy in weatheryears): + raise KeyError("Weather year(s) {} not in the data.".format( + " ".join([str(wy) for wy in weatheryears if wy not in dfprofile.index.year.unique()]))) dfprofile = dfprofile.loc[str(min(weatheryears)):str(max(weatheryears))].copy() ## Use a continuous set of datetimes to avoid interpolating over missing years full_timeseries = pd.date_range(dfprofile.index[0], dfprofile.index[-1], freq='h') diff --git a/reeds/results.py b/reeds/results.py index a2d785d62..34a6a9836 100644 --- a/reeds/results.py +++ b/reeds/results.py @@ -873,9 +873,11 @@ def summarize_load_data( if reg_sub is None: dfout = dfin.copy() else: - print(f"...subsettting to {reg_sub}") - dfout = dfin.loc[:, reg_sub].copy() - # TODO: add error checking for misspecified region list + print(f"...subsetting to {' '.join([reg for reg in reg_sub])}") + if not all(reg in dfin.columns for reg in reg_sub): + raise KeyError("Region(s) {} not in the data.".format( + " ".join([reg for reg in reg_sub if reg not in dfin.columns]))) + dfout = dfin.loc[:, [reg for reg in reg_sub if reg in dfin.columns]].copy() ## aggregate to desired regionality if agg_reg_lvl is None: @@ -899,7 +901,7 @@ def summarize_load_data( hierarchy = pd.read_csv(hierarchy_file_path) # set up region mapping if agg_reg_lvl not in hierarchy.columns: - print(f"ERROR: {agg_reg_lvl} is not a column in hierarchy file. Skipping aggregation.") + raise UserWarning(f"{agg_reg_lvl} is not a column in hierarchy file. Skipping aggregation.") else: region_map = hierarchy[[reg_col, agg_reg_lvl]].set_index(reg_col).squeeze() @@ -911,15 +913,19 @@ def summarize_load_data( # subset to desired model years if model_year_sub is not None: - print(f"...subsetting to {model_year_sub} model years") - # TODO: implement 'last' option, convert int to list of last value - # TODO: warning on missing values? + print(f"...subsetting to {' '.join([str(my) for my in model_year_sub])} model years") + model_year_sub = reeds.io.get_years(casepath)[-1] if model_year_sub in [0,'last'] else model_year_sub + if isinstance(model_year_sub, int): + model_year_sub = [model_year_sub] my_vals = dfout.index.get_level_values('year') + if not all(my in my_vals for my in model_year_sub): + raise KeyError("Model year(s) {} not in the data.".format( + " ".join([str(my) for my in model_year_sub if my not in my_vals]))) dfout = dfout[my_vals.isin(model_year_sub)] ## subset to desired weather years if weather_year_sub is not None: - print(f"...subsetting to {weather_year_sub} weather years") + print(f"...subsetting to {' '.join([str(wy) for wy in weather_year_sub])} weather years") # temporarily reset index to slice idx_names = dfout.index.names dfout = dfout.reset_index() @@ -930,6 +936,9 @@ def summarize_load_data( elif not isinstance(weather_year_sub, list): print("'weather_year_sub' only accepts int or list.") raise Exception + if not all(wy in dfout['weather_year'].unique() for wy in weather_year_sub): + raise KeyError("Weather year(s) {} not in the data.".format( + " ".join([str(wy) for wy in weather_year_sub if wy not in dfout['weather_year'].unique()]))) dfout = dfout.loc[dfout.weather_year.isin(weather_year_sub)] # reset index dfout = dfout.drop('weather_year', axis=1).set_index(idx_names) From 21238596f28f0017126fea3aa07505f7ec1c91ee Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Tue, 22 Sep 2026 15:43:34 -0600 Subject: [PATCH 57/58] Update docstrings --- postprocessing/compare_inputs.py | 112 ++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index e5d5e9e77..3812d1c75 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -31,7 +31,20 @@ #%% Plotting def _parse_weatheryears(weatheryear): - """Parse a weather-year input into a sorted list of unique integer years.""" + """ + Parse a weather-year input into a sorted list of unique integer years. + + Parameters + ---------- + weatheryear : int, str, or iterable of int + The weather year(s) to parse. Can be a single integer, a string with + years separated by underscores or spaces, or an iterable of integers. + + Returns + ------- + list of int + Sorted list of unique weather years. + """ if isinstance(weatheryear, int): return [weatheryear] @@ -59,7 +72,30 @@ def _parse_weatheryears(weatheryear): return years def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, region=None,): + ''' + Plot hourly demand profiles for the specified cases and region + using reeds.input_plots.plot_profile(). + + Parameters + ---------- + cases : dict + Dictionary of case names and their corresponding paths. + colors : dict + Dictionary of case names and their corresponding colors. + year : int or str, optional + Model year to plot. Default is 'last'. + weatheryear : int, str, or iterable of int, optional + Weather year(s) to plot. Default is 2012. + region : str, optional + Region for which to plot the demand profiles. Default is None. + Returns + ------- + matplotlib.figure.Figure + The figure object containing the plot. + matplotlib.axes.Axes + The axes object containing the plot. + ''' selected_weatheryears = _parse_weatheryears(weatheryear) plt.close() @@ -82,6 +118,27 @@ def plot_hourly_demand_profiles(cases, colors, year='last', weatheryear=2012, re return f, ax def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): + ''' + Plot demand profiles in rows by month for the specified cases. + + Parameters + ---------- + cases : dict + Dictionary of case names and their corresponding paths. + colors : dict + Dictionary of case names and their corresponding colors. + year : int or str, optional + Model year to plot. Default is 'last'. + weatheryear : int, str, or iterable of int, optional + Weather year(s) to plot. Default is 2012. + + Returns + ------- + matplotlib.figure.Figure + The figure object containing the plot. + matplotlib.axes.Axes + The axes object containing the plot. + ''' selected_weatheryears = _parse_weatheryears(weatheryear) plt.close() @@ -115,9 +172,26 @@ def plot_demand_yearbymonth(cases, colors, year='last', weatheryear=2012): def plot_peak_and_total_load(cases, colors, weatheryear=None): """ - Compare annual total load and annual peak load by model year. - For each model year and case, compute min/max across weather years and - the value for each selected weather year. + Plots annual total load and annual peak load by model year + for the specified cases, including range across all weather years. + + Parameters + ---------- + cases : dict + Dictionary of case names and their corresponding paths. + colors : dict + Dictionary of case names and their corresponding colors. + weatheryear : int, str, or iterable of int, optional + Weather year(s) to plot with a line. Default is None. + + Returns + ------- + matplotlib.figure.Figure + The figure object containing the plot. + matplotlib.axes.Axes + The axes object containing the plot. + pandas.DataFrame + The statistics by case, indexed by model year. """ selected_weatheryears = _parse_weatheryears(weatheryear) @@ -215,11 +289,8 @@ def plot_peak_and_total_load(cases, colors, weatheryear=None): def plot_regional_peak_demand_maps(cases, year='last', weatheryear=None): """ - Map the max peak demand per state, comparing across cases. - - For each state, peak demand is the highest simultaneous (summed across - BAs in that state) hourly value across ALL available weather years for - the given model year. + Maps peak demand by state across specified weather years (default all available), + comparing across cases. Layout: 1 row x N-cases columns. Base case (first case): absolute peak demand [GW] with a sequential colormap. @@ -234,8 +305,12 @@ def plot_regional_peak_demand_maps(cases, year='last', weatheryear=None): Specific weather year(s) to use. Defaults to None, which uses all available weather years. Returns ------- - f, ax, df_peak - df_peak has one column per case, indexed by state, values in GW. + matplotlib.figure.Figure + The figure object containing the plot. + matplotlib.axes.Axes + The axes object containing the plot. + pandas.DataFrame + The peak demand by state for each case, indexed by state, values in GW. """ basecasename = list(cases.keys())[0] basecasepath = list(cases.values())[0] @@ -338,9 +413,9 @@ def plot_regional_peak_demand_maps(cases, year='last', weatheryear=None): return f, ax, df_peak -def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012): +def plot_regional_total_demand_maps(cases, year='last', weatheryear=2012): """ - Map total annual demand per state, comparing across cases. + Maps total annual demand by state, comparing across cases. For each state, total demand is summed across all BAs in that state over the selected weather year(s) and averaged if more than one year is given. @@ -352,7 +427,6 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 Parameters ---------- cases : dict {casename: casepath} - colors : dict {casename: color} (unused in maps but kept for API parity) year : int or 'last' Model year to plot. Defaults to the last solved year of the base case. weatheryear : int, str, or iterable @@ -360,9 +434,12 @@ def plot_regional_total_demand_maps(cases, colors, year='last', weatheryear=2012 Returns ------- - f, ax, df_total - df_total has one column per case, indexed by state, values in TWh. - df_total has one column per case, indexed by region, values in TWh. + matplotlib.figure.Figure + The figure object containing the plot. + matplotlib.axes.Axes + The axes object containing the plot. + pandas.DataFrame + The total demand by state for each case, indexed by state, values in TWh. """ selected_weatheryears = _parse_weatheryears(weatheryear) basecasename = list(cases.keys())[0] @@ -632,7 +709,6 @@ def saveit(savename, **kwargs): try: f_total, _, _ = plot_regional_total_demand_maps( cases, - colors, year=year, weatheryear=selected_weatheryears, ) From cf75d2c76ef922d88ef447ecdaf141f0168eaab0 Mon Sep 17 00:00:00 2001 From: clairehalloran Date: Wed, 23 Sep 2026 14:47:19 -0600 Subject: [PATCH 58/58] Fix small bugs --- postprocessing/compare_inputs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/postprocessing/compare_inputs.py b/postprocessing/compare_inputs.py index 3812d1c75..2e20d9485 100644 --- a/postprocessing/compare_inputs.py +++ b/postprocessing/compare_inputs.py @@ -211,7 +211,6 @@ def plot_peak_and_total_load(cases, colors, weatheryear=None): reeds.results.summarize_load_data( casepath, use_run=True, - weather_year_sub=selected_weatheryears, ) / 1e3 ) @@ -219,7 +218,7 @@ def plot_peak_and_total_load(cases, colors, weatheryear=None): rows = [] for t in modelyears: profile = dfprofile.loc[t].sum(axis=1) - profile.index = pd.to_datetime(profile.index) + profile.index = pd.to_datetime(profile.index.get_level_values('datetime')) # profile is in GW and timestamps are hourly, so annual sum is GWh. # Convert to TWh for plotting. @@ -337,7 +336,7 @@ def plot_regional_peak_demand_maps(cases, year='last', weatheryear=None): / 1e3 ) - dfprofile.index = pd.to_datetime(dfprofile.index) + dfprofile.index = pd.to_datetime(dfprofile.index.get_level_values('datetime')) # Aggregate BAs to states, then take the max simultaneous hour df_t_st = dfprofile.T.groupby(dfprofile.columns.map(region_to_state)).sum().T peak_st[casename] = df_t_st.max()