Mesh basics#

In order to solve a model numerically in a region, we have to discretise it. There are two main ways of discretising the space: finite-difference and finite-element discretisation. discretisedfield deals only with finite-difference discretisation at the moment. This means that we are dividing our cubic region into smaller “chunks” - small cubes. We refer to the discretised region as a mesh:

\[\text{MESH} = \text{REGION} + \text{DISCRETISATION}\]

In this tutorial, we show how to define it, as well as some basic operations we can perform with meshes.

As we showed in previous tutorials, region is always cuboidal and it is defined by any two diagonally opposite corner points. We are going to use the same region as before, defined by the following two diagonally opposite points

\[\mathbf{p}_{1} = (0, 0, 0)\]
\[\mathbf{p}_{2} = (l_{x}, l_{y}, l_{z})\]

with \(l_{x} = 100 \,\text{nm}\), \(l_{y} = 50 \,\text{nm}\), and \(l_{z} = 20 \,\text{nm}\).

So, let us start by defining the region:

[1]:
import discretisedfield as df

p1 = (0, 0, 0)
p2 = (100e-9, 50e-9, 20e-9)

region = df.Region(p1=p1, p2=p2)

The region is now defined. Another missing piece is the discretisation and we need to decide how we are going to discretise the region. In other words, we need to decide into what size “chunks” we are going to discretise our region in. We refer to the “chunk” as the discretisation cell. In discretisedfield, there are two ways how we can define the discretisation. We can define either:

  1. The number of discretisation cells in all geometric directions, or

  2. The size of a single discretisation cell.

Let us start with the first case. The number of discretisation cells in all geometric directions can be passed using n argument, which is, for a three-dimensional geometry, a length-3 tuple:

\[n = (n_{x}, n_{y}, n_{z})\]

For instance, we want to discretise our region in 5 cells in the x-direction, 2 in the y-direction and 1 cell in the z-direction. Therefore, knowing the region as well as the discretisation n, we pass them both to Mesh:

[2]:
n = (5, 2, 1)

mesh = df.Mesh(region=region, n=n)

The mesh is defined. Based on the region dimensions and the number of discretisation cells, we can ask the mesh to give us the size of a single discretisation cell:

[3]:
mesh.cell
[3]:
array([2.0e-08, 2.5e-08, 2.0e-08])

Knowing this value, we could have defined the mesh passing this value using cell argument, and we would have got exactly the same mesh.

[4]:
cell = (20e-9, 25e-9, 20e-9)

mesh = df.Mesh(region=region, cell=cell)

If we now ask our new mesh about the number of discretisation cells:

[5]:
mesh.n
[5]:
array([5, 2, 1])

There is no difference whatsoever how we are going to define the mesh. However, defining the mesh with cell can result in an error, if the region cannot be divided into chunks of that size. For instance:

[6]:
try:
    mesh = df.Mesh(region=region, cell=(3e-9, 3e-9, 3e-9))
except ValueError:
    print("Exception raised.")
Exception raised.

Let us now have a look at some basic properties we can ask the mesh object for. First of all, region object is a part of the mesh object:

[7]:
mesh.region
[7]:
Region
  • pmin = [0.0, 0.0, 0.0]
  • pmax = [1e-07, 5e-08, 2e-08]
  • dims = ['x', 'y', 'z']
  • units = ['m', 'm', 'm']

Therefore, we can perform all the operations on the region we saw previously, but now through the mesh object (mesh.region). For instance:

[8]:
mesh.region.pmin  # minimum point
[8]:
array([0., 0., 0.])
[9]:
mesh.region.edges  # edge lenghts
[9]:
array([1.e-07, 5.e-08, 2.e-08])
[10]:
mesh.region.centre  # centre point
[10]:
array([5.0e-08, 2.5e-08, 1.0e-08])

By asking the mesh object directly, we can get the number of discretisation cells in all three directions \(n = (n_{x}, n_{y}, n_{z})\):

[11]:
mesh.n
[11]:
array([5, 2, 1])

and the size of a single discretisation cell:

[12]:
mesh.cell
[12]:
array([2.0e-08, 2.5e-08, 2.0e-08])

The total number of discretisation cells is:

[13]:
len(mesh)
[13]:
10

This number is simply \(n_{x}n_{y}n_{z}\). We can conclude that the entire region is now divided into 10 small cubes (discretisation cells). Each cell in the mesh has its index and its coordinate. We can get indices of all discretisation cells:

