Skip to main content

tenferro_linalg/
householder.rs

1use std::fmt;
2use std::ops::Range;
3
4use tenferro_tensor::{Engine, Tensor};
5
6use crate::backend::CompactQrResult;
7use crate::QrOptions;
8
9/// Opaque compact Householder QR state.
10///
11/// The packed reflector tensors remain private so callers cannot accidentally
12/// treat provider state as ordinary tensor values.
13///
14/// # Examples
15///
16/// ```rust
17/// use tenferro_cpu::CpuEngine;
18/// use tenferro_linalg::{QrOptions, TensorLinalgExt};
19/// use tenferro_tensor::Tensor;
20///
21/// let a = Tensor::from_vec_col_major(
22///     vec![3, 2],
23///     vec![1.0_f64, 0.0, 1.0, 0.0, 1.0, 1.0],
24/// )?;
25/// let mut host = tenferro_cpu::CpuEngine::serial();
26/// let qr = a.householder_qr(&mut host)?;
27/// let r = qr.r(QrOptions::default(), &mut host)?;
28/// assert_eq!(r.shape(), &[2, 2]);
29/// # Ok::<(), tenferro_tensor::Error>(())
30/// ```
31#[derive(Clone)]
32pub struct HouseholderQr<T> {
33    pub(crate) packed: T,
34    pub(crate) coeff: T,
35}
36
37impl<T> fmt::Debug for HouseholderQr<T> {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        formatter
40            .debug_struct("HouseholderQr")
41            .finish_non_exhaustive()
42    }
43}
44
45impl HouseholderQr<Tensor> {
46    pub(crate) fn from_backend(state: CompactQrResult) -> Self {
47        Self {
48            packed: state.packed,
49            coeff: state.coeff,
50        }
51    }
52
53    /// Construct compact state for the product of compatible factors `Q * R`.
54    ///
55    /// # Errors
56    ///
57    /// Returns `tenferro_tensor::Error::Validation` for incompatible rank,
58    /// shape, dtype, placement, or a non-trapezoidal R factor;
59    /// `tenferro_tensor::Error::Unsupported` when the provider lacks compact
60    /// QR; or `tenferro_tensor::Error::BackendSource` for provider failures.
61    pub fn from_factors(
62        q: &Tensor,
63        r: &Tensor,
64        session: &mut dyn Engine,
65    ) -> tenferro_tensor::Result<Self> {
66        crate::tensor_ext::with_linalg_backend(session, "householder_qr_from_factors", |backend| {
67            backend
68                .householder_qr_from_factors(q, r)
69                .map(Self::from_backend)
70        })
71    }
72
73    /// Append a column block without refactorizing existing columns.
74    ///
75    /// # Errors
76    ///
77    /// Returns `tenferro_tensor::Error::Validation` for incompatible shape,
78    /// dtype, placement, or malformed state; `tenferro_tensor::Error::Unsupported`
79    /// when append is unavailable; or `tenferro_tensor::Error::BackendSource`
80    /// for reflector or factorization failures.
81    pub fn append_columns(
82        &self,
83        block: &Tensor,
84        session: &mut dyn Engine,
85    ) -> tenferro_tensor::Result<Self> {
86        crate::tensor_ext::with_linalg_backend(session, "householder_qr_append", |backend| {
87            backend
88                .householder_qr_append(&self.packed, &self.coeff, block)
89                .map(Self::from_backend)
90        })
91    }
92
93    /// Extract the thin upper-trapezoidal factor.
94    ///
95    /// # Errors
96    ///
97    /// Returns `tenferro_tensor::Error::Validation` for malformed state,
98    /// `tenferro_tensor::Error::Unsupported` for an unavailable provider path,
99    /// or `tenferro_tensor::Error::BackendSource` for extraction failures.
100    pub fn r(
101        &self,
102        options: QrOptions,
103        session: &mut dyn Engine,
104    ) -> tenferro_tensor::Result<Tensor> {
105        crate::tensor_ext::with_linalg_backend(session, "householder_qr_r", |backend| {
106            backend.householder_qr_r(&self.packed, &self.coeff, options)
107        })
108    }
109
110    /// Materialize a contiguous range of Q columns, up to the full-Q width.
111    ///
112    /// For an `m x n` input with `k = min(m, n)`, columns `0..k` are the thin-Q
113    /// factor and columns `k..m` span the orthogonal complement of the input's
114    /// column space — the cheaper route to a nullspace basis than a full SVD.
115    /// `QrOptions::gauge` is defined by R's diagonal, so `PositiveDiagonal`
116    /// fixes the first `k` columns only; a complement column has no diagonal to
117    /// fix and is returned as the reflector product produced it.
118    ///
119    /// # Errors
120    ///
121    /// Returns `tenferro_tensor::Error::Validation` when the range is outside
122    /// the full-Q width `0..m` or state metadata is malformed,
123    /// `tenferro_tensor::Error::Unsupported` for an unavailable provider path,
124    /// or `tenferro_tensor::Error::BackendSource` for execution failures.
125    pub fn q_columns(
126        &self,
127        columns: Range<usize>,
128        options: QrOptions,
129        session: &mut dyn Engine,
130    ) -> tenferro_tensor::Result<Tensor> {
131        crate::tensor_ext::with_linalg_backend(session, "householder_qr_q_columns", |backend| {
132            backend.householder_qr_q_columns(&self.packed, &self.coeff, columns, options)
133        })
134    }
135}
136
137#[cfg(feature = "autodiff")]
138impl HouseholderQr<tenferro_ad::EagerTensor> {
139    pub(crate) fn from_eager_outputs(
140        packed: tenferro_ad::EagerTensor,
141        coeff: tenferro_ad::EagerTensor,
142    ) -> Self {
143        Self { packed, coeff }
144    }
145
146    /// Construct compact state from compatible eager factors.
147    ///
148    /// # Examples
149    /// ```rust
150    /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
151    /// use tenferro_linalg::HouseholderQr;
152    /// let ctx = EagerRuntime::new()?;
153    /// let state = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
154    ///     let q = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![1.0_f64])?)?;
155    ///     let r = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
156    ///     HouseholderQr::<EagerTensor>::from_factors(&q, &r, s)
157    /// })?;
158    /// assert!(format!("{state:?}").starts_with("HouseholderQr"));
159    /// # Ok::<(), tenferro_ad::Error>(())
160    /// ```
161    /// # Errors
162    ///
163    /// Returns `tenferro_ad::Error::Validation` for known invalid metadata,
164    /// `tenferro_ad::Error::Extension` for unsupported or provider failures,
165    /// or `tenferro_ad::Error::RuntimeState` when eager execution is unavailable.
166    pub fn from_factors(
167        q: &tenferro_ad::EagerTensor,
168        r: &tenferro_ad::EagerTensor,
169        session: &mut tenferro_ad::EagerSession<'_>,
170    ) -> tenferro_ad::Result<Self> {
171        eager_state(crate::eager_ext::apply_linalg_eager_in_session(
172            session,
173            crate::extension::LinalgOp::HouseholderQrFromFactors,
174            &[q, r],
175        )?)
176    }
177
178    /// Append an eager column block functionally.
179    ///
180    /// # Examples
181    /// ```rust
182    /// use tenferro_ad::{EagerRuntime, Tensor};
183    /// use tenferro_linalg::EagerSessionLinalgExt;
184    /// let ctx = EagerRuntime::new()?;
185    /// let state = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
186    ///     let a = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![1.0_f64, 0.0])?)?;
187    ///     let b = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![0.0_f64, 1.0])?)?;
188    ///     s.householder_qr(&a)?.append_columns(&b, s)
189    /// })?;
190    /// assert!(format!("{state:?}").starts_with("HouseholderQr"));
191    /// # Ok::<(), tenferro_ad::Error>(())
192    /// ```
193    /// # Errors
194    ///
195    /// Returns `tenferro_ad::Error::Validation` for known invalid metadata,
196    /// `tenferro_ad::Error::Extension` for unsupported or provider failures,
197    /// or `tenferro_ad::Error::RuntimeState` when eager execution is unavailable.
198    pub fn append_columns(
199        &self,
200        block: &tenferro_ad::EagerTensor,
201        session: &mut tenferro_ad::EagerSession<'_>,
202    ) -> tenferro_ad::Result<Self> {
203        eager_state(crate::eager_ext::apply_linalg_eager_in_session(
204            session,
205            crate::extension::LinalgOp::HouseholderQrAppend,
206            &[&self.packed, &self.coeff, block],
207        )?)
208    }
209
210    /// Extract eager R.
211    ///
212    /// # Examples
213    /// ```rust
214    /// use tenferro_ad::{EagerRuntime, Tensor};
215    /// use tenferro_linalg::{EagerSessionLinalgExt, QrOptions};
216    /// let ctx = EagerRuntime::new()?;
217    /// let r = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
218    ///     let a = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![1.0_f64, 2.0])?)?;
219    ///     s.householder_qr(&a)?.r(QrOptions::default(), s)
220    /// })?;
221    /// assert_eq!(r.shape(), &[1, 1]);
222    /// # Ok::<(), tenferro_ad::Error>(())
223    /// ```
224    /// # Errors
225    ///
226    /// Returns `tenferro_ad::Error::Validation` for known invalid metadata,
227    /// `tenferro_ad::Error::Extension` for unsupported or provider failures,
228    /// or `tenferro_ad::Error::RuntimeState` when eager execution is unavailable.
229    pub fn r(
230        &self,
231        options: QrOptions,
232        session: &mut tenferro_ad::EagerSession<'_>,
233    ) -> tenferro_ad::Result<tenferro_ad::EagerTensor> {
234        eager_one(
235            crate::eager_ext::apply_linalg_eager_in_session(
236                session,
237                crate::extension::LinalgOp::HouseholderQrR {
238                    gauge: options.gauge,
239                },
240                &[&self.packed, &self.coeff],
241            )?,
242            "householder_qr_r",
243        )
244    }
245
246    /// Materialize eager Q columns, up to the full-Q width.
247    ///
248    /// # Examples
249    /// ```rust
250    /// use tenferro_ad::{EagerRuntime, Tensor};
251    /// use tenferro_linalg::{EagerSessionLinalgExt, QrOptions};
252    /// let ctx = EagerRuntime::new()?;
253    /// let q = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
254    ///     let a = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![1.0_f64, 2.0])?)?;
255    ///     s.householder_qr(&a)?.q_columns(0..1, QrOptions::default(), s)
256    /// })?;
257    /// assert_eq!(q.shape(), &[2, 1]);
258    /// # Ok::<(), tenferro_ad::Error>(())
259    /// ```
260    /// Columns `0..k` are the thin-Q factor; columns `k..m` span the orthogonal
261    /// complement of the input's column space. `PositiveDiagonal` fixes only
262    /// the first `k` columns, because the gauge comes from R's diagonal.
263    /// Differentiating through a range that reaches past `k` is unsupported and
264    /// surfaces a typed AD error rather than a silently wrong derivative.
265    ///
266    /// # Errors
267    ///
268    /// Returns `tenferro_ad::Error::Validation` for known invalid metadata,
269    /// `tenferro_ad::Error::Extension` for unsupported or provider failures,
270    /// or `tenferro_ad::Error::RuntimeState` when eager execution is unavailable.
271    pub fn q_columns(
272        &self,
273        columns: Range<usize>,
274        options: QrOptions,
275        session: &mut tenferro_ad::EagerSession<'_>,
276    ) -> tenferro_ad::Result<tenferro_ad::EagerTensor> {
277        eager_one(
278            crate::eager_ext::apply_linalg_eager_in_session(
279                session,
280                crate::extension::LinalgOp::HouseholderQrQColumns {
281                    start: columns.start,
282                    end: columns.end,
283                    gauge: options.gauge,
284                },
285                &[&self.packed, &self.coeff],
286            )?,
287            "householder_qr_q_columns",
288        )
289    }
290}
291
292#[cfg(feature = "autodiff")]
293pub(crate) fn eager_state(
294    outputs: Vec<tenferro_ad::EagerTensor>,
295) -> tenferro_ad::Result<HouseholderQr<tenferro_ad::EagerTensor>> {
296    let mut outputs = outputs.into_iter();
297    match (outputs.next(), outputs.next(), outputs.next()) {
298        (Some(packed), Some(coeff), None) => Ok(HouseholderQr::from_eager_outputs(packed, coeff)),
299        _ => Err(tenferro_ad::Error::Internal(
300            "compact Householder QR returned an unexpected output count".into(),
301        )),
302    }
303}
304
305#[cfg(feature = "autodiff")]
306fn eager_one(
307    outputs: Vec<tenferro_ad::EagerTensor>,
308    op: &'static str,
309) -> tenferro_ad::Result<tenferro_ad::EagerTensor> {
310    let mut outputs = outputs.into_iter();
311    match (outputs.next(), outputs.next()) {
312        (Some(output), None) => Ok(output),
313        _ => Err(tenferro_ad::Error::Internal(format!(
314            "{op} returned an unexpected output count"
315        ))),
316    }
317}
318
319impl HouseholderQr<tenferro_runtime::TracedTensor> {
320    pub(crate) fn from_traced_outputs(
321        packed: tenferro_runtime::TracedTensor,
322        coeff: tenferro_runtime::TracedTensor,
323    ) -> Self {
324        Self { packed, coeff }
325    }
326
327    /// Construct compact state from compatible traced factors.
328    ///
329    /// # Errors
330    ///
331    /// Returns `tenferro_runtime::Error::Validation` for known invalid metadata
332    /// or `tenferro_runtime::Error::Extension` for unsupported operation state.
333    ///
334    /// # Deferred errors
335    ///
336    /// Symbolic shape constraints and backend provider failures may be reported
337    /// during compile or execution.
338    pub fn from_factors(
339        q: &tenferro_runtime::TracedTensor,
340        r: &tenferro_runtime::TracedTensor,
341    ) -> tenferro_runtime::Result<Self> {
342        crate::validation::ensure_float_or_complex("householder_qr_from_factors", q.dtype())?;
343        crate::validation::ensure_float_or_complex("householder_qr_from_factors", r.dtype())?;
344        traced_state(
345            crate::extension::LinalgOp::HouseholderQrFromFactors,
346            &[q, r],
347        )
348    }
349
350    /// Append a traced column block functionally.
351    ///
352    /// # Errors
353    ///
354    /// Returns `tenferro_runtime::Error::Validation` for known invalid metadata
355    /// or `tenferro_runtime::Error::Extension` for unsupported operation state.
356    ///
357    /// # Deferred errors
358    ///
359    /// Symbolic shape constraints and backend provider failures may be reported
360    /// during compile or execution.
361    pub fn append_columns(
362        &self,
363        block: &tenferro_runtime::TracedTensor,
364    ) -> tenferro_runtime::Result<Self> {
365        crate::validation::ensure_float_or_complex("householder_qr_append", block.dtype())?;
366        traced_state(
367            crate::extension::LinalgOp::HouseholderQrAppend,
368            &[&self.packed, &self.coeff, block],
369        )
370    }
371
372    /// Extract traced R.
373    ///
374    /// # Errors
375    ///
376    /// Returns `tenferro_runtime::Error::Validation` for known invalid metadata
377    /// or `tenferro_runtime::Error::Extension` for unsupported operation state.
378    ///
379    /// # Deferred errors
380    ///
381    /// Symbolic shape constraints and backend provider failures may be reported
382    /// during compile or execution.
383    pub fn r(
384        &self,
385        options: QrOptions,
386    ) -> tenferro_runtime::Result<tenferro_runtime::TracedTensor> {
387        traced_one(
388            crate::extension::LinalgOp::HouseholderQrR {
389                gauge: options.gauge,
390            },
391            &[&self.packed, &self.coeff],
392            "householder_qr_r",
393        )
394    }
395
396    /// Materialize traced Q columns, up to the full-Q width.
397    ///
398    /// Columns `0..k` are the thin-Q factor; columns `k..m` span the orthogonal
399    /// complement of the input's column space. `PositiveDiagonal` fixes only
400    /// the first `k` columns, because the gauge comes from R's diagonal.
401    /// Differentiating through a range that reaches past `k` is unsupported and
402    /// surfaces a typed AD error rather than a silently wrong derivative.
403    ///
404    /// # Errors
405    ///
406    /// Returns `tenferro_runtime::Error::Validation` for known invalid metadata
407    /// or `tenferro_runtime::Error::Extension` for unsupported operation state.
408    ///
409    /// # Deferred errors
410    ///
411    /// Symbolic shape constraints and backend provider failures may be reported
412    /// during compile or execution.
413    pub fn q_columns(
414        &self,
415        columns: Range<usize>,
416        options: QrOptions,
417    ) -> tenferro_runtime::Result<tenferro_runtime::TracedTensor> {
418        traced_one(
419            crate::extension::LinalgOp::HouseholderQrQColumns {
420                start: columns.start,
421                end: columns.end,
422                gauge: options.gauge,
423            },
424            &[&self.packed, &self.coeff],
425            "householder_qr_q_columns",
426        )
427    }
428}
429
430fn traced_outputs(
431    op: crate::extension::LinalgOp,
432    inputs: &[&tenferro_runtime::TracedTensor],
433) -> tenferro_runtime::Result<Vec<tenferro_runtime::TracedTensor>> {
434    tenferro_runtime::extension::apply(
435        std::sync::Arc::new(crate::extension::LinalgExtensionOp::new(op)),
436        inputs,
437    )
438}
439
440fn traced_state(
441    op: crate::extension::LinalgOp,
442    inputs: &[&tenferro_runtime::TracedTensor],
443) -> tenferro_runtime::Result<HouseholderQr<tenferro_runtime::TracedTensor>> {
444    let mut outputs = traced_outputs(op, inputs)?.into_iter();
445    match (outputs.next(), outputs.next(), outputs.next()) {
446        (Some(packed), Some(coeff), None) => Ok(HouseholderQr::from_traced_outputs(packed, coeff)),
447        _ => Err(tenferro_runtime::Error::Internal(
448            "compact Householder QR returned an unexpected output count".into(),
449        )),
450    }
451}
452
453fn traced_one(
454    op: crate::extension::LinalgOp,
455    inputs: &[&tenferro_runtime::TracedTensor],
456    name: &'static str,
457) -> tenferro_runtime::Result<tenferro_runtime::TracedTensor> {
458    let mut outputs = traced_outputs(op, inputs)?.into_iter();
459    match (outputs.next(), outputs.next()) {
460        (Some(output), None) => Ok(output),
461        _ => Err(tenferro_runtime::Error::Internal(format!(
462            "{name} returned an unexpected output count"
463        ))),
464    }
465}