diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 673162efc..4cacd668c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.13.15 +current_version = 3.14.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 26e648a6a..a0a81af09 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.13.15 + version: 3.14.0 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 18585e4b6..2355e955f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,10 +5,14 @@ Changelog GEOPHIRES v3 (2023-2026) ------------------------ -3.13 +3.14 ^^^^ -3.13.15: `Well integrity parameterization to trigger redrilling; Drawdown Parameter Schedule; Input params CSV unit and comment parsing; State ITC Amount `__ | `release `__ +3.14: `Well integrity parameterization to trigger redrilling; Drawdown Parameter Schedule; Input params CSV unit and comment parsing; State ITC Amount; SAM-EM Other Incentives and One-time Grants fix `__ | `release `__ | **Changed:** SAM Economic Models inputs with Other Incentives and One-time Grants Etc parameters now calculate Overnight Capital Cost and Total CAPEX correctly. See `the tracking issue `__ for details. + + +3.13 +^^^^ 3.13.9: `Add hip-ra-x-result.json schema and HipRaXResult `__ | `release `__ diff --git a/README.rst b/README.rst index b11200981..23ef84f00 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.13.15.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.14.0.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.15...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.14.0...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://softwareengineerprogrammer.github.io/GEOPHIRES diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index 4b90f28a4..3c08b2f92 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` | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Total CAPEX` = `Overnight Capital Cost` + `Inflation costs during construction` + `Interest during construction` | +| `Total CAPEX` | Installation Costs | `Total Installed Cost` | `Singleowner` | `total_installed_cost` | `Total CAPEX` = `Overnight Capital Cost` + `Inflation costs during construction` + `Interest during construction` - `Other Incentives` - `One-time Grants Etc` | | `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 | | `Royalty Supplemental Payments` | Operating Costs | `Fixed operating cost` schedule | `Singleowner` | `om_fixed` | Royalty supplemental payments during the operational phase are added to the fixed operating cost according to the schedule. | | `Royalty Rate`, `Royalty Rate Escalation`, `Royalty Rate Escalation Start Year`, `Royalty Rate Maximum`; `Royalty Rate Schedule` | Operating Costs | `Variable operating cost` | `Singleowner` | `om_production` | The royalty is modeled as a tax-deductible variable operating expense. GEOPHIRES uses the provided schedule, or calculates a schedule of $/MWh values based on the PPA price and Royalty Rate for each year, with optional escalation, escalation start year, and cap (maximum). This ensures the total annual expense in SAM accurately matches the royalty payment due on gross revenue. | diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png index efdf85f5c..a8bab71b3 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png index 907e45c51..007738953 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png index f65e9275f..85c3aaa01 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png differ diff --git a/docs/_images/singh_et_al_base_simulation-production-temperature.png b/docs/_images/singh_et_al_base_simulation-production-temperature.png index 92368c742..4cd7c51fb 100644 Binary files a/docs/_images/singh_et_al_base_simulation-production-temperature.png and b/docs/_images/singh_et_al_base_simulation-production-temperature.png differ diff --git a/docs/conf.py b/docs/conf.py index 2bba65b0a..3f145048c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.13.15' +version = release = '3.14.0' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 9ae7eb96f..809e2a79d 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.13.15', + version='3.14.0', 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 e9b196401..372245b23 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -3454,8 +3454,15 @@ def calculate_total_capital_costs(self, model: Model) -> None: 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.CCap.value += self.FlatLicenseEtc.value + + if self.econmodel.value != EconomicModel.SAM_SINGLE_OWNER_PPA: + # SAM-EM parameterizes these as ibi_oth_amount + self.CCap.value = ( + self.CCap.value + - self.OtherIncentives.value + - self.TotalGrant.value + ) def calculate_operating_and_maintenance_costs(self, model: Model) -> None: # O&M costs diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 1c2d03021..366e01ece 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -238,7 +238,17 @@ def sf(_v: float, num_sig_figs: int = 5) -> float: sam_economics.after_tax_irr.value = sf(_get_after_tax_irr_pct(single_owner, cash_flow_operational_years, model)) sam_economics.project_npv.value = sf(_get_project_npv_musd(single_owner, cash_flow_operational_years, model)) - sam_economics.capex.value = single_owner.Outputs.adjusted_installed_cost * 1e-6 + + # Add back ibi_oth_amount (OtherIncentives + TotalGrant) which SAM subtracts from + # total_installed_cost to compute adjusted_installed_cost. Incentives are still applied + # by SAM natively in the cash flow/tax basis calculations; we just don't want them to + # also reduce the reported Total CAPEX (which would be inconsistent with Overnight Capital + # Cost, since CCap is not reduced by incentives for SAM Economic Models - see + # Economics.calculate_total_capital_costs). + _ibi_oth_usd = ( + (model.economics.OtherIncentives.quantity() + model.economics.TotalGrant.quantity()).to('USD').magnitude + ) + sam_economics.capex.value = (single_owner.Outputs.adjusted_installed_cost + _ibi_oth_usd) * 1e-6 if model.economics.has_royalties: combined_royalties_usd = [ diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 9be9b660a..da8934884 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -529,21 +529,28 @@ def PrintOutputs(self, model: Model): # Note ITC is in ECONOMIC PARAMETERS category for SAM-EM (not capital costs) f.write(f' {econ.RITCValue.display_name}: {-1 * econ.RITCValue.value:10.2f} {econ.RITCValue.CurrentUnits.value}\n') - additional_capex_modifiers: list[tuple[Parameter, int]] = [ - (econ.FlatLicenseEtc, 1), - (econ.OtherIncentives, -1), - (econ.TotalGrant, -1) - ] - for additional_capex_modifier_entry in additional_capex_modifiers: - additional_capex_modifier_param: Parameter = additional_capex_modifier_entry[0] - additional_capex_modifier_multiplier: int = additional_capex_modifier_entry[1] + def _render_additional_capital_cost_modifiers(additional_modifiers: list[tuple[Parameter, int]]) -> None: + for additional_modifier_entry in additional_modifiers: + additional_modifier_param: Parameter = additional_modifier_entry[0] + additional_modifier_multiplier: int = additional_modifier_entry[1] + + am_render_value = additional_modifier_param.value * additional_modifier_multiplier - acm_render_value = additional_capex_modifier_param.value * additional_capex_modifier_multiplier + if additional_modifier_param.Provided: + am_label = Outputs._field_label(additional_modifier_param.Name, 47) + f.write( + f' {am_label}{am_render_value:10.2f} {additional_modifier_param.CurrentUnits.value}\n') - if additional_capex_modifier_param.Provided: - acm_label = Outputs._field_label(additional_capex_modifier_param.Name, 47) - f.write( - f' {acm_label}{acm_render_value:10.2f} {additional_capex_modifier_param.CurrentUnits.value}\n') + additional_occ_modifiers: list[tuple[Parameter, int]] = [ + (econ.FlatLicenseEtc, 1), + ] + if not is_sam_econ_model: + # For SAM-EM these modify Total CAPEX, not OCC + additional_occ_modifiers.extend([ + (econ.OtherIncentives, -1), + (econ.TotalGrant, -1) + ]) + _render_additional_capital_cost_modifiers(additional_occ_modifiers) if is_sam_econ_model and econ.DoAddOnCalculations.value: # Non-SAM econ models print this in Extended Economics profile @@ -571,6 +578,13 @@ def PrintOutputs(self, model: Model): f.write( f' {idc_label}{econ.interest_during_construction.value:10.2f} {econ.interest_during_construction.CurrentUnits.value}\n') + additional_total_capex_modifiers: list[tuple[Parameter, int]] = [ + (econ.OtherIncentives, -1), + (econ.TotalGrant, -1) + ] if is_sam_econ_model else [] + + _render_additional_capital_cost_modifiers(additional_total_capex_modifiers) + capex_param = econ.CCap if not is_sam_econ_model else econ.capex_total capex_label = Outputs._field_label(capex_param.display_name, 50) f.write(f' {capex_label}{capex_param.value:10.2f} {capex_param.CurrentUnits.value}\n') diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 29acebe52..6e9b3ce48 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.13.15' +__version__ = '3.14.0' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 7ed141cc5..c2f6eb03d 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -275,14 +275,14 @@ class GeophiresXResult: 'Total surface equipment costs', 'Investment Tax Credit', 'One-time Flat License Fees Etc', - 'Other Incentives', - 'One-time Grants Etc', 'Total Add-on CAPEX', 'Overnight Capital Cost', # Displayed for economic models that treat inflation costs as capital costs (SAM-EM) 'Inflation costs during construction', 'Royalty supplemental payments during construction', 'Interest during construction', + 'Other Incentives', + 'One-time Grants Etc', 'Total capital costs', 'Annualized capital costs', # AGS/CLGS diff --git a/tests/geophires_x_tests/other-incentives-and-one-time-flat-fees.txt b/tests/geophires_x_tests/other-incentives-and-one-time-flat-fees.txt new file mode 100644 index 000000000..e2d65c0b7 --- /dev/null +++ b/tests/geophires_x_tests/other-incentives-and-one-time-flat-fees.txt @@ -0,0 +1,97 @@ +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* + +Economic Model, 5, -- SAM Single Owner PPA + +Starting Electricity Sale Price, 0.12 +Electricity Escalation Rate Per Year, 0.00057 +Ending Electricity Sale Price, 0.16 +Electricity Escalation Start Year, 1 + +Discount Rate, 0.12 +Royalty Holder Discount Rate, 0.10 + +Fraction of Investment in Bonds, 0.85 +Inflated Bond Interest Rate, 0.07 + +Inflation Rate, 0.027 + +Construction Years, 2 +Construction CAPEX Schedule, 0.01,0.01,0.03,0.05,0.2,0.3,0.4 +Inflated Bond Interest Rate During Construction, 0.105 + +Investment Tax Credit Rate, 0.3 +Property Tax Rate, 0.0055 +Other Incentives, 5, -- Triggers the double-counting bug in combination with SAM-EM +One-time Flat License Fees Etc, 1.5 + +Capital Cost for Power Plant for Electricity Generation, 2000 +Field Gathering System Capital Cost Adjustment Factor, 0.54 + +Reservoir Stimulation Capital Cost per Injection Well, 0 +# Production wells are not stimulated (parameter intentionally omitted) + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* + +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Supercritical ORC +Plant Lifetime, 20 + +Injectivity Index, 10 +Productivity Index, 5 + +Reservoir Model, 4, -- ANNUAL_PERCENTAGE +Drawdown Parameter Schedule, 0.001 * 10, 0.01 + +Maximum Drawdown, 1 + +Reservoir Volume Option, 1, -- FRAC_NUM_SEP +Number of Fractures per Stimulated Well, 5 +Fracture Shape, 4, -- RECTANGULAR +Fracture Width, 500 +Fracture Height, 100 +Fracture Separation, 250 + +Production Flow Rate per Well, 75 + +Number of Injection Wells per Production Well, 0.5 + +Production Well Diameter, 8.535 +Injection Well Diameter, 8.535 + +Ramey Production Wellbore Model, True +Injection Temperature, 54 + +Water Loss Fraction, 0.05 + +Utilization Factor, 0.913 +Plant Outlet Pressure, 9 bar +Production Wellhead Pressure, 11 bar +Circulation Pump Efficiency, 0.80 + +Well Geometry Configuration, 4, -- L: Vertical section plus lateral(s) + +Number of Production Wells, 10 +Number of Multilateral Sections, 8 +Nonvertical Length per Multilateral Section, 1500 +All-in Nonvertical Drilling Costs, 1900 +Combined Income Tax Rate, 0.25 +Well Drilling Cost Correlation, 16 +Royalty Rate Schedule, 0.03 +Bond Financing Start Year, -1 + +# *** SCENARIO-SPECIFIC RESERVOIR PARAMETERS *** +# ********************************************** +Gradient 1, 45 +Reservoir Depth, 3 +Surface Temperature, 10 +Ambient Temperature, 5 +Reservoir Heat Capacity, 1300.0 +Reservoir Density, 2500.0 +Reservoir Thermal Conductivity, 2.9 + +# *** SIMULATION PARAMETERS *** +# ***************************** +Time steps per year, 12 +Print Output to Console, 0 diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 3e218bb1a..11bdb5b15 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -1423,6 +1423,63 @@ def _itc_output_q(r: GeophiresXResult) -> dict[str, float]: _itc_output_q(r_fed_itc_rate_and_state_itc_amount), ) + def test_other_incentives_one_time_flat_fees_and_total_grants(self): + def _assert_occ_plus_modifiers_equals_total_capex(_r: GeophiresXResult) -> None: + with open(_r.output_file_path, encoding='utf-8') as f: + lines = f.readlines() + + is_parsing = False + occ_val = 0.0 + total_capex_val = 0.0 + intermediate_sum = 0.0 + + for line in lines: + clean_line = line.strip() + + if clean_line.startswith('Overnight Capital Cost:'): + is_parsing = True + occ_val = float(clean_line.split(':')[1].replace('MUSD', '').strip()) + continue + + if is_parsing: + if clean_line.startswith('Total CAPEX:'): + total_capex_val = float(clean_line.split(':')[1].replace('MUSD', '').strip()) + break + + if ':' in clean_line: + val_str = clean_line.split(':')[1].replace('MUSD', '').strip() + try: + intermediate_sum += float(val_str) + except ValueError: + pass + + self.assertAlmostEqual( + occ_val + intermediate_sum, + total_capex_val, + places=2, + msg='Total CAPEX should be the sum of OCC and all intermediate items (e.g., inflation, interest, incentives)', + ) + + _assert_occ_plus_modifiers_equals_total_capex( + GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('other-incentives-and-one-time-flat-fees.txt'), + params={ + # 'Print Output to Console': True, + }, + ) + ) + ) + + _assert_occ_plus_modifiers_equals_total_capex( + GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('other-incentives-and-one-time-flat-fees.txt'), + params={'Print Output to Console': True, 'One-time Grants Etc': 50}, + ) + ) + ) + @staticmethod def _new_model( input_file: Path,