Simulating a simple GeoTES experiment

This page describes a reactive-transport simulation of a simple GeoTES scenario. The transport is handled using the PorousFlow module, while the Geochemistry module is used to simulate a heat exchanger and the geochemistry. The simulations are coupled together in an operator-splitting approach using MultiApps.

In Geological Thermal Energy Storage (GeoTES), hot fluid is pumped into an subsurface confined aquifer, stored there for some time, then pumped back again to produce electricity. Many different scenarios have been proposed, including different borehole geometries (used to pump the fluid), different pumping strategies and cyclicities (pumping down certain boreholes, up other boreholes, etc), different temperatures, different aquifer properties, etc. This page only describes the heating phase of a particularly simple set up.

Two wells (boreholes) are used.

  1. The cold (production) well, which pumps aquifer water from the aquifer.

  2. The hot (injection) well, which pumps hot fluid into the aquifer.

These are connected by a heat-exchanger, which heats the aquifer water produced from (1) and passes it to (2). This means the fluid pumped through the injection well is, in fact, heated aquifer water, which is important practically from a water-conservation standpoint, and to assess the geochemical aspects of circulating aquifer water through the GeoTES system.

Figure 1: Schematic of the simple GeoTES simulation.

Aquifer geochemistry

It is assumed that only HO, Na, Cl and SiO(aq) are present in the system. While this is unrealistically simple for any aquifer, it allows this page to focus on the method of constructing more complicated models avoiding rather lengthy input files.

The database file

A small geochemical database involving only these species is used. Readers who are building their own database might find exploring its structure useful:

{
  "Header": {
    "title": "Very small MOOSE thermodynamic database",
    "original": "moose/modules/geochemistry/python/original_dbs/gwb_llnl.dat",
    "date": "09:14 18-08-2020",
    "original format": "gwb",
    "activity model": "debye-huckel",
    "fugacity model": "tsonopoulos",
    "logk model": "fourth-order",
    "logk model (functional form)": "a0 + a1 T + a2 T^2 + a3 T^3 + a4 T^4",
    "temperatures": [
      0.0,
      25.0,
      60.0,
      100.0,
      150.0,
      200.0,
      250.0,
      300.0
    ],
    "pressures": [
      1.0134,
      1.0134,
      1.0134,
      1.0134,
      4.76,
      15.549,
      39.776,
      85.927
    ],
    "adh": [
      0.4913,
      0.5092,
      0.545,
      0.5998,
      0.6898,
      0.8099,
      0.9785,
      1.2555
    ],
    "bdh": [
      0.3247,
      0.3283,
      0.3343,
      0.3422,
      0.3533,
      0.3655,
      0.3792,
      0.3965
    ],
    "bdot": [
      0.0174,
      0.041,
      0.044,
      0.046,
      0.047,
      0.047,
      0.034,
      0.0
    ],
    "neutral species": {
      "co2": {
        "a": [
          0.1224,
          0.1127,
          0.09341,
          0.08018,
          0.08427,
          0.09892,
          0.1371,
          0.1967
        ],
        "b": [
          -0.004679,
          -0.01049,
          -0.0036,
          -0.001503,
          -0.01184,
          -0.0104,
          -0.007086,
          -0.01809
        ],
        "c": [
          -0.0004114,
          0.001545,
          9.609e-05,
          0.0005009,
          0.003118,
          0.001386,
          -0.002887,
          -0.002497
        ],
        "d": [
          0.0,
          0.0,
          0.0,
          0.0,
          0.0,
          0.0,
          0.0,
          0.0
        ]
      },
      "h2o": {
        "a": [
          1.4203,
          1.45397,
          1.5012,
          1.5551,
          1.6225,
          1.6899,
          1.7573,
          1.8247
        ],
        "note": "Missing array values in original database have been filled using a linear fit. Original values are [500.0000, -.0005362, 500.0000, -.0007132, -.0008312, 500.0000, 500.0000, 500.0000]",
        "b": [
          0.0177,
          0.022357,
          0.0289,
          0.036478,
          0.045891,
          0.0553,
          0.0647,
          0.0741
        ],
        "c": [
          0.0103,
          0.0093804,
          0.008,
          0.0064366,
          0.0045221,
          0.0026,
          0.0006,
          -0.0013
        ],
        "d": [
          -0.0005,
          -0.0005362,
          -0.0006,
          -0.0007132,
          -0.0008312,
          -0.0009,
          -0.0011,
          -0.0012
        ]
      }
    }
  },
  "elements": {
    "Cl": {
      "name": "Chlorine",
      "molecular weight": 35.453
    },
    "H": {
      "name": "Hydrogen",
      "molecular weight": 1.0079
    },
    "Na": {
      "name": "Sodium",
      "molecular weight": 22.9898
    },
    "O": {
      "name": "Oxygen",
      "molecular weight": 15.9994
    },
    "Si": {
      "name": "Silicon",
      "molecular weight": 28.085
    }
  },
  "basis species": {
    "H2O": {
      "elements": {
        "H": 2.0,
        "O": 1.0
      },
      "radius": 0.0,
      "charge": 0.0,
      "molecular weight": 18.0152
    },
    "Cl-": {
      "elements": {
        "Cl": 1.0
      },
      "radius": 3.0,
      "charge": -1.0,
      "molecular weight": 35.453
    },
    "Na+": {
      "elements": {
        "Na": 1.0
      },
      "radius": 4.0,
      "charge": 1.0,
      "molecular weight": 22.9898
    },
    "SiO2(aq)": {
      "elements": {
        "O": 2.0,
        "Si": 1.0
      },
      "radius": -0.5,
      "charge": 0.0,
      "molecular weight": 60.0843
    }
  },
  "secondary species": {
    "NaCl": {
      "species": {
        "Na+": 1.0,
        "Cl-": 1.0
      },
      "charge": 0.0,
      "radius": 4.0,
      "molecular weight": 58.4428,
      "logk": [
        1.855,
        1.5994,
        1.2945,
        0.9856,
        0.635,
        0.2935,
        -0.0407,
        -0.3487
      ],
      "note": "Missing array values in original database have been filled using a fourth-order fit. Original values are [1.8550, 1.5994, 1.2945, .9856, .6350, .2935, 500.0000, 500.0000]"
    }
  },
  "free electron": {
    "e-": {
      "species": {
        "H2O": 0.5,
        "O2(g)": -0.25,
        "H+": -1.0
      },
      "charge": -1.0,
      "radius": 0.0,
      "molecular weight": 0.0,
      "logk": [
        22.76135,
        20.7757,
        18.513025,
        16.4658,
        14.473225,
        12.92125,
        11.68165,
        10.67105
      ]
    }
  },
  "mineral species": {
    "QuartzLike": {
      "species": {
        "SiO2(aq)": 1.0
      },
      "molar volume": 22.688,
      "molecular weight": 60.0843,
      "logk": [-5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0]
    },
    "QuartzUnlike": {
      "species": {
        "SiO2(aq)": 1.0
      },
      "molar volume": 22.688,
      "molecular weight": 60.0843,
      "logk": [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0]
    }
  }
}
(modules/combined/examples/geochem-porous_flow/geotes_2D/small_database.json)

