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::{DType, DotGeneralConfig, Error, Result, Tensor, TypedTensor};
16
17/// Promote two dtypes according to tenferro's public dtype-promotion lattice.
18///
19/// # Examples
20///
21/// ```rust
22/// use tenferro_tensor::validate::promote_dtype;
23/// use tenferro_tensor::DType;
24///
25/// assert_eq!(promote_dtype(DType::I32, DType::F32), DType::F64);
26/// ```
27pub fn promote_dtype(lhs: DType, rhs: DType) -> DType {
28    use DType::*;
29    match (lhs, rhs) {
30        (Bool, Bool) => Bool,
31        (Bool, other) | (other, Bool) => other,
32        (I32, I32) => I32,
33        (I32, I64) | (I64, I32) | (I64, I64) => I64,
34        (I32 | I64, F32 | F64) | (F32 | F64, I32 | I64) => F64,
35        (I32 | I64, C32 | C64) | (C32 | C64, I32 | I64) => C64,
36        (F32, F32) => F32,
37        (F32, F64) | (F64, F32) | (F64, F64) => F64,
38        (F32, C32) | (C32, F32) | (C32, C32) => C32,
39        (F32, C64) | (C64, F32) => C64,
40        (F64, C32 | C64) | (C32 | C64, F64) => C64,
41        (C32, C64) | (C64, C32) | (C64, C64) => C64,
42    }
43}
44
45/// Return whether public `convert` may change `from` into `to`.
46///
47/// Checked conversion follows the same dtype lattice as implicit promotion.
48/// Use explicit `cast` for value-changing projections outside this lattice.
49///
50/// # Examples
51///
52/// ```rust
53/// use tenferro_tensor::validate::can_convert_dtype;
54/// use tenferro_tensor::DType;
55///
56/// assert!(can_convert_dtype(DType::F32, DType::F64));
57/// assert!(!can_convert_dtype(DType::F64, DType::I32));
58/// ```
59pub fn can_convert_dtype(from: DType, to: DType) -> bool {
60    promote_dtype(from, to) == to
61}
62
63/// Validate a public checked dtype conversion.
64///
65/// # Examples
66///
67/// ```rust
68/// use tenferro_tensor::validate::validate_convert_dtype;
69/// use tenferro_tensor::DType;
70///
71/// assert!(validate_convert_dtype("convert", DType::F32, DType::F64).is_ok());
72/// assert!(validate_convert_dtype("convert", DType::C64, DType::F64).is_err());
73/// ```
74pub fn validate_convert_dtype(op: &'static str, from: DType, to: DType) -> Result<()> {
75    if can_convert_dtype(from, to) {
76        return Ok(());
77    }
78
79    Err(Error::UnsupportedDTypeConversion {
80        op,
81        from,
82        to,
83        message: "checked convert only accepts conversions allowed by dtype promotion; use explicit cast for lossy dtype projection".to_string(),
84    })
85}
86
87/// Compute a shape product with overflow reported as a typed tensor error.
88///
89/// # Examples
90///
91/// ```rust
92/// use tenferro_tensor::validate::checked_shape_product;
93///
94/// assert_eq!(checked_shape_product("zeros", "shape", &[2, 3])?, 6);
95/// # Ok::<(), tenferro_tensor::Error>(())
96/// ```
97pub fn checked_shape_product(
98    op: &'static str,
99    role: &'static str,
100    shape: &[usize],
101) -> Result<usize> {
102    shape
103        .iter()
104        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
105        .ok_or_else(|| Error::InvalidConfig {
106            op,
107            message: format!("{role} product overflows for shape {shape:?}"),
108        })
109}
110
111/// Validate a full permutation for a tensor rank.
112///
113/// # Examples
114///
115/// ```rust
116/// use tenferro_tensor::validate::validate_permutation_axes;
117///
118/// validate_permutation_axes("transpose", 2, &[1, 0])?;
119/// # Ok::<(), tenferro_tensor::Error>(())
120/// ```
121pub fn validate_permutation_axes(op: &'static str, rank: usize, perm: &[usize]) -> Result<()> {
122    if perm.len() != rank {
123        return Err(Error::RankMismatch {
124            op,
125            expected: rank,
126            actual: perm.len(),
127        });
128    }
129
130    let mut seen = vec![false; rank];
131    for &axis in perm {
132        if axis >= rank {
133            return Err(Error::AxisOutOfBounds { op, axis, rank });
134        }
135        if seen[axis] {
136            return Err(Error::DuplicateAxis {
137                op,
138                axis,
139                role: "permutation",
140            });
141        }
142        seen[axis] = true;
143    }
144    Ok(())
145}
146
147/// Validate a subset of axes for a tensor rank.
148///
149/// # Examples
150///
151/// ```rust
152/// use tenferro_tensor::validate::validate_unique_axes;
153///
154/// validate_unique_axes("reduce_sum", "axis", 3, &[0, 2])?;
155/// assert!(validate_unique_axes("reduce_sum", "axis", 2, &[2]).is_err());
156/// assert!(validate_unique_axes("reduce_sum", "axis", 2, &[0, 0]).is_err());
157/// # Ok::<(), tenferro_tensor::Error>(())
158/// ```
159pub fn validate_unique_axes(
160    op: &'static str,
161    role: &'static str,
162    rank: usize,
163    axes: &[usize],
164) -> Result<()> {
165    let mut seen = vec![false; rank];
166    for &axis in axes {
167        if axis >= rank {
168            return Err(Error::AxisOutOfBounds { op, axis, rank });
169        }
170        if seen[axis] {
171            return Err(Error::DuplicateAxis { op, axis, role });
172        }
173        seen[axis] = true;
174    }
175    Ok(())
176}
177
178/// Validate rank-2 matrix multiplication shapes and return its dot-general config.
179///
180/// # Examples
181///
182/// ```rust
183/// use tenferro_tensor::validate::matmul_config_for_shapes;
184///
185/// let config = matmul_config_for_shapes("matmul", &[2, 3], &[3, 4])?;
186/// assert_eq!(config.lhs_contracting_dims, vec![1]);
187/// # Ok::<(), tenferro_tensor::Error>(())
188/// ```
189pub fn matmul_config_for_shapes(
190    op: &'static str,
191    lhs_shape: &[usize],
192    rhs_shape: &[usize],
193) -> Result<DotGeneralConfig> {
194    if lhs_shape.len() != 2 {
195        return Err(Error::RankMismatch {
196            op,
197            expected: 2,
198            actual: lhs_shape.len(),
199        });
200    }
201    if rhs_shape.len() != 2 {
202        return Err(Error::RankMismatch {
203            op,
204            expected: 2,
205            actual: rhs_shape.len(),
206        });
207    }
208    if lhs_shape[1] != rhs_shape[0] {
209        return Err(Error::ShapeMismatch {
210            op,
211            lhs: lhs_shape.to_vec(),
212            rhs: rhs_shape.to_vec(),
213        });
214    }
215
216    Ok(DotGeneralConfig {
217        lhs_contracting_dims: vec![1],
218        rhs_contracting_dims: vec![0],
219        lhs_batch_dims: vec![],
220        rhs_batch_dims: vec![],
221    })
222}
223
224/// Trait for detecting singular or non-finite diagonal entries.
225///
226/// Implemented for `f32`, `f64`, `Complex32`, and `Complex64`.
227/// A value is considered singular if it is zero, NaN, infinite,
228/// or (for complex types) if either component is non-finite.
229pub trait DiagSingularity {
230    /// Returns `true` if the value is singular or non-finite.
231    fn is_singular_or_nonfinite(&self) -> bool;
232}
233
234macro_rules! impl_diag_singularity_float {
235    ($($t:ty),* $(,)?) => {
236        $(
237            impl DiagSingularity for $t {
238                fn is_singular_or_nonfinite(&self) -> bool {
239                    !self.is_finite() || *self == 0.0
240                }
241            }
242        )*
243    };
244}
245
246impl_diag_singularity_float!(f64, f32);
247
248macro_rules! impl_diag_singularity_complex {
249    ($($t:ty),* $(,)?) => {
250        $(
251            impl DiagSingularity for $t {
252                fn is_singular_or_nonfinite(&self) -> bool {
253                    !self.re.is_finite() || !self.im.is_finite() || self.norm_sqr() == 0.0
254                }
255            }
256        )*
257    };
258}
259
260impl_diag_singularity_complex!(Complex64, Complex32);
261
262/// Checks that every diagonal element of a (possibly batched) upper-triangular
263/// factor is non-singular and finite.
264///
265/// Iterates over all batch slices and inspects the diagonal entries
266/// `data[i + i * rows]` for `i` in `0..min(rows, cols)`. Returns
267/// [`Error::BackendFailure`] with `op: "solve"` on the first offending entry,
268/// or [`Error::RankMismatch`] when `t` has rank less than two.
269///
270/// # Examples
271///
272/// ```rust
273/// use tenferro_tensor::validate::check_singular_diagonal;
274/// use tenferro_tensor::TypedTensor;
275///
276/// let t = TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0f32, 0.0, 0.0, 2.0]).unwrap();
277/// assert!(check_singular_diagonal(&t).is_ok());
278/// ```
279pub fn check_singular_diagonal<T: DiagSingularity + Copy + std::fmt::Debug>(
280    t: &TypedTensor<T>,
281) -> Result<()> {
282    if t.shape().len() < 2 {
283        return Err(Error::RankMismatch {
284            op: "solve",
285            expected: 2,
286            actual: t.shape().len(),
287        });
288    }
289    let rows = t.shape()[0];
290    let cols = t.shape()[1];
291    let n = rows.min(cols);
292    let batch_total = checked_shape_product("solve", "batch shape", &t.shape()[2..])?;
293    let slice_size = checked_shape_product("solve", "matrix shape", &t.shape()[..2])?;
294    let data = t.host_data()?;
295    for batch_idx in 0..batch_total {
296        let batch = &data[batch_idx * slice_size..(batch_idx + 1) * slice_size];
297        for i in 0..n {
298            let diag = batch[i + i * rows];
299            if diag.is_singular_or_nonfinite() {
300                return Err(Error::backend_failure(
301                    "solve",
302                    if batch_total > 1 {
303                        format!(
304                            "singular matrix: non-finite or zero diagonal at batch {}, position [{},{}] = {:?}",
305                            batch_idx, i, i, diag
306                        )
307                    } else {
308                        format!(
309                            "singular matrix: non-finite or zero diagonal at position [{},{}] = {:?}",
310                            i, i, diag
311                        )
312                    },
313                ));
314            }
315        }
316    }
317    Ok(())
318}
319
320/// Validates that the upper-triangular factor `u` of a matrix decomposition
321/// has no singular (zero) or non-finite diagonal entries.
322///
323/// Dispatches to [`check_singular_diagonal`] after unpacking the concrete
324/// tensor variant. Returns `Ok(())` when all diagonal entries are valid.
325///
326/// # Examples
327///
328/// ```rust
329/// use tenferro_tensor::validate::validate_nonsingular_u;
330/// use tenferro_tensor::{Tensor, TypedTensor};
331///
332/// let t = Tensor::F64(TypedTensor::from_vec_col_major(vec![2, 2], vec![1.0, 0.0, 0.0, 1.0]).unwrap());
333/// assert!(validate_nonsingular_u(&t).is_ok());
334/// ```
335pub fn validate_nonsingular_u(u: &Tensor) -> Result<()> {
336    match u {
337        Tensor::F64(t) => check_singular_diagonal(t),
338        Tensor::F32(t) => check_singular_diagonal(t),
339        Tensor::C64(t) => check_singular_diagonal(t),
340        Tensor::C32(t) => check_singular_diagonal(t),
341        Tensor::I32(_) | Tensor::I64(_) | Tensor::Bool(_) => Err(Error::backend_failure(
342            "solve",
343            format!("unsupported dtype {:?}", u.dtype()),
344        )),
345    }
346}
347
348#[cfg(test)]
349mod tests;