Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fields

import numpy as np
import pyvista as pv

import mefikit as mf

pv.set_plot_theme("dark")
pv.set_jupyter_backend("static")

Field expressions

FieldExpr are composition of floats and mf.sel.field(“fieldname”) or custom fields. Available operations on fields include :

  • binary expresions + - / *
  • unary expr sin(), cos(), abs(), ln(), log10(), exp()
  • primitives :
    • M: the measure, ie length/area/volume of an element
    • C: the node centroid of an element, not the volume barycenter
    • X: the X compo of the node centroid
    • Y: the Y compo of the node centroid
    • Z: the Z compo of the node centroid

Scalar binary operations

toto = mf.Field("toto")
tata = mf.Field("tata")
toto + tata
toto * tata
toto - tata
toto / tata
toto + 2.0
toto - 2.0
toto * 2.0

Scalar unary ops

toto.sin() toto.cos() toto.abs() toto.exp() toto.ln() toto.square() toto.sqrt() toto.tan() toto.log10();

Vector ops

toto.dot(tata)
toto @ tata  # same as dot
toto[0]

Primitives

m = mf.M  # measure: length/area/volume
c = mf.C  # node centroids
x = mf.X  # x compo of node centroid
y = mf.Y  # y compo of node centroid
z = mf.Z  # z compo of node centroid
n = mf.Normal  # normal to n-1 dim elements, ie 2d elems in 3d or 1d elems in 2d
nx = mf.Nx  # x compo of element normal
ny = mf.Ny  # y compo of element normal
nz = mf.Nz  # z compo of element normal

How does it work ?

The operations build a binary operation tree structure. Mefikit knows how to interpret this binary tree to compute fields.

print(toto * mf.M + 3.0 * mf.X)
BinaryExpr {
    operator: Add,
    left: BinaryExpr {
        operator: Mul,
        left: Field(
            "toto",
        ),
        right: Measure,
    },
    right: BinaryExpr {
        operator: Mul,
        left: Array(
            3.0, shape=[], strides=[], layout=CFcf (0xf), dynamic ndim=0,
        ),
        right: X,
    },
}

This is quite handfull because it enables two patterns:

  • reusability and composition of filters
  • evaluation optimizations of selections : some selection filters are evaluated in parallel, some are evaluated first if they are discriminant

Mesh fields mapping

x = np.logspace(-5, 0.0, 50)
z = np.linspace(0.0, 0.1, 3)
mesh2 = mf.build_cmesh(x, x, z)
mesh2.to_pyvista().plot(show_edges=True)

Fields attribute is dictionnary like: fields can be accessed, modified, added, defined through it using field expressions evaluation on the mesh.

Fields expressions are independent from the mesh and light, fields are evaluated field expressions stored alongside the mesh.

mesh2.fields["Measure"] = mf.M
mesh2.to_pyvista().plot()

mesh2.fields["toto"] = mf.X + mf.Y
pvm = mesh2.to_pyvista()
pvm.active_scalars_name = "toto"
pvm.plot()

# List and look up fields by name.
print(mesh2.fields.keys())
print(mesh2.fields.values())
['Measure', 'toto']
[FieldRef("Measure"), FieldRef("toto")]
for n, f in mesh2.fields.items():
    print(n, ":", f)
Measure : FieldRef("Measure")
toto : FieldRef("toto")
del mesh2.fields["toto"]  # remove it
print(mesh2.fields)
FieldsMapping(["Measure"])

Field references

Fields live in a dict-like mapping on the mesh, keyed by name. Each entry is a handle (FieldRef) to read values, reduce them, or write through selectors. Fields reference are always bound to a given mesh.

mes = mesh2.fields["Measure"]
print("shape:", mes.shape, "| elements:", len(mes))
shape: (1,) | elements: 4802

Whole reductions

Field references support reductions evaluated eagerly :

# Whole-domain reductions over every element carrying the field.
print(mes.min(), mes.max(), mes.mean())
3.507414294773286e-13 0.002192327517292864 2.0824239902124112e-05

Numpy input/ouput

# Bulk export as {etype: array} (or a single array via `.numpy()` when the
# mesh has one element type).
vals = mes.values()
print(vals.keys())
shortcut_vals = mes.numpy()
print(shortcut_vals.shape)
assert np.allclose(vals["HEX8"], shortcut_vals)
dict_keys(['HEX8'])
(4802,)
# Bulk import a dict[str, ndarray] as new field
# Field size checks are done so that the field lay on all elements of the same dim.
mesh2.fields["toto"] = {"HEX8": shortcut_vals * 3.0}