Initial conditions

The aquifer temperature is uniformly 50C before injection and production.

The aquifer is assumed to be consist entirely of a mineral called QuartzLike and a kinetic rate is used for its dissolution. The mineral QuartzLike is similar to Quartz but it has a different equilibrium constant structure in order to emphasise the dissolution upon heating.

Each 1kg of solvent water is assumed to hold 0.1mol of Na and Cl, and a certain amount of SiO(aq). The following input file finds the free molality of SiO(aq) when in contact with QuartzLike at 50C:

# Finds the equilibrium free molality of SiO2(aq) when in contact with QuartzLike at 50degC
[UserObjects]
  [definition]
    type = GeochemicalModelDefinition
    database_file = "small_database.json"
    basis_species = "H2O SiO2(aq) Na+ Cl-"
    equilibrium_minerals = "QuartzLike"
  []
[]

[TimeIndependentReactionSolver]
  model_definition = definition
  geochemistry_reactor_name = reactor
  charge_balance_species = "Cl-"
  swap_out_of_basis = "SiO2(aq)"
  swap_into_basis = QuartzLike
  constraint_species = "H2O              Na+              Cl-              QuartzLike"
  constraint_value = "  1.0              0.1              0.1              396.685" # amount of QuartzLike is unimportant (provided it is positive).  396.685 is used simply because the other geotes_2D input files use this amount
  constraint_meaning = "kg_solvent_water bulk_composition bulk_composition free_mineral"
  constraint_unit = "   kg               moles            moles            moles"
  temperature = 50.0
  ramp_max_ionic_strength_initial = 0 # max_ionic_strength in such a simple problem does not need ramping
  add_aux_pH = false # there is no H+ in this system
  precision = 12
