quadmesh

class nekmeshpy.quadmesh.QuadMesh[source]

Bases: object

A quadrilateral surface / cross-section mesh in B-rep form.

EDGE_POINTS = array([[0, 1],        [1, 2],        [2, 3],        [3, 0]])
__init__(line_mesh, quads, orient, interior=None, element_tags=None)[source]

Construct from the B-rep directly: line_mesh (a LineMesh holding every shared edge – its points are the shared corners, its lines the shared edge connectivity, its interior the shared edge-interior HO nodes), quads (Q,4) edge indices into line_mesh.lines (CCW local edge order), orient (Q,4) bool (True where the quad traverses that edge anti-canonically), and interior (Q,(order-1)**2,3) private per-quad nodes (omit / None at order 1).

Parameters:
  • line_mesh (LineMesh)

  • quads (NDArray[int64])

  • orient (NDArray[bool])

  • interior (NDArray[float64] | None)

  • element_tags (Tags | None)

Return type:

None

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

Build a linear QuadMesh from corner points (P,3) + CCW quads (Q,4) connectivity – the corner -> B-rep bridge every factory routes through.

Parameters:
  • points (NDArray[float64])

  • quads (NDArray[int64])

  • element_tags (Tags | None)

  • order (int)

Return type:

QuadMesh

property edge_tags: Tags

The tags on the shared edges, over edge ids.

This is lines’ own element_tags read through, not a table of its own: rung N’s side tags are rung N-1’s element tags. An edge is one stored object every incident quad references, so naming it is naming that object – which is why there is no (quad, side) here to disagree with itself.

property order: int

Global polynomial order (1 = linear).

property points: NDArray[float64]

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

property corners: NDArray[int64]

(Q,4) CCW corner connectivity, derived (memoized) from the stored edge indices + flip. Read-only; the B-rep quads / flip are the source of truth.

property edges: NDArray[int64]

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

Type:

(Ne,2) unique undirected quad 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; a shared edge resolves to the same nodes from either incident quad.

property n_points: int

Number of (shared) points.

property n_quads: int

Number of quadrilaterals.

property n_edge_tags: int

Number of tagged edges.

property edge_group_tags: list[str]

Sorted unique tags of the tagged edges.

property element_group_tags: list[str]

Sorted unique non-empty per-quad element tags present on the section.

nekmeshpy.quadmesh.NO_TAG = ''

str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.

quadmesh.quality

Quad-element quality metrics.

nekmeshpy.quadmesh.quality.corner_scaled_jacobian(points, quads)[source]

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

Parameters:
  • points (NDArray[float64])

  • quads (NDArray[int64])

Return type:

NDArray[float64]

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

Per-quad minimum scaled Jacobian sampled at the (order+1)**2 GLL nodes of the curved element block, shape (Q,) – 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.quadmesh.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.quadmesh.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.quadmesh.quality.corner_summary(points, quads)[source]

Aggregate quality statistics for a quad mesh.

Parameters:
  • points (NDArray[float64])

  • quads (NDArray[int64])

Return type:

QualitySummary

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

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

Parameters:
Return type:

QualitySummary

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

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

Parameters:
  • points (NDArray[float64])

  • quads (NDArray[int64])

  • bins (int)

  • lo (float)

  • hi (float)

Return type:

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

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

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

Parameters:
Return type:

str

Operations

quadmesh.assemble

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

class nekmeshpy.quadmesh.assemble.Seam[source]

Bases: NamedTuple

One stated interface between two sections – the quad rung’s counterpart of hexmesh.Seam, naming edge groups.

a and b name a section by its position in attach’s meshes or by the section itself. tag_a / tag_b are edge-tag names or explicit edge-id arrays (tagged_edges).

a: int | QuadMesh

Alias for field number 0

tag_a: str | NDArray[int64]

Alias for field number 1

b: int | QuadMesh

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.quadmesh.assemble.attach(meshes, seams)[source]

