Spatial refinement - steady state diffusion#

This example shows one possible implementation of how to do a convergence study. It uses the project file from the following benchmark with multiple discretizations to evaluate the accuracy of the numerical solutions. ogs: elliptic neumann benchmark

Here is some theoretical background for the topic of grid convergence:

Nasa convergence reference

More comprehensive reference

At least three meshes of increasing refinement are required for the convergence study. The three finest meshes are used to calculated the Richardson extrapolation. The third coarsest mesh will be used for the topology to evaluate the results. Its nodes should be shared by the finer meshes, otherwise interpolation will influence the results. With unstructured grids this can be achieved as well with refinement by splitting.

The results to analyze are generated on the fly with the following code. If you are only interested in the convergence study, please skip to Hydraulic pressure convergence.

First, the required packages are imported and an output directory is created:

from IPython.display import HTML

import ogstools as ot
from ogstools import examples, workflow
from ogstools.studies import convergence

tmp_path = ot.definitions.temp_dir("steady_state_diffusion", "examples")
report_name = str(tmp_path / "report.ipynb")

The meshes and their boundaries are generated easily via gmsh and Meshes. from_gmsh(). Then we run the different simulations with increasingly fine spatial discretization via ogs6py and store the results for the convergence study.

refinements = 6
edge_cells = [2**i for i in range(refinements)]
simulations_control = []

for n_edge_cells in edge_cells:
    meshes = ot.Meshes.from_gmsh(
        ot.gmsh_tools.rect(n_edge_cells=n_edge_cells, structured_grid=True),
        log=False,
    )

    prj = ot.Project(input_file=examples.prj_steady_state_diffusion).copy(
        tmp_path / f"cells_{n_edge_cells}"
    )
    prefix = "steady_state_diffusion_" + str(n_edge_cells)
    prj.replace_text(prefix, ".//prefix")

    model = ot.Model(prj, meshes)
    sim_c = model.controller()
    simulations_control.append(sim_c)

simulations = [sim.run() for sim in simulations_control]

Here we calculate the analytical solution on one of the meshes:

analytical_solution_path = tmp_path / "analytical_solution.vtu"
sim_last: ot.Simulation = simulations[-1]

solution = examples.anasol.diffusion_head_analytical(
    simulations[-1].meshseries[0]
)
ot.plot.setup.show_element_edges = True
fig = ot.plot.contourf(solution, ot.variables.hydraulic_head)
fig.show()
analytical_solution_path = ot.mesh.save(solution)
plot convergence study steady state diffusion

Hydraulic pressure convergence#

The pressure field of this model is converging well. The convergence ratio is approximately 1 on the whole mesh and looking at the relative errors we see a quadratic convergence behavior.

result_paths = [sim.meshseries_file for sim in simulations]
convergence.run_convergence_study(
    output_name=report_name,
    mesh_paths=result_paths,
    variable_name="hydraulic_head",
    timevalue=1,
    refinement_ratio=2.0,
    reference_solution_path=str(analytical_solution_path),
)
HTML(workflow.jupyter_to_html(report_name, show_input=False))
report
# SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
# SPDX-License-Identifier: BSD-3-Clause

# ---
# jupyter:
#   kernelspec:
#     display_name: .venv
#     language: python
#     name: python3
# ---

Grid convergence

If the shown values are approximately 1, this means that the results are in asymptotic range of convergence.

Contour plot of the grid convergence index across the mesh.

Grid comparison

Visualizing the requested mesh variable on the 3 finest discretizations:

Contour plots of hydraulic_head on the 3 finest discretizations.

Richardson extrapolation

Visualizing the Richardson extrapolation of the requested mesh variable. If a reference solution is provided, the difference between the two is shown as well. Otherwise the difference between the finest discretization and the Richardson extrapolation is shown.

Contour plot of the Richardson extrapolation of hydraulic_head.
/tmp/ipykernel_2856/290069088.py:22: RuntimeWarning: Only one input mesh defines a spatial unit. Assuming both meshes use the spatial unit `1 m`
  diff_mesh = ot.mesh.difference(reference_solution, richardson, variable)
Contour plot of the difference in hydraulic_head between the reference solution and the Richardson extrapolation.

Convergence metrics

mean element length maximum minimum abs. error (max) abs. error (min) abs. error (L2 norm) rel. error (max) rel. error (min) rel. error (L2 norm)
1 1.75 1 0.074685 0 0.58656 0.04458 0 0.053572
0.5 1.6857 1 0.0104 0 0.19746 0.0062075 0 0.018034
0.25 1.678 1 0.0026531 0 0.06575 0.0015836 0 0.0060051
0.125 1.676 1 0.00065028 0 0.0068034 0.00038815 0 0.00062137
0.0625 1.6755 1 0.00016145 0 0.0024513 9.6371e-05 0 0.00022389
0.03125 1.6754 1 4.0085e-05 0 0.0020591 2.3927e-05 0 0.00018807
0 1.6753 1 0 0 6.6613e-16 0 0 6.084e-17

Relative errors

Plot of the relative convergence errors of hydraulic_head.

Absolute values

Plot of the absolute convergence values of hydraulic_head.


Darcy velocity convergence#

For the velocity we some discrepancy of the convergence ratio in the bottom right corner. Thus we know, at these points the mesh isn’t properly converging (at least for the velocity field). We see, that in the bottom right corner, the velocity magnitude seems to be steadily increasing, which is also reflected in the Richardson extrapolation, which shows an anomalous high value in this spot, hinting at a singularity there. This is explained by the notion in the benchmark’s documentation of “incompatible boundary conditions imposed on the bottom right corner of the domain.” Regardless of this, the benchmark gives a convergent solution for the pressure field. The code cells from the templated notebook are show here for transparency.