[]

[Postprocessors]
  [free_moles_SiO2]
    type = PointValue
    point = '0 0 0'
    variable = 'molal_SiO2(aq)'
  []
[]

[Outputs]
  csv = true
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_equilibrium.i)

The result it molality = 0.000555052386. It is necessary to find this initial molality, only because QuartzLike is a kinetic species, so cannot appear the basis (see another similar example). If it were treated as an equilibrium species, its initial free mole number could be specified in the input file below (see an example of this).

Spatially-dependent geochemistry simulation

Because an operator-splitting approach is used, the simulation of the aquifer geochemistry may be considered separately from the porous-flow simulation and the heat exchanger simulation. This section describes how the aquifer geochemistry is simulated.

The model set-up commences with defining the basis species, minerals, and the rate description for the kinetic dissolution of QuartzLike:

[UserObjects]
  [rate_quartz]
    type = GeochemistryKineticRate
    kinetic_species_name = QuartzLike
    intrinsic_rate_constant = 1.0E-2
    multiply_by_mass = true
    area_quantity = 1
    activation_energy = 72800.0
  []
  [definition]
    type = GeochemicalModelDefinition
    database_file = "small_database.json"
    basis_species = "H2O SiO2(aq) Na+ Cl-"
    kinetic_minerals = "QuartzLike"
    kinetic_rate_descriptions = "rate_quartz"
  []
  [nodal_void_volume_uo]
    type = NodalVoidVolume
    porosity = porosity
    execute_on = 'initial timestep_end' # "initial" means this is evaluated properly for the first timestep
  []
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_geochemistry.i)

The parameters in the rate description are similar to those chosen in another example.

A SpatialReactionSolver defines the initial concentrations of the species and how reactants are added to the system

[SpatialReactionSolver]
  model_definition = definition
  geochemistry_reactor_name = reactor
  charge_balance_species = "Cl-"
  constraint_species = "H2O              Na+              Cl-              SiO2(aq)"
  # ASSUME that 1 litre of solution contains:
  constraint_value = "  1.0              0.1              0.1              0.000555052386"
  constraint_meaning = "kg_solvent_water bulk_composition bulk_composition free_concentration"
  constraint_unit = "   kg               moles            moles            molal"
  initial_temperature = 50.0
  kinetic_species_name = QuartzLike
  # Per 1 litre (1000cm^3) of aqueous solution (1kg of solvent water), there is 9000cm^3 of QuartzLike, which means the initial porosity is 0.1.
  kinetic_species_initial_value = 9000
  kinetic_species_unit = cm3
  temperature = temperature
  source_species_names = 'H2O    Na+   Cl-   SiO2(aq)'
  source_species_rates = 'rate_H2O_per_1l rate_Na_per_1l rate_Cl_per_1l rate_SiO2_per_1l'
  ramp_max_ionic_strength_initial = 0 # max_ionic_strength in such a simple problem does not need ramping
  add_aux_pH = false # there is no H+ in this system
  evaluate_kinetic_rates_always = true # implicit time-marching used for stability
  execute_console_output_on = ''
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_geochemistry.i)

Most lines in this block need further discussion.

  • Since this is a SpatialReactionSolver, the system described exists at each node in the mesh. Each node will typically have a different void volume fraction associated to it: nodes in coarsely-meshed regions will be responsible for large volumes of fluid, while nodes in finely-meshed regions, or regions with small porosity, will represent small volumes of fluid. In the above setup, just 1 liter of aqueous solution is modelled at each node. As discussed below, the rates of reactant additions will be adjusted to reflect this. It is assumed that 1 liter of aqueous solution contains 1.0kg of solvent water and the specified quantities of Na, Cl and SiO(aq).

  • The free_molality constraint on SiO(aq) is set according to the equilibrium simulation described above.

  • It is assumed that there is 9000cm of QuartzLike per the 1000cm (1 liter) of aqueous solution. This corresponds to 396.685mol of QuartzLike. The reason for choosing 9000cm of QuartzLike is that the aquifer porosity is assumed to be 0.1 and to be calculated using:

  • The initial temperature of the aquifer is assumed to be 50C, and during simulation it is given by the AuxVariable called temperature. This AuxVariable will be provided by the PorousFlow simulation described below.

  • The source_species_rates are defined by the rate_*_per_1l AuxVariables which are discussed further below.