[14]:
# NBVAL_IGNORE_OUTPUT
mesh.indices
[14]:
<generator object Mesh.indices at 0x7f466521ee30>

This gives us a generator object which we can use as an iterable in different Pyhton contexts. For instance, we can give it to the list.

[15]:
list(mesh.indices)
[15]:
[(0, 0, 0),
 (1, 0, 0),
 (2, 0, 0),
 (3, 0, 0),
 (4, 0, 0),
 (0, 1, 0),
 (1, 1, 0),
 (2, 1, 0),
 (3, 1, 0),
 (4, 1, 0)]

List function now “unpacks” the generator and gives us a list of tuples. Each tuple has three unsigned integers. For instance, we can interpret index (2, 1, 0) as an index which belongs to the third cell in the x-direction, second in the y, and the first in the z direction. Please note that indexing in Python starts from 0. Therefore, we say that the “fifth element” has index 4.

Another thing we can associate to every discretisation cell is its coordinate. The coordinate of the cell is the coordinate of its centre point. So, the coordinate of index (2, 1, 0) cell is:

[16]:
index = (2, 1, 0)

mesh.index2point(index)
[16]:
array([5.00e-08, 3.75e-08, 1.00e-08])

It is very often the case we need to iterate through all discretisation cells and use their coordinates. For that, we can use the mesh object itself, which is also an iterable:

[17]:
list(mesh)
[17]:
[array([1.00e-08, 1.25e-08, 1.00e-08]),
 array([3.00e-08, 1.25e-08, 1.00e-08]),
 array([5.00e-08, 1.25e-08, 1.00e-08]),
 array([7.00e-08, 1.25e-08, 1.00e-08]),
 array([9.00e-08, 1.25e-08, 1.00e-08]),
 array([1.00e-08, 3.75e-08, 1.00e-08]),
 array([3.00e-08, 3.75e-08, 1.00e-08]),
 array([5.00e-08, 3.75e-08, 1.00e-08]),
 array([7.00e-08, 3.75e-08, 1.00e-08]),
 array([9.00e-08, 3.75e-08, 1.00e-08])]

Since mesh object is an iterator itself, we can use it, for example, in for loops:

[18]:
for point in mesh:
    print(point)
[1.00e-08 1.25e-08 1.00e-08]
[3.00e-08 1.25e-08 1.00e-08]
[5.00e-08 1.25e-08 1.00e-08]
[7.00e-08 1.25e-08 1.00e-08]
[9.00e-08 1.25e-08 1.00e-08]
[1.00e-08 3.75e-08 1.00e-08]
[3.00e-08 3.75e-08 1.00e-08]
[5.00e-08 3.75e-08 1.00e-08]
[7.00e-08 3.75e-08 1.00e-08]
[9.00e-08 3.75e-08 1.00e-08]

A function, which is opposite to index2point, is point2index. This function takes any point in the region and returns the index of a cell it belongs to:

[19]:
point = (41.6e-9, 35.2e-9, 4.71e-9)

mesh.point2index(point)
[19]:
(2, 1, 0)

We can also ask the mesh to give us the midpoints along a certain axis:

[20]:
mesh.cells.x
[20]:
array([1.e-08, 3.e-08, 5.e-08, 7.e-08, 9.e-08])
[21]:
mesh.cells.y
[21]:
array([1.25e-08, 3.75e-08])

Similarly, we can get the vertices of the cells along a certain axis:

[22]:
mesh.vertices.x
[22]:
array([0.e+00, 2.e-08, 4.e-08, 6.e-08, 8.e-08, 1.e-07])
[23]:
mesh.vertices.y
[23]:
array([0.0e+00, 2.5e-08, 5.0e-08])

We can compare meshes using == and != relational operators. Let us define two meshes and compare them to the one we have already defined:

[24]:
mesh_same = df.Mesh(region=region, n=(5, 2, 1))
mesh_different = df.Mesh(region=region, n=(10, 5, 7))

mesh == mesh_same
[24]:
True
[25]:
mesh == mesh_different
[25]:
False
[26]:
mesh != mesh_different
[26]:
True

Finally, mesh has its representation string:

[27]:
mesh
[27]:
Mesh
  • Region
    • pmin = [0.0, 0.0, 0.0]
    • pmax = [1e-07, 5e-08, 2e-08]
    • dims = ['x', 'y', 'z']
    • units = ['m', 'm', 'm']
  • n = [5, 2, 1]

