Class MathTolerance
- Namespace
- MathAssertions
- Assembly
- MathAssertions.dll
Tolerance-comparison helpers for floating-point primitives and selected System.Numerics types. NaN-aware, infinity-aware, allocation-free. Callable from any test framework or production code.
public static class MathTolerance
- Inheritance
-
MathTolerance
- Inherited Members
Remarks
The MathAssertions.TUnit adapter delegates to these helpers from its
[GenerateAssertion] extensions; consumers' own [GenerateAssertion]
extensions on private types call them directly to inherit the same semantics.
0.1.0 Cluster 1 covers the tolerance primitives: NaN- and infinity-aware absolute
comparison for double/float, ULP-distance equality,
combined relative+absolute tolerance, finiteness predicates, probability/percentage
range checks, and an invertible-transformation roundtrip-identity helper. 0.1.0
Cluster 2 adds System.Numerics compounds: Vector2,
Vector3, Vector4, Quaternion (component-wise
plus rotational equivalence), Matrix4x4, Plane
(component-wise plus geometric equivalence under the (n, d) = (-n, -d)
sign-flip symmetry), Complex, ReadOnlySpan<T> for
double and float, and a generic
System.Numerics.Tensors.ReadOnlyTensorSpan<T> overload for any
INumber<TSelf>.
Methods
HasAxisAngleApproximately(Quaternion, Vector3, double, double)
Returns true when value, viewed as a rotation in
axis-angle form, is rotationally equivalent (within tolerance) to the
rotation expectedAngleDegrees degrees around expectedAxis.
public static bool HasAxisAngleApproximately(Quaternion value, Vector3 expectedAxis, double expectedAngleDegrees, double tolerance)
Parameters
valueQuaternionThe rotation under test.
expectedAxisVector3The expected rotation axis. Normalized internally; must have finite and non-trivially-zero squared length.
expectedAngleDegreesdoubleThe expected rotation magnitude in degrees.
tolerancedoubleDot-product tolerance on the unit-quaternion forms of
valueand the materialised expected rotation: the comparison passes when|dot(unit_value, unit_expected)| >= 1 - tolerance. Must be non-negative and not NaN. See the calibration note in the remarks for the relationship to an equivalent angular tolerance.
Returns
- bool
true if
valueis rotationally equivalent to the requested axis-angle rotation within tolerance.
Remarks
The comparison is performed on the rotational-equivalence metric, not on axis-component
or angle-magnitude differences. The expected axis-angle pair is materialised as a
quaternion via CreateFromAxisAngle(Vector3, float); that
quaternion is then compared to value through
IsRotationallyEquivalent(Quaternion, Quaternion, double), which evaluates
|dot(unit_a, unit_b)| >= 1 - tolerance. Going through the dot-product metric
handles every edge case the per-component extraction would otherwise stumble on
uniformly: the SO(3) q vs -q double cover; the 180-degree boundary where
(axis, +180), (axis, -180), and (-axis, +-180) all encode the same
rotation; non-unit inputs via the internal normalization both inputs receive.
Identity-rotation edge case: when expectedAngleDegrees is approximately
zero the expected quaternion is approximately the identity, and the comparison reduces
to value being approximately the identity rotation. The expected
axis must still be non-degenerate so a deliberate zero-axis input is rejected.
Tolerance calibration: tolerance is a dot-product tolerance. For small
dot-product tolerances t, the corresponding angular tolerance between the two
rotations is approximately 2 * arccos(1 - t) radians, which for small t
linearises to roughly 2 * sqrt(2 * t) radians; for example t = 1e-4 admits
rotations that differ by up to ~1.62 degrees. Callers used to angle-difference tolerance
should pick tolerance accordingly.
Reference: Hanson, Visualizing Quaternions, §4.6.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.- ArgumentException
expectedAxishas a non-finite component, or its squared length underflows during normalization (no axis is defined).
HasRoundtripIdentity(double, Func<double, double>, Func<double, double>, double)
Returns true when inverse(forward(x)) equals x
within tolerance. Verifies the roundtrip identity for invertible
transformations such as Sin/Asin, Log/Exp, encode/decode pairs, or
degree/radian conversions.
public static bool HasRoundtripIdentity(double x, Func<double, double> forward, Func<double, double> inverse, double tolerance)
Parameters
xdoubleValue to roundtrip.
forwardFunc<double, double>Forward transformation, applied to
x.inverseFunc<double, double>Inverse transformation, applied to the forward result.
tolerancedoubleMaximum allowed absolute difference between
xandinverse(forward(x)). Must be non-negative and not NaN.
Returns
Remarks
The double overload covers the dominant case. Consumers needing the same check on
other types (vectors, quaternions, custom domain types) compose their own predicate
out of the type-specific IsApproximatelyEqual overload, typically inside a
[GenerateAssertion] extension on their own type. Keeping the core surface
double-only avoids locking a generic equality shape into the public API at 0.1.0.
NaN-tolerant equality is delegated to IsApproximatelyEqual(double, double, double); both NaN counts as equal, which is appropriate for transformations whose domain includes NaN as a meaningful "no value" sentinel.
Exceptions
- ArgumentNullException
forwardorinverseis null.- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(double, double, double)
NaN-aware, infinity-aware tolerance comparison for two doubles.
public static bool IsApproximatelyEqual(double a, double b, double tolerance)
Parameters
adoubleFirst value.
bdoubleSecond value.
tolerancedoubleMaximum allowed absolute difference. Must be non-negative and not NaN.
Returns
Remarks
Both NaN: returns true. One NaN: returns false.
Same-sign infinity: returns true. Opposite-sign infinity: returns
false. Both finite: returns Math.Abs(a - b) <= tolerance.
Mirrors TUnit's IsCloseTo primitive semantics.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Complex, Complex, double)
Component-wise tolerance comparison for two Complex values across real and imaginary parts.
public static bool IsApproximatelyEqual(Complex a, Complex b, double tolerance)
Parameters
aComplexFirst complex number.
bComplexSecond complex number.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Matrix4x4, Matrix4x4, double)
Element-wise tolerance comparison for two Matrix4x4 values across all sixteen elements. Elements widen to double before comparing against the caller's double tolerance.
public static bool IsApproximatelyEqual(Matrix4x4 a, Matrix4x4 b, double tolerance)
Parameters
aMatrix4x4First matrix.
bMatrix4x4Second matrix.
tolerancedoubleMaximum allowed absolute difference per element.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Plane, Plane, double)
Component-wise tolerance comparison for two Plane values across
Normal and D. Distinguishes a plane from
its sign-flipped representation; for geometric equivalence (where (n, d)
and (-n, -d) describe the same point set), use
IsGeometricallyEquivalent(Plane, Plane, double) instead.
public static bool IsApproximatelyEqual(Plane a, Plane b, double tolerance)
Parameters
aPlaneFirst plane.
bPlaneSecond plane.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Quaternion, Quaternion, double)
Component-wise tolerance comparison for two Quaternion values across
X, Y, Z, W. Distinguishes the quaternion q from -q; for rotational
equivalence (where q and -q represent the same rotation), use
IsRotationallyEquivalent(Quaternion, Quaternion, double) instead.
public static bool IsApproximatelyEqual(Quaternion a, Quaternion b, double tolerance)
Parameters
aQuaternionFirst quaternion.
bQuaternionSecond quaternion.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Vector2, Vector2, double)
Component-wise tolerance comparison for two Vector2 values. Components widen to double before comparing against the caller's double tolerance; see the Vector3 overload for the full precision-preservation rationale.
public static bool IsApproximatelyEqual(Vector2 a, Vector2 b, double tolerance)
Parameters
aVector2First vector.
bVector2Second vector.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
- bool
true if every component is approximately equal under IsApproximatelyEqual(double, double, double) semantics.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Vector3, Vector3, double)
Component-wise tolerance comparison for two Vector3 values.
public static bool IsApproximatelyEqual(Vector3 a, Vector3 b, double tolerance)
Parameters
aVector3First vector.
bVector3Second vector.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
- bool
true if every component is approximately equal under IsApproximatelyEqual(double, double, double) semantics.
Remarks
Component values widen to double before the per-axis comparison so the
caller's double tolerance is honored at full precision. Casting the tolerance
down to float instead would discard up to 22 bits of mantissa for tight
tolerances such as 1e-9 and produce surprising near-equal-on-every-component
results; the fix is a load-bearing semantic for the rest of the assertion family.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(Vector4, Vector4, double)
Component-wise tolerance comparison for two Vector4 values. Components widen to double before comparing against the caller's double tolerance; see the Vector3 overload for the full precision-preservation rationale.
public static bool IsApproximatelyEqual(Vector4 a, Vector4 b, double tolerance)
Parameters
aVector4First vector.
bVector4Second vector.
tolerancedoubleMaximum allowed absolute difference per component.
Returns
- bool
true if every component is approximately equal under IsApproximatelyEqual(double, double, double) semantics.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(ReadOnlySpan<double>, ReadOnlySpan<double>, double)
Element-wise tolerance comparison for two double spans. A length mismatch returns false rather than throwing; explicit length validation belongs at higher layers (for example the array-overload extensions in the TUnit adapter, which throw on null inputs and surface length-mismatch as a dedicated assertion failure).
public static bool IsApproximatelyEqual(ReadOnlySpan<double> a, ReadOnlySpan<double> b, double tolerance)
Parameters
aReadOnlySpan<double>First span.
bReadOnlySpan<double>Second span.
tolerancedoubleMaximum allowed absolute difference per element.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(ReadOnlySpan<float>, ReadOnlySpan<float>, float)
Element-wise tolerance comparison for two float spans. See the double span overload for length-mismatch semantics.
public static bool IsApproximatelyEqual(ReadOnlySpan<float> a, ReadOnlySpan<float> b, float tolerance)
Parameters
aReadOnlySpan<float>First span.
bReadOnlySpan<float>Second span.
tolerancefloatMaximum allowed absolute difference per element.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual(float, float, float)
NaN-aware, infinity-aware tolerance comparison for two floats. Same semantics as the double overload; the float-typed signature lets callers stay in single precision when that is the natural value type.
public static bool IsApproximatelyEqual(float a, float b, float tolerance)
Parameters
afloatFirst value.
bfloatSecond value.
tolerancefloatMaximum allowed absolute difference. Must be non-negative and not NaN.
Returns
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsApproximatelyEqual<T>(ReadOnlyTensorSpan<T>, ReadOnlyTensorSpan<T>, T)
Element-wise tolerance comparison for two System.Numerics.Tensors.ReadOnlyTensorSpan<T> values for any INumber<TSelf>. Shape mismatch (different rank or different per-dimension length) returns false; only when both shapes match are the elements iterated and compared.
public static bool IsApproximatelyEqual<T>(ReadOnlyTensorSpan<T> a, ReadOnlyTensorSpan<T> b, T tolerance) where T : INumber<T>
Parameters
aReadOnlyTensorSpan<T>First tensor span.
bReadOnlyTensorSpan<T>Second tensor span.
toleranceTMaximum allowed absolute difference per element. Must be non-negative and not NaN.
Returns
Type Parameters
TElement type, any INumber<TSelf>.
Remarks
NaN handling matches the floating-point scalar overloads: both NaN at the same
position counts as equal; one NaN counts as unequal. For integer T
the NaN branches are unreachable because IsNaN(TSelf)
returns false for non-floating-point representations.
Iteration uses the tensor span's own enumerator so strided (non-dense) shapes produced by tensor slicing work correctly.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsCloseInUlps(double, double, long)
ULP-distance equality for two doubles. Returns true when the two
values are within ulpDistance representable doubles of each other
under IEEE 754.
public static bool IsCloseInUlps(double a, double b, long ulpDistance)
Parameters
adoubleFirst value.
bdoubleSecond value.
ulpDistancelongMaximum allowed number of representable doubles between
aandb. Must be non-negative.
Returns
Remarks
ULP ("unit in the last place") distance is the count of representable floats between two values. It is the natural metric for "as close as floating-point can express," scaling with the magnitude of the values without the caller picking an absolute tolerance. Values with opposite signs are never within any finite ULP distance and always compare as false; positive and negative zero are treated as equal.
Reference: Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, ACM Computing Surveys, 1991.
Exceptions
- ArgumentOutOfRangeException
ulpDistanceis negative.
IsCloseInUlps(float, float, int)
ULP-distance equality for two floats. Returns true when the two
values are within ulpDistance representable floats of each other
under IEEE 754. See the double overload for full semantics.
public static bool IsCloseInUlps(float a, float b, int ulpDistance)
Parameters
afloatFirst value.
bfloatSecond value.
ulpDistanceintMaximum allowed number of representable floats between
aandb. Must be non-negative.
Returns
Exceptions
- ArgumentOutOfRangeException
ulpDistanceis negative.
IsFinite(double)
Returns true when v is finite (neither NaN nor
infinite). Mirrors IsFinite(double) as a one-stop predicate
alongside the other tolerance helpers, useful in fluent assertion chains where the
caller wants a single namespace.
public static bool IsFinite(double v)
Parameters
vdoubleValue to check.
Returns
IsFinite(float)
Returns true when v is finite (neither NaN nor
infinite). Mirrors IsFinite(float).
public static bool IsFinite(float v)
Parameters
vfloatValue to check.
Returns
IsGeometricallyEquivalent(Plane, Plane, double)
Returns true when two planes describe the same set of points in
3-space within tolerance. A plane (n, d) and its sign-flipped counterpart
(-n, -d) describe the same geometric plane; this method treats them as
equivalent, while IsApproximatelyEqual(Plane, Plane, double) does not.
public static bool IsGeometricallyEquivalent(Plane a, Plane b, double tolerance)
Parameters
aPlaneFirst plane.
bPlaneSecond plane.
tolerancedoubleMaximum allowed absolute difference per component for either the matching or the flipped representation. Must be non-negative and not NaN.
Returns
- bool
true if the two planes describe the same point set within the requested tolerance under either sign convention.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
IsNonNegativeFinite(double)
Returns true when v is finite and non-negative
(zero is allowed). The natural domain check for magnitudes, distances, durations,
counts cast to double, and similar non-negative quantities.
public static bool IsNonNegativeFinite(double v)
Parameters
vdoubleValue to check.
Returns
IsPercentage(double)
Returns true when v is finite and in the closed
interval [0, 100]. The standard percentage-domain check.
public static bool IsPercentage(double v)
Parameters
vdoubleValue to check.
Returns
IsPoseApproximatelyEqual(Vector3, Quaternion, Vector3, Quaternion, double, double)
Returns true when two rigid poses (a position and an orientation) match
within separate tolerances: the Euclidean PositionDistance(Vector3, Vector3)
is at most positionTolerance AND the geodesic
RotationAngleDegrees(Quaternion, Quaternion) is at most
rotationToleranceDegrees. Position and orientation carry different units
(typically metres vs degrees), so a single shared tolerance would be wrong; the two are
supplied and evaluated independently.
public static bool IsPoseApproximatelyEqual(Vector3 actualPosition, Quaternion actualOrientation, Vector3 expectedPosition, Quaternion expectedOrientation, double positionTolerance, double rotationToleranceDegrees)
Parameters
actualPositionVector3The actual pose translation.
actualOrientationQuaternionThe actual pose rotation.
expectedPositionVector3The expected pose translation.
expectedOrientationQuaternionThe expected pose rotation.
positionTolerancedoubleMaximum allowed Euclidean position distance. Non-negative, not NaN.
rotationToleranceDegreesdoubleMaximum allowed geodesic rotation angle in degrees. Non-negative, not NaN.
Returns
Remarks
Degenerate orientation: a quaternion that is not normalizable (squared magnitude underflows
to zero, e.g. default(Quaternion) = (0, 0, 0, 0)) has no defined rotation axis,
so the geodesic angle from
RotationAngleDegrees(Quaternion, Quaternion) is undefined. Rather than fail a
legitimate default-pose round-trip, the orientation half falls back to a componentwise
comparison against a tight fixed tolerance
(MathAssertions.MathTolerance.DegenerateOrientationComponentTolerance), independent of
rotationToleranceDegrees. Reusing the degrees value would be far too
permissive (a tolerance of 1 or more would accept a zero quaternion as equal to a real
rotation). So (0, 0, 0, 0) compared with itself passes, while (0, 0, 0, 0)
compared with any real rotation fails on the component deltas regardless of the angular tolerance.
Exceptions
- ArgumentOutOfRangeException
Either tolerance is NaN or negative.
IsProbability(double)
Returns true when v is finite and in the closed
interval [0, 1]. The standard probability-domain check.
public static bool IsProbability(double v)
Parameters
vdoubleValue to check.
Returns
IsRelativelyAndAbsolutelyClose(double, double, double, double)
Combined relative + absolute tolerance check. Returns true when
|a - b| <= max(absoluteTolerance, relativeTolerance * max(|a|, |b|)).
public static bool IsRelativelyAndAbsolutelyClose(double a, double b, double relativeTolerance, double absoluteTolerance)
Parameters
adoubleFirst value.
bdoubleSecond value.
relativeTolerancedoubleRelative tolerance, applied against the larger of
|a|and|b|. Must be non-negative and not NaN.absoluteTolerancedoubleAbsolute tolerance, the floor used near zero. Must be non-negative and not NaN.
Returns
Remarks
The textbook combined-tolerance check from Knuth, The Art of Computer Programming, Vol. 2. The absolute term dominates near zero where the relative term collapses; the relative term dominates at large magnitudes where a fixed absolute tolerance would be unreasonably tight or unreasonably loose.
Both NaN compare equal; one NaN compares unequal. When either operand is infinite, the values compare equal only when they have the same bit representation (same sign of infinity); a finite and an infinite value are unequal regardless of either tolerance.
Exceptions
- ArgumentOutOfRangeException
Either tolerance is NaN or negative.
IsRotationallyEquivalent(Quaternion, Quaternion, double)
Returns true when two quaternions represent the same rotation
within tolerance, treating q and -q as equivalent (the well-known
double-cover of SO(3)).
public static bool IsRotationallyEquivalent(Quaternion a, Quaternion b, double tolerance)
Parameters
aQuaternionFirst quaternion.
bQuaternionSecond quaternion.
tolerancedoubleMaximum allowed deviation of
|dot|from1. Must be non-negative and not NaN.
Returns
Remarks
Implementation normalizes both inputs first so non-unit quaternions still produce
the correct verdict; the dot-product test
|q1 . q2| >= 1 - tolerance identifies the same-rotation condition robustly.
Reference: Hanson, Visualizing Quaternions, §4.6.
Exceptions
- ArgumentOutOfRangeException
toleranceis NaN or negative.
PositionDistance(Vector3, Vector3)
Returns the Euclidean distance between two positions, computed in double precision (components widen from float before the subtraction to avoid catastrophic cancellation near large coordinates).
public static double PositionDistance(Vector3 a, Vector3 b)
Parameters
Returns
- double
The Euclidean distance between the two positions.
RotationAngleDegrees(Quaternion, Quaternion)
Returns the geodesic angle between two rotations, in degrees: the magnitude of the single
rotation that carries one orientation onto the other. The SO(3) double cover is handled by
taking the absolute value of the relative quaternion's real part, so a quaternion and its
negation yield a zero angle. Both inputs are normalized internally; the result lies in
[0, 180].
public static double RotationAngleDegrees(Quaternion a, Quaternion b)
Parameters
aQuaternionFirst rotation.
bQuaternionSecond rotation.
Returns
- double
The geodesic angle between the two rotations in degrees, in
[0, 180]; or NaN when either input is not normalizable.
Remarks
The angle is computed from the relative rotation r = a * conj(b) as
theta = 2 * atan2(‖imag(r)‖, |real(r)|). This atan2 form is numerically
stable across the whole 0°-180° range: unlike 2 * acos(|dot|), it does not
sit on the infinite-slope shoulder of acos near dot = 1, where the residual
float error of normalizing a non-unit quaternion would otherwise be amplified into a
spurious non-zero angle (so d(q, q) measured as non-zero for nearly every real
quaternion). The relative quaternion is formed in double precision after a
double-precision normalization, so the metric is symmetric and a quaternion compared with
itself yields exactly 0.
A quaternion that is not normalizable (squared magnitude underflows to zero, e.g.
default(Quaternion) = (0, 0, 0, 0)) has no defined rotation axis, so no
geodesic angle exists; this method returns NaN in that case. Callers
that need a defined verdict for the zero quaternion (such as
IsPoseApproximatelyEqual(Vector3, Quaternion, Vector3, Quaternion, double, double))
fall back to a componentwise comparison.