treams_core/linalg/
saved.rs

1//! Fixed numerical layouts for invocation-owned JAX state. Dimensions are stored
2//! as u64; all other data are IEEE f64 components in column-major order.
3//! treams-rs extension.
4
5use std::sync::OnceLock;
6
7use faer::{Mat, MatMut, MatRef, perm::Perm};
8
9use super::{EigResidual, Equilibration, Lu, SolveResidual, SvdvalsResidual, faer_zeros, view};
10use crate::{
11    Complex, Error, Result, numerics,
12    saved::{Reader, SavedState, Writer, invalid, read_complex, read_matrix, total},
13};
14
15fn matrix_size(rows: usize, columns: usize) -> Result<usize> {
16    if rows == 0 || columns == 0 {
17        return Err(invalid());
18    }
19    rows.checked_mul(columns)
20        .and_then(|count| count.checked_mul(16))
21        .ok_or_else(invalid)
22}
23
24impl Lu {
25    /// Bytes for factors, forward permutation, and canonical row/column scales.
26    pub(crate) fn state_size(n: usize) -> Result<usize> {
27        total(&[matrix_size(n, n)?, n.checked_mul(24).ok_or_else(invalid)?])
28    }
29
30    /// Write numerical factors without refactorizing their operator.
31    pub(crate) fn write_state(&self, writer: &mut Writer) {
32        write_lu(writer, self);
33    }
34
35    /// Restore numerical factors; the containing record provides the dimension.
36    pub(crate) fn read_state(reader: &mut Reader<'_>, n: usize) -> Result<Self> {
37        read_lu(reader, n)
38    }
39}
40
41fn write_matrix(writer: &mut Writer, matrix: MatRef<'_, Complex>) {
42    for j in 0..matrix.ncols() {
43        for i in 0..matrix.nrows() {
44            writer.complex(matrix[(i, j)]);
45        }
46    }
47}
48
49fn read_values(reader: &mut Reader<'_>, mut matrix: MatMut<'_, Complex>) -> Result<()> {
50    for j in 0..matrix.ncols() {
51        for i in 0..matrix.nrows() {
52            matrix[(i, j)] = read_complex(reader)?;
53        }
54    }
55    Ok(())
56}
57
58fn read_faer(reader: &mut Reader<'_>, rows: usize, columns: usize) -> Result<Mat<Complex>> {
59    let mut matrix = faer_zeros(rows, columns)?;
60    read_values(reader, matrix.as_mut())?;
61    Ok(matrix)
62}
63
64/// Canonical LU state uses identity scales when equilibration was unnecessary.
65/// The inverse permutation is derived on restore, rather than stored twice.
66fn write_lu(writer: &mut Writer, lu: &Lu) {
67    write_matrix(writer, view(&lu.factors));
68    for &index in lu.permutation.arrays().0 {
69        writer.usize(index);
70    }
71    let n = lu.factors.nrows();
72    for column in [false, true] {
73        for i in 0..n {
74            writer.f64(lu.equilibration.as_ref().map_or(1.0, |scales| {
75                if column {
76                    scales.column[i]
77                } else {
78                    scales.row[i]
79                }
80            }));
81        }
82    }
83}
84
85fn read_lu(reader: &mut Reader<'_>, n: usize) -> Result<Lu> {
86    let factors = read_matrix(reader, n, n)?;
87    if factors
88        .diagonal()
89        .iter()
90        .any(|&value| value == Complex::default())
91    {
92        return Err(invalid());
93    }
94    let mut forward = numerics::filled(n, 0_usize)?;
95    let mut inverse = numerics::filled(n, usize::MAX)?;
96    for (i, index) in forward.iter_mut().enumerate() {
97        *index = reader.usize()?;
98        let entry = inverse.get_mut(*index).ok_or_else(invalid)?;
99        if *entry != usize::MAX {
100            return Err(invalid());
101        }
102        *entry = i;
103    }
104    let mut row = numerics::filled(n, 1.0)?;
105    let mut column = numerics::filled(n, 1.0)?;
106    for value in row.iter_mut().chain(column.iter_mut()) {
107        *value = reader.f64()?;
108        if !value.is_finite() || *value <= 0.0 {
109            return Err(invalid());
110        }
111    }
112    let equilibration = if row
113        .iter()
114        .chain(&column)
115        .all(|&value| value.to_bits() == 1.0_f64.to_bits())
116    {
117        None
118    } else {
119        Some(Equilibration { row, column })
120    };
121    Ok(Lu {
122        factors,
123        permutation: Perm::new_checked(forward.into_boxed_slice(), inverse.into_boxed_slice(), n),
124        equilibration,
125    })
126}
127
128impl SolveResidual {
129    /// Byte count for the native saved state of an `n × rhs` solution.
130    pub fn state_size(n: usize, rhs: usize) -> Result<usize> {
131        total(&[16, Lu::state_size(n)?, matrix_size(n, rhs)?])
132    }
133}
134
135impl SavedState for SolveResidual {
136    fn save_state(&self) -> Result<Vec<u8>> {
137        let (n, rhs) = self.shape();
138        let mut writer = Writer::new(Self::state_size(n, rhs)?);
139        writer.usize(n);
140        writer.usize(rhs);
141        self.lu.write_state(&mut writer);
142        write_matrix(&mut writer, view(&self.value));
143        Ok(writer.finish())
144    }
145
146    fn from_state(bytes: &[u8]) -> Result<Self> {
147        let mut reader = Reader::new(bytes);
148        let (n, rhs) = (reader.usize()?, reader.usize()?);
149        if bytes.len() != Self::state_size(n, rhs)? {
150            return Err(invalid());
151        }
152        let lu = Lu::read_state(&mut reader, n)?;
153        let value = read_matrix(&mut reader, n, rhs)?;
154        reader.finish()?;
155        Ok(Self { lu, value })
156    }
157}
158
159impl SvdvalsResidual {
160    /// Byte count for the native saved state of an `m × n` operator.
161    pub fn state_size(m: usize, n: usize) -> Result<usize> {
162        let k = m.min(n);
163        total(&[
164            16,
165            matrix_size(m, k)?,
166            matrix_size(n, k)?,
167            k.checked_mul(8).ok_or_else(invalid)?,
168        ])
169    }
170}
171
172impl SavedState for SvdvalsResidual {
173    fn save_state(&self) -> Result<Vec<u8>> {
174        let (m, n) = self.shape();
175        let mut writer = Writer::new(Self::state_size(m, n)?);
176        writer.usize(m);
177        writer.usize(n);
178        write_matrix(&mut writer, self.u.as_ref());
179        write_matrix(&mut writer, self.v.as_ref());
180        for &value in &self.values {
181            writer.f64(value);
182        }
183        Ok(writer.finish())
184    }
185
186    fn from_state(bytes: &[u8]) -> Result<Self> {
187        let mut reader = Reader::new(bytes);
188        let (m, n) = (reader.usize()?, reader.usize()?);
189        if bytes.len() != Self::state_size(m, n)? {
190            return Err(invalid());
191        }
192        let k = m.min(n);
193        let u = read_faer(&mut reader, m, k)?;
194        let v = read_faer(&mut reader, n, k)?;
195        let mut values = numerics::filled(k, 0.0)?;
196        for value in &mut values {
197            *value = reader.f64()?;
198            if !value.is_finite() || *value < 0.0 {
199                return Err(invalid());
200            }
201        }
202        reader.finish()?;
203        Ok(Self { u, v, values })
204    }
205}
206
207impl EigResidual {
208    /// Byte count for the native saved state of an `n × n` eigensystem.
209    pub fn state_size(n: usize) -> Result<usize> {
210        total(&[
211            17,
212            matrix_size(n, n)?,
213            n.checked_mul(24).ok_or_else(invalid)?,
214            Lu::state_size(n)?,
215        ])
216    }
217}
218
219impl SavedState for EigResidual {
220    fn save_state(&self) -> Result<Vec<u8>> {
221        let n = self.values.len();
222        let mut writer = Writer::new(Self::state_size(n)?);
223        writer.usize(n);
224        for &value in &self.values {
225            writer.complex(value);
226        }
227        write_matrix(&mut writer, view(&self.vectors));
228        for &pivot in &self.pivots {
229            writer.usize(pivot);
230        }
231        writer.f64(self.scale);
232        match self.vectors_lu() {
233            Ok(lu) => {
234                writer.byte(0);
235                lu.write_state(&mut writer);
236            }
237            Err(Error::Singular) => {
238                // Singular eigenbases still support a trace pullback. This branch
239                // has the same shape-defined LU slots, with canonical identities.
240                writer.byte(1);
241                for j in 0..n {
242                    for i in 0..n {
243                        writer.complex(if i == j {
244                            Complex::from(1.0)
245                        } else {
246                            Complex::default()
247                        });
248                    }
249                }
250                for i in 0..n {
251                    writer.usize(i);
252                }
253                for _ in 0..2 * n {
254                    writer.f64(1.0);
255                }
256            }
257            Err(error) => return Err(error),
258        }
259        Ok(writer.finish())
260    }
261
262    fn from_state(bytes: &[u8]) -> Result<Self> {
263        let mut reader = Reader::new(bytes);
264        let n = reader.usize()?;
265        if bytes.len() != Self::state_size(n)? {
266            return Err(invalid());
267        }
268        let mut values = numerics::filled(n, Complex::default())?;
269        for value in &mut values {
270            *value = read_complex(&mut reader)?;
271        }
272        let vectors = read_matrix(&mut reader, n, n)?;
273        let mut pivots = numerics::filled(n, 0_usize)?;
274        for pivot in &mut pivots {
275            *pivot = reader.usize()?;
276            if *pivot >= n {
277                return Err(invalid());
278            }
279        }
280        let scale = reader.f64()?;
281        if !scale.is_finite() || scale <= 0.0 {
282            return Err(invalid());
283        }
284        let status = reader.byte()?;
285        let lu = Lu::read_state(&mut reader, n)?;
286        let lu = match status {
287            0 => Ok(lu),
288            1 => Err(Error::Singular),
289            _ => return Err(invalid()),
290        };
291        reader.finish()?;
292        Ok(Self {
293            values,
294            vectors,
295            pivots,
296            scale,
297            vectors_lu: OnceLock::from(lu),
298        })
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use nalgebra::DMatrix;
305
306    use super::*;
307    use crate::linalg::{eig, solve_owned, svdvals};
308
309    #[test]
310    fn solve_state_preserves_factors_scales_and_both_derivatives() {
311        for scaled in [false, true] {
312            let operator = DMatrix::from_fn(3, 3, |i, j| {
313                let value = Complex::new(if (i + 1) % 3 == j { 3.0 } else { 0.2 }, 0.1);
314                let exponent = if scaled {
315                    [-64, 0, 64][i] + [-40, 0, 40][j]
316                } else {
317                    0
318                };
319                value * 2.0_f64.powi(exponent)
320            });
321            let rhs = &operator * DMatrix::from_element(3, 2, Complex::new(0.3, -0.1));
322            let residual = solve_owned(operator.clone(), rhs.clone()).unwrap();
323            assert_eq!(residual.lu.equilibration.is_some(), scaled);
324            assert_ne!(residual.lu.permutation.arrays().0, &[0, 1, 2]);
325            if let Some(scales) = &residual.lu.equilibration {
326                assert_ne!(scales.row, vec![1.0; 3]);
327                assert_ne!(scales.column, vec![1.0; 3]);
328            }
329            let bytes = residual.save_state().unwrap();
330            assert_eq!(bytes.len(), SolveResidual::state_size(3, 2).unwrap());
331            let restored = SolveResidual::from_state(&bytes).unwrap();
332            assert_eq!(restored.save_state().unwrap(), bytes);
333            assert_eq!(restored.value(), residual.value());
334            assert_eq!(restored.lu.factors, residual.lu.factors);
335            assert_eq!(
336                restored.lu.permutation.arrays(),
337                residual.lu.permutation.arrays()
338            );
339            let operator_direction = operator * Complex::from(0.2);
340            let rhs_direction = rhs * Complex::from(0.3);
341            assert_eq!(
342                restored
343                    .pushforward(&operator_direction, rhs_direction.clone())
344                    .unwrap(),
345                residual
346                    .pushforward(&operator_direction, rhs_direction)
347                    .unwrap()
348            );
349            let cotangent = DMatrix::from_element(3, 2, Complex::new(0.5, 0.2));
350            let expected = residual.pullback(cotangent.clone()).unwrap();
351            let actual = restored.pullback(cotangent).unwrap();
352            assert_eq!(actual.operator, expected.operator);
353            assert_eq!(actual.rhs, expected.rhs);
354        }
355    }
356
357    #[test]
358    fn eig_state_carries_the_cached_derivative_factorization() {
359        let operator = DMatrix::from_row_slice(
360            2,
361            2,
362            &[
363                Complex::new(2.0, 0.1),
364                Complex::new(0.2, -0.1),
365                Complex::default(),
366                Complex::new(3.0, -0.2),
367            ],
368        );
369        let residual = eig(&operator).unwrap();
370        assert!(residual.vectors_lu.get().is_none());
371        let bytes = residual.save_state().unwrap();
372        assert_eq!(bytes.len(), EigResidual::state_size(2).unwrap());
373        let restored = EigResidual::from_state(&bytes).unwrap();
374        assert!(restored.vectors_lu.get().unwrap().is_ok());
375        assert_eq!(restored.save_state().unwrap(), bytes);
376        assert_eq!(
377            restored.vectors_lu().unwrap().factors,
378            residual.vectors_lu().unwrap().factors
379        );
380        let direction = DMatrix::from_element(2, 2, Complex::new(0.2, 0.1));
381        assert_eq!(
382            restored.pushforward(&direction).unwrap(),
383            residual.pushforward(&direction).unwrap()
384        );
385        let values = [Complex::new(0.2, 0.3), Complex::new(-0.4, 0.1)];
386        let vectors = DMatrix::from_element(2, 2, Complex::new(-0.2, 0.3));
387        assert_eq!(
388            restored.pullback(&values, vectors.clone()).unwrap(),
389            residual.pullback(&values, vectors).unwrap()
390        );
391    }
392
393    #[test]
394    fn eig_state_preserves_trace_pullbacks_with_a_singular_eigenbasis() {
395        // An exact Jordan eigenbasis, with the undefined inverse recorded as a
396        // numerical status rather than preventing the valid trace derivative.
397        let residual = EigResidual {
398            values: vec![Complex::from(1.0); 2],
399            vectors: DMatrix::from_row_slice(
400                2,
401                2,
402                &[
403                    Complex::from(1.0),
404                    Complex::from(1.0),
405                    Complex::default(),
406                    Complex::default(),
407                ],
408            ),
409            pivots: vec![0; 2],
410            scale: 1.0,
411            vectors_lu: OnceLock::new(),
412        };
413        let bytes = residual.save_state().unwrap();
414        assert_eq!(bytes.len(), EigResidual::state_size(2).unwrap());
415        let restored = EigResidual::from_state(&bytes).unwrap();
416        assert!(matches!(
417            restored.vectors_lu.get(),
418            Some(Err(Error::Singular))
419        ));
420        assert_eq!(restored.save_state().unwrap(), bytes);
421        assert_eq!(
422            restored
423                .pullback(&[Complex::from(1.0); 2], DMatrix::zeros(2, 2))
424                .unwrap(),
425            DMatrix::identity(2, 2)
426        );
427        assert!(restored.pushforward(&DMatrix::identity(2, 2)).is_err());
428    }
429
430    #[test]
431    fn rectangular_svd_state_preserves_both_derivatives() {
432        let tall = DMatrix::from_fn(3, 2, |i, j| {
433            Complex::new(if i == j { [2.0, 3.0, 4.0][i] } else { 0.2 }, 0.1)
434        });
435        for operator in [tall.clone(), tall.transpose()] {
436            let residual = svdvals(&operator).unwrap();
437            let bytes = residual.save_state().unwrap();
438            assert_eq!(
439                bytes.len(),
440                SvdvalsResidual::state_size(operator.nrows(), operator.ncols()).unwrap()
441            );
442            let restored = SvdvalsResidual::from_state(&bytes).unwrap();
443            assert_eq!(restored.save_state().unwrap(), bytes);
444            assert_eq!(restored.values(), residual.values());
445            let direction =
446                DMatrix::from_element(operator.nrows(), operator.ncols(), Complex::new(-0.2, 0.1));
447            assert_eq!(
448                restored.pushforward(&direction).unwrap(),
449                residual.pushforward(&direction).unwrap()
450            );
451            assert_eq!(
452                restored.pullback(&[0.2, -0.4]).unwrap(),
453                residual.pullback(&[0.2, -0.4]).unwrap()
454            );
455        }
456    }
457
458    #[test]
459    fn solve_restore_rejects_wrong_lengths_and_invalid_permutations() {
460        let residual = solve_owned(DMatrix::identity(2, 2), DMatrix::identity(2, 2)).unwrap();
461        let bytes = residual.save_state().unwrap();
462        assert!(SolveResidual::from_state(&bytes[..bytes.len() - 1]).is_err());
463        let mut extended = bytes.clone();
464        extended.push(0);
465        assert!(SolveResidual::from_state(&extended).is_err());
466        let mut invalid_permutation = bytes.clone();
467        let permutation_start = 16 + 16 * 2 * 2;
468        invalid_permutation[permutation_start + 8..permutation_start + 16]
469            .copy_from_slice(&0_u64.to_le_bytes());
470        assert!(SolveResidual::from_state(&invalid_permutation).is_err());
471        let mut invalid_size = bytes;
472        invalid_size[..8].copy_from_slice(&u64::MAX.to_le_bytes());
473        assert!(SolveResidual::from_state(&invalid_size).is_err());
474    }
475}