Skip to main content

tenferro_tensor/validate/
mod.rs

1//! Validation helpers shared across backends and exec layers.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use tenferro_tensor::validate::validate_nonsingular_u;
7//! use tenferro_tensor::{Tensor, TypedTensor};
8//!
9//! let t = Tensor::F64(TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0, 0.0, 0.0, 1.0]).unwrap());
10//! assert!(validate_nonsingular_u(&t).is_ok());
11//! ```
12
13use num_complex::{Complex32, Complex64};
14
15use crate::{
16    DType, DotGeneralConfig, Error, ErrorKind, Result, ShapeMismatch, Tensor, TypedTensor,
17    ValidationError,
18};
19
20/// Domain-specific reasons reported by triangular-factor validation.
21///
22/// The outer tensor error classifies these failures as numerical or unsupported
23/// while retaining this value as its typed source.
24///
25/// # Examples
26///
27/// ```rust
28/// use tenferro_tensor::validate::DiagonalError;
29///
30/// let error = DiagonalError::SingularOrNonFinite {
31///     index: 1,
32/// };
33/// assert!(error.to_string().contains("position [1,1]"));
34/// ```
35#[derive(Debug, thiserror::Error)]
36pub enum DiagonalError {
37    #[error("singular or non-finite diagonal at position [{index},{index}]")]
38    SingularOrNonFinite { index: usize },
39    #[error("singular or non-finite diagonal at batch {batch}, position [{index},{index}]")]
40    BatchedSingularOrNonFinite { batch: usize, index: usize },
41    #[error("triangular solve does not support dtype {dtype:?}")]
42    UnsupportedDType { dtype: DType },
43}
44
45/// Promote two dtypes according to tenferro's public dtype-promotion lattice.
46///
47/// # Examples
48///
49/// ```rust
50/// use tenferro_tensor::validate::promote_dtype;
51/// use tenferro_tensor::DType;
52///
53/// assert_eq!(promote_dtype(DType::I32, DType::F32), DType::F64);
54/// ```
55pub fn promote_dtype(lhs: DType, rhs: DType) -> DType {
56    use DType::*;
57    match (lhs, rhs) {
58        (Bool, Bool) => Bool,
59        (Bool, other) | (other, Bool) => other,
60        (I32, I32) => I32,
61        (I32, I64) | (I64, I32) | (I64, I64) => I64,
62        (I32 | I64, F32 | F64) | (F32 | F64, I32 | I64) => F64,
63        (I32 | I64, C32 | C64) | (C32 | C64, I32 | I64) => C64,
64        (F32, F32) => F32,
65        (F32, F64) | (F64, F32) | (F64, F64) => F64,
66        (F32, C32) | (C32, F32) | (C32, C32) => C32,
67        (F32, C64) | (C64, F32) => C64,
68        (F64, C32 | C64) | (C32 | C64, F64) => C64,
69        (C32, C64) | (C64, C32) | (C64, C64) => C64,
70    }
71}
72
73/// Return whether public `convert` may change `from` into `to`.
74///
75/// Checked conversion follows the same dtype lattice as implicit promotion.
76/// Use explicit `cast` for value-changing projections outside this lattice.
77///
78/// # Examples
79///
80/// ```rust
81/// use tenferro_tensor::validate::can_convert_dtype;
82/// use tenferro_tensor::DType;
83///
84/// assert!(can_convert_dtype(DType::F32, DType::F64));
85/// assert!(!can_convert_dtype(DType::F64, DType::I32));
86/// ```
87pub fn can_convert_dtype(from: DType, to: DType) -> bool {
88    promote_dtype(from, to) == to
89}
90
91/// Validate a public checked dtype conversion.
92///
93/// # Examples
94///
95/// ```rust
96/// use tenferro_tensor::validate::validate_convert_dtype;
97/// use tenferro_tensor::DType;
98///
99/// assert!(validate_convert_dtype("convert", DType::F32, DType::F64).is_ok());
100/// assert!(validate_convert_dtype("convert", DType::C64, DType::F64).is_err());
101/// ```
102/// # Errors
103///
104/// Returns [`crate::Error::UnsupportedDTypeConversion`] when the requested
105/// conversion is outside the checked promotion lattice. Use an explicit cast
106/// for lossy projections.
107pub fn validate_convert_dtype(op: &'static str, from: DType, to: DType) -> Result<()> {
108    if can_convert_dtype(from, to) {
109        return Ok(());
110    }
111
112    Err(Error::unsupported_dtype_conversion(
113        op,
114        from,
115        to,
116        "checked convert only accepts conversions allowed by dtype promotion; use explicit cast for lossy dtype projection",
117    ))
118}
119
120/// Compute a shape product with overflow reported as a typed tensor error.
121///
122/// # Examples
123///
124/// ```rust
125/// use tenferro_tensor::validate::checked_shape_product;
126///
127/// assert_eq!(checked_shape_product("zeros", "shape", &[2, 3])?, 6);
128/// # Ok::<(), tenferro_tensor::Error>(())
129/// ```
130/// # Errors
131///
132/// Returns [`crate::Error::Validation`] containing
133/// [`tenferro_tensor_core::ValidationError::InvalidArgument`] when the
134/// product of `shape` exceeds `usize::MAX`; `role` identifies the shape-like
135/// argument in the diagnostic.
136pub fn checked_shape_product(
137    op: &'static str,
138    role: &'static str,
139    shape: &[usize],
140) -> Result<usize> {
141    if shape.contains(&0) {
142        return Ok(0);
143    }
144    shape
145        .iter()
146        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
147        .ok_or_else(|| {
148            Error::invalid_argument(op, role, format!("product overflows for shape {shape:?}"))
149        })
150}
151
152/// Validate a full permutation for a tensor rank.
153///
154/// # Examples
155///
156/// ```rust
157/// use tenferro_tensor::validate::validate_permutation_axes;
158///
159/// validate_permutation_axes("transpose", 2, &[1, 0])?;
160/// # Ok::<(), tenferro_tensor::Error>(())
161/// ```
162/// # Errors
163///
164/// Returns [`crate::Error::Validation`] with `RankMismatch`,
165/// `AxisOutOfBounds`, or `DuplicateAxis` as appropriate.
166pub fn validate_permutation_axes(op: &'static str, rank: usize, perm: &[usize]) -> Result<()> {
167    if perm.len() != rank {
168        return Err(Error::validation(
169            op,
170            ValidationError::RankMismatch {
171                expected: rank,
172                actual: perm.len(),
173            },
174        ));
175    }
176
177    let mut seen = vec![false; rank];
178    for &axis in perm {
179        if axis >= rank {
180            return Err(Error::validation(
181                op,
182                ValidationError::AxisOutOfBounds { axis, rank },
183            ));
184        }
185        if seen[axis] {
186            return Err(Error::validation(
187                op,
188                ValidationError::DuplicateAxis {
189                    axis,
190                    role: "permutation",
191                },
192            ));
193        }
194        seen[axis] = true;
195    }
196    Ok(())
197}
198
199/// Validate a subset of axes for a tensor rank.
200///
201/// # Examples
202///
203/// ```rust
204/// use tenferro_tensor::validate::validate_unique_axes;
205///
206/// validate_unique_axes("reduce_sum", "axis", 3, &[0, 2])?;
207/// assert!(validate_unique_axes("reduce_sum", "axis", 2, &[2]).is_err());
208/// assert!(validate_unique_axes("reduce_sum", "axis", 2, &[0, 0]).is_err());
209/// # Ok::<(), tenferro_tensor::Error>(())
210/// ```
211/// # Errors
212///
213/// Returns [`crate::Error::Validation`] with `AxisOutOfBounds` for an invalid
214/// axis or `DuplicateAxis` when an axis occurs more than once.
215pub fn validate_unique_axes(
216    op: &'static str,
217    role: &'static str,
218    rank: usize,
219    axes: &[usize],
220) -> Result<()> {
221    let mut seen = vec![false; rank];
222    for &axis in axes {
223        if axis >= rank {
224            return Err(Error::validation(
225                op,
226                ValidationError::AxisOutOfBounds { axis, rank },
227            ));
228        }
229        if seen[axis] {
230            return Err(Error::validation(
231                op,
232                ValidationError::DuplicateAxis { axis, role },
233            ));
234        }
235        seen[axis] = true;
236    }
237    Ok(())
238}
239
240/// Validate rank-2 matrix multiplication shapes and return its dot-general config.
241///
242/// # Examples
243///
244/// ```rust
245/// use tenferro_tensor::validate::matmul_config_for_shapes;
246///
247/// let config = matmul_config_for_shapes("matmul", &[2, 3], &[3, 4])?;
248/// assert_eq!(config.lhs_contracting_dims, vec![1]);
249/// # Ok::<(), tenferro_tensor::Error>(())
250/// ```
251/// # Errors
252///
253/// Returns [`crate::Error::Validation`] with `RankMismatch` for a non-matrix
254/// input or `ShapeMismatch` when the contracting dimensions differ.
255pub fn matmul_config_for_shapes(
256    op: &'static str,
257    lhs_shape: &[usize],
258    rhs_shape: &[usize],
259) -> Result<DotGeneralConfig> {
260    if lhs_shape.len() != 2 {
261        return Err(Error::validation(
262            op,
263            ValidationError::RankMismatch {
264                expected: 2,
265                actual: lhs_shape.len(),
266            },
267        ));
268    }
269    if rhs_shape.len() != 2 {
270        return Err(Error::validation(
271            op,
272            ValidationError::RankMismatch {
273                expected: 2,
274                actual: rhs_shape.len(),
275            },
276        ));
277    }
278    if lhs_shape[1] != rhs_shape[0] {
279        return Err(Error::validation(
280            op,
281            ShapeMismatch::IncompatibleShapes {
282                lhs: lhs_shape.to_vec().into(),
283                rhs: rhs_shape.to_vec().into(),
284            }
285            .into(),
286        ));
287    }
288
289    Ok(DotGeneralConfig {
290        lhs_contracting_dims: vec![1],
291        rhs_contracting_dims: vec![0],
292        lhs_batch_dims: vec![],
293        rhs_batch_dims: vec![],
294    })
295}
296
297/// Trait for detecting singular or non-finite diagonal entries.
298///
299/// Implemented for `f32`, `f64`, `Complex32`, and `Complex64`.
300/// A value is considered singular if it is zero, NaN, infinite,
301/// or (for complex types) if either component is non-finite.
302pub trait DiagSingularity {
303    /// Returns `true` if the value is singular or non-finite.
304    fn is_singular_or_nonfinite(&self) -> bool;
305}
306
307macro_rules! impl_diag_singularity_float {
308    ($($t:ty),* $(,)?) => {
309        $(
310            impl DiagSingularity for $t {
311                fn is_singular_or_nonfinite(&self) -> bool {
312                    !self.is_finite() || *self == 0.0
313                }
314            }
315        )*
316    };
317}
318
319impl_diag_singularity_float!(f64, f32);
320
321macro_rules! impl_diag_singularity_complex {
322    ($($t:ty),* $(,)?) => {
323        $(
324            impl DiagSingularity for $t {
325                fn is_singular_or_nonfinite(&self) -> bool {
326                    // Why not `norm_sqr() == 0`: squaring a representable tiny
327                    // component can underflow and relabel a nonzero pivot as zero.
328                    !self.re.is_finite()
329                        || !self.im.is_finite()
330                        || (self.re == 0.0 && self.im == 0.0)
331                }
332            }
333        )*
334    };
335}
336
337impl_diag_singularity_complex!(Complex64, Complex32);
338
339/// Checks that every diagonal element of a (possibly batched) upper-triangular
340/// factor is non-singular and finite.
341///
342/// Iterates over all batch slices and inspects the diagonal entries
343/// `data[i + i * rows]` for `i` in `0..min(rows, cols)`. Returns
344/// a numerical [`crate::Error::Extension`] carrying a typed
345/// [`DiagonalError::SingularOrNonFinite`] source for the first offending entry,
346/// or [`ValidationError::RankMismatch`] wrapped in [`Error::Validation`] when
347/// `t` has rank less than two.
348///
349/// # Examples
350///
351/// ```rust
352/// use tenferro_tensor::validate::check_singular_diagonal;
353/// use tenferro_tensor::TypedTensor;
354///
355/// let t = TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0f32, 0.0, 0.0, 2.0]).unwrap();
356/// assert!(check_singular_diagonal(&t).is_ok());
357/// ```
358/// # Errors
359///
360/// Returns [`crate::Error::Validation`] with `RankMismatch` for a non-matrix
361/// tensor, a numerical [`crate::Error::Extension`] with a typed
362/// [`DiagonalError::SingularOrNonFinite`] source for a singular or non-finite
363/// diagonal, or an unsupported [`crate::Error::Extension`] with a typed
364/// [`DiagonalError::UnsupportedDType`] source for integer and boolean inputs.
365pub fn check_singular_diagonal<T: DiagSingularity + Copy + std::fmt::Debug>(
366    t: &TypedTensor<T>,
367) -> Result<()> {
368    if t.shape().len() < 2 {
369        return Err(Error::validation(
370            "solve",
371            ValidationError::RankMismatch {
372                expected: 2,
373                actual: t.shape().len(),
374            },
375        ));
376    }
377    let rows = t.shape()[0];
378    let cols = t.shape()[1];
379    let n = rows.min(cols);
380    let batch_total = checked_shape_product("solve", "batch shape", &t.shape()[2..])?;
381    let slice_size = checked_shape_product("solve", "matrix shape", &t.shape()[..2])?;
382    let data = t.host_data()?;
383    for batch_idx in 0..batch_total {
384        let batch = &data[batch_idx * slice_size..(batch_idx + 1) * slice_size];
385        for i in 0..n {
386            let diag = batch[i + i * rows];
387            if diag.is_singular_or_nonfinite() {
388                return Err(Error::extension(
389                    "solve",
390                    "tensor-validation",
391                    ErrorKind::NumericalFailure,
392                    if batch_total > 1 {
393                        DiagonalError::BatchedSingularOrNonFinite {
394                            batch: batch_idx,
395                            index: i,
396                        }
397                    } else {
398                        DiagonalError::SingularOrNonFinite { index: i }
399                    },
400                ));
401            }
402        }
403    }
404    Ok(())
405}
406
407/// Validates that the upper-triangular factor `u` of a matrix decomposition
408/// has no singular (zero) or non-finite diagonal entries.
409///
410/// Dispatches to [`check_singular_diagonal`] after unpacking the concrete
411/// tensor variant. Returns `Ok(())` when all diagonal entries are valid.
412///
413/// # Examples
414///
415/// ```rust
416/// use tenferro_tensor::validate::validate_nonsingular_u;
417/// use tenferro_tensor::{Tensor, TypedTensor};
418///
419/// let t = Tensor::F64(TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0, 0.0, 0.0, 1.0]).unwrap());
420/// assert!(validate_nonsingular_u(&t).is_ok());
421/// ```
422/// # Errors
423///
424/// Returns [`crate::Error::Validation`] with the applicable typed shape, rank,
425/// axis, dtype, or argument source when validation fails. Singular or
426/// non-finite diagonal checks return [`crate::Error::BackendFailure`].
427pub fn validate_nonsingular_u(u: &Tensor) -> Result<()> {
428    match u {
429        Tensor::F64(t) => check_singular_diagonal(t),
430        Tensor::F32(t) => check_singular_diagonal(t),
431        Tensor::C64(t) => check_singular_diagonal(t),
432        Tensor::C32(t) => check_singular_diagonal(t),
433        Tensor::I32(_) | Tensor::I64(_) | Tensor::Bool(_) => Err(Error::extension(
434            "solve",
435            "tensor-validation",
436            ErrorKind::Unsupported,
437            DiagonalError::UnsupportedDType { dtype: u.dtype() },
438        )),
439    }
440}
441
442#[cfg(test)]
443mod tests;