This page was generated from docs\source\notebooks/mesh_structure.ipynb.

The structure of a mesh#

To explain the structure of a mesh, we create a helper function:

[1]:
from sigmaepsilon.mesh import PointData, PolyData
from sigmaepsilon.mesh.cells import T3, Q4, L2
import numpy as np


def generate_mesh(to_standard_form:bool=False):

    coords_T3 = np.array([
        [0, 0, 0],
        [1, 0, 0],
        [1, 1, 0],
        [0, 1, 0],
    ], dtype=float)

    topology_T3 = np.array([
        [0, 1, 2],
        [0, 2, 3],
    ], dtype=int)

    coords_Q4 = np.array([
        [2, 0, 0],
        [3, 0, 0],
        [3, 1, 0],
        [2, 1, 0],
    ], dtype=float)

    topology_Q4 = np.array([
        [0, 1, 2, 3],
    ], dtype=int)

    coords_L2 = np.array([
        [1, 0, 0],
        [2, 1, 0],
        [1, 1, 0],
        [2, 0, 0],
    ], dtype=float)

    topology_L2 = np.array([
        [0, 1],
        [2, 3]
    ], dtype=int)

    pd_T3 = PointData(coords=coords_T3)
    cd_T3 = T3(topo=topology_T3)

    pd_Q4 = PointData(coords=coords_Q4)
    cd_Q4 = Q4(topo=topology_Q4)

    pd_L2 = PointData(coords=coords_L2)
    cd_L2 = L2(topo=topology_L2)

    mesh = PolyData()
    mesh["2d", "triangles"] = PolyData(pd_T3, cd_T3)
    mesh["2d", "quads"] = PolyData(pd_Q4, cd_Q4)
    mesh["lines"] = PolyData(pd_L2, cd_L2)

    if to_standard_form:
        mesh.to_standard_form()

    return mesh


mesh = generate_mesh()

The database model#

A PolyData object is essentially a dictionary, equipped with one or two Awkward arrays to store data attached to the points and the cells. Instances walk and talk like a dictionary but the behaviour is extended to cover nested definitions.

[2]:
isinstance(mesh, dict)
[2]:
True
[3]:
list(mesh.keys())
[3]:
['2d', 'lines']
[4]:
list(mesh.values())
[4]:
[PolyData({'triangles': PolyData({}), 'quads': PolyData({})}), PolyData({})]

As you can see, the values call returns only two items, but the mesh has three blocks. To loop through the subdictionaries (called blocks) with cell data, you can use the cellblocks method of any PolyData instance. Every block has an address and a parent except the root object, that has no parent and address.

[5]:
for block in mesh.cellblocks(inclusive=True):
    print(block.address)
['2d', 'triangles']
['2d', 'quads']
['lines']

The parameter inclusive means to start parsing the structure of the mesh with the instance the call was made upon. In this case it makes no difference, as the root instance of the mesh has no attached cells.

[6]:
for block in mesh.pointblocks(inclusive=True):
    print(block.address)
['2d', 'triangles']
['2d', 'quads']
['lines']

Now bring the mesh into a standard form and repeat the above queries:

[7]:
mesh.to_standard_form()
[7]:
PolyData({'2d': PolyData({'triangles': PolyData({}), 'quads': PolyData({})}), 'lines': PolyData({})})
[8]:
for block in mesh.cellblocks(inclusive=True):
    print(block.address)
['2d', 'triangles']
['2d', 'quads']
['lines']
[9]:
for block in mesh.pointblocks(inclusive=True):
    print(block.address)
[]

An empty list is returned, since the root of the mesh does host a point cloud, but it doesn’t have an address. To see if a block has an attached point or cell related data,use the pointdata and celldata properties (you can also use mesh.pd and mesh.cd):

[10]:
mesh = generate_mesh()
[11]:
mesh["2d", "triangles"].pointdata
[11]:
[{_x: [0, 0, 0], _activity: True, _id: 0},
 {_x: [1, 0, 0], _activity: True, _id: 1},
 {_x: [1, 1, 0], _activity: True, _id: 2},
 {_x: [0, 1, 0], _activity: True, _id: 3}]
------------------------------------------
type: 4 * {
    _x: 3 * float64,
    _activity: bool,
    _id: int32
}
[12]:
type(mesh["2d", "triangles"].pointdata)
[12]:
sigmaepsilon.mesh.data.pointdata.PointData
[13]:
mesh["2d", "triangles"].celldata
[13]:
[{_nodes: [0, 1, 2], _id: 0},
 {_nodes: [0, 2, 3], _id: 1}]
-----------------------------
type: 2 * {
    _nodes: 3 * int32,
    _id: int32
}
[14]:
type(mesh["2d", "triangles"].celldata)
[14]:
sigmaepsilon.mesh.cells.t3.T3

PointData and CellData instances are wrapper objects that wrap Awkward arrays. The databases can be accessed using the db property:

[15]:
type(mesh["2d", "triangles"].pointdata.db)
[15]:
awkward.highlevel.Array
[16]:
type(mesh["2d", "triangles"].celldata.db)
[16]:
awkward.highlevel.Array

PointData and CellData instances are actually represented by their wraooed data objects:

[17]:
mesh["2d", "triangles"].celldata.db
[17]:
[{_nodes: [0, 1, 2], _id: 0},
 {_nodes: [0, 2, 3], _id: 1}]
