import underworld3 as uw
import numpy as np
import sympy
Notebook 5: Stokes Equation
Mesh with embedded internal surface where we apply harmonic thermal forcing. This allows us to compute topography and gravity kernels. We’ll use a cylindrical annulus this timeand we’ll use free-slip boundary conditions throughout.
= 0.075
res = 1.0
r_o = 0.825
r_int = 0.55 r_i
= uw.meshing.AnnulusInternalBoundary(radiusOuter=r_o,
meshball =r_int,
radiusInternal=r_i,
radiusInner=res,
cellSize_Inner=res*0.5,
cellSize_Internal=res,
cellSize_Outer=False,)
centre
meshball.view()
= meshball.CoordinateSystem.X
x, y = meshball.CoordinateSystem.xR
r, th = meshball.CoordinateSystem.unit_e_0
unit_rvec
# Orientation of surface normals
= unit_rvec
Gamma_N
Mesh # 0: .meshes/uw_annulus_internalBoundary_rO1.0rInt0.825_rI0.55_csize0.1_csizefs0.075.msh
No variables are defined on the mesh
| Boundary Name | ID | Min Size | Max Size |
| ------------------------------------------------------ |
| Lower | 1 | 94 | 94 |
| Internal | 2 | 278 | 278 |
| Upper | 3 | 166 | 166 |
| Centre | 10 | 0 | 0 |
| All_Edges | 1000 | 0 | 0 |
| All_Boundaries | 1001 | 132 | 132 |
| UW_Boundaries | -- | 538 | 538 |
| ------------------------------------------------------ |
DM Object: uw_.meshes/uw_annulus_internalBoundary_rO1.0rInt0.825_rI0.55_csize0.1_csizefs0.075.msh 1 MPI process
type: plex
uw_.meshes/uw_annulus_internalBoundary_rO1.0rInt0.825_rI0.55_csize0.1_csizefs0.075.msh in 2 dimensions:
Number of 0-cells per rank: 1142
Number of 1-cells per rank: 3294
Number of 2-cells per rank: 2152
Labels:
depth: 3 strata with value/size (0 (1142), 1 (3294), 2 (2152))
All_Boundaries: 1 strata with value/size (1001 (132))
Elements: 1 strata with value/size (666666 (3022))
Internal: 1 strata with value/size (2 (278))
Lower: 1 strata with value/size (1 (94))
Upper: 1 strata with value/size (3 (166))
celltype: 3 strata with value/size (0 (1142), 1 (3294), 3 (2152))
UW_Boundaries: 3 strata with value/size (1 (94), 2 (278), 3 (166))
Solver setup
We can obtain unit vectors in the natural coordinate system (here \(r\), \(\theta\)) as mesh.CoordinateSystem.unit_e_0
, mesh.CoordinateSystem.unit_e_1
. There is a null space if we apply the boundary conditions exactly, and so we define a function to represent the null space.
We can set solver options via the petsc_options
interface on the solver.
# Create a density structure / buoyancy force
# gravity will vary linearly from zero at the centre
# of the sphere to (say) 1 at the surface
# Null space in velocity (constant v_theta) expressed in x,y coordinates
= r * meshball.CoordinateSystem.rRotN.T * sympy.Matrix((0,1))
v_theta_fn_xy
# Mesh variables for the unknowns
= uw.discretisation.MeshVariable("V0", meshball, 2, degree=2, varsymbol=r"{v_0}")
v_soln = uw.discretisation.MeshVariable("p", meshball, 1, degree=1, continuous=True)
p_soln
= uw.systems.Stokes(
stokes
meshball, =v_soln,
velocityField=p_soln,
pressureField
)
= uw.constitutive_models.ViscousFlowModel
stokes.constitutive_model = 1
stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0e-6
stokes.tolerance
"ksp_monitor", None)
stokes.petsc_options.setValue("snes_monitor", None)
stokes.petsc_options.setValue("fieldsplit_velocity_mg_coarse_pc_type"] = "svd" stokes.petsc_options[
Natural boundary conditions
In FEM, natural boundary conditions are fluxes specified at a boundary (through surface integrals). We can also apply integrals to internal surfaces. We need to compute the vector components of the boundary condition (normal / tangential) and supply them in the Cartesian frame.
The bouyancy force on the internal surface is already radial, so this translates to
-t_init * unit_rvec, "Internal") stokes.add_natural_bc(
To set a no-normal-flow boundary condition, we need to penalise the radial velocity at the boundaries. Symbolically this is
\[ \mathbf{f} = \lambda \left( \mathbf{v} \cdot \Gamma_N \right) \,\, \Gamma_N \]
where \(\lambda\) is a large penalty value. This translates into sympy
code as
stokes.add_natural_bc(* Gamma_N.dot(v_soln.sym) * Gamma_N, "Lower"
penalty )
= sympy.Matrix([0,0])
stokes.bodyforce
# Note, the thermal bouyancy field is localised in the radius using a
# gaussian solely for the purposes of plotting. It is automatically
# a delta function when applied through a surface integral
= sympy.sin(5*th) * sympy.exp(-1000.0 * ((r - r_int) ** 2))
t_init
-t_init * unit_rvec, "Internal")
stokes.add_natural_bc(10000 * Gamma_N.dot(v_soln.sym) * Gamma_N, "Upper")
stokes.add_natural_bc(
if r_i != 0.0:
10000 * Gamma_N.dot(v_soln.sym) * Gamma_N, "Lower")
stokes.add_natural_bc(
stokes.solve()
0 SNES Function norm 2.307507502179e-01
Residual norms for stokes_ solve.
0 KSP Residual norm 8.742204758135e+00
1 KSP Residual norm 7.603223176805e-07
1 SNES Function norm 4.751452850114e-08
Removal of the null space
We can use the uw.maths.Integral
to compute the inner product of the null space and the velocity solution. It is not zero, so we remove it !
# Null space evaluation
= uw.maths.Integral(meshball, v_theta_fn_xy.dot(v_theta_fn_xy))
I0 = I0.evaluate()
vnorm
= v_theta_fn_xy.dot(v_soln.sym)
I0.fn = I0.evaluate()
ns
print(ns/vnorm, vnorm)
with meshball.access(v_soln):
= uw.function.evaluate(ns * v_theta_fn_xy, v_soln.coords) / vnorm
dv -= dv
v_soln.data[...]
= I0.evaluate()
ns print(ns/vnorm, vnorm)
0.0005624988949826505 1.4249504223200373
1.9675625281382883e-19 1.4249504223200373
if uw.mpi.size == 1:
import pyvista as pv
import underworld3.visualisation as vis
= vis.mesh_to_pv_mesh(meshball)
pvmesh "P"] = vis.scalar_fn_to_pv_points(pvmesh, p_soln.sym)
pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
pvmesh.point_data["T"] = vis.scalar_fn_to_pv_points(pvmesh, t_init)
pvmesh.point_data[
= 1
skip = np.zeros((meshball._centroids[::skip].shape[0], 3))
points 0] = meshball._centroids[::skip, 0]
points[:, 1] = meshball._centroids[::skip, 1]
points[:, = pv.PolyData(points)
point_cloud
= pvmesh.streamlines_from_source(
pvstream ="V",
point_cloud, vectors="both",
integration_direction=45,
integrator_type=True,
surface_streamlines=0.01,
initial_step_length=0.5,
max_time=500,
max_steps
)
= pv.Plotter(window_size=(750, 750))
pl
pl.add_mesh(
pvmesh,="coolwarm",
cmap="Grey",
edge_color=0.33,
edge_opacity="T",
scalars=True,
show_edges=False,
use_transparency=1.0,
opacity=False
show_scalar_bar
)
=0.3, show_scalar_bar=False, cmap="Greens", render_lines_as_tubes=False)
pl.add_mesh(pvstream, opacity
"html5/stokes_annulus_plot.html") pl.export_html(
from IPython.display import IFrame
="html5/stokes_annulus_plot.html", width=500, height=400) IFrame(src
Interactive Image: Annulus mesh of triangular elements on which we evaluated Stokes flow driven by an internal delta function buoyancy. Boundary conditions are free slip, imposed using a penalty on the radial velocity at the boundary