diff --git a/ProcessLib/HydroMechanics/Tests.cmake b/ProcessLib/HydroMechanics/Tests.cmake index e46379a874e1bff72b5825d86de441f23f509da0..255bb862e05d5c581611555b405620abe00c02d0 100644 --- a/ProcessLib/HydroMechanics/Tests.cmake +++ b/ProcessLib/HydroMechanics/Tests.cmake @@ -921,5 +921,5 @@ AddTest( ) if(NOT WIN32 AND NOT OGS_USE_MPI) - NotebookTest(NOTEBOOKFILE HydroMechanics/SeabedResponse/Stationary_waves.ipynb RUNTIME 65) + NotebookTest(NOTEBOOKFILE HydroMechanics/SeabedResponse/Stationary_waves.py RUNTIME 65) endif() diff --git a/ProcessLib/LiquidFlow/Tests.cmake b/ProcessLib/LiquidFlow/Tests.cmake index cef3da27d11d28bd60c2a54ebf6b32e4a9ef28e0..03054f3199d3ebd2881652e1ebb80715b5a8b979 100644 --- a/ProcessLib/LiquidFlow/Tests.cmake +++ b/ProcessLib/LiquidFlow/Tests.cmake @@ -632,7 +632,7 @@ if(NOT OGS_USE_MPI) OgsTest(PROJECTFILE Parabolic/LiquidFlow/SimpleSynthetics/PrimaryVariableConstraintDirichletBC/cuboid_1x1x1_hex_1000_Dirichlet_Dirichlet_3.prj) OgsTest(PROJECTFILE Parabolic/LiquidFlow/SimpleSynthetics/FunctionParameterTest.prj) OgsTest(PROJECTFILE Parabolic/LiquidFlow/BlockingConductingFracture/block_conduct_frac.prj) - NotebookTest(NOTEBOOKFILE Parabolic/LiquidFlow/BlockingConductingFracture/BlockingConductingFracture.ipynb RUNTIME 6) + NotebookTest(NOTEBOOKFILE Parabolic/LiquidFlow/BlockingConductingFracture/BlockingConductingFracture.py RUNTIME 6) endif() # inclined mesh diff --git a/ProcessLib/PhaseField/Tests.cmake b/ProcessLib/PhaseField/Tests.cmake index fe25c0693a52473eb435bd7a79d8b9286250c708..19e38d0c22a5bd46590c80f69e018b46ca258bc1 100644 --- a/ProcessLib/PhaseField/Tests.cmake +++ b/ProcessLib/PhaseField/Tests.cmake @@ -160,10 +160,10 @@ AddTest( ) if(OGS_USE_PETSC) - NotebookTest(NOTEBOOKFILE PhaseField/surfing_jupyter_notebook/surfing_pyvista.ipynb RUNTIME 25) - NotebookTest(NOTEBOOKFILE PhaseField/beam_jupyter_notebook/beam.ipynb RUNTIME 1200 PROPERTIES PROCESSORS 3) - NotebookTest(NOTEBOOKFILE PhaseField/tpb_jupyter_notebook/TPB.ipynb RUNTIME 110 PROPERTIES PROCESSORS 4) - NotebookTest(NOTEBOOKFILE PhaseField/kregime_jupyter_notebook/Kregime_Static_jupyter.ipynb RUNTIME 40) - NotebookTest(NOTEBOOKFILE PhaseField/PForthotropy_jupyter_notebook/sen_shear.ipynb RUNTIME 500 PROPERTIES PROCESSORS 4) - NotebookTest(NOTEBOOKFILE PhaseField/Kregime_Propagating_jupyter_notebook/Kregime_Propagating_jupyter.ipynb RUNTIME 550) + NotebookTest(NOTEBOOKFILE PhaseField/surfing_jupyter_notebook/surfing_pyvista.py RUNTIME 25) + NotebookTest(NOTEBOOKFILE PhaseField/beam_jupyter_notebook/beam.py RUNTIME 500 PROPERTIES PROCESSORS 3) + NotebookTest(NOTEBOOKFILE PhaseField/tpb_jupyter_notebook/TPB.py RUNTIME 110 PROPERTIES PROCESSORS 4) + NotebookTest(NOTEBOOKFILE PhaseField/kregime_jupyter_notebook/Kregime_Static_jupyter.py RUNTIME 40) + NotebookTest(NOTEBOOKFILE PhaseField/PForthotropy_jupyter_notebook/sen_shear.py RUNTIME 500 PROPERTIES PROCESSORS 4) + NotebookTest(NOTEBOOKFILE PhaseField/Kregime_Propagating_jupyter_notebook/Kregime_Propagating_jupyter.py RUNTIME 550) endif() diff --git a/ProcessLib/SmallDeformation/Tests.cmake b/ProcessLib/SmallDeformation/Tests.cmake index 3057254300c158c69e990fde481645a275b5d306..e3bd7399531c78be6dfc027a1cea5937524ad70f 100644 --- a/ProcessLib/SmallDeformation/Tests.cmake +++ b/ProcessLib/SmallDeformation/Tests.cmake @@ -356,10 +356,10 @@ if(NOT OGS_USE_PETSC) NotebookTest(NOTEBOOKFILE Mechanics/EvaluatingBbarWithSimpleExamples/evaluating_bbbar_with_simple_examples.py RUNTIME 5) NotebookTest(NOTEBOOKFILE Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole.md RUNTIME 15) if(NOT WIN32) - NotebookTest(NOTEBOOKFILE Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole_convergence_analysis.ipynb RUNTIME 40) + NotebookTest(NOTEBOOKFILE Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole_convergence_analysis.py RUNTIME 40) endif() if (OGS_USE_MFRONT) - NotebookTest(NOTEBOOKFILE Mechanics/PLLC/PLLC.ipynb RUNTIME 7) + NotebookTest(NOTEBOOKFILE Mechanics/PLLC/PLLC.py RUNTIME 7) if(TFEL_WITH_PYTHON) NotebookTest(NOTEBOOKFILE Mechanics/HoekBrown/HoekBrownYieldCriterion.md RUNTIME 20) endif() diff --git a/ProcessLib/TH2M/Tests.cmake b/ProcessLib/TH2M/Tests.cmake index cb581c572aaff3e86fc4b64ab4233e24bca89972..f848a77397ee1c24ab5a7e841078d3950fa8ae25 100644 --- a/ProcessLib/TH2M/Tests.cmake +++ b/ProcessLib/TH2M/Tests.cmake @@ -25,13 +25,13 @@ if (NOT OGS_USE_MPI) OgsTest(PROJECTFILE TH2M/TH2/heatpipe/heat_pipe_strict.prj RUNTIME 80) OgsTest(PROJECTFILE TH2M/H2/dissolution_diffusion/continuous_injection.prj RUNTIME 60) OgsTest(PROJECTFILE TH2M/H2/dissolution_diffusion/bourgeat.prj RUNTIME 60) - NotebookTest(NOTEBOOKFILE TH2M/H2/dissolution_diffusion/phase_appearance.ipynb RUNTIME 60) - NotebookTest(NOTEBOOKFILE TH2M/H2/mcWhorter/mcWhorter.ipynb RUNTIME 55) + NotebookTest(NOTEBOOKFILE TH2M/H2/dissolution_diffusion/phase_appearance.py RUNTIME 60) + NotebookTest(NOTEBOOKFILE TH2M/H2/mcWhorter/mcWhorter.py RUNTIME 55) OgsTest(PROJECTFILE TH2M/H/diffusion/diffusion.prj RUNTIME 10) - NotebookTest(NOTEBOOKFILE TH2M/H/diffusion/diffusion.ipynb RUNTIME 30) + NotebookTest(NOTEBOOKFILE TH2M/H/diffusion/diffusion.py RUNTIME 30) OgsTest(PROJECTFILE TH2M/TH/Ogata-Banks/ogata-banks.prj RUNTIME 60) - NotebookTest(NOTEBOOKFILE TH2M/TH/Ogata-Banks/Ogata-Banks.ipynb RUNTIME 120) - NotebookTest(NOTEBOOKFILE TH2M/TH/idealGasLaw/confined_gas_compression.ipynb RUNTIME 10) + NotebookTest(NOTEBOOKFILE TH2M/TH/Ogata-Banks/Ogata-Banks.py RUNTIME 120) + NotebookTest(NOTEBOOKFILE TH2M/TH/idealGasLaw/confined_gas_compression.py RUNTIME 10) # submesh residuum output OgsTest(PROJECTFILE TH2M/submesh_residuum_assembly/T.xml RUNTIME 1) OgsTest(PROJECTFILE TH2M/submesh_residuum_assembly/p_G.xml RUNTIME 1) @@ -454,5 +454,5 @@ AddTest( ) if(NOT OGS_USE_PETSC) - NotebookTest(NOTEBOOKFILE TH2M/TH2/heatpipe/heatpipe.ipynb RUNTIME 140) + NotebookTest(NOTEBOOKFILE TH2M/TH2/heatpipe/heatpipe.py RUNTIME 140) endif() diff --git a/ProcessLib/Tests.cmake b/ProcessLib/Tests.cmake index 8124203e3e1444a8ca2c0aa37e102423d1f9b090..7405fd865b1924bfaf53d139a2e53bdd477a6dcc 100644 --- a/ProcessLib/Tests.cmake +++ b/ProcessLib/Tests.cmake @@ -6,7 +6,7 @@ if(TARGET ThermoHydroMechanics ) NotebookTest( NOTEBOOKFILE - ThermoHydroMechanics/Linear/Point_injection/SaturatedPointheatsource.ipynb + ThermoHydroMechanics/Linear/Point_injection/SaturatedPointheatsource.py RUNTIME 1800 PROPERTIES PROCESSORS 4 ) diff --git a/Tests/Data/HydroMechanics/SeabedResponse/Stationary_waves.py b/Tests/Data/HydroMechanics/SeabedResponse/Stationary_waves.py new file mode 100644 index 0000000000000000000000000000000000000000..e324cef5a69db69cad59d3e1dd448681e8ce281c --- /dev/null +++ b/Tests/Data/HydroMechanics/SeabedResponse/Stationary_waves.py @@ -0,0 +1,813 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Seabed response to water waves" +# date = "2023-01-11" +# author = "Linda Günther" +# web_subsection = "hydro-mechanics" +# +++ +# + +# %% [markdown] +# |<div style="width:330px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:330px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# # Seabed response to water waves + +# %% [markdown] +# In this benchmark example, the response of a seabed to water waves is determined by the application of the theory of poroelasticity. The seabed is described as a homogeneous poroelastic medium consisting of a solid phase (i.e. sand grains) and pores that are quasi-saturated with a Newtonian fluid, such as water. The pores are not completely saturated with water as they contain small bubbles of air, which cause the compressibility of the pore space. The pressure along the bottom of the sea depends on the water level and therefore varies according to the water waves in space as well as time. Arnold Verruijt in his book "Theory and problems of poroelasticity" [1] derives an analytical solution of the stress distribution in the seabed for the case of sinusoidal water waves. For the analytical solution, some assumptions have been made: +# * disregarded body forces +# * homogenous linear elastic material +# * compressibility of the particles is taken into account +# * water waves are stationary and only dependent on one horizontal direction (x-direction in this example) +# * plain strain condition +# +# The last two assumptions allow a simplification of the three-dimensional seabed to a planar geometry. +# +# For verification of the numerical model, Arnold Verruijt's analytical solution will be compared to the results of the numerical calculation. The behaviour of the seabed depends on various parameters describing the soil, fluid and wave properies. In this example, the parameters of both the analytical and the numerical solution are chosen as follows: + +# %% [markdown] +# #### Input parameters + +# %% [markdown] +# $$ +# \begin{aligned} +# &\begin{array}{llll} +# C_{f} & \textsf{compressibility of the fluid} & \textsf{[1/Pa]} & C_{f} = 0\,\textsf{Pa}^{-1}\\ +# C_{s} & \textsf{compressibility of the solid particles}& \textsf{[1/Pa]} & C_{s} = 0\,\textsf{Pa}^{-1}\\ +# G & \textsf{shear modulus} & \textsf{[Pa]} & G = 100\, \textsf{kPa} \\ +# K & \textsf{bulk modulus} & \textsf{[Pa]} & K = \frac{2}{3} G \ (\textsf{with } \nu = 0) \\ +# L & \textsf{wave length} & \textsf{[m]} & L = 100 \textsf{ m} \\ +# T & \textsf{periodic time} & \textsf{[s]} & T=\frac{1}{f} = 10\,\textsf{s} \\ +# \gamma_{w} & \textsf{volumetric weight of water} & \textsf{[kN/m³]} & \gamma_{w} = 9,81\,\textsf{kN/m³} \\ +# \kappa & \textsf{(intrinsic) permeability} & \textsf{[m²]} & \kappa = 10^{-11}\,\textsf{m² } ( \textsf{equals } k_{f}=10^{-4} \textsf{ m/s} ) \\ +# \mu & \textsf{dynamic viscosity of water} & \textsf{[Pa s]} & \mu = 1.3\,\textsf{mPa s } ( \textsf{at } \theta=10\,\textsf{°C}) \\ +# \nu & \textsf{Poisson's ratio} & \textsf{[-]} & \nu = 0\\ +# \end{array} +# \end{aligned} +# $$ +# + +# %% [markdown] +# #### Derived parameters + +# %% [markdown] +# $$ +# \begin{aligned} +# &\begin{array}{llll} +# c_{v} & \textsf{coefficient of consolidation} & \textsf{[m²/s]} & c_{v} = \frac{kG(1+m)}{(1+SG(1+m))\gamma_{w}} = \frac{k(K+\frac{4}{3}G)}{(1+S(K+\frac{4}{3}G))\gamma_{w}} \\ +# m & \textsf{dimensionless parameter} & \textsf{[-]} & m = \frac{1}{1-2\nu} = \frac{K+\frac{1}{3}G}{G} \\ +# k & \textsf{hydraulic conductivity} & \textsf{[m/s]} & k = \frac{\kappa\rho_{f}g}{\mu} \\ +# C_{m} & \textsf{compressibility of the porous medium} & \textsf{[1/Pa]}& C_{m} = \frac{1}{K} \\ +# S & \textsf{storativity of the pore space}& \textsf{[-]} & S=nC_{f}+(\alpha-n)C_{s} \\ +# \alpha & \textsf{Biot coefficient} & \textsf{[-]} & \alpha=1-\frac{C_{s}}{C_{m}} \\ +# \lambda & \textsf{wave number} & \textsf{[1/m]} & \lambda=\frac{\omega}{c} = \frac{\omega T}{L} \\ +# \xi^2 & \textsf{complex parameter} & \textsf{[1/m²]} & \xi^2 = \lambda^2 + \frac{\omega}{c_{v}}i \\ +# \omega & \textsf{frequency of the wave} & \textsf{[1/s]} & \omega = 2 \pi f \\ +# \end{array} +# \end{aligned} +# $$ + +# %% [markdown] +# ### Analytical solution for stationary waves + +# %% [markdown] +# #### Boundary conditions +# +# The considered region is the half plane $y>0$ with a load in the form of a stationary wave applied on the surface $y=0$. +# The boundary conditions at $y=0$ are: +# +# $$ +# \begin{align} +# p&=\tilde{p}\cdot e^{i(\omega t-\frac{\pi}{2})}\cdot\cos(\lambda x) \\ +# \sigma'_{yy}&=0 \\ +# \sigma'_{xy}&=0 +# \end{align} +# $$ +# +# where $p$ is the pore pressure, $\tilde{p}$ is the amplitude of the applied load, $\sigma'_{yy}$ is the effective vertical stress and $\sigma'_{xy}$ is the effective shear stress. The boundary condition of the pore pressure describes the space- and time-dependent water wave. Compared to Arnold Verruijt's solution, in this example there is a phase shift of $-\frac{\pi}{2}$ in the time-dependent part. The phase shift is necessary to obtain a water wave that starts oszillating from the equilibrium state (sine instead of cosine) and thus to be able to set an initial condition of $p=0$ Pa on the whole domain in the numerical solution. +# +# With these boundary conditions, the following four constants are determined: +# +# $$ +# \begin{align} +# B_{1} &= (1+m)(\xi^2-\lambda^2)-2\lambda(\xi-\lambda) \\ +# B_{2} &= 2m\theta\lambda\xi+\theta[(1+m)(\xi^2-\lambda^2)-2\lambda(\xi-\lambda)] \\ +# B_{3} &= 2m\theta\lambda\\ +# D &= 2\lambda[2\lambda(\xi-\lambda)-(1+m)(1+m\theta)(\xi^2-\lambda^2)] +# \end{align} +# $$ +# +# #### The stresses +# For the given boundary conditions, the pore pressure and the effective stresses can be calculated with the following equations. According to the sign convention commonly used in soil mechanics, compressive stresses are positive and tensile stresses are negative. In all these equations, the real part is to be taken only. +# +# $$ +# \begin{align} +# \frac{p}{\tilde{p}} &= \mathscr{R}\left\{\frac{-2\lambda B_{1}\cdot e^{-\lambda y}-(1+m)(\xi^2-\lambda^2)B_{3}\cdot e^{-\xi y}}{D}\cdot e^{i(\omega t-\frac{\pi}{2})}\cdot\cos(\lambda x)\right\} \\ +# \\ +# \frac{\sigma'_{xx}}{\alpha\tilde{p}} &= \mathscr{R}\left\{\frac{[-2(m-1)\lambda\theta+2\lambda(1+m\theta)\lambda y]B_{1}\cdot e^{-\lambda y}-2\lambda B_{2} \cdot e^{-\lambda y}+[(m-1)(\xi^2-\lambda^2)-2\lambda^2]B_{3} \cdot e^{-\xi y}}{D} \cdot e^{i(\omega t-\frac{\pi}{2})} \cdot \cos(\lambda x) \right\} \\ +# \\ +# \frac{\sigma'_{yy}}{\alpha\tilde{p}} &= \mathscr{R}\left\{\frac{[-2(m+1)\lambda\theta-2\lambda(1+m\theta)\lambda y]B_{1}\cdot e^{-\lambda y}+2\lambda B_{2} \cdot e^{-\lambda y}+[(m-1)(\xi^2-\lambda^2)+2\xi^2]B_{3} \cdot e^{-\xi y}}{D} \cdot e^{i(\omega t-\frac{\pi}{2})} \cdot \cos(\lambda x) \right\} \\ +# \\ +# \frac{\sigma'_{xy}}{\alpha\tilde{p}} &= \mathscr{R}\left\{\frac{[-2\lambda(1+m\theta)\lambda y-2\lambda\theta]B_{1}\cdot e^{-\lambda y}+2\lambda B_{2} \cdot e^{-\lambda y}+2\xi\lambda B_{3} \cdot e^{-\xi y}}{D} \cdot e^{i(\omega t-\frac{\pi}{2})} \cdot \sin(\lambda x) \right\} \\ +# \end{align} +# $$ + +# %% +import matplotlib.pyplot as plt +import numpy as np + +plt.rc("font", size=8) +plt.rc("axes", titlesize=10) +plt.rc("axes", labelsize=10) + +import gmsh +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + + +# %% +def compute_pressure_and_stresses(t, x, z): + n = 0.4 + G = 100e3 # [Pa] + K = 2 / 3 * G # [Pa] (with ny=0) + ny = 0 # E = 3K(1-2ny) = 2G(1+ny) + Cf = 0 # in the book: Cf = 0.001/K + Cs = 0 + Cm = 1 / K + my = 1.3e-3 # [Pa*s] + kappa = 1e-11 # [m²] (medium sand, kf=10e-4 m/s) + gamma_w = 9.81e3 # [Pa/m] + lam = 2 * np.pi * 0.1 * 10 / 100 + omega = 2 * np.pi * 0.1 + + k = kappa * gamma_w / my # Gl. (1.33) + alpha = 1 - Cs / Cm # Gl. (4.15) + S = n * Cf + (alpha - n) * Cs # Gl. (1.28) + theta = S * G / alpha**2 # Gl. (4.13) + m = 1 / (1 - 2 * ny) # = K+1/3*G/G # Gl. (4.5) + cv = ( + k * G * (1 + m) / (alpha**2 * (1 + theta + m * theta) * gamma_w) + ) # Gl. (4.12) + xi_2 = complex(lam**2, (omega / cv)) # Gl. (4.19) + + B1 = (1 + m) * (xi_2 - lam**2) - 2 * lam * (np.sqrt(xi_2) - lam) + B2 = 2 * m * theta * lam * np.sqrt(xi_2) + theta * ( + (1 + m) * (xi_2 - lam**2) - 2 * lam * (np.sqrt(xi_2) - lam) + ) + B3 = 2 * m * theta * lam + D = ( + 2 + * lam + * ( + 2 * lam * (np.sqrt(xi_2) - lam) + - (1 + m) * (1 + m * theta) * (xi_2 - lam**2) + ) + ) + p_rel = np.real( + ( + -2 * lam * B1 * np.exp(-lam * z) + - (1 + m) * (xi_2 - lam**2) * B3 * np.exp(-np.sqrt(xi_2) * z) + ) + / D + * np.exp((omega * t - np.pi * 0.5) * 1j) + * np.cos(lam * x) + ) + sig_xx_rel = np.real( + ( + (-2 * (m - 1) * lam * theta + 2 * lam * (1 + m * theta) * lam * z) + * B1 + * np.exp(-lam * z) + - 2 * lam * B2 * np.exp(-lam * z) + + ((m - 1) * (xi_2 - lam**2) - 2 * lam**2) + * B3 + * np.exp(-np.sqrt(xi_2) * z) + ) + / D + * np.exp((omega * t - np.pi * 0.5) * 1j) + * np.cos(lam * x) + ) + sig_zz_rel = np.real( + ( + (-2 * (m + 1) * lam * theta - 2 * lam * (1 + m * theta) * lam * z) + * B1 + * np.exp(-lam * z) + + 2 * lam * B2 * np.exp(-lam * z) + + ((m - 1) * (xi_2 - lam**2) + 2 * xi_2) * B3 * np.exp(-np.sqrt(xi_2) * z) + ) + / D + * np.exp((omega * t - np.pi * 0.5) * 1j) + * np.cos(lam * x) + ) + sig_xz_rel = np.real( + ( + (-2 * lam * (1 + m * theta) * lam * z - 2 * lam * theta) + * B1 + * np.exp(-lam * z) + + 2 * lam * B2 * np.exp(-lam * z) + + 2 * np.sqrt(xi_2) * lam * B3 * np.exp(-np.sqrt(xi_2) * z) + ) + / D + * np.exp((omega * t - np.pi * 0.5) * 1j) + * np.sin(lam * x) + ) + return p_rel, sig_xx_rel, sig_zz_rel, sig_xz_rel + + +# %% [markdown] +# By evaluating these equations at different times $t$ and depths $y$, we gain a better understanding of the pressure and stress distribution in the seabed. The below plot illustrates the pore pressure and the amplitude of the effective stresses as a function of depth directly underneath an anti-node of the standing water wave (the place where the amplitude is at maximum, i.e. for $x = k \cdot \frac{L}{2}$, where $k=0, 1, 2,$ ...). +# +# Along the top edge of the seabed, the pore pressure is always as large as the applied load and the effective stresses are zero. This means, that all the change in pressure is absorbed by the fluid while the soil particles remain in their initial stress state (in this case zero, since body forces are being disregarded). The increased pore pressure at the top edge cannot propagate freely downwards into the seabed because seepage is limited by the hydraulic conductivity of the soil. Consequently, the pore pressure decreases with depth as the soil matrix gradually takes up the remaining share of the total stress in the seabed. + +# %% +y = np.linspace(0, 100, 1000) +y_rel = y / 100 +colors = { + 0: "orangered", + 2: "gold", + 4: "blueviolet", + 6: "forestgreen", + 8: "darkorange", + 10: "royalblue", +} + +fig, ax = plt.subplots(ncols=2, figsize=(15, 7)) +for idx in (0, 1): + ax[idx].grid(True) + ax[idx].set_ylabel("$y$ / $L$") + ax[idx].set_xlim(-1.1, 1.1) + +for t in [0, 2, 4, 6, 8, 10]: + ax[0].plot( + compute_pressure_and_stresses(t, 0, y)[0], + -y_rel, + color=colors[t], + label=f"t = {t:.1f} s", + ) + +t = 2.5 +ax[0].set_xlabel("$p$ / $\\tilde{p}$") +ax[0].legend() +ax[1].plot( + compute_pressure_and_stresses(t, 0, y)[1], + -y_rel, + color=colors[6], + label=r"$\sigma'_{xx}/(\alpha\tilde{p})$", +) +# ax[1].plot(compute_pressure_and_stresses(t,0,y)[1]+compute_pressure_and_stresses(t,0,y)[0], -y_rel, linestyle = "--", color = colors[3], label = "$\\sigma_{xx}$/$\\alpha\\tilde{p}$") # Total horizontal stress +ax[1].plot( + compute_pressure_and_stresses(t, 0, y)[2], + -y_rel, + color=colors[2], + label=r"$\sigma'_{yy}/(\alpha\tilde{p})$", +) +# ax[1].plot(compute_pressure_and_stresses(t,0,y)[2]+compute_pressure_and_stresses(t,0,y)[0], -y_rel, linestyle = "--", color = colors[1], label = "$\\sigma_{yy}$/$\\alpha\\tilde{p}$") # Total vertical stress +ax[1].plot( + compute_pressure_and_stresses(t, 0, y)[3], + -y_rel, + color=colors[4], + label=r"$\sigma'_{xy}/(\alpha\tilde{p})$", +) +ax[1].set_xlabel(r"$\sigma'/(\alpha\tilde{p})$") +ax[1].legend() + + +# %% [markdown] +# The following plot illustrates the pore pressure and the effective stresses at different depths underneath an anti-node over time. Here it becomes clear that the vertical and the horizontal effective stresses behave symmetrically in space as well as time. + +# %% +t = np.linspace(0, 20, 200) +colors = {1: "gold", 2: "blueviolet", 3: "forestgreen", 4: "royalblue"} + +fig, ax = plt.subplots(ncols=2, figsize=(15, 7)) +for idx in (0, 1): + ax[idx].grid(True) + ax[idx].set_xlabel("$t$ / s") + +for y in np.linspace(0, 100, 6): + ax[0].plot(t, compute_pressure_and_stresses(t, 0, y)[0], color=colors[4]) + ax[0].set_ylabel("$p/\\tilde{p}$") + ax[1].plot( + t, + compute_pressure_and_stresses(t, 0, y)[1], + color=colors[3], + label=r"$\sigma'_{xx}/(\alpha\tilde{p})$", + ) + ax[1].plot( + t, + compute_pressure_and_stresses(t, 0, y)[2], + color=colors[1], + label=r"$\sigma'_{yy}/(\alpha\tilde{p})$", + ) + ax[1].plot( + t, + compute_pressure_and_stresses(t, 0, y)[3], + color=colors[2], + label=r"$\sigma'_{xy}/(\alpha\tilde{p})$", + ) + if y == 0: + ax[1].legend(loc="upper right") + +ax[1].set_ylabel("$\\sigma$'/$\\alpha\\tilde{p}$") + + +ax[0].set_title("Pore pressure over time") +ax[1].set_title("Effective stresses over time") + + +# %% [markdown] +# For a better understanding of the planar pressure and stress distribution, the following 2D color-plots are helpful. Each of the diagramms illustrates the pressure or stress distribution at a timepoint, where the applied load is at maximum. + +# %% +x, y = np.meshgrid(np.linspace(0, 200, 1000), np.linspace(0, 100, 1000)) +t = 2.5 + +fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(15, 7)) +l1 = ax[0][0].contourf(x, -y, compute_pressure_and_stresses(t, x, y)[0], 15) +l2 = ax[0][1].contourf(x, -y, compute_pressure_and_stresses(t, x, y)[1], 15) +l3 = ax[1][1].contourf(x, -y, compute_pressure_and_stresses(t, x, y)[2], 15) +l4 = ax[1][0].contourf(x, -y, compute_pressure_and_stresses(t, x, y)[3], 15) +fig.colorbar(l1, ax=ax[0][0]) +fig.colorbar(l2, ax=ax[0][1]) +fig.colorbar(l3, ax=ax[1][1]) +fig.colorbar(l4, ax=ax[1][0]) +for i in (0, 1): + for j in (0, 1): + ax[i][j].set_aspect("equal") + ax[i][j].set_xlabel("$x$ / m") + ax[i][j].set_ylabel("$y$ / m") +ax[0][0].set_title("$p/\\tilde{p}$") +ax[0][1].set_title("$\\sigma'_{xx}/\\alpha\\tilde{p}$") +ax[1][1].set_title("$\\sigma'_{yy}/\\alpha\\tilde{p}$") +ax[1][0].set_title("$\\sigma'_{xy}/\\alpha\\tilde{p}$") +fig.tight_layout() + + +# %% [markdown] +# ### Numerical solution +# In the following section, the behavior of the seabed under the influence of standing water waves is solved numerically by FEM in OpenGeoSys. The principle of Finite Element Methods is to subdivide a large and complex object into smaller parts (called finite elements), which is achieved by the construction of a mesh of the object. For each of the meshes' nodes, an approximation of the real solution is calculated. Therefore, numerical solutions always contain errors that originate in the so-called space discretization. The finer the resolution of a mesh, the more accurately the approximation matches the real solution. However, a large number of elements and nodes also causes longer calculation times. A description of the meshing process for this specific example follows below. +# +# For transient problems, a time discretization must also be carried out in addition to the space discretization. In doing so, the observed time period is divided into finite time steps. Here too, smaller timesteps come along with more accurate results but longer calculation times. For the numerical simulation of the seabed, a timestep of $\Delta t = 0.25$ s was chosen. With a periodic time of the water wave of $T=10$ s, there are $40$ timesteps per period. The number of timesteps per period should be large enough to enable an accurate representation of the wave's sinusoidal shape. + +# %% [markdown] +# #### Meshing + +# %% [markdown] +# Since the problem can be simplified to a planar geometry, a two-dimensional mesh is constructed for the numerical solution. Here, the Gmsh application programming interface (API) for python is used to generate a structured mesh of rectangular shape. This allows a parametric input of the geometry's dimensions and the mesh refinement. The generated mesh consists of quadratic elements, which have mid-side-nodes. In that way, displacements between nodes at the vertices can be interpolated using a higher order polynomial. +# +# For the further use in OpenGeoSys, the gmsh-mesh is converted to the vtu-format using the msh2vtu script. + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + + +# %% +def generate_mesh_axb(a, b, Nx, Ny, P): + output_file = f"{out_dir}/square_{a}x{b}.msh" + + lc = 0.5 + + # Before using any functions in the Python API, Gmsh must be initialized: + gmsh.initialize() + gmsh.option.setNumber("General.Terminal", 1) + gmsh.model.add("rectangle") + + # Dimensions + dim1 = 1 + dim2 = 2 + + # Outer points (ccw) + gmsh.model.geo.addPoint(0, -b, 0, lc, 1) + gmsh.model.geo.addPoint(a, -b, 0, lc, 2) + gmsh.model.geo.addPoint(a, -b / 2, 0, lc, 3) + gmsh.model.geo.addPoint(a, 0, 0, lc, 4) + gmsh.model.geo.addPoint(0, 0, 0, lc, 5) + gmsh.model.geo.addPoint(0, -b / 2, 0, lc, 6) + + # Outer lines (ccw) + gmsh.model.geo.addLine(1, 2, 1) + gmsh.model.geo.addLine(2, 3, 2) + gmsh.model.geo.addLine(3, 4, 3) + gmsh.model.geo.addLine(4, 5, 4) + gmsh.model.geo.addLine(5, 6, 5) + gmsh.model.geo.addLine(6, 1, 6) + gmsh.model.geo.addLine(6, 3, 7) + + # The third elementary entity is the surface. In order to define a surface + # from the curves defined above, a curve loop has first to be defined (ccw). + gmsh.model.geo.addCurveLoop([1, 2, -7, 6], 1) + gmsh.model.geo.addCurveLoop([7, 3, 4, 5], 2) + + # Add plane surfaces defined by one or more curve loops. + gmsh.model.geo.addPlaneSurface([1], 1) + gmsh.model.geo.addPlaneSurface([2], 2) + + gmsh.model.geo.synchronize() + + # Prepare structured grid + gmsh.model.geo.mesh.setTransfiniteCurve(1, Nx) + gmsh.model.geo.mesh.setTransfiniteCurve(2, int(Ny * 0.3)) + gmsh.model.geo.mesh.setTransfiniteCurve(3, Ny, "Progression", -P) + gmsh.model.geo.mesh.setTransfiniteCurve(4, Nx) + gmsh.model.geo.mesh.setTransfiniteCurve(5, Ny, "Progression", P) + gmsh.model.geo.mesh.setTransfiniteCurve(6, int(Ny * 0.3)) + gmsh.model.geo.mesh.setTransfiniteCurve(7, Nx) + + gmsh.model.geo.mesh.setTransfiniteSurface(1, "Alternate") + gmsh.model.geo.mesh.setTransfiniteSurface(2, "Alternate") + + gmsh.model.geo.mesh.setRecombine(dim2, 1) + gmsh.model.geo.mesh.setRecombine(dim2, 2) + + gmsh.model.geo.synchronize() + + # Physical groups (only this gets saved to file per default) + Bottom = gmsh.model.addPhysicalGroup(dim1, [1]) + gmsh.model.setPhysicalName(dim1, Bottom, "Bottom") + + Right = gmsh.model.addPhysicalGroup(dim1, [2, 3]) + gmsh.model.setPhysicalName(dim1, Right, "Right") + + Top = gmsh.model.addPhysicalGroup(dim1, [4]) + gmsh.model.setPhysicalName(dim1, Top, "Top") + + Left = gmsh.model.addPhysicalGroup(dim1, [5, 6]) + gmsh.model.setPhysicalName(dim1, Left, "Left") + + Plate = gmsh.model.addPhysicalGroup(dim2, [1, 2]) + gmsh.model.setPhysicalName(dim2, Plate, "Plate") + + gmsh.model.geo.synchronize() + + gmsh.model.mesh.generate(dim2) + # gmsh.option.setNumber('Mesh.SecondOrderIncomplete', 1) # serendipity elements + gmsh.model.mesh.setOrder(2) # higher order elements (quadratic) + gmsh.write(output_file) + + gmsh.finalize() + + +# %% +generate_mesh_axb(200, 100, 25, 45, 1.07) + + +# %% +input_file = f"{out_dir}/square_200x100.msh" + + +# %% +# !msh2vtu --keep_ids -r -o {out_dir} {input_file} +assert _exit_code == 0 # noqa: F821 +# %cd {out_dir} +# !identifySubdomains -f -m square_200x100_domain.vtu -- square_200x100_physical_group_*.vtu +# %cd - + + +# %% [markdown] +# The below figure shows the generated mesh with a width of $a=2L=200$ m and a height (or rather depth) of $b=L=100$ m. +# +# **Note:** the origin of coordinates is located at ground level, therefore y-coordinates are negative +# +# As was already visible in the analytical solution, the pressure and stress gradients are particularly high in the upper half of the geometry. Therefore, it makes sense to refine the mesh in this area. + +# %% +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + +mesh = pv.read(f"{out_dir}/square_200x100_domain.vtu") +plotter = pv.Plotter(window_size=[1000, 800]) +plotter.add_mesh(mesh, show_edges=True, show_scalar_bar=False, color=None, scalars=None) + +plotter.show_bounds(ticks="outside", xlabel="x / m", ylabel="y / m") +plotter.view_xy() +plotter.show() + + +# %% [markdown] jp-MarkdownHeadingCollapsed=true +# #### Boundary and initial conditions +# Because it would not be possible and useful to solve the problem for an infinitely extended seabed, the observed domain has limited dimensions. This however calls for the definition of boundary conditions at the edges of the domain. Boundary conditions constrain the value of a process variable (Dirichlet boundary condition) or the derivative of a process variable applied at the boundary of the domain (Neumann boundary condition). The process variables in this example are the (pore) pressure $p$ and the displacement $u$. +# +# In this example, the boundary conditions are defined as follows: +# +# **Top:** +# $$ +# \begin{align} +# p(y=0)&=\tilde{p}\cdot \sin(\omega \cdot t) \cdot \cos(\frac{2 \pi}{L} \cdot x) \\ +# \sigma_{yy}(y=0)&=-\tilde{p}\cdot \sin(\omega \cdot t) \cdot \cos(\frac{2 \pi}{L} \cdot x) +# \end{align} +# $$ +# +# These boundary conditions represent the applied load in the form of a stationary wave with an amplitude of $\tilde{p} = 0.1\cdot10^5$ Pa. The Neumann boundary condition of the displacement equates to the vertical total stress (Attention: downwards direction is negative). +# +# **Bottom**: +# $$ +# %\begin{align} +# u_{y}(y=L) =0 +# %\end{align} +# $$ +# +# **Left and right**: +# $$ +# \begin{align} +# u_{x}(x=0)&=0 \\ +# u_{x}(x=2L)&=0 +# \end{align} +# $$ +# +# Along the vertical lines directly underneath the anti-nodes of the wave, there is no horizontal displacement. Since the maximum loads appear in that area, the material evades symmetrically to both sides from this line to places of lower stress. For this reason, the vertical boundaries of the domain are chosen exactly along these lines. +# +# Since the problem is time-dependent, initial conditions must be specified as well: +# $$ +# \begin{align} +# \vec{u}(t=0)&=\vec{0} \\ +# p(t=0)&=0 \textsf{ Pa} +# \end{align} +# $$ + +# %% [markdown] +# #### Running the numerical model + +# %% +import ogstools as ot + + +# %% +## Helper Functions +def read_timestep_mesh(a, time): + reader = pv.PVDReader(f"{out_dir}/square_{a}x100.pvd") + reader.set_active_time_point(int(time * 4)) # time [s], delta t = 0.25 s + return reader.read()[0] + + +def slice_along_line(mesh, start_point, end_point): + line = pv.Line(start_point, end_point, resolution=2) + return mesh.slice_along_line(line) + + +def get_pressure_sorted(mesh): + pressure = mesh.point_data["pressure_interpolated"] + depth = mesh.points[:, 1] + indices_sorted = np.argsort(depth) + return pressure[indices_sorted] + + +def get_stresses_sorted(mesh): + sigma = mesh.point_data["sigma"] + depth = mesh.points[:, 1] + indices_sorted = np.argsort(depth) + sigma_xx = -sigma[indices_sorted, 0] # switching sign convention + sigma_yy = -sigma[indices_sorted, 1] + # sigma_zz = - sigma[indices_sorted, 2] + sigma_xy = +sigma[indices_sorted, 3] + return sigma_xx, sigma_yy, sigma_xy # ,sigma_zz + + +def get_depth_sorted(mesh): + depth = mesh.points[:, 1] + indices_sorted = np.argsort(depth) + return depth[indices_sorted] + + +def compute_abs_and_rel_pressure_error(pressures, depth, t, x): + num_points = pressures.shape[0] + f_abs = np.zeros(num_points) + f_rel = np.zeros(num_points) + + for pt_idx in range(num_points): + y = -depth[pt_idx] + pressure_ana = compute_pressure_and_stresses(t, x, y)[ + 0 + ] # returns pressure normalised to the pressure amplitude + pressure_num = ( + pressures[pt_idx] / 0.1e5 + ) # absolute pressure divided by pressure amplitude + f_abs[pt_idx] = pressure_num - pressure_ana + + if pressure_ana == 0: + f_rel[pt_idx] = f_abs[pt_idx] / 1e-2 + else: + f_rel[pt_idx] = f_abs[pt_idx] / pressure_ana + + return f_abs, f_rel + + +def compute_abs_and_rel_stress_error(_sigmas, depth, t, x): + num_points = depth.shape[0] + f_abs = np.zeros((3, num_points)) + f_rel = np.zeros((3, num_points)) + + for stress_idx in (0, 1, 2): + for pt_idx in range(num_points): + y = -depth[pt_idx] + sigma_ana = compute_pressure_and_stresses(t, x, y)[ + stress_idx + 1 + ] # returns stresses normalised to the pressure amplitude + sigma_num = ( + sigma[stress_idx][pt_idx] / 0.1e5 + ) # absolute stresses divided by pressure amplitude + f_abs[stress_idx][pt_idx] = sigma_num - sigma_ana + + if sigma_ana == 0: + f_rel[stress_idx][pt_idx] = f_abs[stress_idx][pt_idx] / 1e-2 + else: + f_rel[stress_idx][pt_idx] = f_abs[stress_idx][pt_idx] / sigma_ana + + return f_abs, f_rel + + +# %% +model = ot.Project( + input_file="seabed_response_200x100.prj", output_file="seabed_response_200x100.prj" +) +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir} -m {out_dir}") + + +# %% [markdown] +# The below plot illustrates the pore pressure distribution in the domain at a timepoint where the applied load is at maximum. By comparing this plot to the 2D-colorplots of the analytical solution, it can already be concluded that the numerical solution resembles the analytical solution. + +# %% +time = 2.5 # [s] +reader = pv.get_reader(f"{out_dir}/square_200x100.pvd") +reader.set_active_time_point(int(time * 4)) +mesh = reader.read()[0] + +plotter = pv.Plotter() + +sargs = dict(title="p / Pa", height=0.25, position_x=0.2, position_y=0.02) # noqa: C408 +plotter.add_mesh( + mesh, + scalars="pressure_interpolated", + show_edges=False, + show_scalar_bar=True, + label="p", + scalar_bar_args=sargs, +) +plotter.show_bounds(ticks="outside", xlabel="x / m", ylabel="y / m") +plotter.add_axes() +plotter.view_xy() +plotter.show() + + +# %% [markdown] +# For a more detailed comparison between the analytical and the numerical solution, both solutions are evaluated along the vertical line directly underneath an anti-node of the standing wave. As before, the pore pressure and the amplitude of the effective stresses are illustrated as a function of depth. The results of the numerical solution are marked as dots in the same color as the analytical solution. Additionally, the absolute errors $\Delta p = p_{numerical}-p_{analtical}$ and $\Delta \sigma_{i}' = \sigma_{i, numerical}'-\sigma_{i, analytical}'$ are illustrated on the right. +# +# The plot shows that the absolute errors are very small at about $2 \%$ of the wave's amplitude. They can mostly be ascribed to the space- and time-discretization. Close to the top boundary of the domain, larger errors occur. These errors could originate in the definition of both a pressure and displacement (Neumann-) boundary condition along the top edge. + +# %% +x = 0 +y = np.linspace(0, 100, 1000) +y_rel = y / 100 +colors = { + 0: "orangered", + 2: "gold", + 4: "blueviolet", + 6: "forestgreen", + 8: "darkorange", + 10: "royalblue", +} + +fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(15, 15)) + +## Plotting analytical solution +for t in [2, 4, 6, 8, 10]: + ax[0][0].plot( + compute_pressure_and_stresses(t, x, y)[0], + -y_rel, + color=colors[t], + label=f"analytical, t = {t:.1f} s", + ) + +ax[1][0].plot( + compute_pressure_and_stresses(2.5, x, y)[1], + -y_rel, + color=colors[6], + label="analytical, $\\sigma'_{xx}/\\alpha\\tilde{p}$", +) +ax[1][0].plot( + compute_pressure_and_stresses(2.5, x, y)[2], + -y_rel, + color=colors[2], + label="analytical, $\\sigma'_{yy}/\\alpha\\tilde{p}$", +) +ax[1][0].plot( + compute_pressure_and_stresses(2.5, x, y)[3], + -y_rel, + color=colors[4], + label="analytical, $\\sigma'_{xy}/\\alpha\\tilde{p}$", +) + +## Plotting numerical solution +p1 = (x + 1e-6, 0, 0) +p2 = (x + 1e-6, -100, 0) + +for t_num in (2, 2.5, 4, 6, 8, 10): + mesh = read_timestep_mesh(200, t_num) + + line_mesh = slice_along_line(mesh, p1, p2) + pressure = get_pressure_sorted(line_mesh) + sigma = get_stresses_sorted(line_mesh) + depth = get_depth_sorted(line_mesh) + f_abs_pressure = compute_abs_and_rel_pressure_error(pressure, depth, t_num, x)[0] + f_abs_sigma = compute_abs_and_rel_stress_error(sigma, depth, t_num, x)[0] + + if t_num != 2.5: + ax[0][0].plot( + pressure / 0.1e5, + depth / 100, + "o", + markevery=10, + color=colors[t_num], + label=f"numerical, t = {t_num:.1f} s", + ) + ax[0][0].set_xlabel("$p$ / $\\tilde{p}$") + + ax[0][1].plot( + f_abs_pressure, depth / 100, color=colors[t_num], label=f"t = {t_num:.1f} s" + ) + ax[0][1].set_xlabel("$\\Delta p /\\tilde{p}$") + + if t_num == 2.5: + ax[1][0].plot( + sigma[0] / 0.1e5, + depth / 100, + "o", + markevery=10, + color=colors[6], + label="numerical, $\\sigma'_{xx}/\\alpha\\tilde{p}$", + ) + ax[1][0].plot( + sigma[1] / 0.1e5, + depth / 100, + "o", + markevery=10, + color=colors[2], + label="numerical, $\\sigma'_{yy}/\\alpha\\tilde{p}$", + ) + ax[1][0].plot( + sigma[2] / 0.1e5, + depth / 100, + "o", + markevery=10, + color=colors[4], + label="numerical, $\\sigma'_{xy}/\\alpha\\tilde{p}$", + ) + ax[1][0].set_xlabel("$\\sigma$'/$\\alpha\\tilde{p}$") + + ax[1][1].plot( + f_abs_sigma[0], + depth / 100, + color=colors[6], + label="$\\Delta\\sigma'_{xx}/\\alpha\\tilde{p}$", + ) + ax[1][1].plot( + f_abs_sigma[1], + depth / 100, + color=colors[2], + label="$\\Delta\\sigma'_{yy}/\\alpha\\tilde{p}$", + ) + ax[1][1].plot( + f_abs_sigma[2], + depth / 100, + color=colors[4], + label="$\\Delta\\sigma'_{xy}/\\alpha\\tilde{p}$", + ) + ax[1][1].set_xlabel("$\\Delta\\sigma$'/$\\alpha\\tilde{p}$") + + # ax[1][0].plot(sigma[3]/0.1e5, depth/100, "o", markevery=10, color = colors[4], label = "numerical, $\\sigma'_{zz}/\\alpha\\tilde{p}$") + +## layout settings +ax[0][0].set_title("Comparison numerical and analytical solution") +ax[0][1].set_title("Absolute error") + +for idx_1 in (0, 1): + for idx_2 in (0, 1): + ax[idx_1][idx_2].grid(True) + ax[idx_1][idx_2].set_ylabel("$y$ / $L$") + ax[idx_1][0].set_xlim(-1.1, 1.1) + ax[idx_1][idx_2].legend() + + +# %% [markdown] +# ### References +# [1] Verruijt, A. (2016): *Theory and problems of poroelasticity.* Available online at https://geo.verruijt.net/. diff --git a/Tests/Data/Mechanics/CooksMembrane/CooksMembraneBbar.py b/Tests/Data/Mechanics/CooksMembrane/CooksMembraneBbar.py index 9114c2317a047c26cb9d74560872a63ce5e5ccf0..bfdc3875f7f770f610d20b9ba2d6ecf83ac2628e 100644 --- a/Tests/Data/Mechanics/CooksMembrane/CooksMembraneBbar.py +++ b/Tests/Data/Mechanics/CooksMembrane/CooksMembraneBbar.py @@ -273,3 +273,262 @@ for nedge, output_prefix in zip(nedges, output_prefices): # %% [markdown] # The contour plots show that even with the coarsest mesh, the B bar method still gives reasonable stress result. +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Cook's membrane example" +# date = "2024-06-11" +# author = "Wenqing Wang" +# image = "figures/cooks_membrane.png" +# web_subsection = "small-deformations" +# weight = 3 +# +++ + +# %% [markdown] +# $$ +# \newcommand{\B}{\text{B}} +# \newcommand{\F}{\text{F}} +# \newcommand{\I}{\mathbf I} +# \newcommand{\intD}[1]{\int_{\Omega_e}#1\mathrm{d}\Omega} +# $$ +# +# # Cook's membrane example for nearly icompressible solid +# +# ## B bar method +# Considering a strain decomposition: $\mathbf\epsilon = \underbrace{\mathbf\epsilon- \frac{1}{3}(\epsilon:\mathbf I)}_{\text{deviatoric}}\I + \underbrace{\frac{1}{3}(\epsilon:\mathbf I)}_{\text{dilatational}} \I$. +# The idea of the B bar method is to use another quadrature rule to interpolate the dilatational part, which leads to a modified B matrix [1]: +# $$ +# \bar\B = \underbrace{\B - \B^{\text{dil}}}_{\text{original B elements}}+ \underbrace{{\bar\B}^{\text{dil}}}_{\text{by another quadrature rule} } +# $$ +# There are several methods to form ${\bar\B}^{\text{dil}}$ such as selective integration, generalization of the mean-dilatation formulation. In the current OGS, we use the latter, which reads +# $$ +# {\bar\B}^{\text{dil}} = \frac{\intD{\B^{\text{dil}}(\xi)}}{\intD{}} +# $$ +# +# ## Example +# To verify the implementation of the B bar method, the so called Cook's membrane is used as a benchmark. +# Illustrated in the following figure, this example simulates a tapered +# and swept panel of unit thickness. The left edge is clamped and the right edge is applied with a distributed shearing load $F$ = 100 N/mm. The plane strain condition is considered. This numerical model is exactly the same as that is presented in the paper by T. Elguedj et al [1,2]. +# +# <img src="figures/cooks_membrane.png" alt="Cook's membrane" width="320" height="320" /> +# +# ## Reference +# +# [1] T.J.R. Hughes (1980). Generalization of selective integration procedures to anisotropic and nonlinear media. International Journal for Numerical Methods in Engineering, 15(9), 1413-1418. +# +# [2] T. Elguedj, Y. Bazilevs, V.M. Calo, T.J.R. Hughes (2008), +# $\bar\B$ and $\bar\F$ projection methods for nearly incompressible linear and non-linear elasticity and plasticity using higher-order NURBS elements, Computer Methods in Applied Mechanics and Engineering, 197(33--40), 2732-2762. +# + +# %% +import os +from pathlib import Path + +import ogstools as ot + +out_dir = Path(os.environ.get("ot_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +import xml.etree.ElementTree as ET + +import pyvista as pv + + +# %% +def get_last_vtu_file_name(pvd_file_name): + tree = ET.parse(Path(out_dir) / pvd_file_name) + root = tree.getroot() + # Get the last DataSet tag + last_dataset = root.findall(".//DataSet")[-1] + + # Get the 'file' attribute of the last DataSet tag + file_attribute = last_dataset.attrib["file"] + return f"{out_dir}/" + file_attribute + + +def get_top_uy(pvd_file_name): + top_point = (48.0e-3, 60.0e-3, 0) + file_name = get_last_vtu_file_name(pvd_file_name) + mesh = pv.read(file_name) + p_id = mesh.find_closest_point(top_point) + u = mesh.point_data["displacement"][p_id] + return u[1] + + +# %% +def run_single_test(mesh_name, output_prefix, use_bbar="false"): + model = ot.Project( + input_file="CooksMembrane.prj", output_file=f"{out_dir}/modified.prj" + ) + model.replace_text(mesh_name, xpath="./mesh") + model.replace_text(use_bbar, xpath="./processes/process/use_b_bar") + model.replace_text(output_prefix, xpath="./time_loop/output/prefix") + model.replace_text( + "BiCGSTAB", xpath="./linear_solvers/linear_solver/eigen/solver_type" + ) + model.replace_text("ILUT", xpath="./linear_solvers/linear_solver/eigen/precon_type") + vtu_file_name = output_prefix + "_ts_1_t_1.000000.vtu" + model.replace_text(vtu_file_name, xpath="./test_definition/vtkdiff[1]/file") + model.replace_text(vtu_file_name, xpath="./test_definition/vtkdiff[2]/file") + model.replace_text(vtu_file_name, xpath="./test_definition/vtkdiff[3]/file") + + model.write_input() + + # Run OGS + model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir} -m .") + + # Get uy at the top + return get_top_uy(output_prefix + ".pvd") + + +# %% +mesh_names = [ + "mesh.vtu", + "mesh_n10.vtu", + "mesh_n15.vtu", + "mesh_n20.vtu", + "mesh_n25.vtu", + "mesh_n30.vtu", +] +output_prefices_non_bbar = [ + "cooks_membrane_sd_edge_div_4_non_bbar", + "cooks_membrane_sd_refined_mesh_10_non_bbar", + "cooks_membrane_sd_refined_mesh_15_non_bbar", + "cooks_membrane_sd_refined_mesh_20_non_bbar", + "cooks_membrane_sd_refined_mesh_25_non_bbar", + "cooks_membrane_sd_refined_mesh_30_non_bbar", +] + +uys_at_top_non_bbar = [] +for mesh_name, output_prefix in zip(mesh_names, output_prefices_non_bbar): + uy_at_top = run_single_test(mesh_name, output_prefix) + uys_at_top_non_bbar.append(uy_at_top) + +print(uys_at_top_non_bbar) + +# %% +output_prefices = [ + "cooks_membrane_sd_edge_div_4", + "cooks_membrane_sd_refined_mesh_10", + "cooks_membrane_sd_refined_mesh_15", + "cooks_membrane_sd_refined_mesh_20", + "cooks_membrane_sd_refined_mesh_25", + "cooks_membrane_sd_refined_mesh_30", +] + +uys_at_top_bbar = [] +for mesh_name, output_prefix in zip(mesh_names, output_prefices): + uy_at_top = run_single_test(mesh_name, output_prefix, "true") + uys_at_top_bbar.append(uy_at_top) + +print(uys_at_top_bbar) + +# %% +import matplotlib.pyplot as plt +import numpy as np + +ne = [4, 10, 15, 20, 25, 30] + + +def plot_data(ne, u_y_bbar, uy_non_bbar, file_name=""): + # Plotting + plt.rcParams["figure.figsize"] = [5, 5] + + if len(u_y_bbar) != 0: + plt.plot( + ne, np.array(u_y_bbar) * 1e3, marker="o", linestyle="dashed", label="B bar" + ) + if len(uy_non_bbar) != 0: + plt.plot( + ne, + np.array(uy_non_bbar) * 1e3, + marker="x", + linestyle="dashed", + label="non B bar", + ) + + plt.xlabel("Number of elements per side") + plt.ylabel("Top right corner displacement /mm") + plt.legend() + + plt.tight_layout() + if file_name != "": + plt.savefig(file_name) + plt.show() + + +# %% [markdown] +# ## Result +# +# ### Vertical diplacement at the top point +# +# The following figure shows that the convergence of the solutions obtained by using the B bar method follows the one presented in the paper by T. Elguedj et al [1]. However, the results obtained without the B bar method are quit far from the converged solution with the finest mesh. + +# %% +plot_data(ne, uys_at_top_bbar, uys_at_top_non_bbar, "b_bar_linear.png") + +# %% [markdown] +# ### Contour plot + +# %% +import matplotlib.tri as tri +import vtuIO + +nedges = ["4", "10", "15", "20", "25", "30"] + + +def contour_plot(pvd_file_name, title): + file_name = get_last_vtu_file_name(pvd_file_name) + m_plot = vtuIO.VTUIO(file_name, dim=2) + triang = tri.Triangulation(m_plot.points[:, 0], m_plot.points[:, 1]) + triang = tri.Triangulation(m_plot.points[:, 0], m_plot.points[:, 1]) + s_plot = m_plot.get_point_field("sigma") + s_trace = s_plot[:, 0] + s_plot[:, 1] + s_plot[:, 2] + u_plot = m_plot.get_point_field("displacement") + + fig, ax = plt.subplots(ncols=2, figsize=(8, 3)) + ax[0].set_title(title, loc="left", y=1.12) + plt.subplots_adjust(wspace=0.5) + + contour_stress = ax[0].tricontourf(triang, s_trace, cmap="viridis") + contour_displacement = ax[1].tricontourf(triang, u_plot[:, 1], cmap="gist_rainbow") + fig.colorbar(contour_stress, ax=ax[0], label="Stress trace / MPa") + fig.colorbar(contour_displacement, ax=ax[1], label="Dispplacement / m") + fig.tight_layout() + plt.savefig(pvd_file_name + ".png") + plt.show() + + +# %% [markdown] +# #### Results obtained without the B bar method: + +# %% +for nedge, output_prefix in zip(nedges, output_prefices_non_bbar): + contour_plot(output_prefix + ".pvd", "Number of elements per side: " + nedge) + +# %% [markdown] +# #### Results obtained with the B bar method: + +# %% +for nedge, output_prefix in zip(nedges, output_prefices): + contour_plot(output_prefix + ".pvd", "Number of elements per side: " + nedge) + +# %% [markdown] +# The contour plots show that even with the coarsest mesh, the B bar method still gives reasonable stress result. + +# %% diff --git a/Tests/Data/Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole_convergence_analysis.py b/Tests/Data/Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole_convergence_analysis.py new file mode 100644 index 0000000000000000000000000000000000000000..f4398992185deea848400b61a77d7d58a9b3ffd0 --- /dev/null +++ b/Tests/Data/Mechanics/Linear/DiscWithHole/Linear_Disc_with_hole_convergence_analysis.py @@ -0,0 +1,1075 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [markdown] +# +++ +# title = "Linear elasticity: disc with hole convergence study" +# date = "2022-09-15" +# author = "Linda Günther, Sophia Einspänner, Robert Habel, Christoph Lehmann and Thomas Nagel" +# web_subsection = "small-deformations" +# +++ + +# %% [markdown] +# |<div style="width:330px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:330px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| +# + +# %% [markdown] +# # Convergence Analysis: Disc with a hole + +# %% [markdown] +# For many typical engineering problems that need to be solved using numerical techniques, convergence plays a decisive role. It is important to obtain an accurate numerical solution, i.e., to know the mesh refinement level beyond which the results of the numerical analysis do not change significantly, anymore. +# +# In this study the convergence in stress and displacement values for the disc with hole problem are analysed and compared to each other. This study is based on the accomanying Jupyter notebook "Linear_Disc_with_hole.ipynb", which explains the main features of the analytical and numerical solution of the disc with hole problem. For a better access to the solutions used in this document, it is recommended to read through the previously mentioned Jupyter notebook first. + +# %% [markdown] +# For the evaluation of the stress and displacement convergence, we consider seven steps of mesh refinement, described by an index parameter. The following table lists each refinement index with the corresponding edge length, which is considered representative of the cell size. + +# %% [markdown] +# $$ +# \begin{aligned} +# &\begin{array}{cccc} +# \text {Refinement Index} & \text {Cell Size [cm]} \\ +# \hline 8 & 1.429 \\ +# 16 & 0.667 \\ +# 24 & 0.435 \\ +# 40 & 0.256 \\ +# 60 & 0.169 \\ +# 80 & 0.127 \\ +# 240 & 0.042 \\ +# \end{array} +# \end{aligned} +# $$ + +# %% jupyter={"source_hidden": true} +import logging +import matplotlib.pyplot as plt +import numpy as np + +logging.getLogger("matplotlib.font_manager").disabled = True +logging.getLogger("matplotlib.ticker").disabled = True + +# %% +# Some plot settings +# plt.style.use("seaborn-v0_8-deep") +# plt.rcParams["lines.linewidth"] = 2.0 +# plt.rcParams["lines.color"] = "black" +# plt.rcParams["legend.frameon"] = True +# plt.rcParams["font.family"] = "serif" +# plt.rcParams["legend.fontsize"] = 14 +# plt.rcParams["font.size"] = 14 +# plt.rcParams["axes.spines.right"] = False +# plt.rcParams["axes.spines.top"] = False +# plt.rcParams["axes.spines.left"] = True +# plt.rcParams["axes.spines.bottom"] = True +# plt.rcParams["axes.axisbelow"] = True +# plt.rcParams["figure.figsize"] = (8, 6) + + +# %% jupyter={"source_hidden": true} +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + + +# %% jupyter={"source_hidden": true} +# This list contains all parameters for the +# different meshes we create +STUDY_indices = [8, 16, 24, 40, 60, 80, 240] + +# With this parameter the length of one axis of the square plate is defined +STUDY_mesh_size = 20 + + +# %% jupyter={"source_hidden": true} +def read_last_timestep_mesh(study_idx): + # ATTENTION: The finest resolution (240) is rather expensive to simulate. + # Therefore it is tracked in git and might be in a different + # directory. + d = "out" if study_idx == 240 else out_dir + + reader = pv.PVDReader(f"{d}/disc_with_hole_idx_is_{study_idx}.pvd") + reader.set_active_time_point(-1) # go to last timestep + + return reader.read()[0] + + +def slice_along_line(mesh, start_point, end_point): + line = pv.Line(start_point, end_point, resolution=2) + return mesh.slice_along_line(line) + + +def get_sigma_polar_components(mesh): + sig = mesh.point_data["sigma"] + + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + sigs_polar = vec4_to_mat3x3polar(sig, xs, ys) + + sig_rr = sigs_polar[:, 0, 0] + sig_tt = sigs_polar[:, 1, 1] + sig_rt = sigs_polar[:, 0, 1] + + return sig_rr, sig_tt, sig_rt + + +def get_sort_indices_and_distances_by_distance_from_origin_2D(mesh): + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + dist_from_origin = np.hypot(xs, ys) + indices_sorted = np.argsort(dist_from_origin) + dist_sorted = dist_from_origin[indices_sorted] + + return indices_sorted, dist_sorted + + +def compute_abs_and_rel_stress_error_rr(sigmas_rr_num, rs, theta_degree): + num_points = sigmas_rr_num.shape[0] + f_abs_rr = np.zeros(num_points) + f_rel_rr = np.zeros(num_points) + + for pt_idx in range(num_points): + r = rs[pt_idx] + + sigma_rr_ana = kirsch_sig_rr(10, r, theta_degree, 2) + + sigma_rr_num = sigmas_rr_num[pt_idx] * 1000 + + f_abs_rr[pt_idx] = sigma_rr_num - sigma_rr_ana + + if sigma_rr_ana == 0: + f_rel_rr[pt_idx] = f_abs_rr[pt_idx] / 1e-2 + else: + f_rel_rr[pt_idx] = f_abs_rr[pt_idx] / sigma_rr_ana + + return f_abs_rr, f_rel_rr + + +def compute_abs_and_rel_stress_error_rt(sigmas_rt_num, rs, theta_degree): + num_points = sigmas_rt_num.shape[0] + f_abs_rt = np.zeros(num_points) + f_rel_rt = np.zeros(num_points) + + for pt_idx in range(num_points): + r = rs[pt_idx] + + sigma_rt_ana = kirsch_sig_rt(10, r, theta_degree, 2) + sigma_rt_num = sigmas_rt_num[pt_idx] * 1000 + + f_abs_rt[pt_idx] = sigma_rt_num - sigma_rt_ana + + if sigma_rt_ana == 0: + f_rel_rt[pt_idx] = f_abs_rt[pt_idx] / 1e-2 + else: + f_rel_rt[pt_idx] = f_abs_rt[pt_idx] / sigma_rt_ana + + return f_abs_rt, f_rel_rt + + +def compute_abs_and_rel_stress_error_tt(sigmas_tt_num, rs, theta_degree): + num_points = sigmas_tt_num.shape[0] + f_abs_tt = np.zeros(num_points) + f_rel_tt = np.zeros(num_points) + + for pt_idx in range(num_points): + r = rs[pt_idx] + + sigma_tt_ana = kirsch_sig_tt(10, r, theta_degree, 2) + sigma_tt_num = sigmas_tt_num[pt_idx] * 1000 + + f_abs_tt[pt_idx] = sigma_tt_num - sigma_tt_ana + + if sigma_tt_ana == 0: + f_rel_tt[pt_idx] = f_abs_tt[pt_idx] / 1e-2 + else: + f_rel_tt[pt_idx] = f_abs_tt[pt_idx] / sigma_tt_ana + + return f_abs_tt, f_rel_tt + + +def compute_cell_size(_idx, mesh): + pt1 = (19.999, 0, 0) + pt2 = (19.999, 20, 0) + line_mesh = slice_along_line(mesh, pt1, pt2) + number = ( + line_mesh.points.shape[0] - 1 + ) # number of cells along the right edge of the plate + return STUDY_mesh_size / number # height of plate divided by number of cells + + +def resample_mesh_to_240_resolution(idx): + mesh_fine = read_last_timestep_mesh(240) + mesh_coarse = read_last_timestep_mesh(idx) + return mesh_fine.sample(mesh_coarse) + + +# %% [markdown] +# ## Create Gmsh meshes + +# %% [markdown] +# To generate the meshes and to enable the individual parameters to be adjusted from within this notebook the script "mesh_quarter_of_rectangle_with_hole.py" is used. +# For the considered problem the parameter $a$ and $b$ which define the size of the rectangular quarter of the plate are set to a value of $20\, \text{cm}$. The radius of the central hole is controlled by the value of $r$, which is set at $r = 2\, \text{cm}$. +# +# The arguments Nx, Ny, NR and Nr of the script describe the refinement of the mesh and are determined according to each refinement index. +# +# Parameter $R$ describes the area for a further refinement in the vicinity of the hole. This additional refinement is needed for better capturing the stress and strain gradients near the hole. Because the fine resolution of the mesh is only needed in the area around the hole, the size of $R$ is half the size of the plate. + +# %% jupyter={"source_hidden": true} +import mesh_quarter_of_rectangle_with_hole + + +# %% jupyter={"source_hidden": true} +for idx in STUDY_indices: + """ + a and b seem to be the sizes of the rectangular plate, + r the hole radius, R an auxiliary radius + the other parameters control the mesh resolution + please check that, and maybe document in mesh_quarter_of_rectangle_with_hole.py + """ + output_file = f"{out_dir}/disc_with_hole_idx_is_{idx}.msh" + mesh_quarter_of_rectangle_with_hole.run( + output_file, + a=STUDY_mesh_size, + b=STUDY_mesh_size, + r=2, + R=0.5 * STUDY_mesh_size, + Nx=idx, + Ny=idx, + NR=idx, + Nr=idx, + P=1, + ) + + +# %% [markdown] +# ## Transform to VTU meshes suitable for OGS + +# %% jupyter={"source_hidden": true} +from ogstools.msh2vtu import msh2vtu + +for idx in STUDY_indices: + input_file = f"{out_dir}/disc_with_hole_idx_is_{idx}.msh" + msh2vtu_out_dir = Path(f"{out_dir}/disc_with_hole_idx_is_{idx}") + if not msh2vtu_out_dir.exists(): + msh2vtu_out_dir.mkdir(parents=True) + msh2vtu(filename=input_file, output_path=f"{out_dir}/disc_with_hole_idx_is_{idx}", keep_ids=True, reindex=True) + # %cd {out_dir}/disc_with_hole_idx_is_{idx} + ! identifySubdomains -f -m disc_with_hole_idx_is_{idx}_domain.vtu -- disc_with_hole_idx_is_{idx}_physical_group_*.vtu + # %cd - + + +# %% [markdown] +# ## Visualize the meshes + +# %% [markdown] +# To get a better sense of cell sizes and the additional refinement around the hole, the meshes of refinement indices 8 and 80 are shown below. + +# %% jupyter={"source_hidden": true} +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + + +# %% jupyter={"source_hidden": true} +domain_8 = pv.read( + f"{out_dir}/disc_with_hole_idx_is_8/disc_with_hole_idx_is_8_domain.vtu" +) +domain_80 = pv.read( + f"{out_dir}/disc_with_hole_idx_is_80/disc_with_hole_idx_is_80_domain.vtu" +) + +p = pv.Plotter(shape=(1, 2), border=False) +p.subplot(0, 0) +p.add_mesh(domain_8, show_edges=True, show_scalar_bar=False, color=None, scalars=None) +p.view_xy() +p.show_bounds(ticks="outside", xlabel="x / m", ylabel="y / m") +p.camera.zoom(1.3) + +p.subplot(0, 1) +p.add_mesh(domain_80, show_edges=True, show_scalar_bar=False, color=None, scalars=None) +p.view_xy() +p.show_bounds(ticks="outside", xlabel="x / m", ylabel="y / m") +p.camera.zoom(1.3) +p.window_size = [1000, 500] + +p.show() + + +# %% [markdown] +# ## Run OGS + +# %% jupyter={"source_hidden": true} +import shutil + +import ogstools as ot + +# %% jupyter={"source_hidden": true} +# ATTENTION: We exclude the last study index, because its simulation takes +# too long to be included in the OGS CI pipelines. +for idx in STUDY_indices[:-1]: + prj_file = f"disc_with_hole_idx_is_{idx}.prj" + + shutil.copy2(prj_file, out_dir) + + prj_path = out_dir / prj_file + + model = ot.Project(input_file=prj_path, output_file=prj_path) + model.run_model( + logfile=f"{out_dir}/out.txt", + args=f"-o {out_dir} -m {out_dir}/disc_with_hole_idx_is_{idx}", + ) + + +# %% [markdown] +# ## Error Analysis + +# %% [markdown] +# ### Comparison with analytical solution + +# %% [markdown] +# In this section we compare the numerical solution for the different refinement levels to Kirsch's analytical solution. +# We show plots of the radial, tangential and shear stress distribution along the x-axis. +# Additionally, the absolute error of each refinement level to the analytical solution is plotted for better interpretation of variations. +# +# The plots show that–for the considered axis–the stress distribution around the hole converges to Kirsch´s solution with decreasing size of the mesh cells. +# This decrease of the extrapolation errors can also be seen in the plots for the absolute error. +# Especially in the region close to the hole the error values for finer meshes shrink more and more. +# +# Another effect that is particularly visible in the plots of $\sigma_{rr}$ and $\sigma_{\theta\theta}$ along the x-axis is an increasing convergence of the solutions for each refinement step to the solution for the finest mesh at the outer boundaries. +# The results of the numerical solution therefore do not converge against the Kirsch's solution, but against the numerical solution itself for very fine meshes. +# This discrepancy is caused by the finite size of the plate in the numerical solution: Kirsch's analytical solution assumes an infinite plate. +# +# For this reason, an error analysis against the analytical solution does not make sense. Instead, the individual numerical solutions are compared with a "pseudo" analytical solution. For this purpose, a numerical result with extremely fine mesh resolution (refinement index = 240) is generated. + + +# %% jupyter={"source_hidden": true} +def kirsch_sig_rr(sig, r, theta, a): + return ( + 0.5 + * sig + * ( + (1 - a**2 / r**2) + + (1 + 3 * np.power(a, 4) / np.power(r, 4) - 4 * a**2 / r**2) + * np.cos(2 * np.pi * theta / 180) + ) + * np.heaviside(r + 1e-7 - a, 1) + ) + + +def kirsch_sig_tt(sig, r, theta, a): + return ( + 0.5 + * sig + * ( + (1 + a**2 / r**2) + - (1 + 3 * np.power(a, 4) / np.power(r, 4)) + * np.cos(2 * np.pi * theta / 180) + ) + * np.heaviside(r + 1e-7 - a, 1) + ) + + +def kirsch_sig_rt(sig, r, theta, a): + return ( + -0.5 + * sig + * ( + (1 - 3 * np.power(a, 4) / np.power(r, 4) + 2 * a**2 / r**2) + * np.sin(2 * np.pi * theta / 180) + ) + * np.heaviside(r + 1e-7 - a, 1) + ) + + +# %% jupyter={"source_hidden": true} +def vec4_to_mat3x3cart(vec4): + m = np.zeros((3, 3)) + m[0, 0] = vec4[0] + m[1, 1] = vec4[1] + m[2, 2] = vec4[2] + m[0, 1] = vec4[3] + m[1, 0] = vec4[3] + + return np.matrix(m) + + +def vec4_to_mat3x3cart_multi(vec4): + assert vec4.shape[1] == 4 + + n_pts = vec4.shape[0] + + m = np.zeros((n_pts, 3, 3)) + m[:, 0, 0] = vec4[:, 0] + m[:, 1, 1] = vec4[:, 1] + m[:, 2, 2] = vec4[:, 2] + m[:, 0, 1] = vec4[:, 3] + m[:, 1, 0] = vec4[:, 3] + + return m + + +def vec4_to_mat3x3polar_single(vec4, xs, ys): + m_cart = vec4_to_mat3x3cart(vec4) + + theta = np.arctan2(ys, xs) + + rot = np.matrix(np.eye(3)) + rot[0, 0] = np.cos(theta) + rot[0, 1] = -np.sin(theta) + rot[1, 0] = np.sin(theta) + rot[1, 1] = np.cos(theta) + + return rot.T * m_cart * rot + + +def vec4_to_mat3x3polar_multi(vecs4, xs, ys): + """Convert 4-vectors (Kelvin vector in 2D) to 3x3 matrices in polar coordinates at multiple points at once. + + Parameters + ---------- + vecs4: + NumPy array of 4-vectors, dimensions: (N x 4) + xs: + NumPy array of x coordinates, length: N + ys: + NumPy array of y coordinates, length: N + + Returns + ------- + A Numpy array of the symmetric matrices corresponding to the 4-vectors, dimensions: (N x 3 x 3) + """ + + n_pts = vecs4.shape[0] + assert n_pts == xs.shape[0] + assert n_pts == ys.shape[0] + assert vecs4.shape[1] == 4 + + m_carts = vec4_to_mat3x3cart_multi(vecs4) # vecs4 converted to symmetric matrices + + thetas = np.arctan2(ys, xs) + + rots = np.zeros((n_pts, 3, 3)) # rotation matrices at each point + rots[:, 0, 0] = np.cos(thetas) + rots[:, 0, 1] = -np.sin(thetas) + rots[:, 1, 0] = np.sin(thetas) + rots[:, 1, 1] = np.cos(thetas) + rots[:, 2, 2] = 1 + + # rot.T * m_cart * rot for each point + m_polars = np.einsum("...ji,...jk,...kl", rots, m_carts, rots) + + assert m_polars.shape[0] == n_pts + assert m_polars.shape[1] == 3 + assert m_polars.shape[2] == 3 + + return m_polars + + +def vec4_to_mat3x3polar(vec4, xs, ys): + if len(vec4.shape) == 1: + # only a single 4-vector will be converted + return vec4_to_mat3x3polar_single(vec4, xs, ys) + return vec4_to_mat3x3polar_multi(vec4, xs, ys) + + +# %% jupyter={"source_hidden": true} +# Here we'll collect all simulation results +# accessible via their idx value +# We'll be able to reuse/read them many times +STUDY_num_result_meshes_by_index = {} + + +def read_simulation_result_meshes(): + for idx in STUDY_indices: + mesh = read_last_timestep_mesh(idx) + STUDY_num_result_meshes_by_index[idx] = mesh + + +read_simulation_result_meshes() + + +# %% jupyter={"source_hidden": true} +STUDY_num_result_xaxis_meshes_by_index = {} + + +def compute_xaxis_meshes(): + for idx in STUDY_indices: + mesh = STUDY_num_result_meshes_by_index[idx] + pt1 = (0, 1e-6, 0) + pt2 = (10, 1e-6, 0) + line_mesh = slice_along_line(mesh, pt1, pt2) + + STUDY_num_result_xaxis_meshes_by_index[idx] = line_mesh + + +compute_xaxis_meshes() + + +# %% jupyter={"source_hidden": true} +STUDY_num_result_yaxis_meshes_by_index = {} + + +def compute_yaxis_meshes(): + for idx in STUDY_indices: + mesh = STUDY_num_result_meshes_by_index[idx] + pt1 = (1e-6, 0, 0) + pt2 = (1e-6, 10, 0) + line_mesh = slice_along_line(mesh, pt1, pt2) + + STUDY_num_result_yaxis_meshes_by_index[idx] = line_mesh + + +compute_yaxis_meshes() + + +# %% jupyter={"source_hidden": true} +STUDY_num_result_diagonal_meshes_by_index = {} + + +def compute_diagonal_meshes(): + for idx in STUDY_indices: + mesh = STUDY_num_result_meshes_by_index[idx] + pt1 = (1e-6, 1e-6, 0) + pt2 = (28.28, 28.28, 0) + line_mesh = slice_along_line(mesh, pt1, pt2) + + STUDY_num_result_diagonal_meshes_by_index[idx] = line_mesh + + +compute_diagonal_meshes() + + +# %% [markdown] +# ### Stress distribution along the x-axis + + +# %% jupyter={"source_hidden": true} +def plot_stress_distribution_along_xaxis(): + ### Step 1: Compute data ########################################## + + # These variables will hold the error data for all STUDY_indices + f_abs_rr = {} + f_abs_tt = {} + f_abs_rt = {} + + # Plot setup + fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(22, 10)) + for i in range(2): + for j in range(3): + ax[i][j].axvline(2, color="0.6", linestyle="--", label="Hole Radius") + ax[i][j].grid(True) + ax[i][j].set(xlim=(0, STUDY_mesh_size)) + ax[i][j].set_xlabel("$r$ / cm") + ax[i][1].set_ylabel("$\\Delta\\sigma$ / kPa") + ax[i][2].set_ylabel("$\\Delta\\sigma$ / $\\sigma_{\\mathrm{analytical}}$") + + for iteration, idx in enumerate(STUDY_indices): + # we use the line mesh we extracted before + line_mesh = STUDY_num_result_xaxis_meshes_by_index[idx] + + sig_rr, sig_tt, sig_rt = get_sigma_polar_components(line_mesh) + + ( + indices_sorted, + dist_sorted, + ) = get_sort_indices_and_distances_by_distance_from_origin_2D(line_mesh) + + # sort sigma by distance from origin + sig_rr_sorted = sig_rr[indices_sorted] + sig_tt_sorted = sig_tt[indices_sorted] + sig_rt_sorted = sig_rt[indices_sorted] + + # compute errors + f_abs_rr, f_rel_rr = compute_abs_and_rel_stress_error_rr( + sig_rr_sorted, dist_sorted, -90 + ) + f_abs_tt, f_rel_tt = compute_abs_and_rel_stress_error_tt( + sig_tt_sorted, dist_sorted, -90 + ) + f_abs_rt, f_rel_rt = compute_abs_and_rel_stress_error_rt( + sig_rt_sorted, dist_sorted, -90 + ) + + ### Step 2: Plot data ############################################## + + ax[0][0].set_ylabel("$\\sigma_{rr}$ / kPa") + ax[0][1].set_ylabel("$\\sigma_{\\theta\\theta}$ / kPa") + ax[0][2].set_ylabel("$\\sigma_{r\\theta}$ / kPa") + + # analytical results + if iteration == 0: + r = np.linspace(2, STUDY_mesh_size, 1000) + ax[0][0].plot( + r, + kirsch_sig_rr(10, r, -90, 2), + color="deepskyblue", + linestyle=":", + label="analytical", + ) + ax[0][1].plot( + r, + kirsch_sig_tt(10, r, -90, 2), + color="yellowgreen", + linestyle=":", + label="analytical", + ) + ax[0][2].plot( + r, + kirsch_sig_rt(10, r, -90, 2), + color="orangered", + linestyle=":", + label="analytical", + ) + + # numerical results + cell_size = compute_cell_size(idx, STUDY_num_result_meshes_by_index[idx]) + + if idx == 8: + ax[0][0].plot( + dist_sorted, + sig_rr_sorted * 1000, + color="lightskyblue", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][1].plot( + dist_sorted, + sig_tt_sorted * 1000, + color="limegreen", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][2].plot( + dist_sorted, + sig_rt_sorted * 1000, + color="lightcoral", + label=f"h = {cell_size:.3f} cm", + ) + ax[1][0].plot(dist_sorted, f_abs_rr, color="lightskyblue") + ax[1][1].plot(dist_sorted, f_abs_tt, color="limegreen") + ax[1][2].plot(dist_sorted, f_abs_rt, color="lightcoral") + + if idx == 16: + ax[0][0].plot( + dist_sorted, + sig_rr_sorted * 1000, + color="cornflowerblue", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][1].plot( + dist_sorted, + sig_tt_sorted * 1000, + color="forestgreen", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][2].plot( + dist_sorted, + sig_rt_sorted * 1000, + color="firebrick", + label=f"h = {cell_size:.3f} cm", + ) + ax[1][0].plot(dist_sorted, f_abs_rr, color="cornflowerblue") + ax[1][1].plot(dist_sorted, f_abs_tt, color="forestgreen") + ax[1][2].plot(dist_sorted, f_abs_rt, color="firebrick") + + if idx == 24: + ax[0][0].plot( + dist_sorted, + sig_rr_sorted * 1000, + color="royalblue", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][1].plot( + dist_sorted, + sig_tt_sorted * 1000, + color="darkgreen", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][2].plot( + dist_sorted, + sig_rt_sorted * 1000, + color="darkred", + label=f"h = {cell_size:.3f} cm", + ) + ax[1][0].plot(dist_sorted, f_abs_rr, color="royalblue") + ax[1][1].plot(dist_sorted, f_abs_tt, color="darkgreen") + ax[1][2].plot(dist_sorted, f_abs_rt, color="darkred") + + if idx == 240: + ax[0][0].plot( + dist_sorted, + sig_rr_sorted * 1000, + color="black", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][1].plot( + dist_sorted, + sig_tt_sorted * 1000, + color="black", + label=f"h = {cell_size:.3f} cm", + ) + ax[0][2].plot( + dist_sorted, + sig_rt_sorted * 1000, + color="black", + label=f"h = {cell_size:.3f} cm", + ) + + # final plot settings + for i in range(3): + ax[0][i].legend() + + ax[0][0].set_title("Radial stress distribution") + ax[0][1].set_title("Tangential stress distribution") + ax[0][2].set_title("Shear stress distribution") + + fig.tight_layout() + + +plot_stress_distribution_along_xaxis() + + +# %% [markdown] +# ## Comparison with "pseudo" analytical solution + +# %% [markdown] +# As described before the numerical solutions for each refinement step are compared to the solution for the finest refinement index of 240 in the following section. Although the approximation is already clear from the previous plots of the stress curves along the x axis, mathematical norms are used hereafter for a better representation and interpretation of the relationship. +# In particular, these norms reveal how fast the solution converges to the pseudo analytical one. + +# %% [markdown] +# ### Norm Plots + +# %% [markdown] +# To quantify the total error of a numerical calculation, various error norms can be used: + +# %% [markdown] +# \begin{align} +# \ell_{2}=|w|=\sqrt{\sum_{i=0}^N (w_{i})^2} +# \\ +# RMS =\sqrt{\frac{\sum_{i=0}^N (w_{i})^2}{N}} +# \\ +# L_{2}=||w||=\sqrt{\int_{\Omega} (w(x))^2 dx} +# \end{align} + +# %% [markdown] +# The $\ell_{2}$ norm or Euclidean norm is the square root of the sum of the squared absolute errors at each point of the mesh. +# +# The root mean square ($RMS$) is calculated similarly. Here, however, the influence of the number of points is taken into account by dividing by the square root of the number of points. +# +# The $L_{2}$ norm as integral norm represents a generalization of the $\ell_{2}$ norm for continuous functions. While the Euclidean norm only considers the values on individual mesh nodes, the integral norm considers the solution on the entire mesh. Therefore an advantage of the $L_{2}$ norm is, that big elements are considered with a higher impact than small ones, which produces more homogeneous results. + +# %% [markdown] +# The following plots represent the development of the Euclidean and Integral norm and $RMS$ for the refinement of the mesh. How fast the considered element converge is expressed by the slope of the lines in the plot. +# First the detailed discussed error norms for the stresses are visualised and in addition to them the norms for the associated displacements to draw conclusions about the quality of convergence. +# +# The main conclusion that can be drawn is that the solution for the displacements converge significantly faster than those for the stresses. +# As a practical consequence, it might be possible to get a sufficiently accurate displacement solution already on a relatively coarse mesh, +# whereas for an accurate stress solution a much finer mesh might be necessary. + +# %% jupyter={"source_hidden": true} +from vtkmodules.vtkFiltersParallel import vtkIntegrateAttributes + + +# %% jupyter={"source_hidden": true} +def integrate_mesh_attributes(mesh): + integrator = vtkIntegrateAttributes() + integrator.SetInputData(mesh) + integrator.Update() + return pv.wrap( + integrator.GetOutputDataObject(0) + ) # that is an entire mesh with one point and one cell + + +# %% jupyter={"source_hidden": true} +def compute_ell_2_norm_sigma(_idx, sigmas_test, sigmas_reference): + sig_rr, sig_tt, sig_rt = sigmas_test + sig_rr_240, sig_tt_240, sig_rt_240 = sigmas_reference + + list_rr = (sig_rr_240 - sig_rr) ** 2 + list_tt = (sig_tt_240 - sig_tt) ** 2 + list_rt = (sig_rt_240 - sig_rt) ** 2 + + l2_rr = np.sqrt(sum(list_rr)) + l2_tt = np.sqrt(sum(list_tt)) + l2_rt = np.sqrt(sum(list_rt)) + + return l2_rr, l2_tt, l2_rt + + +# %% jupyter={"source_hidden": true} +def compute_ell_2_norm_displacement(_idx, mesh_resampled_to_240_resolution, mesh_fine): + dis = mesh_resampled_to_240_resolution.point_data["displacement"] + dis_x = dis[:, 0] + dis_y = dis[:, 1] + + dis_240 = mesh_fine.point_data["displacement"] + dis_x_240 = dis_240[:, 0] + dis_y_240 = dis_240[:, 1] + + list_x = (dis_x_240 - dis_x) ** 2 + list_y = (dis_y_240 - dis_y) ** 2 + + l2_x = np.sqrt(sum(list_x)) + l2_y = np.sqrt(sum(list_y)) + + return l2_x, l2_y + + +# %% jupyter={"source_hidden": true} +def compute_root_mean_square_sigma(_idx, sigmas_test, sigmas_reference): + sig_rr, sig_tt, sig_rt = sigmas_test + sig_rr_240, sig_tt_240, sig_rt_240 = sigmas_reference + + l2_rr = np.linalg.norm(sig_rr_240 - sig_rr) + l2_tt = np.linalg.norm(sig_tt_240 - sig_tt) + l2_rt = np.linalg.norm(sig_rt_240 - sig_rt) + + points = sig_rr.shape[0] + return l2_rr / np.sqrt(points), l2_tt / np.sqrt(points), l2_rt / np.sqrt(points) + + +# %% jupyter={"source_hidden": true} +def compute_root_mean_square_displacement( + _idx, mesh_resampled_to_240_resolution, mesh_fine +): + points = mesh_resampled_to_240_resolution.point_data["sigma"].shape[0] + + dis = mesh_resampled_to_240_resolution.point_data["displacement"] + dis_x = dis[:, 0] + dis_y = dis[:, 1] + + dis_240 = mesh_fine.point_data["displacement"] + dis_x_240 = dis_240[:, 0] + dis_y_240 = dis_240[:, 1] + + l2_x = np.linalg.norm(dis_x_240 - dis_x) + l2_y = np.linalg.norm(dis_y_240 - dis_y) + + return l2_x / np.sqrt(points), l2_y / np.sqrt(points) + + +# %% jupyter={"source_hidden": true} +def compute_Ell_2_norm_sigma( + _idx, mesh_resampled_to_240_resolution, sigmas_test, sigmas_reference +): + sig_rr, sig_tt, sig_rt = sigmas_test + sig_rr_240, sig_tt_240, sig_rt_240 = sigmas_reference + + list_rr = (sig_rr_240 - sig_rr) ** 2 + list_tt = (sig_tt_240 - sig_tt) ** 2 + list_rt = (sig_rt_240 - sig_rt) ** 2 + + # We add the squared differences as new point data to the mesh + mesh_resampled_to_240_resolution.point_data["diff_rr_squared"] = list_rr + mesh_resampled_to_240_resolution.point_data["diff_tt_squared"] = list_tt + mesh_resampled_to_240_resolution.point_data["diff_rt_squared"] = list_rt + + # this will integrate all fields at once, so you can add the tt and rt components above and call this only once. + integration_result_mesh = integrate_mesh_attributes( + mesh_resampled_to_240_resolution + ) + + # new: integral norm, the index [0] accesses the data of the single point contained in the mesh + L2_rr = np.sqrt(integration_result_mesh.point_data["diff_rr_squared"][0]) + L2_tt = np.sqrt(integration_result_mesh.point_data["diff_tt_squared"][0]) + L2_rt = np.sqrt(integration_result_mesh.point_data["diff_rt_squared"][0]) + + return L2_rr, L2_tt, L2_rt + + +# %% jupyter={"source_hidden": true} +def compute_Ell_2_norm_displacement(_idx, mesh_resampled_to_240_resolution, mesh_fine): + dis = mesh_resampled_to_240_resolution.point_data["displacement"] + dis_x = dis[:, 0] + dis_y = dis[:, 1] + + dis_240 = mesh_fine.point_data["displacement"] + dis_x_240 = dis_240[:, 0] + dis_y_240 = dis_240[:, 1] + + list_x = (dis_x_240 - dis_x) ** 2 + list_y = (dis_y_240 - dis_y) ** 2 + + # We add the squared differences as new point data to the mesh + mesh_resampled_to_240_resolution.point_data["diff_x_squared"] = list_x + mesh_resampled_to_240_resolution.point_data["diff_y_squared"] = list_y + + # this will integrate all fields at once, so you can add the tt and rt components above and call this only once. + integration_result_mesh = integrate_mesh_attributes( + mesh_resampled_to_240_resolution + ) + + # new: integral norm, the index [0] accesses the data of the single point contained in the mesh + L2_x = np.sqrt(integration_result_mesh.point_data["diff_x_squared"][0]) + L2_y = np.sqrt(integration_result_mesh.point_data["diff_y_squared"][0]) + + return L2_x, L2_y + + +# %% jupyter={"source_hidden": true} +# empty dictionaries +size = {} +l2_rr = {} +l2_tt = {} +l2_rt = {} +rms_rr = {} +rms_tt = {} +rms_rt = {} +L2_rr = {} +L2_tt = {} +L2_rt = {} +l2_x = {} +l2_y = {} +rms_x = {} +rms_y = {} +L2_x = {} +L2_y = {} + + +def compute_error_norms(): + mesh_fine = STUDY_num_result_meshes_by_index[240] + sigmas_reference = get_sigma_polar_components(mesh_fine) + + for idx in STUDY_indices: + if idx != 240: # 240 is the "pseudo" analytical solution we compare against. + mesh_coarse = STUDY_num_result_meshes_by_index[idx] + mesh_resampled_to_240_resolution = mesh_fine.sample(mesh_coarse) + sigmas_test = get_sigma_polar_components(mesh_resampled_to_240_resolution) + + l2_rr[idx], l2_tt[idx], l2_rt[idx] = compute_ell_2_norm_sigma( + idx, sigmas_test, sigmas_reference + ) + rms_rr[idx], rms_tt[idx], rms_rt[idx] = compute_root_mean_square_sigma( + idx, sigmas_test, sigmas_reference + ) + L2_rr[idx], L2_tt[idx], L2_rt[idx] = compute_Ell_2_norm_sigma( + idx, mesh_resampled_to_240_resolution, sigmas_test, sigmas_reference + ) + + l2_x[idx], l2_y[idx] = compute_ell_2_norm_displacement( + idx, mesh_resampled_to_240_resolution, mesh_fine + ) + rms_x[idx], rms_y[idx] = compute_root_mean_square_displacement( + idx, mesh_resampled_to_240_resolution, mesh_fine + ) + L2_x[idx], L2_y[idx] = compute_Ell_2_norm_displacement( + idx, mesh_resampled_to_240_resolution, mesh_fine + ) + size[idx] = compute_cell_size(idx, mesh_coarse) + + +compute_error_norms() + + +# %% jupyter={"source_hidden": true} +def plot_slope_sketch(ax, x0, y0, slopes, xmax=None): + """Plot sketch for slopes. All slopes cross at (x0, y0)""" + if xmax is None: + xmax = 2 * x0 + xs = np.linspace(x0, xmax, 20) + + for slope in slopes: + y_ = xs[0] ** slope + ys = y0 / y_ * xs**slope + ax.plot(xs, ys, color="black") + ax.text(xs[-1] * 1.05, ys[-1], slope) + + +# %% jupyter={"source_hidden": true} +h = np.linspace(size[80], size[8], 1000) +k = np.linspace(1, 2, 20) + +fig, ax = plt.subplots(ncols=3, figsize=(20, 5)) + +ax[0].plot( + size.values(), + l2_rr.values(), + color="firebrick", + linestyle=":", + label=r"$\ell_{2, rr}$", +) +ax[0].plot( + size.values(), + l2_tt.values(), + color="firebrick", + linestyle="--", + label="$\\ell_{2, \\theta\\theta}$", +) +ax[0].plot( + size.values(), l2_rt.values(), color="firebrick", label="$\\ell_{2, r\\theta}$" +) +ax[0].plot( + size.values(), + l2_x.values(), + color="royalblue", + linestyle="--", + label=r"$\ell_{2, x}$", +) +ax[0].plot(size.values(), l2_y.values(), color="royalblue", label=r"$\ell_{2, y}$") + +plot_slope_sketch(ax[0], 1.5e-1, 5e-2, [1, 2, 3], xmax=2.5e-1) + +ax[0].set_title(r"$\ell_2$ norms") +ax[0].set_ylabel(r"$\ell_2$ / kPa or cm") + +ax[1].plot( + size.values(), rms_rr.values(), color="firebrick", linestyle=":", label="$RMS_{rr}$" +) +ax[1].plot( + size.values(), + rms_tt.values(), + color="firebrick", + linestyle="--", + label="$RMS_{\\theta\\theta}$", +) +ax[1].plot(size.values(), rms_rt.values(), color="firebrick", label="$RMS_{r\\theta}$") +ax[1].plot( + size.values(), rms_x.values(), color="royalblue", linestyle="--", label="$RMS_{x}$" +) +ax[1].plot(size.values(), rms_y.values(), color="royalblue", label="$RMS_{y}$") + +plot_slope_sketch(ax[1], 1.5e-1, 1e-4, [1, 2, 3], xmax=2.5e-1) + +ax[1].set_title("Root-mean-square") +ax[1].set_ylabel("RMS / kPa or cm") + +ax[2].plot( + size.values(), L2_rr.values(), color="firebrick", linestyle=":", label="$L_{2, rr}$" +) +ax[2].plot( + size.values(), + L2_tt.values(), + color="firebrick", + linestyle="--", + label="$L_{2, \\theta\\theta}$", +) +ax[2].plot(size.values(), L2_rt.values(), color="firebrick", label="$L_{2, r\\theta}$") +ax[2].plot( + size.values(), L2_x.values(), color="royalblue", linestyle="--", label="$L_{2, x}$" +) +ax[2].plot(size.values(), L2_y.values(), color="royalblue", label="$L_{2, y}$") + +plot_slope_sketch(ax[2], 1.5e-1, 1e-3, [1, 2, 3], xmax=2.5e-1) + +ax[2].set_title("$L_2$ norms (integral norms)") +ax[2].set_ylabel("$L_2$ /kPa or cm") +for i in range(3): + ax[i].legend() + ax[i].set_xlabel("h / cm") + ax[i].loglog(base=10) + + +# %% jupyter={"source_hidden": true} diff --git a/Tests/Data/Mechanics/Linear/SimpleMechanics.py b/Tests/Data/Mechanics/Linear/SimpleMechanics.py new file mode 100644 index 0000000000000000000000000000000000000000..77469d7bb46e5d325fddf4f19f003a3123c8ca7f --- /dev/null +++ b/Tests/Data/Mechanics/Linear/SimpleMechanics.py @@ -0,0 +1,232 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "SimpleMechanics" +# date = "2021-09-10" +# author = "Lars Bilke, Jörg Buchwald" +# web_subsection = "small-deformations" +# +++ +# + +# %% [markdown] +# The following example consists of a simple mechanics problem. + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + + +# %% +import ogstools as ot + +prj_name = "SimpleMechanics" +model = ot.Project(output_file=(out_dir / f"{prj_name}.prj")) +model.geometry.add_geometry(filename="./square_1x1.gml") +model.mesh.add_mesh(filename="./square_1x1_quad_1e2.vtu") +model.processes.set_process( + name="SD", + type="SMALL_DEFORMATION", + integration_order="2", + specific_body_force="0 0", +) +model.processes.set_constitutive_relation( + type="LinearElasticIsotropic", youngs_modulus="E", poissons_ratio="nu" +) +model.processes.add_process_variable( + process_variable="process_variable", process_variable_name="displacement" +) +model.processes.add_secondary_variable(internal_name="sigma", output_name="sigma") +model.time_loop.add_process( + process="SD", + nonlinear_solver_name="basic_newton", + convergence_type="DeltaX", + norm_type="NORM2", + abstol="1e-15", + time_discretization="BackwardEuler", +) +model.time_loop.set_stepping( + process="SD", + type="FixedTimeStepping", + t_initial="0", + t_end="1", + repeat="4", + delta_t="0.25", +) +model.time_loop.add_output( + type="VTK", + prefix=prj_name, + repeat="1", + each_steps="4", + variables=["displacement", "sigma"], +) +model.media.add_property( + medium_id="0", phase_type="Solid", name="density", type="Constant", value="1" +) +model.parameters.add_parameter(name="E", type="Constant", value="1") +model.parameters.add_parameter(name="nu", type="Constant", value="0.3") +model.parameters.add_parameter(name="displacement0", type="Constant", values="0 0") +model.parameters.add_parameter(name="dirichlet0", type="Constant", value="0") +model.parameters.add_parameter(name="dirichlet1", type="Constant", value="0.05") +model.process_variables.set_ic( + process_variable_name="displacement", + components="2", + order="1", + initial_condition="displacement0", +) +model.process_variables.add_bc( + process_variable_name="displacement", + geometrical_set="square_1x1_geometry", + geometry="left", + type="Dirichlet", + component="0", + parameter="dirichlet0", +) +model.process_variables.add_bc( + process_variable_name="displacement", + geometrical_set="square_1x1_geometry", + geometry="bottom", + type="Dirichlet", + component="1", + parameter="dirichlet0", +) +model.process_variables.add_bc( + process_variable_name="displacement", + geometrical_set="square_1x1_geometry", + geometry="top", + type="Dirichlet", + component="1", + parameter="dirichlet1", +) +model.nonlinear_solvers.add_non_lin_solver( + name="basic_newton", + type="Newton", + max_iter="4", + linear_solver="general_linear_solver", +) +model.linear_solvers.add_lin_solver( + name="general_linear_solver", + kind="lis", + solver_type="cg", + precon_type="jacobi", + max_iteration_step="10000", + error_tolerance="1e-16", +) +model.linear_solvers.add_lin_solver( + name="general_linear_solver", + kind="eigen", + solver_type="CG", + precon_type="DIAGONAL", + max_iteration_step="10000", + error_tolerance="1e-16", +) +model.linear_solvers.add_lin_solver( + name="general_linear_solver", + kind="petsc", + prefix="sd", + solver_type="cg", + precon_type="bjacobi", + max_iteration_step="10000", + error_tolerance="1e-16", +) +try: + model.write_input() + model.run_model( + logfile=(out_dir / f"{prj_name}-out.txt"), args=f"-o {out_dir} -m ." + ) +except Exception as inst: + print(f"{type(inst)}: {inst.args[0]}") + +from datetime import datetime + +print(datetime.now()) + + +# %% +import vtuIO + +pvdfile = vtuIO.PVDIO(f"{out_dir}/{prj_name}.pvd", interpolation_backend="scipy", dim=2) +time = pvdfile.timesteps +points = {"pt0": (0.3, 0.5, 0.0), "pt1": (0.24, 0.21, 0.0)} +displacement_linear = pvdfile.read_time_series( + "displacement", points, interpolation_method="linear" +) +displacement_nearest = pvdfile.read_time_series( + "displacement", points, interpolation_method="nearest" +) + +import matplotlib.pyplot as plt + +plt.plot( + time, displacement_linear["pt0"][:, 0], "b-", label="$u_x$ pt0 linear interpolated" +) +plt.plot( + time, + displacement_nearest["pt0"][:, 0], + "b--", + label="$u_x$ pt0 closest point value", +) +plt.plot( + time, displacement_linear["pt0"][:, 1], "g-", label="$u_y$ pt0 linear interpolated" +) +plt.plot( + time, + displacement_nearest["pt0"][:, 1], + "g--", + label="$u_y$ pt0 closest point value", +) +plt.plot( + time, displacement_linear["pt1"][:, 0], "r-", label="$u_x$ pt1 linear interpolated" +) +plt.plot( + time, + displacement_nearest["pt1"][:, 0], + "r--", + label="$u_x$ pt1 closest point value", +) +plt.plot( + time, displacement_linear["pt1"][:, 1], "m-", label="$u_y$ pt1 linear interpolated" +) +plt.plot( + time, + displacement_nearest["pt1"][:, 1], + "m--", + label="$u_y$ pt1 closest point value", +) +plt.legend() +plt.xlabel("t") +plt.ylabel("u") + + +# %% +import matplotlib.pyplot as plt +import numpy as np + +# %% +mesh_series = ot.MeshSeries(f"{out_dir}/{prj_name}.pvd") +points = np.asarray([[0.3, 0.5, 0.0], [0.24, 0.21, 0.0]]) +disp = ot.variables.displacement +labels = [ + f"{i}: {label}" for i, label in enumerate(ot.plot.utils.justified_labels(points)) +] +fig = mesh_series.plot_probe( + points=points[:4], variable=disp, time_unit="a", labels=labels[:4] +) + +# %% diff --git a/Tests/Data/Mechanics/PLLC/PLLC.py b/Tests/Data/Mechanics/PLLC/PLLC.py new file mode 100644 index 0000000000000000000000000000000000000000..3f1d8e589ce5471dc2f7a235227b70e23b5ad408 --- /dev/null +++ b/Tests/Data/Mechanics/PLLC/PLLC.py @@ -0,0 +1,293 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Power Law Linear Creep" +# date = "2023-01-02" +# author = "Florian Zill" +# web_subsection = "small-deformations" +# +++ +# + +# %% [markdown] +# ### Power Law Linear Creep +# +# This benchmark shows the increased creep rate of salt rock at lower deviatoric stress. A two component power law (which we call Power Law Linear Creep, or short PLLC) provides an easy way to capture the power law behaviour (dislocation creep) and the linear behaviour (pressure solution creep). For more details have a look at (Zill et al., 2022). +# + +# %% +import contextlib +import os +from pathlib import Path + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +import vtuIO + + +# %% +prj_name = "uniax_compression" +data_dir = os.environ.get( + "OGS_DATA_DIR", str(str(Path.cwd())).split("/Data/")[0] + "/Data/" +) +input_file = f"{data_dir}/Mechanics/PLLC/{prj_name}.prj" + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +os.chdir(out_dir) + +prj_file = f"{out_dir}/{prj_name}_out.prj" +ogs_model = ot.Project(input_file=input_file, output_file=prj_file) + +# %% [markdown] +# ### Experimental data +# +# A nice overview for the strain rates of salt for different temperatures and differential stresses can be found in (Li et al., 2021). + +# %% +# Unfortunately the source for the WIPP data has gone missing - will be added if it's found again +ExData = { + "WIPP CS 25": ( + 25, + "^", + [ + [9.87970002, 2.013560846e-05], + [11.84642707, 3.178356756e-05], + [7.87388785, 1.66059726e-06], + ], + ), + "WIPP CS 60": ( + 60, + "^", + [ + [3.98589289, 5.7824853e-06], + [5.94266985, 2.075776623e-05], + [7.87388785, 1.953209818e-05], + [9.96978837, 5.841438703e-05], + [11.84642707, 0.00011762092257], + [13.94911482, 0.00026749321794], + [17.9857158, 0.00111804208073], + [1.9814251, 8.7645834e-07], + [3.91418422, 4.01350889e-06], + [5.88897108, 3.34371363e-06], + [7.87388785, 1.129440706e-05], + [9.87970002, 2.99068674e-05], + [11.84642707, 7.681792203e-05], + [13.82306874, 0.00011067584933], + [15.83934389, 0.00052247037957], + ], + ), + "DeVries 1988 25": ( + 25, + "s", + [ + [4.99, 2.10816e-06], + [4.99, 2.4192e-06], + [5, 1.8144e-06], + [9.99, 2.2032e-05], + [14.96, 9.2448e-05], + [14.98, 0.000216], + ], + ), + "DeVries 1988 100": ( + 100, + "s", + [ + [4.95, 9.6768e-05], + [6.77, 0.000292896], + [7.46, 0.000324], + [8.55, 0.000664416], + [8.92, 0.00091584], + [8.98, 0.0009936], + [9.91, 0.00124416], + [10.1, 0.00139968], + [10.22, 0.00093312], + [10.27, 0.00132192], + [12.1, 0.00216], + [12.3, 0.00409536], + [12.35, 0.00320544], + [12.37, 0.00292032], + [12.39, 0.00253152], + [12.4, 0.0026784], + [12.46, 0.0025056], + [12.49, 0.00347328], + [13.57, 0.00273024], + [13.78, 0.00242784], + [14.7, 0.00482112], + [16.87, 0.0095904], + [17.2, 0.0123552], + [19.96, 0.030672], + ], + ), + "DeVries 1988 200": ( + 200, + "s", + [ + [3.47, 0.00117504], + [4.71, 0.0032832], + [6.67, 0.0104544], + [6.78, 0.0132192], + [9.86, 0.214272], + ], + ), + "Berest 2015 14.3": ( + 14.3, + "P", + [ + [0.09909639, 8.944207e-08], + [0.19575886, 1.4118213e-07], + [0.29452325, 1.4118213e-07], + [0.49411031, 9.799173e-08], + ], + ), + "Berest 2017 7.8": ( + 7.8, + "P", + [ + [0.19575886, 2.2285256e-07], + [0.19575886, 9.505469e-08], + [0.19754389, 2.5947583e-07], + [0.19754389, 2.647936e-08], + [0.39379426, 4.9162047e-07], + [0.39738509, 6.801413e-08], + [0.59247161, 4.0957628e-07], + [0.59247161, 5.7241269e-07], + [0.59787408, 1.0735864e-07], + [1.0591736, 1.11804208e-06], + ], + ), +} + + +# %% [markdown] +# ### Parameters +# +# This set of parameters gives a good fit with the experimental data. The grain size is a bit larger than the usual grain size of roughly 1 cm. + +# %% +A1 = 0.18 # d^-1 +Q1 = 54e3 # kJ / mol +A2 = 6.5e-5 # m^3 K d^−1 # noqa: RUF003 +Q2 = 24.5e3 # kJ / mol +dGrain = 5e-2 # m +sref = 1.0 # MPa + + +def BGRa(sig, T): + return A1 * np.exp(-Q1 / (8.3145 * (273.15 + T))) * np.power(sig / sref, 5.0) + + +def PLLC(sig, T): + return A1 * np.exp(-Q1 / (8.3145 * (273.15 + T))) * np.power( + sig / sref, 5.0 + ) + A2 * np.exp(-Q2 / (8.3145 * (273.15 + T))) * sig / sref / np.power( + dGrain, 3 + ) / ( + 273.15 + T + ) + + +# %% [markdown] +# ### Simulation and plot +# +# The experimental data is compared against the model results (analytically and numerically) + +# %% +lo_stresses = np.array([0.2e6, 0.6e6]) +hi_stresses = np.array([2e6, 10e6]) +Exps = { + 7.8: ("blue", lo_stresses), + 14.3: ("orange", lo_stresses), + 25: ("lime", hi_stresses), + 60: ("red", hi_stresses), + 100: ("gray", hi_stresses), + 200: ("mediumpurple", hi_stresses), +} + +fig, ax = plt.subplots(1, 1, figsize=(8, 6)) +ax.set_xlabel("$\\sigma_\\mathrm{ax}$ / MPa") +ax.set_ylabel("$\\dot{\\epsilon}_{zz}$ / d$^{-1}$") +ax.set_xlim(0.15, 30) +ax.set_ylim(1e-15, 1e1) +ax.grid(visible=True, which="both") +points = {"pt0": (1.0, 1.0, 1.0)} + +sigs = np.logspace(-1, 2, 100) +for temp, (col, stresses) in Exps.items(): + # plot analytical curves + if temp >= 25: + ax.plot(sigs, BGRa(sigs, temp), color=col, ls="--") + ax.plot(sigs, PLLC(sigs, temp), color=col, ls="-") + + # simulation in ogs and plot results + eps_dot = [] + ogs_model.replace_parameter_value("T_ref", str(temp + 273.15)) + for stress in stresses: + ogs_model.replace_parameter_value("sigma_ax", str(-stress)) + ogs_model.write_input() + # hide output + with contextlib.redirect_stdout(None): + ogs_model.run_model( + logfile=f"{out_dir}/out.txt", args="-m " + f"{data_dir}/Mechanics/PLLC/" + ) + pvdfile = vtuIO.PVDIO(f"{prj_name}.pvd", dim=3) + eps_zz = pvdfile.read_time_series("epsilon", points)["pt0"][:, 2] + eps_zz_dot = np.abs(np.diff(eps_zz)) / np.diff(pvdfile.timesteps) + # omit the first timestep + eps_dot += [np.mean(eps_zz_dot[1:])] + ax.loglog(1e-6 * stresses, eps_dot, "o", c=col, markeredgecolor="k") + +# plot experimental data points +for _Ex, (temp, m, Data) in ExData.items(): + stresses, eps_dot = np.array(Data).T + ax.loglog(stresses, eps_dot, m, c=Exps[temp][0]) + +# create legend +patches = [ + mpl.patches.Patch(color=col, label=str(temp) + "°C") + for temp, (col, _) in Exps.items() + if temp >= 25 +][::-1] + + +def addLeg(**args): + return patches.append(mpl.lines.Line2D([], [], **args)) + + +addLeg(c="k", label="PLLC") +addLeg(c="k", ls="--", label="BGRa") +addLeg(c="w", ls="None", marker="o", mec="k", label="OGS") +addLeg(c="k", ls="None", marker="s", label="DeVries (1988)") +addLeg(c="k", ls="None", marker="^", label="WIPP CS") +addLeg(c="b", ls="None", marker="P", label="Bérest (2017) 7.8°C") +addLeg(c="orange", ls="None", marker="P", label="Bérest (2015) 14.3°C") +ax.legend(handles=patches, loc="best") + +fig.tight_layout() +plt.show() + + +# %% [markdown] +# ### References +# +# Zill, Florian, Wenqing Wang, and Thomas Nagel. Influence of THM Process Coupling and Constitutive Models on the Simulated Evolution of Deep Salt Formations during Glaciation. The Mechanical Behavior of Salt X. CRC Press, 2022. https://doi.org/10.1201/9781003295808-33. +# +# Li, Shiyuan, and Janos Urai. Numerical Studies of the Deformation of Salt Bodies with Embedded Carbonate Stringers. Online, print. Publikationsserver der RWTH Aachen University, 2012. http://publications.rwth-aachen.de/record/211523/files/4415.pdf diff --git a/Tests/Data/Parabolic/LiquidFlow/BlockingConductingFracture/BlockingConductingFracture.py b/Tests/Data/Parabolic/LiquidFlow/BlockingConductingFracture/BlockingConductingFracture.py new file mode 100644 index 0000000000000000000000000000000000000000..e18d3c3a4e935d7be9dea1396c2430616010f48b --- /dev/null +++ b/Tests/Data/Parabolic/LiquidFlow/BlockingConductingFracture/BlockingConductingFracture.py @@ -0,0 +1,267 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Liquid flow in blocking and conducting fractures" +# date = "2023-08-05" +# author = "Mehran Ghasbeh" +# web_subsection = "liquid-flow" +# +++ +# + +# %% [markdown] +# Comments to: +# +# *Mehran Ghasabeh \ +# Chair of Soil Mechanics and Foundation Engineering \ +# Geotechnical Institute \ +# Technische Universität Bergakademie Freiberg.* + +# %% [markdown] +# # Blocking and Conducting Fractures +# +# The liquid flow in fractured porous media, such as geomaterials including rock, granular materials, and solid and industrial waste, is an essential and interdisciplinary topic that is closely related to a variety of scientific and technical areas, such as hydrology, rock mechanics, geothermal engineering, and earth sciences [1]. The geometrically anisotropic inclusions and the discontinuities, generally called fractures, dominate the liquid flow characteristics in porous media. In general, fractures can act as both passageways and obstacles, drastically altering flow patterns[2]. Therefore, the flow of liquid in a porous medium that is fractured, or fracturing, can be divided into two distinct subproblems: the flow of liquid in the fractures that separate the intact sections of the porous medium and the interstitial flow in the pores of the intact parts, which are called the blocking and the conducting fractures, respectively [3]. +# +# # Problem Description +# +# The current benchmark test addressing liquid flow in a porous medium considers a 2D square solid domain containing blocking and conducting fractures. The blocking fracture is represented by the geometrical discontinuity in the center of the plate that prevents liquid from passing through, and the conducting fracture which depicted by a physically inclined line in the upper part of the domain. The geometrical configuration and boundary conditions are displayed in Fig 1. The whole domain is discretized by triangular elements. As an initial condition, the pressure is $p=0$ in the entire domain, while the Dirichlet boundary conditions are defined by applying pressure on the bottom edge of the plate (intel) with $p=21$ kPa and on the top edge (outlet) with $p=0$. The temperature is set to $\theta=293.15$ K in the whole domain. +# +# <center> +# <img src="https://github.com/mehranqsb/LiquidFlow-OGS/blob/main/fig.png?raw=true" style="width: 545px;"/> +# </center> +# +# The material parameters involving viscosity, density, permeability, porosity and storage capacity are given in the following table: +# +# | Parameters | Porous Medium | Conducting Fracture | Unit | +# | --- | --- | --- | --- | +# | Viscosity | 10<sup>-3</sup> | 10<sup>-3</sup> | Pa.s | +# | Density | 1000 | 1000 | kg/m<sup>3</sup> | +# | Permeability | 10<sup>-12</sup>| 10<sup>-6</sup> | m<sup>2</sup> | +# | Porosity | 0.2 | 1 | - | +# | Storage | 0 | 0 | m<sup>3</sup>/(kg Pa)| +# +# [def]: ttps://github.com/mehranqsb/LiquidFlow-OGS/blob/main/fig.pn +# [def2]: https://github.com/mehranqsb/LiquidFlow-OGS/blob/main/fig.png + +# %% +# HIDDEN +import matplotlib.pyplot as plt +import matplotlib.tri as tri +import numpy as np +import ogstools as ot +import seaborn as sns +import vtuIO + + +# %% +# Setup model +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +model_lf = ot.Project( + input_file="block_conduct_frac.prj", output_file="block_conduct_frac.prj" +) + + +# %% +# Run the analysis +model_lf.run_model(logfile=(out_dir / "block_conduct_frac.txt"), args=f"-o {out_dir}") + + +# %% +# Access VTU/PVD files, outputted by OpenGeoSys FEM Solver. +vtufile = vtuIO.VTUIO("fracture_block_conduct_ts_1_t_1.000000.vtu", dim=2) + + +# %% [markdown] +# # Post-Processing + +# %% +# Get the nodal coordinates from vtufilhe porous media include e +x = vtufile.points[:, 0] +y = vtufile.points[:, 1] + + +# %% +# Triangulation# Post-Processing (Pressure Field in Conducting and Blocking Fracture) +triang = tri.Triangulation(x, y) + + +# %% +# Get the pressure field from vtufile +field = vtufile.get_point_field("pressure") +# Convert the pressure field from Pa to kPa +field = field / 1000.0 + + +# %% +# Get the velocty fields along x and y directions +fieldx = vtufile.get_point_field("v").T[0] +fieldy = vtufile.get_point_field("v").T[1] +fieldx = vtufile.get_point_field("v").T[0] +fieldy = vtufile.get_point_field("v").T[1] + + +# %% +# Post-Processing +levels = np.linspace(np.min(field), np.max(field), 60) +levelsf = np.linspace(np.min(field), np.max(field), 60) + +fig, ax = plt.subplots(ncols=2, figsize=(20, 8)) +contour = ax[0].tricontour(triang, field, levels=levels, cmap=plt.cm.jet) + +contourf = ax[1].tricontourf(triang, field, levels=levelsf, cmap=plt.cm.jet) +plt.clabel(contour, fontsize=10, inline=1) + +fig.colorbar(contour, ax=ax[0], label="$p$ in kPa") +fig.colorbar(contourf, ax=ax[1], label="$p$ in kPa") + +ax[0].set_title("Pressure") +ax[1].set_title("Pressure") + +for i in range(2): + ax[i].set_aspect("equal") + ax[i].set_ylabel("$y$ / m") + ax[i].set_xlabel("$x$ / m") +fig.tight_layout() + + +# %% +# Post-Processing (Velocities Fields in Conducting and Blocking Fracture) +levels_x = np.linspace(np.min(fieldx), np.max(fieldx), 100) +levels_y = np.linspace(np.min(fieldy), np.max(fieldy), 100) + +fig, ax = plt.subplots(ncols=2, figsize=(20, 8)) +contour_x = ax[0].tricontourf(triang, fieldx, levels=levels_x, cmap=plt.cm.jet) +contour_y = ax[1].tricontourf(triang, fieldy, levels=levels_y, cmap=plt.cm.jet) + +contour = ax[0].tricontour(triang, fieldx, levels=levels_x, cmap=plt.cm.winter) +contour = ax[1].tricontour(triang, fieldy, levels=levels_y, cmap=plt.cm.winter) + +fig.colorbar(contour_x, ax=ax[0], label="$v_x$ in m/s") +fig.colorbar(contour_y, ax=ax[1], label="$v_y$ in m/s") +ax[0].set_title("Flow Velocity in x") +ax[1].set_title("Flow Velocity in y") +for i in range(2): + ax[i].set_aspect("equal") + ax[i].set_ylabel("$y$ / m") + ax[i].set_xlabel("$x$ / m") +fig.tight_layout() + + +# %% +# Calculate the magnitude of the velocity vector fieldlevels = np.linspace(np.min(field), np.max(field), 58) +vmag = np.sqrt(fieldx**2.0 + fieldy**2.0) + + +# %% +import matplotlib.cm as cm +import matplotlib.colors as mcolors + +fig, ax = plt.subplots(ncols=2, figsize=(20, 8)) + +nz = mcolors.Normalize() +nz.autoscale(y) + +levels = np.linspace(np.min(vmag), np.max(vmag), 100) +contourf = ax[0].tricontourf(triang, vmag, levels=levels, cmap=plt.cm.jet) +quiverf = ax[1].quiver( + x, + y, + fieldx, + fieldy, + color=cm.jet(nz(y)), + headwidth=0.45, + scale=0.0012, + headlength=0.35, +) + +fig.colorbar(contourf, ax=ax[0], label="$|v|$ in m/s") +ax[0].set_title("Magnitude of Velocity") + +fig.colorbar(contour_x, ax=ax[1], label="$v$ in m/s") +ax[1].set_title("Vector Field of Velocity") + +for i in range(2): + ax[i].set_aspect("equal") + ax[i].set_ylabel("$y$ / m") + ax[i].set_xlabel("$x$ / m") + fig.tight_layout() + + +# %% +pvd_frac = vtuIO.PVDIO("fracture_block_conduct.pvd", dim=2) +line_05 = [(0.5, i, 0) for i in np.linspace(start=0.0, stop=1.0, num=500)] +lines = {"@ x=0.5": line_05} + + +# %% +sns.set_palette("Paired", n_colors=10) +fig, ax = plt.subplots(ncols=2, figsize=(20, 8)) +x = np.linspace(0, 1, 500) +for i in lines: + velocity_x = pvd_frac.read_set_data(1, "v", pointsetarray=lines[i]).T[0] + velocity_y = pvd_frac.read_set_data(1, "v", pointsetarray=lines[i]).T[1] + pressure = ( + pvd_frac.read_set_data(1, "pressure", pointsetarray=lines[i]) / 1000.0 + ) # Converting pressure to kPa. + velocity_magnitude = np.sqrt(velocity_x**2.0 + velocity_y**2.0) + + ax[0].plot(x, pressure, color="black", label="Pressure at $x$ = 0.5 m", ls="-") + ax[0].axis(xmin=0.0, xmax=1.0) + ax[0].axis(ymin=0.0, ymax=25) + ax[0].grid(linestyle="dashed") + + ax[1].plot( + x, + velocity_magnitude, + color="black", + label="Magnitude of Velocity at $x$ = 0.5 m", + ls="-", + ) + ax[1].axis(xmin=0.0, xmax=1.0) + ax[1].axis(ymin=0.0, ymax=1.6e-5) + ax[1].grid(linestyle="dashed") + + ax[0].legend() + ax[0].set_xlabel("$y$ / m") + ax[0].set_ylabel("$p$ / kPa") + + ax[1].legend() + ax[1].set_xlabel("$y$ / m") + ax[1].set_ylabel("$|v|$ / m/s") + fig.tight_layout() + + +# %% [markdown] +# # Conclusion +# +# The current benchmark example shows how blocking and conducting fractures affect liquid flow pattern. For this aim, a 2D domain with a blocking fracture in the center and a conducting fracture in the upper half of the domain is assessed. The data are provided in terms of pressure and velocity. The pore pressure exhibits a jump across the central blocking fracture, thus leading to a discontinuous pressure field. The conducting fracture facilitates fluid flow. Its effect can be seen in the symmetry break of the solution (without the conducting fracture, pressure and velocity fields would be symmetric). Strong velocity gradients are observed in the vicinity of both fractures. + +# %% [markdown] +# # References +# +# [1] Zhou, C. B., Chen, Y. F., Hu, R., & Yang, Z. (2023). Groundwater flow through fractured rocks and seepage control in geotechnical engineering: <i>Theories and practices. Journal of Rock Mechanics and Geotechnical Engineering, 15(1) </i>, 1-36. https://doi.org/10.1016/j.jrmge.2022.10.001. +# +# +# [2] Flemisch, B., et al. (2018). Benchmarks for single-phase flow in fractured porous media. <i>Advances in Water Resources, 111</i>, 239-258. https://doi.org/10.1016/j.advwatres.2017.10.036. +# +# +# [3] de Borst, R. (2017). Fluid flow in fractured and fracturing porous media: A unified view. <i>Mechanics Research Communications, 80</i>, 47-57. https://doi.org/10.1016/j.mechrescom.2016.05.004. +# diff --git a/Tests/Data/PhaseField/Kregime_Propagating_jupyter_notebook/Kregime_Propagating_jupyter.py b/Tests/Data/PhaseField/Kregime_Propagating_jupyter_notebook/Kregime_Propagating_jupyter.py new file mode 100644 index 0000000000000000000000000000000000000000..e3099c75d25a7bda61922c432438d3c4827f00eb --- /dev/null +++ b/Tests/Data/PhaseField/Kregime_Propagating_jupyter_notebook/Kregime_Propagating_jupyter.py @@ -0,0 +1,587 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Mostafa Mollaali, Keita Yoshioka" +# date = "2023-03-03" +# title = "Hydraulic Fracturing in the Toughness-Dominated Regime" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# ## Hydraulic Fracturing in the Toughness-Dominated Regime +# + +# %% [markdown] +# Under the toughness-dominated regime without leak-off, the fluid viscosity dissipation is negligible compared to the energy released for fracture surface formation (**Detournay _et al._, 2016**). Therefore, in this regime, we can neglect the pressure loss within the fracture and use the Sneddon solution for crack opening (**Bourdin _et al._, 2012**, **Sneddon et al., 1969**) to determine the pressure and length evolution. +# +# The work of the pressure force is as follows: +# +# $$ +# \begin{equation} +# \mathcal{W}(R) =\frac{2 p^2 a^2}{E'}. +# \end{equation} +# $$ +# +# Applying Clapeyron's theorem, the elastic energy can be determined as +# +# $$ +# \begin{equation} +# \mathcal{E}(R) =-\frac{\pi p^2 a^2}{E'}, +# \end{equation} +# $$ +# and the energy release rate with respect to the crack length $a$ propagating along the initial inclination is +# $$ +# \begin{equation} +# G(R) = - \frac{\partial \mathcal{E}}{\partial (2 a)} = \frac{\pi p^2 a}{E'}. +# \end{equation} +# $$ +# +# Griffith\'s criterion (**Griffith _et al._, 1920**) states that the fracture propagates when $G=G_c$ and the critical volume for crack propagation is $V_c := \sqrt{\dfrac{4 \pi G_c a^3}{E' }}$ in a quasi-static volume control setting (the fracture propagation is always unstable with pressure control). +# +# +# The evolution of the corresponding pressure and fracture length +# $$ +# \begin{equation} +# p(V) = +# \begin{cases} +# \dfrac{E' V}{2 \pi a_0^2} &\text{for} V < V_c \\ +# \left[ \dfrac{2 E' G_c^2}{\pi V} \right] ^{\frac{1}{3}}&\text{for} V \geq V_c, +# \end{cases} +# \end{equation} +# $$ +# +# $$ +# \begin{equation} +# a(V) = +# \begin{cases} +# a_0 & V < V_c \\ +# \left[ \dfrac{E' V^2}{4 \pi G_c} \right ] ^{\frac{1}{3}} & V \geq V_c. +# \end{cases} +# \end{equation} +# $$ + +# %% [markdown] +# # Problem Description +# +# Based on Sneddon\'s solution (**Sneddon et al., 1969**), we verified the model with plane-strain hydraulic fracture propagation in a toughness-dominated regime. In an infinite 2D domain, the problem was solved with a line fracture $[-a_0, a_0] \times \{0\}$ ($a_0$ = 0.05). We considered a large finite domain $[-40a_0,40a_0] \times [-40a_0,40a_0]$ to account for the infinite boundaries in the closed-form solution. The effective size of an element, $h$, is $1\times10^{-2}$. + +# %% [markdown] +#  + +# %% [markdown] +# * In order to have the hydraulic fracturing in the toughness dominated-regime, add, $\texttt{<pressurized_crack_scheme>propagating</pressurized_crack_scheme>}$ in the project file. +# * **Yoshioka _et al._, 2019** provides additional information on the implementation, use of real material properties, and rescaling of the phase-field energy functional. +# + +# %% [markdown] +# # Input Data + +# %% [markdown] +# Taking advantage of the linearity of the system, the simulations were run with the dimensionless properties listed in the Table below. + +# %% [markdown] +# | **Name** | **Value** | **Symbol** | +# |--------------------------------|------------------ |------------| +# | _Young's modulus_ | 1 | $E$ | +# | _Poisson's ratio_ | 0.15 | $\nu$ | +# | _Fracture toughness_ | 1 | $G_{c}$ | +# | _Regularization parameter_ | 2$h$ | $\ell_s$ | +# | _Length_ | 4 | $L$ | +# | _Height_ | 4 | $H$ | +# | _Initial crack length_ | 0.1 | $2a_0$ | + +# %% +import math +import os +import re +import time + +import gmsh +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +from ogstools.msh2vtu import msh2vtu + +pi = math.pi +plt.rcParams["text.usetex"] = True + + +# %% +E = 1.0 +nu = 0.15 +Gc = 1.0 +h = 0.01 +a0 = 0.05 # half of the initial crack length + +phasefield_model = "AT1" # AT1/AT2 + + +# %% [markdown] +# # Output Directory and Project File + +# %% +# file's name +prj_name = "Kregime_Propagating.prj" +meshname = "mesh_full_pf" + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + + +# %% [markdown] +# # Mesh Generation +# + + +# %% +def mesh_generation(lc, lc_fine): + """ + lc ... characteristic length for coarse meshing + lc_fine ... characteristic length for fine meshing + """ + L = 4.0 # Length + H = 4.0 # Height + b = 0.4 # Length/Height of subdomain with fine mesh + + # Before using any functions in the Python API, Gmsh must be initialized + gmsh.initialize() + gmsh.option.setNumber("General.Terminal", 1) + gmsh.model.add("rectangle") + + # Dimensions + dim0 = 0 + dim1 = 1 + dim2 = 2 + + # Points + gmsh.model.geo.addPoint(-L / 2, -H / 2, 0, lc, 1) + gmsh.model.geo.addPoint(L / 2, -H / 2, 0, lc, 2) + gmsh.model.geo.addPoint(L / 2, H / 2, 0, lc, 3) + gmsh.model.geo.addPoint(-L / 2, H / 2, 0, lc, 4) + gmsh.model.geo.addPoint(-b, -b - lc_fine / 2, 0, lc_fine, 5) + gmsh.model.geo.addPoint(b, -b - lc_fine / 2, 0, lc_fine, 6) + gmsh.model.geo.addPoint(b, b + lc_fine / 2, 0, lc_fine, 7) + gmsh.model.geo.addPoint(-b, b + lc_fine / 2, 0, lc_fine, 8) + + # Lines + gmsh.model.geo.addLine(1, 2, 1) + gmsh.model.geo.addLine(2, 3, 2) + gmsh.model.geo.addLine(3, 4, 3) + gmsh.model.geo.addLine(4, 1, 4) + gmsh.model.geo.addLine(5, 6, 5) + gmsh.model.geo.addLine(6, 7, 6) + gmsh.model.geo.addLine(7, 8, 7) + gmsh.model.geo.addLine(8, 5, 8) + + # Line loops + gmsh.model.geo.addCurveLoop([1, 2, 3, 4], 1) + gmsh.model.geo.addCurveLoop([5, 6, 7, 8], 2) + + # Add plane surfaces defined by one or more curve loops. + gmsh.model.geo.addPlaneSurface([1, 2], 1) + gmsh.model.geo.addPlaneSurface([2], 2) + + gmsh.model.geo.synchronize() + + # Prepare structured grid + gmsh.model.geo.mesh.setTransfiniteCurve( + 6, math.ceil(2 * b / lc_fine + 2), "Progression", 1 + ) + gmsh.model.geo.mesh.setTransfiniteCurve( + 8, math.ceil(2 * b / lc_fine + 2), "Progression", 1 + ) + gmsh.model.geo.mesh.setTransfiniteSurface(2, "Alternate") + + gmsh.model.geo.mesh.setRecombine(dim2, 1) + gmsh.model.geo.mesh.setRecombine(dim2, 2) + + gmsh.model.geo.synchronize() + + # Physical groups + P1 = gmsh.model.addPhysicalGroup(dim0, [1]) + gmsh.model.setPhysicalName(dim0, P1, "P1") + + P2 = gmsh.model.addPhysicalGroup(dim0, [2]) + gmsh.model.setPhysicalName(dim0, P2, "P2") + + Bottom = gmsh.model.addPhysicalGroup(dim1, [1]) + gmsh.model.setPhysicalName(dim1, Bottom, "Bottom") + + Right = gmsh.model.addPhysicalGroup(dim1, [2]) + gmsh.model.setPhysicalName(dim1, Right, "Right") + + Top = gmsh.model.addPhysicalGroup(dim1, [3]) + gmsh.model.setPhysicalName(dim1, Top, "Top") + + Left = gmsh.model.addPhysicalGroup(dim1, [4]) + gmsh.model.setPhysicalName(dim1, Left, "Left") + + Computational_domain = gmsh.model.addPhysicalGroup(dim2, [1, 2]) + gmsh.model.setPhysicalName(dim2, Computational_domain, "Computational_domain") + gmsh.model.geo.synchronize() + + output_file = f"{out_dir}/" + meshname + ".msh" + gmsh.model.mesh.generate(dim2) + gmsh.write(output_file) + gmsh.finalize() + + +# %% [markdown] +# # Pre-Processing + + +# %% +def pre_processing(h, a0): + mesh = pv.read(f"{out_dir}/mesh_full_pf_domain.vtu") + phase_field = np.ones((len(mesh.points), 1)) + pv.set_plot_theme("document") + + for node_id, x in enumerate(mesh.points): + if ( + (mesh.center[0] - x[0]) <= a0 + 0.001 * h + and (mesh.center[0] - x[0]) >= -a0 - 0.001 * h + and (mesh.center[1] - x[1]) < h / 2 + 0.001 * h + and (mesh.center[1] - x[1]) > -h / 2 - 0.001 * h + ): + phase_field[node_id] = 0.0 + + mesh.point_data["phase-field"] = phase_field + mesh.save(f"{out_dir}/mesh_full_pf_OGS_pf_ic.vtu") + + +# %% [markdown] +# # Run the Simulation +# + +# %% +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + + +def Hydraulic_Fracturing_Toughness_Dominated_numerical(h, phasefield_model): + # mesh properties + ls = 2 * h + # generate prefix from properties + filename = f"results_h_{h:0.4f}_{phasefield_model}" + mesh_generation(0.1, h) + # Convert GMSH (.msh) meshes to VTU meshes appropriate for OGS simulation. + input_file = f"{out_dir}/" + meshname + ".msh" + msh2vtu(filename=input_file, output_path=out_dir, reindex=True, keep_ids=True) + # %cd {out_dir} + ! identifySubdomains -f -m mesh_full_pf_domain.vtu -- mesh_full_pf_physical_group_*.vtu + # %cd - + + # As a preprocessing step, define the initial phase-field (crack). + pre_processing(h, a0) + # change properties in prj file #For more information visit: https://ogstools.opengeosys.org/stable/reference/ogstools.ogs6py.html + model = ot.Project( + input_file=prj_name, + output_file=f"{out_dir}/{prj_name}", + MKL=True, + args=f"-o {out_dir}", + ) + model.replace_parameter_value(name="ls", value=ls) + model.replace_text(phasefield_model, xpath="./processes/process/phasefield_model") + model.replace_text(filename, xpath="./time_loop/output/prefix") + model.write_input() + # run simulation with ogs + t0 = time.time() + print(">>> OGS started execution ... <<<") + ! ogs {out_dir}/{prj_name} -o {out_dir} > {out_dir}/log.txt + tf = time.time() + print(">>> OGS terminated execution <<< Elapsed time: ", round(tf - t0, 2), " s.") + + +# %% +Hydraulic_Fracturing_Toughness_Dominated_numerical(h, phasefield_model) + + +# %% [markdown] +# # Post-Processing + +# %% [markdown] +# ## Analytical Solution for the Evolution of Fracture Length and Pressure + + +# %% +def Analytical_solution(phasefield_model, h): + v = np.linspace(1e-10, 0.3, 31) + pres = np.linspace(0, 1.0, 31) + length = np.linspace(0, 1.0, 31) + + ls = 2 * h + + # Effective Gc and a for AT1/A2 + if phasefield_model == "AT1": + Gc_ref = Gc * (3 * h / 8.0 / ls + 1.0) + a_eff = a0 * (1 + pi * ls / (4.0 * a0 * (3 * h / 8.0 / ls + 1.0))) + elif phasefield_model == "AT2": + Gc_ref = Gc * (h / (2.0 * ls) + 1.0) + a_eff = a0 * (1 + pi * ls / (4.0 * a0 * (h / (2.0 * ls) + 1.0))) + + Eprime = E / (1 - nu**2) + + V_c = (4 * pi * Gc_ref * a_eff**3 / Eprime) ** 0.5 + P_c = (Eprime * Gc_ref / (pi * a_eff)) ** 0.5 + + for i in range(len(v)): + if v[i] < V_c: + pres[i] = Eprime * v[i] / (2 * pi * a_eff**2) / P_c + length[i] = a_eff + else: + pres[i] = (2 * Eprime * Gc_ref**2 / (pi * v[i])) ** 0.333333 / P_c + length[i] = (Eprime * v[i] ** 2 / (4 * pi * Gc_ref)) ** 0.333333 + + return v, pres, length, Gc_ref, P_c + + +fluidVolume_analytical = Analytical_solution(phasefield_model, h)[0] +pressure_analytical = Analytical_solution(phasefield_model, h)[1] +length_analytical = Analytical_solution(phasefield_model, h)[2] +Gc_ref = Analytical_solution(phasefield_model, h)[3] +P_c = Analytical_solution(phasefield_model, h)[4] + + +# %% [markdown] +# ## Phase Field Versus Analytical Solution for Fracture Length and Pressure Evolution + +# %% [markdown] +# In phase field approach, we can retrieve the crack length $a$ as: +# +# $$ +# \begin{equation} +# a = \dfrac{\displaystyle \int_\Omega \frac{G_c}{4 c_n} \left(\frac{(1-v)^n}{\ell}+ \ell |\nabla v|^2\right)\,\mathrm{d} \Omega }{G_c \left( \dfrac{h}{4 c_n \ell} + 1 \right)}. +# \end{equation} +# $$ +# where $n=1$ corresponds to $\texttt{AT}_1$ ($c_n = 2/3$) and $n=2$ to $\texttt{AT}_2$ ($c_n = 1/2$). + +# %% +plt.subplots(figsize=(12, 4)) +plt.subplot(1, 2, 1) +fluid_volume = [] +surface_energy = [] +# Open the file for reading +with (out_dir / "log.txt").open() as fd: + # Iterate over the lines + for _i, line in enumerate(fd): + match_surface_energy = re.search( + r"""Surface energy: (\d+\.\d*) Pressure work: (\d+\.\d*) at time: (\d+\.\d*)""", + line, + ) + if match_surface_energy: + surface_energy.append(float(match_surface_energy.group(1))) + fluid_volume.append(float(match_surface_energy.group(3))) + +plt.grid(linestyle="dashed") +plt.xlabel("Volume") +plt.ylabel("Crack length") +plt.plot(fluidVolume_analytical, length_analytical, "black", label="Closed form") +plt.plot( + fluid_volume, + np.array(surface_energy[:]) / Gc_ref / 2, + "bo", + fillstyle="none", + label="Phase-field", +) +legend = plt.legend(loc="lower right") + +plt.subplot(1, 2, 2) +fluid_volume = [] +pressure = [] +# Open the file for reading +with (out_dir / "log.txt").open() as fd: + # Iterate over the lines + for _i, line in enumerate(fd): + match_pressure = re.search( + r"""Pressure: (\d+\.\d*) at time: (\d+\.\d*)""", line + ) + if match_pressure: + fluid_volume.append(float(match_pressure.group(2))) + pressure.append(float(match_pressure.group(1))) + + +plt.xlabel("Volume") +plt.ylabel("$p_f/p_c$") +plt.plot(fluidVolume_analytical, pressure_analytical, "black", label="Closed form") +plt.plot( + np.array(fluid_volume[:]), + np.array(pressure[:]) / P_c, + "g<", + fillstyle="none", + label="Phase-field", +) +plt.grid(linestyle="dashed") +legend = plt.legend(loc="upper right") +plt.show() + + +# %% +plt.subplots(figsize=(12, 4)) +plt.subplot(1, 2, 1) +plt.grid(linestyle="dashed") +plt.plot( + fluidVolume_analytical[1:], + ( + abs(length_analytical[1:] - np.array(surface_energy[:]) / (2 * Gc_ref)) + / (np.array(surface_energy[:]) / (2 * Gc_ref)) + ) + * 100, + "-.ob", + fillstyle="none", + label="Closed form", +) +plt.ylim([0.0, 10]) +plt.xlabel("Volume") +plt.ylabel( + r"$\frac{|a_\mathrm{num}-{a}_\mathrm{ana}|}{{a}_\mathrm{num}}\times 100\%$", + fontsize=14, +) + +plt.subplot(1, 2, 2) +plt.grid(linestyle="dashed") +plt.plot( + fluidVolume_analytical[1:], + (abs(pressure_analytical[1:] - np.array(pressure[:]) / P_c) / pressure[:]) * 100, + "-.<g", + fillstyle="none", + label="Closed form", +) +plt.ylim([0.0, 10]) +plt.xlabel("Volume") +plt.ylabel( + r"$\frac{|p_\mathrm{num}-{p}_\mathrm{ana}|}{{p}_\mathrm{num}}\times 100\%$", + fontsize=14, +) +plt.show() + + +# %% [markdown] +# In order to reduce computation time, we perform the simulation with a coarse mesh; the results for the $\texttt{AT}_1$ Model with a mesh size of $h=0.001$ are provided below. + +# %% [markdown] +# $\texttt{AT}_1$ Model: + +# %% [markdown] +#  + +# %% [markdown] +#  + +# %% [markdown] +# ## Hydraulic Fracturing Animation (Using Phase Field Approach) + +# %% +filename = f"results_h_{h:0.4f}_{phasefield_model}" +reader = pv.get_reader(f"{out_dir}/" + filename + ".pvd") + +plotter = pv.Plotter(shape=(1, 2), border=False) +plotter.open_gif(f"{out_dir}/Kregime_Propagating.gif") +for time_value in reader.time_values: + reader.set_active_time_value(time_value) + mesh = reader.read()[0] + sargs = { + "title": "Phase field", + "title_font_size": 16, + "label_font_size": 12, + "n_labels": 5, + "position_x": 0.25, + "position_y": 0.15, + "fmt": "%.1f", + "width": 0.5, + } + p = pv.Plotter(shape=(1, 2), border=False) + clim = [0, 1.0] + points = mesh.point_data["phasefield"].shape[0] + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + pf = mesh.point_data["phasefield"] + plotter.clear() + plotter.add_mesh( + mesh, + scalars=pf, + show_scalar_bar=True, + colormap="coolwarm", + clim=clim, + scalar_bar_args=sargs, + lighting=False, + ) + plotter.add_text(f"Time: {time_value:0.02f}", color="black") + + plotter.view_xy() + plotter.write_frame() + +plotter.close() + + +# %% [markdown] +# ## Phase Field Profile at Last Time Step + +# %% +mesh = reader.read()[0] + +points = mesh.point_data["phasefield"].shape[0] +xs = mesh.points[:, 0] +ys = mesh.points[:, 1] +pf = mesh.point_data["phasefield"] + +clim = [0, 1.0] +sargs = { + "title": "Phase field", + "title_font_size": 16, + "label_font_size": 12, + "n_labels": 5, + "position_x": 0.25, + "position_y": 0.0, + "fmt": "%.1f", + "width": 0.5, +} +plotter = pv.Plotter(shape=(1, 2), border=False) +plotter.add_mesh( + mesh, + scalars=pf, + show_edges=False, + show_scalar_bar=True, + colormap="coolwarm", + clim=clim, + scalar_bar_args=sargs, + lighting=False, +) + +plotter.view_xy() +plotter.camera.zoom(1.5) +plotter.window_size = [1000, 500] +plotter.show() + + +# %% [markdown] +# ## References +# +# [1] Detournay, Emmanuel. _Mechanics of hydraulic fractures._ Annual review of fluid mechanics 48 (2016): 311-339. +# +# [2] Bourdin, Blaise, Chukwudi Chukwudozie, and Keita Yoshioka. _A variational approach to the numerical simulation of hydraulic fracturing._ In SPE annual technical conference and exhibition. OnePetro, 2012. +# +# [3] Sneddon, Ian Naismith, and Morton Lowengrub. _Crack problems in the classical theory of elasticity._ 1969, 221 P (1969). +# +# [4] Griffith, Alan Arnold. _VI. The phenomena of rupture and flow in solids._ Philosophical transactions of the royal society of london. Series A, containing papers of a mathematical or physical character 221, no. 582-593 (1921): 163-198. +# +# [5] Yoshioka, Keita, Francesco Parisio, Dmitri Naumov, Renchao Lu, Olaf Kolditz, and Thomas Nagel. _Comparative verification of discrete and smeared numerical approaches for the simulation of hydraulic fracturing._ GEM-International Journal on Geomathematics 10, no. 1 (2019): 1-35. +# diff --git a/Tests/Data/PhaseField/PForthotropy_jupyter_notebook/sen_shear.py b/Tests/Data/PhaseField/PForthotropy_jupyter_notebook/sen_shear.py new file mode 100644 index 0000000000000000000000000000000000000000..10d7bec8dcad9cd7f02a02c967010a8173d39561 --- /dev/null +++ b/Tests/Data/PhaseField/PForthotropy_jupyter_notebook/sen_shear.py @@ -0,0 +1,417 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Vahid Ziaei-Rad, Mostafa Mollaali" +# date = "2022-12-15" +# title = "Pre-notched shear test" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# +# ## Problem description +# +# In order to verify the anisotropic phase field model detailed in (Ziaei-Rad et al., 2022), we present a single edge notched shear test specifically designed for materials with anisotropic/orthotropic behavior in the phase field approach to fracture to account for the tension–compression asymmetry. We generalize two existing models for tension-compression asymmetry in isotropic materials, namely the *volumetric-deviatoric* (Amor et al., 2009) and *no-tension* model (Freddi and Royer-Carfagni, 2010), towards materials with anisotropic nature. +# +# The geometry and boundary conditions for this example is shown in the following Figure. +# +# We consider a square plate with an initial horizontal crack placed at the middle height from the left outer surface to the center of the specimen. Plane strain condition was assumed for sake of less computational costs. The user is free to choose either $\texttt{AT}_1$ or $\texttt{AT}_2$ as for the phase field model. +# +# The boundary conditions are as follows. The displacement along any direction on the bottom edge ($y=-L/2$) was fixed to zero. Also, the displacement at the top edge ($y = L/2$) was prescribed along the $x$-direction, where the $y$-direction was taken to be zero. +# +# <img src="figures/shear_model.png" width="60%"> + +# %% [markdown] +# ## Two helper functions + +# %% +import os +import shutil +import time +from types import MethodType +from xml.dom import minidom + +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +import pyvista as pv + +# %% +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +output_dir = out_dir + +# define a method to replace a specific curve (analogue to replace_parameter method) + + +def replace_curve( + self, + name=None, + value=None, + coords=None, + parametertype=None, + valuetag="values", + coordstag="coords", +): + root = self._get_root() + parameterpath = "./curves/curve" + parameterpointer = self._get_parameter_pointer(root, name, parameterpath) + self._set_type_value(parameterpointer, value, parametertype, valuetag=valuetag) + self._set_type_value(parameterpointer, coords, parametertype, valuetag=coordstag) + + +# define a method to change timstepping in project file + + +def set_timestepping(model, repeat_list, delta_t_list): + model.remove_element( + xpath="./time_loop/processes/process/time_stepping/timesteps/pair" + ) + for i in range(len(repeat_list)): + model.add_block( + blocktag="pair", + parent_xpath="./time_loop/processes/process/time_stepping/timesteps", + taglist=["repeat", "delta_t"], + textlist=[repeat_list[i], delta_t_list[i]], + ) + + +# %% [markdown] +# ## Run ogs with specified parameters + +# %% +def ogs_ortho( + phasefield_model, + energy_split_model, + length_scale=1.0, + bc_displacement=1.0, + ts_coords="0 1.0", + values="0 1.0", + repeat_list=None, + delta_t_list=None, + hypre=True, + MPI=True, + ncores=4, +): + without_hypre = "-ksp_type cg -pc_type bjacobi -ksp_atol 1e-14 -ksp_rtol 1e-14" + with_hypre = "-ksp_type cg -pc_type hypre -pc_hypre_type boomeramg -pc_hypre_boomeramg_strong_threshold 0.7 -ksp_atol 1e-8 -ksp_rtol 1e-8" + + prj_name = "shear.prj" + print( + f"> Running single edge notched shear test {phasefield_model} - {energy_split_model} ... <" + ) + # fmt: off + logfile = f"{out_dir}/log_{phasefield_model}_{energy_split_model}_out.txt" # noqa: F841 + # fmt: on + model = ot.Project( + input_file=prj_name, output_file=f"{out_dir}/{prj_name}", MKL=True + ) + + # generate prefix from properties + prefix = f"{phasefield_model}" + f"_{energy_split_model}" + + if MPI: + # partition mesh + ! NodeReordering -i shear.vtu -o {out_dir}/shear.vtu >> {logfile} + ! constructMeshesFromGeometry -m {out_dir}/shear.vtu -g shear.gml >> {logfile} + shutil.move("shear_top.vtu", f"{out_dir}/shear_top.vtu") + shutil.move("shear_bottom.vtu", f"{out_dir}/shear_bottom.vtu") + shutil.move("shear_left.vtu", f"{out_dir}/shear_left.vtu") + shutil.move("shear_right.vtu", f"{out_dir}/shear_right.vtu") + + shutil.move("shear_p_0.vtu", f"{out_dir}/shear_p_0.vtu") + shutil.move("shear_p_1.vtu", f"{out_dir}/shear_p_1.vtu") + shutil.move("shear_p_2.vtu", f"{out_dir}/shear_p_2.vtu") + shutil.move("shear_p_3.vtu", f"{out_dir}/shear_p_3.vtu") + + ! partmesh -s -o {out_dir} -i {out_dir}/shear.vtu >> {logfile} + ! partmesh -m -n {ncores} -o {out_dir} -i {out_dir}/shear.vtu -- {out_dir}/shear_top.vtu {out_dir}/shear_bottom.vtu {out_dir}/shear_left.vtu {out_dir}/shear_right.vtu >> {logfile} + else: + ! NodeReordering -i shear.vtu -o {out_dir}/shear.vtu >> {logfile} + + # change some properties in prj file + model = ot.Project( + input_file=prj_name, output_file=f"{out_dir}/{prj_name}", MKL=True + ) + model.replace_parameter_value(name="ls", value=length_scale) + model.replace_text(phasefield_model, xpath="./processes/process/phasefield_model") + model.replace_text( + energy_split_model, xpath="./processes/process/energy_split_model" + ) + model.replace_text(prefix, xpath="./time_loop/output/prefix") + + model.replace_parameter_value(name="dirichlet_top", value=bc_displacement) + model.replace_curve = MethodType(replace_curve, model) + model.replace_curve(name="dirichlet_time", value=values, coords=ts_coords) + + if repeat_list is not None and delta_t_list is not None: + set_timestepping(model, repeat_list, delta_t_list) + else: + set_timestepping(model, ["1"], ["1e-2"]) + if hypre is True: + model.replace_text( + with_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + else: + model.replace_text( + without_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + model.replace_text(Path("./shear.gml").resolve(), xpath="./geometry") + model.write_input() + # run ogs + t0 = time.time() + if MPI: + print(f" > OGS started execution with MPI - {ncores} cores...") + ! mpirun --bind-to none -np {ncores} ogs {out_dir}/{prj_name} -o {output_dir} >> {logfile} + assert _exit_code == 0 # noqa: F821 + else: + print(" > OGS started execution - ") + ! ogs {out_dir}/{prj_name} -o {output_dir} >> {logfile} + assert _exit_code == 0 # noqa: F821 + tf = time.time() + print(" > OGS terminated execution. Elapsed time: ", round(tf - t0, 2), " s.") + + +# %% [markdown] +# ## Input data +# +# We used the parameters of Opalinus Clay listed in the following Table. Note that the critical surface energy $G_c$ was taken to be independent of the material orientation. +# +# | **Name** | **Value** | **Unit** | **Symbol** | +# |--------------------------------|--------------------|--------------|------------| +# | _Young's modulus_ | 6000 | MPa | $E_1$ | +# | _Young's modulus_ | 13800 | MPa | $E_2$ | +# | _Young's modulus_ | 13800 | MPa | $E_3$ | +# | _Poisson's ratio_ | 0.22 | $-$ | $v_{12}$ | +# | _Poisson's ratio_ | 0.44 | $-$ | $v_{23}$ | +# | _Poisson's ratio_ | 0.22 | $-$ | $v_{13}$ | +# | _Shear modulus_ | 3200 | MPa | $G_{12}$ | +# | _Shear modulus_ | 1600 | MPa | $G_{23}$ | +# | _Shear modulus_ | 3200 | MPa | $G_{13}$ | +# | _Critical energy release rate_ | 0.5 | N/m | $G_{c}$ | +# | _Regularization parameter_ | 0.1 | mm | $\ell$ | +# + +# %% [markdown] +# ## Run Simulations +# > In the following, we used a coarse mesh and also coarse time stepping only for sake of less computational costs. The user is free to apply finer mesh and time stepping. + +# %% +# Alternative parameters +# phasefield_model = ['AT1', 'AT2'] +# energy_split_model = ['OrthoVolDev', 'OrthoMasonry'] + +disp = 1.0e-6 # to change the intensity of the shear loading applied on the top edge +ls = 1.0e-4 # regularization parameter to capture the convergence, though some references consider it as a material parameter (ls/h=4, h=2.5e-5) + +mpi_cores = 4 # MPI cores +## Here we only run one selected case. Based on the user's local device, more/less cores can be added to speed up/save resources. + +# With the AT2 model, we are verifying two different anisotropic models, namely, orthotropic volumetric-deviatoric and orthotropic no-tension: +# For more details of each model, please see the reference of Ziaei Rad et al., 2022. +for b in ["OrthoMasonry", "OrthoVolDev"]: + ogs_ortho( + "AT2", + b, + length_scale=ls, + bc_displacement=disp, + repeat_list=["1"], + delta_t_list=["1.e-2"], + ncores=mpi_cores, + ) + +# %% [markdown] +# ## Results +# +# In the following, the crack paths are shown for orthotropic volumetric--deviatoric and {no-tension} models under the shear loading with material orientation angle ($\alpha=0$). The user is free to change $\alpha$ to her desired angle in the prj file. +# +# An implementation of the presented anisotropic phase-field formulation combined with the unsaturated HM approach is also underway. + +# %% [markdown] +# ## Animation of crack propagation +# The following film shows the crack propagation under the shear loading for no-tension model. + +# %% +reader = pv.get_reader(f"{out_dir}/AT2_OrthoMasonry.pvd") + +plotter = pv.Plotter() + +plotter.open_gif(f"{out_dir}/AT2_OrthoMasonry.gif") +pv.set_plot_theme("document") +for time_value in reader.time_values: + reader.set_active_time_value(time_value) + mesh = reader.read()[0] # This dataset only has 1 block + + sargs = { + "title": "Phase field", + "title_font_size": 20, + "label_font_size": 15, + "n_labels": 5, + "position_x": 0.3, + "position_y": 0.2, + "fmt": "%.1f", + "width": 0.5, + } + clim = [0, 1.0] + points = mesh.point_data["phasefield"].shape[0] + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + pf = mesh.point_data["phasefield"] + plotter.clear() + plotter.add_mesh( + mesh, + scalars=pf, + show_scalar_bar=False, + clim=clim, # colormap="coolwarm" + scalar_bar_args=sargs, + lighting=False, + ) + plotter.add_text(f"Time: {time_value:.0f}", color="black") + + plotter.view_xy() + plotter.write_frame() + +plotter.close() + +# %% [markdown] +# ## Phase field contours at the last time step +# Also, below shows the phase field contours at the last time step for the orthotropic no-tension model. + +# %% +reader = pv.get_reader(f"{out_dir}/AT2_OrthoMasonry.pvd") + +mesh = reader.read()[0] +pv.set_jupyter_backend("static") +p = pv.Plotter(shape=(1, 1), border=False) +p.add_mesh( + mesh, + scalars=pf, + show_edges=False, + show_scalar_bar=True, + clim=clim, + scalar_bar_args=sargs, +) + +p.view_xy() +p.camera.zoom(1.0) +p.window_size = [800, 400] +p.show() + + +# %% [markdown] +# ## Post-processing +# Figures compares the load-deflection curve for both models. As soon as the crack starts to propagate, the load drops. + +# %% +# define function to obtain displacement applied on the top end of the square plate +def displ_midpoint(filename): + data = pv.read(filename) + max_y = max(data.points[:, 1]) + return np.mean( + data.point_data["displacement"][:, 0], + where=np.transpose(data.points[:, 1] == max_y), + ) + + +# define function to obtain force acting on the on the top end of the square plate from vtu file + + +def force_midpoint(filename): + data = pv.read(filename) + max_y = max(data.points[:, 1]) + return np.sum( + data.point_data["NodalForces"][:, 0], + where=np.transpose(data.points[:, 1] == max_y), + ) + + +# define function applying above-mentioned functions on all vtu files listed in the correspondent pvd file, +# returning force-displacement curve + + +def force_displ_from_pvd(pvd): + doc = minidom.parse(str(pvd)) + DataSets = doc.getElementsByTagName("DataSet") + vtu_files = [x.getAttribute("file") for x in DataSets] + forces_sum = [force_midpoint(f"{out_dir}/{x}") for x in vtu_files] + displs_mean = [displ_midpoint(f"{out_dir}/{x}") for x in vtu_files] + return [displs_mean, forces_sum] + + +# %% +# AT2_OrthoVolDev.pvd +prefixes = ["AT2_OrthoVolDev", "AT2_OrthoMasonry"] +labels = [r"volumetric--deviatoric", r"no-tension"] +ls = ["-", "--"] +colors = ["#ffdf4d", "#006ddb"] + +fig, ax = plt.subplots() +plt.rc("text", usetex=True) +fig.set_size_inches(18.5, 10.5) +for i, pre in enumerate(prefixes): + pvd = out_dir / f"{pre}.pvd" + if pvd.is_file(): + curve = force_displ_from_pvd(pvd) + ax.plot( + curve[0], + curve[1], + ls[i % 2], + label=labels[i], + linewidth=5, + color=colors[i], + alpha=1, + ) + +plt.rcParams["xtick.labelsize"] = 16 +plt.rcParams["ytick.labelsize"] = 16 +ax.grid(linestyle="dashed") +ax.set_xlabel(r"$\Delta [m]$", fontsize=18) +ax.set_ylabel("$F_y [N]$", fontsize=18) +plt.legend(fontsize=18, ncol=2) +ax.axhline(y=0, color="black", linewidth=1) +ax.axvline(x=0, color="black", linewidth=1) + +# %% [markdown] +# ## References +# [1] Vahid Ziaei-Rad, Mostafa Mollaali, Thomas Nagel, Olaf Kolditz, Keita Yoshioka, +# Orthogonal decomposition of anisotropic constitutive models for the phase field approach to fracture, +# Journal of the Mechanics and Physics of Solids, +# Volume 171, +# 2023, +# 105143. +# +# [2] Hanen Amor, Jean-Jacques Marigo, Corrado Maurini, +# Regularized formulation of the variational brittle fracture with unilateral contact: Numerical experiments, +# Journal of the Mechanics and Physics of Solids, +# Volume 57, Issue 8, +# 2009, +# Pages 1209-1229. +# +# [3] Francesco Freddi, Gianni Royer-Carfagni, +# Regularized variational theories of fracture: A unified approach, +# Journal of the Mechanics and Physics of Solids, +# Volume 58, Issue 8, +# 2010, +# Pages 1154-1174. diff --git a/Tests/Data/PhaseField/beam_jupyter_notebook/beam.py b/Tests/Data/PhaseField/beam_jupyter_notebook/beam.py new file mode 100644 index 0000000000000000000000000000000000000000..d3b80ae14c5e882dabc4254aaa0cdc550e5b552f --- /dev/null +++ b/Tests/Data/PhaseField/beam_jupyter_notebook/beam.py @@ -0,0 +1,388 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: 'Python 3.10.8 (''.venv'': venv)' +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Matthes Kantzenbach, Keita Yoshioka, Mostafa Mollaali" +# date = "2022-11-28" +# title = "Beam" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# +# ## Problem description +# +# In this example, it is shown, how phasefield models can be applied to simulate crack initiation. +# Consider a bar $\Omega=[0,1]\times [0,0.05]\times [0,0.05]$ where the left end $\partial \Omega_{Left}$ is fixed and a displacement $U_x(t)$ is applied to the right end $\partial \Omega_{Right}$, resulting in a uniaxial state of stress $\boldsymbol{\sigma} = \sigma_{33} \, \mathbf{n}_3 \otimes \mathbf{n}_3$. At a certain point, fracture will initiate resulting in failure of the beam. $\newline$ This will be examined for positive $U_x(t)$ (tensile stress) and for negative $U_x(t)$ (compressive stress). As energy-split models the Isotropic and the Volumetric Deviatoric are used and as phasefield models $\texttt{AT}_1$ and $\texttt{AT}_2$. So in total 8 different cases are tested. +# The aim is to estimate the ultimate load before fracture initiates for each case, as this can be veryfied against analytical results. +# +#  +# +# Also considering homogeneous damage development (i.e.$\nabla v = 0$), the analytical solution for the strengths under uniaxial tension and compression are as follows. +# +# | | Isotropic | Vol-Dev | +# |:---:|:---:|:---:| +# |Tensile $$\texttt{AT}_1$$| $$\sqrt{ \dfrac{3 G_\mathrm{c} E} {8 \ell}}$$| $$\sqrt{\dfrac{3 G_\mathrm{c} E}{8 \ell}}$$ | +# |Tensile $$\texttt{AT}_2$$| $$ \dfrac{3}{16} \sqrt{ \dfrac{3 G_\mathrm{c} E } { \ell}}$$ | $$\dfrac{3}{16} \sqrt{\dfrac{3 G_\mathrm{c} E}{\ell}}$$ | +# |Compressive $$\texttt{AT}_1$$| $$-\sqrt{ \dfrac{3 G_\mathrm{c} E} {8 \ell}}$$ | $$- \sqrt{ \dfrac{9 G_\mathrm{c} E} {16 \ell(1+\nu)}}$$ | +# |Compressive $$\texttt{AT}_2$$| $$ -\dfrac{3}{16} \sqrt{ \dfrac{3 G_\mathrm{c} E } { \ell}}$$ | $$ -\dfrac{9}{32} \sqrt{\dfrac{2 G_\mathrm{c} E}{ \ell (1+\nu)}}$$ | + +# %% [markdown] +# ## Define some helper functions + +# %% +import os +import time +from types import MethodType +from xml.dom import minidom + +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +import pyvista as pv + +# %% +data_dir = os.environ.get("OGS_DATA_DIR", "../../..") + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +output_dir = out_dir + +# define method to be assigned to model, to replace a specific curve, given by name +# (analogue to replace_parameter method) + + +def replace_curve( + self, + name=None, + value=None, + coords=None, + parametertype=None, + valuetag="values", + coordstag="coords", +): + root = self._get_root() + parameterpath = "./curves/curve" + parameterpointer = self._get_parameter_pointer(root, name, parameterpath) + self._set_type_value(parameterpointer, value, parametertype, valuetag=valuetag) + self._set_type_value(parameterpointer, coords, parametertype, valuetag=coordstag) + + +# define method to change timstepping in project file + + +def set_timestepping(model, repeat_list, delta_t_list): + model.remove_element( + xpath="./time_loop/processes/process/time_stepping/timesteps/pair" + ) + for i in range(len(repeat_list)): + model.add_block( + blocktag="pair", + parent_xpath="./time_loop/processes/process/time_stepping/timesteps", + taglist=["repeat", "delta_t"], + textlist=[repeat_list[i], delta_t_list[i]], + ) + + +# %% [markdown] +# ## Define function generating mesh, modifying project file and running ogs with given parameters + + +# %% +def ogs_beam( + phasefield_model, + energy_split_model, + mesh_size=0.01, + length_scale=0.02, + bc_displacement=5, + ts_coords="0 0.05 1", + values="0 0.25 1", + repeat_list=None, + delta_t_list=None, + hypre=False, +): + ##phasefield_model: 'AT1' or 'AT2' + ##energy_split_model: 'VolumetricDeviatoric' or 'Isotropic' + + without_hypre = "-ksp_type cg -pc_type bjacobi -ksp_atol 1e-14 -ksp_rtol 1e-14" + with_hypre = "-ksp_type cg -pc_type hypre -pc_hypre_type boomeramg -pc_hypre_boomeramg_strong_threshold 0.7 -ksp_atol 1e-8 -ksp_rtol 1e-8" + # file's name + prj_name = "beam.prj" + print(f"> Running beam model {phasefield_model} - {energy_split_model} ... <") + logfile = f"{out_dir}/log_{phasefield_model}_{energy_split_model}.txt" # noqa: F841 + # beam dimensions + beam_height = 0.05 + beam_depth = beam_height # noqa: F841 + beam_length = 1.0 # noqa: F841 + # mesh properties + h = mesh_size # noqa: F841, distance between nodes + ls = length_scale + # generate prefix from properties + if energy_split_model == "VolumetricDeviatoric": + prefix = phasefield_model + "_vd" + elif energy_split_model == "Isotropic": + prefix = phasefield_model + "_iso" + else: + raise ValueError( + '"' + + energy_split_model + + '"' + + ' is no valid input for energy_split_model, choose between "VolumetricDeviatoric" and "Isotropic"' + ) + if bc_displacement > 0: + prefix = prefix + "_tensile" + else: + prefix = prefix + "_compressive" + # generate mesh + ! generateStructuredMesh -o {out_dir}/bar_.vtu -e hex --lx {beam_length} --nx {round(beam_length/h)} --ly {beam_height} --ny {round(beam_height/h)} --lz {beam_depth} --nz {round(beam_depth/h)} > {logfile} + ! NodeReordering -i {out_dir}/bar_.vtu -o {out_dir}/bar.vtu >> {logfile} + ! ExtractSurface -i {out_dir}/bar.vtu -o {out_dir}/bar_left.vtu -x 1 -y 0 -z 0 >> {logfile} + ! ExtractSurface -i {out_dir}/bar.vtu -o {out_dir}/bar_right.vtu -x -1 -y 0 -z 0 >> {logfile} + ! partmesh -s -o {out_dir} -i {out_dir}/bar.vtu >> {logfile} + ! partmesh -m -n 3 -o {out_dir} -i {out_dir}/bar.vtu -- {out_dir}/bar_right.vtu {out_dir}/bar_left.vtu >> {logfile} + # change properties in prj file + model = ot.Project( + input_file=prj_name, output_file=f"{out_dir}/{prj_name}", MKL=True + ) + model.replace_parameter_value(name="ls", value=ls) + model.replace_text(phasefield_model, xpath="./processes/process/phasefield_model") + model.replace_text( + energy_split_model, xpath="./processes/process/energy_split_model" + ) + model.replace_text(prefix, xpath="./time_loop/output/prefix") + model.replace_parameter_value(name="dirichlet_right", value=bc_displacement) + model.replace_curve = MethodType(replace_curve, model) + model.replace_curve(name="dirichlet_time", value=values, coords=ts_coords) + if repeat_list is not None and delta_t_list is not None: + set_timestepping(model, repeat_list, delta_t_list) + else: + set_timestepping(model, ["1"], ["1e-2"]) + if hypre is True: + model.replace_text( + with_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + else: + model.replace_text( + without_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + model.write_input() + # run ogs + t0 = time.time() + print(" > OGS started execution ...") + ! mpirun -n 3 ogs {out_dir}/{prj_name} -o {output_dir} >> {logfile} + tf = time.time() + print(" > OGS terminated execution. Elapsed time: ", round(tf - t0, 2), " s.") + + +# %% [markdown] +# ## Input data +# +# The values of the material properties are choosen as follows. +# +# +# | **Name** | **Value** | **Unit** | **Symbol** | +# |--------------------------------|--------------------|--------------|------------| +# | _Young's modulus_ | 1 | Pa | $E$ | +# | _Critical energy release rate_ | 1 | Pa$\cdot$m | $G_{c}$ | +# | _Poisson's ratio_ | 0.15 | $-$ | $\nu$ | +# | _Regularization parameter_ | 3$h$ | m | $\ell$ | +# | _Length_ | $1$ | m | $L$ | +# | _Height_ | $0.05$ | m | $H$ | +# | _Depth_ | $0.05$ | m | $D$ | +# + +# %% [markdown] +# ## Run Simulations + +# %% +pf_ms = ["AT1", "AT2"] +es_ms = ["VolumetricDeviatoric", "Isotropic"] +displ = [4.0, -4.0] +""" +for a in pf_ms: + for c in displ: + ogs_beam(a,es_ms[1],bc_displacement = c,mesh_size = 0.01, length_scale = 0.03) +ogs_beam(pf_ms[1],es_ms[0],bc_displacement = 4,mesh_size = 0.01, length_scale = 0.03) + +# run AT1_vd_tensile with smaller timesteps in critical time range +ogs_beam(pf_ms[0],es_ms[0],bc_displacement = 5,mesh_size = 0.01, length_scale = 0.03,repeat_list=['62','2','20','1'], delta_t_list=['1e-2','1e-3','1e-4','1e-2']) + +# run VolumetricDeviatoric in Compression with Hypre and smaller timesteps in critical time range +ogs_beam(pf_ms[1],es_ms[0],bc_displacement = -4.5,mesh_size = 0.01, length_scale = 0.03, hypre = True, repeat_list=['70','4','30','1'], delta_t_list=['1e-2','1e-3','1e-4','1e-2']) + +# loosen relative error tolerance for displacement process in order to get convergence for the AT1 case +prj_path='./' +prj_name = "beam.prj" +model = ot.Project(input_file=prj_path+prj_name, output_file=prj_path+prj_name, MKL=True) +model.replace_text('1e-6', xpath="./time_loop/processes/process/convergence_criterion/reltol",occurrence=0) +model.write_input() +ogs_beam(pf_ms[0],es_ms[0],bc_displacement = -4.95, mesh_size = 0.01, length_scale = 0.03, hypre= True, repeat_list=['66', '8','3','3','20','1'], delta_t_list=['1e-2','1e-3','1e-4','1e-5','1e-6','1e-2'],ts_coords='0 0.1 1', values ='0 0.5 1') +model = ot.Project(input_file=prj_path+prj_name, output_file=prj_path+prj_name, MKL=True) +model.replace_text('1e-14', xpath="./time_loop/processes/process/convergence_criterion/reltol",occurrence=0) +model.write_input() +""" +## run only cases easy to handle with coarse timestepping: +for a in pf_ms: + for b in es_ms: + for c in displ: + if a == "AT1" and b == "VolumetricDeviatoric": + continue + if a == "AT2" and b == "VolumetricDeviatoric" and c < 0: + ogs_beam( + a, + b, + bc_displacement=c, + mesh_size=0.01, + length_scale=0.03, + hypre=True, + repeat_list=["1"], + delta_t_list=["1e-1"], + ) + else: + ogs_beam( + a, + b, + bc_displacement=c, + mesh_size=0.01, + length_scale=0.03, + repeat_list=["1"], + delta_t_list=["1e-1"], + ) + + +# %% [markdown] +# ## Results +# +# The final timestep of the AT1 iso tensile case is shown exemplary for the phasefield after fracture. +# +#  + +# %% [markdown] +# ## Post-processing +# The force applied to the beam is compared to the change in length of the beam. + + +# %% +# define function to obtain displacement applied at the right end of the beam from rvtu file +def displ_right(filename): + data = pv.read(filename) + data.point_data["displacement"] + max_x = max(data.points[:, 0]) + return np.mean( + data.point_data["displacement"][:, 0], + where=np.transpose(data.points[:, 0] == max_x), + ) + + +# define fuction to obtain force acting on the right end of the beam from vtu file + + +def force_right(filename): + data = pv.read(filename) + data.point_data["NodalForces"] + max_x = max(data.points[:, 0]) + return np.sum( + data.point_data["NodalForces"][:, 0], + where=np.transpose(data.points[:, 0] == max_x), + ) + + +# define function applying obove functions on all vtu file listet in pvd file, returning force-displacement curve + + +def force_displ_from_pvd(pvd): + doc = minidom.parse(str(pvd)) + DataSets = doc.getElementsByTagName("DataSet") + vtu_files = [x.getAttribute("file") for x in DataSets] + forces_right = [force_right(f"{out_dir}/{x}") for x in vtu_files] + displs_right = [displ_right(f"{out_dir}/{x}") for x in vtu_files] + return [forces_right, displs_right] + + +# %% [markdown] +# ## Plot force-strain curves + +# %% +prefixes = [ + "AT1_vd_tensile", + "AT1_iso_tensile", + "AT2_vd_tensile", + "AT2_iso_tensile", + "AT1_vd_compressive", + "AT1_iso_compressive", + "AT2_vd_compressive", + "AT2_iso_compressive", +] +labels = [ + r"$\texttt{AT}_1$ vol-dev tensile", + r"$\texttt{AT}_1$ iso tensile", + r"$\texttt{AT}_2$ vol-dev tensile", + r"$\texttt{AT}_2$ iso tensile", + r"$\texttt{AT}_1$ vol-dev compressive", + r"$\texttt{AT}_1$ iso compressive", + r"$\texttt{AT}_2$ vol-dev compressive", + r"$\texttt{AT}_2$ iso compressive", +] +ls = ["-", "--"] +colors = [ + "#ffdf4d", + "#006ddb", + "#8f4e00", + "#ff6db6", + "#920000", + "#b66dff", + "#db6d00", + "#490092", +] + +fig, ax = plt.subplots() +plt.rc("text", usetex=True) +fig.set_size_inches(18.5, 10.5) +for i, pre in enumerate(prefixes): + pvd = output_dir / f"{pre}.pvd" + if pvd.is_file(): + curve = force_displ_from_pvd(pvd) + ax.plot( + curve[1], + curve[0], + ls[i % 2], + label=labels[i], + linewidth=5, + color=colors[i], + alpha=1, + ) + +plt.rcParams["xtick.labelsize"] = 16 +plt.rcParams["ytick.labelsize"] = 16 +ax.grid(linestyle="dashed") +ax.set_xlabel(r"$\Delta [m]$", fontsize=18) +ax.set_ylabel("$F_y [N]$", fontsize=18) +plt.legend(fontsize=18, ncol=2) +ax.axhline(y=0, color="black", linewidth=1) +ax.axvline(x=0, color="black", linewidth=1) +ax.set_xlim(-4.5, 4.5) + +# %% [markdown] +# Running all the cases with finer timesteps yields the following figure. The failure of the beam in the simulation is observed, when the loading reaches the analytical strength for the corresponding case. +#  diff --git a/Tests/Data/PhaseField/kregime_jupyter_notebook/Kregime_Static_jupyter.py b/Tests/Data/PhaseField/kregime_jupyter_notebook/Kregime_Static_jupyter.py new file mode 100644 index 0000000000000000000000000000000000000000..9f82884bea14209b2cdd59a90cf250dceb8ae6b4 --- /dev/null +++ b/Tests/Data/PhaseField/kregime_jupyter_notebook/Kregime_Static_jupyter.py @@ -0,0 +1,448 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Mostafa Mollaali, Keita Yoshioka" +# date = "2022-12-06" +# title = "Static fracture opening under a constant pressure – Sneddon solution" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# ## Problem description +# +# ### Static fracture opening under a constant pressure +# +# Consider a line fracture $[-a_0, a_0] \times \{0\}$ ($a_0$ = 0.1) with no external loading and an internal fluid pressure of $p=1$ was applied to the fracture surfaces. The results are compared to the analytical solution (**Sneddon et al., 1969**) for the fracture half-opening: +# \begin{eqnarray} +# u(x,0) = \frac{2 p a_0}{ E'} \sqrt{1-(x/a_0)^2} +# , +# \label{eq:sneddon_2D_static} +# \end{eqnarray} +# +# where $u$ is the displacement $E'$ is the plane strain Young's modulus ($E' = E/(1-\nu^2)$) with $\nu$ is Poisson’s ratio, $p$ is the fluid pressure inside the fracture. To account for the infinite boundaries in the closed-form solution, we considered a large finite domain $ [-10a_o,10a_o] \times [-10a_o,10a_o]$. The effective element size, $h$, is $1\times10^{-2}$. +# + +# %% [markdown] +#  + +# %% [markdown] +# * In order to have a static pressurized fracture, add, $\texttt{<pressurized_crack_scheme>static</pressurized_crack_scheme>}$ in the project file. It's important to note that static pressurized fracture implementation assumes $p=1$. **Yoshioka _et al._, 2019** discussed using real material properties and rescaling the phase-field energy functional. +# + +# %% [markdown] +# # Input data + +# %% [markdown] +# The material and geometrical properties are listed in the Table below. It's worth noting that the properties are dimensionless and scaled. + +# %% [markdown] +# | **Name** | **Value** | **Symbol** | +# |--------------------------------|------------------ |------------| +# | _Young's modulus_ | 1 | $E$ | +# | _Poisson's ratio_ | 0.15 | $\nu$ | +# | _Fracture toughness_ | 1 | $G_{c}$ | +# | _Regularization parameter_ | 2$h$ | $\ell_s$ | +# | _Pressure_ | 1 | $p$ | +# | _Length_ | 2 | $L$ | +# | _Height_ | 2 | $H$ | +# | _Initial crack length_ | 0.2 | $2a_0$ | + +# %% +import math +import os +import time + +import gmsh +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +from ogstools.msh2vtu import msh2vtu + +pi = math.pi +plt.rcParams["text.usetex"] = True + +# %% +E = 1.0 +nu = 0.15 +Gc = 1.0 +P = 1.0 +h = 0.01 + + +Orientation = 0 +a0 = 0.1 # half of the initial crack length +n_slices = ( + 2 * (round(3.0 * a0 / h)) + 1 +) # number of slices for calcute width of fracture + +phasefield_model = "AT1" + +# %% +h_list = [0.01] # list of mesh sizes (h) +# h_list =[0.01, 0.005, 0.0025] # list of mesh sizes (h), for mesh sensitivity + +# %% [markdown] +# # Output directory and project file + +# %% +# file's name +prj_name = "Kregime_Static.prj" +meshname = "mesh_full_pf" + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + + +# %% [markdown] +# # Mesh generation +# + + +# %% +def mesh_generation(lc, lc_fine): + """ + lc ... characteristic length for coarse meshing + lc_fine ... characteristic length for fine meshing + """ + L = 4.0 # Length + H = 4.0 # Height + b = 0.4 # Length/Height of subdomain with fine mesh + + # Before using any functions in the Python API, Gmsh must be initialized + gmsh.initialize() + gmsh.option.setNumber("General.Terminal", 1) + gmsh.model.add("rectangle") + + # Dimensions + dim1 = 1 + dim2 = 2 + + # Points + gmsh.model.geo.addPoint(-L / 2, -H / 2, 0, lc, 1) + gmsh.model.geo.addPoint(L / 2, -H / 2, 0, lc, 2) + gmsh.model.geo.addPoint(L / 2, H / 2, 0, lc, 3) + gmsh.model.geo.addPoint(-L / 2, H / 2, 0, lc, 4) + gmsh.model.geo.addPoint(-b, -b - lc_fine / 2, 0, lc_fine, 5) + gmsh.model.geo.addPoint(b, -b - lc_fine / 2, 0, lc_fine, 6) + gmsh.model.geo.addPoint(b, b + lc_fine / 2, 0, lc_fine, 7) + gmsh.model.geo.addPoint(-b, b + lc_fine / 2, 0, lc_fine, 8) + + # Lines + gmsh.model.geo.addLine(1, 2, 1) + gmsh.model.geo.addLine(2, 3, 2) + gmsh.model.geo.addLine(3, 4, 3) + gmsh.model.geo.addLine(4, 1, 4) + gmsh.model.geo.addLine(5, 6, 5) + gmsh.model.geo.addLine(6, 7, 6) + gmsh.model.geo.addLine(7, 8, 7) + gmsh.model.geo.addLine(8, 5, 8) + + # Line loops + gmsh.model.geo.addCurveLoop([1, 2, 3, 4], 1) + gmsh.model.geo.addCurveLoop([5, 6, 7, 8], 2) + + # Add plane surfaces defined by one or more curve loops. + gmsh.model.geo.addPlaneSurface([1, 2], 1) + gmsh.model.geo.addPlaneSurface([2], 2) + + gmsh.model.geo.synchronize() + + # Prepare structured grid + gmsh.model.geo.mesh.setTransfiniteCurve( + 6, math.ceil(2 * b / lc_fine + 2), "Progression", 1 + ) + gmsh.model.geo.mesh.setTransfiniteCurve( + 8, math.ceil(2 * b / lc_fine + 2), "Progression", 1 + ) + gmsh.model.geo.mesh.setTransfiniteSurface(2, "Alternate") + + gmsh.model.geo.mesh.setRecombine(dim2, 1) + gmsh.model.geo.mesh.setRecombine(dim2, 2) + + gmsh.model.geo.synchronize() + + # Physical groups + Bottom = gmsh.model.addPhysicalGroup(dim1, [1]) + gmsh.model.setPhysicalName(dim1, Bottom, "Bottom") + + Right = gmsh.model.addPhysicalGroup(dim1, [2]) + gmsh.model.setPhysicalName(dim1, Right, "Right") + + Top = gmsh.model.addPhysicalGroup(dim1, [3]) + gmsh.model.setPhysicalName(dim1, Top, "Top") + + Left = gmsh.model.addPhysicalGroup(dim1, [4]) + gmsh.model.setPhysicalName(dim1, Left, "Left") + + Computational_domain = gmsh.model.addPhysicalGroup(dim2, [1, 2]) + gmsh.model.setPhysicalName(dim2, Computational_domain, "Computational_domain") + gmsh.model.geo.synchronize() + + output_file = f"{out_dir}/" + meshname + ".msh" + gmsh.model.mesh.generate(dim2) + gmsh.write(output_file) + gmsh.finalize() + + +# %% [markdown] +# # Pre process +# + + +# %% +def pre_processing(h, a0): + mesh = pv.read(f"{out_dir}/mesh_full_pf_domain.vtu") + phase_field = np.ones((len(mesh.points), 1)) + pv.set_plot_theme("document") + + for node_id, x in enumerate(mesh.points): + if ( + (mesh.center[0] - x[0]) <= a0 + 0.001 * h + and (mesh.center[0] - x[0]) >= -a0 - 0.001 * h + and (mesh.center[1] - x[1]) < h / 2 + 0.001 * h + and (mesh.center[1] - x[1]) > -h / 2 - 0.001 * h + ): + phase_field[node_id] = 0.0 + + mesh.point_data["phase-field"] = phase_field + mesh.save(f"{out_dir}/mesh_full_pf_OGS_pf_ic.vtu") + + +# %% [markdown] +# # Run the simulation +# + +# %% +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + + +def sneddon_numerical(h): + # mesh properties + ls = 2 * h + # generate prefix from properties + filename = f"results_h_{h:0.4f}" + mesh_generation(0.1, h) + # Convert GMSH (.msh) meshes to VTU meshes appropriate for OGS simulation. + input_file = f"{out_dir}/" + meshname + ".msh" # noqa: F841 + msh2vtu(filename=input_file, output_path=out_dir, keep_ids=True) + # As a preprocessing step, define the initial phase-field (crack). + pre_processing(h, a0) + # change properties in prj file #For more information visit: https://ogstools.opengeosys.org/stable/reference/ogstools.ogs6py.html + model = ot.Project( + input_file=prj_name, + output_file=f"{out_dir}/{prj_name}", + MKL=True, + args=f"-o {out_dir}", + ) + + gml_file = Path("./Kregime_Static.gml").resolve() + + model.replace_parameter_value(name="ls", value=ls) + model.replace_text(gml_file, xpath="./geometry") + model.replace_text(filename, xpath="./time_loop/output/prefix") + model.write_input() + # run simulation with ogs + t0 = time.time() + print(">>> OGS started execution ... <<<") + + # !ogs {out_dir}/{prj_name} -o {out_dir} -m {out_dir} > {out_dir}/ogs-out.txt + assert _exit_code == 0 # noqa: F821 + tf = time.time() + print(">>> OGS terminated execution <<< Elapsed time: ", round(tf - t0, 2), " s.") + + +# %% +for h_j in h_list: + sneddon_numerical(h=h_j) + + +# %% [markdown] +# # Post processing + + +# %% +# As a post-process, we calculate the fracture opening. +def width_calculation(filename): + reader = pv.get_reader(f"{out_dir}/{filename}.pvd") + # -------------------------------------------------------------------------------- + # Active the results of last time step + # -------------------------------------------------------------------------------- + reader.set_active_time_value(reader.time_values[-1]) + mesh = reader.read()[0] + # -------------------------------------------------------------------------------- + # define grad d + # -------------------------------------------------------------------------------- + mesh_d = mesh.compute_derivative(scalars="phasefield") + mesh_d["gradient"] + + def gradients_to_dict(arr): + """A helper method to label the gradients into a dictionary.""" + keys = np.array( + ["grad_dx", "grad_dy", "grad_dz", "grad_dx", "grad_dy", "grad_dz"] + ) + keys = keys.reshape((2, 3))[:, : arr.shape[1]].ravel() + return dict(zip(keys, mesh_d["gradient"].T)) + + gradients_d = gradients_to_dict(mesh_d["gradient"]) + mesh.point_data.update(gradients_d) + # -------------------------------------------------------------------------------- + # define width at nodes + # -------------------------------------------------------------------------------- + disp = mesh.point_data["displacement"] + grad_dx = mesh.point_data["grad_dx"] + grad_dy = mesh.point_data["grad_dy"] + num_points = disp.shape + Wnode = np.zeros(num_points[0]) + for i, _x in enumerate(mesh.points): + u_x = disp[i][0] + u_y = disp[i][1] + gd_x = grad_dx[i] + gd_y = grad_dy[i] + + Wnode[i] = 0.5 * (u_x * gd_x + u_y * gd_y) + mesh.point_data["Wnode"] = Wnode + # -------------------------------------------------------------------------------- + # define width at nodes + # -------------------------------------------------------------------------------- + # Normalize the vector + normal = [np.cos(Orientation), np.sin(Orientation), 0] + # Make points along that vector for the extent of your slices + point_a = mesh.center + np.array([1.5 * a0, 0, 0]) + point_b = mesh.center + np.array([-1.5 * a0, 0, 0]) + dist_a_b = ((point_b[0] - point_a[0]) ** 2 + (point_b[1] - point_a[1]) ** 2) ** 0.5 + # Define the line/points for the slices + line = pv.Line(point_a, point_b, n_slices) + width_line = np.zeros(len(line.points)) + r_i = np.zeros(len(line.points)) + # Generate all of the slices + slices = pv.MultiBlock() + + for i, point in enumerate(line.points): + slices.append(mesh.slice(normal=normal, origin=point)) + slice_mesh = mesh.slice(normal=normal, origin=point) + y_slice = slice_mesh.points[:, 1] + Wnode_slice = slice_mesh.point_data["Wnode"] + width_i = np.trapz(Wnode_slice, x=y_slice) # noqa: NPY201 + if width_i >= 0: + width_line[i] = width_i + r_i[i] = ( + (point[0] - point_a[0]) ** 2 + (point[1] - point_a[1]) ** 2 + ) ** 0.5 - dist_a_b / 2 + + return r_i, width_line + + +# %% [markdown] +# # Sneddon + + +# %% +def sneddon(h, ls, phasefield_model): + # Effective a for AT1/A2 + if phasefield_model == "AT1": + a_eff = a0 * (1 + pi * ls / (4.0 * a0 * (3 * h / 8.0 / ls + 1.0))) + elif phasefield_model == "AT2": + a_eff = a0 * (1 + pi * ls / (4.0 * a0 * (h / (2.0 * ls) + 1.0))) + + x = np.linspace(-1.0 * a_eff, a_eff, 40) + uy = [] + for i in range(len(x)): + uy.append( + 2 * a_eff * (1 - nu**2) * P / E * math.sqrt(1.0 - ((x[i]) / (a_eff)) ** 2) + ) + + return x, uy, a_eff + + +# %% [markdown] +# ## Opening profile + +# %% +color = ["-.k", "ko", "-.r", "ro", "-.b", "bo", "-.g", "go"] +Label = ["Closed form solution", "VPF-FEM"] +lineWIDTH = [1.5, 1.5, 1.5] + +for j, h_j in enumerate(h_list): + r_i_num = [] + width_line_num = [] + filename = f"results_h_{h_j:0.4f}" + print(filename) + width_calculation(filename) + r_i_num = width_calculation(filename)[0] + width_line_num = width_calculation(filename)[1] + ls = 2 * h_j + + x_Sneddon = sneddon(h_j, ls, phasefield_model)[0] + uy_Sneddon = sneddon(h_j, ls, phasefield_model)[1] + a_eff_Sneddon = sneddon(h_j, ls, phasefield_model)[2] + plt.plot( + np.array(x_Sneddon[:]), + np.array(uy_Sneddon[:]), + color[2 * j], + fillstyle="none", + markersize=0, + linewidth=lineWIDTH[0], + label=f"Closed form solution - $h= ${h_j:0.4f}", + ) + plt.plot( + np.array(r_i_num[:]), + np.array(width_line_num[:]), + color[2 * j + 1], + fillstyle="none", + markersize=6, + linewidth=lineWIDTH[1], + label=f"VPF - $h =${h_j:0.4f}", + ) + # ------------------------------------------------------------------------ +plt.rcParams["figure.figsize"] = [40, 20] +plt.rcParams["figure.dpi"] = 1600 +plt.ylabel("$w_n$ [m]", fontsize=14) +plt.xlabel("$r$ [m]", fontsize=14) +plt.grid(linestyle="dashed") +plt.title(f"{phasefield_model}") + +legend = plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left") +plt.show() + +# %% [markdown] +# To decrease computing time, we perform the simulation with an one coarse mesh; Mesh sensitivity can be investigated in order to assess the convergence of the opening profile for various mesh discretizations. Following are the mesh sensivity results for for Models $\texttt{AT}_1$ and $\texttt{AT}_2$ ($h=0.01,~0.005,~\text{and} ~0.0025$ m.) + +# %% [markdown] +#  with different mesh sizes compared against the Sneddon solution.") +#  with different mesh sizes compared against the Sneddon solution.") + +# %% [markdown] +# +# Our mesh size sensitivity study shows even with coarse mesh. The results away from crack tips agree well with the Sneddon solution. Using finer mesh demonstrates that the opening along the whole crack length is accurate compared with the closed-form solution. +# +# It's worth noting that we estimate width as part of post-processing (using the line integral and a given crack normal vector). However, near crack tips the crack normal vector is different to the given normal vector, so the width values are inaccurate near crack tips. For the sake of simplicity, we neglect determining the normal crack vector here; for more information on the line integral approach, see **Yoshioka et al., 2020**. + +# %% [markdown] +# ## References +# +# [1] Yoshioka, Keita, Francesco Parisio, Dmitri Naumov, Renchao Lu, Olaf Kolditz, and Thomas Nagel. _Comparative verification of discrete and smeared numerical approaches for the simulation of hydraulic fracturing._ GEM-International Journal on Geomathematics **10**, no. 1 (2019): 1-35. +# +# [2] Yoshioka, Keita, Dmitri Naumov, and Olaf Kolditz. _On crack opening computation in variational phase-field models for fracture._ Computer Methods in Applied Mechanics and Engineering 369 (2020): 113210. +# +# [3] Sneddon, Ian Naismith, and Morton Lowengrub. _Crack problems in the classical theory of elasticity._ 1969, 221 P (1969). +# diff --git a/Tests/Data/PhaseField/surfing_jupyter_notebook/surfing_pyvista.py b/Tests/Data/PhaseField/surfing_jupyter_notebook/surfing_pyvista.py new file mode 100644 index 0000000000000000000000000000000000000000..0933d4515e86530d3ecbc669f3ac04f1779439b3 --- /dev/null +++ b/Tests/Data/PhaseField/surfing_jupyter_notebook/surfing_pyvista.py @@ -0,0 +1,589 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Mostafa Mollaali, Keita Yoshioka" +# date = "2022-06-28" +# title = "Surfing boundary" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# ## Problem description +# +# Consider a plate, $\Omega=[0,2]\times [-0.5,0.5]$, with an explicit edge crack, $\Gamma=[0,0.5]\times \{0\}$; that is subjected to a time dependent crack opening displacement: +# +# \begin{eqnarray} +# \label{eq:surfing_bc} +# \mathbf{u}(x,y,t)= \mathbf{U}(x-\text{v}t,y) \quad \text{on} \quad \partial\Omega_D, +# \end{eqnarray} +# where $\text{v}$ is an imposed loading velocity; and $\mathbf{U}$ is the asymptotic solution for the Mode-I crack opening displacement +# \begin{eqnarray} +# \label{eq:asymptotic} +# U_x= \dfrac{K_I}{2\mu} \sqrt{\dfrac{r}{2\pi}} (\kappa-\cos \varphi) \cos \frac{\varphi}{2}, \nonumber +# \\ +# U_y= \dfrac{K_I}{2\mu} \sqrt{\dfrac{r}{2\pi}} (\kappa-\cos \varphi) \sin \frac{\varphi}{2}, +# \end{eqnarray} +# +# +# where $K_I$ is the stress intensity factor, $\kappa=(3-\nu)/(1+\nu)$ and $\mu=E / 2 (1 + \nu) $; $(r,\varphi)$ are the polar coordinate system, where the origin is crack tip. +# Also, we used $G_\mathrm{c}=K_{Ic}^2(1-\nu^2)/E$ as the fracture surface energy under plane strain condition. +# Table 1 lists the material properties and geometry of the numerical model. +# +#  + +# %% [markdown] +# # Input Data + +# %% [markdown] +# +# <!-- <head> +# <style type='text/css'> +# table.test { border-collapse: collapse; } +# table.test td { border-bottom: 1px solid black; } +# </style> +# </head> +# <center> +# <body> +# <table class='test'> +# <caption> Table 1: Surfing boundary example: Material properties and geometrical parameters.</caption> +# <tr style="border-bottom:2px solid black"> +# <td colspan="100%"></td> +# </tr> +# <tr> +# <td >Name</td> +# <td>Symbol</td> +# <td style="width:40%">Value </td> +# <td>Unit</td> +# </tr> +# <tr style="border-bottom:2px solid black"> +# <td colspan="100%"></td> +# </tr> +# <tr> +# <td>Young's modulus</td> +# <td>$E$</td> +# <td>210 $\times 10^3$</td> +# <td>MPa</td> +# </tr> +# <tr> +# <td>Critical energy release rate </td> +# <td>$G_{c}$</td> +# <td>2.7</td> +# <td>MPa$\cdot$mm</td> +# </tr> +# <tr> +# <td>Poisson's ratio</td> +# <td>$\nu$</td> +# <td>0.3</td> +# <td>$-$</td> +# </tr> +# <tr> +# <td>Effective element size</td> +# <td>$h$</td> +# <td>$5 \times 10^{-3}$</td> +# <td>mm</td> +# </tr> +# <tr> +# <td>Regularization parameter</td> +# <td>$\ell_s$</td> +# <td>$1\times10^{-2}$</td> +# <td>mm</td> +# </tr> +# <tr> +# <td>Imposed loading velocity</td> +# <td>$\text{v}$</td> +# <td>1.5</td> +# <td>mm/s</td> +# </tr> +# <tr> +# <td>Length</td> +# <td>$L$</td> +# <td>$2$</td> +# <td>mm</td> +# </tr> +# <tr> +# <td>Height</td> +# <td>$H$</td> +# <td>$1$</td> +# <td>mm</td> +# </tr> +# <tr> +# <td>Initial crack length</td> +# <td>$a_0$</td> +# <td>$0.5$</td> +# <td>mm</td> +# </tr> +# <tr style="border-bottom:2px solid black"> +# <td colspan="100%"></td> +# </tr> +# </table> +# </body> +# </center> --> +# +# | **Name** | **Value** | **Unit** | **Symbol** | +# |--------------------------------|--------------------|--------------|------------| +# | _Young's modulus_ | 210x$10^3$ | MPa | $E$ | +# | _Critical energy release rate_ | 2.7 | MPa$\cdot$mm | $G_{c}$ | +# | _Poisson's ratio_ | 0.3 | $-$ | $\nu$ | +# | _Regularization parameter_ | 2$h$ | mm | $\ell_s$ | +# | _Imposed loading velocity_ | 1.5 | mm/s | $\text{v}$ | +# | _Length_ | $2$ | mm | $L$ | +# | _Height_ | $1$ | mm | $H$ | +# | _Initial crack length_ | $0.5$ | mm | $a_0$ | + +# %% +x_tip_Initial = 0.5 +y_tip_Initial = 0.5 +Height = 1.0 + +Orientation = 0 +h = 0.05 +G_i = 2.7 +ls = 2 * h +# We set ls=2h in our simulation +phasefield_model = "AT1" # AT1 and AT2 + +# %% [markdown] +# ## Paths and project file name + +# %% +import os + +# file's name +prj_name = "surfing.prj" + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% [markdown] +# # Mesh generation + +# %% +# https://www.opengeosys.org/docs/tools/meshing/structured-mesh-generation/ +! generateStructuredMesh -o {out_dir}/surfing_quad_1x2.vtu -e quad --lx 2 --nx {round(2/h)+1} --ly 1 --ny {round(1/h)+1} +! NodeReordering -i {out_dir}/surfing_quad_1x2.vtu -o {out_dir}/surfing_quad_1x2_NR.vtu + +# %% [markdown] +# # Pre-processing +# At fracture, we set the initial phase field to zero. + +# %% +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + +import numpy as np + +mesh = pv.read(f"{out_dir}/surfing_quad_1x2_NR.vtu") +phase_field = np.ones((len(mesh.points), 1)) + + +for node_id, x in enumerate(mesh.points): + if ( + x[0] < x_tip_Initial + h / 10 + and x[1] < Height / 2 + h + and x[1] > Height / 2 - h + ): + phase_field[node_id] = 0.0 + +mesh.point_data["pf-ic"] = phase_field +mesh.save(f"{out_dir}/surfing_quad_1x2_NR_pf_ic.vtu") + +pf_ic = mesh.point_data["pf-ic"] +sargs = { + "title": "pf-ic", + "title_font_size": 20, + "label_font_size": 15, + "n_labels": 5, + "position_x": 0.24, + "position_y": 0.0, + "fmt": "%.1f", + "width": 0.5, +} +clim = [0, 1.0] + +p = pv.Plotter(shape=(1, 1), border=False) +p.add_mesh( + mesh, + scalars=pf_ic, + show_edges=True, + show_scalar_bar=True, + colormap="coolwarm", + clim=clim, + scalar_bar_args=sargs, +) + +p.view_xy() +p.camera.zoom(1.5) +p.window_size = [800, 400] +p.show() + +# %% [markdown] +# # Run the simulation + +# %% +import ogstools as ot + +# Change the length scale and phasefield model in project file +model = ot.Project( + input_file=prj_name, + output_file=f"{out_dir}/{prj_name}", + MKL=True, + args=f"-o {out_dir}", +) + +gml_file = Path("./surfing.gml").resolve() + +model.replace_parameter_value(name="ls", value=2 * h) +model.replace_text(phasefield_model, xpath="./processes/process/phasefield_model") +model.replace_text(gml_file, xpath="./geometry") +model.replace_text(Path("./Surfing_python.py").resolve(), xpath="./python_script") +model.write_input() + +import time + +t0 = time.time() +print(">>> OGS started execution ... <<<") +! ogs {out_dir}/{prj_name} -o {out_dir} -m {out_dir} > {out_dir}/ogs-out.txt +assert _exit_code == 0 # noqa: F821 + +tf = time.time() +print(">>> OGS terminated execution <<< Elapsed time: ", round(tf - t0, 2), " s.") + +# %% [markdown] +# # Results + +# %% [markdown] +# We computed the energy release rate using $G_{\theta}$ method (Destuynder _et al._, 1983; Li _et al._, 2016) and plot the errors against the theoretical numerical toughness i.e. $(G_c^{\text{eff}})_{\texttt{num}}=G_c(1+\frac{h}{2\ell})$ for $\texttt{AT}_2$, +# and $(G_c^{\text{eff}})_{\texttt{num}}=G_c(1+\frac{3h}{8\ell})$ for $\texttt{AT}_1$ (Bourdin _et al._, 2008). +# +# $, 0 outside, and a linear interpolation in between. We set $r_{in}=4\ell$ and $r_{out}=2.5r_{in}$ (see Li et al., 2016).") + +# %% [markdown] +# We computed the energy release rate using $G_{\theta}$ method (Destuynder _et al._, 1983; Li _et al._, 2016) and plot the errors against the theoretical numerical toughness i.e. $(G_c^{\text{eff}})_{\texttt{num}}=G_c(1+\frac{h}{2\ell})$ for $\texttt{AT}_2$, +# and $(G_c^{\text{eff}})_{\texttt{num}}=G_c(1+\frac{3h}{8\ell})$ for $\texttt{AT}_1$ (Bourdin _et al._, 2008). +# +# $, 0 outside, and a linear interpolation in between. We set $r_{in}=4\ell$ and $r_{out}=2.5r_{in}$ (see Li et al., 2016).") + +# %% +R_inn = 4 * ls +R_out = 2.5 * R_inn + +if phasefield_model == "AT1": + G_eff = G_i * (1 + 3 * h / (8 * ls)) +elif phasefield_model == "AT2": + G_eff = G_i * (1 + h / (2 * ls)) + +# %% [markdown] +# We run the simulation with a coarse mesh here to reduce computing time; however, a finer mesh would give a more accurate results. The energy release rate and its error for Models $\texttt{AT}_1$ and $\texttt{AT}_2$ with a mesh size of $h=0.005$ are shown below. + +# %% [markdown] +#  +#  + +# %% [markdown] +# # Post-processing + +# %% +from scipy.spatial import Delaunay + +reader = pv.get_reader(f"{out_dir}/surfing.pvd") +G_theta_time = np.zeros((len(reader.time_values), 2)) + + +for t, time_value in enumerate(reader.time_values): + reader.set_active_time_value(time_value) + + mesh = reader.read()[0] + points = mesh.point_data["phasefield"].shape[0] + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + pf = mesh.point_data["phasefield"] + sigma = mesh.point_data["sigma"] + disp = mesh.point_data["displacement"] + + num_points = disp.shape + theta = np.zeros(num_points) + + # -------------------------------------------------------------------------------- + # find fracture tip + # -------------------------------------------------------------------------------- + min_pf = min(pf[:]) + coord_pf_0p5 = mesh.points[pf < 0.5] + if min_pf <= 0.5: + coord_pf_0p5[np.argmax(coord_pf_0p5, axis=0)[0]][1] + x0 = coord_pf_0p5[np.argmax(coord_pf_0p5, axis=0)[0]][0] + y0 = coord_pf_0p5[np.argmax(coord_pf_0p5, axis=0)[0]][1] + else: + x0 = x_tip_Initial + y0 = y_tip_Initial + Crack_position = [x0, y0] + # -------------------------------------------------------------------------------- + # define \theta + # -------------------------------------------------------------------------------- + for i, x in enumerate(mesh.points): + # distance from the crack tip + R = np.sqrt((x[0] - Crack_position[0]) ** 2 + (x[1] - Crack_position[1]) ** 2) + if R_inn > R: + theta_funct = 1.0 + elif R_out < R: + theta_funct = 0.0 + else: + theta_funct = (R - R_out) / (R_inn - R_out) + theta[i][0] = theta_funct * np.cos(Orientation) + theta[i][1] = theta_funct * np.sin(Orientation) + + mesh.point_data["theta"] = theta + + # -------------------------------------------------------------------------------- + # define grad \theta + # -------------------------------------------------------------------------------- + mesh_theta = mesh.compute_derivative(scalars="theta") + mesh_theta["gradient"] + + keys = np.array( + ["thetax_x", "thetax_y", "thetax_z", "thetay_x", "thetay_y", "thetay_z"] + ) + keys = keys.reshape((2, 3))[:, : mesh_theta["gradient"].shape[1]].ravel() + gradients_theta = dict(zip(keys, mesh_theta["gradient"].T)) + mesh.point_data.update(gradients_theta) + # -------------------------------------------------------------------------------- + # define grad u + # -------------------------------------------------------------------------------- + mesh_u = mesh.compute_derivative(scalars="displacement") + mesh_u["gradient"] + + keys = np.array(["Ux_x", "Ux_y", "Ux_z", "Uy_x", "Uy_y", "Uy_z"]) + keys = keys.reshape((2, 3))[:, : mesh_u["gradient"].shape[1]].ravel() + gradients_u = dict(zip(keys, mesh_u["gradient"].T)) + mesh.point_data.update(gradients_u) + + # -------------------------------------------------------------------------------- + # define G_theta + # -------------------------------------------------------------------------------- + G_theta_i = np.zeros(num_points[0]) + sigma = mesh.point_data["sigma"] + Ux_x = mesh.point_data["Ux_x"] + Ux_y = mesh.point_data["Ux_y"] + Uy_x = mesh.point_data["Uy_x"] + Uy_y = mesh.point_data["Uy_y"] + + thetax_x = mesh.point_data["thetax_x"] + thetax_y = mesh.point_data["thetax_y"] + thetay_x = mesh.point_data["thetay_x"] + thetay_y = mesh.point_data["thetay_y"] + + for i, _x in enumerate(mesh.points): + # --------------------------------------------------------------------------- + sigma_xx = sigma[i][0] + sigma_yy = sigma[i][1] + sigma_xy = sigma[i][3] + + Ux_x_i = Ux_x[i] + Ux_y_i = Ux_y[i] + Uy_x_i = Uy_x[i] + Uy_y_i = Uy_y[i] + + thetax_x_i = thetax_x[i] + thetax_y_i = thetax_y[i] + thetay_x_i = thetay_x[i] + thetay_y_i = thetay_y[i] + # --------------------------------------------------------------------------- + dUdTheta_11 = Ux_x_i * thetax_x_i + Ux_y_i * thetay_x_i + dUdTheta_12 = Ux_x_i * thetax_y_i + Ux_y_i * thetay_y_i + dUdTheta_21 = Uy_x_i * thetax_x_i + Uy_y_i * thetay_x_i + dUdTheta_22 = Uy_x_i * thetax_y_i + Uy_y_i * thetay_y_i + trace_sigma_grad_u_grad_theta = ( + sigma_xx * dUdTheta_11 + + sigma_xy * (dUdTheta_12 + dUdTheta_21) + + sigma_yy * dUdTheta_22 + ) + trace_sigma_grad_u = ( + sigma_xx * Ux_x_i + sigma_xy * (Uy_x_i + Ux_y_i) + sigma_yy * Uy_y_i + ) + div_theta_i = thetax_x_i + thetay_y_i + G_theta_i[i] = ( + trace_sigma_grad_u_grad_theta - 0.5 * trace_sigma_grad_u * div_theta_i + ) + mesh.point_data["G_theta_node"] = G_theta_i + # -------------------------------------------------------------------------------- + # Integral G_theta + # -------------------------------------------------------------------------------- + X = mesh.points[:, 0] + Y = mesh.points[:, 1] + G_theta_i = mesh.point_data["G_theta_node"] + + domain_points = np.array(list(zip(X, Y))) + tri = Delaunay(domain_points) + + def area_from_3_points(x, y, z): + return np.sqrt(np.sum(np.cross(x - y, x - z), axis=-1) ** 2) / 2 + + G_theta = 0 + for vertices in tri.simplices: + mean_value = ( + G_theta_i[vertices[0]] + G_theta_i[vertices[1]] + G_theta_i[vertices[2]] + ) / 3 + area = area_from_3_points( + domain_points[vertices[0]], + domain_points[vertices[1]], + domain_points[vertices[2]], + ) + G_theta += mean_value * area + G_theta_time[t][1] = G_theta + G_theta_time[t][0] = time_value +mesh.save(f"{out_dir}/surfing_Post_Processing.vtu") + +# %% [markdown] +# ## Plots + +# %% +import matplotlib.pyplot as plt + +plt.xlabel("$t$", fontsize=14) +plt.ylabel( + r"$\frac{|{G}_\mathrm{\theta}-({G}_\mathrm{c}^{\mathrm{eff}})_\mathrm{num}|}{({G}_\mathrm{c}^{\mathrm{eff}})_\mathrm{num}}\times 100\%$", + fontsize=14, +) +plt.plot( + G_theta_time[:, 0], + abs(G_theta_time[:, 1]) / G_eff, + "-ob", + fillstyle="none", + linewidth=1.5, + label=f"Phase-field {phasefield_model}", +) +plt.plot( + G_theta_time[:, 0], + np.append(0, np.ones(len(G_theta_time[:, 0]) - 1)), + "-k", + fillstyle="none", + linewidth=1.5, + label="Closed form", +) +plt.grid(linestyle="dashed") +plt.xlim(-0.05, 0.8) +legend = plt.legend(loc="lower right") +plt.show() + +plt.xlabel("$t$", fontsize=14) +plt.ylabel( + r"$\frac{|{G}_\mathrm{\theta}-({G}_\mathrm{c}^{\mathrm{eff}})_\mathrm{num}|}{({G}_\mathrm{c}^{\mathrm{eff}})_\mathrm{num}}\times 100\%$", + fontsize=14, +) +plt.plot( + G_theta_time[:, 0], + abs(G_theta_time[:, 1] - G_eff) / G_eff * 100, + "-ob", + fillstyle="none", + linewidth=1.5, + label=f"Phase-field {phasefield_model}", +) +plt.grid(linestyle="dashed") +plt.xlim(-0.05, 0.8) +# plt.ylim(0,4) +legend = plt.legend(loc="upper right") +plt.show() + +# %% [markdown] +# Hint: Accurate results can be obtained by using the mesh size below 0.02. + +# %% [markdown] +# ## Phase field profile + +# %% [markdown] +# ### Fracture propagation animation + +# %% +plotter = pv.Plotter() + +plotter.open_gif("figures/surfing.gif") +pv.set_plot_theme("document") +for time_value in reader.time_values: + reader.set_active_time_value(time_value) + mesh = reader.read()[0] # This dataset only has 1 block + + sargs = { + "title": "Phase field", + "title_font_size": 20, + "label_font_size": 15, + "n_labels": 5, + "position_x": 0.3, + "position_y": 0.2, + "fmt": "%.1f", + "width": 0.5, + } + clim = [0, 1.0] + points = mesh.point_data["phasefield"].shape[0] + xs = mesh.points[:, 0] + ys = mesh.points[:, 1] + pf = mesh.point_data["phasefield"] + plotter.clear() + plotter.add_mesh( + mesh, + scalars=pf, + show_scalar_bar=False, + colormap="coolwarm", + clim=clim, + scalar_bar_args=sargs, + lighting=False, + ) + plotter.add_text(f"Time: {time_value:.0f}", color="black") + + plotter.view_xy() + plotter.write_frame() + +plotter.close() + +# %% [markdown] +#  + +# %% [markdown] +# ### Phase field profile at last time step + +# %% +mesh = reader.read()[0] + +pv.set_jupyter_backend("static") +p = pv.Plotter(shape=(1, 1), border=False) +p.add_mesh( + mesh, + scalars=pf, + show_edges=False, + show_scalar_bar=True, + colormap="coolwarm", + clim=clim, + scalar_bar_args=sargs, +) + +p.view_xy() +p.camera.zoom(1.5) +p.window_size = [800, 400] +p.show() + +# %% [markdown] +# ## References +# +# [1] B. Bourdin, G.A. Francfort, and J.-J. Marigo, _The variational approach to fracture_, Journal of Elasticity **91** (2008), no. 1-3, 5–148. +# +# [2] Li, Tianyi, Jean-Jacques Marigo, Daniel Guilbaud, and Serguei Potapov. _Numerical investigation of dynamic brittle fracture via gradient damage models._ Advanced Modeling and Simulation in Engineering Sciences **3**, no. 1 (2016): 1-24. +# +# [3] Dubois, Frédéric and Chazal, Claude and +# Petit, Christophe, _A Finite Element Analysis of Creep-Crack Growth in Viscoelastic Media_, Mechanics Time-Dependent Materials **2** (1998), no. 3, 269–286 diff --git a/Tests/Data/PhaseField/tpb_jupyter_notebook/TPB.py b/Tests/Data/PhaseField/tpb_jupyter_notebook/TPB.py new file mode 100644 index 0000000000000000000000000000000000000000..67266edd1e4ca4bc0ba6194c77ba4db28bc98c97 --- /dev/null +++ b/Tests/Data/PhaseField/tpb_jupyter_notebook/TPB.py @@ -0,0 +1,362 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: Python 3.10.6 64-bit +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# author = "Tao You, Keita Yoshioka, Mostafa Mollaali" +# date = "2022-11-28" +# title = "Three point bending test" +# web_subsection = "phase-field" +# +++ +# + +# %% [markdown] +# +# ## Problem description +# +# The notched beam under three-point bending is simulated to show how the phase field models can capture crack propagation and the structural strength. The geometry and boundary conditions of the model are depicted below. The bottom left point is fixed on both $x$ and $y$ directions and bottom right point is fixed on the $y$ direction. A displacement boundary condition $u^{\ast}$ is applied on the midpoint of the upper edge. The crack will initiate at the notch tip and propagate upward to the upper edge. +# +# * Three phase field models are implemented in OGS, i.e., $\texttt{COHESIVE}$, $\texttt{AT}_1$ and $\texttt{AT}_2$. +# * The energy-split models include *EffectiveStress*, *Isotropic* and the *VolumetricDeviatoric*. +# * The softening rules implemented for the $\texttt{COHESIVE}$ model are *Linear* and *Exponential*. +# +# The force-displacement curves ($F^{\ast}$-$u^{\ast}$) for various sets of the above models will be recorded and compared with the experimental data. +# +#  +# + +# %% [markdown] +# ## Define some helper functions + +# %% +import os +import shutil +import time +from pathlib import Path +from types import MethodType +from xml.dom import minidom + +import matplotlib.pyplot as plt +import numpy as np +import ogstools as ot +import pandas as pd +import pyvista as pv + +# %% +data_dir = os.environ.get("OGS_DATA_DIR", "../../..") + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +output_dir = out_dir + +# define function to replace a specific curve, given by name + + +def replace_curve( + self, + name=None, + value=None, + coords=None, + parametertype=None, + valuetag="values", + coordstag="coords", +): + root = self._get_root() + parameterpath = "./curves/curve" + parameterpointer = self._get_parameter_pointer(root, name, parameterpath) + self._set_type_value(parameterpointer, value, parametertype, valuetag=valuetag) + self._set_type_value(parameterpointer, coords, parametertype, valuetag=coordstag) + + +# define function to change time_stepping in project file + + +def set_timestepping(model, repeat_list, delta_t_list): + model.remove_element( + xpath="./time_loop/processes/process/time_stepping/timesteps/pair" + ) + for i in range(len(repeat_list)): + model.add_block( + blocktag="pair", + parent_xpath="./time_loop/processes/process/time_stepping/timesteps", + taglist=["repeat", "delta_t"], + textlist=[repeat_list[i], delta_t_list[i]], + ) + + +# %% [markdown] +# ## Define functions generating mesh, modifying project file and running ogs with given parameters + +# %% +def ogs_TPB( + phasefield_model, + energy_split_model, + softening_curve="Linear", + length_scale=5.0, + bc_displacement=-1.0, + ts_coords="0 1.0", + values="0 1.0", + repeat_list=None, + delta_t_list=None, + hypre=False, + MPI=True, + ncores=4, +): + ## define input file + + without_hypre = "-ksp_type cg -pc_type bjacobi -ksp_atol 1e-14 -ksp_rtol 1e-14" + with_hypre = "-ksp_type cg -pc_type hypre -pc_hypre_type boomeramg -pc_hypre_boomeramg_strong_threshold 0.7 -ksp_atol 1e-8 -ksp_rtol 1e-8" + + prj_name = "TPB.prj" + print( + f"> Running three point bending test {phasefield_model} - {energy_split_model} - {softening_curve} ... <" + ) + logfile = f"{out_dir}/log_{phasefield_model}_{energy_split_model}.txt" # noqa: F841 + model = ot.Project( + input_file=prj_name, output_file=f"{out_dir}/{prj_name}", MKL=True + ) + # generate prefix from properties + prefix = f"{phasefield_model}" + f"_{energy_split_model}" + if phasefield_model == "COHESIVE": + prefix = ( + f"{phasefield_model}" + f"_{energy_split_model}" + f"_{softening_curve}" + ) + + if MPI: + # partition mesh + ! NodeReordering -i TPB.vtu -o {out_dir}/TPB.vtu >> {logfile} + ! constructMeshesFromGeometry -m {out_dir}/TPB.vtu -g TPB.gml >> {logfile} + shutil.move("TPB_left.vtu", f"{out_dir}/TPB_left.vtu") + shutil.move("TPB_right.vtu", f"{out_dir}/TPB_right.vtu") + shutil.move("TPB_top.vtu", f"{out_dir}/TPB_top.vtu") + shutil.copy("TPB.gml", f"{out_dir}/TPB.gml") + ! partmesh -s -o {out_dir} -i {out_dir}/TPB.vtu >> {logfile} + ! partmesh -m -n {ncores} -o {out_dir} -i {out_dir}/TPB.vtu -- {out_dir}/TPB_right.vtu {out_dir}/TPB_left.vtu {out_dir}/TPB_top.vtu >> {logfile} + else: + ! NodeReordering -i TPB.vtu -o {out_dir}/TPB.vtu >> {logfile} + + # change properties in prj file + model = ot.Project( + input_file=prj_name, output_file=f"{out_dir}/{prj_name}", MKL=True + ) + model.replace_parameter_value(name="ls", value=length_scale) + model.replace_text(phasefield_model, xpath="./processes/process/phasefield_model") + model.replace_text( + energy_split_model, xpath="./processes/process/energy_split_model" + ) + model.replace_text(softening_curve, xpath="./processes/process/softening_curve") + model.replace_text(prefix, xpath="./time_loop/output/prefix") + model.replace_parameter_value(name="dirichlet_load", value=bc_displacement) + model.replace_curve = MethodType(replace_curve, model) + model.replace_curve(name="dirichlet_time", value=values, coords=ts_coords) + if repeat_list is not None and delta_t_list is not None: + set_timestepping(model, repeat_list, delta_t_list) + else: + set_timestepping(model, ["1"], ["1e-2"]) + if hypre is True: + model.replace_text( + with_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + else: + model.replace_text( + without_hypre, + xpath="./linear_solvers/linear_solver/petsc/parameters", + occurrence=1, + ) + model.replace_text("TPB.gml", xpath="./geometry") + model.write_input() + # run ogs + t0 = time.time() + if MPI: + print(f" > OGS started execution with MPI - {ncores} cores...") + ! mpirun --bind-to none -np {ncores} ogs {out_dir}/{prj_name} -o {output_dir} >> {logfile} + else: + print(" > OGS started execution ...") + ! ogs {out_dir}/{prj_name} -o {output_dir} >> {logfile} + tf = time.time() + print(" > OGS terminated execution. Elapsed time: ", round(tf - t0, 2), " s.") + + +# %% [markdown] +# ## Input data +# +# The general parameters are chosen as follows. +# +# +# | **Name** | **Value** | **Unit** | **Symbol** | +# |--------------------------------|--------------------|--------------|------------| +# | _Young's modulus_ | 20000 | MPa | $E$ | +# | _Poisson's ratio_ | 0.2 | $-$ | $v$ | +# | _Critical energy release rate_ | 0.113 | N/mm | $G_{c}$ | +# | _Regularization parameter_ | 5 | mm | $\ell$ | +# | _Minimum element size_ | 1 | mm | $h$ | +# +# The material parameters for the **COHESIVE** model are as follows. +# +# | **Name** | **Value** | **Unit** | **Symbol** | +# |--------------------------------|--------------------|--------------|------------| +# | _Tensile strength_ | 2.4 | MPa | $f_t$ | +# | _Irwin's length_ | 392.4 | mm | $\ell_{ch}$ | +# + +# %% [markdown] +# ## Run Simulations +# > In the following, coarse time steps are chosen for the sake of less computational cost. + +# %% +# phasefield_model = ["COHESIVE",'AT1', 'AT2'] +# energy_split_model = ["EffectiveStress",'VolumetricDeviatoric','Isotropic'] +# softening_curve = ['Line','Exponential'] + +disp = -1.0 +ls = 5.0 +mpi_cores = 4 # MPI cores +## only run selected cases +# For the COHESIVE model +for a in ["Linear", "Exponential"]: + ogs_TPB( + "COHESIVE", + "EffectiveStress", + a, + length_scale=ls, + bc_displacement=disp, + repeat_list=["1"], + delta_t_list=["1e-1"], + ncores=mpi_cores, + ) + +# For AT1 and AT2 models with isotropic split +for b in ["AT1", "AT2"]: + ogs_TPB( + b, + "Isotropic", + length_scale=ls, + bc_displacement=disp, + repeat_list=["1"], + delta_t_list=["1e-1"], + ncores=mpi_cores, + ) + + +# %% [markdown] +# ## Results +# +# The phase field profile in the final time step of the *COHESIVE EffectiveStress* case with *Exponential* softening is shown below. +# +#  + +# %% [markdown] +# ## Post-processing +# The force-displacement curves ($F^{\ast}$-$u^{\ast}$) applied to the beam are compared. + +# %% +# define function to obtain displacement applied at the loading point from vtu file +def displ_midpoint(filename): + data = pv.read(filename) + return np.sum( + data.point_data["displacement"][:, 1], + where=(data.points[:, 0] == 225) * (data.points[:, 1] == 100), + ) + + +# define function to obtain force at the loading point from vtu file + + +def force_midpoint(filename): + data = pv.read(filename) + return ( + np.sum( + data.point_data["NodalForces"][:, 1], + where=(data.points[:, 0] == 225) * (data.points[:, 1] == 100), + ) + / 10.0 + ) + + +# define function to apply the above functions on all vtu files listed in pvd file, returning force-displacement curves + + +def force_displ_from_pvd(pvd): + doc = minidom.parse(pvd) + DataSets = doc.getElementsByTagName("DataSet") + vtu_files = [x.getAttribute("file") for x in DataSets] + forces_midpoint = [force_midpoint(f"{out_dir}/{x}") for x in vtu_files] + displs_midpoint = [displ_midpoint(f"{out_dir}/{x}") for x in vtu_files] + return [forces_midpoint, displs_midpoint] + + +# %% [markdown] +# ## Plot force-displacement curves + +# %% +# Load experimental data +data_lower = pd.read_csv("figures/experiment_data_lower_limit.csv") +data_upper = pd.read_csv("figures/experiment_data_upper_limit.csv") + +# %% +prefixes = [ + "AT1_Isotropic", + "AT2_Isotropic", + "COHESIVE_EffectiveStress_Linear", + "COHESIVE_EffectiveStress_Exponential", +] +labels = [ + r"${AT}_1$ Isotropic", + r"${AT}_2$ Isotropic", + r"${COHESIVE}$ EffectiveStress Linear", + r"${COHESIVE}$ EffectiveStress Exponential", +] +ls = ["-.", "--", ".", "-"] +colors = ["#ffdf4d", "#006ddb", "#8f4e00", "#ff6db6"] + +fig, ax = plt.subplots() +for i, pre in enumerate(prefixes): + pvd = f"{output_dir}/{pre}.pvd" + if Path(pvd).is_file(): + curve = force_displ_from_pvd(pvd) + plt.plot( + curve[1], + curve[0], + ls[i % 2], + label=labels[i], + linewidth=2, + color=colors[i], + alpha=1, + ) + +plt.rcParams["xtick.labelsize"] = 12 +plt.rcParams["ytick.labelsize"] = 12 +ax.grid(linestyle="dashed") +ax.set_xlabel("$u^{\\ast}$ [mm]", fontsize=12) +ax.set_ylabel("$F^{\\ast}$ [kN]", fontsize=12) +plt.xlim(plt.xlim()[::-1]) +plt.ylim(plt.ylim()[::-1]) +plt.legend(ncol=1) +ax.axhline(y=0, color="black", linewidth=1) +ax.axvline(x=0, color="black", linewidth=1) +plt.fill_between( + data_upper.iloc[:, 0], 0, data_upper.iloc[:, 1], facecolor="green", alpha=0.3 +) +plt.fill_between( + data_lower.iloc[:, 0], 0, data_lower.iloc[:, 1], facecolor="white", alpha=1 +) + +# %% [markdown] +# Running all the selected cases with fine time steps (100 steps) yields the following figure in which the green patch bounds the experimental data. The cohesive models match the experimental results better than $\texttt{AT}_1$ and $\texttt{AT}_2$. +# +#  diff --git a/Tests/Data/TH2M/H/diffusion/diffusion.py b/Tests/Data/TH2M/H/diffusion/diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..144dd36904e8a0c5a760294a36391fd191d9d87e --- /dev/null +++ b/Tests/Data/TH2M/H/diffusion/diffusion.py @@ -0,0 +1,272 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Gas Diffusion" +# date = "2022-10-19" +# author = "Norbert Grunwald" +# image = "figures/placeholder_diffusion.png" +# web_subsection = "th2m" +# coupling = "h" +# weight = 1 +# +++ +# + +# %% [markdown] +# |<div style="width:300px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:300px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# # 1D Linear Diffusion +# +# ## Analytical Solution +# +# The solution of the diffusion equation for a point x in time t reads: +# +# $ c(x,t) = \left(c_0-c_i\right)\operatorname{erfc}\left(\frac{x}{\sqrt{4Dt}}\right)+c_i$ +# +# where $c$ is the concentration of a solute in mol$\cdot$m$^{-3}$, $c_i$ and $c_b$ are initial and boundary concentrations; $D$ is the diffusion coefficient of the solute in water, x and t are location and time of solution. + +# %% +import numpy as np +from scipy.special import erfc + + +# Analytical solution of the diffusion equation +def Diffusion(x, t): + if ( + not isinstance(t, int) + and not isinstance(t, np.float64) + and not isinstance(t, float) + ): + # In order to avoid a division by zero, the time field is increased + # by a small time unit at the start time (t=0). This should have no + # effect on the result. + tiny = np.finfo(np.float64).tiny + t[t < tiny] = tiny + + d = np.sqrt(4 * D * t) + return (c_b - c_i) * erfc(x / d) + c_i + + +# Utility-function transforming mass fraction into conctration + + +def concentration(xm_WL): + xm_CL = 1.0 - xm_WL + return xm_CL / beta_c + + +# %% [markdown] +# ### Material properties and problem specification + +# %% +# Henry-coefficient and compressibility of solution +H = 7.65e-6 +beta_c = 2.0e-6 + +# Diffusion coefficient +D = 1.0e-9 + +# Boundary and initial gas pressures +pGR_b = 9e5 +pGR_i = 1e5 + +# Boundary and initial concentration +c_b = concentration(1.0 - (beta_c * H * pGR_b)) +c_i = concentration(1.0 - (beta_c * H * pGR_i)) + +# %% [markdown] +# ## Numerical Solution + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +import ogstools as ot + +model = ot.Project(input_file="diffusion.prj", output_file=f"{out_dir}/modified.prj") +model.replace_text(1e7, xpath="./time_loop/processes/process/time_stepping/t_end") +model.replace_text( + 5e4, xpath="./time_loop/processes/process/time_stepping/timesteps/pair/delta_t" +) +# Write every timestep +model.replace_text(1, xpath="./time_loop/output/timesteps/pair/each_steps") +model.write_input() + +# Run OGS +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir} -m .") + +# %% +# Colors +cls1 = ["#4a001e", "#731331", "#9f2945", "#cc415a", "#e06e85", "#ed9ab0"] +cls2 = ["#0b194c", "#163670", "#265191", "#2f74b3", "#5d94cb", "#92b2de"] + +# %% +import vtuIO + +pvdfile = vtuIO.PVDIO(f"{out_dir}/result_diffusion.pvd", dim=2) + +# Get all written timesteps +time = pvdfile.timesteps + +# Select individual timesteps for c vs. x plots for plotting +time_steps = [1e6, 2e6, 4e6, 6e6, 8e6, 1e7] + +# 'Continuous' space axis for c vs. x plots for plotting +length = np.linspace(0, 1.0, 101) + +# Draws a line through the domain for sampling results +x_axis = [(i, 0, 0) for i in length] + +# Discrete locations for c vs. t plots +location = [0.01, 0.05, 0.1, 0.2, 0.5, 1.0] + +# %% +# The sample locations have to be converted into a 'dict' for vtuIO +observation_points = {"x=" + str(x): (x, 0.0, 0.0) for x in location} +# Samples concentration field at the observation points for all timesteps + +c_over_t_at_x = pvdfile.read_time_series("xmWL", observation_points) +for key in c_over_t_at_x: + x = c_over_t_at_x[key] + c_over_t_at_x[key] = concentration(x) + +# Samples concentration field along the domain at certain timesteps +c_over_x_at_t = [] +for t in range(len(time_steps)): + c_over_x_at_t.append( + concentration( + pvdfile.read_set_data(time_steps[t], "xmWL", pointsetarray=x_axis) + ) + ) + +# %% +import matplotlib.pyplot as plt + +plt.rcParams["figure.figsize"] = (14, 4) + +# Plot of concentration vs. time at different locations +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.set_xlabel("$t$ / s", fontsize=12) +ax1.set_ylabel("$c$ / mol m$^{-3}$", fontsize=12) + +ax2.set_xlabel("$t$ / s", fontsize=12) +ax2.set_ylabel(r"$\epsilon_\mathrm{abs}$ / mol m$^{-3}$", fontsize=12) + +label_x = [] +for key, _c in c_over_t_at_x.items(): + x = observation_points[key][0] + label_x.append(key + r" m") + # numerical solution + ax1.plot( + time, + c_over_t_at_x[key], + color=cls1[location.index(x)], + linewidth=3, + linestyle="--", + ) + # analytical solution + ax1.plot( + time, + Diffusion(x, time), + color=cls1[location.index(x)], + linewidth=2, + linestyle="-", + ) + # absolute error + err_abs = Diffusion(x, time) - c_over_t_at_x[key] + ax2.plot( + time, + err_abs, + color=cls1[location.index(x)], + linewidth=1, + linestyle="-", + label=key + r" m", + ) + + +# Hack to force a custom legend: +from matplotlib.lines import Line2D + +custom_lines = [] + +for i in range(6): + custom_lines.append(Line2D([0], [0], color=cls1[i], lw=4)) + +custom_lines.append(Line2D([0], [0], color="black", lw=3, linestyle="--")) +custom_lines.append(Line2D([0], [0], color="black", lw=2, linestyle="-")) +label_x.append("OGS-TH2M") +label_x.append("analytical") + +ax1.legend(custom_lines, label_x, loc="right") +ax2.legend() +fig1.savefig(f"{out_dir}/diffusion_c_vs_t.pdf") + + +# Plot of concentration vs. location at different times +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.set_xlabel("$x$ / m", fontsize=12) +ax1.set_ylabel("$c$ / mol m$^{-3}$", fontsize=12) +ax1.set_xlim(0, 0.4) + +ax2.set_xlabel("$x$ / m", fontsize=12) +ax2.set_ylabel(r"$\epsilon$ / mol $m^{-3}$", fontsize=12) +ax2.set_xlim(0, 0.4) + + +# Plot concentration over domain at five moments +label_t = [] +for t in range(len(time_steps)): + s = r"$t=$" + str(time_steps[t] / 1e6) + r"$\,$Ms" + label_t.append(s) + # numerical solution + ax1.plot(length, c_over_x_at_t[t], color=cls2[t], linewidth=3, linestyle="--") + # analytical solution + ax1.plot( + length, + Diffusion(length, time_steps[t]), + color=cls2[t], + linewidth=2, + linestyle="-", + ) + # absolute error + err_abs = Diffusion(length, time_steps[t]) - c_over_x_at_t[t] + ax2.plot(length, err_abs, color=cls2[t], linewidth=1, linestyle="-", label=s) + +custom_lines = [] + +for i in range(6): + custom_lines.append(Line2D([0], [0], color=cls2[i], lw=4)) + +custom_lines.append(Line2D([0], [0], color="black", lw=3, linestyle="--")) +custom_lines.append(Line2D([0], [0], color="black", lw=2, linestyle="-")) +label_t.append("OGS-TH2M") +label_t.append("analytical") + +ax1.legend(custom_lines, label_t, loc="right") +ax2.legend() + +fig1.savefig(f"{out_dir}/diffusion_c_vs_x.pdf") + +# %% [markdown] +# The numerical approximation approaches the exact solution quite well. Deviations can be reduced if the resolution of the temporal discretisation is increased. diff --git a/Tests/Data/TH2M/H2/dissolution_diffusion/phase_appearance.py b/Tests/Data/TH2M/H2/dissolution_diffusion/phase_appearance.py new file mode 100644 index 0000000000000000000000000000000000000000..6385d95884ad8cae670655ce4d7ba2dd0bad2eed --- /dev/null +++ b/Tests/Data/TH2M/H2/dissolution_diffusion/phase_appearance.py @@ -0,0 +1,218 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: 'Python 3.10.8 (''.venv'': venv)' +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Phase Appearance/Disappearance" +# date = "2022-10-19" +# author = "Norbert Grunwald" +# image = "figures/placeholder_bourgeat.png" +# web_subsection = "th2m" +# coupling = "h2" +# weight = 3 +# +++ +# + +# %% [markdown] +# |<div style="width:300px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:300px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# In <cite>Bourgeat et al. (2013)</cite> a numerical experiment is presented which investigates the transport behaviour of hydrogen in clay rock. This test example was investigated by several institutions using different simulation tools and the results were then compared in detail. +# +# To verify the phase transition, the diffusive mass transport and the two-phase behaviour, this numerical experiment was recalculated with the TH2M process class. The material parameters and boundary conditions used essentially correspond to those of the experiment and are summarised as follows: +# +# +# | Parameter | Symbol | Value | Unit | +# |--------------------------------|:------:|------|---------| +# | binary diffusion coefficient | $D$ | 3.0e-9 | Pa | +# | viscosity liquid | $\mu_\text{LR}$ | 1.0e-3 | Pa s | +# | viscosity gas | $\mu_\text{GR}$ | 9.0e-6 | Pa s | +# | Henry-coefficient | $H$ | 7.65e-6 | mol Pa$^{-1}$m$^{-3}$ | +# | molar mass hydrogen | $M_\mathrm{H_2}$ | 2.0e-3 | kg mol$^{-1}$ | +# | molar mass water | $M_\mathrm{H_2O}$| 1.0e-2 | kg mol$^{-1}$ | +# | density liquid | $\rho_\text{LR}$ | eq. (1) | kg m$^{-3}$ | +# | intrinsic permeability | $\mathbf{k}$ | 5.0e-20 | m$^2$ | +# | porosity | $\phi$ | 0.15 | 1 | +# +# The van Genuchten model was used as the saturation relation with $m=$0.329 and $p_b$=2.0e6 Pa, $s_\mathrm{L}^\mathrm{res}=$0.4 and $s_\mathrm{G}^\mathrm{res}=$0.0. The relative permeabilities $k^\mathrm{rel}_\mathrm{L}$ and $k^\mathrm{rel}_\mathrm{G}$ were determined using the van Genuchten and van Genuchten-Mualem models, respectively. +# +# Unlike in the cited article, the density of the liquid phase was not considered constant, but was represented by a linear equation of state of the form +# +# $$ +# \rho_\text{LR}=\rho^\text{ref}_\text{LR}\left(1+\chi_{c,\text{L}}c^\text{C}_\text{L}\right) \;\;\;(1) +# $$ +# +# Here, $\rho^\text{ref}_\text{LR}$=1000 kg$^{-3}$ is a reference density and $\chi_{c,\text{L}}=M_{H_2}{\rho^\text{ref}_\text{LR}}^{-1}$=2.0e-6 m$^{3}$mol$^{-1}$ is the slope of the function with respect to the hydrogen concentration. +# +# This deviation from the specifications in <cite>Bourgeat et al. (2013)</cite> is necessary because the phase transition model in TH2M evaluates the ratio of the densities of solvent and solution to calculate the dissolved gas fraction. +# Therefore, the equation of state must take into account the concentration of dissolved gases, even if the actual influence on the density seems negligible. +# +#  +# +# The experiment considers a 200 m long, porous quasi-1D region, at the left edge of which a hydrogen mass flow is injected. This injection is kept constant for 500,000 years and then stopped. +# The chosen mass flow is low enough that all the injected hydrogen is initially dissolved in the liquid phase and is transported there primarily by diffusion. +# +# The validity of the test is carried out by comparing the variables gas saturation, gas pressure and water pressure with the results of various codes from <cite>Bourgeat et al. (2013)</cite>. +# +# --- +# +# Bourgeat, A. P., Granet, S., & Smaï, F. (2013). Compositional two-phase flow in saturated–unsaturated porous media: benchmarks for phase appearance/disappearance. Simulation of Flow in Porous Media, 1–29. https://doi.org/10.1515/9783110282245.81 + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +import numpy as np +import ogstools as ot + +model = ot.Project(input_file="bourgeat.prj", output_file=f"{out_dir}/modified.prj") +# This Jupyter notebook version of this test runs not as far as its cTest-counterpart, +# it'll stop after approx. 800 ka +model.replace_text(2.5e13, xpath="./time_loop/processes/process/time_stepping/t_end") + + +# The cTest version shows only a few output-timesteps while this version is supposed to +# output much higher resolution time steps to be able to compare the results. Thus, every +# timestep will be written and the maximum timestep-size of the adaptive time stepping +# method will be reduced drastically +time_end = 1e11 +model.replace_text( + time_end, xpath="./time_loop/processes/process/time_stepping/maximum_dt" +) + +# The following for loop generates a text with output times,which is then replaced by +# the project file API (ogstools.Project) in the project file. +new_line = "\u000A" +timesteps = str(0.4 * 1e6 * 86400 * 365.25) + new_line +for t in np.arange(0.6, 1.1, 0.1): + timesteps += str(t * 1e6 * 86400 * 365.25) + new_line + +model.replace_text(1, xpath="./time_loop/output/timesteps/pair/each_steps") +model.replace_text(timesteps, xpath="./time_loop/output/fixed_output_times") +model.write_input() + +# Run OGS +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir} -m .") + +# %% +# Colors +cls = ["#e6191d", "#337fb8", "#4eae4c", "#984ea3", "#984ea3", "#feff32"] + +# %% +import vtuIO + +# Read PVD-output +pvdfile = vtuIO.PVDIO(f"{out_dir}/result_bourgeat.pvd", dim=2) +point = {"A": (0.0, 0.0, 0.0)} +time = pvdfile.timesteps + +saturation = pvdfile.read_time_series("saturation", point) +gas_pressure = pvdfile.read_time_series("gas_pressure", point) +liquid_pressure = pvdfile.read_time_series("liquid_pressure_interpolated", point) + +num_results = [1.0 - saturation["A"], gas_pressure["A"], liquid_pressure["A"]] + +time_years = time / 365.2425 / 86400 + +# %% +import pandas as pd + +# Read the reference data from CSV files +refs = [ + pd.read_csv("references/bourgeat_sG.csv"), + pd.read_csv("references/bourgeat_pGR.csv"), + pd.read_csv("references/bourgeat_pLR.csv"), +] + +header = list(refs[0].keys()) + +# %% +indices = {"Gas saturation": 0, "Gas pressure": 1, "Liquid pressure": 2} +labels = ["$s_{G}$", "$p_{GR}$", "$p_{LR}$"] + +# %% +import matplotlib.pyplot as plt + +plt.rcParams["figure.figsize"] = (12, 4) + +# Loop over gas_saturation, gas_pressure, and liquid_pressure +for i in indices: + index = indices[i] + ref_time = refs[index]["time"] + + fig1, (ax1, ax2) = plt.subplots(1, 2, sharey=True) + fig1.suptitle(i + r" vs. time at $x=0$m") + + ax1.set_xscale("log") + ax1.set_xlabel("time / a", fontsize=12) + ax1.set_ylabel(labels[index], fontsize=12) + + for r in range(1, len(refs[index].columns)): + ax1.plot( + ref_time, + refs[index][refs[index].keys()[r]], + linewidth=1, + linestyle="-", + label=refs[index].keys()[r], + ) + + ax1.plot( + time_years, + num_results[index], + color="black", + linewidth=2, + linestyle="--", + label="OGS-TH$^2$M", + ) + + ax2.set_xlabel("time / a", fontsize=12) + + for r in range(1, len(refs[index].columns)): + ax2.plot( + ref_time, + refs[index][refs[index].keys()[r]], + linewidth=1, + linestyle="-", + label=refs[index].keys()[r], + ) + ax2.plot( + time_years, + num_results[index], + color="black", + linewidth=2, + linestyle="--", + label="OGS-TH$^2$M", + ) + + ax1.legend() + + +fig1.savefig("results_sG_pGR_pLR.pdf") + +# %% [markdown] +# After about 10,000 years, the dissolution capacity of the liquid phase is exhausted and a separate gas phase is formed. From this point on, an increase in water pressure can be observed. This is shown in the liquid pressure plot +# by comparison with the results shown in <cite>Bourgeat et al. (2013)</cite>. +# +# Regarding the accuracy (especially during times t<10,000 years), it should be noted that the reference graphs from the original paper were digitised manually; in the paper, the graphs were only linearly plotted (like those shown here in the right column), so no real statement can be made about the accuracy of the results for times t<10,000 years. +# +# The comparison of the time evolution of the water pressure, gas pressure und gas saturation shows that the numerical solution calculated by OGS6-TH2M is very close to the results shown in <cite>Bourgeat et al. (2013)</cite>. +# Both the time of formation of the gas phase (approximately at t=15,000 a and the shape and magnitude of the resulting pressure rise and pressure drop due to switching off the source term are within the differences of the reference solutions. +# +# It can be concluded that both the transport and the phase transition behaviour of the TH2M process have been successfully derived and implemented. The self-controlled appearance and disappearance of a free gas phase should be emphasised. diff --git a/Tests/Data/TH2M/H2/mcWhorter/mcWhorter.py b/Tests/Data/TH2M/H2/mcWhorter/mcWhorter.py new file mode 100644 index 0000000000000000000000000000000000000000..a54f8d53ad8b2beae5ddbcb85d7b06e6b1f4c03f --- /dev/null +++ b/Tests/Data/TH2M/H2/mcWhorter/mcWhorter.py @@ -0,0 +1,161 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "McWhorter & Sunada Problem" +# date = "2022-10-19" +# author = "Norbert Grunwald" +# image = "figures/placeholder_mcWhorter.png" +# web_subsection = "th2m" +# coupling = "h2" +# weight = 4 +# +++ +# + +# %% [markdown] +# |<div style="width:330px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:330px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# # McWhorter Problem +# <cite>[McWhorter and Sunada][1]</cite> propose an analytical solution to the two-phase flow equation. A one-dimensional problem was considered which describes the flow of two incompressible, immiscible fluids through a porous medium, where the wetting phase (water) displaces the non-wetting fluid (air or oil) in the horizontal direction (without the influence of gravity). +# +# +#  +# +# ## Analytical solution +# +# +# A detailed semi-analytical solution and a convenient tool for calculating the solution for different material parameters can be found [here](https://mmg.fjfi.cvut.cz/~fucik/index.php?page=exact). +# +# ### Material Parameters +# +# | Property | Symbol | Value | Unit | +# |----------:|:------:|-------|------| +# | Porosity | $\phi$ | 0.15 | 1 | +# | Intrinsic permeability | $K$ | $$1.0\cdot 10^{-10}$$ | $m^2$| +# | Residual saturation of the wetting phase | $$s_\mathrm{L}^{res}$$ | 0.02 | 1 | +# | Residual saturation of the non-wetting phase | $$s_\mathrm{G}^{res}$$ | 0.001 | 1 | +# | Dynamic viscosity of the wetting phase |$\mu_\mathrm{L}$|$$1.0\cdot 10^{-3}$$|Pa s| +# | Dynamic viscosity of the non-wetting pha |$\mu_\mathrm{G}$ |$$5.0\cdot 10^{-3}$$|Pa s| +# | Brooks and Corey model parameter: entry pressure | $p_b$ | 5000 | Pa | +# | Brooks and Corey model parameter: pore size distribution index |$\lambda$ | 3.0 | 1 | +# +# ### Problem Parameters +# +# | Property | Symbol | Value | Unit | +# |----------:|:------:|-------|------| +# | Initial saturation | $$s_\mathrm{L}(t=0)$$ | 0.05 | 1 | +# | Injection boundary saturation | $$s_\mathrm{L}(x=0)$$ | 0.8 | 1 | +# +# +# [1]: https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/WR026i003p00399?casa_token=6yzGcmrd7dkAAAAA:E6QsKTxrf12GO-0CY6qgu4XEcX6iFM4O_mnaVV2gWBO8voVnxYXxLOtnAdUnBskEOPZiwaFAggWnmqpg + +# %% [markdown] +# ## Exact Solution +# +# The exact solution is not yet calculated in this notebook, instead the [online tool](https://mmg.fjfi.cvut.cz/~fucik/index.php?page=exact) by Radek FuÄÃk is used. This tool calculates the solution and outputs the results with arbitrary accuracy as CSV files, which are plotted below. + +# %% +import numpy as np + +# Import analytical solution from a CSV file +exact = np.loadtxt("data/ref_solution_saturation.csv", delimiter=",") +# Zeroth column is location, first column is saturation + +# %% [markdown] +# ## Numerical Solution + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +import ogstools as ot + +# run OGS +model = ot.Project(input_file="mcWhorter_h2.prj", output_file="mcWhorter_h2.prj") +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir}") + +# %% +import vtuIO + +# read OGS results from PVD file +pvdfile = vtuIO.PVDIO( + f"{out_dir}/result_McWhorter_H2.pvd", dim=2, interpolation_backend="vtk" +) + +# %% +import numpy as np + +# The sampling routine requires a line of points in space +x_axis = [(i, 0, 0) for i in exact[:, 0]] + +# Only the last timestep is written (together with the initial condition). +# Thus the second element of the time vector (index = 1) is the to be sampled +time = pvdfile.timesteps[1] + +# The numerical solution is sampled at the same supporting points as the analytical solution +sL_num = pvdfile.read_set_data(time, "saturation", pointsetarray=x_axis) + +# Absolute and relative errors +err_abs = exact[:, 1] - sL_num +err_rel = err_abs / exact[:, 1] + +# %% +import matplotlib.pyplot as plt + +plt.rcParams["figure.figsize"] = (14, 4) +fig1, (ax1, ax2) = plt.subplots(1, 2) + +fig1.suptitle(r"Liquid saturation and errors at t=" + str(time) + " seconds") + +# Saturation vs. time +ax1.set_xlabel("$x$ / m", fontsize=12) +ax1.set_ylabel(r"$s_\mathrm{L}$", fontsize=12) + +ax1.plot(exact[:, 0], sL_num, "b", label=r"$s_\mathrm{L}$ numerical") +ax1.plot(exact[:, 0], exact[:, 1], "g", label=r"$s_\mathrm{L}$ exact") + +lns2 = ax2.plot( + exact[:, 0], err_abs, "b", linewidth=2, linestyle="-", label=r"absolute error" +) + +ax2.set_xlabel("$x$ / m", fontsize=12) +ax2.set_ylabel(r"$\epsilon_\mathrm{abs}$", fontsize=12) + +ax3 = ax2.twinx() +lns3 = ax3.plot( + exact[:, 0], err_rel, "g", linewidth=2, linestyle="-", label=r"relative error" +) +ax3.set_ylabel(r"$\epsilon_\mathrm{rel}$", fontsize=12) + +ax1.legend() + +lns = lns2 + lns3 +labs = [label.get_label() for label in lns] +ax2.legend(lns, labs, loc=0) + + +fig1.savefig(f"{out_dir}/mcWhorter.pdf") + +# %% [markdown] +# The numerical approximation fits the analytical solution very well in the whole area. Only in the area of the saturation front are deviations recognisable. These deviations are mainly due to the grid resolution and are well known in multiphase simulations. The error can be reduced almost arbitrarily by lowering the size of the grid elements. + +# %% diff --git a/Tests/Data/TH2M/TH/Ogata-Banks/Ogata-Banks.py b/Tests/Data/TH2M/TH/Ogata-Banks/Ogata-Banks.py new file mode 100644 index 0000000000000000000000000000000000000000..37317488aeafed25daba9ff7883b07222b711a70 --- /dev/null +++ b/Tests/Data/TH2M/TH/Ogata-Banks/Ogata-Banks.py @@ -0,0 +1,289 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Ogata-Banks Problem" +# date = "2022-10-19" +# author = "Norbert Grunwald" +# image = "TH2M/TH/Ogata-Banks/Ogata-Banks.png" +# web_subsection = "th2m" +# coupling = "th" +# weight = 5 +# +++ +# + +# %% [markdown] +# |<div style="width:300px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:300px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# # Advective-Diffusive transport (Ogata-Banks) + +# %% [markdown] +# The Ogata-Banks analytical solution of the 1D advection-dispersion equation of heat for a point $x$ in time $t$ reads: +# +# $ T(x,t) = \frac{T_0-T_i}{2}\left[\operatorname{erfc}\left(\frac{x-v_xt}{\sqrt{4Dt}}\right)+\operatorname{exp}\left(\frac{xv_x}{D}\right)\operatorname{erfc}\left(\frac{x+v_xt}{\sqrt{4Dt}}\right)\right]+T_i$ +# +# where $T_0$ is the constant temperature at $x=0$ and $T_i$ is initial temperature at $t=0$, $v_x$ is the constant velocity of the fluid medium, and D is thermal diffusivity given by +# +# $D=\frac{\lambda}{\rho c_p}$ +# +# where $\lambda$ is the thermal heat conductivity, $\rho$ is density and $c_p$ is the specific heat capacity of the fluid medium. +# +# --- +# +# The weak form of the energy balance equation (in simplified form for single-phase fluid L and without gravity) in OGS-TH2M is: +# +# $ +# \int_\Omega +# (\Sigma_\alpha\rho_\alpha u_\alpha)'_\mathrm{S} +# \,\delta T\,d\Omega +# -\int_\Omega \rho_\mathrm{L} h_\mathrm{L}\mathbf{w}_\mathrm{LS}\cdot\nabla\delta T\,d\Omega +# +\int_\Omega \lambda^\mathrm{eff}\nabla T\cdot\nabla\,\delta T\,d\Omega +# +\int_{\partial\Omega}\underbrace{\left(\lambda^\mathrm{eff}\nabla T +# -\rho_\mathrm{L}h_\mathrm{L}\mathbf{w}_\mathrm{LS}\right)\cdot\mathbf{n}}_{=-q_n}\,\delta T\,d\Gamma +# $ +# +# This results in an energy contribution across the Neumann boundaries that is dependent on the flowing medium and which may have to be compensated for by the boundary conditions. + +# %% [markdown] +# ## Material properties + +# %% [markdown] +# +# +# + +# %% +# Porosity +phi = 0.15 + +# Effective properties of the porous medium +rho_eff = 1000 +cp_eff = 2000 +lambda_eff = 2.2 + +# Thermal diffusivity +alpha = lambda_eff / (rho_eff * cp_eff) + +# %% [markdown] +# ## Problem definition + +# %% +# Initial temperature +T_i = 300 +# Boundary condition left +T_0 = 330 + +# Time - domain + +# Helper function to simplify conversions from seconds to days and back + + +def day(value): + # Converts seconds to days + return value / 86400 + + +def second(value): + # Converts days to seconds + return value * 86400 + + +# Time discretisation +delta_time = second(0.5) +max_time = second(500) + +domain_size = 50 # metre + +# Groundwater velocity +v_x = 1.5e-6 + +# %% [markdown] +# ## Analytical solution + +# %% +import numpy as np +from scipy.special import erfc + + +def OgataBanks(t, x): + if not isinstance(t, int) and isinstance(t, np.float64): + # In order to avoid a division by zero, the time field is increased + # by a small time unit at the start time (t=0). This should have no + # effect on the result. + tiny = np.finfo(np.float64).tiny + t[t < tiny] = tiny + + d = np.sqrt(4.0 * alpha * t) + a1 = np.divide((x - v_x * t), d, where=t != 0) + a2 = np.divide((x + v_x * t), d, where=t != 0) + + return (T_0 - T_i) / 2.0 * (erfc(a1) + np.exp(v_x * x / alpha) * erfc(a2)) + T_i + + +# %% [markdown] +# ## Numerical solution + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +import ogstools as ot + +# Modifies the project file of the original cTest so that the simulation runs for a longer time. +model = ot.Project(input_file="ogata-banks.prj", output_file=f"{out_dir}/modified.prj") +model.replace_text(max_time, xpath="./time_loop/processes/process/time_stepping/t_end") +model.replace_text( + delta_time, + xpath="./time_loop/processes/process/time_stepping/timesteps/pair/delta_t", +) +# Output every timestep +model.replace_text(1, xpath="./time_loop/output/timesteps/pair/each_steps") +model.write_input() + +# %% +# Run OGS +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir} -m . -s .") + +# %% +# Colors +cls1 = ["#4a001e", "#731331", "#9f2945", "#cc415a", "#e06e85"] +cls2 = ["#0b194c", "#163670", "#265191", "#2f74b3", "#5d94cb"] + +# %% +import vtuIO + +pvdfile = vtuIO.PVDIO(f"{out_dir}/result_ogata-banks.pvd", dim=2) + +# Get all written timesteps +time = pvdfile.timesteps + +# Select individual timesteps for T vs. x plots for plotting +time_steps = [second(10), second(100), second(200), second(300), second(500)] + +# 'Continuous' space axis for T vs. x plots for plotting +length = np.linspace(0, domain_size, 101) + +# Draws a line through the domain for sampling results +x_axis = [(i, 0, 0) for i in length] + +# Discrete locations for T vs. t plots +location = [1.0, 5.0, 10.0, 20.0, 50.0] + +# %% +# The sample locations have to be converted into a 'dict' for vtuIO +observation_points = {"x=" + str(x): (x, 0.0, 0.0) for x in location} +# Samples temperature field at the observation points for all timesteps +T_over_t_at_x = pvdfile.read_time_series("temperature_interpolated", observation_points) + +# %% +# Samples temperature field along the domain at certain timesteps +T_over_x_at_t = [] +for t in range(len(time_steps)): + T_over_x_at_t.append( + pvdfile.read_set_data(time_steps[t], "temperature", pointsetarray=x_axis) + ) + +# %% +import matplotlib.pyplot as plt + +plt.rcParams["figure.figsize"] = (14, 6) +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.set_xlabel("$t$ / d", fontsize=12) +ax1.set_ylabel("$T$ / K", fontsize=12) + +# Plot Temperature over time at five locations +for key, _T in T_over_t_at_x.items(): + x = observation_points[key][0] + # Plot numerical solution + ax1.plot( + day(time), + T_over_t_at_x[key], + color=cls1[location.index(x)], + linewidth=3, + linestyle="--", + label=key + r" m", + ) + # Plot analytical solution + ax1.plot( + day(time), + OgataBanks(time, x), + color=cls1[location.index(x)], + linewidth=2, + linestyle="-", + ) + +ax2.set_xlabel("$x$ / m", fontsize=12) +ax2.set_ylabel("$T$ / K", fontsize=12) + +# Plot Temperature over domain at five moments +for t in range(len(time_steps)): + s = r"$t=$" + str(day(time_steps[t])) + r"$\,$d" + ax2.plot( + length, T_over_x_at_t[t], color=cls2[t], linewidth=3, linestyle="--", label=s + ) + ax2.plot( + length, + OgataBanks(time_steps[t], length), + color=cls2[t], + linewidth=2, + linestyle="-", + ) + +ax1.legend() +ax2.legend() +fig1.savefig(f"{out_dir}/ogata_banks.pdf") + +# %% [markdown] +# For this discretisation, the numerical solution approximates well to the analytical one. Finer resolutions in the time discretisation reduce the deviations considerably. In this benchmark it is easy to see that too coarse resolutions (especially in the time discretisation) yield very plausible results, which can, however, deviate considerably from the exact solution. An analysis of the von Neumann stability criterion is worthwhile here. This criterium demands +# +# $$\text{Ne}=\frac{\alpha\Delta t}{\left(\Delta x\right)^2}\leq\frac{1}{2}$$ +# +# Evaluated for the problem at hand, the following value results: + +# %% +# Spatial discretizations at the left boundary (smallest element) +dx = 0.17 # m + +# von-Neumann-Stability-Criterion +Ne = alpha * delta_time / (dx * dx) +print(Ne) + +# %% [markdown] +# The Neumann criterion is not met in this case. The smallest element is $\Delta x=0.17\text{m}$ in width. A suitable time step for this cell size would be + +# %% +dt = 0.5 * (dx * dx) / alpha +print("Smallest timestep should not exceed", dt, "seconds.") + +# %% [markdown] +# Repeating the test with this time step should give much smaller deviations to the exact solution. +# +# However, the problem at hand has been spatially discretised where element widths are increasing from left to right. That means that the stability criteria is violated only at the left region of the domain. +# The minimum width that an element should have is can be determined by: + +# %% +dx = np.sqrt(2 * alpha * delta_time) +print("Minimum element size should be", dx, " metre.") + +# %% [markdown] +# The elements located at approximately $x>1\text{m}$ satisfy this criterion, therefore the solution presented here can be accepted as an approximation of the exact solution. diff --git a/Tests/Data/TH2M/TH/idealGasLaw/confined_gas_compression.py b/Tests/Data/TH2M/TH/idealGasLaw/confined_gas_compression.py new file mode 100644 index 0000000000000000000000000000000000000000..de71970ac7275f432ce5c9b836cc5e8b72bf91fe --- /dev/null +++ b/Tests/Data/TH2M/TH/idealGasLaw/confined_gas_compression.py @@ -0,0 +1,299 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Confined Gas Compression" +# date = "2022-10-19" +# author = "Norbert Grunwald" +# image = "figures/placeholder_confined_compression.png" +# web_subsection = "th2m" +# coupling = "h2" +# weight = 2 +# +++ +# + +# %% [markdown] +# |<div style="width:330px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:330px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# # Confined compression of a cube +# +# In this test, the thermodynamic relationships between gas pressure, temperature and density are tested. +# For that, a cube-shaped domain consisting of an ideal gas is compressed by 50\% of its initial volume over a short period of time, starting from the top surface. +# The boundaries of that domain are impermeable to fluid flow, therefore gas pressure and density must increase as a result of the decreasing volume. +# Since energy flow across the boundaries is also prevented, this compression is an adiabatic change of state. No frictional losses are taken into account, so the process can be reversed at any time and the entropy in the overall system remains constant. +# +# ## Analytical solution +# +# ### Density evolution +# +# The mass balance for such a system can be found by simplifying the mass balance (eq. 44 in Grunwald et al., 2022). With +# $\phi=s_\mathrm{G}=\alpha_\mathrm{B}=1$ and $\mathrm{A}^\zeta_\alpha=\mathrm{J}^\zeta_\alpha=0$ +# one obtains +# $$ +# 0=\rho_\mathrm{GR}\mathrm{div}\left(\mathbf{u}_\mathrm{S}\right)'_\mathrm{S}+\left(\rho_\mathrm{GR}\right)'_\mathrm{S} +# $$ +# +# With volume strain $e=\text{div}\left(\mathbf{u}_\text{S}\right)'_\text{S}$ we write +# $$ +# \frac{1}{\rho_\text{GR}}\left(\rho_\text{GR}\right)'_\text{S}=-e, +# $$ +# which can be integrated +# $$ +# \int^{\rho_\text{GR}}_{\rho_{\text{GR},0}}\frac{1}{\rho_\text{GR}}\text{d}\,\rho_\text{GR}=-e +# $$ +# so that we find +# $$ +# \ln\left(\rho_\text{GR}\right) - \ln\left(\rho_{\text{GR},0}\right) = -e +# $$ +# or +# $$ +# \rho_\text{GR}=\rho_{\text{GR},0}\exp\left(-e\right). +# $$ + +# %% +import matplotlib.pyplot as plt +import numpy as np + +# %% +# time runs from 0 to 10 in 11 steps +t = np.linspace(0, 10, 11) + +# volume strain is a function of time +e = -t / 100 + +# initial state +p_0 = 1e6 +T_0 = 270 +R = 8.3144621 +M = 0.01 +c_p = 1000 + +c_v = c_p - R / M +rho_0 = p_0 * M / R / T_0 +kappa = c_p / c_v + +# density +rho_GR = rho_0 * np.exp(-e) + +# %% [markdown] +# ### Gas pressure evolution +# +# The evolution of gas pressure can be found from energy balance equation (eq. 51 in the paper): +# Starting from +# $$ +# \left(\Sigma_\alpha\rho_\alpha u_\alpha\right)'_\text{S} +# +\left(\Sigma_\alpha\rho_\alpha h_\alpha\right)\text{div}\left(\mathbf{u}_\text{S}\right)'_\text{S} +# =0, +# $$ +# with $u_\text{S}=h_\text{S}$ and $u_\text{G}=h_\text{G}-\frac{p_\text{GR}}{\rho_\text{GR}}$ and with $h_\alpha=c_{p,\alpha}T$. Considering that $\phi_\alpha=\text{const}$ and $c_{p,\text{S}}=0$, we find +# $$ +# \left(\rho_\text{GR}\right)'_\text{S}\left(c_{p,\text{G}}T-p_\text{GR}\rho_\text{GR}^{-1}\right) +# + +# \rho_\text{GR}\left(c_{p,\text{G}}T-p_\text{GR}\rho_\text{GR}^{-1}\right)'_\text{S} +# + +# \rho_\text{GR}c_{p,\text{G}}Te=0. +# $$ +# Assuming ideal gas behaviour, we can write $\frac{p_\text{GR}}{\rho_\text{GR}}=\frac{M}{RT}$ and find +# $$ +# \left(\rho_\text{GR}\right)'_\text{S}c_{v,\text{G}}T +# + +# \rho_\text{GR}\left(c_{v,\text{G}}T\right)'_\text{S} +# + +# \rho_\text{GR}c_{p,\text{G}}Te=0. +# $$ +# With $c_{v,\text{G}}=\text{const}$ it follows +# $$ +# \frac{\rho_\text{GR}}{p_\text{GR}}\left(p_\text{GR}\right)'_\text{S}c_{v,\text{G}}T +# + +# \rho_\text{GR}c_{p,\text{G}}Te=0. +# $$ +# Density and temperature cancel out so the final equation can be written as +# $$ +# \frac{1}{p_\text{GR}}\left(p_\text{GR}\right)'_\text{S} +# =-\kappa e +# $$ +# with the adiabatic index $\kappa = \frac{c_v}{c_p}$. The equation can be integrated to find the solution for the pressure evolution +# $$ +# p_\text{GR}=p_{\text{GR},0}\exp\left(-\kappa e\right) +# $$ + +# %% +# gas pressure +p_GR = p_0 * np.exp(-kappa * e) + +# %% [markdown] +# ### Temperature evolution +# +# The temperature evolution follows _Poissons_ equations for isentropic processes +# $$ +# T=T_0\left(\frac{p_\text{GR}}{p_{\text{GR},0}}\right)^{\frac{\kappa-1}{\kappa}}. +# $$ + +# %% +# temperature +T = p_GR * M / R / rho_GR + +# %% [markdown] +# ## Numerical solution + +# %% +import ogstools as ot +import vtuIO + +# %% +import os +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# %% +# run OGS +cube_compression = ot.Project( + input_file="compression_gas.prj", output_file="compression_gas.prj" +) +cube_compression.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir}") + +# %% +# read PVD file +pvdfile = vtuIO.PVDIO(f"{out_dir}/result_compression_gas.pvd", dim=2) +# get all timesteps +time = pvdfile.timesteps + +# %% +# read pressure, temperature and density from pvd result file +# at point +point = {"pt0": (0.0, 1.0, 0.0)} + +pressure = pvdfile.read_time_series("gas_pressure_interpolated", point) +p_GR_num = pressure["pt0"] + +temperature = pvdfile.read_time_series("temperature_interpolated", point) +T_num = temperature["pt0"] + +density = pvdfile.read_time_series("gas_density", point) +rho_GR_num = density["pt0"] + +# %% +plt.rcParams["figure.figsize"] = (10, 4) +fig1, (ax1, ax2) = plt.subplots(1, 2) +fig1.suptitle(r"Density evolution, relative and absolute errors") + +ax1.plot(time, rho_GR_num, "kx", label=r"$\rho_\mathrm{GR}$ numerical") +ax1.plot(t, rho_GR, "b", label=r"$\rho_\mathrm{GR}$ analytical") +ax1.set_xlabel(r"$t$ / s") +ax1.set_ylabel(r"$\rho_\mathrm{GR}$ / kgs$^{-1}$") +ax1.legend() +ax1.grid(True) +# ax1.set_xlim(0,1) +# ax1.set_ylim(0,1) + +err_rho_abs = rho_GR - rho_GR_num +err_rho_rel = err_rho_abs / rho_GR + +ax2.plot(t, err_rho_abs, "b", label=r"absolute") +ax2.plot(t, err_rho_rel, "g", label=r"relative") + +ax2.set_xlabel(r"$t$ / s") +ax2.set_ylabel(r"$\epsilon_\mathrm{rel}$ / - and $\epsilon_\mathrm{abs}$ / kgs$^{-1}$") +ax2.legend() +ax2.grid(True) +# ax2.set_xlim(0,1) +# ax2.set_ylim(-0.001,0.02) + +fig1.tight_layout() +plt.show() + +# %% +plt.rcParams["figure.figsize"] = (10, 4) +fig1, (ax1, ax2) = plt.subplots(1, 2) +fig1.suptitle(r"Gas pressure evolution, relative and absolute errors") + + +ax1.plot(time, p_GR_num, "kx", label=r"$p_\mathrm{GR}$ numerical") +ax1.plot(t, p_GR, "b", label=r"$p_\mathrm{GR}$ analytical") +ax1.set_xlabel(r"$t$ / s") +ax1.set_ylabel(r"$p_\mathrm{GR}$ / Pa") +ax1.legend() +ax1.grid(True) +# ax1.set_xlim(0,1) +# ax1.set_ylim(0,1) + +err_p_abs = p_GR - p_GR_num +err_p_rel = err_p_abs / p_GR + +lns1 = ax2.plot(t, err_p_abs, "b", label=r"absolute") +ax3 = ax2.twinx() +lns2 = ax3.plot(t, err_p_rel, "g", label=r"relative") + +# added these three lines +lns = lns1 + lns2 +labs = [label.get_label() for label in lns] +ax2.legend(lns, labs, loc=0) +ax2.grid(True) + +ax2.set_xlabel(r"$t$ / s") +ax2.set_ylabel(r"$\epsilon_\mathrm{abs}$ / Pa") +ax3.set_ylabel(r"$\epsilon_\mathrm{rel}$ / -") + +# ax2.set_xlim(0,1) +ax2.set_ylim(-3500, 200) +ax3.set_ylim(-0.0035, 0.0002) + +fig1.tight_layout() +plt.show() + +# %% +plt.rcParams["figure.figsize"] = (10, 4) +fig1, (ax1, ax2) = plt.subplots(1, 2) +fig1.suptitle(r"Temperature evolution, relative and absolute errors") + + +ax1.plot(time, T_num, "kx", label=r"$T$ numerical") +ax1.plot(t, T, "b", label=r"$T$ analytical") +ax1.set_xlabel(r"$t$ / s") +ax1.set_ylabel(r"$T$ / K") +ax1.legend() +ax1.grid(True) +# ax1.set_xlim(0,1) +# ax1.set_ylim(0,1) + +err_T_abs = T - T_num +err_T_rel = err_T_abs / T + + +lns1 = ax2.plot(t, err_T_abs, "b", label=r"absolute") +ax3 = ax2.twinx() +lns2 = ax3.plot(t, err_T_rel, "g", label=r"relative") + +# added these three lines +lns = lns1 + lns2 +labs = [label.get_label() for label in lns] +ax2.legend(lns, labs, loc=0) +ax2.grid(True) + +ax2.set_xlabel(r"$t$ / s") +ax2.set_ylabel(r"$\epsilon_\mathrm{abs}$ / K") +ax3.set_ylabel(r"$\epsilon_\mathrm{rel}$ / -") + +# ax2.set_xlim(0,1) +ax2.set_ylim(-0.8, 0.05) +ax3.set_ylim(-0.002, 0.000125) + +fig1.tight_layout() +plt.show() diff --git a/Tests/Data/TH2M/TH2/heatpipe/heatpipe.py b/Tests/Data/TH2M/TH2/heatpipe/heatpipe.py new file mode 100644 index 0000000000000000000000000000000000000000..4f7ba80562d325e028e34b36ff072010ac8a12ce --- /dev/null +++ b/Tests/Data/TH2M/TH2/heatpipe/heatpipe.py @@ -0,0 +1,674 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: .venv +# language: python +# name: python3 +# --- + +# %% [raw] +# +++ +# title = "Heat pipe verification problem" +# date = "2022-09-14" +# author = "Kata Kurgyis" +# web_subsection = "th2m" +# image = "figures/placeholder_heatpipe.png" +# coupling = "h2t" +# weight = 6 +# +++ +# + +# %% [markdown] +# |<div style="width:330px"><img src="https://www.ufz.de/static/custom/weblayout/DefaultInternetLayout/img/logos/ufz_transparent_de_blue.png" width="300"/></div>|<div style="width:330px"><img src="https://discourse.opengeosys.org/uploads/default/original/1X/a288c27cc8f73e6830ad98b8729637a260ce3490.png" width="300"/></div>|<div style="width:330px"><img src="https://github.com/nagelt/Teaching_Scripts/raw/9d9e29ecca4b04eaf7397938eacbf116d37ddc93/Images/TUBAF_Logo_blau.png" width="300"/></div>| +# |---|---|--:| + +# %% [markdown] +# In this benchmark problem we describe the influence of a heat flux on a two-phase system (aqueous phase gas phase), also commonly known as the heat pipe effect. The heat pipe effect describes heat transport in a porous medium caused by convection due to capillary forces. Detailed description of the heat-pipe prblem can be found in (Helmig et al., 1997). +# +# This benchmark case considers the heat pipe effect for a two-phase system in a horizontal, one-dimensional column. As illustrated in the figure below, a constant heat flux $q$ is applied at one end of the column, which is sufficient to heat the water at this end of the column above boiling temperature. As the water vaporizes, the vapour moves towards the other end of the column, condensing to water again as it moves through cooler regions and, hence, giving up the latent heat of vaporization. The resulting non-linear water saturation profile along the horizontal column causes a capillary pressure gradient to arise, leading to a flux of the liquid phase pointing back towards the heat source: the mass of the vapour moving away from the heat source is equal to the mass of the condensate moving back towards the heat source. +# +#  +# +# When a heat pipe evolves from a single-phase liquid region as a consequence of constant heat injection, the vapour that is created at the heat source, displaces the water first. As a result, the latent heat of evaporation is given up by the vapour at the condensation front, and a mass flux is formed by the condensate pointing away from the heat source towards the completely cooled end of the column (here liquid exists in a single-phase region). The capillary forces which arise due to the non-linear saturation profile along the column are strong enough to transport the entire condensate back to the heat source, as long as the heat source is strong enough to ensure constant vaporization and liquid saturation reduction at the heat source. Like this, a closed loop of mass cycle is created within the heat pipe, and the heat pipe reaches its maximum length when all the water is evaporized at the heat source and the evaporation front is far enough from the heat source so that the temperature is just enough for the vaporization. At this point the heat pipe will not propagate further and remains stationary. +# +# While heat conduction is essential in the single-phase regions in front and behind the heat pipe for the heat transfer, the temperature gradient between the two ends of the heat pipe is relatively small, and therefore, within the heat pipe itself, heat conduction has no primary importance compared to convection. +# +# This benchmark case assumes that the gas phase contains an additional component - air. The presence of air in the system results in the following considerations that are taken into account: +# +# * While air does not condensate at the considered temperatures, its diffusion in the gas phase must be taken into account. +# * The presence of non-condensable gas obstruct the convective transport of the latent heat of evaporation in the gas phase. +# * As the additional gas component fills the pore space, it reduces the available volume for the liquid phase and consequently results in a lower relative permeability for the liquid phase. +# +# For verification purposes, the numerical solution of this benchmark problem is compared to a semi-analytical solution. The semi-analytical solution is briefly introduced in the following sections. + +# %% [markdown] +# # Analytical solution +# The analytical solution of the heat pipe problem solves a coupled system of first order differential equations for pressure, temperature, saturation and mole fraction derived by (Udell and Fitch, 1985). The here presented solution is slightly modified by (Helmig et al., 1997), as not all original assumptions proposed by (Udell and Fitch, 1985) are satisfied in this benchmark, thus some generalization are applied and will be mentioned alongside the original considerations: +# +# * The porous medium is homogeneous and incompressible: $\phi(z,p)=const.$ and $k=const.$ +# * Interfacial tension and viscosities of the gas phase and the liquid phase are constant: $\sigma=const.$ , $\mu_{\alpha}=const.$ **Attention:** we do not consider the viscosity of the gas phase constant due to the presence of air in the system. Instead, the viscosity of the gas phase is calculated as the mole fraction-weighted average of air and vapour viscosity in the gas phase: +# +# \begin{align} +# \mu_g=x_g^a\mu_g^a+x_g^w\mu_g^w +# \end{align} +# +# * Density changes of the gas and liquid phase due to pressure, temperature and composition variation is neglected: $\rho_{\alpha}=const.$ **Attention:** we take into account the infuence of air presence in the gas phase density. The density of the gas mixture is calculated as the sum of the partial density of the two component. Including the ideal gas law, the gas phase density is: +# +# \begin{align} +# \rho_g=\frac{p_g}{(RT)(x_g^aM^a+x_g^wM^w)} +# \end{align} +# +# * The single-phase regions at the boundary of the heat pipe are not considered in the solution. + +# %% [markdown] +# ### Input parameters +# The following material parameters and heat flow value are considered as input parameters. + +# %% +import math + +import numpy as np + +q = -100.0 # heat injection [W/m²] + +K = 1e-12 # permeability [m²] +phi = 0.4 # porosity [-] + +p_ref = 101325 # reference pressure [Pa] +T_ref = 373.15 # reference temperature [K] + +lambda_G = 0.2 # thermal conductivity of gas phase [W/mK] +lambda_L = 0.5 # thermal conductivity of liquid phase [w/mK] +lambda_S = 1.0 # thermal conductivity of solid matrix [w/mK] +dh_evap = 2258000 # latent heat of evaporation [J/kg] + +D_pm = 2.6e-6 # binary diffusion coefficient [m²/s] +rho_L = 1000.0 # density of liquid phase [kg/m³] +MW = 0.018016 # molecular weight of water component [kg/mol] +MC = 0.028949 # molecular weight of air component [kg/mol] +R = 8.3144621 # universal gas constant + +mu_L = 2.938e-4 # dynamic viscosity of liquid phase [Pa.s] +muA_G = 2.194e-5 # dynamic viscosity of air component in gas phase [Pa.s] +muW_G = 1.227e-5 # dynamic viscosity of water component in gas phase [Pa.s] + +s_LRes = 0.0 # residual saturation of liquid phase [-] +s_GRes = 0.0 # residual saturation of gas phase [-] + +k_rG_min = 1e-5 # used for normalization of BC model +k_rL_min = 1e-5 # used for normalization of BC model +p_thr_BC = 5.0e3 # entry pressure for Brooks-Corey model [Pa] +exp_BC = 3.0 # Corey exponent for Brooks-Corey model [-] + + +# %% [markdown] +# ### Constitutive relations +# +# The system of balance equations from the previous chapter has to be closed by a variety of constitutive relations, which in turn specify the necessary material properties and express the thermodynamic equilibrium between the constituents of liquid and gas phases. We apply the same relations in the analytical solution and in the numerical model as well. +# +# Due to the existence of multiple phases within the pore space, the movement of a fluid phase is obstructed by the presence of the other phase. In multiphase flow applications, this effect is usually realised by introducing relative permeabilities as functions of saturation which calculate the effective permeability of each phase as described in the extended Darcy law. Additionally if the present phases are immiscible, one also needs to consider the arising capillary effects by introducing capillary pressure accounting for the difference of phase pressures. +# +# The Brooks-Corey formulation accounts for residual saturations of both fluid phases and links effective saturation to capillary pressure. Additionally, it also models the relative permeability of both phases as a function of effective liquid saturation. + + +# %% +def capillary_pressure(sL_eff): + return p_thr_BC * (sL_eff ** (-1.0 / exp_BC)) + + +def capillary_pressure_derivative(sL_eff): + return -p_thr_BC / exp_BC * (sL_eff ** (-(exp_BC + 1.0) / exp_BC)) + + +def saturation_effective(p_c): + return (p_c / p_thr_BC) ** (-exp_BC) + + +def relative_permeability_gas(sL_eff): + return max( + k_rG_min, ((1.0 - sL_eff) ** 2) * (1 - (sL_eff ** ((2.0 + exp_BC) / exp_BC))) + ) + + +def relative_permeability_liquid(sL_eff): + return max(k_rL_min, sL_eff ** ((2.0 + 3 * exp_BC) / exp_BC)) + + +# %% [markdown] +# Determining the composition of the gas phase, we assume that the sum of all constituents’ partial pressures accounts for the entire gas phase pressure (Dalton’s law). The partial pressure of water vapour is derived from the true vapour pressure that accounts for the impact of capillary effects as well: due to wettability and capillarity, the interfaces prevailing in porous media are not flat, but rather curving. Above curved interfaces, the vapour pressure may change depending on the direction of curvature. The Kelvin-Laplace equation accounts for this and expresses the true vapour pressure as a function of capillary pressure and the saturation vapour pressure of pure water. +# +# The saturation vapour pressure is determined by the approximate Clausius-Clapeyron equation. + + +# %% +def vapour_pressure(p_sat, p_G, p_c, xA_G, T): + return p_sat * math.exp(-(p_c - xA_G * p_G) * MW / rho_L / R / T) + + +def saturation_vapour_pressure(T): + return p_ref * math.exp((1.0 / T_ref - 1.0 / T) * dh_evap * MW / R) + + +def partial_pressure_vapour(p_G, p_c, xA_G, T): + p_sat = saturation_vapour_pressure(T) + return vapour_pressure(p_sat, p_G, p_c, xA_G, T) + + +# %% [markdown] +# The gas phase density can be determined using an appropriate equation of state for binary mixtures, such as the Peng-Robinson equation of state. For simplicity, we use the thermal equation of state of ideal gases in this work, which gives sufï¬cient results at high temperatures and low pressures. +# +# Dynamic viscosity of composite phases is obtained from the simple mixing rule. + + +# %% +def molar_mass_gas_phase(xA_G): + return xA_G * MC + (1 - xA_G) * MW + + +def density_gas_phase(p_G, xA_G, T): + M = molar_mass_gas_phase(xA_G) + return p_G * M / (R * T) + + +def viscosity_gas_phase(xA_G): + return xA_G * muA_G + (1.0 - xA_G) * muW_G + + +def kinematic_viscosity_gas_phase(p_G, xA_G, T): + mu_G = viscosity_gas_phase(xA_G) + rho_G = density_gas_phase(p_G, xA_G, T) + return mu_G / rho_G + + +# %% [markdown] +# The macroscopic diffusion constant (diffusivity) is determined using a simple correction accounting for porosity and gas saturation on the microscopic diffusion constant ($D_{pm}^G$). + + +# %% +def diffusivity(sL_eff): + return phi * (1.0 - sL_eff) * D_pm + + +# %% [markdown] +# When heat conduction occurs over multiple phases, a mixing rule can describe averaged heat conduction if local thermal equilibrium is assumed. In this case, we apply a very simple model (upper Wiener bound, Wiener 1912) to ï¬nd an effective heat conductivity by averaging individual phase conductivities by volume fraction. + + +# %% +def thermal_conductivity(sL_eff): + sL = sL_eff * (1.0 - s_GRes - s_LRes) + s_LRes + phi_G = (1.0 - sL) * phi + phi_L = sL * phi + phi_S = 1.0 - phi + return lambda_G * phi_G + lambda_L * phi_L + lambda_S * phi_S + + +# %% [markdown] +# ### Governing equations +# The solution for the 4 primary variables (liquid phase saturation, gas phase pressure, mole fraction of air in the gas phase and temperature) is obtained from the numerical integration of a coupled system of first order differential equations. +# +# \begin{align} +# \frac{\partial z}{\partial S_{L,eff}}=\frac{-\frac{dp_c}{dS_{L,eff}}}{\theta \omega +# \frac{1}{k_{rG}}( \frac{1}{1-x_G^a} + \frac{\nu_L}{\nu_G} \frac{k_{rG}}{k_{rL}} )} +# \end{align} +# +# \begin{align} +# \frac{\partial p_G}{\partial S_{L,eff}}=\frac{\frac{dp_c}{dS_{L,eff}}}{1 + +# \frac{\nu_L}{\nu_G} \frac{k_{rG}}{k_{rL}} ({1-x_G^a})} +# \end{align} +# +# \begin{align} +# \frac{\partial x_G^a}{\partial S_{L,eff}}=\frac{-\frac{dp_c}{dS_{L,eff}} \frac{K}{\nu_G \rho_G D_{pm}} +# \frac{1}{1-x_G^a}}{\frac{1}{k_{rG}} ( \frac{1}{1-x_G^a} + \frac{\nu_L}{\nu_G} +# \frac{k_{rG}}{k_{rL}} )} +# \end{align} +# +# \begin{align} +# \frac{\partial T}{\partial S_{L,eff}}=\frac{\frac{dp_c}{dS_{L,eff}} +# \frac{1-\theta}{\theta} \frac{dh_{evap} K}{\nu_g D_{pm}}}{\frac{1}{k_{rG}} +# ( \frac{1}{1-x_G^a} + \frac{\nu_L}{\nu_G} \frac{k_{rG}}{k_{rL}} )} +# \end{align} +# +# The differentail equations given by (Udell and Fitch, 1985) that are shown above are derived on the basis of mass and energy balance. It should be noted that the equation system is integrated over the effective saturation instead of the spatial coordinate $z$. The above given equations are reached when: +# +# * the effective saturation is coupled to the spatial coordinate through the introduction of capillary pressure +# +# \begin{align} +# \frac{dz}{dS_{L,eff}}=\frac{dp_c/dS_{L,eff}}{\partial p_c / \partial z} +# \end{align} +# +# * and the following chain rule is applied to the primary variable derivatives, e.g.: +# +# \begin{align} +# \frac{\partial p_G}{\partial S_{L,eff}}=\frac{\partial p_G}{\partial z} +# \frac{\partial z}{\partial S_{L,eff}} +# \end{align} +# +# The parameter $\eta$ in the energy balance equation represents the ratio of the heat flux caused by convection to the total heat flux $q$: +# +# \begin{align} +# \eta = 1+\frac{\lambda \frac{\partial T}{\partial z}}{q} +# \end{align} +# +# With some algebraic transformation, $\eta$ can also be explicitly expressed by +# +# \begin{align} +# \alpha = 1+ \frac{p_c - x_G^a p_G}{dh_{evap} \rho_L} +# \end{align} +# +# \begin{align} +# \delta = \frac{\rho_L dh_{evap}^2 K \alpha}{\lambda \nu_G T} +# \end{align} +# +# \begin{align} +# \xi = \frac{1}{k_{rG}} (1+ \frac{\rho_L R T}{p_G M^w} \frac{1}{1-x_G^a})+ +# \frac{\nu_L}{\nu_G k_{rL}} +# \end{align} +# +# \begin{align} +# \zeta = \frac{K \rho_L R T}{\rho_G \nu_G D_{pm} M^w} \frac{x_G^a}{1-x_G^a} +# (\frac{p_G M^w}{\rho_L R T} + \frac{1}{1-x_G^a}) +# \end{align} +# +# \begin{align} +# \eta = \frac{\delta}{\delta \xi \zeta} +# \end{align} +# +# Including the reformulated $\eta$ and grouping some of the parameters under $\gamma$ and $\omega$, the 4 governing equation can be calculated in the following form: +# +# \begin{align} +# \gamma = \frac{1}{k_{rG}} +# ( \frac{1}{1-x_G^a} + \frac{\nu_L}{\nu_G} \frac{k_{rG}}{k_{rL}} ) +# \end{align} +# +# \begin{align} +# \omega = \frac{q \nu_G}{dh_{evap} K} +# \end{align} +# +# \begin{align} +# \frac{\partial z}{\partial S_{L,eff}} = \frac{-\frac{dp_c}{dS_{L,eff}}}{\eta \omega +# \gamma} +# \end{align} +# +# \begin{align} +# \frac{\partial p_G}{\partial S_{L,eff}} = \frac{\frac{dp_c}{dS_{L,eff}}}{\gamma k_{rG} +# (1-x_G^a)} +# \end{align} +# +# \begin{align} +# \frac{\partial x_G^a}{\partial S_{L,eff}} = \frac{-\frac{dp_c}{dS_{L,eff}} \frac{K}{\nu_G \rho_G D_{pm}} +# \frac{1}{1-x_G^a}}{\gamma} +# \end{align} +# +# \begin{align} +# \frac{\partial T}{\partial S_{L,eff}} = \frac{\frac{dp_c}{dS_{L,eff}} +# \frac{1-\theta}{\theta} \frac{dh_{evap} K}{\nu_g D_{pm}}}{\gamma} +# \end{align} +# +# The full derivation of the analytical solution can be found in (Helmig et al., 1997). + + +# %% +# Parameter grouping +def alpha_(sL_eff, p_G, xA_G): + p_c = capillary_pressure(sL_eff) + return 1.0 + ((p_c - xA_G * p_G) / (dh_evap * rho_L)) + + +def delta_(sL_eff, p_G, xA_G, T): + alpha = alpha_(sL_eff, p_G, xA_G) + nu_G = kinematic_viscosity_gas_phase(p_G, xA_G, T) + th_cond = thermal_conductivity(sL_eff) + return (rho_L * (dh_evap**2) * K * alpha) / (th_cond * nu_G * T) + + +def xi_(sL_eff, p_G, xA_G, T): + k_rG = relative_permeability_gas(sL_eff) + k_rL = relative_permeability_liquid(sL_eff) + nu_G = kinematic_viscosity_gas_phase(p_G, xA_G, T) + nu_L = mu_L / rho_L + return ((1.0 + ((rho_L * R * T) / (p_G * MW * (1.0 - xA_G)))) / k_rG) + ( + nu_L / nu_G + ) / k_rL + + +def zeta_(sL_eff, p_G, xA_G, T): + D = diffusivity(sL_eff) + mu_G = viscosity_gas_phase(xA_G) + a = K * rho_L * R * T * xA_G + b = mu_G * D * MW * (1.0 - xA_G) + c = p_G * MW / (rho_L * R * T) + 1.0 / (1.0 - xA_G) + return (a / b) * c + + +def eta_(sL_eff, p_G, xA_G, T): + delta = delta_(sL_eff, p_G, xA_G, T) + xi = xi_(sL_eff, p_G, xA_G, T) + zeta = zeta_(sL_eff, p_G, xA_G, T) + return delta / (delta + xi + zeta) + + +def gamma_(sL_eff, p_G, xA_G, T): + k_rG = relative_permeability_gas(sL_eff) + k_rL = relative_permeability_liquid(sL_eff) + nu_G = kinematic_viscosity_gas_phase(p_G, xA_G, T) + nu_L = mu_L / rho_L + return 1.0 / k_rG * ((1.0 / (1.0 - xA_G)) + (nu_L / nu_G) * (k_rG / k_rL)) + + +# Differential equations +# Spatial variable (1D) derivative + + +def dz_dsL_eff(sL_eff, p_G, xA_G, T): + dpC_dsL_eff = capillary_pressure_derivative(sL_eff) + eta = eta_(sL_eff, p_G, xA_G, T) + gamma = gamma_(sL_eff, p_G, xA_G, T) + nu_G = kinematic_viscosity_gas_phase(p_G, xA_G, T) + omega = (q * nu_G) / (dh_evap * K) + return -dpC_dsL_eff / (eta * omega * gamma) + + +# Gas-phase pressure derivative + + +def dp_G_dsL_eff(sL_eff, p_G, xA_G, T): + dpC_dsL_eff = capillary_pressure_derivative(sL_eff) + gamma = gamma_(sL_eff, p_G, xA_G, T) + k_rG = relative_permeability_gas(sL_eff) + return dpC_dsL_eff / (gamma * k_rG * (1.0 - xA_G)) + + +# Mole fraction of air component in gas phase derivative + + +def dxA_G_dsL_eff(sL_eff, p_G, xA_G, T): + dpC_dsL_eff = capillary_pressure_derivative(sL_eff) + gamma = gamma_(sL_eff, p_G, xA_G, T) + mu_G = viscosity_gas_phase(xA_G) + D = diffusivity(sL_eff) + return -dpC_dsL_eff * K / (mu_G * D) * xA_G / (1.0 - xA_G) / gamma + + +# Temperature derivative + + +def dT_dsL_eff(sL_eff, p_G, xA_G, T): + dpC_dsL_eff = capillary_pressure_derivative(sL_eff) + eta = eta_(sL_eff, p_G, xA_G, T) + gamma = gamma_(sL_eff, p_G, xA_G, T) + nu_G = kinematic_viscosity_gas_phase(p_G, xA_G, T) + th_cond = thermal_conductivity(sL_eff) + return dpC_dsL_eff * (1.0 - eta) / eta * dh_evap / (nu_G * th_cond) * K / gamma + + +# %% [markdown] +# ### Numerical integration over effective liquid saturation +# To obtain the analytical solution, the above introduced four coupled differential equation can now be integrated with the well-know Forward Euler method. +# +# To get a unique solution, the following boundary and initial conditions are considered: +# +# * On the left-hand side ($z = 0$) - the cool end: $S_L = 1$ , $p_G = p_{G,i}$ , $x_G^a = x_{G,i}^a$ and $T = T_i$. +# +# * On the right-hand side - the hot end of the the heat pipe, a constant heat-flux is considered: $q = -100 W/m^2$. +# +# * As initial conditions we chose: $S_L = 1$ , $p_G = 101325 Pa$ , $x_G^a = 0.25$ (computed according to Dalton's law using the true vapour pressure assuming $p_{c,i} = 5001 Pa$) and $T = 365 K$. +# + + +# %% +# Define right-hand-sides of the coupled system of first order derivative equations +def dy_dsL_eff(_y, sL_eff, p_G, xA_G, T): + dydsL = np.zeros(4) + dydsL[0] = dz_dsL_eff(sL_eff, p_G, xA_G, T) + dydsL[1] = dp_G_dsL_eff(sL_eff, p_G, xA_G, T) + dydsL[2] = dxA_G_dsL_eff(sL_eff, p_G, xA_G, T) + dydsL[3] = dT_dsL_eff(sL_eff, p_G, xA_G, T) + return dydsL + + +# Numerical integration - Forward Euler method +# to estimate the integrals of the coupled equation system + + +def step_Euler(y, sL_eff, dsL_eff, p_G, xA_G, T): + return y + dsL_eff * dy_dsL_eff(y, sL_eff, p_G, xA_G, T) + + +def full_Euler(dsL_eff, y0, sL_eff_low, sL_eff_high): + max_steps = int(abs((sL_eff_low - sL_eff_high) / dsL_eff)) + sL_eff_list = np.linspace(sL_eff_low, sL_eff_high, max_steps + 1) + M = np.zeros( + (4, max_steps + 1) + ) # Solution matrix containing the 4 primary variable + M[:, 0] = y0 + for i in range(max_steps): + p_G = M[1, i] + xA_G = M[2, i] + T = M[3, i] + if (dz_dsL_eff(sL_eff_list[i], p_G, xA_G, T) * dsL_eff) < 1.0: + M[:, i + 1] = step_Euler(M[:, i], sL_eff_list[i], dsL_eff, p_G, xA_G, T) + else: + M[:, i + 1] = np.nan + return M, sL_eff_list + + +# initial condition +z_0 = 0 +p_G0 = 101325 +p_c0 = 5001 +T_0 = 365 +xA_G0 = 1 - partial_pressure_vapour(p_G0, p_c0, 0, T_0) / p_G0 +y0 = np.array([z_0, p_G0, xA_G0, T_0]) + +# initial effective saturation +sL_eff_0 = saturation_effective(p_c0) + +# integration boundaries and saturation step size +sL_eff_low = sL_eff_0 +sL_eff_high = 10e-16 +n_dsL_eff = 10**2 +dsL_eff = (sL_eff_high - sL_eff_low) / n_dsL_eff + +# execute analytical solution +M, sL_eff_list = full_Euler(dsL_eff, y0, sL_eff_low, sL_eff_high) + +# %% [markdown] +# # Numerical solution +# +# In the following section, the previously analitically solved heatpipe problem is solved using numerical methods (FEM in OpenGeoSys). The numerical solution takes the exact same input parameters, and considers the same - modified - assumptions that are mentioned in the analytical solution. +# +# Additionally, it is necessary to introduce a discretized spacial domain for the numerical simulation. We chose a 1 dimensional domain with length of 1 m discretized into 200 equally spaced identical elements. +# +# The numerical problem considers the same consitutive relationships and identical boundary and initial conditions. We use the TH2M model of OGS to solve the coupled partial differential equations describing the system behavior. Detailed description of the numerical model can be found in (Grunwald et al., 2022). + +# %% +import os +from pathlib import Path + +import ogstools as ot + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +prj_file = "heat_pipe_rough.prj" +model = ot.Project(input_file=prj_file, output_file=prj_file) +model.run_model(logfile=f"{out_dir}/out.txt", args=f"-o {out_dir}") + +# %% [markdown] +# # Results comparison +# +# To compare the results produced by the analytical solution and those by the numerical compution by OpenGeoSys, the four primary variables are plotted along the 1D domain ($z$): liquid phase saturation ($S_{L,eff}$), air molar fraction in the gas phase ($x_G^a$), temperature ($T$) and gas phase pressure ($p_G$). + +# %% +# Import OGS simulation results +import pyvista as pv + +pv.set_plot_theme("document") +pv.set_jupyter_backend("static") + +pvd_file = f"{out_dir}/results_heatpipe_rough.pvd" +reader = pv.get_reader(pvd_file) +reader.set_active_time_value(1.0e7) # set reader to simulation end-time +mesh = reader.read()[0] + +# Define line along mesh and extract data along line for plotting +pt1 = (0, 0.0025, 0) +pt2 = (1, 0.0025, 0) +xaxis = pv.Line(pt1, pt2, resolution=2) +line_mesh = mesh.slice_along_line(xaxis) + +x_num = line_mesh.points[:, 0] # x coordinates of each point +S_num = line_mesh.point_data["saturation"] +xA_G_num = line_mesh.point_data["xnCG"] +p_G_num = line_mesh.point_data["gas_pressure"] +T_num = line_mesh.point_data["temperature"] + +# Resampling dataset via linear interpolation for error calculation +S_num_interp = np.interp(M[0, :], x_num, S_num) +xA_G_num_interp = np.interp(M[0, :], x_num, xA_G_num) +p_G_num_interp = np.interp(M[0, :], x_num, p_G_num) +T_num_interp = np.interp(M[0, :], x_num, T_num) + +# %% [markdown] +# As one can see from the figures below, the numerical results are in really good agreement with the analytical solution. To better understand and visualize the deviation, we also perform a quick error analysis by simply calculating the difference (absolute and relative error) between the analytical and the numerical solution. + +# %% +import matplotlib.pyplot as plt + +plt.rcParams["lines.linewidth"] = 2.0 +plt.rcParams["lines.color"] = "black" +plt.rcParams["legend.frameon"] = True +plt.rcParams["font.family"] = "serif" +plt.rcParams["legend.fontsize"] = 14 +plt.rcParams["font.size"] = 14 +plt.rcParams["axes.axisbelow"] = True +plt.rcParams["figure.figsize"] = (16, 6) + +fig1, (ax1, ax2, ax3) = plt.subplots(1, 3) +ax1.plot(M[0, :], sL_eff_list, "kx", label=r"$S_{L,eff}$ analytical") +ax1.plot(M[0, :], M[2, :], "kx", label=r"$x_G^a$ analytical") +ax1.plot(x_num, S_num, "b", label=r"$S_{L,eff}$ numerical") +ax1.plot(x_num, xA_G_num, "g", label=r"$x_G^a$ numerical") +ax1.set_xlabel(r"$z$ / m") +ax1.set_ylabel(r"$S_{L,eff}$ and $x_G^a$ / -") +ax1.legend() +ax1.grid(True) +ax1.set_xlim(0, 1) +ax1.set_ylim(0, 1) + +ax2.plot(M[0, :], S_num_interp - sL_eff_list, "b", label=r"$\Delta S_{L,eff}$") +ax2.plot(M[0, :], xA_G_num_interp - M[2, :], "g", label=r"$\Delta x_G^a$") +ax2.set_xlabel(r"$z$ / m") +ax2.set_ylabel(r"Absolute error / -") +ax2.legend() +ax2.grid(True) +ax2.set_xlim(0, 1) +ax2.set_ylim(-0.001, 0.02) + +relError_S_w = np.zeros(len(M[0, :])) +relError_xA_G = np.zeros(len(M[0, :])) +for i in range(len(M[0, :])): + if (sL_eff_list[i]) >= 0.001: + relError_S_w[i] = (S_num_interp[i] - sL_eff_list[i]) / sL_eff_list[i] + else: + relError_S_w[i] = np.nan + if (M[2, i]) >= 0.01: + relError_xA_G[i] = (xA_G_num_interp[i] - M[2, i]) / M[2, i] + else: + relError_xA_G[i] = np.nan +ax3.plot(M[0, :], relError_S_w, "b", label=r"$\Delta S_{L,eff}/S_{L,eff-analytical}$") +ax3.plot(M[0, :], relError_xA_G, "g", label=r"$\Delta x_G^a/x_{G-analytical}^a$") +ax3.set_xlabel(r"$z$ / m") +ax3.set_ylabel(r"Relative error / -") +ax3.set_xlim(0, 1) +ax3.legend() +ax3.grid(True) +fig1.tight_layout() +plt.show() + +fig2, (ax1, ax2, ax3) = plt.subplots(1, 3) +ax1.plot(M[0, :], M[3, :], "kx", label=r"$T$ analytical") +ax1.plot(x_num, T_num, "r", label=r"$T$ numerical") +ax1.set_xlabel(r"$z$ / m") +ax1.set_ylabel(r"$T$ / K") +ax1.legend() +ax1.grid(True) +ax1.set_xlim(0, 1) +ax1.set_ylim(365, 375) + +ax2.plot(M[0, :], -T_num_interp + M[3, :], "r", label=r"$\Delta T$") +ax2.set_xlabel(r"$z$ / m") +ax2.set_ylabel(r"Absolute error / K") +ax2.legend() +ax2.grid(True) +ax2.set_xlim(0, 1) +ax2.set_ylim(0, 0.12) + +ax3.plot( + M[0, :], + (-T_num_interp + M[3, :]) / M[3, :], + "r", + label=r"$\Delta T/T_{analytical}$", +) +ax3.set_xlabel(r"$z$ / m") +ax3.set_ylabel(r"Relative error / -") +ax3.set_xlim(0, 1) +ax3.set_ylim(0, 0.0003) +ax3.legend() +ax3.grid(True) +fig2.tight_layout() +plt.show() + +fig3, (ax1, ax2, ax3) = plt.subplots(1, 3) +ax1.plot(M[0, :], M[1, :] / 1000, "kx", label=r"$p_G$ analytical") +ax1.plot(x_num, p_G_num / 1000, "c", label=r"$p_G$ numerical") +ax1.set_xlabel(r"$z$ / m") +ax1.set_ylabel(r"$p_G$ / kPa") +ax1.legend() +ax1.grid(True) +ax1.set_xlim(0, 1) +ax1.set_ylim(100, 106) + +ax2.plot(M[0, :], -p_G_num_interp + M[1, :], "c", label=r"$\Delta p_G$") +ax2.set_xlabel(r"$z$ / m") +ax2.set_ylabel(r"Absolute error / Pa") +ax2.legend() +ax2.grid(True) +ax2.set_xlim(0, 1) +ax2.set_ylim(0, 30) + +ax3.plot( + M[0, :], + (-p_G_num_interp + M[1, :]) / M[1, :], + "c", + label=r"$\Delta p_G/p_{G-analytical}$", +) +ax3.set_xlabel(r"$z$ / m") +ax3.set_ylabel(r"Relative error / -") +ax3.set_xlim(0, 1) +ax3.set_ylim(0, 0.0004) +ax3.legend() +ax3.grid(True) +fig3.tight_layout() +plt.show() + +# %% [markdown] +# ## References +# +# [1] Helmig, R. et al. (1997). Multiphase flow and transport processes in the subsurface: a contribution to the modeling of hydrosystems. Springer-Verlag. +# +# [2] Udell, K. and Fitch, J. (1985) Heat and mass transfer in capillary porous media considering evaporation, condensation, and non-condensible gas effects. 23rd ASME/AIChE national heat transfer conference, Denver, pp. 103-110. +# +# [3] Wiener, O. (1912). “Abhandl. math.-physâ€. In: Kl. Königl. Sächsischen Gesell 32, p. 509. +# +# [4] Grunwald, N. et al. (2022). Non-isothermal two-phase flow in deformable porous media: Systematic open-source implementation and veriï¬cation procedure. Geomech. Geophys. Geo-energ. Geo-resour., 8, 107 +# https://link.springer.com/article/10.1007/s40948-022-00394-2 + +# %% diff --git a/Tests/Data/ThermoHydroMechanics/Linear/Point_injection/SaturatedPointheatsource.py b/Tests/Data/ThermoHydroMechanics/Linear/Point_injection/SaturatedPointheatsource.py new file mode 100644 index 0000000000000000000000000000000000000000..c41d6509e40a6129eff75dc80b09a7966af6a9e1 --- /dev/null +++ b/Tests/Data/ThermoHydroMechanics/Linear/Point_injection/SaturatedPointheatsource.py @@ -0,0 +1,835 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.14.5 +# kernelspec: +# display_name: venv-with-ogs +# language: python +# name: venv-with-ogs +# --- + +# %% [raw] +# +++ +# author = "Jörg Buchwald and Kata Kurgyis" +# date = "2022-11-02" +# title = "Point-Heatsource Problem" +# weight = 8 +# image = "figures/placeholder_pointheatsource.png" +# web_subsection = "th2m" +# coupling = "thm" +# +++ +# + +# %% [markdown] +# ## Problem description +# +# The problem describes a heat source embedded in a fully fluid-saturated porous medium. +# The spherical symmetry is modeled using a 10 m x 10 m disc with a point heat source ($Q=150\;\mathrm{W}$) placed at one corner ($r=0$) and a curved boundary at $r=10\;\mathrm{m}$. Applying rotational axial symmetry at one of the linear boundaries, the model region transforms into a half-space configuration of the spherical symmetrical problemcorresponding to the analytical solution. +# The initial temperature and the excess pore pressure are 273.15 K and 0 Pa, respectively. +# The axis-normal displacements along the symmetry (inner) boundaries were set to zero, whereas the excess pore pressure, as well as the temperature, are set to their initial values along the outer (curved) boundary. +# The heat coming from the point source is propagated through the medium, causing the fluid and the solid to expand at different rates. The resulting pore pressure (gradient) is triggering a thermally driven consolidation process caused by the fluid flowing away from the heat source until equilibrium is reached. +# +#  + +# %% [markdown] +# # Governing equations +# +# For this problem we consider the following assumptions: +# +# * No thermal adverction is considered: $\rho_\text{w}c_\text{w}T_{,i} v_i = 0$. +# +# * Gravitational forces are neglected: $\rho g = 0$. +# +# * Both fluid and solid phases are intrinsically incompressible: $\alpha_B = 1$; $\beta = 0$. +# +# * No external fluid sink or source term: $q_H = 0$. +# +# * The porous medium is isotropic and homogeneous. +# +# These assumptions lead to the following set of governing equation describing the system behavior: +# +# **Energy balance** +# +# $$ +# \begin{gather} +# m \dot T - (K T_{,i})_{,i} = q_T +# % +# \\ +# % +# \text{where}\nonumber +# % +# \\ +# % +# m = \phi \rho_w c_w + (1-\phi) \rho_s c_s +# % +# \\ +# % +# K = \phi K_w + (1 - \phi) K_s +# % +# \\ +# % +# v_i = -\dfrac{k_s}{\eta} (p_{,i}) +# \end{gather} +# $$ +# +# **Mass balance** +# +# $$ +# \begin{gather} +# - a_u \dot T+ \dot u_{i,i} + v_{i,i} = 0 +# % +# \\ +# % +# \text{where}\nonumber +# % +# \\ +# % +# a_u = \phi a_w + (1-\phi) a_s +# \end{gather} +# $$ +# +# **Momentum balance** +# +# $$ +# \begin{equation} +# \sigma_{ij} = \sigma^\prime_{ij} - p \delta_{ij} = 0 +# \end{equation} +# $$ +# +# A detailed description about the problem formulation and equation derivation can be found in the original work of Booker and Savvidou (1985) or Chaudhry et al. (2019). +# +# ## Input parameters +# +# We considered the following set of values as input parameters: +# +#  +# + +# %% [markdown] +# # The analytical solution +# +# +# The analytical solution of the coupled THM consolidation problem is derived in the original work of Booker and Savvidou (1985). In Chaudhry et al. (2019), a corrected solution is given for the effective stress term. +# +# For clarification, the equations below are based on the solid mechanics sign convention (tensile stress is positive). Furthermore, temporal partial derivative is indicated by the dot convention, while spatial partial derivatives are expressed by the comma convention, i.e. $(\cdot)_{,i}=\partial (\cdot)/\partial x_i$. +# +# The analytical solution for the three primary variables are expressed as: +# +# **Temperature** +# +# $$ +# \begin{equation} +# \Delta T = \dfrac{Q}{4 \pi K r} f^{\kappa} +# \end{equation} +# $$ +# +# **Pore pressure** +# +# $$ +# \begin{equation} +# p = \dfrac{X Q}{(1 - \frac{c}{\kappa}) 4 \pi K r} (f^{\kappa}-f^{c}) +# \end{equation} +# $$ +# +# **Displacement of the solid skeleton** +# +# $$ +# \begin{equation} +# u_{i} = \dfrac{Q a_u x_i}{4 \pi K r} g^{\ast} +# \end{equation} +# $$ +# +# In the above equations, the following derived parameters are used: +# +# $$ +# \begin{align} +# \kappa &= \dfrac{K}{m} +# % +# \\ +# % +# c &= \dfrac{k_s}{\eta}(\lambda + 2G) +# % +# \\ +# % +# r &= \sqrt{x_{1}^{2}+x_{2}^{2}+x_{3}^{2}} +# % +# \\ +# % +# X &= a_\text{u}\left(\lambda+2G\right)-b^{\prime} +# % +# \\ +# % +# Y &= \dfrac{1}{\lambda+2G}\left(\dfrac{X}{\left(1-\dfrac{c}{\kappa}\right)a_\text{u}}+\dfrac{b^{\prime}}{a_\text{u}}\right) +# % +# \\ +# % +# Z &= \dfrac{1}{\lambda+2G}\left(\dfrac{X}{\left(1-\dfrac{c}{\kappa}\right)a_\text{u}}\right) +# % +# \\ +# % +# f^{A} &= \text{erfc}\left(\dfrac{r}{2\sqrt{At}}\right),\quad A=\kappa,c +# % +# \\ +# % +# g^{A} &= \dfrac{At}{r^{2}}+\left(\frac{1}{2}-\dfrac{At}{r^{2}}\right)f^{A}-\sqrt{\dfrac{At}{\pi r^{2}}} \exp\left(-\dfrac{r^{2}}{4At}\right) +# % +# \\ +# % +# g^{\ast} &= Yg^{\kappa}-Zg^{c} +# % +# \\ +# % +# g^{A}_{,i} &= \frac{2x_{i}At}{r^{4}}\left(f^{A}-1+\frac{r}{\sqrt{\pi At}}\exp\left(-\frac{r^{2}}{4At}\right)\right),\quad i=1,2,3 +# % +# \\ +# % +# g^{\ast}_{,i} &= Yg^{\kappa}_{,i}-Zg^{c}_{,i} +# \end{align} +# $$ +# +# The corrected form of the effective stress: +# +# $$ +# \begin{align} +# \sigma^{\prime}_{ij|j=i} &= \frac{Q a_\text{u}}{4\pi Kr}\left( 2G\left[g^{\ast}\left(1-\frac{x^{2}_{i}}{r^{2}}\right)+x_{i}g^{\ast}_{,i}\right]+\lambda \left[x_{i}g^{\ast}_{,i}+2g^{\ast}\right]\right)-b^{\prime}\Delta T +# % +# \\ +# % +# \sigma^\prime_{ij|j \neq i} &= \frac{Q a_\text{u}}{4\pi Kr}\left( G\left[x_{i}g^{\ast}_{,j}+x_{j}g^{\ast}_{,i}-2g^{\ast}\dfrac{x_{i}x_{j}}{r^{2}}\right]\right) +# \end{align} +# $$ + +# %% +import matplotlib.pyplot as plt +import numpy as np +from scipy import special as sp + + +class ANASOL: + def __init__(self): + # material parameters + self.phi = 0.16 # porosity of soil + self.k = 2e-20 # coefficient of permeability + self.eta = 1e-3 # viscosity water at 20 deg + self.E = 5.0e9 # Youngs modulus + self.nu = 0.3 # Poisson ratio + self.rho_w = 999.1 # density of pore water + self.c_w = 4280 # specific heat of pore water + self.K_w = 0.6 # thermal conductivity of pore water + self.rho_s = 2290.0 # density of solid matrix + self.c_s = 917.654 # specific heat capacity of solid matrix + self.K_s = 1.838 # themal conductivity of solid matrix + self.a_s = ( + 3 * 1.5e-5 + ) # volumetric expansivity of matrix - value conversion from linear to volumetric expansivity + self.a_w = 4.0e-4 # coefficient of volume expansion of pore water (beta_w) + + # initial and boundary condition + self.Q = ( + 2 * 150 + ) # [Q]=W strength of the heat source - value corrected to account for domain size + self.T0 = 273.15 # initial temperature + + self.Init() + + # derived parameters + def f(self, ka, R, t): + return sp.erfc(R / (2 * np.sqrt(ka * t))) + + def g(self, ka, R, t): + return ( + ka * t / R**2 + + (1 / 2 - ka * t / R**2) * sp.erfc(R / (2 * np.sqrt(ka * t))) + - np.sqrt(ka * t / (np.pi * R**2)) * np.exp(-(R**2) / (4 * ka * t)) + ) + + def gstar(self, R, t): + return self.Y * self.g(self.kappa, R, t) - self.Z * self.g(self.c, R, t) + + def R(self, x, y, z): + return np.sqrt(x**2 + y**2 + z**2) + + def dg_dR(self, ka, i, R, t): + return (2 * i / R**3) * np.sqrt(ka * t / np.pi) * np.exp( + -R * R / (4 * ka * t) + ) + (2 * i * ka * t / R**4) * (self.f(ka, R, t) - 1) + + def dgstar_dR(self, i, R, t): # Subscript R means derivative w.r.t R + return self.Y * self.dg_dR(self.kappa, i, R, t) - self.Z * self.dg_dR( + self.c, i, R, t + ) + + # corrected form of effective stress + def sigma_ii(self, x, y, z, t, ii): # for normal components + R = self.R(x, y, z) + index = {"xx": x, "yy": y, "zz": z} + return (self.Q * self.a_u / (4 * np.pi * self.K * R)) * ( + 2 + * self.G + * ( + self.gstar(R, t) * (1 - index[ii] ** 2 / R**2) + + index[ii] * self.dgstar_dR(index[ii], R, t) + ) + + self.lambd + * ( + x * self.dgstar_dR(x, R, t) + + y * self.dgstar_dR(y, R, t) + + z * self.dgstar_dR(z, R, t) + + 2 * self.gstar(R, t) + ) + ) - self.bprime * (self.temperature(x, y, z, t) - self.T0) + + def sigma_ij(self, x, y, z, t, i, j): # for shear components + R = self.R(x, y, z) + index = {"x": x, "y": y, "z": z} + return (self.Q * self.a_u / (4 * np.pi * self.K * R)) * ( + 2 + * self.G + * ( + index[i] * self.dgstar_dR(index[j], R, t) / 2 + + index[j] * self.dgstar_dR(index[i], R, t) / 2 + - index[i] * index[j] * self.gstar(R, t) / R**2 + ) + ) + + # primary variables + def temperature(self, x, y, z, t): + R = self.R(x, y, z) + return self.Q / (4 * np.pi * self.K * R) * self.f(self.kappa, R, t) + self.T0 + + def porepressure(self, x, y, z, t): + R = self.R(x, y, z) + return ( + self.X + / (1 - self.c / self.kappa) + * self.Q + / (4 * np.pi * self.K * R) + * (self.f(self.kappa, R, t) - self.f(self.c, R, t)) + ) + + def u_i(self, x, y, z, t, i): + R = self.R(x, y, z) + index = {"x": x, "y": y, "z": z} + return ( + self.a_u * index[i] * self.Q / (4 * np.pi * self.K * R) * self.gstar(R, t) + ) + + def Init(self): + # derived constants + self.lambd = ( + self.E * self.nu / ((1 + self.nu) * (1 - 2 * self.nu)) + ) # Lame constant + self.G = self.E / (2 * (1 + self.nu)) # shear constant + + self.K = ( + self.phi * self.K_w + (1 - self.phi) * self.K_s + ) # average thermal conductivity + self.m = ( + self.phi * self.rho_w * self.c_w + (1 - self.phi) * self.rho_s * self.c_s + ) + self.kappa = self.K / self.m # scaled heat conductivity + self.c = self.k / self.eta * (self.lambd + 2 * self.G) + + self.aprime = self.a_s + self.a_u = self.a_s * (1 - self.phi) + self.a_w * self.phi + self.bprime = (self.lambd + 2 * self.G / 3) * self.aprime + + self.X = self.a_u * (self.lambd + 2 * self.G) - self.bprime + self.Y = ( + 1 + / (self.lambd + 2 * self.G) + * (self.X / ((1 - self.c / self.kappa) * self.a_u) + self.bprime / self.a_u) + ) + self.Z = ( + 1 + / (self.lambd + 2 * self.G) + * (self.X / ((1 - self.c / self.kappa) * self.a_u)) + ) + + +ana_model = ANASOL() + +# %% [markdown] +# +# ## The numerical solutions +# +# For the numerical solution we compare the Thermal-Hydro-Mechanical (THM - linear and quadratic mesh), Thermal-2-Phase-Hydro-Mechanical (TH2M) and Thermal-Richard-Mechanical (TRM - quadratic mesh) formulation of OGS. +# +# The TH2M and TRM formulation methods have essential differences when applied to an unsaturated media where a gas phase is also present along side the aqueous phase. The difference originates from the way how the two mobile phases are treated specifically in the equation system: in the TH2M formulation, both the gas phase and the liquid phase is explicitely present and each phase is comprised of the two distinct component of aqueous component and non-aqueous component. In this case, the gas phase has a variable pressure solved explicitely in the governing equations. On the other hand, the TRM model assumes that the gas phase mobility is high and fast enough that gas drainage can occur significantly faster than the other processes in the system and hence, gas pressure doesn't build up. This leads to the simplification, that no gas pressure is calculated in the TRM model explicitely. +# +# The THM model is a simplified form of the general TH2M model, where there is no gas phase, only the aqueous phase is present in the equation system. +# +# In addition to the different formulation, we also compare the performance of the THM formulation with a linear and a quadratic mesh as well. + +# %% +import os + +import ogstools as ot + +data_dir = os.environ.get("OGS_DATA_DIR", "../../..") + +from pathlib import Path + +out_dir = Path(os.environ.get("OGS_TESTRUNNER_OUT_DIR", "_out")) +if not out_dir.exists(): + out_dir.mkdir(parents=True) + +# THM formulation (current working dir) +prj_file_lin = "pointheatsource_linear-mesh.prj" +prj_file_quad = "pointheatsource_quadratic-mesh.prj" +ogs_model_lin = ot.Project( + input_file=prj_file_lin, output_file=f"{out_dir}/{prj_file_lin}" +) +ogs_model_quad = ot.Project( + input_file=prj_file_quad, output_file=f"{out_dir}/{prj_file_quad}" +) + +# TH2M formulation +prj_file_th2m = "point_heatsource.prj" +path_th2m = f"{data_dir}/TH2M/THM/sphere" +prj_filepath_th2m = f"{path_th2m}/{prj_file_th2m}" +ogs_model_th2m = ot.Project( + input_file=prj_filepath_th2m, output_file=f"{out_dir}/pointheatsource_th2m.prj" +) + +# TRM formulation +prj_file_trm = "point_heat_source_2D.prj" +path_trm = f"{data_dir}/ThermoRichardsMechanics/PointHeatSource" +prj_filepath_trm = f"{path_trm}/{prj_file_trm}" +ogs_model_trm = ot.Project( + input_file=prj_filepath_trm, output_file=f"{out_dir}/pointheatsource_trm.prj" +) + +# %% +# Simulation time +t_end = 2e6 # <= was originally 5e6 +ogs_model_lin.set(t_end=t_end) +ogs_model_quad.set(t_end=t_end) +ogs_model_th2m.set(t_end=t_end) +ogs_model_trm.set(t_end=t_end) + +# %% +ogs_model_lin.set(output_prefix="pointheatsource_lin") +ogs_model_quad.set(output_prefix="pointheatsource_quad") +ogs_model_th2m.set(output_prefix="pointheatsource_th2m") +ogs_model_th2m.replace_text( + "150", xpath="./parameters/parameter[name='temperature_source_term']/value" +) +ogs_model_trm.set(output_prefix="pointheatsource_trm") + +# %% +ogs_model_lin.write_input() +ogs_model_quad.write_input() +ogs_model_th2m.write_input() +ogs_model_trm.write_input() + +# %% +import concurrent.futures +from timeit import default_timer as timer + +# Run models in parallel via concurrent.futures +ogs_models = [] +ogs_models.append( + { + "model": ogs_model_lin.prjfile, + "logfile": f"{out_dir}/lin-out.txt", + "args": f"-o {out_dir} -m . -s .", + } +) +ogs_models.append( + { + "model": ogs_model_quad.prjfile, + "logfile": f"{out_dir}/quad-out.txt", + "args": f"-o {out_dir} -m . -s .", + } +) +ogs_models.append( + { + "model": ogs_model_th2m.prjfile, + "logfile": f"{out_dir}/th2m-out.txt", + "args": f"-o {out_dir} -m {path_th2m} -s {path_th2m}", + } +) +ogs_models.append( + { + "model": ogs_model_trm.prjfile, + "logfile": f"{out_dir}/trm-out.txt", + "args": f"-o {out_dir} -m {path_trm} -s {path_trm}", + } +) + + +def run_ogs(model): + prj = model["model"] + print(f"Starting {prj} ...\n") + start_sim = timer() + # Starting via ogs6py does not work ("cannot pickle lxml"), at least on mac. + ! ogs {prj} {model["args"]} > {model["logfile"]} + assert _exit_code == 0 # noqa: F821 + runtime = timer() - start_sim + return [f"Finished {prj} in {runtime} s", runtime] + + +import platform + +if platform.system() == "Darwin": + import multiprocessing as mp + + mp.set_start_method("fork") + +runtimes = [] +start = timer() +with concurrent.futures.ProcessPoolExecutor() as executor: + results = executor.map(run_ogs, ogs_models) + for result in results: + print(result[0]) + runtimes.append(result[1]) +print(f"Elapsed time for all simulations: {timer() - start} s") + +# %% [markdown] +# ## Evaluation and Results +# +# The analytical expressions together with the numerical model can now be evaluated at different points as a function of time (time series) or for a given time as a function of their spatial coordinates (along radial axis). + +# %% +import vtuIO + +# Point of interest +pts = {"pt0": (0.5, 0.5, 0.0)} + +# Time axis for analytical solution +t = np.linspace(1, 50000 * 200, num=201, endpoint=True) + +projects = [ + "pointheatsource_lin", + "pointheatsource_quad", + "pointheatsource_th2m", + "pointheatsource_trm", +] + +pvds = [] +for prj in projects: + pvds.append(vtuIO.PVDIO(f"{out_dir}/{prj}.pvd", dim=2)) + +# %% [markdown] +# ### Time series plots for temperature, pressure and displacement +# +# Comparison between the analytical solution and the numerical solution shows very good agreement, as displayed below in the figures. + +# %% +plt.rcParams["lines.linewidth"] = 2.0 +plt.rcParams["lines.color"] = "black" +plt.rcParams["legend.frameon"] = True +plt.rcParams["font.family"] = "serif" +plt.rcParams["legend.fontsize"] = 14 +plt.rcParams["font.size"] = 14 +plt.rcParams["axes.axisbelow"] = True +plt.rcParams["figure.figsize"] = (16, 6) + +output = { + "T": ( + "temperature", + "temperature_interpolated", + "temperature_interpolated", + "temperature_interpolated", + ), + "p": ( + "pressure", + "pressure_interpolated", + "gas_pressure_interpolated", + "pressure_interpolated", + ), + "u": ("displacement", "displacement", "displacement", "displacement"), + "color": ("r+", "rx", "b+", "g+"), + "label": ("ogs6 thm lin", "ogs6 thm quad", "ogs6 th2m", "ogs6 trm"), +} + +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot( + t, + ana_model.temperature(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t), + "k", + label="analytical", +) +for i, pvd in enumerate(pvds): + ax1.plot( + pvd.timesteps, + pvd.read_time_series(output["T"][i], pts=pts)["pt0"], + output["color"][i], + label=output["label"][i], + ) +ax1.set_xscale("log") +ax1.set_xlabel("t / s") +ax1.set_ylabel("T / K") +ax1.set_xlim(1.0e4, 2.0e7) +ax1.set_ylim(270.0, 292.0) +ax1.legend(loc="lower right") +ax1.set_title("Temperature") + +ax2.set_xscale("log") +ax2.set_xlabel("t / s") +ax2.set_ylabel("error / K") +ax2.set_xlim(1.0e4, 2.0e7) +ax2.set_title("Temperature error / numerical - analytical") + +for i, pvd in enumerate(pvds): + interp_ana_model = np.interp( + pvd.timesteps, + t, + ana_model.temperature(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t), + ) + error = pvd.read_time_series(output["T"][i], pts=pts)["pt0"] - interp_ana_model + ax2.plot(pvd.timesteps, error, output["color"][i], label=output["label"][i]) + assert np.all(error < 0.2) + assert np.all(error > -0.06) + +ax2.legend(loc="upper right") + +fig1.tight_layout() + +# %% +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot( + t, + ana_model.porepressure(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t) / 1.0e6, + "k", + label="analytical", +) +for i, pvd in enumerate(pvds): + ax1.plot( + pvd.timesteps, + pvd.read_time_series(output["p"][i], pts=pts)["pt0"] / 1.0e6, + output["color"][i], + label=output["label"][i], + ) +ax1.set_xscale("log") +ax1.set_xlabel("t / s") +ax1.set_ylabel("p / MPa") +ax1.set_xlim(1.0e4, 2.0e7) +ax1.legend(loc="lower right") +ax1.set_title("Pressure") + +ax2.set_xscale("log") +ax2.set_xlabel("t / s") +ax2.set_ylabel("error / MPa") +ax2.set_xlim(1.0e4, 2.0e7) +ax2.set_title("Pressure error / numerical - analytical") + +for i, pvd in enumerate(pvds): + interp_ana_model = np.interp( + pvd.timesteps, + t, + ana_model.porepressure(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t), + ) + error = pvd.read_time_series(output["p"][i], pts=pts)["pt0"] - interp_ana_model + ax2.plot(pvd.timesteps, error / 1.0e6, output["color"][i], label=output["label"][i]) + assert np.all(error < 0.1 * 1e6) + assert np.all(error > -0.06 * 1e6) + +ax2.legend(loc="upper right") + +fig1.tight_layout() + +# %% +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot( + t, + ana_model.u_i(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t, "x") * 1000, + "k", + label="analytical", +) +for i, pvd in enumerate(pvds): + ax1.plot( + pvd.timesteps, + pvd.read_time_series(output["u"][i], pts=pts)["pt0"][:, 0] * 1000, + output["color"][i], + label=output["label"][i], + ) +ax1.set_xscale("log") +ax1.set_xlabel("t / s") +ax1.set_ylabel("$u_x$ / $10^{-3}$ m") +ax1.set_xlim(1.0e4, 2.0e7) +ax1.legend(loc="lower right") +ax1.set_title("Displacement") + +ax2.set_xscale("log") +ax2.set_xlabel("t / s") +ax2.set_ylabel("error / $10^{-3}$ m") +ax2.set_xlim(1.0e4, 2.0e7) +ax2.set_title("Displacement error / numerical - analytical") + +for i, pvd in enumerate(pvds): + interp_ana_model = np.interp( + pvd.timesteps, + t, + ana_model.u_i(pts["pt0"][0], pts["pt0"][1], pts["pt0"][2], t, "x"), + ) + error = ( + pvd.read_time_series(output["u"][i], pts=pts)["pt0"][:, 0] - interp_ana_model + ) + ax2.plot(pvd.timesteps, error * 1000, output["color"][i], label=output["label"][i]) + assert np.all(error < 0.0005) + assert np.all(error > -0.0035) + +ax2.legend(loc="lower right") + +fig1.tight_layout() + +# %% [markdown] +# ### Plots for temperature, pressure and displacement along the radial axis +# +# The comparison between the analytical and the numerical results along the radial axis generally shows good agreement. The differences observed can be primarily explained by mesh discretization and finite size effects. This is particularly the case for the th2m simulation results, where the differences are slightly more emphasized which is the results of larger time steps. + +# %% +# Time stamp for the results along the radial axis +t_i = 1.0e5 + +# Radial coordinates for plotting +x = np.linspace(start=0.0001, stop=10.0, num=100) +r = [(i, 0, 0) for i in x] + +# %% +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot(x, ana_model.temperature(x, 0, 0, t_i), "k", label="analytical") +for i, pvd in enumerate(pvds): + ax1.plot( + x, + pvd.read_set_data(t_i, output["T"][i], pointsetarray=r, data_type="point"), + output["color"][i], + label=output["label"][i], + ) + +ax1.set_xlim(0, 2.0) +ax1.set_ylim(250.0, 400.0) +ax1.set_xlabel("r / m") +ax1.set_ylabel("T / K") +ax1.legend() +ax1.set_title("Temperature") + +ax2.set_xlim(0, 2.0) +ax2.set_ylim(-3, 1) +ax2.set_xlabel("r / m") +ax2.set_ylabel("error / K") +ax2.set_title("Temperature error / numerical - analytical") + +for i, pvd in enumerate(pvds): + error = pvd.read_set_data( + t_i, output["T"][i], pointsetarray=r, data_type="point" + ) - ana_model.temperature(x, 0, 0, t_i) + ax2.plot(x, error, output["color"][i], label=output["label"][i]) + assert np.all( + error[1:] < 0.5 + ) # do not check first entry, which corresponds to the origin + assert np.all( + error[1:] > -2.5 + ) # do not check first entry, which corresponds to the origin + +ax2.legend() + +fig1.tight_layout() + +# %% +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot(x, ana_model.porepressure(x, 0, 0, t_i) / 1e6, "k", label="analytical") +for i, pvd in enumerate(pvds): + ax1.plot( + x, + pvd.read_set_data(t_i, output["p"][i], pointsetarray=r, data_type="point") + / 1.0e6, + output["color"][i], + label=output["label"][i], + ) + +ax1.set_xlim(0, 2.0) +ax1.set_ylim(0, 35.0) +ax1.set_xlabel("r / m") +ax1.set_ylabel("p / MPa") +ax1.legend() +ax1.set_title("Pressure") + +ax2.set_xlim(0, 2.0) +ax2.set_xlabel("r / m") +ax2.set_ylabel("error / MPa") +ax2.set_title("Pressure error / numerical - analytical") + +for i, pvd in enumerate(pvds): + error = ( + pvd.read_set_data(t_i, output["p"][i], pointsetarray=r, data_type="point") + - ana_model.porepressure(x, 0, 0, t_i) + ) / 1.0e6 + ax2.plot(x, error, output["color"][i], label=output["label"][i]) + assert np.all(error < 2.5) + assert np.all(error > -1.0) + +ax2.legend() + +fig1.tight_layout() + +# %% +fig1, (ax1, ax2) = plt.subplots(1, 2) + +ax1.plot(x, ana_model.u_i(x, 0, 0, t_i, "x") * 1000, "k", label="analytical") +for i, pvd in enumerate(pvds): + ax1.plot( + x, + pvd.read_set_data(t_i, output["u"][i], pointsetarray=r, data_type="point")[:, 0] + * 1000, + output["color"][i], + label=output["label"][i], + ) + +ax1.set_xlim(0, 2.0) +ax1.set_xlabel("r / m") +ax1.set_ylabel("$u_r$ / $10^{-3}$ m") +ax1.legend() +ax1.set_title("Displacement") + +ax2.set_xlim(0, 2.0) +ax2.set_ylim(-0.025, 0.025) +ax2.set_xlabel("r / m") +ax2.set_ylabel("error / $10^{-3}$ m") +ax2.set_title("Displacement error / numerical - analytical") + +for i, pvd in enumerate(pvds): + error = ( + pvd.read_set_data(t_i, output["u"][i], pointsetarray=r, data_type="point")[:, 0] + - ana_model.u_i(x, 0, 0, t_i, "x") + ) * 1000 + ax2.plot(x, error, output["color"][i], label=output["label"][i]) + assert np.all(error[1:] < 0.01) + assert np.all(error[1:] > -0.015) + +ax2.legend() + +fig1.tight_layout() + +# %% [markdown] +# ## Execution times +# +# To compare the performance of the different numerical solutions implemented in OGS6, we compare the execution time of the simulations. The linear thm and trm solutions perform best, while the quadratic thm and th2m solutions take significantly longer time to run. It is also important to mention here, that the time step size selected for the th2m solution are twice as big as the other 3 implementation, yet simulation time still takes longer than any of the other solution. + +# %% +fig = plt.figure() +ax = fig.add_axes([0, 0, 1, 1]) +mesh = ["thm linear", "thm quadratic", "th2m", "trm"] +ax.bar(mesh, runtimes) +plt.ylabel("exec. time / s") +plt.show() + +# %% [markdown] +# ## References +# +# [1] Booker, J. R.; Savvidou, C. (1985), Consolidation around a point heat source. International Journal for Numerical and Analytical Methods in Geomechanics, 1985, 9. Jg., Nr. 2, S. 173-184. +# +# [2] Chaudhry, A. A.; Buchwald, J.; Kolditz, O. and Nagel, T. (2019), Consolidation around a point heatsource (correction & verification). International Journal for Numerical and Analytical Methods in Geomechanics, 2019, <https://doi.org/10.1002/nag.2998>. + +# %%