The Mesh and Executioner are provided in the input file so that it may be run alone in a stand-alone fashion, but these are overwritten when coupled with the PorousFlow simulation (below). The remainder of the input file deals with AuxVariables that are used to transfer information to-and-from the PorousFlow simulation.

The PorousFlow simulation provides temperature, pf_rate_H2O, pf_rate_Na, pf_rate_Cl and pf_rate_SiO2 as AuxVariables. The pf_rate quantities are rates of changes of fluid-component mass (kg.s) at each node, but the Geochemistry module expects rates-of-changes of moles (mol.s) at each node. Moreover, since this input file considers just 1 liter of aqueous solution at every node, the nodal_void_volume is used to convert to mol.s/(liter of aqueous solution). For instance:

  [rate_H2O_per_1l_auxk]
    type = ParsedAux
    coupled_variables = 'pf_rate_H2O nodal_void_volume'
    variable = rate_H2O_per_1l
# pf_rate = change in kg at every node
# pf_rate * 1000 / molar_mass_in_g_per_mole = change in moles at every node
# pf_rate * 1000 / molar_mass / (nodal_void_volume_in_m^3 * 1000) = change in moles per litre of aqueous solution
    expression = 'pf_rate_H2O / 18.0152 / nodal_void_volume'
    execute_on = 'timestep_begin'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_geochemistry.i)

This input file provides the PorousFlow simulation with updated mass-fractions (after QuartzLike dissolution has occurred to increase the SiO(aq) concentration, for instance). These are based on the "transported" mole numbers at each node, which are the mole numbers of the transported species in the original basis (mole numbers of HO, Na, Cl and free moles of SiO(aq) in this case). These are recorded into AuxVariables using, for example:

  [transported_H2O_auxk]
    type = GeochemistryQuantityAux
    variable = transported_H2O
    species = H2O
    quantity = transported_moles_in_original_basis
    execute_on = 'timestep_end'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_geochemistry.i)

The mass fraction is computed using, for example

  [transported_mass_auxk]
    type = ParsedAux
    coupled_variables = 'transported_H2O transported_Na transported_Cl transported_SiO2'
    variable = transported_mass
    expression = 'transported_H2O * 18.0152 + transported_Na * 22.9898 + transported_Cl * 35.453 + transported_SiO2 * 60.0843'
    execute_on = 'timestep_end'
  []
  [massfrac_H2O]
    type = ParsedAux
    coupled_variables = 'transported_H2O transported_mass'
    variable = massfrac_H2O
    expression = 'transported_H2O * 18.0152 / transported_mass'
    execute_on = 'timestep_end'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/aquifer_geochemistry.i)

The PorousFlow simulation

The PorousFlow module is used to perform the transport. It is assumed the aquifer may be adequately modelled using a 2D mesh. The injection well is at and the production well at :

[Mesh]
  [gen]
    type = GeneratedMeshGenerator
    dim = 2
    nx = 14 # for better resolution, use 56 or 112
    ny = 8 # for better resolution, use 32 or 64
    xmin = -70
    xmax = 70
    ymin = -40
    ymax = 40
  []
  [injection_node]
    input = gen
    type = ExtraNodesetGenerator
    new_boundary = injection_node
    coord = '-30 0 0'
  []
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)
1.0 30 0 0
(modules/combined/examples/geochem-porous_flow/geotes_2D/production.bh)

Recall that the PorousFlow module considers mass fractions, not mole-fractions or molalities, etc. Denoting the mass-fraction of Na by f0, the mass-fraction of Cl by f1, and the mass-fraction of SiO(aq) by f2 (the mass-fraction of HO may be worked out by summing mass-fractions to unity), the Variables are:

[Variables]
  [f0]
    initial_condition = 0.002285946
  []
  [f1]
    initial_condition = 0.0035252
  []
  [f2]
    initial_condition = 1.3741E-05
  []
  [porepressure]
    initial_condition = 2E6
  []
  [temperature]
    initial_condition = 50
    scaling = 1E-6 # fluid enthalpy is roughly 1E6
  []
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

The initial conditions are chosen to reflect the temperature and mole numbers of the aquifer geochemistry input file, and an initial porepressure of 2MPa is used, corresponding to an approximate depth of 200m.

Much of the remainder of this input file is typical of PorousFlow simulations. The points of particular interest here involve the transfer of information to the aquifer geochemistry simulation (above) and the heat-exchanger simulation (below).

Transfer to the aquifer geochemistry simulation