In the representation string, we see pmin, pmax, and n we discussed earlier, but there are also bc and subregions we did not and we will look at some more advanced mesh properties in the next tutorials.

Scale and translate#

Similarly to regions, a mesh has two methods scale and translate. Both optionally allow inplace operations.

[28]:
scaled = mesh.scale(2)
scaled
[28]:
Mesh
  • Region
    • pmin = [-5e-08, -2.5e-08, -1e-08]
    • pmax = [1.5e-07, 7.5e-08, 3.0000000000000004e-08]
    • dims = ['x', 'y', 'z']
    • units = ['m', 'm', 'm']
  • n = [5, 2, 1]
[29]:
translated = mesh.translate((-10, 0, 100))
translated
[29]:
Mesh
  • Region
    • pmin = [-10.0, 0.0, 100.0]
    • pmax = [-9.9999999, 5e-08, 100.00000002]
    • dims = ['x', 'y', 'z']
    • units = ['m', 'm', 'm']
  • n = [5, 2, 1]

We can scale with different factors along different directions and pass in a reference_point that does not move when scaling. If not passed the center of the mesh is kept fixed.

[30]:
scaled = mesh.scale((1, 0.75, 2), reference_point=(-10, -10, 0))
scaled
[30]:
Mesh
  • Region
    • pmin = [0.0, -2.5, 0.0]
    • pmax = [1e-07, -2.4999999625, 4e-08]
    • dims = ['x', 'y', 'z']
    • units = ['m', 'm', 'm']
  • n = [5, 2, 1]

It is also possible to scale the mesh inplace.

[31]:
mesh.scale((1, 0.75, 2), inplace=True)
mesh
[31]:
Mesh
  • Region
    • pmin = [0.0, 6.25e-09, -1e-08]
    • pmax = [1e-07, 4.375e-08, 3.0000000000000004e-08]
    • dims = ['x', 'y', 'z']
    • units = ['m', 'm', 'm']
  • n = [5, 2, 1]

Mesh alignment#

We can also check if two meshes are aligned using the is_aligned function.

Two meshes are considered to be aligned if and only if:

1. They have same discretisation cell size.

2. They have common cell coordinates.

for a given tolerance value.

Let’s create two meshes which have two different regions which overlap but the cells centers are located in the same places.

These two meshes are aligned.

[32]:
p1 = (-50, -25, 0)
p2 = (50, 25, 5)
cell = (5, 5, 5)
region1 = df.Region(p1=p1, p2=p2)
mesh1 = df.Mesh(region=region1, cell=cell)

p1 = (-45, -20, 0)
p2 = (10, 20, 5)
cell = (5, 5, 5)
region2 = df.Region(p1=p1, p2=p2)
mesh2 = df.Mesh(region=region2, cell=cell)

mesh1.is_aligned(mesh2)
[32]:
True

If we create a third mesh where the cell centers are not in the same places then this mesh is not aligned.

[33]:
p1 = (-42, -20, 0)
p2 = (13, 20, 5)
cell = (5, 5, 5)
region3 = df.Region(p1=p1, p2=p2)
mesh3 = df.Mesh(region=region3, cell=cell)

mesh1.is_aligned(mesh3)
[33]:
False

However, we can provide the tolerance as an argument so dictate how strict the is_aligned check is.

[34]:
mesh1.is_aligned(mesh3, tolerance=2)
[34]:
True

Other dimensions#

Similar to regions, a mesh can have any dimension >= 1. Plotting is currently only supported for 3d regions.

For example, a 1d mesh

[35]:
region = df.Region(p1=0, p2=10)
mesh = df.Mesh(region=region, n=10)
mesh
[35]:
Mesh
  • Region
    • pmin = [0]
    • pmax = [10]
    • dims = ['x']
    • units = ['m']
  • n = [10]

Or a 4d mesh

[36]:
region = df.Region(p1=(0, 0, 0, 0), p2=(10, 20, 30, 40))
mesh = df.Mesh(region=region, n=(2, 3, 4, 5))
mesh
[36]:
Mesh
  • Region
    • pmin = [0, 0, 0, 0]
    • pmax = [10, 20, 30, 40]
    • dims = ['x0', 'x1', 'x2', 'x3']
    • units = ['m', 'm', 'm', 'm']
  • n = [2, 3, 4, 5]