-
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
base: develop
Are you sure you want to change the base?
Changes from all commits
3e49221
fbee5ba
db2aa10
18c0dc9
0567f10
a7573f4
02e5145
9b590cd
ee1b17a
9f4bdac
0eb034d
ee5fddf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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["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.sum(outputs["wellhead_gas_out"]) | ||||||
|
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.
Suggested change
|
||||||
| outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) | ||||||
|
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.
Suggested change
|
||||||
|
|
||||||
| 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) | ||||||
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