Regional filtered fields

Lazy selections (see selection notebook) allow to compute regional reduction with any field expression, including plain existing field names as strings.

rect = mf.sel.bbox([0.25, 0.25, 0.0], [0.7, 0.7, 0.1])
zone = mesh2.select(rect)
print(zone.mean("toto"), zone.max(mf.M * 4))
0.0013598121884658915 0.003426116772966117

Writes accept scalars, arrays, field expressions or existing field names, targeted by wildcards (...) or selectors.

mesh2.fields["Scratch"] = 0.0  # create by broadcast
mesh2.fields["Scratch"][...] = (
    "Measure"  # whole selection, copy an existing field inplace
)

A field can be overwritten on a specific region. The overwrite is done using an expression formulae which can even reference the previous field values.

sel = mf.sel.bbox([0.0, 0.0, 0.0], [0.3, 1.0, 0.1])
mesh2.fields["Scratch"][sel] = mf.Field("Scratch") * 2  # scaled sub-region
sel2 = mf.sel.sphere(center=[0.5, 0.5, 0.05], r=0.3)
m = mesh2.select(sel2).mean("Scratch")  # compute the mean of scratch in a region
mesh2.fields["Scratch"][sel2] = m  # assign this constant value to the whole region
pvm = mesh2.to_pyvista()
pvm.active_scalars_name = "Scratch"
pvm.plot()

Direct field expression evaluation to numpy

It is not really recommended not to use the .fields storing mecanism as it provides complete integration with mefikit, but it is nevertheless possible to evaluate an expression on a field and export it directly as a numpy array. The eval method does exaclty this.

m = mf.Field("Measure")
m2 = mf.Field("4 * M2")
mesh2.fields["4 * M2"] = 4.0 * m * m
mesh2.eval(m2 - 4.0 * mf.M.square())
{'HEX8': array([0., 0., 0., ..., 0., 0., 0.], shape=(4802,))}

Field to Selection

Field expressions can be converted to threshold selections expressions. The available comparisons are <, <=, >, >=, == :

maxM = mesh2.select(mf.sel.all()).max(mf.M)
minM = mesh2.select(mf.sel.all()).min(mf.M)
meanM = mesh2.select(mf.sel.all()).mean(mf.M)
lb = (minM + meanM) / 2.0
hb = (maxM + meanM) / 2.0
m = mf.Field("Measure")

th = (m > lb) & (m <= hb)
m2sel = mesh2.select(th).to_mesh()
pvm2: pv.UnstructuredGrid = m2sel.to_pyvista()
pvm2.active_scalars_name = "Measure"
pvm2.plot()

Those threshold selections can be combined with other selections.

r = mf.sel.bbox([0.5, 0.5, 0.0], [0.8, 0.8, 0.1])
c = mf.sel.sphere([0.12, 0.12, 0.05], 0.05)
mesh2.select(th - r - c).to_mesh().to_pyvista().plot()

Vector/Matrix/Tensor fields

Normals of hyperplane dim elements

Let first add the elements of the boundaries to the current mesh :

mesh2.boundaries_update()

Now normals can be computed on those elements.

mesh2.fields["N"] = mf.Normal
mesh2.fields["N"].values()
{'QUAD4': array([[ 0.,  0., -1.],
        [ 0.,  0., -1.],
        [ 0.,  0.,  1.],
        ...,
        [ 0.,  0.,  1.],
        [ 0.,  0.,  1.],
        [ 0.,  0.,  1.]], shape=(5194, 3))}

Dot product / matrix multiplication is available through a numpy like syntax with the .dot operator or the @ operator :

ev = mesh2.eval(mf.Normal @ np.array([0.0, 1.0, 0.0]))["QUAD4"]
(ev > 0.5).sum()  # number of faces oriented towards y
np.int64(98)
mesh2.groups["top"] = mf.Nz > 0.9
top = mesh2.select(mf.sel.group("top")).to_mesh()
pt = pv.Plotter()
pt.add_mesh(mesh2.descend(target_dim=1).to_pyvista())
pt.add_mesh(top.to_pyvista(), show_edges=True)
pt.show()