-----------------------------
type: 2 * {
    _nodes: 3 * int32,
    _id: int32
}

In the representation we can see the fields of the database. The fields are also accessible using the fields property of the data object:

[18]:
mesh["2d", "triangles"].celldata.db.fields
[18]:
['_nodes', '_id']

Field names starting with an underscore are internal variables crucial for the object to work properly. Overriding these fields might break the behaviour of the mesh. Besides these reserved field names, you can attach arbitrary data to the databases:

[19]:
db = mesh["2d", "triangles"].celldata.db
number_of_cells = len(db)
db["random_data"] = np.random.rand(number_of_cells)
[20]:
mesh["2d", "triangles"].celldata.db
[20]:
[{_nodes: [0, 1, 2], _id: 0, random_data: 0.479},
 {_nodes: [0, 2, 3], _id: 1, random_data: 0.56}]
-------------------------------------------------
type: 2 * {
    _nodes: 3 * int32,
    _id: int32,
    random_data: float64
}

The newly attached data is now accessible as an Awkward array:

[21]:
mesh["2d", "triangles"].celldata.db.random_data
[21]:
[0.479,
 0.56]
-----------------
type: 2 * float64

or a NumPy array

[22]:
mesh["2d", "triangles"].celldata.db.random_data.to_numpy()
[22]:
array([0.47853953, 0.55976835])

The data is also available like the database was a dictionary:

[23]:
mesh["2d", "triangles"].celldata.db["random_data"]
[23]:
[0.479,
 0.56]
-----------------
type: 2 * float64

When bringing a mesh to a standard form, the Awkward library is smart enough to handle missing data. Let say we attach some random data to one of the point cloud of the mesh before briging it to standard form.

[24]:
db = mesh["2d", "triangles"].pointdata.db
number_of_points = len(db)
db["random_data"] = np.random.rand(number_of_points)
[25]:
mesh.to_standard_form()
[25]:
PolyData({'2d': PolyData({'triangles': PolyData({}), 'quads': PolyData({})}), 'lines': PolyData({})})
[26]:
mesh.pointdata.db
[26]:
[{random_data: 0.931, _activity: True, _id: 0, _x: [0, ..., 0]},
 {random_data: 0.981, _activity: True, _id: 1, _x: [1, ..., 0]},
 {random_data: 0.234, _activity: True, _id: 2, _x: [1, ..., 0]},
 {random_data: 0.589, _activity: True, _id: 3, _x: [0, ..., 0]},
 {random_data: nan, _activity: True, _id: 4, _x: [2, ..., 0]},
 {random_data: nan, _activity: True, _id: 5, _x: [3, ..., 0]},
 {random_data: nan, _activity: True, _id: 6, _x: [3, ..., 0]},
 {random_data: nan, _activity: True, _id: 7, _x: [2, ..., 0]},
 {random_data: nan, _activity: True, _id: 8, _x: [1, ..., 0]},
 {random_data: nan, _activity: True, _id: 9, _x: [2, ..., 0]},
 {random_data: nan, _activity: True, _id: 10, _x: [1, ..., 0]},
 {random_data: nan, _activity: True, _id: 11, _x: [2, ..., 0]}]
----------------------------------------------------------------
type: 12 * {
    random_data: float64,
    _activity: bool,
    _id: int32,
    _x: 3 * float64
}

You can turn PointData and CellData instances to other well known data formats (see the API reference for a full list of supported formats):

[27]:
mesh.pointdata.to_dataframe(fields=["random_data"])
[27]:
random_data
entry
0 0.930941
1 0.980779
2 0.234165
3 0.589487
4 NaN
5 NaN
6 NaN
7 NaN
8 NaN
9 NaN
10 NaN
11 NaN

Root, source and parent#

[28]:
mesh = generate_mesh()

The root is the top level PolyData instance in the mesh. The root of the root object is itself.

[29]:
id(mesh), id(mesh.root), id(mesh["2d", "triangles"].root)
[29]:
(2668746377872, 2668746377872, 2668746377872)

To tell if an instance is a root or not use the is_root method:

[30]:
mesh.is_root(), mesh["2d", "triangles"].is_root()
[30]:
(True, False)

Every block of cells in a mesh -except the root- has a parent, which is the containing PolyData instance. The parent of the root instance is None.

[31]:
id(mesh["2d"]), id(mesh["2d", "triangles"].parent)
[31]:
(2668746378832, 2668746378832)
[32]:
mesh.parent is None
[32]:
True
[33]:
mesh.is_root(), mesh["2d", "triangles"].is_root()
[33]:
(True, False)

Every block with attached cell data has a source, that hosts the pointcloud the indices of the topology of the cells of the block are referring to.

[34]:
id(mesh["2d", "triangles"]), id(mesh["2d", "triangles"].source())
[34]:
(2668746378352, 2668746378352)

To tell if a PolyData hosts point related data, you can use the is_source method of the instance (remember that the mesh is decentralized at the moment):

[35]:
mesh.is_source(), mesh["2d", "triangles"].is_source()
[35]:
(False, True)

After bringing the mesh to a standard form:

[36]:
mesh.to_standard_form()
[36]:
PolyData({'2d': PolyData({'triangles': PolyData({}), 'quads': PolyData({})}), 'lines': PolyData({})})
[37]:
mesh.is_source(), mesh["2d", "triangles"].is_source()
[37]:
(True, False)