Feflowlib: Hydraulic model - conversion and simulation#

Section author: Julian Heinze (Helmholtz Centre for Environmental Research GmbH - UFZ)

In this example we show how a simple flow/hydraulic FEFLOW model can be converted to a pyvista.UnstructuredGrid and then be simulated in OGS.

  1. Necessary imports

import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path

import ifm_contrib as ifm
import numpy as np
import pyvista as pv

import ogstools as ogs
from ogstools.examples import feflow_model_box_Neumann
from ogstools.feflowlib import (
    convert_properties_mesh,
    extract_cell_boundary_conditions,
    setup_prj_file,
    steady_state_diffusion,
)
from ogstools.feflowlib.tools import (
    extract_point_boundary_conditions,
    get_material_properties,
)

1. Load a FEFLOW model (.fem) as a FEFLOW document, convert and save it. More details on how the conversion function works can be found here: ogstools.feflowlib.convert_properties_mesh.

feflow_model = ifm.loadDocument(str(feflow_model_box_Neumann))
pyvista_mesh = convert_properties_mesh(feflow_model)

pv.global_theme.colorbar_orientation = "vertical"
pyvista_mesh.plot(
    show_edges=True,
    off_screen=True,
    scalars="P_HEAD",
    cpos=[0, 1, 0.5],
    scalar_bar_args={"position_x": 0.1, "position_y": 0.25},
)
print(pyvista_mesh)
temp_dir = Path(tempfile.mkdtemp("feflow_test_simulation"))
feflow_mesh_file = temp_dir / "boxNeumann.vtu"
pyvista_mesh.save(feflow_mesh_file)
plot E feflowlib H simulation
UnstructuredGrid (0x7cd473c18220)
  N Cells:    11462
  N Points:   6768
  X Bounds:   0.000e+00, 1.000e+02
  Y Bounds:   0.000e+00, 1.000e+02
  Z Bounds:   -1.000e+02, 0.000e+00
  N Arrays:   23
  1. Extract the point conditions (see: ogstools.feflowlib.extract_point_boundary_conditions).

point_BC_dict = extract_point_boundary_conditions(temp_dir, pyvista_mesh)
# Since there can be multiple point boundary conditions on the bulk mesh,
# they are saved and plotted iteratively.
plotter = pv.Plotter(shape=(len(point_BC_dict), 1))
for i, (path, boundary_condition) in enumerate(point_BC_dict.items()):
    boundary_condition.save(path)
    plotter.subplot(i, 0)
    plotter.add_mesh(boundary_condition, scalars=Path(path).stem)
plotter.show()
path_topsurface, topsurface = extract_cell_boundary_conditions(
    feflow_mesh_file, pyvista_mesh
)
# On the topsurface can be cell based boundary condition.
# The boundary conditions on the topsurface of the model are required for generalization.
topsurface.save(path_topsurface)
plot E feflowlib H simulation
  1. Setup a prj-file (see: ogstools.feflowlib.setup_prj_file) to run a OGS-simulation.

path_prjfile = feflow_mesh_file.with_suffix(".prj")
prj = ogs.Project(output_file=path_prjfile)
# Get the template prj-file configurations for a steady state diffusion process
ssd_model = steady_state_diffusion(temp_dir / "sim_boxNeumann", prj)
# Include the mesh specific configurations to the template.
model = setup_prj_file(
    bulk_mesh_path=feflow_mesh_file,
    mesh=pyvista_mesh,
    material_properties=get_material_properties(pyvista_mesh, "P_CONDX"),
    process="steady state diffusion",
    model=ssd_model,
)
# The model must be written before it can be run.
model.write_input(path_prjfile)
# Simply print the prj-file as an example.
model_prjfile = ET.parse(path_prjfile)
ET.dump(model_prjfile)
<OpenGeoSysProject>
    <meshes>
        <mesh>boxNeumann.vtu</mesh>
        <mesh>topsurface_boxNeumann.vtu</mesh>
        <mesh>P_BC_FLOW.vtu</mesh>
        <mesh>P_BCFLOW_2ND.vtu</mesh>
    </meshes>
    <processes>
        <process>
            <name>SteadyStateDiffusion</name>
            <type>STEADY_STATE_DIFFUSION</type>
            <integration_order>2</integration_order>
            <secondary_variables>
                <secondary_variable internal_name="darcy_velocity" output_name="v" />
            </secondary_variables>
            <process_variables>
                <process_variable>HEAD_OGS</process_variable>
            </process_variables>
        </process>
    </processes>
    <media>
        <medium id="0">
            <properties>
                <property>
                    <name>diffusion</name>
                    <type>Constant</type>
                    <value>1.1574074074074073e-05</value>
                </property>
                <property>
                    <name>reference_temperature</name>
                    <type>Constant</type>
                    <value>293.15</value>
                </property>
            </properties>
        </medium>
    </media>
    <time_loop>
        <processes>
            <process ref="SteadyStateDiffusion">
                <nonlinear_solver>basic_picard</nonlinear_solver>
                <convergence_criterion>
                    <type>DeltaX</type>
                    <norm_type>NORM2</norm_type>
                    <abstol>1e-15</abstol>
                </convergence_criterion>
                <time_discretization>
                    <type>BackwardEuler</type>
                </time_discretization>
                <time_stepping>
                    <type>SingleStep</type>
                </time_stepping>
            </process>
        </processes>
        <output>
            <type>VTK</type>
            <prefix>/tmp/tmpskv4rql7feflow_test_simulation/sim_boxNeumann</prefix>
            <timesteps>
                <pair>
                    <repeat>1</repeat>
                    <each_steps>1</each_steps>
                </pair>
            </timesteps>
            <variables />
        </output>
    </time_loop>
    <parameters>
        <parameter>
            <name>p0</name>
            <type>Constant</type>
            <value>0</value>
        </parameter>
        <parameter>
            <name>P_BC_FLOW</name>
            <type>MeshNode</type>
            <mesh>P_BC_FLOW</mesh>
            <field_name>P_BC_FLOW</field_name>
        </parameter>
        <parameter>
            <name>P_BCFLOW_2ND</name>
            <type>MeshNode</type>
            <mesh>P_BCFLOW_2ND</mesh>
            <field_name>P_BCFLOW_2ND</field_name>
        </parameter>
    </parameters>
    <process_variables>
        <process_variable>
            <name>HEAD_OGS</name>
            <components>1</components>
            <order>1</order>
            <initial_condition>p0</initial_condition>
            <boundary_conditions>
                <boundary_condition>
                    <type>Dirichlet</type>
                    <mesh>P_BC_FLOW</mesh>
                    <parameter>P_BC_FLOW</parameter>
                </boundary_condition>
                <boundary_condition>
                    <type>Neumann</type>
                    <mesh>P_BCFLOW_2ND</mesh>
                    <parameter>P_BCFLOW_2ND</parameter>
                </boundary_condition>
            </boundary_conditions>
        </process_variable>
    </process_variables>
    <nonlinear_solvers>
        <nonlinear_solver>
            <name>basic_picard</name>
            <type>Picard</type>
            <max_iter>10</max_iter>
            <linear_solver>general_linear_solver</linear_solver>
        </nonlinear_solver>
    </nonlinear_solvers>
    <linear_solvers>
        <linear_solver>
            <name>general_linear_solver</name>
            <lis>-i cg -p jacobi -tol 1e-6 -maxiter 100000</lis>
            <eigen>
                <solver_type>CG</solver_type>
                <precon_type>DIAGONAL</precon_type>
                <max_iteration_step>100000</max_iteration_step>
                <error_tolerance>1e-6</error_tolerance>
            </eigen>
            <petsc>
                <prefix>sd</prefix>
                <parameters>-sd_ksp_type cg  -sd_pc_type bjacobi -sd_ksp_rtol 1e-16 -sd_ksp_max_it 10000</parameters>
            </petsc>
        </linear_solver>
    </linear_solvers>
