diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a0f79492..0f0ace4e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.9.40 +current_version = 3.9.42 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 00ae6670..2a854e86 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.40 + version: 3.9.42 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/.gitignore b/.gitignore index 657ac0d3..e1106b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ Geothermal_district_heating_system_with_peaking_boilers.png *.html !docs/*.html +# TODO may want to add this to source +regenerate-schemas.sh + # C extensions *.so diff --git a/README.rst b/README.rst index 2eb8ba44..992a2ac4 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.40.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.42.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.40...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.42...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X diff --git a/docs/Fervo_Project_Cape-4.md b/docs/Fervo_Project_Cape-4.md index e4df0606..e5881ab6 100644 --- a/docs/Fervo_Project_Cape-4.md +++ b/docs/Fervo_Project_Cape-4.md @@ -41,26 +41,27 @@ in source code for the full set of inputs. ### Economic Parameters -| Parameter | Input Value(s) | Source | -|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Economic Model | SAM Single Owner PPA | The SAM Single Owner PPA economic model is used to calculate financial results including LCOE, NPV, IRR, and pro-forma cash flow analysis. See [GEOPHIRES documentation of SAM Economic Models](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html) for details on how System Advisor Model financial models are integrated into GEOPHIRES. | -| Inflation Rate | 2.3% | US inflation rate as of April 2025 | -| PPA Price | Starting at 9.5 cents/kWh, escalating to 10 cents/kWh by project year 11 | Upper end of ranges given in 2024 NREL ATB (NREL, 2024). Both PPAs 'firm for 10 years at less than $100/MWh' estimate given in a podcast. | -| Well Drilling Cost Correlation & Adjustment Factor | Vertical large baseline correlation + adjustment factor = 0.8 to align with Fervo claimed drilling costs of <$4M/well | Akindipe & Witter, 2025; Latimer, 2025. | -| Reservoir Stimulation Capital Cost Adjustment Factor | 2.66 | Estimated cost of ~$2M per well. Typical range for Nth-of-kind projects may be $0.5–2M. | -| Capital Cost for Power Plant for Electricity Generation | $1900/kW | US DOE, 2021. | -| Discount Rate | 12% | Typical discount rates for high-risk projects may be 12–15% | -| Inflated Bond Interest Rate | 5.6% | Typical debt annual interest rate | -| Fraction of Investment in Bonds (percent debt vs. equity) | 60% | Approximate remaining percentage of CAPEX with $1 billion sponsor equity per Matson, 2024. Note that this source says that Fervo ultimately wants to target "15% sponsor equity, 15% bridge loan, and 70% construction to term loans", but this case study does not attempt to model that capital structure. | -| Exploration Capital Cost | $30M | Estimate significantly higher exploration costs than default correlation in consideration of potential risks associated with second/third/fourth-of-a-kind EGS projects | -| Investment Tax Credit Rate (ITC) | 30% | Same as 400 MWe case study (Fervo_Project_Cape-3) | -| Inflation Rate During Construction (additional indirect capital cost) | 15% | Estimate high indirect capital costs in consideration of potential risks associated with unforeseen engineering challenges or construction delays | +| Parameter | Input Value(s) | Source | +|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Economic Model | SAM Single Owner PPA | The SAM Single Owner PPA economic model is used to calculate financial results including LCOE, NPV, IRR, and pro-forma cash flow analysis. See [GEOPHIRES documentation of SAM Economic Models](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html) for details on how System Advisor Model financial models are integrated into GEOPHIRES. | +| Inflation Rate | 2.3% | US inflation rate as of April 2025 | +| PPA Price | Starting at 9.5 cents/kWh, escalating to 10 cents/kWh by project year 11 | Upper end of ranges given in 2024 NREL ATB (NREL, 2024). Both PPAs 'firm for 10 years at less than $100/MWh' estimate given in a podcast. | +| Well Drilling Cost Correlation & Adjustment Factor | Vertical large baseline correlation + adjustment factor = 0.8 to align with Fervo claimed drilling costs of <$4M/well | Akindipe & Witter, 2025; Latimer, 2025. | +| Reservoir Stimulation Capital Cost Adjustment Factor | 2.66 | Estimated cost of ~$2M per well. Typical range for first-of-kind projects may be higher. | +| Capital Cost for Power Plant for Electricity Generation | $1900/kW | US DOE, 2021. | +| Discount Rate | 12% | Typical discount rates for high-risk projects may be 12–15% | +| Inflated Bond Interest Rate | 5.6% | Typical debt annual interest rate | +| Fraction of Investment in Bonds (percent debt vs. equity) | 60% | Approximate remaining percentage of CAPEX with $1 billion sponsor equity per Matson, 2024. Note that this source says that Fervo ultimately wants to target "15% sponsor equity, 15% bridge loan, and 70% construction to term loans", but this case study does not attempt to model that capital structure. | +| Exploration Capital Cost | $30M | Estimate significantly higher exploration costs than default correlation in consideration of potential risks associated with second/third/fourth-of-a-kind EGS projects | +| Investment Tax Credit Rate (ITC) | 30% | Same as 400 MWe case study (Fervo_Project_Cape-3) | +| Inflation Rate During Construction | 15% | Conservatively models the equivalent of a higher annual inflation rate (4.769%) over a 3-year period as a hedge against construction delays and short-term inflation volatility. | ### Technical & Engineering Parameters | Parameter | Input Value(s) | Source | |-------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Plant Lifetime | 30 years | 30-year well life per Fervo Energy, 2025 (Geothermal Mythbusting: Water Use and Impacts). | +| Construction time | 1 year | Calibrated to a 2–6 year construction time for a 1 GW plant (Yusifov & Enriquez, 2025). Note that the Inflation Rate During Construction parameter hedges against potential construction delays. | | Well diameter | 9⅝ inches | Next standard size up from 7", implied by announcement of "increasing casing diameter" | | Flow Rate per Production Well | 107 kg/s | Fercho et al, 2025 models reservoir performance using 100 kg/s per well. The announced increased casing diameter implies higher flow rates, so the case study uses the maximum flow rate achieved at Cape Station of 107 kg/s per Fervo Energy, 2024. | | Number of Doublets | 59 | Estimate based on extrapolation from previous case studies including [Project Red](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Norbeck_Latimer_2023) and [Fervo_Project_Cape-3](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-3) | @@ -161,3 +162,6 @@ System. https://doi.org/10.31223/X52X0B US DOE. (2021). Combined Heat and Power Technology Fact Sheet Series: Waste Heat to Power. https://betterbuildingssolutioncenter.energy.gov/sites/default/files/attachments/Waste_Heat_to_Power_Fact_Sheet.pdf + +Yusifov, M., & Enriquez, N. (2025, July). From Core to Code: Powering the Al Revolution with Geothermal Energy. +Project InnerSpace. https://projectinnerspace.org/resources/Powering-the-AI-Revolution.pdf diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index c71c1d6a..cac5b78e 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -20,7 +20,7 @@ The following table describes how GEOPHIRES parameters are transformed into SAM | `Maximum Total Electricity Generation` | Generation Profile | `Nameplate capacity` | `Singleowner` | `system_capacity` | .. N/A | | `Utilization Factor` | Generation Profile | `Nominal capacity factor` | `Singleowner` | `user_capacity_factor` | .. N/A | | `Net Electricity Generation` | AC Degradation | `Annual AC degradation rate` schedule | `Utilityrate5` | `degradation` | Percentage difference of each year's `Net Electricity Generation` from `Maximum Total Electricity Generation` is input as SAM as the degradation rate schedule in order to match SAM's generation profile to GEOPHIRES | -| `Total CAPEX` × (1 + `Inflation Rate During Construction`) | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | Inflation during construction is treated as an indirect EPC capital cost percentage. 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). | +| {`Total CAPEX` before inflation} × (1 + `Accrued financing during construction (%)`/100); | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Accrued financing during construction (%)` = (1+`Inflation Rate During Construction`) × 100 if `Inflation Rate During Construction` is provided or ((1+`Inflation Rate`) ^ `Construction Years`) × 100 if not. | | `Total O&M Cost`, `Inflation Rate` | Operating Costs | `Fixed operating cost`, `Escalation rate` set to `Inflation Rate` × -1 | `Singleowner` | `om_fixed`, `om_fixed_escal` | .. N/A | | `Plant Lifetime` | Financial Parameters → Analysis Parameters | `Analysis period` | `CustomGeneration`, `Singleowner` | `CustomGeneration.analysis_period`, `Singleowner.term_tenor` | .. N/A | | `Inflation Rate` | Financial Parameters → Analysis Parameters | `Inflation rate` | `Utilityrate5` | `inflation_rate` | .. N/A | @@ -30,7 +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. | -| `Investment Tax Credit Rate` | Incentives → Investment Tax Credit (ITC) | `Federal` → `Percentage (%)` | `Singleowner` | `itc_fed_percent` | .. N/A | +| `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 | | `Production Tax Credit Inflation Adjusted`, `Inflation Rate` | Incentives → Production Tax Credit (PTC) | `Federal` → `Escalation (%/yr)` | `Singleowner` | `ptc_fed_escal` | If `Production Tax Credit Inflation Adjusted` = True, GEOPHIRES set's SAM's PTC escalation rate to the inflation rate. SAM applies the escalation rate to years 2 and later of the project cash flow. Note that this produces escalation rates that are similar to inflation-adjusted equivalents, but not exactly equal. | @@ -135,7 +135,6 @@ 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) - ## Examples ### SAM Single Owner PPA: 50 MWe diff --git a/docs/conf.py b/docs/conf.py index b2fdba14..1480d28b 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.40' +version = release = '3.9.42' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 1e04355c..4b30a95c 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.9.40', + version='3.9.42', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index afa2acfe..c7c6f39e 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -9,7 +9,8 @@ from geophires_x.EconomicsSam import calculate_sam_economics, SamEconomicsCalculations from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter, \ real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \ - project_payback_period_parameter + project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \ + total_capex_parameter_output_parameter from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation @@ -1043,7 +1044,11 @@ def __init__(self, model: Model): PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.TENTH, ErrMessage="assume default inflation rate during construction (0)", - ToolTipText='For SAM Economic Models, this value is treated as an indirect EPC capital cost percentage.' + ToolTipText='The total inflation rate applied to capital costs over the entire construction period, ' + 'entered as a fraction (e.g., 0.15 for 15%). ' + 'This value defines the Accrued financing during construction output. ' + 'Note: For SAM Economic Models, if this parameter is not provided, inflation costs will be ' + 'calculated automatically by compounding Inflation Rate over Construction Years.' ) self.contingency_percentage = self.ParameterDict[self.contingency_percentage.Name] = floatParameter( @@ -1709,7 +1714,7 @@ def __init__(self, model: Model): ) stimulation_contingency_and_indirect_costs_tooltip = ( - f'plus 15% contingency ' # TODO https://github.com/NREL/GEOPHIRES-X/issues/383 + f'plus {self.contingency_percentage.quantity().to(convertible_unit("%")).magnitude:g}% contingency ' f'plus {self.stimulation_indirect_capital_cost_percentage.quantity().to(convertible_unit("%")).magnitude}% ' f'indirect costs' ) @@ -1731,8 +1736,9 @@ def __init__(self, model: Model): ) contingency_and_indirect_costs_tooltip = ( - f'plus 15% contingency ' # TODO https://github.com/NREL/GEOPHIRES-X/issues/383 - f'plus {self.indirect_capital_cost_percentage.quantity().to(convertible_unit("%")).magnitude}% indirect costs' + f'plus {self.contingency_percentage.quantity().to(convertible_unit("%")).magnitude:g}% contingency ' + f'plus {self.indirect_capital_cost_percentage.quantity().to(convertible_unit("%")).magnitude}% ' + f'indirect costs' ) self.Cexpl = self.OutputParameterDict[self.Cexpl.Name] = OutputParameter( @@ -1754,10 +1760,9 @@ def __init__(self, model: Model): UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, - - # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor - ToolTipText="Includes total drilling and completion cost of all injection and production wells and " - "laterals, plus 5% indirect costs." + ToolTipText=f'Includes total drilling and completion cost of all injection and production wells and ' + f'laterals, plus indirect costs ' + f'(default: {self.wellfield_indirect_capital_cost_percentage.DefaultValue}%).' ) self.drilling_and_completion_costs_per_well = self.OutputParameterDict[ self.drilling_and_completion_costs_per_well.Name] = OutputParameter( @@ -1765,10 +1770,8 @@ def __init__(self, model: Model): UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, - - # TODO https://github.com/NREL/GEOPHIRES-X/issues/383?title=Parameterize+indirect+cost+factor - ToolTipText='Includes total drilling and completion cost per well, ' - 'including injection and production wells and laterals, plus 5% indirect costs.' + ToolTipText='Drilling and completion cost per well, including indirect costs ' + f'(default: {self.wellfield_indirect_capital_cost_percentage.DefaultValue}%).' ) self.Coamwell = self.OutputParameterDict[self.Coamwell.Name] = OutputParameter( Name="O&M Wellfield cost", @@ -1829,8 +1832,10 @@ def __init__(self, model: Model): display_name='Total capital costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, - CurrentUnits=CurrencyUnit.MDOLLARS + CurrentUnits=CurrencyUnit.MDOLLARS, ) + self.capex_total = self.OutputParameterDict[self.capex_total.Name] = total_capex_parameter_output_parameter() + # noinspection SpellCheckingInspection self.Coam = self.OutputParameterDict[self.Coam.Name] = OutputParameter( Name="Total O&M Cost", @@ -1965,6 +1970,21 @@ def __init__(self, model: Model): PreferredUnits=PercentUnit.PERCENT, CurrentUnits=PercentUnit.PERCENT ) + self.accrued_financing_during_construction_percentage = self.OutputParameterDict[ + self.accrued_financing_during_construction_percentage.Name] = OutputParameter( + Name='Accrued financing during construction', + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.PERCENT, + CurrentUnits=PercentUnit.PERCENT, + ToolTipText='The accrued inflation on total capital costs over the construction period, ' + f'as defined by {self.inflrateconstruction.Name}. ' + 'For SAM Economic Models, this is calculated automatically by compounding ' + f'{self.RINFL.Name} over Construction Years ' + f'if {self.inflrateconstruction.Name} is not provided.' + ) + + self.inflation_cost_during_construction = self.OutputParameterDict[ + self.inflation_cost_during_construction.Name] = inflation_cost_during_construction_output_parameter() self.after_tax_irr = self.OutputParameterDict[self.after_tax_irr.Name] = ( after_tax_irr_parameter()) @@ -2327,6 +2347,13 @@ def _warn(_msg: str) -> None: self.sync_interest_rate(model) self.sync_well_drilling_and_completion_capital_cost_adjustment_factor(model) + # SAM Economic Models recalculate accrued financing value based on construction years and inflation rate if + # inflation rate during construction is not provided. + # TODO to determine whether the same logic should be applied for other economic models. + self.accrued_financing_during_construction_percentage.value = self.inflrateconstruction.quantity().to( + convertible_unit(self.accrued_financing_during_construction_percentage.CurrentUnits) + ).magnitude + model.logger.info(f'complete {__class__!s}: {sys._getframe().f_code.co_name}') def sync_interest_rate(self, model): @@ -2388,60 +2415,7 @@ def Calculate(self, model: Model) -> None: self.Cstim.value = self.calculate_stimulation_costs(model).to(self.Cstim.CurrentUnits).magnitude self.calculate_field_gathering_costs(model) self.calculate_plant_costs(model) - - if not self.totalcapcost.Valid: - # exploration costs (same as in Geophires v1.2) (M$) - if self.ccexplfixed.Valid: - self.Cexpl.value = self.ccexplfixed.value - else: - self.Cexpl.value = self._contingency_factor * self.ccexpladjfactor.value * self._indirect_cost_factor * ( - 1. + self.cost_one_production_well.value * 0.6) - - # Surface Piping Length Costs (M$) #assumed $750k/km - self.Cpiping.value = 750 / 1000 * model.surfaceplant.piping_length.value - - # district heating network costs - if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: # district heat - if self.dhtotaldistrictnetworkcost.Provided: - self.dhdistrictcost.value = self.dhtotaldistrictnetworkcost.value - elif self.dhpipinglength.Provided: - self.dhdistrictcost.value = self.dhpipinglength.value * self.dhpipingcostrate.value / 1000 # M$ - elif self.dhroadlength.Provided: # check if road length is provided to calculate cost - self.dhdistrictcost.value = self.dhroadlength.value * 0.75 * self.dhpipingcostrate.value / 1000 # M$ (assuming 75% of road length is used for district network piping) - else: # calculate district network cost based on population density - if self.dhlandarea.Provided == False: - model.logger.warning("District heating network cost calculated based on default district area") - if self.dhpopulation.Provided: - self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value - elif model.surfaceplant.dh_number_of_housing_units.Provided: - self.populationdensity.value = model.surfaceplant.dh_number_of_housing_units.value * 2.6 / self.dhlandarea.value # estimate population based on 2.6 number of people per household - else: - model.logger.warning( - "District heating network cost calculated based on default number of people in district") - self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value - - if self.populationdensity.value > 1000: - self.dhpipinglength.value = 7.5 * self.dhlandarea.value # using constant 7.5km of pipe per km^2 when population density is >1500 - else: - self.dhpipinglength.value = max( - self.populationdensity.value / 1000 * 7.5 * self.dhlandarea.value, - self.dhlandarea.value) # scale the piping length based on population density, but with a minimum of 1 km of piping per km^2 of area - self.dhdistrictcost.value = self.dhpipingcostrate.value * self.dhpipinglength.value / 1000 - - else: - self.dhdistrictcost.value = 0 - - self.CCap.value = self.Cexpl.value + self.Cwell.value + self.Cstim.value + self.Cgath.value + self.Cplant.value + self.Cpiping.value + self.dhdistrictcost.value - else: - self.CCap.value = self.totalcapcost.value - - # update the capital costs, assuming the entire ITC is used to reduce the capital costs - if self.RITC.Provided: - self.RITCValue.value = self.RITC.value * self.CCap.value - self.CCap.value = self.CCap.value - self.RITCValue.value - - # Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant - self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value + self.calculate_total_capital_costs(model) # O&M costs # calculate first O&M costs independent of whether oamtotalfixed is provided or not @@ -2612,10 +2586,12 @@ def Calculate(self, model: Model) -> None: if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: self.sam_economics_calculations = calculate_sam_economics(model) - # Distinguish capex from default display name of 'Total capital costs' since SAM Economic Model doesn't - # subtract ITC from this value. - self.CCap.display_name = 'Total CAPEX' - self.CCap.value = self.sam_economics_calculations.capex.quantity().to(self.CCap.CurrentUnits.value).magnitude + # Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs', + # since SAM Economic Model doesn't subtract ITC from this value. + self.capex_total.value = (self.sam_economics_calculations.capex.quantity() + .to(self.capex_total.CurrentUnits.value).magnitude) + self.CCap.value = (self.sam_economics_calculations.capex.quantity() + .to(self.CCap.CurrentUnits.value).magnitude) self.wacc.value = self.sam_economics_calculations.wacc.value self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value @@ -3063,7 +3039,61 @@ def calculate_plant_costs(self, model: Model) -> None: if not self.CAPEX_heat_electricity_plant_ratio.Provided: self.CAPEX_heat_electricity_plant_ratio.value = self.CAPEX_cost_electricity_plant/self.Cplant.value + def calculate_total_capital_costs(self, model): + if not self.totalcapcost.Valid: + # exploration costs (same as in Geophires v1.2) (M$) + if self.ccexplfixed.Valid: + self.Cexpl.value = self.ccexplfixed.value + else: + self.Cexpl.value = self._contingency_factor * self.ccexpladjfactor.value * self._indirect_cost_factor * ( + 1. + self.cost_one_production_well.value * 0.6) + # Surface Piping Length Costs (M$) #assumed $750k/km # TODO parameterize + self.Cpiping.value = 750 / 1000 * model.surfaceplant.piping_length.value + + # district heating network costs + if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: # district heat + if self.dhtotaldistrictnetworkcost.Provided: + self.dhdistrictcost.value = self.dhtotaldistrictnetworkcost.value + elif self.dhpipinglength.Provided: + self.dhdistrictcost.value = self.dhpipinglength.value * self.dhpipingcostrate.value / 1000 # M$ + elif self.dhroadlength.Provided: # check if road length is provided to calculate cost + self.dhdistrictcost.value = self.dhroadlength.value * 0.75 * self.dhpipingcostrate.value / 1000 # M$ (assuming 75% of road length is used for district network piping) + else: # calculate district network cost based on population density + if self.dhlandarea.Provided == False: + model.logger.warning("District heating network cost calculated based on default district area") + if self.dhpopulation.Provided: + self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value + elif model.surfaceplant.dh_number_of_housing_units.Provided: + self.populationdensity.value = model.surfaceplant.dh_number_of_housing_units.value * 2.6 / self.dhlandarea.value # estimate population based on 2.6 number of people per household + else: + model.logger.warning( + "District heating network cost calculated based on default number of people in district") + self.populationdensity.value = self.dhpopulation.value / self.dhlandarea.value + + if self.populationdensity.value > 1000: + self.dhpipinglength.value = 7.5 * self.dhlandarea.value # using constant 7.5km of pipe per km^2 when population density is >1500 + else: + self.dhpipinglength.value = max( + self.populationdensity.value / 1000 * 7.5 * self.dhlandarea.value, + self.dhlandarea.value) # scale the piping length based on population density, but with a minimum of 1 km of piping per km^2 of area + self.dhdistrictcost.value = self.dhpipingcostrate.value * self.dhpipinglength.value / 1000 + + else: + self.dhdistrictcost.value = 0 + + self.CCap.value = self.Cexpl.value + self.Cwell.value + self.Cstim.value + self.Cgath.value + self.Cplant.value + self.Cpiping.value + self.dhdistrictcost.value + else: + self.CCap.value = self.totalcapcost.value + + if self.RITC.Provided and self.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + # update the capital costs, assuming the entire ITC is used to reduce the capital costs + # (not applied for SAM Economic Models since they handle ITC in cash flow, not capex) + self.RITCValue.value = self.RITC.value * self.CCap.value + self.CCap.value = self.CCap.value - self.RITCValue.value + + # Add in the FlatLicenseEtc, OtherIncentives, & TotalGrant + self.CCap.value = self.CCap.value + self.FlatLicenseEtc.value - self.OtherIncentives.value - self.TotalGrant.value def calculate_cashflow(self, model: Model) -> None: """ @@ -3188,3 +3218,5 @@ def _calculate_derived_outputs(self, model: Model) -> None: def __str__(self): return "Economics" + + diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index eedff5a8..78ef3a89 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -37,8 +37,10 @@ moic_parameter, project_vir_parameter, project_payback_period_parameter, + inflation_cost_during_construction_output_parameter, + total_capex_parameter_output_parameter, ) -from geophires_x.GeoPHIRESUtils import is_float, is_int, sig_figs +from geophires_x.GeoPHIRESUtils import is_float, is_int, sig_figs, quantity from geophires_x.OptionList import EconomicModel, EndUseOptions from geophires_x.Parameter import Parameter, OutputParameter, floatParameter from geophires_x.Units import convertible_unit, EnergyCostUnit, CurrencyUnit, Units @@ -55,12 +57,7 @@ class SamEconomicsCalculations: ) ) - capex: OutputParameter = field( - default_factory=lambda: OutputParameter( - UnitType=Units.CURRENCY, - CurrentUnits=CurrencyUnit.MDOLLARS, - ) - ) + capex: OutputParameter = field(default_factory=total_capex_parameter_output_parameter) project_npv: OutputParameter = field( default_factory=lambda: OutputParameter( @@ -337,7 +334,6 @@ def _get_utility_rate_parameters(m: Model) -> dict[str, Any]: def _get_single_owner_parameters(model: Model) -> dict[str, Any]: """ TODO: - - Construction years - Break out indirect costs (instead of lumping all into direct cost): https://github.com/NREL/GEOPHIRES-X/issues/383 """ @@ -354,9 +350,26 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: # calling with PySAM. But, we set it here anyway for the sake of technical compliance. ret['flip_target_year'] = model.surfaceplant.plant_lifetime.value - itc = econ.RITCValue.quantity() - total_capex = econ.CCap.quantity() + itc - ret['total_installed_cost'] = (total_capex * (1 + econ.inflrateconstruction.value)).to('USD').magnitude + total_capex = econ.CCap.quantity() + + if econ.inflrateconstruction.Provided: + inflation_during_construction_factor = 1.0 + econ.inflrateconstruction.quantity().to('dimensionless').magnitude + else: + inflation_during_construction_factor = math.pow( + 1.0 + econ.RINFL.value, model.surfaceplant.construction_years.value + ) + econ.accrued_financing_during_construction_percentage.value = ( + quantity(inflation_during_construction_factor - 1, 'dimensionless') + .to(convertible_unit(econ.accrued_financing_during_construction_percentage.CurrentUnits)) + .magnitude + ) + + econ.inflation_cost_during_construction.value = ( + (total_capex * (inflation_during_construction_factor - 1)) + .to(econ.inflation_cost_during_construction.CurrentUnits) + .magnitude + ) + ret['total_installed_cost'] = (total_capex * inflation_during_construction_factor).to('USD').magnitude opex_musd = econ.Coam.value ret['om_fixed'] = [opex_musd * 1e6] diff --git a/src/geophires_x/EconomicsSamCashFlow.py b/src/geophires_x/EconomicsSamCashFlow.py index efe2492c..814f0e9d 100644 --- a/src/geophires_x/EconomicsSamCashFlow.py +++ b/src/geophires_x/EconomicsSamCashFlow.py @@ -22,6 +22,9 @@ def _calculate_sam_economics_cash_flow(model: Model, single_owner: Singleowner) _soo = single_owner.Outputs profile = [] + + # TODO this and/or related logic will need to be adjusted when multiple construction years are supported + # https://github.com/NREL/GEOPHIRES-X/issues/406 total_duration = model.surfaceplant.plant_lifetime.value + model.surfaceplant.construction_years.value # Prefix with 'Year ' partially as workaround for tabulate applying float formatting to ints, possibly related diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index c8b2af47..71dd8c62 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -1,7 +1,7 @@ from __future__ import annotations from geophires_x.Parameter import OutputParameter -from geophires_x.Units import Units, PercentUnit, TimeUnit +from geophires_x.Units import Units, PercentUnit, TimeUnit, CurrencyUnit def BuildPricingModel(plantlifetime: int, StartPrice: float, EndPrice: float, @@ -121,3 +121,26 @@ def wacc_output_parameter() -> OutputParameter: CurrentUnits=PercentUnit.PERCENT, PreferredUnits=PercentUnit.PERCENT, ) + + +def inflation_cost_during_construction_output_parameter() -> OutputParameter: + return OutputParameter( + Name='Inflation costs during construction', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + ToolTipText='The calculated amount of cost escalation due to inflation over the construction period.' + ) + + +def total_capex_parameter_output_parameter() -> OutputParameter: + return OutputParameter( + Name='Total CAPEX', + 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." + ) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index f6bc8819..486bae3a 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -222,7 +222,7 @@ def PrintOutputs(self, model: Model): 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: - f.write(f' {Outputs._field_label(econ.CCap.display_name, 50)}{econ.CCap.value:10.2f} {econ.CCap.CurrentUnits.value}\n') + 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) f.write(f' Number of injection wells: {model.wellbores.ninj.value:10.0f}'+NL) @@ -268,7 +268,9 @@ def PrintOutputs(self, model: Model): label = Outputs._field_label(field.Name, 49) f.write(f' {label}{field.value:10.2f} {field.CurrentUnits.value}\n') - f.write(f' Accrued financing during construction: {econ.inflrateconstruction.value:10.2f} {econ.inflrateconstruction.CurrentUnits.value}\n') + acf: OutputParameter = econ.accrued_financing_during_construction_percentage + acf_label = Outputs._field_label(acf.display_name, 49) + f.write(f' {acf_label}{acf.value:10.2f} {acf.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') @@ -493,8 +495,14 @@ def PrintOutputs(self, model: Model): # expenditure. pass - capex_label = Outputs._field_label(econ.CCap.display_name, 50) - f.write(f' {capex_label}{econ.CCap.value:10.2f} {econ.CCap.CurrentUnits.value}\n') + if model.economics.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA: + # 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_label = Outputs._field_label(capex_param.display_name, 50) + f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n') if model.economics.econmodel.value == EconomicModel.FCR: f.write(f' Annualized capital costs: {(model.economics.CCap.value*(1+model.economics.inflrateconstruction.value)*model.economics.FCR.value):10.2f} ' + model.economics.CCap.CurrentUnits.value + NL) diff --git a/src/geophires_x/SurfacePlant.py b/src/geophires_x/SurfacePlant.py index e29184b9..81bd010f 100644 --- a/src/geophires_x/SurfacePlant.py +++ b/src/geophires_x/SurfacePlant.py @@ -415,7 +415,7 @@ def __init__(self, model: Model): UnitType=Units.NONE, ErrMessage="assume default number of years in construction (1)", ToolTipText='Number of years spent in construction (assumes whole years, no fractions). ' - 'Capital costs are spread evenly over constructions years e.g. if total capital costs are ' + 'Capital costs are spread evenly over construction years e.g. if total capital costs are ' '$500M and there are 2 construction years, ' 'then $250M will be spent in both the first and second construction years.' ) diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index ac839cc1..94ed322a 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.9.40' +__version__ = '3.9.42' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index eb367580..ea65817b 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -260,6 +260,7 @@ class GeophiresXResult: 'Total surface equipment costs', 'Exploration costs', 'Investment Tax Credit', + 'Inflation costs during construction', 'Total capital costs', 'Annualized capital costs', # AGS/CLGS diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 90967ba1..494a2978 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1252,7 +1252,7 @@ "maximum": 1.0 }, "Construction Years": { - "description": "Number of years spent in construction (assumes whole years, no fractions). Capital costs are spread evenly over constructions years e.g. if total capital costs are $500M and there are 2 construction years, then $250M will be spent in both the first and second construction years.", + "description": "Number of years spent in construction (assumes whole years, no fractions). Capital costs are spread evenly over construction years e.g. if total capital costs are $500M and there are 2 construction years, then $250M will be spent in both the first and second construction years.", "type": "integer", "units": null, "category": "Surface Plant", @@ -1711,7 +1711,7 @@ "maximum": 1.0 }, "Inflation Rate During Construction": { - "description": "For SAM Economic Models, this value is treated as an indirect EPC capital cost percentage.", + "description": "The total inflation rate applied to capital costs over the entire construction period, entered as a fraction (e.g., 0.15 for 15%). This value defines the Accrued financing during construction output. Note: For SAM Economic Models, if this parameter is not provided, inflation costs will be calculated automatically by compounding Inflation Rate over Construction Years.", "type": "number", "units": "", "category": "Economics", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index a2fe3a18..c8615020 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -17,7 +17,11 @@ "description": "LCOE. For SAM economic models, this is the nominal LCOE value (as opposed to real).", "units": "cents/kWh" }, - "Total CAPEX": {}, + "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.", + "units": "MUSD" + }, "Average Direct-Use Heat Production": {}, "Direct-Use heat breakeven price": {}, "Direct-Use heat breakeven price (LCOH)": { @@ -90,7 +94,11 @@ "description": "Weighted Average Cost of Capital displayed for SAM Economic Models. It is calculated per https://samrepo.nrelcloud.org/help/fin_commercial.html?q=wacc: WACC = [ Nominal Discount Rate \u00f7 100 \u00d7 (1 - Debt Percent \u00f7 100) + Debt Percent \u00f7 100 \u00d7 Loan Rate \u00f7 100 \u00d7 (1 - Effective Tax Rate \u00f7 100 ) ] \u00d7 100; Effective Tax Rate = [ Federal Tax Rate \u00f7 100 \u00d7 ( 1 - State Tax Rate \u00f7 100 ) + State Tax Rate \u00f7 100 ] \u00d7 100; ", "units": "%" }, - "Accrued financing during construction": {}, + "Accrued financing during construction": { + "type": "number", + "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": "%" + }, "Project lifetime": {}, "Capacity factor": {}, "Project NPV": { @@ -334,12 +342,12 @@ "properties": { "Drilling and completion costs": { "type": "number", - "description": "Wellfield cost. Includes total drilling and completion cost of all injection and production wells and laterals, plus 5% indirect costs.", + "description": "Wellfield cost. Includes total drilling and completion cost of all injection and production wells and laterals, plus indirect costs (default: 5%).", "units": "MUSD" }, "Drilling and completion costs per well": { "type": "number", - "description": "Includes total drilling and completion cost per well, including injection and production wells and laterals, plus 5% indirect costs.", + "description": "Drilling and completion cost per well, including indirect costs (default: 5%).", "units": "MUSD" }, "Drilling and completion costs per production well": {}, @@ -389,13 +397,22 @@ "description": "Investment Tax Credit Value", "units": "MUSD" }, + "Inflation costs during construction": { + "type": "number", + "description": "The calculated amount of cost escalation due to inflation over the construction period.", + "units": "MUSD" + }, "Total capital costs": { "type": "number", "description": "Total Capital Cost", "units": "MUSD" }, "Annualized capital costs": {}, - "Total CAPEX": {}, + "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.", + "units": "MUSD" + }, "Drilling Cost": {}, "Drilling and Completion Costs": {}, "Drilling and Completion Costs per Well": {}, diff --git a/tests/examples/Fervo_Project_Cape-4.out b/tests/examples/Fervo_Project_Cape-4.out index e461faed..c05d23ba 100644 --- a/tests/examples/Fervo_Project_Cape-4.out +++ b/tests/examples/Fervo_Project_Cape-4.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.39 - Simulation Date: 2025-07-25 - Simulation Time: 14:32 - Calculation Time: 1.794 sec + GEOPHIRES Version: 3.9.41 + Simulation Date: 2025-07-27 + Simulation Time: 11:26 + Calculation Time: 1.738 sec ***SUMMARY OF RESULTS*** @@ -28,7 +28,7 @@ Simulation Metadata Real Discount Rate: 12.00 % Nominal Discount Rate: 14.58 % WACC: 8.30 % - Accrued financing during construction: 15.00 % + Accrued financing during construction: 15.00 % Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 641.24 MUSD @@ -104,6 +104,7 @@ Simulation Metadata Field gathering system costs: 56.44 MUSD Total surface equipment costs: 1560.49 MUSD Exploration costs: 30.00 MUSD + Inflation costs during construction: 344.27 MUSD Total CAPEX: 2639.39 MUSD diff --git a/tests/examples/Fervo_Project_Cape-4.txt b/tests/examples/Fervo_Project_Cape-4.txt index 773c7519..32768b61 100644 --- a/tests/examples/Fervo_Project_Cape-4.txt +++ b/tests/examples/Fervo_Project_Cape-4.txt @@ -17,7 +17,9 @@ Fraction of Investment in Bonds, .6, -- Based on fraction of CAPEX with $1 billi Inflated Bond Interest Rate, .056 Inflation Rate, .023, -- US inflation as of April 2025 -Inflation Rate During Construction, 0.15, -- Treated as indirect EPC capital cost percentage +Inflation Rate During Construction, 0.15, -- Models a higher annual inflation rate (4.769%) over the 3-year construction period as a hedge against short-term inflation volatility. +Construction Years, 1, -- Calibrated to a 2-6 year construction time for a 1 GW plant (Yusifov & Enriquez, 2025) + Combined Income Tax Rate, .28 Investment Tax Credit Rate, 0.3 diff --git a/tests/examples/example_SAM-single-owner-PPA-2.out b/tests/examples/example_SAM-single-owner-PPA-2.out index bcd63c41..0dada8ab 100644 --- a/tests/examples/example_SAM-single-owner-PPA-2.out +++ b/tests/examples/example_SAM-single-owner-PPA-2.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.39 - Simulation Date: 2025-07-25 - Simulation Time: 14:32 - Calculation Time: 0.983 sec + GEOPHIRES Version: 3.9.40 + Simulation Date: 2025-07-26 + Simulation Time: 14:13 + Calculation Time: 0.967 sec ***SUMMARY OF RESULTS*** @@ -28,7 +28,7 @@ Simulation Metadata Real Discount Rate: 7.00 % Nominal Discount Rate: 9.14 % WACC: 6.41 % - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 2877.00 MUSD @@ -105,6 +105,7 @@ Simulation Metadata Field gathering system costs: 70.43 MUSD Total surface equipment costs: 969.26 MUSD Exploration costs: 30.00 MUSD + Inflation costs during construction: 76.64 MUSD Total CAPEX: 1609.42 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA.out b/tests/examples/example_SAM-single-owner-PPA.out index e71832f9..7bb23453 100644 --- a/tests/examples/example_SAM-single-owner-PPA.out +++ b/tests/examples/example_SAM-single-owner-PPA.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.9.39 - Simulation Date: 2025-07-25 - Simulation Time: 14:32 - Calculation Time: 1.177 sec + GEOPHIRES Version: 3.9.40 + Simulation Date: 2025-07-26 + Simulation Time: 14:13 + Calculation Time: 1.146 sec ***SUMMARY OF RESULTS*** @@ -28,7 +28,7 @@ Simulation Metadata Real Discount Rate: 8.00 % Nominal Discount Rate: 10.16 % WACC: 7.57 % - Accrued financing during construction: 5.00 % + Accrued financing during construction: 5.00 % Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 146.15 MUSD @@ -106,6 +106,7 @@ Simulation Metadata Field gathering system costs: 5.80 MUSD Total surface equipment costs: 150.23 MUSD Exploration costs: 3.89 MUSD + Inflation costs during construction: 10.62 MUSD Total CAPEX: 222.97 MUSD diff --git a/tests/geophires_x_tests/generic-egs-case-3_no-inflation-rate-during-construction.txt b/tests/geophires_x_tests/generic-egs-case-3_no-inflation-rate-during-construction.txt new file mode 100644 index 00000000..91492bb1 --- /dev/null +++ b/tests/geophires_x_tests/generic-egs-case-3_no-inflation-rate-during-construction.txt @@ -0,0 +1,59 @@ +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, 3 +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 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 48613856..fd0abccd 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -199,12 +199,17 @@ def test_only_electricity_end_use_supported(self): self.assertIn('Invalid End-Use Option (Direct-Use Heat)', str(e.exception)) def test_only_1_construction_year_supported(self): + # TODO remove this test and uncomment test_multiple_construction_years_supported below once multiple + # construction years are supported https://github.com/NREL/GEOPHIRES-X/issues/406 with self.assertRaises(RuntimeError) as e: self._get_result({'Construction Years': 2}) self.assertIn('Invalid Construction Years (2)', str(e.exception)) self.assertIn('SAM_SINGLE_OWNER_PPA only supports Construction Years = 1.', str(e.exception)) + # def test_multiple_construction_years_supported(self): + # self.assertIsNotNone(self._get_result({'Construction Years': 2})) + def test_ppa_pricing_model(self): self.assertListEqual( [ @@ -526,6 +531,47 @@ def _payback_period(_r: GeophiresXResult) -> float: r: GeophiresXResult = self._get_result(never_pays_back_params) self.assertIsNone(_payback_period(r)) + def test_accrued_financing_during_construction(self): + def _accrued_financing(_r: GeophiresXResult) -> float: + return _r.result['ECONOMIC PARAMETERS']['Accrued financing during construction']['value'] + + params1 = { + 'Construction Years': 1, + 'Inflation Rate': 0.04769, + } + r1: GeophiresXResult = self._get_result( + params1, file_path=self._get_test_file_path('generic-egs-case-3_no-inflation-rate-during-construction.txt') + ) + self.assertAlmostEqual(4.769, _accrued_financing(r1), places=1) + + params2 = { + 'Construction Years': 1, + 'Inflation Rate During Construction': 0.15, + } + r2: GeophiresXResult = self._get_result( + params2, file_path=self._get_test_file_path('generic-egs-case-3_no-inflation-rate-during-construction.txt') + ) + self.assertEqual(15.0, _accrued_financing(r2)) + + # TODO enable when multiple construction years are supported https://github.com/NREL/GEOPHIRES-X/issues/406 + # params3 = { + # 'Construction Years': 3, + # 'Inflation Rate': 0.04769, + # } + # r3: GeophiresXResult = self._get_result( + # params3, file_path=self._get_test_file_path('generic-egs-case-3_no-inflation-rate-during-construction.txt') + # ) + # self.assertEqual(15.0, _accrued_financing(r3)) + # + # params4 = { + # 'Construction Years': 3, + # 'Inflation Rate During Construction': 0.15, + # } + # r4: GeophiresXResult = self._get_result( + # params4, file_path=self._get_test_file_path('generic-egs-case-3_no-inflation-rate-during-construction.txt') + # ) + # self.assertEqual(15.0, _accrued_financing(r4)) + @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: diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index 77352278..2bb56169 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -165,14 +165,7 @@ def get_output_file_for_example(example_file: str): example_files = list( filter( lambda example_file_path_: example_file_path_.startswith( - ( - 'example', - 'Beckers_et_al', - 'SUTRA', - 'Wanju', - 'Fervo', - 'S-DAC-GT' - ) + ('example', 'Beckers_et_al', 'SUTRA', 'Wanju', 'Fervo', 'S-DAC-GT') ) # TOUGH not enabled for testing - see https://github.com/NREL/GEOPHIRES-X/issues/318 and not example_file_path_.startswith(('example6.txt', 'example7.txt'))