-
Notifications
You must be signed in to change notification settings - Fork 27
GeoH2: Arps decline curve #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kbrunik
wants to merge
15
commits into
NatLabRockies:develop
Choose a base branch
from
kbrunik:decline_curves
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+213
−20
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
3e49221
WIP arps decline curve
kbrunik fbee5ba
update decline curve
kbrunik db2aa10
update geoh2 and docstring
kbrunik 18c0dc9
update test
kbrunik 0567f10
Merge branch 'develop' into decline_curves
kbrunik a7573f4
update to be percent
kbrunik 02e5145
update documentation
kbrunik 9b590cd
Merge branch 'decline_curves' of https://github.com/kbrunik/H2Integra…
kbrunik ee1b17a
Merge branch 'develop' into decline_curves
kbrunik 9f4bdac
fix changelog
kbrunik 0eb034d
Merge branch 'develop' into decline_curves
kbrunik ee5fddf
Merge branch 'develop' into decline_curves
kbrunik 0a6b686
Update h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py
kbrunik b77ba18
Update h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py
kbrunik 259cbbf
precommit fix
kbrunik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| from attrs import field, define | ||
|
|
||
| from h2integrate.core.utilities import merge_shared_inputs | ||
| from h2integrate.core.validators import range_val | ||
| from h2integrate.converters.hydrogen.geologic.h2_well_subsurface_baseclass import ( | ||
| GeoH2SubsurfacePerformanceConfig, | ||
| GeoH2SubsurfacePerformanceBaseClass, | ||
|
|
@@ -32,15 +33,39 @@ class NaturalGeoH2PerformanceConfig(GeoH2SubsurfacePerformanceConfig): | |
| Hydrogen flow rate measured immediately after well completion, in kilograms | ||
| per hour (kg/h). | ||
|
|
||
| gas_flow_density (float): | ||
| Density of the wellhead gas flow, in kilograms per cubic meter (kg/m^3). | ||
|
|
||
| ramp_up_time_months (float): | ||
| Number of months after initial flow from the well before full utilization. | ||
|
|
||
| percent_increase_during_rampup (float): | ||
| Percent increase in wellhead flow during ramp-up period in percent (%). | ||
|
|
||
| gas_reservoir_size (float): | ||
| Total amount of hydrogen stored in the geologic accumulation, in tonnes (t). | ||
|
|
||
| use_arps_decline_curve (bool): | ||
| Whether to use the Arps decline curve model for well production decline. | ||
|
|
||
| decline_fit_params (dict): | ||
| (Optional) Parameters for the Arps decline curve model, including: | ||
| - 'Di' (float): Decline rate. | ||
| - 'b' (float): Loss rate. | ||
| - 'fit_name' (str): Name of the well fit to use. If provided, overrides Di and b. | ||
| Options are "Eagle_Ford" or "Permian" or "Bakken". | ||
| """ | ||
|
|
||
| use_prospectivity: bool = field() | ||
| site_prospectivity: float = field() | ||
| wellhead_h2_concentration: float = field() | ||
| initial_wellhead_flow: float = field() | ||
| gas_flow_density: float = field() | ||
| ramp_up_time_months: float = field() | ||
| percent_increase_during_rampup: float = field(validator=range_val(0, 100)) | ||
| gas_reservoir_size: float = field() | ||
| use_arps_decline_curve: bool = field() | ||
| decline_fit_params: dict = field(default=None) | ||
|
|
||
|
|
||
| class NaturalGeoH2PerformanceModel(GeoH2SubsurfacePerformanceBaseClass): | ||
|
|
@@ -109,7 +134,15 @@ def setup(self): | |
| "wellhead_h2_concentration", units="percent", val=self.config.wellhead_h2_concentration | ||
| ) | ||
| self.add_input("initial_wellhead_flow", units="kg/h", val=self.config.initial_wellhead_flow) | ||
| self.add_input("gas_flow_density", units="kg/m**3", val=self.config.gas_flow_density) | ||
| self.add_input("gas_reservoir_size", units="t", val=self.config.gas_reservoir_size) | ||
| self.add_input("ramp_up_time", units="yr/12", val=self.config.ramp_up_time_months) | ||
| self.add_input( | ||
| "percent_increase_during_rampup", | ||
| units="percent", | ||
| val=self.config.percent_increase_during_rampup, | ||
| desc="Percent increase in wellhead flow during ramp-up period in percent (%)", | ||
| ) | ||
|
|
||
| self.add_output("wellhead_h2_concentration_mass", units="percent") | ||
| self.add_output("wellhead_h2_concentration_mol", units="percent") | ||
|
|
@@ -118,6 +151,13 @@ def setup(self): | |
| self.add_output("max_wellhead_gas", units="kg/h") | ||
|
|
||
| def compute(self, inputs, outputs): | ||
| n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] | ||
|
|
||
| # Coerce scalar inputs to Python scalars (handles 0-d and 1-d arrays) | ||
| ramp_up_time = float(np.asarray(inputs["ramp_up_time"]).item()) | ||
| percent_increase = float(np.asarray(inputs["percent_increase_during_rampup"]).item()) | ||
| init_wh_flow = float(np.asarray(inputs["initial_wellhead_flow"]).item()) | ||
|
|
||
| if self.config.rock_type == "peridotite": # TODO: sub-models for different rock types | ||
| # Calculate expected wellhead h2 concentration from prospectivity | ||
| prospectivity = inputs["site_prospectivity"] | ||
|
|
@@ -128,24 +168,102 @@ def compute(self, inputs, outputs): | |
|
|
||
| # Calculated average wellhead gas flow over well lifetime | ||
| init_wh_flow = inputs["initial_wellhead_flow"] | ||
| lifetime = self.options["plant_config"]["plant"]["plant_life"] | ||
| n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] | ||
| avg_wh_flow = (-0.193 * np.log(lifetime) + 0.6871) * init_wh_flow # temp. fit to Arps data | ||
|
|
||
| # Coerce scalar inputs to Python scalars (handles 0-d and 1-d arrays) | ||
| ramp_up_time = float(np.asarray(inputs["ramp_up_time"]).item()) | ||
| percent_increase = float(np.asarray(inputs["percent_increase_during_rampup"]).item()) | ||
| init_wh_flow = float(np.asarray(inputs["initial_wellhead_flow"]).item()) | ||
|
|
||
| # Apply ramp-up assumed linear increase | ||
| ramp_up_steps = int(ramp_up_time * (n_timesteps / 12)) # hrs | ||
| if ramp_up_steps > 0: | ||
| ramp_up_flow = init_wh_flow * ((100 + percent_increase) / 100) | ||
| ramp_up_profile = np.linspace(init_wh_flow, ramp_up_flow, ramp_up_steps) | ||
| else: | ||
| ramp_up_flow = init_wh_flow | ||
| remaining_steps = ( | ||
| n_timesteps * self.options["plant_config"]["plant"]["plant_life"] - ramp_up_steps | ||
| ) # remaining time steps in lifetime | ||
|
|
||
| # Use decline curve modeling if selected | ||
| if self.config.use_arps_decline_curve: | ||
| t = np.arange(remaining_steps) # hrs | ||
| if self.config.decline_fit_params and "fit_name" in self.config.decline_fit_params: | ||
| # decline curves from literature is in million standard cubic feet per hour | ||
| ramp_up_flow_m3 = ramp_up_flow / inputs["gas_flow_density"] # m3/h | ||
| # convert from m3/h to million standard cubic feet per hour (MMSCF/h) | ||
| ramp_up_flow_mmscf = ramp_up_flow_m3 / 28316.846592 # 1 MMSCF = 28316.846592 m3 | ||
|
|
||
| # fits for MMSCF/h based on flow rates Figure 7 in Tang et al. (2024) | ||
| fit_name = self.config.decline_fit_params["fit_name"] | ||
| if fit_name == "Eagle_Ford": | ||
| Di = 0.000157 | ||
| b = 0.932 | ||
| elif fit_name == "Permian": | ||
| Di = 0.000087 | ||
| b = 0.708 | ||
| elif fit_name == "Bakken": | ||
| Di = 0.000076 | ||
| b = 0.784 | ||
| else: | ||
| msg = f"Unknown fit_name '{fit_name}' \ | ||
| for Arps decline curve. Valid options are \ | ||
| 'Eagle_Ford', 'Permian', or 'Bakken'." | ||
| raise ValueError(msg) | ||
| decline_profile = self.arps_decline_curve_fit(t, ramp_up_flow_mmscf, Di, b) | ||
| # convert back to kg/h from MMSCF/h | ||
| decline_profile = decline_profile * 28316.846592 * inputs["gas_flow_density"] | ||
| else: | ||
| Di = self.config.decline_fit_params.get("Di") | ||
| b = self.config.decline_fit_params.get("b") | ||
| decline_profile = self.arps_decline_curve_fit(t, ramp_up_flow, Di, b) | ||
| else: | ||
| # linear decline for rest of lifetime | ||
| decline_profile = np.linspace(ramp_up_flow, 0, remaining_steps) | ||
|
|
||
| wh_flow_profile = np.concatenate((ramp_up_profile, decline_profile)) | ||
|
|
||
| # Calculated hydrogen flow out | ||
| balance_mw = 23.32 # Note: this is based on Aspen models in aspen_surface_processing.py | ||
| h2_mw = 2.016 | ||
| x_h2 = wh_h2_conc / 100 | ||
| w_h2 = x_h2 * h2_mw / (x_h2 * h2_mw + (1 - x_h2) * balance_mw) | ||
| avg_h2_flow = w_h2 * avg_wh_flow | ||
| avg_h2_flow = w_h2 * wh_flow_profile | ||
|
|
||
| # Parse outputs | ||
| outputs["wellhead_h2_concentration_mass"] = w_h2 * 100 | ||
| outputs["wellhead_h2_concentration_mol"] = wh_h2_conc | ||
| outputs["lifetime_wellhead_flow"] = avg_wh_flow | ||
| outputs["wellhead_gas_out_natural"] = np.full(n_timesteps, avg_wh_flow) | ||
| outputs["wellhead_gas_out"] = np.full(n_timesteps, avg_wh_flow) | ||
| outputs["hydrogen_out"] = np.full(n_timesteps, avg_h2_flow) | ||
| outputs["max_wellhead_gas"] = init_wh_flow | ||
| outputs["total_wellhead_gas_produced"] = np.sum(outputs["wellhead_gas_out"]) | ||
| outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) | ||
| outputs["lifetime_wellhead_flow"] = np.average(wh_flow_profile) | ||
| # fill "wellhead_gas_out_natural" with first year profile from wh_flow_profile | ||
| outputs["wellhead_gas_out_natural"] = wh_flow_profile[:n_timesteps] | ||
| outputs["wellhead_gas_out"] = wh_flow_profile[:n_timesteps] | ||
| outputs["hydrogen_out"] = avg_h2_flow[:n_timesteps] | ||
| outputs["max_wellhead_gas"] = ramp_up_flow | ||
| # this is lifetime flow which decreases over time | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to suggest a couple changes here to temporarily fix the problem of not having declining production over the years. This will make |
||
| outputs["total_wellhead_gas_produced"] = np.average(wh_flow_profile) * n_timesteps | ||
| outputs["total_hydrogen_produced"] = np.average(avg_h2_flow) * n_timesteps | ||
|
|
||
| def arps_decline_curve_fit(self, t, qi, Di, b): | ||
| """Arps decline curve model based on Arps (1945) | ||
| https://doi.org/10.2118/945228-G. | ||
|
|
||
| Other Relevant literature: | ||
| Tang et al. (2024) https://doi.org/10.1016/j.jngse.2021.103818 | ||
| Adapted the Arps model from Table 2 to fit the | ||
| monthly gas rates from Figure 7 to characterize natural hydrogen | ||
| well production decline for the three oil shale wells | ||
| (Bakken, Eagle Ford and Permian). | ||
|
|
||
| Args: | ||
| t (np.array): Well production duration from max production. | ||
| qi (float): Maximum initial production rate. | ||
| Di (float): Decline rate. | ||
| b (float): Loss rate. | ||
|
|
||
| Returns: | ||
| (np.array): Production rate at time t. | ||
| """ | ||
| if np.isclose(b, 0): | ||
| return qi * np.exp(-Di * t) | ||
| else: | ||
| return qi / (1 + b * Di * t) ** (1 / b) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LCOH is too low, because "total_hydrogen_produced" is only based off the first year of production