Table of Contents

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

value Quaternion

The rotation under test.

expectedAxis Vector3

The expected rotation axis. Normalized internally; must have finite and non-trivially-zero squared length.

expectedAngleDegrees double

The expected rotation magnitude in degrees.

tolerance double

Dot-product tolerance on the unit-quaternion forms of value and 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 value is 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

tolerance is NaN or negative.

ArgumentException

expectedAxis has 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

x double

Value to roundtrip.

forward Func<double, double>

Forward transformation, applied to x.

inverse Func<double, double>

Inverse transformation, applied to the forward result.

tolerance double

Maximum allowed absolute difference between x and inverse(forward(x)). Must be non-negative and not NaN.

Returns

bool

true if the roundtrip recovers x within tolerance.

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

forward or inverse is null.

ArgumentOutOfRangeException

tolerance is 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

a double

First value.

b double

Second value.

tolerance double

Maximum allowed absolute difference. Must be non-negative and not NaN.

Returns

bool

true if approximately equal under the rules above.

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

tolerance is 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

a Complex

First complex number.

b Complex

Second complex number.

tolerance double

Maximum allowed absolute difference per component.

Returns

bool

true if both real and imaginary parts are approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a Matrix4x4

First matrix.

b Matrix4x4

Second matrix.

tolerance double

Maximum allowed absolute difference per element.

Returns

bool

true if every element is approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a Plane

First plane.

b Plane

Second plane.

tolerance double

Maximum allowed absolute difference per component.

Returns

bool

true if every component is approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a Quaternion

First quaternion.

b Quaternion

Second quaternion.

tolerance double

Maximum allowed absolute difference per component.

Returns

bool

true if every component is approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a Vector2

First vector.

b Vector2

Second vector.

tolerance double

Maximum allowed absolute difference per component.

Returns

bool

true if every component is approximately equal under IsApproximatelyEqual(double, double, double) semantics.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a Vector3

First vector.

b Vector3

Second vector.

tolerance double

Maximum 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

tolerance is 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

a Vector4

First vector.

b Vector4

Second vector.

tolerance double

Maximum allowed absolute difference per component.

Returns

bool

true if every component is approximately equal under IsApproximatelyEqual(double, double, double) semantics.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a ReadOnlySpan<double>

First span.

b ReadOnlySpan<double>

Second span.

tolerance double

Maximum allowed absolute difference per element.

Returns

bool

true if both spans have the same length and every element pair is approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a ReadOnlySpan<float>

First span.

b ReadOnlySpan<float>

Second span.

tolerance float

Maximum allowed absolute difference per element.

Returns

bool

true if both spans have the same length and every element pair is approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a float

First value.

b float

Second value.

tolerance float

Maximum allowed absolute difference. Must be non-negative and not NaN.

Returns

bool

true if approximately equal.

Exceptions

ArgumentOutOfRangeException

tolerance is 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

a ReadOnlyTensorSpan<T>

First tensor span.

b ReadOnlyTensorSpan<T>

Second tensor span.

tolerance T

Maximum allowed absolute difference per element. Must be non-negative and not NaN.

Returns

bool

true when shapes match and every element pair is approximately equal.

Type Parameters

T

Element 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

tolerance is 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

a double

First value.

b double

Second value.

ulpDistance long

Maximum allowed number of representable doubles between a and b. Must be non-negative.

Returns

bool

true if both NaN, both zero (regardless of sign), or within ulpDistance ULPs and same-signed.

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

ulpDistance is 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

a float

First value.

b float

Second value.

ulpDistance int

Maximum allowed number of representable floats between a and b. Must be non-negative.

Returns

bool

true if both NaN, both zero (regardless of sign), or within ulpDistance ULPs and same-signed.

Exceptions

ArgumentOutOfRangeException

ulpDistance is 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

v double

Value to check.

Returns

bool

true if v is finite.

IsFinite(float)

Returns true when v is finite (neither NaN nor infinite). Mirrors IsFinite(float).

public static bool IsFinite(float v)

Parameters

v float

Value to check.

Returns

bool

true if v is finite.

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

a Plane

First plane.

b Plane

Second plane.

tolerance double

Maximum 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

tolerance is 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

v double

Value to check.

Returns

bool

true if v is finite and >= 0.

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

v double

Value to check.

Returns

bool

true if v is finite and 0 <= v <= 100.

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

actualPosition Vector3

The actual pose translation.

actualOrientation Quaternion

The actual pose rotation.

expectedPosition Vector3

The expected pose translation.

expectedOrientation Quaternion

The expected pose rotation.

positionTolerance double

Maximum allowed Euclidean position distance. Non-negative, not NaN.

rotationToleranceDegrees double

Maximum allowed geodesic rotation angle in degrees. Non-negative, not NaN.

Returns

bool

true when both the position and the orientation are within their tolerances.

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

v double

Value to check.

Returns

bool

true if v is finite and 0 <= v <= 1.

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

a double

First value.

b double

Second value.

relativeTolerance double

Relative tolerance, applied against the larger of |a| and |b|. Must be non-negative and not NaN.

absoluteTolerance double

Absolute tolerance, the floor used near zero. Must be non-negative and not NaN.

Returns

bool

true if approximately equal under the combined rule.

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

a Quaternion

First quaternion.

b Quaternion

Second quaternion.

tolerance double

Maximum allowed deviation of |dot| from 1. Must be non-negative and not NaN.

Returns

bool

true if the two quaternions encode the same rotation within the requested tolerance.

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

tolerance is 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

a Vector3

First position.

b Vector3

Second position.

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

a Quaternion

First rotation.

b Quaternion

Second 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.