</OpenGeoSysProject>
  1. Run the model

model.run_model(logfile=temp_dir / "out.log")
OGS finished with project file /tmp/tmpskv4rql7feflow_test_simulation/boxNeumann.prj.
Execution took 0.3034629821777344 s
Project file written to output.
  1. Read the results and plot them.

ms = ogs.MeshSeries(temp_dir / "sim_boxNeumann.pvd")
# Read the last timestep:
ogs_sim_res = ms.mesh(ms.timesteps[-1])
"""
It is also possible to read the file directly with pyvista:
ogs_sim_res = pv.read(temp_dir / "sim_boxNeumann_ts_1_t_1.000000.vtu")
"""
ogs_sim_res.plot(
    show_edges=True,
    off_screen=True,
    scalars="HEAD_OGS",
    cpos=[0, 1, 0.5],
    scalar_bar_args={"position_x": 0.1, "position_y": 0.25},
)
plot E feflowlib H simulation

5.1 Plot the hydraulic head simulated in OGS with ogstools.plot.contourf.

head = ogs.variables.Scalar(
    data_name="HEAD_OGS", data_unit="m", output_unit="m"
)
fig = ogs.plot.contourf(ogs_sim_res.slice(normal="z", origin=[50, 50, 0]), head)
plot E feflowlib H simulation
  1. Calculate the difference to the FEFLOW simulation and plot it.

diff = pyvista_mesh["P_HEAD"] - ogs_sim_res["HEAD_OGS"]
pyvista_mesh["diff_HEAD"] = diff
pyvista_mesh.plot(
    show_edges=True,
    off_screen=True,
    scalars="diff_HEAD",
    cpos=[0, 1, 0.5],
    scalar_bar_args={"position_x": 0.1, "position_y": 0.25},
)
plot E feflowlib H simulation

6.1 Plot the differences in the hydraulic head with ogstools.plot.contourf. Slices are taken along the z-axis.

diff_head = ogs.variables.Scalar(
    data_name="diff_HEAD", data_unit="m", output_unit="m"
)
slices = np.reshape(list(pyvista_mesh.slice_along_axis(n=4, axis="z")), (2, 2))
fig = ogs.plot.contourf(slices, diff_head)
for ax, slice in zip(fig.axes, np.ravel(slices), strict=False):
    ax.set_title(f"z = {slice.center[2]:.1f} {ms.spatial_output_unit}")
z = -99.0 m, z = -66.3 m, z = -33.7 m, z = -1.0 m

Slices are taken along the y-axis.

slices = np.reshape(list(pyvista_mesh.slice_along_axis(n=4, axis="y")), (2, 2))
fig = ogs.plot.contourf(slices, diff_head)
for ax, slice in zip(fig.axes, np.ravel(slices), strict=False):
    ax.set_title(f"y = {slice.center[1]:.1f} {ms.spatial_output_unit}")
y = 1.0 m, y = 33.7 m, y = 66.3 m, y = 99.0 m

Total running time of the script: (0 minutes 3.755 seconds)