hexmesh¶
- class nekmeshpy.hexmesh.HexMesh[source]¶
Bases:
objectAn 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
fis facef+1, wound so the right-hand normal points out of the hex. The array iscore.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(aQuadMeshholding every shared face – itspointsare the shared corners, itsquadsthe shared face connectivity, its edges /interiorthe shared face-boundary / interior HO nodes),hexes(E,6)face indices intoquad_mesh.corners(Nek local-face order),orient(E,6)D4 codes (element-local face frame -> canonical), andinterior(E,(order-1)**3,3)private per-hex nodes (omit /Noneat order 1).
- classmethod from_corners(points, hexes, element_tags=None, *, order=1)[source]¶
Build a linear
HexMeshfrom cornerpoints(P,3)+ Nek-orderhexes(E,8)connectivity – the corner -> B-rep bridge every factory routes through.
- property face_tags: Tags¶
The tags on the shared faces, over face ids.
This is
quads’ ownelement_tagsread 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.re2boundary block wants are reconstructed at export from the hexes that carry each named face (seeface_tag_rows).
- property points: NDArray[float64]¶
The
(P,3)shared corner points – a live view of the shared-faceQuadMesh’spoints(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-repquads/hexes/face_orientare the source of truth.
- property edges: NDArray[int64]¶
min corner id first) – the shared edge topology (the
edgesof the shared-faceQuadMesh). 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 uniqueedgesentry, 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
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.re2exports, 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=1is exactlycorner_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 lessonscaled_jacobian()’s own sampling split teaches about the curved map – except here there is no curvature to blame, because.re2never 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.
- 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. Unlikeorder_scanthere is no “mesh’s own order” floor: the trilinear map is not stored at any particular order, so anyorder >= 1is a legitimate reading of it,order=1included.ordersdefaults toSCAN_ORDERalone, the same solver-order reasoning asorder_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.
- nekmeshpy.hexmesh.quality.solver_metrics(mesh, order=None)[source]¶
Nek5000/nekRS’s own
mesh metrics:block for this mesh – GLL grid spacing, themin(J)/max(J)ratio it prints as “scaled Jacobian”, and corner-edge aspect ratio – of the trilinear map.re2exports, resampled atorder(SCAN_ORDERby default: the solver’s).Matches nekRS’s printed line to its digits. Read
SolverMetricsbefore comparing itsjac_ratiowithscaled_jacobian(): different quantities.- Parameters:
- Return type:
- nekmeshpy.hexmesh.quality.format_solver_metrics(m)[source]¶
The
solver_metrics()block, laid out as nekRS prints it.- Parameters:
m (SolverMetrics)
- Return type:
- 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)**3GLL nodes of the curved element block, shape(N,)– the order-N generalization ofscaled_jacobian.ordermay exceedmesh.order: the stored map is read on the finer lattice rather than re-meshed, so this answers “what will the solver see atlx1 = N” without leaving the toolkit.
- 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.
ordersdefaults toSCAN_ORDERalone – 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.budgetdefaults toSCAN_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. AtSCAN_ORDERthat 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 inOrderScan.skipped. Declined is unchecked, not clean.
- 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.
- 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.
- nekmeshpy.hexmesh.quality.corner_summary(points, hexes)[source]¶
Aggregate quality statistics for a hex mesh.
- Parameters:
points (NDArray[float64])
hexes (NDArray[int64])
- Return type:
- nekmeshpy.hexmesh.quality.format_linear(stats, mesh_order)[source]¶
The linear (
.re2) reading of a report, warning line included..re2has 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:
stats (QualitySummary)
mesh_order (int)
- Return type:
- nekmeshpy.hexmesh.quality.summary(mesh, order)[source]¶
Aggregate statistics for the order-N
scaled_jacobian()metric.- Parameters:
- Return type:
Operations¶
hexmesh.assemble¶
Variable-arity HexMesh operations – the only ones that build a numbering.
- class nekmeshpy.hexmesh.assemble.Seam[source]¶
Bases:
NamedTupleOne stated interface: which face group of which block meets which.
aandbname a block either by its position inattach’smeshesor by the mesh object itself – resolved by identity, so a block appearing twice in the list must be named by index.tag_a/tag_bare face-tag names, or explicit arrays of face ids (tagged_faces).ownsays which side’s node coordinates the seam keeps.attach_tagnames 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.
- nekmeshpy.hexmesh.assemble.attach(meshes, seams)[source]¶
Join blocks along the interfaces each
Seamnames, 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.
- nekmeshpy.hexmesh.assemble.components(mesh)[source]¶
The mesh split into its connected pieces – one
HexMeshper group of hexes reachable through shared corner points, in the order their first hex appears.What
topology_reporttells you about (a mesh that turned out to be two bodies), this hands you as meshes.
- 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()andperiodic_pairsboth start here.sidenames the argument that supplied the group, so a message from an n-ary call says which of several groups was wrong.
- 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 withlinemesh.assemble.loftandQuadMesh.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 (theQuadMesh), 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 + kandn_prof * n_carried + layer * n_swept + k, two contiguous blocks that cannot collide – so every table below is written, never deduplicated.
- 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 –
loftwith the slices evaluated rather than handed in, so every node (the corners and the sweep-direction high-order nodes) comes from callingfand nothing is blended along the sweep.
- nekmeshpy.hexmesh.assemble.loft_spline(slices, *, loop=False, element_tags=None, first_tag=None, last_tag=None)[source]¶
loftwith 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:
loftcuts 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.
- 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.
tolis a fraction ofconform.bbox_scale– the largest of the x/y/z ranges over every point handed in – so the coincidence radius istol * bbox_scale. Not a distance: if you know one, divide byconform.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_tagsdrops 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.Trueclears 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 defaultFalsekeeps today’s behaviour: a buried face keeps whatever it was named. This isattach()’sattach_tag=Nonerule, 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.
- 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.refinealready puts on a quad’s sharedline_mesh.Exact at any order, the same way
linemesh.refine/quadmesh.refineare: the new cell-center, and every child’s own curved interior, are read off the parent’s stored polynomial map via the internalcore.interporder-N kernel. A face shared between two hexes is refined through the one sharedquad_meshexactly 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_blockscall, 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 idquadmesh.refine(mesh.quad_mesh).n_points + e. Child8*e + k(k= 0..7, the_CORNER_IJK[3][k]corner) is the octant nearer cornerk. One call is one level.
- nekmeshpy.hexmesh.assemble.remove(mesh, which)[source]¶
The complement of
select(): everythingwhichdoes not name – the “drop this block and re-fill it” half of the pair.
- nekmeshpy.hexmesh.assemble.select(mesh, which)[source]¶
The named hexes as a block of their own, renumbered from zero.
whichis a tag string (every hex carrying it), an(E,)boolean mask, or an array of hex ids. Kept hexes hold their relative order, theirelement_tagsand whicheverface_tagsrows name them; faces, edges and points nothing kept touches are dropped. The inverse ofmerge()–merge(components(mesh))reproduces a mesh, andselectby a region tag undoes whatmergejoined.The faces a removal exposes are new topological boundary and carry no tag:
face_tagsnames what it named before, so name the hole yourself fromboundary_facesif the export needs it. For the same reason the result is not guaranteed watertight – that is the point of it.
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_tagsnames the block’s own elements, as inextrude– a connector belongs to whichever region it joins, and is the one piece with no section of its own to inherit a name from.
- 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).
- 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_tagsnames the block’s own elements, as inadapter.
- 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
sectiona distancelengthalongaxisinto a hex block.
- 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).
- 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
QuadMeshsectionalong the curvepath– a round pipe bent through a 90-degree elbow or a U-turn, from one O-grid disc.- Parameters:
section (QuadMesh)
path (Callable[[NDArray[float64]], NDArray[float64]])
fractions (NDArray[float64])
tangent (Callable[[NDArray[float64]], NDArray[float64]] | None)
orientation (Literal['transport', 'fixed', 'frenet'])
up (NDArray[float64] | Sequence[float] | Callable[[NDArray[float64]], NDArray[float64]] | None)
twist (float)
close_twist (bool)
loop (bool)
- Return type:
- 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]¶
sweepdriven by aPathrather than by a loose(centerline, tangent, fractions)triple.A path that also carries its own frame (anything from
paths.walk) needs noorientation: with none asked for, that frame is held per station, so a bend out of plane and a distributedrollarrive here as authored. Naming anorientation(or anup) overrides it.- Parameters:
- Return type:
hexmesh.shape¶
Shape factories for the HexMesh rung – the ones owning a shape model rather
than being generic over any input.
hexmesh.lower¶
Fixed-arity HexMesh operations that lower a rung (delta -1).
hexmesh.morph¶
Fixed-arity, rung-preserving HexMesh operations (delta 0).
- nekmeshpy.hexmesh.morph.blend(a, b, fractions)[source]¶
Linearly morph between two conformal blocks
aandb(identicalhexes, equal point count), one block per fractiontwith points(1-t)*a + t*b–t=0reproducesa,t=1reproducesb. Each block returned carriesa’sface_tagsand emptyelement_tags.
- nekmeshpy.hexmesh.morph.mirror(mesh, normal, point=array([0., 0., 0.]))[source]¶
A new block reflected through the plane with
normalthroughpoint, with every hex re-wound so it comes back the right way out.A reflection has determinant
-1and inverts every element: a baretransformby a reflection matrix hands back a block whose scaled Jacobian is negative throughout and whosevolumereads negative – a mesh no solver will take.mirrorpairs 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))])
mergewelds 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.
- nekmeshpy.hexmesh.morph.rotate(mesh, angle, axis=array([0., 0., 1.]), center=array([0., 0., 0.]))[source]¶
A new block rotated by
angleradians about the line throughcenteralongaxis(right-handed,axisneed not be normalized).
- nekmeshpy.hexmesh.morph.scale(mesh, factor, center=array([0., 0., 0.]))[source]¶
A new block scaled about
centerbyfactor– a scalar (uniform) or a(3,)per-axis vector. Every factor must be positive.
- 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.
- nekmeshpy.hexmesh.morph.transform_fn(mesh, fn)[source]¶
A new block with every node – the shared-face
QuadMeshall the way down, and the private per-hexinterior– mapped through an arbitraryfn;transform()for a warp that is not affine.fnis called once per node block with an(N, 3)array and must return(N, 3). Connectivity and tags ride through verbatim.fncan fold space; the result is checked and alogging.warning(not an error) is emitted when any hex comes out inverted – sampled on the stored map atmesh.order, a flag rather than a certificate, and no substitute forquality_summaryat the solver’s order. It does not re-wind a reflection; usemirror()for that.
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:
NamedTupleOne stated periodic pair: which face group maps onto which, and by what map.
tag_a/tag_bare face-tag names or explicit face id arrays, exactly asSeam’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.transformis anaffine.Affinepair(matrix, offset)carryingaontob– build it withaffine.translationoraffine.rotation, the same constructorshexmesh.translateandhexmesh.rotatetake. 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
attachcarries, 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.
- 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:
NamedTupleThe resolved correspondence: what a
'P 'boundary row needs.rowsis(2K, 4)of[element, local face 1-6, partner element, partner local face], lexsorted by(element, face). Both directions are in it – Nek wants aProw 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.worstis the largest residual left by any spec’s transform andtolwhat 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
- nekmeshpy.hexmesh.periodic.periodic_pairs(mesh, specs)[source]¶
The face-to-face correspondence every
Periodicinspecsstates, as the(2K,4)tableto_re2writes'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:
hexmesh.query¶
Read-only HexMesh queries – the operations that leave the ladder.
- class nekmeshpy.hexmesh.query.TagReport[source]¶
Bases:
NamedTupleHow 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 –
boundaryis 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. Ann_untagged_boundaryabove zero means the export has open sides no boundary condition covers; ann_tagged_interiorabove 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.
- nekmeshpy.hexmesh.query.face_rows(mesh, faces)[source]¶
((K,2) [element, local face 1-6] rows, (len(faces),) occurrence counts)– the inverse ofhexes, 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, socountsis 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
hexesface 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).
- 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 overface_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.
- nekmeshpy.hexmesh.query.bounds(mesh, *, high_order=False)[source]¶
The axis-aligned bounding box of the block’s nodes – corners only unless
high_order=Trueasks for the stored edge / face / interior nodes too. Seelinemesh.boundsfor why neither reading bounds the polynomial itself.
- 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 inquads, 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.idsoutside this mask are the tagged interior facestag_report()counts.
- 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 taggedface_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).
- nekmeshpy.hexmesh.query.classify_points(mesh, wall)[source]¶
Flag welded points:
(is_wall, is_fixed). Faces namedwallare wall; all other tagged faces are fixed. A point on both is treated as fixed.
- 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.re2actually 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.re2always 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:
- 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 asscaled_jacobian().Trilinear corners by default – which is what
.re2exports, since that format is linear at any order – andhigh_order=Truefor 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.
- nekmeshpy.hexmesh.query.is_conforming(mesh)[source]¶
Trueif the mesh has no hanging points (no T-junctions).
- nekmeshpy.hexmesh.query.is_overlap_free(mesh)[source]¶
Trueif no two elements geometrically overlap. Independent ofis_watertightandis_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 oftopology_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.
- nekmeshpy.hexmesh.query.is_watertight(mesh)[source]¶
Trueif the mesh boundary is a closed, leak-tight 2-manifold and the mesh is a single connected component. Does not imply conformity.
- 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 – seelinear_scaled_jacobian().
- 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 atorder.Unlike
corner_scaled_jacobian()(the special caseorder=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 –ordershould be that order (SCAN_ORDERby default, the same solver-order defaultorder_scanuses).
- 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.re2actually 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 wayreportwarns on anorder_scandisagreement without changing whatquality_summaryitself returns.- Parameters:
- Return type:
- 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.
- 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.
ordersamples that block on a finer GLL lattice than the mesh’s own – what a solver running atlx1 = orderdoes 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.
- 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.re2map atorder. SeeSolverMetrics.- Parameters:
- Return type:
- nekmeshpy.hexmesh.query.tag_report(mesh)[source]¶
Cross-check
face_tagsagainst the topological boundary (seeTagReport).
- 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_tagsis stored sparse and sorted, so this is itsidsfiltered by name. It is public becausehexmesh.attachworks 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.
- nekmeshpy.hexmesh.query.topology_report(mesh)[source]¶
Watertightness / connectivity report of the mesh.
- Parameters:
mesh (HexMesh)
- Return type:
- 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.
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_tagschanged, by whattagsis: 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 toNO_TAGuntags); a string names every element; aTagsor per-element sequence replaces the table. Geometry and the side tags are untouched.
- nekmeshpy.hexmesh.tag.retag_face(mesh, mapping)[source]¶
The same mesh with its
face_tagsrenamed throughmapping, rows kept in stored order (seeretag_element()for the map’s own rules).Order matters here beyond tidiness:
.re2writes boundary rows in it, and.vtugives a node touched by several rows the last one’s tag.Renaming a tag to
NO_TAGdrops 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_reportis what finds those.
- 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
mergeor a weld the thing you have is a set of faces, not a set of(hex, side)pairs – seeboundary_face_idsandface_tag_rowsfor the two ways to get them. (Its quad counterparttag_edgestakes(quad, side)rows instead, because the factories that use it genuinely think element-locally.)tagsis one name for all of them or one per face;NO_TAGnames nothing, and a face already named is overwritten.