Angle readings¶
The trijunction angle readings: the chord, the second-order (quadric) reading, and their oriented forms.
The angle_reading (or reading) selector of every angle-reading function takes one of
ANGLE_READINGS: "chord" (the default), "quadric", "oriented_chord" and "oriented_quadric".
Angles are in radians in every returned map.
Why this module exists¶
foambryo.geometry._wedges_at_half_edge reads a trijunction wedge angle as
arccos(n_i . n_j) between two incident triangles' face normals. Both normals are
perpendicular to the shared junction edge, so both lie in the plane normal to it, and the
angle between them equals the angle between the two triangles' in-plane traces — the
straight lines from the junction out towards each incident triangle's apex. Production
therefore reads a chord of each interface where the geometry has a tangent.
A flat P1 facet has no curvature to offer, so it cannot report a tangent; a circular arc's
chord is rotated from its tangent by exactly kappa * L / 2, with L the in-plane distance
from the junction-edge midpoint to the incident apex. This chord-for-tangent substitution was
measured to account for 68.4 % of the length-weighted flattening slope of the reconstructed
meshes.
This module supplies the second reading: fit a generic quadric height field to the
interface's own mesh vertices in a 3-D window around each junction-edge midpoint, read that
surface's normal-plane section curvature kappa analytically, and rotate the face normal about
the junction edge by -kappa * L / 2. It is a local patch fit, keyed to foambryo's own trijunction
identification.
The oriented wedge reading¶
arccos returns [0, pi], so a wedge whose interior angle exceeds 180 degrees — a reflex
wedge — is reported as 360 - theta. The three wedges of one junction edge then sum to
360 - 2 (theta - 180) instead of 360, and every average along the line inherits the deficit.
This is not rare on a coarse reconstruction: measured on the benchmark meshes, 2 % to 25 % of
trijunctional edges carry one, at a rate that differs by a factor of six between reconstruction
strategies, so a comparison of two meshes through the unoriented reading compares two different
amounts of clipping as if they were geometry.
ORIENTED_CHORD_READING and ORIENTED_QUADRIC_READING return the oriented wedge angle in
[0, 360): the arc from film e|f to film f|g measured through region f. The magnitude
is still arccos of the same dot product, evaluated by the same expression; the oriented sine
dot(t, cross(n_first, n_second)) is used only to choose between theta and 360 - theta. So
the oriented readings are bitwise identical to their unoriented counterparts on every wedge
that is not reflex, and the three wedges of an edge sum to 360 degrees.
The axis t runs from the lower to the higher vertex key, as everywhere else in this module.
Its sign is then resolved from the geometry, not asserted: three positive angles summing to 360
contain at most one greater than 180, so under the reversed axis the three oriented wedges sum
to 720 rather than 360, and the reader takes whichever assignment sums to 360. A wedge whose
oriented sine is exactly zero — an interior angle of exactly 0 or exactly 180 degrees — reads as
non-reflex under either axis, which reproduces the unoriented reader there.
Which inference methods this changes, and one that it breaks¶
MEASURED on the benchmark meshes. A method's exposure follows from the trigonometric function
its rows use, because the reflection theta -> 360 - theta leaves cos unchanged and flips the
sign of sin:
YoungDupreLocalbuilds every row fromcosof a per-edge wedge, so it is exactly invariant -- its gauge-matched relative error moved by at most1.6e-14over 1035 reconstructed benchmark meshes. It could never see the defect and cannot benefit from it either; a reflex wedge and its reflection are the same row to it.YoungDupreaverages the wedge along the line and then takescosof the mean, so the reflection does move it: up to0.24in absolute relative error, worse on 35 of the 46 reconstruction-variant cohorts measured and better on the rest.ProjectionYoungDupre,LamiandInvLamiusesin, which changes sign. Their error rises by 5-42 %. That is not a defect of the reading: three tensions whose wedge is reflex admit no planar equilibrium at all, and the negative sine is that fact surfacing where the unoriented reading manufactured a plausible positive number instead.LogLamifails outright on a mesh with a reflex wedge: its rows takelog(sin(theta)), which isnanfor a negative sine, and the solve then raises. Measured on 115 of 360 benchmark meshes. PairLogLamiwith an unoriented reading, or repair the reflex edges upstream.
The quadric window needs no reflex guard. It is defined per incident face, transverse to the junction along that face's own chord, and never refers to a wedge angle at all; a reflex wedge is a relation between two faces, so it cannot enter one face's window. What the quadric reading does do is rotate normals about the junction edge, which moves some wedges across 180 degrees in either direction, so its reflex count differs from the chord reading's on the same mesh. That is a property of the corrected geometry, not a defect of the window.
The two junction-edge endpoints¶
The fit keeps a sample when it is inside the window and on the interface's own side of the
junction, eta > 0. Two of the candidate samples are the junction edge's own two endpoints,
and in exact arithmetic their eta is exactly zero — they sit on the eta = 0 axis of the frame
by construction, being the two vertices whose midpoint is the frame's origin. A strict eta > 0.0
therefore resolves them on the sign of a rounding error, and the sign of a rounding error depends
on the units the mesh is expressed in.
MEASURED, on the benchmark's reference meshes: rescaling one mesh by 117x — the ratio between
the normalised frame those meshes are stored in and the voxel frame their reconstructions live in —
changed the kept-sample count on 652 of 1 065 incident faces, changed the admission outcome on
19, and moved line-mean wedge angles by up to 0.14 degrees. The chord reading moves by
2.5e-13 degrees under the same rescaling. That is about eleven orders of magnitude of difference
between two readings of one mesh, and it is why the same reference meshes have been published at
2.032 and at 2.046 degrees of contact-angle error under one name: the frame a mesh is in was
part of that reading's construction and was never stated with it.
An angle between two surfaces cannot depend on the units the surfaces are measured in, so this is a
correctness defect independently of any angle metric. JUNCTION_ENDPOINTS decides those two
samples by vertex identity instead. They are included: they are exact, noise-free samples of
the interface at the very point the frame is anchored, they constrain the along-edge coefficients
that nothing else near eta = 0 constrains, and including them is a statement rather than an
accident. "exclude" and the historical "rounding" remain reachable through
quadric_angle_rotations so that the repair can be measured; neither is reachable through any
reading selector or any inference entry point.
MEASURED after the repair, on the same reference mesh: rescaling by 10, 117 and 1000 moves
the line-mean wedge angles by at most 2.3e-13 degrees, against the chord reading's own 2.5e-13.
The two readings are now equally invariant, to rounding. On a smooth surface whose contact angles
are a closed form the repair moves the reading itself by at most 0.016 degrees and by about
0.001 degrees at typical spacings — so it is a repair whose value is the invariance, not an
accuracy gain, and saying so is part of the repair.
The constant term, and the false reason it used to be omitted¶
The height field is fitted without a constant term. The reason recorded here used to be that
"the junction-edge midpoint is the midpoint of two mesh vertices lying on this interface, so the
interface passes through the origin by construction". That reason is false whenever the junction
line is curved, which on any real mesh it is: the midpoint of two vertices lies on the junction
edge's chord, and the line leaves that chord by about kappa_line * L^2 / 8, as does the
interface bounded by it. The frame's origin is a point the surface does not contain.
The real reason, MEASURED rather than argued, on a smooth surface whose contact angles are a
closed form and whose triangulation was varied over fourteen edge lengths spanning a factor of
seventeen:
- fitting the constant term makes the reading better at every fine triangulation — up to
43 %lower line-mean error at the finest, and better at ten of the fourteen edge lengths tried; - and worse at the four coarsest, by up to
7.4degrees, because at those spacings the extra free parameter is estimated from a window holding barely twice as many samples as the fit has parameters, and the variance it adds exceeds the bias it removes; - the coarse spacings the software is actually run at may or may not be on the worsening side: that depends on how curved real interfaces are relative to their triangulation, which is not a number this project has measured.
So the term stays off — not because the surface passes through the origin, which it does not, but
because switching it on is an improvement at fine triangulations bought with a degradation at coarse
ones, and the operating point is not known well enough to take that trade. fit_constant_term=True
turns it on for measurement. Turning it on also raises the minimum admissible sample count from 10
to 12, which lowers coverage on meshes whose windows are thinly populated. It does not change
_section_curvature, because translating a plane curve does not change its curvature.
What would settle it: the same fourteen-rung measurement with the analytic films' curvature matched to real interfaces', or the equivalent measurement on reconstructions with a ground-truth angle. Either would say which side of the crossover the shipped spacings are on.
Units and conventions¶
- Coordinates are in the mesh's own units: voxel indices on
dw3doutput reconstructed without aspacing, the spacing's unit with one.kappais per mesh length unit andkappa * L / 2is dimensionless (an angle in radians); the reading does not depend on the unit (see "The two junction-edge endpoints"). kappais the signed in-plane curvature of the interface's normal-plane section at the junction, positive when the section turns counter-clockwise about the junction-edge direction (lower vertex key -> higher vertex key) as it leaves the junction.- The junction-edge axis runs from the lower to the higher vertex key. Using a half-edge's own direction instead would flip the sign of the correction on half the half-edges.
Which correction form is applied, and which is not¶
Only the closed form -kappa * L / 2 is applied. It was measured that the alternative —
rotating the mesh's chord onto the fitted patch's own tangent — injects a zero-mean scatter of
about 3.3 degrees at the median and 14.4 degrees at p90 on this patch fit,
because a patch fit's absolute orientation carries the discrepancy between the fitted surface
and the mesh's local orientation, which is not a chord-versus-tangent error and must not be
rotated away. That form is not implemented here. The cost of the form that is implemented is
that the circular-arc model becomes load-bearing: -kappa * L / 2 is exact for an arc and
first-order in kappa * L otherwise.
What enters the fit, exhaustively¶
The fit is generic: a quadric height field is the second-order Taylor statement about
whatever surface is there, and injects only smoothness. No constant-mean-curvature
constraint, no Laplace or Neumann relaxation, no tension, no pressure, no area, no volume, no
ground-truth angle, no reference mesh, no .rec file, no segmentation mask. The complete
list of inputs is:
mesh.v, the mesh's vertex coordinates;mesh.f, its triangles and their two material labels;
and nothing else. quadric_angle_rotations takes a DcelData and, besides the window's
measurement parameters, no other argument, so "no ground truth in the estimator" is structural
here: there is no parameter through which a reference mesh, a ground-truth angle, a tension or a
pressure could reach it.
Identity at zero curvature¶
A fitted kappa of exactly 0.0 produces a rotation of exactly -0.0, and
geometry._wedges_at_half_edge skips the Rodrigues call on a falsy angle, so the chord angle is
returned bitwise. Pinned by tests/test_second_order_angle_reading.py, which also checks the
stronger statement that an explicitly all-zero rotation map, and an empty one, are the identity to
the bit.
The honest qualification, MEASURED rather than asserted: a geometrically flat interface does
not fit to an exactly-zero c_ee, because the least-squares design matrix is not exactly
rank-consistent in binary floating point. On a planar three-film phantom the residual rotation
peaks at 4.1e-17 rad (2.3e-15 degrees) and moves a reported wedge angle by at most 2.8e-14
degrees — sixteen orders of magnitude below the ~5 degree per-line errors this reading exists to
reduce, and below float64's own resolution at 120 degrees. So the bitwise statement is about
kappa == 0, and the practical statement about a flat surface is "identity to 1e-14 degrees".
Hyperparameters are fixed in production, and measurable outside it¶
QUADRIC_WINDOW_FACTOR = 4.0 and QUADRIC_AXIAL_FACTOR = 1.0 are fixed a priori (the axial
one is the isotropic choice, which assumes least). Re-selecting them by held-out fit residual,
with at least 85 % of incident faces covered, returns exactly these values, so they carry no
selection freedom with respect to the metrics they are judged on.
They remain module constants, and no production path can change them: no reading selector,
no inference entry point, no public convenience function and not rotations_for_reading takes a
window. quadric_angle_rotations alone accepts them, keyword-only, defaulting to these
constants, so that the cost of the window can be measured without a second implementation of
the fit living outside this module — two implementations of one fit diverge, and this project has
been bitten by that. Called at the defaults it is bitwise what it was before the parameters
existed, which tests/test_second_order_angle_reading.py executes rather than asserts.
The prohibition that motivated keeping them constants is unchanged and is about selection, not about reachability: a window must never be chosen on the contact-angle flattening slope, the tension contrast or the per-line angle error of a mesh whose accuracy is being judged. A window may be chosen only on meshes that are not on trial — which is what makes measuring it admissible at all.
Coverage, and why the fallback is reported rather than hidden¶
The fit is not admissible everywhere: an incident face whose window holds fewer than two mesh
vertices per fit parameter, or whose design matrix is rank-deficient, gets no rotation and
falls back to the chord reading. Measured on dw3d's 47-case benchmark at min_distance = 3,
coverage is 0.919 of incident faces; at min_distance = 4 it is 0.890. A result that mixes
corrected and uncorrected angles without saying so would be unreadable as science, so
quadric_angle_rotations returns an AngleReadingReport naming the coverage and every
trijunction line that is not fully corrected, and the report-less convenience paths in
foambryo.geometry and foambryo.tension_inference emit an AngleReadingCoverageWarning
instead of staying silent.
Measured behaviour¶
The measured behaviour of the reading is in BENCHMARKS.md (foambryo repository).
AngleReadingCoverageWarning ¶
Bases: UserWarning
Emitted when a quadric reading corrects only part of a mesh's trijunction faces.
Applies to "quadric" and "oriented_quadric"; the chord readings always cover every face.
The uncorrected faces silently fall back to the chord reading, which mixes two angle
conventions in one result. That is admissible — it is strictly better than dropping the
lines — but it must never be invisible. Use the *_with_report entry points in
foambryo.geometry and foambryo.tension_inference to receive the numbers instead of
the warning.
AngleReadingReport
dataclass
¶
AngleReadingReport(reading: str, n_incident_faces: int, n_corrected: int, n_no_samples: int, n_no_fit: int, coverage_fraction: float | None, uncorrected_lines: tuple[tuple[int, ...], ...], line_coverage: dict[tuple[int, ...], tuple[int, int]] = dict(), window_factor: float = QUADRIC_WINDOW_FACTOR, axial_factor: float = QUADRIC_AXIAL_FACTOR, junction_endpoints: str = JUNCTION_ENDPOINTS, fit_constant_term: bool = FIT_CONSTANT_TERM)
What a trijunction angle reading covered, and what it fell back on.
Attributes:
| Name | Type | Description |
|---|---|---|
reading |
str
|
one of |
n_incident_faces |
int
|
|
n_corrected |
int
|
pairs that received a second-order correction. |
n_no_samples |
int
|
pairs whose window held too few interface vertices to fit. |
n_no_fit |
int
|
pairs whose design matrix was rank-deficient or whose curvature was not finite. |
coverage_fraction |
float | None
|
|
uncorrected_lines |
tuple[tuple[int, ...], ...]
|
every trijunction line — identified by its sorted material triple,
the same three materials whose wedge keys |
line_coverage |
dict[tuple[int, ...], tuple[int, int]]
|
|
window_factor |
float
|
the transverse window multiplier actually used. |
axial_factor |
float
|
the axial window multiplier actually used. |
junction_endpoints |
str
|
the policy actually used for the frame's own two junction-edge
endpoints, one of |
fit_constant_term |
bool
|
whether the quadric height field actually fitted a constant term. |
A trijunction line with partial coverage (some but not all of its faces corrected) is
not, by itself, a known source of extra angle error: measured against the chord
reading's own error on the same lines, the per-line displacement this correction makes
does not trend with how much of the line was covered (a near-zero, statistically
insignificant slope, most of what little apparent trend there is already present in the
chord reading, which has no coverage concept at all). A low line_coverage_fraction
is a coverage fact about the mesh, not a warning sign about the correction's accuracy.
is_complete
property
¶
is_complete: bool
True when every trijunctional incident face was read with the selected reading.
line_coverage_fraction
property
¶
line_coverage_fraction: float | None
Fraction of trijunction lines every one of whose faces was corrected.
The line analogue of coverage_fraction, which is per-face: a line with 2 of its
3 faces corrected counts as 0 here (it is in uncorrected_lines) but contributes
2/3 to coverage_fraction. None on a mesh with no trijunctional edge at all,
mirroring coverage_fraction's own convention.
describe ¶
describe() -> str
A one-line human-readable summary, used as the coverage warning's message.
Leads with the two numbers a reader needs to judge the result: the measured face
coverage fraction and how many trijunction lines were not fully corrected (out of how
many), then lists those lines (at most _MAX_LINES_IN_WARNING of them).
fits_quadric_tangents ¶
fits_quadric_tangents(reading: str) -> bool
Whether reading replaces each triangle's chord by a fitted quadric tangent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reading
|
str
|
one of |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True for |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
reads_oriented_wedges ¶
reads_oriented_wedges(reading: str) -> bool
Whether reading returns the oriented wedge angle in [0, 360) rather than [0, 180].
See "The oriented wedge reading" in this module's docstring for what the two ranges mean and why they differ only on reflex wedges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reading
|
str
|
one of |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True for |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
warn_on_partial_coverage ¶
warn_on_partial_coverage(report: AngleReadingReport, stacklevel: int = 3) -> None
Emit an AngleReadingCoverageWarning when a reading fell back on part of the mesh.
Called only from the entry points that drop the report on the floor — the report-returning ones hand the caller the numbers the warning would have described, and stay silent. Complete coverage is silent either way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
report
|
AngleReadingReport
|
the reading's coverage report. |
required |
stacklevel
|
int
|
passed through to |
3
|
validate_reading ¶
validate_reading(reading: str) -> str
Check an angle-reading selector and return it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reading
|
str
|
the selector to check. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
orthonormal_basis ¶
orthonormal_basis(normal: NDArray[float64]) -> tuple[NDArray[np.float64], NDArray[np.float64]]
A right-handed in-plane basis (e1, e2) with cross(e1, e2) = normal.
e1 is built from the world axis least aligned with normal, which is deterministic and
never degenerate. Handedness matters: it fixes the sign of every curvature this module
returns, and it is the reason rodrigues_about and the in-plane angles agree
(cross(axis, e1) = e2 for this basis).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normal
|
NDArray[float64]
|
the plane's normal, any non-zero length. |
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[float64], NDArray[float64]]
|
tuple[NDArray[np.float64], NDArray[np.float64]]: the unit vectors |
rodrigues_about ¶
rodrigues_about(axis: NDArray[float64], vector: NDArray[float64], angle: float) -> NDArray[np.float64]
Rotate vector about the unit axis by angle, counter-clockwise seen from +axis.
For a vector already perpendicular to the axis — which every face normal at a junction edge
is — this reduces to v cos a + (axis x v) sin a, and it agrees with a counter-clockwise
rotation in the (e1, e2) basis orthonormal_basis returns, because cross(axis, e1) = e2
for that right-handed basis.
The rotation is linear, so it acts identically on n and on -n: applying it before
_wedges_at_half_edge's relabelling flip and applying it after are the same operation, and
the sign the mesh happens to carry on a face normal does not enter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
NDArray[float64]
|
unit rotation axis. |
required |
vector
|
NDArray[float64]
|
the vector to rotate. |
required |
angle
|
float
|
rotation angle in radians. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
NDArray[np.float64]: the rotated vector. |
junction_frames ¶
junction_frames(mesh: DcelData) -> list[dict]
One local frame per (trijunctional geometric edge, incident face), from the DCEL.
This walk is production's own trijunction identification, not a second one. A geometric
edge is visited when at least one of its half-edges has more than one twin (the test
compute_angles_tri and compute_trijunction_edges use), it is de-duplicated by its
(min, max) vertex-key pair keeping the lowest incident half-edge, and its three incident
faces are collected in exactly the order geometry._wedges_at_half_edge collects them —
the half-edge's own face first, then its twins'. An edge with any number of incident faces
other than three is a quadrijunction (or higher) and is skipped, exactly as production skips
it. Keying the frames this way is what makes every corrected angle attributable to the same
(trijunction, material) key the chord reading produces.
The frame at one edge:
axisruns from the lower to the higher vertex key, normalised;midpointis the edge midpoint, which is where production's chord is anchored;(e1, e2)isorthonormal_basis(axis), whose handedness fixes every curvature sign;chordis the in-plane unit vector frommidpointtowards the incident face's apex — production's own reading direction — andleveris the in-plane distance to that apex.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
the mesh to walk. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
list[dict]: one entry per |
quadric_angle_rotations ¶
quadric_angle_rotations(mesh: DcelData, *, window_factor: float = QUADRIC_WINDOW_FACTOR, axial_factor: float = QUADRIC_AXIAL_FACTOR, junction_endpoints: str = JUNCTION_ENDPOINTS, fit_constant_term: bool = FIT_CONSTANT_TERM) -> tuple[dict[tuple[tuple[int, int], int], float], AngleReadingReport]
Per-(edge, face) normal rotations that turn the chord reading into the quadric one.
For each (trijunctional geometric edge, incident face) this fits a generic quadric height
field to the incident interface's own mesh vertices inside an anisotropic window around the
junction-edge midpoint, reads the normal-plane section's signed curvature kappa off the
fitted surface, and returns the rotation -kappa * lever / 2 to apply to that face's normal
about the junction-edge axis. That is the exact chord-to-tangent rotation for a circular arc
of curvature kappa whose chord has in-plane length lever.
The window is window_factor * lever transversely and axial_factor times that along the
junction edge. A sample is kept when |xi| <= axial window and hypot(eta, zeta) <= window,
and then when eta > 0 — which keeps the fit on the side of the junction where the interface
actually lies, since an interface is bounded by its junction line — except for the frame's
own two junction-edge endpoints, whose membership junction_endpoints decides explicitly. See
"The two junction-edge endpoints" in this module's docstring for why that exception is a
correctness fix and not a tuning knob.
Failure modes, all of which fall back to the chord reading for that face and are counted in
the returned report: fewer than _SAMPLES_PER_PARAMETER * 5 interface vertices in the
window, a rank-deficient design matrix, or a non-finite curvature.
Determinism: the walk, the k-d trees and the least-squares solve are all deterministic
functions of mesh.v, mesh.f and the four keyword arguments; there is no random seed
anywhere in this module.
The two window factors are measurement affordances, not tuning knobs, and no production path
can reach them. They default to the module constants, rotations_for_reading does not
forward them, and no reading selector, inference entry point or public convenience function
exposes them; a caller who wants a different window has to name this function and say so. They
exist because measuring what the window costs otherwise requires re-implementing the fit
outside this module, and two implementations of one fit diverge. Called at the defaults the
function is bitwise what it was before the parameters existed — the same float in every
entry of the same map — and tests/test_second_order_angle_reading.py executes that identity
rather than asserting it. Choosing a window on any quantity an arm is judged by would be the
tuning this module's own docstring prohibits; a window may be chosen only on meshes that are
not being judged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
the mesh to read, in any length unit. The only mesh-derived argument: see "What enters the fit" in the module docstring. No reference mesh, target angle, tension or pressure can reach this function. |
required |
window_factor
|
float
|
transverse window, as a multiple of the incident face's
own in-plane lever. Keyword-only. Defaults to |
QUADRIC_WINDOW_FACTOR
|
axial_factor
|
float
|
extent along the junction edge, as a multiple of the
transverse window. Keyword-only. Defaults to |
QUADRIC_AXIAL_FACTOR
|
junction_endpoints
|
str
|
one of |
JUNCTION_ENDPOINTS
|
fit_constant_term
|
bool
|
fit the quadric's constant term. Keyword-only. Defaults
to |
FIT_CONSTANT_TERM
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Returns:
| Type | Description |
|---|---|
tuple[dict[tuple[tuple[int, int], int], float], AngleReadingReport]
|
tuple[dict[tuple[tuple[int, int], int], float], AngleReadingReport]: the rotation angles
in radians keyed by |
rotations_for_reading ¶
rotations_for_reading(mesh: DcelData, reading: str) -> tuple[dict[tuple[tuple[int, int], int], float] | None, AngleReadingReport]
Dispatch on the reading selector, with a per-mesh cache for the quadric fit.
The quadric fit costs one k-d tree query and one 5-parameter least-squares solve per
incident trijunction face — measured at roughly 0.5 s on a 47-case benchmark mesh, which is
the same order as one inference. Every angle-reading tension method calls
compute_angles_tri once, so a benchmark that runs eight methods over one mesh would pay
for the fit eight times. The result is cached on the mesh instance under
_angle_reading_rotations.
Cache validity. DcelData never mutates v or f after __init__, so the cache
cannot go stale through any public method of this package. If you mutate those arrays
yourself, del mesh._angle_reading_rotations first.
The cache is shared by "quadric" and "oriented_quadric": the rotations depend only on
the mesh, and whether a wedge is then reported oriented is decided downstream, in
foambryo.geometry. Only the returned report's reading label differs.
This function does not forward a window. It calls quadric_angle_rotations with no window
arguments, so every reading reachable through a reading selector uses
QUADRIC_WINDOW_FACTOR and QUADRIC_AXIAL_FACTOR. A measurement that wants another window
calls quadric_angle_rotations itself; it may then seed this function's cache by assigning
the pair to mesh._angle_reading_rotations, and the report it stores names the window it
used, so a result read that way says so rather than looking like production's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
DcelData
|
the mesh to read. |
required |
reading
|
str
|
one of |
required |
Returns:
| Type | Description |
|---|---|
tuple[dict[tuple[tuple[int, int], int], float] | None, AngleReadingReport]
|
tuple[dict | None, AngleReadingReport]: the rotation map ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |