diff --git a/.gitignore b/.gitignore index 7cf1ec9e6..a81bd2e21 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,8 @@ nosetests.xml .vscode ai.md +Claude outputs/ +.claude # Complexity output/*.html @@ -105,7 +107,7 @@ docs/temp.txt docs/tmp docs/reference/geophires-request.json docs/reference/parameters.rst -docs/Fervo_Project_Cape-5.md +docs/Fervo_Project_Cape-7.md docs/Fervo_Project_Red.md docs/_images/*.bak.png docs/_images/singh-et-al-2025_fig-7_well-and-bench-spacing.xcf diff --git a/README.rst b/README.rst index e7f9eb673..3195542cb 100644 --- a/README.rst +++ b/README.rst @@ -168,11 +168,11 @@ Example-specific web interface deeplinks are listed in the Link column. - Input file - Case report file - Link - * - Case Study: 500 MW EGS Modeled on Fervo Cape Station (2026 Update) (`documentation `__) - - `Fervo_Project_Cape-5.txt `__ - - `.out `__ - - `link `__ - * - 100 MW EGS Modeled on Fervo Cape Station + * - Case Study: 500 MW EGS Modeled on Fervo Cape Station (September 2026 Update) (`documentation `__) + - `Fervo_Project_Cape-7.txt `__ + - `.out `__ + - `link `__ + * - 100 MW EGS Modeled on Fervo Cape Station (February 2026 Update) - `Fervo_Project_Cape-6.txt `__ - `.out `__ - `link `__ @@ -420,6 +420,10 @@ Example-specific web interface deeplinks are listed in the Link column. - `Fervo_Project_Cape-4.txt `__ - `.out `__ - `link `__ + * - Fervo Cape Station 5: [Deprecated] Case Study: 500 MW EGS Modeled on Fervo Cape Station (February 2026 Update) (`documentation `__) + - `Fervo_Project_Cape-5.txt `__ + - `.out `__ + - `link `__ * -   -   -   diff --git a/docs/Fervo_Project_Cape-4.md b/docs/Fervo_Project_Cape-4.md index 273feb383..35e474b82 100644 --- a/docs/Fervo_Project_Cape-4.md +++ b/docs/Fervo_Project_Cape-4.md @@ -2,7 +2,7 @@ .. raw:: html -

⚠️️ This is a deprecated version of the case study. Click here to view an updated version (Fervo_Project_Cape-5).

+

⚠️️ This is a deprecated version of the case study. Click here to view an updated version (Fervo_Project_Cape-7).

--- diff --git a/docs/Fervo_Project_Cape-5.md.jinja b/docs/Fervo_Project_Cape-5.md similarity index 65% rename from docs/Fervo_Project_Cape-5.md.jinja rename to docs/Fervo_Project_Cape-5.md index 343fa0323..3c0bf9c37 100644 --- a/docs/Fervo_Project_Cape-5.md.jinja +++ b/docs/Fervo_Project_Cape-5.md @@ -16,7 +16,13 @@ } -# GEOPHIRES Case Study: 500 MW EGS Modeled on Fervo Cape Station (2026 Update) +# \[Deprecated\] GEOPHIRES Case Study: 500 MW EGS Modeled on Fervo Cape Station (February 2026 Update) + +.. raw:: html + +

⚠️️ This is a deprecated version of the case study. Click here to view an updated version (Fervo_Project_Cape-7).

