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..a3162a52a --- /dev/null +++ b/docs/Fervo_Project_Cape-5.md.jinja @@ -0,0 +1,465 @@ +.. 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 (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. + +| Metric | Result Value | Reference Value(s) | Reference Source | +|---------------|----------------|--------------------|------------------| +| LCOE | {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }} | \$80/MWh | Horne et al, 2025. | +| After-tax IRR | {{ irr_pct ~ '%' }} | 15–25% | Typical levered returns for energy projects | +| NPV | {{ '$' ~ npv_musd ~ 'M' }} | >$0 | Positive NPVs result in profit | +| Project ROI | {{ project_moic }} | | .. N/A | +| 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. | +{# Note that the '.. N/A' entry in the last row is required for the table to render in HTML (presumable m2r2/sphinx build issue) #} + +Hover over the metric names to view the corresponding definitions. +See [GEOPHIRES output parameters documentation](parameters.html#economic-parameters) for more information. + +### Capital Costs + +{# @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 #} + + +### Technical & Engineering Results + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---| +| 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). | +| Maximum Total Electricity Generation | {{ max_total_generation_mwe }} MW | 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). | +| 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. | +| 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). | +| Average Net Electricity Generation | {{ avg_net_generation_mwe }} MW | | | +| Maximum Net Electricity Generation | {{ max_net_generation_mwe}} MW | | | +| 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 }} | 320 | Total wells permitted by environmental assessment (BLM, 2024). | +{# @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). As visible in the graph, the production temperature never actually reaches the threshold; redrilling 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: + +1. [Fervo_Project_Cape-4](Fervo_Project_Cape-4.html) + +{# 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..9beb72465 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..4cb3f7afa 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..aa622b152 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..4c10206ad --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg @@ -0,0 +1,3105 @@ + + + + + + + + 2026-01-18T13:29:17.643563 + 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..d34f1621a 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..d546395ba --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg @@ -0,0 +1,3360 @@ + + + + + + + + 2026-01-18T13:29:17.304350 + 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..5be023224 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..3bda623bd --- /dev/null +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg @@ -0,0 +1,3482 @@ + + + + + + + + 2026-01-18T13:29:17.303369 + 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..f536c4f47 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..10d906857 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_5_graphs.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import json +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 = 1.0 +) -> 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 = [] + + for i in range(1, len(temperatures_celsius)): + temp_increase = temperatures_celsius[i] - temperatures_celsius[i - 1] + if temp_increase >= threshold_degc: + # The temperature jump is detected at index i, but the redrilling event + # occurred at the previous datapoint (i-1) when the minimum was reached. + # Convert to fractional year position (COD = Year 1) + year_position = 1 + (i - 1) / 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(195, 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:.1f}% 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..f4686eee9 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_5_md.py @@ -0,0 +1,506 @@ +#!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() -> 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/geophires-request.json' + with open(schema_file, encoding='utf-8') as f: + return json.loads(f.read()) + + +def _get_parameter_schema(param_name: str) -> dict[str, Any]: + return _get_schema()['properties'][param_name] + + +def _get_parameter_schema_type(param_name: str) -> dict[str, Any]: + return _get_parameter_schema(param_name)['type'] + + +def _get_parameter_category(param_name: str) -> str: + return _get_parameter_schema(param_name)['category'] + + +def _get_parameter_units(param_name: str) -> str | None: + unit = _get_schema()['properties'][param_name]['units'] + + if unit == '': + return 'dimensionless' + + return unit + + +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_parameter_category(param_name) + if category_name is None or category == category_name: + param_val_comment_split = param_val_comment.split( + # ',', + ',' if _get_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_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_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_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 = surf_equip_sim['Maximum Net Electricity Generation']['value'] + 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 = 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), + '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_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 { + '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..24f84d613 --- /dev/null +++ b/src/geophires_docs/watch_docs.py @@ -0,0 +1,113 @@ +#!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('\n[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') + + _log.info(f"\nDocs rebuild complete at {time.strftime('%Y-%m-%d %H:%M:%S')}.") + _say('docs rebuilt') + _log.info(f"\nWaiting for further changes in '{watch_dirs}'...") + + # Update state to the current state + last_states = _get_file_states() + + except KeyboardInterrupt: + _log.info('\nWatcher 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..45526cf0f 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -740,6 +740,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( @@ -1228,7 +1240,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 +1373,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..1355635ef 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": {}, @@ -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..141630b53 --- /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-18 + Simulation Time: 13:25 + Calculation Time: 1.832 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 536.88 MW + Electricity breakeven price: 7.73 cents/kWh + Total CAPEX: 2859.26 MUSD + Number of production wells: 56 + Number of injection wells: 38 + Flowrate per production well: 107.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: 857.78 MUSD + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: 243.00 MUSD + After-tax IRR: 23.79 % + Project VIR=PI=PIR: 1.41 + Project MOIC: 4.72 + Project Payback Period: 5.94 yr + Estimated Jobs Created: 1269 + + ***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: 107.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: 2082.43 kPa + Productivity Index: 1.75 kg/sec/bar + Injectivity Index: 2.11 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.3 degC + Average Production Temperature: 202.7 degC + Minimum Production Temperature: 197.9 degC + Initial Production Temperature: 201.9 degC + Average Reservoir Heat Extraction: 3664.25 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 0.2 degC + Average Injection Well Pump Pressure Drop: -5176.1 kPa + Average Production Well Pump Pressure Drop: 6917.1 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: 1467.52 MUSD + Field gathering system costs: 43.13 MUSD + Total surface equipment costs: 1510.64 MUSD + Exploration costs: 30.00 MUSD + Overnight Capital Cost: 2431.65 MUSD + Interest during construction: 142.34 MUSD + Inflation costs during construction: 285.27 MUSD + Total CAPEX: 2859.26 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 5.75 MUSD/yr + Power plant maintenance costs: 24.87 MUSD/yr + Water costs: 3.15 MUSD/yr + Redrilling costs: 89.10 MUSD/yr + Average Annual Royalty Cost: 12.82 MUSD/yr + Total operating and maintenance costs: 135.69 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 599.67 MW + Average Total Electricity Generation: 595.97 MW + Minimum Total Electricity Generation: 561.22 MW + Initial Total Electricity Generation: 589.90 MW + Maximum Net Electricity Generation: 540.69 MW + Average Net Electricity Generation: 536.88 MW + Minimum Net Electricity Generation: 501.23 MW + Initial Net Electricity Generation: 530.73 MW + Average Annual Total Electricity Generation: 4698.75 GWh + Average Annual Net Electricity Generation: 4232.89 GWh + Initial pumping power/net installed power: 11.15 % + Average Pumping Power: 59.09 MW + Heat to Power Conversion Efficiency: 14.65 % + + ************************************************************ + * 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.89 59.1637 530.7347 14.5683 + 2 1.0049 202.87 59.1237 537.8447 14.6641 + 3 1.0058 203.06 59.1163 539.1668 14.6818 + 4 1.0063 203.15 59.1124 539.8546 14.6910 + 5 1.0066 203.21 59.1099 540.3081 14.6970 + 6 1.0067 203.25 59.1098 540.5619 14.7004 + 7 1.0060 203.10 59.1388 539.4687 14.6855 + 8 0.9995 201.80 59.3570 529.8806 14.5541 + 9 1.0000 201.89 59.0983 530.8001 14.5701 + 10 1.0050 202.90 59.0862 538.0445 14.6673 + 11 1.0058 203.07 59.0664 539.2881 14.6841 + 12 1.0063 203.16 59.0422 539.9690 14.6935 + 13 1.0066 203.22 59.0191 540.4296 14.6999 + 14 1.0067 203.25 59.0038 540.6662 14.7033 + 15 1.0058 203.06 59.0298 539.2898 14.6846 + 16 0.9984 201.56 59.2774 528.2830 14.5337 + 17 1.0017 202.23 58.9814 533.3905 14.6066 + 18 1.0051 202.92 58.9804 538.2979 14.6721 + 19 1.0059 203.08 58.9802 539.4425 14.6873 + 20 1.0063 203.16 58.9802 540.0738 14.6957 + 21 1.0066 203.22 58.9804 540.4980 14.7014 + 22 1.0067 203.25 58.9838 540.6768 14.7037 + 23 1.0056 203.01 59.0289 538.9462 14.6801 + 24 0.9971 201.30 59.3179 526.3599 14.5071 + 25 1.0026 202.41 58.9810 534.6246 14.6231 + 26 1.0052 202.94 58.9811 538.4325 14.6739 + 27 1.0059 203.09 58.9812 539.5064 14.6882 + 28 1.0063 203.17 58.9814 540.1142 14.6962 + 29 1.0066 203.23 58.9816 540.5254 14.7017 + 30 1.0067 203.25 58.9858 540.6561 14.7034 + + + ******************************************************************* + * 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 4222.0 28852.7 1217.11 7.86 + 2 4246.3 28937.3 1112.94 15.75 + 3 4253.7 28963.0 1008.67 23.64 + 4 4258.1 28978.2 904.35 31.54 + 5 4261.0 28988.2 799.99 39.44 + 6 4259.5 28983.2 695.65 47.34 + 7 4225.7 28868.2 591.72 55.21 + 8 4090.0 28402.1 489.48 62.95 + 9 4227.0 28869.2 385.55 70.81 + 10 4247.5 28940.2 281.36 78.70 + 11 4254.6 28964.5 177.09 86.59 + 12 4259.0 28979.2 72.77 94.49 + 13 4261.9 28988.8 -31.59 102.39 + 14 4259.5 28980.4 -135.92 110.29 + 15 4219.9 28845.1 -239.77 118.15 + 16 4092.9 28408.4 -342.04 125.89 + 17 4231.9 28883.1 -446.01 133.76 + 18 4249.0 28942.9 -550.21 141.65 + 19 4255.6 28966.0 -654.49 149.55 + 20 4259.7 28980.2 -758.82 157.44 + 21 4262.3 28989.2 -863.18 165.34 + 22 4258.6 28976.9 -967.49 173.24 + 23 4212.2 28818.7 -1071.24 181.09 + 24 4097.3 28423.0 -1173.56 188.84 + 25 4234.7 28893.1 -1277.58 196.71 + 26 4249.7 28945.4 -1381.78 204.60 + 27 4256.0 28967.4 -1486.07 212.50 + 28 4260.0 28981.1 -1590.40 220.40 + 29 4262.4 28989.6 -1694.76 228.30 + 30 4258.3 28975.7 -1799.07 236.19 + + *************************** + * 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] ($) -34,043,049 -65,654,451 -337,998,843 -1,048,039,577 -945,910,430 +plus: +Inflation cost [construction] ($) -919,162 -3,593,202 -28,123,763 -117,855,471 -134,782,306 +equals: +Nominal capital expenditure [construction] ($) -34,962,211 -69,247,654 -366,122,605 -1,165,895,048 -1,080,692,736 + +Issuance of equity [construction] ($) 34,962,211 69,247,654 109,836,782 349,768,514 324,207,821 +Issuance of debt [construction] ($) 0 0 256,285,824 816,126,533 756,484,915 +Debt balance [construction] ($) 0 0 256,285,824 1,099,322,368 1,971,236,132 +Debt interest payment [construction] ($) 0 0 0 26,910,011 115,428,849 + +Installed cost [construction] ($) -34,962,211 -69,247,654 -366,122,605 -1,192,805,059 -1,196,121,585 +After-tax net cash flow [construction] ($) -34,962,211 -69,247,654 -109,836,782 -349,768,514 -324,207,821 + +ENERGY +Electricity to grid (kWh) 0.0 4,222,308,474 4,246,656,805 4,254,077,539 4,258,464,138 4,261,361,983 4,259,838,404 4,226,101,564 4,090,369,692 4,227,353,669 4,247,826,578 4,254,988,701 4,259,392,408 4,262,298,892 4,259,905,427 4,220,263,261 4,093,268,367 4,232,225,414 4,249,366,482 4,255,997,698 4,260,070,210 4,262,659,891 4,258,995,778 4,212,556,835 4,097,673,094 4,235,086,416 4,250,088,054 4,256,396,046 4,260,335,780 4,262,752,890 4,258,625,740 +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,222,308,474 4,246,656,805 4,254,077,539 4,258,464,138 4,261,361,983 4,259,838,404 4,226,101,564 4,090,369,692 4,227,353,669 4,247,826,578 4,254,988,701 4,259,392,408 4,262,298,892 4,259,905,427 4,220,263,261 4,093,268,367 4,232,225,414 4,249,366,482 4,255,997,698 4,260,070,210 4,262,659,891 4,258,995,778 4,212,556,835 4,097,673,094 4,235,086,416 4,250,088,054 4,256,396,046 4,260,335,780 4,262,752,890 4,258,625,740 + +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 401,119,305 403,432,396 406,562,190 409,408,742 412,116,317 414,397,080 413,524,038 402,574,185 418,465,740 422,913,614 426,052,019 428,920,815 431,643,009 433,828,769 432,197,161 421,524,776 438,246,942 442,444,038 445,560,399 448,414,990 451,117,296 453,157,151 450,617,205 440,663,765 457,855,192 461,899,570 465,011,268 467,870,075 470,565,292 472,537,112 +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,429,629,557 +Total revenue ($) 0 401,119,305 403,432,396 406,562,190 409,408,742 412,116,317 414,397,080 413,524,038 402,574,185 418,465,740 422,913,614 426,052,019 428,920,815 431,643,009 433,828,769 432,197,161 421,524,776 438,246,942 442,444,038 445,560,399 448,414,990 451,117,296 453,157,151 450,617,205 440,663,765 457,855,192 461,899,570 465,011,268 467,870,075 470,565,292 1,902,166,669 + +Property tax net assessed value ($) 0 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 2,859,259,114 + +OPERATING EXPENSES +O&M fixed expense ($) 0 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 122,870,223 +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 7,019,588 7,060,067 7,114,838 7,164,653 7,212,036 7,251,949 7,236,671 7,045,048 7,323,150 7,400,988 14,911,821 15,012,229 15,107,505 15,184,007 15,126,901 14,753,367 15,338,643 15,485,541 15,594,614 15,694,525 15,789,105 15,860,500 15,771,602 15,423,232 16,024,932 16,166,485 16,275,394 16,375,453 16,469,785 16,538,799 +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,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 6,290,370 +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 136,180,181 136,220,660 136,275,431 136,325,246 136,372,629 136,412,542 136,397,264 136,205,641 136,483,743 136,561,581 144,072,414 144,172,822 144,268,098 144,344,600 144,287,494 143,913,960 144,499,236 144,646,134 144,755,207 144,855,118 144,949,698 145,021,093 144,932,195 144,583,825 145,185,525 145,327,078 145,435,987 145,536,046 145,630,378 145,699,392 + +EBITDA ($) 0 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 + +OPERATING ACTIVITIES +EBITDA ($) 0 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 +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 137,986,529 136,525,748 134,962,712 133,290,264 131,500,745 129,585,959 127,537,138 125,344,899 122,999,204 120,489,310 117,803,724 114,930,146 111,855,418 108,565,459 105,045,203 101,278,529 97,248,188 92,935,723 88,321,386 83,384,045 78,101,090 72,448,328 66,399,873 59,928,026 53,003,149 45,593,532 37,665,241 29,181,970 20,104,869 10,392,372 +Cash flow from operating activities ($) 0 126,952,595 130,685,988 135,324,047 139,793,232 144,242,944 148,398,579 149,589,637 141,023,645 158,982,792 165,862,723 164,175,881 169,817,848 175,519,492 180,918,709 182,864,464 176,332,287 196,499,517 204,862,180 212,483,806 220,175,828 228,066,508 235,687,729 239,285,137 236,151,914 259,666,518 270,978,960 281,910,040 293,152,060 304,830,044 1,746,074,905 + +INVESTING ACTIVITIES +Total installed cost ($) -2,859,259,114 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -2,859,259,114 +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,859,259,114 0 0 0 0 0 0 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 ($) 888,022,982 +Size of debt ($) 1,971,236,132 +minus: +Debt principal payment ($) 0 20,868,301 22,329,082 23,892,118 25,564,566 27,354,086 29,268,872 31,317,693 33,509,931 35,855,627 38,365,520 41,051,107 43,924,684 46,999,412 50,289,371 53,809,627 57,576,301 61,606,642 65,919,107 70,533,444 75,470,786 80,753,741 86,406,502 92,454,958 98,926,805 105,851,681 113,261,299 121,189,590 129,672,861 138,749,961 148,462,458 +equals: +Cash flow from financing activities ($) 2,859,259,114 -20,868,301 -22,329,082 -23,892,118 -25,564,566 -27,354,086 -29,268,872 -31,317,693 -33,509,931 -35,855,627 -38,365,520 -41,051,107 -43,924,684 -46,999,412 -50,289,371 -53,809,627 -57,576,301 -61,606,642 -65,919,107 -70,533,444 -75,470,786 -80,753,741 -86,406,502 -92,454,958 -98,926,805 -105,851,681 -113,261,299 -121,189,590 -129,672,861 -138,749,961 -148,462,458 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 126,952,595 130,685,988 135,324,047 139,793,232 144,242,944 148,398,579 149,589,637 141,023,645 158,982,792 165,862,723 164,175,881 169,817,848 175,519,492 180,918,709 182,864,464 176,332,287 196,499,517 204,862,180 212,483,806 220,175,828 228,066,508 235,687,729 239,285,137 236,151,914 259,666,518 270,978,960 281,910,040 293,152,060 304,830,044 1,746,074,905 +Cash flow from investing activities ($) -2,859,259,114 0 0 0 0 0 0 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,859,259,114 -20,868,301 -22,329,082 -23,892,118 -25,564,566 -27,354,086 -29,268,872 -31,317,693 -33,509,931 -35,855,627 -38,365,520 -41,051,107 -43,924,684 -46,999,412 -50,289,371 -53,809,627 -57,576,301 -61,606,642 -65,919,107 -70,533,444 -75,470,786 -80,753,741 -86,406,502 -92,454,958 -98,926,805 -105,851,681 -113,261,299 -121,189,590 -129,672,861 -138,749,961 -148,462,458 +Total pre-tax cash flow ($) 0 106,084,294 108,356,906 111,431,929 114,228,666 116,888,858 119,129,708 118,271,944 107,513,713 123,127,166 127,497,202 123,124,775 125,893,164 128,520,080 130,629,338 129,054,837 118,755,986 134,892,875 138,943,073 141,950,362 144,705,042 147,312,767 149,281,227 146,830,179 137,225,109 153,814,837 157,717,661 160,720,450 163,479,199 166,080,083 1,597,612,447 + +Pre-tax Returns: +Issuance of equity ($) 888,022,982 +Total pre-tax cash flow ($) 0 106,084,294 108,356,906 111,431,929 114,228,666 116,888,858 119,129,708 118,271,944 107,513,713 123,127,166 127,497,202 123,124,775 125,893,164 128,520,080 130,629,338 129,054,837 118,755,986 134,892,875 138,943,073 141,950,362 144,705,042 147,312,767 149,281,227 146,830,179 137,225,109 153,814,837 157,717,661 160,720,450 163,479,199 166,080,083 1,597,612,447 +Total pre-tax returns ($) -888,022,982 106,084,294 108,356,906 111,431,929 114,228,666 116,888,858 119,129,708 118,271,944 107,513,713 123,127,166 127,497,202 123,124,775 125,893,164 128,520,080 130,629,338 129,054,837 118,755,986 134,892,875 138,943,073 141,950,362 144,705,042 147,312,767 149,281,227 146,830,179 137,225,109 153,814,837 157,717,661 160,720,450 163,479,199 166,080,083 1,597,612,447 + +After-tax Returns: +Total pre-tax returns ($) -888,022,982 106,084,294 108,356,906 111,431,929 114,228,666 116,888,858 119,129,708 118,271,944 107,513,713 123,127,166 127,497,202 123,124,775 125,893,164 128,520,080 130,629,338 129,054,837 118,755,986 134,892,875 138,943,073 141,950,362 144,705,042 147,312,767 149,281,227 146,830,179 137,225,109 153,814,837 157,717,661 160,720,450 163,479,199 166,080,083 1,597,612,447 +Federal ITC total income ($) 0 857,777,734 0 0 0 0 0 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 -13,268,124 -1,837,575 -2,767,250 -3,663,076 -4,554,999 -5,387,975 -5,626,717 -3,909,706 -7,509,528 -8,888,575 -8,550,456 -9,681,360 -10,824,226 -11,906,473 -12,296,489 -10,987,147 -15,029,568 -16,705,822 -18,233,538 -19,775,366 -33,535,902 -47,242,427 -47,963,509 -47,335,470 -52,048,855 -54,316,378 -56,507,458 -58,760,865 -61,101,658 -349,991,984 +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 -3,011,797 -417,120 -628,152 -831,500 -1,033,962 -1,223,043 -1,277,236 -887,484 -1,704,625 -2,017,662 -1,940,910 -2,197,620 -2,457,045 -2,702,709 -2,791,241 -2,494,027 -3,411,636 -3,792,137 -4,138,921 -4,488,908 -7,612,480 -10,723,792 -10,887,474 -10,744,912 -11,814,827 -12,329,543 -12,826,907 -13,338,419 -13,869,767 -79,446,408 +Total after-tax returns ($) -888,022,982 947,582,107 106,102,211 108,036,527 109,734,090 111,299,898 112,518,690 111,367,991 102,716,524 113,913,014 116,590,966 112,633,408 114,014,183 115,238,809 116,020,157 113,967,107 105,274,812 116,451,672 118,445,115 119,577,903 120,440,769 106,164,385 91,315,008 87,979,196 79,144,727 89,951,156 91,071,741 91,386,086 91,379,916 91,108,658 1,168,174,054 + +After-tax net cash flow ($) -34,962,211 -69,247,654 -109,836,782 -349,768,514 -324,207,821 947,582,107 106,102,211 108,036,527 109,734,090 111,299,898 112,518,690 111,367,991 102,716,524 113,913,014 116,590,966 112,633,408 114,014,183 115,238,809 116,020,157 113,967,107 105,274,812 116,451,672 118,445,115 119,577,903 120,440,769 106,164,385 91,315,008 87,979,196 79,144,727 89,951,156 91,071,741 91,386,086 91,379,916 91,108,658 1,168,174,054 +After-tax cumulative IRR (%) NaN NaN NaN NaN NaN 3.22 8.18 12.07 14.99 17.16 18.76 19.93 20.74 21.42 21.95 22.34 22.64 22.88 23.07 23.22 23.32 23.42 23.49 23.55 23.60 23.64 23.66 23.68 23.69 23.70 23.71 23.72 23.73 23.73 23.78 +After-tax cumulative NPV ($) -34,962,211 -95,164,998 -178,182,731 -408,017,280 -593,229,494 -122,605,007 -76,791,489 -36,235,841 -423,407 31,155,607 58,910,529 82,793,440 101,943,878 120,407,771 136,837,348 150,636,113 162,779,601 173,450,348 182,790,223 190,766,471 197,172,002 203,332,105 208,779,274 213,560,246 217,746,739 220,954,982 223,354,047 225,363,562 226,935,173 228,488,065 229,854,942 231,047,385 232,084,006 232,982,552 242,998,676 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -888,022,982 546,462,802 -297,330,185 -298,525,664 -299,674,652 -300,816,419 -301,878,390 -302,156,047 -299,857,661 -304,552,726 -306,322,648 -313,418,611 -314,906,632 -316,404,200 -317,808,612 -318,230,054 -316,249,964 -321,795,270 -323,998,923 -325,982,497 -327,974,221 -344,952,911 -361,842,142 -362,638,008 -361,519,038 -367,904,037 -370,827,829 -373,625,183 -376,490,159 -379,456,634 695,636,942 +PPA revenue ($) 0 401,119,305 403,432,396 406,562,190 409,408,742 412,116,317 414,397,080 413,524,038 402,574,185 418,465,740 422,913,614 426,052,019 428,920,815 431,643,009 433,828,769 432,197,161 421,524,776 438,246,942 442,444,038 445,560,399 448,414,990 451,117,296 453,157,151 450,617,205 440,663,765 457,855,192 461,899,570 465,011,268 467,870,075 470,565,292 472,537,112 +Electricity to grid (kWh) 0.0 4,222,308,474 4,246,656,805 4,254,077,539 4,258,464,138 4,261,361,983 4,259,838,404 4,226,101,564 4,090,369,692 4,227,353,669 4,247,826,578 4,254,988,701 4,259,392,408 4,262,298,892 4,259,905,427 4,220,263,261 4,093,268,367 4,232,225,414 4,249,366,482 4,255,997,698 4,260,070,210 4,262,659,891 4,258,995,778 4,212,556,835 4,097,673,094 4,235,086,416 4,250,088,054 4,256,396,046 4,260,335,780 4,262,752,890 4,258,625,740 + +Present value of annual costs ($) 2,146,282,293 +Present value of annual energy nominal (kWh) 27,766,031,299 +LCOE Levelized cost of energy nominal (cents/kWh) 7.73 + +Present value of PPA revenue ($) 2,722,048,915 +Present value of annual energy nominal (kWh) 27,766,031,299 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 +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 137,986,529 136,525,748 134,962,712 133,290,264 131,500,745 129,585,959 127,537,138 125,344,899 122,999,204 120,489,310 117,803,724 114,930,146 111,855,418 108,565,459 105,045,203 101,278,529 97,248,188 92,935,723 88,321,386 83,384,045 78,101,090 72,448,328 66,399,873 59,928,026 53,003,149 45,593,532 37,665,241 29,181,970 20,104,869 10,392,372 +Total state tax depreciation ($) 0 60,759,256 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 60,759,256 0 0 0 0 0 0 0 0 0 +equals: +State taxable income ($) 0 66,193,339 9,167,476 13,805,534 18,274,720 22,724,432 26,880,067 28,071,124 19,505,132 37,464,280 44,344,211 42,657,369 48,299,336 54,000,980 59,400,197 61,345,951 54,813,775 74,981,005 83,343,668 90,965,294 98,657,315 167,307,252 235,687,729 239,285,137 236,151,914 259,666,518 270,978,960 281,910,040 293,152,060 304,830,044 1,746,074,905 + +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 -3,011,797 -417,120 -628,152 -831,500 -1,033,962 -1,223,043 -1,277,236 -887,484 -1,704,625 -2,017,662 -1,940,910 -2,197,620 -2,457,045 -2,702,709 -2,791,241 -2,494,027 -3,411,636 -3,792,137 -4,138,921 -4,488,908 -7,612,480 -10,723,792 -10,887,474 -10,744,912 -11,814,827 -12,329,543 -12,826,907 -13,338,419 -13,869,767 -79,446,408 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 +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 -3,011,797 -417,120 -628,152 -831,500 -1,033,962 -1,223,043 -1,277,236 -887,484 -1,704,625 -2,017,662 -1,940,910 -2,197,620 -2,457,045 -2,702,709 -2,791,241 -2,494,027 -3,411,636 -3,792,137 -4,138,921 -4,488,908 -7,612,480 -10,723,792 -10,887,474 -10,744,912 -11,814,827 -12,329,543 -12,826,907 -13,338,419 -13,869,767 -79,446,408 +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 137,986,529 136,525,748 134,962,712 133,290,264 131,500,745 129,585,959 127,537,138 125,344,899 122,999,204 120,489,310 117,803,724 114,930,146 111,855,418 108,565,459 105,045,203 101,278,529 97,248,188 92,935,723 88,321,386 83,384,045 78,101,090 72,448,328 66,399,873 59,928,026 53,003,149 45,593,532 37,665,241 29,181,970 20,104,869 10,392,372 +Total federal tax depreciation ($) 0 60,759,256 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 121,518,512 60,759,256 0 0 0 0 0 0 0 0 0 +equals: +Federal taxable income ($) 0 63,181,542 8,750,356 13,177,383 17,443,220 21,690,470 25,657,024 26,793,888 18,617,649 35,759,655 42,326,549 40,716,459 46,101,716 51,543,935 56,697,488 58,554,710 52,319,748 71,569,369 79,551,531 86,826,373 94,168,408 159,694,772 224,963,938 228,397,663 225,407,002 247,851,692 258,649,417 269,083,133 279,813,641 290,960,277 1,666,628,497 + +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 -13,268,124 -1,837,575 -2,767,250 -3,663,076 -4,554,999 -5,387,975 -5,626,717 -3,909,706 -7,509,528 -8,888,575 -8,550,456 -9,681,360 -10,824,226 -11,906,473 -12,296,489 -10,987,147 -15,029,568 -16,705,822 -18,233,538 -19,775,366 -33,535,902 -47,242,427 -47,963,509 -47,335,470 -52,048,855 -54,316,378 -56,507,458 -58,760,865 -61,101,658 -349,991,984 + +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 857,777,734 0 0 0 0 0 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 857,777,734 0 0 0 0 0 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,971,236,132 1,950,367,831 1,928,038,749 1,904,146,631 1,878,582,065 1,851,227,979 1,821,959,107 1,790,641,414 1,757,131,483 1,721,275,856 1,682,910,336 1,641,859,229 1,597,934,545 1,550,935,133 1,500,645,761 1,446,836,134 1,389,259,833 1,327,653,191 1,261,734,084 1,191,200,640 1,115,729,854 1,034,976,114 948,569,611 856,114,654 757,187,849 651,336,168 538,074,870 416,885,280 287,212,419 148,462,458 0 +Debt interest payment ($) 0 137,986,529 136,525,748 134,962,712 133,290,264 131,500,745 129,585,959 127,537,138 125,344,899 122,999,204 120,489,310 117,803,724 114,930,146 111,855,418 108,565,459 105,045,203 101,278,529 97,248,188 92,935,723 88,321,386 83,384,045 78,101,090 72,448,328 66,399,873 59,928,026 53,003,149 45,593,532 37,665,241 29,181,970 20,104,869 10,392,372 +Debt principal payment ($) 0 20,868,301 22,329,082 23,892,118 25,564,566 27,354,086 29,268,872 31,317,693 33,509,931 35,855,627 38,365,520 41,051,107 43,924,684 46,999,412 50,289,371 53,809,627 57,576,301 61,606,642 65,919,107 70,533,444 75,470,786 80,753,741 86,406,502 92,454,958 98,926,805 105,851,681 113,261,299 121,189,590 129,672,861 138,749,961 148,462,458 +Debt total payment ($) 0 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 +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 264,939,124 267,211,737 270,286,759 273,083,496 275,743,689 277,984,538 277,126,774 266,368,544 281,981,996 286,352,033 281,979,605 284,747,994 287,374,910 289,484,169 287,909,667 277,610,816 293,747,706 297,797,904 300,805,192 303,559,873 306,167,598 308,136,057 305,685,009 296,079,940 312,669,668 316,572,492 319,575,281 322,334,030 324,934,913 1,756,467,277 +Debt total payment ($) 0 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 158,854,830 +DSCR (pre-tax) 0.0 1.67 1.68 1.70 1.72 1.74 1.75 1.74 1.68 1.78 1.80 1.78 1.79 1.81 1.82 1.81 1.75 1.85 1.87 1.89 1.91 1.93 1.94 1.92 1.86 1.97 1.99 2.01 2.03 2.05 11.06 + +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: 143.17 MUSD + Royalty Holder Average Annual Revenue: 12.82 MUSD/yr + Royalty Holder Total Revenue: 384.73 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..cce9892fe --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-5.txt @@ -0,0 +1,122 @@ +# 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, -- Supercritical ORC +Plant Lifetime, 30, -- 30-year well life per [Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/) (Fervo Energy, 2025). + +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, 107, -- 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, 300 psi, -- Set constant in Singh et al., 2025. Actual production WHP may gradually increase over time if flow rates are kept constant. + +Productivity Index, 1.7458, -- Based on ATB Conservative Scenario (NREL, 2025) derated by 30% per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024). +Injectivity Index, 2.1105, -- See Productivity Index + +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). +Ramey Production Wellbore Model, True, -- Ramey's model estimates the geofluid temperature drop in production wells +Injection Wellbore Temperature Gain, 3 + + +Maximum Drawdown, 0.023, -- This value represents the drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is tuned to keep minimum net electricity generation over the project lifetime ≥500 MWe. + +# *** 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..4a45b8e25 --- /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-18 + Simulation Time: 13:25 + Calculation Time: 1.841 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 108.11 MW + Electricity breakeven price: 7.88 cents/kWh + Total CAPEX: 573.25 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: 171.97 MUSD + Project lifetime: 30 yr + Capacity factor: 90.0 % + Project NPV: 75.50 MUSD + After-tax IRR: 32.77 % + Project VIR=PI=PIR: 1.57 + Project MOIC: 4.56 + Project Payback Period: 3.90 yr + Estimated Jobs Created: 254 + + ***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: 2082.43 kPa + Productivity Index: 1.75 kg/sec/bar + Injectivity Index: 2.11 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.5 degC + Minimum Production Temperature: 197.2 degC + Initial Production Temperature: 201.6 degC + Average Reservoir Heat Extraction: 732.78 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 0.3 degC + Average Injection Well Pump Pressure Drop: -5725.7 kPa + Average Production Well Pump Pressure Drop: 6395.0 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: 8.86 MUSD + Total surface equipment costs: 302.30 MUSD + Exploration costs: 30.00 MUSD + Overnight Capital Cost: 521.88 MUSD + Interest during construction: 12.52 MUSD + Inflation costs during construction: 38.85 MUSD + Total CAPEX: 573.25 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.58 MUSD/yr + Total operating and maintenance costs: 30.36 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.05 MW + Minimum Total Electricity Generation: 111.41 MW + Initial Total Electricity Generation: 117.79 MW + Maximum Net Electricity Generation: 109.00 MW + Average Net Electricity Generation: 108.11 MW + Minimum Net Electricity Generation: 100.26 MW + Initial Net Electricity Generation: 106.84 MW + Average Annual Total Electricity Generation: 938.64 GWh + Average Annual Net Electricity Generation: 852.39 GWh + Initial pumping power/net installed power: 10.25 % + Average Pumping Power: 10.94 MW + Heat to Power Conversion Efficiency: 14.75 % + + ************************************************************ + * 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 10.9540 106.8371 14.6679 + 2 1.0052 202.70 10.9451 108.3579 14.7695 + 3 1.0062 202.90 10.9435 108.6408 14.7883 + 4 1.0067 203.00 10.9426 108.7880 14.7980 + 5 1.0070 203.06 10.9420 108.8853 14.8045 + 6 1.0073 203.11 10.9417 108.9538 14.8090 + 7 1.0072 203.10 10.9432 108.9326 14.8075 + 8 1.0049 202.63 10.9602 108.2412 14.7607 + 9 0.9925 200.14 11.0459 104.5816 14.5095 + 10 1.0038 202.40 10.9385 107.9394 14.7423 + 11 1.0057 202.80 10.9353 108.5143 14.7805 + 12 1.0064 202.94 10.9306 108.7235 14.7946 + 13 1.0068 203.03 10.9253 108.8497 14.8033 + 14 1.0071 203.09 10.9206 108.9386 14.8095 + 15 1.0073 203.12 10.9174 108.9922 14.8132 + 16 1.0068 203.01 10.9204 108.8287 14.8023 + 17 1.0017 201.99 10.9558 107.3250 14.7003 + 18 0.9808 197.77 11.1002 101.1646 14.2705 + 19 1.0049 202.64 10.9132 108.3084 14.7685 + 20 1.0061 202.87 10.9131 108.6381 14.7902 + 21 1.0066 202.98 10.9131 108.7975 14.8007 + 22 1.0070 203.05 10.9132 108.9002 14.8075 + 23 1.0072 203.11 10.9132 108.9732 14.8123 + 24 1.0073 203.11 10.9144 108.9820 14.8128 + 25 1.0056 202.78 10.9271 108.4969 14.7799 + 26 0.9959 200.81 10.9962 105.5988 14.5818 + 27 1.0027 202.20 10.9133 107.6704 14.7262 + 28 1.0055 202.77 10.9134 108.4849 14.7801 + 29 1.0063 202.93 10.9134 108.7146 14.7953 + 30 1.0068 203.02 10.9134 108.8447 14.8038 + + + ******************************************************************* + * 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 850.4 5770.5 260.21 7.39 + 2 855.6 5788.6 239.37 14.81 + 3 857.2 5794.1 218.51 22.23 + 4 858.1 5797.3 197.64 29.66 + 5 858.7 5799.6 176.77 37.09 + 6 859.0 5800.7 155.88 44.52 + 7 857.0 5793.7 135.03 51.95 + 8 842.2 5743.1 114.35 59.30 + 9 825.4 5684.8 93.89 66.59 + 10 853.8 5782.3 73.07 74.00 + 11 856.4 5791.3 52.22 81.42 + 12 857.7 5795.6 31.36 88.84 + 13 858.5 5798.4 10.48 96.27 + 14 859.1 5800.3 -10.40 103.70 + 15 859.0 5799.7 -31.28 111.13 + 16 853.7 5781.8 -52.09 118.54 + 17 826.5 5688.5 -72.57 125.83 + 18 842.5 5742.5 -93.24 133.18 + 19 855.4 5787.2 -114.08 140.60 + 20 857.2 5793.4 -134.93 148.02 + 21 858.2 5796.9 -155.80 155.45 + 22 858.9 5799.3 -176.68 162.88 + 23 859.3 5800.7 -197.56 170.31 + 24 858.0 5796.3 -218.43 177.74 + 25 846.7 5757.8 -239.16 185.11 + 26 822.0 5672.7 -259.58 192.38 + 27 853.1 5779.1 -280.38 199.79 + 28 856.3 5790.3 -301.23 207.20 + 29 857.7 5795.0 -322.09 214.63 + 30 858.5 5797.9 -342.96 222.06 + + *************************** + * 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,480,200 -133,839,124 -374,556,974 +plus: +Inflation cost [construction] ($) -363,965 -7,324,881 -31,165,643 +equals: +Nominal capital expenditure [construction] ($) -13,844,165 -141,164,006 -405,722,617 + +Issuance of equity [construction] ($) 4,153,249 42,349,202 121,716,785 +Issuance of debt [construction] ($) 9,690,915 98,814,804 284,005,832 +Debt balance [construction] ($) 9,690,915 109,523,266 405,029,040 +Debt interest payment [construction] ($) 0 1,017,546 11,499,943 + +Installed cost [construction] ($) -13,844,165 -142,181,552 -417,222,560 +After-tax net cash flow [construction] ($) -4,153,249 -42,349,202 -121,716,785 + +ENERGY +Electricity to grid (kWh) 0.0 850,406,006 855,614,788 857,202,718 858,141,887 858,792,946 859,092,389 857,044,513 842,215,547 825,402,976 853,858,875 856,488,292 857,758,035 858,591,531 859,172,323 859,017,915 853,773,253 826,524,048 842,538,104 855,459,383 857,237,492 858,240,752 858,925,989 859,322,357 858,032,479 846,727,661 822,039,896 853,149,561 856,358,515 857,708,057 858,524,604 +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 850,406,006 855,614,788 857,202,718 858,141,887 858,792,946 859,092,389 857,044,513 842,215,547 825,402,976 853,858,875 856,488,292 857,758,035 858,591,531 859,172,323 859,017,915 853,773,253 826,524,048 842,538,104 855,459,383 857,237,492 858,240,752 858,925,989 859,322,357 858,032,479 846,727,661 822,039,896 853,149,561 856,358,515 857,708,057 858,524,604 + +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 80,788,571 81,283,405 81,922,864 82,501,761 83,053,866 83,572,508 83,861,806 82,890,854 81,706,641 85,010,190 85,760,173 86,376,234 86,949,564 87,498,109 87,972,025 87,921,570 85,586,565 87,725,067 89,558,043 90,232,818 90,827,619 91,389,725 91,921,713 92,272,813 91,539,727 89,339,296 93,206,590 94,045,292 94,682,392 95,261,890 +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,624,138 +Total revenue ($) 0 80,788,571 81,283,405 81,922,864 82,501,761 83,053,866 83,572,508 83,861,806 82,890,854 81,706,641 85,010,190 85,760,173 86,376,234 86,949,564 87,498,109 87,972,025 87,921,570 85,586,565 87,725,067 89,558,043 90,232,818 90,827,619 91,389,725 91,921,713 92,272,813 91,539,727 89,339,296 93,206,590 94,045,292 94,682,392 381,886,028 + +Property tax net assessed value ($) 0 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 573,248,277 + +OPERATING EXPENSES +O&M fixed expense ($) 0 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 27,774,514 +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,413,800 1,422,460 1,433,650 1,443,781 1,453,443 1,462,519 1,467,582 1,450,590 1,429,866 1,487,678 3,001,606 3,023,168 3,043,235 3,062,434 3,079,021 3,077,255 2,995,530 3,070,377 3,134,531 3,158,149 3,178,967 3,198,640 3,217,260 3,229,548 3,203,890 3,126,875 3,262,231 3,291,585 3,313,884 3,334,166 +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,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 1,261,146 +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,449,460 30,458,120 30,469,310 30,479,441 30,489,103 30,498,179 30,503,242 30,486,250 30,465,527 30,523,339 32,037,266 32,058,829 32,078,895 32,098,094 32,114,681 32,112,915 32,031,190 32,106,038 32,170,192 32,193,809 32,214,627 32,234,301 32,252,920 32,265,209 32,239,551 32,162,536 32,297,891 32,327,246 32,349,544 32,369,826 + +EBITDA ($) 0 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 + +OPERATING ACTIVITIES +EBITDA ($) 0 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 +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,352,033 28,051,887 27,730,730 27,387,093 27,019,401 26,625,971 26,205,001 25,754,562 25,272,594 24,756,887 24,205,080 23,614,648 22,982,885 22,306,898 21,583,593 20,809,656 19,981,543 19,095,463 18,147,357 17,132,884 16,047,397 14,885,927 13,643,153 12,313,386 10,890,534 9,368,083 7,739,061 5,996,007 4,130,939 2,135,316 +Cash flow from operating activities ($) 0 21,987,077 22,773,398 23,722,823 24,635,227 25,545,361 26,448,357 27,153,563 26,650,041 25,968,521 29,729,964 29,517,826 30,702,758 31,887,785 33,093,117 34,273,751 34,998,998 33,573,832 36,523,567 39,240,494 40,906,126 42,565,594 44,269,498 46,025,639 47,694,218 48,409,642 47,808,677 53,169,638 55,722,040 58,201,910 347,380,886 + +INVESTING ACTIVITIES +Total installed cost ($) -573,248,277 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -573,248,277 +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,248,277 0 0 0 0 0 0 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,219,236 +Size of debt ($) 405,029,040 +minus: +Debt principal payment ($) 0 4,287,801 4,587,947 4,909,103 5,252,740 5,620,432 6,013,863 6,434,833 6,885,271 7,367,240 7,882,947 8,434,753 9,025,186 9,656,949 10,332,935 11,056,241 11,830,178 12,658,290 13,544,371 14,492,477 15,506,950 16,592,436 17,753,907 18,996,680 20,326,448 21,749,299 23,271,750 24,900,773 26,643,827 28,508,895 30,504,517 +equals: +Cash flow from financing activities ($) 573,248,277 -4,287,801 -4,587,947 -4,909,103 -5,252,740 -5,620,432 -6,013,863 -6,434,833 -6,885,271 -7,367,240 -7,882,947 -8,434,753 -9,025,186 -9,656,949 -10,332,935 -11,056,241 -11,830,178 -12,658,290 -13,544,371 -14,492,477 -15,506,950 -16,592,436 -17,753,907 -18,996,680 -20,326,448 -21,749,299 -23,271,750 -24,900,773 -26,643,827 -28,508,895 -30,504,517 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 21,987,077 22,773,398 23,722,823 24,635,227 25,545,361 26,448,357 27,153,563 26,650,041 25,968,521 29,729,964 29,517,826 30,702,758 31,887,785 33,093,117 34,273,751 34,998,998 33,573,832 36,523,567 39,240,494 40,906,126 42,565,594 44,269,498 46,025,639 47,694,218 48,409,642 47,808,677 53,169,638 55,722,040 58,201,910 347,380,886 +Cash flow from investing activities ($) -573,248,277 0 0 0 0 0 0 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,248,277 -4,287,801 -4,587,947 -4,909,103 -5,252,740 -5,620,432 -6,013,863 -6,434,833 -6,885,271 -7,367,240 -7,882,947 -8,434,753 -9,025,186 -9,656,949 -10,332,935 -11,056,241 -11,830,178 -12,658,290 -13,544,371 -14,492,477 -15,506,950 -16,592,436 -17,753,907 -18,996,680 -20,326,448 -21,749,299 -23,271,750 -24,900,773 -26,643,827 -28,508,895 -30,504,517 +Total pre-tax cash flow ($) 0 17,699,277 18,185,451 18,813,720 19,382,486 19,924,929 20,434,495 20,718,730 19,764,770 18,601,280 21,847,017 21,083,073 21,677,572 22,230,836 22,760,182 23,217,510 23,168,821 20,915,541 22,979,196 24,748,017 25,399,176 25,973,158 26,515,591 27,028,959 27,367,770 26,660,343 24,536,926 28,268,865 29,078,213 29,693,015 316,876,368 + +Pre-tax Returns: +Issuance of equity ($) 168,219,236 +Total pre-tax cash flow ($) 0 17,699,277 18,185,451 18,813,720 19,382,486 19,924,929 20,434,495 20,718,730 19,764,770 18,601,280 21,847,017 21,083,073 21,677,572 22,230,836 22,760,182 23,217,510 23,168,821 20,915,541 22,979,196 24,748,017 25,399,176 25,973,158 26,515,591 27,028,959 27,367,770 26,660,343 24,536,926 28,268,865 29,078,213 29,693,015 316,876,368 +Total pre-tax returns ($) -168,219,236 17,699,277 18,185,451 18,813,720 19,382,486 19,924,929 20,434,495 20,718,730 19,764,770 18,601,280 21,847,017 21,083,073 21,677,572 22,230,836 22,760,182 23,217,510 23,168,821 20,915,541 22,979,196 24,748,017 25,399,176 25,973,158 26,515,591 27,028,959 27,367,770 26,660,343 24,536,926 28,268,865 29,078,213 29,693,015 316,876,368 + +After-tax Returns: +Total pre-tax returns ($) -168,219,236 17,699,277 18,185,451 18,813,720 19,382,486 19,924,929 20,434,495 20,718,730 19,764,770 18,601,280 21,847,017 21,083,073 21,677,572 22,230,836 22,760,182 23,217,510 23,168,821 20,915,541 22,979,196 24,748,017 25,399,176 25,973,158 26,515,591 27,028,959 27,367,770 26,660,343 24,536,926 28,268,865 29,078,213 29,693,015 316,876,368 +Federal ITC total income ($) 0 171,974,483 0 0 0 0 0 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,965,474 318,638 128,331 -54,556 -236,988 -417,989 -559,344 -458,416 -321,808 -1,075,771 -1,033,249 -1,270,762 -1,508,295 -1,749,898 -1,986,550 -2,131,922 -1,846,255 -2,437,514 -2,982,109 -3,315,976 -6,090,335 -8,873,599 -9,225,609 -9,560,068 -9,703,471 -9,583,010 -10,657,588 -11,169,204 -11,666,282 -69,630,762 +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 -446,153 72,329 29,130 -12,384 -53,795 -94,881 -126,968 -104,058 -73,049 -244,195 -234,542 -288,457 -342,375 -397,218 -450,937 -483,936 -419,090 -553,303 -676,924 -752,710 -1,382,475 -2,014,262 -2,094,167 -2,170,087 -2,202,639 -2,175,295 -2,419,219 -2,535,353 -2,648,187 -15,805,830 +Total after-tax returns ($) -168,219,236 187,262,133 18,576,419 18,971,181 19,315,546 19,634,146 19,921,624 20,032,418 19,202,297 18,206,423 20,527,052 19,815,282 20,118,353 20,380,165 20,613,066 20,780,023 20,552,963 18,650,196 19,988,378 21,088,985 21,330,489 18,500,348 15,627,729 15,709,183 15,637,616 14,754,233 12,778,621 15,192,058 15,373,656 15,378,546 231,439,776 + +After-tax net cash flow ($) -4,153,249 -42,349,202 -121,716,785 187,262,133 18,576,419 18,971,181 19,315,546 19,634,146 19,921,624 20,032,418 19,202,297 18,206,423 20,527,052 19,815,282 20,118,353 20,380,165 20,613,066 20,780,023 20,552,963 18,650,196 19,988,378 21,088,985 21,330,489 18,500,348 15,627,729 15,709,183 15,637,616 14,754,233 12,778,621 15,192,058 15,373,656 15,378,546 231,439,776 +After-tax cumulative IRR (%) NaN NaN NaN 8.52 15.45 20.61 24.25 26.77 28.51 29.73 30.55 31.11 31.56 31.87 32.11 32.28 32.41 32.50 32.57 32.62 32.66 32.69 32.71 32.73 32.74 32.74 32.75 32.75 32.75 32.76 32.76 32.76 32.77 +After-tax cumulative NPV ($) -4,153,249 -40,970,958 -132,967,932 -9,917,096 695,170 10,117,364 18,457,559 25,827,987 32,329,539 38,013,318 42,749,937 46,654,312 50,481,370 53,693,182 56,528,188 59,024,970 61,220,438 63,144,602 64,799,161 66,104,437 67,320,646 68,436,219 69,417,186 70,156,868 70,700,085 71,174,809 71,585,647 71,922,646 72,176,396 72,438,668 72,669,408 72,870,074 75,495,544 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -168,219,236 106,473,563 -62,706,986 -62,951,683 -63,186,215 -63,419,720 -63,650,883 -63,829,388 -63,688,558 -63,500,217 -64,483,138 -65,944,891 -66,257,881 -66,569,399 -66,885,044 -67,192,002 -67,368,607 -66,936,369 -67,736,689 -68,469,058 -68,902,329 -72,327,270 -75,761,996 -76,212,530 -76,635,197 -76,785,494 -76,560,674 -78,014,531 -78,671,636 -79,303,846 136,177,886 +PPA revenue ($) 0 80,788,571 81,283,405 81,922,864 82,501,761 83,053,866 83,572,508 83,861,806 82,890,854 81,706,641 85,010,190 85,760,173 86,376,234 86,949,564 87,498,109 87,972,025 87,921,570 85,586,565 87,725,067 89,558,043 90,232,818 90,827,619 91,389,725 91,921,713 92,272,813 91,539,727 89,339,296 93,206,590 94,045,292 94,682,392 95,261,890 +Electricity to grid (kWh) 0.0 850,406,006 855,614,788 857,202,718 858,141,887 858,792,946 859,092,389 857,044,513 842,215,547 825,402,976 853,858,875 856,488,292 857,758,035 858,591,531 859,172,323 859,017,915 853,773,253 826,524,048 842,538,104 855,459,383 857,237,492 858,240,752 858,925,989 859,322,357 858,032,479 846,727,661 822,039,896 853,149,561 856,358,515 857,708,057 858,524,604 + +Present value of annual costs ($) 440,936,703 +Present value of annual energy nominal (kWh) 5,595,253,096 +LCOE Levelized cost of energy nominal (cents/kWh) 7.88 + +Present value of PPA revenue ($) 548,525,497 +Present value of annual energy nominal (kWh) 5,595,253,096 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 +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,352,033 28,051,887 27,730,730 27,387,093 27,019,401 26,625,971 26,205,001 25,754,562 25,272,594 24,756,887 24,205,080 23,614,648 22,982,885 22,306,898 21,583,593 20,809,656 19,981,543 19,095,463 18,147,357 17,132,884 16,047,397 14,885,927 13,643,153 12,313,386 10,890,534 9,368,083 7,739,061 5,996,007 4,130,939 2,135,316 +Total state tax depreciation ($) 0 12,181,526 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 12,181,526 0 0 0 0 0 0 0 0 0 +equals: +State taxable income ($) 0 9,805,552 -1,589,654 -640,229 272,175 1,182,310 2,085,305 2,790,511 2,286,990 1,605,469 5,366,912 5,154,774 6,339,706 7,524,733 8,730,065 9,910,699 10,635,947 9,210,780 12,160,515 14,877,442 16,543,074 30,384,069 44,269,498 46,025,639 47,694,218 48,409,642 47,808,677 53,169,638 55,722,040 58,201,910 347,380,886 + +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 -446,153 72,329 29,130 -12,384 -53,795 -94,881 -126,968 -104,058 -73,049 -244,195 -234,542 -288,457 -342,375 -397,218 -450,937 -483,936 -419,090 -553,303 -676,924 -752,710 -1,382,475 -2,014,262 -2,094,167 -2,170,087 -2,202,639 -2,175,295 -2,419,219 -2,535,353 -2,648,187 -15,805,830 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 +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 -446,153 72,329 29,130 -12,384 -53,795 -94,881 -126,968 -104,058 -73,049 -244,195 -234,542 -288,457 -342,375 -397,218 -450,937 -483,936 -419,090 -553,303 -676,924 -752,710 -1,382,475 -2,014,262 -2,094,167 -2,170,087 -2,202,639 -2,175,295 -2,419,219 -2,535,353 -2,648,187 -15,805,830 +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,352,033 28,051,887 27,730,730 27,387,093 27,019,401 26,625,971 26,205,001 25,754,562 25,272,594 24,756,887 24,205,080 23,614,648 22,982,885 22,306,898 21,583,593 20,809,656 19,981,543 19,095,463 18,147,357 17,132,884 16,047,397 14,885,927 13,643,153 12,313,386 10,890,534 9,368,083 7,739,061 5,996,007 4,130,939 2,135,316 +Total federal tax depreciation ($) 0 12,181,526 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 24,363,052 12,181,526 0 0 0 0 0 0 0 0 0 +equals: +Federal taxable income ($) 0 9,359,399 -1,517,324 -611,099 259,791 1,128,515 1,990,424 2,663,543 2,182,932 1,532,420 5,122,718 4,920,232 6,051,249 7,182,358 8,332,847 9,459,762 10,152,011 8,791,689 11,607,211 14,200,518 15,790,364 29,001,593 42,255,236 43,931,472 45,524,131 46,207,004 45,633,382 50,750,419 53,186,687 55,553,723 331,575,055 + +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,965,474 318,638 128,331 -54,556 -236,988 -417,989 -559,344 -458,416 -321,808 -1,075,771 -1,033,249 -1,270,762 -1,508,295 -1,749,898 -1,986,550 -2,131,922 -1,846,255 -2,437,514 -2,982,109 -3,315,976 -6,090,335 -8,873,599 -9,225,609 -9,560,068 -9,703,471 -9,583,010 -10,657,588 -11,169,204 -11,666,282 -69,630,762 + +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 171,974,483 0 0 0 0 0 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 171,974,483 0 0 0 0 0 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,029,040 400,741,240 396,153,293 391,244,189 385,991,449 380,371,017 374,357,154 367,922,321 361,037,050 353,669,810 345,786,863 337,352,110 328,326,924 318,669,975 308,337,039 297,280,798 285,450,621 272,792,330 259,247,960 244,755,483 229,248,533 212,656,097 194,902,190 175,905,510 155,579,062 133,829,762 110,558,012 85,657,239 59,013,412 30,504,517 0 +Debt interest payment ($) 0 28,352,033 28,051,887 27,730,730 27,387,093 27,019,401 26,625,971 26,205,001 25,754,562 25,272,594 24,756,887 24,205,080 23,614,648 22,982,885 22,306,898 21,583,593 20,809,656 19,981,543 19,095,463 18,147,357 17,132,884 16,047,397 14,885,927 13,643,153 12,313,386 10,890,534 9,368,083 7,739,061 5,996,007 4,130,939 2,135,316 +Debt principal payment ($) 0 4,287,801 4,587,947 4,909,103 5,252,740 5,620,432 6,013,863 6,434,833 6,885,271 7,367,240 7,882,947 8,434,753 9,025,186 9,656,949 10,332,935 11,056,241 11,830,178 12,658,290 13,544,371 14,492,477 15,506,950 16,592,436 17,753,907 18,996,680 20,326,448 21,749,299 23,271,750 24,900,773 26,643,827 28,508,895 30,504,517 +Debt total payment ($) 0 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 +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 50,339,110 50,825,285 51,453,553 52,022,320 52,564,763 53,074,328 53,358,564 52,404,604 51,241,114 54,486,851 53,722,906 54,317,406 54,870,669 55,400,015 55,857,343 55,808,654 53,555,375 55,619,030 57,387,851 58,039,009 58,612,992 59,155,424 59,668,792 60,007,604 59,300,177 57,176,760 60,908,699 61,718,047 62,332,848 349,516,202 +Debt total payment ($) 0 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 32,639,834 +DSCR (pre-tax) 0.0 1.54 1.56 1.58 1.59 1.61 1.63 1.63 1.61 1.57 1.67 1.65 1.66 1.68 1.70 1.71 1.71 1.64 1.70 1.76 1.78 1.80 1.81 1.83 1.84 1.82 1.75 1.87 1.89 1.91 10.71 + +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.80 MUSD + Royalty Holder Average Annual Revenue: 2.58 MUSD/yr + Royalty Holder Total Revenue: 77.47 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..13e7f8ee5 --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-6.txt @@ -0,0 +1,122 @@ +# 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, -- Supercritical ORC +Plant Lifetime, 30, -- 30-year well life per [Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/) (Fervo Energy, 2025). + +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, 300 psi, -- Set constant in Singh et al., 2025. Actual production WHP may gradually increase over time if flow rates are kept constant. + +Productivity Index, 1.7458, -- Based on ATB Conservative Scenario (NREL, 2025) derated by 30% per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024). +Injectivity Index, 2.1105, -- See Productivity Index + +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). +Ramey Production Wellbore Model, True, -- Ramey's model estimates the geofluid temperature drop in production wells +Injection Wellbore Temperature Gain, 3 + + +Maximum Drawdown, 0.023, -- This value represents the drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is tuned to keep minimum net electricity generation over the project lifetime ≥100 MWe. + +# *** 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..9ca0dce3f --- /dev/null +++ b/tests/geophires_x_tests/test_fervo_project_cape_5.py @@ -0,0 +1,495 @@ +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) + + @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', + } + + 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)) + + # FIXME WIP refactor to work with number of injection wells per production well + # num_doublets = inputs_in_markdown['Number of Doublets']['value'] + # self.assertEqual( + # example_result.result['SUMMARY OF RESULTS']['Number of production wells']['value'], num_doublets + # ) + # + # num_fracs_per_well = inputs_in_markdown['Number of Fractures per Well']['value'] + # expected_total_fracs = num_doublets * 2 * num_fracs_per_well + # self.assertEqual( + # expected_total_fracs, example_result.result['RESERVOIR PARAMETERS']['Number of fractures']['value'] + # ) + + # FIXME WIP + # self.assertEqual( + # example_result.result['RESERVOIR PARAMETERS']['Reservoir volume']['value'], + # results_in_markdown['Reservoir Volume']['value'] + # ) + + 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', + '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. + """ + clean_str = re.split(r'\s*\(|,(?!\s*\d)', raw_string)[0].strip() + + if clean_str.startswith('$') and ('M total' in clean_str or 'M baseline cost' in clean_str): + return {'value': float(clean_str.split('M ')[0][1:]), 'unit': 'MUSD'} + + # LCOE format ($X.X/MWh -> 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..74e22039d 100755 --- a/tests/regenerate-example-result.sh +++ b/tests/regenerate-example-result.sh @@ -1,15 +1,19 @@ #!/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")" python -mgeophires_x examples/$1.txt examples/$1.out rm examples/$1.json @@ -18,3 +22,38 @@ then echo "Updating 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 + + echo "Regenerating Fervo_Project_Cape-6..." + + sed -e 's/Construction Years,.*/Construction Years, 3/' \ + -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 + + python -mgeophires_x examples/Fervo_Project_Cape-6.txt examples/Fervo_Project_Cape-6.out + rm examples/Fervo_Project_Cape-6.json + + 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 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]