Geometric transforms
import numpy as np
import pyvista as pv
import mefikit as mf
pv.set_plot_theme("dark")
pv.set_jupyter_backend("static")
coords = np.array(
[
[0.0, 0.0],
[1.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
]
)
mesh = mf.UMesh(coords)
mesh.add_regular_block("QUAD4", np.array([[0, 1, 3, 2]], dtype=np.uint))
All angles are given in radians. A Transform is an affine transformation
represented by a 4x4 homogeneous matrix (last row 0 0 0 1). UMesh objects
are transformed out-of-place: each transform returns a new mesh and leaves the
source unchanged.
Out-of-place transforms
translated = mesh.translate([10.0, 0.0])
translated.to_pyvista().plot()
rotated = mesh.rotate([0.0, 0.0, 1.0], np.pi / 6)
rotated.to_pyvista().plot()
mirrored = mesh.rotate([0.0, 0.0, 1.0], np.pi / 6).mirror([1.0, 0.0, 0.0])
mirrored.to_pyvista().plot()
scaled = mesh.scale([2.0, 3.0])
scaled.to_pyvista().plot()
uniform = mesh.scale_uniform(4.0)
uniform.to_pyvista().plot()
The input mesh is never modified.
Unidirectional (left-to-right) composition
Transform supports composition in both conventions:
a @ b(matrix product) appliesbfirst;a.then(b)appliesa, thenb.
tr = mf.Transform.translation([1.0, 0.0]).then(
mf.Transform.rotation([0.0, 0.0, 1.0], np.pi / 2)
)
out = mesh.transform(tr)
assert np.allclose(out.coords()[1], [0.0, 2.0], atol=1e-9)
matrix = mf.Transform.translation([1.0, 2.0, 3.0]) @ mf.Transform.scaling([2.0])
assert np.allclose(
matrix.matrix(),
mf.Transform.translation([1.0, 2.0, 3.0]).matrix()
@ mf.Transform.scaling([2.0]).matrix(),
)
Rather than a Transform, mesh.transform(...) also accepts a raw 4x4
numpy array, and Transform.from_matrix(...) wraps one.
mat = np.eye(4)
mat[0, 3] = 7.0
assert np.isclose(mesh.transform(mat).coords()[0, 0], 7.0)
Dimensional consistency is enforced: trying to move a 2D (or 1D) mesh out of
its plane (or line) raises a ValueError.
try:
mesh.translate([0.0, 0.0, 0.5])
except ValueError as err:
print(err)
This transform moves the mesh out of its plane/line: rows beyond the space dimension must leave the extra coordinates unchanged.
Duplicating a mesh
duplicate(step, n) returns n copies, each transformed by the powers
step, step @ step, … of step.
ts = mf.Transform.translation([0.0, 3.0, 0.0])
rt = mf.Transform.rotation([0.0, 0.0, 1.0], np.pi / 4)
column = mesh.duplicate(ts @ rt, 3)
assert column.coords().shape[0] == 12
column.to_pyvista().plot(show_edges=True)
Arbitrary arrangements can be built with the module-level aggregate /
concat functions, which concatenate meshes while preserving blocks, fields,
families and groups (element ids are relabelled so that the resulting mesh
stays valid).
line = mf.concat(mesh, mesh.translate([5.0, 0.0]))
three = mf.aggregate([mesh, mesh.translate([5.0, 0.0]), mesh.translate([15.0, 0.0])])
assert line.coords().shape[0] == 8
assert three.coords().shape[0] == 12
line.to_pyvista().plot()
three.to_pyvista().plot()
Transforms preserve the mesh metadata: blocks, fields, families and groups survive untouched.
Reconstructing coordinates
For non-affine coordinate changes, use UMesh.from_mesh. It accepts a
same-shaped coordinate array and preserves the selected source connectivity,
fields, families and groups. Omitting coords reuses the source coordinates.
coords = mesh.coords()
coords[:, 0] *= 2.0
coords[:, 1] *= coords[:, 0] + 1.0
warped = mf.UMesh.from_mesh(
mesh,
coords,
)
# assert np.allclose(warped.coords()[:, 0], mesh.coords()[:, 0] ** 2)
assert np.allclose(warped.coords(), coords)
warped.to_pyvista().plot()
from_mesh can select source element blocks by topological dimension or by
element type. The two selectors are mutually exclusive; the source mesh is
never changed.
surfaces = mf.UMesh.from_mesh(mesh, dim=2)
quads = mf.UMesh.from_mesh(mesh, element_types=["QUAD4"])
Coordinate arrays are structurally validated when a mesh is reconstructed; geometric quality checks are not performed.