+ +--- ## Introduction @@ -25,7 +31,7 @@ a second-of-a-kind (SOAK) analog of Phases I and II of Fervo Energy's [Cape Stat [^author]: Case Study Author: Jonathan Pezzino, Scientific Web Services LLC (SWS Geothermal). GitHub profile: [softwareengineerprogrammer](https://github.com/softwareengineerprogrammer). -Key results include **LCOE =** **{{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }}**, **IRR = {{ irr_pct ~ '%' }}**, and **Total CAPEX =** **{{ '$' ~ capex_usd_per_kw ~ '/kW' }}**. [Click here to go to the Results section](#results). +Key results include **LCOE =** **$85.0/MWh**, **IRR = 22.4%**, and **Total CAPEX =** **$5600/kW**. [Click here to go to the Results section](#results). [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. @@ -94,7 +100,7 @@ from [$9.4M down to $4.8M per well](https://houston.innovationmap.com/fervo-ener 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). +at $4.65M/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 (NOAK) operator. @@ -151,26 +157,96 @@ all available GEOPHIRES input parameters. #### Reservoir Parameters -{{ reservoir_parameters_table_md }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Surface Temperature | 13 ℃ | Surface temperature near Milford, UT (38.4987670, -112.9163432) ([Project InnerSpace, 2025](https://geomap.projectinnerspace.org/test/)). | +| Number of Segments | 3 | .. N/A | +| Gradient 1 | 74 ℃/km | Sedimentary overburden. 200℃ at 8500 ft depth (Fercho et al. 2024); 228.89℃ at 9824 ft (Norbeck et al. 2024). | +| Thickness 1 | 2.5 km | .. N/A | +| Gradient 2 | 41 ℃/km | Crystalline reservoir | +| Thickness 2 | 0.5 km | .. N/A | +| Gradient 3 | 39.1 ℃/km | Sugarloaf appraisal | +| Reservoir Depth | 2.68 km | Extrapolated from surface temperature, gradient, and average production temperature of shallower and deeper producers in Singh et al., 2025. | +| Reservoir Density | 2800 kg/m³ | phyllite + quartzite + diorite + granodiorite ([Norbeck et al., 2023](https://doi.org/10.31223/X52X0B)) | +| Reservoir Heat Capacity | 790 J/kg/K | .. N/A | +| Reservoir Thermal Conductivity | 3.05 W/m/K | .. N/A | +| Reservoir Model | Multiple Parallel Fractures (Gringarten) | See the [reservoir engineering calibration section](#res-eng-params-calibration-section) for additional details. | +| 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 m | 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 | Rectangular | Bench design and fracture geometry in Singh et al., 2025 are given in rectangular dimensions. | +| Fracture Width | 305 m | Matches intra-bench well spacing of 500 ft (corresponding to fracture length of 1000 ft) (Singh. et al., 2025) | +| Fracture Height | 100 m | 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 | 1% | "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%. | #### Well Bores Parameters -{{ well_bores_parameters_table_md }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Number of Production Wells | 56 | Number of production wells required to produce net generation greater than the PPA minimum and total generation less than nameplate capacity (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. | +| Production Flow Rate per Well | 107 kg/sec | 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 case study flow rate is chosen both as a conservative target for long-term sustainability and to achieve a more economically favorable drawdown and redrilling schedule. | +| Production Well Diameter | 8.535 in | 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 in | See Production Well Diameter | +| Production Wellhead Pressure | 303 psi | Modeled at a constant 300 psi in Singh et al., 2025. We use a marginally uprated value to conform to GEOPHIRES's calculated minimum wellhead pressure and nominally align with the gradual increase in WHP for constant flow rates modeled by Singh et al. | +| Injectivity Index | 1.38 kg/sec/bar | Based on ATB Conservative Scenario (NREL, 2025) derated to align with expected parasitic loads of 15–20% and per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024). | +| Productivity Index | 1.13 kg/sec/bar | See Injectivity Index | +| Ramey Production Wellbore Model | True | Ramey's model estimates the geofluid temperature drop in production wells | +| Injection Temperature | 53.6 ℃ | Calibrated with GEOPHIRES model-calculated reinjection temperature (Beckers and McCabe, 2019). Close to upper bound of Project Red injection temperatures (75–125℉; 23.89–51.67℃) (Norbeck and Latimer, 2023). Note: GEOPHIRES enforces a thermodynamic optimum that overrides higher values, such as the 85°C ORC outlet temperature specified in Cape Station's plant design (DeGolyer and MacNaughton, 2024) (intended for silica scaling mitigation), resulting in a "maximum theoretical power" scenario. Support for higher reinjection temperatures may be added in future GEOPHIRES versions. | +| Injection Wellbore Temperature Gain | 3 ℃ | Empirical estimate for high-flow rate wells where rapid fluid velocity minimizes heat uptake during descent (Ramey, 1962). | +| Maximum Drawdown | 0.25% | This value represents the fractional drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is calibrated to maintain the PPA minimum net electricity generation requirement. It is a very small percentage because it is relative to the initial production temperature; the temperature quickly rises higher due to thermal conditioning and plateaus until breakthrough, so any drawdown relative to the initial value signals that the temperature has already declined from its stabilized peak. | #### Surface Plant Parameters -{{ surface_plant_parameters_table_md }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Power Plant Type | Supercritical ORC | Gen 2 ORC units (Turboden, 2025). | +| Plant Lifetime | 30 yr | Sets the project economic horizon, aligned with Fervo's anticipated 30-year well life (Fervo Energy, 2025). Modeling Distinction: While Fervo projects physical wellbore integrity for 30 years, GEOPHIRES simulates "redrilling events" to model thermal management of the reservoir volume. This treats the 30-year lifespan as an aggregate of shorter-lived thermal cycles delineated by discrete redrilling events occurring at intervals dictated by the Maximum Drawdown parameter. The modeled cost of each redrilling event is equivalent to the drilling and stimulation cost of the entire wellfield, serving as a conservative cost proxy for the major interventions (e.g., sidetracking and stimulating laterals into fresh rock, or drilling new wells if necessary) required to sustain the PPA target against thermal depletion. | +| 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 | 91.3% | (DeGolyer and MacNaughton, 2024) | +| Plant Outlet Pressure | 2000 psi | McClure, 2024; Singh et al., 2025. | +| Circulation Pump Efficiency | 80% | .. N/A | +| Project Latitude | 38.506196 | .. N/A | +| Project Longitude | -112.918155 | .. N/A | #### Construction Parameters -{{ construction_parameters_table_md }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Construction Years | 5 yr | 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. | +| Construction CAPEX Schedule | 0.014,0.027,0.139,0.431,0.389 | 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. | #### Economic Parameters -{{ economics_parameters_table_md }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Economic Model | SAM Single Owner PPA | The SAM Single Owner PPA economic model is used to calculate financial results including LCOE, NPV, IRR, and pro-forma cash flow analysis. See [GEOPHIRES documentation of SAM Economic Models](https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html) for details on how System Advisor Model financial models are integrated into GEOPHIRES. | +| Inflation Rate | 2.7% | US inflation as of December 2025. Note: [2024b ATB models lower inflation](https://atb.nrel.gov/electricity/2024b/definitions#inflation). | +| Starting Electricity Sale Price | $95/MWh | 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.57/MWh | Calibrated to reach $100/MWh at project year 11 | +| Fraction of Investment in Bonds | 70% | 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 | 12% | Typical discount rates for higher-risk projects may be 12–15%. | +| Inflated Bond Interest Rate | 7% | 2024b ATB (NREL, 2025) | +| Inflated Bond Interest Rate During Construction | 10.5% | 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 yr | Equity-only for first 2 construction years (ATB) | +| Investment Tax Credit Rate | 30% | Geothermal Drilling and Completions Apprenticeship Program ensures compliance with ITC labor requirements (Southern Utah University, 2024). | +| Combined Income Tax Rate | 25.55% | 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.22% | Utah Inland Port Authority (UIPA) tax differential incentive | +| Capital Cost for Power Plant for Electricity Generation | $1900/kW | [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 | $30M | 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 | vertical large diameter, baseline | 2025 NREL Geothermal Drilling Cost Curve Update (Akindipe and Witter, 2025). | +| Well Drilling and Completion Capital Cost Adjustment Factor | 90% (Yields all-in cost of $4.65M/well) | 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. See [Sensitivity Analysis](#sensitivity-analysis-section) for effect of different drilling costs on results. | +| Reservoir Stimulation Capital Cost per Injection Well | $4M baseline cost; $4.83M all-in cost | 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. See [Sensitivity Analysis](#sensitivity-analysis-section) for effect of different stimulation costs on results. | +| Reservoir Stimulation Capital Cost per Production Well | $4M baseline cost; $4.83M all-in cost | See Reservoir Stimulation Capital Cost per Injection Well | +| Field Gathering System Capital Cost Adjustment Factor | 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 | 1.75% | 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 yr | After the first 10 years of production, the royalty rate escalates to 3.5%. | +| Royalty Rate Escalation | 1.75% | Escalation at Year 11 from 1.75% to 3.5%. | +| Royalty Rate Maximum | 3.5% | No further escalation beyond 3.5%. | +| Water Cost Adjustment Factor | 200% | 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. | @@ -223,16 +299,21 @@ The uprated value is also supported by Figure 2's fracture geometry visualizatio 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 }} +| Parameter | Input Value | Comment | +|-------------------|-------------------------------------------|-------------| +| Number of Production Wells | 4 | .. N/A | +| 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 | 100% | 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 yr | .. N/A | 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: @@ -272,17 +353,17 @@ Note that economic results are derived from the [SAM Single Owner PPA Economic M The case study result's pro-forma cash flow analysis can be viewed in the `Fervo_Project_Cape-5.out` result file in source code and in the web interface under the Cash Flow tab. -{# @formatter:off #} + | Metric | Result Value | Reference Value(s) | Reference Source | |---------------|----------------|--------------------|------------------| -| LCOE | {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }} | \$80/MWh | Horne et al, 2025. | -| After-tax IRR
(at Year {{ operations_year_of_irr }} of Operations) | {{ irr_pct ~ '%' }} | 15–25% | Typical levered returns for energy projects | -| NPV | {{ '$' ~ npv_musd ~ 'M' }} | >$0 | Positive NPVs result in profit | -| Levered Equity
Profitability Index
| {{ project_vir }} | >1.0 | Calculations greater than 1.0 indicate the future anticipated discounted cash inflows are greater than the anticipated discounted cash outflows. | -| Project ROI | {{ project_moic }} | | .. N/A | +| LCOE | $85.0/MWh | \$80/MWh | Horne et al, 2025. | +| After-tax IRR
(at Year 30 of Operations) | 22.4% | 15–25% | Typical levered returns for energy projects | +| NPV | $199.0M | >$0 | Positive NPVs result in profit | +| Levered Equity
Profitability Index
| 1.33 | >1.0 | Calculations greater than 1.0 indicate the future anticipated discounted cash inflows are greater than the anticipated discounted cash outflows. | +| Project ROI | 4.31 | | .. N/A | | Cash Flow | [source code](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-5.out#L225);
[web interface](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-5)[^cash-flow-tab] | | .. N/A | -{# Note that the '.. N/A' entry in the last row is required for the table to render in HTML (presumable m2r2/sphinx build issue) #} -{# @formatter:on #} + + [^cash-flow-tab]: Click the Cash Flow tab to view. @@ -291,46 +372,55 @@ See [GEOPHIRES Economic Outputs documentation](parameters.html#economic-paramete ### Capital Costs (CAPEX) -{# @formatter:off #} + | Metric | Result Value | Reference Value(s) | Reference Source | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------|--------------------------------------------------|------------------| -| WACC | {{ wacc_pct ~ '%' }} | 8.3% | Fervo's target goal is to eventually achieve a "Solar Standard" WACC of 8.3% (Matson, 2024). | -| Exploration Costs | {{ '$' ~ exploration_cost_musd ~ 'M' }} | {{ '$' ~ drilling_costs_per_well_musd*5 ~ 'M' }} | 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025). Case study result conservatively includes additional costs for geophysical survey, indirect costs, and contingency. | -| Well Drilling and Completion Costs | {{ '$' ~ drilling_costs_musd ~ 'M' }} total
({{ '$' ~ drilling_costs_per_well_musd ~ 'M/well' }}) | <$4M/well | Latimer, 2025. | -| Stimulation Costs | {{ '$' ~ stim_costs_musd ~ 'M' }} total
({{ '$' ~ stim_costs_per_well_musd ~ 'M/well' }}) | $4.65M/well | Based on 46%:54% drilling:stimulation cost ratio (Yusifov & Enriquez, 2025). | -| Surface Power Plant Costs | {{ '$' ~ surface_power_plant_costs_gusd ~ 'B' }} | | | -| Field Gathering System Costs | {{ '$' ~ field_gathering_cost_musd ~ 'M' }}
({{ field_gathering_cost_pct_occ ~ '%' }} of OCC) | 2% of OCC | Matson, 2024. | -| Overnight Capital Cost | {{ '$' ~ occ_gusd ~ 'B' }} | | | -| Total CAPEX | {{ '$' ~ total_capex_gusd ~ 'B' }}
(OCC + interest and inflation during construction) | | | -| Total CAPEX: $/kW | {{ '$' ~ capex_usd_per_kw ~ '/kW' }}
(based on maximum net electricity generation) | $5000/kW; $4500/kW; $3000–$6000/kW | McClure, 2024; Horne et al, 2025; Latimer, 2025. | -{# @formatter:on #} +| WACC | 8.31% | 8.3% | Fervo's target goal is to eventually achieve a "Solar Standard" WACC of 8.3% (Matson, 2024). | +| Exploration Costs | $30M | $23.25M | 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 | $437M total
($4.65M/well) | <$4M/well | Latimer, 2025. | +| Stimulation Costs | $454M total
($4.83M/well) | $4.65M/well | Based on 46%:54% drilling:stimulation cost ratio (Yusifov & Enriquez, 2025). | +| Surface Power Plant Costs | $1.47B | | | +| Field Gathering System Costs | $48M
(2% of OCC) | 2% of OCC | Matson, 2024. | +| Overnight Capital Cost | $2.44B | | | +| Total CAPEX | $2.87B
(OCC + interest and inflation during construction) | | | +| Total CAPEX: $/kW | $5600/kW
(based on maximum net electricity generation) | $5000/kW; $4500/kW; $3000–$6000/kW | McClure, 2024; Horne et al, 2025; Latimer, 2025. | + ### Operating Costs (OPEX) -{{ opex_result_outputs_table_md }} +| Metric | Result Value | Reference Value(s) | Reference Source | +|-----|-----|-----|-----| +| Wellfield maintenance costs | $5.81M/yr | .. N/A | The built-in correlation for the wellfield OPEX is similar as the surface plant OPEX: it assumes that it consists of 1% of the total wellfield plus field gathering system costs (for annual non-labor costs) and 25% of the labor costs (the other 75% of the labor costs are assigned to the surface plant OPEX). | +| Power plant maintenance costs | $24.87M/yr | .. N/A | GEOPHIRES estimates the annual surface plant OPEX as the sum of 1.5% of the total plant capital cost (for annual non-labor costs), and 75% of the annual labor costs. The other 25% of the labor costs are assigned to the wellfield OPEX. The labor costs are calculated internally in GEOPHIRES using the 2014 labor costs provided by Beckers (2016), indexed to 2017 using the Bureau of Labor Statistics (BLS) Employment Cost Index for utilities (2018). The original 2014 labor cost correlation expresses the labor costs as a function of the plant size (MW) using an approximate logarithmic curve fit to the built-in labor cost data in GETEM. | +| Water costs | $3.19M/yr | .. N/A | Default correlation: Assumes $3.50/1,000 gallons of water. The default correlation is adjusted by the Water Cost Adjustment Factor parameter value of 200%. | +| Average Annual Royalty Cost | $12.36M/yr | .. N/A | The developer's average annual royalty expense over the project's operational lifetime. This value combines both production-based royalties (percentage of gross revenue) and any scheduled supplemental royalty payments. | +| Redrilling costs | $89.1M/yr | .. N/A | Total redrilling costs over the Plant Lifetime are calculated as (Drilling and completion costs + Stimulation costs) × Number of times redrilling. The total is then divided over Plant Lifetime years to calculate Redrilling costs per year. | +| Total operating and maintenance costs | $135.33M/yr | .. N/A | .. N/A | +| Total operating and maintenance costs: $/kW-yr | $264.21/kW-yr | $226.31/kW-yr | 2024b ATB: 2028 Deep EGS Binary Conservative Scenario (NREL, 2025). | + ### Technical & Engineering Results -{# @formatter:off #} + | Metric | Result Value | Reference Value(s) | Reference Source | |--------------------------------------|-----------------------------------------------------------|--------------------|------------------| -| Minimum Net Electricity Generation | {{ min_net_generation_mwe }} MW | 500 MW | The announced 500 MWe capacity (Fervo Energy, 2025) is interpreted to mean that the PPA penalizes Cape Station if net electricity generation falls below 500 MWe. | -| Average Net Electricity Generation | {{ avg_net_generation_mwe }} MW | | | -| Maximum Net Electricity Generation | {{ max_net_generation_mwe}} MW | | | -| Maximum Total Electricity Generation | {{ max_total_generation_mwe }} MW | Upper bound: 600 MW | Combined nameplate capacity of 10×60 MWe Gen 2 ORCs. A total of 8×60 MWe Gen 2 ORCs have been announced for Phase II; 3 from Turboden and 5 from Baker Hughes (Turboden, 2025; Jacobs, 2025). This equates to 480 MW gross capacity for Phase II's 400 MW net capacity. An equivalent SOAK 500 MW project would therefore require 10 Gen 2 ORC units. (Note that the modular Gen 2 ORCs are not individually modeled in this case study, and are assumed to be combined into a single power plant.) | -| 2-year Average Net Power Production per Production Well | {{ two_year_avg_net_power_mwe_per_production_well }} MW | 7.6–11.5 MW | Figures 4 and 12 (Singh et al., 2025). | -| Heat to Power Conversion Efficiency | {{ heat_to_power_conversion_efficiency_pct ~ '%' }} | 19.5% (Likely omits internal plant parasitic loads, such as condenser fans, which GEOPHIRES explicitly accounts for.) | DeGolyer and MacNaughton, 2024. | -| Injection Pumping Parasitic Load
({Average Pumping Power}/{Average Total Electricity Generation}) | {{ parasitic_loss_pct ~ '%' }} | Upper bound: 16.7% | The Phase II procurement strategy (480 MW gross / 400 MW net) implies a design ceiling of 16.7% for total on-site consumption (including injection pumping). Current SOAK targets for parasitic consumption range from 15–20%, reflecting a planned reduction from the ~25–30% observed in Phase I operations (Norbeck, 2026). | -| Total fracture surface area per well | {{ total_fracture_surface_area_per_well_mm2 }}×10⁶ m²
({{ total_fracture_surface_area_per_well_mft2 }} million ft²) | Project Red: 2.787×10⁶ m²
(30 million ft²) | Greater fracture surface area expected than Project Red (Fercho et al., 2025). | -| Reservoir Volume | {{ reservoir_volume_m3 }} m³ | | Calculated from fracture area × fracture separation × number of fractures per well × number of wells | -| Bottom-hole Temperature
(BHT) | {{ bht_temp_degc ~ '℃' }} | 200–241℃ | Fercho et al., 2024; Singh et al., 2025. | -| Initial Production Temperature | {{ initial_production_temperature_degc ~ '℃' }} | 196–208℃ | Approximate range of initial production temperatures between shallower and deeper producers (Singh et al., 2025). | -| Average Production Temperature | {{ average_production_temperature_degc ~ '℃' }} | 199–209℃ | Approximate range of thermally conditioned production temperatures between shallower and deeper producers (Singh et al., 2025). | -| Number of times redrilling | {{ number_of_times_redrilling }} | 2–5 | Redrilling expected to be required within 5–10 years of project start | -| Total wells drilled over project lifetime | {{ total_wells_including_redrilling }} | Permitted Limit: 320 | The BLM Environmental Assessment (DOI-BLM-UT-C010-2024-0018-EA) authorizes an estimated development of 320 production and injection wells (BLM, 2024). As modeled, the project remains within this regulatory envelope for the first three drilling campaigns (Initial, Year 8, and Year 16), reaching a cumulative total of approximately 282 wells.

The model exceeds the current authorization only during the final redrilling event in Year 24. It is a standard industry assumption that brownfield capacity maintenance activities (e.g. sidetracking existing wells on existing pads) occurring two decades into operations would be authorized through subsequent regulatory actions, such as a Determination of NEPA Adequacy (DNA) or a Categorical Exclusion, given the established baseline of environmental impact. | -{# @formatter:on #} +| Minimum Net Electricity Generation | 499 MW | 500 MW | The announced 500 MWe capacity (Fervo Energy, 2025) is interpreted to mean that the PPA penalizes Cape Station if net electricity generation falls below 500 MWe. | +| Average Net Electricity Generation | 510 MW | | | +| Maximum Net Electricity Generation | 512 MW | | | +| Maximum Total Electricity Generation | 600 MW | Upper bound: 600 MW | Combined nameplate capacity of 10×60 MWe Gen 2 ORCs. A total of 8×60 MWe Gen 2 ORCs have been announced for Phase II; 3 from Turboden and 5 from Baker Hughes (Turboden, 2025; Jacobs, 2025). This equates to 480 MW gross capacity for Phase II's 400 MW net capacity. An equivalent SOAK 500 MW project would therefore require 10 Gen 2 ORC units. (Note that the modular Gen 2 ORCs are not individually modeled in this case study, and are assumed to be combined into a single power plant.) | +| 2-year Average Net Power Production per Production Well | 9.0 MW | 7.6–11.5 MW | Figures 4 and 12 (Singh et al., 2025). | +| Heat to Power Conversion Efficiency | 13.9% | 19.5% (Likely omits internal plant parasitic loads, such as condenser fans, which GEOPHIRES explicitly accounts for.) | DeGolyer and MacNaughton, 2024. | +| Injection Pumping Parasitic Load
({Average Pumping Power}/{Average Total Electricity Generation}) | 14.7% | Upper bound: 16.7% | The Phase II procurement strategy (480 MW gross / 400 MW net) implies a design ceiling of 16.7% for total on-site consumption (including injection pumping). Current SOAK targets for parasitic consumption range from 15–20%, reflecting a planned reduction from the ~25–30% observed in Phase I operations (Norbeck, 2026). | +| Total fracture surface area per well | 4.6×10⁶ m²
(49 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 | 4,225,156,597 m³ | | Calculated from fracture area × fracture separation × number of fractures per well × number of wells | +| Bottom-hole Temperature
(BHT) | 205.38℃ | 200–241℃ | Fercho et al., 2024; Singh et al., 2025. | +| Initial Production Temperature | 202℃ | 196–208℃ | Approximate range of initial production temperatures between shallower and deeper producers (Singh et al., 2025). | +| Average Production Temperature | 203℃ | 199–209℃ | Approximate range of thermally conditioned production temperatures between shallower and deeper producers (Singh et al., 2025). | +| Number of times redrilling | 3 | 2–5 | Redrilling expected to be required within 5–10 years of project start | +| Total wells drilled over project lifetime | 376 | Permitted Limit: 320 | The BLM Environmental Assessment (DOI-BLM-UT-C010-2024-0018-EA) authorizes an estimated development of 320 production and injection wells (BLM, 2024). As modeled, the project remains within this regulatory envelope for the first three drilling campaigns (Initial, Year 8, and Year 16), reaching a cumulative total of approximately 282 wells.

The model exceeds the current authorization only during the final redrilling event in Year 24. It is a standard industry assumption that brownfield capacity maintenance activities (e.g. sidetracking existing wells on existing pads) occurring two decades into operations would be authorized through subsequent regulatory actions, such as a Determination of NEPA Adequacy (DNA) or a Categorical Exclusion, given the established baseline of environmental impact. | + @@ -475,8 +565,7 @@ to view a scenario with reduced redrilling in the web interface, representing an Redrilling is reduced via greater effective fracture surface area per stimulated well, achieved by an increased number of fractures per well and greater fracture height. Stimulation cost is increased to nominally reflect the larger required job size. -{# TODO port scenario to source code instead of out-of-band shared result - (probably would want to generate similar to Fervo_Project_Cape-6 #} + ### ResFrac Profile + Hotter/Deeper Scenario @@ -485,7 +574,7 @@ to view the `Fervo_Project_Cape-6 Variant: ResFrac Profile + Hotter/Deeper Scena Note that this scenario models two redrilling events (rather than three) and yields 100 MWe average net generation (rather than 100 MWe minimum net generation), reflected in the power generation profile below. -{# TODO port scenario to source code instead of out-of-band shared result #} + ![](_images/fervo_project_cape-6_variant-resfrac-deeper-hotter_power-generation-profile.png) @@ -495,7 +584,7 @@ See the [Simulation Comparison section](#simulation-comparison) for details. Documentation is available for the following previous case study versions, which are deprecated in favor of this version. -{# @formatter:off #} + #### `Fervo_Project_Cape-4` [Version documentation](Fervo_Project_Cape-4.html) @@ -517,9 +606,9 @@ Key differences: 1. Refined discount and interest rates 1. Refined tax rates including addition of property tax 1. Fervo_Project_Cape-5 includes more comprehensive sensitivity analysis -{# @formatter:off #} -{# TODO others e.g. Fervo_Project_Cape-3... #} + + diff --git a/docs/Fervo_Project_Cape-7.md.jinja b/docs/Fervo_Project_Cape-7.md.jinja new file mode 100644 index 000000000..129575ca2 --- /dev/null +++ b/docs/Fervo_Project_Cape-7.md.jinja @@ -0,0 +1,819 @@ +.. raw:: html + + + +# GEOPHIRES Case Study: 500 MW EGS Modeled on Fervo Cape Station (September 2026 Update) + +## Introduction + +The GEOPHIRES example `Fervo_Project_Cape-7` is a case study[^author] of a 500 MWe EGS project modeled on +a second-of-a-kind (SOAK) analog of Phases I and II of Fervo Energy's [Cape Station project](https://capestation.com/). + +[^author]: Case Study Author: Jonathan Pezzino, Scientific Web Services LLC (SWS Geothermal). GitHub profile: [softwareengineerprogrammer](https://github.com/softwareengineerprogrammer). + +Key results include **LCOE =** **{{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }}**, **IRR = {{ irr_pct ~ '%' }}**, and **Total CAPEX =** **{{ '$' ~ capex_usd_per_kw ~ '/kW' }}**. [Click here to go to the Results section](#results). + +The September 2026 Update aligns the case study's subsurface design with the Fervo 3.0 well design disclosed for +Cape Station Phase II, models lateral drilling, grid interconnection, and transmission service costs explicitly, and +updates PPA pricing to Fervo's reported range for contracts under negotiation (Fervo Energy, 2026f). +See the [Previous Versions section](#previous-versions-section) for a summary of changes from the February 2026 Update. + +[Click here](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-7) to +interactively explore the case study example in the GEOPHIRES web interface. + +.. raw:: html + + +
+ + Power Production Profile Graph + + + + LCOE Sensitivity Analysis Results Chart + +
+ +### 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 analogous 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, which reached first power in September 2026 (Fervo Energy, 2026e), but operating without access to Fervo’s +private supply chain or proprietary optimization data. + +**Public Data Reliance:** Inputs utilize publicly available data for parameter values where applicable, such as geothermal +gradient and reservoir density. +Where data is proprietary or otherwise unknown, 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, parasitic load, and water loss. + +**Commercial Viability Threshold:** Implicit in this design is a screening for investment-grade returns. +Where public data provided a range of potential values rather than a precise figure, inputs were selected from within +those bounds; the case study does not tune inputs to a target return. +The PPA price is a market input: the starting price of {{ '$' ~ starting_ppa_price_usd_per_mwh ~ '/MWh' }} is the +midpoint of the $100–130/MWh range Fervo reports for contracts under negotiation (Fervo Energy, 2026f), which yields a +levelized PPA price of {{ '$' ~ lppa_usd_per_mwh ~ '/MWh' }} (nominal) with the modeled escalation. +The resulting after-tax IRR of {{ irr_pct ~ '%' }} is read as evidence that a Second-of-a-Kind deployment can clear the +premium over remaining technology risk that a developer would require. +The finding is contingent on current clean firm power pricing rather than on cost assumptions alone: with the +February 2026 Update's PPA terms ($95/MWh starting price, $0.57/MWh annual escalation, no cap) and all other inputs +unchanged, the after-tax IRR is {{ previous_version_ppa_terms_irr_pct ~ '%' }} +and the NPV is {{ previous_version_ppa_terms_npv_display }}. +An illustrative example of selection within a range is Fracture Height, which was set to yield an effective fracture +surface area consistent with the ResFrac simulation results in Singh et al. (2025). + +**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. +Fervo’s drilling costs at Cape Station fell +from [$9.4M to $4.8M per well](https://houston.innovationmap.com/fervo-energy-drilling-utah-project-2667300142.html) +over its Phase I learning curve, for wells with 5,000 ft laterals at roughly 400℉. +Against the 2025 NREL drilling cost curve with the lateral costed explicitly (Akindipe and Witter, 2025), those two +points correspond to drilling cost adjustment factors of 1.14 and 0.58. +This model uses a factor of 0.72, the geometric mean of the ATB-aligned baseline (0.9) and Fervo’s best demonstrated +well (0.58), applied to the larger 3.0 design well (3.06 km, 7,500 ft cased lateral, 430℉) that a developer building +today would drill. +The resulting {{ '$' ~ drilling_costs_per_well_musd ~ 'M/well' }} is therefore not comparable to Fervo's Phase I +figures on a per-well basis; on a like-for-like basis, it sits between Fervo's demonstrated performance and the +unadjusted industry baseline. +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 (NOAK) operator. +Note that the February 2026 Update, at $4.65M per well, corresponded to an effective factor of about +0.58 once the lateral is accounted for, i.e. Fervo's best demonstrated well rather than a SOAK assumption. + +### 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-7.txt](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-7.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. + +### Disclaimer: Independent Analysis + +This case study is an independent techno-economic analysis developed by the author and contributors to the GEOPHIRES +open-source project. It is not affiliated with, sponsored by, or endorsed by Fervo Energy. +The author and contributors are not employees or agents of Fervo Energy, and this work has not been reviewed or +approved by the company. All modeling assumptions, including those derived from public data sources, represent the +independent interpretation of the author and the GEOPHIRES open-source community and do not constitute proprietary +information or official company projections. + +Furthermore, as noted in the [Discussion section](#discussion-section), this analysis provides a validated economic baseline rather than a +fully burdened commercial pro-forma. Actual financial results may vary based on project-specific overhead, financing fees, +and operational complexities not explicitly itemized in this study. + +## 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-7). + +See the [Calibration with Fervo-implemented Field Design section](#res-eng-params-calibration-section) +for a detailed explanation of how key case study reservoir engineering input parameters were derived. + +### Inputs + +See [Fervo_Project_Cape-7.txt](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-7.txt) +in source code for the full set of inputs. +Refer to the [GEOPHIRES Parameters reference](parameters.html#input-parameters) for descriptions and specifications of +all available GEOPHIRES input parameters. + + + +#### 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. + +#### Bench Design and Well Spacing + +Figure 7 in Singh et al., 2025 (below) shows the well and bench spacing for the Cape Station field design.[^fig-7-note] +Each bench consists of 5 wells, with 2 injectors and 3 producers. +This ratio is encoded by the case study's `Number of Injection Wells per Production Well` parameter (see [Well Bores Parameters](#well-bores-parameters-section)). + +The Singh et al. paper models separate upper and lower benches. +GEOPHIRES does not support wells with different depths, so the case study uses a single `Reservoir Depth`. +The February 2026 Update set it to 2.68 km, an approximate average depth between the upper and lower benches of the Phase I +design. +The current version sets it to 3.06 km, the depth at which the segmented gradient reaches the 430℉ (221℃) average +reservoir temperature of the Fervo 3.0 well design used for Phase II (Fervo Energy, 2026f; 2026g). +The [comparison with Singh et al. below](#simulation-comparison) is run at the previous depth so that it remains a +like-for-like comparison with the Phase I design simulated by Singh et al. +See the [Technical & Engineering Results section](#technical-and-engineering-results-section) for relevant temperature results. + +![Singh et al. (2025), Figure 7](_images/singh-et-al-2025_fig-7_well-and-bench-spacing.png) + +[^fig-7-note]: The original figure has been cropped and modified to display: "500 ft horizontal" instead of "700 ft horizontal" and "Inter-bench spacing: Vertical spacing between injectors: 700 ft"; per the Conclusions section of the paper: "Based on the results from the study and additional risk analysis, the 500 ft x 200 ft well spacing with a bench spacing of 700 ft was implemented in the field development." (p. 19). + +#### Fracture Geometry + +Figure 2 in Singh et al., 2025 (below) shows the ResFrac simulated fracture geometry. This figure, along with the bench design in Figure 7, +informed the case study's `Fracture Shape`, `Fracture Width`, `Fracture Height`, and `Number of Fractures per Stimulated Well` parameters (see [Reservoir Parameters](#reservoir-parameters-section)). + +Note that the case study does not attempt to strictly replicate the paper's fracture geometry; +rather, the paper specifications were used to derive approximations and/or constraints on parameters whose final values +were ultimately refined based on holistic techno-economic considerations. For example, the intra-bench horizontal well spacing +of 500 ft dictates that fracture half-lengths must be at least 500 ft in order to achieve flow between injectors and producers. +Thus, a minimum fracture length of 1000 ft (305 m) is required (assuming that fractures propagate approximately symmetrically in the horizontal axis), +reflected in the case study's `Fracture Width` parameter value. + +Similarly, the intra-bench vertical spacing of 200 ft (61 m) dictates that the fracture height must be at least 200 ft[^intra-bench-vertical-spacing-note]. +The case study's `Fracture Height` value was formulated by starting with 61 m and then iteratively uprating until a total effective fracture surface area per well was achieved +that produces results consistent with the paper simulation and other reference values. +The uprated value is also supported by Figure 2's fracture geometry visualization indicating fracture height exceeding intra-bench vertical spacing. + +[^intra-bench-vertical-spacing-note]: Note: unlike horizontal propagation, vertical fracture propagation is assumed to be asymmetrical in the upwards direction per "fracture propagation model with uniform frac gradient and no layering will tend to predict fracture growth directly upwards." (p. 3) and fracture geometry shown in Figures 1 and 2. + + +![Singh et al. (2025), Figure 2](_images/singh-et-al-2025_fig-2_fracture-geometry.png) + + +#### Simulation Comparison + +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 overrides for depth, lateral length, and fracture count return the scenario to the Phase I design simulated by +Singh et al.; the base case's 3.0 design (deeper, with longer laterals and 50% more fracture surface area per well) +has no published numerical simulation counterpart to compare against. + +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 supports the physical plausibility of the case study's reservoir engineering parameters, +as the Year 1 and Year 15 thermal endpoints align closely with the numerical simulation baseline. + +However, the analytical Gringarten model's thermal plateau yields a higher aggregate heat extraction than the numerical model's +gradual decline, representing an optimistic upper bound on performance compared to the conservative heterogeneity +modeled in ResFrac (see Reservoir Modeling Fidelity in the [Discussion section](#discussion-section)). + +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-section) below. + + +## Results + +See [Fervo_Project_Cape-7.out](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-7.out) +in source code for the complete results. +Refer to the [GEOPHIRES Parameters reference](parameters.html#outputs) for descriptions and specifications of +all available GEOPHIRES outputs. + +### 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 pro-forma cash flow analysis can be viewed in the `Fervo_Project_Cape-7.out` result file in source code +and in the web interface under the Cash Flow tab. + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|---------------|----------------|--------------------|------------------| +| LCOE | {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }} | \$80/MWh | Horne et al, 2025. | +| Levelized PPA Price | {{ '$' ~ lppa_usd_per_mwh ~ '/MWh' }} | \$100–130/MWh | Price range Fervo reports for contracts under negotiation (Fervo Energy, 2026f). See the [PPA price discussion in Sensitivity Analysis](#impact-of-ppa-price-on-lcoe) for the relationship between PPA price and LCOE. | +| After-tax IRR
(at Year {{ operations_year_of_irr }} of Operations) | {{ irr_pct ~ '%' }} | 15–25% | Typical levered returns for energy projects | +| NPV | {{ '$' ~ npv_musd ~ 'M' }} | >$0 | Positive NPVs result in profit. Cash flows are discounted at the {{ real_discount_rate_pct ~ '%' }} real discount rate ({{ nominal_discount_rate_pct ~ '%' }} nominal); see Discount Rate in Economic Parameters. | +| Levered Equity
Profitability Index
| {{ project_vir }} | >1.0 | Calculations greater than 1.0 indicate the future anticipated discounted cash inflows are greater than the anticipated discounted cash outflows. | +| Project ROI | {{ project_moic }} | | .. N/A | +| Cash Flow | [source code](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-7.out#L232);
[web interface](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-7)[^cash-flow-tab] | | .. N/A | +{# Note that the '.. N/A' entry in the last row is required for the table to render in HTML (presumable m2r2/sphinx build issue) #} +{# @formatter:on #} + +[^cash-flow-tab]: Click the Cash Flow tab to view. + +Hover over the metric names to view the corresponding definitions. +See [GEOPHIRES Economic Outputs documentation](parameters.html#economic-parameters) for more information. + +### Capital Costs (CAPEX) + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------|--------------------------------------------------|------------------| +| WACC | {{ wacc_pct ~ '%' }} | 8.3% | Fervo's target goal is to eventually achieve a "Solar Standard" WACC of 8.3% (Matson, 2024). | +| Exploration Costs | {{ '$' ~ exploration_cost_musd ~ 'M' }} | {{ '$' ~ exploration_atb_reference_musd ~ 'M' }} | 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025) at the case study's all-in well cost. 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' }}) | $4.8M–$9.4M/well;
{{ '$' ~ drilling_costs_per_well_at_unit_adjustment_factor_musd ~ 'M/well' }};
<$4M/well | Fervo Phase I wells with 5,000 ft laterals, best to first ([InnovationMap](https://houston.innovationmap.com/fervo-energy-drilling-utah-project-2667300142.html)); 2025 NREL drilling cost curve baseline (adjustment factor of 1.0) for the case study's 3.0 design well geometry (Akindipe and Witter, 2025); NOAK target (Latimer, 2025). | +| Stimulation Costs | {{ '$' ~ stim_costs_musd ~ 'M' }} total
({{ '$' ~ stim_costs_per_well_musd ~ 'M/well' }}) | {{ '$' ~ stim_costs_per_well_drilling_ratio_reference_musd ~ 'M/well' }} | Based on 46%:54% drilling:stimulation cost ratio (Yusifov & Enriquez, 2025) applied to the case study's drilling and completion cost per well. | +| Surface Power Plant Costs | {{ '$' ~ surface_power_plant_costs_gusd ~ 'B' }} | ~50% of pre-COD CAPEX | Fervo reports surface power generation equipment as about half of pre-COD CAPEX, with drilling, completion and well pad facilities the other half (Fervo Energy, 2026f). The case study's surface power plant cost is {{ surface_power_plant_pct_of_wellfield_and_plant_capex ~ '%' }} of drilling, completion, stimulation, field gathering, and surface power plant costs combined (excluding exploration and interconnection). | +| Field Gathering System Costs | {{ '$' ~ field_gathering_cost_musd ~ 'M' }}
({{ field_gathering_cost_pct_occ ~ '%' }} of OCC) | 2% of OCC | Matson, 2024. | +| Grid Interconnection Costs | {{ '$' ~ interconnection_cost_musd ~ 'M' }}
({{ '$' ~ interconnection_cost_usd_per_kw_ppa_capacity ~ '/kW' }}) | $417–$500/kW;
$426/kW | PacifiCorp cluster-study estimates for geothermal interconnection requests in Beaver and Millard Counties, Utah; average for 250–750 MW requests across PacifiCorp, BPA, and Duke (Seel et al., 2026). Entered as `One-time Flat License Fees Etc`; see Economic Parameters. | +| 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; {{ '$' ~ capex_usd_per_kw_excluding_interconnection ~ '/kW' }} excluding interconnection; {{ '$' ~ occ_usd_per_kw ~ '/kW' }} overnight capital cost, which excludes inflation and interest during construction) | $7000/kW; $5500/kW; $5000/kW; $4500/kW; $3000–$6000/kW | Cape Station Phase I FOAK overnight capital cost (Fervo Energy, 2026d); Phase II all-in cost target (Fervo Energy, 2026a); McClure, 2024; Horne et al, 2025; Latimer, 2025. Fervo has not disclosed whether its figures include interconnection. | +{# @formatter:on #} + +### Operating Costs (OPEX) + +{{ opex_result_outputs_table_md }} + + + +### Technical & Engineering Results + +{# @formatter:off #} +| Metric | Result Value | Reference Value(s) | Reference Source | +|--------------------------------------|-----------------------------------------------------------|--------------------|------------------| +| Minimum Net Electricity Generation | {{ min_net_generation_mwe }} MW | 500 MW | The announced 500 MWe capacity (Fervo Energy, 2025b) is interpreted to mean that the PPA penalizes Cape Station if net electricity generation falls below 500 MWe. | +| Average Net Electricity Generation | {{ avg_net_generation_mwe }} MW | | | +| Maximum Net Electricity Generation | {{ max_net_generation_mwe}} MW | | | +| Maximum Total Electricity Generation | {{ max_total_generation_mwe }} MW | Upper bound: {{ nameplate_capacity_mw }} MW | Combined nameplate capacity of {{ orc_unit_count }}×{{ orc_unit_gross_capacity_mw }} MWe Gen 2 ORCs. Phase II comprises eight 50 MWe (net) GeoBlocks (Fervo Energy, 2026a) served by 8×60 MWe Gen 2 ORCs, 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. At the parasitic load modeled in this case study ({{ parasitic_loss_pct ~ '%' }} of total generation; see Pumping Parasitic Load below), 500 MWe net requires more than {{ (orc_unit_count - 1) * orc_unit_gross_capacity_mw }} MW gross, so an equivalent SOAK 500 MW project would require {{ orc_unit_count }} Gen 2 ORC units. The February 2026 Update assumed 10 units at a parasitic load of about 15%. (Note that the modular Gen 2 ORCs are not individually modeled in this case study, and are assumed to be combined into a single power plant.) | +| 2-year Average Net Power Production per Production Well | {{ two_year_avg_net_power_mwe_per_production_well }} MW | 7.6–11.5 MW;
15 MW (gross) | Figures 4 and 12 (Singh et al., 2025); gross power per production well for the Fervo 3.0 well design (Fervo Energy, 2026g). The case study value is net of pumping parasitic load, and the Fervo figure appears to require flow rates above the {{ production_flow_rate_kg_per_s_display }} kg/s per well modeled in this case study. | +| Heat to Power Conversion Efficiency | {{ heat_to_power_conversion_efficiency_pct ~ '%' }} | 19.5% (Likely omits internal plant parasitic loads, such as condenser fans, which GEOPHIRES explicitly accounts for.) | DeGolyer and MacNaughton, 2024. | +| Pumping Parasitic Load
({Average Pumping Power}/{Average Total Electricity Generation}) | {{ parasitic_loss_pct ~ '%' }}
({{ initial_pumping_power_pct_of_net ~ '%' }} of initial net generation) | 16.7%;
15–20%;
25–35% | The Phase II procurement strategy (480 MW gross / 400 MW net) implies a design ceiling of 16.7% for total on-site consumption (including injection pumping). Current SOAK targets for parasitic consumption range from 15–20%, reflecting a planned reduction from the ~25–35% observed in Phase I operations (Norbeck, 2026). The case study models parasitic load above the design ceiling because observed Phase I performance is well above it: the Productivity Index and Injectivity Index inputs are derated to place pumping power (production and injection) at 20% or more of net generation (see Reservoir Parameters). | +| 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℃;
221℃ (430℉) | Fercho et al., 2024; Singh et al., 2025. Average reservoir temperature design point of the Fervo 3.0 well design used for Phase II (Fervo Energy, 2026g); Fervo also reports a 3.0 well reaching a 460℉ (238℃) resource (Fervo Energy, 2026a). | +| Initial Production Temperature | {{ initial_production_temperature_degc ~ '℃' }} | 196–208℃ | Approximate range of initial production temperatures between shallower and deeper producers (Singh et al., 2025). This range applies to the Phase I design; the case study base case is deeper (see Bench Design and Well Spacing). | +| 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). This range applies to the Phase I design; the case study base case is deeper (see Bench Design and Well Spacing). | +| Number of times redrilling | {{ number_of_times_redrilling }} | 2–5 | Redrilling expected to be required within 5–10 years of project start. The case study's redrilling events occur in years {{ redrilling_years_display }} of operations (see [Production Temperature Profile section](#production-temperature-profile-section)). | +| Total wells drilled over project lifetime | {{ total_wells_including_redrilling }} | Permitted Limit: 320 | The BLM Environmental Assessment (DOI-BLM-UT-C010-2024-0018-EA) authorizes an estimated development of 320 production and injection wells (BLM, 2024). {% if total_wells_including_redrilling <= 320 %}As modeled, the {{ number_of_wells }} initial wells plus {{ number_of_times_redrilling }} redrilling campaigns of {{ number_of_wells }} wells each (years {{ redrilling_years_display }}) total {{ total_wells_including_redrilling }} wells, within this regulatory envelope.{% else %}As modeled, the {{ number_of_wells }} initial wells plus {{ number_of_times_redrilling }} redrilling campaigns of {{ number_of_wells }} wells each (years {{ redrilling_years_display }}) total {{ total_wells_including_redrilling }} wells, exceeding the current authorization.{% endif %} | +{# @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–{{ first_cycle_peak_year }})**: The initial rise in production temperature, from {{ initial_production_temperature_degc_precise }}°C to a peak of approximately {{ first_cycle_peak_temperature_degc }}°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 {{ first_cycle_peak_year + 1 }}–{{ redrilling_years[0] }})**: Following the conditioning peak, temperature declines as the cold front from injection wells reaches the production zone (thermal breakthrough), reducing the produced fluid enthalpy. The larger fracture surface area per well of the 3.0 design delays breakthrough relative to the February 2026 Update, in which drawdown began in year 6. +3. **Redrilling (Years {{ redrilling_years_display }})**: The model triggers a redrilling event when the next time step's temperature would fall below the threshold defined by the `Maximum Drawdown` parameter (shown as the dashed orange line). The production temperature does not necessarily actually reach the threshold; redrilling typically preemptively restores the wellfield before that occurs. The cost of these events is amortized as an operational expense over the project lifetime. + +#### Power Generation Profile + + + +Power generation is a direct function of production temperature, so the power production profile mirrors the thermal behavior described above. The graph shows both total (gross) electricity generation and net electricity generation after parasitic losses. The gap between the two curves represents the energy consumed by the circulation pumps. + +The horizontal reference lines indicate the 500 MW net PPA minimum production requirement and the {{ nameplate_capacity_mw }} MW nameplate capacity (combined capacity of the individual ORC units, {{ orc_unit_count }}×{{ orc_unit_gross_capacity_mw }} MWe). + + + +## 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. + +The parameter values tested below represent discrete sensitivity ranges intended to illustrate the magnitude of economic impact. +They do not imply a specific statistical probability of occurrence. +Future versions of this case study may incorporate formal uncertainty quantification to map the likelihood of these outcomes. + +Some rows vary more than one input so that the row isolates the intended effect. +The PPA Starting Price and PPA Escalation rows move the ending price (cap) so that the price is held flat after +operating year 15, as in the base case. +The Construction Timeline row holds the equity-only construction period at one year, as in the base case; otherwise a +longer timeline would also shift more of the construction spending to equity. +The ITC={{ itc_rate_excluding_interconnection_pct_1dp }}% case is the rate that removes the interconnection cost from the ITC basis (see Investment Tax Credit +Basis in the [Discussion section](#discussion-section)). +The Utilization Factor row represents 5% and 10% curtailment as flat derates (see Curtailment in the Discussion section). + +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-section). + +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. + +### 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 + + + + +### 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 trade-off was considered in reservoir modeling that guided Fervo's field implementation (Singh et al., 2025). + +The parametric analysis below visualizes the trade-off within the context of the case study GEOPHIRES scenario, +revealing a sawtooth-shaped profile in the economic metrics (LCOE, IRR, NPV) despite the approximately linear increase in electricity production. +This pattern emerges because GEOPHIRES models capacity maintenance as discrete step functions +(shown in the Number of times redrilling chart) where exceeding the `Maximum Drawdown` threshold forces an entire new +redrilling campaign. +While real-world operators would likely mitigate these sharp transitions through continuous make-up drilling, +dynamic flow rate modulation, or other adaptive reservoir management strategies, +the model highlights a fundamental reality: the economic optimum is not necessarily the maximum achievable flow rate. +Instead, it is the rate that effectively balances immediate generation gains against the potential step-wise increase +in operational costs required to offset thermal decline. +See [Redrilling Assumptions](#technical-operational-considerations) below for further discussion. + +In the current version, the number of redrilling events steps {{ flow_rate_parametric_redrilling_steps_display }}. +Within the {{ flow_rate_parametric_base_redrills_word }}-redrilling band, IRR varies by no more than +{{ flow_rate_parametric_max_irr_change_above_base_pct_pts }} percentage points above the base case flow rate of +{{ production_flow_rate_kg_per_s_display }} kg/s. +Flow rates below {{ flow_rate_parametric_minimum_ppa_feasible_kg_per_s }} kg/s (shaded in the charts below) do not meet +the {{ ppa_minimum_net_generation_mw }} MWe minimum net generation requirement with +{{ number_of_production_wells }} production wells, so the higher returns at flow rates with fewer redrilling events are +not attainable under the case study's PPA constraint without additional wells. The dashed line marks the base case flow +rate. + +Flow rate sensitivity analysis graphs + +## Variants + +### 100 MWe Model (Phase I) + +The 100 MWe model, with equivalent capacity to Phase I, has not yet been updated for the September 2026 Update. +The February 2026 Update's 100 MWe model, `Fervo_Project_Cape-6`, is documented in the +[February 2026 Update of the case study](Fervo_Project_Cape-5.html#Fervo_Project_Cape-6-section). + +### Reduced Redrilling Scenario + +This scenario increases `Fracture Height` by {{ reduced_redrilling_fracture_height_increase_pct }}% (from +{{ reduced_redrilling_base_fracture_height_m }} m to {{ reduced_redrilling_fracture_height_m }} m), which increases +fracture surface area per stimulated well by the same proportion and extends the thermal plateau. +Stimulation cost, which the case study parameterizes per unit fracture surface area, increases accordingly. +The number of redrilling events falls from {{ reduced_redrilling_base_redrills_word }} +({{ reduced_redrilling_base_redrilling_years_display }} of operations) to {{ reduced_redrilling_redrills_word }} +({{ reduced_redrilling_redrilling_years_display }}), and minimum net generation remains above the +{{ ppa_minimum_net_generation_mw }} MWe PPA requirement. + +{{ reduced_redrilling_comparison_table_md }} + +GEOPHIRES amortizes the cost of all redrilling events uniformly over the project lifetime (see Redrilling Assumptions +in the [Discussion section](#discussion-section)), so each avoided event reduces operating costs from the first year of +operations. +This overstates the value of avoiding a late redrilling event compared with incurring its cost in the year it occurs. + +[Click here](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-7) to open the +base case in the GEOPHIRES web interface, and set `Fracture Height` to {{ reduced_redrilling_fracture_height_m }} to +reproduce this scenario. + + + +### Previous Versions + +Documentation is available for the following previous case study versions, which are deprecated in favor of this version. + +{# @formatter:off #} +#### `Fervo_Project_Cape-5` (February 2026 Update) + +[Version documentation](Fervo_Project_Cape-5.html) + +Source code: [Fervo_Project_Cape-5.txt](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-5.txt) +and [Fervo_Project_Cape-5.out](https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/tests/examples/Fervo_Project_Cape-5.out) + +Last Updated: 2026-07-03 + +Key differences in the September 2026 Update: + +1. Subsurface design aligned with the Fervo 3.0 well design: 3.06 km reservoir depth (221℃ / 430℉), 7,500 ft cased laterals, 8⅝ inch casing, and 225 fractures per well +1. Lateral drilling cost modeled explicitly (`Well Geometry Configuration`, `Number of Multilateral Sections per Vertical Section`, `Multilaterals Cased`, `All-in Nonvertical Drilling Costs`), with the drilling cost adjustment factor recalibrated to 0.72 against Fervo's disclosed Phase I well costs +1. Stimulation cost parameterized per unit fracture surface area +1. Parasitic load calibrated to 20% or more of net generation (Productivity Index and Injectivity Index derated) +1. Wellfield re-sized to 50 production wells and 34 injection wells; nameplate capacity of 11 Gen 2 ORC units +1. Grid interconnection ($250M) and firm transmission service ($21M/yr) added +1. PPA starting price of $115/MWh with a 1.5%-equivalent escalator from the second operating year, capped at the year 15 price +1. Construction period of 4 years; inflation rate of 3.0%; exploration cost re-derived from the current well cost +1. `ResFrac Profile + Hotter/Deeper Scenario` variant removed. It applied the Singh et al. (2025) temperature profile for the Phase I design to the February 2026 Update's 100 MWe inputs at a 3 km depth; the base case now uses the deeper 3.0 design, to which that profile does not apply +1. Reduced Redrilling Scenario rebuilt on current inputs (greater fracture height) and generated with the documentation instead of linked as a shared result + +The following tables list every input parameter whose value differs between the February 2026 Update and the September 2026 +Update, with the rationale for each change, and compare key results. +See the input files for the full parameter comments and citations. + +{{ previous_version_input_changes_table_md }} + +{{ previous_version_result_changes_table_md }} + +LCOE changes with the PPA price as well as with costs (see [Impact of PPA Price on LCOE](#impact-of-ppa-price-on-lcoe)). +With the February 2026 Update's PPA terms and all other September 2026 Update inputs, LCOE is +{{ '$' ~ previous_version_ppa_terms_lcoe_usd_per_mwh ~ '/MWh' }} rather than {{ '$' ~ lcoe_usd_per_mwh ~ '/MWh' }}; +that difference reflects the PPA price change rather than cost changes. + +#### `Fervo_Project_Cape-4` + +[Version documentation](Fervo_Project_Cape-4.html) + +Last Updated: 2025-08-11 + +Key differences: + +1. Fervo_Project_Cape-5 models multiple construction years instead of a single construction year +1. Fervo_Project_Cape-5 incorporates various reservoir characteristic and engineering updates including: + 1. Segmented geology (gradients) + 1. Ambient and surface temperature refinement + 1. 5-well bench design instead of well pairs (doublets) + 1. 8.5-inch inner well diameter + 1. Productivity/Injectivity indexes instead of impedance model + 1. Stimulation parameters and outcome +1. Fervo_Project_Cape-5 incorporates various reservoir and economic parameter updates including: + 1. BLM royalties + 1. Refined discount and interest rates + 1. Refined tax rates including addition of property tax +1. Fervo_Project_Cape-5 includes more comprehensive sensitivity analysis +{# @formatter:off #} + +{# TODO others e.g. Fervo_Project_Cape-3... #} + + + +## Discussion, Limitations, & Future Work + +While this case study establishes a validated economic baseline for Second-of-a-Kind (SOAK) EGS using standardized +consensus data, users should interpret results (LCOE, IRR, NPV, etc.) as an assessment of intrinsic project +viability rather than a fully burdened commercial pro-forma. +Several financial, technical, and operational factors were not explicitly modeled and represent areas for future refinement. + +Users may wish to explicitly encode these cost factors in their own models by using applicable adjustment parameters. +For OPEX, such parameters may include `Wellfield O&M Cost Adjustment Factor`, +`Surface Plant O&M Cost Adjustment Factor` or `Total O&M Cost`. For CAPEX, `One-time Flat License Fees Etc` or +`Total Capital Cost` may be appropriate. +Note that the case study itself uses `One-time Flat License Fees Etc` for grid interconnection cost and +`Annual License Fees Etc` for transmission service cost, so additional costs entered via these parameters should be +added to the case study values. + +### Financial Considerations + +The case study economic results focus on project-level cash flows but do not explicitly itemize several overhead costs +and administrative realities often associated with commercial project financing. +Including these may reduce the levered IRR. + +1. Transaction & Closing Fees: The Total CAPEX includes interest during construction but does not explicitly budget for debt closing or bank fees. In large-scale energy infrastructure, these transaction costs may constitute a small percent of total debt volume. [Click here](https://github.com/NatLabRockies/GEOPHIRES-X/issues/451) to view the relevant GEOPHIRES tracking issue on GitHub. +1. Reserve Accounts: The cash flow analysis assumes immediate liquidity. It does not model working capital, debt service, or major equipment replacement reserve accounts. Project financial partners may require that the project owner(s) establish and fund reserve accounts. [Click here](https://github.com/NatLabRockies/GEOPHIRES-X/issues/460) to view the relevant GEOPHIRES tracking issue on GitHub. +1. Insurance Premiums: The model does not include a specific line item for insurance. For EGS projects involving deep subsurface risks, insurance premiums could represent an additional annual operating expense. [Click here](https://github.com/NatLabRockies/GEOPHIRES-X/issues/369) to view the relevant GEOPHIRES tracking issue on GitHub. +1. Investment Tax Credit Basis: GEOPHIRES's SAM Single Owner PPA model applies the ITC rate to total installed cost, which includes the grid interconnection cost. Qualified interconnection property is only eligible for the ITC for energy property of 5 MW or less (IRC §48(a)(8)), so the model overstates the credit by approximately {{ '$' ~ (interconnection_share_of_total_capex_musd * 0.3) | round | int ~ 'M' }} (30% of the {{ '$' ~ interconnection_share_of_total_capex_musd ~ 'M' }} interconnection cost including its share of inflation and interest during construction). The case study retains the statutory 30% rate because the same line item carries an unmodeled benefit in the other direction: in non-ISO balancing authorities such as PacifiCorp, network upgrade costs, the majority of interconnection cost, are often refunded to the interconnection customer over up to 20 years with interest once the plant reaches commercial operation (Seel et al., 2026). An ITC rate of {{ itc_rate_excluding_interconnection_pct }}% applied to total installed cost removes the overstatement and reduces the after-tax IRR by approximately {{ itc_excluding_interconnection_irr_reduction_pct_pts }} percentage points. A parameter that excludes a specified amount from the ITC basis, or an ITC amount input, would allow both effects to be modeled explicitly. +1. Debt Term: GEOPHIRES sets the loan term of the SAM Single Owner PPA model to the plant lifetime, so the case study repays debt in level annual payments over {{ project_lifetime_yr }} years, beyond the 15-year PPA term. The minimum pre-tax debt service coverage ratio (DSCR) is {{ min_dscr }}, in operating year {{ min_dscr_year }}. A loan term no longer than the PPA term would increase annual debt service, reducing the DSCR and, at the same debt fraction, the levered IRR. +1. Salvage Value and Decommissioning: The SAM Single Owner PPA model credits a salvage value of {{ salvage_value_pct_of_total_capex ~ '%' }} of total CAPEX ({{ '$' ~ salvage_value_musd ~ 'M' }}) as revenue in the final project year. Well plugging and abandonment and other decommissioning costs are not modeled. + +### Technical & Operational Considerations + +1. Grid Interconnection & Transmission: The case study includes {{ '$' ~ interconnection_cost_musd ~ 'M' }} of network resource interconnection cost in CAPEX (entered as `One-time Flat License Fees Etc`) and {{ '$' ~ transmission_cost_musd_per_yr ~ 'M/yr' }} of long-term firm point-to-point transmission service in OPEX (entered as `Annual License Fees Etc`); see the Economic Parameters table for derivations. Not modeled: refunds of network upgrade costs to the interconnection customer after commercial operation (see Investment Tax Credit Basis above); a second firm transmission reservation that would be required to deliver across an intervening system to a CAISO offtaker, which could roughly double the transmission cost; transmission losses; and escalation of the transmission rate, which GEOPHIRES holds flat in nominal terms. +1. Curtailment: Curtailment is not modeled. The `Utilization Factor` input represents plant availability, and the GEOPHIRES SAM Economic Model integration does not currently support grid-limit or curtailment inputs. The case study assumes network resource interconnection service sufficient for full output. As an illustration, reducing the `Utilization Factor` by 5% and 10% (to {{ curtailment_5pct_utilization_factor }} and {{ curtailment_10pct_utilization_factor }}) as a flat proxy for curtailment reduces the after-tax IRR by approximately {{ curtailment_5pct_irr_reduction_pct_pts }} and {{ curtailment_10pct_irr_reduction_pct_pts }} percentage points, respectively. A per-year utilization factor schedule would also allow a first-year commissioning ramp to be modeled, such as the ramp-up Fervo reports for the first Phase I GeoBlock as it brings the full well system online after first power (Fervo Energy, 2026e). +1. Nominal O&M Costs: GEOPHIRES holds fixed O&M costs, including the transmission service cost, flat in nominal terms. At the case study's 3.0% inflation rate, this represents a declining real cost over the 30-year project lifetime, which is optimistic. +1. Seasonal and Hourly Performance Granularity: As noted in the Surface Plant Parameters, this study uses a fixed annual average ambient temperature. It does not model the seasonal or hourly impacts on air-cooled condenser efficiency. In reality, power output would likely dip during peak summer hours and increase during winter. Future versions of GEOPHIRES may add support for these considerations; [click here](https://github.com/NatLabRockies/GEOPHIRES-X/issues/395) to view the relevant GEOPHIRES tracking issue on GitHub. +1. Redrilling Assumptions: Management of thermal decline is modeled as periodic redrilling events that reset the reservoir's heat content to initial conditions at intervals determined by the `Maximum Drawdown` parameter. The cost of these events is amortized into a uniform annual operating expense. In reality, these events may be scheduled as discrete capital-intensive campaigns punctuated by years of lower spending, or as a continuous program of make-up well drilling to mitigate decline. Fervo describes its approach as a makeup-well drilling program in which later wells benefit from cumulative learnings (Fervo Energy, 2026f). [Click here](https://github.com/NatLabRockies/GEOPHIRES-X/issues/381) to view the relevant GEOPHIRES tracking issue on GitHub.{% if reservoir_heat_content_negative_from_year is not none %} Note that the reservoir heat content and percentage of total heat mined in the annual profile of the result file are cumulative against the initial reservoir and are not reset by redrilling, so the heat content is negative from operating year {{ reservoir_heat_content_negative_from_year }} and {{ final_year_pct_total_heat_mined ~ '%' }} of the initial heat content is mined by the final year. These values reflect that accounting rather than physical depletion: redrilled wells access rock that the initial reservoir heat content does not include.{% endif %} +1. Reservoir Modeling Fidelity: The base case utilizes the Gringarten analytical model, which produces a characteristic thermal plateau maintained until a sharp breakthrough event. A key open question is whether this idealized profile provides an acceptably accurate approximation of drawdown over time, or if it masks the gradual dispersion caused by flow channeling and heterogeneity. The [Project Red case study](Fervo_Project_Red.html) compares the GEOPHIRES Gringarten model against the first two years of measured production temperatures from Fervo's Project Red and finds close alignment when the active fracture count is de-rated for heterogeneous flow; that record does not yet extend to the long-term decline that determines redrilling timing. Future work should evaluate whether Gringarten remains the optimal scoping proxy, or if an alternative analytical solution[^alt-analytical-solution-proposal] would offer a superior balance of speed and realism by better mimicking the shape of numerical simulation outputs. +1. ORC Efficiency: The power generation results rely on the GEOPHIRES built-in supercritical ORC efficiency correlation, which assumes the selection of an optimal working fluid for each specific geofluid temperature. For further details, please refer to the [Surface Plant section in the Theoretical Basis for GEOPHIRES](Theoretical-Basis-for-GEOPHIRES.html#surface-plant). GEOPHIRES warns that the built-in ORC correlations may not be valid above a production temperature of 200℃; the case study production temperature (approximately {{ average_production_temperature_degc ~ '℃' }}) exceeds this, which adds uncertainty to the modeled conversion efficiency and power plant cost. This limitation may be addressed in the future by [FGEM integration](https://github.com/NatLabRockies/GEOPHIRES-X/issues/395). + +[^alt-analytical-solution-proposal]: Possible alternatives might include Residence Time Distribution (RTD) modeling, or a parameterized profile generator (for example, Bezier curves fitted to numerical simulation results, as in the ResFrac Profile variant included in the February 2026 Update). + +--- + +## 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 + +BLS. (2026a, June 10). Consumer Price Index – May 2026. +https://www.bls.gov/news.release/archives/cpi_06102026.htm + +BLS. (2026b, August 12). Consumer Price Index – July 2026. +https://www.bls.gov/news.release/archives/cpi_08122026.htm + +BPA. (2026, June). BPA Facts (DOE/BP-5493). Transmission rates (fiscal years 2024–2025). +https://www.bpa.gov/-/media/Aep/about/publications/general-documents/bpa-facts.pdf + +CTVC. (2025, September 2). The $783m PPA that keeps on drilling #260. +https://www.ctvc.co/the-783m-ppa-that-keeps-on-drilling-260/ + +DeGolyer and MacNaughton. (2024, September 24). +Report as of June 30, 2024 on Heat Initially In Place associated with the Project Cape Area prepared for Fervo Energy. +Securities and Exchange Commission, Exhibit 99.1. +https://www.sec.gov/Archives/edgar/data/1853868/000162828026025821/exhibit991-sx1.htm + +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. (2023a, 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. (2023b, 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. (2025a, March 31). Geothermal Mythbusting: Water Use and +Impacts. https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/ + +Fervo Energy. (2025b, 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. (2025c, 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/ + +Fervo Energy. (2026a, August 12). Fervo Energy Reports Second Quarter 2026 Results. +https://fervoenergy.com/fervo-energy-reports-second-quarter-2026-results/ + +Fervo Energy. (2026b, September 1). Fervo Energy and Google Sign 396 MW PPA. +https://fervoenergy.com/fervo-energy-and-google-sign-396-mw-ppa/ + +Fervo Energy. (2026c, June 22). Fervo Energy Reports First Quarter 2026 Results. +https://ir.fervoenergy.com/news-releases/news-release-details/fervo-energy-reports-first-quarter-2026-results + +Fervo Energy. (2026d, May 11). Form S-1/A Registration Statement (Amendment No. 3). U.S. Securities and Exchange +Commission. https://www.sec.gov/Archives/edgar/data/1853868/000162828026033127/fervoenergy-sx1a3.htm + +Fervo Energy. (2026e, September 24). Fervo Energy Achieves First Power at Cape Station, a Landmark Moment for the +Future of Enhanced Geothermal Systems. +https://fervoenergy.com/fervo-energy-achieves-first-power-at-cape-station-a-landmark-moment-for-the-future-of-enhanced-geothermal-systems/ + +Fervo Energy. (2026f, August 12). Fervo Energy (FRVO) Q2 2026 Earnings Call Transcript. The Motley Fool, +published August 19, 2026. +https://www.fool.com/earnings/call-transcripts/2026/08/19/fervo-energy-frvo-q2-2026-earnings-call-transcript/ +(webcast replay: https://edge.media-server.com/mmc/p/va49yxkc/) + +Fervo Energy. (2026g, August 12). Q2 2026 Quarterly Results [Earnings presentation]. +https://ir.fervoenergy.com/static-files/28260ce5-2ac0-458e-bfb7-2d80f3709cae + +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. (2026, February 9). An Update of Activities and Plans at Fervo. +Oral presentation at the 51st Workshop on Geothermal Reservoir Engineering, Stanford University, Stanford, CA. +https://pangea.stanford.edu/ERE/db/GeoConf/Abstract.php?PaperID=9496 + +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 + +PacifiCorp. (2017, June 1). FAQ: Transmission and Ancillary Service Rate Changes. +https://www.oasis.oati.com/PPW/PPWdocs/Rate_Update_FAQ_20170601.pdf + +Quantum Proppant Technologies. (2020). Well Completion Technology. World +Oil. https://quantumprot.com/uploads/images/2b8583e8ce8038681a19d5ad1314e204.pdf + +Seel, J., Manderlink, N., Mulvaney Kemp, J., Rand, J., Gorman, W., Wiser, R., Cotton, W., Porter, K. (2026, February). +Generator Interconnection Costs to the Transmission System in non-ISO Balancing Authorities. Lawrence Berkeley National +Laboratory. https://eta-publications.lbl.gov/sites/default/files/2026-02/lbnl_2026.02.23_ba_interconnection_costs.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. (2019). GeoVision: Harnessing the Heat Beneath Our Feet (p. 163, drilling cost scenarios). +https://www.energy.gov/sites/prod/files/2019/06/f63/GeoVision-full-report-opt.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 + +Utility Dive. (2026, June 5). Fervo Energy faces transmission constraints in the West, analysts say. +https://www.utilitydive.com/news/fervo-energy-geothermal-transmission-constraints/822141/ + +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/Fervo_Project_Red.md.jinja b/docs/Fervo_Project_Red.md.jinja index 09f6edf7c..8be6ee28c 100644 --- a/docs/Fervo_Project_Red.md.jinja +++ b/docs/Fervo_Project_Red.md.jinja @@ -133,7 +133,7 @@ geometry for the Project Red simulation are detailed below: Note that these parameters represent a simplified, homogenized analytical equivalent of a highly complex, heterogeneous subsurface fracture network. -Further relevant detailed discussion can be found in the [Cape Station case study methodology section](Fervo_Project_Cape-5.html#calibration-with-fervo-implemented-field-design). +Further relevant detailed discussion can be found in the [Cape Station case study methodology section](Fervo_Project_Cape-7.html#calibration-with-fervo-implemented-field-design). See also the [effective number of fractures sensitivity analysis below](#sensitivity-analysis-effective-number-of-fractures-section). ## Results @@ -181,7 +181,8 @@ Both models demonstrate high predictive fidelity, tracking steady-state flowing ### Long-Term Forecast ({{ long_term_forecast_years }}-Year Horizon) To evaluate the model's predictive behavior over a longer timeframe, the GEOPHIRES simulation was extended to an {{ long_term_forecast_years }}-year horizon. -This timeframe aligns with the redrilling interval modeled in the [Cape Station case study](Fervo_Project_Cape-5.html) +This timeframe aligns with the redrilling interval modeled in the +[February 2026 Update of the Cape Station case study](Fervo_Project_Cape-5.html) and provides a plausible view of the anticipated thermal decline before major wellfield intervention would be required. ![](_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png) @@ -289,7 +290,7 @@ The economic inputs utilized in this run are largely generalized default assumpt the literature. However, this parameter is provided primarily for continuity and has not been rigorously calibrated for a full financial assessment here. Users seeking a comprehensive, purpose-built template for EGS techno-economic analysis are strongly encouraged to -utilize the [500 MW EGS Cape Station Case Study](Fervo_Project_Cape-5.html#economic-parameters) instead, which +utilize the [500 MW EGS Cape Station Case Study](Fervo_Project_Cape-7.html#economic-parameters) instead, which features a fully developed SAM Economic Model financial pro-forma. --- diff --git a/docs/GEOPHIRES-Examples.md b/docs/GEOPHIRES-Examples.md index 941a6fe66..1856b7907 100644 --- a/docs/GEOPHIRES-Examples.md +++ b/docs/GEOPHIRES-Examples.md @@ -7,7 +7,7 @@ or in the [web interface](https://gtp.scientificwebservices.com/geophires) under ## Case Study: 500 MW EGS Project Modeled on Fervo Cape Station -See documentation: [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-5.html). +See documentation: [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](Fervo_Project_Cape-7.html). ## Fervo Project Red: Evaluating the Gringarten Model against Empirical EGS Data diff --git a/docs/SAM-Economic-Models.md b/docs/SAM-Economic-Models.md index a0fd84c8f..2df7c1b89 100644 --- a/docs/SAM-Economic-Models.md +++ b/docs/SAM-Economic-Models.md @@ -157,7 +157,7 @@ For detailed information on how these different configurations are modeled finan ## Examples -1. [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-5) | [Case Study Documentation](Fervo_Project_Cape-5.html) +1. [Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=Fervo_Project_Cape-7) | [Case Study Documentation](Fervo_Project_Cape-7.html) 1. [SAM Single Owner PPA: 50 MWe](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA) 1. [50 MWe with Add-ons](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-3) 1. [50 MWe with Royalties](https://gtp.scientificwebservices.com/geophires/?geophires-example-id=example_SAM-single-owner-PPA-4) diff --git a/docs/_images/fervo_project_cape-7-power-production.png b/docs/_images/fervo_project_cape-7-power-production.png new file mode 100644 index 000000000..82a409f73 Binary files /dev/null and b/docs/_images/fervo_project_cape-7-power-production.png differ diff --git a/docs/_images/fervo_project_cape-7-production-temperature.png b/docs/_images/fervo_project_cape-7-production-temperature.png new file mode 100644 index 000000000..cb27a6589 Binary files /dev/null and b/docs/_images/fervo_project_cape-7-production-temperature.png differ diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-flow-rate.png b/docs/_images/fervo_project_cape-7-sensitivity-analysis-flow-rate.png new file mode 100644 index 000000000..ad20527c4 Binary files /dev/null and b/docs/_images/fervo_project_cape-7-sensitivity-analysis-flow-rate.png differ diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.png b/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.png new file mode 100644 index 000000000..3301aeda7 Binary files /dev/null and b/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.png differ diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.svg b/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.svg new file mode 100644 index 000000000..b99708325 --- /dev/null +++ b/docs/_images/fervo_project_cape-7-sensitivity-analysis-irr.svg @@ -0,0 +1,3512 @@ + + + + + + + + 2026-09-29T11:33:29.457862 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.png b/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.png new file mode 100644 index 000000000..e0c8e07bf Binary files /dev/null and b/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.png differ diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.svg b/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.svg new file mode 100644 index 000000000..3660100f7 --- /dev/null +++ b/docs/_images/fervo_project_cape-7-sensitivity-analysis-lcoe.svg @@ -0,0 +1,3765 @@ + + + + + + + + 2026-09-29T11:33:30.929338 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.png b/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.png new file mode 100644 index 000000000..75744363d Binary files /dev/null and b/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.png differ diff --git a/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.svg b/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.svg new file mode 100644 index 000000000..2368ff598 --- /dev/null +++ b/docs/_images/fervo_project_cape-7-sensitivity-analysis-project_npv.svg @@ -0,0 +1,3816 @@ + + + + + + + + 2026-09-29T11:33:31.722355 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_images/fervo_project_cape-7-singh-et-al-base-simulation-production-temperature.png b/docs/_images/fervo_project_cape-7-singh-et-al-base-simulation-production-temperature.png new file mode 100644 index 000000000..c28d19800 Binary files /dev/null and b/docs/_images/fervo_project_cape-7-singh-et-al-base-simulation-production-temperature.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png index a8bab71b3..a6847d675 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-1.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png index 007738953..e459eda22 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-2.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-1.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-1.png index 2877efee3..6671abca2 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-1.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-1.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-2.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-2.png index af7999bab..3bb2293e7 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-2.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-2.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png index 85c3aaa01..cb54ba4dd 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-long-term.png differ diff --git a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png index 6c59add38..15f95cfac 100644 Binary files a/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png differ diff --git a/docs/index.rst b/docs/index.rst index b715415f6..cebd1e183 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,7 +24,7 @@ Contents :caption: Examples GEOPHIRES-Examples - Fervo_Project_Cape-5 + Fervo_Project_Cape-7 Fervo_Project_Red diff --git a/setup.py b/setup.py index 068a52b4e..fd1480961 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def read(*names, **kwargs): 'bumpversion', 'sphinx_py3doc_enhanced_theme', 'opencv-python', # generate_fervo_project_red_2026_docs - 'jinja2', # generate_fervo_project_cape_5_md + 'jinja2', # generate_fervo_project_cape_7_md ], }, ) diff --git a/src/geophires_docs/__init__.py b/src/geophires_docs/__init__.py index 5c02a1600..cccbd9007 100644 --- a/src/geophires_docs/__init__.py +++ b/src/geophires_docs/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import math import os import re from pathlib import Path @@ -24,21 +25,33 @@ def _get_project_root() -> Path: return _get_file_path('../..') -def _get_fpc5_input_file_path(project_root: Path | None = None) -> Path: +def _get_fpc7_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' + return project_root / 'tests/examples/Fervo_Project_Cape-7.txt' -def _get_fpc5_result_file_path(project_root: Path | None = None) -> Path: +def _get_fpc7_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' + return project_root / 'tests/examples/Fervo_Project_Cape-7.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() +_FPC7_INPUT_FILE_PATH: Path = _get_fpc7_input_file_path() +_FPC7_RESULT_FILE_PATH: Path = _get_fpc7_result_file_path() + +_FPC7_PPA_MINIMUM_NET_GENERATION_MW: float = 500.0 + +# Gross capacity of each Gen 2 ORC unit announced for Cape Station Phase II (Turboden, 2025; Jacobs, 2025). +_FPC7_ORC_UNIT_GROSS_CAPACITY_MW: float = 60.0 + + +def _get_fpc7_orc_unit_count(max_total_generation_mw: float) -> int: + """ + :return: Number of Gen 2 ORC units required for nameplate capacity to cover the maximum total (gross) generation. + """ + return math.ceil(max_total_generation_mw / _FPC7_ORC_UNIT_GROSS_CAPACITY_MW) def _get_logger(_name_: str) -> Any: @@ -63,7 +76,7 @@ def error(self, msg): return _PrintLogger() -def _get_input_parameters_dict( # TODO consolidate with FervoProjectCape5TestCase._get_input_parameters +def _get_input_parameters_dict( # TODO consolidate with FervoProjectCape7TestCase._get_input_parameters _params: GeophiresInputParameters, include_parameter_comments: bool = False, include_line_comments: bool = False ) -> dict[str, Any]: comment_idx = 0 diff --git a/src/geophires_docs/__main__.py b/src/geophires_docs/__main__.py index 0783de944..cddde8f84 100644 --- a/src/geophires_docs/__main__.py +++ b/src/geophires_docs/__main__.py @@ -1,6 +1,6 @@ if __name__ == '__main__': - from geophires_docs import generate_fervo_project_cape_5_docs + from geophires_docs import generate_fervo_project_cape_7_docs from geophires_docs import generate_fervo_project_red_2026_docs - generate_fervo_project_cape_5_docs.generate_fervo_project_cape_5_docs() + generate_fervo_project_cape_7_docs.generate_fervo_project_cape_7_docs() generate_fervo_project_red_2026_docs.generate_fervo_project_red_2026_docs() diff --git a/src/geophires_docs/data/Fervo_Project_Cape-7_flow_rate_parametric.csv b/src/geophires_docs/data/Fervo_Project_Cape-7_flow_rate_parametric.csv new file mode 100644 index 000000000..b3dfe6372 --- /dev/null +++ b/src/geophires_docs/data/Fervo_Project_Cape-7_flow_rate_parametric.csv @@ -0,0 +1,52 @@ +flow_kg_per_s,avg_net_mw,min_net_mw,redrills,lcoe_cents_per_kwh,irr_pct,npv_musd +80,417.13,404.96,1,10.56,23.05,272.37 +81,421.81,409.39,1,10.51,23.25,282.22 +82,426.47,413.88,1,10.46,23.44,291.86 +83,431.11,418.45,1,10.42,23.63,301.47 +84,435.74,423.09,1,10.37,23.82,311.07 +85,440.35,427.8,1,10.33,24,320.65 +86,444.94,432.58,1,10.29,24.17,330.07 +87,449.47,436.18,1,10.25,24.34,339.46 +88,454.01,441.1,1,10.21,24.51,348.78 +89,458.53,446.08,1,10.17,24.67,358.09 +90,462.97,449.84,1,10.14,24.83,367.26 +91,467.39,454.95,1,10.1,24.98,376.43 +92,471.72,458.8,1,10.06,25.14,385.44 +93,476,464.03,2,10.9,22.18,255.54 +94,480.31,467.99,2,10.86,22.36,264.45 +95,484.62,471.98,2,10.82,22.53,273.31 +96,488.93,476.01,2,10.78,22.7,282.07 +97,493.3,481.48,2,10.75,22.87,290.9 +98,497.18,485.2,2,10.72,23,298.21 +99,500.3,488.22,2,10.7,23.07,303.13 +100,503.39,491.25,2,10.68,23.14,307.98 +101,506.43,494.3,2,10.66,23.21,312.62 +102,509.43,497.36,2,10.65,23.28,317.12 +103,512.39,500.42,2,10.63,23.34,321.49 +104,515.3,503.5,2,10.62,23.39,325.66 +105,518.09,505.08,2,10.61,23.44,329.59 +106,520.92,508.18,2,10.59,23.49,333.55 +107,523.7,511.28,2,10.58,23.54,337.31 +108,526.43,514.39,2,10.57,23.58,340.9 +109,529.04,515.94,2,10.56,23.61,344.23 +110,531.68,519.07,2,10.55,23.65,347.59 +111,534.27,522.19,2,10.55,23.68,350.7 +112,536.74,523.72,2,10.54,23.7,353.53 +113,539.24,526.86,2,10.53,23.73,356.37 +114,541.6,528.36,2,10.53,23.74,358.92 +115,544.01,531.5,2,10.52,23.76,361.45 +116,546.28,532.97,2,10.52,23.77,363.71 +117,548.59,536.12,2,10.51,23.78,365.85 +118,550.77,537.57,2,10.51,23.79,367.8 +119,552.98,540.71,2,10.51,23.79,369.77 +120,555.05,542.15,2,10.51,23.79,371.31 +121,557.08,543.54,2,10.51,23.79,372.72 +122,559.14,546.68,2,10.51,23.78,374.16 +123,561.06,548.07,2,10.51,23.77,375.24 +124,562.92,549.42,2,10.51,23.76,376.19 +125,564.8,552.53,2,10.51,23.74,377.14 +126,566.54,553.88,2,10.51,23.73,377.77 +127,568.21,555.19,2,10.52,23.7,378.17 +128,569.79,556.47,2,10.52,23.68,378.46 +129,571.33,559.54,3,11.25,20.9,239.92 +130,572.84,560.81,3,11.26,20.88,239.78 diff --git a/src/geophires_docs/fervo_project_cape_7_scenarios.py b/src/geophires_docs/fervo_project_cape_7_scenarios.py new file mode 100644 index 000000000..b2e2fb9e6 --- /dev/null +++ b/src/geophires_docs/fervo_project_cape_7_scenarios.py @@ -0,0 +1,214 @@ +""" +Supplementary Fervo_Project_Cape-7 scenario results cited in the case study documentation: the production flow rate +parametric, which is stored as data and regenerated manually because it runs 51 simulations, and single-input +scenarios, which are simulated when the documentation is generated. + +Regenerate the flow rate parametric data after changing Fervo_Project_Cape-7 inputs: + + python -m geophires_docs.fervo_project_cape_7_scenarios + +FervoProjectCape7TestCase.test_flow_rate_parametric_data_matches_example_result fails when the data is stale. +""" + +from __future__ import annotations + +import csv +import dataclasses +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from geophires_docs import _get_fpc7_input_file_path +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__) + +FPC7_FLOW_RATE_PARAMETRIC_CSV_PATH: Path = ( + Path(__file__).parent / 'data' / 'Fervo_Project_Cape-7_flow_rate_parametric.csv' +) + +_FLOW_RATE_PARAM_NAME = 'Production Flow Rate per Well' + + +@dataclass(frozen=True) +class FlowRateParametricRow: + flow_kg_per_s: float + avg_net_mw: float + min_net_mw: float + redrills: int + lcoe_cents_per_kwh: float + irr_pct: float + npv_musd: float + + @staticmethod + def from_result(flow_kg_per_s: float, result: GeophiresXResult) -> FlowRateParametricRow: + r = result.result + surf_equip_sim = r['SURFACE EQUIPMENT SIMULATION RESULTS'] + econ = r['ECONOMIC PARAMETERS'] + return FlowRateParametricRow( + flow_kg_per_s=flow_kg_per_s, + avg_net_mw=surf_equip_sim['Average Net Electricity Generation']['value'], + min_net_mw=surf_equip_sim['Minimum Net Electricity Generation']['value'], + redrills=int(r['ENGINEERING PARAMETERS']['Number of times redrilling']['value']), + lcoe_cents_per_kwh=r['SUMMARY OF RESULTS']['Electricity breakeven price']['value'], + irr_pct=econ['After-tax IRR']['value'], + npv_musd=econ['Project NPV']['value'], + ) + + +@dataclass(frozen=True) +class RedrillingStep: + flow_below_kg_per_s: float + flow_at_kg_per_s: float + redrills_below: int + redrills_at: int + + +@dataclass(frozen=True) +class FlowRateParametricSummary: + redrilling_steps: list[RedrillingStep] + minimum_ppa_feasible_flow_rate_kg_per_s: float + base_redrills: int + max_irr_change_above_base_within_band_pct_pts: float + + +def generate_fpc7_flow_rate_parametric_csv( + input_params: GeophiresInputParameters | None = None, + flow_rates_kg_per_s: list[float] | None = None, + output_path: Path = FPC7_FLOW_RATE_PARAMETRIC_CSV_PATH, +) -> list[FlowRateParametricRow]: + if input_params is None: + input_params = ImmutableGeophiresInputParameters(from_file_path=_get_fpc7_input_file_path()) + + if flow_rates_kg_per_s is None: + flow_rates_kg_per_s = [float(it) for it in range(80, 131)] + + client = GeophiresXClient() + rows = [] + for flow_rate in flow_rates_kg_per_s: + _log.info(f'Simulating {_FLOW_RATE_PARAM_NAME} = {flow_rate:g} kg/s...') + result = client.get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=input_params.as_file_path(), params={_FLOW_RATE_PARAM_NAME: flow_rate} + ) + ) + rows.append(FlowRateParametricRow.from_result(flow_rate, result)) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w', encoding='utf-8', newline='') as f: + writer = csv.DictWriter( + f, fieldnames=[it.name for it in dataclasses.fields(FlowRateParametricRow)], lineterminator='\n' + ) + writer.writeheader() + for row in rows: + writer.writerow({k: f'{v:g}' if isinstance(v, float) else v for k, v in dataclasses.asdict(row).items()}) + + _log.info(f'✓ Wrote {output_path}') + return rows + + +def load_fpc7_flow_rate_parametric(path: Path = FPC7_FLOW_RATE_PARAMETRIC_CSV_PATH) -> list[FlowRateParametricRow]: + with open(path, encoding='utf-8', newline='') as f: + rows = [ + FlowRateParametricRow( + flow_kg_per_s=float(it['flow_kg_per_s']), + avg_net_mw=float(it['avg_net_mw']), + min_net_mw=float(it['min_net_mw']), + redrills=int(it['redrills']), + lcoe_cents_per_kwh=float(it['lcoe_cents_per_kwh']), + irr_pct=float(it['irr_pct']), + npv_musd=float(it['npv_musd']), + ) + for it in csv.DictReader(f) + ] + + return sorted(rows, key=lambda it: it.flow_kg_per_s) + + +def get_fpc7_flow_rate_parametric_row(rows: list[FlowRateParametricRow], flow_kg_per_s: float) -> FlowRateParametricRow: + matching_rows = [it for it in rows if it.flow_kg_per_s == flow_kg_per_s] + if len(matching_rows) != 1: + raise ValueError(f'Expected exactly one flow rate parametric row for {flow_kg_per_s:g} kg/s.') + + return matching_rows[0] + + +def get_fpc7_flow_rate_parametric_summary( + rows: list[FlowRateParametricRow], + base_flow_rate_kg_per_s: float, + ppa_minimum_net_generation_mw: float, +) -> FlowRateParametricSummary: + """ + :raises ValueError: if the parametric results no longer support the flow rate discussion in the case study + documentation, which assumes that minimum net generation meets the PPA minimum above a single threshold flow + rate and that no flow rate with fewer redrilling events than the base case meets it. + """ + base_row = get_fpc7_flow_rate_parametric_row(rows, base_flow_rate_kg_per_s) + + redrilling_steps = [ + RedrillingStep( + flow_below_kg_per_s=below.flow_kg_per_s, + flow_at_kg_per_s=at.flow_kg_per_s, + redrills_below=below.redrills, + redrills_at=at.redrills, + ) + for below, at in zip(rows, rows[1:]) + if below.redrills != at.redrills + ] + + feasible = [it.min_net_mw >= ppa_minimum_net_generation_mw for it in rows] + if not any(feasible): + raise ValueError(f'No flow rate meets the PPA minimum net generation ({ppa_minimum_net_generation_mw:g} MW).') + + first_feasible_idx = feasible.index(True) + if not all(feasible[first_feasible_idx:]): + raise ValueError( + 'Minimum net generation does not meet the PPA minimum at every flow rate above the lowest one that does; ' + 'update the flow rate discussion in the case study documentation.' + ) + + if any(it.redrills < base_row.redrills and it.min_net_mw >= ppa_minimum_net_generation_mw for it in rows): + raise ValueError( + 'A flow rate with fewer redrilling events than the base case meets the PPA minimum net generation; ' + 'update the flow rate discussion in the case study documentation.' + ) + + band_rows_above_base = [ + it for it in rows if it.flow_kg_per_s >= base_flow_rate_kg_per_s and it.redrills == base_row.redrills + ] + + return FlowRateParametricSummary( + redrilling_steps=redrilling_steps, + minimum_ppa_feasible_flow_rate_kg_per_s=rows[first_feasible_idx].flow_kg_per_s, + base_redrills=base_row.redrills, + max_irr_change_above_base_within_band_pct_pts=max( + abs(it.irr_pct - base_row.irr_pct) for it in band_rows_above_base + ), + ) + + +def get_scenario_results( + input_params: GeophiresInputParameters, + scenario_params_by_name: dict[str, dict[str, Any]], +) -> dict[str, GeophiresXResult]: + """ + :return: Result of each scenario, by scenario name, where each scenario overrides the given base case input + parameters. + """ + client = GeophiresXClient() + results = {} + for scenario_name, scenario_params in scenario_params_by_name.items(): + _log.info(f'Simulating scenario: {scenario_name}...') + results[scenario_name] = client.get_geophires_result( + ImmutableGeophiresInputParameters(from_file_path=input_params.as_file_path(), params=scenario_params) + ) + + return results + + +if __name__ == '__main__': + generate_fpc7_flow_rate_parametric_csv() diff --git a/src/geophires_docs/generate_fervo_project_cape_5_md.py b/src/geophires_docs/generate_fervo_project_cape_5_md.py deleted file mode 100755 index 8f889c71a..000000000 --- a/src/geophires_docs/generate_fervo_project_cape_5_md.py +++ /dev/null @@ -1,603 +0,0 @@ -#!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 _NON_BREAKING_SPACE -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.ParameterUtils import COMMENT_PARAMETER_NAME_PREFIX -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__) - - -def _get_schema(schema_file_name: str) -> dict[str, Any]: - project_root = _current_project_root if _current_project_root is not None else _get_project_root() - schema_file = project_root / 'src/geophires_x_schema_generator' / schema_file_name - with open(schema_file, encoding='utf-8') as f: - return json.loads(f.read()) - - -def _get_geophires_request_schema() -> dict[str, Any]: - return _get_schema('geophires-request.json') - - -def _get_input_parameter_schema(param_name: str) -> dict[str, Any]: - return _get_geophires_request_schema()['properties'][param_name] - - -def _get_input_parameter_schema_type(param_name: str) -> dict[str, Any]: - return _get_input_parameter_schema(param_name)['type'] - - -def _get_input_parameter_category(param_name: str) -> str: - return _get_input_parameter_schema(param_name)['category'] - - -def _get_input_parameter_units(param_name: str) -> str | None: - unit = _get_geophires_request_schema()['properties'][param_name]['units'] - - if unit == '': - return 'dimensionless' - - return unit - - -def _get_geophires_result_schema() -> dict[str, Any]: - return _get_schema('geophires-result.json') - - -def _get_output_parameter_schema(param_name: str) -> dict[str, Any]: - categorized_schema: dict[str, dict[str, Any]] = _get_geophires_result_schema()['properties'] - - for _category, category_data in categorized_schema.items(): - if param_name in category_data['properties']: - return category_data['properties'][param_name] - - raise ValueError(f'Parameter "{param_name}" not found in GEOPHIRES result schema.') - - -def _get_output_parameter_description(param_name: str) -> str: - return _get_output_parameter_schema(param_name)['description'] - - -def _get_unit_display(parameter_units_from_schema: str) -> str: - if parameter_units_from_schema is None: - return '' - - display_unit_prefix = ( - ' ' - if not (parameter_units_from_schema and any(it in parameter_units_from_schema for it in ['%', 'USD', 'MUSD'])) - else '' - ) - display_unit = parameter_units_from_schema - for replacement in [ - ('kilometer', 'km'), - ('degC', '℃'), - ('meter', 'm'), - ('m**3', 'm³'), - ('m**2', 'm²'), - ('MUSD', 'M'), - ('USD', ''), - ]: - display_unit = display_unit.replace(replacement[0], replacement[1]) - - return f'{display_unit_prefix}{display_unit}' - - -def generate_fpc_reservoir_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: - params_to_exclude = [ - 'Maximum Temperature', - 'Reservoir Porosity', - 'Reservoir Volume Option', - ] - - return get_fpc_category_parameters_table_md(input_params, 'Reservoir', params_to_exclude) - - -def generate_fpc_well_bores_parameters_table_md( - input_params: GeophiresInputParameters, result: GeophiresXResult -) -> str: - return get_fpc_category_parameters_table_md( - input_params, - 'Well Bores', - parameters_to_exclude=['Number of Multilateral Sections'], - ) - - -def generate_fpc_surface_plant_parameters_table_md( - input_params: GeophiresInputParameters, result: GeophiresXResult -) -> str: - return get_fpc_category_parameters_table_md( - input_params, - 'Surface Plant', - parameters_to_exclude=['End-Use Option', 'Construction Years'], - ) - - -def generate_fpc_construction_parameters_table_md( - input_params: GeophiresInputParameters, result: GeophiresXResult -) -> str: - input_params_dict = _get_input_parameters_dict( - input_params, include_parameter_comments=True, include_line_comments=True - ) - schedule_param_name = 'Construction CAPEX Schedule' - construction_input_params = {} - for construction_param in ['Construction Years', schedule_param_name]: - construction_input_params[construction_param] = input_params_dict[construction_param] - - # Comment hardcoded here for now because handling of array parameters with comments might be buggy in client or - # web interface... - schedule_param_comment = ( - 'Array of fractions of overnight capital cost expenditure for each year, starting with ' - 'lower costs during initial years for exploration and increasing to higher costs during ' - 'later years as buildout progresses.' - ) - construction_input_params[schedule_param_name] = ( - f'{construction_input_params[schedule_param_name]}' f', -- {schedule_param_comment}' - ) - - return get_fpc_category_parameters_table_md( - ImmutableGeophiresInputParameters(params=construction_input_params), None - ) - - -def generate_fpc_economics_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: - stim_cost_per_well_additional_display_data = f' baseline cost; ${_stim_costs_per_well_musd(result)}M all-in cost' - - drilling_cost_per_well_additional_display_data = ( - f' (Yields all-in cost of ' f'${sig_figs(_drilling_costs_per_well_musd(result),3)}M/well)' - ) - - # Doesn't seem to work as intended... - drilling_cost_per_well_additional_display_data = drilling_cost_per_well_additional_display_data.replace( - ' ', _NON_BREAKING_SPACE - ) - - return get_fpc_category_parameters_table_md( - input_params, - 'Economics', - parameters_to_exclude=[ - 'Ending Electricity Sale Price', - 'Electricity Escalation Start Year', - 'Construction CAPEX Schedule', - 'Time steps per year', - 'Print Output to Console', - ], - additional_display_data_by_param_name={ - 'Reservoir Stimulation Capital Cost per Production Well': stim_cost_per_well_additional_display_data, - 'Reservoir Stimulation Capital Cost per Injection Well': stim_cost_per_well_additional_display_data, - 'Well Drilling and Completion Capital Cost Adjustment Factor': drilling_cost_per_well_additional_display_data, - }, - ) - - -def get_fpc_category_parameters_table_md( - input_params: GeophiresInputParameters, - category_name: str | None, - parameters_to_exclude: list[str] | None = None, - additional_display_data_by_param_name: dict[str, str] | None = None, -) -> str: - if parameters_to_exclude is None: - parameters_to_exclude = [] - - if additional_display_data_by_param_name is None: - additional_display_data_by_param_name = {} - - input_params_dict = _get_input_parameters_dict( - input_params, include_parameter_comments=True, include_line_comments=True - ) - - # noinspection MarkdownIncorrectTableFormatting - table_md = f""" -| Parameter | Input{_NON_BREAKING_SPACE}Value | Comment | -|-------------------|-------------------------------------------|-------------| -""" - - table_entries = [] - for param_name, param_val_comment in input_params_dict.items(): - if param_name.startswith(('#', COMMENT_PARAMETER_NAME_PREFIX)): - continue - - if param_name in parameters_to_exclude: - continue - - category = _get_input_parameter_category(param_name) - if category_name is None or category == category_name: - param_val_comment_split = param_val_comment.split( - # ',', - ',' if _get_input_parameter_schema_type(param_name) != 'array' else ', ', - maxsplit=1, - ) - - param_val = param_val_comment_split[0] - - param_comment = ( - param_val_comment_split[1].replace('-- ', '') if len(param_val_comment_split) > 1 else ' .. N/A ' - ) - param_unit = _get_input_parameter_units(param_name) - if param_unit == 'dimensionless': - param_unit_display = '%' - param_val = sig_figs( - PlainQuantity(float(param_val), 'dimensionless').to('percent').magnitude, - 10, # trim floating point errors - ) - elif param_unit == 'USD/kWh': - price_unit = 'USD/MWh' - param_unit_display = _get_unit_display(price_unit) - param_val = sig_figs( - PlainQuantity(float(param_val), 'USD/kWh').to(price_unit).magnitude, - 10, # trim floating point errors - ) - elif ' ' in param_val: - param_val_split = param_val.split(' ', maxsplit=1) - param_val = param_val_split[0] - param_unit_display = _get_unit_display(param_val_split[1]) - else: - param_unit_display = _get_unit_display(param_unit) - - param_unit_display_prefix = '$' if param_unit and 'USD' in param_unit else '' - - if is_int(param_val): - param_val = int(param_val) - - param_schema = _get_input_parameter_schema(param_name) - if param_schema and 'enum_values' in param_schema: - for enum_value in param_schema['enum_values']: - if enum_value['int_value'] == param_val: - enum_display = enum_value['value'] - # param_val = f'{param_val} ({enum_display})' - param_val = enum_display - break - - param_name_display = param_name.replace(' ', _NON_BREAKING_SPACE, 2) - - additional_display_data = additional_display_data_by_param_name.get(param_name, '') - - table_entries.append( - [ - param_name_display, - f'{param_unit_display_prefix}{param_val}{param_unit_display}{additional_display_data}', - param_comment, - ] - ) - - for table_entry in table_entries: - table_md += f'| {table_entry[0]} | {table_entry[1]} | {table_entry[2]} |\n' - - return table_md.strip() - - -def _q(d: dict[str, Any]) -> PlainQuantity: - return PlainQuantity(d['value'], d['unit']) - - -def get_fpc5_input_parameter_values(input_params: GeophiresInputParameters, result: GeophiresXResult) -> dict[str, Any]: - _log.info('Extracting input parameter values...') - - params = _get_input_parameters_dict(input_params) - r: dict[str, dict[str, Any]] = result.result - - exploration_cost_musd = _q(r['CAPITAL COSTS (M$)']['Exploration costs']).to('MUSD').magnitude - assert exploration_cost_musd == float( - params['Exploration Capital Cost'] - ), 'Exploration cost mismatch between parameters and result' - - return { - 'exploration_cost_musd': round(sig_figs(exploration_cost_musd, 2)), - 'wacc_pct': sig_figs(r['ECONOMIC PARAMETERS']['WACC']['value'], 3), - 'reservoir_volume_m3': f"{r['RESERVOIR PARAMETERS']['Reservoir volume']['value']:,}", - } - - -def get_max_net_generation_mwe(result: GeophiresXResult) -> float: - r: dict[str, dict[str, Any]] = result.result - return _q(r['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Net Electricity Generation']).to('MW').magnitude - - -def get_result_values(result: GeophiresXResult) -> dict[str, Any]: - _log.info('Extracting result values...') - - r: dict[str, dict[str, Any]] = result.result - - econ = r['ECONOMIC PARAMETERS'] - - total_capex_q: PlainQuantity = _q(r['CAPITAL COSTS (M$)']['Total CAPEX']) - - surf_equip_sim = r['SURFACE EQUIPMENT SIMULATION RESULTS'] - min_net_generation_mwe = surf_equip_sim['Minimum Net Electricity Generation']['value'] - avg_net_generation_mwe = surf_equip_sim['Average Net Electricity Generation']['value'] - max_net_generation_mwe = get_max_net_generation_mwe(result) - max_total_generation_mwe = surf_equip_sim['Maximum Total Electricity Generation']['value'] - parasitic_loss_pct = ( - surf_equip_sim['Average Pumping Power']['value'] - / surf_equip_sim['Average Total Electricity Generation']['value'] - * 100.0 - ) - net_power_idx = result.power_generation_profile[0].index('NET POWER (MW)') - - def n_year_avg_net_power_mwe(years: int) -> float: - return np.average([it[net_power_idx] for it in result.power_generation_profile[1:]][:years]) - - two_year_avg_net_power_mwe = n_year_avg_net_power_mwe(2) - two_year_avg_net_power_mwe_per_production_well = two_year_avg_net_power_mwe / _number_of_production_wells(result) - - total_fracture_surface_area_per_well_m2 = _total_fracture_surface_area_per_well_m2(result) - - occ_q = _q(r['CAPITAL COSTS (M$)']['Overnight Capital Cost']) - - field_gathering_cost_musd = _q(r['CAPITAL COSTS (M$)']['Field gathering system costs']).to('MUSD').magnitude - field_gathering_cost_pct_occ = field_gathering_cost_musd / occ_q.to('MUSD').magnitude * 100.0 - - redrills = r['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] - total_wells_including_redrilling = (1 + redrills) * _number_of_wells(result) - - return { - # Economic Results - 'lcoe_usd_per_mwh': sig_figs( - _q(r['SUMMARY OF RESULTS']['Electricity breakeven price']).to('USD / MWh').magnitude, 3 - ), - 'irr_pct': sig_figs(econ['After-tax IRR']['value'], 3), - 'operations_year_of_irr': econ['Project lifetime']['value'], - 'npv_musd': sig_figs(econ['Project NPV']['value'], 3), - 'project_moic': sig_figs(econ['Project MOIC']['value'], 3), - 'project_vir': sig_figs(econ['Project VIR=PI=PIR']['value'], 3), - # Capital Costs - 'drilling_costs_musd': round(sig_figs(_drilling_costs_musd(result), 3)), - 'drilling_costs_per_well_musd': sig_figs(_drilling_costs_per_well_musd(result), 3), - 'stim_costs_musd': round(sig_figs(_stim_costs_musd(result), 3)), - 'stim_costs_per_well_musd': sig_figs(_stim_costs_per_well_musd(result), 3), - 'surface_power_plant_costs_gusd': sig_figs( - _q(r['CAPITAL COSTS (M$)']['Surface power plant costs']).to('GUSD').magnitude, 3 - ), - 'field_gathering_cost_musd': round(sig_figs(field_gathering_cost_musd, 3)), - 'field_gathering_cost_pct_occ': round(sig_figs(field_gathering_cost_pct_occ, 1)), - 'occ_gusd': sig_figs(occ_q.to('GUSD').magnitude, 3), - 'total_capex_gusd': sig_figs(total_capex_q.to('GUSD').magnitude, 3), - 'capex_usd_per_kw': round( - sig_figs((total_capex_q / PlainQuantity(max_net_generation_mwe, 'MW')).to('USD / kW').magnitude, 2) - ), - # Technical & Engineering Results - 'bht_temp_degc': r['RESERVOIR PARAMETERS']['Bottom-hole temperature']['value'], - 'min_net_generation_mwe': round(sig_figs(min_net_generation_mwe, 3)), - 'avg_net_generation_mwe': round(sig_figs(avg_net_generation_mwe, 3)), - 'max_net_generation_mwe': round(sig_figs(max_net_generation_mwe, 3)), - 'max_total_generation_mwe': round(sig_figs(max_total_generation_mwe, 3)), - 'two_year_avg_net_power_mwe_per_production_well': sig_figs(two_year_avg_net_power_mwe_per_production_well, 2), - 'heat_to_power_conversion_efficiency_pct': sig_figs( - _q(surf_equip_sim['Heat to Power Conversion Efficiency']).to('percent').magnitude, 3 - ), - 'parasitic_loss_pct': sig_figs(parasitic_loss_pct, 3), - 'number_of_times_redrilling': redrills, - 'total_wells_including_redrilling': total_wells_including_redrilling, - 'initial_production_temperature_degc': round( - sig_figs(r['RESERVOIR SIMULATION RESULTS']['Initial Production Temperature']['value'], 3) - ), - 'average_production_temperature_degc': round( - sig_figs(r['RESERVOIR SIMULATION RESULTS']['Average Production Temperature']['value'], 3) - ), - 'total_fracture_surface_area_per_well_mm2': sig_figs(total_fracture_surface_area_per_well_m2 / 1e6, 2), - 'total_fracture_surface_area_per_well_mft2': round( - sig_figs( - PlainQuantity(total_fracture_surface_area_per_well_m2, 'm ** 2').to('foot ** 2').magnitude * 1e-6, 2 - ) - ), - # TODO port all input and result values here instead of hardcoding them in the template - } - - -def _number_of_production_wells(result: GeophiresXResult) -> int: - return result.result['SUMMARY OF RESULTS']['Number of production wells']['value'] - - -def _number_of_wells(result: GeophiresXResult) -> int: - r: dict[str, dict[str, Any]] = result.result - - number_of_wells = r['SUMMARY OF RESULTS']['Number of injection wells']['value'] + _number_of_production_wells( - result - ) - - return number_of_wells - - -def _drilling_costs_musd(result: GeophiresXResult) -> float: - r: dict[str, dict[str, Any]] = result.result - - return _q(r['CAPITAL COSTS (M$)']['Drilling and completion costs']).to('MUSD').magnitude - - -def _drilling_costs_per_well_musd(result: GeophiresXResult) -> float: - return _drilling_costs_musd(result) / _number_of_wells(result) - - -def _stim_costs_per_well_musd(result: GeophiresXResult) -> float: - stim_costs_per_well_musd = _stim_costs_musd(result) / _number_of_wells(result) - return stim_costs_per_well_musd - - -def _stim_costs_musd(result: GeophiresXResult) -> float: - r: dict[str, dict[str, Any]] = result.result - - stim_costs_musd = _q(r['CAPITAL COSTS (M$)']['Stimulation costs']).to('MUSD').magnitude - return stim_costs_musd - - -def _total_fracture_surface_area_per_well_m2(result: GeophiresXResult) -> float: - r: dict[str, dict[str, Any]] = result.result - res_params = r['RESERVOIR PARAMETERS'] - return ( - _q(res_params['Fracture area']).to('m ** 2').magnitude - * res_params['Number of fractures']['value'] - / _number_of_wells(result) - ) - - -def generate_res_eng_reference_sim_params_table_md( - base_case_input_params: GeophiresInputParameters, res_eng_reference_sim_params: dict[str, Any] -) -> str: - return get_fpc_category_parameters_table_md( - ImmutableGeophiresInputParameters( - # from_file_path=base_case_input_params.as_file_path(), - params=res_eng_reference_sim_params - ), - None, - ) - - -def generate_fpc_opex_output_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: - table_md = """| Metric | Result Value | Reference Value(s) | Reference Source | -|-----|-----|-----|-----|\n""" - - for output_param_name, result_value_unit_dict in result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'].items(): - if result_value_unit_dict is None: - continue - - unit = result_value_unit_dict['unit'] - value_unit_display = ( - f'${result_value_unit_dict["value"]}M/yr' - if unit == 'MUSD/yr' - else f'{result_value_unit_dict["value"]} {unit}' - ) - - reference_value_display = '.. N/A' - - if output_param_name == 'Total operating and maintenance costs': - reference_source_display = '.. N/A ' - else: - reference_source_display = _get_output_parameter_description(output_param_name) - - if output_param_name == 'Water costs': - water_cost_adjustment_param_name = 'Water Cost Adjustment Factor' - reference_source_display = reference_source_display.split( - f'. Provide {water_cost_adjustment_param_name}', maxsplit=1 - )[0] - water_cost_adjustment_percent = ( - PlainQuantity( - float(_get_input_parameters_dict(input_params)[water_cost_adjustment_param_name]), - 'dimensionless', - ) - .to('percent') - .magnitude - ) - reference_source_display = ( - f'{reference_source_display}. ' - f'The default correlation is adjusted by the {water_cost_adjustment_param_name} parameter value ' - f'of {water_cost_adjustment_percent:.0f}%.' - ) - - if reference_source_display.startswith(('O&M', 'Total O&M')): - reference_source_display = reference_source_display.split('. ', maxsplit=1)[1] - - for suffix in ('s', ''): - reference_source_display = reference_source_display.replace(f'O&M cost{suffix}', 'OPEX') - - table_md += ( - f'| {output_param_name} | {value_unit_display} | {reference_value_display} | {reference_source_display} |\n' - ) - - if output_param_name == 'Total operating and maintenance costs': - opex_usd_per_kw_per_year = ( - _q(result_value_unit_dict) / PlainQuantity(get_max_net_generation_mwe(result), 'MW') - ).to('USD / year / kilowatt') - - reference_source = '2024b ATB: 2028 Deep EGS Binary Conservative Scenario (NREL, 2025). ' - # TODO explain why we're higher than ATB (e.g. redrilling not modeled by ATB) - - table_md += f'| {output_param_name}: $/kW-yr | ${opex_usd_per_kw_per_year.magnitude:.2f}/kW-yr | $226.31/kW-yr | {reference_source} |\n' - - return table_md - - -def generate_fervo_project_cape_5_md( - input_params: GeophiresInputParameters, - result: GeophiresXResult, - res_eng_reference_sim_params: dict[str, Any] | None = None, - project_root: Path = _PROJECT_ROOT, -) -> None: - if res_eng_reference_sim_params is None: - res_eng_reference_sim_params = {} - - result_values: dict[str, Any] = get_result_values(result) - - # noinspection PyDictCreation - template_values = {**get_fpc5_input_parameter_values(input_params, result), **result_values} - - for template_key, md_method in { - 'opex_result_outputs_table_md': generate_fpc_opex_output_table_md, - 'reservoir_parameters_table_md': generate_fpc_reservoir_parameters_table_md, - 'surface_plant_parameters_table_md': generate_fpc_surface_plant_parameters_table_md, - 'well_bores_parameters_table_md': generate_fpc_well_bores_parameters_table_md, - 'economics_parameters_table_md': generate_fpc_economics_parameters_table_md, - 'construction_parameters_table_md': generate_fpc_construction_parameters_table_md, - }.items(): - template_values[template_key] = md_method(input_params, result) - - template_values['reservoir_engineering_reference_simulation_params_table_md'] = ( - generate_res_eng_reference_sim_params_table_md(input_params, res_eng_reference_sim_params) - ) - - docs_dir = project_root / 'docs' - - # Set up Jinja environment - env = Environment(loader=FileSystemLoader(docs_dir), autoescape=True) - template = env.get_template('Fervo_Project_Cape-5.md.jinja') - - # Render template - _log.info('Rendering template...') - output = template.render(**template_values) - - # Write output - output_file = docs_dir / 'Fervo_Project_Cape-5.md' - output_file.write_text(output, encoding='utf-8') - - _log.info(f'✓ Generated {output_file}') - _log.info('\nKey results:') - _log.info(f"\tLCOE: ${template_values['lcoe_usd_per_mwh']}/MWh") - _log.info(f"\tIRR: {template_values['irr_pct']}%") - _log.info(f"\tTotal CAPEX: ${template_values['total_capex_gusd']}B") - - -def main(project_root: Path | None = None): - """ - Generate Fervo_Project_Cape-5.md (markdown documentation) from the Jinja template. - """ - global _current_project_root - - if project_root is None: - project_root = _get_project_root() - - _current_project_root = project_root - - input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( - from_file_path=_get_fpc5_input_file_path(project_root) - ) - result = GeophiresXResult(_get_fpc5_result_file_path(project_root)) - generate_fervo_project_cape_5_md(input_params, result, project_root=project_root) - - -if __name__ == '__main__': - main() diff --git a/src/geophires_docs/generate_fervo_project_cape_5_docs.py b/src/geophires_docs/generate_fervo_project_cape_7_docs.py similarity index 61% rename from src/geophires_docs/generate_fervo_project_cape_5_docs.py rename to src/geophires_docs/generate_fervo_project_cape_7_docs.py index 1c9f0fcb3..98729c90a 100644 --- a/src/geophires_docs/generate_fervo_project_cape_5_docs.py +++ b/src/geophires_docs/generate_fervo_project_cape_7_docs.py @@ -2,11 +2,11 @@ from typing import Any -from geophires_docs import _FPC5_INPUT_FILE_PATH -from geophires_docs import _FPC5_RESULT_FILE_PATH +from geophires_docs import _FPC7_INPUT_FILE_PATH +from geophires_docs import _FPC7_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_docs import generate_fervo_project_cape_7_md +from geophires_docs.generate_fervo_project_cape_7_graphs import generate_fervo_project_cape_7_graphs from geophires_x_client import GeophiresInputParameters from geophires_x_client import GeophiresXClient from geophires_x_client import GeophiresXResult @@ -20,6 +20,13 @@ '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, + 'Reservoir Depth': '2.68, -- Approximate average depth between the upper and lower benches of the Phase I design ' + 'simulated by Singh et al. The case study base case is deeper, corresponding to the Fervo 3.0 well design.', + 'Nonvertical Length per Multilateral Section': '5000 feet, -- Phase I lateral length (BLM, 2024). ' + 'The case study base case uses the longer lateral of the Fervo 3.0 well design.', + 'Number of Fractures per Stimulated Well': '150, -- 12 stages for the 5,000 ft Phase I lateral at the case study ' + 'stage length, clusters per stage, and stimulation success rate. ' + 'The case study base case scales the stage count to the longer 3.0 design lateral.', } @@ -39,21 +46,21 @@ def get_singh_et_al_base_simulation_result(base_input_params: GeophiresInputPara return singh_et_al_base_simulation_input_params, singh_et_al_base_simulation_result -def generate_fervo_project_cape_5_docs(): +def generate_fervo_project_cape_7_docs(): input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( - from_file_path=_FPC5_INPUT_FILE_PATH + from_file_path=_FPC7_INPUT_FILE_PATH ) - result = GeophiresXResult(_FPC5_RESULT_FILE_PATH) + result = GeophiresXResult(_FPC7_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( + generate_fervo_project_cape_7_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( + generate_fervo_project_cape_7_md.generate_fervo_project_cape_7_md( input_params, result, _SINGH_ET_AL_BASE_SIMULATION_PARAMETERS @@ -61,4 +68,4 @@ def generate_fervo_project_cape_5_docs(): if __name__ == '__main__': - generate_fervo_project_cape_5_docs() + generate_fervo_project_cape_7_docs() diff --git a/src/geophires_docs/generate_fervo_project_cape_5_graphs.py b/src/geophires_docs/generate_fervo_project_cape_7_graphs.py similarity index 69% rename from src/geophires_docs/generate_fervo_project_cape_5_graphs.py rename to src/geophires_docs/generate_fervo_project_cape_7_graphs.py index d8564eba5..96b851907 100644 --- a/src/geophires_docs/generate_fervo_project_cape_5_graphs.py +++ b/src/geophires_docs/generate_fervo_project_cape_7_graphs.py @@ -1,19 +1,25 @@ from __future__ import annotations from math import ceil +from math import floor from pathlib import Path from typing import Any import numpy as np from matplotlib import pyplot as plt -from geophires_docs import _FPC5_INPUT_FILE_PATH -from geophires_docs import _FPC5_RESULT_FILE_PATH +from geophires_docs import _FPC7_INPUT_FILE_PATH +from geophires_docs import _FPC7_ORC_UNIT_GROSS_CAPACITY_MW +from geophires_docs import _FPC7_PPA_MINIMUM_NET_GENERATION_MW +from geophires_docs import _FPC7_RESULT_FILE_PATH from geophires_docs import _PROJECT_ROOT +from geophires_docs import _get_fpc7_orc_unit_count from geophires_docs import _get_full_production_temperature_profile from geophires_docs import _get_full_profile from geophires_docs import _get_input_parameters_dict from geophires_docs import _get_logger +from geophires_docs.fervo_project_cape_7_scenarios import FlowRateParametricRow +from geophires_docs.fervo_project_cape_7_scenarios import load_fpc7_flow_rate_parametric from geophires_x_client import GeophiresInputParameters from geophires_x_client import GeophiresXResult from geophires_x_client import ImmutableGeophiresInputParameters @@ -84,7 +90,7 @@ 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', + filename: str = 'fervo_project_cape-7-power-production.png', ) -> str: """ Generate a graph of time vs net power production and save it to the output directory. @@ -116,20 +122,35 @@ def generate_power_production_graph( ax.set_ylabel('Power Production (MW)', fontsize=12) ax.set_title('Power Production Over Project Lifetime', fontsize=14) + ppa_minimum_mw = _FPC7_PPA_MINIMUM_NET_GENERATION_MW + orc_unit_count = _get_fpc7_orc_unit_count(float(total_power.max())) + nameplate_mw = orc_unit_count * _FPC7_ORC_UNIT_GROSS_CAPACITY_MW + # Set axis limits ax.set_xlim(years.min(), years.max()) - ax.set_ylim(480, 630) + ax.set_ylim( + floor((min(float(net_power.min()), ppa_minimum_mw) - 20) / 10) * 10, + ceil((max(float(total_power.max()), nameplate_mw) + 30) / 10) * 10, + ) # 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=ppa_minimum_mw, color='#e69500', linestyle='--', linewidth=1.5, alpha=0.8) + ax.text( + hline_x, + ppa_minimum_mw - 2, + '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.axhline(y=nameplate_mw, color='#33a02c', linestyle='--', linewidth=1.5, alpha=0.8) ax.text( hline_x, - 602, - 'Nameplate capacity (combined capacity of individual ORCs)', + nameplate_mw + 2, + f'Nameplate capacity ({orc_unit_count}×{_FPC7_ORC_UNIT_GROSS_CAPACITY_MW:.0f} MWe ORC units)', ha='left', va='bottom', fontsize=9, @@ -157,7 +178,7 @@ def generate_power_production_graph( 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', + filename: str = 'fervo_project_cape-7-production-temperature.png', ) -> str: """ Generate a graph of time vs production temperature with a horizontal line @@ -198,7 +219,10 @@ def generate_production_temperature_and_drawdown_graph( ax.set_xlabel(_YOE_LABEL, fontsize=12) ax.set_ylabel('Production Temperature (°C)', fontsize=12) ax.set_xlim(years.min(), years.max()) - ax.set_ylim(200, 205) + ax.set_ylim( + floor(min(float(temperatures_celsius.min()), max_drawdown_temp) - 1), + ceil(float(temperatures_celsius.max()) + 1), + ) # Enable minor ticks on x-axis ax.minorticks_on() @@ -210,12 +234,13 @@ def generate_production_temperature_and_drawdown_graph( 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: + y_min, y_max = ax.get_ylim() ax.text( redrill_year + 0.3, - ax.get_ylim()[0] + 0.75, + y_min + 0.05 * (y_max - y_min), f'Redrilling Events (n={len(redrilling_years)})', ha='left', - va='top', + va='bottom', fontsize=9, color=COLOR_REDRILLING, ) @@ -258,7 +283,7 @@ def generate_production_temperature_and_drawdown_graph( def generate_production_temperature_graph( - result: GeophiresXResult, output_dir: Path, filename: str = 'fervo_project_cape-5-production-temperature.png' + result: GeophiresXResult, output_dir: Path, filename: str = 'fervo_project_cape-7-production-temperature.png' ) -> str: """ Generate a graph of time vs production temperature and save it to the output directory. @@ -315,7 +340,86 @@ def generate_production_temperature_graph( return filename -def generate_fervo_project_cape_5_graphs( +def generate_flow_rate_parametric_graph( + rows: list[FlowRateParametricRow], + base_flow_rate_kg_per_s: float, + output_dir: Path, + filename: str = 'fervo_project_cape-7-sensitivity-analysis-flow-rate.png', +) -> None: + """ + Generate the production flow rate parametric graph, with flow rates that do not meet the PPA minimum net + generation shaded and the base case flow rate marked. + """ + _log.info('Generating flow rate parametric graph...') + + color_mark = '#3366cc' + color_ink_muted = '#555555' + color_grid = '#dddddd' + color_infeasible = '#f2f2f2' + + flow_rates = [it.flow_kg_per_s for it in rows] + panels: list[tuple[str, str, str]] = [ + ('Average Net Electricity Production (MW)', 'avg_net_mw', 'MW'), + ('Number of times redrilling (count)', 'redrills', 'count'), + ('Electricity breakeven price (cents/kWh)', 'lcoe_cents_per_kwh', 'cents/kWh'), + ('After-tax IRR (%)', 'irr_pct', '%'), + ('Project NPV (MUSD)', 'npv_musd', 'MUSD'), + ] + + infeasible_flow_rates = [it.flow_kg_per_s for it in rows if it.min_net_mw < _FPC7_PPA_MINIMUM_NET_GENERATION_MW] + infeasible_max_flow_rate = max(infeasible_flow_rates) if len(infeasible_flow_rates) > 0 else None + + fig, axes = plt.subplots(len(panels), 1, figsize=(7, 13.5)) + for ax, (title, field_name, unit) in zip(axes, panels): + values = [getattr(it, field_name) for it in rows] + if infeasible_max_flow_rate is not None: + ax.axvspan( + min(flow_rates) - 1, infeasible_max_flow_rate + 0.5, color=color_infeasible, zorder=0, linewidth=0 + ) + ax.axvline(base_flow_rate_kg_per_s, color=color_ink_muted, linestyle='--', linewidth=1, zorder=1) + ax.scatter(flow_rates, values, s=36, color=color_mark, zorder=3, linewidths=0) + + ax.set_title(title, loc='left', fontsize=13) + ax.set_ylabel(unit, fontsize=10, fontstyle='italic', color=color_ink_muted) + ax.set_xlabel('Production Flow Rate per Well (kg/s)', fontsize=10, fontstyle='italic', color=color_ink_muted) + ax.set_xlim(min(flow_rates) - 1, max(flow_rates) + 1) + ax.grid(True, color=color_grid, linewidth=0.8, zorder=0) + for spine in ('top', 'right'): + ax.spines[spine].set_visible(False) + ax.tick_params(colors=color_ink_muted, labelsize=9) + + if field_name == 'redrills': + ax.set_ylim(0, max(values) + 1) + ax.set_yticks(range(int(max(values)) + 2)) + + top_ax = axes[0] + y_top = top_ax.get_ylim()[1] + top_ax.text( + base_flow_rate_kg_per_s + 0.6, + y_top, + f'Base case ({base_flow_rate_kg_per_s:g} kg/s)', + fontsize=9, + color=color_ink_muted, + va='top', + ) + if infeasible_max_flow_rate is not None: + top_ax.text( + min(flow_rates) - 0.4, + y_top, + f'Minimum net generation\nbelow {_FPC7_PPA_MINIMUM_NET_GENERATION_MW:g} MW', + fontsize=9, + color=color_ink_muted, + va='top', + ) + + fig.tight_layout(h_pad=2.0) + save_path = output_dir / filename + fig.savefig(save_path, dpi=150) + plt.close(fig) + _log.info(f'Saved {save_path}') + + +def generate_fervo_project_cape_7_graphs( base_case: tuple[GeophiresInputParameters, GeophiresXResult], singh_et_al_base_simulation: tuple[GeophiresInputParameters, GeophiresXResult], output_dir: Path, @@ -325,6 +429,11 @@ def generate_fervo_project_cape_5_graphs( generate_power_production_graph(base_case, output_dir) generate_production_temperature_and_drawdown_graph(base_case, output_dir) + generate_flow_rate_parametric_graph( + load_fpc7_flow_rate_parametric(), + float(_get_input_parameters_dict(base_case[0])['Production Flow Rate per Well']), + output_dir, + ) if singh_et_al_base_simulation is not None: singh_et_al_base_simulation_result: GeophiresXResult = singh_et_al_base_simulation[1] @@ -337,7 +446,7 @@ def generate_fervo_project_cape_5_graphs( generate_production_temperature_graph( singh_et_al_base_simulation_result, output_dir, - filename='singh_et_al_base_simulation-production-temperature.png', + filename='fervo_project_cape-7-singh-et-al-base-simulation-production-temperature.png', ) @@ -345,10 +454,10 @@ def generate_fervo_project_cape_5_graphs( docs_dir = _PROJECT_ROOT / 'docs' images_dir = docs_dir / '_images' - input_params_: GeophiresInputParameters = ImmutableGeophiresInputParameters(from_file_path=_FPC5_INPUT_FILE_PATH) + input_params_: GeophiresInputParameters = ImmutableGeophiresInputParameters(from_file_path=_FPC7_INPUT_FILE_PATH) - result_ = GeophiresXResult(_FPC5_RESULT_FILE_PATH) + result_ = GeophiresXResult(_FPC7_RESULT_FILE_PATH) - generate_fervo_project_cape_5_graphs( + generate_fervo_project_cape_7_graphs( (input_params_, result_), None, images_dir # TODO configure (for local development) ) diff --git a/src/geophires_docs/generate_fervo_project_cape_7_md.py b/src/geophires_docs/generate_fervo_project_cape_7_md.py new file mode 100755 index 000000000..d96f6c5c8 --- /dev/null +++ b/src/geophires_docs/generate_fervo_project_cape_7_md.py @@ -0,0 +1,1578 @@ +#!python +""" +Script to generate Fervo_Project_Cape-7.md from its jinja template. +This ensures the markdown documentation stays in sync with actual GEOPHIRES results. +""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import numpy as np +from jinja2 import Environment +from jinja2 import FileSystemLoader +from jinja2 import select_autoescape +from pint.facets.plain import PlainQuantity + +from geophires_docs import _FPC7_ORC_UNIT_GROSS_CAPACITY_MW +from geophires_docs import _FPC7_PPA_MINIMUM_NET_GENERATION_MW +from geophires_docs import _NON_BREAKING_SPACE +from geophires_docs import _PROJECT_ROOT +from geophires_docs import _get_fpc7_input_file_path +from geophires_docs import _get_fpc7_orc_unit_count +from geophires_docs import _get_fpc7_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_docs.fervo_project_cape_7_scenarios import FlowRateParametricRow +from geophires_docs.fervo_project_cape_7_scenarios import get_fpc7_flow_rate_parametric_summary +from geophires_docs.fervo_project_cape_7_scenarios import get_scenario_results +from geophires_docs.fervo_project_cape_7_scenarios import load_fpc7_flow_rate_parametric +from geophires_x.GeoPHIRESUtils import is_int +from geophires_x.GeoPHIRESUtils import sig_figs +from geophires_x.ParameterUtils import COMMENT_PARAMETER_NAME_PREFIX +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__) + + +def _get_schema(schema_file_name: str) -> dict[str, Any]: + project_root = _current_project_root if _current_project_root is not None else _get_project_root() + schema_file = project_root / 'src/geophires_x_schema_generator' / schema_file_name + with open(schema_file, encoding='utf-8') as f: + return json.loads(f.read()) + + +def _get_geophires_request_schema() -> dict[str, Any]: + return _get_schema('geophires-request.json') + + +def _get_input_parameter_schema(param_name: str) -> dict[str, Any]: + return _get_geophires_request_schema()['properties'][param_name] + + +def _get_input_parameter_schema_type(param_name: str) -> dict[str, Any]: + return _get_input_parameter_schema(param_name)['type'] + + +def _get_input_parameter_category(param_name: str) -> str: + return _get_input_parameter_schema(param_name)['category'] + + +def _get_input_parameter_units(param_name: str) -> str | None: + unit = _get_geophires_request_schema()['properties'][param_name]['units'] + + if unit == '': + return 'dimensionless' + + return unit + + +def _get_geophires_result_schema() -> dict[str, Any]: + return _get_schema('geophires-result.json') + + +def _get_output_parameter_schema(param_name: str) -> dict[str, Any]: + categorized_schema: dict[str, dict[str, Any]] = _get_geophires_result_schema()['properties'] + + for _category, category_data in categorized_schema.items(): + if param_name in category_data['properties']: + return category_data['properties'][param_name] + + raise ValueError(f'Parameter "{param_name}" not found in GEOPHIRES result schema.') + + +def _get_output_parameter_description(param_name: str) -> str: + return _get_output_parameter_schema(param_name)['description'] + + +def _get_unit_display(parameter_units_from_schema: str) -> str: + if parameter_units_from_schema is None: + return '' + + display_unit_prefix = ( + ' ' + if not (parameter_units_from_schema and any(it in parameter_units_from_schema for it in ['%', 'USD', 'MUSD'])) + else '' + ) + display_unit = parameter_units_from_schema + for replacement in [ + ('kilometer', 'km'), + ('degC', '℃'), + ('meter', 'm'), + ('m**3', 'm³'), + ('m**2', 'm²'), + ('MUSD', 'M'), + ('USD', ''), + ]: + display_unit = display_unit.replace(replacement[0], replacement[1]) + + return f'{display_unit_prefix}{display_unit}' + + +def generate_fpc_reservoir_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + params_to_exclude = [ + 'Maximum Temperature', + 'Reservoir Porosity', + 'Reservoir Volume Option', + ] + + return get_fpc_category_parameters_table_md(input_params, 'Reservoir', params_to_exclude) + + +def generate_fpc_well_bores_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + return get_fpc_category_parameters_table_md( + input_params, + 'Well Bores', + parameters_to_exclude=['Number of Multilateral Sections'], + ) + + +def generate_fpc_surface_plant_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + return get_fpc_category_parameters_table_md( + input_params, + 'Surface Plant', + parameters_to_exclude=['End-Use Option', 'Construction Years'], + ) + + +def generate_fpc_construction_parameters_table_md( + input_params: GeophiresInputParameters, result: GeophiresXResult +) -> str: + input_params_dict = _get_input_parameters_dict( + input_params, include_parameter_comments=True, include_line_comments=True + ) + schedule_param_name = 'Construction CAPEX Schedule' + construction_input_params = {} + for construction_param in ['Construction Years', schedule_param_name]: + construction_input_params[construction_param] = input_params_dict[construction_param] + + # Comment hardcoded here for now because handling of array parameters with comments might be buggy in client or + # web interface... + schedule_param_comment = ( + 'Array of fractions of overnight capital cost expenditure for each year, starting with ' + 'lower costs during initial years for exploration and increasing to higher costs during ' + 'later years as buildout progresses.' + ) + construction_input_params[schedule_param_name] = ( + f'{construction_input_params[schedule_param_name]}' f', -- {schedule_param_comment}' + ) + + return get_fpc_category_parameters_table_md( + ImmutableGeophiresInputParameters(params=construction_input_params), None + ) + + +def generate_fpc_economics_parameters_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + stim_cost_per_well_additional_display_data = ( + f' baseline cost; ${_stim_costs_per_well_musd(result):.2f}M/well 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 + ) + + input_params_dict = _get_input_parameters_dict(input_params) + stim_cost_per_well_param_names = [ + 'Reservoir Stimulation Capital Cost per Production Well', + 'Reservoir Stimulation Capital Cost per Injection Well', + ] + additional_display_data_by_param_name = { + 'Well Drilling and Completion Capital Cost Adjustment Factor': drilling_cost_per_well_additional_display_data, + 'Reservoir Stimulation Capital Cost per Fracture Surface Area': stim_cost_per_well_additional_display_data, + # Parameter units are MUSD, but the value is an annual cost + 'Annual License Fees Etc': '/yr', + } + value_display_override_by_param_name = {} + for stim_cost_per_well_param_name in stim_cost_per_well_param_names: + if _is_stimulated_well_sentinel(input_params_dict.get(stim_cost_per_well_param_name)): + value_display_override_by_param_name[stim_cost_per_well_param_name] = ( + 'Stimulated (cost from per-area input)' + ) + elif 'Reservoir Stimulation Capital Cost per Fracture Surface Area' not in input_params_dict: + additional_display_data_by_param_name[stim_cost_per_well_param_name] = ( + stim_cost_per_well_additional_display_data + ) + + 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=additional_display_data_by_param_name, + value_display_override_by_param_name=value_display_override_by_param_name, + ) + + +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, + value_display_override_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 = {} + + if value_display_override_by_param_name is None: + value_display_override_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_PARAMETER_NAME_PREFIX)): + continue + + if param_name in parameters_to_exclude: + continue + + category = _get_input_parameter_category(param_name) + if category_name is None or category == category_name: + param_val_comment_split = param_val_comment.split( + # ',', + ',' if _get_input_parameter_schema_type(param_name) != 'array' else ', ', + maxsplit=1, + ) + + param_val = param_val_comment_split[0] + + param_comment = ( + param_val_comment_split[1].replace('-- ', '') if len(param_val_comment_split) > 1 else ' .. N/A ' + ) + + param_name_display = param_name.replace(' ', _NON_BREAKING_SPACE, 2) + + additional_display_data = additional_display_data_by_param_name.get(param_name, '') + + value_display = value_display_override_by_param_name.get( + param_name, _get_input_parameter_value_display(param_name, param_val) + ) + + table_entries.append( + [ + param_name_display, + f'{value_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_input_parameter_value_display(param_name: str, param_val: Any) -> str: + """ + :param param_val: Input parameter value as it appears in the input file, without comment (may include a unit, e.g. + '7500 feet') + :return: Value with display units, e.g. '$115/MWh', '72%', '3.06 km' + """ + param_unit = _get_input_parameter_units(param_name) + if param_unit == 'dimensionless': + param_unit_display = '%' + param_val = sig_figs( + PlainQuantity(float(param_val), 'dimensionless').to('percent').magnitude, + 10, # trim floating point errors + ) + elif param_unit == 'USD/kWh': + price_unit = 'USD/MWh' + param_unit_display = _get_unit_display(price_unit) + param_val = sig_figs( + PlainQuantity(float(param_val), 'USD/kWh').to(price_unit).magnitude, + 10, # trim floating point errors + ) + elif ' ' in param_val: + param_val_split = param_val.split(' ', maxsplit=1) + param_val = param_val_split[0] + param_unit_display = _get_unit_display(param_val_split[1]) + else: + param_unit_display = _get_unit_display(param_unit) + + param_unit_display_prefix = '$' if param_unit and 'USD' in param_unit else '' + + if is_int(param_val): + param_val = int(param_val) + + param_schema = _get_input_parameter_schema(param_name) + if param_schema and 'enum_values' in param_schema: + for enum_value in param_schema['enum_values']: + if enum_value['int_value'] == param_val: + enum_display = enum_value['value'] + # param_val = f'{param_val} ({enum_display})' + param_val = enum_display + break + + return f'{param_unit_display_prefix}{param_val}{param_unit_display}' + + +def get_fpc7_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' + + starting_ppa_price_usd_per_mwh = ( + PlainQuantity(float(params['Starting Electricity Sale Price']), 'USD/kWh').to('USD/MWh').magnitude + ) + + # Drilling and completion cost (including indirect costs) scales linearly with the adjustment factor. + drilling_cost_adjustment_factor = float(params['Well Drilling and Completion Capital Cost Adjustment Factor']) + drilling_costs_per_well_at_unit_adjustment_factor_musd = ( + _drilling_costs_per_well_musd(result) / drilling_cost_adjustment_factor + ) + + # Yusifov & Enriquez, 2025 + drilling_to_stimulation_cost_ratio = 46.0 / 54.0 + + 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']:,}", + 'starting_ppa_price_usd_per_mwh': round(starting_ppa_price_usd_per_mwh), + 'production_flow_rate_kg_per_s_display': f"{float(params['Production Flow Rate per Well']):g}", + 'drilling_costs_per_well_at_unit_adjustment_factor_musd': sig_figs( + drilling_costs_per_well_at_unit_adjustment_factor_musd, 3 + ), + 'stim_costs_per_well_drilling_ratio_reference_musd': sig_figs( + _drilling_costs_per_well_musd(result) / drilling_to_stimulation_cost_ratio, 3 + ), + } + + +def get_max_net_generation_mwe(result: GeophiresXResult) -> float: + r: dict[str, dict[str, Any]] = result.result + return _q(r['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Net Electricity Generation']).to('MW').magnitude + + +def get_result_values(result: GeophiresXResult) -> dict[str, Any]: + _log.info('Extracting result values...') + + r: dict[str, dict[str, Any]] = result.result + + econ = r['ECONOMIC PARAMETERS'] + + total_capex_q: PlainQuantity = _q(r['CAPITAL COSTS (M$)']['Total CAPEX']) + + surf_equip_sim = r['SURFACE EQUIPMENT SIMULATION RESULTS'] + min_net_generation_mwe = surf_equip_sim['Minimum Net Electricity Generation']['value'] + avg_net_generation_mwe = surf_equip_sim['Average Net Electricity Generation']['value'] + max_net_generation_mwe = get_max_net_generation_mwe(result) + max_total_generation_mwe = surf_equip_sim['Maximum Total Electricity Generation']['value'] + parasitic_loss_pct = ( + surf_equip_sim['Average Pumping Power']['value'] + / surf_equip_sim['Average Total Electricity Generation']['value'] + * 100.0 + ) + net_power_idx = result.power_generation_profile[0].index('NET POWER (MW)') + + def n_year_avg_net_power_mwe(years: int) -> float: + return np.average([it[net_power_idx] for it in result.power_generation_profile[1:]][:years]) + + two_year_avg_net_power_mwe = n_year_avg_net_power_mwe(2) + two_year_avg_net_power_mwe_per_production_well = two_year_avg_net_power_mwe / _number_of_production_wells(result) + + total_fracture_surface_area_per_well_m2 = _total_fracture_surface_area_per_well_m2(result) + + occ_q = _q(r['CAPITAL COSTS (M$)']['Overnight Capital Cost']) + + field_gathering_cost_musd = _q(r['CAPITAL COSTS (M$)']['Field gathering system costs']).to('MUSD').magnitude + field_gathering_cost_pct_occ = field_gathering_cost_musd / occ_q.to('MUSD').magnitude * 100.0 + + redrills = r['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + total_wells_including_redrilling = (1 + redrills) * _number_of_wells(result) + redrilling_years = _get_redrilling_years(result) + if len(redrilling_years) != redrills: + raise ValueError( + f'Redrilling years detected from production temperature profile ({redrilling_years}) do not match ' + f'Number of times redrilling ({redrills}).' + ) + + first_cycle_peak_year, first_cycle_peak_temperature_degc = _get_first_cycle_peak_production_temperature( + result, redrilling_years + ) + initial_production_temperature_degc = r['RESERVOIR SIMULATION RESULTS']['Initial Production Temperature']['value'] + + total_capex_musd = total_capex_q.to('MUSD').magnitude + interconnection_cost_musd = _interconnection_cost_musd(result) + interconnection_share_of_total_capex_musd = _interconnection_share_of_total_capex_musd(result) + + max_net_generation_q = PlainQuantity(max_net_generation_mwe, 'MW') + transmission_cost_musd_per_yr = _annual_license_fees_musd_per_yr(result) + + orc_unit_count = _get_fpc7_orc_unit_count(max_total_generation_mwe) + + min_dscr_year, min_dscr = _get_min_dscr(result) + salvage_value_musd = _get_final_year_salvage_value_musd(result) + reservoir_heat_content_negative_from_year, final_year_pct_total_heat_mined = _get_reservoir_heat_content_values( + result + ) + + return { + # Economic Results + 'lcoe_usd_per_mwh': round( + _q(r['SUMMARY OF RESULTS']['Electricity breakeven price']).to('USD / MWh').magnitude, 1 + ), + 'lppa_usd_per_mwh': round(_get_levelized_ppa_price_usd_per_mwh(result), 1), + 'irr_pct': sig_figs(econ['After-tax IRR']['value'], 3), + 'operations_year_of_irr': econ['Project lifetime']['value'], + 'npv_musd': round(econ['Project NPV']['value'], 1), + 'project_moic': sig_figs(econ['Project MOIC']['value'], 3), + 'project_vir': sig_figs(econ['Project VIR=PI=PIR']['value'], 3), + 'real_discount_rate_pct': f"{econ['Real Discount Rate']['value']:g}", + 'nominal_discount_rate_pct': f"{econ['Nominal Discount Rate']['value']:.1f}", + 'project_lifetime_yr': econ['Project lifetime']['value'], + 'min_dscr': f'{min_dscr:.2f}', + 'min_dscr_year': min_dscr_year, + 'salvage_value_musd': f'{salvage_value_musd:,.0f}', + 'salvage_value_pct_of_total_capex': f'{salvage_value_musd / total_capex_musd * 100.0:.0f}', + # 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(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) + ), + 'exploration_atb_reference_musd': sig_figs(_drilling_costs_per_well_musd(result) * 5, 3), + 'interconnection_cost_musd': round(sig_figs(interconnection_cost_musd, 3)), + 'interconnection_cost_usd_per_kw_ppa_capacity': round( + sig_figs( + ( + PlainQuantity(interconnection_cost_musd, 'MUSD') + / PlainQuantity(_FPC7_PPA_MINIMUM_NET_GENERATION_MW, 'MW') + ) + .to('USD / kW') + .magnitude, + 3, + ) + ), + 'interconnection_share_of_total_capex_musd': round(sig_figs(interconnection_share_of_total_capex_musd, 3)), + 'occ_usd_per_kw': round(sig_figs((occ_q / max_net_generation_q).to('USD / kW').magnitude, 2)), + 'capex_usd_per_kw_excluding_interconnection': round( + sig_figs( + ( + PlainQuantity(total_capex_musd - interconnection_share_of_total_capex_musd, 'MUSD') + / max_net_generation_q + ) + .to('USD / kW') + .magnitude, + 2, + ) + ), + 'surface_power_plant_pct_of_wellfield_and_plant_capex': round( + sig_figs(_surface_power_plant_pct_of_wellfield_and_plant_capex(result), 2) + ), + # Operating Costs + 'transmission_cost_musd_per_yr': f'{sig_figs(transmission_cost_musd_per_yr, 3):g}', + # 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': round(two_year_avg_net_power_mwe_per_production_well, 1), + 'heat_to_power_conversion_efficiency_pct': sig_figs( + _q(surf_equip_sim['Heat to Power Conversion Efficiency']).to('percent').magnitude, 3 + ), + '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 + ) + ), + 'initial_pumping_power_pct_of_net': sig_figs( + surf_equip_sim['Initial pumping power/net installed power']['value'], 3 + ), + 'orc_unit_count': orc_unit_count, + 'orc_unit_gross_capacity_mw': round(_FPC7_ORC_UNIT_GROSS_CAPACITY_MW), + 'nameplate_capacity_mw': round(orc_unit_count * _FPC7_ORC_UNIT_GROSS_CAPACITY_MW), + 'number_of_wells': _number_of_wells(result), + 'redrilling_years': redrilling_years, + 'redrilling_years_display': _get_list_display(redrilling_years), + 'initial_production_temperature_degc_precise': round(initial_production_temperature_degc, 1), + 'first_cycle_peak_year': first_cycle_peak_year, + 'first_cycle_peak_temperature_degc': round(first_cycle_peak_temperature_degc, 1), + 'reservoir_heat_content_negative_from_year': reservoir_heat_content_negative_from_year, + 'final_year_pct_total_heat_mined': f'{final_year_pct_total_heat_mined:.1f}', + # TODO port all input and result values here instead of hardcoding them in the template + } + + +_ITC_EXCLUDING_INTERCONNECTION_SCENARIO = 'ITC excluding interconnection from basis' +_CURTAILMENT_5PCT_SCENARIO = 'Curtailment (5% flat derate)' +_CURTAILMENT_10PCT_SCENARIO = 'Curtailment (10% flat derate)' +_REDUCED_REDRILLING_SCENARIO = 'Reduced redrilling (greater fracture height)' +_REDUCED_REDRILLING_FRACTURE_HEIGHT_MULTIPLIER = 1.2 +_PREVIOUS_VERSION_PPA_TERMS_SCENARIO = 'February 2026 Update PPA terms' +_PPA_TERMS_PARAM_NAMES = ( + 'Starting Electricity Sale Price', + 'Electricity Escalation Rate Per Year', + 'Ending Electricity Sale Price', + 'Electricity Escalation Start Year', +) + + +def get_fpc7_scenario_values( + input_params: GeophiresInputParameters, + result: GeophiresXResult, + scenario_results: dict[str, GeophiresXResult] | None = None, + flow_rate_parametric_rows: list[FlowRateParametricRow] | None = None, + previous_input_params: GeophiresInputParameters | None = None, +) -> dict[str, Any]: + """ + :param scenario_results: Results of the scenarios returned by get_fpc7_scenario_input_parameters, by scenario + name; simulated if not provided. + :param flow_rate_parametric_rows: Production flow rate parametric results; loaded from + FPC7_FLOW_RATE_PARAMETRIC_CSV_PATH if not provided. + :param previous_input_params: Input parameters of the previous version of the case study; see + get_fpc7_scenario_input_parameters. + :return: Template values for scenario results cited in the documentation narrative + :raises ValueError: if the reduced redrilling scenario or the previous version PPA terms scenario no longer + supports its description in the documentation + """ + scenario_input_params = get_fpc7_scenario_input_parameters(input_params, result, previous_input_params) + if scenario_results is None: + scenario_results = get_scenario_results(input_params, scenario_input_params) + + base_irr_pct = result.result['ECONOMIC PARAMETERS']['After-tax IRR']['value'] + scenario_irr_changes_pct_pts = { + name: scenario_result.result['ECONOMIC PARAMETERS']['After-tax IRR']['value'] - base_irr_pct + for name, scenario_result in scenario_results.items() + } + + if flow_rate_parametric_rows is None: + flow_rate_parametric_rows = load_fpc7_flow_rate_parametric() + + params = _get_input_parameters_dict(input_params) + flow_rate_summary = get_fpc7_flow_rate_parametric_summary( + flow_rate_parametric_rows, + float(params['Production Flow Rate per Well']), + _FPC7_PPA_MINIMUM_NET_GENERATION_MW, + ) + + def _irr_reduction_display(scenario_name: str) -> str: + return f'{-scenario_irr_changes_pct_pts[scenario_name]:.1f}' + + itc_rate_excluding_interconnection = scenario_input_params[_ITC_EXCLUDING_INTERCONNECTION_SCENARIO][ + 'Investment Tax Credit Rate' + ] + + return { + **_get_reduced_redrilling_scenario_values( + params, result, scenario_input_params, scenario_results[_REDUCED_REDRILLING_SCENARIO] + ), + **_get_previous_version_ppa_terms_scenario_values( + result, scenario_results[_PREVIOUS_VERSION_PPA_TERMS_SCENARIO] + ), + 'itc_rate_excluding_interconnection_pct': f'{itc_rate_excluding_interconnection * 100:.2f}', + 'itc_rate_excluding_interconnection_pct_1dp': f'{itc_rate_excluding_interconnection * 100:.1f}', + 'itc_excluding_interconnection_irr_reduction_pct_pts': _irr_reduction_display( + _ITC_EXCLUDING_INTERCONNECTION_SCENARIO + ), + 'curtailment_5pct_utilization_factor': scenario_input_params[_CURTAILMENT_5PCT_SCENARIO]['Utilization Factor'], + 'curtailment_10pct_utilization_factor': scenario_input_params[_CURTAILMENT_10PCT_SCENARIO][ + 'Utilization Factor' + ], + 'curtailment_5pct_irr_reduction_pct_pts': _irr_reduction_display(_CURTAILMENT_5PCT_SCENARIO), + 'curtailment_10pct_irr_reduction_pct_pts': _irr_reduction_display(_CURTAILMENT_10PCT_SCENARIO), + 'number_of_production_wells': _number_of_production_wells(result), + 'ppa_minimum_net_generation_mw': f'{_FPC7_PPA_MINIMUM_NET_GENERATION_MW:g}', + 'flow_rate_parametric_redrilling_steps_display': _get_list_display( + [ + f'from {_get_count_word(it.redrills_below)} to {_get_count_word(it.redrills_at)} between ' + f'{it.flow_below_kg_per_s:g} and {it.flow_at_kg_per_s:g} kg/s per well' + for it in flow_rate_summary.redrilling_steps + ] + ), + 'flow_rate_parametric_base_redrills_word': _get_count_word(flow_rate_summary.base_redrills), + 'flow_rate_parametric_max_irr_change_above_base_pct_pts': ( + f'{flow_rate_summary.max_irr_change_above_base_within_band_pct_pts:.2f}' + ), + 'flow_rate_parametric_minimum_ppa_feasible_kg_per_s': ( + f'{flow_rate_summary.minimum_ppa_feasible_flow_rate_kg_per_s:g}' + ), + } + + +def get_fpc7_scenario_input_parameters( + input_params: GeophiresInputParameters, + result: GeophiresXResult, + previous_input_params: GeophiresInputParameters | None = None, +) -> dict[str, dict[str, Any]]: + """ + :param previous_input_params: Input parameters of the previous version of the case study, from which the previous + version PPA terms scenario takes its PPA parameters. If not provided, it is loaded from the previous version's + example in tests/examples relative to the package location, which exists only in a source checkout (not when + geophires_docs is installed as a package). + :return: Input parameter overrides for the scenarios cited in the documentation narrative, by scenario name. The + ITC scenario applies the rate to total installed cost that removes the interconnection cost (including its + share of inflation and interest during construction) from the ITC basis. The curtailment scenarios reduce the + utilization factor by 5% and 10% as flat derates. Values are rounded as in the sensitivity analysis. The + reduced redrilling scenario increases fracture height, and with it fracture surface area and stimulation cost + per stimulated well, to extend the thermal plateau. The previous version PPA terms scenario applies the + previous version's PPA starting price, escalation rate, ending price (cap), and escalation start year. + """ + if previous_input_params is None: + previous_input_params = _get_fpc7_previous_version(_PROJECT_ROOT)[0] + + params = _get_input_parameters_dict(input_params) + itc_rate = float(params['Investment Tax Credit Rate']) + total_capex_musd = _q(result.result['CAPITAL COSTS (M$)']['Total CAPEX']).to('MUSD').magnitude + itc_rate_excluding_interconnection = itc_rate * ( + 1.0 - _interconnection_share_of_total_capex_musd(result) / total_capex_musd + ) + utilization_factor = float(params['Utilization Factor']) + + return { + _ITC_EXCLUDING_INTERCONNECTION_SCENARIO: { + 'Investment Tax Credit Rate': round(itc_rate_excluding_interconnection, 4) + }, + _CURTAILMENT_5PCT_SCENARIO: {'Utilization Factor': round(utilization_factor * 0.95, 3)}, + _CURTAILMENT_10PCT_SCENARIO: {'Utilization Factor': round(utilization_factor * 0.90, 3)}, + _REDUCED_REDRILLING_SCENARIO: { + 'Fracture Height': round( + float(params['Fracture Height']) * _REDUCED_REDRILLING_FRACTURE_HEIGHT_MULTIPLIER, 1 + ) + }, + _PREVIOUS_VERSION_PPA_TERMS_SCENARIO: _get_ppa_terms_input_parameters(previous_input_params), + } + + +def _get_ppa_terms_input_parameters(input_params: GeophiresInputParameters) -> dict[str, int | float]: + """ + :raises ValueError: if the input parameters do not set all PPA terms explicitly, in which case the scenario would + inherit the current version's value for the missing term rather than the (default) value it had + """ + params = _get_input_parameters_dict(input_params) + missing_param_names = [it for it in _PPA_TERMS_PARAM_NAMES if it not in params] + if len(missing_param_names) > 0: + raise ValueError(f'PPA terms not set in previous version input parameters: {missing_param_names}') + + return {it: int(float(params[it])) if is_int(params[it]) else float(params[it]) for it in _PPA_TERMS_PARAM_NAMES} + + +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 _is_stimulated_well_sentinel(param_value: str | None) -> bool: + """ + GEOPHIRES interprets -1 for Reservoir Stimulation Capital Cost per Production Well (or Injection Well) as an + indication that the wells are stimulated, with cost apportioned from the per-fracture-surface-area input. + """ + if param_value is None: + return False + + try: + return float(str(param_value).split(',')[0].strip()) == -1 + except ValueError: + return False + + +def _interconnection_cost_musd(result: GeophiresXResult) -> float: + """ + The case study enters grid interconnection cost as One-time Flat License Fees Etc. + """ + flat_fees = result.result['CAPITAL COSTS (M$)'].get('One-time Flat License Fees Etc') + if flat_fees is None: + return 0.0 + + return _q(flat_fees).to('MUSD').magnitude + + +def _annual_license_fees_musd_per_yr(result: GeophiresXResult) -> float: + """ + The case study enters firm transmission service cost as Annual License Fees Etc. + Note the value is output with MUSD (rather than MUSD/yr) units, matching the input parameter units. + """ + annual_fees = result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'].get('Annual License Fees Etc') + if annual_fees is None: + return 0.0 + + return float(annual_fees['value']) + + +def _interconnection_share_of_total_capex_musd(result: GeophiresXResult) -> float: + """ + Interconnection cost is spent on the construction CAPEX schedule along with the rest of overnight capital cost, + so its share of inflation and interest during construction is proportional to its share of overnight capital. + """ + capex = result.result['CAPITAL COSTS (M$)'] + total_capex_musd = _q(capex['Total CAPEX']).to('MUSD').magnitude + occ_musd = _q(capex['Overnight Capital Cost']).to('MUSD').magnitude + return total_capex_musd * _interconnection_cost_musd(result) / occ_musd + + +def _surface_power_plant_pct_of_wellfield_and_plant_capex(result: GeophiresXResult) -> float: + """ + :return: Surface power plant cost as a percentage of drilling, completion, stimulation, field gathering and + surface power plant costs (i.e. excluding exploration and interconnection) + """ + capex = result.result['CAPITAL COSTS (M$)'] + surface_plant_musd = _q(capex['Surface power plant costs']).to('MUSD').magnitude + wellfield_musd = ( + _drilling_costs_musd(result) + + _stim_costs_musd(result) + + _q(capex['Field gathering system costs']).to('MUSD').magnitude + ) + return surface_plant_musd / (surface_plant_musd + wellfield_musd) * 100.0 + + +def _get_levelized_ppa_price_usd_per_mwh(result: GeophiresXResult) -> float: + lppa_row_name = 'LPPA Levelized PPA price nominal (cents/kWh)' + for row in result.result.get('SAM CASH FLOW PROFILE') or []: + if row and row[0] == lppa_row_name: + cents_per_kwh_to_usd_per_mwh = 10.0 + return float(row[1]) * cents_per_kwh_to_usd_per_mwh + + raise ValueError(f'{lppa_row_name} not found in SAM cash flow profile.') + + +def _get_sam_cash_flow_operating_year_values(result: GeophiresXResult, row_name: str) -> dict[int, float]: + """ + :return: Values of the first SAM cash flow profile row with the given name, by operating year (1-based); + construction years and year 0 are excluded. + """ + cash_flow = result.result.get('SAM CASH FLOW PROFILE') or [] + if len(cash_flow) == 0: + raise ValueError('SAM cash flow profile not found in result.') + + year_headers = cash_flow[0][1:] + for row in cash_flow[1:]: + if row and row[0] == row_name: + values_by_year = {} + for year_header, value in zip(year_headers, row[1:]): + year = int(str(year_header).replace('Year', '', 1).strip()) + if year >= 1: + values_by_year[year] = float(value) + + return values_by_year + + raise ValueError(f'{row_name} not found in SAM cash flow profile.') + + +def _get_min_dscr(result: GeophiresXResult) -> tuple[int, float]: + """ + :return: Operating year and value of the minimum pre-tax debt service coverage ratio + """ + dscr_by_year = _get_sam_cash_flow_operating_year_values(result, 'DSCR (pre-tax)') + min_dscr_year = min(dscr_by_year, key=lambda year: dscr_by_year[year]) + return min_dscr_year, dscr_by_year[min_dscr_year] + + +def _get_final_year_salvage_value_musd(result: GeophiresXResult) -> float: + salvage_by_year = _get_sam_cash_flow_operating_year_values(result, 'Salvage value ($)') + return salvage_by_year[max(salvage_by_year)] / 1e6 + + +def _get_reservoir_heat_content_values(result: GeophiresXResult) -> tuple[int | None, float]: + """ + GEOPHIRES computes remaining reservoir heat content as initial heat content minus cumulative heat extracted, which + redrilling does not reset. + + :return: First operating year in which the remaining reservoir heat content in the annual profile is negative (None + if it is not negative in any year), and the percentage of total heat mined in the final year + """ + profile = result.heat_electricity_extraction_generation_profile + year_idx = profile[0].index('YEAR') + heat_content_idx = profile[0].index('RESERVOIR HEAT CONTENT (10^15 J)') + pct_mined_idx = profile[0].index('PERCENTAGE OF TOTAL HEAT MINED (%)') + negative_heat_content_years = [int(row[year_idx]) for row in profile[1:] if float(row[heat_content_idx]) < 0] + first_negative_year = negative_heat_content_years[0] if len(negative_heat_content_years) > 0 else None + return first_negative_year, float(profile[-1][pct_mined_idx]) + + +def _get_annual_production_temperature_profile_degc(result: GeophiresXResult) -> tuple[list[int], list[float]]: + profile = result.power_generation_profile + year_idx = profile[0].index('YEAR') + temp_idx = profile[0].index('GEOFLUID TEMPERATURE (degC)') + return [int(row[year_idx]) for row in profile[1:]], [float(row[temp_idx]) for row in profile[1:]] + + +def _get_redrilling_years(result: GeophiresXResult) -> list[int]: + """ + Redrilling restores production temperature, so the operating year in which a redrilling event occurs appears as a + local minimum in the annual average production temperature profile. + See also generate_fervo_project_cape_7_graphs._get_redrilling_event_indexes, which detects redrilling events at + time step resolution from the full profile. + """ + years, temps = _get_annual_production_temperature_profile_degc(result) + return [years[i] for i in range(1, len(temps) - 1) if temps[i - 1] > temps[i] < temps[i + 1]] + + +def _get_first_cycle_peak_production_temperature( + result: GeophiresXResult, redrilling_years: list[int] +) -> tuple[int, float]: + """ + :return: Year and value of the peak annual production temperature prior to the first redrilling event + """ + years, temps = _get_annual_production_temperature_profile_degc(result) + first_cycle_end_idx = years.index(redrilling_years[0]) if len(redrilling_years) > 0 else len(years) + first_cycle_temps = temps[:first_cycle_end_idx] + peak_idx = int(np.argmax(first_cycle_temps)) + return years[peak_idx], first_cycle_temps[peak_idx] + + +def _get_reduced_redrilling_scenario_values( + params: dict[str, Any], + result: GeophiresXResult, + scenario_input_params: dict[str, dict[str, Any]], + scenario_result: GeophiresXResult, +) -> dict[str, Any]: + base_redrills = int(result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value']) + redrills = int(scenario_result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value']) + min_net_generation_mw = scenario_result.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Minimum Net Electricity Generation' + ]['value'] + if redrills >= base_redrills or min_net_generation_mw < _FPC7_PPA_MINIMUM_NET_GENERATION_MW: + raise ValueError( + f'The reduced redrilling scenario yields {redrills} redrilling events (base case: {base_redrills}) and ' + f'minimum net generation of {min_net_generation_mw} MW; update the scenario or its description in the ' + f'case study documentation.' + ) + + redrilling_years = _get_redrilling_years(scenario_result) + if len(redrilling_years) != redrills: + raise ValueError( + f'Reduced redrilling scenario redrilling years detected from production temperature profile ' + f'({redrilling_years}) do not match Number of times redrilling ({redrills}).' + ) + + base_fracture_height_m = float(params['Fracture Height']) + fracture_height_m = scenario_input_params[_REDUCED_REDRILLING_SCENARIO]['Fracture Height'] + comparison_metric_labels = [ + 'Fracture surface area per well (10⁶ m²)', + 'Redrilling events', + 'Total wells over project lifetime', + 'Stimulation ($M)', + 'Total CAPEX ($M)', + 'Redrilling ($M/yr)', + 'Minimum net generation (MW)', + 'LCOE ($/MWh)', + 'After-tax IRR (%)', + 'Project NPV ($M)', + ] + metrics_by_label = {it[0]: it for it in _FPC7_VERSION_COMPARISON_RESULT_METRICS} + + return { + 'reduced_redrilling_base_fracture_height_m': f'{base_fracture_height_m:g}', + 'reduced_redrilling_fracture_height_m': f'{fracture_height_m:g}', + 'reduced_redrilling_fracture_height_increase_pct': ( + f'{(fracture_height_m / base_fracture_height_m - 1.0) * 100.0:.0f}' + ), + 'reduced_redrilling_base_redrills_word': _get_count_word(base_redrills), + 'reduced_redrilling_redrills_word': _get_count_word(redrills), + 'reduced_redrilling_base_redrilling_years_display': _get_years_display(_get_redrilling_years(result)), + 'reduced_redrilling_redrilling_years_display': _get_years_display(redrilling_years), + 'reduced_redrilling_comparison_table_md': _get_result_comparison_table_md( + result, + scenario_result, + 'Base Case', + 'Reduced Redrilling', + [metrics_by_label[it] for it in comparison_metric_labels], + ), + } + + +def _get_previous_version_ppa_terms_scenario_values( + result: GeophiresXResult, scenario_result: GeophiresXResult +) -> dict[str, Any]: + """ + :raises ValueError: if the scenario no longer supports its description in the documentation, which states that + the previous version's (lower) PPA terms reduce the IRR and the LCOE + """ + base_irr_pct = result.result['ECONOMIC PARAMETERS']['After-tax IRR']['value'] + econ = scenario_result.result['ECONOMIC PARAMETERS'] + irr_pct = econ['After-tax IRR']['value'] + npv_musd = econ['Project NPV']['value'] + base_lcoe_usd_per_mwh = _lcoe_usd_per_mwh(result) + lcoe_usd_per_mwh = _lcoe_usd_per_mwh(scenario_result) + if irr_pct >= base_irr_pct or lcoe_usd_per_mwh >= base_lcoe_usd_per_mwh: + raise ValueError( + f'The {_PREVIOUS_VERSION_PPA_TERMS_SCENARIO} scenario yields an IRR of {irr_pct}% (base case: ' + f'{base_irr_pct}%) and LCOE of ${lcoe_usd_per_mwh:.1f}/MWh (base case: ${base_lcoe_usd_per_mwh:.1f}/MWh); ' + f'update its description in the case study documentation.' + ) + + return { + 'previous_version_ppa_terms_irr_pct': f'{irr_pct:.1f}', + 'previous_version_ppa_terms_npv_display': _get_signed_musd_display(npv_musd), + 'previous_version_ppa_terms_lcoe_usd_per_mwh': f'{lcoe_usd_per_mwh:.1f}', + } + + +def _get_signed_musd_display(value_musd: float) -> str: + rounded_musd = round(value_musd) + return f'-${abs(rounded_musd):,}M' if rounded_musd < 0 else f'${rounded_musd:,}M' + + +def _get_years_display(years: list[int]) -> str: + return f'year{"" if len(years) == 1 else "s"} {_get_list_display(years)}' + + +def _get_count_word(count: int) -> str: + words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'] + return words[count] if 0 <= count < len(words) else str(count) + + +def _get_list_display(items: list[Any]) -> str: + items_str = [str(it) for it in items] + if len(items_str) <= 2: + return ' and '.join(items_str) + + return f'{", ".join(items_str[:-1])}, and {items_str[-1]}' + + +# Previous version of the case study (the February 2026 Update, last updated 2026-07-03), compared with this version in +# the Previous Versions section. +_FPC7_PREVIOUS_VERSION_EXAMPLE_NAME = 'Fervo_Project_Cape-5' + +_FPC7_PREVIOUS_VERSION_LABEL = 'February 2026 Update' +_FPC7_CURRENT_VERSION_LABEL = 'September 2026 Update' + +# Rationale for each input parameter whose value differs from the previous version, shown in the Previous Versions +# input changes table. See the input file comments for full details and citations. +_FPC7_PREVIOUS_VERSION_INPUT_CHANGE_RATIONALE_BY_PARAM_NAME: dict[str, str] = { + 'Inflation Rate': ( + 'Updated for 2026 inflation: US CPI-U was 3.4% year over year in July 2026, with core at 2.5% (BLS, 2026b). ' + 'The February 2026 Update used December 2025 inflation.' + ), + 'Starting Electricity Sale Price': ( + 'Midpoint of the $100–130/MWh range Fervo reports for contracts under negotiation (Fervo Energy, 2026f), ' + 'replacing 2024b ATB Geysers–Sacramento pricing.' + ), + 'Electricity Escalation Rate Per Year': ( + 'Linear equivalent of a 1.5% per year compounding escalator on the new starting price (CTVC, 2025), ' + 'replacing an escalator calibrated to reach $100/MWh in project year 11.' + ), + 'Ending Electricity Sale Price': ( + 'Caps escalation at the operating year 15 price, holding the price flat after a 15-year PPA term. ' + 'The February 2026 Update had no effective cap.' + ), + 'Electricity Escalation Start Year': ( + 'First escalation step in the second operating year, matching a PPA that escalates from the first ' + 'anniversary of commercial operation.' + ), + 'Construction Years': ( + "A SOAK developer is modeled with one fewer year than the 5-year FOAK timeline, informed by Fervo's Phase I " + 'build pace and rig capacity (Fervo Energy, 2026e; 2026f).' + ), + 'Construction CAPEX Schedule': ( + 'The first two years of the 5-year DOE-ATB hybrid schedule are merged to match the 4-year construction period.' + ), + 'Exploration Capital Cost': ( + 'Re-derived from the 2024b ATB exploration assumption of 5 full-size wells at the new per-well drilling cost.' + ), + 'Well Drilling and Completion Capital Cost Adjustment Factor': ( + "Geometric mean of the ATB-aligned baseline (0.9) and Fervo's best demonstrated Phase I well (0.58), now " + 'applied to both the vertical section and the lateral. The February 2026 Update applied 0.9 to the vertical ' + 'section only.' + ), + 'All-in Nonvertical Drilling Costs': ( + 'Per-meter lateral cost from the 2025 NREL drilling cost curve at 3.06 km (Akindipe and Witter, 2025), so the ' + 'lateral is costed explicitly.' + ), + 'Multilaterals Cased': ( + "Fervo's laterals are cased and cemented for plug-and-perf stimulation (Norbeck et al., 2024)." + ), + 'Reservoir Stimulation Capital Cost per Fracture Surface Area': ( + 'Stimulation is priced per unit fracture area so that cost tracks fracture count and geometry; equivalent to ' + "the February 2026 Update's $4M per 150-fracture well." + ), + 'Reservoir Stimulation Capital Cost per Production Well': ( + 'Production wells are stimulated and costed from the per-area input.' + ), + 'Reservoir Stimulation Capital Cost per Injection Well': 'Replaced by the per-area input.', + 'One-time Flat License Fees Etc': ( + 'Grid interconnection cost of $500/kW for 500 MWe, based on PacifiCorp cluster-study estimates for ' + 'geothermal requests in Beaver and Millard Counties (Seel et al., 2026). Not included in the February 2026 ' + 'Update.' + ), + 'Annual License Fees Etc': ( + "Long-term firm point-to-point transmission service for 500 MW at PacifiCorp's 2017 tariff escalated to 2026 " + '(PacifiCorp, 2017). Not included in the February 2026 Update.' + ), + 'Reservoir Depth': ( + 'Depth at which the reservoir reaches about 221℃ (430℉), the Fervo 3.0 design point for Phase II (Fervo ' + 'Energy, 2026f; 2026g). 2.68 km corresponded to the roughly 400℉ Phase I design.' + ), + 'Number of Fractures per Stimulated Well': ( + '18 stages for the 7,500 ft 3.0 lateral instead of 12 for a 5,000 ft lateral, at the same stage length.' + ), + 'Number of Production Wells': ( + 'Re-sized for the 3.0 design to meet the 500 MWe PPA minimum within the nameplate capacity of 11 Gen 2 ORC ' + 'units, with about 27% more power per well at 430℉ (Fervo Energy, 2026f).' + ), + 'Nonvertical Length per Multilateral Section': ( + 'Lateral length of the Fervo 3.0 design (Fervo Energy, 2026f; 2026g), up from the 5,000 ft Phase I laterals.' + ), + 'Well Geometry Configuration': ( + 'L configuration (vertical section plus one lateral), required to cost the lateral explicitly.' + ), + 'Number of Multilateral Sections per Vertical Section': ( + 'One lateral per well, costed explicitly instead of folded into the vertical well cost.' + ), + 'Number of Multilateral Sections': 'Replaced by Number of Multilateral Sections per Vertical Section.', + 'Production Well Diameter': ( + 'Inner diameter of the 8⅝ inch casing Fervo disclosed for the 3.0 design (Fervo Energy, 2026f; 2026g), ' + 'replacing the inferred 9⅝ inch casing.' + ), + 'Injection Well Diameter': 'Same as production wells.', + 'Injectivity Index': ( + 'Derated to yield a parasitic load of at least 20% of net generation, between the 15–20% goal and the 25–35% ' + 'observed in Phase I operations (Norbeck, 2026).' + ), + 'Productivity Index': 'Derated in proportion with the Injectivity Index.', +} + + +def _get_fpc7_previous_version(project_root: Path) -> tuple[GeophiresInputParameters, GeophiresXResult]: + examples_dir = project_root / 'tests/examples' + return ( + ImmutableGeophiresInputParameters(from_file_path=examples_dir / f'{_FPC7_PREVIOUS_VERSION_EXAMPLE_NAME}.txt'), + GeophiresXResult(str(examples_dir / f'{_FPC7_PREVIOUS_VERSION_EXAMPLE_NAME}.out')), + ) + + +def _get_non_comment_input_parameters_dict(input_params: GeophiresInputParameters) -> dict[str, str]: + return { + k: v + for k, v in _get_input_parameters_dict(input_params).items() + if not k.startswith(('#', COMMENT_PARAMETER_NAME_PREFIX)) + } + + +def _get_version_comparison_input_value_display(param_name: str, param_val: str | None) -> str: + if param_val is None: + # Defaults are not displayed because they are not necessarily the effective values; for example, per-well + # stimulation cost defaults do not apply when stimulation cost per fracture surface area is provided. + return 'Not set' + + if _is_stimulated_well_sentinel(param_val): + return 'Stimulated (cost from per-area input)' + + if _get_input_parameter_schema_type(param_name) == 'array': + return param_val.replace(',', ', ') + + try: + value_display = _get_input_parameter_value_display(param_name, param_val) + except ValueError: + # e.g. boolean values + return param_val + + if param_name == 'Annual License Fees Etc': + # Parameter units are MUSD, but the value is an annual cost + value_display += '/yr' + + return value_display + + +def _format_version_comparison_number(value: float | None, decimals: int) -> str: + if value is None: + return '.. N/A' + + return f'{value:,.{decimals}f}' + + +def _format_version_comparison_change( + previous_value: float | None, value: float | None, decimals: int, is_percent: bool +) -> str: + if previous_value is None or value is None: + return '.. N/A' + + change = value - previous_value + if round(change, decimals) == 0: + return 'No change' + + if is_percent: + return f'{change:+,.{decimals}f} pts' + + relative_change_pct = 0.0 if previous_value == 0 else change / abs(previous_value) * 100.0 + if round(relative_change_pct) == 0: + return f'{change:+,.{decimals}f}' + + return f'{change:+,.{decimals}f} ({relative_change_pct:+.0f}%)' + + +def _get_result_comparison_table_md( + result_a: GeophiresXResult, + result_b: GeophiresXResult, + column_a: str, + column_b: str, + metrics: list[tuple[str, Callable[[GeophiresXResult], float | None], int, bool]], +) -> str: + table_md = f'| Result | {column_a} | {column_b} | Change |\n|---|---|---|---|\n' + for label, getter, decimals, is_percent in metrics: + value_a = getter(result_a) + value_b = getter(result_b) + table_md += ( + f'| {label} ' + f'| {_format_version_comparison_number(value_a, decimals)} ' + f'| {_format_version_comparison_number(value_b, decimals)} ' + f'| {_format_version_comparison_change(value_a, value_b, decimals, is_percent)} |\n' + ) + + return table_md.strip() + + +def _result_value(category: str, field: str) -> Callable[[GeophiresXResult], float | None]: + def _get(result: GeophiresXResult) -> float | None: + entry = result.result.get(category, {}).get(field) + if entry is None: + return None + + return float(entry['value']) + + return _get + + +def _result_value_or_zero(category: str, field: str) -> Callable[[GeophiresXResult], float | None]: + """ + For outputs that are only printed when the corresponding input is non-zero + """ + + def _get(result: GeophiresXResult) -> float | None: + value = _result_value(category, field)(result) + return 0.0 if value is None else value + + return _get + + +def _average_pumping_power_pct_of_total_generation(result: GeophiresXResult) -> float: + surf_equip_sim = result.result['SURFACE EQUIPMENT SIMULATION RESULTS'] + return ( + surf_equip_sim['Average Pumping Power']['value'] + / surf_equip_sim['Average Total Electricity Generation']['value'] + * 100.0 + ) + + +def _total_wells_including_redrilling(result: GeophiresXResult) -> float: + redrills = result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + return float((1 + redrills) * _number_of_wells(result)) + + +def _lcoe_usd_per_mwh(result: GeophiresXResult) -> float: + return float(_q(result.result['SUMMARY OF RESULTS']['Electricity breakeven price']).to('USD / MWh').magnitude) + + +_CAPEX = 'CAPITAL COSTS (M$)' +_OPEX = 'OPERATING AND MAINTENANCE COSTS (M$/yr)' +_ECON = 'ECONOMIC PARAMETERS' +_SURF = 'SURFACE EQUIPMENT SIMULATION RESULTS' + +# (label, value getter, display decimals, whether the value is a percentage) +_FPC7_VERSION_COMPARISON_RESULT_METRICS: list[tuple[str, Callable[[GeophiresXResult], float | None], int, bool]] = [ + ('LCOE ($/MWh)', _lcoe_usd_per_mwh, 1, False), + ('Levelized PPA price ($/MWh)', _get_levelized_ppa_price_usd_per_mwh, 1, False), + ('After-tax IRR (%)', _result_value(_ECON, 'After-tax IRR'), 1, True), + ('Project NPV ($M)', _result_value(_ECON, 'Project NPV'), 1, False), + ('Levered equity profitability index', _result_value(_ECON, 'Project VIR=PI=PIR'), 2, False), + ('WACC (%)', _result_value(_ECON, 'WACC'), 2, True), + ('Investment Tax Credit ($M)', _result_value(_ECON, 'Investment Tax Credit'), 1, False), + ('Total CAPEX ($M)', _result_value(_CAPEX, 'Total CAPEX'), 1, False), + ('Total CAPEX ($/kW)', _result_value('SUMMARY OF RESULTS', 'Total CAPEX ($/kW)'), 0, False), + ('Overnight capital cost ($M)', _result_value(_CAPEX, 'Overnight Capital Cost'), 1, False), + ('Exploration ($M)', _result_value(_CAPEX, 'Exploration costs'), 1, False), + ('Well drilling and completion ($M)', _drilling_costs_musd, 1, False), + ('Well drilling and completion per well ($M)', _drilling_costs_per_well_musd, 2, False), + ('Stimulation ($M)', _stim_costs_musd, 1, False), + ('Stimulation per well ($M)', _stim_costs_per_well_musd, 2, False), + ('Surface power plant ($M)', _result_value(_CAPEX, 'Surface power plant costs'), 1, False), + ('Field gathering system ($M)', _result_value(_CAPEX, 'Field gathering system costs'), 1, False), + ('Grid interconnection ($M)', _result_value_or_zero(_CAPEX, 'One-time Flat License Fees Etc'), 1, False), + ('Total O&M ($M/yr)', _result_value(_OPEX, 'Total operating and maintenance costs'), 1, False), + ('Redrilling ($M/yr)', _result_value(_OPEX, 'Redrilling costs'), 1, False), + ('Transmission service ($M/yr)', _result_value_or_zero(_OPEX, 'Annual License Fees Etc'), 1, False), + ('Production wells', _result_value('SUMMARY OF RESULTS', 'Number of production wells'), 0, False), + ('Injection wells', _result_value('SUMMARY OF RESULTS', 'Number of injection wells'), 0, False), + ('Redrilling events', _result_value('ENGINEERING PARAMETERS', 'Number of times redrilling'), 0, False), + ('Total wells over project lifetime', _total_wells_including_redrilling, 0, False), + ('Minimum net generation (MW)', _result_value(_SURF, 'Minimum Net Electricity Generation'), 1, False), + ('Average net generation (MW)', _result_value(_SURF, 'Average Net Electricity Generation'), 1, False), + ('Maximum net generation (MW)', _result_value(_SURF, 'Maximum Net Electricity Generation'), 1, False), + ('Maximum total generation (MW)', _result_value(_SURF, 'Maximum Total Electricity Generation'), 1, False), + ( + 'Average annual net generation (GWh)', + _result_value(_SURF, 'Average Annual Net Electricity Generation'), + 0, + False, + ), + ( + 'Initial pumping power / net installed power (%)', + _result_value(_SURF, 'Initial pumping power/net installed power'), + 1, + True, + ), + ('Average pumping power / average total generation (%)', _average_pumping_power_pct_of_total_generation, 1, True), + ('Heat to power conversion efficiency (%)', _result_value(_SURF, 'Heat to Power Conversion Efficiency'), 1, True), + ('Bottom-hole temperature (℃)', _result_value('RESERVOIR PARAMETERS', 'Bottom-hole temperature'), 1, False), + ( + 'Initial production temperature (℃)', + _result_value('RESERVOIR SIMULATION RESULTS', 'Initial Production Temperature'), + 1, + False, + ), + ( + 'Average production temperature (℃)', + _result_value('RESERVOIR SIMULATION RESULTS', 'Average Production Temperature'), + 1, + False, + ), + ( + 'Fracture surface area per well (10⁶ m²)', + lambda r: _total_fracture_surface_area_per_well_m2(r) / 1e6, + 2, + False, + ), +] + + +def generate_res_eng_reference_sim_params_table_md( + base_case_input_params: GeophiresInputParameters, res_eng_reference_sim_params: dict[str, Any] +) -> str: + return get_fpc_category_parameters_table_md( + ImmutableGeophiresInputParameters( + # from_file_path=base_case_input_params.as_file_path(), + params=res_eng_reference_sim_params + ), + None, + ) + + +def generate_fpc_opex_output_table_md(input_params: GeophiresInputParameters, result: GeophiresXResult) -> str: + table_md = """| Metric | Result Value | Reference Value(s) | Reference Source | +|-----|-----|-----|-----|\n""" + + for output_param_name, result_value_unit_dict in result.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'].items(): + if result_value_unit_dict is None: + continue + + unit = result_value_unit_dict['unit'] + value_unit_display = ( + f'${result_value_unit_dict["value"]}M/yr' + if unit == 'MUSD/yr' + else f'{result_value_unit_dict["value"]} {unit}' + ) + + reference_value_display = '.. N/A' + + if output_param_name == 'Total operating and maintenance costs': + reference_source_display = '.. N/A ' + elif output_param_name == 'Annual License Fees Etc': + # Output unit is MUSD (matching the input parameter) although the value is an annual cost + value_unit_display = f'${result_value_unit_dict["value"]}M/yr' + reference_value_display = '$16.4M/yr; ~$10M/yr' + reference_source_display = ( + 'Long-term firm point-to-point transmission service for 500 MW, entered as ' + '`Annual License Fees Etc` (see Economic Parameters). Reference values apply 2017 PacifiCorp ' + 'OATT Schedule 7 and 1 rates (PacifiCorp, 2017) and the BPA fiscal year 2024–2025 long-term firm ' + 'point-to-point rate (BPA, 2026) to 500 MW; the case study value escalates the PacifiCorp rate at ' + '3% per year to 2026.' + ) + else: + reference_source_display = _get_output_parameter_description(output_param_name) + + if output_param_name == 'Water costs': + water_cost_adjustment_param_name = 'Water Cost Adjustment Factor' + reference_source_display = reference_source_display.split( + f'. Provide {water_cost_adjustment_param_name}', maxsplit=1 + )[0] + water_cost_adjustment_percent = ( + PlainQuantity( + float(_get_input_parameters_dict(input_params)[water_cost_adjustment_param_name]), + 'dimensionless', + ) + .to('percent') + .magnitude + ) + reference_source_display = ( + f'{reference_source_display}. ' + f'The default correlation is adjusted by the {water_cost_adjustment_param_name} parameter value ' + f'of {water_cost_adjustment_percent:.0f}%.' + ) + + if reference_source_display.startswith(('O&M', 'Total O&M')): + reference_source_display = reference_source_display.split('. ', maxsplit=1)[1] + + for suffix in ('s', ''): + reference_source_display = reference_source_display.replace(f'O&M cost{suffix}', 'OPEX') + + table_md += ( + f'| {output_param_name} | {value_unit_display} | {reference_value_display} | {reference_source_display} |\n' + ) + + if output_param_name == 'Total operating and maintenance costs': + opex_usd_per_kw_per_year = ( + _q(result_value_unit_dict) / PlainQuantity(get_max_net_generation_mwe(result), 'MW') + ).to('USD / year / kilowatt') + + reference_source = '2024b ATB: 2028 Deep EGS Binary Conservative Scenario (NREL, 2025). ' + # TODO explain why we're higher than ATB (e.g. redrilling not modeled by ATB) + + transmission_cost_musd_per_yr = _annual_license_fees_musd_per_yr(result) + if transmission_cost_musd_per_yr != 0: + opex_excluding_transmission_usd_per_kw_per_year = ( + (_q(result_value_unit_dict) - PlainQuantity(transmission_cost_musd_per_yr, 'MUSD / year')) + / PlainQuantity(get_max_net_generation_mwe(result), 'MW') + ).to('USD / year / kilowatt') + reference_source += ( + f'The case study value includes transmission service ' + f'(${(opex_usd_per_kw_per_year - opex_excluding_transmission_usd_per_kw_per_year).magnitude:.0f}' + f'/kW-yr); excluding it, total OPEX is ' + f'${opex_excluding_transmission_usd_per_kw_per_year.magnitude:.0f}/kW-yr.' + ) + + table_md += f'| {output_param_name}: $/kW-yr | ${opex_usd_per_kw_per_year.magnitude:.2f}/kW-yr | $226.31/kW-yr | {reference_source} |\n' + + return table_md + + +def generate_fpc7_previous_version_input_changes_table_md( + previous_input_params: GeophiresInputParameters, input_params: GeophiresInputParameters +) -> str: + """ + :return: Markdown table of input parameters whose values differ between the previous version and this version, + including parameters that were added or removed, with the rationale for each change + :raises ValueError: if a changed parameter has no rationale in + _FPC7_PREVIOUS_VERSION_INPUT_CHANGE_RATIONALE_BY_PARAM_NAME + """ + previous_params = _get_non_comment_input_parameters_dict(previous_input_params) + params = _get_non_comment_input_parameters_dict(input_params) + + param_names = list(params.keys()) + [it for it in previous_params if it not in params] + + table_md = ( + f'| Parameter | {_FPC7_PREVIOUS_VERSION_LABEL} | {_FPC7_CURRENT_VERSION_LABEL} | Rationale |\n' + f'|---|---|---|---|\n' + ) + params_missing_rationale = [] + for param_name in param_names: + previous_value_display = _get_version_comparison_input_value_display( + param_name, previous_params.get(param_name) + ) + value_display = _get_version_comparison_input_value_display(param_name, params.get(param_name)) + if previous_value_display == value_display: + continue + + rationale = _FPC7_PREVIOUS_VERSION_INPUT_CHANGE_RATIONALE_BY_PARAM_NAME.get(param_name) + if rationale is None: + params_missing_rationale.append(param_name) + continue + + table_md += f'| {param_name} | {previous_value_display} | {value_display} | {rationale} |\n' + + if len(params_missing_rationale) > 0: + raise ValueError( + f'No rationale for changed input parameters {params_missing_rationale}; add them to ' + f'_FPC7_PREVIOUS_VERSION_INPUT_CHANGE_RATIONALE_BY_PARAM_NAME.' + ) + + return table_md.strip() + + +def generate_fpc7_previous_version_result_changes_table_md( + previous_result: GeophiresXResult, result: GeophiresXResult +) -> str: + """ + :return: Markdown table comparing key results of the previous version and this version + """ + return _get_result_comparison_table_md( + previous_result, + result, + _FPC7_PREVIOUS_VERSION_LABEL, + _FPC7_CURRENT_VERSION_LABEL, + _FPC7_VERSION_COMPARISON_RESULT_METRICS, + ) + + +def generate_fervo_project_cape_7_md( + input_params: GeophiresInputParameters, + result: GeophiresXResult, + res_eng_reference_sim_params: dict[str, Any] | None = None, + project_root: Path = _PROJECT_ROOT, + previous_version: tuple[GeophiresInputParameters, GeophiresXResult] | None = None, + scenario_results: dict[str, GeophiresXResult] | None = None, +) -> None: + if res_eng_reference_sim_params is None: + res_eng_reference_sim_params = {} + + if previous_version is None: + previous_version = _get_fpc7_previous_version(project_root) + + result_values: dict[str, Any] = get_result_values(result) + + # noinspection PyDictCreation + template_values = { + **get_fpc7_input_parameter_values(input_params, result), + **result_values, + **get_fpc7_scenario_values(input_params, result, scenario_results, previous_input_params=previous_version[0]), + } + + for template_key, md_method in { + 'opex_result_outputs_table_md': generate_fpc_opex_output_table_md, + 'reservoir_parameters_table_md': generate_fpc_reservoir_parameters_table_md, + 'surface_plant_parameters_table_md': generate_fpc_surface_plant_parameters_table_md, + 'well_bores_parameters_table_md': generate_fpc_well_bores_parameters_table_md, + 'economics_parameters_table_md': generate_fpc_economics_parameters_table_md, + 'construction_parameters_table_md': generate_fpc_construction_parameters_table_md, + }.items(): + template_values[template_key] = md_method(input_params, result) + + template_values['reservoir_engineering_reference_simulation_params_table_md'] = ( + generate_res_eng_reference_sim_params_table_md(input_params, res_eng_reference_sim_params) + ) + + template_values['previous_version_input_changes_table_md'] = generate_fpc7_previous_version_input_changes_table_md( + previous_version[0], input_params + ) + template_values['previous_version_result_changes_table_md'] = ( + generate_fpc7_previous_version_result_changes_table_md(previous_version[1], result) + ) + + docs_dir = project_root / 'docs' + + # Set up Jinja environment + # The template renders markdown, so only HTML and XML templates are autoescaped. HTML-escaping characters such as + # '&' in parameter comments and table labels would be escaped again by the markdown-to-reStructuredText conversion + # and render literally in the documentation (e.g. 'O&M'). + env = Environment(loader=FileSystemLoader(docs_dir), autoescape=select_autoescape(['html', 'xml'])) + template = env.get_template('Fervo_Project_Cape-7.md.jinja') + + # Render template + _log.info('Rendering template...') + output = template.render(**template_values) + + # Write output + output_file = docs_dir / 'Fervo_Project_Cape-7.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-7.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_fpc7_input_file_path(project_root) + ) + result = GeophiresXResult(_get_fpc7_result_file_path(project_root)) + generate_fervo_project_cape_7_md(input_params, result, project_root=project_root) + + +if __name__ == '__main__': + main() diff --git a/src/geophires_docs/generate_fervo_project_red_2026_docs.py b/src/geophires_docs/generate_fervo_project_red_2026_docs.py index 96db0c49c..ac6199bae 100644 --- a/src/geophires_docs/generate_fervo_project_red_2026_docs.py +++ b/src/geophires_docs/generate_fervo_project_red_2026_docs.py @@ -55,8 +55,10 @@ _SAVEFIG_ARGS = { 'dpi': _GRAPH_DPI, 'metadata': { - # TODO: intended to prevent spurious image diffs after graph/doc regeneration, but does not work as intended. - 'Date': None + # Prevent spurious image diffs after graph/doc regeneration: Matplotlib writes its version to PNG metadata + # (Software), so otherwise pixel-identical images differ whenever the Matplotlib version changes. + 'Date': None, + 'Software': None, }, } diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index f70d6bf9d..060077aa3 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -3404,9 +3404,9 @@ def calculate_plant_costs(self, model: Model) -> None: def _check_temperature_for_ORC(temperature: float) -> None: if temperature > 200.: - msg = ('The simulated production temperature exceeds 200 degrees Celsius. The built-in ORC utilization ' - 'efficiency correlations may not be valid above this temperature. Consider using a single or double ' - 'flash plant, or providing a custom correlation via a surface plant module. For more information, ' + msg = ('The simulated production temperature exceeds 200 degrees Celsius. The built-in ORC utilization ' + 'efficiency correlations may not be valid above this temperature. Consider using a single or double ' + 'flash plant, or providing a custom correlation via a surface plant module. For more information, ' 'see: https://natlabrockies.github.io/GEOPHIRES-X/Theoretical-Basis-for-GEOPHIRES.html#surface-plant') print(f'Warning: {msg}') model.logger.warning(msg) diff --git a/tests/examples/Fervo_Project_Cape-7.out b/tests/examples/Fervo_Project_Cape-7.out new file mode 100644 index 000000000..ecefeea0b --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-7.out @@ -0,0 +1,482 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.18.1 + Simulation Date: 2026-09-28 + Simulation Time: 07:55 + Calculation Time: 3.482 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 523.70 MW + Electricity breakeven price: 10.58 cents/kWh + Total CAPEX: 3733.93 MUSD + Total CAPEX ($/kW): 7099 USD/kW + Number of production wells: 50 + Number of injection wells: 34 + Flowrate per production well: 107.0 kg/sec + Well depth: 3.1 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.36 % + WACC: 8.41 % + Investment Tax Credit: 1120.18 MUSD + Project lifetime: 30 yr + Capacity factor: 91.3 % + Project NPV: 337.31 MUSD + After-tax IRR: 23.54 % + Project VIR=PI=PIR: 1.38 + Project MOIC: 4.88 + Project Payback Period: 4.96 yr + Estimated Jobs Created: 1347 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 50 + Number of Injection Wells: 34 + Well depth: 3.1 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: 1.0 degC + Flowrate per production well: 107.0 kg/sec + Injection well casing ID: 7.825 in + Production well casing ID: 7.825 in + Number of times redrilling: 2 + 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 + Project location: 38.506196, -112.918155 + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 220.85 degC + Fracture model = Rectangular + Well separation: fracture height: 100.00 meter + Fracture width: 305.00 meter + Fracture area: 30500.00 m**2 + Reservoir volume calculated with fracture separation and number of fractures as input + Number of fractures: 18900 + Fracture separation: 9.83 meter + Reservoir volume: 5663609797 m**3 + Reservoir hydrostatic pressure: 28782.99 kPa + Plant outlet pressure: 13789.51 kPa + Production wellhead pressure: 2702.05 kPa + Productivity Index: 0.98 kg/sec/bar + Injectivity Index: 1.20 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: 218.4 degC + Average Production Temperature: 218.1 degC + Minimum Production Temperature: 216.2 degC + Initial Production Temperature: 216.7 degC + Average Reservoir Heat Extraction: 3616.52 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 1.0 degC + Average Injection Well Pump Pressure Drop: 1729.9 kPa + Average Production Well Pump Pressure Drop: 12497.4 kPa + + + ***CAPITAL COSTS (M$)*** + + Exploration costs: 56.00 MUSD + Drilling and completion costs: 712.00 MUSD + Drilling and completion costs per vertical production well: 4.40 MUSD + Drilling and completion costs per vertical injection well: 4.40 MUSD + Drilling and completion costs per non-vertical section: 4.08 MUSD + Stimulation costs: 609.09 MUSD + Stimulation costs per well: 7.25 MUSD + Surface power plant costs: 1552.99 MUSD + Field gathering system costs: 50.20 MUSD + Total surface equipment costs: 1603.20 MUSD + One-time Flat License Fees Etc: 250.00 MUSD + Overnight Capital Cost: 3230.28 MUSD + Inflation costs during construction: 318.13 MUSD + Interest during construction: 185.52 MUSD + Total CAPEX: 3733.93 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 8.58 MUSD/yr + Power plant maintenance costs: 26.18 MUSD/yr + Water costs: 2.85 MUSD/yr + Redrilling costs: 88.07 MUSD/yr + Annual License Fees Etc: 21.00 MUSD + Average Annual Royalty Cost: 16.66 MUSD/yr + Total operating and maintenance costs: 163.35 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.22 MW/(kg/s) + Maximum Total Electricity Generation: 634.60 MW + Average Total Electricity Generation: 632.38 MW + Minimum Total Electricity Generation: 620.27 MW + Initial Total Electricity Generation: 623.07 MW + Maximum Net Electricity Generation: 525.99 MW + Average Net Electricity Generation: 523.70 MW + Minimum Net Electricity Generation: 511.28 MW + Initial Net Electricity Generation: 514.30 MW + Average Annual Total Electricity Generation: 5057.80 GWh + Average Annual Net Electricity Generation: 4188.58 GWh + Initial pumping power/net installed power: 21.15 % + Average Pumping Power: 108.68 MW + Heat to Power Conversion Efficiency: 14.48 % + + ************************************************************ + * 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 216.67 108.7641 514.3027 14.3459 + 2 1.0053 217.82 108.7294 521.9599 14.4555 + 3 1.0063 218.04 108.7229 523.3973 14.4759 + 4 1.0068 218.15 108.7195 524.1468 14.4865 + 5 1.0072 218.23 108.7172 524.6432 14.4935 + 6 1.0074 218.28 108.7156 525.0097 14.4987 + 7 1.0076 218.33 108.7142 525.2982 14.5028 + 8 1.0078 218.36 108.7132 525.5329 14.5061 + 9 1.0079 218.39 108.7122 525.7370 14.5090 + 10 1.0080 218.41 108.7121 525.8697 14.5109 + 11 1.0080 218.40 108.7152 525.8133 14.5100 + 12 1.0071 218.22 108.7398 524.5645 14.4921 + 13 1.0030 217.31 108.8522 518.4502 14.4040 + 14 1.0038 217.50 108.7064 519.8278 14.4255 + 15 1.0058 217.94 108.7013 522.7334 14.4668 + 16 1.0066 218.09 108.6923 523.7836 14.4817 + 17 1.0070 218.19 108.6791 524.4140 14.4908 + 18 1.0073 218.25 108.6627 524.8616 14.4973 + 19 1.0075 218.30 108.6457 525.2068 14.5024 + 20 1.0077 218.34 108.6303 525.4849 14.5066 + 21 1.0079 218.37 108.6181 525.7161 14.5100 + 22 1.0080 218.40 108.6097 525.9077 14.5128 + 23 1.0080 218.41 108.6060 525.9938 14.5141 + 24 1.0078 218.37 108.6116 525.6787 14.5095 + 25 1.0060 217.98 108.6608 523.0589 14.4719 + 26 0.9991 216.47 108.8487 512.8622 14.3240 + 27 1.0050 217.76 108.5996 521.6761 14.4532 + 28 1.0062 218.01 108.5996 523.3522 14.4769 + 29 1.0068 218.14 108.5996 524.1651 14.4884 + 30 1.0071 218.22 108.5997 524.6893 14.4958 + + + ******************************************************************* + * 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 4154.4 28810.9 1953.94 5.04 + 2 4181.1 28900.9 1849.90 10.10 + 3 4189.3 28928.4 1745.75 15.16 + 4 4194.2 28944.8 1641.55 20.22 + 5 4197.6 28956.2 1537.31 25.29 + 6 4200.2 28965.0 1433.04 30.36 + 7 4202.2 28972.0 1328.74 35.42 + 8 4204.0 28977.8 1224.42 40.49 + 9 4205.4 28982.5 1120.08 45.57 + 10 4205.9 28984.2 1015.74 50.64 + 11 4202.0 28971.3 911.44 55.70 + 12 4176.2 28885.8 807.45 60.76 + 13 4125.5 28715.3 704.08 65.78 + 14 4171.9 28869.3 600.15 70.83 + 15 4185.4 28914.6 496.05 75.89 + 16 4191.8 28936.1 391.88 80.95 + 17 4196.1 28950.0 287.66 86.02 + 18 4199.2 28960.1 183.41 91.09 + 19 4201.7 28968.0 79.12 96.15 + 20 4203.7 28974.5 -25.19 101.22 + 21 4205.4 28980.0 -129.51 106.29 + 22 4206.6 28983.8 -233.86 111.37 + 23 4206.2 28982.3 -338.19 116.44 + 24 4196.6 28950.6 -442.41 121.50 + 25 4150.3 28797.0 -546.08 126.54 + 26 4143.0 28769.6 -649.65 131.57 + 27 4180.1 28893.9 -753.67 136.63 + 28 4189.2 28924.9 -857.80 141.69 + 29 4194.4 28942.5 -961.99 146.75 + 30 4197.9 28954.1 -1066.23 151.82 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + 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] (%) 4.10 13.90 43.10 38.90 +Overnight capital expenditure [construction] ($) -132,441,575 -449,009,240 -1,392,251,673 -1,256,579,817 +plus: +Inflation cost [construction] ($) -3,973,247 -27,344,663 -129,099,321 -157,711,837 +plus: +Royalty supplemental payments [construction] ($) 0 0 0 0 +equals: +Nominal capital expenditure [construction] ($) -136,414,822 -476,353,903 -1,521,350,994 -1,414,291,654 + +Issuance of equity [construction] ($) 136,414,822 142,906,171 456,405,298 424,287,496 +Issuance of debt [construction] ($) 0 333,447,732 1,064,945,696 990,004,158 +Debt balance [construction] ($) 0 333,447,732 1,433,405,440 2,573,917,169 +Debt interest payment [construction] ($) 0 0 35,012,012 150,507,571 + +Installed cost [construction] ($) -136,414,822 -476,353,903 -1,556,363,006 -1,564,799,225 +After-tax net cash flow [construction] ($) -136,414,822 -142,906,171 -456,405,298 -424,287,496 + +ENERGY +Electricity to grid (kWh) 0.0 4,154,494,569 4,181,212,950 4,189,406,019 4,194,260,188 4,197,662,540 4,200,258,425 4,202,338,883 4,204,085,390 4,205,483,759 4,205,957,420 4,202,077,639 4,176,289,208 4,125,595,949 4,172,045,018 4,185,497,243 4,191,945,128 4,196,175,382 4,199,312,558 4,201,791,085 4,203,809,718 4,205,525,496 4,206,682,370 4,206,256,569 4,196,703,431 4,150,395,274 4,143,145,859 4,180,156,805 4,189,338,914 4,194,530,322 4,197,975,491 +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,154,494,569 4,181,212,950 4,189,406,019 4,194,260,188 4,197,662,540 4,200,258,425 4,202,338,883 4,204,085,390 4,205,483,759 4,205,957,420 4,202,077,639 4,176,289,208 4,125,595,949 4,172,045,018 4,185,497,243 4,191,945,128 4,196,175,382 4,199,312,558 4,201,791,085 4,203,809,718 4,205,525,496 4,206,682,370 4,206,256,569 4,196,703,431 4,150,395,274 4,143,145,859 4,180,156,805 4,189,338,914 4,194,530,322 4,197,975,491 + +REVENUE +PPA price (cents/kWh) 0.0 11.50 11.68 11.87 12.05 12.23 12.42 12.60 12.78 12.96 13.15 13.33 13.51 13.70 13.88 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 14.06 +PPA revenue ($) 0 477,766,875 488,491,109 497,114,918 505,366,410 513,458,082 521,462,083 529,410,652 537,324,154 545,198,915 552,957,222 560,136,949 564,341,961 565,041,621 579,038,128 588,480,912 589,387,485 589,982,259 590,423,346 590,771,827 591,055,646 591,296,885 591,459,541 591,399,674 590,056,502 583,545,575 582,526,308 587,730,047 589,021,051 589,750,963 590,235,354 +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,866,965,478 +Total revenue ($) 0 477,766,875 488,491,109 497,114,918 505,366,410 513,458,082 521,462,083 529,410,652 537,324,154 545,198,915 552,957,222 560,136,949 564,341,961 565,041,621 579,038,128 588,480,912 589,387,485 589,982,259 590,423,346 590,771,827 591,055,646 591,296,885 591,459,541 591,399,674 590,056,502 583,545,575 582,526,308 587,730,047 589,021,051 589,750,963 2,457,200,832 + +Property tax net assessed value ($) 0 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 3,733,930,956 + +OPERATING EXPENSES +O&M fixed expense ($) 0 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 146,685,464 +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 8,360,920 8,548,594 8,699,511 8,843,912 8,985,516 9,125,586 9,264,686 9,403,173 9,540,981 9,676,751 19,604,793 19,751,969 19,776,457 20,266,334 20,596,832 20,628,562 20,649,379 20,664,817 20,677,014 20,686,948 20,695,391 20,701,084 20,698,989 20,651,978 20,424,095 20,388,421 20,570,552 20,615,737 20,641,284 20,658,237 +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 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 8,214,648 +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 163,261,032 163,448,706 163,599,623 163,744,024 163,885,628 164,025,698 164,164,798 164,303,285 164,441,093 164,576,863 174,504,905 174,652,080 174,676,569 175,166,446 175,496,944 175,528,674 175,549,491 175,564,929 175,577,126 175,587,059 175,595,503 175,601,196 175,599,100 175,552,089 175,324,207 175,288,533 175,470,663 175,515,849 175,541,396 175,558,349 + +EBITDA ($) 0 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 + +OPERATING ACTIVITIES +EBITDA ($) 0 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 +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 180,174,202 178,266,805 176,225,890 174,042,112 171,705,469 169,205,261 166,530,038 163,667,550 160,604,687 157,327,424 153,820,753 150,068,615 146,053,827 141,758,004 137,161,473 132,243,186 126,980,618 121,349,670 115,324,556 108,877,684 101,979,531 94,598,507 86,700,812 78,250,278 69,208,206 59,533,190 49,180,922 38,103,995 26,251,684 13,569,711 +Cash flow from operating activities ($) 0 134,331,641 146,775,598 157,289,405 167,580,274 177,866,985 188,231,125 198,715,816 209,353,320 220,153,134 231,052,934 231,811,291 239,621,265 244,311,226 262,113,678 275,822,495 281,615,626 287,452,150 293,508,747 299,870,145 306,590,903 313,721,851 321,259,838 329,099,762 336,254,135 339,013,162 347,704,586 363,078,462 375,401,207 387,957,884 2,268,072,772 + +INVESTING ACTIVITIES +Total installed cost ($) -3,733,930,956 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -3,733,930,956 +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 ($) -3,733,930,956 0 0 0 0 0 0 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 ($) 1,160,013,787 +Size of debt ($) 2,573,917,169 +minus: +Debt principal payment ($) 0 27,248,526 29,155,923 31,196,837 33,380,616 35,717,259 38,217,467 40,892,690 43,755,178 46,818,040 50,095,303 53,601,974 57,354,113 61,368,901 65,664,724 70,261,254 75,179,542 80,442,110 86,073,058 92,098,172 98,545,044 105,443,197 112,824,221 120,721,916 129,172,450 138,214,522 147,889,538 158,241,806 169,318,732 181,171,043 193,853,016 +equals: +Cash flow from financing activities ($) 3,733,930,956 -27,248,526 -29,155,923 -31,196,837 -33,380,616 -35,717,259 -38,217,467 -40,892,690 -43,755,178 -46,818,040 -50,095,303 -53,601,974 -57,354,113 -61,368,901 -65,664,724 -70,261,254 -75,179,542 -80,442,110 -86,073,058 -92,098,172 -98,545,044 -105,443,197 -112,824,221 -120,721,916 -129,172,450 -138,214,522 -147,889,538 -158,241,806 -169,318,732 -181,171,043 -193,853,016 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 134,331,641 146,775,598 157,289,405 167,580,274 177,866,985 188,231,125 198,715,816 209,353,320 220,153,134 231,052,934 231,811,291 239,621,265 244,311,226 262,113,678 275,822,495 281,615,626 287,452,150 293,508,747 299,870,145 306,590,903 313,721,851 321,259,838 329,099,762 336,254,135 339,013,162 347,704,586 363,078,462 375,401,207 387,957,884 2,268,072,772 +Cash flow from investing activities ($) -3,733,930,956 0 0 0 0 0 0 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 ($) 3,733,930,956 -27,248,526 -29,155,923 -31,196,837 -33,380,616 -35,717,259 -38,217,467 -40,892,690 -43,755,178 -46,818,040 -50,095,303 -53,601,974 -57,354,113 -61,368,901 -65,664,724 -70,261,254 -75,179,542 -80,442,110 -86,073,058 -92,098,172 -98,545,044 -105,443,197 -112,824,221 -120,721,916 -129,172,450 -138,214,522 -147,889,538 -158,241,806 -169,318,732 -181,171,043 -193,853,016 +Total pre-tax cash flow ($) 0 107,083,116 117,619,675 126,092,568 134,199,658 142,149,726 150,013,658 157,823,127 165,598,142 173,335,094 180,957,631 178,209,317 182,267,153 182,942,325 196,448,954 205,561,241 206,436,084 207,010,040 207,435,689 207,771,973 208,045,859 208,278,654 208,435,618 208,377,846 207,081,685 200,798,641 199,815,048 204,836,656 206,082,475 206,786,840 2,074,219,755 + +Pre-tax Returns: +Issuance of equity ($) 1,160,013,787 +Total pre-tax cash flow ($) 0 107,083,116 117,619,675 126,092,568 134,199,658 142,149,726 150,013,658 157,823,127 165,598,142 173,335,094 180,957,631 178,209,317 182,267,153 182,942,325 196,448,954 205,561,241 206,436,084 207,010,040 207,435,689 207,771,973 208,045,859 208,278,654 208,435,618 208,377,846 207,081,685 200,798,641 199,815,048 204,836,656 206,082,475 206,786,840 2,074,219,755 +Total pre-tax returns ($) -1,160,013,787 107,083,116 117,619,675 126,092,568 134,199,658 142,149,726 150,013,658 157,823,127 165,598,142 173,335,094 180,957,631 178,209,317 182,267,153 182,942,325 196,448,954 205,561,241 206,436,084 207,010,040 207,435,689 207,771,973 208,045,859 208,278,654 208,435,618 208,377,846 207,081,685 200,798,641 199,815,048 204,836,656 206,082,475 206,786,840 2,074,219,755 + +After-tax Returns: +Total pre-tax returns ($) -1,160,013,787 107,083,116 117,619,675 126,092,568 134,199,658 142,149,726 150,013,658 157,823,127 165,598,142 173,335,094 180,957,631 178,209,317 182,267,153 182,942,325 196,448,954 205,561,241 206,436,084 207,010,040 207,435,689 207,771,973 208,045,859 208,278,654 208,435,618 208,377,846 207,081,685 200,798,641 199,815,048 204,836,656 206,082,475 206,786,840 2,074,219,755 +Federal ITC total income ($) 0 1,120,179,287 0 0 0 0 0 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 -11,021,590 2,388,596 281,156 -1,781,597 -3,843,517 -5,920,957 -8,022,561 -10,154,795 -12,319,564 -14,504,374 -14,656,383 -16,221,853 -17,161,933 -20,730,345 -23,478,209 -24,639,413 -25,809,315 -27,023,330 -28,298,440 -29,645,582 -46,979,461 -64,394,928 -65,966,402 -67,400,460 -67,953,493 -69,695,646 -72,777,262 -75,247,295 -77,764,218 -454,623,847 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,501,845 542,199 63,821 -404,413 -872,459 -1,344,027 -1,821,081 -2,305,087 -2,796,479 -3,292,420 -3,326,925 -3,682,279 -3,895,672 -4,705,683 -5,329,435 -5,593,022 -5,858,584 -6,134,159 -6,423,603 -6,729,397 -10,664,100 -14,617,323 -14,974,039 -15,299,563 -15,425,099 -15,820,559 -16,520,070 -17,080,755 -17,652,084 -103,197,311 +Total after-tax returns ($) -1,160,013,787 1,213,738,967 120,550,471 126,437,545 132,013,648 137,433,751 142,748,674 147,979,485 153,138,260 158,219,052 163,160,837 160,226,009 162,363,021 161,884,721 171,012,926 176,753,597 176,203,649 175,342,141 174,278,200 173,049,930 171,670,880 150,635,094 129,423,367 127,437,405 124,381,662 117,420,049 114,298,843 115,539,324 113,754,425 111,370,539 1,516,398,598 + +After-tax net cash flow ($) -136,414,822 -142,906,171 -456,405,298 -424,287,496 1,213,738,967 120,550,471 126,437,545 132,013,648 137,433,751 142,748,674 147,979,485 153,138,260 158,219,052 163,160,837 160,226,009 162,363,021 161,884,721 171,012,926 176,753,597 176,203,649 175,342,141 174,278,200 173,049,930 171,670,880 150,635,094 129,423,367 127,437,405 124,381,662 117,420,049 114,298,843 115,539,324 113,754,425 111,370,539 1,516,398,598 +After-tax cumulative IRR (%) NaN NaN NaN NaN 2.29 6.85 10.64 13.61 15.88 17.62 18.94 19.96 20.75 21.36 21.82 22.17 22.45 22.68 22.86 23.01 23.12 23.21 23.28 23.33 23.37 23.40 23.42 23.44 23.45 23.46 23.47 23.47 23.48 23.54 +After-tax cumulative NPV ($) -136,414,822 -260,293,264 -603,250,668 -879,622,954 -194,285,754 -135,280,230 -81,633,337 -33,078,545 10,739,341 50,191,871 85,644,548 117,448,127 145,931,788 171,394,092 193,069,114 212,108,740 228,564,650 243,633,834 257,135,079 268,802,240 278,866,489 287,537,762 295,001,493 301,419,881 306,301,908 309,937,972 313,041,535 315,667,353 317,816,150 319,629,325 321,218,136 322,574,123 323,724,930 337,307,758 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -136,414,822 -142,906,171 -456,405,298 -424,287,496 735,972,092 -367,940,638 -370,677,373 -373,352,762 -376,024,331 -378,713,410 -381,431,167 -384,185,894 -386,979,863 -389,796,385 -399,910,941 -401,978,940 -403,156,900 -408,025,202 -411,727,315 -413,183,836 -414,640,117 -416,145,145 -417,721,896 -419,384,767 -440,661,791 -462,036,174 -463,962,269 -465,674,840 -466,125,527 -468,227,465 -472,190,723 -475,266,626 -478,380,425 926,163,243 +PPA revenue ($) 0 0 0 0 477,766,875 488,491,109 497,114,918 505,366,410 513,458,082 521,462,083 529,410,652 537,324,154 545,198,915 552,957,222 560,136,949 564,341,961 565,041,621 579,038,128 588,480,912 589,387,485 589,982,259 590,423,346 590,771,827 591,055,646 591,296,885 591,459,541 591,399,674 590,056,502 583,545,575 582,526,308 587,730,047 589,021,051 589,750,963 590,235,354 +Electricity to grid (kWh) 0 0 0 0 4,154,494,569 4,181,212,950 4,189,406,019 4,194,260,188 4,197,662,540 4,200,258,425 4,202,338,883 4,204,085,390 4,205,483,759 4,205,957,420 4,202,077,639 4,176,289,208 4,125,595,949 4,172,045,018 4,185,497,243 4,191,945,128 4,196,175,382 4,199,312,558 4,201,791,085 4,203,809,718 4,205,525,496 4,206,682,370 4,206,256,569 4,196,703,431 4,150,395,274 4,143,145,859 4,180,156,805 4,189,338,914 4,194,530,322 4,197,975,491 + +Present value of annual costs ($) 1,853,308,890 + +Present value of annual energy costs ($) 1,853,308,890 +Present value of annual energy nominal (kWh) 17,511,358,840 +LCOE Levelized cost of energy nominal (cents/kWh) 10.58 + +Present value of PPA revenue ($) 2,190,616,648 +Present value of annual energy nominal (kWh) 17,511,358,840 +LPPA Levelized PPA price nominal (cents/kWh) 12.51 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 +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 180,174,202 178,266,805 176,225,890 174,042,112 171,705,469 169,205,261 166,530,038 163,667,550 160,604,687 157,327,424 153,820,753 150,068,615 146,053,827 141,758,004 137,161,473 132,243,186 126,980,618 121,349,670 115,324,556 108,877,684 101,979,531 94,598,507 86,700,812 78,250,278 69,208,206 59,533,190 49,180,922 38,103,995 26,251,684 13,569,711 +Total state tax depreciation ($) 0 79,346,033 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 79,346,033 0 0 0 0 0 0 0 0 0 +equals: +State taxable income ($) 0 54,985,609 -11,916,468 -1,402,661 8,888,209 19,174,919 29,539,059 40,023,751 50,661,254 61,461,069 72,360,869 73,119,225 80,929,200 85,619,160 103,421,612 117,130,430 122,923,560 128,760,085 134,816,681 141,178,079 147,898,837 234,375,818 321,259,838 329,099,762 336,254,135 339,013,162 347,704,586 363,078,462 375,401,207 387,957,884 2,268,072,772 + +State income tax rate (frac) 0.0 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 +State tax benefit (liability) ($) 0 -2,501,845 542,199 63,821 -404,413 -872,459 -1,344,027 -1,821,081 -2,305,087 -2,796,479 -3,292,420 -3,326,925 -3,682,279 -3,895,672 -4,705,683 -5,329,435 -5,593,022 -5,858,584 -6,134,159 -6,423,603 -6,729,397 -10,664,100 -14,617,323 -14,974,039 -15,299,563 -15,425,099 -15,820,559 -16,520,070 -17,080,755 -17,652,084 -103,197,311 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -2,501,845 542,199 63,821 -404,413 -872,459 -1,344,027 -1,821,081 -2,305,087 -2,796,479 -3,292,420 -3,326,925 -3,682,279 -3,895,672 -4,705,683 -5,329,435 -5,593,022 -5,858,584 -6,134,159 -6,423,603 -6,729,397 -10,664,100 -14,617,323 -14,974,039 -15,299,563 -15,425,099 -15,820,559 -16,520,070 -17,080,755 -17,652,084 -103,197,311 +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 180,174,202 178,266,805 176,225,890 174,042,112 171,705,469 169,205,261 166,530,038 163,667,550 160,604,687 157,327,424 153,820,753 150,068,615 146,053,827 141,758,004 137,161,473 132,243,186 126,980,618 121,349,670 115,324,556 108,877,684 101,979,531 94,598,507 86,700,812 78,250,278 69,208,206 59,533,190 49,180,922 38,103,995 26,251,684 13,569,711 +Total federal tax depreciation ($) 0 79,346,033 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 158,692,066 79,346,033 0 0 0 0 0 0 0 0 0 +equals: +Federal taxable income ($) 0 52,483,763 -11,374,269 -1,338,840 8,483,795 18,302,460 28,195,032 38,202,670 48,356,167 58,664,590 69,068,449 69,792,301 77,246,921 81,723,488 98,715,929 111,800,995 117,330,538 122,901,501 128,682,522 134,754,477 141,169,440 223,711,719 306,642,516 314,125,722 320,954,572 323,588,064 331,884,027 346,558,392 358,320,452 370,305,800 2,164,875,461 + +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 -11,021,590 2,388,596 281,156 -1,781,597 -3,843,517 -5,920,957 -8,022,561 -10,154,795 -12,319,564 -14,504,374 -14,656,383 -16,221,853 -17,161,933 -20,730,345 -23,478,209 -24,639,413 -25,809,315 -27,023,330 -28,298,440 -29,645,582 -46,979,461 -64,394,928 -65,966,402 -67,400,460 -67,953,493 -69,695,646 -72,777,262 -75,247,295 -77,764,218 -454,623,847 + +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 1,120,179,287 0 0 0 0 0 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 1,120,179,287 0 0 0 0 0 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 ($) 2,573,917,169 2,546,668,643 2,517,512,721 2,486,315,884 2,452,935,268 2,417,218,009 2,379,000,542 2,338,107,853 2,294,352,675 2,247,534,634 2,197,439,331 2,143,837,357 2,086,483,244 2,025,114,343 1,959,449,620 1,889,188,366 1,814,008,824 1,733,566,714 1,647,493,656 1,555,395,484 1,456,850,441 1,351,407,244 1,238,583,023 1,117,861,108 988,688,657 850,474,136 702,584,598 544,342,792 375,024,060 193,853,016 0 +Debt interest payment ($) 0 180,174,202 178,266,805 176,225,890 174,042,112 171,705,469 169,205,261 166,530,038 163,667,550 160,604,687 157,327,424 153,820,753 150,068,615 146,053,827 141,758,004 137,161,473 132,243,186 126,980,618 121,349,670 115,324,556 108,877,684 101,979,531 94,598,507 86,700,812 78,250,278 69,208,206 59,533,190 49,180,922 38,103,995 26,251,684 13,569,711 +Debt principal payment ($) 0 27,248,526 29,155,923 31,196,837 33,380,616 35,717,259 38,217,467 40,892,690 43,755,178 46,818,040 50,095,303 53,601,974 57,354,113 61,368,901 65,664,724 70,261,254 75,179,542 80,442,110 86,073,058 92,098,172 98,545,044 105,443,197 112,824,221 120,721,916 129,172,450 138,214,522 147,889,538 158,241,806 169,318,732 181,171,043 193,853,016 +Debt total payment ($) 0 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 +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 314,505,843 325,042,403 333,515,295 341,622,386 349,572,454 357,436,385 365,245,854 373,020,869 380,757,822 388,380,359 385,632,044 389,689,880 390,365,053 403,871,682 412,983,969 413,858,811 414,432,768 414,858,417 415,194,701 415,468,587 415,701,382 415,858,345 415,800,573 414,504,413 408,221,368 407,237,775 412,259,383 413,505,203 414,209,568 2,281,642,483 +Debt total payment ($) 0 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 207,422,728 +DSCR (pre-tax) 0.0 1.52 1.57 1.61 1.65 1.69 1.72 1.76 1.80 1.84 1.87 1.86 1.88 1.88 1.95 1.99 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.97 1.96 1.99 1.99 2.0 11.0 + +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: 194.79 MUSD + Royalty Holder Average Annual Revenue: 14.70 MUSD/yr + Royalty Holder Total Revenue: 499.80 MUSD diff --git a/tests/examples/Fervo_Project_Cape-7.txt b/tests/examples/Fervo_Project_Cape-7.txt new file mode 100644 index 000000000..33513db14 --- /dev/null +++ b/tests/examples/Fervo_Project_Cape-7.txt @@ -0,0 +1,155 @@ +# Case Study: 500 MWe EGS Project Modeled on Fervo Cape Station Phase II (September 2026 Update) +# See documentation: https://softwareengineerprogrammer.github.io/GEOPHIRES/Fervo_Project_Cape-7.html +# September 2026 Update: subsurface design aligned with the Fervo 3.0 well design disclosed for Cape Station Phase II +# (Fervo Energy Q2 2026 earnings call, 2026-08-12). The case study remains a second-of-a-kind (SOAK) analog; +# the wellfield is re-sized for the 3.0 design rather than matched to Fervo's as-built well count, and +# lateral drilling cost is now modeled explicitly rather than folded into the vertical well cost. + +# *** 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, .03, -- US CPI-U was 3.4% year over year in July 2026 (core 2.5%) (BLS, 2026b), down from 4.2% in May 2026, when the energy index was up 23.5% year over year (BLS, 2026a). 3.0% is between core and headline and above the Federal Reserve's 2% target, a conservative assumption for a 30-year horizon. The February 2026 Update used 2.7%, US inflation as of December 2025. Note: [2024b ATB models lower inflation](https://atb.nrel.gov/electricity/2024b/definitions#inflation). GEOPHIRES holds fixed O&M flat in nominal terms; inflation affects capital cost escalation during construction and the nominal discount rate. + +Starting Electricity Sale Price, 0.115, -- Midpoint of the $100-130/MWh range Fervo reports for contracts currently under negotiation, of which approximately half are with hyperscale data center buyers (Fervo Energy, 2026f; 2026g). Above the prices implied by Fervo's existing contracts: Corsac Station's 15-year PPA at $107/MWh (CTVC, 2025), and Fervo's company-wide revenue backlog of approximately $7.2B across 658 MW of binding PPAs (Fervo Energy, 2026g), which implies roughly $91/MWh assuming 15-year terms at 91.3% capacity factor. Pricing for the subsequent 396 MW Google PPA was not disclosed (Fervo Energy, 2026b). The SOAK project is assumed to contract in 2026 or later for a 2030 commercial operation date. The February 2026 Update used $95/MWh, aligned 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.00183, -- $1.83/MWh per year. GEOPHIRES supports only linear (additive) escalation; this value reproduces the 15-year present value, at the case study WACC of approximately 8.4%, of a 1.5% per year compounding escalator on the $115/MWh starting price. 1.5% is the escalator in Ormat's 2025 Heber 1 geothermal PPA (CTVC, 2025); Fervo's PPA escalation terms are not disclosed. Reaches $140.6/MWh in operating year 15 versus $141.7/MWh under compounding. The February 2026 Update used $0.57/MWh per year, calibrated to reach $100/MWh at project year 11 from a $95/MWh start. +Ending Electricity Sale Price, 0.1406, -- Caps escalation at $140.6/MWh, the price reached in operating year 15, so that the price is held flat after the 15-year PPA term rather than continuing to escalate through the 30-year project lifetime. This represents an assumed post-contract re-contracting or merchant price equal to the final contract price. Note that this value does not directly determine price at the end of the project life, but rather acts as a cap on the price to which the starting price can escalate; the February 2026 Update set it to $1/kWh (no effective cap). +Electricity Escalation Start Year, 0, -- The first escalation step is applied in the second operating year, matching a PPA whose price escalates annually from the first anniversary of commercial operation. The February 2026 Update used 1, which GEOPHIRES applies as the first step in the third operating year. + +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. Reference: Fervo closed a $421.4M non-recourse project debt facility (the Project Granite Facility) for Cape Station Phase I in Q1 2026 (Fervo Energy, 2026a; 2026c). Against the approximately $7,000/kW FOAK cost of the roughly 100 MW Phase I (Fervo Energy, 2026d; 2026e), that is roughly 60% debt. Fervo also entered into an agreement with Liberty Mutual to monetize Phase I tax credits, providing additional non-sponsor capital (Fervo Energy, 2026c). +Discount Rate, 0.12, -- Typical discount rates for higher-risk projects may be 12–15%. This is a real (inflation-adjusted) rate; the SAM Economic Model discounts nominal cash flows at the equivalent nominal rate, (1 + discount rate) × (1 + inflation rate) − 1. +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 the first construction year. The 4-year schedule merges the first two years (exploration and early development) of the 5-year DOE-ATB hybrid schedule, so the equity-only share of overnight capital cost (4.1%) is unchanged from the February 2026 Update, which was equity-only for the first 2 of 5 construction years (ATB). + +Construction Years, 4, -- Fervo's FOAK timeline at Cape Station ran from ground breaking in September 2023 (Fervo Energy, 2023b) to expected full-scale production in 2028 (Fervo Energy, 2025c), which the February 2026 Update modeled as 5 years. Phase I (approximately 100 MWe in three 33 MWe GeoBlocks) reached first power in September 2026, three years after ground breaking, with contractual COD expected by October 1, 2026 for the first GeoBlock and by January 1, 2027 for the other two; Phase II (400 MWe) is under construction with expected COD in 2028 (Fervo Energy, 2026e). A SOAK developer is modeled with one fewer year: Fervo reports Phase I went from site preparation to a constructed power unit in under two years, and estimates that at steady state each of its three rigs could drill about 1.5 wells per month (Fervo Energy, 2026f), a pace at which this case study's 84-well initial campaign takes roughly 19 months. See [GEOPHIRES documentation](SAM-EM_Multiple-Construction-Years.html) for details on how construction years affect CAPEX, IRR, and other calculations. + +# ATB advanced scenario (5-year) +# Construction CAPEX Schedule, 0.09,0.28,0.1,0.34,0.28 + +# DOE scenario (alternative, 5-year) +# Construction CAPEX Schedule, 0.014,0.027,0.137,0.274,0.548 + +# DOE-ATB hybrid scenario (5-year, February 2026 Update) +# Construction CAPEX Schedule, 0.014,0.027,0.139,0.431,0.389 + +# DOE-ATB hybrid scenario compressed to 4 years: first two years of the 5-year schedule (exploration and early development) merged +Construction CAPEX Schedule, 0.041,0.139,0.431,0.389 + +Investment Tax Credit Rate, 0.3, -- Statutory rate; Geothermal Drilling and Completions Apprenticeship Program ensures compliance with ITC labor requirements (Southern Utah University, 2024). GEOPHIRES's SAM Single Owner PPA model applies this rate to total installed cost, which includes the interconnection cost entered under One-time Flat License Fees Etc. Under IRC §48(a)(8) qualified interconnection property is in the ITC basis only for energy property of 5 MW or less, so the model overstates the credit by approximately $87M (30% of the $289M interconnection cost including its inflation and interest during construction); an equivalent rate of 0.2768 on total installed cost would remove it and reduces IRR by about 1.4 points. The statutory rate is retained for clarity because the overstatement is offset in direction, and likely in magnitude, by an unmodeled benefit on the same line item: in non-ISO balancing authorities such as PacifiCorp, network upgrade costs, the majority of interconnection cost, are often refunded to the interconnection customer over up to 20 years with interest once the plant reaches commercial operation (Seel et al., 2026). Taxpayer-owned point-of-interconnection facilities may also qualify as integral power conditioning and transfer equipment regardless of project size, which would reduce the overstatement further. +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). Note: Fervo states that drilling, completion and well pad facilities are about half of pre-COD CAPEX and surface power generation equipment about half (Fervo Energy, 2026f); this case study yields roughly 47% subsurface (drilling, completion, stimulation and gathering) / 53% surface plant, excluding interconnection and exploration, so the plant cost may be modestly overstated and/or the well costs understated relative to Fervo's actuals. +Exploration Capital Cost, 56, -- Equivalent to 2024b ATB NF-EGS conservative scenario exploration assumption of 5 full-size wells (NREL, 2025) at this case study's all-in well cost of $8.48M, plus $1M for geophysical and field work, plus 15% contingency, plus 12% indirect costs: (5 × $8.48M + $1M) × 1.15 × 1.12 = $55.9M. The February 2026 Update used $30M, derived the same way from a $4.65M well. Recompute this value whenever the per-well drilling and completion cost changes. + +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.72, -- Applied to both the vertical correlation and the lateral cost. Yields $8.5M/well all-in (vertical $4.40M + lateral $4.08M, each including 5% indirect costs) versus $11.8M at factor 1.0 for the 3.06 km TVD, 7,500 ft lateral geometry. Calibration: Fervo's first Cape Station well ($9.4M) and best Phase I well ($4.8M), both 5,000 ft lateral 2.0-design wells, correspond to factors of 1.14 and 0.58 respectively against the same correlation with the lateral costed explicitly. The SOAK value of 0.72 is the geometric mean of the ATB-aligned baseline (0.9) and Fervo's best demonstrated well (0.58), decomposing as 0.70 for a fast follower's learning position times 1.029 for the temperature adjustment at 221℃ (NREL 2025 drilling cost curves). It is about 4% above the INTERMEDIATE1 drilling technology scenario (US DOE, 2019) at factor 1.0 ($8.1M/well for the same geometry, with the lateral at that scenario's per-meter cost). Fervo cites batch drilling among its drilling efficiency gains, reports a record 21-day spud-to-total-depth time on the Sawtooth 7 well, and has trialed rotary steerable systems on deeper wells (Fervo Energy, 2026a; 2026f). Reference points: 0.58 ($6.8M/well) for Fervo-demonstrated performance; 0.9 ($10.6M/well) for the unadjusted ATB baseline; Latimer's stated target of under $4M/well (Latimer, 2025) corresponds to roughly 0.35 and is treated as a NOAK aspiration rather than a SOAK comparable. Note: the February 2026 Update used 0.9 on the vertical correlation only with the lateral folded in, which yielded $4.65M/well at 2.68 km; against the lateral-inclusive correlation that was an effective factor of about 0.58, i.e. Fervo's best demonstrated well rather than a conservative SOAK assumption. See [Sensitivity Analysis](#sensitivity-analysis-section) for effect of different drilling costs on results. +All-in Nonvertical Drilling Costs, 2360, -- USD per meter of lateral before the adjustment factor and indirect costs. Value from the 2025 NREL geothermal drilling cost curve (Akindipe and Witter, 2025) for the vertical large diameter baseline correlation at 3.06 km, as computed by the SWS Geothermal Platform drilling cost calculator; the calculator assumes the same per-meter cost for lateral and vertical sections. Scaled by the Well Drilling and Completion Capital Cost Adjustment Factor and by Multilaterals Cased. +Multilaterals Cased, True, -- Fervo's laterals are cased and cemented for plug-and-perf multistage stimulation (Norbeck et al., 2024). GEOPHIRES halves the lateral cost when this is False (the default), on the assumption that casing and cementing are 50% of drilling cost. + +Reservoir Stimulation Capital Cost per Fracture Surface Area, 0.875, -- USD per square meter of nominal (single-face, planar) fracture area, before adjustment factor, indirect costs and contingency; GEOPHIRES multiplies by Fracture Width × Fracture Height × Number of Fractures per Stimulated Well × number of stimulated wells. Equivalent to $4M per 12-stage 5,000 ft lateral (150 fractures) and $6M per 18-stage 7,500 ft lateral (225 fractures) at the case study fracture geometry, i.e. the same per-stage calibration used in the February 2026 Update: high-intensity U.S. shale wells (Baytex Energy, 2024; Quantum Proppant Technologies, 2020), 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. Fervo also reports sourcing equipment to pump at the higher pressures required by the deeper Phase II wells (Fervo Energy, 2026f). Parameterizing by area rather than per well makes stimulation cost track fracture count and geometry automatically in sensitivity and variant scenarios. Note that all-in costs per well are higher than the direct cost because they include indirect costs and contingency. See [Sensitivity Analysis](#sensitivity-analysis-section) for effect of different stimulation costs on results. +Reservoir Stimulation Capital Cost per Production Well, -1, -- Indicates that production wells are stimulated (in addition to injection wells, which GEOPHIRES stimulates by default); the per-area cost is then apportioned across all wells. + +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/). + +One-time Flat License Fees Etc, 250, -- Grid interconnection cost (point-of-interconnection facilities plus transmission network upgrades), $500/kW for 500 MWe. Based on PacifiCorp cluster-study cost estimates for recent geothermal interconnection requests in Beaver and Millard Counties, Utah: $417/kW (ERIS) to $500/kW (NRIS) for a 40 MW request in Beaver County (2021 study) and $450/kW for a 40 MW request in Millard County (2022 study); recent requests of 250-750 MW across PacifiCorp, BPA and Duke averaged $426/kW, and projects with high interconnection costs cluster along the same transmission lines, especially in southern Utah and Wyoming (Seel et al., 2026). The NRIS figure is used because the PPA structure requires firm delivery. Fervo's actual Cape Station interconnection cost is not publicly disclosed; an MLQ.ai analysis of Fervo's IPO filing, reported by Utility Dive, puts Fervo's Cape Station Phase II interconnection and transmission rights at approximately 290 MW against 384 MW of contracted capacity (Utility Dive, 2026). GEOPHIRES adds this parameter to overnight capital cost without indirect cost or contingency loading, spreads it over the construction schedule with inflation and interest during construction, and includes it in the depreciable and property-tax basis. Not modeled: in non-ISO balancing authorities such as PacifiCorp, network upgrade costs paid up front are often refunded to the interconnection customer over up to 20 years with interest once the plant reaches commercial operation (Seel et al., 2026), so the value here is conservative. The February 2026 Update did not include interconnection cost (see the Discussion section of the case study documentation). +Annual License Fees Etc, 21, -- Long-term firm point-to-point transmission service for 500 MW, $M per year, held flat in nominal terms by GEOPHIRES. PacifiCorp OATT Schedule 7 (firm point-to-point) was $32,029/MW-year and Schedule 1 (scheduling, system control and dispatch) $801/MW-year effective June 2017 (PacifiCorp, 2017); escalated at 3% per year to 2026 (approximately $42,800/MW-year, an assumption pending the 2026 Transmission Formula Annual Update in FERC docket ER26-2546) and applied to the 500 MW contracted capacity. The unescalated 2017 rate gives $16.4M/year; reserving the 525 MW maximum net output gives $22.5M/year. For comparison, BPA's long-term firm point-to-point rate for fiscal years 2024-2025 was $1.648/kW-month, about $20,000/MW-year (BPA, 2026). Assumes delivery within or at the edge of PacifiCorp's system; delivery to a CAISO offtaker across an intervening system (e.g. the Intermountain HVDC line or NV Energy) would add a second firm reservation and charge. Transmission losses (in kind) are not included. Insurance premiums, which the case study documentation lists as unmodeled, are not included in this value. + +Royalty Rate, 0.0175, -- The BLM royalty structure is 1.75% of gross proceeds from electricity sales for the first 10 years of production (Code of Federal Regulations, 2024). +Royalty Rate Escalation Start Year, 11, -- After the first 10 years of production, the royalty rate escalates to 3.5%. +Royalty Rate Escalation, 0.0175, -- Escalation at Year 11 from 1.75% to 3.5%. +Royalty Rate Maximum, 0.035, -- No further escalation beyond 3.5%. + + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Gen 2 ORC units (Turboden, 2025). +Plant Lifetime, 30, -- Sets the project economic horizon, aligned with Fervo's anticipated 30-year well life (Fervo Energy, 2025a). Modeling Distinction: While Fervo projects physical wellbore integrity for 30 years, GEOPHIRES simulates "redrilling events" to model thermal management of the reservoir volume. This treats the 30-year lifespan as an aggregate of shorter-lived thermal cycles delineated by discrete redrilling events occurring at intervals dictated by the Maximum Drawdown parameter. The modeled cost of each redrilling event is equivalent to the drilling and stimulation cost of the entire wellfield, serving as a conservative cost proxy for the major interventions (e.g., sidetracking and stimulating laterals into fresh rock, or drilling new wells if necessary) required to sustain the PPA target against thermal depletion. Fervo describes its approach as a makeup-well drilling program in which later wells benefit from cumulative learnings (Fervo Energy, 2026f). + +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, 3.06, -- Depth at which the segmented gradient yields a bottom-hole temperature of approximately 221℃ (430℉), the average reservoir temperature design point of the Fervo 3.0 well design used for Cape Station Phase II (Fervo Energy, 2026f; 2026g). The previous value of 2.68 km was extrapolated from surface temperature, gradient, and average production temperature of shallower and deeper producers in Singh et al., 2025, corresponding to the roughly 400℉ Phase I design. Fervo reports 460℉ at the Sawtooth 7 well (Fervo Energy, 2026a), so 430℉ is not an upper bound for the resource. + +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 Model, 1, -- See the [reservoir engineering calibration section](#res-eng-params-calibration-section) for additional details. +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, 225, -- The model assumes an Extreme Limited Entry stimulation design (Fervo Energy, 2023a) utilizing 15 clusters per stage (derived from Singh et al., 2025) and 81–85% stimulation success rate per 2024b ATB Moderate Scenario (NREL, 2025). The previous 12-stage count for a 5,000 ft lateral is scaled proportionally to 18 stages for the 7,500 ft lateral of the Fervo 3.0 design, preserving the stage length of approximately 417 ft. Fervo attributes more stimulated reservoir volume per well and a more gradual decline profile to the longer laterals (Fervo Energy, 2026f). +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, 100, -- 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, -- Fervo states that "long-term modeling, calibrated to early field data, predicts high circulation recapture rates" and that previous studies have shown recapture rates of 80% to 99% ([Geothermal Mythbusting: Water Use and Impacts](https://fervoenergy.com/geothermal-mythbusting-water-use-and-impacts/); Fervo Energy, 2025a). The 1% loss fraction corresponds to the top of that range. 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, .913, -- (DeGolyer and MacNaughton, 2024) +Plant Outlet Pressure, 2000 psi, -- McClure, 2024; Singh et al., 2025. +Circulation Pump Efficiency, 0.80 + +# *** Well Bores Parameters *** + +Number of Production Wells, 50, -- Number of production wells required to produce net generation greater than the PPA minimum and total generation less than nameplate capacity (11 × 60 MWe Gen 2 ORCs = 660 MWe gross; the February 2026 Update assumed 10 units, which cannot deliver 500 MWe net at a parasitic load of 20% or more). Reduced from 56 in the February 2026 Update; Fervo reports approximately 27% more power output for the same amount of steel at 430℉ versus 400℉ (Fervo Energy, 2026f), partly offset by the higher parasitic load now modeled (see Injectivity Index). 49 producers also satisfies the PPA minimum (500.2 MWe) but with lower NPV; 50 provides an 11 MWe margin. The resulting 84 initial wells (50 producers, 34 injectors) for 500 MWe, about 0.17 wells per MWe, is fewer wells per MWe than the up to 80 wells (0.20 per MWe) Fervo anticipates for the 400 MWe Phase II program (Fervo Energy, 2026f). The resulting average of ~12.6 MW gross (~10.5 MWe net) per production well is below the 15 MW gross power per production well Fervo shows for the 3.0 design (Fervo Energy, 2026g), which appears to require flow rates above the 107 kg/s modeled here (see Production Flow Rate per Well). +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, 7500 feet, -- Lateral length of the Fervo 3.0 well design used for Cape Station Phase II (Fervo Energy, 2026f; 2026g); Phase I used 5,000 ft laterals, the target length given in the 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. +Well Geometry Configuration, 4, -- L configuration: vertical section to reservoir depth plus one lateral per well. Required for GEOPHIRES to cost the lateral explicitly. +Number of Multilateral Sections per Vertical Section, 1, -- One lateral per well (producers and injectors); GEOPHIRES multiplies by the total well count, so this does not need updating when the number of wells changes. The February 2026 Update set Number of Multilateral Sections to 0 and folded lateral cost into the vertical well cost via the adjustment factor; the lateral is now costed explicitly (see All-in Nonvertical Drilling Costs and Multilaterals Cased). Requires GEOPHIRES 3.16.1 or later. + +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 case study flow rate is chosen both as a conservative target for long-term sustainability and to achieve a more economically favorable drawdown and redrilling schedule. Pumping load is set by the Productivity and Injectivity Index parameters rather than by flow rate; see Injectivity Index. +# The ATB Advanced Scenario models sustained flow rates of 110 kg/s (NREL, 2024). + +Production Well Diameter, 7.825, -- Inner diameter of 8⅝ inch, 36 lb/ft casing. Fervo disclosed 8⅝ inch casing for the 3.0 well design (up from 7 inch in Phase I) (Fervo Energy, 2026f; 2026g); the February 2026 Update inferred 9⅝ inch (8.535 in ID) from a less specific announcement (Fervo Energy, 2025b). Casing weight is not disclosed; API IDs for 8⅝ in range from about 7.5 in (49 lb/ft) to 8.1 in (24 lb/ft). +Injection Well Diameter, 7.825, -- See Production Well Diameter + +Production Wellhead Pressure, 303 psi, -- Modeled at a constant 300 psi in Singh et al., 2025. We use a marginally uprated value to conform to GEOPHIRES's calculated minimum wellhead pressure and nominally align with the gradual increase in WHP for constant flow rates modeled by Singh et al. + +Injectivity Index, 1.20, -- Based on ATB Conservative Scenario (NREL, 2025) derated per analyses that suggest lower productivity/injectivitity (Xing et al., 2025; Yearsley and Kombrink, 2024), and further derated in this version (from 1.38) to yield a parasitic load of at least 20% of net generation. Norbeck (2026) reported observed parasitic loads of approximately 25–35% in Phase I operations with a goal of eventually reaching 15–20%; a SOAK case is not credibly modeled below the low end of that goal. The February 2026 Update's ~16% aligned with the 16.7% ceiling implied by Fervo's Phase II procurement of 480 MW gross for 400 MW net, which is a design target rather than observed performance. Result: 21.1% initial and 20.8% average pumping power relative to net generation (17.2% relative to gross generation). The production-side pressure drop is dominated by flow ÷ Productivity Index; the injection side becomes pump-limited once flow ÷ Injectivity Index exceeds the plant outlet pressure, which occurs below an Injectivity Index of roughly 1.38 at 107 kg/s per producer. +Productivity Index, 0.98, -- See Injectivity Index. Derated from 1.13 in proportion with the Injectivity Index (ratio 1.22 preserved). + +Ramey Production Wellbore Model, True, -- Ramey's model estimates the geofluid temperature drop in production wells +Injection Temperature, 53.6, -- Calibrated with GEOPHIRES model-calculated reinjection temperature (Beckers and McCabe, 2019). Close to upper bound of Project Red injection temperatures (75–125℉; 23.89–51.67℃) (Norbeck and Latimer, 2023). Note: GEOPHIRES enforces a thermodynamic optimum that overrides higher values, such as the 85°C ORC outlet temperature specified in Cape Station's plant design (DeGolyer and MacNaughton, 2024) (intended for silica scaling mitigation), resulting in a "maximum theoretical power" scenario. Support for higher reinjection temperatures may be added in future GEOPHIRES versions. +Injection Wellbore Temperature Gain, 3, -- Empirical estimate for high-flow rate wells where rapid fluid velocity minimizes heat uptake during descent (Ramey, 1962). + +Maximum Drawdown, 0.0025, -- This value represents the fractional drop in production temperature compared to the initial temperature that is allowed before the wellfield is redrilled. It is calibrated to maintain the PPA minimum net electricity generation requirement. It is a very small percentage because it is relative to the initial production temperature; the temperature quickly rises higher due to thermal conditioning and plateaus until breakthrough, so any drawdown relative to the initial value signals that the temperature has already declined from its stabilized peak. + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 + +Project Latitude, 38.506196 +Project Longitude, -112.918155 + +# *** ADDITIONAL REFERENCES (September 2026 Update) *** +# Fervo Energy. (2026a, August 12). Fervo Energy Reports Second Quarter 2026 Results. https://fervoenergy.com/fervo-energy-reports-second-quarter-2026-results/ +# Fervo Energy. (2026b, September 1). Fervo Energy and Google Sign 396 MW PPA. https://fervoenergy.com/fervo-energy-and-google-sign-396-mw-ppa/ +# Fervo Energy. (2026c, June 22). First Quarter 2026 Results. https://ir.fervoenergy.com/news-releases/news-release-details/fervo-energy-reports-first-quarter-2026-results +# Fervo Energy. (2026d, May 11). Form S-1/A (Amendment No. 3). Cape Station Phase I overnight capital cost of approximately $7,000/kW. https://www.sec.gov/Archives/edgar/data/1853868/000162828026033127/fervoenergy-sx1a3.htm +# Fervo Energy. (2026e, September 24). Fervo Energy Achieves First Power at Cape Station, a Landmark Moment for the Future of Enhanced Geothermal Systems. https://fervoenergy.com/fervo-energy-achieves-first-power-at-cape-station-a-landmark-moment-for-the-future-of-enhanced-geothermal-systems/ +# Fervo Energy. (2026f, August 12). Fervo Energy (FRVO) Q2 2026 Earnings Call Transcript. The Motley Fool, published August 19, 2026. https://www.fool.com/earnings/call-transcripts/2026/08/19/fervo-energy-frvo-q2-2026-earnings-call-transcript/ (webcast replay: https://edge.media-server.com/mmc/p/va49yxkc/) +# Fervo Energy. (2026g, August 12). Q2 2026 Quarterly Results [Earnings presentation]. https://ir.fervoenergy.com/static-files/28260ce5-2ac0-458e-bfb7-2d80f3709cae +# BLS. (2026a, June 10). Consumer Price Index - May 2026. https://www.bls.gov/news.release/archives/cpi_06102026.htm +# BLS. (2026b, August 12). Consumer Price Index - July 2026. https://www.bls.gov/news.release/archives/cpi_08122026.htm +# CTVC. (2025, September 2). The $783m PPA that keeps on drilling #260. https://www.ctvc.co/the-783m-ppa-that-keeps-on-drilling-260/ +# PacifiCorp. (2017, June 1). FAQ: Transmission and Ancillary Service Rate Changes. https://www.oasis.oati.com/PPW/PPWdocs/Rate_Update_FAQ_20170601.pdf +# BPA. (2026, June). BPA Facts (DOE/BP-5493). Transmission rates (fiscal years 2024-2025). https://www.bpa.gov/-/media/Aep/about/publications/general-documents/bpa-facts.pdf +# US DOE. (2019). GeoVision: Harnessing the Heat Beneath Our Feet (p. 163, drilling cost scenarios). https://www.energy.gov/sites/prod/files/2019/06/f63/GeoVision-full-report-opt.pdf +# Seel, J., Manderlink, N., Mulvaney Kemp, J., Rand, J., Gorman, W., Wiser, R., Cotton, W., Porter, K. (2026, February). Generator Interconnection Costs to the Transmission System in non-ISO Balancing Authorities. Lawrence Berkeley National Laboratory. https://eta-publications.lbl.gov/sites/default/files/2026-02/lbnl_2026.02.23_ba_interconnection_costs.pdf +# Utility Dive. (2026, June 5). Fervo Energy faces transmission constraints in the West, analysts say. https://www.utilitydive.com/news/fervo-energy-geothermal-transmission-constraints/822141/ 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_7_graphs.py similarity index 87% rename from tests/geophires_docs_tests/test_generate_fervo_project_cape_5_graphs.py rename to tests/geophires_docs_tests/test_generate_fervo_project_cape_7_graphs.py index 53ba49cc0..c54b7e7c4 100644 --- a/tests/geophires_docs_tests/test_generate_fervo_project_cape_5_graphs.py +++ b/tests/geophires_docs_tests/test_generate_fervo_project_cape_7_graphs.py @@ -1,17 +1,18 @@ 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_docs.generate_fervo_project_cape_7_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): +class FervoProjectCape7GraphsTestCase(BaseTestCase): def test_get_redrilling_event_indexes(self) -> None: for example_id in [ + 'Fervo_Project_Cape-7', 'Fervo_Project_Cape-5', # 'Fervo_Project_Cape-6' # TODO (requires tuning) ]: diff --git a/tests/geophires_x_tests/test_fervo_project_cape_5.py b/tests/geophires_x_tests/test_fervo_project_cape_5.py index 99b8c64b6..32fd1fecb 100644 --- a/tests/geophires_x_tests/test_fervo_project_cape_5.py +++ b/tests/geophires_x_tests/test_fervo_project_cape_5.py @@ -2,13 +2,11 @@ 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 @@ -69,7 +67,7 @@ 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. + TODO consolidate with src/geophires_docs/generate_fervo_project_cape_7_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. """ @@ -156,16 +154,10 @@ def test_case_study_documentation(self): Useful for catching when minor updates are made to the case study which need to be manually synced to the documentation. - Note that the relevant Markdown is largely generated from input and result in - src/geophires_docs/generate_fervo_project_cape_5_md.py, meaning this test is somewhat redundant, - but it still serves as a useful sanity check for the generated documentation. + docs/Fervo_Project_Cape-5.md is a static copy of the documentation generated for the February 2026 Update of + the case study, which Fervo_Project_Cape-7 supersedes. """ - 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') ) diff --git a/tests/geophires_x_tests/test_fervo_project_cape_7.py b/tests/geophires_x_tests/test_fervo_project_cape_7.py new file mode 100644 index 000000000..d82e75c99 --- /dev/null +++ b/tests/geophires_x_tests/test_fervo_project_cape_7.py @@ -0,0 +1,667 @@ +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_7_md +from geophires_docs.fervo_project_cape_7_scenarios import FlowRateParametricRow +from geophires_docs.fervo_project_cape_7_scenarios import get_fpc7_flow_rate_parametric_row +from geophires_docs.fervo_project_cape_7_scenarios import get_fpc7_flow_rate_parametric_summary +from geophires_docs.fervo_project_cape_7_scenarios import load_fpc7_flow_rate_parametric +from geophires_x.GeoPHIRESUtils import quantity +from geophires_x.GeoPHIRESUtils import sig_figs +from geophires_x.Parameter import HasQuantity +from geophires_x.ParameterUtils import COMMENT_PARAMETER_NAME_PREFIX +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 FervoProjectCape7TestCase(BaseTestCase): + + def test_internal_consistency(self): + + fpc7_result: GeophiresXResult = GeophiresXResult( + self._get_test_file_path('../examples/Fervo_Project_Cape-7.out') + ) + fpc7_input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.txt') + ) + fpc7_input_params_dict: dict[str, Any] = self._get_input_parameters(fpc7_input_params) + + def _q(dict_val: str) -> PlainQuantity: + spl = dict_val.split(' ') + return quantity(float(spl[0]), spl[1]) + + lateral_length_q = _q(fpc7_input_params_dict['Nonvertical Length per Multilateral Section']) + frac_sep_q = quantity(float(fpc7_input_params_dict['Fracture Separation']), 'meter') + number_of_fracs_per_well = int(fpc7_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(fpc7_result) + number_of_fracs = int(fpc7_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(fpc7_input_params_dict['Number of Production Wells']) + input_num_injection_wells: int = math.ceil( + float(fpc7_input_params_dict['Number of Injection Wells per Production Well']) * input_num_production_wells + ) + fpc7_input_params_number_of_wells = input_num_production_wells + input_num_injection_wells + self.assertEqual(result_number_of_wells, fpc7_input_params_number_of_wells) + + num_redrills = fpc7_result.result['ENGINEERING PARAMETERS']['Number of times redrilling']['value'] + total_wells_over_project_lifetime = (1 + num_redrills) * fpc7_input_params_number_of_wells + + additional_future_permitted_wells_factor = ( + 1.25 # speculative - see documentation reference source for assumptions + ) + base_blm_permitted_wells = 320 + + self.assertLess( + total_wells_over_project_lifetime, base_blm_permitted_wells * additional_future_permitted_wells_factor + ) + + @staticmethod + def _get_input_parameters( + params: GeophiresInputParameters, include_parameter_comments: bool = False, include_line_comments: bool = False + ) -> dict[str, Any]: + """ + TODO consolidate with src/geophires_docs/generate_fervo_project_cape_7_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_PARAMETER_NAME_PREFIX}{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_7_results_against_reference_values(self): + """ + Asserts that the results conform to some of the key reference values claimed in + docs/Fervo_Project_Cape-7.md{.jinja}. + """ + + r = GeophiresXClient().get_geophires_result( + GeophiresInputParameters(from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.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, 520) + + max_total_gen = r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Maximum Total Electricity Generation'][ + 'value' + ] + self.assertGreater(max_total_gen, 600) + self.assertLess(max_total_gen, 660) # Exceeding 660 MW would require a 12th 60 MWe Gen 2 ORC unit + + lcoe = r.result['SUMMARY OF RESULTS']['Electricity breakeven price']['value'] + self.assertGreater(lcoe, 9.5) + self.assertLess(lcoe, 11.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 + total_wells_over_project_lifetime = self._number_of_wells(r) * (1 + redrills) + self.assertLessEqual(total_wells_over_project_lifetime, max_phase_2_permitted_wells) + self.assertGreater(total_wells_over_project_lifetime, max_phase_2_permitted_wells * 0.75) + + # Between Fervo's best demonstrated Phase I well (adjustment factor 0.58) and the unadjusted ATB baseline (0.9) + well_cost = r.result['CAPITAL COSTS (M$)']['Drilling and completion costs']['value'] / self._number_of_wells(r) + self.assertLess(well_cost, 10.6) + self.assertGreater(well_cost, 6.8) + + pumping_power_pct = r.result['SURFACE EQUIPMENT SIMULATION RESULTS'][ + 'Initial pumping power/net installed power' + ]['value'] + self.assertGreater(pumping_power_pct, 20) + self.assertLess(pumping_power_pct, 30) + + 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 that the relevant Markdown is largely generated from input and result in + src/geophires_docs/generate_fervo_project_cape_7_md.py, meaning this test is somewhat redundant, + but it still serves as a useful sanity check for the generated documentation. + """ + + def generate_documentation_markdown() -> None: + generate_fervo_project_cape_7_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-7.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-7.out')) + + expected_drilling_cost_MUSD_per_well = 8.48 + # 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_USD_per_m2 = 0.875 + expected_all_in_stim_cost_MUSD_per_well = 7.25 + 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_USD_per_m2, + inputs_in_markdown['Reservoir Stimulation Capital Cost per Fracture Surface Area']['value'], + ) + self.assertEqual( + 'USD/m**2', inputs_in_markdown['Reservoir Stimulation Capital Cost per Fracture Surface Area']['unit'] + ) + self.assertEqual( + 'Stimulated', inputs_in_markdown['Reservoir Stimulation Capital Cost per Production Well']['value'] + ) + + class _Q(HasQuantity): + def __init__(self, vu: dict[str, Any]): + self.value = vu['value'] + + # https://stackoverflow.com/questions/2280334/shortest-way-of-creating-an-object-with-arbitrary-attributes-in-python + self.CurrentUnits = type('', (), {})() + + self.CurrentUnits.value = vu['unit'] + + capex_q = _Q(results_in_markdown['Total CAPEX']).quantity() + markdown_capex_USD_per_kW = ( + capex_q.to('USD').magnitude + / _Q(results_in_markdown['Maximum Net Electricity Generation']).quantity().to('kW').magnitude + ) + self.assertAlmostEqual( + sig_figs(markdown_capex_USD_per_kW, 2), results_in_markdown['Total CAPEX: $/kW']['value'] + ) + + field_mapping = { + 'LCOE': 'Electricity breakeven price', + 'Project capital costs: Total CAPEX': 'Total CAPEX', + 'Well Drilling and Completion Costs': 'Drilling and completion costs per well', + 'Well Drilling and Completion Costs total': 'Drilling and completion costs', + 'Stimulation Costs total': 'Stimulation costs', + 'Reservoir Volume': 'Reservoir volume', + } + + ignore_keys = [ + 'Total CAPEX: $/kW', # See https://github.com/NREL/GEOPHIRES-X/issues/391 + 'Total fracture surface area per production well', + 'Stimulation Costs', # remapped to 'Stimulation Costs total' + ] + + example_result_values = {} + for key, _ in results_in_markdown.items(): + if key not in ignore_keys: + mapped_key = field_mapping.get(key) if key in field_mapping else key + entry = example_result._get_result_field(mapped_key) + if entry is not None and 'value' in entry: + entry['value'] = sig_figs(entry['value'], 3) + + example_result_values[key] = entry + + for ignore_key in ignore_keys: + if ignore_key in results_in_markdown: + del results_in_markdown[ignore_key] + + result_capex_USD_per_kW = ( + _Q(example_result._get_result_field('Total CAPEX')).quantity().to('USD').magnitude + / _Q(example_result._get_result_field('Maximum Net Electricity Generation')).quantity().to('kW').magnitude + ) + self.assertAlmostEqual(sig_figs(result_capex_USD_per_kW, 2), sig_figs(markdown_capex_USD_per_kW, 2)) + + num_prod_wells = inputs_in_markdown['Number of Production Wells']['value'] + self.assertEqual( + example_result.result['SUMMARY OF RESULTS']['Number of production wells']['value'], num_prod_wells + ) + + # Calculate expected total fractures based on input configuration + num_inj_per_prod = inputs_in_markdown['Number of Injection Wells per Production Well']['value'] + num_inj_wells = math.ceil(num_prod_wells * num_inj_per_prod) + total_wells = num_prod_wells + num_inj_wells + num_fracs_per_well = inputs_in_markdown['Number of Fractures per Stimulated Well']['value'] + + # Assuming all wells are stimulated + expected_total_fracs = total_wells * num_fracs_per_well + self.assertEqual( + expected_total_fracs, example_result.result['RESERVOIR PARAMETERS']['Number of fractures']['value'] + ) + + self.assertAlmostEqual( + example_result.result['RESERVOIR PARAMETERS']['Reservoir volume']['value'], + results_in_markdown['Reservoir Volume']['value'], + delta=1, # Tolerance for potential formatting/float parsing differences + ) + + additional_expected_stim_indirect_cost_frac = 0.00 + expected_stim_cost_total_MUSD = ( + expected_all_in_stim_cost_MUSD_per_well + * self._number_of_wells(example_result) + * (1.0 + additional_expected_stim_indirect_cost_frac) + ) + self.assertAlmostEqualWithinSigFigs( + expected_stim_cost_total_MUSD, + example_result.result['CAPITAL COSTS (M$)']['Stimulation costs']['value'], + num_sig_figs=3, + ) + + def test_flow_rate_parametric_data_matches_example_result(self) -> None: + """ + The flow rate parametric data is regenerated manually (see geophires_docs.fervo_project_cape_7_scenarios), so + this test fails when Fervo_Project_Cape-7 changes without the data being regenerated. + """ + input_params = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.txt') + ) + result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-7.out')) + base_flow_rate = float(self._get_input_parameters(input_params)['Production Flow Rate per Well']) + + base_row = get_fpc7_flow_rate_parametric_row(load_fpc7_flow_rate_parametric(), base_flow_rate) + expected_base_row = FlowRateParametricRow.from_result(base_flow_rate, result) + self.assertEqual(expected_base_row.redrills, base_row.redrills) + for field_name in ['avg_net_mw', 'min_net_mw', 'lcoe_cents_per_kwh', 'irr_pct', 'npv_musd']: + with self.subTest(field_name=field_name): + self.assertAlmostEqual(getattr(expected_base_row, field_name), getattr(base_row, field_name), places=2) + + def test_get_fpc7_flow_rate_parametric_summary(self) -> None: + def _row(flow: float, min_net_mw: float, redrills: int, irr_pct: float) -> FlowRateParametricRow: + return FlowRateParametricRow( + flow_kg_per_s=flow, + avg_net_mw=min_net_mw + 10, + min_net_mw=min_net_mw, + redrills=redrills, + lcoe_cents_per_kwh=10.0, + irr_pct=irr_pct, + npv_musd=300.0, + ) + + rows = [ + _row(90, 480, 1, 25.0), + _row(91, 490, 2, 22.0), + _row(92, 500, 2, 23.0), + _row(93, 510, 2, 23.3), + _row(94, 520, 2, 23.1), + _row(95, 530, 3, 21.0), + ] + summary = get_fpc7_flow_rate_parametric_summary(rows, 92, 500) + self.assertEqual( + [(90, 91, 1, 2), (94, 95, 2, 3)], + [ + (it.flow_below_kg_per_s, it.flow_at_kg_per_s, it.redrills_below, it.redrills_at) + for it in summary.redrilling_steps + ], + ) + self.assertEqual(92, summary.minimum_ppa_feasible_flow_rate_kg_per_s) + self.assertEqual(2, summary.base_redrills) + self.assertAlmostEqual(0.3, summary.max_irr_change_above_base_within_band_pct_pts, places=6) + + with self.assertRaises(ValueError): + # A flow rate with fewer redrilling events than the base case meets the PPA minimum. + get_fpc7_flow_rate_parametric_summary([_row(90, 500, 1, 25.0), *rows[1:]], 92, 500) + + with self.assertRaises(ValueError): + # Base case flow rate is not in the parametric data. + get_fpc7_flow_rate_parametric_summary(rows, 107, 500) + + def test_scenario_input_parameters(self) -> None: + input_params = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.txt') + ) + result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-7.out')) + # noinspection PyProtectedMember + previous_input_params, _ = generate_fervo_project_cape_7_md._get_fpc7_previous_version( + Path(self._get_test_file_path('../../')).absolute() + ) + scenario_params = generate_fervo_project_cape_7_md.get_fpc7_scenario_input_parameters( + input_params, result, previous_input_params + ) + + # Rates and utilization factors stated in the Investment Tax Credit Rate and Utilization Factor discussions and + # used in the sensitivity analysis, the reduced redrilling scenario's fracture height (+20%), and the February + # 2026 Update's PPA terms stated in the Modeling Overview. + self.assertEqual( + [ + {'Investment Tax Credit Rate': 0.2768}, + {'Utilization Factor': 0.867}, + {'Utilization Factor': 0.822}, + {'Fracture Height': 120.0}, + { + 'Starting Electricity Sale Price': 0.095, + 'Electricity Escalation Rate Per Year': 0.00057, + 'Ending Electricity Sale Price': 1, + 'Electricity Escalation Start Year': 1, + }, + ], + list(scenario_params.values()), + ) + + def test_result_values(self) -> None: + result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-7.out')) + values = generate_fervo_project_cape_7_md.get_result_values(result) + + self.assertEqual(round(result.result['ECONOMIC PARAMETERS']['Project NPV']['value'], 1), values['npv_musd']) + + # The SAM Single Owner PPA template's salvage percentage + self.assertEqual('50', values['salvage_value_pct_of_total_capex']) + + self.assertGreaterEqual(values['min_dscr_year'], 1) + self.assertGreater(float(values['min_dscr']), 1.0) + + # The base case redrills, and the remaining reservoir heat content in the annual profile is not reset by + # redrilling (see the Redrilling Assumptions discussion in the case study documentation). + self.assertGreater(values['number_of_times_redrilling'], 0) + self.assertIsNotNone(values['reservoir_heat_content_negative_from_year']) + self.assertGreater(float(values['final_year_pct_total_heat_mined']), 100.0) + + def test_signed_musd_display(self) -> None: + # noinspection PyProtectedMember + display = generate_fervo_project_cape_7_md._get_signed_musd_display + + self.assertEqual('-$13M', display(-12.6)) + self.assertEqual('$337M', display(337.31)) + self.assertEqual('$1,234M', display(1234.4)) + self.assertEqual('$0M', display(-0.4)) + + def test_previous_version_comparison_tables(self) -> None: + # noinspection PyProtectedMember + previous_input_params, previous_result = generate_fervo_project_cape_7_md._get_fpc7_previous_version( + Path(self._get_test_file_path('../../')).absolute() + ) + input_params = ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.txt') + ) + result = GeophiresXResult(self._get_test_file_path('../examples/Fervo_Project_Cape-7.out')) + + input_changes_md = generate_fervo_project_cape_7_md.generate_fpc7_previous_version_input_changes_table_md( + previous_input_params, input_params + ) + self.assertIn('| Reservoir Depth | 2.68 km | 3.06 km | Depth at which the reservoir reaches', input_changes_md) + self.assertIn('| Number of Multilateral Sections | 0 | Not set |', input_changes_md) + self.assertNotIn('| Fracture Separation |', input_changes_md) # Unchanged + + with self.assertRaises(ValueError): + # Changed parameters without a rationale are not silently omitted. + generate_fervo_project_cape_7_md.generate_fpc7_previous_version_input_changes_table_md( + previous_input_params, + ImmutableGeophiresInputParameters( + from_file_path=self._get_test_file_path('../examples/Fervo_Project_Cape-7.txt'), + params={'Fracture Separation': 12}, + ), + ) + + result_changes_md = generate_fervo_project_cape_7_md.generate_fpc7_previous_version_result_changes_table_md( + previous_result, result + ) + self.assertIn('| LCOE ($/MWh) | 85.0 |', result_changes_md) + self.assertIn('| Redrilling events | 3 |', result_changes_md) + + # Previous and this version compared to themselves + self.assertEqual( + '| Parameter | February 2026 Update | September 2026 Update | Rationale |\n|---|---|---|---|', + generate_fervo_project_cape_7_md.generate_fpc7_previous_version_input_changes_table_md( + input_params, input_params + ), + ) + self.assertNotIn( + 'pts', + generate_fervo_project_cape_7_md.generate_fpc7_previous_version_result_changes_table_md(result, result), + ) + + def parse_markdown_results_structured(self, markdown_text: str) -> dict: + """ + Parses result values from markdown into a structured dictionary with values and units. + """ + raw_results = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + + # Pattern to strip HTML tags and extract text content + html_tag_pattern = re.compile(r'<[^>]+>') + + try: + results_start_index = markdown_text.index('## Results') + search_area = markdown_text[results_start_index:] + + matches = table_pattern.findall(search_area) + + # Use key_ and value_ to avoid shadowing + for match in matches: + key_ = match[0].strip() + # Strip HTML tags from the key (e.g., LCOE -> LCOE) + key_ = html_tag_pattern.sub('', key_).strip() + value_ = match[1].strip() + if key_.lower() not in ('metric', 'parameter'): + raw_results[key_] = value_ + except ValueError: + print("Warning: '## Results' section not found.") + return {} + + # Consistency check + special_case_pattern = re.compile(r'LCOE\s*=\s*(\S+)\s*and\s*IRR\s*=\s*(\S+)') + special_case_match = special_case_pattern.search(markdown_text) + if special_case_match: + lcoe_text = special_case_match.group(1).rstrip('.,;') + lcoe_table_base = raw_results.get('LCOE', '').split('(')[0].strip() + if lcoe_text != lcoe_table_base: + raise ValueError( + f'LCOE mismatch: Text value ({lcoe_text}) does not match table value ({lcoe_table_base}).' + ) + + # Now, process the raw results into the structured format + structured_results = {} + # Use key_ and value_ to avoid shadowing + for key_, value_ in raw_results.items(): + if key_ in [ + 'After-tax IRR', + 'Average Production Temperature', + 'LCOE', + 'Maximum Total Electricity Generation', + 'Minimum Net Electricity Generation', + 'Maximum Net Electricity Generation', + 'Number of times redrilling', + 'Reservoir Volume', + 'Total CAPEX', + 'Total CAPEX: $/kW', + 'WACC', + 'Well Drilling and Completion Costs', + 'Stimulation Costs', + ]: + structured_results[key_] = self._parse_value_unit(value_) + + # Handle drilling and stimulation costs in format: "$464M total ($4.46M/well)" + for result_with_total_key in ['Well Drilling and Completion Costs', 'Stimulation Costs']: + entry = structured_results[result_with_total_key] + + unit_str = entry['unit'] + # unit_str is like "total; $4.46M/well" after _parse_value_unit processes "$464M total ($4.46M/well)" + # The entry['value'] is 464 (total MUSD) + # We need to extract per-well value from unit string + + # Parse per-well value from the parenthetical part + per_well_match = re.search(r'\$(\d+\.?\d*)M/well', unit_str) + if per_well_match: + per_well_value = float(per_well_match.group(1)) + # Store total in 'X total' key + structured_results[f'{result_with_total_key} total'] = { + 'value': entry['value'], + 'unit': 'MUSD', + } + # Update entry to be per-well value + entry['value'] = per_well_value + entry['unit'] = 'MUSD/well' + + return structured_results + + def parse_markdown_inputs_structured(self, markdown_text: str) -> dict: + """ + Parses all input values from all tables under the '## Inputs' section + of a markdown file into a structured dictionary. + """ + try: + # Isolate the content from "## Inputs" to the next "## " header + sections = re.split(r'(^###\s.*)', markdown_text, flags=re.MULTILINE) + inputs_header_index = next(i for i, s in enumerate(sections) if s.startswith('### Inputs')) + inputs_content = sections[inputs_header_index + 1] + except (StopIteration, IndexError): + print("Warning: '## Inputs' section not found or is empty.") + return {} + + raw_inputs = {} + table_pattern = re.compile(r'^\s*\|\s*(?!-)([^|]+?)\s*\|\s*([^|]+?)\s*\|', re.MULTILINE) + matches = table_pattern.findall(inputs_content) + + for match in matches: + key_ = match[0].strip() + value_ = match[1].strip() + if key_.lower() not in ('parameter', 'metric'): + raw_inputs[key_] = value_ + + structured_inputs = {} + for key_, value_ in raw_inputs.items(): + key_ = key_.replace(' ', ' ') + if key_ == 'Construction CAPEX Schedule': + parsed_value_unit = {'value': value_, 'unit': 'percent'} + else: + parsed_value_unit = self._parse_value_unit(value_) + structured_inputs[key_] = parsed_value_unit + + return structured_inputs + + # noinspection PyMethodMayBeStatic + def _parse_value_unit(self, raw_string: str) -> dict: + """ + A helper function to parse a string and extract a numerical value and its unit. + It handles various formats like currency, percentages, text, and scientific notation. + """ + # Split on open parenthesis '(', comma ',' (if not followed by digit), or HTML break tag ' cents/kWh) + match = re.match(r'^\$(\d+\.?\d*)/MWh$', clean_str) + if match: + value = float(match.group(1)) + return {'value': round(value / 10, 2), 'unit': 'cents/kWh'} + + # Billion dollar format ($X.XB -> MUSD) + match = re.match(r'^\$(\d+\.?\d*)B$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value * 1000, 'unit': 'MUSD'} + + # Million dollar format ($X.XM or $X.XM/unit) + match = re.match(r'^\$(\d+\.?\d*)M(\/.*)?$', clean_str) + if match: + value = float(match.group(1)) + unit_suffix = match.group(2) + unit = 'MUSD' + if unit_suffix: + unit = f'MUSD{unit_suffix}' + return {'value': value, 'unit': unit} + + # Dollar per kW format ($X/kW -> USD/kW) + match = re.match(r'^\$(\d+\.?\d*)/kW$', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'USD/kW'} + + # Dollar per square meter format, optionally followed by additional display data ($X/m² ... -> USD/m**2) + match = re.match(r'^\$(\d+\.?\d*)/m²', clean_str) + if match: + value = float(match.group(1)) + return {'value': value, 'unit': 'USD/m**2'} + + # 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'} diff --git a/tests/regenerate-example-result.sh b/tests/regenerate-example-result.sh index f236a0e02..d85016e58 100755 --- a/tests/regenerate-example-result.sh +++ b/tests/regenerate-example-result.sh @@ -58,9 +58,12 @@ fi if [[ $1 == "Fervo_Project_Cape-5" ]] then - python ../src/geophires_docs/generate_fervo_project_cape_5_docs.py - ./regenerate-example-result.sh Fervo_Project_Cape-6 +fi + +if [[ $1 == "Fervo_Project_Cape-7" ]] +then + python ../src/geophires_docs/generate_fervo_project_cape_7_docs.py if [ ! -f regenerate-example-result.env ] && [ -f regenerate-example-result.env.template ]; then echo "Creating regenerate-example-result.env from template..."