import underworld3 as uw
import numpy as np
import sympy
Notebook 8: Particle Swarms
We used a particle swarm to track the flow in Example 7. We called this a “passive” swarm because the points did not influence the flow in any way but was simply carried along.
Particle swarms are unstructured data objects that live within the computational domain. Their points can be moved arbitrarily through the domain and points may migrate from one process to another when the coordinates are changed. By default they carry only the particle location, but we can add scalar, vector and tensor variables to the swarm and they will be transported with the particles.
Particle transport is usually through a velocity or displacement field that incrementally changes the locations. This is a common use, but particles can be used to represent any unstructured field. For example, during mesh adaptation, the nodal points from the previous mesh are equivalent to a disconnected swarm from the point of view of the new mesh. The same is true when reading data save from one mesh to the MeshVariables
on another.
= 12
res = 8
width
= uw.meshing.UnstructuredSimplexBox(
mesh =1/res,
cellSize=(0.0,0.0),
minCoords=(width, 1.0),
maxCoords=3,
qdegree
)
# Coordinate directions etc
= mesh.CoordinateSystem.X x, y
# original y coordinate
= uw.discretisation.MeshVariable("Y0",
y0
mesh, =uw.VarType.SCALAR,
vtype=r"y_0")
varsymbol
with mesh.access(y0):
0] = uw.function.evalf(y, y0.coords) y0.data[:,
Deform the mesh
Move all nodes down to meet an undulating lower surface. The displacement field is smooth and continuous, so there is no particular need to remesh in this case. However, it is generally better to consider either deforming the mesh with gmsh
before triangulation, or remeshing (both are possible with underworld3, but not as simple to demonstrate).
= mesh.data
new_coords = (1-y) * (sympy.sin(sympy.pi * x)/10)
dy 1] = uw.function.evalf(y-dy, mesh.data)
new_coords[:,
display(dy)
mesh.deform_mesh(new_coords)
\(\displaystyle \frac{\left(1 - \mathrm{y}\right) \sin{\left(\mathrm{x} \pi \right)}}{10}\)
# y coordinate after 1st deformation
= uw.discretisation.MeshVariable("Y1",
y1
mesh, =uw.VarType.SCALAR,
vtype=r"y_1")
varsymbol
with mesh.access(y1):
0] = uw.function.evalf(y, y1.coords) y1.data[:,
Deform the mesh again
Now we tilt the lower surface down. The variable y1
is negative in regions where the surface was low in the deformed mesh so it is a good first approximation to where ponds of dense fluid might form (e.g. water under ice).
= mesh.data
new_coords = (1-y) * x/16
dy 1] = uw.function.evalf(y-dy, mesh.data)
new_coords[:,
mesh.deform_mesh(new_coords)
if uw.mpi.size == 1:
import pyvista as pv
import underworld3.visualisation as vis
= vis.mesh_to_pv_mesh(mesh)
pvmesh "y0"] = vis.scalar_fn_to_pv_points(pvmesh, y0.sym)
pvmesh.point_data["y1"] = vis.scalar_fn_to_pv_points(pvmesh, y1.sym)
pvmesh.point_data[
= pv.Plotter(window_size=(750, 250))
pl
pl.add_mesh(
pvmesh,="y1",
scalars="RdBu_r",
cmap="Grey",
edge_color=0.33,
edge_opacity=True,
show_edges=False,
use_transparency=1,
opacity=True,
show_scalar_bar
)
3)
pl.camera.zoom(
Add a swarm to this mesh
A swarm object can be constructed either by adding local points (Example 7) or by filling the mesh with a given density. The density value that we provide (fill_param
) adds particles on the Gaussian integration points: 0 will populate the centroids of the mesh elements A value of 1 provides three points per triangle, four per quad, four in a tetrahedron, eight in a hexahedron (the points that support linear interpolation in standard FEM).
Note: in the current implementation of underworld
swarm, we an only start adding points when the swarm variables are all finalised.
= uw.swarm.Swarm(mesh)
swarm
= uw.swarm.SwarmVariable("S",
s
swarm, =uw.VarType.SCALAR,
vtype=1,
proxy_degree=True,
proxy_continuous=r"\Sigma",
varsymbol
)
## It is a limitation of the swarm that we
## can only start adding points when the swarm variables
## are all finalised.
0)
swarm.populate(
## Set some values on s
with swarm.access(s):
0] = uw.function.evaluate(y1.sym, swarm.particle_coordinates.data)
s.data[:,
Proxy Variables
Swarm variables are completely unstructured (they are not located on a grid and they also have no neighbour information). We want to be able to mix their values with mesh-based variables in sympy
expressions. To do this, each swarm variable has (by default) a proxy mesh variable which is constructed through a projection to nodal point values. It is accessed as SwarmVariable.sym
and, symbolically, it is bracketed \(\left< \cdot \right>\) as a reminder that there is a projection in between the values and the representation on the mesh.
+ y1.sym s.sym
\(\displaystyle \left[\begin{matrix}{\left<\Sigma\right>}(\mathbf{x}) + {y_1}(\mathbf{x})\end{matrix}\right]\)
if uw.mpi.size == 1:
import pyvista as pv
import underworld3.visualisation as vis
= vis.mesh_to_pv_mesh(mesh)
pvmesh "y0"] = vis.scalar_fn_to_pv_points(pvmesh, y0.sym)
pvmesh.point_data["y1"] = vis.scalar_fn_to_pv_points(pvmesh, y1.sym)
pvmesh.point_data["S"] = vis.scalar_fn_to_pv_points(pvmesh, s.sym)
pvmesh.point_data["delta"] = vis.scalar_fn_to_pv_points(pvmesh, s.sym - y1.sym)
pvmesh.point_data[
= vis.swarm_to_pv_cloud(swarm)
swarm_points
= pv.Plotter(window_size=(750, 250))
pl
pl.add_mesh(
pvmesh,="RdBu_r",
cmap="Grey",
edge_color="delta",
scalars=0.33,
edge_opacity=True,
show_edges=False,
use_transparency=0.75,
opacity=True,
show_scalar_bar
)
pl.add_points(swarm_points.points, =3,
point_size='points',
style="Black",
color=1,
opacity
)
f"html5/terrain_flow_mesh_only.html")
pl.export_html(
from IPython.display import IFrame
=f"html5/terrain_flow_mesh_only.html", width=750, height=300) IFrame(src
Interactive Image: Convection model output
# Mesh variables for the unknowns
= uw.discretisation.MeshVariable("V0", mesh, 2, degree=2, varsymbol=r"{v_0}")
v_soln = uw.discretisation.MeshVariable("p", mesh, 1, degree=1, continuous=True) p_soln
= True
fixed_base
= uw.systems.Stokes(
stokes
mesh, =v_soln,
velocityField=p_soln,
pressureField
)
= sympy.Matrix((0,0))
stokes.bodyforce
= uw.constitutive_models.ViscousFlowModel
stokes.constitutive_model = sympy.Piecewise(
stokes.constitutive_model.Parameters.shear_viscosity_0 1)/100, s.sym[0] < 0.05),
(sympy.sympify(1, True, )
(
)
1, 0), "Top")
stokes.add_essential_bc((
if fixed_base == True:
0, 0), "Bottom")
stokes.add_essential_bc((else:
# Free slip on the (deformed) base below a certain Level:
# 1) Penalise normal velocity below
# 2) Penalise the entire velocity vector above
= mesh.Gamma
Gamma = uw.function.expression(r"|\Gamma|", sympy.sqrt(Gamma.dot(Gamma)), "Scaling for surface normals")
GammaNorm = Gamma / GammaNorm
GammaN
10000 * v_soln.sym.dot(GammaN) * GammaN, "Bottom")
stokes.add_natural_bc(
= 1.0e-4 stokes.tolerance
stokes.constitutive_model.Parameters.shear_viscosity_0.sym
\(\displaystyle \begin{cases} \frac{1}{100} & \text{for}\: {\left<\Sigma\right>}(\mathbf{x}) < 0.05 \\1 & \text{otherwise} \end{cases}\)
stokes.solve()
if uw.mpi.size == 1:
import pyvista as pv
import underworld3.visualisation as vis
= vis.mesh_to_pv_mesh(mesh)
pvmesh "S"] = vis.scalar_fn_to_pv_points(pvmesh, s.sym)
pvmesh.point_data["eta"] = vis.scalar_fn_to_pv_points(pvmesh, stokes.constitutive_model.Parameters.shear_viscosity_0.sym)
pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v_soln.sym)
pvmesh.point_data[
= vis.swarm_to_pv_cloud(swarm)
pvswarm "eta"] = vis.scalar_fn_to_pv_points(pvswarm, stokes.constitutive_model.Parameters.shear_viscosity_0.sym)
pvswarm.point_data[
= pv.Plotter(window_size=(750, 250))
pl
pl.add_mesh(
pvmesh,="RdBu_r",
cmap="Grey",
edge_color="eta",
scalars=0.33,
edge_opacity=True,
show_edges=False,
use_transparency=0.05,
opacity=False,
show_scalar_bar
)
pl.add_points(
pvswarm,="eta",
scalars="Blues_r",
cmap=0.33,
opacity=5,
point_size
)
pl.add_arrows(pvmesh.points, "V"],
pvmesh.point_data[="Greens",
cmap=0.5,
mag=False)
show_scalar_bar
f"html5/terrain_flow_plot.html")
pl.export_html(
from IPython.display import IFrame
=f"html5/terrain_flow_plot.html", width=750, height=300) IFrame(src
Interactive Image: Convection model output
Exercise - 8.1
Look at the Stokes example and try with a free slip base and low / high viscosity for the trapped material. You could also see what mixing the boundary condition looks like: free slip in the troughs, fixed in the highs. This is possible with a single boundary condition if you use sympy.Piecewise
to switch between different penalty conditions.
## Free slip base (conditional)
= mesh.Gamma
Gamma = uw.function.expression(r"|\Gamma|", sympy.sqrt(Gamma.dot(Gamma)), "Scaling for surface normals")
GammaNorm = Gamma / GammaNorm
GammaN
= sympy.Piecewise((1.0, y1.sym[0] < -0.05), (0.0, True))
bc_mask = 10000 * bc_mask * GammaN.dot(v_soln.sym) * GammaN + (1-bc_mask) * v_soln.sym
nbc "Lower") stokes.add_natural_bc(nbc,
Give that a try !