RTES Stochastic Simulation: Parametric Sweeping
Problem Statement
The performance of RTES is highly relevant to the formation characteristics of the site and operational conditions, which are the critical attributes of the techno-economic performance of a potential site. This example demonstrates the capability of FALCON to generate synthetic data for RTES potential site identification using automated stochastic simulation. Following Jin et al. (2022) and Jin et al. (2021), a generic caprock-reservoir-bedrock formation with varying formation characteristics (permeability, porosity, thermal conductivity, thickness, in-situ temperature and pore pressure) was simulated. Figure 1 shows the boundary conditions and the geometry of the generic formation. Note a common doublet well configuration was adopted for this case, and half of the domain was simulated due to symmetry. In addition to the formation characteristics, the operational conditions of injection rate, fluid injection temperature, and well distance were also investigated to reveal their influence on RTES performance.

Figure 1: Geometry and boundary conditions of the generic formation with doublet system for simulation.
FALCON input files
Physics simulation input file
In this example, only the physics of heat transfer and fluid flow is considered, and the formation is assumed homogeneous and fully saturated with uniform in-situ temperature and pore pressure. To enable the flexbility of convergence implemented in unsaturated kernels, we used the PorousFlowUnsaturated Action as
[PorousFlowUnsaturated]
relative_permeability_type = COREY
relative_permeability_exponent = 0
add_darcy_aux = true
coupling_type = ThermoHydro
gravity = '0 0 -9.8'
porepressure = porepressure
temperature = temperature
fp = tabulated_water
use_displaced_mesh = false
[]
Mesh: The generic formation was meshed using CUBIT. As a result, the thickness of formation was descretized into 10 cases with thickness ranging from 10 meters to 100 meters. The loading of the Exodus mesh file in input file is via
[Mesh]
[./fmg]
type = FileMeshGenerator
file = base_final_50.e
[]
[]
Initial Condition: Given the initial temperature and pore pressure is correlated with depth in general as
(1) (2)
We coded the equation in the input file as
depth = 2000
pp_ini_bc = ${fparse if(depth<=2000, 1000*9.8*depth, 1000*9.8*depth+(depth-2000)*9.8*1000)}
T_ini_bc = ${fparse (80+3.28084*depth*1.1/100-32)*5/9 + 273.15}
and we assigned the initial condition as
[ICs<<<{"href": "../syntax/ICs/index.html"}>>>]
[./init_pp]
type = FunctionIC<<<{"description": "An initial condition that uses a normal function of x, y, z to produce values (and optionally gradients) for a field variable.", "href": "../source/ics/FunctionIC.html"}>>>
function<<<{"description": "The initial condition function."}>>> = ${pp_ini_bc}
variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = porepressure
[../]
[./init_temp]
type = ConstantIC<<<{"description": "Sets a constant field value.", "href": "../source/ics/ConstantIC.html"}>>>
value<<<{"description": "The value to be set in IC"}>>> = ${T_ini_bc}
variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = temperature
[../]
[]
(examples/rtes_stochastic/base_cycle_50.i)Boundary Condition: The simulated domain has a 1 km length in the horizontal direction, and we assigned drained boundary conditions with pore pressure and temperature equal to the initial values for all side faces, except the symmetrical surface. While the top and bottom surfaces were undrained. The input file section is !listing examples/rtes_stochastic/base_cycle_50.i block=BCs
Operation: In order to perform parametric study of operational conditions, we first defined variables in input file as
distance_between_wells = 200
pro_well_x = ${fparse 0.1 + distance_between_wells / 2}
inj_well_x = ${fparse 0.1 - distance_between_wells / 2}
well_length = 50 #fixed number in accordance with the mesh
aquifer_mid= ${fparse 0 + 0}
aquifer_top= ${fparse 0 + well_length/2}
aquifer_bot= ${fparse 0 - well_length/2}
#---Define a scalar variable to sweep
inj_ext_exponent = 1.5
inj_ext_flux= ${fparse 10^inj_ext_exponent / well_length / 2 }
injection_temp = 473.15
The above variables help to assign injection parameters for the operation. A typical seasonal operation with injection-storage-extraction-rest cycle was simulated. In the summer, cold fluid is extracted from the cold well and heated to an assigned temperature, then re-injected into the reservoir through the hot well at the same rate as the extraction. The same injection and extraction rates at the charging summer period ensure the pore pressure disturbance in the reservoir is minimized. In the winter, fluid is extracted from the hot well for the generation of electricity with the post-generation energy-depleted fluid being injected back into the reservoir through the cold well. The extraction and injection rates during the winter discharging period also remain the same as the summer charging time, and the injected fluid temperature into the cold well is the same as the initial reservoir temperature. For the fall and spring seasons, no fluid circulation is performed. Note in this example, high-temperature RTES is targeted to generate electricity when thermal energy is extracted. The energy extraction period is terminated when the temperature of the extracted hot well fluid is reduced by 20\% in relation to the difference between the fluid injection temperature and the initial reservoir temperature, i.e., we stop discharging when the temperature of the hot well satisfies . This high cut-off temperature is adopted to ensure the extracted hot fluid can be used for electricity generation. This operation was achieved by the combination of DiracKernels, Controls, Functions, and Postprocessors of FALCON.
First, we defined three groups of DiracKernels to realize the charging, discharging and rest operations from the two wells. Note the predefined variables of injection/extraction rate, well locations, and injection temperature are used as
[DiracKernels<<<{"href": "../syntax/DiracKernels/index.html"}>>>]
[./charge_injection_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_in_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '-${inj_ext_flux} -${inj_ext_flux}' # ~5 kg/s over length of 10(injection_length)/2
[../]
[./charge_injection_T]
type = EnthalpySink
fluid_phase = 0
variable = temperature
SumQuantityUO = heat_enthalpy_in_inc
line_base = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length = ${well_length}
line_direction = '0 0 1'
pressure = porepressure
T_in = ${injection_temp}
fp = tabulated_water
p_or_t_vals = '-1e9 1e9'
fluxes = '-${inj_ext_flux} -${inj_ext_flux}'
[../]
[./charge_production_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_out_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '${inj_ext_flux} ${inj_ext_flux}'
[../]
[./charge_production_T]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = heat_enthalpy_out_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
use_enthalpy<<<{"description": "Multiply the flux by the fluid enthalpy"}>>> = true
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '${inj_ext_flux} ${inj_ext_flux}'
[../]
[./recover_production_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_in_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '${inj_ext_flux} ${inj_ext_flux}' # ~5 kg/s over length of 10(injection_length)/2
[../]
[./recover_production_T]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = heat_enthalpy_in_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
use_enthalpy<<<{"description": "Multiply the flux by the fluid enthalpy"}>>> = true
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '${inj_ext_flux} ${inj_ext_flux}'
[../]
[./recover_injection_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_out_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '-${inj_ext_flux} -${inj_ext_flux}'
[../]
[./recover_injection_T]
type = EnthalpySink
fluid_phase = 0
variable = temperature
SumQuantityUO = heat_enthalpy_out_inc
line_base = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length = ${well_length}
line_direction = '0 0 1'
pressure = porepressure
T_in = ${T_ini_bc}
fp = tabulated_water
p_or_t_vals = '-1e9 1e9'
fluxes = '-${inj_ext_flux} -${inj_ext_flux}'
[../]
[./rest_production_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_in_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '0 0' # ~5 kg/s over length of 10(injection_length)/2
[../]
[./rest_production_T]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = heat_enthalpy_in_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${inj_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
use_enthalpy<<<{"description": "Multiply the flux by the fluid enthalpy"}>>> = true
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '0 0'
[../]
[./rest_injection_P]
type = PorousFlowPolyLineSink<<<{"description": "Approximates a polyline sink by using a number of point sinks with given weighting whose positions are read from a file. NOTE: if you are using PorousFlowPorosity that depends on volumetric strain, you should set strain_at_nearest_qp=true in your GlobalParams, to ensure the nodal Porosity Material uses the volumetric strain at the Dirac quadpoints, and can therefore be computed", "href": "../source/dirackernels/PorousFlowPolyLineSink.html"}>>>
fluid_phase<<<{"description": "The fluid phase whose pressure (and potentially mobility, enthalpy, etc) controls the flux to the line sink. For p_or_t=temperature, and without any use_*, this parameter is irrelevant"}>>> = 0
variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = porepressure
SumQuantityUO<<<{"description": "User Object of type=PorousFlowSumQuantity in which to place the total outflow from the line sink for each time step."}>>> = fluid_mass_out_inc
line_base<<<{"description": "Line base point x,y,z coordinates. This is the same format as a single-line point_file. Note this is only used if there is no point file specified."}>>> = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length<<<{"description": "Line length. Note this is only used if there is only one point in the point_file."}>>> = ${well_length}
line_direction<<<{"description": "Line direction. Note this is only used if there is only one point in the point_file."}>>> = '0 0 1'
p_or_t_vals<<<{"description": "Tuple of pressure (or temperature) values. Must be monotonically increasing."}>>> = '-1e9 1e9'
fluxes<<<{"description": "Tuple of flux values (measured in kg.m^-1.s^-1 if no 'use_*' are employed). These flux values are multiplied by the line-segment length to achieve a flux in kg.s^-1. A piecewise-linear fit is performed to the (p_or_t_vals,flux) pairs to obtain the flux at any arbitrary pressure (or temperature). If a quad-point pressure is less than the first pressure value, the first flux value is used. If quad-point pressure exceeds the final pressure value, the final flux value is used. This flux is OUT of the medium: hence positive values of flux means this will be a SINK, while negative values indicate this flux will be a SOURCE."}>>> = '0 0'
[../]
[./rest_injection_T]
type = EnthalpySink
fluid_phase = 0
variable = temperature
SumQuantityUO = heat_enthalpy_out_inc
line_base = '1 ${pro_well_x} 0 ${aquifer_bot}'
line_length = ${well_length}
line_direction = '0 0 1'
pressure = porepressure
T_in = ${T_ini_bc}
fp = tabulated_water
p_or_t_vals = '-1e9 1e9'
fluxes = '0 0'
[../]
[]
(examples/rtes_stochastic/base_cycle_50.i)The above three groups of DiracKernels, which control the well operation, are sequentially activated using Controls.
[Controls<<<{"href": "../syntax/Controls/index.html"}>>>]
[summer_injection]
type = ConditionalFunctionEnableControl<<<{"description": "Control for enabling/disabling objects when a function value is true", "href": "../source/controls/ConditionalFunctionEnableControl.html"}>>>
conditional_function<<<{"description": "The function to give a true or false value"}>>> = inj_function_summer
enable_objects<<<{"description": "A list of object tags to enable."}>>> = 'DiracKernel::charge_injection_P DiracKernel::charge_injection_T DiracKernel::charge_production_P DiracKernel::charge_production_T'
[]
[winter_injection]
type = ConditionalFunctionEnableControl<<<{"description": "Control for enabling/disabling objects when a function value is true", "href": "../source/controls/ConditionalFunctionEnableControl.html"}>>>
conditional_function<<<{"description": "The function to give a true or false value"}>>> = inj_function_winter
enable_objects<<<{"description": "A list of object tags to enable."}>>> = 'DiracKernel::recover_injection_P DiracKernel::recover_injection_T DiracKernel::recover_production_P DiracKernel::recover_production_T'
[]
[rest_injection]
type = ConditionalFunctionEnableControl<<<{"description": "Control for enabling/disabling objects when a function value is true", "href": "../source/controls/ConditionalFunctionEnableControl.html"}>>>
conditional_function<<<{"description": "The function to give a true or false value"}>>> = rest_function
enable_objects<<<{"description": "A list of object tags to enable."}>>> = 'DiracKernel::rest_injection_P DiracKernel::rest_injection_T DiracKernel::rest_production_P DiracKernel::rest_production_T'
[]
[]
(examples/rtes_stochastic/base_cycle_50.i)These controls are based on Functions, which are constructed in terms of season only for the charging operation and in terms of season and postprocessor values for the charging and rest operationsn as
[Functions<<<{"href": "../syntax/Functions/index.html"}>>>]
[./inj_function_summer]
type = ParsedFunction<<<{"description": "Function created by parsing a string", "href": "../source/functions/MooseParsedFunction.html"}>>>
value<<<{"description": "The user defined function."}>>> = '(t/24/3600/365.25-floor(t/24/3600/365.25))>0 & (t/24/3600/365.25-floor(t/24/3600/365.25))<=0.25'
[../]
[./inj_function_winter]
type = ParsedFunction<<<{"description": "Function created by parsing a string", "href": "../source/functions/MooseParsedFunction.html"}>>>
vals<<<{"description": "Constant numeric values, postprocessor names, function names, and scalar variables for vars."}>>> = 'termination'
vars<<<{"description": "Variables (excluding t,x,y,z) that are bound to the values provided by the corresponding items in the vals vector."}>>> = 'b_switch'
value<<<{"description": "The user defined function."}>>> = '((t/24/3600/365.25-floor(t/24/3600/365.25))>0.5 & (t/24/3600/365.25-floor(t/24/3600/365.25)) <=0.75) & b_switch <= 0'
[../]
[./rest_function]
type = ParsedFunction<<<{"description": "Function created by parsing a string", "href": "../source/functions/MooseParsedFunction.html"}>>>
vals<<<{"description": "Constant numeric values, postprocessor names, function names, and scalar variables for vars."}>>> = 'termination'
vars<<<{"description": "Variables (excluding t,x,y,z) that are bound to the values provided by the corresponding items in the vals vector."}>>> = 'b_switch'
value<<<{"description": "The user defined function."}>>> = '(((t/24/3600/365.25-floor(t/24/3600/365.25))>0.25 & (t/24/3600/365.25-floor(t/24/3600/365.25)) <=0.5) | ((t/24/3600/365.25-floor(t/24/3600/365.25))>0.75 & (t/24/3600/365.25-floor(t/24/3600/365.25)) <=1)) | (((t/24/3600/365.25-floor(t/24/3600/365.25))>0.5 & (t/24/3600/365.25-floor(t/24/3600/365.25)) <=0.75) & b_switch >=1)'
[../]
[./Fiss_Function]
type = PiecewiseLinear<<<{"description": "Linearly interpolates between pairs of x-y data", "href": "../source/functions/PiecewiseLinear.html"}>>>
x<<<{"description": "The abscissa values"}>>> = '0. 7889400. 15778800. 23668200.
31557600. 39447000. 47336400. 55225800.
63115200. 71004600. 78894000. 86783400.
94672800. 102562200. 110451600. 118341000.
126230400. 134119800. 142009200. 149898600.
157788000. 165677400. 173566800. 181456200.
189345600. 197235000. 205124400. 213013800.
220903200. 228792600. 236682000. 244571400.
252460800. 260350200. 268239600. 276129000.
284018400. 291907800. 299797200. 307686600.
315576000.'
y<<<{"description": "The ordinate values"}>>> = '0 1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4'
[../]
[]
(examples/rtes_stochastic/base_cycle_50.i)The b_swith value is from the following termination postprocessor, its value changes to 1 when and changes to 0 when :
[./termination]
type = PorousFlowTemperatureDropTerminator
enthalpypostprocessor = hotwell_enthalpy_J
masspostprocessor = hotwell_mass_kg
T_inj = ${injection_temp}
T_init = ${T_ini_bc}
P_drop = 80
timepostprocessor = time
[../]
Materials: Constant material parameters,including porosity, permeability, thermal conductivity, specific heat and density, are assigned with valuse listed in Table 1. The input file section for material is
[Materials<<<{"href": "../syntax/Materials/index.html"}>>>]
[./internal_energy_aquifer]
type = PorousFlowMatrixInternalEnergy<<<{"description": "This Material calculates the internal energy of solid rock grains, which is specific_heat_capacity * density * temperature. Kernels multiply this by (1 - porosity) to find the energy density of the porous rock in a rock-fluid system", "href": "../source/materials/PorousFlowMatrixInternalEnergy.html"}>>>
specific_heat_capacity<<<{"description": "Specific heat capacity of the rock grains (J/kg/K)."}>>> = 930.0
density<<<{"description": "Density of the rock grains"}>>> = 2650.0
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'aquifer_HEX8 aquifer_WEDGE'
[../]
[./thermal_conductivity_aquifer]
type = PorousFlowThermalConductivityIdeal<<<{"description": "This Material calculates rock-fluid combined thermal conductivity by using a weighted sum. Thermal conductivity = dry_thermal_conductivity + S^exponent * (wet_thermal_conductivity - dry_thermal_conductivity), where S is the aqueous saturation", "href": "../source/materials/PorousFlowThermalConductivityIdeal.html"}>>>
dry_thermal_conductivity<<<{"description": "The thermal conductivity of the rock matrix when the aqueous saturation is zero"}>>> = '${Tcond_aquifer} 0 0 0 ${Tcond_aquifer} 0 0 0 ${Tcond_aquifer}'
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'aquifer_HEX8 aquifer_WEDGE'
[../]
[./porosity_aquifer]
type = PorousFlowPorosityConst<<<{"description": "This Material calculates the porosity assuming it is constant", "href": "../source/materials/PorousFlowPorosityConst.html"}>>>
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'aquifer_HEX8 aquifer_WEDGE'
porosity<<<{"description": "The porosity (assumed indepenent of porepressure, temperature, strain, etc, for this material). This should be a real number, or a constant monomial variable (not a linear lagrange or other kind of variable)."}>>> = 0.01
[../]
[./permeability_aquifer]
type = PorousFlowPermeabilityConst<<<{"description": "This Material calculates the permeability tensor assuming it is constant", "href": "../source/materials/PorousFlowPermeabilityConst.html"}>>>
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'aquifer_HEX8 aquifer_WEDGE'
permeability<<<{"description": "The permeability tensor (usually in m^2), which is assumed constant for this material"}>>> = '${perm_aquifer} 0 0 0 ${perm_aquifer} 0 0 0 ${perm_aquifer}'
[../]
[./internal_energy_caps]
type = PorousFlowMatrixInternalEnergy<<<{"description": "This Material calculates the internal energy of solid rock grains, which is specific_heat_capacity * density * temperature. Kernels multiply this by (1 - porosity) to find the energy density of the porous rock in a rock-fluid system", "href": "../source/materials/PorousFlowMatrixInternalEnergy.html"}>>>
specific_heat_capacity<<<{"description": "Specific heat capacity of the rock grains (J/kg/K)."}>>> = 1000.0
density<<<{"description": "Density of the rock grains"}>>> = 2500.0
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'caps_HEX8 caps_WEDGE'
[../]
[./thermal_conductivity_caps]
type = PorousFlowThermalConductivityIdeal<<<{"description": "This Material calculates rock-fluid combined thermal conductivity by using a weighted sum. Thermal conductivity = dry_thermal_conductivity + S^exponent * (wet_thermal_conductivity - dry_thermal_conductivity), where S is the aqueous saturation", "href": "../source/materials/PorousFlowThermalConductivityIdeal.html"}>>>
dry_thermal_conductivity<<<{"description": "The thermal conductivity of the rock matrix when the aqueous saturation is zero"}>>> = '2.5 0 0 0 2.5 0 0 0 2.5'
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'caps_HEX8 caps_WEDGE'
[../]
[./porosity_caps]
type = PorousFlowPorosityConst<<<{"description": "This Material calculates the porosity assuming it is constant", "href": "../source/materials/PorousFlowPorosityConst.html"}>>>
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'caps_HEX8 caps_WEDGE'
porosity<<<{"description": "The porosity (assumed indepenent of porepressure, temperature, strain, etc, for this material). This should be a real number, or a constant monomial variable (not a linear lagrange or other kind of variable)."}>>> = 0.01
[../]
[./permeability_caps]
type = PorousFlowPermeabilityConst<<<{"description": "This Material calculates the permeability tensor assuming it is constant", "href": "../source/materials/PorousFlowPermeabilityConst.html"}>>>
block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = 'caps_HEX8 caps_WEDGE'
permeability<<<{"description": "The permeability tensor (usually in m^2), which is assumed constant for this material"}>>> = '1E-18 0 0 0 1E-18 0 0 0 1E-18'
[../]
[]
(examples/rtes_stochastic/base_cycle_50.i)Note the variables of reservoir permeability and thermal conductivity are defined at the start of the input file are used to in this section.
#---Define a scalar variable to replace the tensor components
Tcond_aquifer = 2.0
#---Define a scalar variable to replace the tensor components
perm_exponent = -12
perm_aquifer = ${fparse 10^perm_exponent}
Table 1: Modeling parameters with uncertain/fixed values used for the simulations
Parameters | Symbol | Units | Reservoir | Caprock |
---|---|---|---|---|
Permeability | ||||
Porosity | - | |||
Themal Conductivity | | | |||
Specific Heat | 930 | 1000 | ||
Grain Density | 2650 | 2500 | ||
Formation Thickness | 10-100 | 50 | ||
Well Distance | 100-200 | - | ||
Flow Rate | 1-1000 | - | ||
Inj. Temperature | 100-300 | - | ||
Formation Depth | 1000-3000 | - |
Stochastic sampling input file
The stochastic Tools Module implemented in MOOSE was used to perform automated parameteric sweeping. As shown in Table 1, uniform distributions with min-max range were used for all sweeping parameters except the ones with several orders of magnitude difference in min-max values (i.e., permeability, flow rate). A logorithmic scaling was performed for the two parameters and their exponents of min-max values was used to define the uniform distribution ranges. Accordinlgy, the distribution section is
[Distributions<<<{"href": "../syntax/Distributions/index.html"}>>>]
[depth]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = 1000
upper_bound<<<{"description": "Distribution upper bound"}>>> = 3000
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[injection_temp]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = '${fparse 100 + 273.15}'
upper_bound<<<{"description": "Distribution upper bound"}>>> = '${fparse 300 + 273.15}'
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[inj_ext_rate]
# need to adopt the aquifer thickness
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = 0.5
upper_bound<<<{"description": "Distribution upper bound"}>>> = 2.5
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[distance_between_wells]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = 100
upper_bound<<<{"description": "Distribution upper bound"}>>> = 200
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[perm_aquifer]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = -15
upper_bound<<<{"description": "Distribution upper bound"}>>> = -11
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[porosity]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = 0.01
upper_bound<<<{"description": "Distribution upper bound"}>>> = 0.3
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[Tcond_aquifer]
type = Uniform<<<{"description": "Continuous uniform distribution.", "href": "../source/distributions/Uniform.html"}>>>
lower_bound<<<{"description": "Distribution lower bound"}>>> = 2
upper_bound<<<{"description": "Distribution upper bound"}>>> = 4
n = 2
cartprod = '${lower_bound} ${fparse (upper_bound - lower_bound) / (n - 1)} ${n}'
[]
[]
(examples/rtes_stochastic/master_cycle_50.i)A Monte Carlo sampler was adopted to sample the values of depth, injection fluid temperature, flow rate exponent, distance between wells, reservoir permeability exponent, porosity, thermal conductivity from their corresponding uniform distribution defined previously. The input file seciton is
[Samplers<<<{"href": "../syntax/Samplers/index.html"}>>>]
[montecarlo]
type = MonteCarlo<<<{"description": "Monte Carlo Sampler.", "href": "../source/samplers/MonteCarloSampler.html"}>>>
num_rows<<<{"description": "The number of rows per matrix to generate."}>>> = 50000
distributions<<<{"description": "The distribution names to be sampled, the number of distributions provided defines the number of columns per matrix."}>>> = 'depth injection_temp inj_ext_rate distance_between_wells perm_aquifer porosity Tcond_aquifer'
execute_on<<<{"description": "The list of flag(s) indicating when this object should be executed. For a description of each flag, see https://mooseframework.inl.gov/source/interfaces/SetupInterface.html."}>>> = 'PRE_MULTIAPP_SETUP'
[]
[cartesian]
type = CartesianProduct<<<{"description": "Provides complete Cartesian product for the supplied variables.", "href": "../source/samplers/CartesianProductSampler.html"}>>>
linear_space_items<<<{"description": "A list of triplets, each item should include the min, step size, and number of steps."}>>> = '${Distributions/depth/cartprod}
${Distributions/injection_temp/cartprod}
${Distributions/inj_ext_rate/cartprod}
${Distributions/distance_between_wells/cartprod}
${Distributions/perm_aquifer/cartprod}
${Distributions/porosity/cartprod}
${Distributions/Tcond_aquifer/cartprod}'
execute_on<<<{"description": "The list of flag(s) indicating when this object should be executed. For a description of each flag, see https://mooseframework.inl.gov/source/interfaces/SetupInterface.html."}>>> = 'PRE_MULTIAPP_SETUP'
[]
[]
(examples/rtes_stochastic/master_cycle_50.i)In order to assign the sampled values to the corresponding parameters of the physics input file, a Control section is needed to connect the exact variables inside the physics simulation input file to the distributions of stochastic simulation input file as
[Controls<<<{"href": "../syntax/Controls/index.html"}>>>]
[cmdline]
type = MultiAppCommandLineControl<<<{"description": "Control for modifying the command line arguments of MultiApps.", "href": "../source/controls/MultiAppSamplerControl.html"}>>>
multi_app<<<{"description": "The name of the MultiApp to control."}>>> = runner
sampler<<<{"description": "The Sampler object to utilize for altering the command line options of the MultiApp."}>>> = ${sampler}
param_names<<<{"description": "The names of the command line parameters to set via the sampled data."}>>> = 'depth injection_temp inj_ext_exponent distance_between_wells perm_exponent Materials/porosity_aquifer/porosity Tcond_aquifer'
[]
[]
(examples/rtes_stochastic/master_cycle_50.i)Note the sequence of param_names in Control section needs to match with the distributions in montecarlo section. The use of defined variables in the physics simulation input file greatly simplfies the connection between the two input files.
The transfer section was used to collect RTES performance metrics of each realized sample. It includes recovery efficiencty (the ratio of extracted thermal energy over the injected energy), storage capacity (total amount of energy injected each year) and operating time (the function time of extraction to generate electricity). These performance metrics were defined in Postprocessors of the physics simultion input file,
[./recovery_rate]
type = PorousFlowRecoveryRateSeason
hotwellenergy = hotwell_enthalpy_J
coldwellenergy = coldwell_enthalpy_J
InjectionIndicator = is_summer
ProductionIndicator = is_winter
[../]
[./total_energy]
type = PorousFlowEnergyAccumulator
hotwellenergy = hotwell_enthalpy_J
coldwellenergy = coldwell_enthalpy_J
ProductionIndicator = is_winter
[../]
[./total_recovery_time]
type = PorousFlowRecoveryTimeAccumulator
targetpostprocessor = is_winter
dtpostprocessor = step_dt
[../]
```
and supplied in the stochatic simulation input file as
```
[Transfers]
[results]
type = SamplerPostprocessorTransfer
multi_app = runner
sampler = ${sampler}
to_vector_postprocessor = results
from_postprocessor = 'recovery_rate total_recovery_time total_energy'
[]
[]
The final section of the stochastic simulatiom input file is the MultiApps section to define the physics simultion input file that it can work with as
[MultiApps]
[runner]
type = SamplerFullSolveMultiApp
sampler = ${sampler}
input_files = 'base_cycle_50.i'
mode = batch-reset
ignore_solve_not_converge = true
[]
[]
Results
As shown in Figure 2, we simulate 10 years for the seasonal cycle scenario of each case. The pore pressure evolution sampled from a node close to the middle of the hot well shows spikes at the start of injection of each summer, which is resulted from the line sink boundary condition and fluid viscosity dropping with increasing temperature. This effect is less significant over time because the RTES system has been warmed up as a portion of thermal energy is not extracted during the winter in each cycle. The warming trend is clearly shown in Figure 2 as the temperature at the end of each cycle continues to increase throughout the 10 years of operation.

Figure 2: Pore pressure and temperature evolution at the middle of the hot well for the scenario of injection-storage-extraction-rest seasonal cycle of 10 years operation. Note that the extraction operation only occurs when the temperature of the fluid in the hot well () exceeds the cut-off temperature (i.e.,), and the hot well temperature can raise again due to heat conduction when extraction stops. This temperature raise will restart the extraction which results in a zigzag-shaped curve during the extraction season as shown above. Note the temperature of the extracted fluid from the hot well has a discrepancy with the temperature at the middle of the well.
Figure 3 shows the histograms of the formation parameters (green) and the operating conditions (magenta) of all the realized simulations. The screening-out process of the physically meaningless cases and the unreasonable cases in practice also results in the non-uniform distribution in flow rate and permeability.

Figure 3: Histograms of the input parameters for all the realized simulations of the seasonal cycle operation. The y-axis stands for the realized simulation numbers after screening. We color-code the formation characteristics in green and the operation conditions in magenta.
Figure 4 plots the histograms of the RTES performance metrics for the seasonal cycle case. Because we stop and restart the extraction process during the winter season according to the fluid temperature in the hot well, the recovery efficiency is high, and the operation time spans from 50 days to 80 days with a close to uniform distribution. The predicted annual storage capacity is close to a power-law distribution, and we plotted it in the logarithmic scale.

Figure 4: Histograms of the output parameters for all the realized simulations of the seasonal cycle operation.Note the performance metrics are all annual average values.
References
- W Jin, R Podgorney, T McLing, and RW Carlsen.
Geothermal battery optimization using stochastic hydro-thermal simulations and machine learning algorithms.
In 55th US Rock Mechanics/Geomechanics Symposium. OnePetro, 2021.[BibTeX]
- Wencheng Jin, Trevor Atkinson, Christine Doughty, Ghanashyam Neupane, Nicolas Spycher, Travis McLing, Patrick Dobson, Robert Smith, and Robert Podgorney.
Machine-learning-assisted high-temperature reservoir thermal energy storage optimization.
Renewable Energy, 2022.[BibTeX]