Join sections along the edge groups each Seam names, in one pass – the quad rung’s hexmesh.attach, with the same contract one rung down.

No tolerance: within a named pair of groups the pairing is nearest-neighbour, proved by bijectivity rather than by a distance. The joined edges are cleared of their names unless the seam’s attach_tag names them, and own says whose coordinates the seam keeps.

Parameters:
Return type:

QuadMesh

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

The section split into its connected pieces – one QuadMesh per group of quads reachable through shared corner points, in the order their first quad appears.

Parameters:

mesh (QuadMesh)

Return type:

list[QuadMesh]

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

Loft a stack of conformal LineMesh profiles into a quad section (the general primitive behind extrude, and the middle rung of the uniform sweep shared with linemesh.assemble.loft and HexMesh.loft).

Parameters:
Return type:

QuadMesh

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

Loft a section from a parametrized family of profiles – 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:

QuadMesh

nekmeshpy.quadmesh.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 profiles, rather than blended straight between the two bounding a layer.

Same arguments, same numbering, same tags, and the profiles given come back verbatim as the levels – the spline interpolates them, so this adds curvature between profiles without moving any. It is the automatic form of loft(..., sweep_nodes=...): where that asks the caller for the intermediate profiles, this fits them. Every node block a profile stores is fitted, its interior as well as its points, so the result is curved along the sweep at every node rather than only at the corners.

Reach for it when a sweep has a feature that turns sharply across a handful of profiles: 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:

QuadMesh

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

Merge quad sections into one, welding coincident boundary points.

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(...).

The proximity join: told nothing about what meets what, it infers every seam from coordinates at one tolerance. attach() is told which edge group meets which and confines the search to those two.

Parameters:
Return type:

QuadMesh

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

Uniform H-refinement: split every quad into 4 – its own true center point plus the edge midpoints linemesh.refine already put on its shared line_mesh.

Exact at any order, the same way linemesh.refine is: the new 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, not a bilinear guess through the corners. An edge shared between two quads is refined through the one shared line_mesh exactly once, so both neighbours land on the identical midpoint automatically – no coincidence weld needed for that; the new per-quad center points are then welded/deduped implicitly too, since each is private to its own quad and never shared.

Quad q’s new center is point id linemesh.refine(mesh.line_mesh).n_points + q. Child 4*q + k (k = 0..3, the _CORNER_IJK[2][k] corner) is the quadrant nearer corner k. One call is one level.

Parameters:

mesh (QuadMesh)

Return type:

QuadMesh

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

The complement of select(): everything which does not name.

Parameters:
Return type:

QuadMesh

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

The named quads as a section of their own, renumbered from zero.

which is a tag string (every quad carrying it), a (Q,) boolean mask, or an array of quad ids. Kept quads hold their relative order, their element_tags and whichever edge_tags rows name them; edges and points nothing kept touches are dropped. The inverse of merge().

Removing elements can open the section up, so the result is not guaranteed to be simply connected – or connected at all. Ask components() if that matters.

Parameters:
Return type:

QuadMesh

quadmesh.lift

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

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

Ring O-grid filling the region between an inner and an outer closed loop – e.g. a circular body inside a square far-field box.

Parameters:
Return type:

QuadMesh

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

Sweep a LineMesh a distance length along axis into a quad section (the straight special case of loft).

Parameters:
  • line (LineMesh)

  • 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:

QuadMesh

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

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

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.lift.sweep(profile, 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 strip swept from one LineMesh profile along the curve path.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.lift.sweep_path(profile, 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, which carries its own analytic tangent and junction table – so this asks for an element length along the sweep instead of a station array.

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:

QuadMesh

quadmesh.shape

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

nekmeshpy.quadmesh.shape.box(half_sizes, n, *, patch_tags=None, element_tags=None, order=1)[source]

Closed box surface centred at the origin: six quad patches welded with merge. half_sizes is a scalar (cube) or (sx, sy, sz); n is a scalar or (nx, ny, nz) cells per axis.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.half_box(half_sizes, n, *, n_vertical=None, patch_tags=None, rim_tag='', element_tags=None, order=1)[source]

The upper half of a box: the five patches of the box surface that bound [-sx, sx] x [-sy, sy] x [0, sz], welded with merge and left open at the ``z = 0`` rim (the z_min patch is dropped).

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.half_ogrid(arc, spine, radial, *, center_scale=0.7, quadrant_scale=0.7, wall_tag='', element_tags=None)[source]

Structured half-circle O-grid over a half-disk split along the spine line (A1..A2); the wall arc ((4*Ntheta+1, 3), arc[0]=A1, arc[-1]=A2) is the open boundary. Built as two quadrant_ogrid quarters split at the arc’s own apex (its midpoint, directly opposite the spine) and merged there: the spine’s own [north caps, center fan, south caps] halves become the O -> A1 / O -> A2 seams verbatim (nothing resampled, same as the rest of spine), and a fresh O -> apex seam (via quadrant_seam_fractions) is the only new geometry. center_scale/quadrant_scale are as there.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.hemisphere(radius, n, *, n_vertical=None, element_tag='', rim_tag='', order=1)[source]

Cubed-hemisphere surface of radius sitting on the ground plane z = 0: a unit half_box projected radially onto the sphere. Untagged unless element_tag is given.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.ogrid(boundary, n_side, radial, *, center_scale=0.7, quadrant_scale=0.7, wall_tag='', element_tags=None)[source]

O-grid filling the closed boundary: four quadrant_ogrid quarters meeting at the loop’s centroid, split at the loop’s own quarter points. center_scale places each quarter’s own hub corner; quadrant_scale places the seam corner shared between neighbours (via quadrant_seam_fractions) – see there for what each one does.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.quadrant_core(arc, seam1, seam2, *, center_scale=0.5)[source]

The core quarter of a quadrant_ogrid as an (n+1, n+1, 3) grid, indexed [i][j] with i running O -> M1 along seam1 and j running O -> M2 along seam2.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.quadmesh.shape.quadrant_ogrid(arc, seam1, seam2, radial, *, center_scale=0.5, wall_tag='', element_tag='', side_tags=None)[source]

Quarter-disk O-grid: the 90-degree sibling of half_ogrid.

Untagged unless element_tag is given, like every other factory. Naming the quadrant itself is what lets a disc assembled from four of them hand a per-quadrant cap name to loft or a single patch name to hexmesh.tetra.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.quadrant_seam_fractions(n_side, radial, quadrant_scale=0.7)[source]

The normalized O -> A positions of the n_side+1 + Nradial seam points that quadrant_ogrid requires, ascending: the n_side+1 core fan, then the Nradial ring stations. The seam’s shared corner M sits at quadrant_scale * R along O -> A directly – an independent knob from quadrant_ogrid’s own center_scale, which places its hub corner K at center_scale * R along the arc’s bisector instead. Unless quadrant_scale is chosen to land M exactly on the chord between neighbouring quadrants’ K corners (quadrant_scale == center_scale * cos(45deg)), the merged core’s boundary bows into an octagon rather than sitting flush as a square.

Parameters:
  • n_side (int)

  • radial (int | NDArray[float64])

  • quadrant_scale (float)

Return type:

NDArray[float64]

nekmeshpy.quadmesh.shape.rectangle(corners, nx, ny, *, x_frac=None, y_frac=None, side_tags=None, element_tags=None, order=1)[source]

Structured quad grid over the rectangle with four CCW corners corners = [c0, c1, c2, c3]: nx cells along c0->c1 (bottom/top), ny along c1->c2 (left/right). x_frac / y_frac are read along c0->c1 / c1->c2; the opposite sides run the other way and take them mirrored, so grading stays aligned.

Parameters:
  • corners (NDArray[float64] | Sequence[NDArray[float64]])

  • nx (int)

  • ny (int)

  • x_frac (NDArray[float64] | None)

  • y_frac (NDArray[float64] | None)

  • side_tags (Mapping[str, str] | None)

  • element_tags (str | Tags | None)

  • order (int)

Return type:

QuadMesh

nekmeshpy.quadmesh.shape.sphere(radius, n, *, element_tag='', order=1)[source]

Closed cubed-sphere surface of radius about the origin: a unit box projected radially onto the sphere (same connectivity, so it pairs by index with a same-n box for HexMesh.annulus).

Untagged unless element_tag is given, like every other factory: a shape does not name itself, because the name belongs to the model the caller is assembling.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.spine_fractions(n_theta, radial, quadrant_scale=0.7)[source]

The normalized A1 -> A2 positions of the 2*n_theta+1 + 2*Nradial spine points that half_ogrid and spined_ogrid require, in ascending order: Nradial north caps, the 2*n_theta+1 center fan, then Nradial south caps (see half_ogrid for what each region is). quadrant_scale plays the same role here as it does in quadrant_seam_fractions: it is the fraction of each half (O -> A1 and O -> A2) given to the center fan before the Nradial O-ring caps take over.

Parameters:
  • n_theta (int)

  • radial (int | NDArray[float64])

  • quadrant_scale (float)

Return type:

NDArray[float64]

nekmeshpy.quadmesh.shape.spined_ogrid(boundary, radial, *, spine=None, center_scale=0.7, quadrant_scale=0.7, wall_tag='', element_tags=None)[source]

Full-disk O-grid over a closed boundary split along a spine diameter into two half_ogrid halves (each in turn built from two quadrant_ogrid quarters) welded along the spine – the clean way to O-grid a disk that has a natural A1..A2 seam (a saddle-split vessel or pipe cross-section), so the caller need not hand-roll the arc split and merge. center_scale/quadrant_scale are as in quadrant_ogrid/ quadrant_seam_fractions.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.structured(edges, *, side_tags=None, element_tags=None)[source]

Transfinite (Coons-patch) quad grid over the surface bounded by four open edge lines edges = [bottom, right, top, left] in CCW loop order. The lines must share corners (form a closed loop).

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.tri_patch(surface, ab, bc, ca, *, order=1, tip_bias=0.3333333333333333, mids=None, element_tag='')[source]

The curved triangle bounded by three surface curves, meshed as the three quadrilateral patches about an interior tip – the all-quad split of a triangle, and the shape HexMesh.tetra wants for the curved side of a curvilinear tetrahedron.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.shape.tri_patch_tip(u_ab, u_bc, u_ca, *, tip_bias=0.3333333333333333)[source]

Where tri_patch() puts its tip, in surface parameters, given the three curves’ middle nodes.

Parameters:
  • u_ab (NDArray[float64])

  • u_bc (NDArray[float64])

  • u_ca (NDArray[float64])

  • tip_bias (float)

Return type:

NDArray[float64]

quadmesh.ports

A cross-section together with the two facts a bare section cannot state about itself: which way it faces and where its axis is.

nekmeshpy.quadmesh.ports.NORMAL_TOL = 1e-12

How far normal may stray from unit length before Port refuses it.

class nekmeshpy.quadmesh.ports.Port[source]

Bases: object

An open end of a meshed component: its cross-section, the outward direction, the axis point, and the nominal radius.

section: QuadMesh

The cross-section itself.

normal: NDArray[float64]

Unit vector pointing out of the component, along which a connector leaves.

center: NDArray[float64]

The axis point the section was built about – deliberately not the centroid, which an O-grid’s grading shifts slightly off it.

radius: float

Nominal radius, for checking that two ports being joined are the same size.

reversed()[source]

The same section faced the other way – for the end of a component that is about to be continued into rather than out of.

Return type:

Port

faces(other)[source]

Whether the two ports point at each other, rather than the same way or apart. The check hexmesh.bridge cannot make from geometry alone.

Parameters:

other (Port)

Return type:

bool

__init__(section, normal, center, radius)
Parameters:
  • section (QuadMesh)

  • normal (NDArray[float64])

  • center (NDArray[float64])

  • radius (float)

Return type:

None

nekmeshpy.quadmesh.ports.port(section, *, outward, center=None, radius=None)[source]

A Port from a section plus the side that faces out.

Parameters:
Return type:

Port

quadmesh.lower

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

nekmeshpy.quadmesh.lower.boundary_mesh(mesh, tag=None)[source]

A section’s boundary as a LineMesh, carrying the section’s own nodes.

Parameters:
Return type:

LineMesh

quadmesh.morph

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

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

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

Parameters:
Return type:

list[QuadMesh]

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

A new section reflected through the plane with normal through point, with every quad re-wound so it keeps the orientation it was authored with.

A reflection has determinant -1, so it inverts every element: the bare transform of a reflection matrix hands back a section whose scaled Jacobian has flipped sign throughout. mirror pairs the coordinate map with the winding fix, so the result measures exactly as its original did and lifts to a hex block the right way out.

The symmetric-domain idiom – mesh the half you can, recover the whole: quadmesh.merge([half, mirror(half, normal=(1, 0, 0))]), which welds along the symmetry plane the two now share.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.morph.offset(mesh, distance, crease=np.float64(0.5235987755982988))[source]

A new section displaced by distance along its own surface normal – the cross product of the two parametric tangents (computed from the underlying GLL nodes, so high-order interior nodes follow the same rule as corners). Consistent CCW winding across the section gives every quad’s normal the same sign, so no reference direction is needed.

At a point shared by more than one quad (a corner, or a shared-edge node), the offset direction is the average of every incident quad’s own normal at that node, renormalized – so a quad’s private interior node (touched by no other quad) is simply displaced by its own element’s normal.

This is the building block for skinning: loft a section and its offset copy to get a thin, perpendicular boundary-layer hex shell.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.morph.place_on_path(section, path, fractions, *, origin=None, orientation=None, up=None, twist=0.0, close_twist=True, normal=None, loop=False)[source]

Where sweep would put section at each of fractions, without building the block: one rigidly placed copy per station, through the same frames.sweep_placements the sweep itself uses – orientation and up included, so a path carrying its own frame places the same way here as it sweeps.

Parameters:
Return type:

list[QuadMesh]

nekmeshpy.quadmesh.morph.reindex(structure, target, sigma)[source]

target’s own geometry, reached through structure’s own index labels: point i takes target’s point sigma[i], and every shared-edge and per-quad interior node follows its relabelled corners.

Parameters:
Return type:

QuadMesh

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

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

Parameters:
Return type:

QuadMesh

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

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

Parameters:
Return type:

QuadMesh

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

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

Parameters:
Return type:

QuadMesh

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

A new section with every node – shared corners, shared edge interiors and private face interiors alike – 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.

Unlike the affine transform(), fn can fold space; this checks the result and emits a logging.warning (not an error) when any element comes out inverted. The check signs each element against its own corners’ mean normal, so it catches a local fold – a strip winding the opposite way from its neighbours – but not a global reflection, which flips every element together and reads clean; that one mirror() is for, and transform_fn never re-winds. A curved fn keeps a curved section curved; it moves the stored nodes, it does not add any, so a straight-subdivided section stays faceted between its corners.

Parameters:
Return type:

QuadMesh

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

A new section shifted rigidly by vector (3,). Bit-exact: the offset is added without a matmul, so translating by 0 returns the identical coordinates – which is what lets extrude place its slices through here.

Parameters:
Return type:

QuadMesh

quadmesh.query

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

nekmeshpy.quadmesh.query.angle_defect(mesh)[source]

(P,) discrete Gaussian curvature of every vertex by the angle defect, read off the mesh with each quad split along its shorter diagonal: 2 pi - sum(angles) at an interior vertex, pi - sum(angles) at a boundary one (the turning of the boundary). The integral of the Gaussian curvature over the vertex’s dual cell, to the mesh’s own resolution.

The quads are triangulated first on purpose: a non-planar quad’s four corner angles sum to less than 2 pi, so defects read off the raw corners do not add up to 2 pi chi; those of a triangle mesh always do (see gauss_bonnet_residual()).

Parameters:

mesh (QuadMesh)

Return type:

NDArray[float64]

nekmeshpy.quadmesh.query.gauss_bonnet_residual(mesh)[source]

sum(angle_defect) - 2 pi chi with chi = V - E + F: zero (to rounding) for any triangulated surface, with or without boundary. It does not measure quality – every mesh satisfies it – it catches a corrupted topology or an angle bug, and it must hold before and after a smoothing round.

What it can and cannot see. On a closed mesh the residual is exactly 2 pi (E - 2Q), so it is non-zero only when the edge count is inconsistent with a closed surface: it jumps by 2 pi for every non-manifold fin and by pi for a quad that names one vertex twice. On a mesh with a boundary it also fires on a pinch vertex (pinch_vertices()). It is blind to an unwelded seam (the cut mesh is still a valid surface with a boundary), to reversed winding, to self-intersection, to a collapsed edge, to holes and to isolated vertices – check_connectivity() is the check for the first of these, not this one.

Parameters:

mesh (QuadMesh)

Return type:

float

nekmeshpy.quadmesh.query.pinch_vertices(mesh)[source]

Ids of the pinch vertices: points whose incident quads do not form one connected fan.

Around a vertex each quad is a wedge between its two edges at that vertex; two wedges are neighbours when they share an edge. A manifold vertex has one fan (a closed ring inside, an open one on the boundary); a vertex where two patches, or two sides of a folded sheet, touch has two or more. Neither the gap check, the edge counts nor is_conforming sees it – every edge is still used once or twice – but the boundary angle-defect formula assumes one fan, so gauss_bonnet_residual does fire. Cost: one union-find over 4Q wedges.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[int64]

class nekmeshpy.quadmesh.query.ConnectivityReport[source]

Bases: NamedTuple

The preflight gap check of one mesh (read-only; nothing is welded or moved).

A gap is a group of stored points that gencon’s relative-tolerance rule would fuse while the mesh keeps them apart – an open seam or a duplicated vertex; gap_points are their ids and n_gap_clusters the number of groups. A group only counts when its members are directly within connectivity_tol of the local length of each other and no two of them are corners of one quad. Otherwise (gencon’s rule also chains, so on a tangled mesh vertices that merely sit close are linked, or a vertex lies on top of its quad’s other corner) it is counted in n_chained_clusters: a possible tangle, not a seam, whose fix is untangling and not welding.

n_points: int

Alias for field number 0

n_quads: int

Alias for field number 1

n_components: int

Alias for field number 2

connectivity_tol: float

Alias for field number 3

n_gap_clusters: int

Number of gap clusters (see the class docstring for what counts as a gap).

gap_points: NDArray[int64]

Alias for field number 5

n_gap_on_boundary: int

Of those, how many sit on the boundary (an open seam) vs inside (a duplicated vertex).

worst_gap_in_lengths: float

Largest separation inside a gap cluster, in units of that cluster’s local length.

n_chained_clusters: int

Alias for field number 8

n_nonmanifold_edges: int

Alias for field number 9

n_degenerate_quads: int

Alias for field number 10

n_collapsed_edges: int

Alias for field number 11

n_pinch_vertices: int

Alias for field number 12

passed: bool

Alias for field number 13

notes: tuple[str, ...]

Alias for field number 14

static __new__(_cls, n_points, n_quads, n_components, connectivity_tol, n_gap_clusters, gap_points, n_gap_on_boundary, worst_gap_in_lengths, n_chained_clusters, n_nonmanifold_edges, n_degenerate_quads, n_collapsed_edges, n_pinch_vertices, passed, notes)

Create new instance of ConnectivityReport(n_points, n_quads, n_components, connectivity_tol, n_gap_clusters, gap_points, n_gap_on_boundary, worst_gap_in_lengths, n_chained_clusters, n_nonmanifold_edges, n_degenerate_quads, n_collapsed_edges, n_pinch_vertices, passed, notes)

Parameters:
  • n_points (int)

  • n_quads (int)

  • n_components (int)

  • connectivity_tol (float)

  • n_gap_clusters (int)

  • gap_points (IntArray)

  • n_gap_on_boundary (int)

  • worst_gap_in_lengths (float)

  • n_chained_clusters (int)

  • n_nonmanifold_edges (int)

  • n_degenerate_quads (int)

  • n_collapsed_edges (int)

  • n_pinch_vertices (int)

  • passed (bool)

  • notes (tuple[str, ...])

nekmeshpy.quadmesh.query.check_connectivity(mesh, *, connectivity_tol=0.2)[source]

Preflight gap check: run gencon’s relative-tolerance vertex rule over the mesh’s own corner coordinates and report every pair of stored points it would fuse but the mesh keeps apart (an open seam, a duplicated vertex), plus non-manifold edges, degenerate quads and the component count. Read-only – fixing a gap is a separate, explicit act.

A mesh the toolkit built is conformal by construction, so on its own output this only reports what a coordinate-based weld would still find (a seam closer than connectivity_tol of the local size that merge left open). It cannot see a leak between two distinct objects that are welded: that needs the component and non-manifold lines, and the user’s eye on the picture.

Parameters:
Return type:

ConnectivityReport

class nekmeshpy.quadmesh.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.quadmesh.query.area(mesh, *, high_order=False)[source]

Total surface area – element_areas() summed.

The wetted area of a block’s wall is this over the wall’s own mesh: quadmesh.area(hexmesh.boundary_mesh(block, "wall")).

Parameters:
Return type:

float

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

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

Parameters:
Return type:

Bounds

nekmeshpy.quadmesh.query.boundary_edge_ids(mesh)[source]

(n_edges,) mask of the shared edges carried by exactly one quad.

The edge-id form of boundary_edges(), and the cheaper one: the edges are already deduplicated in line_mesh, so this is a bincount over the incidence. It is how a name is checked against the topology it was meant for – edge_tags.ids outside this mask are the tagged interior edges tag_report() counts.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[bool]

nekmeshpy.quadmesh.query.boundary_edges(mesh)[source]

(K,2) array of [quad id, local edge (1-4)] for every edge on the section boundary (borne by a single quad).

Parameters:

mesh (QuadMesh)

Return type:

NDArray[int64]

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

Sorted unique quad ids with at least one edge on the section boundary.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[int64]

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

Sorted unique point ids lying on the section boundary.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[int64]

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

The area-weighted centroid integral x dA / integral dA – the mass property, not the mean of the points.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.quadmesh.query.edge_rows(mesh, edges)[source]

((K,2) [quad, local edge 1-4] rows, (len(edges),) occurrence counts) – the inverse of quads, for the given shared-edge ids; the quad rung’s hexmesh.face_rows.

A boundary edge is carried by one quad and yields one row, an interior one by two and yields two, so counts is how a caller expands a per-edge value onto the rows it produced.

Parameters:
  • mesh (QuadMesh)

  • edges (NDArray[int64])

Return type:

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

nekmeshpy.quadmesh.query.edge_tag_rows(mesh)[source]

((K,2) [quad, local edge 1-4] rows, their tags) for every named edge, lexsorted by (quad, edge) – edge_rows() run over edge_tags.ids; the quad rung’s hexmesh.face_tag_rows.

Parameters:

mesh (QuadMesh)

Return type:

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

nekmeshpy.quadmesh.query.element_areas(mesh, *, high_order=False)[source]

(Q,) surface area of each quad, integrated over the element as it sits in 3-D (so a warped quad is measured as the ruled surface it is, not as a projection).

Bilinear corners by default; high_order=True integrates the curved element the mesh stores. A curved area is not a polynomial – the integrand carries a square root – so that reading is a quadrature approximation converging with the order, where hexmesh.element_volumes is exact.

Parameters:
Return type:

NDArray[float64]

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

(Q, (order+1)**2, 3) – each quad’s own node block, assembled natively from the B-rep: shared corners, then the shared edge-interior nodes in element traversal order, then the private per-quad interiors, each written at its lattice slot. Nothing is resampled or deduplicated.

The quad rung of linemesh.element_blocks, one dimension up.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[float64]

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

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

Parameters:

mesh (QuadMesh)

Return type:

bool

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

True if no two quads geometrically overlap. Independent of is_watertight() and is_conforming(), which are purely topological (facet-sharing) checks, and computed only when asked for, as at the hex rung.

Parameters:

mesh (QuadMesh)

Return type:

bool

nekmeshpy.quadmesh.query.overlapping_quads(mesh)[source]

Ids of the quads that overlap another quad – the which behind is_overlap_free().

A branched star (one quad covering its neighbours) can have every signed Jacobian positive, so the inverted-quad count cannot see it; this does. Each pair is tested in the first quad’s own mean plane, so quads on a curved surface are read locally. Cost: a KD-tree over centroids, then a half-plane test of 9 sample points per candidate pair.

Parameters:

mesh (QuadMesh)

Return type:

NDArray[int64]

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

True if the mesh boundary is a closed curve and the mesh is a single connected component. Does not imply conformity.

Parameters:

mesh (QuadMesh)

Return type:

bool

nekmeshpy.quadmesh.query.plane_normal(mesh, *, hint=None, check=True)[source]

The unit normal of the plane a planar section lies in: the smallest right singular vector of its centred points, i.e. the exact least-squares plane.

Parameters:
Return type:

NDArray[float64]

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

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

Parameters:
Return type:

QualitySummary

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

Human-readable summary: element/point counts, scaled-Jacobian quality (curved, and on the solver-order lattice), per-name tagged-edge counts cross-checked against the boundary (tag_report()), and the topology report – the quad rung’s hexmesh.report. There is no corner-only reading and no linear one: .re2 is hex-only, so nothing here is exported flat.

Parameters:

mesh (QuadMesh)

Return type:

str

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

Per-quad minimum scaled Jacobian (n_quads,), 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.quadmesh.query.tag_report(mesh)[source]

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

Parameters:

mesh (QuadMesh)

Return type:

TagReport

nekmeshpy.quadmesh.query.tagged_edges(mesh, tag)[source]

The shared-edge ids carrying tag, ascending – the quad rung’s counterpart of hexmesh.tagged_faces, and what quadmesh.attach pairs.

A tag that names nothing raises rather than returning an empty group.

Parameters:
Return type:

NDArray[int64]

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

Watertightness / connectivity report of the mesh.

Parameters:

mesh (QuadMesh)

Return type:

TopologyReport

quadmesh.tag

Vocabulary-only QuadMesh 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.quadmesh.tag.retag_edge(mesh, mapping)[source]

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

Renaming a tag to NO_TAG drops its rows rather than storing an empty name: a side-tag table is a named subset, so leaving it is leaving the table. That is how a boundary name that has stopped meaning anything is retired.

Parameters:
Return type:

QuadMesh

nekmeshpy.quadmesh.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:

QuadMesh

nekmeshpy.quadmesh.tag.tag_edges(mesh, rows, tags)[source]

The same mesh with the shared edge each (quad, side) row names given its tag.

The authoring bridge. A factory knows its geometry element-locally – “side 1 of the outermost ring is the wall” – while storage is one tag per shared edge, so the row is resolved through mesh.quads[quad, side - 1] and the tag written there. What changes is only where it lands: two rows that name the same edge from either side of it no longer become two entries that could disagree, and the later row wins.

tags is one name for all the rows or one name per row, as hexmesh.tag_faces takes them.

Rows tagged NO_TAG name nothing, so a partly-tagged row block can be handed over whole.

Parameters:
Return type:

QuadMesh