The Mesh ======== The finite element mesh is a fundamental construct for Underworld modelling. It will generally determine your domain geometry, and the resolution of the finite element system. For parallel simulations, the mesh topology will also determine the domain decomposition for the problem. Currently ``underworld`` only provides curvilinear mesh capabilities. Overview: ^^^^^^^^^ 1. Creating mesh objects. 2. Element types. 3. Deforming the mesh. 4. Loading and saving the mesh. 5. Special sets. 6. Mesh variables 7. Setting values on a mesh variables. 8. Gradients of mesh variable fields. 9. Loading and saving mesh variable data. **Keywords:** mesh variables, finite elements, load, save, initial conditions Creating the mesh ----------------- First create an 2x2 element mesh. By default the mesh will be of rectangular geometry, with domain extents specified via the ``minCoord`` and ``maxCoord`` constructor parameters. .. raw:: html
# This example creates a mesh, and visualises it.
import underworld as uw
import underworld.visualisation as vis
mesh = uw.mesh.FeMesh_Cartesian( elementRes = (2, 2),
minCoord = (0.0, 0.0),
maxCoord = (2.0, 1.0) )
# visualising the result
figMesh = vis.Figure(figsize=(800,400))
figMesh.append( vis.objects.Mesh(mesh, nodeNumbers=True, pointsize=10) )
figMesh.show()
.. raw:: html
# This example simply prints mesh node information.
import underworld as uw
mesh = uw.mesh.FeMesh_Cartesian(elementRes=(2, 2))
for item in mesh.data:
print(item)
.. parsed-literal::
[ 0. 0.]
[ 0.5 0. ]
[ 1. 0.]
[ 0. 0.5]
[ 0.5 0.5]
[ 1. 0.5]
[ 0. 1.]
[ 0.5 1. ]
[ 1. 1.]
Note that data in ``underworld`` is always published as arrays of shape
``(num_items, count)``, where ``num_items`` is the number of items in
the dataset (in this case, 3x3 nodes, so ``9``), and ``count`` is the
size of each data item (so ``1`` for scalar data, ``2`` or ``3`` for
vectors in 2- and 3-dimensions respectively, etc).
In parallel, each process will be assigned responsibility for a
subsection of the domain, and as such the length (``num_items``) of the
arrays will generally be different across processes. As such, while it
might be tempting to use ``numpy.reshape`` to reshape your arrays to
reflect the cartesian mesh topology, this approach will **not** work in
parallel and is therefore not usually recommended. You should instead
traverse data arrays as 1-d lists (each item of size ``count``).
Following this pattern will usually result in models which can safely be
run in serial or parallel.
Element Types
-------------
Underworld supports two primary element types: a linear element type
(``Q1``) and a quadratic element type (``Q2``). Interpolation using
these element types will provide continuous results across element
boundaries, though note that interpolant derivatives will be
discontinous across boundaries. Underworld also supports *mixed* element
types, where a secondary element type may also be specified. This
formulation is critical for stable solutions to Stokes type problems,
with velocity fields being represented by the primary element type, and
pressure fields by the secondary type. Secondary element types are not
continuous across element boundaries. Supported secondary types are
constant (``dQ0``), linear (``dPc1``) and bilinear (``dQ1``). Refer to
http://femtable.org for further details on element types.
Mesh element types are specified as a textual argument to the
``elementType`` constructor parameter. A primary type must always be
provided (``Q1`` or ``Q2``), and where a secondary type is required, the
primary/secondary types are provided as a pair (``Q1/dQ0``, ``Q2/dPc1``
or ``Q2/dQ1``). For mixed type elements, two mesh objects are generated
by the constructor, one for the primary and one for the secondary
elements. The primary mesh will be the object returned directly by the
constructor, with the secondary mesh available via the primary mesh’s
``subMesh`` attribute.
Note that where constructor parameters are omitted, default values are
used. Defaults values are baked into the ``underworld`` API and may be
determined in Jupyter by pressing ``shift``-``tab`` with the cursor
inside the constructor parenthesis, or by using the Python ``help()``
function (with the class/function of interest as the argument). Default
values are useful for quickly mocking up models, but may change without
notice so should not be relied upon.
Element boundaries (and therefore domain geometry and resolution) is
entirely determined by the primary mesh, with the location of the
secondary mesh nodes slaved to the primary mesh nodes. Secondary mesh
nodes are visualised below.
.. raw:: html
# Create a mesh with submesh
import underworld as uw
import underworld.visualisation as vis
mesh = uw.mesh.FeMesh_Cartesian( elementType="Q2/dpc1", maxCoord=(2.0, 1.0) )
submesh = mesh.subMesh
figMesh = vis.Figure(figsize=(800,400))
figMesh.append( vis.objects.Mesh(mesh) )
figMesh.append( vis.objects.Mesh(submesh, nodeNumbers=True, pointsize=10) )
figMesh.show()
.. raw:: html
# A trivial deformation
import underworld as uw
import underworld.visualisation as vis
mesh = uw.mesh.FeMesh_Cartesian(elementRes=(2, 2), maxCoord=(2.0, 1.0) )
with mesh.deform_mesh():
mesh.data[4][0] += 0.05
figMesh = vis.Figure(figsize=(800,400))
figMesh.append( vis.objects.Mesh(mesh, nodeNumbers=True, pointsize=10) )
figMesh.show()
.. raw:: html
# A more useful deformation
import underworld as uw
import underworld.visualisation as vis
mesh = uw.mesh.FeMesh_Cartesian(elementRes=(32,32), maxCoord=(2.0, 1.0))
with mesh.deform_mesh():
for index, coord in enumerate(mesh.data):
mesh.data[index][1] = mesh.data[index][1]**0.5
figMesh = vis.Figure(figsize=(800,400))
figMesh.append(vis.objects.Mesh(mesh))
figMesh.show()
.. raw:: html
# In this example we'll walk through some of the mesh loading mechanisms
import underworld as uw
mesh = uw.mesh.FeMesh_Cartesian()
# trivially deform
with mesh.deform_mesh():
mesh.data[3]+=0.1
# save
mesh.save('deformedMesh.h5')
# create second mesh
mesh2 = uw.mesh.FeMesh_Cartesian()
# confirm that it is different to first
import numpy as np
result = np.allclose(mesh.data,mesh2.data)
if result==True: # raise error if result not as expected
raise RuntimeError("Mesh objects should not match. Something wrong has happenend")
# now load data onto second mesh, and re-test
mesh2.load('deformedMesh.h5')
result = np.allclose(mesh.data,mesh2.data)
if result==False: # raise error if result not as expected
raise RuntimeError("Mesh objects should match. Something wrong has happenend")
# let's remove the saved file as it is no longer required.
if uw.mpi.rank==0:
import os;
os.remove('deformedMesh.h5')
Special sets
------------
Special sets are sets of nodes which are in some way special to a given
mesh object. For instance, for the cartesian mesh you will often wish to
tie special values to the walls to form your problem boundary
conditions. The list of special sets is provided via the ``specialSets``
list object. The sets are named with respect to the ``(I,J,K)``
cartesian indexing of the mesh, with the ``I`` index usually used for
the horizontal (left/right), ``J`` for the vertical, and ``K`` for
horizontal (front/back). Note that this is just a matter of convention,
and you are free to chose how the cartesian coordinates map within your
model. So, for example, ``MinI_VertexSet`` specifies the set of nodes
belonings to what we would normally consider to be the left wall, and
similarly ``MaxI_VertexSet`` the right wall. We also provide the alias
(``Left_VertexSet``, ``Right_VertexSet``, etc) for the most common use
case.
.. raw:: html
import underworld as uw
mesh = uw.mesh.FeMesh_Cartesian(elementRes=(2,2))
print("Available special sets for this mesh are:")
print(mesh.specialSets.keys())
leftset = mesh.specialSets['MinI_VertexSet']
print("\nIndices in the left set wall set:")
print(leftset)
.. parsed-literal::
Available special sets for this mesh are:
['MaxI_VertexSet', 'Top_VertexSet', 'Left_VertexSet', 'MinI_VertexSet', 'AllWalls_VertexSet', 'Bottom_VertexSet', 'Right_VertexSet', 'MinJ_VertexSet', 'MaxJ_VertexSet', 'Empty']
Indices in the left set wall set:
FeMesh_IndexSet([0, 3, 6])
You can confirm that these are indeed the left nodes with reference to
the mesh visualisation at the top of the page. Note that the special set
indices always specify parallel **local** indices. In almost all
instances, you will interact with only local data and identifiers. This
pattern is critical to successful parallel operation, and is usually a
natural way of constructing your models. You should also note that the
above visualisation shows the global node numbering, though for these
serial demonstrations the values are identical. Your checkpointed mesh
(and mesh variable) data will however be ordered according to its global
numbering. If for some reason you need to determine the global
identifiers, they are stored in the ``data_elgId`` array.
Mesh Variables
--------------
Mesh variables are used to encode spatial data across the mesh.
Underworld mesh variables assign data for each node of the mesh, so for
example you might assign a temperature value at each mesh node. Mesh
variables also leverage the mesh element shape functions to form
interpolations within elements, allowing our discrete datasets to have
continuous analogues formed.
Mesh variables may be used for any number of purposes when constructing
models, but most importantly they form the unknowns for the finite
element numerical systems you will be solving.
We create a mesh variable by using the mesh’s ``add_variable()`` method,
and specifying the ``nodeDofCount`` (node degree of freedom count). So
for example, setting ``nodeDofCount=1`` yields a scalar mesh variable,
having a single data value recorded at each node of the mesh, as you may
require for perhaps a temperature field.
Setting values on the MeshVariable
----------------------------------
As you will find with most Underworld data carrying objects, you will
access the data directly via the ``data`` attribute. Our data is stored
there as a numpy array, and you may therefore accessed or modified it
using standard Numpy operations.
Let’s initialise a variable with a function based on its spatial
coordinates
.. math::
T = 100\exp(1-z)
This variable may be used to represent a temperature field. We will walk
over the mesh vertex data to access coordinate information. As the
temperature field array is (by design) a 1-1 map to the vertex array, we
know the index of the mesh vertex will be the index of the associated
temperature datum. Note that the Python *built-in* ``enumerate()`` acts
to return **both** the index of a piece of data in a list (or array),
and the data itself.
.. raw:: html
# create a mesh & meshvariable, and then initialise with data.
import underworld as uw
import underworld.visualisation as vis
import math
mesh = uw.mesh.FeMesh_Cartesian( maxCoord=(2., 1.) )
# now create our mesh variable
temperatureField = mesh.add_variable( nodeDofCount=1 )
pi = math.pi
for index, coord in enumerate(mesh.data):
temperatureField.data[index] = 100.*math.exp(1.-coord[1])
# vis results
fig = vis.Figure(figsize=(800,400))
fig.append( vis.objects.Surface(mesh, temperatureField) )
fig.append( vis.objects.Mesh(mesh) )
fig.show()
.. raw:: html
# vector fields
import underworld as uw
import underworld.visualisation as vis
import math
mesh = uw.mesh.FeMesh_Cartesian( maxCoord=(2., 1.) )
velocityField = mesh.add_variable( nodeDofCount=2 )
coordmid = (1., 0.5)
for index, coord in enumerate(mesh.data):
vx = coordmid[1] - coord[1]
vz = coord[0] - coordmid[0]
velocityField.data[index] = (vx, vz)
# visualise both the velocity and temperature
fig = vis.Figure(figsize=(800,400))
fig.append( vis.objects.VectorArrows(mesh, velocityField, scaling=0.2, arrowHead=0.2) )
fig.show()
.. raw:: html
# mesh variable loading & saving
import underworld as uw
import numpy as np
mesh = uw.mesh.FeMesh_Cartesian()
meshvariable = mesh.add_variable(1)
# this might be a pattern you use in your models.
# set `restart` to required value, but note that
# you *must* first run with `True` to ensure that
# a file has been created!
restart=False
restartFile = "meshvariable.h5"
if not restart:
# not restarting, so init as required.
meshvariable.data[:] = 1.234
else:
meshvariable.load(restartFile)
# confirm that data is correct at this point!
result = np.allclose(meshvariable.data,1.234)
if result==False: # raise error if result not as expected
raise RuntimeError("Mesh variable does not contain expected values.")
# now save. generally this will occur after
# you've done something interesting, but we
# don't have time for that in this example.
ignore = meshvariable.save(restartFile)
# let's remove the saved file as it is no longer required.
if uw.mpi.rank==0:
import os;
os.remove(restartFile)