tensor4all_core/matrix_luci.rs
1//! Matrix LU-based Cross Interpolation (MatrixLUCI) implementation.
2//!
3//! [`MatrixLUCI`] provides a higher-level [`Matrix`] API over the lower-level
4//! `matrixluci` substrate. It decomposes a matrix into left and right factors
5//! via LU cross interpolation and implements [`AbstractMatrixCI`].
6
7use crate::error::{MatrixCIError, Result};
8use crate::matrixlu::{
9 rrlu, rrlu_left_mut, rrlu_mut, validate_luci_options, validate_owned_luci_input, RrLU,
10 RrLUOptions,
11};
12use crate::matrixluci::block_rook::LazyBlockRookKernel;
13use crate::matrixluci::factors::CrossFactors;
14use crate::matrixluci::source::LazyMatrixSource;
15use crate::matrixluci::types::{PivotKernelOptions, PivotSelectionCore};
16use crate::matrixluci::PivotKernel;
17use crate::scalar::Scalar;
18use crate::traits::AbstractMatrixCI;
19use tensor4all_tensorbackend::{
20 explicit::Session, mat_mul_owned, submatrix, triangular_solve_matrix_owned,
21 CpuExecutionContext, Matrix,
22};
23
24/// Where MatrixLUCI factor assembly runs its matrix products and solves.
25///
26/// `Global` keeps the compatibility routes of the legacy entry points;
27/// `Context` runs every operation in one explicit concrete session of the
28/// caller's context and never consults the process-global default.
29pub(crate) enum Linalg<'a, 'session> {
30 Global,
31 Context(&'a CpuExecutionContext),
32 /// A caller-supplied session, entered by the caller rather than opened here.
33 ///
34 /// This is the form a worker lane uses: the session was entered by the phase that owns
35 /// the worker, so re-entering a context here would either be refused or pick another
36 /// backend.
37 Session(&'a mut Session<'session>),
38}
39
40impl Linalg<'_, '_> {
41 fn mat_mul<T>(&mut self, a: Matrix<T>, b: Matrix<T>) -> std::result::Result<Matrix<T>, String>
42 where
43 T: Scalar + crate::MatrixLuciScalar,
44 {
45 match self {
46 Self::Global => mat_mul_owned(a, b).map_err(|err| err.to_string()),
47 Self::Context(context) => context
48 .with_concrete_session(|session| session.mat_mul(&a, &b))
49 .map_err(|err| err.to_string())?
50 .map_err(|err| err.to_string()),
51 Self::Session(session) => session.mat_mul(&a, &b).map_err(|err| err.to_string()),
52 }
53 }
54
55 fn triangular_solve<T>(
56 &mut self,
57 a: Matrix<T>,
58 b: Matrix<T>,
59 left_side: bool,
60 lower: bool,
61 ) -> std::result::Result<Matrix<T>, String>
62 where
63 T: Scalar + crate::MatrixLuciScalar,
64 {
65 match self {
66 Self::Global => triangular_solve_matrix_owned(a, b, left_side, lower, false, false)
67 .map_err(|err| err.to_string()),
68 Self::Context(context) => context
69 .with_concrete_session(|session| {
70 T::matrix_luci_triangular_solve_in(session, &a, &b, left_side, lower)
71 })
72 .map_err(|err| err.to_string())?
73 .map_err(|err| err.to_string()),
74 Self::Session(session) => {
75 T::matrix_luci_triangular_solve_in(session, &a, &b, left_side, lower)
76 .map_err(|err| err.to_string())
77 }
78 }
79 }
80}
81
82/// Matrix LU-based Cross Interpolation.
83///
84/// This is a higher-level [`Matrix`] wrapper around the lower-level `matrixluci`
85/// substrate.
86///
87/// # Examples
88///
89/// ```
90/// use tensor4all_core::{AbstractMatrixCI, MatrixLUCI};
91/// use tensor4all_tensorbackend::from_vec2d;
92///
93/// let m = from_vec2d(vec![
94/// vec![1.0_f64, 2.0, 3.0],
95/// vec![4.0, 5.0, 6.0],
96/// vec![7.0, 8.0, 9.0],
97/// ]);
98///
99/// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
100/// // The approximation must have at most rank min(nrows, ncols)
101/// assert!(ci.rank() <= 3);
102/// // Reconstructed matrix should match original at pivot positions
103/// let row_indices = ci.row_indices().to_vec();
104/// let col_indices = ci.col_indices().to_vec();
105/// for (&i, &j) in row_indices.iter().zip(col_indices.iter()) {
106/// let approx = ci.evaluate(i, j);
107/// let exact = m[[i, j]];
108/// assert!((approx - exact).abs() < 1e-10);
109/// }
110/// ```
111#[derive(Debug, Clone)]
112pub struct MatrixLUCI<T: Scalar + crate::MatrixLuciScalar> {
113 nrows: usize,
114 ncols: usize,
115 row_indices: Vec<usize>,
116 col_indices: Vec<usize>,
117 left: Matrix<T>,
118 right: Matrix<T>,
119 pivot_errors: Vec<f64>,
120}
121
122/// High-level factors produced by MatrixLUCI.
123///
124/// This is the public result of the LUCI factorization facade. It exposes
125/// the selected pivot metadata and the left and right factors
126/// needed by higher-level tensor-network code without exposing the low-level
127/// pivot kernel substrate.
128///
129/// Related types: [`MatrixLUCI`] is the owning CI wrapper, while
130/// [`MatrixACA`](crate::MatrixACA) and [`RrLU`](crate::RrLU) are related
131/// matrix factorization entry points.
132///
133/// # Examples
134///
135/// ```
136/// use tensor4all_core::matrix_luci_factors_from_matrix;
137/// use tensor4all_tensorbackend::from_vec2d;
138///
139/// let m = from_vec2d(vec![
140/// vec![1.0_f64, 2.0],
141/// vec![3.0, 4.0],
142/// ]);
143/// let factors = matrix_luci_factors_from_matrix(&m, None).unwrap();
144/// assert!(factors.rank >= 1);
145/// assert_eq!(factors.row_indices.len(), factors.rank);
146/// assert_eq!(factors.left.nrows(), m.nrows());
147/// assert_eq!(factors.right.ncols(), m.ncols());
148/// ```
149#[derive(Debug, Clone)]
150pub struct MatrixLuciFactors<T> {
151 /// Selected row indices.
152 pub row_indices: Vec<usize>,
153 /// Selected column indices.
154 pub col_indices: Vec<usize>,
155 /// Pivot error history, or one measured maximum residual for fixed pivots.
156 pub pivot_errors: Vec<f64>,
157 /// Selected rank.
158 pub rank: usize,
159 /// Left factor.
160 pub left: Matrix<T>,
161 /// Right factor.
162 pub right: Matrix<T>,
163}
164
165/// Row interpolation `A ~= interpolation * A[rows, :]` selected by MatrixLUCI.
166/// Unlike [`MatrixLuciFactors`], this does not build an unused right factor.
167///
168/// # Examples
169/// ```
170/// use tensor4all_core::matrix_luci_row_interpolation_owned_in;
171/// use tensor4all_tensorbackend::{default_cpu_execution_context, Matrix};
172/// let a = Matrix::from_col_major_vec(2, 1, vec![2.0_f64, 4.0]);
173/// let id = matrix_luci_row_interpolation_owned_in(a, None, &default_cpu_execution_context())?;
174/// assert_eq!(id.rows, vec![1]);
175/// assert_eq!(id.interpolation.as_col_major_slice(), &[0.5, 1.0]);
176/// # Ok::<(), Box<dyn std::error::Error>>(())
177/// ```
178#[derive(Clone, Debug)]
179pub struct MatrixLuciRowInterpolation<T> {
180 /// Original matrix row indices, in pivot order.
181 pub rows: Vec<usize>,
182 /// Row weights, with an identity submatrix at the selected rows.
183 pub interpolation: Matrix<T>,
184 /// Accepted LU pivot magnitudes followed by the maximum absolute entry of
185 /// the remaining Schur complement. This is a local matrix diagnostic.
186 pub pivot_magnitudes: Vec<f64>,
187}
188
189/// Selects a row interpolant without constructing the complementary right factor.
190///
191/// Consumes column-major `a`, uses `options` for pivot rank/tolerances, and
192/// performs the factor solve in the supplied CPU `context`. `left_orthogonal`
193/// is always set to true because this operation returns row interpolation.
194/// A zero matrix yields zero columns; the caller chooses its zero-rank convention.
195///
196/// # Errors
197/// Returns [`MatrixCIError::InvalidArgument`] for negative or nonfinite
198/// tolerances, infinite input magnitudes, or backend solve failures; returns
199/// [`MatrixCIError::NaNEncountered`] for NaN input or nonfinite factors or
200/// residual magnitudes.
201///
202/// # Examples
203/// ```
204/// use tensor4all_core::{matrix_luci_row_interpolation_owned_in, RrLUOptions};
205/// use tensor4all_tensorbackend::{default_cpu_execution_context, Matrix};
206/// let a = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 2.0, 3.0, 6.0]);
207/// let id = matrix_luci_row_interpolation_owned_in(a, Some(RrLUOptions::default()),
208/// &default_cpu_execution_context())?;
209/// assert_eq!(id.rows, vec![1]);
210/// assert_eq!(id.interpolation.as_col_major_slice(), &[0.5, 1.0]);
211/// # Ok::<(), Box<dyn std::error::Error>>(())
212/// ```
213pub fn matrix_luci_row_interpolation_owned_in<T>(
214 mut a: Matrix<T>,
215 options: Option<RrLUOptions>,
216 context: &CpuExecutionContext,
217) -> Result<MatrixLuciRowInterpolation<T>>
218where
219 T: Scalar + crate::MatrixLuciScalar,
220{
221 let mut options = options.unwrap_or_default();
222 options.left_orthogonal = true;
223 validate_owned_luci_input(&a, &options)?;
224 let lu = rrlu_left_mut(&mut a, Some(options))?;
225 let rank = lu.n_pivot;
226 Ok(MatrixLuciRowInterpolation {
227 rows: lu.row_permutation[..rank].to_vec(),
228 interpolation: cols_times_pivot_solve(
229 &lu.l,
230 &lu.row_permutation,
231 rank,
232 &mut Linalg::Context(context),
233 )?,
234 pivot_magnitudes: lu.pivot_magnitudes,
235 })
236}
237
238// Validation for the new owned/context facades lives with the rrLU entry
239// points (`matrixlu::validate_owned_luci_input`) so the selection-only path
240// checks inputs and tolerances identically.
241
242pub(crate) fn map_backend_error(err: crate::matrixluci::MatrixLuciError) -> MatrixCIError {
243 match err {
244 crate::matrixluci::MatrixLuciError::NaNEncountered { matrix } => {
245 MatrixCIError::NaNEncountered {
246 matrix: matrix.to_string(),
247 }
248 }
249 crate::matrixluci::MatrixLuciError::InvalidArgument { message } => {
250 MatrixCIError::InvalidArgument { message }
251 }
252 }
253}
254
255fn factors_to_public<T>(
256 selection: PivotSelectionCore,
257 factors: CrossFactors<T>,
258 left_orthogonal: bool,
259) -> Result<MatrixLuciFactors<T>>
260where
261 T: Scalar + crate::MatrixLuciScalar,
262{
263 let left = if left_orthogonal {
264 factors.cols_solve_pivot().map_err(map_backend_error)?
265 } else {
266 factors.pivot_cols.clone()
267 };
268 let right = if left_orthogonal {
269 factors.pivot_rows.clone()
270 } else {
271 factors.solve_pivot_rows().map_err(map_backend_error)?
272 };
273
274 Ok(MatrixLuciFactors {
275 row_indices: selection.row_indices,
276 col_indices: selection.col_indices,
277 pivot_errors: selection.pivot_errors,
278 rank: selection.rank,
279 left,
280 right,
281 })
282}
283
284fn backend_linalg_error(message: impl Into<String>) -> MatrixCIError {
285 MatrixCIError::InvalidArgument {
286 message: message.into(),
287 }
288}
289
290fn index_range(start: usize, end: usize) -> Vec<usize> {
291 (start..end).collect()
292}
293
294fn identity_rect<T: Scalar>(nrows: usize, ncols: usize) -> Matrix<T> {
295 let mut result = Matrix::zeros(nrows, ncols);
296 for i in 0..nrows.min(ncols) {
297 result[[i, i]] = T::one();
298 }
299 result
300}
301
302fn apply_row_permutation<T: Scalar>(matrix: &Matrix<T>, permutation: &[usize]) -> Matrix<T> {
303 let mut result = Matrix::zeros(matrix.nrows(), matrix.ncols());
304 for col in 0..matrix.ncols() {
305 for (new_row, &old_row) in permutation.iter().enumerate().take(matrix.nrows()) {
306 result[[old_row, col]] = matrix[[new_row, col]];
307 }
308 }
309 result
310}
311
312fn apply_col_permutation<T: Scalar>(matrix: &Matrix<T>, permutation: &[usize]) -> Matrix<T> {
313 let mut result = Matrix::zeros(matrix.nrows(), matrix.ncols());
314 for (new_col, &old_col) in permutation.iter().enumerate().take(matrix.ncols()) {
315 for row in 0..matrix.nrows() {
316 result[[row, old_col]] = matrix[[row, new_col]];
317 }
318 }
319 result
320}
321
322pub(crate) fn rrlu_colmatrix<T>(lu: &RrLU<T>, linalg: &mut Linalg<'_, '_>) -> Result<Matrix<T>>
323where
324 T: Scalar + crate::MatrixLuciScalar,
325{
326 let rank = lu.npivots();
327 if rank == 0 {
328 return Ok(Matrix::zeros(lu.nrows(), 0));
329 }
330 let rows = index_range(0, rank);
331 let cols = index_range(0, rank);
332 let right_pivot_cols = submatrix(lu.right_unpermuted(), &rows, &cols);
333 linalg
334 .mat_mul(lu.left(true), right_pivot_cols)
335 .map_err(|err| backend_linalg_error(format!("MatrixLUCI colmatrix multiply failed: {err}")))
336}
337
338pub(crate) fn rrlu_rowmatrix<T>(lu: &RrLU<T>, linalg: &mut Linalg<'_, '_>) -> Result<Matrix<T>>
339where
340 T: Scalar + crate::MatrixLuciScalar,
341{
342 let rank = lu.npivots();
343 if rank == 0 {
344 return Ok(Matrix::zeros(0, lu.ncols()));
345 }
346 let rows = index_range(0, rank);
347 let cols = index_range(0, rank);
348 let left_pivot_rows = submatrix(lu.left_unpermuted(), &rows, &cols);
349 linalg
350 .mat_mul(left_pivot_rows, lu.right(true))
351 .map_err(|err| backend_linalg_error(format!("MatrixLUCI rowmatrix multiply failed: {err}")))
352}
353
354pub(crate) fn rrlu_cols_times_pivot_solve<T>(
355 lu: &RrLU<T>,
356 linalg: &mut Linalg<'_, '_>,
357) -> Result<Matrix<T>>
358where
359 T: Scalar + crate::MatrixLuciScalar,
360{
361 cols_times_pivot_solve(
362 lu.left_unpermuted(),
363 lu.row_permutation(),
364 lu.npivots(),
365 linalg,
366 )
367}
368
369fn cols_times_pivot_solve<T>(
370 left: &Matrix<T>,
371 row_permutation: &[usize],
372 rank: usize,
373 linalg: &mut Linalg<'_, '_>,
374) -> Result<Matrix<T>>
375where
376 T: Scalar + crate::MatrixLuciScalar,
377{
378 let mut result = identity_rect(left.nrows(), rank);
379 if rank > 0 && rank < left.nrows() {
380 let pivot_rows = index_range(0, rank);
381 let pivot_cols = index_range(0, rank);
382 let rest_rows = index_range(rank, left.nrows());
383 let pivot = submatrix(left, &pivot_rows, &pivot_cols);
384 let rest = submatrix(left, &rest_rows, &pivot_cols);
385 let solved = linalg
386 .triangular_solve(pivot, rest, false, true)
387 .map_err(|err| {
388 backend_linalg_error(format!("MatrixLUCI lower triangular solve failed: {err}"))
389 })?;
390 for row in 0..solved.nrows() {
391 for col in 0..solved.ncols() {
392 result[[rank + row, col]] = solved[[row, col]];
393 }
394 }
395 }
396 Ok(apply_row_permutation(&result, row_permutation))
397}
398
399pub(crate) fn rrlu_pivot_solve_times_rows<T>(
400 lu: &RrLU<T>,
401 linalg: &mut Linalg<'_, '_>,
402) -> Result<Matrix<T>>
403where
404 T: Scalar + crate::MatrixLuciScalar,
405{
406 let rank = lu.npivots();
407 let mut result = identity_rect(rank, lu.ncols());
408 if rank > 0 && rank < lu.ncols() {
409 let pivot_rows = index_range(0, rank);
410 let pivot_cols = index_range(0, rank);
411 let rest_cols = index_range(rank, lu.ncols());
412 let pivot = submatrix(lu.right_unpermuted(), &pivot_rows, &pivot_cols);
413 let rest = submatrix(lu.right_unpermuted(), &pivot_rows, &rest_cols);
414 let solved = linalg
415 .triangular_solve(pivot, rest, true, false)
416 .map_err(|err| {
417 backend_linalg_error(format!("MatrixLUCI upper triangular solve failed: {err}"))
418 })?;
419 for row in 0..solved.nrows() {
420 for col in 0..solved.ncols() {
421 result[[row, rank + col]] = solved[[row, col]];
422 }
423 }
424 }
425 Ok(apply_col_permutation(&result, lu.col_permutation()))
426}
427
428fn factors_from_rrlu<T>(lu: &RrLU<T>, linalg: &mut Linalg<'_, '_>) -> Result<MatrixLuciFactors<T>>
429where
430 T: Scalar + crate::MatrixLuciScalar,
431{
432 let left = if lu.is_left_orthogonal() {
433 rrlu_cols_times_pivot_solve(lu, linalg)?
434 } else {
435 rrlu_colmatrix(lu, linalg)?
436 };
437 let right = if lu.is_left_orthogonal() {
438 rrlu_rowmatrix(lu, linalg)?
439 } else {
440 rrlu_pivot_solve_times_rows(lu, linalg)?
441 };
442
443 Ok(MatrixLuciFactors {
444 row_indices: lu.row_indices(),
445 col_indices: lu.col_indices(),
446 pivot_errors: lu.pivot_errors(),
447 rank: lu.npivots(),
448 left,
449 right,
450 })
451}
452
453pub(crate) fn dense_matrix_luci_factors_from_matrix<T>(
454 a: &Matrix<T>,
455 options: RrLUOptions,
456) -> Result<MatrixLuciFactors<T>>
457where
458 T: Scalar + crate::MatrixLuciScalar,
459{
460 validate_owned_luci_input(a, &options)?;
461 let lu = rrlu(a, Some(options))?;
462 factors_from_rrlu(&lu, &mut Linalg::Global)
463}
464
465pub(crate) fn dense_matrix_luci_factors_from_matrix_owned<T>(
466 mut a: Matrix<T>,
467 options: RrLUOptions,
468 mut linalg: Linalg<'_, '_>,
469) -> Result<MatrixLuciFactors<T>>
470where
471 T: Scalar + crate::MatrixLuciScalar,
472{
473 let lu = rrlu_mut(&mut a, Some(options))?;
474 factors_from_rrlu(&lu, &mut linalg)
475}
476
477pub(crate) fn lazy_matrix_luci_factors_from_blocks<T, F>(
478 nrows: usize,
479 ncols: usize,
480 fill_block: F,
481 options: RrLUOptions,
482) -> Result<MatrixLuciFactors<T>>
483where
484 T: Scalar + crate::MatrixLuciScalar,
485 F: Fn(&[usize], &[usize], &mut [T]),
486 LazyBlockRookKernel: PivotKernel<T>,
487{
488 validate_luci_options(&options)?;
489 let source = LazyMatrixSource::new(nrows, ncols, fill_block);
490 let kernel_options = PivotKernelOptions {
491 max_bond_dim: options.max_bond_dim,
492 rel_tol: options.rel_tol,
493 abs_tol: options.abs_tol,
494 left_orthogonal: options.left_orthogonal,
495 };
496
497 let selection = LazyBlockRookKernel
498 .factorize(&source, &kernel_options)
499 .map_err(map_backend_error)?;
500 let factors = CrossFactors::from_source(&source, &selection).map_err(map_backend_error)?;
501 factors_to_public(selection, factors, options.left_orthogonal)
502}
503
504/// Factorize a dense matrix with MatrixLUCI.
505///
506/// Returns the selected pivot metadata together with left and
507/// right factors. This is the public facade used by higher-level crates.
508///
509/// # Arguments
510///
511/// * `a` - Dense matrix to factorize.
512/// * `options` - Optional rank and tolerance controls. `None` uses the
513///
514/// default LUCI settings.
515///
516/// # Returns
517///
518/// A [`MatrixLuciFactors`] value containing the selected pivot indices,
519/// error history, rank, and factors.
520///
521/// # Errors
522///
523/// Returns [`MatrixCIError::InvalidArgument`] for negative or non-finite
524/// tolerances, infinite input magnitudes, or backend failures; returns
525/// [`MatrixCIError::NaNEncountered`] for NaN input or non-finite factors.
526///
527/// # Examples
528///
529/// ```
530/// use tensor4all_core::matrix_luci_factors_from_matrix;
531/// use tensor4all_tensorbackend::from_vec2d;
532///
533/// let m = from_vec2d(vec![
534/// vec![1.0_f64, 0.0],
535/// vec![0.0, 2.0],
536/// ]);
537/// let factors = matrix_luci_factors_from_matrix(&m, None).unwrap();
538/// assert_eq!(factors.left.nrows(), 2);
539/// assert_eq!(factors.right.ncols(), 2);
540/// assert_eq!(factors.row_indices.len(), factors.rank);
541/// ```
542pub fn matrix_luci_factors_from_matrix<T>(
543 a: &Matrix<T>,
544 options: Option<RrLUOptions>,
545) -> Result<MatrixLuciFactors<T>>
546where
547 T: Scalar + crate::MatrixLuciScalar,
548{
549 <T as crate::MatrixLuciScalar>::matrix_luci_factors_from_matrix(a, options.unwrap_or_default())
550}
551
552/// Factorize a dense matrix with MatrixLUCI while consuming the input matrix.
553///
554/// This is the owned-buffer counterpart of [`matrix_luci_factors_from_matrix`].
555/// It reuses the local matrix storage for the rrLU pivot selection step.
556///
557/// # Errors
558///
559/// Returns a [`MatrixCIError`] if the factorization fails, for example if the
560/// pivot block is singular or the backend rejects a factor solve.
561pub fn matrix_luci_factors_from_matrix_owned<T>(
562 a: Matrix<T>,
563 options: Option<RrLUOptions>,
564) -> Result<MatrixLuciFactors<T>>
565where
566 T: Scalar + crate::MatrixLuciScalar,
567{
568 let options = options.unwrap_or_default();
569 validate_owned_luci_input(&a, &options)?;
570 dense_matrix_luci_factors_from_matrix_owned(a, options, Linalg::Global)
571}
572
573/// Factorize a dense matrix with MatrixLUCI in a caller-owned CPU context.
574///
575/// Context-scoped counterpart of [`matrix_luci_factors_from_matrix_owned`].
576/// The rank-revealing pivot search is identical; the left and right factors
577/// are assembled with `context`'s configured backend (matrix products and
578/// triangular solves). No process-global context is consulted.
579///
580/// # Arguments
581///
582/// * `a` - Dense column-major matrix to factorize; its storage is reused.
583/// * `options` - Optional rank and tolerance controls. `None` uses the
584/// default LUCI settings.
585/// * `context` - Caller-owned CPU execution context running the factor
586/// assembly.
587///
588/// # Returns
589///
590/// A [`MatrixLuciFactors`] value with the selected pivots, pivot error
591/// history, rank, and factors satisfying `a ~= left * right`.
592///
593/// # Errors
594///
595/// Returns [`MatrixCIError::InvalidArgument`] for negative or nonfinite
596/// tolerances, infinite input magnitudes, or backend solve failures; returns
597/// [`MatrixCIError::NaNEncountered`] for NaN input or factors.
598///
599/// # Examples
600///
601/// ```
602/// use tensor4all_core::{matrix_luci_factors_from_matrix_owned_in, RrLUOptions};
603/// use tensor4all_tensorbackend::{from_vec2d, mat_mul, CpuExecutionContext};
604/// use tensor4all_tensorbackend::CpuBackend;
605///
606/// let context = CpuExecutionContext::from_backend(CpuBackend::new());
607/// // Rank-1 matrix [1 2; 2 4].
608/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![2.0, 4.0]]);
609/// let options = RrLUOptions { left_orthogonal: true, ..RrLUOptions::default() };
610/// let factors = matrix_luci_factors_from_matrix_owned_in(a.clone(), Some(options), &context)?;
611/// assert_eq!(factors.rank, 1);
612/// let rebuilt = mat_mul(&factors.left, &factors.right)?;
613/// for (x, y) in rebuilt.as_col_major_slice().iter().zip(a.as_col_major_slice()) {
614/// assert!((x - y).abs() < 1e-12);
615/// }
616/// # Ok::<(), Box<dyn std::error::Error>>(())
617/// ```
618pub fn matrix_luci_factors_from_matrix_owned_in<T>(
619 a: Matrix<T>,
620 options: Option<RrLUOptions>,
621 context: &CpuExecutionContext,
622) -> Result<MatrixLuciFactors<T>>
623where
624 T: Scalar + crate::MatrixLuciScalar,
625{
626 let options = options.unwrap_or_default();
627 validate_owned_luci_input(&a, &options)?;
628 dense_matrix_luci_factors_from_matrix_owned(a, options, Linalg::Context(context))
629}
630
631/// Factorize a dense matrix with MatrixLUCI in a session the caller already entered.
632///
633/// Session-taking counterpart of [`matrix_luci_factors_from_matrix_owned_in`], for callers that own their
634/// execution rather than a context: a worker lane entered by the phase that owns it, for example, whose session
635/// must be used as it is. The rank-revealing pivot search is identical; the factor assembly's matrix products
636/// and triangular solves run on `session`, and neither a process-global context nor a fresh session entry is
637/// consulted.
638///
639/// # Arguments
640///
641/// * `a` - Dense column-major matrix to factorize; its storage is reused.
642/// * `options` - Optional rank and tolerance controls. `None` uses the default LUCI settings.
643/// * `session` - An already-entered explicit session that runs the factor assembly.
644///
645/// # Returns
646///
647/// A [`MatrixLuciFactors`] value with the selected pivots, pivot error history, rank, and factors satisfying
648/// `a ~= left * right`.
649///
650/// # Errors
651///
652/// Returns [`MatrixCIError::InvalidArgument`] for negative or nonfinite tolerances, infinite input magnitudes,
653/// or backend solve failures; returns [`MatrixCIError::NaNEncountered`] for NaN input or factors.
654///
655/// # Examples
656///
657/// ```
658/// use tensor4all_core::{matrix_luci_factors_from_matrix_owned_on_session, RrLUOptions};
659/// use tensor4all_tensorbackend::{from_vec2d, mat_mul, CpuExecutionContext};
660/// use tensor4all_tensorbackend::CpuBackend;
661///
662/// let context = CpuExecutionContext::from_backend(CpuBackend::new());
663/// let a = from_vec2d(vec![vec![1.0_f64, 2.0], vec![2.0, 4.0]]);
664/// let factors = context.with_concrete_session(|session| {
665/// matrix_luci_factors_from_matrix_owned_on_session(
666/// a.clone(),
667/// Some(RrLUOptions { left_orthogonal: true, ..RrLUOptions::default() }),
668/// session,
669/// )
670/// })??;
671/// assert_eq!(factors.rank, 1);
672/// # Ok::<(), Box<dyn std::error::Error>>(())
673/// ```
674pub fn matrix_luci_factors_from_matrix_owned_on_session<T>(
675 a: Matrix<T>,
676 options: Option<RrLUOptions>,
677 session: &mut Session<'_>,
678) -> Result<MatrixLuciFactors<T>>
679where
680 T: Scalar + crate::MatrixLuciScalar,
681{
682 let options = options.unwrap_or_default();
683 validate_owned_luci_input(&a, &options)?;
684 dense_matrix_luci_factors_from_matrix_owned(a, options, Linalg::Session(session))
685}
686
687/// Conservative logical working-byte estimate for owned dense LUCI factors.
688///
689/// Includes the consumed matrix, rrLU L/U, factor construction, permutation
690/// and triangular-solve operands/results, and pivot/index metadata. The
691/// factor rank is bounded by `min(nrows, ncols, max_rank)`. The estimate sums
692/// potentially overlapping factor buffers conservatively; it excludes
693/// allocator overhead and provider-private workspace. Callers must add their
694/// own buffers retained across the factorization.
695///
696/// Returns `None` when checked shape/byte arithmetic overflows `usize`.
697///
698/// # Examples
699///
700/// ```
701/// use tensor4all_core::matrix_luci_factors_working_bytes;
702/// let bytes = matrix_luci_factors_working_bytes::<f64>(32, 32, 32).unwrap();
703/// // Even just the matrix and full-rank L/U require three matrix payloads.
704/// assert!(bytes >= 3 * 32 * 32 * std::mem::size_of::<f64>());
705/// assert!(matrix_luci_factors_working_bytes::<f64>(usize::MAX, 2, 1).is_none());
706/// ```
707pub fn matrix_luci_factors_working_bytes<T: Scalar>(
708 nrows: usize,
709 ncols: usize,
710 max_rank: usize,
711) -> Option<usize> {
712 let rank = max_rank.min(nrows).min(ncols);
713 let axes = nrows.checked_add(ncols)?;
714 let matrix = nrows.checked_mul(ncols)?;
715 // L/U, both output factors, permutation copies, and triangular solve
716 // operands/results. Charge both orientations rather than assuming which
717 // side has the larger solve, including Matrix/backend operand copies.
718 let factors = axes.checked_mul(rank)?.checked_mul(6)?;
719 let pivots = rank.checked_mul(rank)?.checked_mul(2)?;
720 let scalar_bytes = matrix
721 .checked_add(factors)?
722 .checked_add(pivots)?
723 .checked_mul(std::mem::size_of::<T>())?;
724 let index_bytes = axes
725 .checked_add(rank)?
726 .checked_mul(4)?
727 .checked_mul(std::mem::size_of::<usize>())?;
728 let error_bytes = rank
729 .checked_add(1)?
730 .checked_mul(std::mem::size_of::<f64>())?;
731 scalar_bytes
732 .checked_add(index_bytes)?
733 .checked_add(error_bytes)
734}
735
736/// Factorize a lazily supplied matrix with MatrixLUCI block-rook search.
737///
738/// The caller provides a block-fill closure that receives row and column
739/// index lists and writes the corresponding matrix block in column-major
740/// order.
741/// Exactly zero starting residual columns are skipped. An all-zero residual
742/// can require inspecting all remaining entries, without materializing the
743/// full candidate matrix.
744///
745/// # Arguments
746///
747/// * `nrows` - Number of matrix rows.
748/// * `ncols` - Number of matrix columns.
749/// * `fill_block` - Closure that fills `out` with `A[rows, cols]` in
750///
751/// column-major order.
752/// * `options` - Rank and tolerance controls.
753///
754/// # Returns
755///
756/// A [`MatrixLuciFactors`] value containing the pivot metadata and
757/// factors.
758///
759/// # Errors
760///
761/// Returns [`MatrixCIError::InvalidArgument`] for negative or non-finite
762/// tolerances; returns a [`MatrixCIError`] if the lazy factorization fails or
763/// if the block callback is inconsistent.
764///
765/// # Examples
766///
767/// ```
768/// use tensor4all_core::matrix_luci_factors_from_blocks;
769/// use tensor4all_core::RrLUOptions;
770///
771/// let factors = matrix_luci_factors_from_blocks(
772/// 2,
773/// 2,
774/// |rows, cols, out| {
775/// for (j, &col) in cols.iter().enumerate() {
776/// for (i, &row) in rows.iter().enumerate() {
777/// out[i + rows.len() * j] = if row == col { 1.0 } else { 0.0 };
778/// }
779/// }
780/// },
781/// RrLUOptions::default(),
782/// ).unwrap();
783/// assert_eq!(factors.rank, 2);
784/// ```
785pub fn matrix_luci_factors_from_blocks<T, F>(
786 nrows: usize,
787 ncols: usize,
788 fill_block: F,
789 options: RrLUOptions,
790) -> Result<MatrixLuciFactors<T>>
791where
792 T: Scalar + crate::MatrixLuciScalar,
793 F: Fn(&[usize], &[usize], &mut [T]),
794{
795 <T as crate::MatrixLuciScalar>::matrix_luci_factors_from_blocks(
796 nrows, ncols, fill_block, options,
797 )
798}
799
800impl<T> MatrixLUCI<T>
801where
802 T: Scalar + crate::MatrixLuciScalar,
803{
804 /// Create a MatrixLUCI from a dense matrix.
805 ///
806 /// # Errors
807 ///
808 /// Returns [`MatrixCIError`] when `a` is not a valid matrix for LU cross
809 /// interpolation or when the factor construction fails.
810 ///
811 /// # Examples
812 ///
813 /// ```
814 /// use tensor4all_core::{AbstractMatrixCI, MatrixLUCI};
815 /// use tensor4all_tensorbackend::from_vec2d;
816 ///
817 /// let m = from_vec2d(vec![
818 /// vec![2.0_f64, 0.0],
819 /// vec![0.0, 3.0],
820 /// ]);
821 /// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
822 /// assert!(ci.rank() >= 1);
823 /// ```
824 pub fn from_matrix(a: &Matrix<T>, options: Option<RrLUOptions>) -> Result<Self> {
825 let factors = matrix_luci_factors_from_matrix(a, options)?;
826
827 Ok(Self {
828 nrows: a.nrows(),
829 ncols: a.ncols(),
830 row_indices: factors.row_indices,
831 col_indices: factors.col_indices,
832 left: factors.left,
833 right: factors.right,
834 pivot_errors: factors.pivot_errors,
835 })
836 }
837
838 /// Left CI factor (shape: `nrows x rank`).
839 ///
840 /// The approximation is `left * right`.
841 ///
842 /// # Examples
843 ///
844 /// ```
845 /// use tensor4all_core::{AbstractMatrixCI, MatrixLUCI};
846 /// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
847 ///
848 /// let m = from_vec2d(vec![
849 /// vec![1.0_f64, 2.0],
850 /// vec![3.0, 4.0],
851 /// ]);
852 /// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
853 /// let reconstructed = mat_mul(&ci.left(), &ci.right()).unwrap();
854 /// for i in 0..2 {
855 /// for j in 0..2 {
856 /// assert!((reconstructed[[i, j]] - m[[i, j]]).abs() < 1e-10);
857 /// }
858 /// }
859 /// ```
860 pub fn left(&self) -> Matrix<T> {
861 self.left.clone()
862 }
863
864 /// Right CI factor (shape: `rank x ncols`).
865 ///
866 /// The approximation is `left * right`.
867 ///
868 /// # Examples
869 ///
870 /// ```
871 /// use tensor4all_core::{AbstractMatrixCI, MatrixLUCI};
872 /// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
873 ///
874 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
875 /// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
876 /// let r = ci.right();
877 /// assert_eq!(r.nrows(), ci.rank());
878 /// assert_eq!(r.ncols(), ci.ncols());
879 /// // left * right reconstructs the matrix
880 /// let recon = mat_mul(&ci.left(), &r).unwrap();
881 /// for i in 0..2 {
882 /// for j in 0..2 {
883 /// assert!((recon[[i, j]] - m[[i, j]]).abs() < 1e-10);
884 /// }
885 /// }
886 /// ```
887 pub fn right(&self) -> Matrix<T> {
888 self.right.clone()
889 }
890
891 /// Pivot error history (one entry per pivot, plus a final residual estimate).
892 ///
893 /// # Examples
894 ///
895 /// ```
896 /// use tensor4all_core::MatrixLUCI;
897 /// use tensor4all_tensorbackend::from_vec2d;
898 ///
899 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
900 /// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
901 /// let errs = ci.pivot_errors();
902 /// assert!(!errs.is_empty());
903 /// // All errors are non-negative
904 /// for &e in &errs {
905 /// assert!(e >= 0.0);
906 /// }
907 /// ```
908 pub fn pivot_errors(&self) -> Vec<f64> {
909 self.pivot_errors.clone()
910 }
911
912 /// Last pivot error (the residual estimate after all pivots).
913 ///
914 /// # Examples
915 ///
916 /// ```
917 /// use tensor4all_core::MatrixLUCI;
918 /// use tensor4all_tensorbackend::from_vec2d;
919 ///
920 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
921 /// let ci = MatrixLUCI::from_matrix(&m, None).unwrap();
922 /// let err = ci.last_pivot_error();
923 /// assert!(err >= 0.0);
924 /// ```
925 pub fn last_pivot_error(&self) -> f64 {
926 self.pivot_errors.last().copied().unwrap_or(0.0)
927 }
928}
929
930impl<T> AbstractMatrixCI<T> for MatrixLUCI<T>
931where
932 T: Scalar + crate::MatrixLuciScalar,
933{
934 fn nrows(&self) -> usize {
935 self.nrows
936 }
937
938 fn ncols(&self) -> usize {
939 self.ncols
940 }
941
942 fn rank(&self) -> usize {
943 self.row_indices.len()
944 }
945
946 fn row_indices(&self) -> &[usize] {
947 &self.row_indices
948 }
949
950 fn col_indices(&self) -> &[usize] {
951 &self.col_indices
952 }
953
954 fn evaluate(&self, i: usize, j: usize) -> T {
955 let mut sum = T::zero();
956 for k in 0..self.rank() {
957 sum = sum + self.left[[i, k]] * self.right[[k, j]];
958 }
959 sum
960 }
961
962 fn submatrix(&self, rows: &[usize], cols: &[usize]) -> Matrix<T> {
963 let r = self.rank();
964 let mut result = Matrix::zeros(rows.len(), cols.len());
965 for (j_out, &col) in cols.iter().enumerate() {
966 for (i_out, &row) in rows.iter().enumerate() {
967 let mut sum = T::zero();
968 for k in 0..r {
969 sum = sum + self.left[[row, k]] * self.right[[k, col]];
970 }
971 result[[i_out, j_out]] = sum;
972 }
973 }
974 result
975 }
976}
977
978#[cfg(test)]
979mod tests;