#[non_exhaustive]pub struct ScaleTolerancePolicy {
pub id: &'static str,
pub relative_orthogonality: f64,
pub equal_axis: f64,
pub common_factor: f64,
pub singular_determinant_relative: f64,
pub scalar_absolute: f64,
pub scalar_relative: f64,
pub rotation_residual_radians: f64,
pub postcondition_unit_scale_residual: f64,
pub proof_sample_work_budget: u64,
pub f32_rounding_ulps: u32,
}Expand description
Fixed Appendix D tolerance identity and thresholds. Classification and
proof share this one versioned policy and compute in f64, narrowing
only at the writer model boundary. There is exactly one supported
instance, ScaleTolerancePolicy::APPENDIX_D_V6: a policy change is a
new policy identity, not a runtime knob.
The superseded v5 identity is deliberately not retained as an alias:
use animsmith_core::ScaleTolerancePolicy;
let _ = ScaleTolerancePolicy::APPENDIX_D_V5;Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.id: &'static strStable policy identity recorded in producer evidence.
relative_orthogonality: f64Relative orthogonality tolerance for rejecting shear.
equal_axis: f64Relative tolerance for equal-length affine columns (uniform scale).
common_factor: f64Relative tolerance for one common factor across an affected domain.
This is the normative input band: it is what an operator’s declared
factor is judged against, and
Self::postcondition_unit_scale_residual is derived from it so that
a plan this band accepts is guaranteed to produce a candidate that
satisfies the unit-scale postcondition.
singular_determinant_relative: f64abs(det) <= singular_determinant_relative * product(axis_lengths)
classifies a linear part as singular.
scalar_absolute: f64Absolute term of the scalar/vector comparison tolerance.
scalar_relative: f64Relative term of the scalar/vector comparison tolerance.
rotation_residual_radians: f64Maximum shortest-path rotation residual, in radians.
postcondition_unit_scale_residual: f64Maximum postcondition unit-scale residual, measured per axis
(L-infinity) as max(|scale_axis - 1|) — not as an L2 norm over the
three axes.
The norm is normative, and it is the same dimensionless per-axis
relative quantity Self::common_factor and Self::equal_axis
measure, so the input band and this postcondition are directly
commensurable (DESIGN.md Appendix D §D.1). This value is derived
from Self::common_factor rather than declared independently: see
Self::APPENDIX_D_V6 for the composition argument and
Self::UNIT_SCALE_BANDS for the multiplier.
proof_sample_work_budget: u64Maximum sampled proof work prove_scale will perform, in
per-sample-time work units.
Total work is sample_time_count * per_sample_work_units, where the
per-sample cost counts every pass the sampled obligations actually
make — bones, skin slots, and skinned vertices, each once per document
side. See the private per_sample_work_units for the exact formula
and for why the slot term cannot be folded into either of the other
two. A
document above this budget is refused with
ScaleError::ProofSamplingBudgetExceeded before any sampling
runs; proof never silently samples a subset.
This is part of the versioned policy identity, not a per-run flag — DESIGN.md Appendix D §D.6/§D.7 forbid per-run tolerance knobs, and a budget that changed per run would make two evidence records carrying the same policy id describe different amounts of checking.
f32_rounding_ulps: u32How many binary32 ulps of operand magnitude an obligation that
compares f32-rounded arithmetic may deviate by, on top of
Self::scalar_absolute and Self::scalar_relative.
The term this multiplies is absolute, not relative: it is
f32_rounding_ulps * magnitude * f32::EPSILON where magnitude is
the largest quantity the compared arithmetic passed through, not the
quantity being compared. Where the compared value is that largest
quantity the term adds 4 * 2^-23 = 4.77e-7 of it — twenty times
less than Self::scalar_relative already allows — so it cannot
loosen the obligations it applies to in their own regime.
It exists for the regime where the two diverge. A rotation can make
the compared quantity orders of magnitude smaller than the operands
it was computed from while it still carries those operands’ absolute
rounding error: a bound component near zero on a mesh 4000 units
across, a near-identity W * B whose translation column cancelled two
3190-magnitude terms, or a world translation whose parent chain
cancelled two of them one composition earlier. A purely relative band
is then derived from the small number and the error from the large
one, and prove_scale refuses a correct candidate that
plan_scale accepted. See Self::APPENDIX_D_V6 for the
measurement this count comes from and DESIGN.md Appendix D §D.1 for
which magnitude each obligation takes it from.
The count is only as meaningful as that magnitude. Two revisions of
this policy have now found the base wrong rather than the count too
small — first the skinned extent alone, which missed the W * B
composition, then abs(W) * abs(B) alone, which missed what W’s own
parent chain had already cancelled — and in both the measured excess
was hundreds of thousands of ulps, not a factor of two. A residual
above this count is evidence about the base before it is evidence
about the count.
Implementations§
Source§impl ScaleTolerancePolicy
impl ScaleTolerancePolicy
Sourcepub const UNIT_SCALE_BANDS: f64 = 4.0
pub const UNIT_SCALE_BANDS: f64 = 4.0
How many Self::common_factor bands
Self::postcondition_unit_scale_residual is derived from.
Three of them are analytic and one is float headroom; see
Self::APPENDIX_D_V6.
Sourcepub const APPENDIX_D_V6: Self
pub const APPENDIX_D_V6: Self
The only supported tolerance policy: DESIGN.md Appendix D, version 6.
Version 6 supersedes appendix-d-v5, which superseded v4, v3, v2 and
v1. Each identity change is a change of meaning, not a retune:
Self::postcondition_unit_scale_residualis a per-axis (L-infinity) residual derived fromSelf::common_factor, instead of v1’s independently declared1e-5L2 norm over three axes. Under v1 the two were incommensurable, and a source whose observed factor had relative erroreproduced a postcondition residual ofsqrt(3) * e, so everyein(5.77e-6, 1e-5]was accepted byplan_scaleand then rejected byprove_scale.Self::proof_sample_work_budgetbounds the sampled proof work a document may demand.Self::f32_rounding_ulpsis new in v3, and adds an absolutef32-rounding term to the five obligations that comparef32-rounded arithmetic against a base that a rotation can make arbitrarily smaller than the operands the arithmetic ran on —ProofResidualKind::Bounds,ProofResidualKind::SkinMatrix,ProofResidualKind::UnaffectedInverseBind,ProofResidualKind::RestTranslation, andProofResidualKind::Trajectory. Without itplan_scaleaccepts andprove_scalerefuses a correct candidate whenevermagnitude / componentis large.- v4 widens finite non-negative weight normalization and accumulation to binary64, makes the Bounds magnitude a weight-proportional combination of each influence’s transform and slot-composition provenance, and removes v3’s blended-point L2 stage. Bounds residuals are per axis, and the normalized blend is already bounded by those weighted operands.
- v5 makes parent-chain translation provenance additive per composed
link, in binary64, instead of taking a depth-independent maximum.
Each spatial row carries the new local contribution plus a parent
term capped by
contribution / EPSILON. This provisions the smaller of one parent-scale ulp and losing the entire contribution, so zero and underflowed descendants cannot charge the same translated parent repeatedly. Only the three spatial output rows participate; the affine homogeneous row contributes zero under this cap. The same recurrence constructs rest and sampled poses, and its result reaches RestTranslation, Trajectory, SkinMatrix and Bounds through their existing consumers. - v6 changes only the association of the shared affine axis-length mean: the three finite widened lengths are sorted ascending before the ordinary sum and division by three. This removes authored-column order from the classifier, planning, and proof witness without changing any numeric threshold, the v5 parent-chain provenance recurrence, or the evidence schema.
postcondition_unit_scale_residual is
UNIT_SCALE_BANDS * common_factor = 4e-5, rounded up to the next
power of two, 2^-14 = 6.103515625e-5. That value is also
512 * 2^-23, and so lies on the binary32 mantissa grid the
composed-scale measurement lives on. Landing on that grid is what
makes §D.1’s inclusive “at most” reachable for this obligation: the
measured residual near unit magnitude is always an integer multiple of
2^-23, so a bound off that grid could never be met with equality and
would be an exclusive bound wearing an inclusive name.
The four bands are:
- one for
ScaleError::FactorMismatch, which binds the domain’s observed common factors_0to the caller’s declared factors_declared; - one for
ScaleError::MixedFactor, which binds each affected node’s observed factors_itos_0; - one for
AffineDomainViolation::NonUniformScale, which binds each individual axis of nodeitos_i; and - one reserved as headroom for the
f32world-matrix composition and decomposition that produces the measured composed scale.
The first three compose, and the third is easy to miss: s_i is the
average of node i’s three world axis lengths (the affine
classifier returns that average), while the postcondition measures an
individual
axis, and the equal-axis check permits each axis its own further band
away from that average. The candidate’s composed scale on axis k of
node i is axis_ik / s_declared, and each of the three bands is
stated relative to max of its operands, so each contributes at most
c / (1 - c) when re-expressed relative to the smaller one. The
analytic worst case is therefore (1 - c)^-3 - 1 = 3.00006e-5 for
c = 1e-5.
Three bands rounded up (2^-15 = 3.0517578125e-5) would leave that
worst case only 4 binary32 ulps of room — 2^-15 - 3.00006e-5 = 5.17e-7 = 4.34 * 2^-23 — which is not headroom for a float
measurement, it is a rounding artefact. A fourth band makes the
reserved-headroom claim above true rather than aspirational, and it
does not blunt the obligation: every build defect this check exists to
catch — a dropped rebase, a factor applied twice, a stale no-op — is
>= 1e-3, so 6.1e-5 still leaves better than a 16x detection
margin.
Sourcepub fn observed_factor_divergence_ceiling(&self) -> f64
pub fn observed_factor_divergence_ceiling(&self) -> f64
The expected ceiling on ScaleProof::observed_factor_divergence:
Self::common_factor plus
Self::postcondition_unit_scale_residual, 7.103515625e-5 under
Self::APPENDIX_D_V6.
For rest/bind, ScalePlan::observed_factor and
ScaleProof::observed_factor are independent witnesses measured from
genuinely different state — the raw source projection composed through
parent_source_node_index, and the normalized skeleton composed
through world_rest_matrices. Their independence is the point. For
whole-document conversion both fields are the declared factor, so their
divergence is exactly zero. For rest/bind, the sum comes from:
- planning binds its witness to the caller’s declared factor within
Self::common_factor, or refuses withScaleError::FactorMismatch; and - for a candidate the internal reference builder produced from the
source
under proof, that candidate’s composed root scale is the proof
witness divided by the declared factor, so the unit-scale
postcondition binds the proof witness to the declared factor within
Self::postcondition_unit_scale_residual.
The two bands are not stated the same way, and the sum is a ceiling
only up to that difference. Planning’s is relative to the max of its
two operands, exactly as this divergence is. The postcondition’s is not
a relative band on the two witnesses at all: it is an absolute
L-infinity deviation from 1 on the candidate’s composed scale, and
that candidate’s scale is the proof witness rebased by the declared
factor — so what it bounds is |proved - declared| as a fraction of
the declared factor, not as a fraction of max(planned, proved).
Reported, not enforced, and expected rather than proved. Nothing
refuses a document for exceeding this. The second step above holds for
a candidate this module built from the source it is being proved
against, which prove_scale deliberately does not require, and it
costs the binary32 rounding of the rebase on the way — so the sum is
the ceiling the design guarantees, not a bound proved to the last ulp.
A divergence beyond it means the two witnesses were composed from state
that does not agree — most often differing stored transforms, since
crate::model::SourceNodeAsset::local_rest and
crate::model::Bone::rest are separately stored descriptions of the
same rest pose. It is not evidence of disagreeing parent chains: under
crate::model::SourceSkeletonCoverage::Complete coverage the two
chains are required to describe the same tree, and every entry point in
this module refuses a document where they do not.
Derived from two bands this policy already declares rather than introduced as a third, so it adds no tolerance and no policy identity — and a consumer of the evidence record does not have to sum two separate policy fields to know what the recorded divergence means.
Sourcepub fn scalar_tolerance(&self, before: f64, after: f64) -> f64
pub fn scalar_tolerance(&self, before: f64, after: f64) -> f64
abs_error <= scalar_absolute + scalar_relative * max(abs(before), abs(after)).
Every proof call site must pass the actual before/after magnitudes of the specific residual being checked — never a proxy such as the plan’s declared factor — so a residual near a large coordinate gets a correspondingly looser absolute tolerance than one near a small coordinate.
Sourcepub fn f32_rounded_tolerance(
&self,
before: f64,
after: f64,
magnitude: f64,
) -> f64
pub fn f32_rounded_tolerance( &self, before: f64, after: f64, magnitude: f64, ) -> f64
Self::scalar_tolerance plus Self::f32_rounding_ulps binary32
ulps of magnitude.
magnitude is the largest quantity the compared f32 arithmetic
passed through — never the quantity being compared, which is what
before/after already carry. The two coincide for a comparison
whose operands are its own magnitude, and diverge without limit for
one whose result was made small by cancellation; DESIGN.md Appendix D
§D.1 names the magnitude each obligation takes.
The added term is absolute in magnitude and so cannot widen a
comparison relative to its own operands: at magnitude == max(before, after) it is f32_rounding_ulps * 2^-23 = 4.77e-7 of
them, against the 1e-5 Self::scalar_relative already allows.
Trait Implementations§
Source§impl Clone for ScaleTolerancePolicy
impl Clone for ScaleTolerancePolicy
Source§fn clone(&self) -> ScaleTolerancePolicy
fn clone(&self) -> ScaleTolerancePolicy
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more