The PorousFlow input file transfers the rates of changes of each species (kg.s) at each node to the aquifer geochemistry simulation. This is achieved through saving these changes from Kernel residual evaluations into AuxKernels, using the save_component_rate_in:

[PorousFlowFullySaturated]
  coupling_type = ThermoHydro
  porepressure = porepressure
  temperature = temperature
  mass_fraction_vars = 'f0 f1 f2'
  save_component_rate_in = 'rate_Na rate_Cl rate_SiO2 rate_H2O' # change in kg at every node / dt
  fp = the_simple_fluid
  temperature_unit = Celsius
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

Transfer from the aquifer geochemistry simulation

At the end of each time-step, the aquifer geochemistry simulation provides updated mass-fractions to the PorousFlow simulation:

  [massfrac_from_geochem]
    type = MultiAppCopyTransfer
    source_variable = 'massfrac_Na massfrac_Cl massfrac_SiO2'
    variable = 'f0 f1 f2'
    from_multi_app = react
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

Interfacing with the heat-exchanger simulation

This input file simulates fluid-and-heat injection through the well at and fluid-and-heat extraction from the well at .

The heat injection is modelled using a fixed value of temperature at the injection_node:

[BCs]
  [injection_temperature]
    type = MatchedValueBC
    variable = temperature
    v = injection_temperature
    boundary = injection_node
  []
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

The injection_temperature variable is set by the exchanger simulation (below) via a MultiApp Transfer. The fluid injection is modelled using a PorousFlowPolyLineSink, such as:

  [inject_Na]
    type = PorousFlowPolyLineSink
    SumQuantityUO = injected_mass
    fluxes = ${injection_rate}
    p_or_t_vals = 0.0
    line_length = 1.0
    multiplying_var = injection_rate_massfrac_Na
    point_file = injection.bh
    variable = f0
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

In this block:

  • The SumQuantityUO measures the injected mass, for purposes of checking that the correct amount of fluid has been injected

  • The ${injected_rate} is a constant specified at the top of the input file as 1kg.s.m.

  • The combination of ${injected_rate}, p_or_t_vals, line_length and multiplying_var means that the variable (which is f0, or the mass-fraction of Na) will be subjected to a source of size injection_rate_massfrac_Na at the borehole's position

The four injection_rate_massfrac_* variables are set by the exchanger simulation (below) via a MultiApp Transfer.

The fluid and heat production from the other well are also simulated by PorousFlowPolyLineSinks, such as:

  [produce_Na]
    type = PorousFlowPolyLineSink
    SumQuantityUO = produced_mass_Na
    fluxes = ${production_rate}
    p_or_t_vals = 0.0
    line_length = 1.0
    mass_fraction_component = 0
    point_file = production.bh
    variable = f0
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

In this block:

  • the produced_mass_Na records the amount of Na produced;

  • the ${injected_rate} is a constant specified at the top of the input file as 1kg.s.m.

  • the mass_fraction_component = 0 instructs PorousFlow to capture only the zeroth mass fraction (Na)

Heat energy is captured using a similar technique (notice the use_enthalpy flag):

  [produce_heat]
    type = PorousFlowPolyLineSink
    SumQuantityUO = produced_heat
    fluxes = ${production_rate}
    p_or_t_vals = 0.0
    line_length = 1.0
    use_enthalpy = true
    point_file = production.bh
    variable = temperature
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

The produced_mass_* quantities and the temperature at the production well are provided to the heat-exchanger input file (below). However, that input file works in mole units, not mass fractions, so a translation is needed:

  [kg_Na_produced_this_timestep]
    type = PorousFlowPlotQuantity
    uo = produced_mass_Na
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)
  [mole_rate_Na_produced]
    type = FunctionValuePostprocessor
    function = moles_Na
    indirect_dependencies = 'kg_Na_produced_this_timestep dt'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)
  [moles_Na]
    type = ParsedFunction
    symbol_names = 'kg_Na dt'
    symbol_values = 'kg_Na_produced_this_timestep dt'
    expression = 'kg_Na * 1000 / 22.9898 / dt'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/porous_flow.i)

The heat-exchanger simulation

This input file uses a TimeDependentReactionSolver object, but in the absence of data concerning the kinetics of QuartzLike in heat-exchangers, this mineral is treated as an equilibrium species (non-kinetic):

