Skip to main content

tenferro_linalg/
extension.rs

1use std::any::Any;
2use std::hash::Hasher;
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_cpu::with_cpu_exec_session;
7use tenferro_extension_macros::define_extension_runtime;
8use tenferro_ops::SymDim;
9use tenferro_runtime::extension::{ExtensionExecutionContext, ExtensionOp};
10use tenferro_tensor::{BackendSession, DType, Error, ErrorKind, Tensor, TensorBackend, TensorRead};
11
12#[cfg(feature = "cuda")]
13use tenferro_gpu::cuda::with_cuda_exec_session;
14
15use crate::backend::LinalgBackend;
16use crate::RankRevealingQrOptions;
17
18mod gauge;
19#[cfg(all(test, not(feature = "cuda")))]
20mod tests;
21
22pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
23
24pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
25
26/// Default derivative regularization used by decomposition AD rules.
27///
28/// This epsilon is used only when differentiating decomposition formulas with
29/// repeated or nearly repeated spectral values. It is not a solver tolerance.
30///
31/// # Examples
32///
33/// ```rust
34/// use tenferro_linalg::{SvdOptions, DEFAULT_DECOMPOSITION_DERIVATIVE_EPS};
35///
36/// let options = SvdOptions::default();
37/// assert_eq!(options.derivative_eps, DEFAULT_DECOMPOSITION_DERIVATIVE_EPS);
38/// ```
39pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
40
41/// Singular-vector gauge convention used by [`SvdOptions`].
42///
43/// # Examples
44///
45/// ```rust
46/// use tenferro_linalg::{SvdGauge, SvdOptions};
47///
48/// let options = SvdOptions::default().gauge(SvdGauge::CanonicalPivot);
49/// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
50/// ```
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum SvdGauge {
53    /// Leave the backend's raw singular vector signs or phases unchanged.
54    Raw,
55    /// Make each left singular vector's max-absolute pivot entry positive-real
56    /// and adjust the matching `VT` row so reconstruction is preserved.
57    CanonicalPivot,
58}
59
60/// Eigenvector gauge convention used by [`EighOptions`].
61///
62/// # Examples
63///
64/// ```rust
65/// use tenferro_linalg::{EighGauge, EighOptions};
66///
67/// let options = EighOptions::default().gauge(EighGauge::CanonicalPivot);
68/// assert_eq!(options.gauge, EighGauge::CanonicalPivot);
69/// ```
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum EighGauge {
72    /// Leave the backend's raw eigenvector signs or phases unchanged.
73    Raw,
74    /// Make each eigenvector's max-absolute pivot entry positive-real.
75    CanonicalPivot,
76}
77
78/// QR factor gauge convention used by [`QrOptions`].
79///
80/// # Examples
81///
82/// ```rust
83/// use tenferro_linalg::{QrGauge, QrOptions};
84///
85/// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
86/// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
87/// ```
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum QrGauge {
90    /// Leave the backend's raw QR signs or phases unchanged.
91    Raw,
92    /// Make each `R` diagonal entry positive-real, compensating `Q`.
93    PositiveDiagonal,
94}
95
96/// Options for singular value decomposition.
97///
98/// # Examples
99///
100/// ```rust
101/// use tenferro_linalg::{SvdGauge, SvdOptions};
102///
103/// let options = SvdOptions::default()
104///     .gauge(SvdGauge::CanonicalPivot)
105///     .derivative_eps(1.0e-10);
106/// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
107/// assert_eq!(options.derivative_eps, 1.0e-10);
108/// ```
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct SvdOptions {
111    /// Singular-vector gauge convention.
112    pub gauge: SvdGauge,
113    /// AD derivative regularization for repeated or nearly repeated singular values.
114    pub derivative_eps: f64,
115}
116
117impl Default for SvdOptions {
118    fn default() -> Self {
119        Self {
120            gauge: SvdGauge::Raw,
121            derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
122        }
123    }
124}
125
126impl SvdOptions {
127    /// Return options with the requested singular-vector gauge.
128    ///
129    /// # Examples
130    ///
131    /// ```rust
132    /// use tenferro_linalg::{SvdGauge, SvdOptions};
133    ///
134    /// let options = SvdOptions::default().gauge(SvdGauge::CanonicalPivot);
135    /// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
136    /// ```
137    pub fn gauge(mut self, gauge: SvdGauge) -> Self {
138        self.gauge = gauge;
139        self
140    }
141
142    /// Return options with an explicit derivative epsilon.
143    ///
144    /// # Examples
145    ///
146    /// ```rust
147    /// use tenferro_linalg::SvdOptions;
148    ///
149    /// let options = SvdOptions::default().derivative_eps(1.0e-9);
150    /// assert_eq!(options.derivative_eps, 1.0e-9);
151    /// ```
152    pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
153        self.derivative_eps = derivative_eps;
154        self
155    }
156}
157
158/// Options for Hermitian eigenvalue decomposition.
159///
160/// # Examples
161///
162/// ```rust
163/// use tenferro_linalg::EighOptions;
164///
165/// let options = EighOptions::default().derivative_eps(1.0e-10);
166/// assert_eq!(options.derivative_eps, 1.0e-10);
167/// ```
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub struct EighOptions {
170    /// Eigenvector gauge convention.
171    pub gauge: EighGauge,
172    /// AD derivative regularization for repeated or nearly repeated eigenvalues.
173    pub derivative_eps: f64,
174}
175
176impl Default for EighOptions {
177    fn default() -> Self {
178        Self {
179            gauge: EighGauge::Raw,
180            derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
181        }
182    }
183}
184
185impl EighOptions {
186    /// Return options with the requested eigenvector gauge.
187    ///
188    /// # Examples
189    ///
190    /// ```rust
191    /// use tenferro_linalg::{EighGauge, EighOptions};
192    ///
193    /// let options = EighOptions::default().gauge(EighGauge::CanonicalPivot);
194    /// assert_eq!(options.gauge, EighGauge::CanonicalPivot);
195    /// ```
196    pub fn gauge(mut self, gauge: EighGauge) -> Self {
197        self.gauge = gauge;
198        self
199    }
200
201    /// Return options with an explicit derivative epsilon.
202    ///
203    /// # Examples
204    ///
205    /// ```rust
206    /// use tenferro_linalg::EighOptions;
207    ///
208    /// let options = EighOptions::default().derivative_eps(1.0e-9);
209    /// assert_eq!(options.derivative_eps, 1.0e-9);
210    /// ```
211    pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
212        self.derivative_eps = derivative_eps;
213        self
214    }
215}
216
217/// Options for QR decomposition.
218///
219/// # Examples
220///
221/// ```rust
222/// use tenferro_linalg::{QrGauge, QrOptions};
223///
224/// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
225/// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
226/// ```
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub struct QrOptions {
229    /// QR sign or phase convention.
230    pub gauge: QrGauge,
231}
232
233impl Default for QrOptions {
234    fn default() -> Self {
235        Self {
236            gauge: QrGauge::Raw,
237        }
238    }
239}
240
241impl QrOptions {
242    /// Return options with the requested QR gauge.
243    ///
244    /// # Examples
245    ///
246    /// ```rust
247    /// use tenferro_linalg::{QrGauge, QrOptions};
248    ///
249    /// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
250    /// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
251    /// ```
252    pub fn gauge(mut self, gauge: QrGauge) -> Self {
253        self.gauge = gauge;
254        self
255    }
256}
257
258pub(crate) fn validate_derivative_eps(
259    op: &'static str,
260    derivative_eps: f64,
261) -> tenferro_tensor::Result<()> {
262    if derivative_eps.is_finite() && derivative_eps > 0.0 {
263        Ok(())
264    } else {
265        Err(Error::invalid_argument(
266            op,
267            "derivative_eps",
268            format!("must be positive and finite, got {derivative_eps}"),
269        ))
270    }
271}
272
273#[derive(Clone, Copy, Debug, PartialEq)]
274#[doc(hidden)]
275#[allow(dead_code)]
276pub(crate) enum LinalgOp {
277    Cholesky,
278    Lu,
279    LuFactor,
280    LuSolvePrepared {
281        transpose_a: bool,
282        conjugate_a: bool,
283    },
284    SignDetFromLuFactor,
285    LogAbsDetFromLuFactor,
286    FullPivLu,
287    FullPivLuSolve {
288        transpose_a: bool,
289    },
290    /// Solve `a @ x = b` with partial-pivot LU (same kernel as
291    /// `LinalgBackend::solve`). Two inputs (matrix, rhs) to one output.
292    /// Only the eager surface (autodiff feature) constructs this variant;
293    /// the traced `solve` composite stays LuFactor + LuSolvePrepared.
294    #[cfg_attr(not(feature = "autodiff"), allow(dead_code))]
295    Solve,
296    Svd {
297        derivative_eps: f64,
298        gauge: SvdGauge,
299    },
300    /// Full-matrices SVD: `U` is `m x m` and `Vh` is `n x n`, so the trailing
301    /// `Vh` rows span the input's right nullspace. Value-only: AD is
302    /// intentionally unsupported (see the linalg AD support manifest).
303    SvdFull,
304    SvdVals {
305        derivative_eps: f64,
306    },
307    Qr {
308        gauge: QrGauge,
309    },
310    RankRevealingQr {
311        gauge: QrGauge,
312        rtol: f64,
313        atol: f64,
314    },
315    HouseholderQrFactor,
316    HouseholderQrFromFactors,
317    HouseholderQrAppend,
318    HouseholderQrR {
319        gauge: QrGauge,
320    },
321    HouseholderQrQColumns {
322        start: usize,
323        end: usize,
324        gauge: QrGauge,
325    },
326    /// Internal AD residual operation for symbolic full thin-Q recovery.
327    HouseholderQrThinQ {
328        gauge: QrGauge,
329    },
330    /// Internal linear operation for abstract-state column append.
331    HouseholderQrAppendTangent,
332    /// Internal transpose operation that splits an appended state cotangent.
333    HouseholderQrSplitTangent {
334        right: bool,
335    },
336    Eigh {
337        derivative_eps: f64,
338        gauge: EighGauge,
339    },
340    EighVals {
341        derivative_eps: f64,
342    },
343    Eig {
344        input_dtype: DType,
345    },
346    EigVals {
347        input_dtype: DType,
348    },
349    TriangularSolve {
350        left_side: bool,
351        lower: bool,
352        transpose_a: bool,
353        unit_diagonal: bool,
354    },
355}
356
357impl LinalgOp {
358    fn output_count(self) -> usize {
359        match self {
360            Self::Cholesky
361            | Self::EighVals { .. }
362            | Self::EigVals { .. }
363            | Self::FullPivLuSolve { .. }
364            | Self::LogAbsDetFromLuFactor
365            | Self::LuSolvePrepared { .. }
366            | Self::SignDetFromLuFactor
367            | Self::Solve
368            | Self::SvdVals { .. }
369            | Self::TriangularSolve { .. } => 1,
370            Self::Svd { .. } | Self::SvdFull => 3,
371            Self::RankRevealingQr { .. } | Self::Lu => 4,
372            Self::Qr { .. }
373            | Self::HouseholderQrFactor
374            | Self::HouseholderQrFromFactors
375            | Self::HouseholderQrAppend
376            | Self::Eigh { .. }
377            | Self::Eig { .. } => 2,
378            Self::HouseholderQrR { .. }
379            | Self::HouseholderQrQColumns { .. }
380            | Self::HouseholderQrThinQ { .. }
381            | Self::HouseholderQrAppendTangent
382            | Self::HouseholderQrSplitTangent { .. } => 1,
383            Self::LuFactor => 3,
384            Self::FullPivLu => 5,
385        }
386    }
387
388    fn input_count(self) -> usize {
389        match self {
390            Self::FullPivLuSolve { .. }
391            | Self::Solve
392            | Self::TriangularSolve { .. }
393            | Self::HouseholderQrFromFactors
394            | Self::HouseholderQrR { .. }
395            | Self::HouseholderQrQColumns { .. }
396            | Self::HouseholderQrThinQ { .. } => 2,
397            Self::LogAbsDetFromLuFactor => 2,
398            Self::SignDetFromLuFactor | Self::HouseholderQrAppend => 3,
399            Self::HouseholderQrAppendTangent => 4,
400            Self::HouseholderQrSplitTangent { .. } => 3,
401            Self::LuSolvePrepared { .. } => 4,
402            _ => 1,
403        }
404    }
405
406    fn tag(self) -> u8 {
407        match self {
408            Self::Cholesky => 0,
409            Self::Lu => 1,
410            Self::FullPivLu => 2,
411            Self::FullPivLuSolve { .. } => 3,
412            Self::Svd { .. } => 4,
413            Self::Qr { .. } => 5,
414            Self::Eigh { .. } => 6,
415            Self::Eig { .. } => 7,
416            Self::TriangularSolve { .. } => 9,
417            Self::LuFactor => 10,
418            Self::LuSolvePrepared { .. } => 11,
419            Self::SvdVals { .. } => 12,
420            Self::EighVals { .. } => 13,
421            Self::EigVals { .. } => 14,
422            Self::SvdFull => 15,
423            Self::LogAbsDetFromLuFactor => 16,
424            Self::SignDetFromLuFactor => 17,
425            Self::Solve => 18,
426            Self::HouseholderQrFactor => 19,
427            Self::HouseholderQrFromFactors => 20,
428            Self::HouseholderQrAppend => 21,
429            Self::HouseholderQrR { .. } => 22,
430            Self::HouseholderQrQColumns { .. } => 23,
431            Self::HouseholderQrThinQ { .. } => 24,
432            Self::HouseholderQrAppendTangent => 25,
433            Self::HouseholderQrSplitTangent { .. } => 26,
434            Self::RankRevealingQr { .. } => 27,
435        }
436    }
437}
438
439#[derive(Clone, Debug, PartialEq)]
440#[doc(hidden)]
441pub(crate) struct LinalgExtensionOp {
442    op: LinalgOp,
443}
444
445impl LinalgExtensionOp {
446    pub(crate) fn new(op: LinalgOp) -> Self {
447        Self { op }
448    }
449
450    pub(crate) fn op(&self) -> LinalgOp {
451        self.op
452    }
453}
454
455impl ExtensionOp for LinalgExtensionOp {
456    fn family_id(&self) -> &'static str {
457        LINALG_EXTENSION_FAMILY_ID
458    }
459
460    fn payload_hash(&self, hasher: &mut dyn Hasher) {
461        hasher.write_u8(self.op.tag());
462        match self.op {
463            LinalgOp::Svd {
464                derivative_eps,
465                gauge,
466            } => {
467                hasher.write_u64(derivative_eps.to_bits());
468                hash_svd_gauge(hasher, gauge);
469            }
470            LinalgOp::SvdVals { derivative_eps } | LinalgOp::EighVals { derivative_eps } => {
471                hasher.write_u64(derivative_eps.to_bits());
472            }
473            LinalgOp::Qr { gauge }
474            | LinalgOp::HouseholderQrR { gauge }
475            | LinalgOp::HouseholderQrThinQ { gauge } => {
476                hash_qr_gauge(hasher, gauge);
477            }
478            LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
479                hash_qr_gauge(hasher, gauge);
480                hasher.write_u64(rtol.to_bits());
481                hasher.write_u64(atol.to_bits());
482            }
483            LinalgOp::HouseholderQrQColumns { start, end, gauge } => {
484                hasher.write_usize(start);
485                hasher.write_usize(end);
486                hash_qr_gauge(hasher, gauge);
487            }
488            LinalgOp::Eigh {
489                derivative_eps,
490                gauge,
491            } => {
492                hasher.write_u64(derivative_eps.to_bits());
493                hash_eigh_gauge(hasher, gauge);
494            }
495            LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
496                hash_dtype(hasher, input_dtype);
497            }
498            LinalgOp::FullPivLuSolve { transpose_a }
499            | LinalgOp::HouseholderQrSplitTangent { right: transpose_a } => {
500                hasher.write_u8(u8::from(transpose_a));
501            }
502            LinalgOp::LuSolvePrepared {
503                transpose_a,
504                conjugate_a,
505            } => {
506                hasher.write_u8(u8::from(transpose_a));
507                hasher.write_u8(u8::from(conjugate_a));
508            }
509            LinalgOp::TriangularSolve {
510                left_side,
511                lower,
512                transpose_a,
513                unit_diagonal,
514            } => {
515                hasher.write_u8(u8::from(left_side));
516                hasher.write_u8(u8::from(lower));
517                hasher.write_u8(u8::from(transpose_a));
518                hasher.write_u8(u8::from(unit_diagonal));
519            }
520            LinalgOp::Cholesky
521            | LinalgOp::Lu
522            | LinalgOp::LuFactor
523            | LinalgOp::LogAbsDetFromLuFactor
524            | LinalgOp::SignDetFromLuFactor
525            | LinalgOp::FullPivLu
526            | LinalgOp::SvdFull
527            | LinalgOp::Solve
528            | LinalgOp::HouseholderQrFactor
529            | LinalgOp::HouseholderQrFromFactors
530            | LinalgOp::HouseholderQrAppend
531            | LinalgOp::HouseholderQrAppendTangent => {}
532        }
533    }
534
535    fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
536        other
537            .as_any()
538            .downcast_ref::<Self>()
539            .is_some_and(|that| self == that)
540    }
541
542    fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
543        Arc::new(self.clone())
544    }
545
546    fn as_any(&self) -> &dyn Any {
547        self
548    }
549
550    fn input_count(&self) -> usize {
551        self.op.input_count()
552    }
553
554    fn output_count(&self) -> usize {
555        self.op.output_count()
556    }
557
558    fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
559        tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
560    }
561
562    fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
563        tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
564    }
565
566    fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
567        match self.op {
568            LinalgOp::Svd { derivative_eps, .. } if live_outputs == [false, true, false] => {
569                Some(Arc::new(Self::new(LinalgOp::SvdVals { derivative_eps })))
570            }
571            LinalgOp::Eigh { derivative_eps, .. } if live_outputs == [true, false] => {
572                Some(Arc::new(Self::new(LinalgOp::EighVals { derivative_eps })))
573            }
574            LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
575                Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
576            }
577            _ => None,
578        }
579    }
580
581    fn infer_output_meta(
582        &self,
583        ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
584    ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
585        let input_dtypes = (0..self.input_count())
586            .map(|input| ctx.input_dtype(input))
587            .collect::<Result<Vec<_>, _>>()?;
588        let input_shapes = (0..self.input_count())
589            .map(|input| ctx.input_shape(input))
590            .collect::<Result<Vec<_>, _>>()?;
591        let metas = match self.op {
592            LinalgOp::Cholesky => {
593                require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
594                vec![(promote_dtypes(&input_dtypes), input_shapes[0].to_vec())]
595            }
596            LinalgOp::FullPivLuSolve { .. } => {
597                require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
598                require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
599                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
600            }
601            LinalgOp::Solve => {
602                require_matrix_meta("tenferro-linalg.solve", input_shapes[0])?;
603                require_matrix_meta("tenferro-linalg.solve", input_shapes[1])?;
604                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
605            }
606            LinalgOp::TriangularSolve { .. } => {
607                require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
608                require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
609                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
610            }
611            LinalgOp::LuSolvePrepared { .. } => {
612                require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
613                require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
614                vec![(
615                    promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
616                    input_shapes[3].to_vec(),
617                )]
618            }
619            LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
620            LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
621            LinalgOp::SignDetFromLuFactor => {
622                vec![signdet_from_lu_factor_meta(
623                    input_dtypes[0],
624                    input_shapes[0],
625                    input_shapes[1],
626                    input_shapes[2],
627                )?]
628            }
629            LinalgOp::LogAbsDetFromLuFactor => {
630                vec![logabsdet_from_lu_factor_meta(
631                    input_dtypes[0],
632                    input_shapes[0],
633                    input_shapes[1],
634                )?]
635            }
636            LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
637            LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
638            LinalgOp::SvdFull => svd_full_meta(input_dtypes[0], input_shapes[0])?,
639            LinalgOp::SvdVals { .. } => {
640                vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
641            }
642            LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
643            LinalgOp::RankRevealingQr { .. } => {
644                rank_revealing_qr_meta(input_dtypes[0], input_shapes[0])?
645            }
646            LinalgOp::HouseholderQrFactor => {
647                householder_qr_factor_meta(input_dtypes[0], input_shapes[0])?
648            }
649            LinalgOp::HouseholderQrFromFactors => {
650                householder_qr_from_factors_meta(&input_dtypes, &input_shapes)?
651            }
652            LinalgOp::HouseholderQrAppend => {
653                householder_qr_append_meta(&input_dtypes, &input_shapes)?
654            }
655            LinalgOp::HouseholderQrR { .. } => vec![householder_qr_r_meta(
656                &input_dtypes,
657                input_shapes[0],
658                input_shapes[1],
659            )?],
660            LinalgOp::HouseholderQrQColumns { start, end, .. } => {
661                vec![householder_qr_q_columns_meta(
662                    &input_dtypes,
663                    input_shapes[0],
664                    input_shapes[1],
665                    start,
666                    end,
667                )?]
668            }
669            LinalgOp::HouseholderQrThinQ { .. } => {
670                vec![householder_qr_thin_q_meta(
671                    &input_dtypes,
672                    input_shapes[0],
673                    input_shapes[1],
674                )?]
675            }
676            LinalgOp::HouseholderQrAppendTangent => {
677                vec![householder_qr_append_tangent_meta(
678                    &input_dtypes,
679                    &input_shapes,
680                )?]
681            }
682            LinalgOp::HouseholderQrSplitTangent { right } => {
683                vec![householder_qr_split_tangent_meta(
684                    &input_dtypes,
685                    &input_shapes,
686                    right,
687                )?]
688            }
689            LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
690            LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
691            LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
692            LinalgOp::EigVals { input_dtype } => {
693                vec![eig_values_meta(input_dtype, input_shapes[0])?]
694            }
695        };
696        Ok(metas)
697    }
698}
699
700pub(crate) fn execute_linalg_extension_reads<B: BackendSession + ?Sized>(
701    op: &LinalgExtensionOp,
702    inputs: &[TensorRead<'_>],
703    ctx: &mut ExtensionExecutionContext<'_, B>,
704) -> tenferro_tensor::Result<Vec<Tensor>> {
705    execute_linalg_extension_reads_on_session(op, inputs, ctx.backend_mut())
706}
707
708pub(crate) fn execute_linalg_extension_reads_owner<B: TensorBackend>(
709    op: &LinalgExtensionOp,
710    inputs: &[TensorRead<'_>],
711    ctx: &mut ExtensionExecutionContext<'_, B>,
712) -> tenferro_tensor::Result<Vec<Tensor>> {
713    let (backend, caches) = ctx.parts_mut();
714    backend.with_backend_session(|session| {
715        let mut session_ctx = ExtensionExecutionContext::new(session, caches);
716        execute_linalg_extension_reads(op, inputs, &mut session_ctx)
717    })
718}
719
720fn execute_linalg_extension_reads_on_session<B: BackendSession + ?Sized>(
721    op: &LinalgExtensionOp,
722    inputs: &[TensorRead<'_>],
723    session: &mut B,
724) -> tenferro_tensor::Result<Vec<Tensor>> {
725    if let Some(result) = with_cpu_exec_session(session, |session| {
726        execute_linalg_extension_reads_in_session(op, inputs, session)
727    }) {
728        return result;
729    }
730    #[cfg(feature = "cuda")]
731    if let Some(result) = with_cuda_exec_session(session, |session| {
732        execute_linalg_extension_reads_in_session(op, inputs, session)
733    }) {
734        return result;
735    }
736    Err(Error::unsupported(
737        "linalg_extension",
738        "selected backend session does not expose a linalg execution capability",
739    ))
740}
741
742fn execute_linalg_extension_reads_in_session<S: LinalgBackend>(
743    op: &LinalgExtensionOp,
744    inputs: &[TensorRead<'_>],
745    session: &mut S,
746) -> tenferro_tensor::Result<Vec<Tensor>> {
747    if op.op() == LinalgOp::HouseholderQrAppendTangent {
748        let left = session.to_contiguous_read(inputs[0].clone())?;
749        let right = session.to_contiguous_read(inputs[1].clone())?;
750        return Ok(vec![session.concatenate(&[&left, &right], 1)?]);
751    }
752    if let LinalgOp::HouseholderQrSplitTangent { right } = op.op() {
753        let cotangent_shape = inputs[0].clone().tensor_view().shape().to_vec();
754        let left_shape = inputs[1].clone().tensor_view().shape().to_vec();
755        let right_shape = inputs[2].clone().tensor_view().shape().to_vec();
756        let config =
757            householder_qr_split_config(&cotangent_shape, &left_shape, &right_shape, right)?;
758        let cotangent = session.to_contiguous_read(inputs[0].clone())?;
759        return Ok(vec![session.slice(&cotangent, &config)?]);
760    }
761    if op.op() == LinalgOp::Cholesky {
762        return Ok(vec![session.cholesky_read(inputs[0].clone())?]);
763    }
764    if let LinalgOp::TriangularSolve {
765        left_side,
766        lower,
767        transpose_a,
768        unit_diagonal,
769    } = op.op()
770    {
771        match session.triangular_solve_read(
772            inputs[0].clone(),
773            inputs[1].clone(),
774            left_side,
775            lower,
776            transpose_a,
777            unit_diagonal,
778        ) {
779            Ok(output) => return Ok(vec![output]),
780            Err(error) if error.kind() == ErrorKind::Unsupported => {}
781            Err(error) => return Err(error),
782        }
783    }
784
785    // Linalg kernels operate on compact tensors; materialization is explicit
786    // here so borrowed views cannot bypass provider errors.
787    let materialized_inputs = inputs
788        .iter()
789        .cloned()
790        .map(|input| session.to_contiguous_read(input))
791        .collect::<tenferro_tensor::Result<Vec<_>>>()?;
792    let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
793    execute_linalg(op.op(), &input_refs, session)
794}
795
796fn linalg_session_supported<B: BackendSession + 'static>(op: &LinalgExtensionOp) -> bool {
797    // The `supports_session` contract (capability.rs) requires that an op is
798    // admitted to a scheduler session only when the session executor genuinely
799    // executes it without returning `Unsupported`. Admission is exactly
800    // per-op/per-backend so `apply_eager` keeps the native prepared path for
801    // every op the session can actually run (issue #1665).
802    let type_id = std::any::TypeId::of::<B>();
803    if type_id == std::any::TypeId::of::<tenferro_cpu::CpuBackend>() {
804        // The CPU backend type does not carry its provider kind (faer vs BLAS)
805        // at this type-only seam, and the BLAS provider does not implement
806        // in-session full-matrices SVD, so SvdFull is conservatively rejected
807        // and falls back to the compiled path. Every other CPU linalg kernel
808        // runs in-session on both faer and BLAS providers.
809        return op.op() != LinalgOp::SvdFull;
810    }
811    #[cfg(feature = "cuda")]
812    {
813        if type_id == std::any::TypeId::of::<tenferro_gpu::cuda::CudaBackend>() {
814            return match op.op() {
815                // Complete-pivoting LU and general eig have no CUDA kernels.
816                LinalgOp::FullPivLu | LinalgOp::FullPivLuSolve { .. } => false,
817                LinalgOp::Eig { .. } | LinalgOp::EigVals { .. } => false,
818                // Plain partial-pivot solve runs in-session via cuSOLVER
819                // getrf plus prepared pivot/triangular solves
820                // (`gpu/linalg.rs::solve` = lu_factor + lu_solve_prepared, no
821                // Unsupported path for F32/F64/C32/C64), so it is admitted.
822                LinalgOp::Solve => true,
823                // Full-matrices SVD falls back to the default `svd_full`
824                // impl, which reports `Unsupported`.
825                LinalgOp::SvdFull => false,
826                // Conjugate-only prepared LU solve is unsupported on CUDA.
827                LinalgOp::LuSolvePrepared {
828                    transpose_a: false,
829                    conjugate_a: true,
830                } => false,
831                _ => true,
832            };
833        }
834    }
835    false
836}
837
838fn execute_linalg_extension_in_session(
839    op: &LinalgExtensionOp,
840    session: &mut dyn BackendSession,
841    _extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
842    inputs: &[TensorRead<'_>],
843) -> tenferro_tensor::Result<Vec<Tensor>> {
844    // Reuse the existing session executor that the eager and scheduler paths
845    // already share; it downcasts the borrowed session to the CPU/CUDA exec
846    // session and runs the same forward kernel for every LinalgOp.
847    execute_linalg_extension_reads_on_session(op, inputs, session)
848}
849
850define_extension_runtime! {
851    runtime = LinalgRuntime,
852    family_id = LINALG_EXTENSION_FAMILY_ID,
853    op_type = LinalgExtensionOp,
854    execute = execute_linalg_extension_reads_owner,
855    execute_reads = execute_linalg_extension_reads_owner,
856    execute_in_session = execute_linalg_extension_in_session,
857    session_supported = linalg_session_supported,
858    backend_bound = TensorBackend,
859}
860
861fn execute_linalg<B: LinalgBackend>(
862    op: LinalgOp,
863    inputs: &[&Tensor],
864    backend: &mut B,
865) -> tenferro_tensor::Result<Vec<Tensor>> {
866    match op {
867        LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
868        LinalgOp::Lu => backend.lu(inputs[0]),
869        LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
870        LinalgOp::SignDetFromLuFactor => {
871            Ok(vec![signdet_from_lu_factor(inputs[1], inputs[2], backend)?])
872        }
873        LinalgOp::LogAbsDetFromLuFactor => Ok(vec![logabsdet_from_lu_factor(inputs[1], backend)?]),
874        LinalgOp::LuSolvePrepared {
875            transpose_a,
876            conjugate_a,
877        } => Ok(vec![backend.lu_solve_prepared(
878            inputs[0],
879            inputs[1],
880            inputs[2],
881            inputs[3],
882            transpose_a,
883            conjugate_a,
884        )?]),
885        LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
886        LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
887            inputs[0],
888            inputs[1],
889            transpose_a,
890        )?]),
891        LinalgOp::Solve => Ok(vec![backend.solve(inputs[0], inputs[1])?]),
892        LinalgOp::Svd {
893            derivative_eps,
894            gauge,
895        } => backend.svd_with_options(
896            inputs[0],
897            SvdOptions {
898                derivative_eps,
899                gauge,
900            },
901        ),
902        LinalgOp::SvdFull => backend.svd_full(inputs[0]),
903        LinalgOp::SvdVals { .. } => Ok(vec![backend.svd_values(inputs[0])?]),
904        LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
905        LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
906            backend.rank_revealing_qr(inputs[0], RankRevealingQrOptions { gauge, rtol, atol })
907        }
908        LinalgOp::HouseholderQrFactor => {
909            let state = backend.householder_qr(inputs[0])?;
910            Ok(vec![state.packed, state.coeff])
911        }
912        LinalgOp::HouseholderQrFromFactors => {
913            let state = backend.householder_qr_from_factors(inputs[0], inputs[1])?;
914            Ok(vec![state.packed, state.coeff])
915        }
916        LinalgOp::HouseholderQrAppend => {
917            let state = backend.householder_qr_append(inputs[0], inputs[1], inputs[2])?;
918            Ok(vec![state.packed, state.coeff])
919        }
920        LinalgOp::HouseholderQrR { gauge } => Ok(vec![backend.householder_qr_r(
921            inputs[0],
922            inputs[1],
923            QrOptions { gauge },
924        )?]),
925        LinalgOp::HouseholderQrQColumns { start, end, gauge } => Ok(vec![backend
926            .householder_qr_q_columns(inputs[0], inputs[1], start..end, QrOptions { gauge })?]),
927        LinalgOp::HouseholderQrThinQ { gauge } => {
928            let end = inputs[1].shape().first().copied().ok_or_else(|| {
929                Error::rank_mismatch("tenferro-linalg.householder_qr_thin_q", 1, 0)
930            })?;
931            Ok(vec![backend.householder_qr_q_columns(
932                inputs[0],
933                inputs[1],
934                0..end,
935                QrOptions { gauge },
936            )?])
937        }
938        LinalgOp::HouseholderQrAppendTangent => {
939            Ok(vec![backend.concatenate(&[inputs[0], inputs[1]], 1)?])
940        }
941        LinalgOp::HouseholderQrSplitTangent { right } => {
942            let config = householder_qr_split_config(
943                inputs[0].shape(),
944                inputs[1].shape(),
945                inputs[2].shape(),
946                right,
947            )?;
948            Ok(vec![backend.slice(inputs[0], &config)?])
949        }
950        LinalgOp::Eigh {
951            derivative_eps,
952            gauge,
953        } => backend.eigh_with_options(
954            inputs[0],
955            EighOptions {
956                derivative_eps,
957                gauge,
958            },
959        ),
960        LinalgOp::EighVals { .. } => Ok(vec![backend.eigh_values(inputs[0])?]),
961        LinalgOp::Eig { .. } => backend.eig(inputs[0]),
962        LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
963        LinalgOp::TriangularSolve {
964            left_side,
965            lower,
966            transpose_a,
967            unit_diagonal,
968        } => Ok(vec![backend.triangular_solve(
969            inputs[0],
970            inputs[1],
971            left_side,
972            lower,
973            transpose_a,
974            unit_diagonal,
975        )?]),
976    }
977}
978
979/// Sign (or complex phase) of the determinant from an LU factorization.
980///
981/// The sign is built from the per-pivot signs rather than from the determinant
982/// product, so it is magnitude-independent: a determinant whose product would
983/// underflow or overflow still reports its mathematical sign. `Sign` maps an
984/// exactly zero pivot to zero, so finite singular LU factors report zero sign
985/// for both real and complex inputs, matching the existing eager composite.
986fn signdet_from_lu_factor<B: LinalgBackend + ?Sized>(
987    packed_lu: &Tensor,
988    parity: &Tensor,
989    backend: &mut B,
990) -> tenferro_tensor::Result<Tensor> {
991    let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
992    let sign_diag = backend.sign_read(TensorRead::from_tensor(&diag))?;
993    let sign_u = backend.reduce_prod_read(TensorRead::from_tensor(&sign_diag), &[0])?;
994    backend.mul_read(
995        TensorRead::from_tensor(parity),
996        TensorRead::from_tensor(&sign_u),
997    )
998}
999
1000fn logabsdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1001    packed_lu: &Tensor,
1002    backend: &mut B,
1003) -> tenferro_tensor::Result<Tensor> {
1004    let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1005    let abs = backend.abs_read(TensorRead::from_tensor(&diag))?;
1006    let log = backend.log_read(TensorRead::from_tensor(&abs))?;
1007    backend.reduce_sum_read(TensorRead::from_tensor(&log), &[0])
1008}
1009
1010pub(crate) fn apply_svd_gauge(
1011    gauge: SvdGauge,
1012    outputs: &mut [Tensor],
1013) -> tenferro_tensor::Result<()> {
1014    match gauge {
1015        SvdGauge::Raw => Ok(()),
1016        SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
1017    }
1018}
1019
1020fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
1021    if outputs.len() != 3 {
1022        return Err(Error::invalid_argument(
1023            "tenferro-linalg.svd",
1024            "outputs",
1025            format!(
1026                "canonical SVD gauge expected three outputs, got {}",
1027                outputs.len()
1028            ),
1029        ));
1030    }
1031
1032    let (u_slice, rest) = outputs.split_at_mut(1);
1033    let (singular_slice, vt_slice) = rest.split_at_mut(1);
1034    let u = &mut u_slice[0];
1035    let singular_values = &singular_slice[0];
1036    let vt = &mut vt_slice[0];
1037    let u_shape = u.shape().to_vec();
1038    let s_shape = singular_values.shape().to_vec();
1039    let vt_shape = vt.shape().to_vec();
1040    if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
1041        return Err(Error::invalid_argument(
1042            "tenferro-linalg.svd",
1043            "outputs",
1044            format!(
1045                "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1046            ),
1047        ));
1048    }
1049
1050    let m = u_shape[0];
1051    let k = u_shape[1];
1052    let n = vt_shape[1];
1053    if s_shape[0] != k
1054        || vt_shape[0] != k
1055        || u_shape[2..] != vt_shape[2..]
1056        || s_shape[1..] != u_shape[2..]
1057    {
1058        return Err(Error::invalid_argument(
1059            "tenferro-linalg.svd",
1060            "outputs",
1061            format!(
1062                "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1063            ),
1064        ));
1065    }
1066    let layout = canonical_svd_gauge_layout(m, k, n, &u_shape[2..])?;
1067
1068    match (u, vt) {
1069        (Tensor::F64(u), Tensor::F64(vt)) => {
1070            canonicalize_svd_gauge_f64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1071        }
1072        (Tensor::F32(u), Tensor::F32(vt)) => {
1073            canonicalize_svd_gauge_f32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1074        }
1075        (Tensor::C64(u), Tensor::C64(vt)) => {
1076            canonicalize_svd_gauge_c64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1077        }
1078        (Tensor::C32(u), Tensor::C32(vt)) => {
1079            canonicalize_svd_gauge_c32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1080        }
1081        (u, vt) => Err(Error::dtype_mismatch(
1082            "tenferro-linalg.svd",
1083            u.dtype(),
1084            vt.dtype(),
1085        )),
1086    }
1087}
1088
1089#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1090struct CanonicalSvdGaugeLayout {
1091    m: usize,
1092    k: usize,
1093    batch_count: usize,
1094    u_batch_len: usize,
1095    vt_batch_len: usize,
1096    u_len: usize,
1097    vt_len: usize,
1098}
1099
1100impl CanonicalSvdGaugeLayout {
1101    fn validate_storage(self, u_len: usize, vt_len: usize) -> tenferro_tensor::Result<()> {
1102        if u_len != self.u_len {
1103            return Err(Error::invalid_argument(
1104                "tenferro-linalg.svd",
1105                "U storage",
1106                format!(
1107                    "canonical SVD gauge expected U storage length {}, got {u_len}",
1108                    self.u_len
1109                ),
1110            ));
1111        }
1112        if vt_len != self.vt_len {
1113            return Err(Error::invalid_argument(
1114                "tenferro-linalg.svd",
1115                "VT storage",
1116                format!(
1117                    "canonical SVD gauge expected VT storage length {}, got {vt_len}",
1118                    self.vt_len
1119                ),
1120            ));
1121        }
1122        Ok(())
1123    }
1124}
1125
1126fn canonical_svd_gauge_layout(
1127    m: usize,
1128    k: usize,
1129    n: usize,
1130    batch_shape: &[usize],
1131) -> tenferro_tensor::Result<CanonicalSvdGaugeLayout> {
1132    let batch_count = tenferro_tensor::validate::checked_shape_product(
1133        "tenferro-linalg.svd",
1134        "canonical SVD batch",
1135        batch_shape,
1136    )?;
1137    let u_batch_len = tenferro_tensor::validate::checked_shape_product(
1138        "tenferro-linalg.svd",
1139        "canonical SVD U batch",
1140        &[m, k],
1141    )?;
1142    let vt_batch_len = tenferro_tensor::validate::checked_shape_product(
1143        "tenferro-linalg.svd",
1144        "canonical SVD VT batch",
1145        &[k, n],
1146    )?;
1147    let u_len = tenferro_tensor::validate::checked_shape_product(
1148        "tenferro-linalg.svd",
1149        "canonical SVD U storage",
1150        &[u_batch_len, batch_count],
1151    )?;
1152    let vt_len = tenferro_tensor::validate::checked_shape_product(
1153        "tenferro-linalg.svd",
1154        "canonical SVD VT storage",
1155        &[vt_batch_len, batch_count],
1156    )?;
1157    Ok(CanonicalSvdGaugeLayout {
1158        m,
1159        k,
1160        batch_count,
1161        u_batch_len,
1162        vt_batch_len,
1163        u_len,
1164        vt_len,
1165    })
1166}
1167
1168fn canonicalize_svd_gauge_f64(
1169    u: &mut [f64],
1170    vt: &mut [f64],
1171    layout: CanonicalSvdGaugeLayout,
1172) -> tenferro_tensor::Result<()> {
1173    layout.validate_storage(u.len(), vt.len())?;
1174    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1175        return Ok(());
1176    }
1177    for (u_batch, vt_batch) in u
1178        .chunks_exact_mut(layout.u_batch_len)
1179        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1180    {
1181        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1182            let pivot = max_abs_pivot_f64(u_column);
1183            let pivot_value = u_column[pivot];
1184            if pivot_value < 0.0 {
1185                for value in u_column {
1186                    *value = -*value;
1187                }
1188                for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1189                    vt_column[col] = -vt_column[col];
1190                }
1191            }
1192        }
1193    }
1194    Ok(())
1195}
1196
1197fn canonicalize_svd_gauge_f32(
1198    u: &mut [f32],
1199    vt: &mut [f32],
1200    layout: CanonicalSvdGaugeLayout,
1201) -> tenferro_tensor::Result<()> {
1202    layout.validate_storage(u.len(), vt.len())?;
1203    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1204        return Ok(());
1205    }
1206    for (u_batch, vt_batch) in u
1207        .chunks_exact_mut(layout.u_batch_len)
1208        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1209    {
1210        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1211            let pivot = max_abs_pivot_f32(u_column);
1212            let pivot_value = u_column[pivot];
1213            if pivot_value < 0.0 {
1214                for value in u_column {
1215                    *value = -*value;
1216                }
1217                for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1218                    vt_column[col] = -vt_column[col];
1219                }
1220            }
1221        }
1222    }
1223    Ok(())
1224}
1225
1226fn canonicalize_svd_gauge_c64(
1227    u: &mut [Complex64],
1228    vt: &mut [Complex64],
1229    layout: CanonicalSvdGaugeLayout,
1230) -> tenferro_tensor::Result<()> {
1231    layout.validate_storage(u.len(), vt.len())?;
1232    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1233        return Ok(());
1234    }
1235    for (u_batch, vt_batch) in u
1236        .chunks_exact_mut(layout.u_batch_len)
1237        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1238    {
1239        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1240            let pivot = max_abs_pivot_c64(u_column);
1241            let pivot_value = u_column[pivot];
1242            let pivot_norm = pivot_value.norm();
1243            if pivot_norm == 0.0 {
1244                continue;
1245            }
1246            let phase = pivot_value.conj() / pivot_norm;
1247            let vt_phase = phase.conj();
1248            for value in u_column {
1249                *value *= phase;
1250            }
1251            for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1252                vt_column[col] *= vt_phase;
1253            }
1254        }
1255    }
1256    Ok(())
1257}
1258
1259fn canonicalize_svd_gauge_c32(
1260    u: &mut [Complex32],
1261    vt: &mut [Complex32],
1262    layout: CanonicalSvdGaugeLayout,
1263) -> tenferro_tensor::Result<()> {
1264    layout.validate_storage(u.len(), vt.len())?;
1265    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1266        return Ok(());
1267    }
1268    for (u_batch, vt_batch) in u
1269        .chunks_exact_mut(layout.u_batch_len)
1270        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1271    {
1272        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1273            let pivot = max_abs_pivot_c32(u_column);
1274            let pivot_value = u_column[pivot];
1275            let pivot_norm = pivot_value.norm();
1276            if pivot_norm == 0.0 {
1277                continue;
1278            }
1279            let phase = pivot_value.conj() / pivot_norm;
1280            let vt_phase = phase.conj();
1281            for value in u_column {
1282                *value *= phase;
1283            }
1284            for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1285                vt_column[col] *= vt_phase;
1286            }
1287        }
1288    }
1289    Ok(())
1290}
1291
1292fn max_abs_pivot_f64(u_column: &[f64]) -> usize {
1293    let mut pivot = 0;
1294    let mut pivot_abs = u_column[0].abs();
1295    for (row, value) in u_column.iter().enumerate().skip(1) {
1296        let candidate_abs = value.abs();
1297        if candidate_abs > pivot_abs {
1298            pivot = row;
1299            pivot_abs = candidate_abs;
1300        }
1301    }
1302    pivot
1303}
1304
1305fn max_abs_pivot_f32(u_column: &[f32]) -> usize {
1306    let mut pivot = 0;
1307    let mut pivot_abs = u_column[0].abs();
1308    for (row, value) in u_column.iter().enumerate().skip(1) {
1309        let candidate_abs = value.abs();
1310        if candidate_abs > pivot_abs {
1311            pivot = row;
1312            pivot_abs = candidate_abs;
1313        }
1314    }
1315    pivot
1316}
1317
1318fn max_abs_pivot_c64(u_column: &[Complex64]) -> usize {
1319    let mut pivot = 0;
1320    let mut pivot_abs = u_column[0].norm_sqr();
1321    for (row, value) in u_column.iter().enumerate().skip(1) {
1322        let candidate_abs = value.norm_sqr();
1323        if candidate_abs > pivot_abs {
1324            pivot = row;
1325            pivot_abs = candidate_abs;
1326        }
1327    }
1328    pivot
1329}
1330
1331fn max_abs_pivot_c32(u_column: &[Complex32]) -> usize {
1332    let mut pivot = 0;
1333    let mut pivot_abs = u_column[0].norm_sqr();
1334    for (row, value) in u_column.iter().enumerate().skip(1) {
1335        let candidate_abs = value.norm_sqr();
1336        if candidate_abs > pivot_abs {
1337            pivot = row;
1338            pivot_abs = candidate_abs;
1339        }
1340    }
1341    pivot
1342}
1343
1344fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1345    if shape.len() < 2 {
1346        return Err(Error::rank_mismatch(op, 2, shape.len()));
1347    }
1348    Ok(())
1349}
1350
1351fn matrix_meta_parts<'a>(
1352    op: &'static str,
1353    shape: &'a [SymDim],
1354) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1355    require_matrix_meta(op, shape)?;
1356    Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1357}
1358
1359fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1360    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1361    let k = m.clone().min(n.clone());
1362    Ok(vec![
1363        (dtype, matrix_shape(m.clone(), m, batch)),
1364        (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1365        (dtype, matrix_shape(k, n, batch)),
1366        (dtype, batch.to_vec()),
1367    ])
1368}
1369
1370fn lu_factor_meta(
1371    dtype: DType,
1372    shape: &[SymDim],
1373) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1374    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1375    let k = m.min(n);
1376    Ok(vec![
1377        (dtype, shape.to_vec()),
1378        (DType::I32, vector_shape(k, batch)),
1379        (dtype, batch.to_vec()),
1380    ])
1381}
1382
1383fn signdet_from_lu_factor_meta(
1384    input_dtype: DType,
1385    input_shape: &[SymDim],
1386    packed_shape: &[SymDim],
1387    parity_shape: &[SymDim],
1388) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1389    let (_, _, batch) = matrix_meta_parts("tenferro-linalg.signdet_from_lu_factor", input_shape)?;
1390    require_matrix_meta(
1391        "tenferro-linalg.signdet_from_lu_factor_packed",
1392        packed_shape,
1393    )?;
1394    if parity_shape.len() != batch.len() {
1395        return Err(Error::rank_mismatch(
1396            "tenferro-linalg.signdet_from_lu_factor_parity",
1397            batch.len(),
1398            parity_shape.len(),
1399        ));
1400    }
1401    Ok((input_dtype, batch.to_vec()))
1402}
1403
1404fn logabsdet_from_lu_factor_meta(
1405    input_dtype: DType,
1406    input_shape: &[SymDim],
1407    packed_shape: &[SymDim],
1408) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1409    let (_, _, batch) = matrix_meta_parts("tenferro-linalg.logabsdet_from_lu_factor", input_shape)?;
1410    require_matrix_meta(
1411        "tenferro-linalg.logabsdet_from_lu_factor_packed",
1412        packed_shape,
1413    )?;
1414    Ok((singular_values_dtype(input_dtype), batch.to_vec()))
1415}
1416
1417fn full_piv_lu_meta(
1418    dtype: DType,
1419    shape: &[SymDim],
1420) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1421    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1422    Ok(vec![
1423        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1424        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1425        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1426        (dtype, matrix_shape(n.clone(), n, batch)),
1427        (singular_values_dtype(dtype), batch.to_vec()),
1428    ])
1429}
1430
1431fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1432    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1433    let k = m.clone().min(n.clone());
1434    Ok(vec![
1435        (dtype, matrix_shape(m, k.clone(), batch)),
1436        (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1437        (dtype, matrix_shape(k, n, batch)),
1438    ])
1439}
1440
1441fn svd_full_meta(
1442    dtype: DType,
1443    shape: &[SymDim],
1444) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1445    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_full", shape)?;
1446    let k = m.clone().min(n.clone());
1447    Ok(vec![
1448        (dtype, matrix_shape(m.clone(), m, batch)),
1449        (singular_values_dtype(dtype), vector_shape(k, batch)),
1450        (dtype, matrix_shape(n.clone(), n, batch)),
1451    ])
1452}
1453
1454fn svd_values_meta(
1455    dtype: DType,
1456    shape: &[SymDim],
1457) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1458    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1459    let k = m.min(n);
1460    Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1461}
1462
1463fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1464    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1465    let k = m.clone().min(n.clone());
1466    Ok(vec![
1467        (dtype, matrix_shape(m, k.clone(), batch)),
1468        (dtype, matrix_shape(k, n, batch)),
1469    ])
1470}
1471
1472fn rank_revealing_qr_meta(
1473    dtype: DType,
1474    shape: &[SymDim],
1475) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1476    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.rank_revealing_qr", shape)?;
1477    let k = m.clone().min(n.clone());
1478    Ok(vec![
1479        (dtype, matrix_shape(m, k.clone(), batch)),
1480        (dtype, matrix_shape(k, n.clone(), batch)),
1481        (DType::I64, vector_shape(n, batch)),
1482        (DType::I64, batch.to_vec()),
1483    ])
1484}
1485
1486fn require_householder_rank2(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1487    if shape.len() != 2 {
1488        return Err(Error::rank_mismatch(op, 2, shape.len()));
1489    }
1490    Ok(())
1491}
1492
1493fn householder_qr_factor_meta(
1494    dtype: DType,
1495    shape: &[SymDim],
1496) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1497    require_householder_rank2("tenferro-linalg.householder_qr", shape)?;
1498    let k = shape[0].clone().min(shape[1].clone());
1499    Ok(vec![(dtype, shape.to_vec()), (dtype, vec![k])])
1500}
1501
1502fn householder_qr_from_factors_meta(
1503    dtypes: &[DType],
1504    shapes: &[&[SymDim]],
1505) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1506    const OP: &str = "tenferro-linalg.householder_qr_from_factors";
1507    require_householder_rank2(OP, shapes[0])?;
1508    require_householder_rank2(OP, shapes[1])?;
1509    if dtypes[0] != dtypes[1] {
1510        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1511    }
1512    require_static_extent_equal(OP, "q.cols/r.rows", &shapes[0][1], &shapes[1][0])?;
1513    if let (Some(q_cols), Some(q_rows), Some(r_cols)) = (
1514        shapes[0][1].constant_value(),
1515        shapes[0][0].constant_value(),
1516        shapes[1][1].constant_value(),
1517    ) {
1518        if q_cols > q_rows.min(r_cols) {
1519            return Err(Error::invalid_argument(
1520                OP,
1521                "shape",
1522                "Q column count exceeds min(Q rows, R columns)",
1523            ));
1524        }
1525    }
1526    let m = shapes[0][0].clone();
1527    let n = shapes[1][1].clone();
1528    let k = m.clone().min(n.clone());
1529    Ok(vec![(dtypes[0], vec![m, n]), (dtypes[0], vec![k])])
1530}
1531
1532fn householder_qr_append_meta(
1533    dtypes: &[DType],
1534    shapes: &[&[SymDim]],
1535) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1536    const OP: &str = "tenferro-linalg.householder_qr_append";
1537    require_householder_state_meta(OP, dtypes, shapes[0], shapes[1])?;
1538    require_householder_rank2(OP, shapes[2])?;
1539    if dtypes[0] != dtypes[2] {
1540        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[2]));
1541    }
1542    require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[2][0])?;
1543    let m = shapes[0][0].clone();
1544    let width = shapes[0][1].clone() + shapes[2][1].clone();
1545    let k = m.clone().min(width.clone());
1546    Ok(vec![(dtypes[0], vec![m, width]), (dtypes[0], vec![k])])
1547}
1548
1549fn require_static_extent_equal(
1550    op: &'static str,
1551    field: &'static str,
1552    lhs: &SymDim,
1553    rhs: &SymDim,
1554) -> tenferro_tensor::Result<()> {
1555    if let (Some(lhs), Some(rhs)) = (lhs.constant_value(), rhs.constant_value()) {
1556        if lhs != rhs {
1557            return Err(Error::invalid_argument(
1558                op,
1559                field,
1560                format!("expected equal extents, got {lhs} and {rhs}"),
1561            ));
1562        }
1563    }
1564    Ok(())
1565}
1566
1567fn require_householder_state_meta(
1568    op: &'static str,
1569    dtypes: &[DType],
1570    packed: &[SymDim],
1571    coeff: &[SymDim],
1572) -> tenferro_tensor::Result<()> {
1573    require_householder_rank2(op, packed)?;
1574    if coeff.len() != 1 {
1575        return Err(Error::rank_mismatch(op, 1, coeff.len()));
1576    }
1577    if dtypes[0] != dtypes[1] {
1578        return Err(Error::dtype_mismatch(op, dtypes[0], dtypes[1]));
1579    }
1580    let expected = packed[0].clone().min(packed[1].clone());
1581    require_static_extent_equal(op, "coeff", &coeff[0], &expected)
1582}
1583
1584fn householder_qr_r_meta(
1585    dtypes: &[DType],
1586    packed: &[SymDim],
1587    coeff: &[SymDim],
1588) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1589    require_householder_state_meta("tenferro-linalg.householder_qr_r", dtypes, packed, coeff)?;
1590    Ok((dtypes[0], vec![coeff[0].clone(), packed[1].clone()]))
1591}
1592
1593fn householder_qr_q_columns_meta(
1594    dtypes: &[DType],
1595    packed: &[SymDim],
1596    coeff: &[SymDim],
1597    start: usize,
1598    end: usize,
1599) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1600    require_householder_state_meta(
1601        "tenferro-linalg.householder_qr_q_columns",
1602        dtypes,
1603        packed,
1604        coeff,
1605    )?;
1606    if start > end {
1607        return Err(Error::invalid_argument(
1608            "tenferro-linalg.householder_qr_q_columns",
1609            "range",
1610            format!("invalid Q-column range {start}..{end}"),
1611        ));
1612    }
1613    if coeff[0].constant_value().is_some_and(|k| end > k) {
1614        return Err(Error::invalid_argument(
1615            "tenferro-linalg.householder_qr_q_columns",
1616            "range",
1617            format!("Q-column range {start}..{end} exceeds thin-Q width"),
1618        ));
1619    }
1620    Ok((
1621        dtypes[0],
1622        vec![packed[0].clone(), SymDim::from(end - start)],
1623    ))
1624}
1625
1626fn householder_qr_thin_q_meta(
1627    dtypes: &[DType],
1628    packed: &[SymDim],
1629    coeff: &[SymDim],
1630) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1631    require_householder_state_meta(
1632        "tenferro-linalg.householder_qr_thin_q",
1633        dtypes,
1634        packed,
1635        coeff,
1636    )?;
1637    Ok((dtypes[0], vec![packed[0].clone(), coeff[0].clone()]))
1638}
1639
1640fn householder_qr_split_config(
1641    cotangent: &[usize],
1642    left: &[usize],
1643    right_shape: &[usize],
1644    take_right: bool,
1645) -> tenferro_tensor::Result<tenferro_tensor::SliceConfig> {
1646    const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1647    for shape in [cotangent, left, right_shape] {
1648        if shape.len() != 2 {
1649            return Err(Error::rank_mismatch(OP, 2, shape.len()));
1650        }
1651    }
1652    let total_width = left[1]
1653        .checked_add(right_shape[1])
1654        .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1655    if cotangent[0] != left[0] || cotangent[0] != right_shape[0] || cotangent[1] != total_width {
1656        return Err(Error::invalid_argument(
1657            OP,
1658            "shape",
1659            "cotangent shape does not match appended factors",
1660        ));
1661    }
1662    let selected = if take_right { right_shape } else { left };
1663    let start = if take_right { left[1] } else { 0 };
1664    let end = start
1665        .checked_add(selected[1])
1666        .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1667    Ok(tenferro_tensor::SliceConfig {
1668        starts: vec![0, start],
1669        limits: vec![selected[0], end],
1670        strides: vec![1, 1],
1671    })
1672}
1673
1674fn householder_qr_append_tangent_meta(
1675    dtypes: &[DType],
1676    shapes: &[&[SymDim]],
1677) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1678    const OP: &str = "tenferro-linalg.householder_qr_append_tangent";
1679    for shape in shapes {
1680        require_householder_rank2(OP, shape)?;
1681    }
1682    if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1683        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1684    }
1685    require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[1][0])?;
1686    require_static_extent_equal(OP, "left tangent", &shapes[0][0], &shapes[2][0])?;
1687    require_static_extent_equal(OP, "right tangent", &shapes[1][0], &shapes[3][0])?;
1688    require_static_extent_equal(OP, "anchor rows", &shapes[2][0], &shapes[3][0])?;
1689    Ok((
1690        dtypes[0],
1691        vec![
1692            shapes[2][0].clone(),
1693            shapes[2][1].clone() + shapes[3][1].clone(),
1694        ],
1695    ))
1696}
1697
1698fn householder_qr_split_tangent_meta(
1699    dtypes: &[DType],
1700    shapes: &[&[SymDim]],
1701    right: bool,
1702) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1703    const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1704    for shape in shapes {
1705        require_householder_rank2(OP, shape)?;
1706    }
1707    if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1708        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1709    }
1710    require_static_extent_equal(OP, "left rows", &shapes[0][0], &shapes[1][0])?;
1711    require_static_extent_equal(OP, "right rows", &shapes[0][0], &shapes[2][0])?;
1712    let expected_width = shapes[1][1].clone() + shapes[2][1].clone();
1713    require_static_extent_equal(OP, "width", &shapes[0][1], &expected_width)?;
1714    let selected = if right { shapes[2] } else { shapes[1] };
1715    Ok((dtypes[0], selected.to_vec()))
1716}
1717
1718fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1719    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
1720    Ok(vec![
1721        (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
1722        (dtype, matrix_shape(n.clone(), n, batch)),
1723    ])
1724}
1725
1726fn eigh_values_meta(
1727    dtype: DType,
1728    shape: &[SymDim],
1729) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1730    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
1731    Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
1732}
1733
1734fn eig_meta(
1735    input_dtype: DType,
1736    shape: &[SymDim],
1737) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1738    let dtype = eig_output_dtype(input_dtype);
1739    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
1740    Ok(vec![
1741        (dtype, vector_shape(n.clone(), batch)),
1742        (dtype, matrix_shape(n.clone(), n, batch)),
1743    ])
1744}
1745
1746fn eig_values_meta(
1747    input_dtype: DType,
1748    shape: &[SymDim],
1749) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1750    let dtype = eig_output_dtype(input_dtype);
1751    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
1752    Ok((dtype, vector_shape(n, batch)))
1753}
1754
1755fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1756    let mut shape = vec![rows, cols];
1757    shape.extend_from_slice(batch);
1758    shape
1759}
1760
1761fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
1762    let mut shape = vec![len];
1763    shape.extend_from_slice(batch);
1764    shape
1765}
1766
1767fn eig_output_dtype(dtype: DType) -> DType {
1768    match dtype {
1769        DType::F64 | DType::C64 => DType::C64,
1770        DType::F32 | DType::C32 => DType::C32,
1771        DType::I32 | DType::I64 | DType::Bool => DType::C64,
1772    }
1773}
1774
1775fn singular_values_dtype(dtype: DType) -> DType {
1776    match dtype {
1777        DType::C64 => DType::F64,
1778        DType::C32 => DType::F32,
1779        other => other,
1780    }
1781}
1782
1783fn promote_dtypes(dtypes: &[DType]) -> DType {
1784    dtypes
1785        .iter()
1786        .copied()
1787        .reduce(tenferro_tensor::validate::promote_dtype)
1788        .unwrap_or(DType::F64)
1789}
1790
1791fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
1792    let tag = match dtype {
1793        DType::F64 => 0,
1794        DType::F32 => 1,
1795        DType::I64 => 2,
1796        DType::C64 => 3,
1797        DType::C32 => 4,
1798        DType::I32 => 5,
1799        DType::Bool => 6,
1800    };
1801    hasher.write_u8(tag);
1802}
1803
1804fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
1805    let tag = match gauge {
1806        SvdGauge::Raw => 0,
1807        SvdGauge::CanonicalPivot => 1,
1808    };
1809    hasher.write_u8(tag);
1810}
1811
1812fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
1813    let tag = match gauge {
1814        EighGauge::Raw => 0,
1815        EighGauge::CanonicalPivot => 1,
1816    };
1817    hasher.write_u8(tag);
1818}
1819
1820fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
1821    let tag = match gauge {
1822        QrGauge::Raw => 0,
1823        QrGauge::PositiveDiagonal => 1,
1824    };
1825    hasher.write_u8(tag);
1826}