From 696793eb75a67ed44bc9f9eabb5ec77f77bb988f Mon Sep 17 00:00:00 2001 From: Michael Bino Date: Mon, 16 Feb 2026 11:54:34 -0500 Subject: [PATCH 1/4] Add pandas 2.0+ frequency alias compatibility - Create vatic/compat.py with helper functions that handle both old ('H') and new ('h') hourly frequency aliases - Update loaders.py and stats_manager.py to use compatibility helpers - Pin pandas>=2.1 in setup.py while maintaining backward compatibility via try/except fallback in the helpers --- setup.py | 2 +- vatic/compat.py | 29 +++++++++++++++++++++++++++++ vatic/data/loaders.py | 14 ++++++++------ vatic/stats_manager.py | 3 ++- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 vatic/compat.py diff --git a/setup.py b/setup.py index d8a5c5f..ece120a 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ python_requires='>=3.8,<3.12', install_requires=[ - 'numpy>1.21', 'pandas', 'scipy', 'dill', 'matplotlib', + 'numpy>1.21', 'pandas>=2.1', 'scipy', 'dill', 'matplotlib', 'pyomo>=6', 'gurobipy', 'gridx-egret @ git+https://github.com/shrivats-pu/Egret.git' ], diff --git a/vatic/compat.py b/vatic/compat.py new file mode 100644 index 0000000..a374dda --- /dev/null +++ b/vatic/compat.py @@ -0,0 +1,29 @@ +"""Compatibility utilities for different dependency versions.""" + +import pandas as pd + + +def hourly_resample(df_or_series): + """Resample to hourly frequency with backward compatibility. + + pandas 2.2+ requires lowercase frequency aliases ('h'), + while older versions use uppercase ('H'). + """ + try: + return df_or_series.resample('h') + except ValueError: + return df_or_series.resample('H') + + +def hourly_freq(): + """Return the hourly frequency string compatible with installed pandas. + + pandas 2.2+ requires lowercase frequency aliases ('h'), + while older versions use uppercase ('H'). + """ + try: + # Test if lowercase works + pd.date_range(start='2020-01-01', periods=1, freq='h') + return 'h' + except ValueError: + return 'H' diff --git a/vatic/data/loaders.py b/vatic/data/loaders.py index 8b00e4f..aad5ccc 100644 --- a/vatic/data/loaders.py +++ b/vatic/data/loaders.py @@ -33,6 +33,8 @@ import pandas as pd from datetime import datetime +from vatic.compat import hourly_resample + import os _ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -566,8 +568,8 @@ def load_by_bus(self, load_fcsts = self.get_forecasts('Load', start_date, end_date) if load_actls is None: - load_actls = self.get_actuals( - 'Load', start_date, end_date).resample('H').mean() + load_actls = hourly_resample( + self.get_actuals('Load', start_date, end_date)).mean() site_dfs = dict() for zone, zone_df in self.bus_df.groupby('Area'): @@ -628,8 +630,8 @@ def create_timeseries( for asset_type in self.timeseries_cohorts: gen_fcsts = self.get_forecasts(asset_type, start_date, end_date) - gen_actls = self.get_actuals( - asset_type, start_date, end_date).resample('H').mean() + gen_actls = hourly_resample( + self.get_actuals(asset_type, start_date, end_date)).mean() gen_fcsts.columns = pd.MultiIndex.from_tuples( [('fcst', asset_name) for asset_name in gen_fcsts.columns]) @@ -714,8 +716,8 @@ def create_scenario_timeseries( gen_df = pd.concat([gen_df, gen_scens], axis=1) for asset_type in self.no_scenario_renews: - new_actuals = self.get_actuals( - asset_type, start_date, end_date).resample('H').mean() + new_actuals = hourly_resample( + self.get_actuals(asset_type, start_date, end_date)).mean() for asset_name, asset_actuals in new_actuals.iteritems(): gen_df['actl', asset_name] = asset_actuals diff --git a/vatic/stats_manager.py b/vatic/stats_manager.py index 3a27733..f167dcc 100644 --- a/vatic/stats_manager.py +++ b/vatic/stats_manager.py @@ -12,6 +12,7 @@ from .model_data import VaticModelData from .time_manager import VaticTime +from .compat import hourly_freq import matplotlib.pyplot as plt import seaborn as sns @@ -603,7 +604,7 @@ def generate_commitment_heatmaps(self) -> None: t.strftime('%m/%d\n%-I%p') if i % 6 == 0 else "" for i, t in enumerate(pd.date_range(start=ruc_time.when, periods=commits.shape[1], - freq='H')) + freq=hourly_freq())) ] ylbls = [ From c4a1baae06715c9da1688f78a510509d5c115420 Mon Sep 17 00:00:00 2001 From: Michael Bino Date: Mon, 16 Feb 2026 12:53:22 -0500 Subject: [PATCH 2/4] Fix pandas Series positional indexing with .iloc Use .iloc[-1] instead of [-1] for accessing the last element of a pandas Series, as newer pandas versions interpret [-1] as a label lookup rather than positional indexing. --- vatic/data_providers.py | 2 +- vatic/stats_manager.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vatic/data_providers.py b/vatic/data_providers.py index d803ce1..71226af 100644 --- a/vatic/data_providers.py +++ b/vatic/data_providers.py @@ -665,7 +665,7 @@ def _get_model_for_date(self, # we can just copy the final value of the first day else: day_data['MaxNondispatchablePower'].update({ - (gen, i): float(gen_data[gen][-1]) for i in range(25, 49)}) + (gen, i): float(gen_data[gen].iloc[-1]) for i in range(25, 49)}) # for dispatchable renewables, minimum output is always zero for gen in self.template['DispatchRenewables']: diff --git a/vatic/stats_manager.py b/vatic/stats_manager.py index f167dcc..4f29237 100644 --- a/vatic/stats_manager.py +++ b/vatic/stats_manager.py @@ -387,17 +387,17 @@ def save_output(self, sim_runtime=None) -> dict[str, pd.DataFrame]: tgen_gby = report_dfs['thermal_detail'].groupby('Generator') # get the final output for each generator - final_dispatch = tgen_gby.apply(lambda x: round(x['Dispatch'][-1], 2)) + final_dispatch = tgen_gby.apply(lambda x: round(x['Dispatch'].iloc[-1], 2)) # get the final on/off state for each generator final_bool = tgen_gby.apply( - lambda x: (x['Unit State'][-1] * 2 - 1)) + lambda x: (x['Unit State'].iloc[-1] * 2 - 1)) # find how long it has been since each generator was in a state not # matching its final state, combine this info with final on/off last_conds = tgen_gby.apply( lambda x: (x['Unit State'] - != x['Unit State'][-1])[::-1].argmax() + != x['Unit State'].iloc[-1])[::-1].argmax() ) * final_bool # for generators which were on or off for the entire simulation From f1fd86735793d543b05765d1c4228d86007d8be9 Mon Sep 17 00:00:00 2001 From: Michael Bino Date: Mon, 16 Feb 2026 12:58:53 -0500 Subject: [PATCH 3/4] Lower pandas requirement to >=1.0 for Python 3.8 support pandas 2.1+ requires Python 3.9+, so we can't pin >=2.1 while supporting Python 3.8. The compatibility helpers handle both old and new pandas versions, so a lower minimum is safe. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ece120a..2cb839c 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ python_requires='>=3.8,<3.12', install_requires=[ - 'numpy>1.21', 'pandas>=2.1', 'scipy', 'dill', 'matplotlib', + 'numpy>1.21', 'pandas>=1.0', 'scipy', 'dill', 'matplotlib', 'pyomo>=6', 'gurobipy', 'gridx-egret @ git+https://github.com/shrivats-pu/Egret.git' ], From 304ff64cde738a997048a4892e37c4b8edfd902e Mon Sep 17 00:00:00 2001 From: Michael Bino Date: Mon, 16 Feb 2026 13:40:52 -0500 Subject: [PATCH 4/4] Update ref job to ubuntu-22.04 for better runner availability --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05e0cf4..d76e6b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: jobs: ref: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3