Skip to main content

ScaleTolerancePolicy

Struct ScaleTolerancePolicy 

Source
#[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
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§id: &'static str

Stable policy identity recorded in producer evidence.

§relative_orthogonality: f64

Relative orthogonality tolerance for rejecting shear.

§equal_axis: f64

Relative tolerance for equal-length affine columns (uniform scale).

§common_factor: f64

Relative 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: f64

abs(det) <= singular_determinant_relative * product(axis_lengths) classifies a linear part as singular.

§scalar_absolute: f64

Absolute term of the scalar/vector comparison tolerance.

§scalar_relative: f64

Relative term of the scalar/vector comparison tolerance.

§rotation_residual_radians: f64

Maximum shortest-path rotation residual, in radians.

§postcondition_unit_scale_residual: f64

Maximum 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: u64

Maximum 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: u32

How 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

Source

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.

Source

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:

  1. Self::postcondition_unit_scale_residual is a per-axis (L-infinity) residual derived from Self::common_factor, instead of v1’s independently declared 1e-5 L2 norm over three axes. Under v1 the two were incommensurable, and a source whose observed factor had relative error e produced a postcondition residual of sqrt(3) * e, so every e in (5.77e-6, 1e-5] was accepted by plan_scale and then rejected by prove_scale.
  2. Self::proof_sample_work_budget bounds the sampled proof work a document may demand.
  3. Self::f32_rounding_ulps is new in v3, and adds an absolute f32-rounding term to the five obligations that compare f32-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, and ProofResidualKind::Trajectory. Without it plan_scale accepts and prove_scale refuses a correct candidate whenever magnitude / component is large.
  4. 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.
  5. 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.
  6. 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 factor s_0 to the caller’s declared factor s_declared;
  • one for ScaleError::MixedFactor, which binds each affected node’s observed factor s_i to s_0;
  • one for AffineDomainViolation::NonUniformScale, which binds each individual axis of node i to s_i; and
  • one reserved as headroom for the f32 world-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.

Source

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 with ScaleError::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.

Source

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.

Source

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

Source§

fn clone(&self) -> ScaleTolerancePolicy

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for ScaleTolerancePolicy

Source§

impl Debug for ScaleTolerancePolicy

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for ScaleTolerancePolicy

Source§

fn eq(&self, other: &ScaleTolerancePolicy) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for ScaleTolerancePolicy

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.