Note
Go to the end to download the full example code.
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.
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)
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
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)
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>
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.
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},
)
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)
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},
)
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}")
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}")
Total running time of the script: (0 minutes 3.755 seconds)