From 57e5674e4a712db60d99d814fad6fa26d2e84220 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:43:09 -0800 Subject: [PATCH 01/10] Note https://github.com/NatLabRockies/GEOPHIRES-X/issues/458\?title\=SAM+Economic+Models+do+not+calculate+carbon+revenue in SAM-EM Limitations section --- docs/SAM-Economic-Models.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index 326679507..b6a70f79a 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -51,6 +51,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM 1. Only Electricity end-use is supported 2. Add-ons with electricity and heat are not currently supported. (Add-ons CAPEX, OPEX, and profit are supported.) +3. Carbon Revenue is not currently supported, but will be in future releases. See [tracking issue](https://github.com/NatLabRockies/GEOPHIRES-X/issues/458?title=SAM+Economic+Models+do+not+calculate+carbon+revenue) for details. ## Multiple Construction Years From f89d5d97d08a762817f746c904901a94fe400d54 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:03:51 -0800 Subject: [PATCH 02/10] WIP - insert backfilled annual costs and electricity to grid rows in preparation to add backfilled PV/LCOE rows --- src/geophires_x/EconomicsSam.py | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index af12e2db9..2dd52f07b 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -184,6 +184,9 @@ def _get_row(row_name__: str) -> list[Any]: if self._royalties_rate_schedule is not None: ret = self._insert_royalties_rate_schedule(ret) + # FIXME WIP + ret = self._insert_calculated_levelized_metrics_line_items(ret) + return ret def _insert_royalties_rate_schedule(self, cf_ret: list[list[Any]]) -> list[list[Any]]: @@ -206,6 +209,61 @@ def _get_row_index(row_name_: str) -> list[Any]: return ret + # noinspection DuplicatedCode + def _insert_calculated_levelized_metrics_line_items(self, cf_ret: list[list[Any]]) -> list[list[Any]]: + """ + FIXME WIP re: https://github.com/NatLabRockies/GEOPHIRES-X/issues/444#issuecomment-3730443078 + """ + + ret = cf_ret.copy() + + def _get_row_index(row_name_: str) -> list[Any]: + return [it[0] for it in ret].index(row_name_) + + annual_costs_usd_row_name = 'Annual costs ($)' + annual_costs = cf_ret[_get_row_index(annual_costs_usd_row_name)].copy() + after_tax_net_cash_flow_usd = cf_ret[_get_row_index('After-tax net cash flow ($)')] + + annual_costs_backfilled = [ + *after_tax_net_cash_flow_usd[1 : (self._pre_revenue_years_count + 1)], + *annual_costs[(self._pre_revenue_years_count + 1) :], + ] + + ret.insert( + _get_row_index(annual_costs_usd_row_name) + 1, + [ + *['Annual costs [backfilled] ($)'], + *annual_costs_backfilled, + ], + ) + + electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' + electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() + electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] + + ret.insert( + # _get_row_index(electricity_to_grid_kwh_row_name), + _get_row_index(annual_costs_usd_row_name) + 4, + [ + *['Electricity to grid [backfilled] (kWh)'], + *electricity_to_grid_backfilled, + ], + ) + + # ret.insert( + # _get_row_index('Present value of annual costs ($)'), + # [ + # *['Present value of annual costs [backfilled] ($)'], + # *([''] * (self._pre_revenue_years_count)), + # *[ + # quantity(it, 'dimensionless').to(convertible_unit('percent')).magnitude + # for it in self._royalties_rate_schedule + # ], + # ], + # ) + + return ret + @property def sam_after_tax_net_cash_flow_all_years(self) -> list[float]: return _after_tax_net_cash_flow_all_years(self.sam_cash_flow_profile, self._pre_revenue_years_count) From f00e42c2d0e92626820ce099aa470fa9d30bd887 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:15:38 -0800 Subject: [PATCH 03/10] Present value of annual costs [backfilled] ($) line item (still WIP) --- src/geophires_x/EconomicsSam.py | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 2dd52f07b..8c32bcab0 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -242,7 +242,7 @@ def _get_row_index(row_name_: str) -> list[Any]: electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] ret.insert( - # _get_row_index(electricity_to_grid_kwh_row_name), + # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name _get_row_index(annual_costs_usd_row_name) + 4, [ *['Electricity to grid [backfilled] (kWh)'], @@ -250,17 +250,30 @@ def _get_row_index(row_name_: str) -> list[Any]: ], ) - # ret.insert( - # _get_row_index('Present value of annual costs ($)'), - # [ - # *['Present value of annual costs [backfilled] ($)'], - # *([''] * (self._pre_revenue_years_count)), - # *[ - # quantity(it, 'dimensionless').to(convertible_unit('percent')).magnitude - # for it in self._royalties_rate_schedule - # ], - # ], - # ) + annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() + pv_of_annual_costs_backfilled = [] + for year in range(self._pre_revenue_years_count): + pv_at_year = abs( + round( + npf.npv( + self.nominal_discount_rate.quantity().to('dimensionless').magnitude, + annual_costs_backfilled_pv_processed, + ) + ) + ) + + pv_of_annual_costs_backfilled.append(pv_at_year) + + cost_at_year = annual_costs_backfilled_pv_processed.pop(0) + annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year + + ret.insert( + _get_row_index('Present value of annual costs ($)') + 1, + [ + *['Present value of annual costs [backfilled] ($)'], + *pv_of_annual_costs_backfilled, + ], + ) return ret From 28725162ac66d13aab9198fb3e4fa6f1dfa95616 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:20:47 -0800 Subject: [PATCH 04/10] minor code cleanup/readability refactor --- src/geophires_x/EconomicsSam.py | 68 ++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 8c32bcab0..c37dfec74 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -237,43 +237,49 @@ def _get_row_index(row_name_: str) -> list[Any]: ], ) - electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' - electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() - electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] - - ret.insert( - # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name - _get_row_index(annual_costs_usd_row_name) + 4, - [ - *['Electricity to grid [backfilled] (kWh)'], - *electricity_to_grid_backfilled, - ], - ) + def backfill_electricity_to_grid() -> None: + electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' + electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() + electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] + + ret.insert( + # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name + _get_row_index(annual_costs_usd_row_name) + 4, + [ + *['Electricity to grid [backfilled] (kWh)'], + *electricity_to_grid_backfilled, + ], + ) - annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() - pv_of_annual_costs_backfilled = [] - for year in range(self._pre_revenue_years_count): - pv_at_year = abs( - round( - npf.npv( - self.nominal_discount_rate.quantity().to('dimensionless').magnitude, - annual_costs_backfilled_pv_processed, + backfill_electricity_to_grid() + + def backfill_pv_of_annual_costs() -> None: + annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() + pv_of_annual_costs_backfilled = [] + for year in range(self._pre_revenue_years_count): + pv_at_year = abs( + round( + npf.npv( + self.nominal_discount_rate.quantity().to('dimensionless').magnitude, + annual_costs_backfilled_pv_processed, + ) ) ) - ) - pv_of_annual_costs_backfilled.append(pv_at_year) + pv_of_annual_costs_backfilled.append(pv_at_year) - cost_at_year = annual_costs_backfilled_pv_processed.pop(0) - annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year + cost_at_year = annual_costs_backfilled_pv_processed.pop(0) + annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year - ret.insert( - _get_row_index('Present value of annual costs ($)') + 1, - [ - *['Present value of annual costs [backfilled] ($)'], - *pv_of_annual_costs_backfilled, - ], - ) + ret.insert( + _get_row_index('Present value of annual costs ($)') + 1, + [ + *['Present value of annual costs [backfilled] ($)'], + *pv_of_annual_costs_backfilled, + ], + ) + + backfill_pv_of_annual_costs() return ret From 271be5288fc0e16a7fb54ef2e0182ca52d581593 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:24:51 -0800 Subject: [PATCH 05/10] Present value of annual energy nominal [backfilled] (kWh) line item (still WIP) --- src/geophires_x/EconomicsSam.py | 55 ++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index c37dfec74..4e2c4d26a 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -237,21 +237,18 @@ def _get_row_index(row_name_: str) -> list[Any]: ], ) - def backfill_electricity_to_grid() -> None: - electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' - electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() - electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] + electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' + electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() + electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] - ret.insert( - # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name - _get_row_index(annual_costs_usd_row_name) + 4, - [ - *['Electricity to grid [backfilled] (kWh)'], - *electricity_to_grid_backfilled, - ], - ) - - backfill_electricity_to_grid() + ret.insert( + # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name + _get_row_index(annual_costs_usd_row_name) + 4, + [ + *['Electricity to grid [backfilled] (kWh)'], + *electricity_to_grid_backfilled, + ], + ) def backfill_pv_of_annual_costs() -> None: annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() @@ -281,6 +278,36 @@ def backfill_pv_of_annual_costs() -> None: backfill_pv_of_annual_costs() + def backfill_pv_of_annual_energy() -> None: + electricity_to_grid_backfilled_pv_processed = electricity_to_grid_backfilled.copy() + pv_of_electricity_to_grid_backfilled = [] + for year in range(self._pre_revenue_years_count): + pv_at_year = abs( + round( + npf.npv( + self.nominal_discount_rate.quantity().to('dimensionless').magnitude, + electricity_to_grid_backfilled_pv_processed, + ) + ) + ) + + pv_of_electricity_to_grid_backfilled.append(pv_at_year) + + electricity_to_grid_at_year = electricity_to_grid_backfilled_pv_processed.pop(0) + electricity_to_grid_backfilled_pv_processed[0] = ( + electricity_to_grid_backfilled_pv_processed[0] + electricity_to_grid_at_year + ) + + ret.insert( + _get_row_index('Present value of annual energy nominal (kWh)') + 1, + [ + *['Present value of annual energy nominal [backfilled] (kWh)'], + *pv_of_electricity_to_grid_backfilled, + ], + ) + + backfill_pv_of_annual_energy() + return ret @property From b67cf570493c8643457ae1483c66eda9cc7f9523 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:33:31 -0800 Subject: [PATCH 06/10] LCOE Levelized cost of energy nominal [backfilled] (cents/kWh) line item --- src/geophires_x/EconomicsSam.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 4e2c4d26a..9ed74624d 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -250,6 +250,8 @@ def _get_row_index(row_name_: str) -> list[Any]: ], ) + pv_of_annual_costs_backfilled_row_name = 'Present value of annual costs [backfilled] ($)' + def backfill_pv_of_annual_costs() -> None: annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() pv_of_annual_costs_backfilled = [] @@ -271,13 +273,15 @@ def backfill_pv_of_annual_costs() -> None: ret.insert( _get_row_index('Present value of annual costs ($)') + 1, [ - *['Present value of annual costs [backfilled] ($)'], + *[pv_of_annual_costs_backfilled_row_name], *pv_of_annual_costs_backfilled, ], ) backfill_pv_of_annual_costs() + pv_of_electricity_to_grid_backfilled_row_name = 'Present value of annual energy nominal [backfilled] (kWh)' + def backfill_pv_of_annual_energy() -> None: electricity_to_grid_backfilled_pv_processed = electricity_to_grid_backfilled.copy() pv_of_electricity_to_grid_backfilled = [] @@ -301,13 +305,35 @@ def backfill_pv_of_annual_energy() -> None: ret.insert( _get_row_index('Present value of annual energy nominal (kWh)') + 1, [ - *['Present value of annual energy nominal [backfilled] (kWh)'], + *[pv_of_electricity_to_grid_backfilled_row_name], *pv_of_electricity_to_grid_backfilled, ], ) backfill_pv_of_annual_energy() + def backfill_lcoe_nominal() -> None: + pv_of_annual_costs_backfilled_row = ret[_get_row_index(pv_of_annual_costs_backfilled_row_name)][1:] + pv_of_electricity_to_grid_backfilled_row = ret[ + _get_row_index(pv_of_electricity_to_grid_backfilled_row_name) + ][1:] + + lcoe_nominal_backfilled = [] + for year in range(len(pv_of_annual_costs_backfilled_row)): + lcoe_nominal_backfilled.append( + pv_of_annual_costs_backfilled_row[year] * 100 / pv_of_electricity_to_grid_backfilled_row[year] + ) + + ret.insert( + _get_row_index('LCOE Levelized cost of energy nominal (cents/kWh)') + 1, + [ + *['LCOE Levelized cost of energy nominal [backfilled] (cents/kWh)'], + *lcoe_nominal_backfilled, + ], + ) + + backfill_lcoe_nominal() + return ret @property From 1780d6424b6259ad301638fea630a3075f261295 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:24:15 -0800 Subject: [PATCH 07/10] backfill native rows instead of adding separate backfilled rows --- src/geophires_x/EconomicsSam.py | 152 ++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 66 deletions(-) diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 9ed74624d..7183f4ded 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -215,11 +215,14 @@ def _insert_calculated_levelized_metrics_line_items(self, cf_ret: list[list[Any] FIXME WIP re: https://github.com/NatLabRockies/GEOPHIRES-X/issues/444#issuecomment-3730443078 """ + insert_backfilled_rows: bool = False + ret = cf_ret.copy() def _get_row_index(row_name_: str) -> list[Any]: return [it[0] for it in ret].index(row_name_) + # Backfill annual costs annual_costs_usd_row_name = 'Annual costs ($)' annual_costs = cf_ret[_get_row_index(annual_costs_usd_row_name)].copy() after_tax_net_cash_flow_usd = cf_ret[_get_row_index('After-tax net cash flow ($)')] @@ -229,108 +232,125 @@ def _get_row_index(row_name_: str) -> list[Any]: *annual_costs[(self._pre_revenue_years_count + 1) :], ] - ret.insert( - _get_row_index(annual_costs_usd_row_name) + 1, - [ - *['Annual costs [backfilled] ($)'], - *annual_costs_backfilled, - ], - ) + if insert_backfilled_rows: + ret.insert( + _get_row_index(annual_costs_usd_row_name) + 1, + [ + *['Annual costs [backfilled] ($)'], + *annual_costs_backfilled, + ], + ) electricity_to_grid_kwh_row_name = 'Electricity to grid (kWh)' electricity_to_grid = cf_ret[_get_row_index(electricity_to_grid_kwh_row_name)].copy() electricity_to_grid_backfilled = [0 if it == '' else it for it in electricity_to_grid[1:]] - ret.insert( - # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name - _get_row_index(annual_costs_usd_row_name) + 4, - [ - *['Electricity to grid [backfilled] (kWh)'], - *electricity_to_grid_backfilled, - ], - ) + if insert_backfilled_rows: + ret.insert( + # _get_row_index(electricity_to_grid_kwh_row_name), # there are multiple rows with this name + _get_row_index(annual_costs_usd_row_name) + 4, + [ + *['Electricity to grid [backfilled] (kWh)'], + *electricity_to_grid_backfilled, + ], + ) + else: + pass # TODO: update existing row (requires finding the right row among duplicates) pv_of_annual_costs_backfilled_row_name = 'Present value of annual costs [backfilled] ($)' - def backfill_pv_of_annual_costs() -> None: - annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() - pv_of_annual_costs_backfilled = [] - for year in range(self._pre_revenue_years_count): - pv_at_year = abs( - round( - npf.npv( - self.nominal_discount_rate.quantity().to('dimensionless').magnitude, - annual_costs_backfilled_pv_processed, - ) + # Backfill PV of annual costs + annual_costs_backfilled_pv_processed = annual_costs_backfilled.copy() + pv_of_annual_costs_backfilled = [] + for year in range(self._pre_revenue_years_count): + pv_at_year = abs( + round( + npf.npv( + self.nominal_discount_rate.quantity().to('dimensionless').magnitude, + annual_costs_backfilled_pv_processed, ) ) + ) + + pv_of_annual_costs_backfilled.append(pv_at_year) - pv_of_annual_costs_backfilled.append(pv_at_year) + cost_at_year = annual_costs_backfilled_pv_processed.pop(0) + annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year - cost_at_year = annual_costs_backfilled_pv_processed.pop(0) - annual_costs_backfilled_pv_processed[0] = annual_costs_backfilled_pv_processed[0] + cost_at_year + pv_of_annual_costs_backfilled_row = [ + *[pv_of_annual_costs_backfilled_row_name], + *pv_of_annual_costs_backfilled, + ] + pv_of_annual_costs_row_index = _get_row_index('Present value of annual costs ($)') + if insert_backfilled_rows: ret.insert( - _get_row_index('Present value of annual costs ($)') + 1, - [ - *[pv_of_annual_costs_backfilled_row_name], - *pv_of_annual_costs_backfilled, - ], + pv_of_annual_costs_row_index + 1, + pv_of_annual_costs_backfilled_row, ) + else: + ret[pv_of_annual_costs_row_index][1:] = pv_of_annual_costs_backfilled - backfill_pv_of_annual_costs() - + # Backfill PV of electricity to grid pv_of_electricity_to_grid_backfilled_row_name = 'Present value of annual energy nominal [backfilled] (kWh)' - def backfill_pv_of_annual_energy() -> None: - electricity_to_grid_backfilled_pv_processed = electricity_to_grid_backfilled.copy() - pv_of_electricity_to_grid_backfilled = [] - for year in range(self._pre_revenue_years_count): - pv_at_year = abs( - round( - npf.npv( - self.nominal_discount_rate.quantity().to('dimensionless').magnitude, - electricity_to_grid_backfilled_pv_processed, - ) + electricity_to_grid_backfilled_pv_processed = electricity_to_grid_backfilled.copy() + pv_of_electricity_to_grid_backfilled = [] + for year in range(self._pre_revenue_years_count): + pv_at_year = abs( + round( + npf.npv( + self.nominal_discount_rate.quantity().to('dimensionless').magnitude, + electricity_to_grid_backfilled_pv_processed, ) ) + ) - pv_of_electricity_to_grid_backfilled.append(pv_at_year) + pv_of_electricity_to_grid_backfilled.append(pv_at_year) - electricity_to_grid_at_year = electricity_to_grid_backfilled_pv_processed.pop(0) - electricity_to_grid_backfilled_pv_processed[0] = ( - electricity_to_grid_backfilled_pv_processed[0] + electricity_to_grid_at_year - ) + electricity_to_grid_at_year = electricity_to_grid_backfilled_pv_processed.pop(0) + electricity_to_grid_backfilled_pv_processed[0] = ( + electricity_to_grid_backfilled_pv_processed[0] + electricity_to_grid_at_year + ) + pv_of_annual_energy_row_index = _get_row_index('Present value of annual energy nominal (kWh)') + if insert_backfilled_rows: ret.insert( - _get_row_index('Present value of annual energy nominal (kWh)') + 1, + pv_of_annual_energy_row_index + 1, [ *[pv_of_electricity_to_grid_backfilled_row_name], *pv_of_electricity_to_grid_backfilled, ], ) - - backfill_pv_of_annual_energy() + else: + ret[pv_of_annual_energy_row_index][1:] = pv_of_electricity_to_grid_backfilled def backfill_lcoe_nominal() -> None: - pv_of_annual_costs_backfilled_row = ret[_get_row_index(pv_of_annual_costs_backfilled_row_name)][1:] - pv_of_electricity_to_grid_backfilled_row = ret[ - _get_row_index(pv_of_electricity_to_grid_backfilled_row_name) - ][1:] + # pv_of_annual_costs_backfilled_row = ret[_get_row_index(pv_of_annual_costs_backfilled_row_name)][1:] + pv_of_electricity_to_grid_backfilled_row = pv_of_electricity_to_grid_backfilled + pv_of_annual_costs_backfilled_row_values = pv_of_annual_costs_backfilled_row[ + 1 if isinstance(pv_of_annual_costs_backfilled_row[0], str) else 0 : + ] lcoe_nominal_backfilled = [] - for year in range(len(pv_of_annual_costs_backfilled_row)): + for _year in range(len(pv_of_annual_costs_backfilled_row_values)): lcoe_nominal_backfilled.append( - pv_of_annual_costs_backfilled_row[year] * 100 / pv_of_electricity_to_grid_backfilled_row[year] + pv_of_annual_costs_backfilled_row_values[_year] + * 100 + / pv_of_electricity_to_grid_backfilled_row[_year] ) - ret.insert( - _get_row_index('LCOE Levelized cost of energy nominal (cents/kWh)') + 1, - [ - *['LCOE Levelized cost of energy nominal [backfilled] (cents/kWh)'], - *lcoe_nominal_backfilled, - ], - ) + lcoe_nominal_row_index = _get_row_index('LCOE Levelized cost of energy nominal (cents/kWh)') + if insert_backfilled_rows: + ret.insert( + lcoe_nominal_row_index + 1, + [ + *['LCOE Levelized cost of energy nominal [backfilled] (cents/kWh)'], + *lcoe_nominal_backfilled, + ], + ) + else: + ret[lcoe_nominal_row_index][1:] = lcoe_nominal_backfilled backfill_lcoe_nominal() From 225e846f6dd8b782a9915fbdc1ad8c7345c85c2e Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:33:46 -0800 Subject: [PATCH 08/10] handle GHA logs assertion failure (https://github.com/softwareengineerprogrammer/GEOPHIRES/actions/runs/21375418744/job/61530255924) --- tests/geophires_x_tests/test_reservoir.py | 57 ++++++++++++----------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/geophires_x_tests/test_reservoir.py b/tests/geophires_x_tests/test_reservoir.py index 5d333b0a8..a5923fa81 100644 --- a/tests/geophires_x_tests/test_reservoir.py +++ b/tests/geophires_x_tests/test_reservoir.py @@ -323,34 +323,37 @@ def _del_metadata(r: GeophiresXResult) -> GeophiresXResult: del r.result['Simulation Metadata'] return r - with self.assertLogs(level='INFO') as logs: - _del_metadata( - GeophiresXClient().get_geophires_result( - GeophiresInputParameters( - from_file_path=self._get_test_file_path('../examples/example5b.txt'), - params={ - 'Reservoir Output Profile': ','.join( - [str(it) for it in [30 * v for v in [*([10] * 7), 9, 8, 7]]] - ) - }, + try: + with self.assertLogs(level='INFO') as logs: + _del_metadata( + GeophiresXClient().get_geophires_result( + GeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/example5b.txt'), + params={ + 'Reservoir Output Profile': ','.join( + [str(it) for it in [30 * v for v in [*([10] * 7), 9, 8, 7]]] + ) + }, + ) ) ) - ) - self.assertHasLogRecordWithMessage( - logs, 'Reservoir temperature extrapolation result', treat_substring_match_as_match=True - ) + self.assertHasLogRecordWithMessage( + logs, 'Reservoir temperature extrapolation result', treat_substring_match_as_match=True + ) - self.assertHasLogRecordWithMessage( - logs, - # TODO make this less hard-coded - '[207.73, 177.48, 147.23, 116.97, 86.72, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' - '80.0, 80.0]', - treat_substring_match_as_match=True, - ) + self.assertHasLogRecordWithMessage( + logs, + # TODO make this less hard-coded + '[207.73, 177.48, 147.23, 116.97, 86.72, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 80.0, ' + '80.0, 80.0]', + treat_substring_match_as_match=True, + ) + except AssertionError as ae: + self._handle_assert_logs_failure(ae) From 2e9ddb67d46ba42dfd21ede353cdfed676a2e678 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:12:01 -0800 Subject: [PATCH 09/10] fix e5bd50fae4710cebc838e83a04e5886f26e14f0f --- tests/base_test_case.py | 15 +++++++++++++++ tests/geophires_x_tests/test_economics_sam.py | 11 ----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/base_test_case.py b/tests/base_test_case.py index 390c5a445..f8d408f8b 100644 --- a/tests/base_test_case.py +++ b/tests/base_test_case.py @@ -1,8 +1,10 @@ from __future__ import annotations import inspect +import logging import numbers import os.path +import sys import unittest from geophires_x.GeoPHIRESUtils import sig_figs @@ -11,6 +13,8 @@ # noinspection PyProtectedMember from geophires_x_client import _get_logger +_log = logging.getLogger(__name__) + class BaseTestCase(unittest.TestCase): maxDiff = None @@ -123,6 +127,17 @@ def assertAlmostEqualWithinSigFigs(self, expected: float | int, actual: float | def _is_github_actions(self): return 'CI' in os.environ or 'TOXPYTHON' in os.environ + def _handle_assert_logs_failure(self, ae: AssertionError): + if sys.version_info[:2] == (3, 8) and self._is_github_actions(): + # FIXME - see + # https://github.com/softwareengineerprogrammer/GEOPHIRES/actions/runs/19646240874/job/56262028512#step:5:344 + _log.warning( + f'WARNING: Skipping logs assertion in GitHub Actions ' + f'for Python {sys.version_info.major}.{sys.version_info.minor}' + ) + else: + raise ae + @staticmethod def get_input_parameter(params: GeophiresInputParameters, param_name: str) -> float | str | None: """ diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 84f61af13..f79f49fd9 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -1137,14 +1137,3 @@ def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None m.Calculate() return m - - def _handle_assert_logs_failure(self, ae: AssertionError): - if sys.version_info[:2] == (3, 8) and self._is_github_actions(): - # FIXME - see - # https://github.com/softwareengineerprogrammer/GEOPHIRES/actions/runs/19646240874/job/56262028512#step:5:344 - _log.warning( - f'WARNING: Skipping logs assertion in GitHub Actions ' - f'for Python {sys.version_info.major}.{sys.version_info.minor}' - ) - else: - raise ae From ce44e3e6aafc555c1a7606ceb03c2b2bf6adc820 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:21:32 -0800 Subject: [PATCH 10/10] fix logging context side effect introduced in previous commit --- tests/base_test_case.py | 5 +---- tests/test_base_test_case.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/base_test_case.py b/tests/base_test_case.py index f8d408f8b..48df59cdb 100644 --- a/tests/base_test_case.py +++ b/tests/base_test_case.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -import logging import numbers import os.path import sys @@ -13,8 +12,6 @@ # noinspection PyProtectedMember from geophires_x_client import _get_logger -_log = logging.getLogger(__name__) - class BaseTestCase(unittest.TestCase): maxDiff = None @@ -131,7 +128,7 @@ def _handle_assert_logs_failure(self, ae: AssertionError): if sys.version_info[:2] == (3, 8) and self._is_github_actions(): # FIXME - see # https://github.com/softwareengineerprogrammer/GEOPHIRES/actions/runs/19646240874/job/56262028512#step:5:344 - _log.warning( + print( f'WARNING: Skipping logs assertion in GitHub Actions ' f'for Python {sys.version_info.major}.{sys.version_info.minor}' ) diff --git a/tests/test_base_test_case.py b/tests/test_base_test_case.py index 872966c23..908b92fe0 100644 --- a/tests/test_base_test_case.py +++ b/tests/test_base_test_case.py @@ -33,7 +33,7 @@ def test_assertAlmostEqualWithinPercentage_bad_arguments(self): self.assertIn(str(msg_type_error), '(you may have meant to pass percent=10)') - with self.assertLogs(level='INFO') as logs: + with self.assertLogs('tests.base_test_case', level='INFO') as logs: with self.assertRaises(AssertionError): self.assertAlmostEqualWithinPercentage([1, 2, 3], [1.1, 2.2, 3.3], percent=10.5)