import underworld3 as uw
import numpy as np
import sympy
Notebook 6: Time-dependence

We’ll look at a convection problem which couples Stokes Flow with time-dependent advection/diffusion.
The starting point is our previous notebook where we solved for Stokes flow in a cylindrical annulus geometry. We then add an advection-diffusion solver to evolve temperature. The Stokes buoyancy force is proportional to the temperature anomaly, and the velocity solution is fed back into the temperature advection term.
= 10
res = 1.0
r_o = 0.55
r_i
= 3.0e4
rayleigh_number
= uw.meshing.Annulus(radiusOuter=r_o,
meshball =r_i,
radiusInner=1/res,
cellSize=3,
qdegree
)
# Coordinate directions etc
= 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 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.discretisation.MeshVariable("T", meshball, 1, degree=3, continuous=True) t_soln
Create linked solvers
We create the Stokes solver as we did in the previous notebook. The buoyancy force is proportional to the temperature anomaly (t_soln
). Solvers can either be provided with unknowns as pre-defined meshVariables, or they will define their own. When solvers are coupled, explicitly defining unknowns makes everything clearer.
The advection-diffusion solver evolved t_soln
using the Stokes velocity v_soln
in the fluid-transport term.
Curved free-slip boundaries
In the annulus, a free slip boundary corresponds to zero radial velocity. However, in this mesh, \(v_r\) is not one of the unknowns (\(\mathbf{v} = (v_x, v_y)\)). We apply a non linear boundary condition that penalises \(v_r\) on the boundary as discussed previously in Example 5.
= uw.systems.Stokes(
stokes =v_soln,
meshball, velocityField=p_soln,
pressureField
)
= rayleigh_number * t_soln.sym * unit_rvec
stokes.bodyforce
= uw.constitutive_models.ViscousFlowModel
stokes.constitutive_model = 1
stokes.constitutive_model.Parameters.shear_viscosity_0 = 1.0e-3
stokes.tolerance
"fieldsplit_velocity_mg_coarse_pc_type"] = "svd"
stokes.petsc_options[
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(
# Create solver for the energy equation (Advection-Diffusion of temperature)
= uw.systems.AdvDiffusion(
adv_diff
meshball,=t_soln,
u_Field=v_soln,
V_fn=2,
order=False,
verbose
)
= uw.constitutive_models.DiffusionModel
adv_diff.constitutive_model = 1
adv_diff.constitutive_model.Parameters.diffusivity
## Boundary conditions for this solver
+1.0, "Lower")
adv_diff.add_dirichlet_bc(-0.0, "Upper") adv_diff.add_dirichlet_bc(
Underworld expressions
Note that
type(stokes.constitutive_model.Parameters.shear_viscosity_0))
display(type(adv_diff.constitutive_model.Parameters.diffusivity))
display(
stokes.constitutive_model.Parameters.shear_viscosity_0.sym
underworld3.function.expressions.UWexpression
underworld3.function.expressions.UWexpression
\(\displaystyle 1\)
Initial condition
We need to set an initial condition for the temperature field as the coupled system is an initial value problem. Choose whatever works but remember that the boundary conditions will over-rule values you set on the lower and upper boundaries.
# Initial temperature
= 0.1 * sympy.sin(3 * th) * sympy.cos(np.pi * (r - r_i) / (r_o - r_i)) + (
init_t - r
r_o / (r_o - r_i)
)
with meshball.access(t_soln):
0] = uw.function.evaluate(init_t, t_soln.coords) t_soln.data[:,
Initial velocity solve
The first solve allows us to determine the magnitude of the velocity field and is useful to keep separated to check convergence rates etc.
For non-linear problems, we usually need an initial guess using a reasonably close linear problem.
zero_init_guess
is used to reset any information in the vector of unknowns (i.e. do not use any initial information if zero_init_guess==True
).
=True) stokes.solve(zero_init_guess
# Keep the initialisation separate
# so we can run the loop again in a notebook
= 50
max_steps = 0
timestep = 0.0 elapsed_time
=0.1) adv_diff.solve(timestep
# Null space ?
for step in range(0, max_steps):
=False)
stokes.solve(zero_init_guess= adv_diff.estimate_dt()
delta_t =delta_t)
adv_diff.solve(timestep
+= 1
timestep += delta_t
elapsed_time
if timestep%5 == 0:
print(f"Timestep: {timestep}, time {elapsed_time}")
Timestep: 5, time 0.007655378472205266
Timestep: 10, time 0.01387213547299994
Timestep: 15, time 0.017197945909787137
Timestep: 20, time 0.019855789890665786
Timestep: 25, time 0.02250450167934171
Timestep: 30, time 0.02546333347384266
Timestep: 35, time 0.028720014807997008
Timestep: 40, time 0.032113891988035236
Timestep: 45, time 0.035501720967277196
Timestep: 50, time 0.038852418750145654
# visualise it
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_soln.sym)
pvmesh.point_data[
= vis.meshVariable_to_pv_mesh_object(t_soln)
pvmesh_t "T"] = vis.scalar_fn_to_pv_points(pvmesh_t, t_soln.sym)
pvmesh_t.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=1.0,
max_time=500,
max_steps
)
= pv.Plotter(window_size=(750, 750))
pl
pl.add_mesh(
pvmesh_t,="RdBu_r",
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/annulus_convection_plot.html")
pl.export_html(# pl.show(cpos="xy", jupyter_backend="trame")
from IPython.display import IFrame
="html5/annulus_convection_plot.html", width=500, height=400) IFrame(src
Interactive Image: Convection model output
Exercise - Null space
Based on our previous notebook, can you see how to calculate and (if necessary) remove rigid-body the rotation null-space from the solution ?
The use of a coarse-level singular-value decomposition for the velocity solver should help, in this case, but it’s wise to check anyway.
"fieldsplit_velocity_mg_coarse_pc_type"] = "svd" stokes.petsc_options[
Exercise - Heat flux
Could you calculate the radial heat flux field ? Its surface average value plotted against time tells you if you have reached a steady state.
Hint:
\[ Q_\textrm{surf} = \nabla T \cdot \hat{r} + T (\mathbf{v} \cdot \hat{r} ) \]
= -meshball.vector.gradient(t_soln.sym).dot(unit_rvec) +\
Q_surf 0] * v_soln.sym.dot(unit_rvec) t_soln.sym[