core¶
core.mesh¶
Generic shared-point unstructured mesh – the gmsh/meshio-style data model.
- class nekmeshpy.core.mesh.Mesh[source]¶
Bases:
objectShared-point mesh model:
points(P,3)plus a{cell_type: connectivity}dict, namedpoint_setsandcell_sets, and gmshfield_data. Serves as the meshio bridge.
core.boundary_condition¶
Named boundary conditions: a face tag name and its Nek5000 BC code.
- class nekmeshpy.core.boundary_condition.BoundaryConditions[source]¶
Bases:
objectBoundary names to Nek BC codes, one table per solved field.
velocityis the momentum field and is required.temperatureis the second field Nek’s.re2carries once a mesh has a solid region (nelgv < nelgt); it covers every element, and a name left out of it stays conformal ('E '). Each table maps a name to aBCSpec.Each entry carries a
cbcand aboundaryID(seeSide). Within one table each is all-or-none: state it for every boundary or for none, and a table that states none takes the defaults. A table’sboundaryIDset is its own – velocity’s and temperature’s are separate – and runs1..kwithout a gap, though several boundaries may share one..vtupaints it asbc_id; a table that states none is painted bytag_of()instead, so its boundaries still differ.There are deliberately no presets here. A name-to-code table is a statement about one piece of geometry – which opening is the inlet, which surface is a measurement plane – so it belongs in the mesher that knows, next to the tags it names, where a reader meets it at the same time as the mesh. A built-in
nek_default()put the carotid’s own vocabulary in the toolkit and let two other examples inherit a mapping neither of them stated.Periodicity is not stated here: a
'P 'code needs a correspondence between two named face groups, whichperiodic=supplies to the writer.- tag_of(field, name)[source]¶
The 1-based position of
nameinfield’s own table: the id a viewer paints when the table states noboundaryID.
- numbered(field)[source]¶
Whether
field’s table statesboundaryID(for every boundary, since it is all-or-none).
- side(field, name, region)[source]¶
(cbc, boundaryID)fieldwrites for a row of boundarynameowned by an element inregion;Nonemeans “write no row from this side”.A name with one
Sidereads the same from everywhere. One with per-region entries must name every region it actually borders, since a missing key is far more likely a typo than an intent to drop the face.
core.topology¶
Mesh topology / validity checks, decoupled from the mesh containers.
- class nekmeshpy.core.topology.TopologyReport[source]¶
Bases:
NamedTupleFacet inventory and validity verdicts for an all-hex mesh.
- watertight: bool¶
Truewhen the boundary is a closed, non-empty 2-manifold. Says nothing about conformity: a T-junction interface still reads watertight.
- static __new__(_cls, n_elements, n_faces, n_boundary_faces, n_internal_faces, n_nonmanifold_faces, n_open_edges, n_hanging_points, n_components, watertight, conformal)¶
Create new instance of TopologyReport(n_elements, n_faces, n_boundary_faces, n_internal_faces, n_nonmanifold_faces, n_open_edges, n_hanging_points, n_components, watertight, conformal)
- nekmeshpy.core.topology.hex_report(points, hexes)[source]¶
Topology / watertightness report for an all-hex mesh.
- Parameters:
points (NDArray[float64])
hexes (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.is_watertight(points, hexes)[source]¶
Trueif the all-hex mesh’s boundary is a closed 2-manifold.- Parameters:
points (NDArray[float64])
hexes (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.count_overlapping_pairs(points, hexes)[source]¶
Number of hex pairs whose volumes geometrically overlap (see
_count_overlapping_pairs) – independent of, and not part of,hex_report(): unlike watertight/conformal (a fixed-size facet-incidence scan), this is a geometric broad-then-narrow-phase search whose candidate count can run into the hundreds of thousands on a large mesh, so it is not folded into the fast report everyis_watertight()/hexmesh.is_conformingcall already pays for – call it explicitly where the extra work is wanted (a summary, typically, alongside those two).- Parameters:
points (NDArray[float64])
hexes (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.is_overlap_free(points, hexes)[source]¶
Trueif no two hexes geometrically overlap (seecount_overlapping_pairs()).- Parameters:
points (NDArray[float64])
hexes (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.quad_report(points, quads)[source]¶
Topology / watertightness report for an all-quad mesh –
hex_report()one rung down: the facets are edges, the boundary is a curve, and the boundary’s own “open edge” is an open point (a boundary point not shared by exactly two boundary edges), which is whatTopologyReport.n_open_edgescounts here.- Parameters:
points (NDArray[float64])
quads (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.overlapping_quads(points, quads)[source]¶
Sorted ids of the quads that take part in at least one overlapping pair (see
count_overlapping_quad_pairs()): the which behind its count.- Parameters:
points (NDArray[float64])
quads (NDArray[int64])
- Return type:
NDArray[int64]
- nekmeshpy.core.topology.count_overlapping_quad_pairs(points, quads)[source]¶
Number of quad pairs whose areas geometrically overlap – independent of, and not part of,
quad_report(), for the same cost reason ascount_overlapping_pairs().- Parameters:
points (NDArray[float64])
quads (NDArray[int64])
- Return type:
- nekmeshpy.core.topology.is_overlap_free_quads(points, quads)[source]¶
Trueif no two quads geometrically overlap (seecount_overlapping_quad_pairs()).- Parameters:
points (NDArray[float64])
quads (NDArray[int64])
- Return type:
- class nekmeshpy.core.topology.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.core.topology.surface_report(points, tris)[source]¶
Topology report for a triangle surface mesh.
- nekmeshpy.core.topology.is_closed(points, tris)[source]¶
Trueif the surface is a closed 2-manifold.- Parameters:
points (NDArray[float64])
tris (NDArray[int64])
- Return type:
core.quality¶
The shared schema of the per-rung scaled-Jacobian quality summaries.
- nekmeshpy.core.quality.POOR_THRESHOLD = 0.2¶
Scaled Jacobian below which an element is counted as poor. Part of the public schema: it names both the
n_poorfield and thepoor (<...)report line.
- nekmeshpy.core.quality.SCAN_ORDER = 7¶
The order a report double-checks the mesh at. This is the polynomial order the solver will run it at, which is the only reading that predicts anything, so there is no universal right value – change it to match your solver.
- nekmeshpy.core.quality.SCAN_BUDGET = 15000000¶
Sampled points a default
order_scanmay spend, summed over its orders. The work isn_elements * (order+1)**dim, and atSCAN_ORDERthat is 512 points a hex – so a mesh too big for the cap is reported as not checked rather than quietly costing minutes inside a routine report. Raise it (or callorder_scandirectly) when you want the answer anyway.
- class nekmeshpy.core.quality.OrderScan[source]¶
Bases:
NamedTupleWhat the stored map looks like read on lattices finer than the mesh’s own.
An element’s Jacobian is sampled, exact at its
(order+1)**dimGLL nodes and silent between them, so a cleanQualitySummaryat the mesh’s own order is not a certificate. This is the same map at more points – what a solver running at its own polynomial order computes.
- class nekmeshpy.core.quality.SolverMetrics[source]¶
Bases:
NamedTupleThe three numbers Nek5000/nekRS print as
mesh metrics:, of the trilinear (.re2) map read at one polynomialorder.:attr:`jac_ratio` is not a scaled Jacobian in :class:`QualitySummary`’s sense. nekRS’s line reads
scaled Jacobianbut computesmin(J)/max(J)of the Jacobian determinant over an element’s GLL nodes – how uniform the map is, blind to skew, where the angle-based metric is blind to a size gradient. The two agree on a cube and on nothing else, so0.08here and0.22there are the same mesh.- aspect_min: float¶
Longest over shortest of an element’s 12 corner-to-corner edges – smallest, largest, mean.
- static __new__(_cls, order, spacing_min, spacing_max, jac_ratio_min, jac_ratio_max, jac_ratio_mean, aspect_min, aspect_max, aspect_mean)¶
Create new instance of SolverMetrics(order, spacing_min, spacing_max, jac_ratio_min, jac_ratio_max, jac_ratio_mean, aspect_min, aspect_max, aspect_mean)
- class nekmeshpy.core.quality.QualitySummary[source]¶
Bases:
NamedTupleAggregate scaled-Jacobian statistics over one mesh’s elements.
- n_poor: int¶
Elements below
POOR_THRESHOLD– includesn_inverted.
core.measure¶
Metric measures over an element’s node block: extent, size, centroid.
The rung-agnostic kernel behind linemesh.length / quadmesh.area /
hexmesh.volume and the bounds / centroid each rung spells the same way.
Everything here takes a node block – (E, (order+1)**dim, 3) in the
lexicographic lattice order the element_blocks queries return – so the same
quadrature serves the corner (linear) and the curved order-N reading of a mesh, and
nothing in this module knows a container.
- class nekmeshpy.core.measure.Bounds[source]¶
Bases:
NamedTupleThe axis-aligned bounding box of a point set –
min/maxcorners, with the derived readings (size,center,diagonal) that callers actually ask for.- min: NDArray[float64]¶
Per-axis minimum
(3,).
- max: NDArray[float64]¶
Per-axis maximum
(3,).
- property size: NDArray[float64]¶
Per-axis extent
max - min(3,).
- property center: NDArray[float64]¶
The box centre – the midpoint of the extremes, not a centroid.
- static __new__(_cls, min, max)¶
Create new instance of Bounds(min, max)
- Parameters:
min (Point)
max (Point)
- nekmeshpy.core.measure.bounds_of(points)[source]¶
The bounding box of
points(any leading shape, trailing axis the 3 components). Raises on an empty set: a box around nothing has no corners.- Parameters:
points (NDArray[float64])
- Return type:
- nekmeshpy.core.measure.centroid_of(blocks, dim)[source]¶
The measure-weighted centroid
integral x d(Omega) / integral d(Omega)of the elementsblocksdescribes – the mass-property centroid, not the mean of the nodes.- Parameters:
blocks (NDArray[float64])
dim (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.measure.corner_blocks(points, conn, dim)[source]¶
(E, 2**dim, 3)node blocks of the straight-sided elementsconnnames – the order-1 block of a mesh read at its corners only, whatever order it stores.- Parameters:
points (NDArray[float64])
conn (NDArray[int64])
dim (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.measure.integrate(blocks, dim, *, moments=False)[source]¶
(measure (E,), moment (E,3))– each element’sintegral d(Omega)and, whenmomentsis asked for, itsintegral x d(Omega)(zeros otherwise).At
dim=3the measure is the signed volume integral of the isoparametric map, so an inverted hex contributes negatively and a wholly mis-oriented mesh reads negative – the same sign convention as the scaled Jacobian. A length (dim=1) or a surface area (dim=2) embedded in 3-D has no such sign and comes back non-negative.
core.stations¶
Where a sweep’s stations are, and what lays out across them.
Rung-agnostic by nature: a station is a parameter along the sweep, and a level is the
copy of the slice sitting at one. Validating a sweep’s fractions, refining them onto
the node lattice, sampling a path at them, and laying a slice’s entity table out across
them are all that one subject, which belongs to no rung in particular – every loft /
sweep / extrude on the ladder reads from here.
- class nekmeshpy.core.stations.Sampled[source]¶
Bases:
ProtocolWhat an evaluated sweep needs of a slice, at any rung. The three containers share no base class, so this is structural – it names the reads, nothing more.
- property points: NDArray[float64]¶
- __init__(*args, **kwargs)¶
- nekmeshpy.core.stations.at_levels(table, level, per_level)[source]¶
tablelaid out at everylevelof a sweep –level * per_level + table, level-major, keepingtable’s trailing shape:(n_level,) x (k, ...) -> (n_level * k, ...).Every id a sweep writes is one of these: a section point / edge / quad id, shifted by how many of that entity one level owns. The
loftbodies above pick the table and the levels; this is the layout they share.- Parameters:
table (NDArray[int64])
level (NDArray[int64])
per_level (int)
- Return type:
NDArray[int64]
- nekmeshpy.core.stations.check_fraction_count(fr, *, loop, name)[source]¶
Raise unless
frcarries enough fractions for at least one sweep layer (two, withloop=True– the last fraction is the wrap back onto the first profile rather than a level of its own).
- nekmeshpy.core.stations.refined_lattice(fractions, order)[source]¶
The
n*order + 1parameter positions of every node of the order-orderchain graded byfractions(n = len(fractions) - 1elements): elementi’s nodeasits atfr[i] + g[a]*(fr[i+1] - fr[i])for the GLL nodesgon[0, 1], and the chain ends atfr[-1].- Parameters:
fractions (NDArray[float64])
order (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.stations.spline_levels(blocks, t, *, loop)[source]¶
Resample a per-level node block onto the sweep lattice through a cubic spline in the level index, rather than a straight blend between the two bounding levels.
blocksis any node array stacked over the levels –(n_lev,) + rest– and the levels are taken to sit at0, 1, ... n_lev-1, which is exactly whereloft’s own layers put them. So evaluating attreturns levelkverbatim at integert = k: the spline interpolates, it does not approximate, and a sweep resampled this way still passes through every profile it was given.This is what a plain
loftcannot do. Its sweep-direction interior nodes are a straight blend of the two profiles bounding their layer, so the surface is linear between profiles however high the order is – the trap the module docs call “high order in storage, linear in geometry”. A spline sees the neighbouring profiles too, so a station’s high-order nodes bend the way the stack bends, and a feature that turns sharply across a few profiles is resolved instead of being cut across.loopcloses the spline periodically, so the seam layer is no less smooth than any other – an open spline would leave the one place a closed sweep cannot show a joint. Fewer than three levels carry no curvature to fit, and fall back to the straight blend.- Parameters:
blocks (NDArray[float64])
t (NDArray[float64])
loop (bool)
- Return type:
NDArray[float64]
- nekmeshpy.core.stations.sweep_lattice(fractions, order, *, loop, name)[source]¶
Validate a sweep’s
fractionsand return(fr, node lattice).
- nekmeshpy.core.stations.split_evaluated(profs, order, *, loop, conn, noun, elems, name)[source]¶
Validate a sweep whose slices were evaluated on the refined node lattice rather than handed in, and split them into
(slices, sweep_nodes).Every rung above the line does this identically – one slice per lattice value, all index-paired with the first, the wrap level dropped on a loop – so the only things that vary are what to compare (
conn, which doubles as the element count) and what to call the pieces in the errors (noun/elems). Those nouns are not decoration: they are what the rungs’ own tests match on.The lattice itself is not an argument: the slices are the sweep, one per lattice value by construction at every caller, so passing the parameters alongside them only offered a second thing to disagree with. What remains is the one property the list must have on its own – a whole number of layers of
orderlevels each – and positions are reported as level indices, which every caller shares, rather than as parameter values, which only an evaluated sweep has.
core.affine¶
Affine maps behind the rung-preserving translate / rotate / scale.
- nekmeshpy.core.affine.ORIGIN: NDArray[float64] = array([0., 0., 0.])¶
The world origin – default centre for
rotation()/scaling().
- nekmeshpy.core.affine.Z_AXIS: NDArray[float64] = array([0., 0., 1.])¶
The
+zaxis – default rotation axis, matching the sweep factories’axis=.
- nekmeshpy.core.affine.apply(P, matrix, offset)[source]¶
Map every coordinate of
P(any leading shape, trailing axis 3) through the affine pair, returning a new array of the same shape. Withmatrix=Nonethis is the exact translationP + offset.- Parameters:
P (NDArray[float64])
matrix (NDArray[float64] | None)
offset (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.affine.scaling(factor, center=array([0., 0., 0.]))[source]¶
The affine map that scales about
centerbyfactor– a scalar (uniform) or a(3,)per-axis vector. A zero or negative component is rejected: it would collapse or invert every element.
- nekmeshpy.core.affine.rotation(angle, axis=array([0., 0., 1.]), center=array([0., 0., 0.]))[source]¶
The affine map that rotates by
angleradians about the line throughcenterwith directionaxis(right-handed;axisneed not be normalized).
- nekmeshpy.core.affine.reflection(normal, point=array([0., 0., 0.]))[source]¶
The affine map that mirrors through the plane with unit
normal(need not be normalized) passing throughpoint– the Householder matrixI - 2 n n^T.Its determinant is
-1, so it turns every element inside out: this is the mapmirrorapplies to the coordinates, paired with a re-winding of the connectivity. Handing it totransformalone leaves an inverted mesh.
core.frames¶
Moving orthonormal frames along a sampled curve – the placement half of a sweep.
- nekmeshpy.core.frames.SEGMENT_TOL: float = 1e-12¶
Segments shorter than this fraction of the curve’s overall extent are treated as repeated points. Relative, so the check is scale-free: a curve modelled in metres and the same curve in millimetres are rejected (or accepted) identically.
- nekmeshpy.core.frames.PARALLEL_TOL: float = 1e-12¶
A tangent whose component along
upexceeds this in magnitude leaves too little ofupin the cross-section plane forfixed_up()to normalize meaningfully; at1 - 1e-12the surviving perpendicular component is ~1.4e-6 ofup, already at the edge of what double precision resolves in a direction.
- nekmeshpy.core.frames.PLANAR_TOL: float = 1e-09¶
A profile point this far off its own best-fit plane, relative to the profile’s overall extent, is not a planar cross-section. Relative, so a section modelled in metres and the same section in millimetres are judged identically; loose enough to pass a section assembled from several factories, tight enough that a genuinely bowed profile is caught rather than sheared.
- nekmeshpy.core.frames.CURVATURE_TOL: float = 1e-10¶
Below this the unit-tangent turning per sample is indistinguishable from round-off and
frenet()has no principal normal to speak of. Absolute, because the quantity it bounds –|t[i+1] - t[i-1]| / 2– is already dimensionless.
- nekmeshpy.core.frames.tangents(points, *, loop=False)[source]¶
Unit tangents
(K,3)of the(K,3)sampled curvepoints.- Parameters:
points (NDArray[float64])
loop (bool)
- Return type:
NDArray[float64]
- nekmeshpy.core.frames.fixed_up(tangents, up)[source]¶
(K,3,3)frames from Gram-Schmidt of a constant worldupagainst each unit tangent:u = normalize(up - (up.w) w),v = w x u,w =the tangent.
- nekmeshpy.core.frames.parallel_transport(points, tangents, up0, *, loop=False, distribute=True)[source]¶
(K,3,3)rotation-minimizing frames alongpoints, seeded byup0.
- nekmeshpy.core.frames.spin(u, tangents, angles)[source]¶
Rotate each cross-section vector
u[k]byangles[k]abouttangents[k], right-handed about the tangent.umust already be perpendicular to its tangent; the result is renormalized, not reprojected.- Parameters:
u (NDArray[float64])
tangents (NDArray[float64])
angles (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.frames.holonomy(points, tangents, up0)[source]¶
The residual twist, in radians, of transporting a frame once around the closed curve
points(the closing segmentpoints[K-1] -> points[0]included).
- nekmeshpy.core.frames.frenet(points, tangents)[source]¶
(K,3,3)Frenet-Serret frames:uthe principal normal,vthe binormal,wthe tangent.- Parameters:
points (NDArray[float64])
tangents (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.frames.frame_transform(R_from, origin_from, R_to, origin_to)[source]¶
The single rigid
(matrix, offset)carrying the frame(R_from, origin_from)onto(R_to, origin_to), in the convention ofnekmeshpy.core.affine.apply()–p @ matrix.T + offset.- Parameters:
R_from (NDArray[float64])
origin_from (NDArray[float64])
R_to (NDArray[float64])
origin_to (NDArray[float64])
- Return type:
tuple[NDArray[float64], NDArray[float64]]
- nekmeshpy.core.frames.plane_frame(points, *, normal=None, origin=None, hint=None)[source]¶
The local frame of a planar cross-section: the
(R, origin)pair naming the plane the profile was authored in, ready to hand toframe_transform()as the_fromhalf of a sweep.
- nekmeshpy.core.frames.sweep_placements(profile_points, path_points, *, orientation='transport', up=None, twist=0.0, close_twist=True, loop=False, origin=None, normal=None, path_tangents=None)[source]¶
One rigid
(matrix, offset)per path station, carrying a planar profile from its own plane onto the moving frame of the sampled curvepath_points(K,3).
core.paths¶
A declarative 3-D turtle-walk path builder.
A path is a move table – line(), arc(), helix() – walked by
walk() into a Path. The turtle carries a full orthonormal frame,
not just a heading, which is what makes the vocabulary three-dimensional: tilt rolls
a bend’s own plane about the direction of travel, so one arc verb reaches every
direction out of the current frame, and roll spins the frame about that direction as
the move travels. Every move starts at the previous one’s end point and keeps its
frame, so a walk is C1 by construction – no fillet fitting, no corner rounding.
Each move is a constant screw, closed-form in arc length: exact total length (no
quadrature), an exact analytic tangent (differencing a sampled path is only O(h**2), and
worst exactly at a junction where the curvature jumps), and an s in [0, 1] that is
true arc length, so an element length is a station count.
The frame the walk carried comes back on Path.up, and
hexmesh.sweep_path holds it per station
unless told otherwise – which is the only way an out-of-plane bend or a distributed
twist can reach the mesh, since a sweep’s own generators fix the frame only up to how
the path itself was rolled.
- class nekmeshpy.core.paths.Arc[source]¶
Bases:
NamedTupleA circular bend of
radiusthroughangledegrees, in the planetiltdegrees round from the frame’s own up.- tilt: float¶
0 turns left, 180 right, 90 pitches up, anything between is oblique.
- Type:
Degrees the bend plane is rolled about the heading
- class nekmeshpy.core.paths.Helix[source]¶
Bases:
NamedTupleA constant screw: a bend of
radiusthroughangledegrees while advancingriseper full turn along the bend axis.
- nekmeshpy.core.paths.LENGTH_TOL = 1e-12¶
A move shorter than this carries no length to distribute a roll over, and would put two junctions on one
s. Absolute rather than relative to the walk: the walk’s own extent is not known until every move has been read, and the quantity is a length that has to be divided by, so what matters is that it is above the reciprocal’s noise.
- class nekmeshpy.core.paths.Line[source]¶
Bases:
NamedTupleA straight run of
length, spinning the framerolldegrees as it goes.
- class nekmeshpy.core.paths.Path[source]¶
Bases:
NamedTupleA curve in space, exposed as continuous callables of normalized arc length
s in [0, 1]rather than as its own segment table – whathexmesh.lift.sweep_pathconsumes.walk()builds one; so can a caller with a parametrization of their own, which is why every field is a callable and onlyupmay be left out.- tangent: Callable[[NDArray[float64]], NDArray[float64]]¶
(K,)in[0, 1]->(K, 3)unit tangents, the analytic derivative ofcenterline. A direction: no origin enters it.
- break_fractions: NDArray[float64]¶
Normalized
sof every junction between moves, strictly increasing.
- up: Callable[[NDArray[float64]], NDArray[float64]] | None¶
(K,)in[0, 1]->(K, 3)unit vectors perpendicular totangent: the cross-section reference axis the path itself carries, so a sweep along it needs noorientation/upof its own.Noneon a path that names no frame, which leaves the sweep to build one.
- static __new__(_cls, centerline, tangent, total_length, break_fractions, up=None)¶
Create new instance of Path(centerline, tangent, total_length, break_fractions, up)
- Parameters:
centerline (Callable[[FloatArray], PointArray])
tangent (Callable[[FloatArray], PointArray])
total_length (float)
break_fractions (FloatArray)
up (Callable[[FloatArray], PointArray] | None)
- nekmeshpy.core.paths.arc(radius, angle, *, tilt=0.0, roll=0.0)[source]¶
A bend of
radiusthroughangledegrees –tiltpicks which way it bends (0 left, 180 right, 90 up),rolltwists the section as it travels.
- nekmeshpy.core.paths.helix(radius, angle, *, rise=0.0, tilt=0.0, roll=0.0)[source]¶
A screw of
radiusthroughangledegrees, climbingriseper full turn along the axistiltnames.
- nekmeshpy.core.paths.line(length, *, roll=0.0)[source]¶
A straight run of
length, twistingrolldegrees over its own length.
- nekmeshpy.core.paths.resolve_frame(path, orientation, up)[source]¶
The
(orientation, up)a sweep alongpathshould run with.A path built by
walk()carries its own frame, and with noorientationasked for that frame is the answer – held per station, which is what"fixed"means. An explicitorientation(or an explicitup) overrides it, because a caller who names a generator is asking for that generator’s frame, not the path’s.
- nekmeshpy.core.paths.sample_up(up, t)[source]¶
Evaluate a callable
upon the station parameterst, and pass anything else through untouched – the one place a path’s own frame becomes the(K,3)fieldframes.sweep_placementsconsumes.
- nekmeshpy.core.paths.walk(moves, *, start=(0.0, 0.0, 0.0), heading=(1.0, 0.0, 0.0), up=(0.0, 0.0, 1.0))[source]¶
Walk
movesin space fromstart, setting out alongheadingwith the cross-section’s own up alongup, into aPaththat carries its own moving frame.upis normalized and projected offheading– it names the frame’s phase, not a length, so only its component across the direction of travel is used, and only a seed parallel toheadingis refused. From there the moves steer: a positive angle turns toward the walk’s left (up x heading),tiltrolls that bend plane about the heading (90 pitches up), androllspins the frame right-handed about the heading over the move’s own length.
core.surfaces¶
Curves carried as their parametrization on a surface, rather than as points.
- class nekmeshpy.core.surfaces.SurfaceCurve[source]¶
Bases:
NamedTupleA curve living in a surface’s parameter domain.
- g: Callable[[NDArray[float64]], NDArray[float64]]¶
(K,)curve parameters ->(K, 2)surface parameters. Vectorized.
- fr: NDArray[float64]¶
The curve-parameter values of this curve’s own nodes, one per node. May run descending – that is how
reverse()traverses a curve the other way, and whatloft_fnaccepts a descending sequence for.
- static __new__(_cls, g, fr)¶
Create new instance of SurfaceCurve(g, fr)
- Parameters:
g (Callable[[FloatArray], FloatArray])
fr (FloatArray)
- nekmeshpy.core.surfaces.blend(a, b, lam)[source]¶
The two curves interpolated in parameter space at
lam– so the result is still exactly on the surface, which a point-space lerp would not be.- Parameters:
a (SurfaceCurve)
b (SurfaceCurve)
lam (float)
- Return type:
- nekmeshpy.core.surfaces.curve(g, fr)[source]¶
A
SurfaceCurvefrom a parametrization and its node parameters.- Parameters:
- Return type:
- nekmeshpy.core.surfaces.node(c, i)[source]¶
The
(2,)surface parameters of nodei.- Parameters:
c (SurfaceCurve)
i (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.surfaces.reparam(c, pa, pb)[source]¶
The straight
pa -> pbparameter segment, but expressed onc’s own parameter domain and carryingc’s own nodes – so the two can beblend()-ed station by station.- Parameters:
c (SurfaceCurve)
- Return type:
- nekmeshpy.core.surfaces.reverse(c)[source]¶
The same curve traversed the other way, by reversing its node parameters.
- Parameters:
c (SurfaceCurve)
- Return type:
- nekmeshpy.core.surfaces.ruled(pa, pb, n)[source]¶
The straight segment from
patopbin parameter space, withnelements evenly spaced in the parameter, on the domain[0, 1].
- nekmeshpy.core.surfaces.segment(c, i0, i1)[source]¶
The piece of
cbetween nodesi0andi1, as a parametrization on[0, 1]– ready for a Coons patch, which wants plain callables.- Parameters:
c (SurfaceCurve)
i0 (int)
i1 (int)
- Return type:
Callable[[NDArray[float64]], NDArray[float64]]
- nekmeshpy.core.surfaces.shift(c, delta)[source]¶
The same curve with a constant offset added in parameter space.
- Parameters:
c (SurfaceCurve)
- Return type:
core.fields¶
Mesh-sizing fields (gmsh-style) and 1-D point distributions.
- class nekmeshpy.core.fields.Field[source]¶
Bases:
objectBase sizing field:
field(points) -> sizesmapping(k,3)points toktarget edge sizes.
- class nekmeshpy.core.fields.AxisLinearField[source]¶
Bases:
FieldSize varies linearly along one axis from
size0atc0tosize1atc1(clamped outside).
- class nekmeshpy.core.fields.DistanceField[source]¶
Bases:
FieldSize grows from
size_nearat the given points tosize_farbeyonddist_far(linear ramp on distance to the nearest source point).
- class nekmeshpy.core.fields.MinField[source]¶
Bases:
FieldPointwise minimum of several fields (the finest constraint wins).
- nekmeshpy.core.fields.geometric_spacing(n, ratio=1.0)[source]¶
n+1normalized positions in[0,1]with a geometric size ratio between consecutive cells (ratio==1-> uniform).
- nekmeshpy.core.fields.uniform_spacing(n)[source]¶
n+1uniformly spaced positions in[0, 1]; shorthand forgeometric_spacing(n, 1.0).- Parameters:
n (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.fields.symmetric_spacing(n, ratio=1.0)[source]¶
n+1positions in[0,1]clustered toward both ends: geometric spacing per half, mirrored (nmust be even;ratio==1-> uniform).
- nekmeshpy.core.fields.validate_layers(positions)[source]¶
Normalize a layer specification to the flattened array of normalized layer positions every sweep / fill factory consumes.
- Parameters:
positions (int | NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.fields.gll_nodes(order)[source]¶
The
order+1Gauss-Lobatto-Legendre nodes mapped to[0, 1], ascending.- Parameters:
order (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.fields.gll_weights(order)[source]¶
The
order+1GLL quadrature weights on[0, 1](sum to 1), matchinggll_nodes(). Used by the (deferred) order-N quality metric.- Parameters:
order (int)
- Return type:
NDArray[float64]
- nekmeshpy.core.fields.lagrange_matrix(nodes, eval_pts)[source]¶
The
(len(eval_pts), len(nodes))matrix of Lagrange basis values: rowiholdsL_k(eval_pts[i])for each nodek.M @ finterpolates nodal valuesftoeval_pts; each row sums to 1 (partition of unity).- Parameters:
nodes (NDArray[float64])
eval_pts (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.fields.lagrange_derivative_matrix(nodes, eval_pts)[source]¶
The
(len(eval_pts), len(nodes))matrix of Lagrange basis derivatives: entry(i, k)isL'_k(eval_pts[i]).- Parameters:
nodes (NDArray[float64])
eval_pts (NDArray[float64])
- Return type:
NDArray[float64]
core.selection¶
Resolving a select / remove argument to a mask over a mesh’s elements.
- nekmeshpy.core.selection.Selection¶
one tag name, several tag names, a boolean mask over the elements, or element ids.
- Type:
A
select/removeargument
alias of
str|Sequence[str] |NDArray[str_] |NDArray[bool] |NDArray[int64] |Sequence[int]
- nekmeshpy.core.selection.mask_for_selection(which, tags)[source]¶
The
(n_elements,)boolean mask aselect/removeargument names.whichis a tag name or a list / array of them (resolved byTags.flatnonzero; a name absent from the mesh’s vocabulary is an error, since a silent empty selection is almost always a typo), a ready(n_elements,)boolean mask, or an array of element ids.
core.smoother¶
The rung-agnostic smoother engine behind quadmesh.smoother and hexmesh.smoother.
core.smoother.constraints¶
Smoother constraints on points: what each vertex may do, and how the target geometry holds it.
Rung-agnostic: nothing here knows a quad from a hex. A smoother never guesses. Every vertex gets one
mobility – FREE, ON_SURFACE, ON_CURVE, TANGENT or FROZEN – resolved once from the
constraints the caller states (resolve, in each rung, which owns the connectivity that needs), and the two
operations the smoothing methods share act on it:
snap()projects constrained vertices back onto their target (x += w (P(x) - x);w = 1is the full projection) and restores frozen ones;constrain_direction()removes from a gradient or a step whatever the target forbids – the component along the surface normal, everything but the tangent on a curve, all of it on a frozen vertex – using the target’s own normal, never the mesh’s.
Strict geometry mode (the default) is both, every step.
Precedence is FROZEN > ON_CURVE > ON_SURFACE > TANGENT > FREE; a vertex claimed by two surfaces,
or by two curves (a corner), is frozen and listed in the report – it is never given an
order-dependent projection implicitly. Unclaimed boundary vertices are frozen by default,
so a block’s seam cannot drift (boundary="free" opens them, "tangent" lets them move along the boundary only).
- nekmeshpy.core.smoother.constraints.CORNER_DEG = 30.0¶
The turning angle, in degrees, above which a boundary vertex is a corner. It is frozen by
resolve(..., boundary="tangent"), and frozen, ending its chains, byboundary="slide".
- nekmeshpy.core.smoother.constraints.Select¶
A constraint names its vertices as point ids, a boolean mask, or an edge-tag name (the points of every edge carrying it – “this tagged loop lies on this curve”).
- class nekmeshpy.core.smoother.constraints.Surface[source]¶
Bases:
objectA target surface:
project(P)the closest point,normal(P)the unit normal at that foot point, both(N, 3) -> (N, 3).
- class nekmeshpy.core.smoother.constraints.Curve[source]¶
Bases:
objectA target curve:
project(P)the closest point,tangent(P)the unit tangent at that foot point.
- nekmeshpy.core.smoother.constraints.plane(point, normal)[source]¶
The plane through
pointwithnormal.- Parameters:
point (NDArray[float64])
normal (NDArray[float64])
- Return type:
- nekmeshpy.core.smoother.constraints.sphere(center, radius)[source]¶
The sphere of
radiusaboutcenter; the normal points outward.
- nekmeshpy.core.smoother.constraints.line(point, direction)[source]¶
The straight line through
pointalongdirection.- Parameters:
point (NDArray[float64])
direction (NDArray[float64])
- Return type:
- nekmeshpy.core.smoother.constraints.circle(center, radius, axis=(0.0, 0.0, 1.0))[source]¶
The circle of
radiusaboutcenterin the plane normal toaxis; the tangent runs counter-clockwise aboutaxis.
- class nekmeshpy.core.smoother.constraints.Tangent[source]¶
Bases:
objectMoves only along the boundary: a boundary vertex whose move is cut down to the tangent of the circle through it and its two boundary neighbours (
circle_tangent(), from the current positions). Nothing pulls it back onto a curve – that isOnCurve. A corner (seeboundary_corners()) or a vertex without two boundary neighbours is frozen instead; a vertex that is not on the boundary is an error.
- class nekmeshpy.core.smoother.constraints.OnSurface[source]¶
Bases:
objectStays on
surface.slide=Truemoves tangentially then re-projected;slide=Falseprojects once onto the surface and then holds still. Without asurfaceit is fitted: the input quads touching these vertices, as a triangle surface (surface_from_trimesh()).
- class nekmeshpy.core.smoother.constraints.OnCurve[source]¶
Bases:
objectStays on
curve, moving along it only whenslide=True. Without acurveit is fitted: the boundary chains through these vertices, as meshed, are cut at corners (boundary_corners()) and at boundary vertices not named here, and each piece gets a spline through it (spline_curve(), closed for a loop); a corner is frozen.
- class nekmeshpy.core.smoother.constraints.KeepLayer[source]¶
Bases:
objectKeep a boundary layer exactly as meshed: every vertex within
ringsquad-rings ofseeds(the interface or wall vertices,seedsincluded) is frozen, so the layer’s thickness, its growth ratio and its tangential spacing cannot change. The layers keep their geometry exactly at the price of not sliding; one ring protects the first row of cells off the seeds, two the first two. A square-seeking metric will otherwise inflate a thin layer (measured on a blanket-module section: edges next to an interface grew up to 34x).
- class nekmeshpy.core.smoother.constraints.SlideColumns[source]¶
Bases:
objectRigid columns that slide together along
direction: every column is a set of vertices (e.g. the rows across a wall’s thickness) with one shared degree of freedom, so they all move by the same amount alongdirection– the cells between the rows stay rectangles while the spacing along the wall adapts. Each column also stays on its own straight line. If any member of a column ends up frozen (a corner, a vertex that must not leave the boundary), the whole column is frozen: a rigid unit cannot move with one member pinned.
- class nekmeshpy.core.smoother.constraints.Resolved[source]¶
Bases:
NamedTuplePer-vertex mobility, resolved once.
kind[i]isFREE/ON_SURFACE/ON_CURVE/FROZEN/TANGENT;target[i]indexessurfacesorcurvesfor the first two;anchoris where a frozen vertex lives (the input position, or its one-time projection forslide=False).- kind: NDArray[int64]¶
Alias for field number 0
- target: NDArray[int64]¶
Alias for field number 1
- anchor: NDArray[float64]¶
Alias for field number 2
- notes: tuple[str, ...]¶
Human-readable notes on every conflict and how it was resolved, never silently.
- tie: NDArray[int64]¶
tie[i]is the rigid column vertexibelongs to (-1none);tie_dir[i]its slide direction.
- tie_dir: NDArray[float64]¶
Alias for field number 7
- nbr: NDArray[int64]¶
nbr[i]the two boundary neighbours of aTANGENTvertex (-1for every other vertex).
- bfaces: NDArray[int64]¶
(F, 4)the outward boundary faces a hexTANGENTvertex with nonbrslides in the tangent plane of (empty on a quad mesh, whoseTANGENTvertices all have two boundary neighbours).
- static __new__(_cls, kind, target, anchor, surfaces, curves, notes, tie, tie_dir, nbr, bfaces)¶
Create new instance of Resolved(kind, target, anchor, surfaces, curves, notes, tie, tie_dir, nbr, bfaces)
- nekmeshpy.core.smoother.constraints.circle_tangent(xp, x, xn)[source]¶
Unit tangent at
xof the circle throughxp,x,xn(rows), orientedxp -> xn.With
a = xp - xandc = xn - xthe tangent at the middle point of the circumscribed circle is|a|^2 c - |c|^2 a: perpendicular to the radius, in the plane of the three points, exact on a circle and second-order on uneven spacing. Collinear points give the edge direction; the degenerate casexp == xn(no direction at all) returns a zero row.- Parameters:
xp (NDArray[float64])
x (NDArray[float64])
xn (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.constraints.turning_deg(P, ids, nb, normals=None)[source]¶
Turning angle (degrees) of the boundary at
ids, whose neighbours arenb(n, 2). Withnormals((len(P), 3), e.g. the area-weighted vertex normals of the mesh) both edges are first projected onto the tangent plane at the vertex, so the turn is the one within the surface and its own bending does not count.- Parameters:
P (NDArray[float64])
ids (NDArray[int64])
nb (NDArray[int64])
normals (NDArray[float64] | None)
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.constraints.boundary_corners(P, adj, corner_deg=30.0, normals=None)[source]¶
The corners of the boundary graph
adj(vertex -> neighbours along it): a vertex without exactly two neighbours, or one whose turn is a jump – abovecorner_degand above three times the mean turn of its two neighbours. The second test is what separates a corner from a coarsely sampled smooth curve: on a circle ofNpoints every vertex turns360/N, which can be far abovecorner_deg, but its neighbours turn about the same (a 3:1 ellipse on 12 points peaks at 2.4), so the ratio is near 1; at a real corner the neighbours (edge interiors) turn little, so the ratio is large. (A closed loop of only four points is such a circle.)normalsmakes the turns geodesic, seeturning_deg().
- nekmeshpy.core.smoother.constraints.spline_curve(X, closed, name='boundary')[source]¶
A smooth
Curvethrough the ordered pointsX(2-D or 3-D): a cubic spline in the chord-length parameter, periodic whenclosedand not-a-knot otherwise (a straight line for two points). The foot point is the nearest of a dense sampling refined by a few Newton steps on the spline itself, kept only where it lowers the distance, so a projection never ends farther than the sample it started from.
- nekmeshpy.core.smoother.constraints.snap(points, r, weight=1.0)[source]¶
pointswith every constrained vertex movedweightof the way to its target (1= onto it) and every frozen vertex put back at its anchor. A new array.
- nekmeshpy.core.smoother.constraints.constrain_direction(d, points, r)[source]¶
d(P, 3)(a gradient or a step) with the forbidden part removed, using the target’s own normal/tangent at each vertex’s foot point. A new array.- Parameters:
d (NDArray[float64])
points (NDArray[float64])
r (Resolved)
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.constraints.vertex_length(points, corners)[source]¶
(P,)the shortest edge at each vertex – the length unit every step, finite difference and tolerance of the optimizer is measured in.- Parameters:
points (NDArray[float64])
corners (NDArray[int64])
- Return type:
NDArray[float64]
- class nekmeshpy.core.smoother.constraints.OnTargetReport[source]¶
Bases:
NamedTupleThe geometry self-check of one state: how far constrained vertices sit from their targets and how far frozen ones moved, in absolute units and in local edge lengths.
- static __new__(_cls, n_checked, max_distance, mean_distance, max_distance_in_edges, max_frozen_move, tol, passed)¶
Create new instance of OnTargetReport(n_checked, max_distance, mean_distance, max_distance_in_edges, max_frozen_move, tol, passed)
- nekmeshpy.core.smoother.constraints.on_target_report(points, corners, r, *, tol=1e-06)[source]¶
Distance of every
ON_SURFACE/ON_CURVEvertex to its target, and the largest displacement of a frozen vertex from its anchor; passes when the former is withintollocal edge lengths and the latter is exactly0(totolof the edge).- Parameters:
- Return type:
- nekmeshpy.core.smoother.constraints.surface_from_trimesh(points, tris, *, k=8, name='trimesh')[source]¶
A
Surfaceread off a triangle mesh – a CAD tessellation or a scanned wall.projectreturns the closest point on thektriangles whose centroids are nearest the query (exact point-triangle distance within that candidate set);normalblends the area-weighted vertex normals with the barycentric weights of that foot point, so it is continuous across triangles. It is an approximation of the true surface: the triangles’ chord error remains, sharp edges are smoothed over by the blended normal, and akthat is too small on a very graded tessellation can miss the nearest triangle. Cost: one KD-tree query andktriangle distances per point.
- nekmeshpy.core.smoother.constraints.claimed_surfaces(r)[source]¶
(vertex ids, surface)for every surface that holds vertices inr– the groupssurface_normalsreads its normals from.
core.smoother.metric¶
The smoother’s shape metric, its objective and analytic gradient, for any element dimension.
Every reading is taken off the corner Jacobian J = [e_1 ... e_d] of an element corner – the d edge
vectors leaving it, right-handed – so one function serves quads (d = 2) and hexes (d = 3) and the rung
only builds the edge vectors from its connectivity and scatters the gradient back onto its vertices.
Signed scaled Jacobian (s = 1 only for d orthogonal edges, s <= 0 inverted or degenerate):
s = det J / (|e_1| ... |e_d|)
Shape metric (Knupp’s mean-ratio measure to the power d; phi >= 1, 1 only for a square or cube corner):
phi = (|J|_F^2 / d)^d / det J^2 = K / s^2, K = ((sum |e_i|^2) / d)^d / prod |e_i|^2
phi is blind to orientation, so inversion is a separate signed penalty kappa max(0, eps - s). The
barrier-free phi_delta = K c / s_delta^2 (s_delta = (s + sqrt(s^2 + 4 delta^2)) / 2, c = s_delta(1)^2)
removes the wall at a zero Jacobian that a line search cannot cross; delta = 0 is the exact metric. The
objective is the sum over elements of the corner mean (1 / 2^d) of w (phi - 1) + kappa max(0, eps - s);
a vertex’s gradient involves only its incident elements, so it does not depend on the element count.
A quad is a surface in 3-D with 2-D topology: det J is read through a reference normal n per element,
det J = (e_1 x e_2) . n (normal=), so a mesh on a curved surface is never assumed planar. For d = 2
the exact metric is evaluated as (A + B)^2 / (4 |e_1 x e_2|^2) and for d = 3 as K / s^2; the two
are the same function, written the way each is best conditioned, and the tests check them against the
single determinant formula above.
- nekmeshpy.core.smoother.metric.KAPPA = 1000.0¶
Inversion-penalty weight per corner, on the sum objective (see module docstring).
- nekmeshpy.core.smoother.metric.EPS = 0.05¶
Scaled-Jacobian threshold below which a corner is penalised.
- nekmeshpy.core.smoother.metric.DELTA = 0.01¶
Barrier-removal width (in units of the signed sine
s) the optimizer uses to untangle:phiblows up as a corner’s Jacobian goes through zero, so a plainphiis a wall between an inverted corner and a valid one that no line search crosses. Knupp’s remedy replacessbys_d = (s + sqrt(s^2 + 4 d^2)) / 2, positive and finite for everysand decreasing smoothly through zero.delta = 0is the exact metric.
- class nekmeshpy.core.smoother.metric.CornerMetrics[source]¶
Bases:
NamedTuplePer-corner readings, each
(n_elements, corners_per_element)in the element’s own corner order.- phi: NDArray[float64]¶
phi >= 1,1at a square / cube corner; blind to orientation.
- sj: NDArray[float64]¶
Signed scaled Jacobian in
[-1, 1];<= 0is inverted or degenerate.
- static __new__(_cls, phi, sj)¶
Create new instance of CornerMetrics(phi, sj)
- Parameters:
phi (FloatArray)
sj (FloatArray)
- nekmeshpy.core.smoother.metric.regularised(s, delta)[source]¶
(s_d, c, ds_d/ds): the barrier-free sine, the constants_d(1)^2that makes a perfect corner score exactly 1, and the derivative ofs_dwith respect tos.
- nekmeshpy.core.smoother.metric.scatter(n, index, values)[source]¶
Sum
values (n_elements, k, 3)ontonvertices byindex (n_elements, k)– the vertex assembly a shared-id mesh needs no gather-scatter library for.- Parameters:
n (int)
index (NDArray[int64])
values (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.metric.corner_metrics(edges, normals=None, *, delta=0.0)[source]¶
phiand the signed scaled Jacobian at every corner.edgesis thededge vectors leaving each corner, each(n_elements, corners, 3);normals(n_elements, 3)is required ford = 2.delta > 0returns the barrier-freephi_dthe optimizer descends on; the default is the exact metric the report prints.- Parameters:
- Return type:
- nekmeshpy.core.smoother.metric.corner_gradient(edges, normals=None, *, kappa=1000.0, eps=0.05, delta=0.0, weights=None)[source]¶
dF/d e_jfor every edge vector, each(n_elements, corners, 3)(reference normals held fixed): the gradient with respect to thededge vectors leaving each corner. The corner’s own vertex receives minus their sum and each edge’s far end its own term –assemble_gradient()does that with the rung’s connectivity.
- nekmeshpy.core.smoother.metric.objective_value(m, dim, *, kappa=1000.0, eps=0.05, weights=None)[source]¶
F = sum_e (1/2^d) sum_c [ w_e (phi - 1) + kappa max(0, eps - s) ]for the readingsm–0only for an all-square (cube), all-positive mesh.weights(n_elements,)scale the shape term per element (default 1):0makes an element’s shape irrelevant while the inversion penalty still protects it. Divide by the element count for the mean the report shows.- Parameters:
m (CornerMetrics)
dim (int)
kappa (float)
eps (float)
weights (NDArray[float64] | None)
- Return type:
- nekmeshpy.core.smoother.metric.evaluate(m, dim, *, kappa=1000.0, eps=0.05, weights=None)[source]¶
(F, n_inverted)from one set of corner readings: the objective and the number of elements with a corner whose signed scaled Jacobian is<= 0.
- nekmeshpy.core.smoother.metric.assemble_gradient(n_points, corner_index, far_index, grads)[source]¶
The vertex gradient
(n_points, 3)from per-corner edge gradients:corner_index(n_elements, k)the vertex at each corner,far_index[j]the vertex at the far end of edgejof that corner,grads[j]fromcorner_gradient(). The far end receivesgrads[j], the corner itself-sum_j grads[j].
core.smoother.quality¶
Quality readings of a smoothed mesh, from per-corner and per-element numbers any rung can supply.
The rung builds the corner metrics (nekmeshpy.core.smoother.metric), its edge lengths and its
neighbour table; the reductions here turn them into the numbers a report prints. The rung keeps its own
Quality tuple (its element count is named n_quads / n_hexes) and fills it from reduce_corners();
ShapeStats has the same fields at every rung and is shared.
- class nekmeshpy.core.smoother.quality.ShapeStats[source]¶
Bases:
NamedTupleEdge- and size-based readings that complement a rung’s
Quality: aspect ratio (longest over shortest edge of an element), node spacing (the shortest and longest edge anywhere), corner angles, and the size jump between neighbouring elements (size_ratio_*: the larger over the smaller of the two characteristic sizes of every pair of elements sharing an edge or face, so1is a perfectly graded mesh). The scaled Jacobian does not see that last one at all.- static __new__(_cls, aspect_max, aspect_p95, spacing_min, spacing_max, angle_min_deg, angle_max_deg, size_ratio_max, size_ratio_p95, size_ratio_mean)¶
Create new instance of ShapeStats(aspect_max, aspect_p95, spacing_min, spacing_max, angle_min_deg, angle_max_deg, size_ratio_max, size_ratio_p95, size_ratio_mean)
- nekmeshpy.core.smoother.quality.history_row(q)[source]¶
(n_inverted, sj_min, objective_mean)of a rung’sQuality– what a stage’s history logs.
- nekmeshpy.core.smoother.quality.with_scaled_jacobian(q, sj)[source]¶
A rung’s
Qualityq(a corner reading) with its scaled-Jacobian fields – inverted count, min, mean, median and the 1% / 5% / 10% quantiles – replaced by those of the per-element valuessj(n,): how a curved mesh is read on the nodes it stores. The shape metricphiand the objective stay the corner reading (the metric is defined on corners).
- nekmeshpy.core.smoother.quality.reduce_corners(m, objective)[source]¶
The fields of a rung’s
Qualityfrom corner readingsmand the objectiveF, in order: element count, inverted count, then the worst-corner scaled Jacobian’s min / mean / median / 1% / 5% / 10% quantiles, the worst-cornerphi’s mean and max, andFper element. Per-element values are the worst corner (sjits minimum,phiits maximum).
- nekmeshpy.core.smoother.quality.failing_mask(m, edge, *, sj_target=0.05, short_edge=0.1)[source]¶
(n_elements,)the elements a local stage should work on: a corner with signed scaled Jacobian belowsj_target, or an edge (edge(n_elements, k)lengths) shorter thanshort_edgetimes the median.- Parameters:
m (CornerMetrics)
edge (NDArray[float64])
sj_target (float)
short_edge (float)
- Return type:
NDArray[bool]
- nekmeshpy.core.smoother.quality.corner_angles(u, v)[source]¶
Angle in degrees between the edge vectors
uandv((n, k, 3)each) at every corner.- Parameters:
u (NDArray[float64])
v (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.quality.pair_ratios(size, pairs)[source]¶
(M,)larger-over-smaller ratio of the element sizessizefor every neighbour pair inpairs(M, 2).- Parameters:
size (NDArray[float64])
pairs (NDArray[int64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.quality.shape_stats(edge, angles, ratios)[source]¶
ShapeStatsfromedge(n, k)lengths, cornerangles(degrees, any shape) and the neighbour sizeratios(pair_ratios()).- Parameters:
edge (NDArray[float64])
angles (NDArray[float64])
ratios (NDArray[float64])
- Return type:
core.smoother.schedule¶
Smoothing stages, schedules and the method registry, shared by every rung.
A stage is one smoothing method run once and described by a StageLog. A schedule is an
ordered list of (method, knobs). Each rung owns one Registry – the names its
run_schedule knows (laplace, optimize, …) – and a third party plugs another backend in with
register under a new name. The three preset schedules are the same at every rung.
- class nekmeshpy.core.smoother.schedule.StageLog[source]¶
Bases:
NamedTupleWhat one smoothing stage did – the part of the round report this stage owns.
- history: tuple[tuple[int, int, float, float], ...]¶
Per pass
(pass, n_inverted, sj_min, objective_mean)of the accepted iterates.
- points: NDArray[float64] | None¶
The vertex positions after this stage, only when
run_schedule(..., record=True)asked for them.
- static __new__(_cls, method, passes_requested, passes_done, stopped, reason, before, after, history, points=None)¶
Create new instance of StageLog(method, passes_requested, passes_done, stopped, reason, before, after, history, points)
- nekmeshpy.core.smoother.schedule.Schedule¶
A schedule is an ordered list of
(method, knobs); the methods are names in a registry.
- class nekmeshpy.core.smoother.schedule.Method[source]¶
Bases:
NamedTupleOne registered smoothing method:
fn(mesh, resolved, **knobs) -> (mesh, StageLog).local: the schedule runner hands itelements=(the failing elements) when it runs;weights: it acceptsweights=and receives the runner’s.
- class nekmeshpy.core.smoother.schedule.Registry[source]¶
Bases:
objectThe stage names one rung’s schedule runner knows.
entriesis the live name ->Methodtable.- register(name, fn, *, local=False, weights=False, replace=False)[source]¶
Make
fnavailable torun()as the stagename– the hook another backend plugs in by.fn(mesh, resolved, **knobs)must return a new mesh and aStageLog, leavemeshuntouched and honourresolved. An existing name is refused unlessreplace=True.
- run(mesh, resolved, schedule, *, failing, weights=None, common=None, record=False)[source]¶
Run the stages in order, each on the previous stage’s result.
failing(mesh)picks the elements a local stage works on, at the moment it runs;commonknobs (e.g. the reference normals) go to every stage unless the schedule overrides them. Returns the final mesh and oneStageLogper stage;meshis untouched.record=Truekeeps a copy of the vertex positions after every stage inStageLog.points.
- nekmeshpy.core.smoother.schedule.run_carried(registry, mesh, resolved, schedule, *, curved, linearize, carry, failing, weights=None, common=None, record=False)[source]¶
Registry.run()for a curved mesh. A stage named incurved(tmop) runs on the curved mesh and returns one; every other stage is linear: each maximal run of them is handed the straight-sidedlinearize(mesh)and the curved nodes follow the result throughcarry(curved, moved). Carrying is additive (a node keeps its offset from the straight-sided element), so one carry per run of linear stages equals one per stage.meshis untouched.
- nekmeshpy.core.smoother.schedule.user_schedule(outer=10, n_lap=3, n_opt=20, *, w=0.97, final_local=True)[source]¶
The tuned shape – loops of loops:
outertimeslaplace(n_lap)thenoptimize(n_opt), closed by two local Laplace passes that lift the smallest edges. Only a starting suggestion; the caller proposes and the user edits it.
- nekmeshpy.core.smoother.schedule.untangle_schedule(deltas=(1.0, 0.3, 0.1, 0.03, 0.01), iters=60)[source]¶
Optimizer stages with a shrinking barrier width: a wide
deltafirst smooths the landscape enough for a folded vertex to be pulled through, then each narrower one sharpens it back toward the exact metric.
- nekmeshpy.core.smoother.schedule.suggest_schedule(start, *, outer=10)[source]¶
The schedule to propose for a mesh in the state
start(a rung’sQuality): the tuned shape, preceded by the untangling continuation when any element starts inverted – a Laplace pass on a tangled mesh has nothing to repair it with. A suggestion, never silently run.
core.smoother.session¶
The interactive layer of the smoother, shared by every rung: one SmoothSession holds the mesh, its
constraints and a stack of accepted states, and every round answers the same questions
(RoundReport) before anything is accepted.
The session never edits in place and never accepts silently: SmoothSession.propose() runs a schedule on a
copy and returns a Proposal (report + candidate mesh + before/after files); the caller – a person at
the prompt, or an assistant reading the report to a person – decides; accept() pushes it,
undo() pops it. Meshes are immutable by contract, so a state is a reference and undo costs
nothing. review="print" / "auto" in run() are the fast paths that do not wait for
an answer.
Everything that depends on the mesh type sits behind a RungAdapter – how a mesh is linearised and its
curved nodes rebuilt (a curved mesh stays curved through a round: see the rung’s run_schedule), how constraints resolve, which input checks run, how quality is read, how a round’s files
are written, and which stages exist (the rung’s own registry, so a stage registered there – including a third
party’s – runs through the session with no change here). The session itself is only the state stack, the round
counter and the verdict. core/ imports no rung, so the adapter is the only door to one.
- class nekmeshpy.core.smoother.session.Count[source]¶
Bases:
NamedTupleAn integer a rung counts per mesh that must not rise in a round (quads in an overlap, …).
noteis the%dwarning printed whileafteris above zero.
- class nekmeshpy.core.smoother.session.Audit[source]¶
Bases:
NamedTupleA rung’s own part of a verdict: extra
warnings/failures, structureddatathe rung’s report type reads back, and text lines appended to section 3 (quality) and section 4 (geometry) of the printout.
- class nekmeshpy.core.smoother.session.RoundFields[source]¶
Bases:
NamedTupleThe fields of a
RoundReport, in order.- static __new__(_cls, round, constraints, stages, before, after, shape_before, shape_after, geometry_before, geometry_after, propagate, undo, warnings, failures, files, counts=(), data={}, detail3=(), detail4=())¶
Create new instance of RoundFields(round, constraints, stages, before, after, shape_before, shape_after, geometry_before, geometry_after, propagate, undo, warnings, failures, files, counts, data, detail3, detail4)
- class nekmeshpy.core.smoother.session.RoundReport[source]¶
Bases:
RoundFieldsWhat a round owes, plus the verdict.
failuresblock acceptance;warningsdo not. Printing it gives the full, honest before/after table. A rung subclasses this (no new fields) to set the words (noun/plural/angle_word) and to read itsAuditdataback as attributes.- noun = 'element'¶
- plural = 'elements'¶
- angle_word = 'corner'¶
- propagate_label = 'propagate'¶
- class nekmeshpy.core.smoother.session.Proposal[source]¶
Bases:
NamedTupleA round that has been run but not accepted: the candidate mesh, its report, the schedule.
- report: RoundReport¶
Alias for field number 1
- class nekmeshpy.core.smoother.session.RungAdapter[source]¶
Bases:
objectEverything the session needs from one mesh type. Subclass it per rung; the defaults are the “nothing to do” answers, so a rung overrides only what it has.
report_typeis theRoundReportsubclass it fills.- report_type¶
alias of
RoundReport
- propagate = ''¶
Section 5 of the report (“propagate to the blocks above?”); empty when the question does not arise.
- no_rebuild = 'this rung cannot rebuild a high-order mesh yet'¶
The explanation printed in place of a rebuilt high-order mesh when the rung has none.
- preflight(mesh)[source]¶
Refuse or warn about the input mesh (a gap gate, a curved mesh the rung cannot take). Raises.
- Parameters:
mesh (Any)
- Return type:
None
- linearize(mesh)[source]¶
The straight-sided order-1 shadow of a curved
mesh(what the linear stages of a schedule run on).
- resolve(mesh, constraints, boundary)[source]¶
The per-vertex mobility (
Resolved) ofconstraintsonmesh.
- prepare(mesh, resolved)[source]¶
Called once with the linear mesh and its first resolution (reference normals, input checks).
- rebuild(base, points, resolved, order)[source]¶
The curved mesh of the smoothed vertices
pointscarried onbase(seeSmoothSession.rebuild_high_order()). Returns(mesh, report).
- counts(mesh)[source]¶
(label, value, note)per integer the verdict watches (it may not rise); empty by default.
- audit(before, after, resolved)[source]¶
The rung’s own warnings / failures and report sections for the round
before -> after.
- describe(resolved, *, sj_target, geom_tol)[source]¶
Section 1 of the report: what the constraints were.
- class nekmeshpy.core.smoother.session.SmoothSession[source]¶
Bases:
objectOne smoothing job on one mesh, through a
RungAdapter. A rung’s session subclasses this with its own constructor (the options its adapter takes) and binds the adapter; see the module docstring for the flow. A curved mesh stays curved (keep_curved); passkeep_curved=Falseto smooth its straight-sided shadow.- __init__(mesh, constraints, adapter, *, boundary='frozen', sj_target=0.05, geom_tol=1e-06, weights=None, out_dir=None, name='smooth', keep_curved=True)[source]¶
- rebuild_high_order(order=None)[source]¶
The curved mesh of the current accepted vertices: the original high-order nodes carried along and re-projected onto the targets. With a linear input and
order=Nthe mesh is lifted to orderN. The session state is not changed. RaisesNotImplementedErrorat a rung without a rebuild.
- set_constraints(constraints, *, boundary=None)[source]¶
Change the constraints – a state of its own, so
undo()restores the old ones.
- propose(schedule=None)[source]¶
Run
schedule(default:suggest()) on a copy; the session is unchanged.
- accept(proposal)[source]¶
Push an accepted proposal (refused if it failed or is stale).
- Parameters:
proposal (Proposal)
- Return type:
None
- reject(proposal)[source]¶
Drop a proposal. Nothing was pushed, so there is nothing to undo; the method exists so a reviewer’s answer has a name (and is checked for staleness like
accept()).- Parameters:
proposal (Proposal)
- Return type:
None
- undo(n=1)[source]¶
Pop
naccepted states (constraint changes count as states); the input is the floor.
- history()[source]¶
The trend over accepted states: inverted elements, min scaled J, objective per element.
- Return type:
- run(rounds, *, review='print', adjust='same', schedule=None, stall=0.001, ideal=1e-06)[source]¶
The fast paths, which do not wait for an answer.
"print"runsroundsrounds on the same trajectory (oradjust-ed each time), accepting every passing round, printing each report, and stopping at the first FAIL."auto"additionally stops when the mean objective is already belowideal(a square lattice is 0; nothing left to gain), when a round improves it by less thanstall(relative), or when it raises a regression warning – the interactive path ispropose()+ a human +accept().
- nekmeshpy.core.smoother.session.adjust_schedule(schedule, mode)[source]¶
The “more aggressive / more conservative / same” step, as one table of knobs and never as a loosened constraint: Laplace shrink
w(0.99 / keep / 0.92), Laplace passes (2 / keep / 6), optimizer iterations (10 / keep / 40) and optimizer step capalpha_max(0.25 / keep / keep).
- nekmeshpy.core.smoother.session.build_report(adapter, before, after, resolved, logs, *, rnd=0, undo='', propagate='', files=None, sj_target=0.05, geom_tol=1e-06)[source]¶
The
RoundReportof smoothingbeforeintoafterunderresolved, every reading taken throughadapterand the verdict fromverdict().
- nekmeshpy.core.smoother.session.verdict(report_type, logs, before, after, shape_before, shape_after, geometry, counts=(), audit=None)[source]¶
(warnings, failures)of a round – the one place it is decided. Fails: off target (geometryis the after reading), more inverted elements, a watched count that rose, anything the rung’sauditfails. Warns: what remains, any metric that got worse, a stage rolled back, Laplace and the optimizer fighting.
core.smoother.descent¶
One nonlinear conjugate-gradient driver with a line search, for every rung’s shape optimizer.
The optimizer is the same loop at the quad and the hex rung: Fletcher-Reeves on the objective, the constraints enforced every step. What differs – how the objective and its gradient are evaluated on the elements in play, how a gradient or a step is cut down to what the targets allow, how a trial is snapped back onto the targets – is passed in as callables, so this module imports no mesh type.
Everything is measured in one unit, the local edge length l_v (shortest incident edge, frozen at stage
start): the variables are u_v = dx_v / l_v, the search direction is normalised so its largest move is one
l_v, and the line-search variable alpha is that move in edge lengths, capped at alpha_max. Nothing
depends on the element count or the model’s units.
- class nekmeshpy.core.smoother.descent.DescentResult[source]¶
Bases:
NamedTupleWhat
minimize()ended with: the last accepted positionsP(not yet snapped a final time), the objectivefand inverted countninvthere, one(iteration, n_inverted, sj_min, objective_mean)per accepted iterate, why it stopped and how many iterations were accepted.- P: NDArray[float64]¶
Alias for field number 0
- nekmeshpy.core.smoother.descent.minimize(P, *, evaluate, gradient, project, snap, summary, ell, movable, tangent, n_used, iters, alpha_max=1.0, restart='halve', tol_step=0.0001, tol_rel=1e-06)[source]¶
Conjugate-gradient descent from
P(already snapped onto the targets).evaluate(Q)is(F, n_inverted)over the elements in play;gradient(P)the analyticdF/dx(P, 3)over the same;project(d, P)removes from a gradient or stepdwhatever the targets forbid at positionsP;snap(Q)puts a trial back on the targets;summary(P)is the(n_inverted, sj_min, objective_mean)of the whole mesh logged per accepted iterate.ell(P, 1)is the local length unit,movablethe vertices that may move,tangentthose whose direction is re-cut after the CG mix,n_usedthe element count for the mean-excess test.Each step: take the gradient, project it, form the CG direction, and line-search
alphaonF(snap(x + alpha l s))– the objective after the snap, so the search sees what the mesh will actually become. A trial that would add an inverted element is never accepted.Stops on
iters, a step belowtol_stepedge lengths, a relative drop ofFbelowtol_rel, a mean excess below1e-10(already ideal), a zero projected gradient, or a line search that finds no step loweringF.restart="halve"halvesbetaon every second iteration,"none"is plain Fletcher-Reeves.- Parameters:
P (NDArray[float64])
gradient (Callable[[NDArray[float64]], NDArray[float64]])
project (Callable[[NDArray[float64], NDArray[float64]], NDArray[float64]])
snap (Callable[[NDArray[float64]], NDArray[float64]])
summary (Callable[[NDArray[float64]], tuple[int, float, float]])
ell (NDArray[float64])
movable (NDArray[bool])
tangent (NDArray[bool])
n_used (int)
iters (int)
alpha_max (float)
restart (str)
tol_step (float)
tol_rel (float)
- Return type:
core.smoother.blocks¶
Per-element node blocks of a curved mesh: how a native stage carries the curved nodes, and how they reach TMOP.
Per-element node blocks, as arrays: the rung-agnostic half of smoothing a curved (order N) mesh.
A curved mesh stores its nodes once per entity (a shared vertex, a shared edge’s interior, a shared face’s
interior, a private element interior); the smoother thinks in per-element blocks (E, (N+1)**dim, 3) in
nekmeshpy’s lexicographic Gauss-Lobatto order (axis 0 fastest). A rung turns a mesh into blocks with its own
element_blocks and back with its own with_blocks; this module holds what is the same at every rung:
scatter_blocks()– blocks back onto the flat node table, through an id table that the rung gets by running its ownelement_blockson node ids instead of coordinates (so every orientation convention of the storage is reused, none re-derived, and no coordinate search is made);carry_blocks()– the curved nodes follow a displacement of the element’s corners, interpolated multilinearly, so a shared edge or face (which sees only its own corners) stays conformal;node_fixed()– which nodes inherit a vertex constraint: a node on an entity is held in a component when every vertex of that entity holds it, and an element’s own interior is never held;min_det_blocks()– the Jacobian determinant of a block on a Gauss-Lobatto lattice (the quadrature points of the TMOP run).
- nekmeshpy.core.smoother.blocks.lattice_weights(order, dim)[source]¶
((order+1)**dim, 2**dim)multilinear weights of an element’s corners at its Gauss-Lobatto lattice (corners in the rung’s connectivity order).
- nekmeshpy.core.smoother.blocks.carry_blocks(blocks, displacement, order, dim)[source]¶
blocks(E, M, 3)moved by the cornerdisplacement(E, 2**dim, 3), interpolated multilinearly to every node:x_new = x_old + sum_c w_c(x) d_c. A node on an edge or face sees only that entity’s corners (the other weights are exactly zero), so two elements sharing it agree; an element whose corners did not move is returned bit for bit.
- nekmeshpy.core.smoother.blocks.scatter_blocks(ids, blocks, flat)[source]¶
The flat node table
flat(n, 3)withblocks(E, M, 3)written atids(E, M). A node shared by several elements is written by each; the blocks of a conformal mesh agree (to rounding), and the last writer wins. Nodes no element references keep their value inflat. A copy.- Parameters:
ids (NDArray[int64])
blocks (NDArray[float64])
flat (NDArray[float64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.blocks.node_fixed(vertex_fixed, cells, order, dim)[source]¶
(E, M, k)the components held at every node of every block, fromvertex_fixed(P, k)(the components held at each vertex) and the corner idscells(E, 2**dim).A node lies on one lowest-dimensional entity of its element – a vertex, an edge, a face, or the interior. It is held in a component when every vertex of that entity holds it; an element’s interior is held in a component only when all its corners do (a frozen element stays as it is), so it is free otherwise. The entity is read from the lattice index: an index at an end (
0ororder) pins that axis, and the entity’s vertices are the corners agreeing on every pinned axis.
- nekmeshpy.core.smoother.blocks.min_det_blocks(coords, order, dim, npts)[source]¶
(E,)the smallest Jacobian determinant of each block over thenpts-per-axis Gauss-Lobatto lattice.coords(E, M, dim)are the node coordinates in a frame of the element’s own dimension; the map is the order-ordertensor Lagrange interpolant, differentiated analytically, the determinant taken with respect to the reference cube[0, 1]**dim.
core.smoother.mfem¶
The optional MFEM / TMOP bridge, shared by both rungs’ ("tmop", {...}) stage. It needs
PyMFEM (pip install mfem); the module itself imports without it, and
calling it without says so. The guide: TMOP via the MFEM backend.
The MFEM / TMOP bridge: one smoothing method for quads and hexes, written once.
TMOP (the Target-Matrix Optimization Paradigm of MFEM) minimises a
sum over elements of a metric of the map from a target element to the actual one, with a Newton or
L-BFGS solver. This module hands a mesh to it as plain arrays and takes the vertex positions back, so the
quad rung and the hex rung each register a three-line wrapper (("tmop", {...}) in a schedule) and share
everything else here.
Optional. MFEM is imported lazily, inside the functions that need it, so this module (and every
smoother module that registers it) imports without PyMFEM; calling it without raises an
ImportError naming the install (pip install mfem, the optional smooth extra). PyMFEM is a
large binary wheel and wraps a C++ library that aborts the process on some invalid input, so the bridge
checks what it can before handing over (tmop() refuses an inverted input for every metric but the
untangling 313).
What it does, in order:
Arrays in.
points(N, 3)andcells(C, 4)quads or(C, 8)hexes in nekmeshpy’s corner order, which is MFEM’sSQUARE/CUBEvertex order (NEK_TO_MFEMstates the permutation explicitly, and the tests check that a round trip keeps every Jacobian positive). TMOP needs the element dimension to equal the space dimension, so a quad mesh must be planar: its points are rewritten in an orthonormal frame of that plane (plane_frame()) and a quad mesh on a curved 3-D surface is refused – smooth it with the rung’s own optimizer.Constraints to essential DOFs (
fixed_components()), one rule per mobility kind ofResolved:FROZEN-> every component fixed;ON_SURFACEon a plane whose normal is a coordinate axis -> that one component fixed, the vertex slides in the plane (the quad rung has no surface constraint to speak of: a vertex “on” the mesh’s own plane stays free);ON_CURVEon a line along a coordinate axis -> the other components fixed, the vertex slides on the line;anything else – a curved surface or curve,
TANGENT, a rigidslide_column– is held fixed (for now: the optimizer cannot yet follow a curved target); the count is in the stage’sreason.
Every constrained vertex is first projected onto its target (
snap()), so a component is fixed at the target’s coordinate, not wherever the input happened to be.Targets – what “good” means per element, as the target Jacobian
Wof the metric:"input":Wis a box with the element’s own edge lengths and ideal (orthogonal) skew – the element is asked to become orthogonal at the size and aspect it already has, so a graded mesh stays graded (MFEM’sGIVEN_SHAPE_AND_SIZEfrom per-element size and aspect-ratio fields);"input_smoothed": the same, with each element’s edge lengths first relaxedsweepstimes toward its face neighbours’ (weightsweep_weight), so the grading itself is smoothed too;"ideal": the ideal shape at the element’s own size (IDEAL_SHAPE_GIVEN_SIZE): wants cubes / squares, which inflates a thin layer – use withdesignedand a quality you can afford.
designedelements (a boundary layer, a region to leave alone) are instead given their own input Jacobian as target, so only their shape is asked to stay and a layer keeps its thickness when its two faces are held. An element whose every vertex is fully fixed carries zero weight.Metric:
metricis a TMOP metric id; the defaults are2(2-D shape) and303(3-D shape).80and333(shape + size,metric_weight) work;313untangles (shifted barrier, finite differences, one integrator:target="ideal").Solver:
"newton"(MINRES, l1-Jacobi) or"lbfgs".
Orders. tmop() is the order-1 entry (the vertices are the nodes). tmop_blocks() is the curved one:
a mesh of order N goes in as per-element node blocks, an MFEM mesh with an H1 Gauss-Lobatto nodal space of
order N is built over the order-1 cells, every node is a degree of freedom, and the optimised blocks come back at
the same order. Before optimising it reads the geometry back from MFEM’s own element map and compares it with the
blocks (1e-12 of the extent), so a disagreement between the two libraries’ node orders is an error, not a bad
mesh. A vertex is held as at order 1 (at its input coordinates), the nodes on an edge or face inherit the hold of
the vertices of that entity, interior nodes are free. Targets are built from each element’s corner extents, the
quadrature order defaults to 2N + 2 (Gauss-Lobatto, so it contains the nodes), and the log carries a
BlockReading: minimum determinant at the quadrature points, scaled Jacobian at the nodes and on the finer
sample_order lattice. Not available at order N: sliding along a curved target (held fixed, as at order 1).
- nekmeshpy.core.smoother.mfem.available()[source]¶
Whether PyMFEM can be imported (nothing is imported to find out).
- Return type:
- nekmeshpy.core.smoother.mfem.NEK_TO_MFEM: dict[int, NDArray[int64]] = {2: array([0, 1, 2, 3]), 3: array([0, 1, 2, 3, 4, 5, 6, 7])}¶
The MFEM vertex order of a quad (
SQUARE) and a hex (CUBE), as nekmeshpy’s own corner indices:mfem_vertex[k] = corner[NEK_TO_MFEM[dim][k]]. It is the identity today (a quad runs 0-1-2-3 cyclic, a hex 0-3 on the bottom face and 4-7 above them); it is a table so that the one place the two libraries’ conventions meet is stated, and tested, instead of assumed.
- class nekmeshpy.core.smoother.mfem.Frame[source]¶
Bases:
NamedTupleAn orthonormal frame of the mesh’s space:
coords = (points - origin) @ basis(N, dim), andpoints = origin + coords @ basis.Tfor a mesh that lies in the frame’s span.- origin: NDArray[float64]¶
Alias for field number 0
- basis: NDArray[float64]¶
Alias for field number 1
- static __new__(_cls, origin, basis)¶
Create new instance of Frame(origin, basis)
- Parameters:
origin (FloatArray)
basis (FloatArray)
- nekmeshpy.core.smoother.mfem.plane_frame(points, cells)[source]¶
The frame the optimizer works in. A hex mesh keeps the coordinate axes. A quad mesh must lie in one plane (to
1e-9of its extent; otherwise aValueError, since TMOP needs the element dimension to equal the space dimension): when that plane is a coordinate plane the frame is its two axes, else the plane’s principal axes; the second one is flipped if needed so the quads keep positive area.- Parameters:
points (NDArray[float64])
cells (NDArray[int64])
- Return type:
- class nekmeshpy.core.smoother.mfem.FixedMap[source]¶
Bases:
NamedTupleThe essential components of a
Resolved.- points: NDArray[float64]¶
(N, 3)the input with every constrained vertex projected onto its target.
- nekmeshpy.core.smoother.mfem.fixed_components(points, r, basis=None)[source]¶
Map
rto essential components in the framebasis(3, dim)(default the 3 coordinate axes).The rules are in the module docstring. A target counts as an axis-aligned plane when its normal is the same unit coordinate axis at every vertex it holds (a sphere or a triangulated surface is not, even where one vertex happens to sit on an axis), and an axis-aligned line likewise for its tangent.
- nekmeshpy.core.smoother.mfem.element_extents(coords, cells)[source]¶
(C, dim)the mean edge length of each element along each of its reference axes.- Parameters:
coords (NDArray[float64])
cells (NDArray[int64])
- Return type:
NDArray[float64]
- nekmeshpy.core.smoother.mfem.regions(element_tags, designed=None)[source]¶
(attributes, designed_mask)from a mesh’s dense element tags ((C,)strings,""untagged): each distinct tag is an integer attribute (1-based, in sorted order), anddesigned– a tag name, a list of names, a boolean(C,)mask orNone– becomes the masktmop()takes. A name no element carries is an error.
- nekmeshpy.core.smoother.mfem.to_mfem_mesh(coords, cells, attributes=None)[source]¶
An in-memory
mfem.Meshofcoords(N, dim)andcells(quads or hexes in nekmeshpy corner order), vertexiof the array being vertexiof the mesh and elementebeing elemente.attributes(C,)ints become the element attributes (default1).- Parameters:
coords (NDArray[float64])
cells (NDArray[int64])
attributes (NDArray[int64] | None)
- Return type:
- nekmeshpy.core.smoother.mfem.from_mfem_mesh(mesh)[source]¶
(coords (N, dim), cells in nekmeshpy corner order, attributes)read back from anmfem.Mesh– the inverse ofto_mfem_mesh()(the vertex coordinates, not any nodal grid function).
- nekmeshpy.core.smoother.mfem.min_det(coords, cells, n=3)[source]¶
The smallest Jacobian determinant of the multilinear elements over an
n-per-axis lattice (Gauss-Lobatto-like: ends included) – the check made before MFEM is entered, since a negative determinant makes most TMOP metrics abort the process.n = 3covers the corners and the midpoints.
- class nekmeshpy.core.smoother.mfem.TmopEnergy[source]¶
Bases:
NamedTupleThe reading a stage logs when no rung
qualitywas supplied: the TMOP energy only.
- nekmeshpy.core.smoother.mfem.tmop(points, cells, resolved, *, attributes=None, designed=None, target='input', metric=None, metric_weight=0.5, solver='newton', iters=50, rtol=1e-08, sweeps=5, sweep_weight=0.5, quad_order=4, order=1, quality=None, verbose=False)[source]¶
One TMOP run on the order-1 mesh
(points, cells)underresolved: the new vertex positions(N, 3)and aStageLog(itsreasoncarries the energy before and after, the Newton / L-BFGS iterations and what was fixed). The rules are in the module docstring.attributes(C,)ints label the elements (regions);designedselects the elements that keep their own input shape as target: a boolean(C,)mask, or integer attribute values (needsattributes).targetis"input","input_smoothed"(sweepsrelaxations of weightsweep_weighton the target edge lengths) or"ideal".metricdefaults toDEFAULT_METRIC;metric_weightis the weight of the combined metrics 80 and 333.solveris"newton"or"lbfgs", stopping atitersor a relative residualrtol.quad_orderis the Gauss-Lobatto quadrature order (4 includes the corners, so the determinant check of the line search covers the geometry a.re2file keeps).quality(points), when given, reads the rung’s ownQualityfor the log (and enables the rollback: a run that ends with more inverted elements than it began with is refused and the input returned).Raises
ImportErrorwithout PyMFEM,ValueErrorfor a non-planar quad mesh, an inverted input (except metric 313) or a bad knob, ororder != 1(tmop_blocks()is the curved entry).
- class nekmeshpy.core.smoother.mfem.BlockReading[source]¶
Bases:
NamedTupleThe reading a curved stage logs, taken on the node blocks.
det_min/n_invertedare at the TMOP quadrature points (the Jacobian the line search guards),sj_minat the stored nodes andsj_sampledon thesample_orderlattice – the order the solver will run at, which is the one that can see a fold between the nodes.- static __new__(_cls, energy, n_inverted, sj_min, objective_mean, det_min, sj_sampled, sample_order)¶
Create new instance of BlockReading(energy, n_inverted, sj_min, objective_mean, det_min, sj_sampled, sample_order)
- nekmeshpy.core.smoother.mfem.tmop_blocks(blocks, cells, resolved, *, order, attributes=None, designed=None, target='input', metric=None, metric_weight=0.5, solver='newton', iters=50, rtol=1e-08, sweeps=5, sweep_weight=0.5, quad_order=None, sample_order=None, verbose=False)[source]¶
One TMOP run on a curved mesh given as node blocks: every Gauss-Lobatto node is a degree of freedom, and the new blocks come back at the same order.
blocks(E, (order+1)**dim, 3)are the elements’ nodes in nekmeshpy’s lexicographic order (a rung’selement_blocks);cells(E, 2**dim)the corner ids intoresolved(the order-1 topology). Shared nodes must agree between the elements that share them – checked, by reading the blocks back from MFEM before anything is optimised (to1e-12of the mesh extent), which also catches any mismatch between the two libraries’ node orders.What moves. Every node except the held ones. A vertex is held as at order 1 (
fixed_components(): frozen in every component, an axis-aligned plane in its normal, an axis-aligned line in the two others, anything curved or tangent in all of them), at its input coordinates – vertices are not re-snapped onto their targets here. The nodes on an edge or face inherit the hold of their entity’s vertices (node_fixed(): held in a component when every vertex of the entity holds it), so a frozen wall keeps its curved edge and face nodes bit for bit and a plane keeps its nodes on the plane; interior nodes are free. Sliding along a curved target is not available, the vertices and entities on one are held.Targets, metric, solver as
tmop(); the targets are built from each element’s corner extents (one target per element, read at every quadrature point).quad_orderis the Gauss-Lobatto quadrature order (default2*order + 2, which includes the nodes and grows with the order);sample_order(defaultmax(order, SCAN_ORDER)) is the lattice the logged scaled Jacobian is also read on. The log carries aBlockReadingbefore and after, and a run that raises the number of elements with a non-positive determinant at the quadrature points, or does not lower the energy, is refused and the input returned.- Parameters:
blocks (NDArray[float64])
cells (NDArray[int64])
resolved (Resolved)
order (int)
attributes (NDArray[int64] | None)
target (str)
metric (int | None)
metric_weight (float)
solver (str)
iters (int)
rtol (float)
sweeps (int)
sweep_weight (float)
quad_order (int | None)
sample_order (int | None)
verbose (bool)
- Return type: