hexmesh

class nekmeshpy.hexmesh.HexMesh[source]

Bases: object

An all-hexahedral volume mesh in shared-point form.

FACE_POINTS: NDArray[int64] = array([[0, 1, 5, 4],        [1, 2, 6, 5],        [2, 3, 7, 6],        [3, 0, 4, 7],        [0, 3, 2, 1],        [4, 5, 6, 7]])

Nek face -> the 4 corner point positions (0-based); row f is face f+1, wound so the right-hand normal points out of the hex. The array is core.conform._LOCAL_FACES, where that winding is justified, rather than a copy of it – and it is read-only, so the two cannot be edited apart.

__init__(quad_mesh, hexes, orient, interior=None, element_tags=None)[source]

Construct from the B-rep directly: quad_mesh (a QuadMesh holding every shared face – its points are the shared corners, its quads the shared face connectivity, its edges / interior the shared face-boundary / interior HO nodes), hexes (E,6) face indices into quad_mesh.corners (Nek local-face order), orient (E,6) D4 codes (element-local face frame -> canonical), and interior (E,(order-1)**3,3) private per-hex nodes (omit / None at order 1).

Parameters:
  • quad_mesh (QuadMesh)

  • hexes (NDArray[int64])

  • orient (NDArray[int64])

  • interior (NDArray[float64] | None)

  • element_tags (Tags | None)

Return type:

None

classmethod from_corners(points, hexes, element_tags=None, *, order=1)[source]

Build a linear HexMesh from corner points (P,3) + Nek-order hexes (E,8) connectivity – the corner -> B-rep bridge every factory routes through.

Parameters:
  • points (NDArray[float64])

  • hexes (NDArray[int64])

  • element_tags (Tags | None)

  • order (int)

Return type:

HexMesh

property face_tags: Tags

The tags on the shared faces, over face ids.

This is quads’ own element_tags read through, not a table of its own: rung N’s side tags are rung N-1’s element tags. A face is one stored object both its hexes reference, so naming it is naming that object – which is why there is no (hex, side) here for the two sides to disagree over. The (element, face) rows the .re2 boundary block wants are reconstructed at export from the hexes that carry each named face (see face_tag_rows).

property order: int

Global polynomial order (1 = linear).

property points: NDArray[float64]

The (P,3) shared corner points – a live view of the shared-face QuadMesh’s points (the single source of truth), so an in-place edit (mesh.points[:] = X) moves the shared corners for every hex.

property corners: NDArray[int64]

(E,8) Nek-order corner connectivity, derived (memoized) from the shared faces. Read-only; the B-rep quads / hexes / face_orient are the source of truth.

property edges: NDArray[int64]

min corner id first) – the shared edge topology (the edges of the shared-face QuadMesh). Non-empty at every order (edges are first-class B-rep storage).

Type:

(Ne,2) unique undirected hex edges (canonical

property edge_nodes: NDArray[float64]

(Ne, order-1, 3) shared high-order interior nodes of each unique edges entry, in canonical (min->max corner) order. Empty at order 1.

property faces: NDArray[int64]

sorted corner ids) – the shared face topology. Non-empty at every order (faces are first-class B-rep storage).

Type:

(Nf,4) unique hex faces (canonical

property face_nodes: NDArray[float64]

(Nf, (order-1)**2, 3) shared high-order interior nodes of each unique faces entry, in the canonical D4-normalized frame. Empty at order 1; a shared face resolves to the same nodes from either incident hex.

property n_points: int

Number of (shared) points.

property n_hexes: int

Number of hexahedra.

property n_face_tags: int

Number of tagged faces.

property face_group_tags: list[str]

Sorted unique tags of the tagged faces.

property element_group_tags: list[str]

Sorted unique non-empty per-hex element tags present on the mesh.

hexmesh.quality

Hex-element quality metrics.

nekmeshpy.hexmesh.quality.linear_scaled_jacobian(mesh, order)[source]

Per-hex minimum scaled Jacobian of the trilinear hex through the 8 corners alone, resampled at order – what .re2 exports, read the way a solver actually builds and checks its own working geometry: from the corners, at its own polynomial order, not at nekmeshpy’s stored (possibly curved) one.

order=1 is exactly corner_scaled_jacobian() – the 8 vertices alone. That is not sufficient in general: a trilinear hex’s Jacobian is a polynomial in each direction, so it can fold between the 8 corners while every corner itself reads positive, the same lesson scaled_jacobian()’s own sampling split teaches about the curved map – except here there is no curvature to blame, because .re2 never carried any. Confirmed against a real Nek5000 run: a mesh clean at every corner still failed the solver’s own Neg-Jacobian check at its working order, and reads exactly the same negative minimum here once resampled at that order.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.quality.linear_order_scan(mesh, orders=None, *, budget=None)[source]

linear_scaled_jacobian() read at several orders, order_scan()’s report shape and budget – but of the trilinear (corners-only) map, not the curved one. Unlike order_scan there is no “mesh’s own order” floor: the trilinear map is not stored at any particular order, so any order >= 1 is a legitimate reading of it, order=1 included.

orders defaults to SCAN_ORDER alone, the same solver-order reasoning as order_scan: it is the one reading that predicts what a real run’s own geometry generation will find, because it is built from the same corners the solver reads out of .re2.

Parameters:
Return type:

OrderScan

nekmeshpy.hexmesh.quality.solver_metrics(mesh, order=None)[source]

Nek5000/nekRS’s own mesh metrics: block for this mesh – GLL grid spacing, the min(J)/max(J) ratio it prints as “scaled Jacobian”, and corner-edge aspect ratio – of the trilinear map .re2 exports, resampled at order (SCAN_ORDER by default: the solver’s).

Matches nekRS’s printed line to its digits. Read SolverMetrics before comparing its jac_ratio with scaled_jacobian(): different quantities.

Parameters:
Return type:

SolverMetrics

nekmeshpy.hexmesh.quality.format_solver_metrics(m)[source]

The solver_metrics() block, laid out as nekRS prints it.

Parameters:

m (SolverMetrics)

Return type:

str

nekmeshpy.hexmesh.quality.corner_scaled_jacobian(points, hexes)[source]

Per-hex minimum corner scaled Jacobian, shape (N,).

Parameters:
  • points (NDArray[float64])

  • hexes (NDArray[int64])

Return type:

NDArray[float64]

nekmeshpy.hexmesh.quality.scaled_jacobian(mesh, order)[source]

Per-hex minimum scaled Jacobian sampled at the (order+1)**3 GLL nodes of the curved element block, shape (N,) – the order-N generalization of scaled_jacobian.

order may exceed mesh.order: the stored map is read on the finer lattice rather than re-meshed, so this answers “what will the solver see at lx1 = N” without leaving the toolkit.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.quality.order_scan(mesh, orders=None, *, budget=None)[source]

Read the mesh’s own map on finer GLL lattices and report what each one sees.

orders defaults to SCAN_ORDER alone – the order the solver runs, which is the only reading that predicts what the solver will do. A sweep of intermediate orders is not worth its cost and does not certify anything either: the reading is not monotone in the sampling order, and a real mesh has read positive at 2, negative at 3 and 4, positive again at 5, and negative at 8 and 11. Checking the order you actually run is the whole point.

budget defaults to SCAN_BUDGET, read here rather than captured as a default argument, so assigning the module constant at runtime actually takes effect. It caps the sampled points. At SCAN_ORDER that is 512 points a hex, so a large mesh runs to minutes; rather than spend it inside a routine report, the order is declined and returned in OrderScan.skipped. Declined is unchecked, not clean.

Parameters:
Return type:

OrderScan

nekmeshpy.hexmesh.quality.format_linear_scan(scan)[source]

The linear_order_scan() block of a report, warning line included.

Unlike format_scan(), there is no “mesh’s own order” to compare against – the trilinear map is not stored at any order, so a disagreement here is not “clean at N, folded at M” but simply “folds once resampled at all”, which is exactly what a real solver’s own geometry generation would find building its working GLL nodes from the same corners.

Parameters:

scan (OrderScan)

Return type:

str

nekmeshpy.hexmesh.quality.format_scan(scan, mesh_order)[source]

The order_scan() block of a report, warning line included.

The warning is the point of the whole thing: a mesh can be clean at its own order and folded at the one the solver runs, and the two readings are of the same geometry – only the sampling differs.

Parameters:
Return type:

str

nekmeshpy.hexmesh.quality.corner_summary(points, hexes)[source]

Aggregate quality statistics for a hex mesh.

Parameters:
  • points (NDArray[float64])

  • hexes (NDArray[int64])

Return type:

QualitySummary

nekmeshpy.hexmesh.quality.format_linear(stats, mesh_order)[source]

The linear (.re2) reading of a report, warning line included.

.re2 has no curved format – every mesh, at any stored order, exports as the straight-sided hex through its 8 corners alone (corner_scaled_jacobian), which is a genuinely different map from the curved one the mesh stores above order 1, not a coarser sampling of the same one. An element can be clean at its own order only because of the curvature its interior nodes carry – valid as a mesh, inverted as the file the solver actually reads.

Parameters:
Return type:

str

nekmeshpy.hexmesh.quality.summary(mesh, order)[source]

Aggregate statistics for the order-N scaled_jacobian() metric.

Parameters:
Return type:

QualitySummary

nekmeshpy.hexmesh.quality.histogram(points, hexes, bins=10, lo=0.0, hi=1.0)[source]

(counts, edges) histogram of the scaled Jacobian distribution.

Parameters:
  • points (NDArray[float64])

  • hexes (NDArray[int64])

  • bins (int)

  • lo (float)

  • hi (float)

Return type:

tuple[NDArray[int64], NDArray[float64]]

nekmeshpy.hexmesh.quality.format_report(stats, hist=None)[source]

Human-readable multi-line quality report from summary() output.

Parameters:
Return type:

str

Operations

hexmesh.assemble

Variable-arity HexMesh operations – the only ones that build a numbering.

class nekmeshpy.hexmesh.assemble.Seam[source]

Bases: NamedTuple

One stated interface: which face group of which block meets which.

a and b name a block either by its position in attach’s meshes or by the mesh object itself – resolved by identity, so a block appearing twice in the list must be named by index. tag_a / tag_b are face-tag names, or explicit arrays of face ids (tagged_faces).

own says which side’s node coordinates the seam keeps. attach_tag names the welded-shut faces; None – the default – clears them, because a buried face that keeps its name makes the exporter write one boundary row from each side of it.

a: int | HexMesh

Alias for field number 0

tag_a: str | NDArray[int64]

Alias for field number 1

b: int | HexMesh

Alias for field number 2

tag_b: str | NDArray[int64]

Alias for field number 3

own: str

Alias for field number 4

attach_tag: str | None

Alias for field number 5

static __new__(_cls, a, tag_a, b, tag_b, own='a', attach_tag=None)

Create new instance of Seam(a, tag_a, b, tag_b, own, attach_tag)

Parameters:
nekmeshpy.hexmesh.assemble.attach(meshes, seams)[source]

Join blocks along the interfaces each Seam names, in one pass.

Every seam states which face group of which block meets which, so no tolerance is needed anywhere: inside a named pair of groups the pairing is nearest-neighbour, and what proves it is bijectivity – equal point counts plus an injective map is a one-to-one correspondence however far apart the two halves sit. A seam with a real gap joins; a seam whose halves do not correspond is refused however close they are.

Contrast merge(), which is told nothing and infers every seam in the assembly from coordinates at one global tolerance.

One pass is the point of the n-ary form. Chaining binary joins would concatenate and rebuild the whole accumulated mesh once per link – work quadratic in the block count, since 32 blocks of 120 hexes cost 63240 hex-passes chained against 3840 in a single pass. Here every seam is paired first (each cheap, and confined to its own groups), then the blocks are welded and stitched exactly once:

mesh = hexmesh.attach([core, leg_p, leg_m],
                      [Seam(core, "port_p", leg_p, "join"),
                       Seam(core, "port_m", leg_m, "join")])

Blocks are concatenated in list order and a welded point keeps the lowest id, so the first block’s own numbering comes through untouched.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.components(mesh)[source]

The mesh split into its connected pieces – one HexMesh per group of hexes reachable through shared corner points, in the order their first hex appears.

What topology_report tells you about (a mesh that turned out to be two bodies), this hands you as meshes.

Parameters:

mesh (HexMesh)

Return type:

list[HexMesh]

nekmeshpy.hexmesh.assemble.face_group(mesh, which, side)[source]

One stated group’s face ids, from a tag name or given outright, checked to be on the block’s boundary.

The resolution step every operation that is told which faces it acts on shares – attach() and periodic_pairs both start here. side names the argument that supplied the group, so a message from an n-ary call says which of several groups was wrong.

Parameters:
Return type:

NDArray[int64]

nekmeshpy.hexmesh.assemble.loft(slices, *, loop=False, sweep_nodes=None, element_tags=None, first_tag=None, last_tag=None)[source]

Loft a stack of conformal quad profiles into a hex block (the general primitive behind extrude, and the top rung of the uniform sweep shared with linemesh.assemble.loft and QuadMesh.loft).

The body climbs the ladder the result is stored on: the shared edges and their nodes (the LineMesh), then the shared faces and theirs (the QuadMesh), then the hexes and their private interiors. Every entity of a sweep is carried (a section quad / edge sitting at one level) or swept (a section edge / point dragged across one layer), and both families are arithmetic – level * n_carried + k and n_prof * n_carried + layer * n_swept + k, two contiguous blocks that cannot collide – so every table below is written, never deduplicated.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.loft_fn(f, fractions, *, loop=False, order=None, element_tags=None, first_tag=None, last_tag=None)[source]

Loft a block from a parametrized family of sections – loft with the slices evaluated rather than handed in, so every node (the corners and the sweep-direction high-order nodes) comes from calling f and nothing is blended along the sweep.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.loft_spline(slices, *, loop=False, element_tags=None, first_tag=None, last_tag=None)[source]

loft with the sweep-direction nodes read off a cubic spline through the whole stack of sections, rather than blended straight between the two bounding a layer.

Same arguments, same numbering, same tags, and the sections given come back verbatim as the levels – the spline interpolates them, so this adds curvature between sections without moving any. It is the automatic form of loft(..., sweep_nodes=...): where that asks the caller for the intermediate sections, this fits them. All three node blocks a section stores are fitted – its shared corners, its shared edges’ interiors and its faces’ own – so the block is curved along the sweep at every node, not only on the skeleton.

Reach for it when a sweep has a feature that turns sharply across a handful of sections: loft cuts the corner with a chord however high the order, and refining the order alone will not fix that – the nodes it adds land on the same chord.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.merge(meshes, *, tol=1e-07, clear_seam_tags=False)[source]

Stitch several hex blocks into one, coordinate-welding coincident seam points in a single pass. tol is a fraction of conform.bbox_scale – the largest of the x/y/z ranges over every point handed in – so the coincidence radius is tol * bbox_scale. Not a distance: if you know one, divide by conform.bbox_scale(...).

This is the proximity join: it is told nothing about what meets what and infers every seam in the assembly from coordinates, at one tolerance, over every block’s whole boundary at once. When you know which face group meets which, attach() states it and confines the search to those two groups.

clear_seam_tags drops face names off the faces this weld buries – a face that comes out with a hex on both sides. A buried face that keeps a name makes the exporter write a boundary row from each side of it, so an interface you tag on the section for one block’s benefit and then weld shut has to be cleared afterwards; this does it in the weld. True clears every buried face; a name or list of names clears only those, leaving every other tag (a genuine conjugate interface such as a "solid"/fluid wall) on the buried face and still subject to the two-different-names-raise rule. The default False keeps today’s behaviour: a buried face keeps whatever it was named. This is attach()’s attach_tag=None rule, brought to the proximity join – mesh a block with its whole outer surface named, weld, and what stays named is exactly the part that stayed on the boundary.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.refine(mesh)[source]

Uniform H-refinement: split every hex into 8 – its own true cell-center point plus the face-centers/edge-midpoints quadmesh.refine already puts on a quad’s shared line_mesh.

Exact at any order, the same way linemesh.refine/quadmesh.refine are: the new cell-center, and every child’s own curved interior, are read off the parent’s stored polynomial map via the internal core.interp order-N kernel. A face shared between two hexes is refined through the one shared quad_mesh exactly once, so both neighbours land on the identical sub-faces automatically.

Every quad this rung ever needs – the 4 sub-quads of each of the hex’s own 6 faces, and the 12 new quads cutting through its interior – goes through a single, combined quadmesh._helpers.entities_from_blocks call, not two separate ones. A new interior quad’s own edge from an edge-midpoint to a face-center is one of that face’s own quadmesh.refine spokes (the face IS a quad, and the hex edge bounding it IS one of its own sides) – so deduplicating the interior quads against the boundary ones after the fact, rather than in the same pass, mistook that shared spoke for two different edges and left the mesh with a torn seam there (a valence-1 edge where a real one has valence 2, easy to miss until the result is refined again and the wrong edge count changes which faces new octants can find).

Hex e’s new cell-center is point id quadmesh.refine(mesh.quad_mesh).n_points + e. Child 8*e + k (k = 0..7, the _CORNER_IJK[3][k] corner) is the octant nearer corner k. One call is one level.

Parameters:

mesh (HexMesh)

Return type:

HexMesh

nekmeshpy.hexmesh.assemble.remove(mesh, which)[source]

The complement of select(): everything which does not name – the “drop this block and re-fill it” half of the pair.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.assemble.select(mesh, which)[source]

The named hexes as a block of their own, renumbered from zero.

which is a tag string (every hex carrying it), an (E,) boolean mask, or an array of hex ids. Kept hexes hold their relative order, their element_tags and whichever face_tags rows name them; faces, edges and points nothing kept touches are dropped. The inverse of merge() – merge(components(mesh)) reproduces a mesh, and select by a region tag undoes what merge joined.

The faces a removal exposes are new topological boundary and carry no tag: face_tags names what it named before, so name the hole yourself from boundary_faces if the export needs it. For the same reason the result is not guaranteed watertight – that is the point of it.

Parameters:
Return type:

HexMesh

hexmesh.lift

Fixed-arity HexMesh operations that raise a rung (delta +1).

nekmeshpy.hexmesh.lift.adapter(a, b, *, axis=None, layers=2, max_deviation=0.2, radius_tol=0.05, element_tags=None)[source]

A short block morphing between two same-connectivity sections whose node patterns differ slightly – and whose both end faces are bit-exact.

element_tags names the block’s own elements, as in extrude – a connector belongs to whichever region it joins, and is the one piece with no section of its own to inherit a name from.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.lift.annulus(inner, outer, radial, *, inner_tag=None, outer_tag=None, element_tags=None)[source]

Shell O-grid filling the region between an inner and an outer closed quad surface (e.g. a sphere inside a cubic far-field box).

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.lift.bridge(a, b, *, layers=4, stub_fraction=0.3, stub_max=1.5, blend_layers=6, radius_tol=0.05, element_tags=None)[source]

A connector between two same-radius sections whose node patterns are too far apart for adapter – two legs of different T-junctions, built by different algorithms.

element_tags names the block’s own elements, as in adapter.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.lift.extrude(section, length, layers, *, axis=array([0., 0., 1.]), origin=array([0., 0., 0.]), element_tags=None, first_tag=None, last_tag=None)[source]

Sweep a single quad section a distance length along axis into a hex block.

Parameters:
  • section (QuadMesh)

  • length (float)

  • layers (int | NDArray[float64])

  • axis (NDArray[float64])

  • origin (NDArray[float64])

  • element_tags (str | Tags | None)

  • first_tag (str | Tags | None)

  • last_tag (str | Tags | None)

Return type:

HexMesh

nekmeshpy.hexmesh.lift.from_grid(P, *, side_tags=None, element_tag='', order=1)[source]

Build hexes from a structured point grid P (ni+1,nj+1,nk+1,3).

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.lift.sweep(section, path, fractions, *, origin, tangent=None, orientation='transport', up=None, twist=0.0, close_twist=True, normal=None, loop=False, element_tags=None, first_tag=None, last_tag=None)[source]

A block swept from one QuadMesh section along the curve path – a round pipe bent through a 90-degree elbow or a U-turn, from one O-grid disc.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.lift.sweep_path(section, path, *, origin, target_length=None, layers=None, fractions=None, orientation=None, up=None, twist=0.0, close_twist=True, normal=None, loop=False, element_tags=None, first_tag=None, last_tag=None)[source]

sweep driven by a Path rather than by a loose (centerline, tangent, fractions) triple.

A path that also carries its own frame (anything from paths.walk) needs no orientation: with none asked for, that frame is held per station, so a bend out of plane and a distributed roll arrive here as authored. Naming an orientation (or an up) overrides it.

Parameters:
Return type:

HexMesh

hexmesh.shape

Shape factories for the HexMesh rung – the ones owning a shape model rather than being generic over any input.

nekmeshpy.hexmesh.shape.tetra(faces, *, center=None, element_tag='')[source]

Mesh the curvilinear tetrahedron enclosed by four triangular faces.

element_tag names all four octants, so a tetra filling a corner of a larger region can carry that region’s name like any other block does.

Parameters:
Return type:

HexMesh

hexmesh.lower

Fixed-arity HexMesh operations that lower a rung (delta -1).

nekmeshpy.hexmesh.lower.boundary_mesh(mesh, tag=None, *, template=None)[source]

A block’s boundary surface as a QuadMesh, carrying the block’s own nodes.

Parameters:
Return type:

QuadMesh

hexmesh.morph

Fixed-arity, rung-preserving HexMesh operations (delta 0).

nekmeshpy.hexmesh.morph.blend(a, b, fractions)[source]

Linearly morph between two conformal blocks a and b (identical hexes, equal point count), one block per fraction t with points (1-t)*a + t*b – t=0 reproduces a, t=1 reproduces b. Each block returned carries a’s face_tags and empty element_tags.

Parameters:
Return type:

list[HexMesh]

nekmeshpy.hexmesh.morph.mirror(mesh, normal, point=array([0., 0., 0.]))[source]

A new block reflected through the plane with normal through point, with every hex re-wound so it comes back the right way out.

A reflection has determinant -1 and inverts every element: a bare transform by a reflection matrix hands back a block whose scaled Jacobian is negative throughout and whose volume reads negative – a mesh no solver will take. mirror pairs the coordinate map with the winding fix, so the result measures exactly as its original did.

The symmetric-domain idiom – mesh the half the geometry lets you, then recover the whole:

full = hexmesh.merge([half, hexmesh.mirror(half, normal=(1, 0, 0))])

merge welds the two along the symmetry plane they now share, so the seam comes out conformal rather than doubled. The faces that were tagged on the symmetry plane are interior afterwards and still carry their tag: drop those rows, or leave the plane untagged on the half in the first place.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.morph.rotate(mesh, angle, axis=array([0., 0., 1.]), center=array([0., 0., 0.]))[source]

A new block rotated by angle radians about the line through center along axis (right-handed, axis need not be normalized).

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.morph.scale(mesh, factor, center=array([0., 0., 0.]))[source]

A new block scaled about center by factor – a scalar (uniform) or a (3,) per-axis vector. Every factor must be positive.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.morph.transform(mesh, matrix, offset=array([0., 0., 0.]))[source]

A new block with every node mapped through the affine p @ matrix.T + offset.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.morph.transform_fn(mesh, fn)[source]

A new block with every node – the shared-face QuadMesh all the way down, and the private per-hex interior – mapped through an arbitrary fn; transform() for a warp that is not affine. fn is called once per node block with an (N, 3) array and must return (N, 3). Connectivity and tags ride through verbatim.

fn can fold space; the result is checked and a logging.warning (not an error) is emitted when any hex comes out inverted – sampled on the stored map at mesh.order, a flag rather than a certificate, and no substitute for quality_summary at the solver’s order. It does not re-wind a reflection; use mirror() for that.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.morph.translate(mesh, vector)[source]

A new block shifted rigidly by vector (3,). Bit-exact: the offset is added without a matmul, so translating by 0 returns the identical coordinates.

Parameters:
Return type:

HexMesh

hexmesh.periodic

Periodic face pairing: attach’s stated correspondence, without the weld.

A Nek5000 'P  ' boundary row names the element and face on the other side of the domain, so a periodic boundary is not a name on a face – it is a correspondence between two face groups. This module computes it and nothing else: no coordinate is moved, no entity is fused, no numbering is invented, and the mesh comes back untouched. What is produced is a table the .re2 writer fills bc(1) / bc(2) from.

The pairing is attach’s – each of a’s corners to its nearest on b, proved by bijectivity rather than by a tolerance – with one difference that the whole design turns on: a periodic pair is told its transform. attach needs none because its two halves are meant to end up in the same place, so nearest-neighbour reads the intended correspondence directly. Periodic halves sit a lattice vector apart and are often each other’s rotations, where the nearest face across the gap is emphatically not the periodic image. Stating the map both makes the pairing possible and makes it checkable: the worst residual after mapping is compared against conform.entity_tol – the toolkit-wide coincidence tolerance – so a mis-stated pitch is an error quoting the number rather than a silently twisted mesh.

class nekmeshpy.hexmesh.periodic.Periodic[source]

Bases: NamedTuple

One stated periodic pair: which face group maps onto which, and by what map.

tag_a / tag_b are face-tag names or explicit face id arrays, exactly as Seam’s are. They must name different groups: a single tag covering both ends of a periodic cell cannot say which end is which, so split it at the point it is applied rather than guessing here from a coordinate.

transform is an affine.Affine pair (matrix, offset) carrying a onto b – build it with affine.translation or affine.rotation, the same constructors hexmesh.translate and hexmesh.rotate take. It is verified, not trusted.

Two things it cannot verify:

A group with a rotational symmetry of its own pairs bijectively at residual zero onto a cyclic shift of the intended correspondence – the same trap attach carries, and a real one for a rotationally periodic cut through a bladed or lattice section, where nothing in the geometry distinguishes the two readings. A translation-periodic pair is safe from it.

And Nek identifies the two sides’ values in the global Cartesian frame: it does not rotate a vector across a periodic face. A rotational pair is therefore a periodicity for a scalar, and for a velocity field only where that field is invariant in the global frame. A translation is unconditionally fine.

tag_a: str | NDArray[int64] | Sequence[int]

Alias for field number 0

tag_b: str | NDArray[int64] | Sequence[int]

Alias for field number 1

transform: tuple[NDArray[float64] | None, NDArray[float64]]

Alias for field number 2

static __new__(_cls, tag_a, tag_b, transform)

Create new instance of Periodic(tag_a, tag_b, transform)

Parameters:
  • tag_a (FaceGroup)

  • tag_b (FaceGroup)

  • transform (affine.Affine)

class nekmeshpy.hexmesh.periodic.PeriodicPairs[source]

Bases: NamedTuple

The resolved correspondence: what a 'P  ' boundary row needs.

rows is (2K, 4) of [element, local face 1-6, partner element, partner local face], lexsorted by (element, face). Both directions are in it – Nek wants a P row on each side of the pair, and emitting the two from one table is what makes them agree by construction rather than by a second pass.

worst is the largest residual left by any spec’s transform and tol what it was checked against, kept for a caller that wants to log how exactly the two halves corresponded.

rows: NDArray[int64]

Alias for field number 0

worst: float

Alias for field number 1

tol: float

Alias for field number 2

partner_of()[source]

{(element, face): (partner element, partner face)} – the lookup a writer indexes its boundary rows by.

Return type:

dict[tuple[int, int], tuple[int, int]]

static __new__(_cls, rows, worst, tol)

Create new instance of PeriodicPairs(rows, worst, tol)

Parameters:
nekmeshpy.hexmesh.periodic.periodic_pairs(mesh, specs)[source]

The face-to-face correspondence every Periodic in specs states, as the (2K,4) table to_re2 writes 'P  ' rows from.

Nothing is welded and nothing is renamed: the two sides stay two distinct boundary faces with their own names, which is what a periodic boundary is. Contrast attach, which pairs the same way and then fuses the two into one interior face:

pairs = hexmesh.periodic_pairs(mesh, [
    hexmesh.Periodic("inlet", "outlet", affine.translation([0, 0, LEAD])),
    hexmesh.Periodic("cut_lo", "cut_hi", affine.translation([0, 0, LEAD]))])

Every face may be claimed once. A face in two specs, or in both sides of one, would need two partners and get whichever spec ran last – so it raises instead.

Parameters:
Return type:

PeriodicPairs

hexmesh.query

Read-only HexMesh queries – the operations that leave the ladder.

class nekmeshpy.hexmesh.query.TagReport[source]

Bases: NamedTuple

How a mesh’s side-tag table lines up with its topological boundary – the same report at the hex rung (faces) and the quad rung (edges).

The two are independent by design – boundary is what connectivity says, a side-tag table is a named subset that may also name interior entities – so they can disagree in exactly two ways, and both are worth seeing. An n_untagged_boundary above zero means the export has open sides no boundary condition covers; an n_tagged_interior above zero means it will write boundary conditions onto sides that are not on the boundary at all. Either can be deliberate (a flux-measurement plane is a tagged interior face; so is a conjugate fluid/solid interface that keeps the fluid’s wall condition), so these are counts to recognise, not assertions.

n_rows: int

Named sides. One tag per shared entity, so this counts entities, not rows – the two could differ only while a tag was addressed by (element, side).

n_untagged_boundary: int

Sides on the topological boundary that carry no name.

n_tagged_interior: int

Named sides that are not on the topological boundary.

static __new__(_cls, n_rows, n_untagged_boundary, n_tagged_interior)

Create new instance of TagReport(n_rows, n_untagged_boundary, n_tagged_interior)

Parameters:
  • n_rows (int)

  • n_untagged_boundary (int)

  • n_tagged_interior (int)

nekmeshpy.hexmesh.query.face_rows(mesh, faces)[source]

((K,2) [element, local face 1-6] rows, (len(faces),) occurrence counts) – the inverse of hexes, for the given shared-face ids.

A face id is a row of quad_mesh; this says which hexes reference it and as which of their six local faces. A boundary face is carried by one hex and yields one row, an interior one by two and yields two, so counts is how a caller expands a per-face value onto the rows it produced. Rows come out grouped by input face, ascending within a group; face_tag_rows() lexsorts them.

Built from the sparse side: the flat hexes face ids are argsorted once and the wanted ids located in them, so nothing the size of (E,6) is ever materialised per face (at chimera’s 438k elements a string array that shape alone is ~170 MB).

Parameters:
  • mesh (HexMesh)

  • faces (NDArray[int64])

Return type:

tuple[NDArray[int64], NDArray[int64]]

nekmeshpy.hexmesh.query.face_tag_rows(mesh)[source]

((K,2) [element, local face 1-6] rows, their tags) for every named face, lexsorted by (element, face).

The inverse of how the tags are stored – face_rows() run over face_tags.ids. A tag names a shared face; a boundary face is carried by one hex and yields one row, an interior one by two and yields two – which is the honest reading of “this face is named”, and what lets an exporter give the two sides different codes from the regions on either side.

Parameters:

mesh (HexMesh)

Return type:

tuple[NDArray[int64], NDArray[str_]]

nekmeshpy.hexmesh.query.bounds(mesh, *, high_order=False)[source]

The axis-aligned bounding box of the block’s nodes – corners only unless high_order=True asks for the stored edge / face / interior nodes too. See linemesh.bounds for why neither reading bounds the polynomial itself.

Parameters:
Return type:

Bounds

nekmeshpy.hexmesh.query.boundary_elements(mesh)[source]

Sorted unique element ids with at least one face on the domain boundary.

Parameters:

mesh (HexMesh)

Return type:

NDArray[int64]

nekmeshpy.hexmesh.query.boundary_face_ids(mesh)[source]

(n_faces,) mask of the shared faces carried by exactly one hex.

The face-id form of boundary_faces(), and the cheaper one: the faces are already deduplicated in quads, so this is a bincount over the incidence rather than a hash of every element’s corner tuples.

It is also how a name is checked against the topology it was meant for – face_tags.ids outside this mask are the tagged interior faces tag_report() counts.

Parameters:

mesh (HexMesh)

Return type:

NDArray[bool]

nekmeshpy.hexmesh.query.boundary_faces(mesh)[source]

(K,2) of [element id, local face (1-6)] for every face on the topological domain boundary (a quad carried by a single hex). Distinct from the tagged face_tags, which may also carry interior planes.

Parameters:

mesh (HexMesh)

Return type:

NDArray[int64]

nekmeshpy.hexmesh.query.boundary_points(mesh)[source]

Sorted unique point ids lying on the domain boundary.

Parameters:

mesh (HexMesh)

Return type:

NDArray[int64]

nekmeshpy.hexmesh.query.centroid(mesh, *, high_order=False)[source]

The volume-weighted centroid integral x dV / integral dV – the mass property, not the mean of the points (which would weight a finely meshed corner over a coarse bulk).

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.classify_points(mesh, wall)[source]

Flag welded points: (is_wall, is_fixed). Faces named wall are wall; all other tagged faces are fixed. A point on both is treated as fixed.

Parameters:
Return type:

tuple[NDArray[bool], NDArray[bool]]

nekmeshpy.hexmesh.query.corner_scaled_jacobian(mesh)[source]

Per-hex minimum scaled Jacobian (n_hexes,) of the linear hex through the 8 corners alone – the geometry .re2 actually exports, at any stored order.

Not a substitute for scaled_jacobian(): a corner reading cannot see where a curved mesh’s interior nodes went, so it cannot certify a curved element the way the curved block can. This answers a narrower, different question – what happens when that curvature is discarded, which .re2 always does – and an element can disagree in either direction: curved-clean/corner-inverted (only valid because of its own curvature) is the case worth watching for before export.

Parameters:

mesh (HexMesh)

Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.corner_summary(mesh)[source]

Aggregate statistics over corner_scaled_jacobian() – see there.

Parameters:

mesh (HexMesh)

Return type:

QualitySummary

nekmeshpy.hexmesh.query.element_blocks(mesh)[source]

(E, (order+1)**3, 3) – each hex’s own node block, assembled natively from the B-rep: shared corners, then the shared edge-interior nodes in element traversal order, then the shared face interiors turned out of each face’s canonical frame into this hex’s, then the private per-hex interiors, each written at its lattice slot. Nothing is resampled or deduplicated.

The top rung of linemesh.element_blocks / quadmesh.element_blocks; this one has the extra face family.

Parameters:

mesh (HexMesh)

Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.element_volumes(mesh, *, high_order=False)[source]

(E,) signed volume of each hex – the integral of the isoparametric Jacobian, so an inverted hex reads negative, on the same sign convention as scaled_jacobian().

Trilinear corners by default – which is what .re2 exports, since that format is linear at any order – and high_order=True for the curved element the mesh stores. Both readings are exact: the quadrature is taken high enough to integrate the determinant of an order-N map without error.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.is_conforming(mesh)[source]

True if the mesh has no hanging points (no T-junctions).

Parameters:

mesh (HexMesh)

Return type:

bool

nekmeshpy.hexmesh.query.is_overlap_free(mesh)[source]

True if no two elements geometrically overlap. Independent of is_watertight and is_conforming, which are purely topological (facet-sharing) checks – a duplicated or mis-placed piece that never shares a face with the rest of the mesh would still read watertight and conformal. Not part of topology_report: this is a geometric search whose cost a plain watertight/conforming check should not have to pay, so it is computed only when asked for.

Parameters:

mesh (HexMesh)

Return type:

bool

nekmeshpy.hexmesh.query.is_watertight(mesh)[source]

True if the mesh boundary is a closed, leak-tight 2-manifold and the mesh is a single connected component. Does not imply conformity.

Parameters:

mesh (HexMesh)

Return type:

bool

nekmeshpy.hexmesh.query.linear_order_scan(mesh, orders=None, *, budget=None)[source]

order_scan’s report shape, for the trilinear (.re2) map instead of the curved one – see linear_scaled_jacobian().

Parameters:
Return type:

OrderScan

nekmeshpy.hexmesh.query.linear_scaled_jacobian(mesh, *, order=None)[source]

Per-hex minimum scaled Jacobian of the trilinear hex through the 8 corners alone – .re2’s own geometry – resampled at order.

Unlike corner_scaled_jacobian() (the special case order=1: the 8 vertices only), this catches a fold that sits between the corners, which a real solver’s own geometry generation would find building its working nodes from the same corners at its own polynomial order – order should be that order (SCAN_ORDER by default, the same solver-order default order_scan uses).

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.quality_summary(mesh, *, order=None)[source]

Aggregate scaled-Jacobian statistics over the curved elements – see scaled_jacobian.

Above order 1, this also checks the linear reading – corner_summary, the straight-sided hex through the 8 corners alone – because that, not the curved geometry just summarised, is what .re2 actually exports: it has no curved format at any stored order. An element can be clean here and still invert once flattened for export, if it depends on its own curvature to stay valid; that is logged as a warning rather than folded into the returned value, the same way report warns on an order_scan disagreement without changing what quality_summary itself returns.

Parameters:
Return type:

QualitySummary

nekmeshpy.hexmesh.query.report(mesh)[source]

Human-readable summary: element/point counts, scaled-Jacobian quality, per-name tagged-face counts cross-checked against the boundary (tag_report), and the topology report.

Parameters:

mesh (HexMesh)

Return type:

str

nekmeshpy.hexmesh.query.scaled_jacobian(mesh, *, order=None)[source]

Per-hex minimum scaled Jacobian (n_hexes,), read off the curved element the mesh actually stores.

There is deliberately no corner-only reading. A corner scaled Jacobian cannot see where the high-order nodes went, so it reports a contented number for a mesh whose interior nodes are anywhere at all – a node moved clean outside the element still scores the same. Anything that has to be trusted must read the curved block.

order samples that block on a finer GLL lattice than the mesh’s own – what a solver running at lx1 = order does to it. The default reads the mesh’s own order, where the value is exact at the nodes and silent between them: positive there is not a proof the element is not folded.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.hexmesh.query.solver_metrics(mesh, *, order=None)[source]

Nek5000/nekRS’s mesh metrics: block – GLL spacing, min(J)/max(J) (which nekRS calls “scaled Jacobian”) and aspect ratio – of the .re2 map at order. See SolverMetrics.

Parameters:
Return type:

SolverMetrics

nekmeshpy.hexmesh.query.tag_report(mesh)[source]

Cross-check face_tags against the topological boundary (see TagReport).

Parameters:

mesh (HexMesh)

Return type:

TagReport

nekmeshpy.hexmesh.query.tagged_faces(mesh, tag)[source]

The shared-face ids carrying tag, ascending.

The handle every face group is addressed by at this rung – face_tags is stored sparse and sorted, so this is its ids filtered by name. It is public because hexmesh.attach works from exactly this list, and a caller who wants to see, reorder or subset the group it will pair needs to be able to get at it.

A tag that names nothing raises rather than returning an empty group: a mis-spelled interface name is otherwise invisible until the solver reads the mesh.

Parameters:
Return type:

NDArray[int64]

nekmeshpy.hexmesh.query.topology_report(mesh)[source]

Watertightness / connectivity report of the mesh.

Parameters:

mesh (HexMesh)

Return type:

TopologyReport

nekmeshpy.hexmesh.query.volume(mesh, *, high_order=False)[source]

Total volume – element_volumes() summed.

The first number to check after is_watertight(): a mesh can be watertight, conforming and free of inverted elements and still enclose the wrong region because a profile landed at the wrong station. Being signed, it also catches a mesh whose elements are wound inside out – that comes back negative rather than plausible.

Parameters:
Return type:

float

hexmesh.tag

Vocabulary-only HexMesh operations: rename the tags, touch nothing else.

Separate from morph, which is the delta-0 rung for operations on the geometry. These change neither coordinates nor connectivity nor numbering – only what the two tag tables call things – so a caller reading the sibling list can tell at a glance that a retag cannot have moved a node.

nekmeshpy.hexmesh.tag.retag_element(mesh, tags)[source]

The same mesh with its element_tags changed, by what tags is: a mapping renames the existing names (a tag the map does not name is left alone, the map applies simultaneously, a key naming nothing raises, a rename to NO_TAG untags); a string names every element; a Tags or per-element sequence replaces the table. Geometry and the side tags are untouched.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.tag.retag_face(mesh, mapping)[source]

The same mesh with its face_tags renamed through mapping, rows kept in stored order (see retag_element() for the map’s own rules).

Order matters here beyond tidiness: .re2 writes boundary rows in it, and .vtu gives a node touched by several rows the last one’s tag.

Renaming a tag to NO_TAG drops its rows rather than storing an empty name: a side-tag table is a named subset of the boundary, so leaving it is leaving the table. That is the way to retire a name that has stopped meaning anything – an "inlet" welded shut into an interior plane, which would otherwise export as a boundary condition on a face that is no longer on the boundary:

mesh = hexmesh.retag_face(mesh, {"inlet": "", "outlet": ""})

tag_report is what finds those.

Parameters:
Return type:

HexMesh

nekmeshpy.hexmesh.tag.tag_faces(mesh, faces, tags)[source]

The same mesh with the given shared faces named, by face id.

The entity-side authoring form, and the natural handle at this rung: after a merge or a weld the thing you have is a set of faces, not a set of (hex, side) pairs – see boundary_face_ids and face_tag_rows for the two ways to get them. (Its quad counterpart tag_edges takes (quad, side) rows instead, because the factories that use it genuinely think element-locally.)

tags is one name for all of them or one per face; NO_TAG names nothing, and a face already named is overwritten.

Parameters:
Return type:

HexMesh