Effective Permeability and Thermal Conductivity Calibration Example using the Utah FORGE Native State Model

This example is prepared for the Year 1 Milestone 3.4.3 from the Utah FORGE project titled "Role of Fluid and Temperature in Fracture Mechanics and Coupled THMC Processes for Enhanced Geothermal Systems" (project number Purdue_5-2557).

Problem Statement

This example uses the synthetic data set generated from the FORGE site model to infer the hydrogeological and geomechanical properties. In this initial study, the effective permeability and thermal conductivity values for pressure and temperature data are identified. Spatially distributed heterogeneous fields will be tested in the next updates. The details for the scalable site characterization can be found in Lee et al. (2018) and Kadeethum et al. (2021).

For the joint inversion example, 17,010 pressure, temperature, and displacement data points were created at the wells of 16A-32, 56-32, 58-32, 78-32, and 78B-32 from the native state model (Phase 3 updated in July 2022)Liu et al. (2022). A summary of the FALCON FORGE native statement model is available here with input and mesh available for down load from the GDR website. A Gauss-Newton solver with a line search is used to find the effective parameters.

Input File

The FORGE native state model assumes a two layer domain of isotropic and homogenous materials. For testing purposes, the native state model was modified in this example to create a coarser 1000 element mesh using the following MOOSE MeshGenerator

MeshGenerator used for creating coarse FORGE native state model.

