diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4915a551..20217b64 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.47 +current_version = 3.9.48 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 8cef84ab..b803036f 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.47 + version: 3.9.48 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index 4569b460..50ad772f 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.47.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.48.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.47...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.48...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 cde53599..b0ec6fb1 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.47' +version = release = '3.9.48' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 5b79185b..9d4710c5 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.47', + version='3.9.48', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/AGSWellBores.py b/src/geophires_x/AGSWellBores.py index 210a94fe..8998e2b3 100644 --- a/src/geophires_x/AGSWellBores.py +++ b/src/geophires_x/AGSWellBores.py @@ -1028,6 +1028,8 @@ def Calculate(self, model: Model) -> None: # (b/c we are not generating electricity) = thermosiphon is happening! self.PumpingPower.value = [max(x, 0.) for x in self.PumpingPower.value] + self._sync_output_params_from_input_params() + model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') def __str__(self): diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 4b0073ae..b89cb1fa 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import math import sys +# noinspection PyPackageRequirements import numpy as np import numpy_financial as npf from pint.facets.plain import PlainQuantity @@ -349,7 +352,7 @@ def CalculateFinancialPerformance(plantlifetime: int, return NPV, IRR, VIR, MOIC -def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: +def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple[float, float, float]: """ CalculateLCOELCOH calculates the levelized cost of electricity and heat for the project. :param econ: Economics object @@ -357,21 +360,45 @@ def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: :param model: The model object :type model: :class:`~geophires_x.Model.Model` :return: LCOE: The levelized cost of electricity and LCOH: The levelized cost of heat and LCOC: The levelized cost of cooling - :rtype: tuple + :rtype: tuple[float, float, float] """ LCOE = LCOH = LCOC = 0.0 CCap_elec = (econ.CCap.value * econ.CAPEX_heat_electricity_plant_ratio.value) Coam_elec = (econ.Coam.value * econ.CAPEX_heat_electricity_plant_ratio.value) CCap_heat = (econ.CCap.value * (1.0 - econ.CAPEX_heat_electricity_plant_ratio.value)) Coam_heat = (econ.Coam.value * (1.0 - econ.CAPEX_heat_electricity_plant_ratio.value)) + + def _capex_total_plus_construction_inflation() -> float: + # TODO should be return value instead of mutating econ + econ.inflation_cost_during_construction.value = quantity( + econ.CCap.value * econ.inflrateconstruction.value, + econ.CCap.CurrentUnits + ).to(econ.inflation_cost_during_construction.CurrentUnits).magnitude + + return econ.CCap.value + econ.inflation_cost_during_construction.value + + def _construction_inflation_cost_elec_heat() -> tuple[float, float]: + construction_inflation_cost_elec = CCap_elec * econ.inflrateconstruction.value + construction_inflation_cost_heat = CCap_heat * econ.inflrateconstruction.value + + # TODO should be return value instead of mutating econ + econ.inflation_cost_during_construction.value = quantity( + construction_inflation_cost_elec+ construction_inflation_cost_heat, + econ.CCap.CurrentUnits + ).to(econ.inflation_cost_during_construction.CurrentUnits).magnitude + + return CCap_elec + construction_inflation_cost_elec, CCap_heat + construction_inflation_cost_heat + # Calculate LCOE/LCOH/LCOC if econ.econmodel.value == EconomicModel.FCR: + capex_total_plus_infl = _capex_total_plus_construction_inflation() + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: - LCOE = (econ.FCR.value * (1 + econ.inflrateconstruction.value) * econ.CCap.value + econ.Coam.value) / \ + LCOE = (econ.FCR.value * capex_total_plus_infl + econ.Coam.value) / \ np.average(model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh elif (model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]): - LCOH = (econ.FCR.value * (1 + econ.inflrateconstruction.value) * econ.CCap.value + econ.Coam.value + + LCOH = (econ.FCR.value * capex_total_plus_infl + econ.Coam.value + econ.averageannualpumpingcosts.value) / np.average( model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu @@ -382,38 +409,40 @@ def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: - LCOE = (econ.FCR.value * (1 + econ.inflrateconstruction.value) * CCap_elec + Coam_elec) / np.average(model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh - LCOH = (econ.FCR.value * (1 + econ.inflrateconstruction.value) * CCap_heat + Coam_heat + econ.averageannualpumpingcosts.value) / np.average(model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh + capex_elec_plus_infl, capex_heat_plus_infl = _construction_inflation_cost_elec_heat() + LCOE = (econ.FCR.value * capex_elec_plus_infl + Coam_elec) / np.average(model.surfaceplant.NetkWhProduced.value) * 1E8 # cents/kWh + LCOH = (econ.FCR.value * capex_heat_plus_infl + Coam_heat + econ.averageannualpumpingcosts.value) / np.average(model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: - LCOC = (econ.FCR.value * ( - 1 + econ.inflrateconstruction.value) * econ.CCap.value + econ.Coam.value + econ.averageannualpumpingcosts.value) / np.average( + LCOC = (econ.FCR.value * capex_total_plus_infl + econ.Coam.value + econ.averageannualpumpingcosts.value) / np.average( model.surfaceplant.cooling_kWh_Produced.value) * 1E8 # cents/kWh LCOC = LCOC * 2.931 # $/Million Btu elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: - LCOH = (econ.FCR.value * ( - 1 + econ.inflrateconstruction.value) * econ.CCap.value + econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value) / np.average( + LCOH = (econ.FCR.value * capex_total_plus_infl + + econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualheatpumpelectricitycost.value) / np.average( model.surfaceplant.HeatkWhProduced.value) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: - LCOH = (econ.FCR.value * ( - 1 + econ.inflrateconstruction.value) * econ.CCap.value + econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualngcost.value) / model.surfaceplant.annual_heating_demand.value * 1E2 # cents/kWh + LCOH = (econ.FCR.value * capex_total_plus_infl + + econ.Coam.value + econ.averageannualpumpingcosts.value + econ.averageannualngcost.value) / model.surfaceplant.annual_heating_demand.value * 1E2 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif econ.econmodel.value == EconomicModel.STANDARDIZED_LEVELIZED_COST: discount_vector = 1. / np.power(1 + econ.discountrate.value, np.linspace(0, model.surfaceplant.plant_lifetime.value - 1, model.surfaceplant.plant_lifetime.value)) + capex_total_plus_infl = _capex_total_plus_construction_inflation() + if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: - LCOE = ((1 + econ.inflrateconstruction.value) * econ.CCap.value + np.sum( + LCOE = (capex_total_plus_infl + np.sum( econ.Coam.value * discount_vector)) / np.sum( model.surfaceplant.NetkWhProduced.value * discount_vector) * 1E8 # cents/kWh elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and \ model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: econ.averageannualpumpingcosts.value = np.average( model.surfaceplant.PumpingkWh.value) * model.surfaceplant.electricity_cost_to_buy.value / 1E6 # M$/year - LCOH = ((1 + econ.inflrateconstruction.value) * econ.CCap.value + np.sum(( - econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum( + LCOH = (capex_total_plus_infl + np.sum(( + econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * discount_vector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU @@ -424,24 +453,30 @@ def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: - LCOE = ((1 + econ.inflrateconstruction.value) * CCap_elec + np.sum(Coam_elec * discount_vector)) / np.sum(model.surfaceplant.NetkWhProduced.value * discount_vector) * 1E8 # cents/kWh - LCOH = ((1 + econ.inflrateconstruction.value) * CCap_heat + + capex_elec_plus_infl, capex_heat_plus_infl = _construction_inflation_cost_elec_heat() + + LCOE = (capex_elec_plus_infl + np.sum(Coam_elec * discount_vector)) / np.sum(model.surfaceplant.NetkWhProduced.value * discount_vector) * 1E8 # cents/kWh + LCOH = (capex_heat_plus_infl + np.sum((Coam_heat + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum(model.surfaceplant.HeatkWhProduced.value * discount_vector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/MMBTU elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: - LCOC = ((1 + econ.inflrateconstruction.value) * econ.CCap.value + np.sum(( - econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum( + capex_total_plus_infl = _capex_total_plus_construction_inflation() + + LCOC = (capex_total_plus_infl + np.sum(( + econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum( model.surfaceplant.cooling_kWh_Produced.value * discount_vector) * 1E8 # cents/kWh LCOC = LCOC * 2.931 # $/Million Btu elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: - LCOH = ((1 + econ.inflrateconstruction.value) * econ.CCap.value + np.sum( + capex_total_plus_infl = _capex_total_plus_construction_inflation() + LCOH = (capex_total_plus_infl + np.sum( (econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 + model.surfaceplant.heat_pump_electricity_kwh_used.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6) * discount_vector)) / np.sum( model.surfaceplant.HeatkWhProduced.value * discount_vector) * 1E8 # cents/kWh LCOH = LCOH * 2.931 # $/Million Btu elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT and model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: - LCOH = ((1 + econ.inflrateconstruction.value) * econ.CCap.value + np.sum( + capex_total_plus_infl = _capex_total_plus_construction_inflation() + LCOH = (capex_total_plus_infl + np.sum( (econ.Coam.value + model.surfaceplant.PumpingkWh.value * model.surfaceplant.electricity_cost_to_buy.value / 1E6 + econ.annualngcost.value) * discount_vector)) / np.sum( model.surfaceplant.annual_heating_demand.value * discount_vector) * 1E2 # cents/kWh @@ -457,10 +492,12 @@ def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: CRF = i_ave / (1 - np.power(1 + i_ave, -model.surfaceplant.plant_lifetime.value)) inflation_vector = np.power(1 + econ.RINFL.value, np.linspace(1, model.surfaceplant.plant_lifetime.value, model.surfaceplant.plant_lifetime.value)) discount_vector = 1. / np.power(1 + i_ave, np.linspace(1, model.surfaceplant.plant_lifetime.value, model.surfaceplant.plant_lifetime.value)) - NPV_cap = np.sum((1 + econ.inflrateconstruction.value) * econ.CCap.value * CRF * discount_vector) - NPV_fc = np.sum((1 + econ.inflrateconstruction.value) * econ.CCap.value * econ.PTR.value * inflation_vector * discount_vector) - NPV_it = np.sum(econ.CTR.value / (1 - econ.CTR.value) * ((1 + econ.inflrateconstruction.value) * econ.CCap.value * CRF - econ.CCap.value / model.surfaceplant.plant_lifetime.value) * discount_vector) - NPV_itc = (1 + econ.inflrateconstruction.value) * econ.CCap.value * econ.RITC.value / (1 - econ.CTR.value) + capex_total_plus_infl = _capex_total_plus_construction_inflation() + + NPV_cap = np.sum(capex_total_plus_infl * CRF * discount_vector) + NPV_fc = np.sum(capex_total_plus_infl * econ.PTR.value * inflation_vector * discount_vector) + NPV_it = np.sum(econ.CTR.value / (1 - econ.CTR.value) * (capex_total_plus_infl * CRF - econ.CCap.value / model.surfaceplant.plant_lifetime.value) * discount_vector) + NPV_itc = capex_total_plus_infl * econ.RITC.value / (1 - econ.CTR.value) if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: NPV_oandm = np.sum(econ.Coam.value * inflation_vector * discount_vector) @@ -479,21 +516,22 @@ def CalculateLCOELCOHLCOC(econ, model: Model) -> tuple: EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: + capex_elec_plus_infl, capex_heat_plus_infl = _construction_inflation_cost_elec_heat() - NPVcap_elec = np.sum((1 + econ.inflrateconstruction.value) * CCap_elec * CRF * discount_vector) - NPVfc_elec = np.sum((1 + econ.inflrateconstruction.value) * CCap_elec * econ.PTR.value * inflation_vector * discount_vector) - NPVit_elec = np.sum(econ.CTR.value / (1 - econ.CTR.value) * ((1 + econ.inflrateconstruction.value) * CCap_elec * CRF - CCap_elec / model.surfaceplant.plant_lifetime.value) * discount_vector) - NPVitc_elec = (1 + econ.inflrateconstruction.value) * CCap_elec * econ.RITC.value / (1 - econ.CTR.value) + NPVcap_elec = np.sum(capex_elec_plus_infl * CRF * discount_vector) + NPVfc_elec = np.sum(capex_elec_plus_infl * econ.PTR.value * inflation_vector * discount_vector) + NPVit_elec = np.sum(econ.CTR.value / (1 - econ.CTR.value) * (capex_elec_plus_infl * CRF - CCap_elec / model.surfaceplant.plant_lifetime.value) * discount_vector) + NPVitc_elec = capex_elec_plus_infl * econ.RITC.value / (1 - econ.CTR.value) NPVoandm_elec = np.sum(Coam_elec * inflation_vector * discount_vector) NPVgrt_elec = econ.GTR.value / (1 - econ.GTR.value) * (NPVcap_elec + NPVoandm_elec + NPVfc_elec + NPVit_elec - NPVitc_elec) LCOE = ((NPVcap_elec + NPVoandm_elec + NPVfc_elec + NPVit_elec + NPVgrt_elec - NPVitc_elec) / np.sum(model.surfaceplant.NetkWhProduced.value * inflation_vector * discount_vector) * 1E8) - NPV_cap_heat = np.sum((1 + econ.inflrateconstruction.value) * CCap_heat * CRF * discount_vector) + NPV_cap_heat = np.sum(capex_heat_plus_infl * CRF * discount_vector) NPV_fc_heat = np.sum((1 + econ.inflrateconstruction.value) * (econ.CCap.value * (1.0 - econ.CAPEX_heat_electricity_plant_ratio.value)) * econ.PTR.value * inflation_vector * discount_vector) - NPV_it_heat = np.sum(econ.CTR.value / (1 - econ.CTR.value) * ((1 + econ.inflrateconstruction.value) * CCap_heat * CRF - CCap_heat / model.surfaceplant.plant_lifetime.value) * discount_vector) - NPV_itc_heat = (1 + econ.inflrateconstruction.value) * CCap_heat * econ.RITC.value / (1 - econ.CTR.value) + NPV_it_heat = np.sum(econ.CTR.value / (1 - econ.CTR.value) * (capex_heat_plus_infl * CRF - CCap_heat / model.surfaceplant.plant_lifetime.value) * discount_vector) + NPV_itc_heat = capex_heat_plus_infl * econ.RITC.value / (1 - econ.CTR.value) NPV_oandm_heat = np.sum((econ.Coam.value * (1.0 - econ.CAPEX_heat_electricity_plant_ratio.value)) * inflation_vector * discount_vector) NPV_grt_heat = econ.GTR.value / (1 - econ.GTR.value) * (NPV_cap_heat + NPV_oandm_heat + NPV_fc_heat + NPV_it_heat - NPV_itc_heat) @@ -760,7 +798,7 @@ def __init__(self, model: Model): "Surface Plant Capital Cost", DefaultValue=-1.0, Min=0, - Max=1000, + Max=10000, UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 71dd8c62..1f586a14 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -139,8 +139,8 @@ def total_capex_parameter_output_parameter() -> OutputParameter: UnitType=Units.CURRENCY, CurrentUnits=CurrencyUnit.MDOLLARS, PreferredUnits=CurrencyUnit.MDOLLARS, - ToolTipText="The total capital expenditure (CAPEX) required to construct the plant. " - "This value includes all direct and indirect costs, contingency, and any cost escalation from " - "inflation during construction. It is used as the total installed cost input for " - "SAM Economic Models." + ToolTipText='The total capital expenditure (CAPEX) required to construct the plant. ' + 'This value includes all direct and indirect costs, and contingency. ' + 'For SAM Economic models, it also includes any cost escalation from inflation during construction. ' + 'It is used as the total installed cost input for SAM Economic Models.' ) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 51bcdee5..26c082fa 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -274,6 +274,20 @@ def PrintOutputs(self, model: Model): acf_label = Outputs._field_label(acf.display_name, 49) f.write(f' {acf_label}{acf.value:10.2f} {acf.CurrentUnits.value}\n') + display_inflation_costs_in_economic_parameters: bool = ( + econ.econmodel.value in [EconomicModel.BICYCLE, + EconomicModel.FCR, + EconomicModel.STANDARDIZED_LEVELIZED_COST] + and + econ.inflation_cost_during_construction.value != 0. + ) + if display_inflation_costs_in_economic_parameters: + # Inflation cost is displayed here for economic models that don't treat inflation cost as a + # capital cost + icc: OutputParameter = econ.inflation_cost_during_construction + icc_label = Outputs._field_label(icc.display_name, 49) + f.write(f' {icc_label}{icc.value:10.2f} {icc.CurrentUnits.value}\n') + f.write(f' Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} {model.surfaceplant.plant_lifetime.CurrentUnits.value}\n') f.write(f' Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %\n') @@ -327,8 +341,8 @@ def PrintOutputs(self, model: Model): f.write(' User-provided production well temperature drop\n') f.write(f' Constant production well temperature drop: {model.wellbores.tempdropprod.value:10.1f} ' + model.wellbores.tempdropprod.PreferredUnits.value + NL) f.write(f' Flowrate per production well: {model.wellbores.prodwellflowrate.value:10.1f} ' + model.wellbores.prodwellflowrate.CurrentUnits.value + NL) - f.write(f' Injection well casing ID: {model.wellbores.injwelldiam.value:10.3f} ' + model.wellbores.injwelldiam.CurrentUnits.value + NL) - f.write(f' Production well casing ID: {model.wellbores.prodwelldiam.value:10.3f} ' + model.wellbores.prodwelldiam.CurrentUnits.value + NL) + f.write(f' {model.wellbores.injection_well_casing_inner_diameter.display_name}: {model.wellbores.injection_well_casing_inner_diameter.value:10.3f} {model.wellbores.injection_well_casing_inner_diameter.CurrentUnits.value}\n') + f.write(f' {model.wellbores.production_well_casing_inner_diameter.display_name}: {model.wellbores.production_well_casing_inner_diameter.value:10.3f} {model.wellbores.production_well_casing_inner_diameter.CurrentUnits.value}\n') f.write(f' {model.wellbores.redrill.display_name}: {model.wellbores.redrill.value:10.0f}\n') if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: f.write(' Power plant type: ' + str(model.surfaceplant.plant_type.value.value) + NL) @@ -497,8 +511,8 @@ def PrintOutputs(self, model: Model): # expenditure. pass - if is_sam_econ_model: - # TODO calculate & display for other economic models + display_inflation_during_construction_in_capital_costs = is_sam_econ_model + if display_inflation_during_construction_in_capital_costs: 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') diff --git a/src/geophires_x/SBTWellbores.py b/src/geophires_x/SBTWellbores.py index 86082a5d..6cce5737 100644 --- a/src/geophires_x/SBTWellbores.py +++ b/src/geophires_x/SBTWellbores.py @@ -286,6 +286,8 @@ def Calculate(self, model: Model) -> None: # (b/c we are not generating electricity) = thermosiphon is happening! self.PumpingPower.value = [max(x, 0.) for x in self.PumpingPower.value] + self._sync_output_params_from_input_params() + model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') def CalculateNonverticalPressureDrop(self, model, value, time_max, al): diff --git a/src/geophires_x/SUTRAOutputs.py b/src/geophires_x/SUTRAOutputs.py index 58a047c1..813188cf 100644 --- a/src/geophires_x/SUTRAOutputs.py +++ b/src/geophires_x/SUTRAOutputs.py @@ -104,8 +104,8 @@ def PrintOutputs(self, model: Model): f.write(f' Pump efficiency: {pump_efficiency_display}{NL}') f.write(f" Lifetime Average Well Flow Rate: {np.average(abs(model.wellbores.ProductionWellFlowRates.value)):10.1f} " + model.wellbores.ProductionWellFlowRates.CurrentUnits.value + NL) - f.write(f" Injection well casing ID: {model.wellbores.injwelldiam.value:10.3f} " + model.wellbores.injwelldiam.CurrentUnits.value + NL) - f.write(f" Production well casing ID: {model.wellbores.prodwelldiam.value:10.3f} " + model.wellbores.prodwelldiam.CurrentUnits.value + NL) + f.write(f' {model.wellbores.injection_well_casing_inner_diameter.display_name}: {model.wellbores.injection_well_casing_inner_diameter.value:10.3f} {model.wellbores.injection_well_casing_inner_diameter.CurrentUnits.value}\n') + f.write(f' {model.wellbores.production_well_casing_inner_diameter.display_name}: {model.wellbores.production_well_casing_inner_diameter.value:10.3f} {model.wellbores.production_well_casing_inner_diameter.CurrentUnits.value}\n') f.write(NL) f.write(NL) f.write(" ***RESERVOIR SIMULATION RESULTS***" + NL) diff --git a/src/geophires_x/SUTRAWellBores.py b/src/geophires_x/SUTRAWellBores.py index ab77aea1..60fa81d9 100644 --- a/src/geophires_x/SUTRAWellBores.py +++ b/src/geophires_x/SUTRAWellBores.py @@ -342,4 +342,6 @@ def Calculate(self, model: Model) -> None: / model.surfaceplant.pump_efficiency.value ) - model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + self._sync_output_params_from_input_params() + + model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index dca1eb52..9d32bd30 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -3,7 +3,7 @@ from pint.facets.plain import PlainQuantity from .Parameter import floatParameter, intParameter, boolParameter, OutputParameter, ReadParameter, \ - coerce_int_params_to_enum_values + coerce_int_params_to_enum_values, Parameter from geophires_x.GeoPHIRESUtils import vapor_pressure_water_kPa, quantity, static_pressure_MPa from geophires_x.GeoPHIRESUtils import density_water_kg_per_m3 from geophires_x.GeoPHIRESUtils import viscosity_water_Pa_sec @@ -738,6 +738,7 @@ def __init__(self, model: Model): "same value." ) + # noinspection SpellCheckingInspection self.prodwelldiam = self.ParameterDict[self.prodwelldiam.Name] = floatParameter( "Production Well Diameter", DefaultValue=8.0, @@ -748,9 +749,10 @@ def __init__(self, model: Model): CurrentUnits=LengthUnit.INCHES, Required=True, ErrMessage="assume default production well diameter (8 inch)", - ToolTipText="Inner diameter of production wellbore (assumed constant along the wellbore) to calculate \ - frictional pressure drop and wellbore heat transmission with Rameys model" + ToolTipText='Inner diameter of production wellbore (assumed constant along the wellbore) to calculate ' + 'frictional pressure drop and wellbore heat transmission with Rameys model' ) + # noinspection SpellCheckingInspection self.injwelldiam = self.ParameterDict[self.injwelldiam.Name] = floatParameter( "Injection Well Diameter", DefaultValue=8.0, @@ -761,8 +763,8 @@ def __init__(self, model: Model): CurrentUnits=LengthUnit.INCHES, Required=True, ErrMessage="assume default injection well diameter (8 inch)", - ToolTipText="Inner diameter of production wellbore (assumed constant along the wellbore) to calculate " - "frictional pressure drop and wellbore heat transmission with Rameys model" + ToolTipText='Inner diameter of injection wellbore (assumed constant along the wellbore) to calculate ' + 'frictional pressure drop and wellbore heat transmission with Rameys model' ) self.rameyoptionprod = self.ParameterDict[self.rameyoptionprod.Name] = boolParameter( "Ramey Production Wellbore Model", @@ -1097,6 +1099,21 @@ def __init__(self, model: Model): self.MyPath = __file__ # Results - used by other objects or printed in output downstream + + self.injection_well_casing_inner_diameter = self.OutputParameterDict[self.injection_well_casing_inner_diameter.Name] = OutputParameter( + Name='Injection well casing ID', + UnitType=self.injwelldiam.UnitType, + PreferredUnits=self.injwelldiam.PreferredUnits, + CurrentUnits=self.injwelldiam.CurrentUnits, + ToolTipText=self.injwelldiam.ToolTipText, + ) + self.production_well_casing_inner_diameter = self.OutputParameterDict[self.production_well_casing_inner_diameter.Name] = OutputParameter( + Name='Production well casing ID', + UnitType=self.prodwelldiam.UnitType, + PreferredUnits=self.prodwelldiam.PreferredUnits, + CurrentUnits=self.prodwelldiam.CurrentUnits, + ToolTipText=self.prodwelldiam.ToolTipText, + ) self.production_reservoir_pressure = self.OutputParameterDict[self.production_reservoir_pressure.Name] = OutputParameter( Name="Calculated Reservoir Pressure", value=self.Phydrostatic.value, @@ -1537,4 +1554,19 @@ def Calculate(self, model: Model) -> None: # negative pumping power values become zero (b/c we are not generating electricity) self.PumpingPower.value = [0. if x < 0. else x for x in self.PumpingPower.value] + self._sync_output_params_from_input_params() + model.logger.info(f'complete {self.__class__.__name__}: {__name__}') + + def _sync_output_params_from_input_params(self) -> None: + """ + Handles setting output parameters whose values are based on 1:1 corresponding input parameters. + """ + + def _set_output_param_from_input_param(input_param: Parameter, output_param: OutputParameter) -> None: + output_param.value = input_param.quantity().to(output_param.CurrentUnits).magnitude + + # Injection/production well casing ID have same value as inputs but exist as separate output parameters due to + # having a different display name. + _set_output_param_from_input_param(self.injwelldiam, self.injection_well_casing_inner_diameter) + _set_output_param_from_input_param(self.prodwelldiam, self.production_well_casing_inner_diameter) diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index eaf2a48e..d10ed0f6 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.47' +__version__ = '3.9.48' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index c44e3b2f..e2ad9029 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -79,6 +79,8 @@ class GeophiresXResult: 'Nominal Discount Rate', 'WACC', 'Accrued financing during construction', + # Displayed for economic models that don't treat inflation costs as capital costs (non-SAM-EM) + 'Inflation costs during construction', 'Project lifetime', 'Capacity factor', 'Project NPV', @@ -261,6 +263,7 @@ class GeophiresXResult: 'Total surface equipment costs', 'Exploration costs', 'Investment Tax Credit', + # Displayed for economic models that treat inflation costs as capital costs (SAM-EM) 'Inflation costs during construction', 'Total Add-on CAPEX', 'Total capital costs', diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 494a2978..292d7dbd 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1510,7 +1510,7 @@ "category": "Economics", "default": -1.0, "minimum": 0, - "maximum": 1000 + "maximum": 10000 }, "Surface Plant Capital Cost Adjustment Factor": { "description": "Multiplier for built-in surface plant capital cost correlation", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 9b2251cc..fa345748 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -20,7 +20,7 @@ }, "Total CAPEX": { "type": "number", - "description": "The total capital expenditure (CAPEX) required to construct the plant. This value includes all direct and indirect costs, contingency, and any cost escalation from inflation during construction. It is used as the total installed cost input for SAM Economic Models.", + "description": "The total capital expenditure (CAPEX) required to construct the plant. This value includes all direct and indirect costs, and contingency. For SAM Economic models, it also includes any cost escalation from inflation during construction. It is used as the total installed cost input for SAM Economic Models.", "units": "MUSD" }, "Average Direct-Use Heat Production": {}, @@ -100,6 +100,11 @@ "description": "The accrued inflation on total capital costs over the construction period, as defined by Inflation Rate During Construction. For SAM Economic Models, this is calculated automatically by compounding Inflation Rate over Construction Years if Inflation Rate During Construction is not provided.", "units": "%" }, + "Inflation costs during construction": { + "type": "number", + "description": "The calculated amount of cost escalation due to inflation over the construction period.", + "units": "MUSD" + }, "Project lifetime": {}, "Capacity factor": {}, "Project NPV": { @@ -212,8 +217,16 @@ }, "Average production well temperature drop": {}, "Flowrate per production well": {}, - "Injection well casing ID": {}, - "Production well casing ID": {}, + "Injection well casing ID": { + "type": "number", + "description": "Inner diameter of injection wellbore (assumed constant along the wellbore) to calculate frictional pressure drop and wellbore heat transmission with Rameys model", + "units": "in" + }, + "Production well casing ID": { + "type": "number", + "description": "Inner diameter of production wellbore (assumed constant along the wellbore) to calculate frictional pressure drop and wellbore heat transmission with Rameys model", + "units": "in" + }, "Number of times redrilling": { "type": "number", "description": "redrill", @@ -424,7 +437,7 @@ "Annualized capital costs": {}, "Total CAPEX": { "type": "number", - "description": "The total capital expenditure (CAPEX) required to construct the plant. This value includes all direct and indirect costs, contingency, and any cost escalation from inflation during construction. It is used as the total installed cost input for SAM Economic Models.", + "description": "The total capital expenditure (CAPEX) required to construct the plant. This value includes all direct and indirect costs, and contingency. For SAM Economic models, it also includes any cost escalation from inflation during construction. It is used as the total installed cost input for SAM Economic Models.", "units": "MUSD" }, "Drilling Cost": {}, diff --git a/tests/examples/Fervo_Norbeck_Latimer_2023.out b/tests/examples/Fervo_Norbeck_Latimer_2023.out index f133e97a..f8d443af 100644 --- a/tests/examples/Fervo_Norbeck_Latimer_2023.out +++ b/tests/examples/Fervo_Norbeck_Latimer_2023.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 0.481 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.475 sec ***SUMMARY OF RESULTS*** @@ -24,7 +24,8 @@ Simulation Metadata ***ECONOMIC PARAMETERS*** Economic Model = BICYCLE - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 1.41 MUSD Project lifetime: 10 yr Capacity factor: 90.0 % Project NPV: -13.03 MUSD diff --git a/tests/examples/Fervo_Project_Cape-2.out b/tests/examples/Fervo_Project_Cape-2.out index f4debd0d..b58acb30 100644 --- a/tests/examples/Fervo_Project_Cape-2.out +++ b/tests/examples/Fervo_Project_Cape-2.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 0.722 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.709 sec ***SUMMARY OF RESULTS*** @@ -24,7 +24,8 @@ Simulation Metadata ***ECONOMIC PARAMETERS*** Economic Model = BICYCLE - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 2.54 MUSD Project lifetime: 15 yr Capacity factor: 90.0 % Project NPV: 42.34 MUSD diff --git a/tests/examples/Fervo_Project_Cape-3.out b/tests/examples/Fervo_Project_Cape-3.out index 93f1c996..1d91ce0e 100644 --- a/tests/examples/Fervo_Project_Cape-3.out +++ b/tests/examples/Fervo_Project_Cape-3.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 0.956 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.951 sec ***SUMMARY OF RESULTS*** @@ -24,7 +24,8 @@ Simulation Metadata ***ECONOMIC PARAMETERS*** Economic Model = BICYCLE - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 53.65 MUSD Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 4580.36 MUSD diff --git a/tests/examples/Fervo_Project_Cape.out b/tests/examples/Fervo_Project_Cape.out index af4fa2e9..ec3f23f1 100644 --- a/tests/examples/Fervo_Project_Cape.out +++ b/tests/examples/Fervo_Project_Cape.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.28 - Simulation Date: 2025-07-02 - Simulation Time: 12:19 - Calculation Time: 0.720 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.712 sec ***SUMMARY OF RESULTS*** @@ -24,7 +24,8 @@ Simulation Metadata ***ECONOMIC PARAMETERS*** Economic Model = BICYCLE - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 24.14 MUSD Project lifetime: 15 yr Capacity factor: 90.0 % Project NPV: 520.01 MUSD diff --git a/tests/examples/example10_HP.out b/tests/examples/example10_HP.out index 51991cce..95394cca 100644 --- a/tests/examples/example10_HP.out +++ b/tests/examples/example10_HP.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.7 - Simulation Date: 2025-05-15 - Simulation Time: 10:13 - Calculation Time: 0.096 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.105 sec ***SUMMARY OF RESULTS*** @@ -26,7 +26,8 @@ Simulation Metadata Economic Model = Standard Levelized Cost Interest Rate: 5.00 % - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 1.39 MUSD Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 15.18 MUSD @@ -64,7 +65,7 @@ Simulation Metadata m/A Drawdown Parameter: 0.00002 1/year Bottom-hole temperature: 109.50 degC Reservoir volume calculated with fracture separation and number of fractures as input - Number of fractures: 12.00 + Number of fractures: 12 Fracture separation: 80.00 meter Reservoir volume: 176000000 m**3 Reservoir hydrostatic pressure: 20488.96 kPa diff --git a/tests/examples/example11_AC.out b/tests/examples/example11_AC.out index 5cff8880..cb655659 100644 --- a/tests/examples/example11_AC.out +++ b/tests/examples/example11_AC.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.7 - Simulation Date: 2025-05-15 - Simulation Time: 10:13 - Calculation Time: 0.096 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.107 sec ***SUMMARY OF RESULTS*** @@ -27,7 +27,8 @@ Simulation Metadata Economic Model = Standard Levelized Cost Interest Rate: 5.00 % - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 1.32 MUSD Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 10.11 MUSD @@ -65,7 +66,7 @@ Simulation Metadata m/A Drawdown Parameter: 0.00002 1/year Bottom-hole temperature: 109.50 degC Reservoir volume calculated with fracture separation and number of fractures as input - Number of fractures: 12.00 + Number of fractures: 12 Fracture separation: 80.00 meter Reservoir volume: 176000000 m**3 Reservoir hydrostatic pressure: 20488.96 kPa diff --git a/tests/examples/example13.out b/tests/examples/example13.out index 006909f4..7b881308 100644 --- a/tests/examples/example13.out +++ b/tests/examples/example13.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.36 - Simulation Date: 2025-07-25 - Simulation Time: 11:30 - Calculation Time: 0.037 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:44 + Calculation Time: 0.036 sec ***SUMMARY OF RESULTS*** @@ -27,7 +27,7 @@ Simulation Metadata Economic Model = Standard Levelized Cost Interest Rate: 5.00 % - Accrued financing during construction: 0.00 % + Accrued financing during construction: 0.00 % Project lifetime: 30 yr Capacity factor: 80.0 % Project NPV: -31.42 MUSD diff --git a/tests/examples/example3.out b/tests/examples/example3.out index 01516178..9d4c402a 100644 --- a/tests/examples/example3.out +++ b/tests/examples/example3.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.7 - Simulation Date: 2025-05-15 - Simulation Time: 10:12 - Calculation Time: 0.113 sec + GEOPHIRES Version: 3.9.47 + Simulation Date: 2025-07-31 + Simulation Time: 08:57 + Calculation Time: 0.124 sec ***SUMMARY OF RESULTS*** @@ -26,7 +26,8 @@ Simulation Metadata ***ECONOMIC PARAMETERS*** Economic Model = BICYCLE - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % + Inflation costs during construction: 5.18 MUSD Project lifetime: 35 yr Capacity factor: 90.0 % Project NPV: -2.38 MUSD @@ -66,7 +67,7 @@ Simulation Metadata m/A Drawdown Parameter: 0.00002 1/year Bottom-hole temperature: 232.00 degC Reservoir volume calculated with fracture separation and number of fractures as input - Number of fractures: 12.00 + Number of fractures: 12 Fracture separation: 80.00 meter Reservoir volume: 176000000 m**3 Reservoir hydrostatic pressure: 29019.48 kPa diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index d64e67c4..0ac9abf1 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import math import os import tempfile import uuid from pathlib import Path -from typing import Optional +from typing import Any from geophires_x.OptionList import PlantType from geophires_x.OptionList import WellDrillingCostCorrelation @@ -269,7 +271,7 @@ def _sanitize_nan(self, r: GeophiresXResult) -> None: except TypeError: pass - def _get_unequal_dicts_approximate_percent_difference(self, d1: dict, d2: dict) -> Optional[float]: + def _get_unequal_dicts_approximate_percent_difference(self, d1: dict, d2: dict) -> float | None: for i in range(99): try: self.assertDictAlmostEqual(d1, d2, percent=i) @@ -965,31 +967,39 @@ def test_sbt_coaxial_raises_error(self): self.assertIn('SBT with coaxial configuration is not implemented', str(e.exception)) def test_production_well_stimulation_cost(self): - def _get_result(prod_well_stim_MUSD: Optional[int] = None) -> GeophiresXResult: + def _get_result( + prod_well_stim_MUSD: int | None = None, + inj_well_stim_MUSD: int | None = None, + additional_params: dict[str, Any] | None = None, + ) -> GeophiresXResult: + if additional_params is None: + additional_params = {} + p = {} if prod_well_stim_MUSD is not None: p['Reservoir Stimulation Capital Cost per Production Well'] = prod_well_stim_MUSD + if inj_well_stim_MUSD is not None: + p['Reservoir Stimulation Capital Cost per Injection Well'] = inj_well_stim_MUSD - return GeophiresXClient().get_geophires_result( - ImmutableGeophiresInputParameters( - from_file_path=self._get_test_file_path('geophires_x_tests/generic-egs-case.txt'), - params=p, - ) + input_params: ImmutableGeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('geophires_x_tests/generic-egs-case.txt'), + params={**p, **additional_params}, ) + return GeophiresXClient().get_geophires_result(input_params) result_no_prod_stim: GeophiresXResult = _get_result() result_prod_stim: GeophiresXResult = _get_result(1.25) - # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor - indirect_and_contingency = 1.05 * 1.15 + default_contingency_factor = 1.15 + indirect_and_contingency = 1.05 * default_contingency_factor # default indirect cost factor and contingency self.assertAlmostEqual( ( 2 * ( result_no_prod_stim.result['CAPITAL COSTS (M$)']['Stimulation costs']['value'] - / (indirect_and_contingency) + / indirect_and_contingency ) ) * indirect_and_contingency, @@ -997,11 +1007,33 @@ def _get_result(prod_well_stim_MUSD: Optional[int] = None) -> GeophiresXResult: places=1, ) + doublets = 59 + # fmt:off + result_4M_per_well: GeophiresXResult = _get_result( + 4, + 4, + { + 'Reservoir Stimulation Indirect Capital Cost Percentage': 0, + 'Number of Production Wells': doublets, + 'Number of Injection Wells': doublets, + + # offset contingency + 'Reservoir Stimulation Capital Cost Adjustment Factor': 1/default_contingency_factor, + } + ) + # fmt:on + + self.assertAlmostEqual( + (4 * doublets * 2), + result_4M_per_well.result['CAPITAL COSTS (M$)']['Stimulation costs']['value'], + places=1, + ) + def test_indirect_costs(self): def _get_result( - indirect_cost_percent: Optional[int] = None, - stimulation_indirect_cost_percent: Optional[int] = None, - wellfield_indirect_cost_percent: Optional[int] = None, + indirect_cost_percent: int | None = None, + stimulation_indirect_cost_percent: int | None = None, + wellfield_indirect_cost_percent: int | None = None, input_file_path: str = 'geophires_x_tests/generic-egs-case.txt', ) -> float: p = {} @@ -1099,7 +1131,7 @@ def wellfield_cost(result_cap_costs): def test_contingency(self): def _get_result( - contingency_percentage: Optional[int] = None, + contingency_percentage: int | None = None, input_file_path: str = 'geophires_x_tests/generic-egs-case.txt', ) -> float: p = {}