pub struct Loss {
pub volume_error: f64,
pub boundary_fraction: f64,
pub thin_runs: usize,
pub small_triangles: usize,
pub retried_rows: usize,
pub ambiguous_rows: usize,
}Expand description
What a rasterisation lost, reported rather than left to be discovered.
A missing feature has no symptom. It does not make a solver fail, produce a NaN or trip the
conservation audit — it produces a smooth, plausible answer about a different object, which is the
failure this workspace is organised around not having. So every Voxels carries one of these and
Loss::is_clean says in one call whether anything here needs reading.
Fields§
§volume_error: f64voxel volume / mesh volume − 1.
The aggregate of what the grid kept and did not, and it is signed, because the cells the surface bulges out of and the ones it cuts into partly cancel.
That cancellation is why this is not a discretisation error with an order, and it is worth
saying plainly because the expectation is so natural. Rasterising a sphere of radius 10 mm — a
32×64 tessellation, and the numbers move a few tenths of a point with that — at 2.5, 2.0 and
1.5 mm gives +4.9%, +5.8%, −2.3%: the first refinement makes it worse and the second changes
its sign. Sliding the mesh relative to the grid changes none of it. What is left after the
cancellation is a lattice-point count, and its error is erratic by nature.
So use it as a result and not as a bound on the next resolution.
boundary_fraction: f64The share of the voxel volume in cells that have a face on the outside.
The rasterisation’s uncertainty rather than its error: the volume that sat close enough to the surface for the answer to have gone either way.
Unlike the volume error this is clean, because it is a surface area rather than a cancellation:
the layer is one cell thick over an area A, so the fraction is A·dx/V — first order, with a
coefficient. For a sphere that is c·3dx/R, and the tests measure c ≈ 0.82.
It is the number to put in front of someone choosing a cell size. Forty-three percent of your object’s volume is in cells that could have gone either way says what 2 mm on a 20 mm ball means in a way that a step count does not.
§It does not bound the volume error, and an earlier version of this sentence said it did
The argument is seductive and wrong: “only cells the surface passes through can be misclassified, and those are these”. They are not. This counts exposed cells that came out filled; a cell the surface passes through whose centre landed outside is misclassified too and appears in neither the numerator nor the denominator. The two are also fractions of different volumes — this of the voxel volume, the error of the mesh’s.
The 0.4 mm plate in a_designed_shape.rs is the counterexample and it was in the suite the whole
time: at a 2 mm cell it reports a volume error of +4.0 against a boundary fraction of 1.0.
thin_runs: usizeSolid runs one or two cells thick, counted along all three axes.
A feature two cells across is not resolved by any scheme here — a seven-point stencil has no interior in it, a trilinear element has one element — and a feature one cell across exists only by luck of where the surface fell relative to the cell centres. Move the mesh half a cell and it may not be there at all.
small_triangles: usizeTriangles smaller than one face of a cell. See Mesh::triangles_below.
retried_rows: usizeScanlines whose first ray was degenerate and which a perturbed one decided.
Not a loss — these rows came out right — but the count says the mesh has geometry lined up with the grid, which is the common case rather than the exotic one: a cube on cell boundaries sends every row through a face diagonal, and this reads in the hundreds.
It is here because the alternative is a mechanism nothing can see working. Loss::ambiguous_rows
counts only the rows where all the perturbations failed, and no mesh in the test suite has ever
produced one — so without this field the whole retry path would be exercised only by inference
from a cell count, and a broken retry would look exactly like a mesh that never needed it.
ambiguous_rows: usizeScanlines no ray could decide, after every perturbation was tried.
A ray through a closed surface must cross it an even number of times, and must not pass through an edge. A row that fails both tests on all four rays cannot be filled by parity. These rows are left empty, which is visible as a slot missing from the shape rather than as a subtly wrong fill — and the count is here so it is not only visible in a picture.
No mesh in the test suite has produced one, which is stated rather than left to be assumed:
the branch is written and reasoned about and is not covered by a measurement, so a caller who sees
a nonzero value here is in territory this crate has not walked. Loss::retried_rows is the
nearby thing that is exercised.
Implementations§
Source§impl Loss
impl Loss
Sourcepub fn is_clean(&self) -> bool
pub fn is_clean(&self) -> bool
Whether anything was lost that a caller should look at.
The thresholds are deliberately loose and deliberately stated: 2% of volume, and any thin run or ambiguous row at all. Volume error is a smooth thing that a caller trades against cost, so it gets a number; a feature one cell thick and a row that could not be filled are not trade-offs, they are things that either happened or did not.
Three fields are deliberately not here, for two different reasons.
Loss::boundary_fraction and Loss::small_triangles are left out because no threshold on
either is right for more than one physics: a diffusion problem run to steady state barely notices
a boundary layer that would move a stress concentration by a factor of two, and a large flat face
tessellated into a thousand slivers loses nothing at all. Putting a number on those would be this
library guessing, which is the one thing it does not do — so they are reported and the judgement is
the caller’s.
Loss::retried_rows is left out because it is not a loss. Those rows came out right.