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;