Source code for nekmeshpy.core.quality
"""The shared schema of the per-rung scaled-Jacobian quality summaries."""
from __future__ import annotations
from typing import NamedTuple
#: Scaled Jacobian below which an element is counted as *poor*. Part of the public
#: schema: it names both the ``n_poor`` field and the ``poor (<...)`` report line.
POOR_THRESHOLD = 0.2
#: 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.
SCAN_ORDER = 7
#: Sampled points a default ``order_scan`` may spend, summed over its orders.
#: The work is ``n_elements * (order+1)**dim``, and at :data:`SCAN_ORDER` that 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 call
#: ``order_scan`` directly) when you want the answer anyway.
SCAN_BUDGET = 15_000_000
[docs]
class OrderScan(NamedTuple):
"""What 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)**dim`` GLL nodes and
silent between them, so a clean :class:`QualitySummary` at 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."""
#: Sampling orders actually read, ascending.
orders: tuple[int, ...]
#: Smallest scaled Jacobian at each of :attr:`orders`.
min_sj: tuple[float, ...]
#: Elements with a non-positive scaled Jacobian at each of :attr:`orders`.
n_inverted: tuple[int, ...]
#: Orders the budget refused, ascending -- unchecked, not clean.
skipped: tuple[int, ...]
@property
def clean(self) -> bool:
"""True when at least one order was read and none found an inverted element.
A scan that read *nothing* is not clean -- it is unchecked, which is the same
reason :attr:`skipped` exists rather than the budget dropping orders in
silence."""
return bool(self.orders) and not any(self.n_inverted)
@property
def worst(self) -> tuple[int, float]:
"""``(order, min_sj)`` of the harshest reading."""
if not self.orders:
raise ValueError("OrderScan.worst: no order was sampled")
i = min(range(len(self.orders)), key=lambda k: self.min_sj[k])
return self.orders[i], self.min_sj[i]
[docs]
class SolverMetrics(NamedTuple):
"""The three numbers Nek5000/nekRS print as ``mesh metrics:``, of the trilinear
(``.re2``) map read at one polynomial ``order``.
**:attr:`jac_ratio` is not a scaled Jacobian in :class:`QualitySummary`'s sense.**
nekRS's line reads ``scaled Jacobian`` but computes ``min(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, so ``0.08`` here and ``0.22`` there are the same
mesh."""
#: Polynomial order the map was sampled at.
order: int
#: Shortest / longest distance between neighbouring GLL nodes, over the mesh.
spacing_min: float
spacing_max: float
#: ``min(J)/max(J)`` per element -- smallest, largest, mean.
jac_ratio_min: float
jac_ratio_max: float
jac_ratio_mean: float
#: Longest over shortest of an element's 12 corner-to-corner edges -- smallest,
#: largest, mean.
aspect_min: float
aspect_max: float
aspect_mean: float
[docs]
class QualitySummary(NamedTuple):
"""Aggregate scaled-Jacobian statistics over one mesh's elements."""
#: Number of elements the statistics cover.
n_elements: int
#: Smallest per-element scaled Jacobian; ``<= 0`` means inverted / degenerate.
min: float
#: Largest per-element scaled Jacobian; ``1`` is a perfect corner.
max: float
#: Mean over the elements.
mean: float
#: Median over the elements.
median: float
#: Elements with a non-positive scaled Jacobian (inverted or degenerate).
n_inverted: int
#: Elements below :data:`POOR_THRESHOLD` -- includes ``n_inverted``.
n_poor: int