Skip to main content

la_stack/
error.rs

1#![forbid(unsafe_code)]
2
3//! Typed error categories and helpers for linear algebra operations.
4
5use core::fmt;
6
7/// Arithmetic operation associated with a computation failure.
8///
9/// This identifies both non-finite intermediates/results through
10/// [`NonFiniteOrigin::Computation`] and finite-endpoint range exhaustion through
11/// [`LaError::IntervalRangeExhausted`].
12///
13/// # Examples
14/// ```
15/// use la_stack::ArithmeticOperation;
16///
17/// assert_eq!(ArithmeticOperation::LuSolve.to_string(), "LU solve");
18/// ```
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum ArithmeticOperation {
22    /// Matrix infinity-norm calculation.
23    MatrixInfinityNorm,
24    /// Matrix symmetry validation.
25    SymmetryCheck,
26    /// LU factorization.
27    LuFactorization,
28    /// Rectangular numerical column-basis selection.
29    ColumnBasis,
30    /// LDLT factorization.
31    LdltFactorization,
32    /// Forward or backward substitution with an LU factorization.
33    LuSolve,
34    /// Forward, diagonal, or backward substitution with an LDLT factorization.
35    LdltSolve,
36    /// Determinant calculation.
37    Determinant,
38    /// Determinant error-bound calculation.
39    DeterminantErrorBound,
40    /// Outward-rounded interval addition.
41    IntervalAddition,
42    /// Exact-real subtraction enclosed by an outward-rounded interval.
43    IntervalSubtraction,
44    /// Outward-rounded interval multiplication.
45    IntervalMultiplication,
46    /// Outward-rounded interval reciprocal.
47    IntervalReciprocal,
48    /// Outward-rounded interval division.
49    IntervalDivision,
50    /// Interval Gaussian elimination and back substitution.
51    IntervalSolve,
52    /// Outward-rounded interval square.
53    IntervalSquare,
54    /// Division-free interval determinant calculation.
55    IntervalDeterminant,
56    /// Vector dot-product calculation.
57    VectorDotProduct,
58    /// `axis ยท (left - right)` without first rounding the vector difference.
59    VectorDotDifference,
60    /// Vector squared-norm calculation.
61    VectorSquaredNorm,
62    /// Overflow- and underflow-safe vector Euclidean-norm calculation.
63    VectorNorm,
64    /// Unsigned angle between two vectors.
65    VectorAngle,
66}
67
68impl fmt::Display for ArithmeticOperation {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(match self {
71            Self::MatrixInfinityNorm => "matrix infinity norm",
72            Self::SymmetryCheck => "symmetry check",
73            Self::LuFactorization => "LU factorization",
74            Self::ColumnBasis => "column basis selection",
75            Self::LdltFactorization => "LDLT factorization",
76            Self::LuSolve => "LU solve",
77            Self::LdltSolve => "LDLT solve",
78            Self::Determinant => "determinant",
79            Self::DeterminantErrorBound => "determinant error bound",
80            Self::IntervalAddition => "interval addition",
81            Self::IntervalSubtraction => "interval subtraction",
82            Self::IntervalMultiplication => "interval multiplication",
83            Self::IntervalReciprocal => "interval reciprocal",
84            Self::IntervalDivision => "interval division",
85            Self::IntervalSolve => "interval solve",
86            Self::IntervalSquare => "interval square",
87            Self::IntervalDeterminant => "interval determinant",
88            Self::VectorDotProduct => "vector dot product",
89            Self::VectorDotDifference => "vector dot difference",
90            Self::VectorSquaredNorm => "vector squared norm",
91            Self::VectorNorm => "vector Euclidean norm",
92            Self::VectorAngle => "vector angle",
93        })
94    }
95}
96
97/// Factorization whose pivot policy rejected a matrix as numerically singular.
98///
99/// # Examples
100/// ```
101/// use la_stack::FactorizationKind;
102///
103/// assert_eq!(FactorizationKind::Ldlt.to_string(), "LDLT");
104/// ```
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum FactorizationKind {
108    /// LU factorization with partial pivoting.
109    Lu,
110    /// LDLT factorization without pivoting.
111    Ldlt,
112}
113
114impl fmt::Display for FactorizationKind {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.write_str(match self {
117            Self::Lu => "LU",
118            Self::Ldlt => "LDLT",
119        })
120    }
121}
122
123/// Reason a raw tolerance cannot become a [`crate::Tolerance`].
124///
125/// # Examples
126/// ```
127/// use la_stack::prelude::*;
128///
129/// match LaError::invalid_tolerance(-1.0) {
130///     LaError::InvalidTolerance {
131///         reason: InvalidToleranceReason::Negative,
132///         ..
133///     } => {}
134///     _ => unreachable!("a finite negative tolerance has a negative reason"),
135/// }
136/// ```
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138#[non_exhaustive]
139pub enum InvalidToleranceReason {
140    /// The tolerance is finite but negative.
141    Negative,
142    /// The tolerance is NaN or positive/negative infinity.
143    NotFinite,
144}
145
146/// Endpoint of an interval constructor input.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum IntervalBound {
150    /// Lower endpoint.
151    Lower,
152    /// Upper endpoint.
153    Upper,
154}
155
156/// Operand of a binary scalar operation used to construct an interval.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum IntervalOperand {
160    /// Left operand.
161    Left,
162    /// Right operand.
163    Right,
164}
165
166/// Operand of a binary vector operation.
167///
168/// Identifies which input needs correction when [`LaError::ZeroVector`] or
169/// [`NonFiniteLocation::VectorOperandEntry`] reports an invalid direction or
170/// coordinate, including errors from [`crate::VectorAngle::angle`].
171///
172/// # Examples
173/// ```
174/// use la_stack::prelude::*;
175///
176/// match [1.0, 0.0].as_slice().angle(&[0.0, -0.0]) {
177///     Err(LaError::ZeroVector { operand, .. }) => {
178///         assert_eq!(operand, VectorOperand::Right);
179///     }
180///     _ => unreachable!("the right input is an all-zero vector"),
181/// }
182/// ```
183#[derive(Clone, Copy, Debug, PartialEq, Eq)]
184#[non_exhaustive]
185pub enum VectorOperand {
186    /// Left operand (`self` for a method).
187    Left,
188    /// Right operand (the method argument).
189    Right,
190}
191
192impl fmt::Display for VectorOperand {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        f.write_str(match self {
195            Self::Left => "left",
196            Self::Right => "right",
197        })
198    }
199}
200
201/// Location at which a non-finite value was observed.
202///
203/// # Examples
204/// ```
205/// use la_stack::prelude::*;
206///
207/// match LaError::non_finite_input_matrix(1, 2) {
208///     LaError::NonFinite {
209///         location: NonFiniteLocation::MatrixCell { row, col, .. },
210///         ..
211///     } => assert_eq!((row, col), (1, 2)),
212///     _ => unreachable!("constructor returns a matrix-cell location"),
213/// }
214/// ```
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum NonFiniteLocation {
218    /// Raw coefficient supplied to a sparse polynomial constructor.
219    #[non_exhaustive]
220    PolynomialTerm {
221        /// Zero-based position in the supplied term iterator.
222        index: usize,
223        /// Exponent supplied alongside the invalid coefficient.
224        exponent: u128,
225    },
226    /// Cell `(row, col)` in matrix-shaped storage or computation.
227    #[non_exhaustive]
228    MatrixCell {
229        /// Matrix row.
230        row: usize,
231        /// Matrix column.
232        col: usize,
233    },
234    /// Entry in a vector input.
235    #[non_exhaustive]
236    VectorEntry {
237        /// Vector index.
238        index: usize,
239    },
240    /// Entry in one of two borrowed vector operands.
241    #[non_exhaustive]
242    VectorOperandEntry {
243        /// Operand containing the non-finite coordinate.
244        operand: VectorOperand,
245        /// Coordinate index within that operand.
246        index: usize,
247    },
248    /// Indexed step in a factorization, solve, or reduction.
249    #[non_exhaustive]
250    Step {
251        /// Step index.
252        index: usize,
253    },
254    /// Endpoint supplied to an interval constructor.
255    #[non_exhaustive]
256    IntervalBound {
257        /// Bound at which the non-finite value was observed.
258        bound: IntervalBound,
259    },
260    /// Operand supplied to a binary scalar interval constructor.
261    #[non_exhaustive]
262    IntervalOperand {
263        /// Operand at which the non-finite value was observed.
264        operand: IntervalOperand,
265    },
266    /// Scalar value without a meaningful matrix or vector coordinate.
267    Scalar,
268}
269
270/// Work category limited by a polynomial determinant budget.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272#[non_exhaustive]
273pub enum PolynomialWork {
274    /// Complete structural column-subset transition envelope.
275    SubsetTransitions,
276    /// Individual nonzero rational coefficient-pair products.
277    TermProducts,
278}
279
280impl fmt::Display for PolynomialWork {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        f.write_str(match self {
283            Self::SubsetTransitions => "subset transitions",
284            Self::TermProducts => "coefficient-pair products",
285        })
286    }
287}
288
289/// Provenance of a non-finite value.
290///
291/// # Examples
292/// ```
293/// use la_stack::prelude::*;
294///
295/// let err = LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant);
296/// match err {
297///     LaError::NonFinite {
298///         origin: NonFiniteOrigin::Computation { operation, .. },
299///         ..
300///     } => assert_eq!(operation, ArithmeticOperation::Determinant),
301///     _ => unreachable!("constructor preserves computation provenance"),
302/// }
303/// ```
304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
305#[non_exhaustive]
306pub enum NonFiniteOrigin {
307    /// The caller supplied a non-finite input.
308    Input,
309    /// Finite inputs produced a non-finite arithmetic result.
310    #[non_exhaustive]
311    Computation {
312        /// Operation that produced the value.
313        operation: ArithmeticOperation,
314    },
315}
316
317/// Computed LDLT condition that violates the no-pivot positive-semidefinite
318/// factorization requirements.
319///
320/// These values are computed in binary64. They explain why LDLT rejected the
321/// matrix, but do not prove that the stored matrix is exactly indefinite.
322///
323/// # Examples
324/// ```
325/// use la_stack::prelude::*;
326///
327/// match LaError::not_positive_semidefinite_negative(1, -3.0) {
328///     LaError::NotPositiveSemidefinite {
329///         violation: PositiveSemidefiniteViolation::NegativePivot { value, .. },
330///         ..
331///     } => assert_eq!(value, -3.0),
332///     _ => unreachable!("constructor preserves the PSD violation"),
333/// }
334/// ```
335#[derive(Clone, Copy, Debug, PartialEq)]
336#[non_exhaustive]
337pub enum PositiveSemidefiniteViolation {
338    /// LDLT produced a strictly negative diagonal pivot.
339    #[non_exhaustive]
340    NegativePivot {
341        /// Observed negative pivot value.
342        value: f64,
343    },
344    /// A zero diagonal pivot still has a non-zero coupling below it.
345    #[non_exhaustive]
346    ZeroPivotCoupling {
347        /// Row containing the non-zero coupling.
348        row: usize,
349        /// Observed coupling value.
350        value: f64,
351    },
352}
353
354/// Mathematical or numerical reason a matrix was classified as singular.
355///
356/// # Examples
357/// ```
358/// use la_stack::prelude::*;
359///
360/// match LaError::singular_numerical(1, FactorizationKind::Lu, 0.0, 1e-12) {
361///     LaError::Singular {
362///         reason: SingularityReason::Numerical { factorization, .. }, ..
363///     } => assert_eq!(factorization, FactorizationKind::Lu),
364///     _ => unreachable!("constructor preserves the singularity reason"),
365/// }
366/// ```
367#[derive(Clone, Copy, Debug, PartialEq)]
368#[non_exhaustive]
369pub enum SingularityReason {
370    /// The algorithm proved that the pivot is exactly zero.
371    Exact,
372    /// A finite pivot was rejected by the factorization tolerance.
373    #[non_exhaustive]
374    Numerical {
375        /// Factorization applying the tolerance policy.
376        factorization: FactorizationKind,
377        /// Absolute magnitude of the rejected pivot.
378        pivot_magnitude: f64,
379        /// Tolerance against which the magnitude was compared.
380        tolerance: f64,
381    },
382}
383
384/// Reason an exact result cannot satisfy an exact-to-`f64` conversion contract.
385///
386/// `RequiresRounding` is recoverable when the caller is willing to opt into a
387/// rounded exact-to-`f64` API. `NotFinite` means no finite `f64` can represent
388/// the result even after rounding.
389///
390/// # Examples
391/// ```
392/// use la_stack::prelude::*;
393///
394/// let err = LaError::unrepresentable(None, UnrepresentableReason::RequiresRounding);
395/// assert!(err.requires_rounding());
396///
397/// let err = LaError::unrepresentable(None, UnrepresentableReason::NotFinite);
398/// assert_eq!(
399///     err.unrepresentable_reason(),
400///     Some(UnrepresentableReason::NotFinite)
401/// );
402/// ```
403#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404#[non_exhaustive]
405pub enum UnrepresentableReason {
406    /// A finite `f64` exists only after rounding, but the requested conversion
407    /// requires an exact binary64 representation.
408    RequiresRounding,
409    /// No finite `f64` can represent the exact value after rounding.
410    NotFinite,
411}
412
413/// Linear algebra errors.
414///
415/// This enum and each struct-style variant are `#[non_exhaustive]` so downstream
416/// matches must retain a wildcard for future error categories and fields.
417///
418/// # Examples
419/// ```
420/// use la_stack::prelude::*;
421///
422/// match LaError::singular_exact(2) {
423///     LaError::Singular {
424///         pivot_col,
425///         reason: SingularityReason::Exact,
426///         ..
427///     } => assert_eq!(pivot_col, 2),
428///     _ => unreachable!("constructor returns an exact singularity"),
429/// }
430/// ```
431#[derive(Clone, Copy, Debug, PartialEq)]
432#[non_exhaustive]
433pub enum LaError {
434    /// An exact polynomial determinant exceeded a caller-selected work limit.
435    #[non_exhaustive]
436    PolynomialBudgetExceeded {
437        /// Work category that exhausted its limit.
438        work: PolynomialWork,
439        /// Caller-selected maximum for this category.
440        limit: usize,
441        /// Completed units of this category before the rejected request.
442        completed: usize,
443        /// Additional units rejected before performing their arithmetic.
444        requested: usize,
445    },
446    /// A polynomial product needs an exponent greater than `u128::MAX`.
447    #[non_exhaustive]
448    PolynomialExponentOverflow {
449        /// Row of the matrix entry being multiplied into a partial determinant.
450        row: usize,
451        /// Column of that entry.
452        col: usize,
453        /// Partial determinant term's exponent.
454        left: u128,
455        /// Matrix entry term's exponent.
456        right: u128,
457    },
458    /// Two vector operands have unequal ambient lengths.
459    #[non_exhaustive]
460    DimensionMismatch {
461        /// Left operand length.
462        left: usize,
463        /// Right operand length.
464        right: usize,
465    },
466    /// An operation requiring a direction received empty vector storage.
467    EmptyVector,
468    /// An operation requiring a direction received an all-zero vector.
469    #[non_exhaustive]
470    ZeroVector {
471        /// Operand whose coordinates are all zero (of either sign).
472        operand: VectorOperand,
473    },
474    /// A matrix is exactly or numerically singular.
475    #[non_exhaustive]
476    Singular {
477        /// Factorization column or step where a usable pivot was unavailable.
478        pivot_col: usize,
479        /// Typed reason for the singularity classification.
480        reason: SingularityReason,
481    },
482    /// A caller input or arithmetic intermediate/result is NaN or infinite.
483    #[non_exhaustive]
484    NonFinite {
485        /// Typed location of the value.
486        location: NonFiniteLocation,
487        /// Whether the value came from input or a particular computation.
488        origin: NonFiniteOrigin,
489    },
490    /// An exact-real interval intermediate or result cannot be enclosed by
491    /// finite binary64 endpoints.
492    #[non_exhaustive]
493    IntervalRangeExhausted {
494        /// Operation whose mathematical intermediate or result exceeded the
495        /// finite interval endpoint domain.
496        operation: ArithmeticOperation,
497    },
498    /// A reciprocal or division received a divisor interval containing zero.
499    /// No finite connected enclosure is supplied, including for `0 / [0, 0]`.
500    #[non_exhaustive]
501    IntervalZeroDivisor {
502        /// Divisor's lower endpoint.
503        lower: f64,
504        /// Divisor's upper endpoint.
505        upper: f64,
506    },
507    /// Interval elimination could not certify a pivot separated from zero.
508    /// This is inconclusive, even for point inputs; it never proves singularity.
509    #[non_exhaustive]
510    IntervalSolveInconclusive {
511        /// First elimination column without a certified nonzero pivot.
512        pivot_col: usize,
513    },
514    /// An exact result cannot satisfy the requested finite-`f64` conversion.
515    #[non_exhaustive]
516    Unrepresentable {
517        /// Failed vector component, or `None` for a scalar result.
518        index: Option<usize>,
519        /// Reason the conversion contract cannot be satisfied.
520        reason: UnrepresentableReason,
521    },
522    /// A finite interval's lower bound is greater than its upper bound.
523    #[non_exhaustive]
524    InvertedInterval {
525        /// Rejected lower bound.
526        lower: f64,
527        /// Rejected upper bound.
528        upper: f64,
529    },
530    /// Exact determinant scaling overflowed the internal exponent representation.
531    #[non_exhaustive]
532    DeterminantScaleOverflow {
533        /// Matrix dimension `D`.
534        dim: usize,
535        /// Minimum decomposed binary64 exponent among non-zero entries.
536        min_exponent: i32,
537    },
538    /// A matrix algorithm or runtime dispatch helper does not support a dimension.
539    #[non_exhaustive]
540    UnsupportedDimension {
541        /// Matrix dimension requested by the caller.
542        requested: usize,
543        /// Largest matrix dimension supported by the operation.
544        max: usize,
545    },
546    /// A matrix index is outside the `Dร—D` storage domain.
547    #[non_exhaustive]
548    IndexOutOfBounds {
549        /// Requested row.
550        row: usize,
551        /// Requested column.
552        col: usize,
553        /// Matrix dimension `D`; valid indices are less than this value.
554        dim: usize,
555    },
556    /// A raw tolerance is negative or non-finite.
557    #[non_exhaustive]
558    InvalidTolerance {
559        /// Raw value supplied by the caller.
560        value: f64,
561        /// Typed reason the value violates the tolerance invariant.
562        reason: InvalidToleranceReason,
563    },
564    /// A matrix required to be symmetric has an asymmetric off-diagonal pair.
565    #[non_exhaustive]
566    Asymmetric {
567        /// Row of the upper-triangular entry.
568        row: usize,
569        /// Column of the upper-triangular entry.
570        col: usize,
571        /// Matrix dimension `D`.
572        dim: usize,
573        /// Observed entry at `(row, col)`.
574        upper: f64,
575        /// Observed entry at `(col, row)`.
576        lower: f64,
577        /// Maximum absolute difference allowed by the symmetry check.
578        allowed_abs_diff: f64,
579    },
580    /// A computed LDLT pivot violates the no-pivot positive-semidefinite
581    /// factorization requirements.
582    ///
583    /// This diagnoses a binary64 factorization rejection; it is not an exact
584    /// certificate that the stored matrix is indefinite.
585    #[non_exhaustive]
586    NotPositiveSemidefinite {
587        /// LDLT pivot column or step where the violation was detected.
588        pivot_col: usize,
589        /// Typed PSD-domain violation.
590        violation: PositiveSemidefiniteViolation,
591    },
592}
593
594impl LaError {
595    /// Construct a [`LaError::Singular`] error proving that the pivot at
596    /// `pivot_col` is exactly zero.
597    #[inline]
598    #[must_use]
599    pub const fn singular_exact(pivot_col: usize) -> Self {
600        Self::Singular {
601            pivot_col,
602            reason: SingularityReason::Exact,
603        }
604    }
605
606    /// Construct a [`LaError::Singular`] error for a pivot rejected by a
607    /// factorization tolerance, preserving the factorization, pivot magnitude,
608    /// and tolerance in [`SingularityReason::Numerical`].
609    #[inline]
610    #[must_use]
611    pub const fn singular_numerical(
612        pivot_col: usize,
613        factorization: FactorizationKind,
614        pivot_magnitude: f64,
615        tolerance: f64,
616    ) -> Self {
617        Self::Singular {
618            pivot_col,
619            reason: SingularityReason::Numerical {
620                factorization,
621                pivot_magnitude,
622                tolerance,
623            },
624        }
625    }
626
627    /// Construct a [`LaError::NonFinite`] input error located at matrix cell
628    /// `(row, col)`.
629    #[inline]
630    #[must_use]
631    pub const fn non_finite_input_matrix(row: usize, col: usize) -> Self {
632        Self::NonFinite {
633            location: NonFiniteLocation::MatrixCell { row, col },
634            origin: NonFiniteOrigin::Input,
635        }
636    }
637
638    /// Construct a [`LaError::NonFinite`] input error located at vector entry
639    /// `index`.
640    #[inline]
641    #[must_use]
642    pub const fn non_finite_input_vector(index: usize) -> Self {
643        Self::NonFinite {
644            location: NonFiniteLocation::VectorEntry { index },
645            origin: NonFiniteOrigin::Input,
646        }
647    }
648
649    /// Construct a [`LaError::NonFinite`] input error for a scalar without an
650    /// index or matrix coordinate.
651    #[inline]
652    #[must_use]
653    pub const fn non_finite_input_scalar() -> Self {
654        Self::NonFinite {
655            location: NonFiniteLocation::Scalar,
656            origin: NonFiniteOrigin::Input,
657        }
658    }
659
660    /// Construct a [`LaError::NonFinite`] input error for a specific interval
661    /// endpoint.
662    #[inline]
663    #[must_use]
664    pub const fn non_finite_input_interval_bound(bound: IntervalBound) -> Self {
665        Self::NonFinite {
666            location: NonFiniteLocation::IntervalBound { bound },
667            origin: NonFiniteOrigin::Input,
668        }
669    }
670
671    /// Construct a [`LaError::NonFinite`] input error for a specific binary
672    /// scalar operand.
673    #[inline]
674    #[must_use]
675    pub const fn non_finite_input_interval_operand(operand: IntervalOperand) -> Self {
676        Self::NonFinite {
677            location: NonFiniteLocation::IntervalOperand { operand },
678            origin: NonFiniteOrigin::Input,
679        }
680    }
681
682    /// Construct a [`LaError::NonFinite`] computation error at matrix cell
683    /// `(row, col)`, retaining the originating `operation`.
684    #[inline]
685    #[must_use]
686    pub const fn non_finite_computation_matrix(
687        operation: ArithmeticOperation,
688        row: usize,
689        col: usize,
690    ) -> Self {
691        Self::NonFinite {
692            location: NonFiniteLocation::MatrixCell { row, col },
693            origin: NonFiniteOrigin::Computation { operation },
694        }
695    }
696
697    /// Construct a [`LaError::NonFinite`] computation error at `index`,
698    /// retaining the originating `operation`.
699    #[inline]
700    #[must_use]
701    pub const fn non_finite_computation_step(operation: ArithmeticOperation, index: usize) -> Self {
702        Self::NonFinite {
703            location: NonFiniteLocation::Step { index },
704            origin: NonFiniteOrigin::Computation { operation },
705        }
706    }
707
708    /// Construct a scalar [`LaError::NonFinite`] computation error retaining
709    /// the originating `operation`.
710    #[inline]
711    #[must_use]
712    pub const fn non_finite_computation_scalar(operation: ArithmeticOperation) -> Self {
713        Self::NonFinite {
714            location: NonFiniteLocation::Scalar,
715            origin: NonFiniteOrigin::Computation { operation },
716        }
717    }
718
719    /// Construct an [`LaError::IntervalRangeExhausted`] failure retaining the
720    /// responsible interval operation.
721    #[inline]
722    #[must_use]
723    pub const fn interval_range_exhausted(operation: ArithmeticOperation) -> Self {
724        Self::IntervalRangeExhausted { operation }
725    }
726
727    /// Construct a [`LaError::Unrepresentable`] conversion failure for a scalar
728    /// (`index = None`) or vector component (`index = Some(_)`).
729    #[inline]
730    #[must_use]
731    pub const fn unrepresentable(index: Option<usize>, reason: UnrepresentableReason) -> Self {
732        Self::Unrepresentable { index, reason }
733    }
734
735    /// Construct a [`LaError::InvertedInterval`] error preserving both rejected
736    /// finite bounds.
737    #[inline]
738    #[must_use]
739    pub const fn inverted_interval(lower: f64, upper: f64) -> Self {
740        Self::InvertedInterval { lower, upper }
741    }
742
743    /// Return the typed exact-to-`f64` conversion reason, or `None` for every
744    /// other error variant.
745    #[inline]
746    #[must_use]
747    pub const fn unrepresentable_reason(&self) -> Option<UnrepresentableReason> {
748        match self {
749            Self::Unrepresentable { reason, .. } => Some(*reason),
750            _ => None,
751        }
752    }
753
754    /// Return whether this is a `RequiresRounding` conversion failure for which
755    /// retrying with an explicit rounded API may succeed.
756    #[inline]
757    #[must_use]
758    pub const fn requires_rounding(&self) -> bool {
759        matches!(
760            self,
761            Self::Unrepresentable {
762                reason: UnrepresentableReason::RequiresRounding,
763                ..
764            }
765        )
766    }
767
768    /// Construct a [`LaError::DeterminantScaleOverflow`] retaining the matrix
769    /// dimension and minimum decomposed entry exponent.
770    #[inline]
771    #[must_use]
772    pub const fn determinant_scale_overflow(dim: usize, min_exponent: i32) -> Self {
773        Self::DeterminantScaleOverflow { dim, min_exponent }
774    }
775
776    /// Construct a [`LaError::UnsupportedDimension`] retaining the requested
777    /// and maximum supported dimensions.
778    #[inline]
779    #[must_use]
780    pub const fn unsupported_dimension(requested: usize, max: usize) -> Self {
781        Self::UnsupportedDimension { requested, max }
782    }
783
784    /// Construct a [`LaError::IndexOutOfBounds`] retaining the requested matrix
785    /// coordinates and dimension.
786    #[inline]
787    #[must_use]
788    pub const fn index_out_of_bounds(row: usize, col: usize, dim: usize) -> Self {
789        Self::IndexOutOfBounds { row, col, dim }
790    }
791
792    /// Construct an invalid-tolerance error and classify its typed reason.
793    ///
794    /// This low-level constructor assumes `value` has already failed the
795    /// tolerance invariant; raw caller input should normally be parsed through
796    /// [`crate::Tolerance::try_new`].
797    /// Non-finiteness takes precedence over negativity, so negative infinity is
798    /// classified as [`InvalidToleranceReason::NotFinite`].
799    #[inline]
800    #[must_use]
801    pub const fn invalid_tolerance(value: f64) -> Self {
802        let reason = if value.is_finite() {
803            InvalidToleranceReason::Negative
804        } else {
805            InvalidToleranceReason::NotFinite
806        };
807        Self::InvalidTolerance { value, reason }
808    }
809
810    /// Construct a [`LaError::Asymmetric`] error for the pair `(row, col)` and
811    /// `(col, row)`, retaining both observed values and the effective absolute
812    /// difference bound.
813    #[inline]
814    #[must_use]
815    pub const fn asymmetric(
816        row: usize,
817        col: usize,
818        dim: usize,
819        upper: f64,
820        lower: f64,
821        allowed_abs_diff: f64,
822    ) -> Self {
823        Self::Asymmetric {
824            row,
825            col,
826            dim,
827            upper,
828            lower,
829            allowed_abs_diff,
830        }
831    }
832
833    /// Construct a [`LaError::NotPositiveSemidefinite`] error for a computed
834    /// negative LDLT diagonal pivot.
835    #[inline]
836    #[must_use]
837    pub const fn not_positive_semidefinite_negative(pivot_col: usize, value: f64) -> Self {
838        Self::NotPositiveSemidefinite {
839            pivot_col,
840            violation: PositiveSemidefiniteViolation::NegativePivot { value },
841        }
842    }
843
844    /// Construct a [`LaError::NotPositiveSemidefinite`] error for a computed
845    /// zero LDLT diagonal with a non-zero coupling at `row`, distinguishing the
846    /// observed factorization violation from an uncoupled singular pivot.
847    #[inline]
848    #[must_use]
849    pub const fn not_positive_semidefinite_zero_coupling(
850        pivot_col: usize,
851        row: usize,
852        value: f64,
853    ) -> Self {
854        Self::NotPositiveSemidefinite {
855            pivot_col,
856            violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
857        }
858    }
859}
860
861/// Write the structured location portion of [`LaError::NonFinite`]'s public
862/// display contract without allocating an intermediate string.
863///
864fn write_non_finite_location(
865    f: &mut fmt::Formatter<'_>,
866    location: NonFiniteLocation,
867) -> fmt::Result {
868    match location {
869        NonFiniteLocation::PolynomialTerm { index, exponent } => {
870            write!(f, "polynomial term {index} (exponent {exponent})")
871        }
872        NonFiniteLocation::MatrixCell { row, col } => {
873            write!(f, "matrix cell ({row}, {col})")
874        }
875        NonFiniteLocation::VectorEntry { index } => write!(f, "vector entry {index}"),
876        NonFiniteLocation::VectorOperandEntry { operand, index } => {
877            write!(f, "{operand} vector entry {index}")
878        }
879        NonFiniteLocation::Step { index } => write!(f, "step {index}"),
880        NonFiniteLocation::IntervalBound {
881            bound: IntervalBound::Lower,
882        } => f.write_str("interval lower bound"),
883        NonFiniteLocation::IntervalBound {
884            bound: IntervalBound::Upper,
885        } => f.write_str("interval upper bound"),
886        NonFiniteLocation::IntervalOperand {
887            operand: IntervalOperand::Left,
888        } => f.write_str("left interval operand"),
889        NonFiniteLocation::IntervalOperand {
890            operand: IntervalOperand::Right,
891        } => f.write_str("right interval operand"),
892        NonFiniteLocation::Scalar => f.write_str("scalar value"),
893    }
894}
895
896/// Write a [`LaError::NonFinite`] message from its structured location and
897/// origin while distinguishing scalar input from a computed scalar result.
898fn write_non_finite(
899    f: &mut fmt::Formatter<'_>,
900    location: NonFiniteLocation,
901    origin: NonFiniteOrigin,
902) -> fmt::Result {
903    match (location, origin) {
904        (NonFiniteLocation::Scalar, NonFiniteOrigin::Input) => {
905            f.write_str("non-finite scalar input")
906        }
907        (NonFiniteLocation::Scalar, NonFiniteOrigin::Computation { operation }) => {
908            write!(f, "non-finite scalar result computed during {operation}")
909        }
910        (location, NonFiniteOrigin::Input) => {
911            f.write_str("non-finite input value at ")?;
912            write_non_finite_location(f, location)
913        }
914        (location, NonFiniteOrigin::Computation { operation }) => {
915            write!(f, "non-finite value computed during {operation} at ")?;
916            write_non_finite_location(f, location)
917        }
918    }
919}
920
921/// Write exact-conversion failures with their optional component context.
922fn write_unrepresentable(
923    f: &mut fmt::Formatter<'_>,
924    index: Option<usize>,
925    reason: UnrepresentableReason,
926) -> fmt::Result {
927    f.write_str(match reason {
928        UnrepresentableReason::RequiresRounding => {
929            "exact result requires rounding to fit finite f64"
930        }
931        UnrepresentableReason::NotFinite => {
932            "exact result has no finite f64 representation after rounding"
933        }
934    })?;
935    if let Some(index) = index {
936        write!(f, " at index {index}")?;
937    }
938    Ok(())
939}
940
941fn write_singularity(
942    f: &mut fmt::Formatter<'_>,
943    pivot_col: usize,
944    reason: SingularityReason,
945) -> fmt::Result {
946    match reason {
947        SingularityReason::Exact => {
948            write!(f, "matrix is exactly singular at pivot column {pivot_col}")
949        }
950        SingularityReason::Numerical {
951            factorization,
952            pivot_magnitude,
953            tolerance,
954        } => write!(
955            f,
956            "matrix is numerically singular during {factorization} factorization at pivot column {pivot_col}: pivot magnitude {pivot_magnitude} <= tolerance {tolerance}"
957        ),
958    }
959}
960
961impl fmt::Display for LaError {
962    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963        match *self {
964            Self::PolynomialBudgetExceeded {
965                work,
966                limit,
967                completed,
968                requested,
969            } => write!(
970                f,
971                "polynomial determinant budget exceeded for {work}: limit {limit}, completed {completed}, requested {requested} more"
972            ),
973            Self::PolynomialExponentOverflow {
974                row,
975                col,
976                left,
977                right,
978            } => write!(
979                f,
980                "polynomial exponent overflow at matrix cell ({row}, {col}): {left} + {right} exceeds u128::MAX"
981            ),
982            Self::DimensionMismatch { left, right } => {
983                write!(
984                    f,
985                    "vector dimension mismatch: left length {left}, right length {right}"
986                )
987            }
988            Self::EmptyVector => f.write_str("an empty vector has no direction"),
989            Self::ZeroVector { operand } => write!(f, "the {operand} zero vector has no direction"),
990            Self::Singular { pivot_col, reason } => write_singularity(f, pivot_col, reason),
991            Self::NonFinite { location, origin } => write_non_finite(f, location, origin),
992            Self::IntervalRangeExhausted { operation } => write!(
993                f,
994                "exact-real intermediate or result of {operation} has no enclosure with finite binary64 endpoints"
995            ),
996            Self::IntervalZeroDivisor { lower, upper } => {
997                write!(f, "interval divisor [{lower}, {upper}] contains zero")
998            }
999            Self::IntervalSolveInconclusive { pivot_col } => write!(
1000                f,
1001                "interval solve is inconclusive at pivot column {pivot_col}; no pivot excludes zero"
1002            ),
1003            Self::Unrepresentable { index, reason } => write_unrepresentable(f, index, reason),
1004            Self::InvertedInterval { lower, upper } => write!(
1005                f,
1006                "invalid interval bounds [{lower}, {upper}]; expected lower <= upper"
1007            ),
1008            Self::DeterminantScaleOverflow { dim, min_exponent } => write!(
1009                f,
1010                "exact determinant scale exponent overflows for dimension {dim} with minimum entry exponent {min_exponent}"
1011            ),
1012            Self::UnsupportedDimension { requested, max } => write!(
1013                f,
1014                "unsupported matrix dimension {requested}; maximum supported dimension is {max}"
1015            ),
1016            Self::IndexOutOfBounds { row, col, dim } => write!(
1017                f,
1018                "matrix index ({row}, {col}) is out of bounds for dimension {dim}"
1019            ),
1020            Self::InvalidTolerance {
1021                value,
1022                reason: InvalidToleranceReason::Negative,
1023            } => write!(f, "invalid tolerance {value}; expected value >= 0"),
1024            Self::InvalidTolerance {
1025                value,
1026                reason: InvalidToleranceReason::NotFinite,
1027            } => write!(f, "invalid tolerance {value}; expected a finite value"),
1028            Self::Asymmetric {
1029                row,
1030                col,
1031                dim,
1032                upper,
1033                lower,
1034                allowed_abs_diff,
1035            } => write!(
1036                f,
1037                "matrix is not symmetric for dimension {dim}: entry ({row}, {col}) = {upper} and entry ({col}, {row}) = {lower} differ by more than allowed absolute difference {allowed_abs_diff}"
1038            ),
1039            Self::NotPositiveSemidefinite {
1040                pivot_col,
1041                violation: PositiveSemidefiniteViolation::NegativePivot { value },
1042            } => write!(
1043                f,
1044                "LDLT rejected the matrix at pivot column {pivot_col}: computed diagonal value {value} < 0"
1045            ),
1046            Self::NotPositiveSemidefinite {
1047                pivot_col,
1048                violation: PositiveSemidefiniteViolation::ZeroPivotCoupling { row, value },
1049            } => write!(
1050                f,
1051                "LDLT rejected the matrix at pivot column {pivot_col}: computed zero diagonal has non-zero coupling at row {row} with value {value}"
1052            ),
1053        }
1054    }
1055}
1056
1057impl std::error::Error for LaError {}
1058
1059#[cfg(test)]
1060mod tests {
1061    use std::error::Error;
1062
1063    use super::*;
1064    use crate::MAX_STACK_MATRIX_DISPATCH_DIM;
1065
1066    #[test]
1067    fn polynomial_errors_display_work_and_location_without_exact_feature() {
1068        assert_eq!(
1069            PolynomialWork::SubsetTransitions.to_string(),
1070            "subset transitions"
1071        );
1072        assert_eq!(
1073            PolynomialWork::TermProducts.to_string(),
1074            "coefficient-pair products"
1075        );
1076        assert_eq!(
1077            LaError::PolynomialBudgetExceeded {
1078                work: PolynomialWork::TermProducts,
1079                limit: 4,
1080                completed: 4,
1081                requested: 1,
1082            }
1083            .to_string(),
1084            "polynomial determinant budget exceeded for coefficient-pair products: limit 4, completed 4, requested 1 more"
1085        );
1086        assert_eq!(
1087            LaError::PolynomialExponentOverflow {
1088                row: 1,
1089                col: 2,
1090                left: u128::MAX,
1091                right: 1,
1092            }
1093            .to_string(),
1094            format!(
1095                "polynomial exponent overflow at matrix cell (1, 2): {} + 1 exceeds u128::MAX",
1096                u128::MAX
1097            )
1098        );
1099    }
1100
1101    #[test]
1102    fn category_displays_are_concise() {
1103        assert_eq!(FactorizationKind::Lu.to_string(), "LU");
1104        assert_eq!(FactorizationKind::Ldlt.to_string(), "LDLT");
1105        assert_eq!(
1106            ArithmeticOperation::MatrixInfinityNorm.to_string(),
1107            "matrix infinity norm"
1108        );
1109        assert_eq!(
1110            ArithmeticOperation::SymmetryCheck.to_string(),
1111            "symmetry check"
1112        );
1113        assert_eq!(
1114            ArithmeticOperation::LuFactorization.to_string(),
1115            "LU factorization"
1116        );
1117        assert_eq!(
1118            ArithmeticOperation::LdltFactorization.to_string(),
1119            "LDLT factorization"
1120        );
1121        assert_eq!(ArithmeticOperation::LuSolve.to_string(), "LU solve");
1122        assert_eq!(ArithmeticOperation::LdltSolve.to_string(), "LDLT solve");
1123        assert_eq!(ArithmeticOperation::Determinant.to_string(), "determinant");
1124        assert_eq!(
1125            ArithmeticOperation::DeterminantErrorBound.to_string(),
1126            "determinant error bound"
1127        );
1128        assert_eq!(
1129            ArithmeticOperation::VectorDotProduct.to_string(),
1130            "vector dot product"
1131        );
1132        assert_eq!(
1133            ArithmeticOperation::VectorDotDifference.to_string(),
1134            "vector dot difference"
1135        );
1136        assert_eq!(
1137            ArithmeticOperation::VectorSquaredNorm.to_string(),
1138            "vector squared norm"
1139        );
1140        assert_eq!(
1141            ArithmeticOperation::VectorNorm.to_string(),
1142            "vector Euclidean norm"
1143        );
1144        assert_eq!(
1145            ArithmeticOperation::IntervalAddition.to_string(),
1146            "interval addition"
1147        );
1148        assert_eq!(
1149            ArithmeticOperation::IntervalSubtraction.to_string(),
1150            "interval subtraction"
1151        );
1152        assert_eq!(
1153            ArithmeticOperation::IntervalMultiplication.to_string(),
1154            "interval multiplication"
1155        );
1156        assert_eq!(
1157            ArithmeticOperation::IntervalSquare.to_string(),
1158            "interval square"
1159        );
1160        assert_eq!(
1161            ArithmeticOperation::IntervalDeterminant.to_string(),
1162            "interval determinant"
1163        );
1164    }
1165
1166    #[test]
1167    fn singular_constructors_and_displays_preserve_reason() {
1168        let exact = LaError::singular_exact(3);
1169        assert_eq!(
1170            exact,
1171            LaError::Singular {
1172                pivot_col: 3,
1173                reason: SingularityReason::Exact,
1174            }
1175        );
1176        assert_eq!(
1177            exact.to_string(),
1178            "matrix is exactly singular at pivot column 3"
1179        );
1180
1181        let numerical = LaError::singular_numerical(2, FactorizationKind::Lu, 1e-14, 1e-12);
1182        assert_eq!(
1183            numerical,
1184            LaError::Singular {
1185                pivot_col: 2,
1186                reason: SingularityReason::Numerical {
1187                    factorization: FactorizationKind::Lu,
1188                    pivot_magnitude: 1e-14,
1189                    tolerance: 1e-12,
1190                },
1191            }
1192        );
1193        assert_eq!(
1194            numerical.to_string(),
1195            "matrix is numerically singular during LU factorization at pivot column 2: pivot magnitude 0.00000000000001 <= tolerance 0.000000000001"
1196        );
1197    }
1198
1199    #[test]
1200    fn non_finite_constructors_preserve_location_and_origin() {
1201        assert_eq!(
1202            LaError::non_finite_input_matrix(1, 2),
1203            LaError::NonFinite {
1204                location: NonFiniteLocation::MatrixCell { row: 1, col: 2 },
1205                origin: NonFiniteOrigin::Input,
1206            }
1207        );
1208        assert_eq!(
1209            LaError::non_finite_input_vector(3).to_string(),
1210            "non-finite input value at vector entry 3"
1211        );
1212        assert_eq!(
1213            LaError::non_finite_input_scalar().to_string(),
1214            "non-finite scalar input"
1215        );
1216        assert_eq!(
1217            LaError::non_finite_input_interval_bound(IntervalBound::Upper).to_string(),
1218            "non-finite input value at interval upper bound"
1219        );
1220        assert_eq!(
1221            LaError::non_finite_input_interval_bound(IntervalBound::Lower).to_string(),
1222            "non-finite input value at interval lower bound"
1223        );
1224        assert_eq!(
1225            LaError::non_finite_input_interval_operand(IntervalOperand::Left).to_string(),
1226            "non-finite input value at left interval operand"
1227        );
1228        assert_eq!(
1229            LaError::non_finite_input_interval_operand(IntervalOperand::Right).to_string(),
1230            "non-finite input value at right interval operand"
1231        );
1232        assert_eq!(
1233            LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 2, 1)
1234                .to_string(),
1235            "non-finite value computed during LU factorization at matrix cell (2, 1)"
1236        );
1237        assert_eq!(
1238            LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1).to_string(),
1239            "non-finite value computed during LU solve at step 1"
1240        );
1241        assert_eq!(
1242            LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant).to_string(),
1243            "non-finite scalar result computed during determinant"
1244        );
1245    }
1246
1247    #[test]
1248    fn unrepresentable_helpers_preserve_recovery_reason() {
1249        let rounding = LaError::unrepresentable(Some(2), UnrepresentableReason::RequiresRounding);
1250        let scalar_rounding =
1251            LaError::unrepresentable(None, UnrepresentableReason::RequiresRounding);
1252        let indexed_not_finite =
1253            LaError::unrepresentable(Some(2), UnrepresentableReason::NotFinite);
1254        let not_finite = LaError::unrepresentable(None, UnrepresentableReason::NotFinite);
1255        assert_eq!(
1256            rounding.unrepresentable_reason(),
1257            Some(UnrepresentableReason::RequiresRounding)
1258        );
1259        assert!(rounding.requires_rounding());
1260        assert_eq!(
1261            rounding.to_string(),
1262            "exact result requires rounding to fit finite f64 at index 2"
1263        );
1264        assert_eq!(
1265            scalar_rounding.to_string(),
1266            "exact result requires rounding to fit finite f64"
1267        );
1268        assert_eq!(
1269            indexed_not_finite.to_string(),
1270            "exact result has no finite f64 representation after rounding at index 2"
1271        );
1272        assert_eq!(
1273            not_finite.to_string(),
1274            "exact result has no finite f64 representation after rounding"
1275        );
1276        assert!(!not_finite.requires_rounding());
1277        assert_eq!(LaError::singular_exact(0).unrepresentable_reason(), None);
1278    }
1279
1280    #[test]
1281    fn unrepresentable_display_propagates_writer_failure() {
1282        struct LimitedWrites {
1283            remaining: usize,
1284        }
1285
1286        impl fmt::Write for LimitedWrites {
1287            fn write_str(&mut self, _: &str) -> fmt::Result {
1288                if self.remaining == 0 {
1289                    return Err(fmt::Error);
1290                }
1291                self.remaining -= 1;
1292                Ok(())
1293            }
1294        }
1295
1296        for reason in [
1297            UnrepresentableReason::RequiresRounding,
1298            UnrepresentableReason::NotFinite,
1299        ] {
1300            for index in [None, Some(2)] {
1301                let error = LaError::unrepresentable(index, reason);
1302                for remaining in [0, 1] {
1303                    // One write accepts the base message, then rejects any
1304                    // optional index suffix. Zero rejects the base message.
1305                    let expected = if remaining == 1 && index.is_none() {
1306                        Ok(())
1307                    } else {
1308                        Err(fmt::Error)
1309                    };
1310                    assert_eq!(
1311                        fmt::write(&mut LimitedWrites { remaining }, format_args!("{error}")),
1312                        expected,
1313                        "reason={reason:?}, index={index:?}, remaining={remaining}"
1314                    );
1315                }
1316            }
1317        }
1318    }
1319
1320    #[test]
1321    fn invalid_tolerance_classifies_non_finite_before_negative() {
1322        assert_eq!(
1323            LaError::invalid_tolerance(-1.0),
1324            LaError::InvalidTolerance {
1325                value: -1.0,
1326                reason: InvalidToleranceReason::Negative,
1327            }
1328        );
1329        assert_eq!(
1330            LaError::invalid_tolerance(f64::NEG_INFINITY),
1331            LaError::InvalidTolerance {
1332                value: f64::NEG_INFINITY,
1333                reason: InvalidToleranceReason::NotFinite,
1334            }
1335        );
1336        assert_eq!(
1337            LaError::invalid_tolerance(-1.0).to_string(),
1338            "invalid tolerance -1; expected value >= 0"
1339        );
1340        assert_eq!(
1341            LaError::invalid_tolerance(f64::NEG_INFINITY).to_string(),
1342            "invalid tolerance -inf; expected a finite value"
1343        );
1344    }
1345
1346    #[test]
1347    fn inverted_interval_error_preserves_both_bounds() {
1348        let error = LaError::inverted_interval(2.0, 1.0);
1349        assert_eq!(
1350            error,
1351            LaError::InvertedInterval {
1352                lower: 2.0,
1353                upper: 1.0,
1354            }
1355        );
1356        assert_eq!(
1357            error.to_string(),
1358            "invalid interval bounds [2, 1]; expected lower <= upper"
1359        );
1360    }
1361
1362    #[test]
1363    fn interval_range_error_preserves_operation() {
1364        let error = LaError::interval_range_exhausted(ArithmeticOperation::IntervalSquare);
1365        assert_eq!(
1366            error,
1367            LaError::IntervalRangeExhausted {
1368                operation: ArithmeticOperation::IntervalSquare,
1369            }
1370        );
1371        assert_eq!(
1372            error.to_string(),
1373            "exact-real intermediate or result of interval square has no enclosure with finite binary64 endpoints"
1374        );
1375    }
1376
1377    #[test]
1378    fn asymmetric_error_retains_observed_values_and_bound() {
1379        let err = LaError::asymmetric(0, 2, 3, 1.0, 1.5, 1e-12);
1380        assert_eq!(
1381            err,
1382            LaError::Asymmetric {
1383                row: 0,
1384                col: 2,
1385                dim: 3,
1386                upper: 1.0,
1387                lower: 1.5,
1388                allowed_abs_diff: 1e-12,
1389            }
1390        );
1391        assert_eq!(
1392            err.to_string(),
1393            "matrix is not symmetric for dimension 3: entry (0, 2) = 1 and entry (2, 0) = 1.5 differ by more than allowed absolute difference 0.000000000001"
1394        );
1395    }
1396
1397    #[test]
1398    fn positive_semidefinite_errors_preserve_distinct_violations() {
1399        assert_eq!(
1400            LaError::not_positive_semidefinite_negative(1, -3.0).to_string(),
1401            "LDLT rejected the matrix at pivot column 1: computed diagonal value -3 < 0"
1402        );
1403        assert_eq!(
1404            LaError::not_positive_semidefinite_zero_coupling(0, 1, 2.0).to_string(),
1405            "LDLT rejected the matrix at pivot column 0: computed zero diagonal has non-zero coupling at row 1 with value 2"
1406        );
1407    }
1408
1409    #[test]
1410    fn remaining_helpers_and_displays_preserve_fields() {
1411        assert_eq!(
1412            LaError::determinant_scale_overflow(3, -1074).to_string(),
1413            "exact determinant scale exponent overflows for dimension 3 with minimum entry exponent -1074"
1414        );
1415        assert_eq!(
1416            LaError::unsupported_dimension(8, MAX_STACK_MATRIX_DISPATCH_DIM).to_string(),
1417            "unsupported matrix dimension 8; maximum supported dimension is 7"
1418        );
1419        assert_eq!(
1420            LaError::index_out_of_bounds(3, 0, 3).to_string(),
1421            "matrix index (3, 0) is out of bounds for dimension 3"
1422        );
1423    }
1424
1425    #[test]
1426    fn is_std_error_with_no_source() {
1427        let err = LaError::singular_exact(0);
1428        let error: &dyn Error = &err;
1429        assert!(error.source().is_none());
1430    }
1431}