[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
  [generate]
    type = GeneratedMeshGenerator<<<{"description": "Create a line, square, or cube mesh with uniformly spaced or biased elements.", "href": "../source/meshgenerators/GeneratedMeshGenerator.html"}>>>
    dim<<<{"description": "The dimension of the mesh to be generated"}>>> = 3
    nx<<<{"description": "Number of elements in the X direction"}>>> = 10
    xmin<<<{"description": "Lower X Coordinate of the generated mesh"}>>> = -40
    xmax<<<{"description": "Upper X Coordinate of the generated mesh"}>>> = 4040
    ny<<<{"description": "Number of elements in the Y direction"}>>> = 10
    ymin<<<{"description": "Lower Y Coordinate of the generated mesh"}>>> = -40
    ymax<<<{"description": "Upper Y Coordinate of the generated mesh"}>>> = 4040
    nz<<<{"description": "Number of elements in the Z direction"}>>> = 10
    zmin<<<{"description": "Lower Z Coordinate of the generated mesh"}>>> = -2360
    zmax<<<{"description": "Upper Z Coordinate of the generated mesh"}>>> = 1804
  []
  #ALL OF THESE SIDESET NAMES ARE MIXED UP WITH THE DEFAULT SIDESET NAMES
  [bottom_40m_granitoid_40m]
    type = RenameBlockGenerator<<<{"description": "Changes the block IDs and/or block names for a given set of blocks defined by either block ID or block name. The changes are independent of ordering. The merging of blocks is supported.", "href": "../source/meshgenerators/RenameBlockGenerator.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = generate
    old_block<<<{"description": "Elements with these block ID(s)/name(s) will be given the new block information specified in 'new_block'"}>>> = '0'
    new_block<<<{"description": "The new block ID(s)/name(s) to be given by the elements defined in 'old_block'."}>>> = 'bottom_40m_granitoid_40m'
  []
  [granitoid_40m_surface_40m]
    type = ParsedSubdomainMeshGenerator<<<{"description": "Uses a parsed expression (`combinatorial_geometry`) to determine if an element (via its centroid) is inside the region defined by the expression and assigns a new block ID.", "href": "../source/meshgenerators/ParsedSubdomainMeshGenerator.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = bottom_40m_granitoid_40m
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z > 1000'
    block_id<<<{"description": "Subdomain id to set for inside of the combinatorial"}>>> = 1
    block_name<<<{"description": "Subdomain name to set for inside of the combinatorial"}>>> = granitoid_40m_surface_40m
  []
  [bottom_40m_granitoid_40m_right]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = granitoid_40m_surface_40m
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = bottom
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z < 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'bottom_40m_granitoid_40m_right'
  []
  [granitoid_40m_surface_40m_right]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = bottom_40m_granitoid_40m_right
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = bottom
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z > 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'granitoid_40m_surface_40m_right'
  []
  [bottom_40m_granitoid_40m_left]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = granitoid_40m_surface_40m_right
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = top
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z < 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'bottom_40m_granitoid_40m_left'
  []
  [granitoid_40m_surface_40m_left]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = bottom_40m_granitoid_40m_left
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = top
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z > 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'granitoid_40m_surface_40m_left'
  []
  [bottom_40m_granitoid_40m_front]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = granitoid_40m_surface_40m_left
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = left
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z < 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'bottom_40m_granitoid_40m_front'
  []
  [granitoid_40m_surface_40m_front]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = bottom_40m_granitoid_40m_front
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = left
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z > 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'granitoid_40m_surface_40m_front'
  []
  [bottom_40m_granitoid_40m_back]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = granitoid_40m_surface_40m_front
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = right
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z < 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'bottom_40m_granitoid_40m_back'
  []
  [granitoid_40m_surface_40m_back]
    type = ParsedGenerateSideset<<<{"description": "A MeshGenerator that adds element sides to a sideset if the centroid of the side satisfies the `combinatorial_geometry` expression.", "href": "../source/meshgenerators/ParsedGenerateSideset.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = bottom_40m_granitoid_40m_back
    included_boundaries<<<{"description": "A set of boundary names or ids whose sides will be included in the new sidesets.  A side is only added if it also belongs to one of these boundaries."}>>> = right
    combinatorial_geometry<<<{"description": "Function expression encoding a combinatorial geometry"}>>> = 'z > 1000'
    new_sideset_name<<<{"description": "The name of the new sideset"}>>> = 'granitoid_40m_surface_40m_back'
  []

  [rename]
    type = RenameBoundaryGenerator<<<{"description": "Changes the boundary IDs and/or boundary names for a given set of boundaries defined by either boundary ID or boundary name. The changes are independent of ordering. The merging of boundaries is supported.", "href": "../source/meshgenerators/RenameBoundaryGenerator.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = granitoid_40m_surface_40m_back
    old_boundary<<<{"description": "Elements with these boundary ID(s)/name(s) will be given the new boundary information specified in 'new_boundary'"}>>> = 'front back'
    new_boundary<<<{"description": "The new boundary ID(s)/name(s) to be given by the boundary elements defined in 'old_boundary'."}>>> = 'surface_40m bottom_40m'
  []

  [add_internal_sideset]
    type = SideSetsBetweenSubdomainsGenerator<<<{"description": "MeshGenerator that creates a sideset composed of the nodes located between two or more subdomains.", "href": "../source/meshgenerators/SideSetsBetweenSubdomainsGenerator.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = rename
    primary_block<<<{"description": "The primary set of blocks for which to draw a sideset between"}>>> = 0
    paired_block<<<{"description": "The paired set of blocks for which to draw a sideset between"}>>> = 1
    new_boundary<<<{"description": "The list of boundary names to create on the supplied subdomain"}>>> = granitoid_40m
  []

  use_displaced_mesh<<<{"description": "Create the displaced mesh if the 'displacements' parameter is set. If this is 'false', a displaced mesh will not be created, regardless of whether 'displacements' is set."}>>> = false
[]
(examples/forge_native_state_parameter_calibration/FORGE_NS_Ph3_coarse.i)

In order to increase the sensitity of the measurement data to changes in material parameters, the mesh was locally refined around the toe of well 78-32 using the following Adaptivity block

Mesh refinement around the toe of well 78-32.

[Adaptivity<<<{"href": "../syntax/Adaptivity/index.html"}>>>]
  #refine around DiracKernels sources
  # should refine more than one level but this should run fast
  initial_steps<<<{"description": "The number of adaptive steps to do based on the initial condition."}>>> = 2
  max_h_level<<<{"description": "Maximum number of times a single element can be refined. If 0 then infinite."}>>> = 1
  marker<<<{"description": "The name of the Marker to use to actually adapt the mesh."}>>> = box
  [Markers<<<{"href": "../syntax/Adaptivity/Markers/index.html"}>>>]
    [box]
      type = BoxMarker<<<{"description": "Marks the region inside and outside of a 'box' domain for refinement or coarsening.", "href": "../source/markers/BoxMarker.html"}>>>
      bottom_left<<<{"description": "The bottom left point (in x,y,z with spaces in-between)."}>>> = '2027 1495 379'
      top_right<<<{"description": "The bottom left point (in x,y,z with spaces in-between)."}>>> = '2527 2095 879'
      inside<<<{"description": "How to mark elements inside the box."}>>> = refine
      outside<<<{"description": "How to mark elements outside the box."}>>> = do_nothing
    []
  []
[]
(examples/forge_native_state_parameter_calibration/FORGE_NS_Ph3_coarse.i)

Material properties for the granitoid and sedimentary layers given in Utah FORGE Native State Model, Table 1. The thermal conductivity and permeability being calibrated for the granitoid layer are shown in the below material section of the input file:

Listing 1: Materials being calibrated.

[Materials<<<{"href": "../syntax/Materials/index.html"}>>>]
  [permeability_granite]
    type = PorousFlowPermeabilityConst<<<{"description": "This Material calculates the permeability tensor assuming it is constant", "href": "../source/materials/PorousFlowPermeabilityConst.html"}>>>
    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'
    block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = bottom_40m_granitoid_40m
  []

  [thermal_conductivity_granitoid]
    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"}>>> = '3.05 0 0  0 3.05 0  0 0 3.05'
    block<<<{"description": "The list of blocks (ids or names) that this object will be applied"}>>> = bottom_40m_granitoid_40m
  []
[]
(examples/forge_native_state_parameter_calibration/FORGE_NS_Ph3_coarse.i)

Transient Simulation

Material calibration was performed using a transient simulation with a point source at the toe of well 78-32 ([2327.3, 1795.9, 679.59] in the native state model mesh coordinate). The injection rate was increased from 0 to 0.1 for the first 50 seconds and maintained at the same rate of 0.1 until = 100 s. The injection temperature was constant at 323.15 .

Point source at the toe of well 78-32

[DiracKernels<<<{"href": "../syntax/DiracKernels/index.html"}>>>]
  [source]
    #placed at the bottom of well 78_32
    type = PorousFlowPointSourceFromPostprocessor<<<{"description": "Point source (or sink) that adds (or removes) fluid at a mass flux rate specified by a postprocessor.", "href": "../source/dirackernels/PorousFlowPointSourceFromPostprocessor.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = pressure
    mass_flux<<<{"description": "The postprocessor name holding the mass flux at this point in kg/s (positive is flux in, negative is flux out)"}>>> = mass_flux_in
    point<<<{"description": "The x,y,z coordinates of the point source (or sink)"}>>> = '2327.3 1795.9 679.59'
  []
  [source_h]
    #placed at the bottom of well 78_32
    type = PorousFlowPointEnthalpySourceFromPostprocessor<<<{"description": "Point source that adds heat energy corresponding to injection of a fluid with specified mass flux rate (specified by a postprocessor) at given temperature (specified by a postprocessor)", "href": "../source/dirackernels/PorousFlowPointEnthalpySourceFromPostprocessor.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
    mass_flux<<<{"description": "The postprocessor name holding the mass flux of injected fluid at this point in kg/s (please ensure this is positive so that this object acts like a source)"}>>> = mass_flux_in
    point<<<{"description": "The x,y,z coordinates of the point source"}>>> = '2327.3 1795.9 679.59'
    T_in<<<{"description": "The postprocessor name holding the temperature of injected fluid (measured in K)"}>>> = T_in
    pressure<<<{"description": "Pressure used to calculate the injected fluid enthalpy (measured in Pa)"}>>> = pressure
    fp<<<{"description": "The name of the user object used to calculate the fluid properties of the injected fluid"}>>> = true_water
  []
[]
(examples/forge_native_state_parameter_calibration/FORGE_NS_Ph3_coarse.i)

Pressure, temperature, and displacement data were extracted every 10s from well 16A-32, 56-32, 58-32, 78-32, and 78B-32. The simulation reached steady state at 70s.

Results

The permeability and thermal conductivity of the granitoid layer were estimated in this case study. The initial log-permeability and log-conductivity were set to -16 and 1, respectively. With the Gauss-Newton solver, log-permeability and log-conductivity were optimized as follows:

where is the unknown variable log-permeabilty or log-conductivity at -th iteration, is the parameter error matrix, is the observation error matrix, is the Jacobian matrix, is the observed data, and is the simulated data. is the step length set to in this example.

Listing 2: Script for effective parameter inversion.

import os
import time
import argparse
import pandas as pd
import numpy as np

parser = argparse.ArgumentParser()
parser.add_argument(
    "--falcon_input",
    default="FORGE_NS_Ph3_coarse.i",
    help="input file of the native state model",
)
parser.add_argument(
    "--falcon_run", default="~/projects/falcon/falcon-opt", help="path to falcon"
)
parser.add_argument("--n_cores", type=int, default=2, help="cpu cores")
parser.add_argument("--max_iter", type=int, default=1, help="max iterations")
parser.add_argument("--nK", type=int, default=2, help="number of parameters")
parser.add_argument("--precision", type=float, default=1.0e-5, help="precision")
parser.add_argument("--alpha", type=float, default=0.01, help="line search step length")
parser.add_argument("--K_Perm", type=float, default=-16.0, help="initial permeability")
parser.add_argument(
    "--K_Cond", type=float, default=1.0, help="initial thermal conductivity"
)
parser.add_argument("--folder_ref", default="./Ref/", help="path to reference data")
parser.add_argument("--outf", default="./Results", help="output folder")
par = parser.parse_args()

outf = par.outf
data_f = outf + "/data"  # output data
try:
    os.makedirs(data_f)
except OSError:
    pass

falcon_input = par.falcon_input
falcon_run = par.falcon_run
n_cores = par.n_cores
max_iter = int(par.max_iter)
nK = par.nK
folder_ref = par.folder_ref  # reference data
folder_simul = "./"  # simulated data
num_file = 7  # number of simulation time steps

## Define a function to read data
def Read_data(folder, num_file):
    # data comes from GDR repository https://gdr.openei.org/submissions/1397
    well_16A = "FORGE_NS_Ph3_coarse_point_data_16A_"
    well_56 = "FORGE_NS_Ph3_coarse_point_data_56_32_"
    well_58 = "FORGE_NS_Ph3_coarse_point_data_58_32_"
    well_78 = "FORGE_NS_Ph3_coarse_point_data_78_32_"
    well_78B = "FORGE_NS_Ph3_coarse_point_data_78B_32_"

    (
        data_pressure_16A,
        data_temper_16A,
        data_dispi_16A,
        data_dispj_16A,
        data_dispk_16A,
    ) = ([], [], [], [], [])
    data_pressure_56, data_temper_56, data_dispi_56, data_dispj_56, data_dispk_56 = (
        [],
        [],
        [],
        [],
        [],
    )
    data_pressure_58, data_temper_58, data_dispi_58, data_dispj_58, data_dispk_58 = (
        [],
        [],
        [],
        [],
        [],
    )
    data_pressure_78, data_temper_78, data_dispi_78, data_dispj_78, data_dispk_78 = (
        [],
        [],
        [],
        [],
        [],
    )
    (
        data_pressure_78B,
        data_temper_78B,
        data_dispi_78B,
        data_dispj_78B,
        data_dispk_78B,
    ) = ([], [], [], [], [])

    for i in range(1, num_file + 1):
        string = str(i)
        str_16A = well_16A + string.zfill(4) + ".csv"
        str_56 = well_56 + string.zfill(4) + ".csv"
        str_58 = well_58 + string.zfill(4) + ".csv"
        str_78 = well_78 + string.zfill(4) + ".csv"
        str_78B = well_78B + string.zfill(4) + ".csv"

        pressure_16A = np.array(pd.read_csv(folder + str_16A, usecols=["pressure"]))
        temper_16A = np.array(pd.read_csv(folder + str_16A, usecols=["temperature"]))
        dispi_16A = np.array(pd.read_csv(folder + str_16A, usecols=["disp_i"]))
        dispj_16A = np.array(pd.read_csv(folder + str_16A, usecols=["disp_j"]))
        dispk_16A = np.array(pd.read_csv(folder + str_16A, usecols=["disp_k"]))

        pressure_56 = np.array(pd.read_csv(folder + str_56, usecols=["pressure"]))
        temper_56 = np.array(pd.read_csv(folder + str_56, usecols=["temperature"]))
        dispi_56 = np.array(pd.read_csv(folder + str_56, usecols=["disp_i"]))
        dispj_56 = np.array(pd.read_csv(folder + str_56, usecols=["disp_j"]))
        dispk_56 = np.array(pd.read_csv(folder + str_56, usecols=["disp_k"]))

        pressure_58 = np.array(pd.read_csv(folder + str_58, usecols=["pressure"]))
        temper_58 = np.array(pd.read_csv(folder + str_58, usecols=["temperature"]))
        dispi_58 = np.array(pd.read_csv(folder + str_58, usecols=["disp_i"]))
        dispj_58 = np.array(pd.read_csv(folder + str_58, usecols=["disp_j"]))
        dispk_58 = np.array(pd.read_csv(folder + str_58, usecols=["disp_k"]))

        pressure_78 = np.array(pd.read_csv(folder + str_78, usecols=["pressure"]))
        temper_78 = np.array(pd.read_csv(folder + str_78, usecols=["temperature"]))
        dispi_78 = np.array(pd.read_csv(folder + str_78, usecols=["disp_i"]))
        dispj_78 = np.array(pd.read_csv(folder + str_78, usecols=["disp_j"]))
        dispk_78 = np.array(pd.read_csv(folder + str_78, usecols=["disp_k"]))

        pressure_78B = np.array(pd.read_csv(folder + str_78B, usecols=["pressure"]))
        temper_78B = np.array(pd.read_csv(folder + str_78B, usecols=["temperature"]))
        dispi_78B = np.array(pd.read_csv(folder + str_78B, usecols=["disp_i"]))
        dispj_78B = np.array(pd.read_csv(folder + str_78B, usecols=["disp_j"]))
        dispk_78B = np.array(pd.read_csv(folder + str_78B, usecols=["disp_k"]))

        data_pressure_16A.append(pressure_16A), data_temper_16A.append(temper_16A)
        data_dispi_16A.append(dispi_16A), data_dispj_16A.append(
            dispj_16A
        ), data_dispk_16A.append(dispk_16A)

        data_pressure_56.append(pressure_56), data_temper_56.append(temper_56)
        data_dispi_56.append(dispi_56), data_dispj_56.append(
            dispj_56
        ), data_dispk_56.append(dispk_56)

        data_pressure_58.append(pressure_58), data_temper_58.append(temper_58)
        data_dispi_58.append(dispi_58), data_dispj_58.append(
            dispj_58
        ), data_dispk_58.append(dispk_58)

        data_pressure_78.append(pressure_78), data_temper_78.append(temper_78)
        data_dispi_78.append(dispi_78), data_dispj_78.append(
            dispj_78
        ), data_dispk_78.append(dispk_78)

        data_pressure_78B.append(pressure_78B), data_temper_78B.append(temper_78B)
        data_dispi_78B.append(dispi_78B), data_dispj_78B.append(
            dispj_78B
        ), data_dispk_78B.append(dispk_78B)

    data_pressure_16A = np.array(data_pressure_16A).reshape(-1, 1)
    data_temper_16A = np.array(data_temper_16A).reshape(-1, 1)
    data_dispi_16A = np.array(data_dispi_16A).reshape(-1, 1)
    data_dispj_16A = np.array(data_dispj_16A).reshape(-1, 1)
    data_dispk_16A = np.array(data_dispk_16A).reshape(-1, 1)

    data_pressure_56 = np.array(data_pressure_56).reshape(-1, 1)
    data_temper_56 = np.array(data_temper_56).reshape(-1, 1)
    data_dispi_56 = np.array(data_dispi_56).reshape(-1, 1)
    data_dispj_56 = np.array(data_dispj_56).reshape(-1, 1)
    data_dispk_56 = np.array(data_dispk_56).reshape(-1, 1)

    data_pressure_58 = np.array(data_pressure_58).reshape(-1, 1)
    data_temper_58 = np.array(data_temper_58).reshape(-1, 1)
    data_dispi_58 = np.array(data_dispi_58).reshape(-1, 1)
    data_dispj_58 = np.array(data_dispj_58).reshape(-1, 1)
    data_dispk_58 = np.array(data_dispk_58).reshape(-1, 1)

    data_pressure_78 = np.array(data_pressure_78).reshape(-1, 1)
    data_temper_78 = np.array(data_temper_78).reshape(-1, 1)
    data_dispi_78 = np.array(data_dispi_78).reshape(-1, 1)
    data_dispj_78 = np.array(data_dispj_78).reshape(-1, 1)
    data_dispk_78 = np.array(data_dispk_78).reshape(-1, 1)

    data_pressure_78B = np.array(data_pressure_78B).reshape(-1, 1)
    data_temper_78B = np.array(data_temper_78B).reshape(-1, 1)
    data_dispi_78B = np.array(data_dispi_78B).reshape(-1, 1)
    data_dispj_78B = np.array(data_dispj_78B).reshape(-1, 1)
    data_dispk_78B = np.array(data_dispk_78B).reshape(-1, 1)

    pressure = np.concatenate(
        (
            data_pressure_16A,
            data_pressure_56,
            data_pressure_58,
            data_pressure_78,
            data_pressure_78B,
        ),
        axis=0,
    )
    temper = np.concatenate(
        (
            data_temper_16A,
            data_temper_56,
            data_temper_58,
            data_temper_78,
            data_temper_78B,
        ),
        axis=0,
    )
    dispi = np.concatenate(
        (data_dispi_16A, data_dispi_56, data_dispi_58, data_dispi_78, data_dispi_78B),
        axis=0,
    )
    dispj = np.concatenate(
        (data_dispj_16A, data_dispj_56, data_dispj_58, data_dispj_78, data_dispj_78B),
        axis=0,
    )
    dispk = np.concatenate(
        (data_dispk_16A, data_dispk_56, data_dispk_58, data_dispk_78, data_dispk_78B),
        axis=0,
    )
    data = np.concatenate((pressure, temper, dispi, dispj, dispk), axis=0)

    return data

## Forward modeling function
def Forward(K_log):
    K_Perm = K_log[0]
    K_Cond = K_log[1]

    K_Perm = np.power(10, K_Perm)
    K_Cond = np.power(10, K_Cond)

    K_Perm = np.format_float_scientific(K_Perm)
    K_Cond = float(K_Cond)

    K_Perm_str = (
        "Materials/permeability_granite/permeability='"
        + str(K_Perm)
        + " 0 0  0 "
        + str(K_Perm)
        + " 0  0 0 "
        + str(K_Perm)
        + "'"
    )

    K_Cond_str = (
        "Materials/thermal_conductivity_granitoid/dry_thermal_conductivity='"
        + str(K_Cond)
        + " 0 0  0 "
        + str(K_Cond)
        + " 0  0 0 "
        + str(K_Cond)
        + "'"
    )

    cli_arg = K_Perm_str + " " + K_Cond_str

    # run simulations
    run_command = (
        "mpiexec -n"
        + " "
        + str(n_cores)
        + " "
        + falcon_run
        + " "
        + "-i"
        + " "
        + falcon_input
        + " "
        + cli_arg
        + " "
        + "--n-threads=2 --timing"
    )
    os.system(run_command)
    simul = Read_data(folder=folder_simul, num_file=num_file)

    return simul

## Delete data produced by previous simulations
def remove_data(folder):
    filePath = folder
    name = os.listdir(filePath)
    for i in name:
        path = "./{}".format(i)
        if "point_data" in i:
            os.remove(path)

## Assume known error matrix R
obs_true = Read_data(folder=folder_ref, num_file=num_file)
np.save(data_f + "/Ref_Data.npy", obs_true)
nobs = len(obs_true)
std_pressure = obs_true[0] * 0.01  # 1% std error
std_temper = obs_true[int(nobs / 5)] * 0.01  # 1% std error
std_dispi = obs_true[int(2 * nobs / 5)] * 0.01  # 1% std error
std_dispj = obs_true[int(3 * nobs / 5)] * 0.01  # 1% std error
std_dispk = obs_true[int(4 * nobs / 5)] * 0.01  # 1% std error
std_pressure = np.abs(std_pressure)
std_temper = np.abs(std_temper)
std_dispi = np.abs(std_dispi)
std_dispj = np.abs(std_dispj)
std_dispk = np.abs(std_dispk)

err_pressure = std_pressure * np.random.randn(int(nobs / 5), 1)
err_temper = std_temper * np.random.randn(int(nobs / 5), 1)
err_dispi = std_dispi * np.random.randn(int(nobs / 5), 1)
err_dispj = std_dispj * np.random.randn(int(nobs / 5), 1)
err_dispk = std_dispk * np.random.randn(int(nobs / 5), 1)
err = np.concatenate(
    (err_pressure, err_temper, err_dispi, err_dispj, err_dispk), axis=0
)
obs = obs_true + err

invR = np.eye(nobs)
for p in range(int(nobs / 5)):
    invR[p, p] = 1 / (std_pressure**2)
for t in range(int(nobs / 5), int(2 * nobs / 5)):
    invR[t, t] = 1 / (std_temper**2)
for i in range(int(2 * nobs / 5), int(3 * nobs / 5)):
    invR[i, i] = 1 / (std_dispi**2)
for j in range(int(3 * nobs / 5), int(4 * nobs / 5)):
    invR[j, j] = 1 / (std_dispj**2)
for k in range(int(4 * nobs / 5), nobs):
    invR[k, k] = 1 / (std_dispk**2)

## Initialization
K_Perm = par.K_Perm
K_Cond = par.K_Cond
K_init = np.vstack((K_Perm, K_Cond))  # initial parameters

std_Perm = 2
std_Cond = 0.05
std_Par = np.vstack((std_Perm, std_Cond))
Sigma = np.eye(nK)
invSigma = np.eye(nK)
for i in range(nK):
    Sigma[i, i] = std_Par[i] ** 2
    invSigma[i, i] = 1 / Sigma[i, i]

precision = par.precision
alpha = par.alpha
K_cur = np.copy(K_init)
Para = np.zeros([max_iter + 1, nK])

## Run inversion
Time_start = time.time()
for i in range(max_iter):
    print("###### Iteration %d ######" % (i + 1))
    remove_data(folder="./")
    simul_K_cur = Forward(K_cur)

    Para[i, 0] = K_cur[0][0]
    Para[i, 1:nK] = K_cur[1:nK].squeeze()
    np.save(data_f + "/Simul_Data_Iter_" + str(i) + ".npy", simul_K_cur)
    np.save(data_f + "/Parameters.npy", Para)

    J_cur = np.zeros([nobs, nK])
    for j in range(nK):
        zerovec = np.zeros([nK, 1])
        zerovec[j] = 1.0
        mag = np.dot(K_cur.T, zerovec)
        absmag = np.dot(abs(K_cur.T), abs(zerovec))
        if mag >= 0:
            signmag = 1.0
        else:
            signmag = -1.0

        delta = (
            signmag
            * np.sqrt(precision)
            * (max(abs(mag), absmag))
            / ((np.linalg.norm(zerovec) + np.finfo(float).eps) ** 2)
        )
        if delta == 0:
            delta = np.sqrt(precision)

        remove_data(folder="./")
        simul_K_delta = Forward(K_cur + zerovec * delta)
        J_cur[:, j : j + 1] = (simul_K_delta - simul_K_cur) / delta

    Jk_cur = np.dot(J_cur, K_cur)
    solve_a = np.dot(np.dot(J_cur.T, invR), J_cur) + invSigma
    solve_b = -np.dot(invSigma, K_cur) + np.dot(
        np.dot(J_cur.T, invR), (obs - simul_K_cur)
    )
    dk = np.linalg.solve(solve_a, solve_b)
    K_cur = np.copy(K_cur + alpha * dk)

## Final results
remove_data(folder="./")
simul_K_cur = Forward(K_cur)

Para[max_iter, 0] = K_cur[0][0]
Para[max_iter, 1:nK] = K_cur[1:nK].squeeze()
np.save(data_f + "/Simul_Data_Iter_" + str(max_iter) + ".npy", simul_K_cur)
np.save(data_f + "/Parameters.npy", Para)

Time_end = time.time()
time_elapsed = Time_end - Time_start
print(
    "Time elapsed {:.0f}hr {:.0f}m {:.0f}s".format(
        (time_elapsed // 60) // 60, (time_elapsed // 60) % 60, time_elapsed % 60
    )
)
(examples/forge_native_state_parameter_calibration/optimization_script.py)

The input parameters in the python optimization script shown in Listing 2 include the MOOSE input file line numbers for permeability and thermal conductivity shown Listing 1 which use zero based line numbers.

Figure 1 shows the estimation results of permeability and thermal conductivity for 8 iterations. Figure 2 presents the pressure, temperature, and displacement fits, with the fitting error shown as root mean square error (RMSE).

Figure 1: Estimation of permeability and thermal conductivity at each iteration

Figure 2: Pressure, temperature and displacement fitting at the last iteration

References

  1. Teeratorn Kadeethum, Daniel O’Malley, Jan Niklas Fuhg, Youngsoo Choi, Jonghyun Lee, Hari S Viswanathan, and Nikolaos Bouklas. A framework for data-driven solution and parameter estimation of pdes using conditional generative adversarial networks. Nature Computational Science, 1(12):819–829, 2021.[BibTeX]
  2. Jonghyun Lee, Amalia Kokkinaki, and Peter K Kitanidis. Fast large-scale joint inversion for deep aquifer characterization using pressure and heat tracer measurements. Transport in Porous Media, 123:533–543, 2018.[BibTeX]
  3. Ruijie Liu, Rob Podgorney, Aleta Finnila, Pengju Xing, John McLennan, and Joe Moore. Development of a coupled multi-field utah forge native state model: phase 3 update. In GRC Transactions, volume 46. 2022.[BibTeX]