convergence.run_convergence_study(
    output_name=report_name,
    mesh_paths=result_paths,
    variable_name="velocity",
    timevalue=1,
    refinement_ratio=2.0,
)
HTML(workflow.jupyter_to_html(report_name, show_input=True))
report
# SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
# SPDX-License-Identifier: BSD-3-Clause

# ---
# jupyter:
#   kernelspec:
#     display_name: .venv
#     language: python
#     name: python3
# ---
# Import required modules and customize plot settings.
# pylint:disable=C0413
import numpy as np
import pyvista as pv
from IPython.display import display

import ogstools as ot
from ogstools import studies

ot.plot.setup.reset()
ot.plot.setup.show_element_edges = True
ot.plot.setup.combined_colorbar = False


def _show_with_alt(fig: object, alt: str) -> None:
    """Display a matplotlib figure with alt text, then close it."""
    if isinstance(fig, ot.plot.contourplots.plt.Figure):
        display(fig, metadata={"image/png": {"alt": alt}})
        ot.plot.contourplots.plt.close(fig)
# Here, the meshes are read, a Variable object is created from the variable
# name and the Richardson extrapolation calculated.
# The 3 finest meshes of those provided will be used for the Richardson
# extrapolation.

mesh_series = [ot.MeshSeries(mesh_path) for mesh_path in mesh_paths]
timestep_sizes = [np.mean(np.diff(ms.timevalues)) for ms in mesh_series]
meshes = [ms.mesh(ms.closest_timestep(timevalue)) for ms in mesh_series]
topology: pv.UnstructuredGrid = meshes[-3]
variable = ot.variables.Variable.find(variable_name, meshes[0])
richardson = studies.convergence.richardson_extrapolation(
    meshes, variable, topology, refinement_ratio
)

Grid convergence

If the shown values are approximately 1, this means that the results are in asymptotic range of convergence.

fig = ot.plot.contourf(richardson, "grid_convergence")
_show_with_alt(
    fig, "Contour plot of the grid convergence index across the mesh."
)
Contour plot of the grid convergence index across the mesh.

Grid comparison

Visualizing the requested mesh variable on the 3 finest discretizations:

fig = ot.plot.contourf(meshes[-3:], variable)
_show_with_alt(
    fig,
    f"Contour plots of {variable.output_name} on the 3 finest discretizations.",
)
Contour plots of darcy_velocity on the 3 finest discretizations.

Richardson extrapolation

Visualizing the Richardson extrapolation of the requested mesh variable. If a reference solution is provided, the difference between the two is shown as well. Otherwise the difference between the finest discretization and the Richardson extrapolation is shown.

fig = ot.plot.contourf(richardson, variable)
_show_with_alt(
    fig,
    f"Contour plot of the Richardson extrapolation of {variable.output_name}.",
)

data_key = variable.data_name
if reference_solution_path is None:
    diff_mesh = ot.mesh.difference(
        richardson, topology.sample(meshes[-1]), variable
    )
    fig = ot.plot.contourf(diff_mesh, variable)
    _show_with_alt(
        fig,
        f"Contour plot of the difference in {variable.output_name} between "
        "the finest discretization and the Richardson extrapolation.",
    )
else:
    ms = ot.MeshSeries(reference_solution_path)
    timestep = ms.closest_timestep(timevalue)
    reference_solution = topology.sample(ms.mesh(timestep))
    diff_mesh = ot.mesh.difference(reference_solution, richardson, variable)
    fig = ot.plot.contourf(diff_mesh, variable)
    _show_with_alt(
        fig,
        f"Contour plot of the difference in {variable.output_name} between "
        "the reference solution and the Richardson extrapolation.",
    )
Contour plot of the Richardson extrapolation of darcy_velocity.
/tmp/ipykernel_2973/290069088.py:9: RuntimeWarning: Only one input mesh defines a spatial unit. Assuming both meshes use the spatial unit `1 m`
  diff_mesh = ot.mesh.difference(
Contour plot of the difference in darcy_velocity between the finest discretization and the Richardson extrapolation.

Convergence metrics

metrics = studies.convergence.convergence_metrics(
    meshes, richardson, variable, timestep_sizes
)
metrics.style.format("{:,.5g}").hide()
mean element length maximum minimum abs. error (max) abs. error (min) abs. error (L2 norm) rel. error (max) rel. error (min) rel. error (L2 norm)
1 1.0607 1.1938e-16 -333.69 -9.4327e-16 334.01 0.99683 0.88766 0.99759
0.5 1.1571 2.2825e-16 -333.59 -8.3441e-16 333.6 0.99654 0.78521 0.99637
0.25 1.5878 1.453e-16 -333.16 -9.1735e-16 333.16 0.99526 0.86326 0.99508
0.125 2.0261 1.0596e-15 -332.73 -3.0776e-18 332.73 0.99395 0.0028962 0.99377
0.0625 2.4666 1.6637e-16 -332.29 -8.9628e-16 332.29 0.99263 0.84344 0.99245
0.03125 2.9076 5.671e-16 -331.84 -4.9555e-16 331.84 0.99131 0.46633 0.99113
0 334.75 1.0627e-15 0 0 3.1402e-16 0 0 9.3789e-19

Relative errors

ot.plot.contourplots.plt.rcdefaults()
fig = studies.convergence.plot_convergence_errors(metrics)
_show_with_alt(
    fig, f"Plot of the relative convergence errors of {variable.output_name}."
)
Plot of the relative convergence errors of darcy_velocity.

Absolute values

fig = studies.convergence.plot_convergence(metrics, variable)
_show_with_alt(
    fig, f"Plot of the absolute convergence values of {variable.output_name}."
)
Plot of the absolute convergence values of darcy_velocity.


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