[TimeDependentReactionSolver]
  model_definition = definition
  include_moose_solve = false
  geochemistry_reactor_name = reactor
  charge_balance_species = "Cl-"
  swap_out_of_basis = "SiO2(aq)"
  swap_into_basis = "QuartzLike"
  constraint_species = "H2O              Na+              Cl-              QuartzLike"
  constraint_value = "  1.0E-2           0.1E-2           0.1E-2           1E-10"
  constraint_meaning = "kg_solvent_water bulk_composition bulk_composition free_mineral"
  constraint_unit = "   kg               moles            moles            moles"
  initial_temperature = 50.0
  mode = 4
  temperature = 200
  cold_temperature = 40.0
  source_species_names = 'H2O    Na+   Cl-   SiO2(aq)'
  source_species_rates = 'production_rate_H2O production_rate_Na production_rate_Cl production_rate_SiO2'
  ramp_max_ionic_strength_initial = 0 # max_ionic_strength in such a simple problem does not need ramping
  add_aux_pH = false # there is no H+ in this system
  evaluate_kinetic_rates_always = true # implicit time-marching used for stability
  execute_console_output_on = '' # only CSV output used in this example
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/exchanger.i)

Notice the following points:

  • QuartzLike is provided with a very small initial free_mole_mineral_species, which means that SiO(aq) will find its initial concentration at equilibrium.

  • mode = 4, which means "heat-exchanger" mode is used. This means, at each time-step:

    1. any fluid present in the system is removed;

    2. fluid added by the source_species is added;

    3. the system is taken to cold_temperature;

    4. any precipitates are recorded and removed from the system (in the case of QuartzLike, a little precipitation will occur because cold_temperature is less than the aquifer temperature);

    5. the system is heated to temperature;

    6. any precipitates are recorded and removed from the system (in the case of QuartzLike, no further precipitation will occur because it is more soluble at higher temperatures).

  • The source_species are set by the production_rate_* AuxVariables. These are quantities pumped from the production well in the PorousFlow simulation, and are provided by a MultiApp Transfer from the PorousFlow simulation as described above.

This simulation provides the PorousFlow simulation with the injection temperature (200C). It also provides the mass-fractions of the injected fluid. This is achieved by recording the "transported" mole numbers:

  [transported_H2O_auxk]
    type = GeochemistryQuantityAux
    variable = transported_H2O
    species = H2O
    quantity = transported_moles_in_original_basis
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/exchanger.i)

and converting mole numbers to mass fractions:

  [transported_mass_auxk]
    type = ParsedAux
    coupled_variables = 'transported_H2O transported_Na transported_Cl transported_SiO2'
    variable = transported_mass
    expression = 'transported_H2O * 18.0152 + transported_Na * 22.9898 + transported_Cl * 35.453 + transported_SiO2 * 60.0843'
  []
  [massfrac_H2O]
    type = ParsedAux
    coupled_variables = 'transported_mass transported_H2O'
    variable = massfrac_H2O
    expression = '18.0152 * transported_H2O / transported_mass'
  []
(modules/combined/examples/geochem-porous_flow/geotes_2D/exchanger.i)

Quantities of interest are recorded by Postprocessors:

[Postprocessors]
  [cumulative_moles_precipitated_quartz]
    type = PointValue
    variable = dumped_quartz
  []
  [production_temperature]
    type = PointValue
    variable = production_temperature
  []
  [mass_heated_this_timestep]
    type = PointValue
    variable = transported_mass
  []
[]
(modules/combined/examples/geochem-porous_flow/geotes_2D/exchanger.i)

Results

The figures below depict how heat has advected from the hot well to the cold well, as well as how QuartzLike dissolution has caused small changes in the porosity of the aquifer.

Figure 2: Aquifer temperature after injection of heated aquifer-brine into the system. The green dots show the injection and production borehole positions.

Figure 3: Aquifer porosity after injection of heated aquifer-brine into the system that causes dissolution of the aquifer quartz. The green dots show the injection and production borehole positions.

Comparison with QuartzUnlike

A very similar set of simulations, aquifer_un_quartz_geochemistry.i and exchanger_un_quartz.i may be found in the same directory. Instead of QuartzLike, these use the QuartzUnlike mineral, which precipitates upon heating rather than cooling. It is expected that the hot-fluid injection will have little impact upon the aquifer structure, but that lots of QuartzUnlike will precipitate in the heat exchanger as it heats the produced reservoir fluid. Indeed, the following figure shows this occurs:

Figure 4: Minerals precipitated in the heat exchanger for the QuartzLike and QuartzUnlike cases. There are no QuartzLike precipitates because it becomes more soluble as it is heated, in contrast to the QuartzUnlike mineral.