diff --git a/.bumpversion.cfg b/.bumpversion.cfg index de52943a4..c558d9ed0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.13.3 +current_version = 3.13.4 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index fefc72172..c74407c56 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.13.3 + version: 3.13.4 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eec2f5057..d92010248 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,7 @@ GEOPHIRES v3 (2023-2026) 3.13 ^^^^ -3.13: `SAM-EM: Support all End-Use Options (Direct-Use Heat and CHP/Cogeneration) and Absorption Chiller Surface Application `__; `documentation `__ | `Project Red 2026 Update `__; `documentation `__ | `release `__ +3.13: `SAM-EM: Support all End-Use Options (Direct-Use Heat and CHP/Cogeneration) and Absorption Chiller Surface Application `__; `documentation `__ | `Project Red 2026 Update `__; `documentation `__ | `release `__ 3.12 ^^^^ diff --git a/README.rst b/README.rst index 052dfb678..8cdf78287 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.3.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.13.4.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.3...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.13.4...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://softwareengineerprogrammer.github.io/GEOPHIRES diff --git a/docs/Fervo_Project_Red.md.jinja b/docs/Fervo_Project_Red.md.jinja index 89eb2c8a1..09f6edf7c 100644 --- a/docs/Fervo_Project_Red.md.jinja +++ b/docs/Fervo_Project_Red.md.jinja @@ -124,9 +124,9 @@ geometry for the Project Red simulation are detailed below: | :--- | :--- | :--- | | Number of Fractures | `{{ input_params['Number of Fractures'] }}` | {{ input_params_comments['Number of Fractures'] }} | | Fracture Shape | `{{ input_params['Fracture Shape'] }}` | {{ input_params_comments['Fracture Shape'] }} | -| Fracture Height | `{{ input_params['Fracture Height'] }}` | {{ input_params_comments['Fracture Height'] }} | -| Fracture Width | `{{ input_params['Fracture Width'] }}` | {{ input_params_comments['Fracture Width'] }} | -| Fracture Separation | `{{ input_params['Fracture Separation'] }} meter` | {{ input_params_comments['Fracture Separation'] }} | +| Fracture Height | `{{ input_params['Fracture Height'] | replace("foot", "feet") }}` | {{ input_params_comments['Fracture Height'] }} | +| Fracture Width | `{{ input_params['Fracture Width'] | replace("foot", "feet") }}` | {{ input_params_comments['Fracture Width'] }} | +| Fracture Separation | `{{ input_params['Fracture Separation'] }} meters` | {{ input_params_comments['Fracture Separation'] }} | .. raw:: html @@ -134,6 +134,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). +See also the [effective number of fractures sensitivity analysis below](#sensitivity-analysis-effective-number-of-fractures-section). ## Results @@ -172,33 +173,65 @@ The variance analysis (results displayed in legend captions) evaluates the predi Both models demonstrate high predictive fidelity, tracking steady-state flowing temperatures within 1.5°C of the empirical data. * **Overall Fit:** GEOPHIRES mathematically achieves a tighter overall fit, yielding a lower Root Mean Square Error (RMSE) and a higher coefficient of determination (R²). -* **Systematic Bias:** The Fervo model exhibits slightly less systemic underestimation, with a cold bias of {{ fervo_bias_degc }}°C compared to the GEOPHIRES cold bias of {{ geophires_bias_degc }}°C. +* **Systematic Bias:** The GEOPHIRES model exhibits slightly less systemic underestimation, with a cold bias of {{ geophires_bias_degc }}°C compared to the Fervo cold bias of {{ fervo_bias_degc }}°C. * **R² Context:** While the Fervo model yields a relatively low R² ({{ fervo_r2 }}), GEOPHIRES achieves a stronger R² of {{ geophires_r2 }}, indicating it more successfully captures the underlying physical trend of the data (the slight thermal drawdown) rather than simply averaging the noise. However, it is important to note that the absolute R² ceiling for both models is inherently suppressed by the dataset. Because the steady-state temperature profile is essentially a flat plateau, natural sensor variance and minor reservoir oscillations may account for a disproportionately large portion of the total variance, keeping the R² scores modest despite the favorable absolute error (RMSE).
-### Long-Term Forecast (8-Year Horizon) +### 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 8-year horizon. -This timeframe aligns with the redrilling interval modeled in the [Cape Station case study](Fervo_Project_Cape-5.html) and provides a -realistic view of the anticipated thermal decline before major wellfield intervention would be required. +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) +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) As shown in the forecast above, the Gringarten analytical model predicts a gradual onset of thermal decline following the initial two-year plateau, which eventually accelerates into a more pronounced drawdown. - When interpreting the trailing edge of the empirical dataset, minor deviations between the extracted data and the forecast curve are to be expected given the inherent imprecision of image-based data digitization and the presence of localized wellbore transients. -Because Fervo has explicitly characterized this two-year operational period as demonstrating highly stable -flowing temperatures with marginal to no actual reservoir drawdown, the GEOPHIRES parameterization -deliberately favors a stabilized, slightly more optimistic decline curve rather than overfitting to potential -extraction artifacts. -This 8-year forecast establishes a testable predictive baseline, which can be further rigorously validated, tuned, -and recalibrated as additional multi-year operational data becomes available -to determine if late-stage variations represent minor transients or the onset of non-linear geological thresholds. +Because Fervo has explicitly characterized this two-year operational period as demonstrating highly stable flowing +temperatures with marginal to no actual reservoir drawdown, the GEOPHIRES parameterization deliberately favors a +stabilized, slightly more optimistic decline curve rather than overfitting to potential extraction artifacts. + +While this {{ long_term_forecast_years }}-year forecast establishes a testable predictive baseline, understanding the true bounds of this decline +requires accounting for inherent subsurface uncertainty. +To explore how variations in reservoir geometry might accelerate or delay the onset of this long-term drawdown, the +following sensitivity analysis expands our single baseline into a predictive envelope. + +
+ +#### Sensitivity Analysis: Effective Number of Fractures + +As detailed in the methodology's [GEOPHIRES Reservoir Parameters](#geophires-reservoir-parameters) table, the baseline +`Number of Fractures` ({{ input_params['Number of Fractures'] }}) represents a deliberately de-rated analytical +equivalent designed to proxy a highly complex, heterogeneous subsurface fracture network. While this calibrated +baseline provides the tightest statistical fit for the current two-year empirical window, assigning an exact number to +the effective fracture surface area inherently relies on analytical interpretation. + +To evaluate the bounding envelope of the reservoir's thermal drawdown, a sensitivity analysis was performed on this +effective `Number of Fractures`. By expanding the model to include other plausible values (ranging from {{ fracture_sensitivity_range_low }} to {{ fracture_sensitivity_range_high }} +fractures), the analysis demonstrates the sensitivity of the long-term thermal decline to the idealized fracture surface area. + +![](_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-1.png) + +*Detail view of the sensitivity curves overlapping the empirical steady-state data (Years 0–3):* + +![](_images/fervo_project_red-2026_production-temperature-data-vs-modeling-fracture-sensitivity-2.png) + +While the {{ input_params['Number of Fractures'] }}-fracture baseline is currently the most empirically supported +estimate based on existing data alignment, time and additional multi-year operational data will ultimately determine +which structural interpretation within this predictive envelope is the most accurate. + +To further quantify the impact of these varying fracture geometries, the corresponding average annual net electricity +generation over the {{ long_term_forecast_years }}-year horizon was also evaluated. While the flowing temperatures across all sensitivity cases +essentially converge by year {{ long_term_forecast_years }}, their distinct intermediate thermal decline paths result in measurable differences in +total power output. Utilizing Average Annual Net Electricity Generation (GWh) isolates these aggregate differences, +providing a relative basis for comparing the lifecycle performance of each sensitivity case. + +![](_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png) ## Discussion 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 285f91c84..efdf85f5c 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 400cd1a11..907e45c51 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 new file mode 100644 index 000000000..3d1bb8818 Binary files /dev/null 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 new file mode 100644 index 000000000..344e73f5f Binary files /dev/null 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 7e26cf520..f65e9275f 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 new file mode 100644 index 000000000..6c59add38 Binary files /dev/null and b/docs/_images/fervo_project_red-2026_production-temperature-data-vs-modeling-power-sensitivity.png differ diff --git a/docs/conf.py b/docs/conf.py index d8e422c95..67b064e81 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.13.3' +version = release = '3.13.4' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 38bfc4609..96624646a 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.13.3', + version='3.13.4', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_docs/generate_fervo_project_red_2026_docs.py b/src/geophires_docs/generate_fervo_project_red_2026_docs.py index dabe4f89e..2b81ab4a9 100644 --- a/src/geophires_docs/generate_fervo_project_red_2026_docs.py +++ b/src/geophires_docs/generate_fervo_project_red_2026_docs.py @@ -4,11 +4,13 @@ from typing import Any import cv2 +import matplotlib.patches as mpatches import matplotlib.pyplot as plt import numpy as np import pandas as pd from jinja2 import Environment from jinja2 import FileSystemLoader +from pint.facets.plain import PlainQuantity from scipy.interpolate import interp1d from scipy.ndimage import maximum_filter @@ -45,6 +47,19 @@ _STATISTICAL_Z_SCORE = 2.0 _STATISTICAL_MIN_STD = 1.5 +_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS = 8 + +NUMBER_OF_FRACTURES_PARAM_NAME = 'Number of Fractures' + +_GRAPH_DPI = 300 +_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 + }, +} + @dataclass class _StatsAlignmentResult: @@ -349,13 +364,13 @@ def _generate_production_temperature_comparison_graph( output_path_stem.parent.mkdir(parents=True, exist_ok=True) ax.set_ylim(0.0, 200.0) - fig.savefig(f'{output_path_stem}-1.png', dpi=150, bbox_inches='tight') + fig.savefig(f'{output_path_stem}-1.png', bbox_inches='tight', **_SAVEFIG_ARGS) ax.set_ylim( 175, 185, ) - fig.savefig(f'{output_path_stem}-2.png', dpi=150, bbox_inches='tight') + fig.savefig(f'{output_path_stem}-2.png', bbox_inches='tight', **_SAVEFIG_ARGS) plt.close(fig) @@ -417,7 +432,10 @@ def _generate_long_term_forecast_graph( ax.set_xlabel('Time (Years)', fontsize=12) ax.set_ylabel('Flowing Temperature (°C)', fontsize=12) - ax.set_title('Project Red GEOPHIRES Temperature Forecast: 8-Year Horizon', fontsize=13) + ax.set_title( + f'Project Red GEOPHIRES Temperature Forecast: {_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS}-Year Horizon', + fontsize=13, + ) ax.set_xlim(0.0, 8.0) ax.set_ylim(0.0, 200.0) @@ -426,7 +444,7 @@ def _generate_long_term_forecast_graph( ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12), ncol=1, frameon=False, fontsize=11) output_path.parent.mkdir(parents=True, exist_ok=True) - fig.savefig(output_path, dpi=150, bbox_inches='tight') + fig.savefig(output_path, bbox_inches='tight', **_SAVEFIG_ARGS) plt.close(fig) @@ -479,7 +497,7 @@ def get_project_red_production_temperature_profile_series( def get_long_term_geophires_profile() -> pd.Series: long_term_input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( from_file_path=_get_file_path('../../tests/examples/Fervo_Project_Red-2026.txt'), - params={'Plant Lifetime': 8, 'Print Output to Console': 0}, + params={'Plant Lifetime': _LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS, 'Print Output to Console': 0}, ) long_term_result: GeophiresXResult = GeophiresXClient().get_geophires_result(long_term_input_params) @@ -492,6 +510,227 @@ def get_long_term_geophires_profile() -> pd.Series: return pd.Series(data=geophires_y, index=geophires_x) +def _get_fracture_sensitivity_fracture_counts() -> list[int]: + base_input_params: GeophiresInputParameters = get_project_red_input_params_and_result()[0] + base_number_of_fractures = int(_get_input_parameters_dict(base_input_params)[NUMBER_OF_FRACTURES_PARAM_NAME]) + return [ + base_number_of_fractures, + base_number_of_fractures - 6, + base_number_of_fractures - 3, + base_number_of_fractures + 3, + base_number_of_fractures + 6, + ] + + +def _generate_fracture_sensitivity_graph( + df_prod: pd.DataFrame, + steady_state_start_years: float, + sensitivity_graph_path: Path, + power_graph_path: Path, + power_csv_path: Path, + show_excluded_measured_temperatures: bool = False, + calculate_stats: bool = True, +) -> pd.DataFrame: + _log.info( + f'Running {_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS}-year fracture sensitivity analysis (including power generation)...' + ) + + # noinspection DuplicatedCode + is_steady_state = _get_steady_state_mask(df_prod, steady_state_start_years) + + df_included = df_prod[is_steady_state] + df_excluded = df_prod[~is_steady_state] + + fig, ax = plt.subplots(figsize=(10, 6)) + + ax.scatter( + df_included['Time_Years'], + df_included['Temperature_C'], + facecolors='none', + edgecolors='#d62728', + s=22, + linewidths=1.0, + alpha=0.85, + label='Measured Temperature (Steady State)', + ) + + if not df_excluded.empty and show_excluded_measured_temperatures: + ax.scatter( + df_excluded['Time_Years'], + df_excluded['Temperature_C'], + facecolors='none', + edgecolors='gray', + s=22, + linewidths=1.0, + alpha=0.5, + label='Measured Temperature (Thermal Conditioning & Transient Operations)', + ) + + base_input_params: GeophiresInputParameters = get_project_red_input_params_and_result()[0] + base_number_of_fractures = int(_get_input_parameters_dict(base_input_params)[NUMBER_OF_FRACTURES_PARAM_NAME]) + + fracture_counts = _get_fracture_sensitivity_fracture_counts() + client = GeophiresXClient() + + colors = { + base_number_of_fractures: 'green', + fracture_counts[1]: '#1C6CA4', + fracture_counts[2]: '#1f77b4', + fracture_counts[3]: '#ff7f0e', + fracture_counts[4]: '#9467bd', + } + line_styles = { + base_number_of_fractures: '-.', + } + + if calculate_stats: + _log.info( + f'--- FRACTURE SENSITIVITY STATISTICAL ALIGNMENT (Steady-State > {steady_state_start_years} Years) ---' + ) + y_true = df_included['Temperature_C'].values + ss_tot = float(np.sum((y_true - np.mean(y_true)) ** 2)) + + power_data = [] + + for frac_count in fracture_counts: + input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( + from_file_path=base_input_params.as_file_path(), + params={ + NUMBER_OF_FRACTURES_PARAM_NAME: frac_count, + 'Plant Lifetime': _LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS, + 'Gringarten-Stehfest Precision': 10, # Speed up build with only minor effect on precision + 'Print Output to Console': 0, + }, + ) + result: GeophiresXResult = client.get_geophires_result(input_params) + + avg_generation_param: str = 'Average Annual Net Electricity Generation' + avg_generation_vu: dict[str, Any] = result.result['SURFACE EQUIPMENT SIMULATION RESULTS'][avg_generation_param] + avg_generation_u = 'GWh' + avg_generation_v: float = ( + PlainQuantity(avg_generation_vu['value'], avg_generation_vu['unit']).to(avg_generation_u).magnitude + ) + + # noinspection DuplicatedCode + profile = _get_full_production_temperature_profile((input_params, result)) + time_steps_per_year: int = int(_get_input_parameters_dict(input_params)['Time steps per year']) + + geophires_x = [float(step) / float(time_steps_per_year) for step, _ in enumerate(profile)] + geophires_y = [q.magnitude for q in profile] + + final_temp_degc = float(geophires_y[-1]) if geophires_y else 0.0 + power_data.append( + { + NUMBER_OF_FRACTURES_PARAM_NAME: frac_count, + f'{avg_generation_param} ({avg_generation_u})': avg_generation_v, + f'Year {_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS} Flowing Temperature (°C)': final_temp_degc, + } + ) + + if calculate_stats and not df_included.empty: + geo_interp = interp1d(geophires_x, geophires_y, kind='linear', fill_value='extrapolate') + y_geo = geo_interp(df_included['Time_Years']) + + rmse_g = float(np.sqrt(((y_true - y_geo) ** 2).mean())) + bias_g = float((y_geo - y_true).mean()) + ss_res_g = float(np.sum((y_true - y_geo) ** 2)) + r2_g = 1.0 - (ss_res_g / ss_tot) if ss_tot != 0.0 else 0.0 + + _log.info(f'{frac_count} Fractures: RMSE={rmse_g:.2f}°C, R²={r2_g:.4f}, Bias={bias_g:.2f}°C') + + label_prefix = 'GEOPHIRES: ' if frac_count == base_number_of_fractures else '' + label_suffix = ' (Baseline)' if frac_count == base_number_of_fractures else '' + + ax.plot( + geophires_x, + geophires_y, + color=colors[frac_count], + linestyle=line_styles.get(frac_count, ':'), + linewidth=1.5 if frac_count != base_number_of_fractures else 2.0, + label=f'{label_prefix}{frac_count} Fractures{label_suffix}', + ) + + ax.set_xlabel('Time (Years)', fontsize=12) + ax.set_ylabel('Flowing Temperature (°C)', fontsize=12) + ax.set_title('Project Red GEOPHIRES Temperature Forecast: Effective Number of Fractures Sensitivity', fontsize=13) + + ax.grid(True, linestyle='--', alpha=0.5) + ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=2, frameon=False, fontsize=11) + + sensitivity_graph_path.parent.mkdir(parents=True, exist_ok=True) + + def _savefig(version: int | str) -> None: + fig.savefig( + sensitivity_graph_path.with_name(f'{sensitivity_graph_path.stem}-{version}').with_suffix( + sensitivity_graph_path.suffix + ), + bbox_inches='tight', + **_SAVEFIG_ARGS, + ) + + ax.set_xlim(0.0, _LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS) + ax.set_ylim(0, 200) + _savefig(1) + + ax.set_xlim(0.0, _LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS * 0.375) + ax.set_ylim(160, 190) + _savefig(2) + + plt.close(fig) + + df_power = pd.DataFrame(power_data) + df_power = df_power.sort_values(NUMBER_OF_FRACTURES_PARAM_NAME).reset_index(drop=True) + df_power.to_csv(power_csv_path, index=False) + + fig_pwr, ax_pwr = plt.subplots(figsize=(8, 5)) + x_labels = [str(x) for x in df_power[NUMBER_OF_FRACTURES_PARAM_NAME]] + y_values = df_power[f'{avg_generation_param} ({avg_generation_u})'] + + bars = ax_pwr.bar(x_labels, y_values, color='#1f77b4', alpha=0.8, edgecolor='black') + + baseline_idx = df_power.index[df_power[NUMBER_OF_FRACTURES_PARAM_NAME] == base_number_of_fractures].tolist()[0] + bars[baseline_idx].set_color('green') + bars[baseline_idx].set_edgecolor('black') + + ax_pwr.set_xlabel('Effective Number of Fractures', fontsize=12) + ax_pwr.set_ylabel(f'{avg_generation_param} ({avg_generation_u})', fontsize=12) + ax_pwr.set_title( + f'GEOPHIRES: {_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS}-Year Average Power Production by Fracture Count', + fontsize=13, + ) + + y_max = y_values.max() + y_min = y_values.min() + y_padding = (y_max - y_min) * 0.4 + if y_padding == 0: + y_padding = max(y_max * 0.1, 1.0) + ax_pwr.set_ylim(max(0.0, y_min - y_padding), y_max + y_padding) + + for bar in bars: + height = bar.get_height() + ax_pwr.annotate( + f'{height:.2f} {avg_generation_u}', + xy=(bar.get_x() + bar.get_width() / 2, height), + xytext=(0, 4), + textcoords='offset points', + ha='center', + va='bottom', + fontsize=11, + ) + + ax_pwr.grid(True, axis='y', linestyle='--', alpha=0.5) + + baseline_patch = mpatches.Patch(color='green', label='Baseline Model') + sensitivity_patch = mpatches.Patch(color='#1f77b4', alpha=0.8, label='Sensitivity Cases') + ax_pwr.legend(handles=[baseline_patch, sensitivity_patch], loc='upper left', frameon=False) + + power_graph_path.parent.mkdir(parents=True, exist_ok=True) + fig_pwr.savefig(power_graph_path, bbox_inches='tight', **_SAVEFIG_ARGS) + plt.close(fig_pwr) + + return df_power + + def generate_fervo_project_red_2026_md( input_params: GeophiresInputParameters, result: GeophiresXResult, @@ -530,6 +769,9 @@ def _get_input_params_dict_with_nbsp() -> dict[str, Any]: 'geophires_rmse_degc': f'{geophires_stats_alignment.rmse_degc:.2f}', 'geophires_r2': f'{geophires_stats_alignment.r2:.4f}', 'geophires_bias_degc': f'{geophires_stats_alignment.bias_degc:.2f}', + 'long_term_forecast_years': _LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS, + 'fracture_sensitivity_range_low': min(_get_fracture_sensitivity_fracture_counts()), + 'fracture_sensitivity_range_high': max(_get_fracture_sensitivity_fracture_counts()), } # Set up Jinja environment @@ -635,7 +877,7 @@ def generate_fervo_project_red_2026_docs(): ) # 8-year long-term simulation graph - _log.info('Running long-term 8-year forecast simulation...') + _log.info(f'Running long-term {_LONG_TERM_FORECAST_PLANT_LIFETIME_YEARS}-year forecast simulation...') long_term_series = get_long_term_geophires_profile() long_term_graph_path = _get_file_path(f'../../docs/_images/{_GENERATED_GRAPH_FILENAME_STEM}-long-term.png') @@ -648,6 +890,15 @@ def generate_fervo_project_red_2026_docs(): ) _log.info(f'Wrote long-term graph: {long_term_graph_path}') + _df_power_sensitivity = _generate_fracture_sensitivity_graph( + df_actual, + _STEADY_STATE_START_YEARS, + _get_file_path(f'../../docs/_images/{_GENERATED_GRAPH_FILENAME_STEM}-fracture-sensitivity.png'), + _get_file_path(f'../../docs/_images/{_GENERATED_GRAPH_FILENAME_STEM}-power-sensitivity.png'), + _BUILD_DIR / 'project_red_2026_power_sensitivity.csv', + ) + _log.info('Wrote sensitivity graphs and power data') + generate_fervo_project_red_2026_md( *get_project_red_input_params_and_result(), fervo_stat_result, geophires_stat_result ) diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 457fbcd2b..5ed46f104 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.13.3' +__version__ = '3.13.4' diff --git a/tests/examples/Fervo_Project_Red-2026.out b/tests/examples/Fervo_Project_Red-2026.out index f78528b7f..5586c832d 100644 --- a/tests/examples/Fervo_Project_Red-2026.out +++ b/tests/examples/Fervo_Project_Red-2026.out @@ -4,18 +4,18 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.13.2 - Simulation Date: 2026-04-22 - Simulation Time: 09:52 - Calculation Time: 1.056 sec + GEOPHIRES Version: 3.13.3 + Simulation Date: 2026-04-23 + Simulation Time: 09:27 + Calculation Time: 1.039 sec ***SUMMARY OF RESULTS*** End-Use Option: Electricity - Average Net Electricity Production: 1.98 MW - Electricity breakeven price: 45.95 cents/kWh - Total CAPEX: 28.09 MUSD - Total CAPEX ($/kW): 13698 USD/kW + Average Net Electricity Production: 1.99 MW + Electricity breakeven price: 45.69 cents/kWh + Total CAPEX: 28.10 MUSD + Total CAPEX ($/kW): 13692 USD/kW Number of production wells: 1 Number of injection wells: 1 Flowrate per production well: 36.0 kg/sec @@ -32,9 +32,9 @@ Simulation Metadata Investment Tax Credit: 8.43 MUSD Project lifetime: 2 yr Capacity factor: 90.0 % - Project NPV: -9.32 MUSD - After-tax IRR: -81.53 % - Project VIR=PI=PIR: 0.04 + Project NPV: -9.31 MUSD + After-tax IRR: -81.22 % + Project VIR=PI=PIR: 0.05 Project MOIC: -1.11 Project Payback Period: N/A Estimated Jobs Created: 6 @@ -48,7 +48,7 @@ Simulation Metadata Pump efficiency: 80.0 % Injection temperature: 56.4 degC Production Wellbore heat transmission calculated with Ramey's model - Average production well temperature drop: 4.7 degC + Average production well temperature drop: 4.8 degC Flowrate per production well: 36.0 kg/sec Injection well casing ID: 4.276 in Production well casing ID: 4.276 in @@ -72,9 +72,9 @@ Simulation Metadata Fracture width: 111.25 meter Fracture area: 10172.88 m**2 Reservoir volume calculated with fracture separation and number of fractures as input - Number of fractures: 63 + Number of fractures: 65 Fracture separation: 10.00 meter - Reservoir volume: 6307187 m**3 + Reservoir volume: 6510645 m**3 Reservoir hydrostatic pressure: 22008.33 kPa Plant outlet pressure: 10342.14 kPa Production wellhead pressure: 1551.32 kPa @@ -87,15 +87,15 @@ Simulation Metadata ***RESERVOIR SIMULATION RESULTS*** - Maximum Production Temperature: 181.7 degC - Average Production Temperature: 179.9 degC - Minimum Production Temperature: 172.9 degC + Maximum Production Temperature: 181.8 degC + Average Production Temperature: 180.2 degC + Minimum Production Temperature: 174.3 degC Initial Production Temperature: 177.9 degC - Average Reservoir Heat Extraction: 18.66 MW + Average Reservoir Heat Extraction: 18.70 MW Production Wellbore Heat Transmission Model = Ramey Model - Average Production Well Temperature Drop: 4.7 degC + Average Production Well Temperature Drop: 4.8 degC Average Injection Well Pump Pressure Drop: 2653.5 kPa - Average Production Well Pump Pressure Drop: 10755.5 kPa + Average Production Well Pump Pressure Drop: 10748.8 kPa ***CAPITAL COSTS (M$)*** @@ -106,12 +106,12 @@ Simulation Metadata Drilling and completion costs per vertical injection well: 3.02 MUSD Drilling and completion costs per non-vertical section: 1.80 MUSD Stimulation costs: 1.51 MUSD - Surface power plant costs: 10.48 MUSD + Surface power plant costs: 10.49 MUSD Field gathering system costs: 1.61 MUSD Total surface equipment costs: 12.09 MUSD - Overnight Capital Cost: 27.35 MUSD + Overnight Capital Cost: 27.36 MUSD Inflation costs during construction: 0.74 MUSD - Total CAPEX: 28.09 MUSD + Total CAPEX: 28.10 MUSD ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** @@ -125,18 +125,18 @@ Simulation Metadata ***SURFACE EQUIPMENT SIMULATION RESULTS*** Initial geofluid availability: 0.15 MW/(kg/s) Maximum Total Electricity Generation: 2.75 MW - Average Total Electricity Generation: 2.68 MW - Minimum Total Electricity Generation: 2.40 MW + Average Total Electricity Generation: 2.69 MW + Minimum Total Electricity Generation: 2.46 MW Initial Total Electricity Generation: 2.60 MW Maximum Net Electricity Generation: 2.05 MW - Average Net Electricity Generation: 1.98 MW - Minimum Net Electricity Generation: 1.70 MW + Average Net Electricity Generation: 1.99 MW + Minimum Net Electricity Generation: 1.76 MW Initial Net Electricity Generation: 1.90 MW - Average Annual Total Electricity Generation: 21.09 GWh - Average Annual Net Electricity Generation: 15.60 GWh + Average Annual Total Electricity Generation: 21.20 GWh + Average Annual Net Electricity Generation: 15.70 GWh Initial pumping power/net installed power: 36.67 % Average Pumping Power: 0.70 MW - Heat to Power Conversion Efficiency: 10.59 % + Heat to Power Conversion Efficiency: 10.64 % ************************************************************ * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * @@ -144,8 +144,8 @@ Simulation Metadata YEAR THERMAL GEOFLUID PUMP NET FIRST LAW DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY (degC) (MW) (MW) (%) - 1 1.0000 177.95 0.6970 1.9009 10.3509 - 2 1.0201 181.52 0.6968 2.0424 10.8041 + 1 1.0000 177.95 0.6970 1.9009 10.3534 + 2 1.0207 181.63 0.6968 2.0470 10.8210 ******************************************************************* @@ -154,8 +154,8 @@ Simulation Metadata YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED (GWh/year) (GWh/year) (10^15 J) (%) - 1 16.0 148.8 1.38 27.94 - 2 15.2 145.5 0.86 55.25 + 1 16.0 148.8 1.44 27.06 + 2 15.4 146.2 0.92 53.66 *************************** * SAM CASH FLOW PROFILE * @@ -164,69 +164,69 @@ Simulation Metadata Year 0 Year 1 Year 2 CONSTRUCTION Capital expenditure schedule [construction] (%) 100.0 -Overnight capital expenditure [construction] ($) -27,352,942 +Overnight capital expenditure [construction] ($) -27,356,676 plus: -Inflation cost [construction] ($) -738,529 +Inflation cost [construction] ($) -738,630 plus: Royalty supplemental payments [construction] ($) 0 equals: -Nominal capital expenditure [construction] ($) -28,091,471 +Nominal capital expenditure [construction] ($) -28,095,306 -Issuance of equity [construction] ($) 8,427,441 -Issuance of debt [construction] ($) 19,664,030 -Debt balance [construction] ($) 19,664,030 +Issuance of equity [construction] ($) 8,428,592 +Issuance of debt [construction] ($) 19,666,714 +Debt balance [construction] ($) 19,666,714 Debt interest payment [construction] ($) 0 -Installed cost [construction] ($) -28,091,471 -After-tax net cash flow [construction] ($) -8,427,441 +Installed cost [construction] ($) -28,095,306 +After-tax net cash flow [construction] ($) -8,428,592 ENERGY -Electricity to grid (kWh) 0.0 16,133,125 15,257,506 +Electricity to grid (kWh) 0.0 16,141,623 15,462,625 Electricity from grid (kWh) 0.0 0.0 0.0 -Electricity to grid net (kWh) 0.0 16,133,125 15,257,506 +Electricity to grid net (kWh) 0.0 16,141,623 15,462,625 REVENUE PPA price (cents/kWh) 0.0 9.50 9.50 -PPA revenue ($) 0 1,532,647 1,449,463 +PPA revenue ($) 0 1,533,454 1,468,949 Curtailment payment revenue ($) 0 0 0 Capacity payment revenue ($) 0 0 0 -Salvage value ($) 0 0 14,045,736 -Total revenue ($) 0 1,532,647 15,495,199 +Salvage value ($) 0 0 14,047,653 +Total revenue ($) 0 1,533,454 15,516,603 -Property tax net assessed value ($) 0 28,091,471 28,091,471 +Property tax net assessed value ($) 0 28,095,306 28,095,306 OPERATING EXPENSES -O&M fixed expense ($) 0 878,561 878,561 +O&M fixed expense ($) 0 878,898 878,898 O&M production-based expense ($) 0 0 0 O&M capacity-based expense ($) 0 0 0 Fuel expense ($) 0 0 0 Electricity purchase ($) 0 0 0 -Property tax expense ($) 0 61,801 61,801 +Property tax expense ($) 0 61,810 61,810 Insurance expense ($) 0 0 0 -Total operating expenses ($) 0 940,362 940,362 +Total operating expenses ($) 0 940,707 940,707 -EBITDA ($) 0 592,285 14,554,837 +EBITDA ($) 0 592,747 14,575,895 OPERATING ACTIVITIES -EBITDA ($) 0 592,285 14,554,837 +EBITDA ($) 0 592,747 14,575,895 Interest earned on reserves ($) 0 0 0 plus PBI if not available for debt service: Federal PBI income ($) 0 0 0 State PBI income ($) 0 0 0 Utility PBI income ($) 0 0 0 Other PBI income ($) 0 0 0 -Debt interest payment ($) 0 1,376,482 711,515 -Cash flow from operating activities ($) 0 -784,197 13,843,322 +Debt interest payment ($) 0 1,376,670 711,612 +Cash flow from operating activities ($) 0 -783,923 13,864,283 INVESTING ACTIVITIES -Total installed cost ($) -28,091,471 +Total installed cost ($) -28,095,306 Debt closing costs ($) 0 Debt up-front fee ($) 0 minus: Total IBI income ($) 0 Total CBI income ($) 0 equals: -Purchase of property ($) -28,091,471 +Purchase of property ($) -28,095,306 plus: Reserve (increase)/decrease debt service ($) 0 0 0 Reserve (increase)/decrease working capital ($) 0 0 0 @@ -238,89 +238,89 @@ Reserve capital spending major equipment 1 ($) 0 0 0 Reserve capital spending major equipment 2 ($) 0 0 0 Reserve capital spending major equipment 3 ($) 0 0 0 equals: -Cash flow from investing activities ($) -28,091,471 0 0 +Cash flow from investing activities ($) -28,095,306 0 0 FINANCING ACTIVITIES -Issuance of equity ($) 8,427,441 -Size of debt ($) 19,664,030 +Issuance of equity ($) 8,428,592 +Size of debt ($) 19,666,714 minus: -Debt principal payment ($) 0 9,499,531 10,164,499 +Debt principal payment ($) 0 9,500,828 10,165,886 equals: -Cash flow from financing activities ($) 28,091,471 -9,499,531 -10,164,499 +Cash flow from financing activities ($) 28,095,306 -9,500,828 -10,165,886 PROJECT RETURNS Pre-tax Cash Flow: -Cash flow from operating activities ($) 0 -784,197 13,843,322 -Cash flow from investing activities ($) -28,091,471 0 0 -Cash flow from financing activities ($) 28,091,471 -9,499,531 -10,164,499 -Total pre-tax cash flow ($) 0 -10,283,728 3,678,823 +Cash flow from operating activities ($) 0 -783,923 13,864,283 +Cash flow from investing activities ($) -28,095,306 0 0 +Cash flow from financing activities ($) 28,095,306 -9,500,828 -10,165,886 +Total pre-tax cash flow ($) 0 -10,284,751 3,698,397 Pre-tax Returns: -Issuance of equity ($) 8,427,441 -Total pre-tax cash flow ($) 0 -10,283,728 3,678,823 -Total pre-tax returns ($) -8,427,441 -10,283,728 3,678,823 +Issuance of equity ($) 8,428,592 +Total pre-tax cash flow ($) 0 -10,284,751 3,698,397 +Total pre-tax returns ($) -8,428,592 -10,284,751 3,698,397 After-tax Returns: -Total pre-tax returns ($) -8,427,441 -10,283,728 3,678,823 -Federal ITC total income ($) 0 8,427,441 0 +Total pre-tax returns ($) -8,428,592 -10,284,751 3,698,397 +Federal ITC total income ($) 0 8,428,592 0 Federal PTC income ($) 0 0 0 -Federal tax benefit (liability) ($) 0 276,843 -2,535,516 +Federal tax benefit (liability) ($) 0 276,804 -2,539,685 State ITC total income ($) 0 0 0 State PTC income ($) 0 0 0 -State tax benefit (liability) ($) 0 62,842 -575,549 -Total after-tax returns ($) -8,427,441 -1,516,602 567,758 +State tax benefit (liability) ($) 0 62,833 -576,496 +Total after-tax returns ($) -8,428,592 -1,516,522 582,217 -After-tax net cash flow ($) -8,427,441 -1,516,602 567,758 -After-tax cumulative IRR (%) NaN NaN -81.53 -After-tax cumulative NPV ($) -8,427,441 -9,745,950 -9,316,823 +After-tax net cash flow ($) -8,428,592 -1,516,522 582,217 +After-tax cumulative IRR (%) NaN NaN -81.22 +After-tax cumulative NPV ($) -8,428,592 -9,747,032 -9,306,976 AFTER-TAX LCOE AND PPA PRICE -Annual costs ($) -8,427,441 -3,049,249 -881,705 -PPA revenue ($) 0 1,532,647 1,449,463 -Electricity to grid (kWh) 0 16,133,125 15,257,506 +Annual costs ($) -8,428,592 -3,049,976 -886,733 +PPA revenue ($) 0 1,533,454 1,468,949 +Electricity to grid (kWh) 0 16,141,623 15,462,625 -Present value of annual costs ($) 11,744,826 +Present value of annual costs ($) 11,750,409 -Present value of annual energy costs ($) 11,744,826 -Present value of annual energy nominal (kWh) 25,557,931 -LCOE Levelized cost of energy nominal (cents/kWh) 45.95 +Present value of annual energy costs ($) 11,750,409 +Present value of annual energy nominal (kWh) 25,720,353 +LCOE Levelized cost of energy nominal (cents/kWh) 45.69 -Present value of PPA revenue ($) 2,428,003 -Present value of annual energy nominal (kWh) 25,557,931 +Present value of PPA revenue ($) 2,443,433 +Present value of annual energy nominal (kWh) 25,720,353 LPPA Levelized PPA price nominal (cents/kWh) 9.50 PROJECT STATE INCOME TAXES -EBITDA ($) 0 592,285 14,554,837 +EBITDA ($) 0 592,747 14,575,895 State taxable PBI income ($) 0 0 0 Interest earned on reserves ($) 0 0 0 State taxable IBI income ($) 0 State taxable CBI income ($) 0 minus: -Debt interest payment ($) 0 1,376,482 711,515 -Total state tax depreciation ($) 0 596,944 1,193,888 +Debt interest payment ($) 0 1,376,670 711,612 +Total state tax depreciation ($) 0 597,025 1,194,051 equals: -State taxable income ($) 0 -1,381,141 12,649,434 +State taxable income ($) 0 -1,380,948 12,670,233 State income tax rate (frac) 0.0 0.05 0.05 -State tax benefit (liability) ($) 0 62,842 -575,549 +State tax benefit (liability) ($) 0 62,833 -576,496 PROJECT FEDERAL INCOME TAXES -EBITDA ($) 0 592,285 14,554,837 +EBITDA ($) 0 592,747 14,575,895 Interest earned on reserves ($) 0 0 0 -State tax benefit (liability) ($) 0 62,842 -575,549 +State tax benefit (liability) ($) 0 62,833 -576,496 State ITC total income ($) 0 0 0 State PTC income ($) 0 0 0 Federal taxable IBI income ($) 0 Federal taxable CBI income ($) 0 Federal taxable PBI income ($) 0 0 0 minus: -Debt interest payment ($) 0 1,376,482 711,515 -Total federal tax depreciation ($) 0 596,944 1,193,888 +Debt interest payment ($) 0 1,376,670 711,612 +Total federal tax depreciation ($) 0 597,025 1,194,051 equals: -Federal taxable income ($) 0 -1,318,299 12,073,885 +Federal taxable income ($) 0 -1,318,115 12,093,737 Federal income tax rate (frac) 0.0 0.21 0.21 -Federal tax benefit (liability) ($) 0 276,843 -2,535,516 +Federal tax benefit (liability) ($) 0 276,804 -2,539,685 CASH INCENTIVES Federal IBI income ($) 0 @@ -346,29 +346,29 @@ Federal PTC income ($) 0 0 0 State PTC income ($) 0 0 0 Federal ITC amount income ($) 0 0 0 -Federal ITC percent income ($) 0 8,427,441 0 -Federal ITC total income ($) 0 8,427,441 0 +Federal ITC percent income ($) 0 8,428,592 0 +Federal ITC total income ($) 0 8,428,592 0 State ITC amount income ($) 0 0 0 State ITC percent income ($) 0 0 0 State ITC total income ($) 0 0 0 DEBT REPAYMENT -Debt balance ($) 19,664,030 10,164,499 0 -Debt interest payment ($) 0 1,376,482 711,515 -Debt principal payment ($) 0 9,499,531 10,164,499 -Debt total payment ($) 0 10,876,013 10,876,013 +Debt balance ($) 19,666,714 10,165,886 0 +Debt interest payment ($) 0 1,376,670 711,612 +Debt principal payment ($) 0 9,500,828 10,165,886 +Debt total payment ($) 0 10,877,498 10,877,498 DSCR (DEBT FRACTION) -EBITDA ($) 0 592,285 14,554,837 +EBITDA ($) 0 592,747 14,575,895 minus: Reserves major equipment 1 funding ($) 0 0 0 Reserves major equipment 2 funding ($) 0 0 0 Reserves major equipment 3 funding ($) 0 0 0 Reserves receivables funding ($) 0 0 0 equals: -Cash available for debt service (CAFDS) ($) 0 592,285 14,554,837 -Debt total payment ($) 0 10,876,013 10,876,013 +Cash available for debt service (CAFDS) ($) 0 592,747 14,575,895 +Debt total payment ($) 0 10,877,498 10,877,498 DSCR (pre-tax) 0.0 0.05 1.34 RESERVES diff --git a/tests/examples/Fervo_Project_Red-2026.txt b/tests/examples/Fervo_Project_Red-2026.txt index 218e9bfd7..b89cf3bcf 100644 --- a/tests/examples/Fervo_Project_Red-2026.txt +++ b/tests/examples/Fervo_Project_Red-2026.txt @@ -17,7 +17,7 @@ Reservoir Thermal Conductivity, 2.7 Number of Segments, 1 Gradient 1, 76.1 -Number of Fractures, 63, -- Fervo estimates between 75 and 100 fractures were created. This value is de-rated in the model to account for the physical reality of imperfect flow distribution and uneven utilization across the entire stimulated rock volume (Norbeck and Latimer, 2023). +Number of Fractures, 65, -- Fervo estimates between 75 and 100 fractures were created. This value is de-rated in the model to account for the physical reality of imperfect flow distribution and uneven utilization across the entire stimulated rock volume (Norbeck and Latimer, 2023). Fracture Shape, 4, -- Rectangular geometry (Shape 4), representing standard transverse hydraulic fractures along a horizontal wellbore. Fracture Height, 300 foot, -- Estimated vertical propagation of the stimulated fracture network. Fracture Width, 365 foot, -- Set to match the distance between the injection and production wellbores, assuming a dipole flow field directly connecting the laterals. diff --git a/tests/geophires_x_tests/test_fervo_project_red_2026.py b/tests/geophires_x_tests/test_fervo_project_red_2026.py index bf3a26ecc..d188e44d3 100644 --- a/tests/geophires_x_tests/test_fervo_project_red_2026.py +++ b/tests/geophires_x_tests/test_fervo_project_red_2026.py @@ -45,4 +45,4 @@ def _vuq(v_u: dict[str, Any]) -> PlainQuantity: avg_production_temp_q = _vuq(r.result['RESERVOIR SIMULATION RESULTS']['Average Production Temperature']) self.assertGreater(avg_production_temp_q, _q(346, 'degF')) - self.assertLess(avg_production_temp_q, _q(356, 'degF')) + self.assertLess(avg_production_temp_q, _q(357, 'degF'))