diff --git a/.aiexclude b/.aiexclude new file mode 100644 index 000000000..03bd4129b --- /dev/null +++ b/.aiexclude @@ -0,0 +1 @@ +*.env diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b16532369..b7456090b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.10.24 +current_version = 3.11.4 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 56f2be8c4..1f3f47d70 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.10.24 + version: 3.11.4 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/.editorconfig b/.editorconfig index 586c7367d..74879ed01 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,3 +18,7 @@ indent_size = 2 [*.tsv] indent_style = tab + +[*.jinja] +trim_trailing_whitespace = false +indent_size = unset diff --git a/.gitignore b/.gitignore index a922924be..1b686f1b4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,11 @@ requirements_2025-08-11.txt .build .cache .eggs + .env +*.env +tests/regenerate-example-result.env + .installed.cfg .ve bin @@ -92,8 +96,10 @@ output/*/index.html # Sphinx/docs docs/_build +docs/temp.txt docs/reference/geophires-request.json docs/reference/parameters.rst +docs/Fervo_Project_Cape-5.md docs/geophires-request.json docs/parameters.rst docs/hip-ra-x-request.json diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c2f9f01e..34796358a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,10 +4,19 @@ Changelog GEOPHIRES v3 (2023-2025) ------------------------ +3.11 +^^^^ + +3.11.3: `SAM Economic Models ITC result output `__ | `release `__ + +3.11: `SAM Economic Models Project Payback Period fix `__ | `release `__ + 3.10 ^^^^ +3.10.25: `Add Number of Injection Wells per Production Well parameter `__ + 3.10: `SAM Economic Models: Multiple Construction Years; Number of Fractures per Stimulated Well parameter; Royalty Rate Escalation Start Year parameter `__ | `release `__ 3.9 diff --git a/MANIFEST.in b/MANIFEST.in index 3440150e6..86c6af67b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ graft src graft ci graft tests +include .aiexclude include .bumpversion.cfg include .cookiecutterrc include .coveragerc diff --git a/README.rst b/README.rst index ad9530116..6d560d810 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.10.24.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.11.4.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.10.24...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.11.4...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X @@ -168,6 +168,10 @@ Example-specific web interface deeplinks are listed in the Link column. - Input file - Case report file - Link + * - Case Study: 500 MWe EGS modeled on Fervo Cape Station (`documentation `__) + - `Fervo_Project_Cape-4.txt `__ + - `.out `__ + - `link `__ * - Example 1: EGS Electricity - `example1.txt `__ - `.out `__ @@ -288,10 +292,10 @@ Example-specific web interface deeplinks are listed in the Link column. - `Fervo_Project_Cape-3.txt `__ - `.out `__ - `link `__ - * - Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station (`documentation `__) - - `Fervo_Project_Cape-4.txt `__ - - `.out `__ - - `link `__ + * - 100 MWe EGS modeled on Fervo Cape Station Phase I + - `Fervo_Project_Cape-5.txt `__ + - `.out `__ + - `link `__ * - Superhot Rock (SHR) Example 1 - `example_SHR-1.txt `__ - `.out `__ diff --git a/docs/Fervo_Project_Cape-4.md b/docs/Fervo_Project_Cape-4.md index 4c0c3f8ea..9aa975219 100644 --- a/docs/Fervo_Project_Cape-4.md +++ b/docs/Fervo_Project_Cape-4.md @@ -1,4 +1,9 @@ -# Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station +# [Deprecated] Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station + +**⚠️ This is a previous version of the case study. The case study has been updated since the release of this version.** +[Click here](Fervo_Project_Cape-5.html) to find the latest version. + +--- The GEOPHIRES example `Fervo_Project_Cape-4` is a case study of a 500 MWe EGS Project modeled on Fervo Cape Station with its April 2025-announced diff --git a/docs/Fervo_Project_Cape-5.md.jinja b/docs/Fervo_Project_Cape-5.md.jinja new file mode 100644 index 000000000..84c9b252b --- /dev/null +++ b/docs/Fervo_Project_Cape-5.md.jinja @@ -0,0 +1,490 @@ +.. raw:: html + + + +# GEOPHIRES Case Study: 500 MW EGS modeled on Fervo Cape Station + +## Introduction + +The GEOPHIRES example `Fervo_Project_Cape-5`[^author] is a case study of a 500 MWe EGS project modeled +on Phases I and II of [Fervo Energy's Cape Station](https://capestation.com/). + +[^author]: Author: Jonathan Pezzino, Scientific Web Services LLC (GitHub: [softwareengineerprogrammer](https://github.com/softwareengineerprogrammer)) + +Key results include LCOE = {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }} and IRR = {{ irr_pct ~ '%' }}. ([Jump to the Results section](#results)). + +.. raw:: html + + +
+ + Power Production Profile Graph + + + + LCOE Sensitivity Analysis Results Chart + +
+ +[Click here](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-5) to +interactively explore the case study example in the GEOPHIRES web interface. + +### Modeling Overview: A Consensus-Based Second-of-a-Kind Analog + +This case study models a 500 MWe Enhanced Geothermal System (EGS) project designed to represent a "Second-of-a-Kind" ( +SOAK) deployment. +Rather than serving as an exact facsimile of Cape Station as built, this study estimates what a non-Fervo developer +could achieve on a geologically identical site, relying primarily on publicly available data and standardized +engineering estimates. +The model assumes the developer is a "fast follower": benefiting from the proof-of-concept established by Cape Station +Phase I but operating without access to Fervo’s private supply chain or proprietary optimization data. + +**Public Data Reliance:** Inputs utilize exact values for publicly available parameters, such as geothermal +gradient and reservoir density. +Where data is proprietary, values are inferred from public announcements or extrapolated from standard industry +correlations. + +**Conservative Constraints:** To ensure the model serves as a robust feasibility test, some inputs are intentionally +conservative compared to Fervo’s stated targets, such as drilling costs and water loss. + +**Fast Follower Advantage:** By entering the market after Fervo’s initial de-risking campaigns, the modeled developer +avoids the high "tuition costs" of early experimentation. +For example, while Fervo’s initial drilling costs at Cape Station ranged +from [$9.4M down to $4.8M per well](https://houston.innovationmap.com/fervo-energy-drilling-utah-project-2667300142.html) +as they climbed the learning curve, +this model assumes a developer can bypass those initial high-cost outliers, +instead initiating their campaign at a stabilized commercial baseline (modeled here +at {{ '$' ~ drilling_costs_per_well_musd ~ 'M/well' }}, aligned with the NREL ATB and 2025 cost curves). +This reflects a developer who capitalizes on established industry knowledge to skip the "First-of-a-Kind" (FOAK) +premiums but has not yet achieved the fully optimized learning rates of a mature "Nth-of-a-kind" operator. + +### Intended Use Cases + +This case study is designed to function as a public utility for the geothermal sector, serving two primary roles: + +**Industry Benchmark:** By relying primarily on verifiable public data and independent expert consensus, this model +establishes a transparent baseline for EGS viability. +It tests the premise that Fervo’s success at Cape Station is a replicable standard for the next-generation geothermal +industry. +The results serve as reference points for what is achievable using current technology in high-grade resources. + +**Template for Resource Assessment & Custom Modeling:** The example input file (`Fervo_Project_Cape-5.txt`) is intended +as customizable template for modeling other resources. +Users can input local geologic data (gradient, rock properties) into this template to evaluate how a Cape Station-style +design would perform in different geographies (e.g., Nevada vs. Utah vs. International). +Different plant sizes and performance targets can be modeled by adjusting the number of production wells, fractures per well, +and other technical & engineering parameters. +The model allows users to stress-test economic assumptions, such as the PPA price or Investment Tax Credit (ITC), to see +how policy changes impact the feasibility of replicating this design elsewhere. + +## Methodology + +The Inputs and Results tables document key assumptions, inputs, and a comparison of results with reference +values. +Note that these are not the exhaustive sets of inputs and results, which are available in source code and +the [web interface](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-5). + +### Inputs + +See [Fervo_Project_Cape-5.txt](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-5.txt) +in source code for the full set of inputs. + +#### Reservoir Parameters + +{{ reservoir_parameters_table_md }} + +#### Well Bores Parameters + +{{ well_bores_parameters_table_md }} + +#### Surface Plant Parameters + +{{ surface_plant_parameters_table_md }} + +#### Construction Parameters + +{{ construction_parameters_table_md }} + + +#### Economic Parameters + +{{ economics_parameters_table_md }} + +### Calibration with Fervo-implemented Field Design + +[Designing the Record-Breaking Enhanced Geothermal System at Project Cape](https://www.resfrac.com/wp-content/uploads/2025/06/Singh-2025-Fervo-Project-Cape.pdf) (Singh et al., 2025) +describes reservoir modeling (ResFrac) that informed the Cape Station field implementation[^field-implementation-configuration-note]. + +[^field-implementation-configuration-note]: Note on Configuration: While the specific Bearskin and Gold pads (Phase II) utilize an inverted 2:3 ratio (3 injectors for 2 producers), this case study assumes the 3:2 ratio identified in the paper's optimization studies ("Study 1") represents the standard repeating module for the full-scale 400+ MWe system. The higher injector count in Phase II is interpreted as a transient requirement for field delineation and initial pressure support (boundary conditions) rather than the long-term commercial standard. + +An equivalent GEOPHIRES simulation was run using the case study's reservoir engineering parameters, with the following modifications to align with Singh et al.'s modeling scenario: + +{{ reservoir_engineering_reference_simulation_params_table_md }} + +The following table compares the average production temperature profile from the "700 ft bench spacing" scenario in Singh et al. with the GEOPHIRES simulation. +Note that both figures show temperature in Fahrenheit rather than Celsius. + +{# @formatter:off #} +| Reference Simulation: Fervo-implemented Design (Fig. 18.) | GEOPHIRES Simulation: Case Study Equivalent Scenario | +|---|---| +| | | +{# @formatter:on #} + +While the initial and final (Year 15) temperatures are consistent, the production curves exhibit distinct profiles due to the different modeling approaches: + +1. **Reference Simulation (Left):** The Singh et al. (2025) curve reflects a fully coupled numerical simulation (ResFrac) that accounts for complex fracture heterogeneity, inter-well interference, and variable flow paths. The gradual decline starting around Year 3 indicates thermal dispersion, where cold injection fluid mixes with hot reservoir fluid along faster flow paths earlier in the project life. +1. **GEOPHIRES Simulation (Right):** The GEOPHIRES result utilizes the Gringarten (1975) analytical solution for flow in fractured rock. This model assumes a uniform thermal sweep across an idealized fracture surface. Consequently, it maintains a flat, maximum production temperature for a longer duration until the cold front reaches the production well (thermal breakthrough), resulting in a sharper, later decline. + +Despite these structural differences, the comparison validates the basis for the case study's reservoir engineering parameters, as the aggregate heat extraction and year-15 endpoint align closely with the numerical simulation baseline. + +The calibration simulation above represents a 15-year unmitigated thermal decline without redrilling. +In the full case study results, the model includes redrilling events that restore production temperature when drawdown thresholds are reached, +resulting in the cyclical profile shown in the [Production Temperature section](#production-temperature-profile) below. + +## Results + +See [Fervo_Project_Cape-5.out](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-5.out) +in source code for the complete results. + +### Economic Results + +Note that economic results are derived from the [SAM Single Owner PPA Economic Model](SAM-Economic-Models.html#sam-single-owner-ppa) pro-forma cash flow analysis. +The case study result's cash flow analysis can be viewed in the web interface and in the `Fervo_Project_Cape-5.out` result file in source code. + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|---------------|----------------|--------------------|------------------| +| LCOE | {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }} | \$80/MWh | Horne et al, 2025. | +| After-tax IRR
(at Year {{ operations_year_of_irr }} of Operations) | {{ irr_pct ~ '%' }} | 15–25% | Typical levered returns for energy projects | +| NPV | {{ '$' ~ npv_musd ~ 'M' }} | >$0 | Positive NPVs result in profit | +| Levered Equity
Profitability Index
| {{ project_vir }} | >1.0 | Calculations greater than 1.0 indicate the future anticipated discounted cash inflows are greater than the anticipated discounted cash outflows. | +| Project ROI | {{ project_moic }} | | .. N/A | +{# Note that the '.. N/A' entry in the last row is required for the table to render in HTML (presumable m2r2/sphinx build issue) #} +{# @formatter:on #} + +Hover over the metric names to view the corresponding definitions. +See [GEOPHIRES output parameters documentation](parameters.html#economic-parameters) for more information. + +### Capital Costs (CAPEX) + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------|--------------------------------------------------|------------------| +| WACC | {{ wacc_pct ~ '%' }} | 8.3% | Fervo's target goal is to eventually achieve a "Solar Standard" WACC of 8.3% (Matson, 2024). | +| Exploration Costs | {{ '$' ~ exploration_cost_musd ~ 'M' }} | {{ '$' ~ drilling_costs_per_well_musd*5 ~ 'M' }} | 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025). Case study result conservatively includes additional costs for geophysical survey, indirect costs, and contingency. | +| Well Drilling and Completion Costs | {{ '$' ~ drilling_costs_musd ~ 'M' }} total
({{ '$' ~ drilling_costs_per_well_musd ~ 'M/well' }}) | <$4M/well | Latimer, 2025. | +| Stimulation Costs | {{ '$' ~ stim_costs_musd ~ 'M' }} total
({{ '$' ~ stim_costs_per_well_musd ~ 'M/well' }}) | $4.65M/well | Based on 46%:54% drilling:stimulation cost ratio (Yusifov & Enriquez, 2025). | +| Surface Power Plant Costs | {{ '$' ~ surface_power_plant_costs_gusd ~ 'B' }} | | | +| Field Gathering System Costs | {{ '$' ~ field_gathering_cost_musd ~ 'M' }}
({{ field_gathering_cost_pct_occ ~ '%' }} of OCC) | 2% of OCC | Matson, 2024. | +| Overnight Capital Cost | {{ '$' ~ occ_gusd ~ 'B' }} | | | +| Total CAPEX | {{ '$' ~ total_capex_gusd ~ 'B' }}
(OCC + interest and inflation during construction) | | | +| Total CAPEX: $/kW | {{ '$' ~ capex_usd_per_kw ~ '/kW' }}
(based on maximum net electricity generation) | $5000/kW; $4500/kW; $3000–$6000/kW | McClure, 2024; Horne et al, 2025; Latimer, 2025. | +{# @formatter:on #} + +### Operating Costs (OPEX) + +{{ opex_result_outputs_table_md }} + +### Technical & Engineering Results + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|--------------------------------------|-----------------------------------------------------------|--------------------|------------------| +| Minimum Net Electricity Generation | {{ min_net_generation_mwe }} MW | 500 MW | The announced 500 MWe capacity (Fervo Energy, 2025) is interpreted to mean that the PPA penalizes Cape Station if net electricity generation falls below 500 MWe. | +| Average Net Electricity Generation | {{ avg_net_generation_mwe }} MW | | | +| Maximum Net Electricity Generation | {{ max_net_generation_mwe}} MW | | | +| Maximum Total Electricity Generation | {{ max_total_generation_mwe }} MW | Upper bound: 600 MW | Combined nameplate capacity of 10×60 MWe Gen 2 ORCs. A total of 8×60 MWe Gen 2 ORCs have been announced for Phase II; 3 from Turboden and 5 from Baker Hughes (Turboden, 2025; Jacobs, 2025). This equates to 480 MW gross capacity for Phase II's 400 MW net capacity. An equivalent SOAK 500 MW project would therefore require 10 Gen 2 ORC units. (Note that the modular Gen 2 ORCs are not individually modeled in this case study, and are assumed to be combined into a single power plant). | +| 2-year Average Net Power Production per Production Well | {{ two_year_avg_net_power_mwe_per_production_well }} MW | 7.6–11.5 MW | Figures 4 and 12 (Singh et al., 2025). | +| Injection Pumping Parasitic Load
(Average Pumping Power/Average Total Electricity Generation) | {{ parasitic_loss_pct ~ '%' }} | Upper bound: 16.7% | Procurement of 480 MW of Gen 2 ORC units for 400 MW net capacity in Phase II allows for up to 16.7% total on-site consumption (80 MW; including injection pumping power). | +| Total fracture surface area per well | {{ total_fracture_surface_area_per_well_mm2 }}×10⁶ m²
({{ total_fracture_surface_area_per_well_mft2 }} million ft²) | Project Red: 2.787×10⁶ m²
(30 million ft²) | Greater fracture surface area expected than Project Red (Fercho et al, 2025). | +| Reservoir Volume | {{ reservoir_volume_m3 }} m³ | | Calculated from fracture area × fracture separation × number of fractures per well × number of wells | +| Bottom-hole Temperature
(BHT) | {{ bht_temp_degc ~ '℃' }} | 200–241℃ | Fercho et al., 2024; Singh et al., 2025. | +| Initial Production Temperature | {{ initial_production_temperature_degc ~ '℃' }} | 196–208℃ | Approximate range of initial production temperatures between shallower and deeper producers (Singh et al., 2025). | +| Average Production Temperature | {{ average_production_temperature_degc ~ '℃' }} | 199–209℃ | Approximate range of thermally conditioned production temperatures between shallower and deeper producers (Singh et al., 2025). | +| Number of times redrilling | {{ number_of_times_redrilling }} | 2–5 | Redrilling expected to be required within 5–10 years of project start | +| Total wells drilled over project lifetime | {{ total_wells_including_redrilling }} | Permitted Limit: 320 | The BLM Environmental Assessment (DOI-BLM-UT-C010-2024-0018-EA) authorizes an estimated development of 320 production and injection wells (BLM, 2024). As modeled, the project remains within this regulatory envelope for the first three drilling campaigns (Initial, Year 8, and Year 16), reaching a cumulative total of approximately 282 wells.

The model exceeds the current authorization only during the final redrilling event in Year 24. It is a standard industry assumption that brownfield capacity maintenance activities (e.g. sidetracking existing wells on existing pads) occurring two decades into operations would be authorized through subsequent regulatory actions, such as a Determination of NEPA Adequacy (DNA) or a Categorical Exclusion, given the established baseline of environmental impact. | +{# @formatter:on #} + +#### Production Temperature Profile + + + +The production temperature profile exhibits distinctive cyclical behavior driven by the interaction between wellbore physics and reservoir thermal evolution: + +1. **Thermal Conditioning (Years 1–6)**: The initial rise in production temperature, peaking at approximately 203°C, is driven by the thermal conditioning of the production wellbores. As hot geofluid continuously flows through the wells, the wellbore casing and surrounding rock heat up, reducing conductive heat loss as predicted by the Ramey wellbore model. +2. **Reservoir Drawdown (Years 6–8)**: Following the conditioning peak, temperature declines as the cold front from injection wells reaches the production zone (thermal breakthrough), reducing the produced fluid enthalpy. +3. **Redrilling (End of Years 8, 16, 24)**: The model triggers a redrilling event when the next time step's temperature would fall below the threshold defined by the `Maximum Drawdown` parameter (shown as the dashed orange line). The production temperature does not necessarily actually reach the threshold; redrilling typically preemptively restores the wellfield before that occurs. The cost of these events is amortized as an operational expense over the project lifetime. + +#### Power Generation Profile + + + +Power generation is a direct function of production temperature, so the power production profile mirrors the thermal behavior described above. The graph shows both total (gross) electricity generation and net electricity generation after parasitic losses. The gap between the two curves represents the energy consumed by the circulation pumps. + +The horizontal reference lines indicate the 500 MW net PPA minimum production requirement and the 600 MW nameplate capacity (combined capacity of the individual ORC units). + +### Sensitivity Analysis + +The following charts show the sensitivity of key metrics to various inputs. +Each chart shows the sensitivity of a single metric, such as LCOE, to the set of tested input values. +The leftmost chart column shows the parameter being tested and its baseline case study input value in parentheses. +The bars for each row show the deltas of the metric value from the baseline case study value for the values tested for +that parameter. +Green bars indicate favorable outcomes, such as lower LCOE or higher IRR, while gray bars indicate unfavorable outcomes, +such as higher LCOE or lower IRR. + +Click the bars to view the sensitivity analysis result for the input value in the web interface. + +Note that the sensitivity analysis scenarios do not necessarily conform to all constraints and assumptions documented in the case study methodology. +For example, scenarios for Bond Interest Rate have different weighted average cost of capital (WACC) values due to the effect of interest rate on WACC. +This is particularly relevant for technical parameters pertaining to reservoir engineering. +In a real-world design, these variables are physically coupled; for instance, targeting a higher production flow rate +would typically necessitate a larger fracture surface area to mitigate the resulting acceleration in thermal drawdown. +See the [discussion of flow rate below](#impact-of-flow-rate-on-project-economics). + +### LCOE + +.. raw:: html + + + + + + LCOE Sensitivity Analysis Chart + +#### Impact of PPA Price on LCOE + +The sensitivity analysis reveals a positive correlation between the Power Purchase +Agreement (PPA) price and the Levelized Cost of Electricity (LCOE). While counterintuitive, this is a function of SAM +Economic Models treating federal and state income taxes as operating cash outflows. + +In SAM Economic Models, the PPA price is a fixed input that determines project revenue. A higher PPA price generates +higher taxable income, which in turn increases the project's annual income tax liability (a negative cash flow). Because +the LCOE calculation aggregates all lifetime project costs, including the tax burden, the additional tax costs incurred +from higher revenues result in a higher calculated LCOE. Conversely, a lower PPA price reduces taxable income, lowers +tax liability, and decreases the resulting LCOE. + +### IRR + +.. raw:: html + + + + IRR Sensitivity Analysis Chart + +### NPV + +.. raw:: html + + + + NPV Sensitivity Analysis Chart + +Users may wish to perform their own sensitivity analysis +using [GEOPHIRES's Monte Carlo simulation module](Monte-Carlo-User-Guide.html) or other data analysis tools. + +### Impact of Flow Rate on Project Economics + +Higher flow rate per production well does not necessarily result in improved project economics (e.g. lower LCOE or higher IRR). +Higher flow rates result in increased generation in the short term, but also cause faster thermal decline. +Additional make-up wells may need to be drilled to compensate for increased thermal decline and maintain a minimum net generation (redrilling), +the cost of which may offset incremental revenue from increased generation. +This tradeoff was considered in reservoir modeling that guided Fervo's field implementation (Singh et al., 2025). + +## 100 MWe Model (Phase I) + +The case study also includes a 100 MWe model, `Fervo_Project_Cape-6`, with equivalent capacity to Phase I. +Note that like the 500 MWe model, `Fervo_Project_Cape-6` represents a SOAK project and not the real-world Phase I implementation as built by Fervo. +[Click here](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-6) to view the +100 MWe model in the GEOPHIRES web interface. + +## Previous Versions + +Documentation is available for the following previous case study versions, which are deprecated in favor of the current version: + +{# @formatter:off #} +1. Version: [Fervo_Project_Cape-4](Fervo_Project_Cape-4.html). (Last Updated: 2025-08-11.) Key differences: + 1. Fervo_Project_Cape-5 models multiple construction years instead of a single construction year + 1. Fervo_Project_Cape-5 incorporates various reservoir characteristic and engineering updates including: + 1. Segmented geology (gradients) + 1. Ambient and surface temperature refinement + 1. 5-well bench design instead of well pairs (doublets) + 1. 8.5-inch inner well diameter + 1. Productivity/Injectivity indexes instead of impedance model + 1. Stimulation parameters and outcome + 1. Fervo_Project_Cape-5 incorporates various reservoir economic parameter updates including: + 1. BLM royalties + 1. Refined discount and interest rates + 1. Refined tax rates including addition of property tax + 1. Fervo_Project_Cape-5 includes substantially expanded sensitivity analysis +{# @formatter:off #} + +{# TODO others e.g. Fervo_Project_Cape-3... #} + +--- + +## References + +Akindipe, D. and Witter. E. (2025). "2025 Geothermal Drilling Cost Curves +Update". https://pangea.stanford.edu/ERE/db/GeoConf/papers/SGW/2025/Akindipe.pdf?t=1740084555 + +Baytex Energy. (2024). Eagle Ford Presentation. +https://www.baytexenergy.com/content/uploads/2024/04/24-04-Baytex-Eagle-Ford-Presentation.pdf + +Beckers, K., McCabe, K. (2019) GEOPHIRES v2.0: updated geothermal techno-economic simulation tool. Geotherm Energy +7,5. https://doi.org/10.1186/s40517-019-0119-6 + +Fercho, S., Matson, G., McConville, E., Rhodes, G., Jordan, R., Norbeck, J.. (2024, February 12). +Geology, Temperature, Geophysics, Stress Orientations, and Natural Fracturing in the Milford +Valley, UT Informed by the Drilling Results of the First Horizontal Wells at the Cape Modern +Geothermal Project. https://pangea.stanford.edu/ERE/db/GeoConf/papers/SGW/2024/Fercho.pdf + +Fercho, S., Norbeck, J., Dadi, S., Matson, G., Borell, J., McConville, E., Webb, S., Bowie, C., & Rhodes, G. (2025). +Update on the geology, temperature, fracturing, and resource potential at the Cape Geothermal Project informed by data +acquired from the drilling of additional horizontal EGS wells. Proceedings of the 50th Workshop on Geothermal Reservoir +Engineering, Stanford University, Stanford, CA. https://pangea.stanford.edu/ERE/pdf/IGAstandard/SGW/2025/Fercho.pdf + +Fervo Energy. (2023, September 19). Fervo’s Commercialization Plans for Enhanced Geothermal Systems ( +EGS). https://egi.utah.edu/wp-content/uploads/2023/09/09.45-Emma-McConville-Fervo_EGI_Sept-19-2023.pdf + +Fervo Energy. (2023, September 25). Fervo Energy Breaks Ground on the World’s Largest Next-gen Geothermal Project. +https://fervoenergy.com/fervo-energy-breaks-ground-on-the-worlds-largest-next-gen-geothermal-project/ + +Fervo Energy. (2024, September 10). Fervo Energy’s Record-Breaking Production Results Showcase Rapid Scale Up of +Enhanced +Geothermal. https://www.businesswire.com/news/home/20240910997008/en/Fervo-Energys-Record-Breaking-Production-Results-Showcase-Rapid-Scale-Up-of-Enhanced-Geothermal + +Fervo Energy. (2025, March 31). Geothermal Mythbusting: Water Use and +Impacts. https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/ + +Fervo Energy. (2025, April 15). Fervo Energy Announces 31 MW Power Purchase Agreement with Shell +Energy. https://fervoenergy.com/fervo-energy-announces-31-mw-power-purchase-agreement-with-shell-energy/ + +Fervo Energy (2025, June 11). Fervo Energy Secures $206 Million In New Financing To Accelerate Cape Station Development. +https://fervoenergy.com/fervo-secures-new-financing-to-accelerate-development/ + +Gradl, C. (2018). Review of Recent Unconventional Completion Innovations and their Applicability to EGS Wells. Stanford +Geothermal Workshop. +https://pangea.stanford.edu/ERE/pdf/IGAstandard/SGW/2018/Gradl.pdf + +Horne, R., Genter, A., McClure, M. et al. (2025) Enhanced geothermal systems for clean firm energy generation. Nat. Rev. +Clean Technol. 1, 148–160. https://doi.org/10.1038/s44359-024-00019-9 + +Jacobs, Trent. (2024, September 16). Fervo and FORGE Report Breakthrough Test Results, Signaling More Progress for +Enhanced +Geothermal. https://jpt.spe.org/fervo-and-forge-report-breakthrough-test-results-signaling-more-progress-for-enhanced-geothermal + +Jacobs, Trent. (2025, September 5). Baker Hughes Nabs Award for Next Phase of Fervo Energy's Geothermal Power Plant in +Utah. +https://jpt.spe.org/baker-hughes-nabs-award-for-next-phase-of-fervo-energygeothermal-power-plant-in-utah + +Ko, S., Ghassemi, A., & Uddenberg, M. (2023). Selection and Testing of Proppants for EGS. +Proceedings, 48th Workshop on Geothermal Reservoir Engineering, Stanford University, Stanford, California. +https://pangea.stanford.edu/ERE/db/GeoConf/papers/SGW/2023/Ko.pdf + +Latimer, T. (2025, February 12). Catching up with enhanced geothermal (D. Roberts, +Interviewer). https://www.volts.wtf/p/catching-up-with-enhanced-geothermal + +Matson, M. (2024, September 11). Fervo Energy Technology Day 2024: Entering "the Geothermal Decade" with Next-Generation +Geothermal +Energy. https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/ + +McClure, M. (2024, September 12). Digesting the Bonkers, Incredible, Off-the-Charts, Spectacular Results from the Fervo +and FORGE Enhanced Geothermal Projects. ResFrac Corporation Blog. +https://www.resfrac.com/blog/digesting-the-bonkers-incredible-off-the-charts-spectacular-results-from-the-fervo-and-forge-enhanced-geothermal-projects + +NCEI. US Climate +Normals. https://www.ncei.noaa.gov/access/us-climate-normals/#dataset=normals-annualseasonal&timeframe=30&station=USC00425654 + +NREL. (2024). Annual Technology Baseline: Geothermal (2024). +https://atb.nrel.gov/electricity/2024/geothermal + +NREL. (2025, February 26). Annual Technology Baseline: Geothermal (2024b). +https://atb.nrel.gov/electricity/2024b/geothermal + +Norbeck, J., Gradl, C., Latimer, T. (2024, September 10). Deployment of Enhanced Geothermal System Technology Leads to +Rapid Cost Reductions and Performance Improvements. https://doi.org/10.31223/X5VH8C + +Norbeck J., Latimer T. (2023). Commercial-Scale Demonstration of a First-of-a-Kind Enhanced Geothermal +System. https://doi.org/10.31223/X52X0B + +Quantum Proppant Technologies. (2020). Well Completion Technology. World +Oil. https://quantumprot.com/uploads/images/2b8583e8ce8038681a19d5ad1314e204.pdf + +Shiozawa, S., & McClure, M. (2014). EGS Designs with Horizontal Wells, Multiple Stages, and Proppant. ResFrac. +https://www.resfrac.com/wp-content/uploads/2024/07/Shiozawa.pdf + +Singh, A., Galban, G., McClure, M. (2025, June 9). +Proceedings of the 2025 Unconventional Resources Technology Conference. +https://www.resfrac.com/wp-content/uploads/2025/06/Singh-2025-Fervo-Project-Cape.pdf + +Southern Utah University. (2024, October 23). Fervo Energy, Southern Utah University, and Elemental Impact Launch +Geothermal Drilling & Completions Apprenticeship Program. +https://www.suu.edu/news/2024/10/geothermal-energy-joint-campaign.html + +Turboden. (2025, October 2). Turboden selected to deliver 180 MW of Fervo’s Gen 2 ORC Power Plants at Cape Station in +Utah. https://www.turboden.com/company/media/press/press-releases/4881/turboden-selected-to-deliver-180-mw-of-fervos-gen-2-orc-power-plants-at-cape-station-in-utah + +U.S. Department of the Interior Bureau of Land Management. (2024, October). +Finding of No Significant Impact and Decision Record DOI-BLM-UT-C010-2024-0018-EA. +https://eplanning.blm.gov/public_projects/2033002/200625761/20120795/251020775/DOI-BLM-UT-C010-2024-0018-EA_FONSI_DR_%20Fervo%20EA_signed.pdf + +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 + +Xing, P., England, K., Moore, J., McLennan, J. (2025, February 10). +Analysis of the 2024 Circulation Tests at Utah FORGE and the Response of Fiber Optic Sensing +Data. +https://pangea.stanford.edu/ERE/pdf/IGAstandard/SGW/2025/Xing2.pdf + +Yearsley, E., Kombrink, H. (2024, November 6). +A critical look at Fervo dataset suggests lower output. +https://geoexpro.com/a-critical-look-at-fervo-dataset-suggests-lower-output/ + +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 + +--- + +## Footnotes diff --git a/docs/GEOPHIRES-Examples.md b/docs/GEOPHIRES-Examples.md index 02ac82876..cb549abb9 100644 --- a/docs/GEOPHIRES-Examples.md +++ b/docs/GEOPHIRES-Examples.md @@ -7,4 +7,4 @@ or in the [web interface](https://gtp.scientificwebservices.com/geophires) under ## Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station -See [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-4.html). +See documentation: [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-5.html). diff --git a/docs/Monte-Carlo-User-Guide.md b/docs/Monte-Carlo-User-Guide.md index 816de8c76..54dc6b61e 100644 --- a/docs/Monte-Carlo-User-Guide.md +++ b/docs/Monte-Carlo-User-Guide.md @@ -1,4 +1,4 @@ -# GEOPHIRES Monte Carlo User Guide +# Monte Carlo User Guide ## Example Setup diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index b71470518..326679507 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -121,9 +121,9 @@ Output Parameters: ### Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station -[Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-4) +[Web interface link](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-5) -See [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-4.html). +Documentation: [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-4.html). ### SAM Single Owner PPA diff --git a/docs/_images/fervo_project_cape-5-net-power-production.png b/docs/_images/fervo_project_cape-5-net-power-production.png new file mode 100644 index 000000000..6d2f4ff2f Binary files /dev/null and b/docs/_images/fervo_project_cape-5-net-power-production.png differ diff --git a/docs/_images/fervo_project_cape-5-power-production.png b/docs/_images/fervo_project_cape-5-power-production.png new file mode 100644 index 000000000..2badf85fa Binary files /dev/null and b/docs/_images/fervo_project_cape-5-power-production.png differ diff --git a/docs/_images/fervo_project_cape-5-production-temperature.png b/docs/_images/fervo_project_cape-5-production-temperature.png new file mode 100644 index 000000000..d89f565ee Binary files /dev/null and b/docs/_images/fervo_project_cape-5-production-temperature.png differ diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.png b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.png new file mode 100644 index 000000000..018025c15 Binary files /dev/null and b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.png differ diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg new file mode 100644 index 000000000..f1ae7ba27 --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg @@ -0,0 +1,3127 @@ + + + + + + + + 2026-01-21T15:29:30.222583 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.png b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.png new file mode 100644 index 000000000..08c810a43 Binary files /dev/null and b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.png differ diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg new file mode 100644 index 000000000..e77355d3b --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg @@ -0,0 +1,3287 @@ + + + + + + + + 2026-01-21T15:29:30.766882 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.png b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.png new file mode 100644 index 000000000..3cdfcb370 Binary files /dev/null and b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.png differ diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg new file mode 100644 index 000000000..adeed0c0a --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg @@ -0,0 +1,3432 @@ + + + + + + + + 2026-01-21T15:29:30.186500 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/singh-et-al-2025_wht-700-ft-bench-spacing.png b/docs/_images/singh-et-al-2025_wht-700-ft-bench-spacing.png new file mode 100644 index 000000000..d027727ae Binary files /dev/null and b/docs/_images/singh-et-al-2025_wht-700-ft-bench-spacing.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 new file mode 100644 index 000000000..b9717a443 Binary files /dev/null and b/docs/_images/singh_et_al_base_simulation-production-temperature.png differ diff --git a/docs/conf.py b/docs/conf.py index 26674a995..4914aded6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.10.24' +version = release = '3.11.4' pygments_style = 'trac' templates_path = ['./templates'] @@ -38,6 +38,13 @@ html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False +# Add jQuery as the first script - ensures it's available for sidebar.js +html_js_files = [ + ( + 'https://code.jquery.com/jquery-3.7.1.min.js', + {'integrity': 'sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=', 'crossorigin': 'anonymous'}, + ), +] html_sidebars = { '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], } diff --git a/docs/index.rst b/docs/index.rst index b145a94c3..5461acb6a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,8 +8,8 @@ Contents overview Theoretical-Basis-for-GEOPHIRES GEOPHIRES-Examples - Monte-Carlo-User-Guide SAM-Economic-Models + Monte-Carlo-User-Guide How-to-extend-GEOPHIRES-X .. toctree:: diff --git a/docs/requirements.txt b/docs/requirements.txt index 1b8df5e70..0f4a325d8 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx>=1.3 sphinx-py3doc-enhanced-theme m2r2 +Jinja2 diff --git a/docs/templates/layout.html b/docs/templates/layout.html index 681f7aefb..024fe5301 100644 --- a/docs/templates/layout.html +++ b/docs/templates/layout.html @@ -1,10 +1,21 @@ {% extends "!layout.html" %} {%- block extrahead %} + + {% endblock %} diff --git a/docs/watch_docs.py b/docs/watch_docs.py deleted file mode 100755 index 130ef909e..000000000 --- a/docs/watch_docs.py +++ /dev/null @@ -1,80 +0,0 @@ -#!python - -import os -import subprocess -import time - - -def get_file_states(directory): - """ - Returns a dictionary of file paths and their modification times. - """ - states = {} - for root, _, files in os.walk(directory): - for filename in files: - # Ignore hidden files, temporary editor files, and this script itself - # fmt:off - if (filename.startswith('.') or - filename.endswith('~') or filename == os.path.basename(__file__)): # noqa: PTH119 - # fmt:on - continue - - filepath = os.path.join(root, filename) - - # Avoid watching build directories if they are generated inside docs/ - if '_build' in filepath or 'build' in filepath: - continue - - try: - states[filepath] = os.path.getmtime(filepath) # noqa: PTH204 - except OSError: - pass - return states - - -def main(): - # Determine paths relative to this script - script_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.dirname(script_dir) - - # Watch the directory where the script is located (docs/) - watch_dir = script_dir - - command = ['tox', '-e', 'docs'] - poll_interval = 2 # Seconds - - print(f"Watching '{watch_dir}' for changes...") - print(f"Project root determined as: '{project_root}'") - print(f"Command to run: {' '.join(command)}") - print('Press Ctrl+C to stop.') - - # Initial state - last_states = get_file_states(watch_dir) - - try: - while True: - time.sleep(poll_interval) - current_states = get_file_states(watch_dir) - - if current_states != last_states: - print('\n[Change Detected] Running docs build...') - - try: - # Run tox from the project root so it finds tox.ini - subprocess.run(command, cwd=project_root, check=False) # noqa: S603 - except FileNotFoundError: - print("Error: 'tox' command not found. Please ensure tox is installed.") - except Exception as e: - print(f'An error occurred: {e}') - - print(f"\nWaiting for further changes in '{watch_dir}'...") - - # Update state to the current state - last_states = get_file_states(watch_dir) - - except KeyboardInterrupt: - print('\nWatcher stopped.') - - -if __name__ == '__main__': - main() diff --git a/setup.py b/setup.py index 7aa1b7fe8..8d61f1693 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.10.24', + version='3.11.4', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_docs/.gitignore b/src/geophires_docs/.gitignore new file mode 100644 index 000000000..c0a8efa16 --- /dev/null +++ b/src/geophires_docs/.gitignore @@ -0,0 +1,2 @@ +*_data.csv +singh_et_al_reservoir_output.txt diff --git a/src/geophires_docs/__init__.py b/src/geophires_docs/__init__.py new file mode 100644 index 000000000..1851f5e73 --- /dev/null +++ b/src/geophires_docs/__init__.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +from geophires_x_client import GeophiresInputParameters + + +def _get_file_path(file_name) -> Path: + return Path(os.path.join(os.path.abspath(os.path.dirname(__file__)), file_name)) + + +def _get_project_root() -> Path: + return _get_file_path('../..') + + +def _get_fpc5_input_file_path(project_root: Path | None = None) -> Path: + if project_root is None: + project_root = _get_project_root() + return project_root / 'tests/examples/Fervo_Project_Cape-5.txt' + + +def _get_fpc5_result_file_path(project_root: Path | None = None) -> Path: + if project_root is None: + project_root = _get_project_root() + return project_root / 'tests/examples/Fervo_Project_Cape-5.out' + + +_PROJECT_ROOT: Path = _get_project_root() +_FPC5_INPUT_FILE_PATH: Path = _get_fpc5_input_file_path() +_FPC5_RESULT_FILE_PATH: Path = _get_fpc5_result_file_path() + + +def _get_logger(_name_: str) -> Any: + # TODO consolidate _get_logger methods into a commonly accessible utility + + # sh = logging.StreamHandler(sys.stdout) + # sh.setLevel(logging.INFO) + # sh.setFormatter(logging.Formatter(fmt='[%(asctime)s][%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) + # + # ret = logging.getLogger(__name__) + # ret.addHandler(sh) + # return ret + + # noinspection PyMethodMayBeStatic + class _PrintLogger: + def info(self, msg): + print(f'[INFO] {msg}') + + def error(self, msg): + print(f'[ERROR] {msg}') + + return _PrintLogger() + + +def _get_input_parameters_dict( # TODO consolidate with FervoProjectCape5TestCase._get_input_parameters + _params: GeophiresInputParameters, include_parameter_comments: bool = False, include_line_comments: bool = False +) -> dict[str, Any]: + comment_idx = 0 + ret: dict[str, Any] = {} + for line in _params.as_text().split('\n'): + parts = line.strip().split(', ') # TODO generalize for array-type params + field = parts[0].strip() + if len(parts) >= 2 and not field.startswith('#'): + fieldValue = parts[1].strip() + if include_parameter_comments and len(parts) > 2: + fieldValue += ', ' + (', '.join(parts[2:])).strip() + ret[field] = fieldValue.strip() + + if include_line_comments and field.startswith('#'): + ret[f'_COMMENT-{comment_idx}'] = line.strip() + comment_idx += 1 + + # TODO preserve newlines + + return ret diff --git a/src/geophires_docs/__main__.py b/src/geophires_docs/__main__.py new file mode 100644 index 000000000..545afd1aa --- /dev/null +++ b/src/geophires_docs/__main__.py @@ -0,0 +1,4 @@ +if __name__ == '__main__': + from geophires_docs import generate_fervo_project_cape_5_docs + + generate_fervo_project_cape_5_docs.generate_fervo_project_cape_5_docs() diff --git a/src/geophires_docs/generate_fervo_project_cape_5_docs.py b/src/geophires_docs/generate_fervo_project_cape_5_docs.py new file mode 100644 index 000000000..1c9f0fcb3 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_5_docs.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Any + +from geophires_docs import _FPC5_INPUT_FILE_PATH +from geophires_docs import _FPC5_RESULT_FILE_PATH +from geophires_docs import _PROJECT_ROOT +from geophires_docs import generate_fervo_project_cape_5_md +from geophires_docs.generate_fervo_project_cape_5_graphs import generate_fervo_project_cape_5_graphs +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters + +_SINGH_ET_AL_BASE_SIMULATION_PARAMETERS: dict[str, Any] = { + 'Number of Production Wells': 4, + 'Number of Injection Wells per Production Well': '1.2, -- The Singh et al. scenario has 4 producers and 6 injectors. ' + 'We model one fewer injector here to account for the combined injection rate being lower for ' + 'the higher bench separation cases.', + 'Maximum Drawdown': '1, -- Redrilling not modeled in Singh et al. scenario. ' + '(The equivalent GEOPHIRES simulation allows drawdown to reach up to 100% without triggering redrilling)', + 'Plant Lifetime': 15, +} + + +# fmt:off +def get_singh_et_al_base_simulation_result(base_input_params: GeophiresInputParameters) \ + -> tuple[GeophiresInputParameters,GeophiresXResult]: + singh_et_al_base_simulation_input_params = ImmutableGeophiresInputParameters( + from_file_path=base_input_params.as_file_path(), + params=_SINGH_ET_AL_BASE_SIMULATION_PARAMETERS, + ) + # fmt:on + + singh_et_al_base_simulation_result = GeophiresXClient().get_geophires_result( + singh_et_al_base_simulation_input_params + ) + + return singh_et_al_base_simulation_input_params, singh_et_al_base_simulation_result + + +def generate_fervo_project_cape_5_docs(): + input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=_FPC5_INPUT_FILE_PATH + ) + result = GeophiresXResult(_FPC5_RESULT_FILE_PATH) + + singh_et_al_base_simulation: tuple[GeophiresInputParameters,GeophiresXResult] = get_singh_et_al_base_simulation_result(input_params) + + generate_fervo_project_cape_5_graphs( + (input_params, result), + singh_et_al_base_simulation, + _PROJECT_ROOT / 'docs/_images' + ) + + generate_fervo_project_cape_5_md.generate_fervo_project_cape_5_md( + input_params, + result, + _SINGH_ET_AL_BASE_SIMULATION_PARAMETERS + ) + + +if __name__ == '__main__': + generate_fervo_project_cape_5_docs() diff --git a/src/geophires_docs/generate_fervo_project_cape_5_graphs.py b/src/geophires_docs/generate_fervo_project_cape_5_graphs.py new file mode 100644 index 000000000..945803af9 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_5_graphs.py @@ -0,0 +1,372 @@ +from __future__ import annotations + +import json +from math import ceil +from pathlib import Path +from typing import Any + +import numpy as np +from matplotlib import pyplot as plt +from pint.facets.plain import PlainQuantity + +from geophires_docs import _FPC5_INPUT_FILE_PATH +from geophires_docs import _FPC5_RESULT_FILE_PATH +from geophires_docs import _PROJECT_ROOT +from geophires_docs import _get_input_parameters_dict +from geophires_docs import _get_logger +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters + +_log = _get_logger(__name__) + +_YOE_LABEL = 'Years of Operation' + + +def _get_full_net_production_profile(input_and_result: tuple[GeophiresInputParameters, GeophiresXResult]): + return _get_full_profile(input_and_result, 'Net Electricity Production') + + +def _get_full_total_electricity_generation_profile(input_and_result: tuple[GeophiresInputParameters, GeophiresXResult]): + return _get_full_profile(input_and_result, 'Total Electricity Production') + + +def _get_full_production_temperature_profile(input_and_result: tuple[GeophiresInputParameters, GeophiresXResult]): + return _get_full_profile(input_and_result, 'Produced Temperature') + + +def _get_full_thermal_drawdown_profile(input_and_result: tuple[GeophiresInputParameters, GeophiresXResult]): + return _get_full_profile(input_and_result, 'Thermal Drawdown') + + +def _get_full_profile(input_and_result: tuple[GeophiresInputParameters, GeophiresXResult], profile_key: str): + input_params: GeophiresInputParameters = input_and_result[0] + result = GeophiresXClient().get_geophires_result(input_params) + + with open(result.json_output_file_path, encoding='utf-8') as f: + full_result_obj = json.load(f) + + net_gen_obj = full_result_obj[profile_key] + net_gen_obj_unit = net_gen_obj['CurrentUnits'].replace('CELSIUS', 'degC') + profile = [PlainQuantity(it, net_gen_obj_unit) for it in net_gen_obj['value']] + return profile + + +def _get_redrilling_event_indexes( + input_and_result: tuple[GeophiresInputParameters, GeophiresXResult], threshold_degc: float = 0.5 +) -> list[float]: + """ + Detect redrilling events from a production temperature profile. + + A redrilling event is identified when a datapoint's temperature is more than + `threshold_degc` higher than the previous datapoint (indicating a sudden temperature + recovery from drilling new wells). + + TODO include redrilling events in GEOPHIRES results so they don't need to be calculated here + + :param threshold_degc: Temperature increase threshold to detect redrilling (default 1.0°C) + + :return: List of fractional year positions where redrilling events occur (COD = Year 1) + """ + temperatures_celsius: list[float] = [ + it.to('degC').magnitude for it in _get_full_production_temperature_profile(input_and_result) + ] + + input_params_dict: dict[str, Any] = _get_input_parameters_dict(input_and_result[0]) + time_steps_per_year: int = int(input_params_dict['Time steps per year']) + + redrilling_positions = [] + + step_size = 6 + for i in range(ceil(time_steps_per_year * 0.25), len(temperatures_celsius) - (step_size - 1), step_size): + temp_increase = temperatures_celsius[i] - temperatures_celsius[i - step_size] + if temp_increase >= threshold_degc: + # The temperature jump is detected at index i, but the redrilling event + # occurred somewhere in the step range when the minimum was reached. + # Find the actual local minimum within the step range. + search_start = max(0, i - (2 * step_size - 1)) + search_end = i + 1 # exclusive + local_min_idx = search_start + min( + range(search_end - search_start), key=lambda offset: temperatures_celsius[search_start + offset] + ) + + # Convert to fractional year position (COD = Year 1) + year_position = 1 + local_min_idx / time_steps_per_year + redrilling_positions.append(year_position) + + return redrilling_positions + + +def generate_power_production_graph( + # result: GeophiresXResult, + input_and_result: tuple[GeophiresInputParameters, GeophiresXResult], + output_dir: Path, + filename: str = 'fervo_project_cape-5-power-production.png', +) -> str: + """ + Generate a graph of time vs net power production and save it to the output directory. + """ + _log.info('Generating power production graph...') + + profile = _get_full_net_production_profile(input_and_result) + total_generation_profile = _get_full_total_electricity_generation_profile(input_and_result) + time_steps_per_year = int(_get_input_parameters_dict(input_and_result[0])['Time steps per year']) + + # profile is a list of PlainQuantity values with time_steps_per_year datapoints per year + # Convert to numpy arrays for plotting + net_power = np.array([p.magnitude for p in profile]) + total_power = np.array([p.magnitude for p in total_generation_profile]) + + # Generate time values: each datapoint represents 1/time_steps_per_year of a year + # Cash flow year convention: COD = Year 1, so first datapoint is at year 1 + years = np.array([1 + i / time_steps_per_year for i in range(len(profile))]) + + # Create the figure + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot the data + ax.plot(years, total_power, color='#9933e6', linewidth=2, label='Total Electricity Production (gross generation)') + ax.plot(years, net_power, color='#3399e6', linewidth=2, label='Net Electricity Production (after parasitic losses)') + + # Set labels and title + ax.set_xlabel(_YOE_LABEL, fontsize=12) + ax.set_ylabel('Power Production (MW)', fontsize=12) + ax.set_title('Power Production Over Project Lifetime', fontsize=14) + + # Set axis limits + ax.set_xlim(years.min(), years.max()) + ax.set_ylim(480, 630) + + # Add horizontal reference lines + hline_x = 1.5 + ax.axhline(y=500, color='#e69500', linestyle='--', linewidth=1.5, alpha=0.8) + ax.text(hline_x, 498, 'PPA Minimum Production Requirement', ha='left', va='top', fontsize=9, color='#e69500') + + ax.axhline(y=600, color='#33a02c', linestyle='--', linewidth=1.5, alpha=0.8) + ax.text( + hline_x, + 602, + 'Nameplate capacity (combined capacity of individual ORCs)', + ha='left', + va='bottom', + fontsize=9, + color='#33a02c', + ) + + # Add grid for better readability + ax.grid(True, linestyle='--', alpha=0.7) + + # Add legend + ax.legend(loc='best') + + # Ensure the output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + # Save the figure + save_path = output_dir / filename + plt.savefig(save_path, dpi=150, bbox_inches='tight') + plt.close(fig) + + _log.info(f'✓ Generated {save_path}') + return filename + + +def generate_production_temperature_and_drawdown_graph( + input_and_result: tuple[GeophiresInputParameters, GeophiresXResult], + output_dir: Path, + filename: str = 'fervo_project_cape-5-production-temperature.png', +) -> str: + """ + Generate a graph of time vs production temperature with a horizontal line + showing the temperature threshold at which maximum drawdown is reached. + """ + _log.info('Generating production temperature graph...') + + temp_profile = _get_full_production_temperature_profile(input_and_result) + input_params_dict = _get_input_parameters_dict(input_and_result[0]) + time_steps_per_year = int(input_params_dict['Time steps per year']) + + # Get maximum drawdown from input parameters (as a decimal, e.g., 0.03 for 3%) + max_drawdown = float(input_params_dict.get('Maximum Drawdown')) + + # Convert to numpy arrays + temperatures_celsius = np.array([p.magnitude for p in temp_profile]) + + # Calculate the temperature at maximum drawdown threshold + # Drawdown = (T_initial - T_threshold) / T_initial + # So: T_threshold = T_initial * (1 - max_drawdown) + initial_temp = temperatures_celsius[0] + max_drawdown_temp = initial_temp * (1 - max_drawdown) + + # Generate time values: Cash flow year convention: COD = Year 1 + years = np.array([1 + i / time_steps_per_year for i in range(len(temp_profile))]) + + # Get redrilling event years + redrilling_years = _get_redrilling_event_indexes(input_and_result) + + # Colors + COLOR_TEMPERATURE = '#e63333' + COLOR_THRESHOLD = '#e69500' + COLOR_REDRILLING = '#3366cc' + + # Create the figure + fig, ax = plt.subplots(figsize=(10, 6)) + + ax.set_xlabel(_YOE_LABEL, fontsize=12) + ax.set_ylabel('Production Temperature (°C)', fontsize=12) + ax.set_xlim(years.min(), years.max()) + ax.set_ylim(200, 205) + + # Enable minor ticks on x-axis + ax.minorticks_on() + ax.tick_params(axis='x', which='minor', bottom=True) + ax.tick_params(axis='y', which='minor', left=False) + + # Add vertical lines for redrilling events + for i, redrill_year in enumerate(redrilling_years): + ax.axvline(x=redrill_year, color=COLOR_REDRILLING, linestyle=':', linewidth=1.5, alpha=0.7) + # Only add label for the first redrilling event to avoid legend clutter + if i == 0: + ax.text( + redrill_year + 0.3, + ax.get_ylim()[0] + 0.75, + f'Redrilling Events (n={len(redrilling_years)})', + ha='left', + va='top', + fontsize=9, + color=COLOR_REDRILLING, + ) + + # Add horizontal line for maximum drawdown threshold + ax.axhline(y=max_drawdown_temp, color=COLOR_THRESHOLD, linestyle='--', linewidth=1.5, alpha=0.8) + max_drawdown_pct = max_drawdown * 100 + ax.text( + years.max() * 0.98, + max_drawdown_temp - 0.25, + f'Redrilling Threshold ({max_drawdown_pct:.2f}% drawdown = {max_drawdown_temp:.1f}°C)', + ha='right', + va='top', + fontsize=9, + color=COLOR_THRESHOLD, + ) + + # Plot temperature last so it renders over threshold and redrilling lines + ax.plot(years, temperatures_celsius, color=COLOR_TEMPERATURE, linewidth=2, label='Production Temperature') + + # Title + ax.set_title('Production Temperature Over Project Lifetime', fontsize=14) + + # Add grid + ax.grid(True, linestyle='--', alpha=0.7) + + # Legend + ax.legend(loc='best') + + # Ensure the output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + # Save the figure + save_path = output_dir / filename + plt.savefig(save_path, dpi=150, bbox_inches='tight') + plt.close(fig) + + _log.info(f'✓ Generated {save_path}') + return filename + + +def generate_production_temperature_graph( + result: GeophiresXResult, output_dir: Path, filename: str = 'fervo_project_cape-5-production-temperature.png' +) -> str: + """ + Generate a graph of time vs production temperature and save it to the output directory. + """ + _log.info('Generating production temperature graph...') + + # Extract data from power generation profile + profile = result.power_generation_profile + headers = profile[0] + data = profile[1:] + + # Find the indices for YEAR and THERMAL DRAWDOWN columns + year_idx = headers.index('YEAR') + # Look for production temperature column - could be labeled differently + temp_idx = headers.index('GEOFLUID TEMPERATURE (degC)') + + # Extract years and temperature values + years = np.array([row[year_idx] for row in data]) + temperatures_celsius = np.array([row[temp_idx] for row in data]) + + # Convert Celsius to Fahrenheit + temperatures_fahrenheit = temperatures_celsius * 9 / 5 + 32 + + # Create the figure - taller than wide (portrait orientation) + fig, ax = plt.subplots(figsize=(6, 8)) + + # Plot the data - just the curve, no markers + ax.plot(years, temperatures_fahrenheit, color='#e63333', linewidth=2) + + # Set labels and title + ax.set_xlabel('Simulation time (Years)', fontsize=12) + ax.set_ylabel('Wellhead temperature (°F)', fontsize=12) + # ax.set_title('Production Temperature Over Project Lifetime', fontsize=14) + + # Set axis limits + ax.set_xlim(years.min(), years.max()) + ax.set_ylim(200, 450) + + # Set y-axis ticks every 50 degrees, with 400 explicitly labeled but not 200 or 450 + ax.set_yticks([250, 300, 350, 400]) + + # Add grid for better readability + ax.grid(True, linestyle='--', alpha=0.7) + + # Ensure the output directory exists + output_dir.mkdir(parents=True, exist_ok=True) + + # Save the figure + save_path = output_dir / filename + plt.savefig(save_path, dpi=150, bbox_inches='tight') + plt.close(fig) + + _log.info(f'✓ Generated {save_path}') + return filename + + +def generate_fervo_project_cape_5_graphs( + base_case: tuple[GeophiresInputParameters, GeophiresXResult], + singh_et_al_base_simulation: tuple[GeophiresInputParameters, GeophiresXResult], + output_dir: Path, +) -> None: + # base_case_input_params: GeophiresInputParameters = base_case[0] + # base_case_result: GeophiresXResult = base_case[1] + + generate_power_production_graph(base_case, output_dir) + generate_production_temperature_and_drawdown_graph(base_case, output_dir) + + if singh_et_al_base_simulation is not None: + singh_et_al_base_simulation_result: GeophiresXResult = singh_et_al_base_simulation[1] + + # generate_net_power_graph( + # singh_et_al_base_simulation_result, output_dir, + # filename='singh_et_al_base_simulation-net-power-production.png' + # ) + + generate_production_temperature_graph( + singh_et_al_base_simulation_result, + output_dir, + filename='singh_et_al_base_simulation-production-temperature.png', + ) + + +if __name__ == '__main__': + docs_dir = _PROJECT_ROOT / 'docs' + images_dir = docs_dir / '_images' + + input_params_: GeophiresInputParameters = ImmutableGeophiresInputParameters(from_file_path=_FPC5_INPUT_FILE_PATH) + + result_ = GeophiresXResult(_FPC5_RESULT_FILE_PATH) + + generate_fervo_project_cape_5_graphs( + (input_params_, result_), None, images_dir # TODO configure (for local development) + ) diff --git a/src/geophires_docs/generate_fervo_project_cape_5_md.py b/src/geophires_docs/generate_fervo_project_cape_5_md.py new file mode 100755 index 000000000..c7c3cdff0 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_5_md.py @@ -0,0 +1,599 @@ +#!python +""" +Script to generate Fervo_Project_Cape-5.md from its jinja template. +This ensures the markdown documentation stays in sync with actual GEOPHIRES results. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import numpy as np +from jinja2 import Environment +from jinja2 import FileSystemLoader +from pint.facets.plain import PlainQuantity + +from geophires_docs import _PROJECT_ROOT +from geophires_docs import _get_fpc5_input_file_path +from geophires_docs import _get_fpc5_result_file_path +from geophires_docs import _get_input_parameters_dict +from geophires_docs import _get_logger +from geophires_docs import _get_project_root +from geophires_x.GeoPHIRESUtils import is_int +from geophires_x.GeoPHIRESUtils import sig_figs +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters + +# Module-level variable to hold the current project root for schema access +_current_project_root: Path | None = None + +_log = _get_logger(__name__) +_NON_BREAKING_SPACE = '\xa0' + + +def _get_schema(schema_file_name: str) -> dict[str, Any]: + project_root = _current_project_root if _current_project_root is not None else _get_project_root() + schema_file = project_root / 'src/geophires_x_schema_generator' / schema_file_name + with open(schema_file, encoding='utf-8') as f: + return json.loads(f.read()) + + +def _get_geophires_request_schema() -> dict[str, Any]: + return _get_schema('geophires-request.json') + + +def _get_input_parameter_schema(param_name: str) -> dict[str, Any]: + return _get_geophires_request_schema()['properties'][param_name] + + +def _get_input_parameter_schema_type(param_name: str) -> dict[str, Any]: + return _get_input_parameter_schema(param_name)['type'] + + +def _get_input_parameter_category(param_name: str) -> str: + return _get_input_parameter_schema(param_name)['category'] + + +def _get_input_parameter_units(param_name: str) -> str | None: + unit = _get_geophires_request_schema()['properties'][param_name]['units'] + + if unit == '': + return 'dimensionless' + + return unit + + +def _get_geophires_result_schema() -> dict[str, Any]: + return _get_schema('geophires-result.json') + + +def _get_output_parameter_schema(param_name: str) -> dict[str, Any]: + categorized_schema: dict[str, dict[str, Any]] = _get_geophires_result_schema()['properties'] + + for _category, category_data in categorized_schema.items(): + if param_name in category_data['properties']: + return category_data['properties'][param_name] + + raise ValueError(f'Parameter "{param_name}" not found in GEOPHIRES result schema.') + + +def _get_output_parameter_description(param_name: str) -> str: + return _get_output_parameter_schema(param_name)['description'] + + +def _get_unit_display(parameter_units_from_schema: str) -> str: + if parameter_units_from_schema is None: + return '' + + display_unit_prefix = ( + ' ' + if not (parameter_units_from_schema and any(it in parameter_units_from_schema for it in ['%', 'USD', 'MUSD'])) + else '' + ) + display_unit = parameter_units_from_schema + for replacement in [ + ('kilometer', 'km'), + ('degC', '℃'), + ('meter', 'm'), + ('m**3', 'm³'), + ('m**2', 'm²'), + ('MUSD', 'M'), + ('USD', ''), + ]: + display_unit = display_unit.replace(replacement[0], replacement[1]) + + return f'{display_unit_prefix}{display_unit}' + + +def generate_fpc_reservoir_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + params_to_exclude = [ + 'Maximum Temperature', + 'Reservoir Porosity', + 'Reservoir Volume Option', + ] + + return get_fpc_category_parameters_table_md(input_params, 'Reservoir', params_to_exclude) + + +def generate_fpc_well_bores_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + return get_fpc_category_parameters_table_md( + input_params, + 'Well Bores', + parameters_to_exclude=['Number of Multilateral Sections'], + ) + + +def generate_fpc_surface_plant_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + return get_fpc_category_parameters_table_md( + input_params, + 'Surface Plant', + parameters_to_exclude=['End-Use Option', 'Construction Years'], + ) + + +def generate_fpc_construction_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + input_params_dict = _get_input_parameters_dict( + input_params, include_parameter_comments=True, include_line_comments=True + ) + schedule_param_name = 'Construction CAPEX Schedule' + construction_input_params = {} + for construction_param in ['Construction Years', schedule_param_name]: + construction_input_params[construction_param] = input_params_dict[construction_param] + + # Comment hardcoded here for now because handling of array parameters with comments might be buggy in client or + # web interface... + schedule_param_comment = ( + 'Array of fractions of overnight capital cost expenditure for each year, starting with ' + 'lower costs during initial years for exploration and increasing to higher costs during ' + 'later years as buildout progresses.' + ) + construction_input_params[schedule_param_name] = ( + f'{construction_input_params[schedule_param_name]}' f', -- {schedule_param_comment}' + ) + + return get_fpc_category_parameters_table_md( + ImmutableGeophiresInputParameters(params=construction_input_params), None + ) + + +def generate_fpc_economics_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + stim_cost_per_well_additional_display_data = f' baseline cost; ${_stim_costs_per_well_musd(result)}M all-in cost' + + drilling_cost_per_well_additional_display_data = ( + f' (Yields all-in cost of ' f'${sig_figs(_drilling_costs_per_well_musd(result),3)}M/well)' + ) + + # Doesn't seem to work as intended... + drilling_cost_per_well_additional_display_data = drilling_cost_per_well_additional_display_data.replace( + ' ', _NON_BREAKING_SPACE + ) + + return get_fpc_category_parameters_table_md( + input_params, + 'Economics', + parameters_to_exclude=[ + 'Ending Electricity Sale Price', + 'Electricity Escalation Start Year', + 'Construction CAPEX Schedule', + 'Time steps per year', + 'Print Output to Console', + ], + additional_display_data_by_param_name={ + 'Reservoir Stimulation Capital Cost per Production Well': stim_cost_per_well_additional_display_data, + 'Reservoir Stimulation Capital Cost per Injection Well': stim_cost_per_well_additional_display_data, + 'Well Drilling and Completion Capital Cost Adjustment Factor': drilling_cost_per_well_additional_display_data, + }, + ) + + +def get_fpc_category_parameters_table_md( + input_params: GeophiresInputParameters, + category_name: str | None, + parameters_to_exclude: list[str] | None = None, + additional_display_data_by_param_name: dict[str, str] | None = None, +) -> str: + if parameters_to_exclude is None: + parameters_to_exclude = [] + + if additional_display_data_by_param_name is None: + additional_display_data_by_param_name = {} + + input_params_dict = _get_input_parameters_dict( + input_params, include_parameter_comments=True, include_line_comments=True + ) + + # noinspection MarkdownIncorrectTableFormatting + table_md = f""" +| Parameter | Input{_NON_BREAKING_SPACE}Value | Comment | +|-------------------|-------------------------------------------|-------------| +""" + + table_entries = [] + for param_name, param_val_comment in input_params_dict.items(): + if param_name.startswith(('#', '_COMMENT-')): + continue + + if param_name in parameters_to_exclude: + continue + + category = _get_input_parameter_category(param_name) + if category_name is None or category == category_name: + param_val_comment_split = param_val_comment.split( + # ',', + ',' if _get_input_parameter_schema_type(param_name) != 'array' else ', ', + maxsplit=1, + ) + + param_val = param_val_comment_split[0] + + param_comment = ( + param_val_comment_split[1].replace('-- ', '') if len(param_val_comment_split) > 1 else ' .. N/A ' + ) + param_unit = _get_input_parameter_units(param_name) + if param_unit == 'dimensionless': + param_unit_display = '%' + param_val = sig_figs( + PlainQuantity(float(param_val), 'dimensionless').to('percent').magnitude, + 10, # trim floating point errors + ) + elif param_unit == 'USD/kWh': + price_unit = 'USD/MWh' + param_unit_display = _get_unit_display(price_unit) + param_val = sig_figs( + PlainQuantity(float(param_val), 'USD/kWh').to(price_unit).magnitude, + 10, # trim floating point errors + ) + elif ' ' in param_val: + param_val_split = param_val.split(' ', maxsplit=1) + param_val = param_val_split[0] + param_unit_display = _get_unit_display(param_val_split[1]) + else: + param_unit_display = _get_unit_display(param_unit) + + param_unit_display_prefix = '$' if param_unit and 'USD' in param_unit else '' + + if is_int(param_val): + param_val = int(param_val) + + param_schema = _get_input_parameter_schema(param_name) + if param_schema and 'enum_values' in param_schema: + for enum_value in param_schema['enum_values']: + if enum_value['int_value'] == param_val: + enum_display = enum_value['value'] + # param_val = f'{param_val} ({enum_display})' + param_val = enum_display + break + + param_name_display = param_name.replace(' ', _NON_BREAKING_SPACE, 2) + + additional_display_data = additional_display_data_by_param_name.get(param_name, '') + + table_entries.append( + [ + param_name_display, + f'{param_unit_display_prefix}{param_val}{param_unit_display}{additional_display_data}', + param_comment, + ] + ) + + for table_entry in table_entries: + table_md += f'| {table_entry[0]} | {table_entry[1]} | {table_entry[2]} |\n' + + return table_md.strip() + + +def _q(d: dict[str, Any]) -> PlainQuantity: + return PlainQuantity(d['value'], d['unit']) + + +def get_fpc5_input_parameter_values(input_params: GeophiresInputParameters, result: GeophiresXResult) -> dict[str, Any]: + _log.info('Extracting input parameter values...') + + params = _get_input_parameters_dict(input_params) + r: dict[str, dict[str, Any]] = result.result + + exploration_cost_musd = _q(r['CAPITAL COSTS (M$)']['Exploration costs']).to('MUSD').magnitude + assert exploration_cost_musd == float( + params['Exploration Capital Cost'] + ), 'Exploration cost mismatch between parameters and result' + + return { + 'exploration_cost_musd': round(sig_figs(exploration_cost_musd, 2)), + 'wacc_pct': sig_figs(r['ECONOMIC PARAMETERS']['WACC']['value'], 3), + 'reservoir_volume_m3': f"{r['RESERVOIR PARAMETERS']['Reservoir volume']['value']:,}", + } + + +def get_max_net_generation_mwe(result: GeophiresXResult) -> float: + r: dict[str, dict[str, Any]] = result.result + return _q(r['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Net Electricity Generation']).to('MW').magnitude + + +def get_result_values(result: GeophiresXResult) -> dict[str, Any]: + _log.info('Extracting result values...') + + r: dict[str, dict[str, Any]] = result.result + + econ = r['ECONOMIC PARAMETERS'] + + total_capex_q: PlainQuantity = _q(r['CAPITAL COSTS (M$)']['Total CAPEX']) + + surf_equip_sim = r['SURFACE EQUIPMENT SIMULATION RESULTS'] + min_net_generation_mwe = surf_equip_sim['Minimum Net Electricity Generation']['value'] + avg_net_generation_mwe = surf_equip_sim['Average Net Electricity Generation']['value'] + max_net_generation_mwe = get_max_net_generation_mwe(result) + max_total_generation_mwe = surf_equip_sim['Maximum Total Electricity Generation']['value'] + parasitic_loss_pct = ( + surf_equip_sim['Average Pumping Power']['value'] + / surf_equip_sim['Average Total Electricity Generation']['value'] + * 100.0 + ) + net_power_idx = result.power_generation_profile[0].index('NET POWER (MW)') + + def n_year_avg_net_power_mwe(years: int) -> float: + return np.average([it[net_power_idx] for it in result.power_generation_profile[1:]][:years]) + + two_year_avg_net_power_mwe = n_year_avg_net_power_mwe(2) + two_year_avg_net_power_mwe_per_production_well = two_year_avg_net_power_mwe / _number_of_production_wells(result) + + total_fracture_surface_area_per_well_m2 = _total_fracture_surface_area_per_well_m2(result) + + occ_q = _q(r['CAPITAL COSTS (M$)']['Overnight Capital Cost']) + + field_gathering_cost_musd = _q(r['CAPITAL COSTS (M$)']['Field gathering system costs']).to('MUSD').magnitude + field_gathering_cost_pct_occ = field_gathering_cost_musd / occ_q.to('MUSD').magnitude * 100.0 + + redrills = r['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + total_wells_including_redrilling = (1 + redrills) * _number_of_wells(result) + + return { + # Economic Results + 'lcoe_usd_per_mwh': sig_figs( + _q(r['SUMMARY OF RESULTS']['Electricity breakeven price']).to('USD / MWh').magnitude, 3 + ), + 'irr_pct': sig_figs(econ['After-tax IRR']['value'], 3), + 'operations_year_of_irr': econ['Project lifetime']['value'], + 'npv_musd': sig_figs(econ['Project NPV']['value'], 3), + 'project_moic': sig_figs(econ['Project MOIC']['value'], 3), + 'project_vir': sig_figs(econ['Project VIR=PI=PIR']['value'], 3), + # Capital Costs + 'drilling_costs_musd': round(sig_figs(_drilling_costs_musd(result), 3)), + 'drilling_costs_per_well_musd': sig_figs(_drilling_costs_per_well_musd(result), 3), + 'stim_costs_musd': round(sig_figs(_stim_costs_musd(result), 3)), + 'stim_costs_per_well_musd': sig_figs(_stim_costs_per_well_musd(result), 3), + 'surface_power_plant_costs_gusd': sig_figs( + _q(r['CAPITAL COSTS (M$)']['Surface power plant costs']).to('GUSD').magnitude, 3 + ), + 'field_gathering_cost_musd': round(sig_figs(field_gathering_cost_musd, 3)), + 'field_gathering_cost_pct_occ': round(sig_figs(field_gathering_cost_pct_occ, 1)), + 'occ_gusd': sig_figs(occ_q.to('GUSD').magnitude, 3), + 'total_capex_gusd': sig_figs(total_capex_q.to('GUSD').magnitude, 3), + 'capex_usd_per_kw': round( + sig_figs((total_capex_q / PlainQuantity(max_net_generation_mwe, 'MW')).to('USD / kW').magnitude, 2) + ), + # Technical & Engineering Results + 'bht_temp_degc': r['RESERVOIR PARAMETERS']['Bottom-hole temperature']['value'], + 'min_net_generation_mwe': round(sig_figs(min_net_generation_mwe, 3)), + 'avg_net_generation_mwe': round(sig_figs(avg_net_generation_mwe, 3)), + 'max_net_generation_mwe': round(sig_figs(max_net_generation_mwe, 3)), + 'max_total_generation_mwe': round(sig_figs(max_total_generation_mwe, 3)), + 'two_year_avg_net_power_mwe_per_production_well': sig_figs(two_year_avg_net_power_mwe_per_production_well, 2), + 'parasitic_loss_pct': sig_figs(parasitic_loss_pct, 3), + 'number_of_times_redrilling': redrills, + 'total_wells_including_redrilling': total_wells_including_redrilling, + 'initial_production_temperature_degc': round( + sig_figs(r['RESERVOIR SIMULATION RESULTS']['Initial Production Temperature']['value'], 3) + ), + 'average_production_temperature_degc': round( + sig_figs(r['RESERVOIR SIMULATION RESULTS']['Average Production Temperature']['value'], 3) + ), + 'total_fracture_surface_area_per_well_mm2': sig_figs(total_fracture_surface_area_per_well_m2 / 1e6, 2), + 'total_fracture_surface_area_per_well_mft2': round( + sig_figs( + PlainQuantity(total_fracture_surface_area_per_well_m2, 'm ** 2').to('foot ** 2').magnitude * 1e-6, 2 + ) + ), + # TODO port all input and result values here instead of hardcoding them in the template + } + + +def _number_of_production_wells(result: GeophiresXResult) -> int: + return result.result['SUMMARY OF RESULTS']['Number of production wells']['value'] + + +def _number_of_wells(result: GeophiresXResult) -> int: + r: dict[str, dict[str, Any]] = result.result + + number_of_wells = r['SUMMARY OF RESULTS']['Number of injection wells']['value'] + _number_of_production_wells( + result + ) + + return number_of_wells + + +def _drilling_costs_musd(result: GeophiresXResult) -> float: + r: dict[str, dict[str, Any]] = result.result + + return _q(r['CAPITAL COSTS (M$)']['Drilling and completion costs']).to('MUSD').magnitude + + +def _drilling_costs_per_well_musd(result: GeophiresXResult) -> float: + return _drilling_costs_musd(result) / _number_of_wells(result) + + +def _stim_costs_per_well_musd(result: GeophiresXResult) -> float: + stim_costs_per_well_musd = _stim_costs_musd(result) / _number_of_wells(result) + return stim_costs_per_well_musd + + +def _stim_costs_musd(result: GeophiresXResult) -> float: + r: dict[str, dict[str, Any]] = result.result + + stim_costs_musd = _q(r['CAPITAL COSTS (M$)']['Stimulation costs']).to('MUSD').magnitude + return stim_costs_musd + + +def _total_fracture_surface_area_per_well_m2(result: GeophiresXResult) -> float: + r: dict[str, dict[str, Any]] = result.result + res_params = r['RESERVOIR PARAMETERS'] + return ( + _q(res_params['Fracture area']).to('m ** 2').magnitude + * res_params['Number of fractures']['value'] + / _number_of_wells(result) + ) + + +def generate_res_eng_reference_sim_params_table_md( + base_case_input_params: GeophiresInputParameters, res_eng_reference_sim_params: dict[str, Any] +) -> str: + return get_fpc_category_parameters_table_md( + ImmutableGeophiresInputParameters( + # from_file_path=base_case_input_params.as_file_path(), + params=res_eng_reference_sim_params + ), + None, + ) + + +def generate_fpc_opex_output_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + table_md = """| Metric | Result Value | Reference Value(s) | Reference Source | +|-----|-----|-----|-----|\n""" + + for output_param_name, result_value_unit_dict in result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'].items(): + if result_value_unit_dict is None: + continue + + unit = result_value_unit_dict['unit'] + value_unit_display = ( + f'${result_value_unit_dict["value"]}M/yr' + if unit == 'MUSD/yr' + else f'{result_value_unit_dict["value"]} {unit}' + ) + + reference_value_display = '.. N/A' + + if output_param_name == 'Total operating and maintenance costs': + reference_source_display = '.. N/A ' + else: + reference_source_display = _get_output_parameter_description(output_param_name) + + if output_param_name == 'Water costs': + water_cost_adjustment_param_name = 'Water Cost Adjustment Factor' + reference_source_display = reference_source_display.split( + f'. Provide {water_cost_adjustment_param_name}', maxsplit=1 + )[0] + water_cost_adjustment_percent = ( + PlainQuantity( + float(_get_input_parameters_dict(input_params)[water_cost_adjustment_param_name]), + 'dimensionless', + ) + .to('percent') + .magnitude + ) + reference_source_display = ( + f'{reference_source_display}. ' + f'The default correlation is adjusted by the {water_cost_adjustment_param_name} parameter value ' + f'of {water_cost_adjustment_percent:.0f}%.' + ) + + if reference_source_display.startswith(('O&M', 'Total O&M')): + reference_source_display = reference_source_display.split('. ', maxsplit=1)[1] + + for suffix in ('s', ''): + reference_source_display = reference_source_display.replace(f'O&M cost{suffix}', 'OPEX') + + table_md += ( + f'| {output_param_name} | {value_unit_display} | {reference_value_display} | {reference_source_display} |\n' + ) + + if output_param_name == 'Total operating and maintenance costs': + opex_usd_per_kw_per_year = ( + _q(result_value_unit_dict) / PlainQuantity(get_max_net_generation_mwe(result), 'MW') + ).to('USD / year / kilowatt') + + reference_source = '2024b ATB: 2028 Deep EGS Binary Conservative Scenario (NREL, 2025). ' + # TODO explain why we're higher than ATB (e.g. redrilling not modeled by ATB) + + table_md += f'| {output_param_name}: $/kW-yr | ${opex_usd_per_kw_per_year.magnitude:.2f}/kW-yr | $226.31/kW-yr | {reference_source} |\n' + + return table_md + + +def generate_fervo_project_cape_5_md( + input_params: GeophiresInputParameters, + result: GeophiresXResult, + res_eng_reference_sim_params: dict[str, Any] | None = None, + project_root: Path = _PROJECT_ROOT, +) -> None: + if res_eng_reference_sim_params is None: + res_eng_reference_sim_params = {} + + result_values: dict[str, Any] = get_result_values(result) + + # noinspection PyDictCreation + template_values = {**get_fpc5_input_parameter_values(input_params, result), **result_values} + + for template_key, md_method in { + 'opex_result_outputs_table_md': generate_fpc_opex_output_table_md, + 'reservoir_parameters_table_md': generate_fpc_reservoir_parameters_table_md, + 'surface_plant_parameters_table_md': generate_fpc_surface_plant_parameters_table_md, + 'well_bores_parameters_table_md': generate_fpc_well_bores_parameters_table_md, + 'economics_parameters_table_md': generate_fpc_economics_parameters_table_md, + 'construction_parameters_table_md': generate_fpc_construction_parameters_table_md, + }.items(): + template_values[template_key] = md_method(input_params, result) + + template_values['reservoir_engineering_reference_simulation_params_table_md'] = ( + generate_res_eng_reference_sim_params_table_md(input_params, res_eng_reference_sim_params) + ) + + docs_dir = project_root / 'docs' + + # Set up Jinja environment + env = Environment(loader=FileSystemLoader(docs_dir), autoescape=True) + template = env.get_template('Fervo_Project_Cape-5.md.jinja') + + # Render template + _log.info('Rendering template...') + output = template.render(**template_values) + + # Write output + output_file = docs_dir / 'Fervo_Project_Cape-5.md' + output_file.write_text(output, encoding='utf-8') + + _log.info(f'✓ Generated {output_file}') + _log.info('\nKey results:') + _log.info(f"\tLCOE: ${template_values['lcoe_usd_per_mwh']}/MWh") + _log.info(f"\tIRR: {template_values['irr_pct']}%") + _log.info(f"\tTotal CAPEX: ${template_values['total_capex_gusd']}B") + + +def main(project_root: Path | None = None): + """ + Generate Fervo_Project_Cape-5.md (markdown documentation) from the Jinja template. + """ + global _current_project_root + + if project_root is None: + project_root = _get_project_root() + + _current_project_root = project_root + + input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=_get_fpc5_input_file_path(project_root) + ) + result = GeophiresXResult(_get_fpc5_result_file_path(project_root)) + generate_fervo_project_cape_5_md(input_params, result, project_root=project_root) + + +if __name__ == '__main__': + main() diff --git a/src/geophires_docs/watch_docs.py b/src/geophires_docs/watch_docs.py new file mode 100755 index 000000000..941f4a6e1 --- /dev/null +++ b/src/geophires_docs/watch_docs.py @@ -0,0 +1,114 @@ +#!python +# Automatically rebuilds docs locally when changes are detected. +# Usage, from the project root: +# ./src/geophires_docs/watch_docs.py + +import argparse +import os +import subprocess +import time +from pathlib import Path +from typing import Any + +from geophires_docs import _get_logger + +_log = _get_logger(__name__) + + +def get_file_states(directory) -> dict[str, Any]: + """ + Returns a dictionary of file paths and their modification times. + """ + states = {} + for root, _, files in os.walk(directory): + for filename in files: + # Ignore hidden files, temporary editor files, and this script itself + # fmt:off + if (filename.startswith('.') or + filename.endswith('~') or filename == os.path.basename(__file__)): # noqa: PTH119 + # fmt:on + continue + + filepath = os.path.join(root, filename) + + # Avoid watching build directories if they are generated inside docs/ + if '_build' in filepath or 'build' in filepath: + continue + + try: + states[filepath] = os.path.getmtime(filepath) # noqa: PTH204 + except OSError: + pass + return states + + +def main(): + parser = argparse.ArgumentParser(description='Automatically rebuilds docs locally when changes are detected.') + parser.add_argument('--no-say', action='store_true', help='Disable audio notifications via the say command') + args = parser.parse_args() + + # Determine paths relative to this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root: str = Path(__file__).parent.parent.parent + + def _say(msg) -> None: + if args.no_say: + return + try: + subprocess.run(['say', msg], cwd=project_root, check=False) # noqa: S603,S607 + except subprocess.CalledProcessError: + pass + + # Watch the directory where the script is located (docs/) + watch_dirs = [script_dir, Path(project_root) / 'docs', Path(project_root) / 'tests' / 'examples'] + + command = ['tox', '-e', 'docs'] + poll_interval = 2 # Seconds + + _log.info(f"Watching '{watch_dirs}' for changes...") + _log.info(f"Project root determined as: '{project_root}'") + _log.info(f"Command to run: {' '.join(command)}") + _log.info('Press Ctrl+C to stop.') + + def _get_file_states() -> dict: + states = {} + for watch_dir in watch_dirs: + states = {**states, **get_file_states(watch_dir)} + + return states + + # Initial state + last_states = _get_file_states() + + try: + while True: + time.sleep(poll_interval) + current_states = _get_file_states() + + if current_states != last_states: + _log.info('[Change Detected] Running docs build...') + time.sleep(1) + + try: + # Run tox from the project root so it finds tox.ini + subprocess.run(command, cwd=project_root, check=False) # noqa: S603 + except FileNotFoundError: + _log.error("Error: 'tox' command not found. Please ensure tox is installed.") + except Exception as e: + _log.error(f'An error occurred: {e}') + _say('error rebuilding docs') + + print('\n') + _log.info(f"Docs rebuild complete at {time.strftime('%Y-%m-%d %H:%M:%S')}.") + _say('docs rebuilt') + _log.info(f"Waiting for further changes in '{watch_dirs}'...") + + # Update state to the current state + last_states = _get_file_states() + + except KeyboardInterrupt: + _log.info('Watcher stopped.') + + +if __name__ == '__main__': + main() diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index f47b02d5c..762d3ea49 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -15,7 +15,7 @@ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \ interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \ overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \ - _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET + _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation @@ -1011,9 +1011,9 @@ def __init__(self, model: Model): 'Royalty Rate Escalation Start Year', DefaultValue=1, AllowableRange=list(range(1, model.surfaceplant.plant_lifetime.AllowableRange[-1], 1)), - UnitType=Units.PERCENT, - PreferredUnits=PercentUnit.TENTH, - CurrentUnits=PercentUnit.TENTH, + UnitType=Units.NONE, + PreferredUnits=TimeUnit.YEAR, + CurrentUnits=TimeUnit.YEAR, ToolTipText=f'The first year that the {self.royalty_escalation_rate.Name} is applied. ' f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}.' ) @@ -2302,13 +2302,7 @@ def __init__(self, model: Model): self.ProjectMOIC = self.OutputParameterDict[self.ProjectMOIC.Name] = moic_parameter() self.ProjectPaybackPeriod = self.OutputParameterDict[self.ProjectPaybackPeriod.Name] = ( project_payback_period_parameter()) - self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter( - Name="Investment Tax Credit Value", - display_name='Investment Tax Credit', - UnitType=Units.CURRENCY, - PreferredUnits=CurrencyUnit.MDOLLARS, - CurrentUnits=CurrencyUnit.MDOLLARS - ) + self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = investment_tax_credit_output_parameter() self.cost_one_production_well = self.OutputParameterDict[self.cost_one_production_well.Name] = OutputParameter( Name="Cost of One Production Well", UnitType=Units.CURRENCY, @@ -3585,9 +3579,11 @@ def _calculate_sam_economics(self, model: Model) -> None: self.ProjectMOIC.value = self.sam_economics_calculations.moic.value self.ProjectVIR.value = self.sam_economics_calculations.project_vir.value - # TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 self.ProjectPaybackPeriod.value = self.sam_economics_calculations.project_payback_period.value + self.RITCValue.value = self.sam_economics_calculations.investment_tax_credit.quantity().to( + self.RITCValue.CurrentUnits).magnitude + # noinspection SpellCheckingInspection def _calculate_derived_outputs(self, model: Model) -> None: """ diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 419d04094..450def3b9 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -24,6 +24,7 @@ # noinspection PyPackageRequirements import PySAM.Utilityrate5 as UtilityRate +from pint.facets.plain import PlainQuantity from tabulate import tabulate from geophires_x import Model as Model @@ -43,6 +44,7 @@ royalty_cost_output_parameter, overnight_capital_cost_output_parameter, _SAM_EM_MOIC_RETURNS_TAX_QUALIFIER, + investment_tax_credit_output_parameter, ) from geophires_x.EconomicsSamPreRevenue import ( _AFTER_TAX_NET_CASH_FLOW_ROW_NAME, @@ -96,7 +98,8 @@ class SamEconomicsCalculations: project_vir: OutputParameter = field(default_factory=project_vir_parameter) project_payback_period: OutputParameter = field(default_factory=project_payback_period_parameter) - """TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413""" + + investment_tax_credit: OutputParameter = field(default_factory=investment_tax_credit_output_parameter) @property def _pre_revenue_years_count(self) -> int: @@ -368,6 +371,11 @@ def sf(_v: float, num_sig_figs: int = 5) -> float: sam_economics.project_payback_period.value = _calculate_project_payback_period( sam_economics.sam_cash_flow_profile, model ) + sam_economics.investment_tax_credit.value = ( + _calculate_investment_tax_credit_value(sam_economics.sam_cash_flow_profile) + .to(sam_economics.investment_tax_credit.CurrentUnits.value) + .magnitude + ) return sam_economics @@ -522,21 +530,43 @@ def _calculate_project_vir(cash_flow: list[list[Any]], model: Model) -> float: def _calculate_project_payback_period(cash_flow: list[list[Any]], model) -> float | None: """ - TODO remove or clarify project payback period: https://github.com/NREL/GEOPHIRES-X/issues/413 - """ + Calculates the Simple Payback Period (SPB). + SPB is the time required for the cumulative non-discounted after-tax net cash flow to turn positive. + The calculation assumes annual cash flows. The returned value represents the number of years + from the start of the provided cash flow list until the investment is recovered. + """ try: + # Get flattened annual after-tax cash flow after_tax_cash_flow = _after_tax_net_cash_flow_all_years(cash_flow, _pre_revenue_years_count(model)) - cumm_cash_flow = np.zeros(len(after_tax_cash_flow)) - cumm_cash_flow[0] = after_tax_cash_flow[0] - for year in range(1, len(after_tax_cash_flow)): - cumm_cash_flow[year] = cumm_cash_flow[year - 1] + after_tax_cash_flow[year] - if cumm_cash_flow[year] >= 0: - year_before_full_recovery = year - 1 - payback_period = ( - year_before_full_recovery - + abs(cumm_cash_flow[year_before_full_recovery]) / after_tax_cash_flow[year] - ) + + cumulative_cash_flow = np.zeros(len(after_tax_cash_flow)) + cumulative_cash_flow[0] = after_tax_cash_flow[0] + + # Handle edge case where the first year is already positive + if cumulative_cash_flow[0] >= 0: + # If the project is profitable immediately (rare for SPB), return 0 or fraction. + # For standard SPB logic where Index 0 is an investment year, this is an edge case. + pass + + for year_index in range(1, len(after_tax_cash_flow)): + cumulative_cash_flow[year_index] = cumulative_cash_flow[year_index - 1] + after_tax_cash_flow[year_index] + + if cumulative_cash_flow[year_index] >= 0: + # Payback occurred in this year (year_index). + # We need to calculate how far into this year the break-even point occurred. + + previous_year_index = year_index - 1 + unrecovered_cost_at_start_of_year = abs(cumulative_cash_flow[previous_year_index]) + cash_flow_in_current_year = after_tax_cash_flow[year_index] + + # Fraction of the current year required to recover the remaining cost + fraction_of_year = unrecovered_cost_at_start_of_year / cash_flow_in_current_year + + # Total years elapsed = Full years prior to this one + fraction of this one. + # If we are at year_index, the number of full years passed is equal to year_index. + # Example: If year_index is 5 (6th year), 5 full years (Indices 0..4) have passed. + payback_period = year_index + fraction_of_year return float(payback_period) @@ -546,6 +576,19 @@ def _calculate_project_payback_period(cash_flow: list[list[Any]], model) -> floa return None +def _calculate_investment_tax_credit_value(sam_cash_flow_profile) -> PlainQuantity: + total_itc_sum_q: PlainQuantity = quantity(0, 'USD') + + for itc_line_item in ['Federal ITC total income ($)', 'State ITC total income ($)']: + itc_numeric_entries = [ + float(it) for it in _cash_flow_profile_row(sam_cash_flow_profile, itc_line_item) if is_float(it) + ] + itc_sum_q = quantity(sum(itc_numeric_entries), 'USD') + total_itc_sum_q += itc_sum_q + + return total_itc_sum_q + + def get_sam_cash_flow_profile_tabulated_output(model: Model, **tabulate_kw_args) -> str: """ Note model must have already calculated economics for this to work (used in Outputs) diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 78614f491..9d494b865 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -71,7 +71,15 @@ def project_vir_parameter() -> OutputParameter: UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, - ToolTipText='For SAM Economic Models, VIR = PV(Returns) / abs(PV(Investment)).', + ToolTipText="Value Investment Ratio (VIR). " + "VIR is frequently referred to interchangeably as Profitability Index (PI) or " + "Profit Investment Ratio (PIR) in financial literature. " + "All three terms describe the same fundamental ratio: the present value of future cash flows " + "divided by the initial investment. " + "For SAM Economic Models, this metric is calculated as the Levered Equity Profitability Index. " + "It is calculated as the Present Value of After-Tax Equity Cash Flows (Returns) divided by the " + "Present Value of Equity Invested. It measures the efficiency of the sponsor's specific capital " + "contribution, accounting for leverage.", ) @@ -83,7 +91,10 @@ def project_payback_period_parameter() -> OutputParameter: CurrentUnits=TimeUnit.YEAR, ToolTipText='The time at which cumulative cash flow reaches zero. ' 'For projects that never pay back, the calculated value will be "N/A". ' - 'For SAM Economic Models, after-tax net cash flow is used to calculate the cumulative cash flow.', + 'For SAM Economic Models, this is Simple Payback Period (SPB): the time at which cumulative non-discounted ' + 'cash flow reaches zero, calculated using non-discounted after-tax net cash flow. ' + 'See https://samrepo.nrelcloud.org/help/mtf_payback.html for important considerations regarding the ' + 'limitations of this metric.', ) @@ -195,3 +206,16 @@ def royalty_cost_output_parameter() -> OutputParameter: ToolTipText='The annual costs paid to a royalty holder, calculated as a percentage of the ' 'project\'s gross annual revenue. This is modeled as a variable operating expense.', ) + + +def investment_tax_credit_output_parameter() -> OutputParameter: + return OutputParameter( + Name="Investment Tax Credit Value", + display_name='Investment Tax Credit', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + ToolTipText='Represents the total undiscounted ITC sum. ' + 'For SAM Economic Models, this accounts for the standard Year 1 Federal ITC as well as any ' + 'applicable State ITCs or multi-year credit schedules.', + ) diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 98bc6a680..a20c6ae23 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -266,6 +266,12 @@ def PrintOutputs(self, model: Model): label = Outputs._field_label(field.Name, 49) f.write(f' {label}{field.value:10.2f} {field.CurrentUnits.value}\n') + if econ.RITCValue.value and is_sam_econ_model: + # Non-SAM-EMs (inaccurately) treat ITC as a capital cost and thus are displayed in the capital + # costs category rather than here. + f.write( + f' {econ.RITCValue.display_name}: {abs(econ.RITCValue.value):10.2f} {econ.RITCValue.CurrentUnits.value}\n') + if not is_sam_econ_model: # (parameter is ambiguous to the point of meaninglessness for SAM-EM) acf: OutputParameter = econ.accrued_financing_during_construction_percentage acf_label = Outputs._field_label(acf.display_name, 49) @@ -346,15 +352,15 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***RESOURCE CHARACTERISTICS***\n') f.write(NL) - f.write(f' Maximum reservoir temperature: {model.reserv.Tmax.value:10.1f} ' + model.reserv.Tmax.CurrentUnits.value + NL) - f.write(f' Number of segments: {model.reserv.numseg.value:10.0f} ' + NL) + f.write(f' Maximum reservoir temperature: {model.reserv.Tmax.value:10.1f} {model.reserv.Tmax.CurrentUnits.value}\n') + f.write(f' Number of segments: {model.reserv.numseg.value:10.0f}\n') if model.reserv.numseg.value == 1: - f.write(f' Geothermal gradient: {model.reserv.gradient.value[0]:10.4g} ' + model.reserv.gradient.CurrentUnits.value + NL) + f.write(f' Geothermal gradient: {model.reserv.gradient.value[0]:10.4g} {model.reserv.gradient.CurrentUnits.value}\n') else: for i in range(1, model.reserv.numseg.value): - f.write(f' Segment {str(i):s} Geothermal gradient: {model.reserv.gradient.value[i-1]:10.4g} ' + model.reserv.gradient.CurrentUnits.value +NL) + f.write(f' Segment {str(i):s} Geothermal gradient: {model.reserv.gradient.value[i-1]:10.4g} {model.reserv.gradient.CurrentUnits.value}\n') f.write(f' Segment {str(i):s} Thickness: {round(model.reserv.layerthickness.value[i-1], 10)} {model.reserv.layerthickness.CurrentUnits.value}\n') - f.write(f' Segment {str(i+1):s} Geothermal gradient: {model.reserv.gradient.value[i]:10.4g} ' + model.reserv.gradient.CurrentUnits.value + NL) + f.write(f' Segment {str(i+1):s} Geothermal gradient: {model.reserv.gradient.value[i]:10.4g} {model.reserv.gradient.CurrentUnits.value}\n') f.write(NL) f.write(NL) @@ -495,17 +501,9 @@ def PrintOutputs(self, model: Model): f.write(f' Drilling and completion costs per redrilled well: {(econ.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value)):10.2f} {econ.Cwell.CurrentUnits.value}\n') f.write(f' Stimulation costs (for redrilling): {econ.Cstim.value:10.2f} {econ.Cstim.CurrentUnits.value}\n') - if model.economics.RITCValue.value: - if not is_sam_econ_model: - f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') - else: - # TODO Extract value from SAM Cash Flow Profile per - # https://github.com/NREL/GEOPHIRES-X/issues/404. - # For now we skip displaying the value because it can be/probably is usually mathematically - # inaccurate, and even if it's not, it's redundant with the cash flow profile and also - # misleading/confusing/wrong to display it as a capital cost since it is not a capital - # expenditure. - pass + if model.economics.RITCValue.value and not is_sam_econ_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') display_occ_and_inflation_during_construction_in_capital_costs = is_sam_econ_model if display_occ_and_inflation_during_construction_in_capital_costs: @@ -909,15 +907,15 @@ def get_sam_cash_flow_profile_output(self, model): # number that results in a separator line at least as wide as the table (narrower would be unsightly). spaces_per_tab = 4 - # The tabluate library has native separating line functionality (per https://pypi.org/project/tabulate/) but + # The tabulate library has native separating line functionality (per https://pypi.org/project/tabulate/) but # I wasn't able to get it to replicate the formatting as coded below. - separator_line = len(cfp_o.split('\n')[0].replace('\t',' ' * spaces_per_tab)) * '-' + separator_line = len(cfp_o.split('\n')[0].replace('\t', ' ' * spaces_per_tab)) * '-' ret += separator_line + '\n' ret += cfp_o ret += '\n' + separator_line - ret += '\n\n' + ret += '\n' return ret diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index 3eea04cec..639769d34 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -284,7 +284,9 @@ def __init__(self, model: Model): PreferredUnits=LengthUnit.METERS, CurrentUnits=LengthUnit.METERS, ErrMessage="assume default fracture width (500 m)", - ToolTipText="Width of each fracture" + ToolTipText="Total horizontal length of each fracture plane (from tip to tip). " + "Note: In some contexts this is called 'Fracture Length'; it refers to the fracture's lateral " + "extent, not its aperture or thickness." ) fracnumb_allowable_range = list(range(1, _MAX_ALLOWED_FRACTURES + 1, 1)) diff --git a/src/geophires_x/SurfacePlant.py b/src/geophires_x/SurfacePlant.py index b12f9ed5a..f3cb8b6d9 100644 --- a/src/geophires_x/SurfacePlant.py +++ b/src/geophires_x/SurfacePlant.py @@ -538,10 +538,11 @@ def __init__(self, model: Model): CurrentUnits=EnergyFrequencyUnit.KWPERYEAR ) self.ElectricityProduced = self.OutputParameterDict[self.ElectricityProduced.Name] = OutputParameter( - Name="Total Electricity Generation", + Name="Total Electricity Production", UnitType=Units.POWER, PreferredUnits=PowerUnit.MW, CurrentUnits=PowerUnit.MW + # TODO tooltip text - should reference that this is gross production ) self.NetElectricityProduced = self.OutputParameterDict[self.NetElectricityProduced.Name] = OutputParameter( Name="Net Electricity Production", diff --git a/src/geophires_x/SurfacePlantSupercriticalORC.py b/src/geophires_x/SurfacePlantSupercriticalORC.py index e418165d2..7f971096b 100644 --- a/src/geophires_x/SurfacePlantSupercriticalORC.py +++ b/src/geophires_x/SurfacePlantSupercriticalORC.py @@ -14,7 +14,7 @@ def __init__(self, model: Model): :return: None """ - model.logger.info("Init " + self.__class__.__name__ + ": " + __name__) + model.logger.info(f'Init {self.__class__.__name__}: {__name__}') super().__init__(model) # Initialize all the parameters in the superclass # Set up all the Parameters that will be predefined by this class using the different types of parameter classes. @@ -33,7 +33,7 @@ def __init__(self, model: Model): sclass = self.__class__.__name__ self.MyClass = sclass self.MyPath = __file__ - model.logger.info("Complete " + self.__class__.__name__ + ": " + __name__) + model.logger.info(f"Complete {self.__class__.__name__}: {__name__}") def __str__(self): return "SurfacePlantSupercriticalORC" diff --git a/src/geophires_x/UPPReservoir.py b/src/geophires_x/UPPReservoir.py index 0ba0040da..3717c13ee 100644 --- a/src/geophires_x/UPPReservoir.py +++ b/src/geophires_x/UPPReservoir.py @@ -83,23 +83,19 @@ def Calculate(self, model: Model): with open(model.reserv.filenamereservoiroutput.value, encoding='UTF-8') as f: contentprodtemp = f.readlines() except: - model.logger.critical('Error: GEOPHIRES could not read reservoir output file (' - + model.reserv.filenamereservoiroutput.value+') and will abort simulation.') - print('Error: GEOPHIRES could not read reservoir output file (' + msg = ('Error: GEOPHIRES could not read reservoir output file (' + model.reserv.filenamereservoiroutput.value+') and will abort simulation.') - sys.exit() + model.logger.critical(msg) + raise RuntimeError(msg) numlines = len(contentprodtemp) if numlines != model.surfaceplant.plant_lifetime.value*model.economics.timestepsperyear.value+1: - model.logging.critical('Error: Reservoir output file (' + msg = ('Error: Reservoir output file (' + model.reserv.filenamereservoiroutput.value + ') does not have required ' + str(model.surfaceplant.plant_lifetime.value * model.economics.timestepsperyear.value + 1) + ' lines. GEOPHIRES will abort simulation.') - print('Error: Reservoir output file (' + - model.reserv.filenamereservoiroutput.value +') does not have required ' + - str(model.surfaceplant.plant_lifetime.value * model.economics.timestepsperyear.value + 1) + - ' lines. GEOPHIRES will abort simulation.') - sys.exit() + model.logger.critical(msg) + raise RuntimeError(msg) for i in range(0, numlines-1): model.reserv.Tresoutput.value[i] = float(contentprodtemp[i].split(',')[1].strip('\n')) diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index 9e6e9249e..94b14e28d 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -520,11 +520,12 @@ def ProdPressureDropAndPumpingPowerUsingIndexes( else: Pprodwellhead = ppwellhead_kPa if Pprodwellhead < Pminimum_kPa: - Pprodwellhead = Pminimum_kPa - msg = (f'Provided production wellhead pressure ({Pprodwellhead:.2f} kPa) ' - f'under minimum pressure ({Pminimum_kPa:.2f} kPa). ' + msg = (f'Provided production wellhead pressure ({Pprodwellhead:.3f} kPa) ' + f'under minimum pressure ({Pminimum_kPa:.3f} kPa). ' f'GEOPHIRES will assume minimum wellhead pressure.') + Pprodwellhead = Pminimum_kPa + print(f'Warning: {msg}') model.logger.warning(msg) @@ -740,6 +741,18 @@ def __init__(self, model: Model): ToolTipText="Pass this parameter to set the Number of Production Wells and Number of Injection Wells to " "same value." ) + # noinspection SpellCheckingInspection + self.ninj_per_production_well = self.ParameterDict[self.ninj_per_production_well.Name] = floatParameter( + "Number of Injection Wells per Production Well", + DefaultValue=1, + Min=0, + Max=max_doublets-1, + UnitType=Units.NONE, + Required=False, + ToolTipText="Number of (identical) injection wells per production well. " + "For example, provide 0.666 to specify a 3:2 production:injection well ratio. " + "The number of injection wells will be rounded up to the nearest integer." + ) # noinspection SpellCheckingInspection self.prodwelldiam = self.ParameterDict[self.prodwelldiam.Name] = floatParameter( @@ -1140,8 +1153,20 @@ def __init__(self, model: Model): ) self.redrill = self.OutputParameterDict[self.redrill.Name] = OutputParameter( Name="redrill", + # TODO pivot Name to more user-friendly display name in all contexts if feasible (such as output parameters + # documentation, where Name is prepended to ToolTipText, resulting in the documentation entry beginning + # clumsily and confusingly with "redrill. The total number of redrilling events [...]") display_name='Number of times redrilling', - UnitType=Units.NONE + UnitType=Units.NONE, + ToolTipText="The total number of redrilling events triggered when production temperature drops below " + "the 'Maximum Drawdown' threshold. In the reservoir simulation, this resets the thermal " + "decline curve by repeating the initial production temperature profile. Economically, each " + "event incurs the full cost of drilling and stimulating the wellfield. " + "The cost of all redrilling events is summed and amortized over the project lifetime as an " + "operational expense. " + # "This accounts for the heavy capital expenditure (e.g., sidetracking and stimulating " + # "laterals into fresh rock) required to access undepleted reservoir volume and sustain " + # "target power output." ) self.PumpingPowerProd = self.OutputParameterDict[self.PumpingPowerProd.Name] = OutputParameter( Name="PumpingPowerProd", @@ -1228,7 +1253,7 @@ def __init__(self, model: Model): PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL ) - self.NonverticalProducedTemperature = self.OutputParameterDict[self.ProducedTemperature.Name] = OutputParameter( + self.NonverticalProducedTemperature = self.OutputParameterDict[self.NonverticalProducedTemperature.Name] = OutputParameter( Name="Nonvertical Produced Temperature", value=[0.0], UnitType=Units.TEMPERATURE, @@ -1361,20 +1386,35 @@ def read_parameters(self, model: Model) -> None: coerce_int_params_to_enum_values(self.ParameterDict) - if self.doublets_count.Provided: - def _error(num_wells_param_:intParameter): - msg = f'{num_wells_param_.Name} may not be provided when {self.doublets_count.Name} is provided.' - model.logger.error(msg) - raise ValueError(msg) + self._set_well_counts_from_parameters(model) + + model.logger.info(f"read parameters complete {self.__class__.__name__}: {__name__}") + + def _set_well_counts_from_parameters(self, model: Model): + mutually_exclusive_well_count_params = [self.doublets_count, self.ninj_per_production_well] + provided_well_count_params = [it for it in mutually_exclusive_well_count_params if it.Provided] + if len(provided_well_count_params) > 1: + raise ValueError(f'Only one of [{", ".join([it.Name for it in mutually_exclusive_well_count_params])}] ' + f'may be provided.') + def _raise_incompatible_param_error(incompatible_param: intParameter, with_param: intParameter): + msg = f'{incompatible_param.Name} may not be provided when {with_param.Name} is provided.' + model.logger.error(msg) + raise ValueError(msg) + + if self.doublets_count.Provided: for num_wells_param in [self.ninj, self.nprod]: if num_wells_param.Provided: - _error(num_wells_param) + _raise_incompatible_param_error(num_wells_param, self.doublets_count) self.ninj.value = self.doublets_count.value self.nprod.value = self.doublets_count.value - model.logger.info(f"read parameters complete {self.__class__.__name__}: {__name__}") + if self.ninj_per_production_well.Provided: + if self.ninj.Provided: + _raise_incompatible_param_error(self.ninj, self.ninj_per_production_well) + + self.ninj.value = int(math.ceil(self.nprod.value * self.ninj_per_production_well.value)) def Calculate(self, model: Model) -> None: """ diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 8a9ccd93e..f4e8eab8d 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.10.24' +__version__ = '3.11.4' diff --git a/src/geophires_x_client/geophires_x_result.py b/src/geophires_x_client/geophires_x_result.py index 91b14c26c..d96393cd9 100644 --- a/src/geophires_x_client/geophires_x_result.py +++ b/src/geophires_x_client/geophires_x_result.py @@ -81,6 +81,8 @@ class GeophiresXResult: 'Accrued financing during construction', # Displayed for economic models that don't treat inflation costs as capital costs (non-SAM-EM) 'Inflation costs during construction', + # Displayed as economic parameter for SAM-EM (non-SAM-EMs treat as capital cost) + 'Investment Tax Credit', 'Project lifetime', 'Capacity factor', 'Project NPV', @@ -717,7 +719,7 @@ def _get_unlabeled_string_field( return None @property - def power_generation_profile(self): + def power_generation_profile(self) -> list[list[str | float]]: return self.result['POWER GENERATION PROFILE'] def _get_power_generation_profile(self): diff --git a/src/geophires_x_schema_generator/__init__.py b/src/geophires_x_schema_generator/__init__.py index 34019d9bf..1342f822a 100644 --- a/src/geophires_x_schema_generator/__init__.py +++ b/src/geophires_x_schema_generator/__init__.py @@ -27,6 +27,7 @@ from geophires_x.SUTRAWellBores import SUTRAWellBores from geophires_x.TDPReservoir import TDPReservoir from geophires_x.TOUGH2Reservoir import TOUGH2Reservoir +from geophires_x.UPPReservoir import UPPReservoir # noinspection PyProtectedMember from geophires_x_client import GeophiresXResult, _get_logger @@ -62,6 +63,7 @@ def get_parameter_sources(self) -> list: (LHSReservoir(dummy_model), 'Reservoir'), (MPFReservoir(dummy_model), 'Reservoir'), (SFReservoir(dummy_model), 'Reservoir'), + (UPPReservoir(dummy_model), 'Reservoir'), (CylindricalReservoir(dummy_model), 'Reservoir'), (SBTReservoir(dummy_model), 'Reservoir'), (SUTRAReservoir(dummy_model), 'Reservoir'), diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index be86aef39..2ce2cd6ec 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -305,7 +305,7 @@ "maximum": 10000 }, "Fracture Width": { - "description": "Width of each fracture", + "description": "Total horizontal length of each fracture plane (from tip to tip). Note: In some contexts this is called 'Fracture Length'; it refers to the fracture's lateral extent, not its aperture or thickness.", "type": "number", "units": "meter", "category": "Reservoir", @@ -430,6 +430,15 @@ "minimum": 8, "maximum": 15 }, + "Reservoir Output File Name": { + "description": "File name of reservoir output in case reservoir model 5 is selected", + "type": "string", + "units": null, + "category": "Reservoir", + "default": null, + "minimum": null, + "maximum": null + }, "Cylindrical Reservoir Input Depth": { "description": "Depth of the inflow end of a cylindrical reservoir", "type": "number", @@ -664,6 +673,15 @@ "minimum": 0, "maximum": 200 }, + "Number of Injection Wells per Production Well": { + "description": "Number of (identical) injection wells per production well. For example, provide 0.666 to specify a 3:2 production:injection well ratio. The number of injection wells will be rounded up to the nearest integer.", + "type": "number", + "units": null, + "category": "Well Bores", + "default": 1, + "minimum": 0, + "maximum": 199 + }, "Production Well Diameter": { "description": "Inner diameter of production wellbore (assumed constant along the wellbore) to calculate frictional pressure drop and wellbore heat transmission with Rameys model", "type": "number", @@ -1668,7 +1686,7 @@ "Royalty Rate Escalation Start Year": { "description": "The first year that the Royalty Rate Escalation is applied. The value is specified as a project year index corresponding to the Year row in the cash flow profile.", "type": "integer", - "units": "", + "units": "yr", "category": "Economics", "default": 1, "minimum": 1, diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 9b1e88947..f3ea69504 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -109,6 +109,11 @@ "description": "The calculated amount of cost escalation due to inflation over the construction period.", "units": "MUSD" }, + "Investment Tax Credit": { + "type": "number", + "description": "Investment Tax Credit Value. Represents the total undiscounted ITC sum. For SAM Economic Models, this accounts for the standard Year 1 Federal ITC as well as any applicable State ITCs or multi-year credit schedules.", + "units": "MUSD" + }, "Project lifetime": {}, "Capacity factor": {}, "Project NPV": { @@ -128,7 +133,7 @@ }, "Project VIR=PI=PIR": { "type": "number", - "description": "Project Value Investment Ratio. For SAM Economic Models, VIR = PV(Returns) / abs(PV(Investment)).", + "description": "Project Value Investment Ratio. Value Investment Ratio (VIR). VIR is frequently referred to interchangeably as Profitability Index (PI) or Profit Investment Ratio (PIR) in financial literature. All three terms describe the same fundamental ratio: the present value of future cash flows divided by the initial investment. For SAM Economic Models, this metric is calculated as the Levered Equity Profitability Index. It is calculated as the Present Value of After-Tax Equity Cash Flows (Returns) divided by the Present Value of Equity Invested. It measures the efficiency of the sponsor's specific capital contribution, accounting for leverage.", "units": "" }, "Project MOIC": { @@ -139,7 +144,7 @@ "Fixed Charge Rate (FCR)": {}, "Project Payback Period": { "type": "number", - "description": "The time at which cumulative cash flow reaches zero. For projects that never pay back, the calculated value will be \"N/A\". For SAM Economic Models, after-tax net cash flow is used to calculate the cumulative cash flow.", + "description": "The time at which cumulative cash flow reaches zero. For projects that never pay back, the calculated value will be \"N/A\". For SAM Economic Models, this is Simple Payback Period (SPB): the time at which cumulative non-discounted cash flow reaches zero, calculated using non-discounted after-tax net cash flow. See https://samrepo.nrelcloud.org/help/mtf_payback.html for important considerations regarding the limitations of this metric.", "units": "yr" }, "CHP: Percent cost allocation for electrical plant": {}, @@ -248,7 +253,7 @@ }, "Number of times redrilling": { "type": "number", - "description": "redrill", + "description": "redrill. The total number of redrilling events triggered when production temperature drops below the 'Maximum Drawdown' threshold. In the reservoir simulation, this resets the thermal decline curve by repeating the initial production temperature profile. Economically, each event incurs the full cost of drilling and stimulating the wellfield. The cost of all redrilling events is summed and amortized over the project lifetime as an operational expense. ", "units": null }, "Power plant type": {}, @@ -439,7 +444,7 @@ }, "Investment Tax Credit": { "type": "number", - "description": "Investment Tax Credit Value", + "description": "Investment Tax Credit Value. Represents the total undiscounted ITC sum. For SAM Economic Models, this accounts for the standard Year 1 Federal ITC as well as any applicable State ITCs or multi-year credit schedules.", "units": "MUSD" }, "Overnight Capital Cost": { diff --git a/tests/.gitignore b/tests/.gitignore index dd02483df..83ec4ce7b 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,5 @@ +*.env + HIP.out *.log MC_*Result.json diff --git a/tests/examples/Fervo_Project_Cape-4.out b/tests/examples/Fervo_Project_Cape-4.out index e85f55118..58aa7850a 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.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.777 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:41 + Calculation Time: 2.234 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 12.00 % Nominal Discount Rate: 14.58 % WACC: 8.30 % + Investment Tax Credit: 798.26 MUSD Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 483.35 MUSD After-tax IRR: 27.55 % Project VIR=PI=PIR: 1.45 Project MOIC: 4.20 - Project Payback Period: 2.33 yr + Project Payback Period: 3.33 yr Estimated Jobs Created: 1300 ***ENGINEERING PARAMETERS*** diff --git a/tests/examples/Fervo_Project_Cape-4.txt b/tests/examples/Fervo_Project_Cape-4.txt index d2c1fe956..100882722 100644 --- a/tests/examples/Fervo_Project_Cape-4.txt +++ b/tests/examples/Fervo_Project_Cape-4.txt @@ -1,4 +1,4 @@ -# Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station +# [Deprecated] Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station # 500 MWe EGS Case Study Modeled on Fervo Cape Station after Fervo's April 2025 upsizing announcement: # https://fervoenergy.com/fervo-energy-announces-31-mw-power-purchase-agreement-with-shell-energy/ # See documentation: https://softwareengineerprogrammer.github.io/GEOPHIRES/Fervo_Project_Cape-4.html diff --git a/tests/examples/Fervo_Project_Cape-5.out b/tests/examples/Fervo_Project_Cape-5.out new file mode 100644 index 000000000..f644a4ba1 --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-5.out @@ -0,0 +1,471 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.11.4 + Simulation Date: 2026-01-21 + Simulation Time: 15:25 + Calculation Time: 1.930 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 512.19 MW + Electricity breakeven price: 7.89 cents/kWh + Total CAPEX: 2794.02 MUSD + Number of production wells: 56 + Number of injection wells: 38 + Flowrate per production well: 103.0 kg/sec + Well depth: 2.7 kilometer + Segment 1 Geothermal gradient: 74 degC/km + Segment 1 Thickness: 2.5 kilometer + Segment 2 Geothermal gradient: 41 degC/km + Segment 2 Thickness: 0.5 kilometer + Segment 3 Geothermal gradient: 39.1 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = SAM Single Owner PPA + Real Discount Rate: 12.00 % + Nominal Discount Rate: 15.02 % + WACC: 8.31 % + Investment Tax Credit: 838.21 MUSD + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: 204.80 MUSD + After-tax IRR: 22.75 % + Project VIR=PI=PIR: 1.35 + Project MOIC: 4.42 + Project Payback Period: 5.95 yr + Estimated Jobs Created: 1224 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 56 + Number of Injection Wells: 38 + Well depth: 2.7 kilometer + Water loss rate: 1.0 % + Pump efficiency: 80.0 % + Injection temperature: 56.6 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 0.2 degC + Flowrate per production well: 103.0 kg/sec + Injection well casing ID: 8.535 in + Production well casing ID: 8.535 in + Number of times redrilling: 3 + Power plant type: Supercritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 500.0 degC + Number of segments: 3 + Segment 1 Geothermal gradient: 74 degC/km + Segment 1 Thickness: 2.5 kilometer + Segment 2 Geothermal gradient: 41 degC/km + Segment 2 Thickness: 0.5 kilometer + Segment 3 Geothermal gradient: 39.1 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 205.38 degC + Fracture model = Rectangular + Well separation: fracture height: 95.00 meter + Fracture width: 305.00 meter + Fracture area: 28975.00 m**2 + Reservoir volume calculated with fracture separation and number of fractures as input + Number of fractures: 14100 + Fracture separation: 9.83 meter + Reservoir volume: 4013898767 m**3 + Reservoir hydrostatic pressure: 25324.54 kPa + Plant outlet pressure: 13789.51 kPa + Production wellhead pressure: 2089.11 kPa + Productivity Index: 1.50 kg/sec/bar + Injectivity Index: 1.81 kg/sec/bar + Reservoir density: 2800.00 kg/m**3 + Reservoir thermal conductivity: 3.05 W/m/K + Reservoir heat capacity: 790.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 203.2 degC + Average Production Temperature: 202.9 degC + Minimum Production Temperature: 201.3 degC + Initial Production Temperature: 201.8 degC + Average Reservoir Heat Extraction: 3530.82 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 0.2 degC + Average Injection Well Pump Pressure Drop: -4387.9 kPa + Average Production Well Pump Pressure Drop: 7600.2 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 436.98 MUSD + Drilling and completion costs per well: 4.65 MUSD + Stimulation costs: 454.02 MUSD + Surface power plant costs: 1411.47 MUSD + Field gathering system costs: 43.69 MUSD + Total surface equipment costs: 1455.16 MUSD + Exploration costs: 30.00 MUSD + Overnight Capital Cost: 2376.17 MUSD + Interest during construction: 139.09 MUSD + Inflation costs during construction: 278.77 MUSD + Total CAPEX: 2794.02 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 5.75 MUSD/yr + Power plant maintenance costs: 24.01 MUSD/yr + Water costs: 3.03 MUSD/yr + Redrilling costs: 89.10 MUSD/yr + Average Annual Royalty Cost: 12.23 MUSD/yr + Total operating and maintenance costs: 134.13 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 576.77 MW + Average Total Electricity Generation: 574.70 MW + Minimum Total Electricity Generation: 563.65 MW + Initial Total Electricity Generation: 566.92 MW + Maximum Net Electricity Generation: 514.32 MW + Average Net Electricity Generation: 512.19 MW + Minimum Net Electricity Generation: 500.79 MW + Initial Net Electricity Generation: 504.30 MW + Average Annual Total Electricity Generation: 4531.06 GWh + Average Annual Net Electricity Generation: 4038.19 GWh + Initial pumping power/net installed power: 12.42 % + Average Pumping Power: 62.52 MW + Heat to Power Conversion Efficiency: 14.51 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 201.76 62.6225 504.3004 14.3936 + 2 1.0051 202.78 62.5841 511.4006 14.4941 + 3 1.0060 202.97 62.5770 512.7211 14.5127 + 4 1.0065 203.07 62.5733 513.4082 14.5224 + 5 1.0068 203.13 62.5709 513.8623 14.5287 + 6 1.0070 203.18 62.5697 514.1680 14.5330 + 7 1.0068 203.13 62.5813 513.8260 14.5281 + 8 1.0033 202.42 62.6919 508.8521 14.4568 + 9 1.0036 202.49 62.5620 509.4372 14.4669 + 10 1.0056 202.88 62.5532 512.1162 14.5046 + 11 1.0062 203.01 62.5375 513.0867 14.5184 + 12 1.0066 203.10 62.5160 513.6731 14.5269 + 13 1.0069 203.15 62.4933 514.0872 14.5330 + 14 1.0071 203.18 62.4765 514.2879 14.5360 + 15 1.0061 202.99 62.4984 512.9234 14.5167 + 16 0.9989 201.53 62.7177 502.6676 14.3691 + 17 1.0048 202.72 62.4488 511.1555 14.4926 + 18 1.0059 202.95 62.4472 512.6966 14.5142 + 19 1.0064 203.05 62.4467 513.4419 14.5247 + 20 1.0068 203.12 62.4466 513.9215 14.5314 + 21 1.0070 203.17 62.4471 514.2544 14.5360 + 22 1.0069 203.15 62.4558 514.1335 14.5342 + 23 1.0044 202.65 62.5388 510.5476 14.4828 + 24 1.0027 202.29 62.4471 508.1782 14.4509 + 25 1.0054 202.84 62.4473 511.9822 14.5042 + 26 1.0062 203.00 62.4474 513.0551 14.5192 + 27 1.0066 203.08 62.4475 513.6623 14.5277 + 28 1.0069 203.14 62.4477 514.0765 14.5335 + 29 1.0071 203.18 62.4493 514.3193 14.5369 + 30 1.0065 203.06 62.4736 513.4575 14.5245 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 4013.5 27753.4 1221.07 7.56 + 2 4037.8 27837.9 1120.85 15.15 + 3 4045.2 27863.6 1020.54 22.74 + 4 4049.6 27878.8 920.18 30.34 + 5 4052.6 27889.2 819.78 37.94 + 6 4053.3 27891.8 719.37 45.54 + 7 4037.4 27837.5 619.15 53.13 + 8 3987.5 27665.0 519.56 60.67 + 9 4029.5 27808.4 419.45 68.25 + 10 4041.7 27850.7 319.18 75.84 + 11 4047.6 27870.7 218.85 83.43 + 12 4051.5 27883.6 118.47 91.03 + 13 4054.1 27892.1 18.06 98.63 + 14 4051.5 27883.0 -82.32 106.23 + 15 4014.0 27754.7 -182.24 113.80 + 16 4002.7 27712.6 -282.00 121.35 + 17 4037.0 27831.4 -382.20 128.93 + 18 4045.3 27860.3 -482.49 136.53 + 19 4050.0 27876.7 -582.85 144.12 + 20 4053.2 27887.8 -683.25 151.72 + 21 4054.6 27892.8 -783.66 159.32 + 22 4043.9 27856.6 -883.94 166.92 + 23 3989.6 27669.5 -983.55 174.46 + 24 4026.2 27793.8 -1083.61 182.03 + 25 4041.2 27846.0 -1183.86 189.62 + 26 4047.5 27868.0 -1284.18 197.21 + 27 4051.4 27881.8 -1384.56 204.81 + 28 4054.1 27891.1 -1484.96 212.41 + 29 4053.1 27887.9 -1585.36 220.01 + 30 4029.0 27805.7 -1685.46 227.59 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Year -4 Year -3 Year -2 Year -1 Year 0 Year 1 Year 2 Year 3 Year 4 Year 5 Year 6 Year 7 Year 8 Year 9 Year 10 Year 11 Year 12 Year 13 Year 14 Year 15 Year 16 Year 17 Year 18 Year 19 Year 20 Year 21 Year 22 Year 23 Year 24 Year 25 Year 26 Year 27 Year 28 Year 29 Year 30 +CONSTRUCTION +Capital expenditure schedule [construction] (%) 1.40 2.70 13.90 43.10 38.90 +Overnight capital expenditure [construction] ($) -33,266,320 -64,156,475 -330,287,039 -1,024,127,436 -924,328,475 +plus: +Inflation cost [construction] ($) -898,191 -3,511,220 -27,482,089 -115,166,472 -131,707,104 +equals: +Nominal capital expenditure [construction] ($) -34,164,511 -67,667,695 -357,769,127 -1,139,293,908 -1,056,035,578 + +Issuance of equity [construction] ($) 34,164,511 67,667,695 107,330,738 341,788,173 316,810,674 +Issuance of debt [construction] ($) 0 0 250,438,389 797,505,736 739,224,905 +Debt balance [construction] ($) 0 0 250,438,389 1,074,240,156 1,926,260,277 +Debt interest payment [construction] ($) 0 0 0 26,296,031 112,795,216 + +Installed cost [construction] ($) -34,164,511 -67,667,695 -357,769,127 -1,165,589,939 -1,168,830,795 +After-tax net cash flow [construction] ($) -34,164,511 -67,667,695 -107,330,738 -341,788,173 -316,810,674 + +ENERGY +Electricity to grid (kWh) 0.0 4,013,775,228 4,038,092,175 4,045,504,457 4,049,887,905 4,052,897,502 4,053,607,976 4,037,669,997 3,987,820,599 4,029,801,127 4,042,030,041 4,047,921,955 4,051,802,365.0 4,054,405,439 4,051,800,995 4,014,266,841 4,002,951,225 4,037,265,788 4,045,577,076.0 4,050,264,859 4,053,450,308 4,054,864,459 4,044,209,514 3,989,877,799 4,026,490,507 4,041,477,344 4,047,779,650 4,051,721,527 4,054,403,980 4,053,407,823 4,029,278,429 +Electricity from grid (kWh) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +Electricity to grid net (kWh) 0.0 4,013,775,228 4,038,092,175 4,045,504,457 4,049,887,905 4,052,897,502 4,053,607,976 4,037,669,997 3,987,820,599 4,029,801,127 4,042,030,041 4,047,921,955 4,051,802,365.0 4,054,405,439 4,051,800,995 4,014,266,841 4,002,951,225 4,037,265,788 4,045,577,076.0 4,050,264,859 4,053,450,308 4,054,864,459 4,044,209,514 3,989,877,799 4,026,490,507 4,041,477,344 4,047,779,650 4,051,721,527 4,054,403,980 4,053,407,823 4,029,278,429 + +REVENUE +PPA price (cents/kWh) 0.0 9.50 9.50 9.56 9.61 9.67 9.73 9.79 9.84 9.90 9.96 10.01 10.07 10.13 10.18 10.24 10.30 10.36 10.41 10.47 10.53 10.58 10.64 10.70 10.75 10.81 10.87 10.93 10.98 11.04 11.10 +PPA revenue ($) 0 381,308,647 383,618,757 386,628,861 389,356,223 391,955,717 394,334,984 395,086,009 392,481,303 398,910,014 402,424,511 405,318,425 408,016,498 410,589,639 412,635,413 411,101,067 412,223,917 418,058,872 421,225,485 424,022,228 426,666,179 429,126,306 430,303,892 426,797,228 433,008,789 436,924,116 439,912,692 442,650,577 445,254,645 447,455,690 447,088,734 +Curtailment payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Capacity payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1,397,011,034 +Total revenue ($) 0 381,308,647 383,618,757 386,628,861 389,356,223 391,955,717 394,334,984 395,086,009 392,481,303 398,910,014 402,424,511 405,318,425 408,016,498 410,589,639 412,635,413 411,101,067 412,223,917 418,058,872 421,225,485 424,022,228 426,666,179 429,126,306 430,303,892 426,797,228 433,008,789 436,924,116 439,912,692 442,650,577 445,254,645 447,455,690 1,844,099,768 + +Property tax net assessed value ($) 0 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 2,794,022,067 + +OPERATING EXPENSES +O&M fixed expense ($) 0 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 121,892,342 +Royalty rate (%) 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 +O&M production-based expense ($) 0 6,672,901 6,713,328 6,766,005 6,813,734 6,859,225 6,900,862 6,914,005 6,868,423 6,980,925 7,042,429 14,186,145 14,280,577 14,370,637 14,442,239 14,388,537 14,427,837 14,632,061 14,742,892 14,840,778 14,933,316 15,019,421 15,060,636 14,937,903 15,155,308 15,292,344 15,396,944 15,492,770 15,583,913 15,660,949 15,648,106 +O&M capacity-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Fuel expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Electricity purchase ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Property tax expense ($) 0 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 6,146,849 +Insurance expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total operating expenses ($) 0 134,712,092 134,752,519 134,805,195 134,852,924 134,898,415 134,940,053 134,953,196 134,907,613 135,020,116 135,081,619 142,225,335 142,319,768 142,409,828 142,481,430 142,427,728 142,467,027 142,671,251 142,782,082 142,879,968 142,972,507 143,058,611 143,099,827 142,977,093 143,194,498 143,331,534 143,436,135 143,531,961 143,623,103 143,700,140 143,687,296 + +EBITDA ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 + +OPERATING ACTIVITIES +EBITDA ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +plus PBI if not available for debt service: +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 134,838,219 133,410,768 131,883,394 130,249,105 128,500,415 126,629,317 124,627,242 122,485,021 120,192,846 117,740,218 115,115,906 112,307,892 109,303,317 106,088,423 102,648,485 98,967,752 95,029,367 90,815,296 86,306,239 81,481,549 76,319,130 70,795,342 64,884,889 58,560,704 51,793,826 44,553,267 36,805,868 28,516,152 19,646,155 10,155,259 +Cash flow from operating activities ($) 0 111,758,336 115,455,470 119,940,271 124,254,194 128,556,887 132,765,615 135,505,572 135,088,669 143,697,052 149,602,674 147,977,184 153,388,838 158,876,494 164,065,561 166,024,855 170,789,138 180,358,254 187,628,107 194,836,020 202,212,124 209,748,564 216,408,724 218,935,246 231,253,587 241,798,755 251,923,291 262,312,748 273,115,390 284,109,395 1,690,257,213 + +INVESTING ACTIVITIES +Total installed cost ($) -2,794,022,067 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -2,794,022,067 +plus: +Reserve (increase)/decrease debt service ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease working capital ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease receivables ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash flow from investing activities ($) -2,794,022,067 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +FINANCING ACTIVITIES +Issuance of equity ($) 867,761,790 +Size of debt ($) 1,926,260,277 +minus: +Debt principal payment ($) 0 20,392,169 21,819,620 23,346,994 24,981,283 26,729,973 28,601,071 30,603,146 32,745,367 35,037,542 37,490,170 40,114,482 42,922,496 45,927,071 49,141,965 52,581,903 56,262,636 60,201,021 64,415,092 68,924,149 73,748,839 78,911,258 84,435,046 90,345,499 96,669,684 103,436,562 110,677,121 118,424,520 126,714,236 135,584,233 145,075,129 +equals: +Cash flow from financing activities ($) 2,794,022,067 -20,392,169 -21,819,620 -23,346,994 -24,981,283 -26,729,973 -28,601,071 -30,603,146 -32,745,367 -35,037,542 -37,490,170 -40,114,482 -42,922,496 -45,927,071 -49,141,965 -52,581,903 -56,262,636 -60,201,021 -64,415,092 -68,924,149 -73,748,839 -78,911,258 -84,435,046 -90,345,499 -96,669,684 -103,436,562 -110,677,121 -118,424,520 -126,714,236 -135,584,233 -145,075,129 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 111,758,336 115,455,470 119,940,271 124,254,194 128,556,887 132,765,615 135,505,572 135,088,669 143,697,052 149,602,674 147,977,184 153,388,838 158,876,494 164,065,561 166,024,855 170,789,138 180,358,254 187,628,107 194,836,020 202,212,124 209,748,564 216,408,724 218,935,246 231,253,587 241,798,755 251,923,291 262,312,748 273,115,390 284,109,395 1,690,257,213 +Cash flow from investing activities ($) -2,794,022,067 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Cash flow from financing activities ($) 2,794,022,067 -20,392,169 -21,819,620 -23,346,994 -24,981,283 -26,729,973 -28,601,071 -30,603,146 -32,745,367 -35,037,542 -37,490,170 -40,114,482 -42,922,496 -45,927,071 -49,141,965 -52,581,903 -56,262,636 -60,201,021 -64,415,092 -68,924,149 -73,748,839 -78,911,258 -84,435,046 -90,345,499 -96,669,684 -103,436,562 -110,677,121 -118,424,520 -126,714,236 -135,584,233 -145,075,129 +Total pre-tax cash flow ($) 0 91,366,167 93,635,850 96,593,278 99,272,911 101,826,914 104,164,543 104,902,426 102,343,302 108,659,510 112,112,504 107,862,702 110,466,342 112,949,423 114,923,596 113,442,952 114,526,502 120,157,233 123,213,015 125,911,872 128,463,285 130,837,307 131,973,678 128,589,747 134,583,903 138,362,193 141,246,170 143,888,228 146,401,154 148,525,162 1,545,182,084 + +Pre-tax Returns: +Issuance of equity ($) 867,761,790 +Total pre-tax cash flow ($) 0 91,366,167 93,635,850 96,593,278 99,272,911 101,826,914 104,164,543 104,902,426 102,343,302 108,659,510 112,112,504 107,862,702 110,466,342 112,949,423 114,923,596 113,442,952 114,526,502 120,157,233 123,213,015 125,911,872 128,463,285 130,837,307 131,973,678 128,589,747 134,583,903 138,362,193 141,246,170 143,888,228 146,401,154 148,525,162 1,545,182,084 +Total pre-tax returns ($) -867,761,790 91,366,167 93,635,850 96,593,278 99,272,911 101,826,914 104,164,543 104,902,426 102,343,302 108,659,510 112,112,504 107,862,702 110,466,342 112,949,423 114,923,596 113,442,952 114,526,502 120,157,233 123,213,015 125,911,872 128,463,285 130,837,307 131,973,678 128,589,747 134,583,903 138,362,193 141,246,170 143,888,228 146,401,154 148,525,162 1,545,182,084 + +After-tax Returns: +Total pre-tax returns ($) -867,761,790 91,366,167 93,635,850 96,593,278 99,272,911 101,826,914 104,164,543 104,902,426 102,343,302 108,659,510 112,112,504 107,862,702 110,466,342 112,949,423 114,923,596 113,442,952 114,526,502 120,157,233 123,213,015 125,911,872 128,463,285 130,837,307 131,973,678 128,589,747 134,583,903 138,362,193 141,246,170 143,888,228 146,401,154 148,525,162 1,545,182,084 +Federal ITC total income ($) 0 838,206,620 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal tax benefit (liability) ($) 0 -10,500,385 659,558 -239,398 -1,104,102 -1,966,556 -2,810,174 -3,359,385 -3,275,819 -5,001,326 -6,185,078 -5,859,257 -6,943,996 -8,043,969 -9,084,092 -9,476,822 -10,431,799 -12,349,881 -13,807,086 -15,251,877 -16,730,380 -30,142,036 -43,378,047 -43,884,475 -46,353,625 -48,467,351 -50,496,764 -52,579,279 -54,744,614 -56,948,308 -338,803,607 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,383,534 149,716 -54,342 -250,626 -446,398 -637,895 -762,563 -743,594 -1,135,276 -1,403,981 -1,330,022 -1,576,252 -1,825,940 -2,062,043 -2,151,191 -2,367,966 -2,803,360 -3,134,139 -3,462,099 -3,797,711 -6,842,090 -9,846,597 -9,961,554 -10,522,038 -11,001,843 -11,462,510 -11,935,230 -12,426,750 -12,926,977 -76,906,703 +Total after-tax returns ($) -867,761,790 916,688,868 94,445,124 96,299,537 97,918,183 99,413,960 100,716,474 100,780,477 98,323,889 102,522,908 104,523,444 100,673,423 101,946,094 103,079,514 103,777,461 101,814,938 101,726,737 105,003,992 106,271,790 107,197,896 107,935,194 93,853,181 78,749,034 74,743,718 77,708,240 78,892,998 79,286,896 79,373,719 79,229,789 78,649,877 1,129,471,774 + +After-tax net cash flow ($) -34,164,511 -67,667,695 -107,330,738 -341,788,173 -316,810,674 916,688,868 94,445,124 96,299,537 97,918,183 99,413,960 100,716,474 100,780,477 98,323,889 102,522,908 104,523,444 100,673,423 101,946,094 103,079,514 103,777,461 101,814,938 101,726,737 105,003,992 106,271,790 107,197,896 107,935,194 93,853,181 78,749,034 74,743,718 77,708,240 78,892,998 79,286,896 79,373,719 79,229,789 78,649,877 1,129,471,774 +After-tax cumulative IRR (%) NaN NaN NaN NaN NaN 2.71 7.32 11.00 13.82 15.93 17.52 18.70 19.57 20.26 20.80 21.20 21.51 21.76 21.96 22.11 22.24 22.34 22.42 22.48 22.53 22.57 22.60 22.62 22.63 22.65 22.66 22.67 22.67 22.68 22.75 +After-tax cumulative NPV ($) -34,164,511 -92,993,708 -174,117,302 -398,707,931 -579,694,330 -124,413,226 -83,633,083 -47,483,363 -15,527,127 12,679,501 37,523,177 59,135,591 77,467,068 94,084,765 108,813,830 121,147,373 132,005,505 141,550,341 149,904,652 157,030,403 163,220,049 168,774,587 173,661,918 177,947,912 181,699,714 184,535,917 186,604,844 188,312,051 189,855,136 191,217,123 192,407,124 193,442,825 194,341,614 195,117,287 204,801,571 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -867,761,790 535,380,222 -289,173,633 -290,329,324 -291,438,040 -292,541,757 -293,618,510 -294,305,532 -294,157,414 -296,387,105 -297,901,067 -304,645,002 -306,070,404 -307,510,125 -308,857,953 -309,286,129 -310,497,180 -313,054,880 -314,953,695 -316,824,332 -318,730,986 -335,273,125 -351,554,858 -352,053,510 -355,300,549 -358,031,117 -360,625,796 -363,276,857 -366,024,856 -368,805,813 682,383,039 +PPA revenue ($) 0 381,308,647 383,618,757 386,628,861 389,356,223 391,955,717 394,334,984 395,086,009 392,481,303 398,910,014 402,424,511 405,318,425 408,016,498 410,589,639 412,635,413 411,101,067 412,223,917 418,058,872 421,225,485 424,022,228 426,666,179 429,126,306 430,303,892 426,797,228 433,008,789 436,924,116 439,912,692 442,650,577 445,254,645 447,455,690 447,088,734 +Electricity to grid (kWh) 0.0 4,013,775,228 4,038,092,175 4,045,504,457 4,049,887,905 4,052,897,502 4,053,607,976 4,037,669,997 3,987,820,599 4,029,801,127 4,042,030,041 4,047,921,955 4,051,802,365.0 4,054,405,439 4,051,800,995 4,014,266,841 4,002,951,225 4,037,265,788 4,045,577,076.0 4,050,264,859 4,053,450,308 4,054,864,459 4,044,209,514 3,989,877,799 4,026,490,507 4,041,477,344 4,047,779,650 4,051,721,527 4,054,403,980 4,053,407,823 4,029,278,429 + +Present value of annual costs ($) 2,089,221,121 +Present value of annual energy nominal (kWh) 26,465,637,421 +LCOE Levelized cost of energy nominal (cents/kWh) 7.89 + +Present value of PPA revenue ($) 2,594,693,319 +Present value of annual energy nominal (kWh) 26,465,637,421 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 +State taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State taxable IBI income ($) 0 +State taxable CBI income ($) 0 +minus: +Debt interest payment ($) 0 134,838,219 133,410,768 131,883,394 130,249,105 128,500,415 126,629,317 124,627,242 122,485,021 120,192,846 117,740,218 115,115,906 112,307,892 109,303,317 106,088,423 102,648,485 98,967,752 95,029,367 90,815,296 86,306,239 81,481,549 76,319,130 70,795,342 64,884,889 58,560,704 51,793,826 44,553,267 36,805,868 28,516,152 19,646,155 10,155,259 +Total state tax depreciation ($) 0 59,372,969 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 59,372,969 0 0 0 0 0 0 0 0 0 +equals: +State taxable income ($) 0 52,385,367 -3,290,467 1,194,333 5,508,256 9,810,949 14,019,677 16,759,634 16,342,731 24,951,114 30,856,736 29,231,246 34,642,900 40,130,556 45,319,623 47,278,917 52,043,200 61,612,316 68,882,169 76,090,083 83,466,186 150,375,596 216,408,724 218,935,246 231,253,587 241,798,755 251,923,291 262,312,748 273,115,390 284,109,395 1,690,257,213 + +State income tax rate (frac) 0.0 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 +State tax benefit (liability) ($) 0 -2,383,534 149,716 -54,342 -250,626 -446,398 -637,895 -762,563 -743,594 -1,135,276 -1,403,981 -1,330,022 -1,576,252 -1,825,940 -2,062,043 -2,151,191 -2,367,966 -2,803,360 -3,134,139 -3,462,099 -3,797,711 -6,842,090 -9,846,597 -9,961,554 -10,522,038 -11,001,843 -11,462,510 -11,935,230 -12,426,750 -12,926,977 -76,906,703 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,383,534 149,716 -54,342 -250,626 -446,398 -637,895 -762,563 -743,594 -1,135,276 -1,403,981 -1,330,022 -1,576,252 -1,825,940 -2,062,043 -2,151,191 -2,367,966 -2,803,360 -3,134,139 -3,462,099 -3,797,711 -6,842,090 -9,846,597 -9,961,554 -10,522,038 -11,001,843 -11,462,510 -11,935,230 -12,426,750 -12,926,977 -76,906,703 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal taxable IBI income ($) 0 +Federal taxable CBI income ($) 0 +Federal taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +minus: +Debt interest payment ($) 0 134,838,219 133,410,768 131,883,394 130,249,105 128,500,415 126,629,317 124,627,242 122,485,021 120,192,846 117,740,218 115,115,906 112,307,892 109,303,317 106,088,423 102,648,485 98,967,752 95,029,367 90,815,296 86,306,239 81,481,549 76,319,130 70,795,342 64,884,889 58,560,704 51,793,826 44,553,267 36,805,868 28,516,152 19,646,155 10,155,259 +Total federal tax depreciation ($) 0 59,372,969 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 118,745,938 59,372,969 0 0 0 0 0 0 0 0 0 +equals: +Federal taxable income ($) 0 50,001,832 -3,140,751 1,139,991 5,257,631 9,364,551 13,381,781 15,997,071 15,599,137 23,815,839 29,452,754 27,901,225 33,066,648 38,304,616 43,257,580 45,127,726 49,675,234 58,808,956 65,748,031 72,627,984 79,668,475 143,533,506 206,562,127 208,973,692 220,731,549 230,796,912 240,460,781 250,377,518 260,688,640 271,182,417 1,613,350,510 + +Federal income tax rate (frac) 0.0 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 +Federal tax benefit (liability) ($) 0 -10,500,385 659,558 -239,398 -1,104,102 -1,966,556 -2,810,174 -3,359,385 -3,275,819 -5,001,326 -6,185,078 -5,859,257 -6,943,996 -8,043,969 -9,084,092 -9,476,822 -10,431,799 -12,349,881 -13,807,086 -15,251,877 -16,730,380 -30,142,036 -43,378,047 -43,884,475 -46,353,625 -48,467,351 -50,496,764 -52,579,279 -54,744,614 -56,948,308 -338,803,607 + +CASH INCENTIVES +Federal IBI income ($) 0 +State IBI income ($) 0 +Utility IBI income ($) 0 +Other IBI income ($) 0 +Total IBI income ($) 0 + +Federal CBI income ($) 0 +State CBI income ($) 0 +Utility CBI income ($) 0 +Other CBI income ($) 0 +Total CBI income ($) 0 + +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +TAX CREDITS +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Federal ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC percent income ($) 0 838,206,620 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC total income ($) 0 838,206,620 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +State ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC percent income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +DEBT REPAYMENT +Debt balance ($) 1,926,260,277 1,905,868,109 1,884,048,488 1,860,701,494 1,835,720,211 1,808,990,238 1,780,389,167 1,749,786,020 1,717,040,654 1,682,003,112 1,644,512,941 1,604,398,459 1,561,475,964 1,515,548,893 1,466,406,928 1,413,825,025 1,357,562,388 1,297,361,368 1,232,946,275 1,164,022,127 1,090,273,287 1,011,362,030 926,926,984 836,581,485 739,911,801 636,475,239 525,798,117 407,373,598 280,659,362 145,075,129 0 +Debt interest payment ($) 0 134,838,219 133,410,768 131,883,394 130,249,105 128,500,415 126,629,317 124,627,242 122,485,021 120,192,846 117,740,218 115,115,906 112,307,892 109,303,317 106,088,423 102,648,485 98,967,752 95,029,367 90,815,296 86,306,239 81,481,549 76,319,130 70,795,342 64,884,889 58,560,704 51,793,826 44,553,267 36,805,868 28,516,152 19,646,155 10,155,259 +Debt principal payment ($) 0 20,392,169 21,819,620 23,346,994 24,981,283 26,729,973 28,601,071 30,603,146 32,745,367 35,037,542 37,490,170 40,114,482 42,922,496 45,927,071 49,141,965 52,581,903 56,262,636 60,201,021 64,415,092 68,924,149 73,748,839 78,911,258 84,435,046 90,345,499 96,669,684 103,436,562 110,677,121 118,424,520 126,714,236 135,584,233 145,075,129 +Debt total payment ($) 0 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 +minus: +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash available for debt service (CAFDS) ($) 0 246,596,555 248,866,238 251,823,666 254,503,299 257,057,302 259,394,931 260,132,814 257,573,690 263,889,898 267,342,892 263,093,090 265,696,730 268,179,811 270,153,984 268,673,339 269,756,890 275,387,621 278,443,403 281,142,260 283,693,673 286,067,695 287,204,066 283,820,135 289,814,291 293,592,581 296,476,558 299,118,616 301,631,542 303,755,550 1,700,412,472 +Debt total payment ($) 0 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 155,230,388 +DSCR (pre-tax) 0.0 1.59 1.60 1.62 1.64 1.66 1.67 1.68 1.66 1.70 1.72 1.69 1.71 1.73 1.74 1.73 1.74 1.77 1.79 1.81 1.83 1.84 1.85 1.83 1.87 1.89 1.91 1.93 1.94 1.96 10.95 + +RESERVES +Reserves working capital funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves debt service funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves total reserves balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest on reserves (%/year) 1.75 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ***EXTENDED ECONOMICS*** + + Royalty Holder NPV: 136.57 MUSD + Royalty Holder Average Annual Revenue: 12.23 MUSD/yr + Royalty Holder Total Revenue: 367.03 MUSD diff --git a/tests/examples/Fervo_Project_Cape-5.txt b/tests/examples/Fervo_Project_Cape-5.txt new file mode 100644 index 000000000..372a67b5d --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-5.txt @@ -0,0 +1,121 @@ +# Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station Phase II +# See documentation: https://softwareengineerprogrammer.github.io/GEOPHIRES/Fervo_Project_Cape-5.html + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- 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, .027, -- US inflation as of December 2025 + +Starting Electricity Sale Price, 0.095, -- Aligns with Geysers - Sacramento pricing in [2024b ATB](https://atb.nrel.gov/electricity/2024/geothermal) (NREL, 2025). See Sensitivity Analysis for effect of different prices on results. +Electricity Escalation Rate Per Year, 0.00057, -- Calibrated to reach $100/MWh at project year 11 +Ending Electricity Sale Price, 1, -- Note that this value does not directly determine price at the end of the project life, but rather as a cap as the maximum price to which the starting price can escalate. +Electricity Escalation Start Year, 1 + +Fraction of Investment in Bonds, .7, -- Approximate debt required to cover CAPEX after $1 billion sponsor equity per [Matson, 2024](https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/). 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 precisely. +Discount Rate, 0.12, -- Typical discount rates for higher-risk projects may be 12–15%. +Inflated Bond Interest Rate, .07, -- 2024b ATB (NREL, 2025) + +Inflated Bond Interest Rate During Construction, 0.105, -- Higher than interest rate during normal operation to account for increased risk of default prior to COD. Value aligns with ATB discount rate (NREL, 2025). +Bond Financing Start Year, -2, -- Equity-only for first 2 construction years (ATB) + +Construction Years, 5, -- Ground broken in 2023 (Fervo Energy, 2023). Expected to reach full scale production in 2028 (Fervo Energy, 2025). See [GEOPHIRES documentation](SAM-EM_Multiple-Construction-Years.html) for details on how construction years affect CAPEX, IRR, and other calculations. + +# ATB advanced scenario +# Construction CAPEX Schedule, 0.09,0.28,0.1,0.34,0.28 + +# DOE scenario (alternative) +# Construction CAPEX Schedule, 0.014,0.027,0.137,0.274,0.548 + +# DOE-ATB hybrid scenario +Construction CAPEX Schedule, 0.014,0.027,0.139,0.431,0.389 + +Investment Tax Credit Rate, 0.3, -- Geothermal Drilling and Completions Apprenticeship Program ensures compliance with ITC labor requirements (Southern Utah University, 2024). +Combined Income Tax Rate, .2555, -- Federal Corporate Income Tax Rate of 21% plus Utah Corporate Franchise and Income Tax Rate of 4.55%. (Note: This input uses a simple summation of statutory rates; the effective combined rate calculated in the model may differ due to standard federal-state tax interactions.) +Property Tax Rate, 0.0022, -- Utah Inland Port Authority (UIPA) tax differential incentive + +Capital Cost for Power Plant for Electricity Generation, 1900, -- [US DOE, 2021](https://betterbuildingssolutioncenter.energy.gov/sites/default/files/attachments/Waste_Heat_to_Power_Fact_Sheet.pdf). Pricing information not publicly available for Turboden or Baker Hughes Gen 2 ORC units (Turboden, 2025; Jacobs, 2025). +Exploration Capital Cost, 30, -- Equivalent to 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025), plus $1M for geophysical and field work, plus 15% contingency, plus 12% indirect costs. + +Well Drilling Cost Correlation, 3, -- 2025 NREL Geothermal Drilling Cost Curve Update (Akindipe and Witter, 2025). +Well Drilling and Completion Capital Cost Adjustment Factor, 0.9, -- 2024b Geothermal ATB ([NREL, 2025](https://atb.nrel.gov/electricity/2024b/geothermal)). Note: Fervo has claimed lower drilling costs equivalent to an adjustment factor of 0.8 (Latimer, 2025); the case study conservatively uses the higher ATB-aligned value. + +Reservoir Stimulation Capital Cost per Injection Well, 4, -- The baseline stimulation cost is calibrated from costs of high-intensity U.S. shale wells (Baytex Energy, 2024; Quantum Proppant Technologies, 2020), which are the closest technological analogue for multi-stage EGS (Gradl, 2018). Costs are also driven by the requirement for high-strength ceramic proppant rather than standard sand, which would crush or chemically degrade (diagenesis) over a 30-year lifecycle at 200℃ (Ko et al., 2023; Shiozawa and McClure, 2014) and the premium for ultra-high-temperature (HT) downhole tools. Note that all-in costs per well are higher than the baseline cost because they include additional indirect costs and contingency. +Reservoir Stimulation Capital Cost per Production Well, 4, -- See Reservoir Stimulation Capital Cost per Injection Well + +Field Gathering System Capital Cost Adjustment Factor, 0.54, -- Gathering costs represent 2% of facilities CAPEX per [Matson, 2024](https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/). + +Royalty Rate, 0.0175, -- The BLM royalty structure is 1.75% of gross proceeds from electricity sales for the first 10 years of production (Code of Federal Regulations, 2024). +Royalty Rate Escalation Start Year, 11, -- After the first 10 years of production, the royalty rate escalates to 3.5%. +Royalty Rate Escalation, 0.0175, -- Escalation at Year 11 from 1.75% to 3.5%. +Royalty Rate Maximum, 0.035, -- No further escalation beyond 3.5%. + + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Gen 2 ORC units (Turboden, 2025). +Plant Lifetime, 30, -- Sets the project economic horizon, aligned with Fervo's anticipated 30-year well life (Fervo Energy, 2025). Modeling Distinction: While Fervo projects physical wellbore integrity for 30 years, GEOPHIRES simulates "redrilling events" to model thermal management of the reservoir volume. This treats the 30-year lifespan as an aggregate of shorter-lived thermal cycles delineated by discrete redrilling events occurring at intervals dictated by the Maximum Drawdown parameter. The modeled cost of each redrilling event is equivalent to the drilling and stimulation cost of the entire wellfield, serving as a conservative cost proxy for the major interventions (e.g., sidetracking and stimulating laterals into fresh rock, or drilling new wells if necessary) required to sustain the 500 MW PPA target against thermal depletion. + +Reservoir Model, 1 + +Surface Temperature, 13, -- Surface temperature near Milford, UT (38.4987670, -112.9163432) ([Project InnerSpace, 2025](https://geomap.projectinnerspace.org/test/)). + +Number of Segments, 3 +Gradient 1, 74, -- Sedimentary overburden. 200℃ at 8500 ft depth (Fercho et al. 2024); 228.89℃ at 9824 ft (Norbeck et al. 2024). +Thickness 1, 2.5 +Gradient 2, 41, -- Crystalline reservoir +Thickness 2, 0.5 +Gradient 3, 39.1, -- Sugarloaf appraisal + +Reservoir Depth, 2.68, -- Extrapolated from surface temperature, gradient, and average production temperature of shallower and deeper producers in Singh et al., 2025. + +Reservoir Density, 2800, -- phyllite + quartzite + diorite + granodiorite ([Norbeck et al., 2023](https://doi.org/10.31223/X52X0B)) +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 + +Reservoir Volume Option, 1, -- FRAC_NUM_SEP: Reservoir volume calculated with fracture separation and number of fractures as input + +Number of Fractures per Stimulated Well, 150, -- The model assumes an Extreme Limited Entry stimulation design (Fervo Energy, 2023) utilizing 12 stages with 15 clusters per stage (derived from Singh et al., 2025) and 81–85% stimulation success rate per 2024b ATB Moderate Scenario (NREL, 2025). +Fracture Separation, 9.8255, -- Based on 30 foot cluster spacing (Singh et al., 2025) marginally uprated to align with long-term thermal decline behavior trend towards wider fracture spacing (Fercho et al., 2025). + +Fracture Shape, 4, -- Bench design and fracture geometry in Singh et al., 2025 are given in rectangular dimensions. +Fracture Width, 305, -- Matches intra-bench well spacing of 500 ft (corresponding to fracture length of 1000 ft) (Singh. et al., 2025) +Fracture Height, 95, -- Actual fracture geometry is irregular and heterogeneous; this height complies with the minimum height required by the implemented bench design (200 ft; 60.96 meters) and yields an effective fracture surface area consistent with simulation results in Singh. et al., 2025. + +Water Loss Fraction, 0.01, -- "Long-term modeling, calibrated to early field data, predicts circulation recapture rates exceeding 99%" ([Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/); Fervo Energy, 2025). Modeling in Singh et al., 2025 predicts fluid loss of 0.36% to 0.49%. +Water Cost Adjustment Factor, 2, -- Local scarcity may increase procurement costs. Development near/on land with active/shut-in oil and gas wells could potentially utilize waste water to recover losses and offset costs. + +Ambient Temperature, 11.17, -- Average annual temperature of Milford, Utah ([NCEI](https://www.ncei.noaa.gov/access/us-climate-normals/#dataset=normals-annualseasonal&timeframe=30&station=USC00425654)). Note that this value affects heat to power conversion efficiency. The effects of hourly and seasonal ambient temperature fluctuations on efficiency and power generation are not modeled in this version of the case study. + +Utilization Factor, .9 +Plant Outlet Pressure, 2000 psi, -- McClure, 2024; Singh et al., 2025. +Circulation Pump Efficiency, 0.80 + +# *** Well Bores Parameters *** + +Number of Production Wells, 56, -- Number of production wells required to produce net generation greater than 500 MW (PPA minimum) and total generation less than 600 MW (Gen 2 ORCs gross capacity). +Number of Injection Wells per Production Well, 0.666, -- Modeled on the reference case 5-well bench pattern (3 producers : 2 injectors) described in Singh et al., 2025. + +Nonvertical Length per Multilateral Section, 5000 feet, -- Target lateral length given in environmental assessment (BLM, 2024). Note that lateral length is assumed to be an upper bound constraining the number of fractures per well for a given cluster spacing. +Number of Multilateral Sections, 0, -- This parameter is set to 0 because, for this case study, the cost of horizontal drilling is included within the 'vertical drilling cost.' This approach allows us to more directly convey the overall well drilling and completion cost. + +Production Flow Rate per Well, 103, -- Cape Station pilot testing reported a sustained flow rate of 95–100 kg/s and maximum flow rate of 107 kg/s (Fervo Energy, 2024). Modeling by Singh et al. suggests initial flow rates of 120–130 kg/sec that gradually decrease over time (Singh et al., 2025). The ATB Advanced Scenario models sustained flow rates of 110 kg/s (NREL, 2024). +Production Well Diameter, 8.535, -- Inner diameter of 9⅝ inch casing size, the next standard casing size up from 7 inches, implied by announcement of “increasing casing diameter” (Fervo Energy, 2025). +Injection Well Diameter, 8.535, -- See Production Well Diameter + +Production Wellhead Pressure, 303 psi, -- Modeled at a constant 300 psi in Singh et al., 2025. We use a marginally uprated value to conform to GEOPHIRES's calculated minimum wellhead pressure and nominally align with the gradual increase in WHP for constant flow rates modeled by Singh et al. + +Injectivity Index, 1.809, -- Based on ATB Conservative Scenario (NREL, 2025) derated by 40% per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024). +Productivity Index, 1.4964, -- See Injectivity Index + +Ramey Production Wellbore Model, True, -- Ramey's model estimates the geofluid temperature drop in production wells +Injection Temperature, 53.6, -- Calibrated with GEOPHIRES model-calculated reinjection temperature (Beckers and McCabe, 2019). Close to upper bound of Project Red injection temperatures (75–125℉; 23.89–51.67℃) (Norbeck and Latimer, 2023). Note: GEOPHIRES enforces a thermodynamic optimum that overrides higher values, such as Fervo's considered operational target of 80°C (intended for silica scaling mitigation), resulting in a "maximum theoretical power" scenario. Support for higher reinjection temperatures may be added in future GEOPHIRES versions. +Injection Wellbore Temperature Gain, 3, -- Empirical estimate for high-flow rate wells where rapid fluid velocity minimizes heat uptake during descent (Ramey, 1962). + +Maximum Drawdown, 0.0025, -- This value represents the fractional drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is calibrated to maintain the PPA minimum net electricity generation requirement of 500 MWe. It is a very small percentage because it is relative to the initial production temperature; the temperature quickly rises higher due to thermal conditioning and plateaus until breakthrough, so any drawdown relative to the initial value signals that the temperature has already declined from its stabilized peak. + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 diff --git a/tests/examples/Fervo_Project_Cape-6.out b/tests/examples/Fervo_Project_Cape-6.out new file mode 100644 index 000000000..460f97f9f --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-6.out @@ -0,0 +1,471 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.11.4 + Simulation Date: 2026-01-21 + Simulation Time: 15:25 + Calculation Time: 1.931 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 106.92 MW + Electricity breakeven price: 7.95 cents/kWh + Total CAPEX: 573.58 MUSD + Number of production wells: 12 + Number of injection wells: 8 + Flowrate per production well: 100.0 kg/sec + Well depth: 2.7 kilometer + Segment 1 Geothermal gradient: 74 degC/km + Segment 1 Thickness: 2.5 kilometer + Segment 2 Geothermal gradient: 41 degC/km + Segment 2 Thickness: 0.5 kilometer + Segment 3 Geothermal gradient: 39.1 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = SAM Single Owner PPA + Real Discount Rate: 12.00 % + Nominal Discount Rate: 15.02 % + WACC: 8.14 % + Investment Tax Credit: 172.07 MUSD + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: 71.53 MUSD + After-tax IRR: 31.96 % + Project VIR=PI=PIR: 1.54 + Project MOIC: 4.43 + Project Payback Period: 3.90 yr + Estimated Jobs Created: 255 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 12 + Number of Injection Wells: 8 + Well depth: 2.7 kilometer + Water loss rate: 1.0 % + Pump efficiency: 80.0 % + Injection temperature: 56.6 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 0.3 degC + Flowrate per production well: 100.0 kg/sec + Injection well casing ID: 8.535 in + Production well casing ID: 8.535 in + Number of times redrilling: 3 + Power plant type: Supercritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 500.0 degC + Number of segments: 3 + Segment 1 Geothermal gradient: 74 degC/km + Segment 1 Thickness: 2.5 kilometer + Segment 2 Geothermal gradient: 41 degC/km + Segment 2 Thickness: 0.5 kilometer + Segment 3 Geothermal gradient: 39.1 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 205.38 degC + Fracture model = Rectangular + Well separation: fracture height: 95.00 meter + Fracture width: 305.00 meter + Fracture area: 28975.00 m**2 + Reservoir volume calculated with fracture separation and number of fractures as input + Number of fractures: 3000 + Fracture separation: 9.83 meter + Reservoir volume: 853796894 m**3 + Reservoir hydrostatic pressure: 25324.54 kPa + Plant outlet pressure: 13789.51 kPa + Production wellhead pressure: 2089.11 kPa + Productivity Index: 1.50 kg/sec/bar + Injectivity Index: 1.81 kg/sec/bar + Reservoir density: 2800.00 kg/m**3 + Reservoir thermal conductivity: 3.05 W/m/K + Reservoir heat capacity: 790.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 203.1 degC + Average Production Temperature: 202.8 degC + Minimum Production Temperature: 201.4 degC + Initial Production Temperature: 201.6 degC + Average Reservoir Heat Extraction: 734.33 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 0.3 degC + Average Injection Well Pump Pressure Drop: -4529.3 kPa + Average Production Well Pump Pressure Drop: 7347.4 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 92.98 MUSD + Drilling and completion costs per well: 4.65 MUSD + Stimulation costs: 96.60 MUSD + Surface power plant costs: 293.44 MUSD + Field gathering system costs: 9.16 MUSD + Total surface equipment costs: 302.61 MUSD + Exploration costs: 30.00 MUSD + Overnight Capital Cost: 522.18 MUSD + Interest during construction: 12.52 MUSD + Inflation costs during construction: 38.88 MUSD + Total CAPEX: 573.58 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 1.71 MUSD/yr + Power plant maintenance costs: 6.48 MUSD/yr + Water costs: 0.63 MUSD/yr + Redrilling costs: 18.96 MUSD/yr + Average Annual Royalty Cost: 2.55 MUSD/yr + Total operating and maintenance costs: 30.33 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 119.91 MW + Average Total Electricity Generation: 119.50 MW + Minimum Total Electricity Generation: 117.38 MW + Initial Total Electricity Generation: 117.79 MW + Maximum Net Electricity Generation: 107.35 MW + Average Net Electricity Generation: 106.92 MW + Minimum Net Electricity Generation: 104.74 MW + Initial Net Electricity Generation: 105.19 MW + Average Annual Total Electricity Generation: 942.13 GWh + Average Annual Net Electricity Generation: 843.00 GWh + Initial pumping power/net installed power: 11.98 % + Average Pumping Power: 12.57 MW + Heat to Power Conversion Efficiency: 14.56 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 201.65 12.5977 105.1935 14.4422 + 2 1.0052 202.70 12.5893 106.7137 14.5454 + 3 1.0062 202.90 12.5877 106.9965 14.5645 + 4 1.0067 203.00 12.5869 107.1437 14.5744 + 5 1.0070 203.06 12.5864 107.2410 14.5809 + 6 1.0073 203.11 12.5861 107.3094 14.5855 + 7 1.0072 203.10 12.5875 107.2883 14.5840 + 8 1.0049 202.63 12.6034 106.5979 14.5366 + 9 1.0027 202.20 12.5846 105.9991 14.4976 + 10 1.0055 202.77 12.5831 106.8152 14.5526 + 11 1.0063 202.93 12.5800 107.0480 14.5684 + 12 1.0068 203.02 12.5756 107.1824 14.5777 + 13 1.0071 203.08 12.5707 107.2763 14.5843 + 14 1.0073 203.12 12.5664 107.3395 14.5889 + 15 1.0070 203.06 12.5664 107.2499 14.5829 + 16 1.0032 202.30 12.5902 106.1324 14.5062 + 17 1.0041 202.47 12.5599 106.4141 14.5273 + 18 1.0058 202.82 12.5594 106.9132 14.5608 + 19 1.0065 202.95 12.5593 107.1070 14.5738 + 20 1.0069 203.03 12.5592 107.2239 14.5816 + 21 1.0072 203.09 12.5593 107.3058 14.5871 + 22 1.0073 203.12 12.5597 107.3503 14.5901 + 23 1.0066 202.98 12.5655 107.1389 14.5755 + 24 1.0008 201.80 12.6049 105.4108 14.4564 + 25 1.0048 202.61 12.5594 106.6135 14.5407 + 26 1.0060 202.86 12.5594 106.9739 14.5649 + 27 1.0066 202.98 12.5594 107.1408 14.5761 + 28 1.0070 203.05 12.5594 107.2467 14.5831 + 29 1.0072 203.10 12.5595 107.3219 14.5882 + 30 1.0073 203.12 12.5604 107.3423 14.5895 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 837.4 5770.5 260.21 7.39 + 2 842.6 5788.6 239.37 14.81 + 3 844.2 5794.1 218.51 22.23 + 4 845.1 5797.3 197.64 29.66 + 5 845.8 5799.6 176.77 37.09 + 6 846.1 5800.7 155.88 44.52 + 7 844.0 5793.7 135.03 51.95 + 8 833.1 5756.1 114.30 59.32 + 9 839.9 5779.1 93.50 66.72 + 10 843.2 5790.3 72.65 74.14 + 11 844.5 5795.0 51.79 81.57 + 12 845.4 5798.0 30.92 89.00 + 13 846.0 5800.0 10.04 96.43 + 14 846.1 5800.3 -10.84 103.86 + 15 842.5 5787.7 -31.68 111.27 + 16 833.0 5754.9 -52.40 118.65 + 17 841.4 5783.5 -73.22 126.06 + 18 843.7 5791.8 -94.07 133.48 + 19 844.9 5795.9 -114.93 140.90 + 20 845.7 5798.6 -135.81 148.33 + 21 846.2 5800.4 -156.69 155.76 + 22 845.9 5799.3 -177.57 163.19 + 23 839.7 5778.2 -198.37 170.60 + 24 834.5 5759.7 -219.10 177.98 + 25 842.2 5786.4 -239.93 185.39 + 26 844.1 5793.0 -260.79 192.81 + 27 845.1 5796.6 -281.66 200.24 + 28 845.8 5799.1 -302.53 207.67 + 29 846.3 5800.6 -323.41 215.10 + 30 845.5 5797.9 -344.29 222.53 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Year -2 Year -1 Year 0 Year 1 Year 2 Year 3 Year 4 Year 5 Year 6 Year 7 Year 8 Year 9 Year 10 Year 11 Year 12 Year 13 Year 14 Year 15 Year 16 Year 17 Year 18 Year 19 Year 20 Year 21 Year 22 Year 23 Year 24 Year 25 Year 26 Year 27 Year 28 Year 29 Year 30 +CONSTRUCTION +Capital expenditure schedule [construction] (%) 2.58 25.60 71.80 +Overnight capital expenditure [construction] ($) -13,488,068 -133,917,250 -374,775,614 +plus: +Inflation cost [construction] ($) -364,178 -7,329,157 -31,183,836 +equals: +Nominal capital expenditure [construction] ($) -13,852,246 -141,246,407 -405,959,450 + +Issuance of equity [construction] ($) 4,155,674 42,373,922 121,787,835 +Issuance of debt [construction] ($) 9,696,572 98,872,485 284,171,615 +Debt balance [construction] ($) 9,696,572 109,587,198 405,265,468 +Debt interest payment [construction] ($) 0 1,018,140 11,506,656 + +Installed cost [construction] ($) -13,852,246 -142,264,548 -417,466,106 +After-tax net cash flow [construction] ($) -4,155,674 -42,373,922 -121,787,835 + +ENERGY +Electricity to grid (kWh) 0.0 837,448,880 842,655,838 844,243,212 845,182,053 845,832,891 846,132,445 844,087,895 833,172,101 839,982,442 843,209,571 844,588,918 845,471,079 846,098,763 846,193,349 842,512,963 833,071,018 841,422,038 843,801,407 844,979,207 845,749,074 846,273,674 845,943,004 839,755,838 834,516,431 842,257,829 844,155,110 845,197,005 845,902,148 846,337,827 845,520,009 +Electricity from grid (kWh) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +Electricity to grid net (kWh) 0.0 837,448,880 842,655,838 844,243,212 845,182,053 845,832,891 846,132,445 844,087,895 833,172,101 839,982,442 843,209,571 844,588,918 845,471,079 846,098,763 846,193,349 842,512,963 833,071,018 841,422,038 843,801,407 844,979,207 845,749,074 846,273,674 845,943,004 839,755,838 834,516,431 842,257,829 844,155,110 845,197,005 845,902,148 846,337,827 845,520,009 + +REVENUE +PPA price (cents/kWh) 0.0 9.50 9.50 9.56 9.61 9.67 9.73 9.79 9.84 9.90 9.96 10.01 10.07 10.13 10.18 10.24 10.30 10.36 10.41 10.47 10.53 10.58 10.64 10.70 10.75 10.81 10.87 10.93 10.98 11.04 11.10 +PPA revenue ($) 0 79,557,644 80,052,305 80,684,324 81,255,803 81,800,499 82,311,764 82,594,001 82,000,798 83,149,862 83,949,945 84,568,688 85,138,938 85,684,422 86,176,331 86,281,753 85,789,653 87,129,252 87,856,602 88,460,873 89,023,548 89,561,143 90,008,336 89,828,682 89,743,897 91,056,494 91,742,777 92,337,773 92,896,974 93,427,233 93,818,900 +Curtailment payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Capacity payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 286,791,450 +Total revenue ($) 0 79,557,644 80,052,305 80,684,324 81,255,803 81,800,499 82,311,764 82,594,001 82,000,798 83,149,862 83,949,945 84,568,688 85,138,938 85,684,422 86,176,331 86,281,753 85,789,653 87,129,252 87,856,602 88,460,873 89,023,548 89,561,143 90,008,336 89,828,682 89,743,897 91,056,494 91,742,777 92,337,773 92,896,974 93,427,233 380,610,350 + +Property tax net assessed value ($) 0 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 573,582,899 + +OPERATING EXPENSES +O&M fixed expense ($) 0 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 27,777,560 +Royalty rate (%) 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 1.75 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 3.50 +O&M production-based expense ($) 0 1,392,259 1,400,915 1,411,976 1,421,977 1,431,509 1,440,456 1,445,395 1,435,014 1,455,123 1,469,124 2,959,904 2,979,863 2,998,955 3,016,172 3,019,861 3,002,638 3,049,524 3,074,981 3,096,131 3,115,824 3,134,640 3,150,292 3,144,004 3,141,036 3,186,977 3,210,997 3,231,822 3,251,394 3,269,953 3,283,662 +O&M capacity-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Fuel expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Electricity purchase ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Property tax expense ($) 0 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 1,261,882 +Insurance expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total operating expenses ($) 0 30,431,702 30,440,358 30,451,419 30,461,419 30,470,952 30,479,899 30,484,838 30,474,457 30,494,565 30,508,567 31,999,347 32,019,306 32,038,398 32,055,614 32,059,304 32,042,081 32,088,967 32,114,424 32,135,573 32,155,267 32,174,083 32,189,735 32,183,447 32,180,479 32,226,420 32,250,440 32,271,265 32,290,837 32,309,396 32,323,104 + +EBITDA ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 + +OPERATING ACTIVITIES +EBITDA ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +plus PBI if not available for debt service: +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 28,368,583 28,068,262 27,746,918 27,403,080 27,035,173 26,641,514 26,220,297 25,769,596 25,287,346 24,771,338 24,219,210 23,628,432 22,996,300 22,319,919 21,596,192 20,821,803 19,993,207 19,106,610 18,157,950 17,142,885 16,056,765 14,894,616 13,651,117 12,320,573 10,896,891 9,373,552 7,743,578 5,999,507 4,133,350 2,136,563 +Cash flow from operating activities ($) 0 20,757,359 21,543,685 22,485,987 23,391,303 24,294,374 25,190,352 25,888,865 25,756,745 27,367,951 28,670,040 28,350,132 29,491,200 30,649,724 31,800,797 32,626,257 32,925,770 35,047,078 36,635,569 38,167,349 39,725,396 41,330,295 42,923,985 43,994,118 45,242,844 47,933,182 50,118,785 52,322,930 54,606,630 56,984,486 346,150,683 + +INVESTING ACTIVITIES +Total installed cost ($) -573,582,899 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -573,582,899 +plus: +Reserve (increase)/decrease debt service ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease working capital ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease receivables ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash flow from investing activities ($) -573,582,899 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +FINANCING ACTIVITIES +Issuance of equity ($) 168,317,431 +Size of debt ($) 405,265,468 +minus: +Debt principal payment ($) 0 4,290,304 4,590,625 4,911,969 5,255,807 5,623,713 6,017,373 6,438,589 6,889,290 7,371,541 7,887,549 8,439,677 9,030,454 9,662,586 10,338,967 11,062,695 11,837,083 12,665,679 13,552,277 14,500,936 15,516,002 16,602,122 17,764,270 19,007,769 20,338,313 21,761,995 23,285,335 24,915,308 26,659,380 28,525,536 30,522,324 +equals: +Cash flow from financing activities ($) 573,582,899 -4,290,304 -4,590,625 -4,911,969 -5,255,807 -5,623,713 -6,017,373 -6,438,589 -6,889,290 -7,371,541 -7,887,549 -8,439,677 -9,030,454 -9,662,586 -10,338,967 -11,062,695 -11,837,083 -12,665,679 -13,552,277 -14,500,936 -15,516,002 -16,602,122 -17,764,270 -19,007,769 -20,338,313 -21,761,995 -23,285,335 -24,915,308 -26,659,380 -28,525,536 -30,522,324 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 20,757,359 21,543,685 22,485,987 23,391,303 24,294,374 25,190,352 25,888,865 25,756,745 27,367,951 28,670,040 28,350,132 29,491,200 30,649,724 31,800,797 32,626,257 32,925,770 35,047,078 36,635,569 38,167,349 39,725,396 41,330,295 42,923,985 43,994,118 45,242,844 47,933,182 50,118,785 52,322,930 54,606,630 56,984,486 346,150,683 +Cash flow from investing activities ($) -573,582,899 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Cash flow from financing activities ($) 573,582,899 -4,290,304 -4,590,625 -4,911,969 -5,255,807 -5,623,713 -6,017,373 -6,438,589 -6,889,290 -7,371,541 -7,887,549 -8,439,677 -9,030,454 -9,662,586 -10,338,967 -11,062,695 -11,837,083 -12,665,679 -13,552,277 -14,500,936 -15,516,002 -16,602,122 -17,764,270 -19,007,769 -20,338,313 -21,761,995 -23,285,335 -24,915,308 -26,659,380 -28,525,536 -30,522,324 +Total pre-tax cash flow ($) 0 16,467,055 16,953,060 17,574,019 18,135,497 18,670,661 19,172,979 19,450,276 18,867,455 19,996,410 20,782,491 19,910,455 20,460,745 20,987,138 21,461,830 21,563,562 21,088,686 22,381,399 23,083,292 23,666,413 24,209,394 24,728,174 25,159,714 24,986,349 24,904,531 26,171,187 26,833,451 27,407,621 27,947,250 28,458,950 315,628,359 + +Pre-tax Returns: +Issuance of equity ($) 168,317,431 +Total pre-tax cash flow ($) 0 16,467,055 16,953,060 17,574,019 18,135,497 18,670,661 19,172,979 19,450,276 18,867,455 19,996,410 20,782,491 19,910,455 20,460,745 20,987,138 21,461,830 21,563,562 21,088,686 22,381,399 23,083,292 23,666,413 24,209,394 24,728,174 25,159,714 24,986,349 24,904,531 26,171,187 26,833,451 27,407,621 27,947,250 28,458,950 315,628,359 +Total pre-tax returns ($) -168,317,431 16,467,055 16,953,060 17,574,019 18,135,497 18,670,661 19,172,979 19,450,276 18,867,455 19,996,410 20,782,491 19,910,455 20,460,745 20,987,138 21,461,830 21,563,562 21,088,686 22,381,399 23,083,292 23,666,413 24,209,394 24,728,174 25,159,714 24,986,349 24,904,531 26,171,187 26,833,451 27,407,621 27,947,250 28,458,950 315,628,359 + +After-tax Returns: +Total pre-tax returns ($) -168,317,431 16,467,055 16,953,060 17,574,019 18,135,497 18,670,661 19,172,979 19,450,276 18,867,455 19,996,410 20,782,491 19,910,455 20,460,745 20,987,138 21,461,830 21,563,562 21,088,686 22,381,399 23,083,292 23,666,413 24,209,394 24,728,174 25,159,714 24,986,349 24,904,531 26,171,187 26,833,451 27,407,621 27,947,250 28,458,950 315,628,359 +Federal ITC total income ($) 0 172,074,870 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal tax benefit (liability) ($) 0 -1,717,558 567,979 379,099 197,633 16,617 -162,978 -302,991 -276,508 -599,466 -860,464 -796,340 -1,025,061 -1,257,281 -1,488,008 -1,653,467 -1,713,503 -2,138,709 -2,457,114 -2,764,152 -3,076,454 -5,841,300 -8,603,898 -8,818,401 -9,068,702 -9,607,967 -10,046,060 -10,487,870 -10,945,626 -11,422,255 -69,384,174 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -389,877 128,928 86,054 44,862 3,772 -36,995 -68,777 -62,766 -136,076 -195,321 -180,765 -232,684 -285,396 -337,770 -375,329 -388,957 -485,476 -557,752 -627,448 -698,340 -1,325,945 -1,953,041 -2,001,732 -2,058,549 -2,180,960 -2,280,405 -2,380,693 -2,484,602 -2,592,794 -15,749,856 +Total after-tax returns ($) -168,317,431 186,434,491 17,649,967 18,039,171 18,377,991 18,691,049 18,973,006 19,078,508 18,528,181 19,260,868 19,726,707 18,933,350 19,203,001 19,444,460 19,636,051 19,534,766 18,986,226 19,757,214 20,068,425 20,274,813 20,434,600 17,560,928 14,602,775 14,166,215 13,777,280 14,382,261 14,506,986 14,539,058 14,517,023 14,443,901 230,494,329 + +After-tax net cash flow ($) -4,155,674 -42,373,922 -121,787,835 186,434,491 17,649,967 18,039,171 18,377,991 18,691,049 18,973,006 19,078,508 18,528,181 19,260,868 19,726,707 18,933,350 19,203,001 19,444,460 19,636,051 19,534,766 18,986,226 19,757,214 20,068,425 20,274,813 20,434,600 17,560,928 14,602,775 14,166,215 13,777,280 14,382,261 14,506,986 14,539,058 14,517,023 14,443,901 230,494,329 +After-tax cumulative IRR (%) NaN NaN NaN 8.11 14.75 19.75 23.31 25.80 27.55 28.77 29.61 30.23 30.69 31.01 31.25 31.43 31.56 31.66 31.73 31.79 31.83 31.87 31.89 31.91 31.92 31.92 31.93 31.93 31.94 31.94 31.94 31.94 31.96 +After-tax cumulative NPV ($) -4,155,674 -40,994,875 -133,045,550 -10,538,561 -455,555 8,503,748 16,439,119 23,455,520 29,647,484 35,060,611 39,630,947 43,761,448 47,439,289 50,508,152 53,214,169 55,596,318 57,687,726 59,496,583 61,025,015 62,407,769 63,628,849 64,701,353 65,641,119 66,343,241 66,850,830 67,278,927 67,640,890 67,969,392 68,257,464 68,508,462 68,726,345 68,914,815 71,529,560 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -168,317,431 106,876,847 -62,402,338 -62,645,153 -62,877,812 -63,109,449 -63,338,758 -63,515,493 -63,472,618 -63,888,994 -64,223,238 -65,635,338 -65,935,937 -66,239,962 -66,540,279 -66,746,987 -66,803,427 -67,372,038 -67,788,177 -68,186,060 -68,588,948 -72,000,215 -75,405,561 -75,662,467 -75,966,617 -76,674,233 -77,235,791 -77,798,714 -78,379,951 -78,983,332 136,675,429 +PPA revenue ($) 0 79,557,644 80,052,305 80,684,324 81,255,803 81,800,499 82,311,764 82,594,001 82,000,798 83,149,862 83,949,945 84,568,688 85,138,938 85,684,422 86,176,331 86,281,753 85,789,653 87,129,252 87,856,602 88,460,873 89,023,548 89,561,143 90,008,336 89,828,682 89,743,897 91,056,494 91,742,777 92,337,773 92,896,974 93,427,233 93,818,900 +Electricity to grid (kWh) 0.0 837,448,880 842,655,838 844,243,212 845,182,053 845,832,891 846,132,445 844,087,895 833,172,101 839,982,442 843,209,571 844,588,918 845,471,079 846,098,763 846,193,349 842,512,963 833,071,018 841,422,038 843,801,407 844,979,207 845,749,074 846,273,674 845,943,004 839,755,838 834,516,431 842,257,829 844,155,110 845,197,005 845,902,148 846,337,827 845,520,009 + +Present value of annual costs ($) 439,219,680 +Present value of annual energy nominal (kWh) 5,523,896,792 +LCOE Levelized cost of energy nominal (cents/kWh) 7.95 + +Present value of PPA revenue ($) 541,565,767 +Present value of annual energy nominal (kWh) 5,523,896,792 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 +State taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State taxable IBI income ($) 0 +State taxable CBI income ($) 0 +minus: +Debt interest payment ($) 0 28,368,583 28,068,262 27,746,918 27,403,080 27,035,173 26,641,514 26,220,297 25,769,596 25,287,346 24,771,338 24,219,210 23,628,432 22,996,300 22,319,919 21,596,192 20,821,803 19,993,207 19,106,610 18,157,950 17,142,885 16,056,765 14,894,616 13,651,117 12,320,573 10,896,891 9,373,552 7,743,578 5,999,507 4,133,350 2,136,563 +Total state tax depreciation ($) 0 12,188,637 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 12,188,637 0 0 0 0 0 0 0 0 0 +equals: +State taxable income ($) 0 8,568,723 -2,833,588 -1,891,286 -985,970 -82,899 813,079 1,511,592 1,379,472 2,990,677 4,292,767 3,972,859 5,113,926 6,272,450 7,423,524 8,248,983 8,548,496 10,669,805 12,258,296 13,790,076 15,348,123 29,141,659 42,923,985 43,994,118 45,242,844 47,933,182 50,118,785 52,322,930 54,606,630 56,984,486 346,150,683 + +State income tax rate (frac) 0.0 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 +State tax benefit (liability) ($) 0 -389,877 128,928 86,054 44,862 3,772 -36,995 -68,777 -62,766 -136,076 -195,321 -180,765 -232,684 -285,396 -337,770 -375,329 -388,957 -485,476 -557,752 -627,448 -698,340 -1,325,945 -1,953,041 -2,001,732 -2,058,549 -2,180,960 -2,280,405 -2,380,693 -2,484,602 -2,592,794 -15,749,856 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -389,877 128,928 86,054 44,862 3,772 -36,995 -68,777 -62,766 -136,076 -195,321 -180,765 -232,684 -285,396 -337,770 -375,329 -388,957 -485,476 -557,752 -627,448 -698,340 -1,325,945 -1,953,041 -2,001,732 -2,058,549 -2,180,960 -2,280,405 -2,380,693 -2,484,602 -2,592,794 -15,749,856 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal taxable IBI income ($) 0 +Federal taxable CBI income ($) 0 +Federal taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +minus: +Debt interest payment ($) 0 28,368,583 28,068,262 27,746,918 27,403,080 27,035,173 26,641,514 26,220,297 25,769,596 25,287,346 24,771,338 24,219,210 23,628,432 22,996,300 22,319,919 21,596,192 20,821,803 19,993,207 19,106,610 18,157,950 17,142,885 16,056,765 14,894,616 13,651,117 12,320,573 10,896,891 9,373,552 7,743,578 5,999,507 4,133,350 2,136,563 +Total federal tax depreciation ($) 0 12,188,637 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 24,377,273 12,188,637 0 0 0 0 0 0 0 0 0 +equals: +Federal taxable income ($) 0 8,178,846 -2,704,660 -1,805,232 -941,108 -79,127 776,084 1,442,815 1,316,706 2,854,602 4,097,446 3,792,093 4,881,243 5,987,054 7,085,753 7,873,655 8,159,540 10,184,329 11,700,543 13,162,628 14,649,783 27,815,713 40,970,944 41,992,386 43,184,295 45,752,223 47,838,381 49,942,236 52,122,028 54,391,692 330,400,827 + +Federal income tax rate (frac) 0.0 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 +Federal tax benefit (liability) ($) 0 -1,717,558 567,979 379,099 197,633 16,617 -162,978 -302,991 -276,508 -599,466 -860,464 -796,340 -1,025,061 -1,257,281 -1,488,008 -1,653,467 -1,713,503 -2,138,709 -2,457,114 -2,764,152 -3,076,454 -5,841,300 -8,603,898 -8,818,401 -9,068,702 -9,607,967 -10,046,060 -10,487,870 -10,945,626 -11,422,255 -69,384,174 + +CASH INCENTIVES +Federal IBI income ($) 0 +State IBI income ($) 0 +Utility IBI income ($) 0 +Other IBI income ($) 0 +Total IBI income ($) 0 + +Federal CBI income ($) 0 +State CBI income ($) 0 +Utility CBI income ($) 0 +Other CBI income ($) 0 +Total CBI income ($) 0 + +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +TAX CREDITS +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Federal ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC percent income ($) 0 172,074,870 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC total income ($) 0 172,074,870 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +State ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC percent income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +DEBT REPAYMENT +Debt balance ($) 405,265,468 400,975,164 396,384,539 391,472,571 386,216,764 380,593,051 374,575,678 368,137,089 361,247,799 353,876,258 345,988,709 337,549,032 328,518,578 318,855,992 308,517,025 297,454,330 285,617,247 272,951,568 259,399,291 244,898,354 229,382,353 212,780,231 195,015,960 176,008,191 155,669,878 133,907,883 110,622,548 85,707,240 59,047,860 30,522,324 0 +Debt interest payment ($) 0 28,368,583 28,068,262 27,746,918 27,403,080 27,035,173 26,641,514 26,220,297 25,769,596 25,287,346 24,771,338 24,219,210 23,628,432 22,996,300 22,319,919 21,596,192 20,821,803 19,993,207 19,106,610 18,157,950 17,142,885 16,056,765 14,894,616 13,651,117 12,320,573 10,896,891 9,373,552 7,743,578 5,999,507 4,133,350 2,136,563 +Debt principal payment ($) 0 4,290,304 4,590,625 4,911,969 5,255,807 5,623,713 6,017,373 6,438,589 6,889,290 7,371,541 7,887,549 8,439,677 9,030,454 9,662,586 10,338,967 11,062,695 11,837,083 12,665,679 13,552,277 14,500,936 15,516,002 16,602,122 17,764,270 19,007,769 20,338,313 21,761,995 23,285,335 24,915,308 26,659,380 28,525,536 30,522,324 +Debt total payment ($) 0 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 +minus: +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash available for debt service (CAFDS) ($) 0 49,125,942 49,611,946 50,232,905 50,794,383 51,329,547 51,831,865 52,109,163 51,526,341 52,655,296 53,441,378 52,569,341 53,119,632 53,646,024 54,120,716 54,222,448 53,747,573 55,040,285 55,742,179 56,325,300 56,868,281 57,387,060 57,818,601 57,645,235 57,563,418 58,830,074 59,492,337 60,066,508 60,606,137 61,117,837 348,287,245 +Debt total payment ($) 0 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 32,658,887 +DSCR (pre-tax) 0.0 1.50 1.52 1.54 1.56 1.57 1.59 1.60 1.58 1.61 1.64 1.61 1.63 1.64 1.66 1.66 1.65 1.69 1.71 1.72 1.74 1.76 1.77 1.77 1.76 1.80 1.82 1.84 1.86 1.87 10.66 + +RESERVES +Reserves working capital funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves debt service funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves total reserves balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest on reserves (%/year) 1.75 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ***EXTENDED ECONOMICS*** + + Royalty Holder NPV: 31.43 MUSD + Royalty Holder Average Annual Revenue: 2.55 MUSD/yr + Royalty Holder Total Revenue: 76.62 MUSD diff --git a/tests/examples/Fervo_Project_Cape-6.txt b/tests/examples/Fervo_Project_Cape-6.txt new file mode 100644 index 000000000..4a5f7f5ef --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-6.txt @@ -0,0 +1,121 @@ +# Case Study: 100 MWe EGS Project Modeled on Fervo Cape Station Phase I +# See documentation: https://softwareengineerprogrammer.github.io/GEOPHIRES/Fervo_Project_Cape-5.html + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- 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, .027, -- US inflation as of December 2025 + +Starting Electricity Sale Price, 0.095, -- Aligns with Geysers - Sacramento pricing in [2024b ATB](https://atb.nrel.gov/electricity/2024/geothermal) (NREL, 2025). See Sensitivity Analysis for effect of different prices on results. +Electricity Escalation Rate Per Year, 0.00057, -- Calibrated to reach $100/MWh at project year 11 +Ending Electricity Sale Price, 1, -- Note that this value does not directly determine price at the end of the project life, but rather as a cap as the maximum price to which the starting price can escalate. +Electricity Escalation Start Year, 1 + +Fraction of Investment in Bonds, .7, -- Approximate debt required to cover CAPEX after $1 billion sponsor equity per [Matson, 2024](https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/). 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 precisely. +Discount Rate, 0.12, -- Typical discount rates for higher-risk projects may be 12–15%. +Inflated Bond Interest Rate, .07, -- 2024b ATB (NREL, 2025) + +Inflated Bond Interest Rate During Construction, 0.105, -- Higher than interest rate during normal operation to account for increased risk of default prior to COD. Value aligns with ATB discount rate (NREL, 2025). +Bond Financing Start Year, -2, -- Equity-only for first 2 construction years (ATB) + +Construction Years, 3 + +# ATB advanced scenario +# Construction CAPEX Schedule, 0.09,0.28,0.1,0.34,0.28 + +# DOE scenario (alternative) +# Construction CAPEX Schedule, 0.014,0.027,0.137,0.274,0.548 + +# DOE-ATB hybrid scenario +Construction CAPEX Schedule, 0.014,0.027,0.139,0.431,0.389 + +Investment Tax Credit Rate, 0.3, -- Geothermal Drilling and Completions Apprenticeship Program ensures compliance with ITC labor requirements (Southern Utah University, 2024). +Combined Income Tax Rate, .2555, -- Federal Corporate Income Tax Rate of 21% plus Utah Corporate Franchise and Income Tax Rate of 4.55%. (Note: This input uses a simple summation of statutory rates; the effective combined rate calculated in the model may differ due to standard federal-state tax interactions.) +Property Tax Rate, 0.0022, -- Utah Inland Port Authority (UIPA) tax differential incentive + +Capital Cost for Power Plant for Electricity Generation, 1900, -- [US DOE, 2021](https://betterbuildingssolutioncenter.energy.gov/sites/default/files/attachments/Waste_Heat_to_Power_Fact_Sheet.pdf). Pricing information not publicly available for Turboden or Baker Hughes Gen 2 ORC units (Turboden, 2025; Jacobs, 2025). +Exploration Capital Cost, 30, -- Equivalent to 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025), plus $1M for geophysical and field work, plus 15% contingency, plus 12% indirect costs. + +Well Drilling Cost Correlation, 3, -- 2025 NREL Geothermal Drilling Cost Curve Update (Akindipe and Witter, 2025). +Well Drilling and Completion Capital Cost Adjustment Factor, 0.9, -- 2024b Geothermal ATB ([NREL, 2025](https://atb.nrel.gov/electricity/2024b/geothermal)). Note: Fervo has claimed lower drilling costs equivalent to an adjustment factor of 0.8 (Latimer, 2025); the case study conservatively uses the higher ATB-aligned value. + +Reservoir Stimulation Capital Cost per Injection Well, 4, -- The baseline stimulation cost is calibrated from costs of high-intensity U.S. shale wells (Baytex Energy, 2024; Quantum Proppant Technologies, 2020), which are the closest technological analogue for multi-stage EGS (Gradl, 2018). Costs are also driven by the requirement for high-strength ceramic proppant rather than standard sand, which would crush or chemically degrade (diagenesis) over a 30-year lifecycle at 200℃ (Ko et al., 2023; Shiozawa and McClure, 2014) and the premium for ultra-high-temperature (HT) downhole tools. Note that all-in costs per well are higher than the baseline cost because they include additional indirect costs and contingency. +Reservoir Stimulation Capital Cost per Production Well, 4, -- See Reservoir Stimulation Capital Cost per Injection Well + +Field Gathering System Capital Cost Adjustment Factor, 0.54, -- Gathering costs represent 2% of facilities CAPEX per [Matson, 2024](https://www.linkedin.com/pulse/fervo-energy-technology-day-2024-entering-geothermal-decade-matson-n4stc/). + +Royalty Rate, 0.0175, -- The BLM royalty structure is 1.75% of gross proceeds from electricity sales for the first 10 years of production (Code of Federal Regulations, 2024). +Royalty Rate Escalation Start Year, 11, -- After the first 10 years of production, the royalty rate escalates to 3.5%. +Royalty Rate Escalation, 0.0175, -- Escalation at Year 11 from 1.75% to 3.5%. +Royalty Rate Maximum, 0.035, -- No further escalation beyond 3.5%. + + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Gen 2 ORC units (Turboden, 2025). +Plant Lifetime, 30, -- Sets the project economic horizon, aligned with Fervo's anticipated 30-year well life (Fervo Energy, 2025). Modeling Distinction: While Fervo projects physical wellbore integrity for 30 years, GEOPHIRES simulates "redrilling events" to model thermal management of the reservoir volume. This treats the 30-year lifespan as an aggregate of shorter-lived thermal cycles delineated by discrete redrilling events occurring at intervals dictated by the Maximum Drawdown parameter. The modeled cost of each redrilling event is equivalent to the drilling and stimulation cost of the entire wellfield, serving as a conservative cost proxy for the major interventions (e.g., sidetracking and stimulating laterals into fresh rock, or drilling new wells if necessary) required to sustain the 500 MW PPA target against thermal depletion. + +Reservoir Model, 1 + +Surface Temperature, 13, -- Surface temperature near Milford, UT (38.4987670, -112.9163432) ([Project InnerSpace, 2025](https://geomap.projectinnerspace.org/test/)). + +Number of Segments, 3 +Gradient 1, 74, -- Sedimentary overburden. 200℃ at 8500 ft depth (Fercho et al. 2024); 228.89℃ at 9824 ft (Norbeck et al. 2024). +Thickness 1, 2.5 +Gradient 2, 41, -- Crystalline reservoir +Thickness 2, 0.5 +Gradient 3, 39.1, -- Sugarloaf appraisal + +Reservoir Depth, 2.68, -- Extrapolated from surface temperature, gradient, and average production temperature of shallower and deeper producers in Singh et al., 2025. + +Reservoir Density, 2800, -- phyllite + quartzite + diorite + granodiorite ([Norbeck et al., 2023](https://doi.org/10.31223/X52X0B)) +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 + +Reservoir Volume Option, 1, -- FRAC_NUM_SEP: Reservoir volume calculated with fracture separation and number of fractures as input + +Number of Fractures per Stimulated Well, 150, -- The model assumes an Extreme Limited Entry stimulation design (Fervo Energy, 2023) utilizing 12 stages with 15 clusters per stage (derived from Singh et al., 2025) and 81–85% stimulation success rate per 2024b ATB Moderate Scenario (NREL, 2025). +Fracture Separation, 9.8255, -- Based on 30 foot cluster spacing (Singh et al., 2025) marginally uprated to align with long-term thermal decline behavior trend towards wider fracture spacing (Fercho et al., 2025). + +Fracture Shape, 4, -- Bench design and fracture geometry in Singh et al., 2025 are given in rectangular dimensions. +Fracture Width, 305, -- Matches intra-bench well spacing of 500 ft (corresponding to fracture length of 1000 ft) (Singh. et al., 2025) +Fracture Height, 95, -- Actual fracture geometry is irregular and heterogeneous; this height complies with the minimum height required by the implemented bench design (200 ft; 60.96 meters) and yields an effective fracture surface area consistent with simulation results in Singh. et al., 2025. + +Water Loss Fraction, 0.01, -- "Long-term modeling, calibrated to early field data, predicts circulation recapture rates exceeding 99%" ([Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/); Fervo Energy, 2025). Modeling in Singh et al., 2025 predicts fluid loss of 0.36% to 0.49%. +Water Cost Adjustment Factor, 2, -- Local scarcity may increase procurement costs. Development near/on land with active/shut-in oil and gas wells could potentially utilize waste water to recover losses and offset costs. + +Ambient Temperature, 11.17, -- Average annual temperature of Milford, Utah ([NCEI](https://www.ncei.noaa.gov/access/us-climate-normals/#dataset=normals-annualseasonal&timeframe=30&station=USC00425654)). Note that this value affects heat to power conversion efficiency. The effects of hourly and seasonal ambient temperature fluctuations on efficiency and power generation are not modeled in this version of the case study. + +Utilization Factor, .9 +Plant Outlet Pressure, 2000 psi, -- McClure, 2024; Singh et al., 2025. +Circulation Pump Efficiency, 0.80 + +# *** Well Bores Parameters *** + +Number of Production Wells, 12 +Number of Injection Wells per Production Well, 0.666, -- Modeled on the reference case 5-well bench pattern (3 producers : 2 injectors) described in Singh et al., 2025. + +Nonvertical Length per Multilateral Section, 5000 feet, -- Target lateral length given in environmental assessment (BLM, 2024). Note that lateral length is assumed to be an upper bound constraining the number of fractures per well for a given cluster spacing. +Number of Multilateral Sections, 0, -- This parameter is set to 0 because, for this case study, the cost of horizontal drilling is included within the 'vertical drilling cost.' This approach allows us to more directly convey the overall well drilling and completion cost. + +Production Flow Rate per Well, 100 +Production Well Diameter, 8.535, -- Inner diameter of 9⅝ inch casing size, the next standard casing size up from 7 inches, implied by announcement of “increasing casing diameter” (Fervo Energy, 2025). +Injection Well Diameter, 8.535, -- See Production Well Diameter + +Production Wellhead Pressure, 303 psi, -- Modeled at a constant 300 psi in Singh et al., 2025. We use a marginally uprated value to conform to GEOPHIRES's calculated minimum wellhead pressure and nominally align with the gradual increase in WHP for constant flow rates modeled by Singh et al. + +Injectivity Index, 1.809, -- Based on ATB Conservative Scenario (NREL, 2025) derated by 40% per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024). +Productivity Index, 1.4964, -- See Injectivity Index + +Ramey Production Wellbore Model, True, -- Ramey's model estimates the geofluid temperature drop in production wells +Injection Temperature, 53.6, -- Calibrated with GEOPHIRES model-calculated reinjection temperature (Beckers and McCabe, 2019). Close to upper bound of Project Red injection temperatures (75–125℉; 23.89–51.67℃) (Norbeck and Latimer, 2023). Note: GEOPHIRES enforces a thermodynamic optimum that overrides higher values, such as Fervo's considered operational target of 80°C (intended for silica scaling mitigation), resulting in a "maximum theoretical power" scenario. Support for higher reinjection temperatures may be added in future GEOPHIRES versions. +Injection Wellbore Temperature Gain, 3, -- Empirical estimate for high-flow rate wells where rapid fluid velocity minimizes heat uptake during descent (Ramey, 1962). + +Maximum Drawdown, 0.0025, -- This value represents the fractional drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is calibrated to maintain the PPA minimum net electricity generation requirement of 100 MWe. It is a very small percentage because it is relative to the initial production temperature; the temperature quickly rises higher due to thermal conditioning and plateaus until breakthrough, so any drawdown relative to the initial value signals that the temperature has already declined from its stabilized peak. + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 diff --git a/tests/examples/example_SAM-single-owner-PPA-2.out b/tests/examples/example_SAM-single-owner-PPA-2.out index 6bd01e4ff..99b69f05a 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.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.013 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:42 + Calculation Time: 1.293 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 7.00 % Nominal Discount Rate: 9.14 % WACC: 6.41 % + Investment Tax Credit: 482.83 MUSD Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 2877.00 MUSD After-tax IRR: 59.73 % Project VIR=PI=PIR: 4.58 Project MOIC: 9.59 - Project Payback Period: 1.13 yr + Project Payback Period: 2.13 yr Estimated Jobs Created: 976 ***ENGINEERING PARAMETERS*** diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out index 85e785dac..27f23e159 100644 --- a/tests/examples/example_SAM-single-owner-PPA-3.out +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.189 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:42 + Calculation Time: 1.702 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 8.00 % Nominal Discount Rate: 10.16 % WACC: 7.57 % + Investment Tax Credit: 82.64 MUSD Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 210.63 MUSD After-tax IRR: 30.00 % Project VIR=PI=PIR: 2.27 Project MOIC: 4.61 - Project Payback Period: 2.94 yr + Project Payback Period: 3.94 yr Estimated Jobs Created: 125 ***ENGINEERING PARAMETERS*** diff --git a/tests/examples/example_SAM-single-owner-PPA-4.out b/tests/examples/example_SAM-single-owner-PPA-4.out index f7e2cfebe..f514996af 100644 --- a/tests/examples/example_SAM-single-owner-PPA-4.out +++ b/tests/examples/example_SAM-single-owner-PPA-4.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.205 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:42 + Calculation Time: 1.265 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 8.00 % Nominal Discount Rate: 10.16 % WACC: 7.57 % + Investment Tax Credit: 67.74 MUSD Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 103.00 MUSD After-tax IRR: 22.13 % Project VIR=PI=PIR: 1.76 Project MOIC: 3.38 - Project Payback Period: 4.29 yr + Project Payback Period: 5.29 yr Estimated Jobs Created: 125 ***ENGINEERING PARAMETERS*** @@ -434,7 +435,6 @@ Interest earned on reserves ($) 0 0 0 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ***EXTENDED ECONOMICS*** Royalty Holder NPV: 50.59 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-5.out b/tests/examples/example_SAM-single-owner-PPA-5.out index 7d3ac7f2c..6ec8f2d85 100644 --- a/tests/examples/example_SAM-single-owner-PPA-5.out +++ b/tests/examples/example_SAM-single-owner-PPA-5.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.769 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:42 + Calculation Time: 2.272 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 8.00 % Nominal Discount Rate: 10.48 % WACC: 7.25 % + Investment Tax Credit: 213.19 MUSD Project lifetime: 30 yr Capacity factor: 90.0 % Project NPV: 108.32 MUSD After-tax IRR: 16.54 % Project VIR=PI=PIR: 1.58 Project MOIC: 5.72 - Project Payback Period: 8.92 yr + Project Payback Period: 9.92 yr Estimated Jobs Created: 250 ***ENGINEERING PARAMETERS*** @@ -69,9 +70,9 @@ Simulation Metadata Well separation: fracture height: 500.00 meter Fracture area: 250000.00 m**2 Reservoir volume calculated with fracture separation and number of fractures as input - Number of fractures: 1663 + Number of fractures: 1650 Fracture separation: 26.00 meter - Reservoir volume: 10803000000 m**3 + Reservoir volume: 10718500000 m**3 Reservoir hydrostatic pressure: 24578.69 kPa Plant outlet pressure: 6894.76 kPa Production wellhead pressure: 2240.80 kPa @@ -179,36 +180,36 @@ Simulation Metadata YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED (GWh/year) (GWh/year) (10^15 J) (%) - 1 859.5 5629.4 3310.87 0.61 - 2 865.7 5651.2 3290.53 1.22 - 3 867.6 5657.8 3270.16 1.83 - 4 868.8 5661.7 3249.78 2.44 - 5 869.6 5664.5 3229.38 3.05 - 6 870.2 5666.6 3208.98 3.67 - 7 870.7 5668.3 3188.58 4.28 - 8 871.1 5669.6 3168.17 4.89 - 9 871.4 5670.8 3147.75 5.51 - 10 871.7 5671.9 3127.33 6.12 - 11 872.0 5672.8 3106.91 6.73 - 12 872.2 5673.6 3086.49 7.34 - 13 872.4 5674.4 3066.06 7.96 - 14 872.6 5675.0 3045.63 8.57 - 15 872.8 5675.6 3025.20 9.18 - 16 872.9 5676.2 3004.76 9.80 - 17 873.1 5676.7 2984.33 10.41 - 18 873.2 5677.2 2963.89 11.02 - 19 873.4 5677.7 2943.45 11.64 - 20 873.5 5678.1 2923.01 12.25 - 21 873.6 5678.5 2902.56 12.87 - 22 873.7 5678.9 2882.12 13.48 - 23 873.8 5679.3 2861.67 14.09 - 24 873.9 5679.6 2841.23 14.71 - 25 874.0 5679.9 2820.78 15.32 - 26 874.1 5680.2 2800.33 15.93 - 27 874.2 5680.5 2779.88 16.55 - 28 874.3 5680.8 2759.43 17.16 - 29 874.4 5681.1 2738.98 17.78 - 30 874.4 5681.4 2718.53 18.39 + 1 859.5 5629.4 3284.81 0.61 + 2 865.7 5651.2 3264.47 1.23 + 3 867.6 5657.8 3244.10 1.84 + 4 868.8 5661.7 3223.72 2.46 + 5 869.6 5664.5 3203.33 3.08 + 6 870.2 5666.6 3182.93 3.70 + 7 870.7 5668.3 3162.52 4.31 + 8 871.1 5669.6 3142.11 4.93 + 9 871.4 5670.8 3121.70 5.55 + 10 871.7 5671.9 3101.28 6.17 + 11 872.0 5672.8 3080.86 6.78 + 12 872.2 5673.6 3060.43 7.40 + 13 872.4 5674.4 3040.00 8.02 + 14 872.6 5675.0 3019.57 8.64 + 15 872.8 5675.6 2999.14 9.26 + 16 872.9 5676.2 2978.71 9.87 + 17 873.1 5676.7 2958.27 10.49 + 18 873.2 5677.2 2937.83 11.11 + 19 873.4 5677.7 2917.39 11.73 + 20 873.5 5678.1 2896.95 12.35 + 21 873.6 5678.5 2876.51 12.97 + 22 873.7 5678.9 2856.06 13.59 + 23 873.8 5679.3 2835.62 14.20 + 24 873.9 5679.6 2815.17 14.82 + 25 874.0 5679.9 2794.72 15.44 + 26 874.1 5680.2 2774.28 16.06 + 27 874.2 5680.5 2753.83 16.68 + 28 874.3 5680.8 2733.37 17.30 + 29 874.4 5681.1 2712.92 17.92 + 30 874.4 5681.4 2692.47 18.54 *************************** * SAM CASH FLOW PROFILE * @@ -232,9 +233,9 @@ Installed cost [construction] ($) -6,132,082 -12,546,240 -44,921 After-tax net cash flow [construction] ($) -6,132,082 -12,546,240 -44,921,812 -22,977,507 -47,011,979 -48,093,255 -98,398,799 ENERGY -Electricity to grid (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,192 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 +Electricity to grid (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,191 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 Electricity from grid (kWh) 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -Electricity to grid net (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,192 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 +Electricity to grid net (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,191 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 REVENUE PPA price (cents/kWh) 0.0 8.0 8.0 8.32 8.64 8.97 9.29 9.61 9.93 10.25 10.58 10.90 11.22 11.54 11.86 12.19 12.51 12.83 13.15 13.47 13.80 14.12 14.44 14.76 15.08 15.41 15.73 16.05 16.37 16.69 17.02 @@ -328,14 +329,14 @@ After-tax cumulative NPV ($) -6,132,082 -17,487,790 -54,288 AFTER-TAX LCOE AND PPA PRICE Annual costs ($) -280,081,674 163,885,873 -45,513,073 -46,385,473 -47,249,566 -48,114,308 -48,983,020 -49,857,494 -50,738,953 -51,628,379 -52,526,647 -53,434,591 -54,353,042 -55,282,847 -56,224,884 -57,180,074 -58,149,386 -59,133,850 -60,134,558 -61,152,671 -62,189,430 -67,252,394 -72,336,738 -73,437,733 -74,563,229 -75,714,948 -76,894,734 -78,104,556 -79,346,524 -80,622,890 179,112,172 PPA revenue ($) 0 68,759,205 69,260,654 72,207,619 75,099,377 77,968,011 80,824,249 83,672,930 86,516,616 89,356,814 92,194,478 95,030,242 97,864,549 100,697,719 103,529,986 106,361,530 109,192,487 112,022,966 114,853,053 117,682,815 120,512,311 123,341,585 126,170,676 128,999,615 131,828,431 134,657,144 137,485,776 140,314,341 143,142,854 145,971,328 148,799,249 -Electricity to grid (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,192 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 +Electricity to grid (kWh) 0.0 859,490,068 865,758,172 867,671,460 868,803,529 869,596,370 870,200,786 870,686,054 871,089,570 871,433,728 871,732,959 871,997,081 872,233,061 872,446,015 872,639,805 872,817,414 872,981,191 873,133,019 873,274,427 873,406,675 873,530,811 873,647,716 873,758,141 873,862,724 873,962,017 874,056,500 874,146,590 874,232,654 874,315,016 874,393,962 874,466,670 Present value of annual costs ($) 554,074,192 -Present value of annual energy nominal (kWh) 7,877,183,891 +Present value of annual energy nominal (kWh) 7,877,183,890 LCOE Levelized cost of energy nominal (cents/kWh) 7.03 Present value of PPA revenue ($) 809,659,354 -Present value of annual energy nominal (kWh) 7,877,183,891 +Present value of annual energy nominal (kWh) 7,877,183,890 LPPA Levelized PPA price nominal (cents/kWh) 10.28 PROJECT STATE INCOME TAXES diff --git a/tests/examples/example_SAM-single-owner-PPA-5.txt b/tests/examples/example_SAM-single-owner-PPA-5.txt index fe78ef48d..b46d60ad2 100644 --- a/tests/examples/example_SAM-single-owner-PPA-5.txt +++ b/tests/examples/example_SAM-single-owner-PPA-5.txt @@ -46,7 +46,7 @@ Plant Lifetime, 30 Reservoir Model, 1 Reservoir Volume Option, 1, -- FRAC_NUM_SEP: Reservoir volume calculated with fracture separation and number of fractures as input -Number of Fractures, 1663, -- 55 fractures per well +Number of Fractures per Stimulated Well, 55 Fracture Shape, 3, -- Square Fracture Separation, 26, diff --git a/tests/examples/example_SAM-single-owner-PPA.out b/tests/examples/example_SAM-single-owner-PPA.out index ae6a8d647..8f3fee6ea 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.10.22 - Simulation Date: 2025-12-15 - Simulation Time: 09:15 - Calculation Time: 1.196 sec + GEOPHIRES Version: 3.11.3 + Simulation Date: 2026-01-17 + Simulation Time: 09:41 + Calculation Time: 1.703 sec ***SUMMARY OF RESULTS*** @@ -28,13 +28,14 @@ Simulation Metadata Real Discount Rate: 8.00 % Nominal Discount Rate: 10.16 % WACC: 7.57 % + Investment Tax Credit: 67.74 MUSD Project lifetime: 20 yr Capacity factor: 90.0 % Project NPV: 126.12 MUSD After-tax IRR: 24.35 % Project VIR=PI=PIR: 1.93 Project MOIC: 3.85 - Project Payback Period: 3.91 yr + Project Payback Period: 4.91 yr Estimated Jobs Created: 125 ***ENGINEERING PARAMETERS*** diff --git a/tests/geophires_docs_tests/__init__.py b/tests/geophires_docs_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/geophires_docs_tests/test_generate_fervo_project_cape_5_graphs.py b/tests/geophires_docs_tests/test_generate_fervo_project_cape_5_graphs.py new file mode 100644 index 000000000..216a4ac80 --- /dev/null +++ b/tests/geophires_docs_tests/test_generate_fervo_project_cape_5_graphs.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from base_test_case import BaseTestCase +from geophires_docs.generate_fervo_project_cape_5_graphs import _get_redrilling_event_indexes +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters + + +class FervoProjectCape5GraphsTestCase(BaseTestCase): + + def test_get_redrilling_event_indexes(self) -> None: + input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-5.txt') + ) + r: GeophiresXResult = GeophiresXClient().get_geophires_result(input_params) + + redrilling_indexes = _get_redrilling_event_indexes((input_params, r)) + self.assertEqual( + r.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'], len(redrilling_indexes) + ) diff --git a/tests/geophires_x_client_tests/test_geophires_x_result.py b/tests/geophires_x_client_tests/test_geophires_x_result.py index 0bf5fab2c..d02767ab1 100644 --- a/tests/geophires_x_client_tests/test_geophires_x_result.py +++ b/tests/geophires_x_client_tests/test_geophires_x_result.py @@ -1,4 +1,11 @@ +from __future__ import annotations + +import json +from typing import Any + +from geophires_x_client import GeophiresXClient from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters from tests.base_test_case import BaseTestCase @@ -67,3 +74,15 @@ def test_ags_clgs_style_output(self) -> None: def test_sutra_reservoir_model_in_summary(self) -> None: r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/SUTRAExample1.out')) self.assertEqual('SUTRA Model', r.result['SUMMARY OF RESULTS']['Reservoir Model']) + + def test_produced_temperature_json_output(self) -> None: + r: GeophiresXResult = GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters(from_file_path=self._get_test_file_path('client_test_input_1.txt')) + ) + with open(r.json_output_file_path, encoding='utf-8') as f: + r_json_obj: dict[str, Any] = json.load(f) + + prod_temp_key: str = 'Produced Temperature' + self.assertIn(prod_temp_key, r_json_obj) + self.assertGreater(len(r_json_obj[prod_temp_key]['value']), 100) + self.assertTrue(all(it > 0 for it in r_json_obj[prod_temp_key]['value'])) diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index a3d892e1d..75e7952ae 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -67,13 +67,21 @@ def _lcoe(r: GeophiresXResult) -> float: base_lcoe = _lcoe(base_result) self.assertGreater(base_lcoe, 7) - ir = base_result.result['ECONOMIC PARAMETERS']['Interest Rate'] + econ = base_result.result['ECONOMIC PARAMETERS'] + + ir = econ['Interest Rate'] self.assertIsNone(ir) - rdr = base_result.result['ECONOMIC PARAMETERS']['Real Discount Rate'] + rdr = econ['Real Discount Rate'] self.assertEqual(rdr['value'], 7.0) self.assertEqual(rdr['unit'], '%') + itc_output = econ['Investment Tax Credit'] + self.assertIsNotNone(itc_output) + self.assertAlmostEqualWithinPercentage( + base_result.result['CAPITAL COSTS (M$)']['Total CAPEX']['value'] * 0.3, itc_output['value'], percent=5 + ) + def test_drawdown(self): r = self._get_result( {'Plant Lifetime': 20, 'End-Use Option': 1}, file_path=self._get_test_file_path('../examples/example13.txt') diff --git a/tests/geophires_x_tests/test_fervo_project_cape_5.py b/tests/geophires_x_tests/test_fervo_project_cape_5.py new file mode 100644 index 000000000..59714459e --- /dev/null +++ b/tests/geophires_x_tests/test_fervo_project_cape_5.py @@ -0,0 +1,515 @@ +from __future__ import annotations + +import math +import re +from pathlib import Path +from typing import Any + +from pint.facets.plain import PlainQuantity + +from base_test_case import BaseTestCase +from geophires_docs import generate_fervo_project_cape_5_md +from geophires_x.GeoPHIRESUtils import quantity +from geophires_x.GeoPHIRESUtils import sig_figs +from geophires_x.Parameter import HasQuantity +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult +from geophires_x_client import ImmutableGeophiresInputParameters + + +class FervoProjectCape5TestCase(BaseTestCase): + """ + FIXME WIP - see https://github.com/softwareengineerprogrammer/GEOPHIRES/pull/117 + """ + + def test_internal_consistency(self): + + fpc5_result: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/Fervo_Project_Cape-5.out') + ) + fpc5_input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-5.txt') + ) + fpc5_input_params_dict: dict[str, Any] = self._get_input_parameters(fpc5_input_params) + + def _q(dict_val: str) -> PlainQuantity: + spl = dict_val.split(' ') + return quantity(float(spl[0]), spl[1]) + + lateral_length_q = _q(fpc5_input_params_dict['Nonvertical Length per Multilateral Section']) + frac_sep_q = quantity(float(fpc5_input_params_dict['Fracture Separation']), 'meter') + number_of_fracs_per_well = int(fpc5_input_params_dict['Number of Fractures per Stimulated Well']) + + self.assertLess(number_of_fracs_per_well * frac_sep_q, lateral_length_q) + + result_number_of_wells: int = self._number_of_wells(fpc5_result) + number_of_fracs = int(fpc5_result.result['RESERVOIR PARAMETERS']['Number of fractures']['value']) + self.assertEqual(number_of_fracs, result_number_of_wells * number_of_fracs_per_well) + + input_num_production_wells: int = int(fpc5_input_params_dict['Number of Production Wells']) + input_num_injection_wells: int = math.ceil( + float(fpc5_input_params_dict['Number of Injection Wells per Production Well']) * input_num_production_wells + ) + fpc5_input_params_number_of_wells = input_num_production_wells + input_num_injection_wells + self.assertEqual(result_number_of_wells, fpc5_input_params_number_of_wells) + + num_redrills = fpc5_result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + total_wells_over_project_lifetime = (1 + num_redrills) * fpc5_input_params_number_of_wells + + additional_future_permitted_wells_factor = ( + 1.25 # speculative - see documentation reference source for assumptions + ) + base_blm_permitted_wells = 320 + + self.assertLess( + total_wells_over_project_lifetime, base_blm_permitted_wells * additional_future_permitted_wells_factor + ) + + @staticmethod + def _get_input_parameters( + params: GeophiresInputParameters, include_parameter_comments: bool = False, include_line_comments: bool = False + ) -> dict[str, Any]: + """ + TODO consolidate with src/geophires_docs/generate_fervo_project_cape_5_md.py:30 as a common utility function. + Note doing so is non-trivial because there would need to be a mechanism to ensure parsing exactly matches + GEOPHIRES behavior, which may diverge from the below implementation under some circumstances. + """ + + comment_idx = 0 + ret: dict[str, Any] = {} + for line in params.as_text().split('\n'): + parts = line.strip().split(', ') # TODO generalize for array-type params + field = parts[0].strip() + if len(parts) >= 2 and not field.startswith('#'): + fieldValue = parts[1].strip() + if include_parameter_comments and len(parts) > 2: + fieldValue += ', ' + (', '.join(parts[2:])).strip() + ret[field] = fieldValue.strip() + + if include_line_comments and field.startswith('#'): + ret[f'_COMMENT-{comment_idx}'] = line.strip() + comment_idx += 1 + + # TODO preserve newlines + + return ret + + @staticmethod + def _number_of_wells(result: GeophiresXResult) -> int: + r: dict[str, dict[str, Any]] = result.result + + number_of_wells = ( + r['SUMMARY OF RESULTS']['Number of injection wells']['value'] + + r['SUMMARY OF RESULTS']['Number of production wells']['value'] + ) + + return number_of_wells + + def test_fervo_project_cape_5_results_against_reference_values(self): + """ + Asserts that results conform to some of the key reference values claimed in docs/Fervo_Project_Cape-5.md. + """ + + r = GeophiresXClient().get_geophires_result( + GeophiresInputParameters(from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-5.txt')) + ) + + min_net_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Minimum Net Electricity Generation']['value'] + self.assertGreater(min_net_gen, 500) + self.assertLess(min_net_gen, 505) + + max_total_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Total Electricity Generation'][ + 'value' + ] + self.assertGreater(max_total_gen, 550) + self.assertLess(max_total_gen, 600) + + lcoe = r.result['SUMMARY OF RESULTS']['Electricity breakeven price']['value'] + self.assertGreater(lcoe, 7.5) + self.assertLess(lcoe, 8.5) + + redrills = r.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + self.assertGreater(redrills, 1) + self.assertLess(redrills, 6) + max_phase_2_permitted_wells = 320 + self.assertLess(self._number_of_wells(r) * redrills, max_phase_2_permitted_wells) + self.assertGreater(self._number_of_wells(r) * redrills, max_phase_2_permitted_wells * 0.75) + + well_cost = r.result['CAPITAL COSTS (M$)']['Drilling and completion costs per well']['value'] + self.assertLess(well_cost, 5.0) + self.assertGreater(well_cost, 4.0) + + pumping_power_pct = r.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Initial pumping power/net installed power' + ]['value'] + self.assertGreater(pumping_power_pct, 5) + self.assertLess(pumping_power_pct, 15) + + num_prod_wells = r.result['SUMMARY OF RESULTS']['Number of production wells']['value'] + num_inj_wells = r.result['SUMMARY OF RESULTS']['Number of injection wells']['value'] + self.assertTrue(num_prod_wells * 0.5 < num_inj_wells < num_prod_wells) + self.assertTrue(74 < num_inj_wells + num_prod_wells < 124) + + def test_case_study_documentation(self): + """ + Parses result values from case study documentation Markdown and checks that they match the actual result. + Useful for catching when minor updates are made to the case study which need to be manually synced to the + documentation. + + Note: for future case studies, generate the documentation Markdown from the input/result rather than writing + (partially) by hand so that they are guaranteed to be in sync and don't need to be tested like this, + which has proved messy. + + Update 2026-01-13: Markdown is now partially generated from input and result in + src/geophires_docs/generate_fervo_project_cape_5_md.py. + """ + + def generate_documentation_markdown() -> None: + generate_fervo_project_cape_5_md.main(project_root=Path(self._get_test_file_path('../../')).absolute()) + + generate_documentation_markdown() # Ensure we're testing the latest version of the generated doc + + documentation_file_content = '\n'.join( + self._get_test_file_content('../../docs/Fervo_Project_Cape-5.md', encoding='utf-8') + ) + inputs_in_markdown = self.parse_markdown_inputs_structured(documentation_file_content) + results_in_markdown = self.parse_markdown_results_structured(documentation_file_content) + + example_result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-5.out')) + + expected_drilling_cost_MUSD_per_well = 4.46 + # number_of_doublets = inputs_in_markdown['Number of Doublets']['value'] + number_of_wells = self._number_of_wells(example_result) + self.assertAlmostEqualWithinPercentage( + expected_drilling_cost_MUSD_per_well * number_of_wells, + results_in_markdown['Well Drilling and Completion Costs']['value'], + percent=5, + ) + self.assertEqual('MUSD', results_in_markdown['Well Drilling and Completion Costs']['unit']) + + expected_base_stim_cost_MUSD_per_well = 4.0 + expected_all_in_stim_cost_MUSD_per_well = 4.83 + self.assertAlmostEqualWithinSigFigs( + expected_all_in_stim_cost_MUSD_per_well * number_of_wells, + results_in_markdown['Stimulation Costs']['value'], + 3, + ) + self.assertEqual('MUSD', results_in_markdown['Stimulation Costs']['unit']) + + self.assertEqual( + expected_base_stim_cost_MUSD_per_well, + inputs_in_markdown['Reservoir Stimulation Capital Cost per Production Well']['value'], + ) + self.assertEqual('MUSD', inputs_in_markdown['Reservoir Stimulation Capital Cost per Production Well']['unit']) + + class _Q(HasQuantity): + def __init__(self, vu: dict[str, Any]): + self.value = vu['value'] + + # https://stackoverflow.com/questions/2280334/shortest-way-of-creating-an-object-with-arbitrary-attributes-in-python + self.CurrentUnits = type('', (), {})() + + self.CurrentUnits.value = vu['unit'] + + capex_q = _Q(results_in_markdown['Total CAPEX']).quantity() + markdown_capex_USD_per_kW = ( + capex_q.to('USD').magnitude + / _Q(results_in_markdown['Maximum Net Electricity Generation']).quantity().to('kW').magnitude + ) + self.assertAlmostEqual( + sig_figs(markdown_capex_USD_per_kW, 2), results_in_markdown['Total CAPEX: $/kW']['value'] + ) + + field_mapping = { + 'LCOE': 'Electricity breakeven price', + 'Project capital costs: Total CAPEX': 'Total CAPEX', + 'Well Drilling and Completion Costs': 'Drilling and completion costs per well', + 'Well Drilling and Completion Costs total': 'Drilling and completion costs', + 'Stimulation Costs total': 'Stimulation costs', + 'Reservoir Volume': 'Reservoir volume', + } + + ignore_keys = [ + 'Total CAPEX: $/kW', # See https://github.com/NREL/GEOPHIRES-X/issues/391 + 'Total fracture surface area per production well', + 'Stimulation Costs', # remapped to 'Stimulation Costs total' + ] + + example_result_values = {} + for key, _ in results_in_markdown.items(): + if key not in ignore_keys: + mapped_key = field_mapping.get(key) if key in field_mapping else key + entry = example_result._get_result_field(mapped_key) + if entry is not None and 'value' in entry: + entry['value'] = sig_figs(entry['value'], 3) + + example_result_values[key] = entry + + for ignore_key in ignore_keys: + if ignore_key in results_in_markdown: + del results_in_markdown[ignore_key] + + result_capex_USD_per_kW = ( + _Q(example_result._get_result_field('Total CAPEX')).quantity().to('USD').magnitude + / _Q(example_result._get_result_field('Maximum Net Electricity Generation')).quantity().to('kW').magnitude + ) + self.assertAlmostEqual(sig_figs(result_capex_USD_per_kW, 2), sig_figs(markdown_capex_USD_per_kW, 2)) + + num_prod_wells = inputs_in_markdown['Number of Production Wells']['value'] + self.assertEqual( + example_result.result['SUMMARY OF RESULTS']['Number of production wells']['value'], num_prod_wells + ) + + # Calculate expected total fractures based on input configuration + num_inj_per_prod = inputs_in_markdown['Number of Injection Wells per Production Well']['value'] + num_inj_wells = math.ceil(num_prod_wells * num_inj_per_prod) + total_wells = num_prod_wells + num_inj_wells + num_fracs_per_well = inputs_in_markdown['Number of Fractures per Stimulated Well']['value'] + + # Assuming all wells are stimulated + expected_total_fracs = total_wells * num_fracs_per_well + self.assertEqual( + expected_total_fracs, example_result.result['RESERVOIR PARAMETERS']['Number of fractures']['value'] + ) + + self.assertAlmostEqual( + example_result.result['RESERVOIR PARAMETERS']['Reservoir volume']['value'], + results_in_markdown['Reservoir Volume']['value'], + delta=1, # Tolerance for potential formatting/float parsing differences + ) + + additional_expected_stim_indirect_cost_frac = 0.00 + expected_stim_cost_total_MUSD = ( + expected_all_in_stim_cost_MUSD_per_well + * self._number_of_wells(example_result) + * (1.0 + additional_expected_stim_indirect_cost_frac) + ) + self.assertAlmostEqualWithinSigFigs( + expected_stim_cost_total_MUSD, + example_result.result['CAPITAL COSTS (M$)']['Stimulation costs']['value'], + num_sig_figs=3, + ) + + def parse_markdown_results_structured(self, markdown_text: str) -> dict: + """ + Parses result values from markdown into a structured dictionary with values and units. + """ + raw_results = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + + # Pattern to strip HTML tags and extract text content + html_tag_pattern = re.compile(r'<[^>]+>') + + try: + results_start_index = markdown_text.index('## Results') + search_area = markdown_text[results_start_index:] + + matches = table_pattern.findall(search_area) + + # Use key_ and value_ to avoid shadowing + for match in matches: + key_ = match[0].strip() + # Strip HTML tags from the key (e.g., LCOE -> LCOE) + key_ = html_tag_pattern.sub('', key_).strip() + value_ = match[1].strip() + if key_.lower() not in ('metric', 'parameter'): + raw_results[key_] = value_ + except ValueError: + print("Warning: '## Results' section not found.") + return {} + + # Consistency check + special_case_pattern = re.compile(r'LCOE\s*=\s*(\S+)\s*and\s*IRR\s*=\s*(\S+)') + special_case_match = special_case_pattern.search(markdown_text) + if special_case_match: + lcoe_text = special_case_match.group(1).rstrip('.,;') + lcoe_table_base = raw_results.get('LCOE', '').split('(')[0].strip() + if lcoe_text != lcoe_table_base: + raise ValueError( + f'LCOE mismatch: Text value ({lcoe_text}) does not match table value ({lcoe_table_base}).' + ) + + # Now, process the raw results into the structured format + structured_results = {} + # Use key_ and value_ to avoid shadowing + for key_, value_ in raw_results.items(): + if key_ in [ + 'After-tax IRR', + 'Average Production Temperature', + 'LCOE', + 'Maximum Total Electricity Generation', + 'Minimum Net Electricity Generation', + 'Maximum Net Electricity Generation', + 'Number of times redrilling', + 'Reservoir Volume', + 'Total CAPEX', + 'Total CAPEX: $/kW', + 'WACC', + 'Well Drilling and Completion Costs', + 'Stimulation Costs', + ]: + structured_results[key_] = self._parse_value_unit(value_) + + # Handle drilling and stimulation costs in format: "$464M total ($4.46M/well)" + for result_with_total_key in ['Well Drilling and Completion Costs', 'Stimulation Costs']: + entry = structured_results[result_with_total_key] + + unit_str = entry['unit'] + # unit_str is like "total; $4.46M/well" after _parse_value_unit processes "$464M total ($4.46M/well)" + # The entry['value'] is 464 (total MUSD) + # We need to extract per-well value from unit string + + # Parse per-well value from the parenthetical part + per_well_match = re.search(r'\$(\d+\.?\d*)M/well', unit_str) + if per_well_match: + per_well_value = float(per_well_match.group(1)) + # Store total in 'X total' key + structured_results[f'{result_with_total_key} total'] = { + 'value': entry['value'], + 'unit': 'MUSD', + } + # Update entry to be per-well value + entry['value'] = per_well_value + entry['unit'] = 'MUSD/well' + + return structured_results + + def parse_markdown_inputs_structured(self, markdown_text: str) -> dict: + """ + Parses all input values from all tables under the '## Inputs' section + of a markdown file into a structured dictionary. + """ + try: + # Isolate the content from "## Inputs" to the next "## " header + sections = re.split(r'(^###\s.*)', markdown_text, flags=re.MULTILINE) + inputs_header_index = next(i for i, s in enumerate(sections) if s.startswith('### Inputs')) + inputs_content = sections[inputs_header_index + 1] + except (StopIteration, IndexError): + print("Warning: '## Inputs' section not found or is empty.") + return {} + + raw_inputs = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + matches = table_pattern.findall(inputs_content) + + for match in matches: + key_ = match[0].strip() + value_ = match[1].strip() + if key_.lower() not in ('parameter', 'metric'): + raw_inputs[key_] = value_ + + structured_inputs = {} + for key_, value_ in raw_inputs.items(): + key_ = key_.replace(' ', ' ') + if key_ == 'Construction CAPEX Schedule': + parsed_value_unit = {'value': value_, 'unit': 'percent'} + else: + parsed_value_unit = self._parse_value_unit(value_) + structured_inputs[key_] = parsed_value_unit + + return structured_inputs + + # noinspection PyMethodMayBeStatic + def _parse_value_unit(self, raw_string: str) -> dict: + """ + A helper function to parse a string and extract a numerical value and its unit. + It handles various formats like currency, percentages, text, and scientific notation. + """ + # Split on open parenthesis '(', comma ',' (if not followed by digit), or HTML break tag ' cents/kWh) + match = re.match(r'^\$(\d+\.?\d*)/MWh$', clean_str) + if match: + value = float(match.group(1)) + return {'value': round(value / 10, 2), 'unit': 'cents/kWh'} + + # Billion dollar format ($X.XB -> MUSD) + match = re.match(r'^\$(\d+\.?\d*)B$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value * 1000, 'unit': 'MUSD'} + + # Million dollar format ($X.XM or $X.XM/unit) + match = re.match(r'^\$(\d+\.?\d*)M(\/.*)?$', clean_str) + if match: + value = float(match.group(1)) + unit_suffix = match.group(2) + unit = 'MUSD' + if unit_suffix: + unit = f'MUSD{unit_suffix}' + return {'value': value, 'unit': unit} + + # Dollar per kW format ($X/kW -> USD/kW) + match = re.match(r'^\$(\d+\.?\d*)/kW$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'USD/kW'} + + # Percentage format (X.X%) + match = re.search(r'(\d+\.?\d*)%$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': '%'} + + # Temperature format (X℃ -> degC) + match = re.search(r'(\d+\.?\d*)\s*℃$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'degC'} + + # Scientific notation format (X.X*10⁶ Y) + match = re.match(r'^(\d+\.?\d*)\s*[×xX]\s*10[⁶6]\s*(.*)$', clean_str) + if match: + base_value = float(match.group(1)) + unit = match.group(2).strip() + return {'value': base_value * 1e6, 'unit': unit} + + # Generic number and unit parser + if clean_str.startswith('9⅝'): + parts = clean_str.split(' ') + value = 9.0 + 5.0 / 8.0 + unit = parts[1] if len(parts) > 1 else 'unknown' + return {'value': value, 'unit': unit} + + match = re.search(r'([\d\.,]+)\s*(.*)', clean_str) + if match: + value_str = match.group(1).replace(',', '').replace(' ', '') + unit = match.group(2).strip() + + if '.' in value_str: + value = float(value_str) + else: + value = int(value_str) + + return {'value': value, 'unit': unit if unit else 'count'} + + # Fallback for text-only values + return {'value': clean_str, 'unit': 'text'} + + def test_fervo_project_cape_6(self) -> None: + """ + Fervo_Project_Cape-6 is derived from Fervo_Project_Cape-5 - see tests/regenerate-example-result.sh + """ + + fpc5_result: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/Fervo_Project_Cape-6.out') + ) + + min_net_gen_dict = fpc5_result.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Minimum Net Electricity Generation' + ] + fpc5_min_net_gen_mwe = quantity(min_net_gen_dict['value'], min_net_gen_dict['unit']).to('MW').magnitude + self.assertGreater(fpc5_min_net_gen_mwe, 100) + + max_total_gen_dict = fpc5_result.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Maximum Total Electricity Generation' + ] + fpc5_max_total_net_gen_mwe = ( + quantity(max_total_gen_dict['value'], max_total_gen_dict['unit']).to('MW').magnitude + ) + self.assertLess(fpc5_max_total_net_gen_mwe, 120) diff --git a/tests/geophires_x_tests/test_reservoir.py b/tests/geophires_x_tests/test_reservoir.py index d2fc7343f..e14f839e7 100644 --- a/tests/geophires_x_tests/test_reservoir.py +++ b/tests/geophires_x_tests/test_reservoir.py @@ -3,6 +3,7 @@ import copy import os import sys +import uuid from pathlib import Path from typing import Any @@ -283,3 +284,22 @@ def _get_result( with self.assertRaises(RuntimeError) as e: _get_result(_MAX_ALLOWED_FRACTURES, 59) self.assertIn(f'({_MAX_ALLOWED_FRACTURES*59*2}) must not exceed {_MAX_ALLOWED_FRACTURES}', str(e.exception)) + + def test_user_provided_profile_file_not_found(self) -> None: + non_existent_file_path: Path | None = None + while non_existent_file_path is None or non_existent_file_path.exists(): + non_existent_file_path = Path(f'non-existent-file_{uuid.uuid4()!s}.txt') + + with self.assertRaises(RuntimeError) as re: + GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('generic-egs-case.txt'), + params={ + 'Reservoir Model': '5, -- USER_PROVIDED_PROFILE', + 'Reservoir Output File Name': non_existent_file_path, + }, + ) + ) + + exception_message = str(re.exception) + self.assertIn('GEOPHIRES could not read reservoir output file', exception_message) diff --git a/tests/geophires_x_tests/test_well_bores.py b/tests/geophires_x_tests/test_well_bores.py index 7db431f1b..30b2ebace 100644 --- a/tests/geophires_x_tests/test_well_bores.py +++ b/tests/geophires_x_tests/test_well_bores.py @@ -82,6 +82,50 @@ def test_number_of_doublets_non_integer(self): self.assertEqual(prod_inj_lcoe_2[0], 199) self.assertEqual(prod_inj_lcoe_2[1], 199) + def test_number_of_injection_wells_per_production_well(self): + r_ratio: GeophiresXResult = self._get_result( + { + 'Number of Production Wells': 63, + 'Number of Injection Wells per Production Well': 0.666, # 3:2 ratio + } + ) + + r_explicit_counts: GeophiresXResult = self._get_result( + {'Number of Production Wells': 63, 'Number of Injection Wells': 42} + ) + + self.assertEqual(self._prod_inj_lcoe_production(r_explicit_counts), self._prod_inj_lcoe_production(r_ratio)) + + self.assertEqual( + self._prod_inj_lcoe_production( + self._get_result( + { + 'Number of Production Wells': 2, # default value + 'Number of Injection Wells per Production Well': 3, + } + ) + ), + self._prod_inj_lcoe_production(self._get_result({'Number of Injection Wells per Production Well': 3})), + ) + + with self.assertRaises(RuntimeError): + self._get_result( + { + 'Number of Production Wells': 63, + 'Number of Injection Wells per Production Well': 0.6666, # 3:2 ratio + 'Number of Injection Wells': 42, + } + ) + + with self.assertRaises(RuntimeError): + self._get_result( + { + 'Number of Production Wells': 63, + 'Number of Injection Wells per Production Well': 0.6666, # 3:2 ratio + 'Number of Doublets': 52, + } + ) + # noinspection PyMethodMayBeStatic def _get_result(self, _params) -> GeophiresXResult: params = GeophiresInputParameters( diff --git a/tests/regenerate-example-result.env.template b/tests/regenerate-example-result.env.template new file mode 100644 index 000000000..e71088891 --- /dev/null +++ b/tests/regenerate-example-result.env.template @@ -0,0 +1,4 @@ +# export GEOPHIRES_FPC5_SENSITIVITY_ANALYSIS_PROJECT_ROOT= + +export SWS_GTP_CLIENT_USERNAME= +export SWS_GTP_CLIENT_USER_PASSWORD= diff --git a/tests/regenerate-example-result.sh b/tests/regenerate-example-result.sh index faef5cee3..0d2234da7 100755 --- a/tests/regenerate-example-result.sh +++ b/tests/regenerate-example-result.sh @@ -1,20 +1,66 @@ #!/bin/zsh +set -e + +STASH_PWD=$(pwd) + +cd "$(dirname "$0")" # Use this script to regenerate example results in cases where changes in GEOPHIRES # calculations alter the example test output. Example: # ./tests/regenerate-example-result.sh SUTRAExample1 # See https://github.com/NREL/GEOPHIRES-X/issues/107 -# Note: make sure your virtualenv is activated before running or this script will fail +# Note: make sure your virtualenv is activated and you have run pip install -e . before running or this script will fail # or generate incorrect results. -cd "$(dirname "$0")" +echo "Regenerating example: $1..." + +if [[ $1 == "Fervo_Project_Cape-6" ]] +then + echo "Syncing Fervo_Project_Cape-6.txt from Fervo_Project_Cape-5.txt..." + + sed -e 's/Construction Years,.*/Construction Years, 3/' \ + -e 's/^Bond Financing Start Year.*/Bond Financing Start Year, -2/' \ + -e 's/^Number of Production Wells,.*/Number of Production Wells, 12/' \ + -e 's/^Production Flow Rate per Well.*/Production Flow Rate per Well, 100/' \ + -e 's/500 MWe/100 MWe/' \ + -e 's/Phase II/Phase I/' \ + examples/Fervo_Project_Cape-5.txt > examples/Fervo_Project_Cape-6.txt +fi + python -mgeophires_x examples/$1.txt examples/$1.out rm examples/$1.json if [[ $1 == "example1_addons" ]] then - echo "Updating CSV..." + echo "Updating example1_addons CSV..." python regenerate_example_result_csv.py example1_addons fi + +if [[ $1 == "Fervo_Project_Cape-5" ]] +then + python ../src/geophires_docs/generate_fervo_project_cape_5_docs.py + + ./regenerate-example-result.sh Fervo_Project_Cape-6 + + if [ ! -f regenerate-example-result.env ] && [ -f regenerate-example-result.env.template ]; then + echo "Creating regenerate-example-result.env from template..." + cp regenerate-example-result.env.template regenerate-example-result.env + fi + + source regenerate-example-result.env + if [ -n "$GEOPHIRES_FPC5_SENSITIVITY_ANALYSIS_PROJECT_ROOT" ]; then + echo "Updating sensitivity analysis..." + STASH_PWD_2=$(pwd) + cd $GEOPHIRES_FPC5_SENSITIVITY_ANALYSIS_PROJECT_ROOT + source venv/bin/activate + python -m fpc_sensitivity_analysis.generate_geophires_fpc5_sensitivity_analysis + deactivate + cd $STASH_PWD_2 + fi +fi + +cd $STASH_PWD + +echo "Regenerated example $1." diff --git a/tests/test_geophires_x.py b/tests/test_geophires_x.py index e80c99290..a65f932f2 100644 --- a/tests/test_geophires_x.py +++ b/tests/test_geophires_x.py @@ -184,7 +184,8 @@ def get_output_file_for_example(example_file: str): ) # TOUGH not enabled for testing - see https://github.com/NREL/GEOPHIRES-X/issues/318 and not example_file_path_.startswith(('example6.txt', 'example7.txt')) - and '.out' not in example_file_path_, + and '.out' not in example_file_path_ + and '.json' not in example_file_path_, self._list_test_files_dir(test_files_dir='examples'), ) ) diff --git a/tox.ini b/tox.ini index ce5b99e78..320d5c6a9 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,7 @@ usedevelop = false deps = pytest pytest-cov + Jinja2 commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} @@ -61,8 +62,10 @@ deps = -r{toxinidir}/docs/requirements.txt commands = python src/geophires_x_schema_generator/main.py --build-path docs/ + python src/geophires_docs/__main__.py sphinx-build {posargs:-E} -b html docs dist/docs sphinx-build docs dist/docs + python -c "import shutil; shutil.copytree('docs/_images', 'dist/docs/_images', dirs_exist_ok=True)" ; TODO re-enable linkcheck probably - `sphinx-build -b linkcheck docs dist/docs` [testenv:report]