From eb9899bcd34a0ccc85d062f50ab7462b5ad658c9 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:32:53 -0700 Subject: [PATCH 01/35] WIP - adjust econ capex/opex with add ons for SAM-EM --- src/geophires_x/EconomicsAddOns.py | 17 +++-- .../geophires_x_tests/egs-sam-em-add-ons.txt | 69 +++++++++++++++++++ tests/geophires_x_tests/test_economics_sam.py | 7 ++ 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 tests/geophires_x_tests/egs-sam-em-add-ons.txt diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 6a047f55..2af9ea4e 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -5,7 +5,7 @@ import numpy_financial as npf import geophires_x.Economics as Economics import geophires_x.Model as Model -from geophires_x.OptionList import EndUseOptions +from geophires_x.OptionList import EndUseOptions, EconomicModel from geophires_x.Parameter import listParameter, OutputParameter from geophires_x.Units import * @@ -271,7 +271,9 @@ def Calculate(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: Nothing, but it does make calculations and set values in the model """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f"Init {str(__class__)}: {sys._getframe().f_code.co_name}") + + is_sam_em = model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA # sum all the AddOn values together, so we can treat all AddOns together. If an AddOn slot is not used, # it has zeros for the values, so this won't create problems @@ -302,6 +304,12 @@ def Calculate(self, model: Model) -> None: # Calculate the adjusted OPEX and CAPEX self.AdjustedProjectCAPEX.value = model.economics.CCap.value + self.AddOnCAPEXTotal.value self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value + + if is_sam_em: + model.economics.CCap.value = self.AdjustedProjectCAPEX.value + model.economics.Coam.value = self.AdjustedProjectOPEX.value + # FIXME WIP + AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.construction_years.value @@ -386,8 +394,9 @@ def Calculate(self, model: Model) -> None: self.AdjustedProjectCAPEX.value + ( self.AdjustedProjectOPEX.value * model.surfaceplant.plant_lifetime.value)) - # recalculate LCOE/LCOH - self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOHLCOC(self, model) + if not is_sam_em: + # recalculate LCOE/LCOH + self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOHLCOC(self, model) self._calculate_derived_outputs(model) model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') diff --git a/tests/geophires_x_tests/egs-sam-em-add-ons.txt b/tests/geophires_x_tests/egs-sam-em-add-ons.txt new file mode 100644 index 00000000..218e7bfd --- /dev/null +++ b/tests/geophires_x_tests/egs-sam-em-add-ons.txt @@ -0,0 +1,69 @@ + Reservoir Model, 1 +Reservoir Volume Option, 1 +Reservoir Density, 2800 +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 +Reservoir Impedance, 0.001 + +Number of Fractures, 149 +Fracture Shape, 4 +Fracture Height, 2000 +Fracture Width, 10000 +Fracture Separation, 30 + +Number of Segments, 1 + +Production Well Diameter, 7 +Injection Well Diameter, 7 +Well Separation, 365 feet +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Ramey Production Wellbore Model, 1 +Utilization Factor, .9 +Water Loss Fraction, 0.05 +Maximum Drawdown, 1 +Ambient Temperature, 10 degC +#Surface Temperature, 10 degC +End-Use Option, 1 + +Plant Lifetime, 25 + +Circulation Pump Efficiency, 0.80 + +Economic Model, 5 +Starting Electricity Sale Price, 0.15 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.004053223 +Electricity Escalation Start Year, 1 +Fraction of Investment in Bonds, .5 +Combined Income Tax Rate, .3 +Inflated Bond Interest Rate, .05 +Inflation Rate, .02 +Investment Tax Credit Rate, .3, -- https://programs.dsireusa.org/system/program/detail/658 +Production Tax Credit Electricity, 0.0275, -- https://programs.dsireusa.org/system/program/detail/734 +Property Tax Rate, 0 +Time steps per year, 10 +Maximum Temperature, 500 + + +Print Output to Console, 0 +Surface Temperature, 12 +Reservoir Depth, 5.4 +Gradient 1, 36.7 +Power Plant Type, 4 + +Number of Injection Wells, 54 +Number of Production Wells, 54 +Production Flow Rate per Well, 80 + + +# AddOns +Do AddOn Calculations, True +AddOn Nickname 1, Desalinization +AddOn CAPEX 1, 10 +AddOn OPEX 1, 10 +AddOn Electricity Gained 1, -100 +AddOn Heat Gained 1, 0.0 +AddOn Profit Gained 1, 0.05 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index fd0abccd..308591aa 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -572,6 +572,13 @@ def _accrued_financing(_r: GeophiresXResult) -> float: # ) # self.assertEqual(15.0, _accrued_financing(r4)) + def test_add_ons(self): + add_ons_result = self._get_result({}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt')) + self.assertIsNotNone(add_ons_result) + + with open(add_ons_result.output_file_path, encoding='utf-8') as f: + print(f.read()) + @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: if additional_params is not None: From 490b9d898814cd18c5cc6d5968a1da6661641b99 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:01:10 -0700 Subject: [PATCH 02/35] WIP stash - adjust econ capex/opex with add-ons --- src/geophires_x/EconomicsAddOns.py | 2 +- tests/geophires_x_tests/egs-sam-em-add-ons.txt | 8 +++----- tests/geophires_x_tests/test_economics_sam.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 2af9ea4e..236fc40c 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -307,7 +307,7 @@ def Calculate(self, model: Model) -> None: if is_sam_em: model.economics.CCap.value = self.AdjustedProjectCAPEX.value - model.economics.Coam.value = self.AdjustedProjectOPEX.value + model.economics.Coam.value = self.AdjustedProjectOPEX.value - np.sum(self.AddOnProfitGainedPerYear.value) # FIXME WIP AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value diff --git a/tests/geophires_x_tests/egs-sam-em-add-ons.txt b/tests/geophires_x_tests/egs-sam-em-add-ons.txt index 218e7bfd..b39164fd 100644 --- a/tests/geophires_x_tests/egs-sam-em-add-ons.txt +++ b/tests/geophires_x_tests/egs-sam-em-add-ons.txt @@ -62,8 +62,6 @@ Production Flow Rate per Well, 80 # AddOns Do AddOn Calculations, True AddOn Nickname 1, Desalinization -AddOn CAPEX 1, 10 -AddOn OPEX 1, 10 -AddOn Electricity Gained 1, -100 -AddOn Heat Gained 1, 0.0 -AddOn Profit Gained 1, 0.05 +AddOn CAPEX 1, 562 +AddOn OPEX 1, 0.1 +AddOn Profit Gained 1, 107 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 308591aa..fb33d2e0 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -574,10 +574,26 @@ def _accrued_financing(_r: GeophiresXResult) -> float: def test_add_ons(self): add_ons_result = self._get_result({}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt')) + no_add_ons_result = self._get_result( + {'Do AddOn Calculations': False}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt') + ) self.assertIsNotNone(add_ons_result) with open(add_ons_result.output_file_path, encoding='utf-8') as f: + print('With Add-Ons:\n') + print(f.read()) + print('\n------------\n') + + with open(no_add_ons_result.output_file_path, encoding='utf-8') as f: + print('Without Add-Ons:\n') print(f.read()) + print('\n------------\n') + + # FIXME WIP + self.assertLess( + no_add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + ) @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: From d3012f8e1d5f61389a31d3b2af79a1b9c29c4f32 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:55:57 -0700 Subject: [PATCH 03/35] Don't override 'Do AddOn Calculations' automatically if it was explicitly provided --- src/geophires_x/Economics.py | 2 +- tests/geophires_x_tests/egs-sam-em-add-ons.txt | 1 - tests/geophires_x_tests/test_economics_sam.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index c7c6f39e..890d8965 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -2334,7 +2334,7 @@ def _warn(_msg: str) -> None: # we can determine on-the-fly if Addons, CCUS, or S-DAC-GT are being used in the user input file for key in model.InputParameters.keys(): - if key.startswith("AddOn"): + if key.startswith("AddOn") and not self.DoAddOnCalculations.Provided: self.DoAddOnCalculations.value = True break diff --git a/tests/geophires_x_tests/egs-sam-em-add-ons.txt b/tests/geophires_x_tests/egs-sam-em-add-ons.txt index b39164fd..e5d2c0a4 100644 --- a/tests/geophires_x_tests/egs-sam-em-add-ons.txt +++ b/tests/geophires_x_tests/egs-sam-em-add-ons.txt @@ -60,7 +60,6 @@ Production Flow Rate per Well, 80 # AddOns -Do AddOn Calculations, True AddOn Nickname 1, Desalinization AddOn CAPEX 1, 562 AddOn OPEX 1, 0.1 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index fb33d2e0..3ef1c366 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -573,7 +573,9 @@ def _accrued_financing(_r: GeophiresXResult) -> float: # self.assertEqual(15.0, _accrued_financing(r4)) def test_add_ons(self): - add_ons_result = self._get_result({}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt')) + add_ons_result = self._get_result( + {'Do AddOn Calculations': True}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt') + ) no_add_ons_result = self._get_result( {'Do AddOn Calculations': False}, file_path=self._get_test_file_path('egs-sam-em-add-ons.txt') ) From 01a95150bffa939af71c6fdce801af9fbbe8989d Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:14:45 -0700 Subject: [PATCH 04/35] model add on profit as fixed capacity payment (WIP - TODO to unit test) --- src/geophires_x/EconomicsAddOns.py | 2 +- src/geophires_x/EconomicsSam.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 236fc40c..2af9ea4e 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -307,7 +307,7 @@ def Calculate(self, model: Model) -> None: if is_sam_em: model.economics.CCap.value = self.AdjustedProjectCAPEX.value - model.economics.Coam.value = self.AdjustedProjectOPEX.value - np.sum(self.AddOnProfitGainedPerYear.value) + model.economics.Coam.value = self.AdjustedProjectOPEX.value # FIXME WIP AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 78ef3a89..e1b14ccc 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -415,6 +415,12 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: ret['ibi_oth_amount'] = (econ.OtherIncentives.quantity() + econ.TotalGrant.quantity()).to('USD').magnitude + if model.economics.DoAddOnCalculations.value: + add_on_profit_per_year = np.sum(model.addeconomics.AddOnProfitGainedPerYear.quantity().to('USD/yr').magnitude) + add_on_profit_series = [add_on_profit_per_year] + ret['cp_capacity_payment_amount'] = add_on_profit_series + ret['cp_capacity_payment_type'] = 1 + return ret From 8dfa6c8246dd680d684e0702047c15dea98321e5 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:30:21 -0700 Subject: [PATCH 05/35] Remove erroneously-added Model import from 4361af074f275aa952f6800daf4329ce8f7fb129 --- src/geophires_x/Model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/geophires_x/Model.py b/src/geophires_x/Model.py index 63a48dd9..d7adfb0f 100644 --- a/src/geophires_x/Model.py +++ b/src/geophires_x/Model.py @@ -1,5 +1,4 @@ import sys -from email.policy import default from pathlib import Path import logging import time From 11b49ff33e86793d51518a6ef14419eb910dba88 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:39:13 -0700 Subject: [PATCH 06/35] RuntimeError instead of sys.exit in OutputsAddOns.py --- src/geophires_x/OutputsAddOns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index 1ee2caff..6dd3bd57 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -114,7 +114,7 @@ def PrintOutputs(self, model) -> tuple: print(err_msg) model.logger.critical(str(ex)) model.logger.critical(err_msg) - sys.exit() + raise RuntimeError(err_msg) from ex model.logger.info(f'Complete {str(__class__)}: {__name__}') From 130e4b4fee43325b61e8e77ed394dbde16c86a5a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:44:10 -0700 Subject: [PATCH 07/35] update inaccurate OutputsAddOns return type doc --- src/geophires_x/OutputsAddOns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index 6dd3bd57..cf6555db 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -16,7 +16,7 @@ def PrintOutputs(self, model) -> tuple: The PrintOutputs function prints the results of the AddOns to a text file and to the screen. :param model: Model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` - :return: None + :return: tuple of addon_df, addon_results: list[OutputTableItem] """ model.logger.info(f'Init {str(__class__)}: {__name__}') From f112096bb7de645f71a9eb5f5050b9da7941c96d Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:51:02 -0700 Subject: [PATCH 08/35] Move OutputTableItem to separate OutputsUtils.py in preparation for refactoring AddOns/S-DAC output/rich output flow --- src/geophires_x/OutputsAddOns.py | 6 +++--- src/geophires_x/OutputsRich.py | 16 +--------------- src/geophires_x/OutputsS_DAC_GT.py | 2 +- src/geophires_x/OutputsUtils.py | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 src/geophires_x/OutputsUtils.py diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index cf6555db..47ad843f 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -1,7 +1,7 @@ import sys import pandas as pd from geophires_x.Outputs import Outputs -from geophires_x.OutputsRich import OutputTableItem +from geophires_x.OutputsUtils import OutputTableItem NL = "\n" @@ -10,13 +10,13 @@ class OutputsAddOns(Outputs): """ Class to handle output of the AddOns values """ - def PrintOutputs(self, model) -> tuple: + def PrintOutputs(self, model) -> tuple[pd.DataFrame, list]: """ The PrintOutputs function prints the results of the AddOns to a text file and to the screen. :param model: Model: The container class of the application, giving access to everything else, including the logger :type model: :class:`~geophires_x.Model.Model` - :return: tuple of addon_df, addon_results: list[OutputTableItem] + :return: tuple of addon_df, addon_results """ model.logger.info(f'Init {str(__class__)}: {__name__}') diff --git a/src/geophires_x/OutputsRich.py b/src/geophires_x/OutputsRich.py index d4e2b6ec..170e0d39 100644 --- a/src/geophires_x/OutputsRich.py +++ b/src/geophires_x/OutputsRich.py @@ -1,4 +1,3 @@ -import dataclasses import datetime import string import time @@ -20,6 +19,7 @@ from geophires_x.MatplotlibUtils import plt_subplots from geophires_x.OptionList import EndUseOptions, PlantType, EconomicModel, ReservoirModel, FractureShape, \ ReservoirVolume +from geophires_x.OutputsUtils import OutputTableItem from geophires_x.Parameter import intParameter, strParameter @@ -30,20 +30,6 @@ VERTICAL_WELL_DEPTH_OUTPUT_NAME = 'Well depth' -@dataclasses.dataclass -class OutputTableItem: - parameter: str = '' - value: str = '' - units: str = '' - - def __init__(self, parameter: str, value: str = '', units: str = ''): - self.parameter = parameter - self.value = value - self.units = units - if self.units: - self.units = UpgradeSymbologyOfUnits(self.units) - - def print_outputs_rich(output_file: str, text_output_file: strParameter, html_output_file: strParameter, model: Model): """ TODO Implementation of rich output in this method/file is duplicative of Outputs.PrintOutputs. This adds undue diff --git a/src/geophires_x/OutputsS_DAC_GT.py b/src/geophires_x/OutputsS_DAC_GT.py index 7bdc59df..ba283858 100644 --- a/src/geophires_x/OutputsS_DAC_GT.py +++ b/src/geophires_x/OutputsS_DAC_GT.py @@ -1,7 +1,7 @@ import sys import pandas as pd from geophires_x.Outputs import Outputs -from geophires_x.OutputsRich import OutputTableItem +from geophires_x.OutputsUtils import OutputTableItem NL = "\n" diff --git a/src/geophires_x/OutputsUtils.py b/src/geophires_x/OutputsUtils.py new file mode 100644 index 00000000..1a920f56 --- /dev/null +++ b/src/geophires_x/OutputsUtils.py @@ -0,0 +1,17 @@ +import dataclasses + +from geophires_x.GeoPHIRESUtils import UpgradeSymbologyOfUnits + + +@dataclasses.dataclass +class OutputTableItem: + parameter: str = '' + value: str = '' + units: str = '' + + def __init__(self, parameter: str, value: str = '', units: str = ''): + self.parameter = parameter + self.value = value + self.units = units + if self.units: + self.units = UpgradeSymbologyOfUnits(self.units) From f79a2daf45a2ac615f24198a653e00e1512bf7a6 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:02:54 -0700 Subject: [PATCH 09/35] Extract calls to AddOns & S-DAC PrintOutputs from OutputsRich to Outputs --- src/geophires_x/Outputs.py | 20 +++++++++++++++++++- src/geophires_x/OutputsRich.py | 27 +++++++++++---------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 486bae3a..d11ddc85 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -6,6 +6,7 @@ # noinspection PyPackageRequirements import numpy as np +import pandas as pd import geophires_x import geophires_x.Model as Model @@ -759,6 +760,15 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(NL) + addon_df = pd.DataFrame() + sdac_df = pd.DataFrame() + addon_results = [] + sdac_results = [] + + if model.economics.DoAddOnCalculations.value: + addon_df, addon_results = model.addoutputs.PrintOutputs(model) + if model.economics.DoSDACGTCalculations.value: + sdac_df, sdac_results = model.sdacgtoutputs.PrintOutputs(model) except BaseException as ex: tb = sys.exc_info()[2] @@ -769,7 +779,15 @@ def PrintOutputs(self, model: Model): model.logger.critical(msg) raise RuntimeError(msg) from ex - print_outputs_rich(self.output_file, self.text_output_file, self.html_output_file, model) + print_outputs_rich( + self.text_output_file, + self.html_output_file, + model, + sdac_results, + addon_results, + sdac_df, + addon_df + ) model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/OutputsRich.py b/src/geophires_x/OutputsRich.py index 170e0d39..cc9e45bc 100644 --- a/src/geophires_x/OutputsRich.py +++ b/src/geophires_x/OutputsRich.py @@ -30,7 +30,14 @@ VERTICAL_WELL_DEPTH_OUTPUT_NAME = 'Well depth' -def print_outputs_rich(output_file: str, text_output_file: strParameter, html_output_file: strParameter, model: Model): +def print_outputs_rich( + text_output_file: strParameter, + html_output_file: strParameter, + model: Model, + sdac_results: list, + addon_results: list, + sdac_df: pd.DataFrame, + addon_df: pd.DataFrame): """ TODO Implementation of rich output in this method/file is duplicative of Outputs.PrintOutputs. This adds undue code complexity, maintenance overhead, inconsistency, and potential for bugs. Rich output should instead be @@ -49,9 +56,7 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou CAPEX = [] OPEX = [] surface_equipment_results = [] - addon_results = [] - sdac_resa_results = [] - pumping_power_results = [] + # addon_results = [] simulation_metadata.append(OutputTableItem('GEOPHIRES Version', geophires_x.__version__)) simulation_metadata.append(OutputTableItem('Simulation Date', datetime.datetime.now().strftime('%Y-%m-%d'))) @@ -878,18 +883,8 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou pumping_power_profiles = pumping_power_profiles.reset_index() - addon_df = pd.DataFrame() - sdac_df = pd.DataFrame() - addon_results: list[OutputTableItem] = [] - sdac_results: list[OutputTableItem] = [] - - if model.economics.DoAddOnCalculations.value: - addon_df, addon_results = model.addoutputs.PrintOutputs(model) - if model.economics.DoSDACGTCalculations.value: - sdac_df, sdac_results = model.sdacgtoutputs.PrintOutputs(model) - if text_output_file.Provided: - Write_Text_Output(output_file, simulation_metadata, summary, economic_parameters, + Write_Text_Output(text_output_file.value, simulation_metadata, summary, economic_parameters, engineering_parameters, resource_characteristics, reservoir_parameters, reservoir_stimulation_results, CAPEX, OPEX, @@ -897,7 +892,7 @@ def print_outputs_rich(output_file: str, text_output_file: strParameter, html_ou pumping_power_profiles, sdac_df, addon_df) # Get rid of any trailing spaces in that output file - they are confusing the testing code - with open(output_file, 'r+') as fp: + with open(text_output_file.value, 'r+') as fp: lines = fp.readlines() fp.seek(0) fp.truncate() From 539b474142222254dcd335443275c47c5e6d7522 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:13:05 -0700 Subject: [PATCH 10/35] Consolidate Outputs SAM-EM checks into is_sam_econ_model --- src/geophires_x/Outputs.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index d11ddc85..6e809989 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -176,6 +176,7 @@ def PrintOutputs(self, model: Model): with open(self.output_file, 'w', encoding='UTF-8') as f: econ: Economics = model.economics + is_sam_econ_model = econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA f.write(' *****************\n') f.write(' ***CASE REPORT***\n') @@ -222,7 +223,7 @@ def PrintOutputs(self, model: Model): f.write(f' {model.economics.LCOE.display_name}: {model.economics.LCOE.value:10.2f} {model.economics.LCOE.CurrentUnits.value}\n') f.write(f' {model.economics.LCOH.display_name}: {model.economics.LCOH.value:10.2f} {model.economics.LCOH.CurrentUnits.value}\n') - if econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: f.write(f' {Outputs._field_label(econ.capex_total.display_name, 50)}{econ.capex_total.value:10.2f} {econ.capex_total.CurrentUnits.value}\n') f.write(f' Number of production wells: {model.wellbores.nprod.value:10.0f}'+NL) @@ -255,10 +256,10 @@ def PrintOutputs(self, model: Model): # https://github.com/softwareengineerprogrammer/GEOPHIRES/commit/535c02d4adbeeeca553b61e9b996fccf00016529 f.write(f' {model.economics.interest_rate.Name}: {model.economics.interest_rate.value:10.2f} {model.economics.interest_rate.CurrentUnits.value}\n') - elif model.economics.econmodel.value in (EconomicModel.BICYCLE, EconomicModel.SAM_SINGLE_OWNER_PPA): + elif is_sam_econ_model or model.economics.econmodel.value == EconomicModel.BICYCLE: f.write(f' Economic Model = {model.economics.econmodel.value.value}\n') - if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: sam_econ_fields: list[OutputParameter] = [ econ.real_discount_rate, econ.nominal_discount_rate, @@ -282,7 +283,7 @@ def PrintOutputs(self, model: Model): f.write(f' {npv_field_label}{e_npv.value:10.2f} {e_npv.PreferredUnits.value}\n') irr_output_param: OutputParameter = econ.ProjectIRR \ - if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA else econ.after_tax_irr + if not is_sam_econ_model else econ.after_tax_irr irr_field_label = Outputs._field_label(irr_output_param.display_name, 49) irr_display_value = f'{irr_output_param.value:10.2f}' \ if not math.isnan(irr_output_param.value) else 'NaN' @@ -485,7 +486,7 @@ def PrintOutputs(self, model: Model): f.write(f' Stimulation costs (for redrilling): {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') if model.economics.RITCValue.value: - if model.economics.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + if not is_sam_econ_model: f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') else: # TODO Extract value from SAM Cash Flow Profile per @@ -496,12 +497,12 @@ def PrintOutputs(self, model: Model): # expenditure. pass - if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: # TODO calculate & display for other economic models icc_label = Outputs._field_label(econ.inflation_cost_during_construction.display_name, 47) f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') - capex_param = econ.CCap if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA else econ.capex_total + capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total capex_label = Outputs._field_label(capex_param.display_name, 50) f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n') @@ -733,10 +734,10 @@ def PrintOutputs(self, model: Model): model.surfaceplant.RemainingReservoirHeatContent.value[i], (model.reserv.InitialReservoirHeatContent.value-model.surfaceplant.RemainingReservoirHeatContent.value[i])*100/model.reserv.InitialReservoirHeatContent.value)+NL) - if econ.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + if not is_sam_econ_model: self.write_revenue_and_cashflow_profile_output(model, f) - if econ.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + if is_sam_econ_model: f.write(self.get_sam_cash_flow_profile_output(model)) # if we are dealing with overpressure and two different reservoirs, show a table reporting the values From 77cd6ae063d2b7a82e5720da2f22631eeedd9478 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:27:34 -0700 Subject: [PATCH 11/35] Don't print extended economics for SAM-EM Add-ons. Add example_SAM-single-owner-PPA-3 with absorption chiller add-on. --- src/geophires_x/Outputs.py | 9 +- .../example_SAM-single-owner-PPA-3.out | 413 ++++++++++++++++++ .../example_SAM-single-owner-PPA-3.txt | 90 ++++ 3 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 tests/examples/example_SAM-single-owner-PPA-3.out create mode 100644 tests/examples/example_SAM-single-owner-PPA-3.txt diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 6e809989..ca148870 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -762,12 +762,13 @@ def PrintOutputs(self, model: Model): f.write(NL) addon_df = pd.DataFrame() - sdac_df = pd.DataFrame() addon_results = [] - sdac_results = [] - - if model.economics.DoAddOnCalculations.value: + if model.economics.DoAddOnCalculations.value and not is_sam_econ_model: + # SAM econ models incorporate add-on economics into main economics, not as separate extended economics. addon_df, addon_results = model.addoutputs.PrintOutputs(model) + + sdac_df = pd.DataFrame() + sdac_results = [] if model.economics.DoSDACGTCalculations.value: sdac_df, sdac_results = model.sdacgtoutputs.PrintOutputs(model) diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out new file mode 100644 index 00000000..7de1081c --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -0,0 +1,413 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.9.43 + Simulation Date: 2025-07-28 + Simulation Time: 13:24 + Calculation Time: 1.159 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 58.87 MW + Electricity breakeven price: 7.64 cents/kWh + Total CAPEX: 275.47 MUSD + Number of production wells: 6 + Number of injection wells: 6 + Flowrate per production well: 100.0 kg/sec + Well depth: 2.6 kilometer + Geothermal gradient: 74 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = SAM Single Owner PPA + Real Discount Rate: 8.00 % + Nominal Discount Rate: 10.16 % + WACC: 7.57 % + Accrued financing during construction: 5.00 % + Project lifetime: 20 yr + Capacity factor: 90.0 % + Project NPV: 210.63 MUSD + After-tax IRR: 30.00 % + Project VIR=PI=PIR: 2.27 + Project MOIC: 5.70 + Project Payback Period: 2.94 yr + Estimated Jobs Created: 125 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 6 + Number of Injection Wells: 6 + Well depth: 2.6 kilometer + Water loss rate: 10.0 % + Pump efficiency: 80.0 % + Injection temperature: 56.7 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 2.2 degC + Flowrate per production well: 100.0 kg/sec + Injection well casing ID: 9.625 in + Production well casing ID: 9.625 in + Number of times redrilling: 0 + Power plant type: Supercritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 500.0 degC + Number of segments: 1 + Geothermal gradient: 74 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 202.40 degC + Fracture model = Square + Well separation: fracture height: 165.00 meter + Fracture area: 27225.00 m**2 + Number of fractures calculated with reservoir volume and fracture separation as input + Number of fractures: 4083 + Fracture separation: 18.00 meter + Reservoir volume: 2000000000 m**3 + Reservoir impedance: 0.0010 GPa.s/m**3 + Reservoir density: 2800.00 kg/m**3 + Reservoir thermal conductivity: 3.05 W/m/K + Reservoir heat capacity: 790.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 200.4 degC + Average Production Temperature: 200.2 degC + Minimum Production Temperature: 198.6 degC + Initial Production Temperature: 198.6 degC + Average Reservoir Heat Extraction: 360.65 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 2.2 degC + Total Average Pressure Drop: -1320.2 kPa + Average Injection Well Pressure Drop: 483.7 kPa + Average Reservoir Pressure Drop: 630.2 kPa + Average Production Well Pressure Drop: 442.7 kPa + Average Buoyancy Pressure Drop: -2876.7 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 49.18 MUSD + Drilling and completion costs per vertical production well: 3.37 MUSD + Drilling and completion costs per vertical injection well: 3.37 MUSD + Drilling and completion costs per non-vertical section: 2.14 MUSD + Stimulation costs: 9.06 MUSD + Surface power plant costs: 144.44 MUSD + Field gathering system costs: 5.80 MUSD + Total surface equipment costs: 150.23 MUSD + Exploration costs: 3.89 MUSD + Inflation costs during construction: 13.12 MUSD + Total CAPEX: 275.47 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 1.13 MUSD/yr + Power plant maintenance costs: 3.90 MUSD/yr + Water costs: 1.58 MUSD/yr + Total operating and maintenance costs: 7.60 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 59.02 MW + Average Total Electricity Generation: 58.87 MW + Minimum Total Electricity Generation: 57.74 MW + Initial Total Electricity Generation: 57.74 MW + Maximum Net Electricity Generation: 59.02 MW + Average Net Electricity Generation: 58.87 MW + Minimum Net Electricity Generation: 57.74 MW + Initial Net Electricity Generation: 57.74 MW + Average Annual Total Electricity Generation: 464.13 GWh + Average Annual Net Electricity Generation: 464.13 GWh + Average Pumping Power: 0.00 MW + Heat to Power Conversion Efficiency: 16.32 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 198.64 0.0000 57.7389 16.1868 + 2 1.0055 199.73 0.0000 58.5226 16.2814 + 3 1.0065 199.93 0.0000 58.6666 16.2987 + 4 1.0070 200.03 0.0000 58.7412 16.3077 + 5 1.0074 200.10 0.0000 58.7905 16.3136 + 6 1.0076 200.15 0.0000 58.8268 16.3179 + 7 1.0078 200.19 0.0000 58.8554 16.3213 + 8 1.0080 200.22 0.0000 58.8787 16.3241 + 9 1.0081 200.25 0.0000 58.8984 16.3265 + 10 1.0082 200.27 0.0000 58.9153 16.3285 + 11 1.0083 200.30 0.0000 58.9302 16.3303 + 12 1.0084 200.31 0.0000 58.9434 16.3318 + 13 1.0085 200.33 0.0000 58.9552 16.3332 + 14 1.0086 200.34 0.0000 58.9660 16.3345 + 15 1.0087 200.36 0.0000 58.9758 16.3357 + 16 1.0087 200.37 0.0000 58.9848 16.3368 + 17 1.0088 200.38 0.0000 58.9931 16.3378 + 18 1.0088 200.39 0.0000 59.0009 16.3387 + 19 1.0089 200.40 0.0000 59.0081 16.3396 + 20 1.0089 200.41 0.0000 59.0149 16.3404 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 459.4 2826.8 606.53 1.65 + 2 462.0 2836.1 596.32 3.31 + 3 462.8 2838.9 586.10 4.96 + 4 463.3 2840.6 575.87 6.62 + 5 463.7 2841.7 565.64 8.28 + 6 463.9 2842.6 555.41 9.94 + 7 464.1 2843.3 545.17 11.60 + 8 464.3 2843.9 534.94 13.26 + 9 464.4 2844.4 524.70 14.92 + 10 464.5 2844.9 514.45 16.58 + 11 464.7 2845.2 504.21 18.24 + 12 464.8 2845.6 493.97 19.90 + 13 464.8 2845.9 483.72 21.56 + 14 464.9 2846.2 473.48 23.23 + 15 465.0 2846.4 463.23 24.89 + 16 465.1 2846.7 452.98 26.55 + 17 465.1 2846.9 442.73 28.21 + 18 465.2 2847.1 432.48 29.87 + 19 465.2 2847.3 422.23 31.53 + 20 465.3 2847.5 411.98 33.20 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Year 0 Year 1 Year 2 Year 3 Year 4 Year 5 Year 6 Year 7 Year 8 Year 9 Year 10 Year 11 Year 12 Year 13 Year 14 Year 15 Year 16 Year 17 Year 18 Year 19 Year 20 +ENERGY +Electricity to grid (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 +Electricity from grid (kWh) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +Electricity to grid net (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 + +REVENUE +PPA price (cents/kWh) 0.0 8.0 8.0 8.32 8.64 8.97 9.29 9.61 9.93 10.25 10.58 10.90 11.22 11.54 11.86 12.19 12.51 12.83 13.15 13.47 13.80 +PPA revenue ($) 0 36,751,456 36,964,904 38,519,865 40,051,439 41,573,241 43,089,812 44,603,196 46,114,476 47,624,287 49,133,030 50,640,973 52,148,303 53,655,154 55,161,626 56,667,791 58,173,709 59,679,427 61,184,986 62,690,409 64,195,415 +Curtailment payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Capacity payment revenue ($) 0 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 15,000,000 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 137,736,712 +Total revenue ($) 0 51,751,456 51,964,904 53,519,865 55,051,439 56,573,241 58,089,812 59,603,196 61,114,476 62,624,287 64,133,030 65,640,973 67,148,303 68,655,154 70,161,626 71,667,791 73,173,709 74,679,427 76,184,986 77,690,409 216,932,127 + +Property tax net assessed value ($) 0 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 275,473,424 + +OPERATING EXPENSES +O&M fixed expense ($) 0 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 +O&M production-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +O&M capacity-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Fuel expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Electricity purchase ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Property tax expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Insurance expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total operating expenses ($) 0 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 7,599,142 + +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 + +OPERATING ACTIVITIES +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +plus PBI if not available for debt service: +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Cash flow from operating activities ($) 0 38,642,845 39,022,913 40,752,827 42,468,100 44,182,786 45,901,885 47,627,924 49,362,491 51,106,754 52,861,671 54,628,098 56,406,835 58,198,664 60,004,362 61,824,716 63,660,531 65,512,641 67,381,912 69,269,232 208,911,943 + +INVESTING ACTIVITIES +Total installed cost ($) -275,473,424 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -275,473,424 +plus: +Reserve (increase)/decrease debt service ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease working capital ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease receivables ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash flow from investing activities ($) -275,473,424 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +FINANCING ACTIVITIES +Issuance of equity ($) 165,284,055 +Size of debt ($) 110,189,370 +minus: +Debt principal payment ($) 0 3,332,412 3,499,032 3,673,984 3,857,683 4,050,567 4,253,096 4,465,750 4,689,038 4,923,490 5,169,664 5,428,147 5,699,555 5,984,532 6,283,759 6,597,947 6,927,844 7,274,237 7,637,948 8,019,846 8,420,838 +equals: +Cash flow from financing activities ($) 275,473,424 -3,332,412 -3,499,032 -3,673,984 -3,857,683 -4,050,567 -4,253,096 -4,465,750 -4,689,038 -4,923,490 -5,169,664 -5,428,147 -5,699,555 -5,984,532 -6,283,759 -6,597,947 -6,927,844 -7,274,237 -7,637,948 -8,019,846 -8,420,838 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 38,642,845 39,022,913 40,752,827 42,468,100 44,182,786 45,901,885 47,627,924 49,362,491 51,106,754 52,861,671 54,628,098 56,406,835 58,198,664 60,004,362 61,824,716 63,660,531 65,512,641 67,381,912 69,269,232 208,911,943 +Cash flow from investing activities ($) -275,473,424 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Cash flow from financing activities ($) 275,473,424 -3,332,412 -3,499,032 -3,673,984 -3,857,683 -4,050,567 -4,253,096 -4,465,750 -4,689,038 -4,923,490 -5,169,664 -5,428,147 -5,699,555 -5,984,532 -6,283,759 -6,597,947 -6,927,844 -7,274,237 -7,637,948 -8,019,846 -8,420,838 +Total pre-tax cash flow ($) 0 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 + +Pre-tax Returns: +Issuance of equity ($) 165,284,055 +Total pre-tax cash flow ($) 0 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 +Total pre-tax returns ($) -165,284,055 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 + +After-tax Returns: +Total pre-tax returns ($) -165,284,055 35,310,434 35,523,881 37,078,843 38,610,417 40,132,219 41,648,789 43,162,174 44,673,453 46,183,264 47,692,007 49,199,951 50,707,281 52,214,132 53,720,603 55,226,769 56,732,686 58,238,404 59,743,963 61,249,387 200,491,104 +Federal ITC total income ($) 0 82,642,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal tax benefit (liability) ($) 0 -6,403,699 -5,334,677 -5,672,529 -6,007,522 -6,342,400 -6,678,140 -7,015,235 -7,353,996 -7,694,651 -8,037,386 -8,382,369 -8,729,757 -9,079,701 -9,432,354 -9,787,869 -10,146,403 -10,508,120 -10,873,189 -11,241,783 -38,514,004 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 +Total after-tax returns ($) -165,284,055 109,253,530 28,277,134 29,373,150 30,449,662 31,516,557 32,577,051 33,632,517 34,683,616 35,730,674 36,773,837 37,813,148 38,848,579 39,880,058 40,907,478 41,930,703 42,949,579 43,963,932 44,973,574 45,978,291 148,172,798 + +After-tax cumulative IRR (%) NaN -33.90 -14.01 0.64 10.10 16.19 20.21 22.94 24.84 26.19 27.16 27.87 28.40 28.80 29.10 29.32 29.50 29.63 29.73 29.81 30.0 +After-tax cumulative NPV ($) -165,284,055 -66,106,921 -42,805,225 -20,832,763 -155,799 19,271,800 37,501,025 54,585,116 70,578,228 85,534,586 99,507,908 112,550,973 124,715,298 136,050,903 146,606,134 156,427,530 165,559,744 174,045,485 181,925,494 189,238,538 210,632,437 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -165,284,055 57,502,074 -23,687,770 -24,146,716 -24,601,778 -25,056,684 -25,512,761 -25,970,679 -26,430,860 -26,893,613 -27,359,192 -27,827,825 -28,299,724 -28,775,096 -29,254,148 -29,737,088 -30,224,130 -30,715,494 -31,211,412 -31,712,118 68,977,383 +PPA revenue ($) 0 36,751,456 36,964,904 38,519,865 40,051,439 41,573,241 43,089,812 44,603,196 46,114,476 47,624,287 49,133,030 50,640,973 52,148,303 53,655,154 55,161,626 56,667,791 58,173,709 59,679,427 61,184,986 62,690,409 64,195,415 +Electricity to grid (kWh) 0.0 459,393,200 462,061,296 462,867,882 463,343,815 463,676,568 463,929,931 464,133,155 464,302,013 464,445,938 464,571,006 464,681,345 464,779,886 464,868,777 464,949,642 465,023,726 465,092,011 465,155,313 465,214,308 465,269,475 465,319,040 + +Present value of annual costs ($) 298,190,010 +Present value of annual energy nominal (kWh) 3,903,105,303.0 +LCOE Levelized cost of energy nominal (cents/kWh) 7.64 + +Present value of PPA revenue ($) 382,501,304 +Present value of annual energy nominal (kWh) 3,903,105,303.0 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +State taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State taxable IBI income ($) 0 +State taxable CBI income ($) 0 +minus: +Debt interest payment ($) 0 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Total state tax depreciation ($) 0 5,853,810 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 +equals: +State taxable income ($) 0 32,789,035 27,315,293 29,045,206 30,760,479 32,475,165 34,194,264 35,920,304 37,654,871 39,399,133 41,154,051 42,920,477 44,699,215 46,491,044 48,296,742 50,117,095 51,952,910 53,805,020 55,674,291 57,561,612 197,204,322 + +State income tax rate (frac) 0.0 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 +State tax benefit (liability) ($) 0 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,295,232 -1,912,071 -2,033,164 -2,153,234 -2,273,262 -2,393,599 -2,514,421 -2,635,841 -2,757,939 -2,880,784 -3,004,433 -3,128,945 -3,254,373 -3,380,772 -3,508,197 -3,636,704 -3,766,351 -3,897,200 -4,029,313 -13,804,303 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal taxable IBI income ($) 0 +Federal taxable CBI income ($) 0 +Federal taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +minus: +Debt interest payment ($) 0 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Total federal tax depreciation ($) 0 5,853,810 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 11,707,621 +equals: +Federal taxable income ($) 0 30,493,802 25,403,222 27,012,042 28,607,246 30,201,904 31,800,666 33,405,882 35,019,030 36,641,194 38,273,267 39,916,044 41,570,270 43,236,671 44,915,970 46,608,899 48,316,206 50,038,669 51,777,091 53,532,299 183,400,020 + +Federal income tax rate (frac) 0.0 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 +Federal tax benefit (liability) ($) 0 -6,403,699 -5,334,677 -5,672,529 -6,007,522 -6,342,400 -6,678,140 -7,015,235 -7,353,996 -7,694,651 -8,037,386 -8,382,369 -8,729,757 -9,079,701 -9,432,354 -9,787,869 -10,146,403 -10,508,120 -10,873,189 -11,241,783 -38,514,004 + +CASH INCENTIVES +Federal IBI income ($) 0 +State IBI income ($) 0 +Utility IBI income ($) 0 +Other IBI income ($) 0 +Total IBI income ($) 0 + +Federal CBI income ($) 0 +State CBI income ($) 0 +Utility CBI income ($) 0 +Other CBI income ($) 0 +Total CBI income ($) 0 + +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +TAX CREDITS +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Federal ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC percent income ($) 0 82,642,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC total income ($) 0 82,642,027 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +State ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC percent income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +DEBT REPAYMENT +Debt balance ($) 110,189,370 106,856,958 103,357,926 99,683,942 95,826,259 91,775,692 87,522,596 83,056,846 78,367,808 73,444,319 68,274,654 62,846,507 57,146,952 51,162,420 44,878,661 38,280,714 31,352,869 24,078,633 16,440,684 8,420,838 0 +Debt interest payment ($) 0 5,509,468 5,342,848 5,167,896 4,984,197 4,791,313 4,588,785 4,376,130 4,152,842 3,918,390 3,672,216 3,413,733 3,142,325 2,857,348 2,558,121 2,243,933 1,914,036 1,567,643 1,203,932 822,034 421,042 +Debt principal payment ($) 0 3,332,412 3,499,032 3,673,984 3,857,683 4,050,567 4,253,096 4,465,750 4,689,038 4,923,490 5,169,664 5,428,147 5,699,555 5,984,532 6,283,759 6,597,947 6,927,844 7,274,237 7,637,948 8,019,846 8,420,838 +Debt total payment ($) 0 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +minus: +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash available for debt service (CAFDS) ($) 0 44,152,314 44,365,761 45,920,723 47,452,297 48,974,099 50,490,670 52,004,054 53,515,334 55,025,144 56,533,887 58,041,831 59,549,161 61,056,012 62,562,483 64,068,649 65,574,566 67,080,284 68,585,843 70,091,267 209,332,985 +Debt total payment ($) 0 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 8,841,880 +DSCR (pre-tax) 0.0 4.99 5.02 5.19 5.37 5.54 5.71 5.88 6.05 6.22 6.39 6.56 6.73 6.91 7.08 7.25 7.42 7.59 7.76 7.93 23.68 + +RESERVES +Reserves working capital funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves debt service funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves total reserves balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest on reserves (%/year) 1.75 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/tests/examples/example_SAM-single-owner-PPA-3.txt b/tests/examples/example_SAM-single-owner-PPA-3.txt new file mode 100644 index 00000000..53b8d79c --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-3.txt @@ -0,0 +1,90 @@ +# Example: SAM Single Owner PPA Economic Model: 50 MWe with Add-Ons +# This example models example_SAM-single-owner with a Waste Heat Absorption Chiller Add-on +# See "SAM Economic Models" in GEOPHIRES documentation: https://nrel.github.io/GEOPHIRES-X/SAM-Economic-Models.html + +# *** ADD-ONS *** +# *************** +AddOn Nickname 1, Waste Heat Absorption Chiller +AddOn CAPEX 1, 50 +AddOn OPEX 1, 1 +AddOn Profit Gained 1, 15 + + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- SAM Single Owner PPA +Capital Cost for Power Plant for Electricity Generation, 1900 + +Starting Electricity Sale Price, 0.08 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.00322 +Electricity Escalation Start Year, 1 + +Fraction of Investment in Bonds, .4 +Inflated Bond Interest Rate, .05 +Discount Rate, 0.08 +Inflation Rate, .02 +Inflation Rate During Construction, 0.05 + +Combined Income Tax Rate, .28 +Investment Tax Credit Rate, 0.3 +Property Tax Rate, 0 + + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Supercritical ORC +Plant Lifetime, 20 + +Reservoir Model, 1 + +Reservoir Volume Option, 2, -- RES_VOL_FRAC_SEP (Specify reservoir volume and fracture separation) +Reservoir Volume, 2000000000, -- m**3 +Fracture Shape, 3, -- Square +Fracture Separation, 18 +Fracture Height, 165 + +Reservoir Density, 2800 +Reservoir Depth, 2.6, -- km +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 +Reservoir Impedance, 0.001 + +Number of Segments, 1 +Gradient 1, 74 + +Number of Injection Wells, 6 +Number of Production Wells, 6 + +Production Flow Rate per Well, 100 + +Production Well Diameter, 9.625 +Injection Well Diameter, 9.625 + +Well Separation, 365 feet + +Ramey Production Wellbore Model, 1 +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Production Wellhead Pressure, 325 psi + +Utilization Factor, .9 +Water Loss Fraction, 0.10 +Maximum Drawdown, 0.0066 +Ambient Temperature, 10, -- degC +Surface Temperature, 10, -- degC +Circulation Pump Efficiency, 0.80 + +Well Geometry Configuration, 4 +Has Nonvertical Section, True +Multilaterals Cased, True +Number of Multilateral Sections, 3 +Nonvertical Length per Multilateral Section, 1433, -- meters + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 From faeae030216e1418bc326662476612ac14cc83e3 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:32:14 -0700 Subject: [PATCH 12/35] raise NotImplementedError for AddOn Electricity/Heat for SAM-EM --- src/geophires_x/EconomicsAddOns.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 2af9ea4e..4a5d7715 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -222,6 +222,8 @@ def read_parameters(self, model: Model) -> None: model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the parameters for the parent. + is_sam_econ_model = model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA + # Deal with all the parameter values that the user has provided that relate to this extension. # super.read_parameter will have already dealt with all the regular values, but anything unusual # may not be dealt with, so check. @@ -244,12 +246,21 @@ def read_parameters(self, model: Model) -> None: if key.startswith("AddOn OPEX"): val = float(model.InputParameters[key].sValue) self.AddOnOPEXPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Electricity Gained"): + if is_sam_econ_model: + raise NotImplementedError('AddOn Electricity is not supported for SAM Economic Models') + val = float(model.InputParameters[key].sValue) self.AddOnElecGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Heat Gained"): + if is_sam_econ_model: + raise NotImplementedError('AddOn Heat is not supported for SAM Economic Models') + val = float(model.InputParameters[key].sValue) self.AddOnHeatGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion + if key.startswith("AddOn Profit Gained"): val = float(model.InputParameters[key].sValue) self.AddOnProfitGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion From 61455f30deeb94a285fea50776d728dbd8e35d6f Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:37:56 -0700 Subject: [PATCH 13/35] Print Total Add-On CAPEX in Capital Costs for SAM-EM --- src/geophires_x/Outputs.py | 5 +++++ tests/examples/example_SAM-single-owner-PPA-3.out | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index ca148870..a65570a9 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -502,6 +502,11 @@ def PrintOutputs(self, model: Model): icc_label = Outputs._field_label(econ.inflation_cost_during_construction.display_name, 47) f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') + if econ.DoAddOnCalculations.value: + aoc_label = Outputs._field_label('Total Add-on CAPEX', 47) # TODO define dedicated OutputParameter + f.write( + f' {aoc_label}{model.addeconomics.AddOnCAPEXTotal.value:10.2f} {model.addeconomics.AddOnCAPEXTotal.CurrentUnits.value}\n') + capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total capex_label = Outputs._field_label(capex_param.display_name, 50) f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n') diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out index 7de1081c..8ca284fc 100644 --- a/tests/examples/example_SAM-single-owner-PPA-3.out +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -6,7 +6,7 @@ Simulation Metadata ---------------------- GEOPHIRES Version: 3.9.43 Simulation Date: 2025-07-28 - Simulation Time: 13:24 + Simulation Time: 13:37 Calculation Time: 1.159 sec ***SUMMARY OF RESULTS*** @@ -107,6 +107,7 @@ Simulation Metadata Total surface equipment costs: 150.23 MUSD Exploration costs: 3.89 MUSD Inflation costs during construction: 13.12 MUSD + Total Add-on CAPEX: 50.00 MUSD Total CAPEX: 275.47 MUSD From 19ec4bb64611bd53214950bbfd606a3a9a501c79 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:41:37 -0700 Subject: [PATCH 14/35] Print Add-on OPEX in opex category for SAM-EM --- src/geophires_x/Outputs.py | 11 ++++++++++- tests/examples/example_SAM-single-owner-PPA-3.out | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index a65570a9..c8cb03cc 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -503,7 +503,9 @@ def PrintOutputs(self, model: Model): f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') if econ.DoAddOnCalculations.value: - aoc_label = Outputs._field_label('Total Add-on CAPEX', 47) # TODO define dedicated OutputParameter + # Non-SAM econ models print this in Extended Economics profile + # TODO define dedicated OutputParameter + aoc_label = Outputs._field_label('Total Add-on CAPEX', 47) f.write( f' {aoc_label}{model.addeconomics.AddOnCAPEXTotal.value:10.2f} {model.addeconomics.AddOnCAPEXTotal.CurrentUnits.value}\n') @@ -536,6 +538,13 @@ def PrintOutputs(self, model: Model): redrill_label = Outputs._field_label(econ.redrilling_annual_cost.display_name, 47) f.write(f' {redrill_label}{econ.redrilling_annual_cost.value:10.2f} {econ.redrilling_annual_cost.CurrentUnits.value}\n') + if econ.DoAddOnCalculations.value and is_sam_econ_model: + # Non-SAM econ models print this in Extended Economics profile + # TODO define dedicated OutputParameter + aoc_label = Outputs._field_label('Total Add-on OPEX', 47) + f.write( + f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') + f.write(f' {econ.Coam.display_name}: {(econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value):10.2f} {econ.Coam.CurrentUnits.value}\n') else: f.write(f' {econ.Coam.display_name}: {econ.Coam.value:10.2f} {econ.Coam.CurrentUnits.value}\n') diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out index 8ca284fc..50a70f80 100644 --- a/tests/examples/example_SAM-single-owner-PPA-3.out +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -6,8 +6,8 @@ Simulation Metadata ---------------------- GEOPHIRES Version: 3.9.43 Simulation Date: 2025-07-28 - Simulation Time: 13:37 - Calculation Time: 1.159 sec + Simulation Time: 13:40 + Calculation Time: 1.157 sec ***SUMMARY OF RESULTS*** @@ -116,6 +116,7 @@ Simulation Metadata Wellfield maintenance costs: 1.13 MUSD/yr Power plant maintenance costs: 3.90 MUSD/yr Water costs: 1.58 MUSD/yr + Total Add-on OPEX: 1.00 MUSD/yr Total operating and maintenance costs: 7.60 MUSD/yr From e6224d9b63a84ebf517a9f462c570327b3471dfe Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:52:22 -0700 Subject: [PATCH 15/35] result fields & unit test/code cleanup --- src/geophires_x/EconomicsAddOns.py | 2 +- src/geophires_x_client/geophires_x_result.py | 2 ++ tests/geophires_x_tests/test_economics_sam.py | 30 +++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index 4a5d7715..ce9f4bb6 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -317,9 +317,9 @@ def Calculate(self, model: Model) -> None: self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value if is_sam_em: + # SAM econ models incorporate add-ons into main economics, not as separate extended economics model.economics.CCap.value = self.AdjustedProjectCAPEX.value model.economics.Coam.value = self.AdjustedProjectOPEX.value - # FIXME WIP AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.construction_years.value diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index ea65817b..00bc218b 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -261,6 +261,7 @@ class GeophiresXResult: 'Exploration costs', 'Investment Tax Credit', 'Inflation costs during construction', + 'Total Add-on CAPEX', 'Total capital costs', 'Annualized capital costs', # AGS/CLGS @@ -289,6 +290,7 @@ class GeophiresXResult: 'Average annual auxiliary fuel cost', 'Average annual pumping cost', 'Redrilling costs', + 'Total Add-on OPEX', 'Total average annual O&M costs', 'Total operating and maintenance costs', # AGS/CLGS diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 3ef1c366..653282bf 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -581,20 +581,24 @@ def test_add_ons(self): ) self.assertIsNotNone(add_ons_result) - with open(add_ons_result.output_file_path, encoding='utf-8') as f: - print('With Add-Ons:\n') - print(f.read()) - print('\n------------\n') - - with open(no_add_ons_result.output_file_path, encoding='utf-8') as f: - print('Without Add-Ons:\n') - print(f.read()) - print('\n------------\n') - - # FIXME WIP - self.assertLess( - no_add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + self.assertGreater( add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + no_add_ons_result.result['SUMMARY OF RESULTS']['Total CAPEX']['value'], + ) + + self.assertGreater(add_ons_result.result['CAPITAL COSTS (M$)']['Total Add-on CAPEX']['value'], 0) + + self.assertGreater( + add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']['Total operating and maintenance costs'][ + 'value' + ], + no_add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'][ + 'Total operating and maintenance costs' + ]['value'], + ) + + self.assertGreater( + add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']['Total Add-on OPEX']['value'], 0 ) @staticmethod From 989efeb07e4234ba39b5162a389e8f107ceecc82 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:55:25 -0700 Subject: [PATCH 16/35] Assert AddOn Electricity/Heat are not supported (for now) --- tests/geophires_x_tests/test_economics_sam.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 653282bf..cbb01772 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -601,6 +601,18 @@ def test_add_ons(self): add_ons_result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']['Total Add-on OPEX']['value'], 0 ) + with self.assertRaises(RuntimeError, msg='AddOn Electricity is not supported for SAM Economic Models'): + self._get_result( + {'Do AddOn Calculations': True, 'AddOn Electricity Gained 1': 100}, + file_path=self._get_test_file_path('egs-sam-em-add-ons.txt'), + ) + + with self.assertRaises(RuntimeError, msg='AddOn Heat is not supported for SAM Economic Models'): + self._get_result( + {'Do AddOn Calculations': True, 'AddOn Heat Gained 1': 100}, + file_path=self._get_test_file_path('egs-sam-em-add-ons.txt'), + ) + @staticmethod def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None, read_and_calculate=True) -> Model: if additional_params is not None: From 1a3d124d054855146fef9b98c54febe9625c01bc Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:11:15 -0700 Subject: [PATCH 17/35] Document SAM-EM Add-ons support --- docs/SAM-Economic-Models.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index cac5b78e..fe6e2943 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -30,6 +30,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM | `Fraction of Investment in Bonds` | Financial Parameters → Project Term Debt | `Debt percent` | `Singleowner` | `debt_percent` | .. N/A | | `Inflated Bond Interest Rate` | Financial Parameters → Project Term Debt | `Annual interest rate` | `Singleowner` | `term_int_rate` | .. N/A | | `Starting Electricity Sale Price`, `Ending Electricity Sale Price`, `Electricity Escalation Rate Per Year`, `Electricity Escalation Start Year` | Revenue | `PPA price` | `Singleowner` | `ppa_price_input` | GEOPHIRES's pricing model is used to create a PPA price schedule that is passed to SAM. | +| `Total AddOn Profit Gained` | Revenue → Capacity Payments | `Fixed amount`, `Capacity payment amount` | `Singleowner` | `cp_capacity_payment_type = 1`, `cp_capacity_payment_amount` | | | `Investment Tax Credit Rate` | Incentives → Investment Tax Credit (ITC) | `Federal` → `Percentage (%)` | `Singleowner` | `itc_fed_percent` | Note that unlike the BICYCLE Economic Model's `Total capital costs`, SAM Economic Model's `Total CAPEX` is the total installed cost and does not subtract ITC value (if present). | | `Production Tax Credit Electricity` | Incentives → Production Tax Credit (PTC) | `Federal` → `Amount ($/kWh)` | `Singleowner` | `ptc_fed_amount` | .. N/A | | `Production Tax Credit Duration` | Incentives → Production Tax Credit (PTC) | `Federal` → `Term (years)` | `Singleowner` | `ptc_fed_term` | .. N/A | @@ -48,7 +49,9 @@ The following table describes how GEOPHIRES parameters are transformed into SAM ### Limitations 1. Only Electricity end-use is supported -2. Only 1 construction year is supported. Note that the `Inflation Rate During Construction` parameter can be used to partially account for longer construction periods. +2. Only 1 construction year is supported. Note that the `Inflation Rate During Construction` parameter can be used to + partially account for longer construction periods. +3. Add-ons electricity and heat are not currently supported. (Add-ons CAPEX, OPEX, and profit are supported.) ## Using SAM Economic Models with Existing GEOPHIRES Inputs @@ -135,6 +138,14 @@ You can then manually enter the parameters from the logged mapping into the SAM ![](sam-desktop-app-manually-enter-system-capacity-from-geophires-log.png) +## Add-Ons + +SAM Economic Models incorporate add-ons directly, unlike other GEOPHIRES economic models, which calculate separate +extended economics. +Total Add-on CAPEX is added to Total CAPEX. +Total Add-on OPEX is added to Total operating and maintenance costs. +Total AddOn Profit Gained per year is treated as fixed amount Capacity payment revenue. + ## Examples ### SAM Single Owner PPA: 50 MWe From 216673f88a82edbd9f9f135c8e219c9bf059d54a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:15:38 -0700 Subject: [PATCH 18/35] OutputsAddOns py38 compatibility --- src/geophires_x/OutputsAddOns.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index 47ad843f..d9b66ab9 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import pandas as pd from geophires_x.Outputs import Outputs From 86b682121ebc341e9857e9938ec663425b748e39 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:19:30 -0700 Subject: [PATCH 19/35] =?UTF-8?q?Bump=20version:=203.9.43=20=E2=86=92=203.?= =?UTF-8?q?9.44?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c1ce4bc1..09329652 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.43 +current_version = 3.9.44 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index d4a01198..9a43ed34 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.43 + version: 3.9.44 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index a0ea909a..63e89af0 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.43.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.44.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.43...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.44...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 8dbb519f..a9705458 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.43' +version = release = '3.9.44' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index ed100cc3..a297029d 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.43', + version='3.9.44', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 2037d6cd..d3a341d4 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.43' +__version__ = '3.9.44' From 92b18cce1d3114127f6ca0d1fb9f27bd2b7e37c4 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:27:27 -0700 Subject: [PATCH 20/35] Add example_SAM-single-owner-PPA-3 to README examples list --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 63e89af0..1f6f8853 100644 --- a/README.rst +++ b/README.rst @@ -308,7 +308,10 @@ Example-specific web interface deeplinks are listed in the Link column. - `example_SAM-single-owner-PPA-2.txt `__ - `.out `__ - `link `__ - + * - SAM Single Owner PPA: 50 MWe with Add-on + - `example_SAM-single-owner-PPA-3.txt `__ + - `.out `__ + - `link `__ .. raw:: html From 6e567342563ebcd5d4fba56659497daa73cdefb7 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:31:05 -0700 Subject: [PATCH 21/35] Schema & CSV test update --- src/geophires_x_schema_generator/geophires-result.json | 2 ++ tests/example1_addons.csv | 4 +++- tests/examples/example1_addons.out | 10 +++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index c8615020..fa9038a8 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -402,6 +402,7 @@ "description": "The calculated amount of cost escalation due to inflation over the construction period.", "units": "MUSD" }, + "Total Add-on CAPEX": {}, "Total capital costs": { "type": "number", "description": "Total Capital Cost", @@ -460,6 +461,7 @@ "description": "Total redrilling costs over the Plant Lifetime are calculated as (Drilling and completion costs + Stimulation costs) \u00d7 Number of times redrilling. The total is then divided over Plant Lifetime years to calculate Redrilling costs per year.", "units": "MUSD/yr" }, + "Total Add-on OPEX": {}, "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", diff --git a/tests/example1_addons.csv b/tests/example1_addons.csv index 34b82892..4c6b596b 100644 --- a/tests/example1_addons.csv +++ b/tests/example1_addons.csv @@ -79,11 +79,13 @@ CAPITAL COSTS (M$),Surface power plant costs,,20.8,MUSD CAPITAL COSTS (M$),Field gathering system costs,,2.3,MUSD CAPITAL COSTS (M$),Total surface equipment costs,,23.1,MUSD CAPITAL COSTS (M$),Exploration costs,,4.49,MUSD +CAPITAL COSTS (M$),Total Add-on CAPEX,,70.0,MUSD CAPITAL COSTS (M$),Total capital costs,,25.67,MUSD CAPITAL COSTS (M$),Annualized capital costs,,1.28,MUSD OPERATING AND MAINTENANCE COSTS (M$/yr),Wellfield maintenance costs,,0.39,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Power plant maintenance costs,,0.9,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Water costs,,0.06,MUSD/yr +OPERATING AND MAINTENANCE COSTS (M$/yr),Total Add-on OPEX,,1.7,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Total operating and maintenance costs,,-0.86,MUSD/yr SURFACE EQUIPMENT SIMULATION RESULTS,Initial geofluid availability,,0.11,MW/(kg/s) SURFACE EQUIPMENT SIMULATION RESULTS,Maximum Total Electricity Generation,,5.62,MW @@ -99,7 +101,7 @@ SURFACE EQUIPMENT SIMULATION RESULTS,Average Annual Net Electricity Generation,, SURFACE EQUIPMENT SIMULATION RESULTS,Average Pumping Power,,0.2,MW SURFACE EQUIPMENT SIMULATION RESULTS,Initial pumping power/net installed power,,3.82,% SURFACE EQUIPMENT SIMULATION RESULTS,Heat to Power Conversion Efficiency,,10.07,% -Simulation Metadata,GEOPHIRES Version,,3.9.28, +Simulation Metadata,GEOPHIRES Version,,3.9.44, POWER GENERATION PROFILE,THERMAL DRAWDOWN,1,1.0, POWER GENERATION PROFILE,THERMAL DRAWDOWN,2,1.0056, POWER GENERATION PROFILE,THERMAL DRAWDOWN,3,1.0073, diff --git a/tests/examples/example1_addons.out b/tests/examples/example1_addons.out index 6cadf1d9..89151b40 100644 --- a/tests/examples/example1_addons.out +++ b/tests/examples/example1_addons.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:18 - Calculation Time: 0.882 sec + GEOPHIRES Version: 3.9.44 + Simulation Date: 2025-07-28 + Simulation Time: 14:30 + Calculation Time: 0.858 sec ***SUMMARY OF RESULTS*** @@ -26,7 +26,7 @@ Simulation Metadata Economic Model = Fixed Charge Rate (FCR) Fixed Charge Rate (FCR): 5.00 - Accrued financing during construction: 0.00 % + Accrued financing during construction: 0.00 % Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 72.08 MUSD From 77dc22ca53ac74246afa3820ad72716596aeffd7 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:01:55 -0700 Subject: [PATCH 22/35] Ensure fields that may occur in multiple categories are only parsed in the categories in which they are present in the case report --- src/geophires_x_client/geophires_x_result.py | 77 ++++++++++++++++--- tests/example1_addons.csv | 3 - tests/examples/example1_addons.out | 6 +- tests/geophires-result_example-3.csv | 1 - .../test_geophires_x_result.py | 15 ++++ tests/regenerate-example-result.sh | 2 +- tests/regenerate_example_result_csv.py | 18 ++++- 7 files changed, 100 insertions(+), 22 deletions(-) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 00bc218b..9f9f364f 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -381,9 +381,9 @@ def __init__(self, output_file_path, logger_name=None): self._logger = _get_logger(logger_name) self.output_file_path = output_file_path - f = open(self.output_file_path) - self._lines = list(f.readlines()) - f.close() + with open(self.output_file_path, encoding='utf-8') as f: + self._lines = list(f.readlines()) + self._lines_by_category = self._get_lines_by_category() # TODO generic-er result value map @@ -393,19 +393,26 @@ def __init__(self, output_file_path, logger_name=None): fields = category_fields[1] self.result[category] = {} + category_lines = self._lines_by_category.get(category, []) + for field in fields: if isinstance(field, _EqualSignDelimitedField): - self.result[category][field.field_name] = self._get_equal_sign_delimited_field(field.field_name) + self.result[category][field.field_name] = self._get_equal_sign_delimited_field( + field.field_name, search_lines=category_lines + ) elif isinstance(field, _UnlabeledStringField): self.result[category][field.field_name] = self._get_unlabeled_string_field( - field.field_name, field.marker_prefixes + field.field_name, field.marker_prefixes, search_lines=category_lines ) else: is_string_field = isinstance(field, _StringValueField) field_name = field.field_name if is_string_field else field indent = 4 if category != 'Simulation Metadata' else 1 self.result[category][field_name] = self._get_result_field( - field_name, is_string_value_field=is_string_field, min_indentation_spaces=indent + field_name, + is_string_value_field=is_string_field, + min_indentation_spaces=indent, + search_lines=category_lines, ) try: @@ -446,6 +453,35 @@ def __init__(self, output_file_path, logger_name=None): if self._get_end_use_option() is not None: self.result['metadata']['End-Use Option'] = self._get_end_use_option().name + def _get_lines_by_category(self) -> dict[str, list[str]]: + """ + Parses the raw output file lines into a dictionary where keys are + category headers and values are the lines belonging to that category. + """ + lines_by_category = {} + current_category = None + known_headers = list(self._RESULT_FIELDS_BY_CATEGORY.keys()) + + for line in self._lines: + + def get_header_content(h_: str) -> str: + if h_ == 'Simulation Metadata': + return h_ + return f'***{h_}***' + + # Check if the line is a category header + found_header = next((h for h in known_headers if get_header_content(h) == line.strip()), None) + + if found_header: + current_category = found_header + if current_category not in lines_by_category: + lines_by_category[current_category] = [] + elif current_category: + # Append the line to the current category if one has been found + lines_by_category[current_category].append(line) + + return lines_by_category + @property def direct_use_heat_breakeven_price_USD_per_MMBTU(self): summary = self.result['SUMMARY OF RESULTS'] @@ -552,9 +588,18 @@ def _json_fields(self) -> MappingProxyType: except FileNotFoundError: return {} - def _get_result_field(self, field_name: str, is_string_value_field: bool = False, min_indentation_spaces: int = 4): + def _get_result_field( + self, + field_name: str, + is_string_value_field: bool = False, + min_indentation_spaces: int = 4, + search_lines: list[str] | None = None, + ): + if search_lines is None: + search_lines = self._lines + # TODO make this less fragile with proper regex - matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, self._lines)) + matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Field not found: {field_name}') @@ -592,14 +637,17 @@ def normalize_spaces(matched_line): return {'value': self._parse_number(str_val, field=f'field "{field_name}"'), 'unit': unit} - def _get_equal_sign_delimited_field(self, field_name): + def _get_equal_sign_delimited_field(self, field_name, search_lines: list[str] | None = None): + if search_lines is None: + search_lines = self._lines + metadata_markers = ( f' {field_name} = ', # Previous versions of GEOPHIRES erroneously included an extra space after the field name so we include # the pattern for it for backwards compatibility with existing .out files. f' {field_name} = ', ) - matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), self._lines)) + matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Equal sign-delimited field not found: {field_name}') @@ -619,8 +667,13 @@ def _get_equal_sign_delimited_field(self, field_name): self._logger.error(f'Unexpected error extracting equal sign-delimited field {field_name}') # Shouldn't happen return None - def _get_unlabeled_string_field(self, field_name: str, marker_prefixes: list[str]): - matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), self._lines)) + def _get_unlabeled_string_field( + self, field_name: str, marker_prefixes: list[str], search_lines: list[str] | None = None + ): + if search_lines is None: + search_lines = self._lines + + matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), search_lines)) if len(matching_lines) == 0: self._logger.debug(f'Unlabeled string field not found: {field_name}') diff --git a/tests/example1_addons.csv b/tests/example1_addons.csv index 4c6b596b..0599d06c 100644 --- a/tests/example1_addons.csv +++ b/tests/example1_addons.csv @@ -71,7 +71,6 @@ RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.0,degC RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,217.9,kPa RESERVOIR SIMULATION RESULTS,Average Production Well Pump Pressure Drop,,1112.0,kPa -RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,5.39,MW CAPITAL COSTS (M$),Drilling and completion costs,,17.38,MUSD CAPITAL COSTS (M$),Drilling and completion costs per well,,4.35,MUSD CAPITAL COSTS (M$),Stimulation costs,,3.02,MUSD @@ -79,13 +78,11 @@ CAPITAL COSTS (M$),Surface power plant costs,,20.8,MUSD CAPITAL COSTS (M$),Field gathering system costs,,2.3,MUSD CAPITAL COSTS (M$),Total surface equipment costs,,23.1,MUSD CAPITAL COSTS (M$),Exploration costs,,4.49,MUSD -CAPITAL COSTS (M$),Total Add-on CAPEX,,70.0,MUSD CAPITAL COSTS (M$),Total capital costs,,25.67,MUSD CAPITAL COSTS (M$),Annualized capital costs,,1.28,MUSD OPERATING AND MAINTENANCE COSTS (M$/yr),Wellfield maintenance costs,,0.39,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Power plant maintenance costs,,0.9,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Water costs,,0.06,MUSD/yr -OPERATING AND MAINTENANCE COSTS (M$/yr),Total Add-on OPEX,,1.7,MUSD/yr OPERATING AND MAINTENANCE COSTS (M$/yr),Total operating and maintenance costs,,-0.86,MUSD/yr SURFACE EQUIPMENT SIMULATION RESULTS,Initial geofluid availability,,0.11,MW/(kg/s) SURFACE EQUIPMENT SIMULATION RESULTS,Maximum Total Electricity Generation,,5.62,MW diff --git a/tests/examples/example1_addons.out b/tests/examples/example1_addons.out index 89151b40..a29a34fa 100644 --- a/tests/examples/example1_addons.out +++ b/tests/examples/example1_addons.out @@ -5,9 +5,9 @@ Simulation Metadata ---------------------- GEOPHIRES Version: 3.9.44 - Simulation Date: 2025-07-28 - Simulation Time: 14:30 - Calculation Time: 0.858 sec + Simulation Date: 2025-07-29 + Simulation Time: 06:46 + Calculation Time: 0.898 sec ***SUMMARY OF RESULTS*** diff --git a/tests/geophires-result_example-3.csv b/tests/geophires-result_example-3.csv index 59715605..47104012 100644 --- a/tests/geophires-result_example-3.csv +++ b/tests/geophires-result_example-3.csv @@ -51,7 +51,6 @@ RESERVOIR SIMULATION RESULTS,Average Reservoir Heat Extraction,,133.3,MW RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey Model, RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.6,degC RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,650.2,kPa -RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,19.7,MW CAPITAL COSTS (M$),Drilling and completion costs,,34.45,MUSD CAPITAL COSTS (M$),Drilling and completion costs per well,,5.74,MUSD CAPITAL COSTS (M$),Stimulation costs,,4.53,MUSD diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index 9185ac47..ae7f6ece 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -16,6 +16,12 @@ def test_get_sam_cash_flow_row_name_unit_split(self) -> None: actual = GeophiresXResult._get_sam_cash_flow_row_name_unit_split(case[0]) self.assertListEqual(actual, case[1]) + def test_get_lines_by_category(self) -> None: + r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out')) + lines_by_cat = r._get_lines_by_category() + res_params_lines = lines_by_cat['RESERVOIR PARAMETERS'] + self.assertGreater(len(res_params_lines), 0) + def test_reservoir_volume_calculation_note(self) -> None: r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out')) field_name = 'Reservoir volume calculation note' @@ -37,3 +43,12 @@ def test_sam_economic_model_result_csv(self) -> None: r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('sam-em-csv-test.out')) as_csv = r.as_csv() self.assertIsNotNone(as_csv) + + def test_multicategory_fields_only_in_case_report_category(self) -> None: + r: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/example_SAM-single-owner-PPA-3.out') + ) + self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on CAPEX']) + self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on OPEX']) + self.assertIn('Total Add-on CAPEX', r.result['CAPITAL COSTS (M$)']) + self.assertIn('Total Add-on OPEX', r.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']) diff --git a/tests/regenerate-example-result.sh b/tests/regenerate-example-result.sh index be7624a7..faef5cee 100755 --- a/tests/regenerate-example-result.sh +++ b/tests/regenerate-example-result.sh @@ -16,5 +16,5 @@ rm examples/$1.json if [[ $1 == "example1_addons" ]] then echo "Updating CSV..." - python regenerate_example_result_csv.py + python regenerate_example_result_csv.py example1_addons fi diff --git a/tests/regenerate_example_result_csv.py b/tests/regenerate_example_result_csv.py index 0c8319a9..30a86306 100644 --- a/tests/regenerate_example_result_csv.py +++ b/tests/regenerate_example_result_csv.py @@ -1,3 +1,4 @@ +import argparse import os from geophires_x_client import GeophiresXResult @@ -8,5 +9,18 @@ def _get_file_path(file_name: str) -> str: if __name__ == '__main__': - with open(_get_file_path('example1_addons.csv'), 'w', encoding='utf-8') as csvfile: - csvfile.write(GeophiresXResult(_get_file_path('examples/example1_addons.out')).as_csv()) + parser = argparse.ArgumentParser(description='Regenerate a CSV result file from a GEOPHIRES-X example .out file.') + parser.add_argument( + 'example_name', + type=str, + nargs='?', # Makes the argument optional + default='example1_addons', + help='The base name of the example file (e.g., "example1_addons"). Defaults to "example1_addons".', + ) + args = parser.parse_args() + + example_name = args.example_name + example_relative_path = f'{"examples/" if example_name.startswith("example") else ""}{example_name}.out' + + with open(_get_file_path(f'{example_name}.csv'), 'w', encoding='utf-8') as csvfile: + csvfile.write(GeophiresXResult(_get_file_path(example_relative_path)).as_csv()) From 762564b0c103e8e439ed97dc8b456b6a3e7a9174 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:13:05 -0700 Subject: [PATCH 23/35] test that Average Net Electricity Production/Generation appear in expected categories --- tests/geophires_x_client_tests/test_geophires_x_result.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index ae7f6ece..d9a5a6e6 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -50,5 +50,10 @@ def test_multicategory_fields_only_in_case_report_category(self) -> None: ) self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on CAPEX']) self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on OPEX']) + self.assertIn('Total Add-on CAPEX', r.result['CAPITAL COSTS (M$)']) self.assertIn('Total Add-on OPEX', r.result['OPERATING AND MAINTENANCE COSTS (M$/yr)']) + + self.assertIsNone(r.result['RESERVOIR SIMULATION RESULTS']['Average Net Electricity Production']) + self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['Average Net Electricity Production']) + self.assertIsNotNone(r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Average Net Electricity Generation']) From 1e6f1b4361a97cce193f31403f6b4b04a681710f Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:28:27 -0700 Subject: [PATCH 24/35] AddOn CAPEX/OPEX total OutputParameters --- src/geophires_x/EconomicsAddOns.py | 2 ++ src/geophires_x/Outputs.py | 12 ++++------ .../geophires-result.json | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/geophires_x/EconomicsAddOns.py b/src/geophires_x/EconomicsAddOns.py index ce9f4bb6..17f6fa62 100644 --- a/src/geophires_x/EconomicsAddOns.py +++ b/src/geophires_x/EconomicsAddOns.py @@ -108,12 +108,14 @@ def multi_addon_tooltip_text(param_name: str) -> str: # results self.AddOnCAPEXTotal = self.OutputParameterDict[self.AddOnCAPEXTotal.Name] = OutputParameter( "AddOn CAPEX Total", + display_name='Total Add-on CAPEX', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, ) self.AddOnOPEXTotalPerYear = self.OutputParameterDict[self.AddOnOPEXTotalPerYear.Name] = OutputParameter( "AddOn OPEX Total Per Year", + display_name='Total Add-on OPEX', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index c8cb03cc..51bcdee5 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -504,10 +504,8 @@ def PrintOutputs(self, model: Model): if econ.DoAddOnCalculations.value: # Non-SAM econ models print this in Extended Economics profile - # TODO define dedicated OutputParameter - aoc_label = Outputs._field_label('Total Add-on CAPEX', 47) - f.write( - f' {aoc_label}{model.addeconomics.AddOnCAPEXTotal.value:10.2f} {model.addeconomics.AddOnCAPEXTotal.CurrentUnits.value}\n') + aoc_label = Outputs._field_label(model.addeconomics.AddOnCAPEXTotal.display_name, 47) + f.write(f' {aoc_label}{model.addeconomics.AddOnCAPEXTotal.value:10.2f} {model.addeconomics.AddOnCAPEXTotal.CurrentUnits.value}\n') capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total capex_label = Outputs._field_label(capex_param.display_name, 50) @@ -540,10 +538,8 @@ def PrintOutputs(self, model: Model): if econ.DoAddOnCalculations.value and is_sam_econ_model: # Non-SAM econ models print this in Extended Economics profile - # TODO define dedicated OutputParameter - aoc_label = Outputs._field_label('Total Add-on OPEX', 47) - f.write( - f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') + aoc_label = Outputs._field_label(model.addeconomics.AddOnOPEXTotalPerYear.display_name, 47) + f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') f.write(f' {econ.Coam.display_name}: {(econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value):10.2f} {econ.Coam.CurrentUnits.value}\n') else: diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index fa9038a8..015a43fc 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -152,8 +152,16 @@ "Project VIR=PI=PIR (including AddOns)": {}, "Project MOIC (including AddOns)": {}, "Project Payback Period (including AddOns)": {}, - "Total Add-on CAPEX": {}, - "Total Add-on OPEX": {}, + "Total Add-on CAPEX": { + "type": "number", + "description": "AddOn CAPEX Total", + "units": "MUSD" + }, + "Total Add-on OPEX": { + "type": "number", + "description": "AddOn OPEX Total Per Year", + "units": "MUSD/yr" + }, "Total Add-on Net Elec": {}, "Total Add-on Net Heat": {}, "Total Add-on Profit": {}, @@ -402,7 +410,11 @@ "description": "The calculated amount of cost escalation due to inflation over the construction period.", "units": "MUSD" }, - "Total Add-on CAPEX": {}, + "Total Add-on CAPEX": { + "type": "number", + "description": "AddOn CAPEX Total", + "units": "MUSD" + }, "Total capital costs": { "type": "number", "description": "Total Capital Cost", @@ -461,7 +473,11 @@ "description": "Total redrilling costs over the Plant Lifetime are calculated as (Drilling and completion costs + Stimulation costs) \u00d7 Number of times redrilling. The total is then divided over Plant Lifetime years to calculate Redrilling costs per year.", "units": "MUSD/yr" }, - "Total Add-on OPEX": {}, + "Total Add-on OPEX": { + "type": "number", + "description": "AddOn OPEX Total Per Year", + "units": "MUSD/yr" + }, "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", From e9a277c46cc4c3459e8274a380fe7ac301e6d8e9 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:36:42 -0700 Subject: [PATCH 25/35] Clarify SAM-EM Add-ons limitations. Deeplink to add-ons section of documentation from example_SAM-single-owner-PPA-3 description. --- docs/SAM-Economic-Models.md | 5 ++++- tests/examples/example_SAM-single-owner-PPA-3.txt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index fe6e2943..64c0bef6 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -51,7 +51,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM 1. Only Electricity end-use is supported 2. Only 1 construction year is supported. Note that the `Inflation Rate During Construction` parameter can be used to partially account for longer construction periods. -3. Add-ons electricity and heat are not currently supported. (Add-ons CAPEX, OPEX, and profit are supported.) +3. Add-ons with electricity and heat are not currently supported. (Add-ons CAPEX, OPEX, and profit are supported.) ## Using SAM Economic Models with Existing GEOPHIRES Inputs @@ -146,6 +146,9 @@ Total Add-on CAPEX is added to Total CAPEX. Total Add-on OPEX is added to Total operating and maintenance costs. Total AddOn Profit Gained per year is treated as fixed amount Capacity payment revenue. +Add-ons CAPEX, OPEX, and profit are supported. +Add-ons with electricity and heat are not currently supported, but may be supported in the future. + ## Examples ### SAM Single Owner PPA: 50 MWe diff --git a/tests/examples/example_SAM-single-owner-PPA-3.txt b/tests/examples/example_SAM-single-owner-PPA-3.txt index 53b8d79c..792e315a 100644 --- a/tests/examples/example_SAM-single-owner-PPA-3.txt +++ b/tests/examples/example_SAM-single-owner-PPA-3.txt @@ -1,6 +1,6 @@ # Example: SAM Single Owner PPA Economic Model: 50 MWe with Add-Ons # This example models example_SAM-single-owner with a Waste Heat Absorption Chiller Add-on -# See "SAM Economic Models" in GEOPHIRES documentation: https://nrel.github.io/GEOPHIRES-X/SAM-Economic-Models.html +# See "SAM Economic Models" in GEOPHIRES documentation: https://nrel.github.io/GEOPHIRES-X/SAM-Economic-Models.html#add-ons # *** ADD-ONS *** # *************** From 78b07263fbcc3aca2f342ce0c78f1398704472ad Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:37:54 -0700 Subject: [PATCH 26/35] =?UTF-8?q?Bump=20version:=203.9.44=20=E2=86=92=203.?= =?UTF-8?q?9.45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 09329652..e0ec3143 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.44 +current_version = 3.9.45 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 9a43ed34..157b6b23 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.44 + version: 3.9.45 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 1f6f8853..667fcc4b 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.44.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.45.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.44...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.45...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index a9705458..49e3d52d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.44' +version = release = '3.9.45' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index a297029d..94c0a139 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.44', + version='3.9.45', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index d3a341d4..7fcf7601 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.44' +__version__ = '3.9.45' From 555ebb88b09c74f255867954e8ef54f199761a37 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 07:39:05 -0700 Subject: [PATCH 27/35] CHANGELOG entry --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53739884..62811c2a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ GEOPHIRES v3 (2023-2025) v3.9 adds the `SAM Single Owner PPA Economic Model `__ +v3.9.45 adds `Add-Ons support for SAM Economic Models `__ + 3.8 ^^^ From e5e9bb216afbd66e444dd87a4d8ebaffdcc937b0 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:34:25 -0700 Subject: [PATCH 28/35] Search all lines for AGS/CLGS-style output, since these are currently recategorized into non-AGS/CLGS-style categories by the client (TODO to eventually address this backwards-compatibly) --- src/geophires_x_client/geophires_x_result.py | 16 +++++++++++++--- .../test_geophires_x_result.py | 6 ++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 9f9f364f..13ec4b25 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -383,6 +383,9 @@ def __init__(self, output_file_path, logger_name=None): with open(self.output_file_path, encoding='utf-8') as f: self._lines = list(f.readlines()) + + self.is_ags_clgs_style_output = '***AGS/CLGS STYLE OUTPUT***' in [_l.strip() for _l in self._lines] + self._lines_by_category = self._get_lines_by_category() # TODO generic-er result value map @@ -394,15 +397,16 @@ def __init__(self, output_file_path, logger_name=None): self.result[category] = {} category_lines = self._lines_by_category.get(category, []) + search_lines = category_lines if not self.is_ags_clgs_style_output else self._lines for field in fields: if isinstance(field, _EqualSignDelimitedField): self.result[category][field.field_name] = self._get_equal_sign_delimited_field( - field.field_name, search_lines=category_lines + field.field_name, search_lines=search_lines ) elif isinstance(field, _UnlabeledStringField): self.result[category][field.field_name] = self._get_unlabeled_string_field( - field.field_name, field.marker_prefixes, search_lines=category_lines + field.field_name, field.marker_prefixes, search_lines=search_lines ) else: is_string_field = isinstance(field, _StringValueField) @@ -412,7 +416,7 @@ def __init__(self, output_file_path, logger_name=None): field_name, is_string_value_field=is_string_field, min_indentation_spaces=indent, - search_lines=category_lines, + search_lines=search_lines, ) try: @@ -465,6 +469,12 @@ def _get_lines_by_category(self) -> dict[str, list[str]]: for line in self._lines: def get_header_content(h_: str) -> str: + """ + TODO adjust this to also work with AGS/CLGS-style headers like '### Cost Results ###' + For now, AGS-style results are parsed from all lines according to the categories defined in + _RESULT_FIELDS_BY_CATEGORY. + """ + if h_ == 'Simulation Metadata': return h_ return f'***{h_}***' diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index d9a5a6e6..e3bccf63 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -57,3 +57,9 @@ def test_multicategory_fields_only_in_case_report_category(self) -> None: self.assertIsNone(r.result['RESERVOIR SIMULATION RESULTS']['Average Net Electricity Production']) self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['Average Net Electricity Production']) self.assertIsNotNone(r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Average Net Electricity Generation']) + + def test_ags_clgs_style_output(self) -> None: + r: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/Beckers_et_al_2023_Tabulated_Database_Uloop_sCO2_elec.out') + ) + self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['LCOE']) From 665aa1c7df8b6c016ef9b7bbf2bca947896a0fe6 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:34:43 -0700 Subject: [PATCH 29/35] =?UTF-8?q?Bump=20version:=203.9.45=20=E2=86=92=203.?= =?UTF-8?q?9.46?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e0ec3143..d46aeef1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.45 +current_version = 3.9.46 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 157b6b23..98ab4811 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.45 + version: 3.9.46 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 667fcc4b..be919189 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.45.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.46.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.45...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.46...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 49e3d52d..95ee8cf2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.45' +version = release = '3.9.46' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 94c0a139..5a19964e 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.45', + version='3.9.46', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 7fcf7601..332ebc95 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.45' +__version__ = '3.9.46' From 0e38d57dbec68bf5624882b30ab1e33aed640dea Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:35:02 -0700 Subject: [PATCH 30/35] sync changelog entry to v3.9.46 --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 62811c2a..e906774c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ GEOPHIRES v3 (2023-2025) v3.9 adds the `SAM Single Owner PPA Economic Model `__ -v3.9.45 adds `Add-Ons support for SAM Economic Models `__ +v3.9.46 adds `Add-Ons support for SAM Economic Models `__ 3.8 From f03097e25da1ec01109a27cf59bdaadae798d38a Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:10:33 -0700 Subject: [PATCH 31/35] Parse SUTRA Reservoir Model in SUMMARY OF RESULTS (was previously being semi-erroneously recategorized into RESERVOIR PARAMETERS) --- src/geophires_x_client/geophires_x_result.py | 1 + tests/geophires_x_client_tests/test_geophires_x_result.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 13ec4b25..c44e3b2f 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -40,6 +40,7 @@ class GeophiresXResult: _StringValueField('End-Use Option'), _StringValueField('End-Use'), _StringValueField('Surface Application'), + _EqualSignDelimitedField('Reservoir Model'), # SUTRA only 'Average Net Electricity Production', 'Electricity breakeven price', 'Total CAPEX', diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index e3bccf63..0bf5fab2 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -63,3 +63,7 @@ def test_ags_clgs_style_output(self) -> None: self._get_test_file_path('../examples/Beckers_et_al_2023_Tabulated_Database_Uloop_sCO2_elec.out') ) self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['LCOE']) + + def test_sutra_reservoir_model_in_summary(self) -> None: + r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/SUTRAExample1.out')) + self.assertEqual('SUTRA Model', r.result['SUMMARY OF RESULTS']['Reservoir Model']) From 3e18f6239d8191517935fe3a92d3a9502d9b1b12 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:12:49 -0700 Subject: [PATCH 32/35] regenerate schema to sync with previous commit --- src/geophires_x_schema_generator/geophires-result.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 015a43fc..9b2251cc 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -11,6 +11,7 @@ "End-Use Option": {}, "End-Use": {}, "Surface Application": {}, + "Reservoir Model": {}, "Average Net Electricity Production": {}, "Electricity breakeven price": { "type": "number", From 392898cc930bf35897bdf63c3ea9788a68ac88a5 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:16:15 -0700 Subject: [PATCH 33/35] =?UTF-8?q?Bump=20version:=203.9.46=20=E2=86=92=203.?= =?UTF-8?q?9.47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/geophires_x/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d46aeef1..4915a551 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.46 +current_version = 3.9.47 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 98ab4811..8cef84ab 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.9.46 + version: 3.9.47 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index be919189..4569b460 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.46.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.47.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.46...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.47...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/conf.py b/docs/conf.py index 95ee8cf2..cde53599 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.9.46' +version = release = '3.9.47' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 5a19964e..5b79185b 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.46', + version='3.9.47', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 332ebc95..eaf2a48e 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.46' +__version__ = '3.9.47' From a04c772abed4e1dc7acf3552c33c3d3bec36341d Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:16:33 -0700 Subject: [PATCH 34/35] sync v3.9.47 CHANGELOG entry --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e906774c..1ceb48de 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ GEOPHIRES v3 (2023-2025) v3.9 adds the `SAM Single Owner PPA Economic Model `__ -v3.9.46 adds `Add-Ons support for SAM Economic Models `__ +v3.9.47 adds `Add-Ons support for SAM Economic Models `__ 3.8 From ff7ca58eb2ae209f19970af36f928025247d8600 Mon Sep 17 00:00:00 2001 From: softwareengineerprogrammer <4056124+softwareengineerprogrammer@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:44:07 -0700 Subject: [PATCH 35/35] CHANGELOG entry in reverse chronological order (for consistency) --- CHANGELOG.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1ceb48de..86d83382 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,10 +10,9 @@ GEOPHIRES v3 (2023-2025) `release `__ -v3.9 adds the `SAM Single Owner PPA Economic Model `__ - v3.9.47 adds `Add-Ons support for SAM Economic Models `__ +v3.9 adds the `SAM Single Owner PPA Economic Model `__ 3.8 ^^^