treams_core/tmatrix/metric/
saved.rs1use super::MetricResidual;
5use crate::saved::{Reader, SavedState, Writer, invalid};
6use crate::{Complex, DerivativeError, Error, Result};
7use nalgebra::DMatrix;
8
9const GRADIENT_ERRORS: [DerivativeError; 4] = [
13 DerivativeError::ZeroChiralityContrast,
14 DerivativeError::InvalidSingularValueCotangent,
15 DerivativeError::UnresolvedSingularValue,
16 DerivativeError::UnequalSingularValueWeights,
17];
18
19impl MetricResidual {
20 pub fn state_size(dimension: usize) -> Result<usize> {
22 if dimension == 0 {
23 return Err(invalid());
24 }
25 dimension
26 .checked_mul(dimension)
27 .and_then(|count| count.checked_mul(16))
28 .and_then(|bytes| bytes.checked_add(25))
29 .ok_or_else(invalid)
30 }
31}
32
33impl SavedState for MetricResidual {
34 fn save_state(&self) -> Result<Vec<u8>> {
35 let status = match &self.gradient {
36 Ok(_) => 0,
37 Err(Error::Derivative(reason)) => GRADIENT_ERRORS
38 .iter()
39 .zip(1_u8..)
40 .find_map(|(known, status)| (known == reason).then_some(status))
41 .ok_or_else(invalid)?,
42 Err(_) => return Err(invalid()),
43 };
44 let mut writer = Writer::new(Self::state_size(self.dimension)?);
45 writer.usize(self.dimension);
46 writer.byte(status);
47 for gradient in self.ks_gradient {
48 writer.f64(gradient);
49 }
50 match &self.gradient {
51 Ok(gradient) => {
52 for &value in gradient.iter() {
53 writer.complex(value);
54 }
55 }
56 Err(_) => {
57 for _ in 0..self.dimension * self.dimension {
60 writer.complex(Complex::default());
61 }
62 }
63 }
64 Ok(writer.finish())
65 }
66
67 fn from_state(bytes: &[u8]) -> Result<Self> {
68 let mut reader = Reader::new(bytes);
69 let dimension = reader.usize()?;
70 if bytes.len() != Self::state_size(dimension)? {
71 return Err(invalid());
72 }
73 let status = reader.byte()?;
74 let error = if status == 0 {
75 None
76 } else {
77 Some(
78 *GRADIENT_ERRORS
79 .get(usize::from(status) - 1)
80 .ok_or_else(invalid)?,
81 )
82 };
83 let ks_gradient = [reader.f64()?, reader.f64()?];
84 let gradient = if let Some(reason) = error {
85 for _ in 0..dimension * dimension {
86 if reader.complex()? != Complex::default() {
87 return Err(invalid());
88 }
89 }
90 Err(Error::Derivative(reason))
91 } else {
92 let values = (0..dimension * dimension)
93 .map(|_| reader.complex())
94 .collect::<Result<Vec<_>>>()?;
95 Ok(DMatrix::from_vec(dimension, dimension, values))
96 };
97 reader.finish()?;
98 Ok(Self {
99 gradient,
100 ks_gradient,
101 dimension,
102 })
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 #![allow(clippy::float_cmp)] use super::*;
111 use crate::tmatrix::metric::{Metric, metric};
112
113 #[test]
114 fn roundtrip_preserves_repeated_pushforward_and_pullback() {
115 let matrix = DMatrix::from_row_slice(
116 2,
117 2,
118 &[
119 Complex::new(-0.3, 0.03),
120 Complex::new(0.02, 0.01),
121 Complex::new(0.04, -0.02),
122 Complex::new(-0.2, -0.01),
123 ],
124 );
125 let tangent = matrix.map(|z| Complex::new(z.im, z.re));
126 for kind in [
127 Metric::CircularDichroism,
128 Metric::DualityBreaking,
129 Metric::Chirality,
130 ] {
131 let (_, residual) = metric(&matrix, &[0, 1], [1.0, 1.3], kind).unwrap();
132 let bytes = residual.save_state().unwrap();
133 assert_eq!(bytes.len(), MetricResidual::state_size(2).unwrap());
134 let restored = MetricResidual::from_state(&bytes).unwrap();
135 assert_eq!(restored.shape(), (2, 2));
136 assert_eq!(restored.save_state().unwrap(), bytes);
137 for weight in [1.2, 0.0, -0.7, 1.2] {
138 let expected = residual.pullback(weight).unwrap();
139 let actual = restored.pullback(weight).unwrap();
140 assert_eq!(actual.matrix, expected.matrix);
141 assert_eq!(actual.ks, expected.ks);
142 assert_eq!(
143 restored.pushforward(&tangent, [0.2, -0.1]).unwrap(),
144 residual.pushforward(&tangent, [0.2, -0.1]).unwrap()
145 );
146 }
147 }
148 }
149
150 #[test]
151 fn roundtrip_preserves_deferred_errors_and_zero_directions() {
152 for reason in GRADIENT_ERRORS {
153 let message = reason.to_string();
154 let residual = MetricResidual {
155 gradient: Err(Error::Derivative(reason)),
156 ks_gradient: [0.0; 2],
157 dimension: 2,
158 };
159 let bytes = residual.save_state().unwrap();
160 assert!(bytes[25..].iter().all(|&byte| byte == 0));
161 let restored = MetricResidual::from_state(&bytes).unwrap();
162 assert_eq!(restored.save_state().unwrap(), bytes);
163 for _ in 0..2 {
164 assert_eq!(restored.pullback(1.0).unwrap_err().to_string(), message);
165 assert_eq!(
166 restored
167 .pushforward(&DMatrix::identity(2, 2), [0.0; 2])
168 .unwrap_err()
169 .to_string(),
170 message
171 );
172 assert_eq!(restored.pullback(0.0).unwrap().matrix, DMatrix::zeros(2, 2));
173 assert_eq!(
174 restored
175 .pushforward(&DMatrix::zeros(2, 2), [0.0; 2])
176 .unwrap(),
177 0.0
178 );
179 }
180 }
181 }
182
183 #[test]
184 fn metric_errors_survive_roundtrip() {
185 for (entries, expected_value, message) in [
186 ([0.5, 0.0, 0.0, 0.5], 0.0, "zero contrast"),
187 ([0.0, 0.0, 0.0, 0.5], 1.0, "below numerical resolution"),
188 (
189 [0.3, 0.0, 0.1, 0.2],
190 (1.0_f64 / 7.0).sqrt(),
191 "below numerical resolution",
192 ),
193 ] {
194 let matrix = DMatrix::from_row_slice(2, 2, &entries.map(|x| Complex::new(x, 0.0)));
195 let (value, residual) = metric(&matrix, &[0, 1], [1.0; 2], Metric::Chirality).unwrap();
196 assert!((value - expected_value).abs() < 1e-15);
197 let expected = residual.pullback(1.0).unwrap_err().to_string();
198 assert!(expected.contains(message), "{expected}");
199 let bytes = residual.save_state().unwrap();
200 let restored = MetricResidual::from_state(&bytes).unwrap();
201 assert_eq!(restored.pullback(1.0).unwrap_err().to_string(), expected);
202 assert_eq!(
203 restored
204 .pushforward(&matrix, [0.0; 2])
205 .unwrap_err()
206 .to_string(),
207 expected
208 );
209 assert_eq!(restored.pullback(0.0).unwrap().matrix, DMatrix::zeros(2, 2));
210 assert_eq!(
211 restored
212 .pushforward(&DMatrix::zeros(2, 2), [0.0; 2])
213 .unwrap(),
214 0.0
215 );
216 assert_eq!(restored.save_state().unwrap(), bytes);
217 }
218 }
219
220 #[test]
221 fn rejects_invalid_shape_status_and_length_before_allocation() {
222 let (_, residual) = metric(
223 &DMatrix::identity(2, 2),
224 &[0, 1],
225 [1.0; 2],
226 Metric::Chirality,
227 )
228 .unwrap();
229 let bytes = residual.save_state().unwrap();
230 for len in 0..bytes.len() {
231 assert!(MetricResidual::from_state(&bytes[..len]).is_err());
232 }
233 let mut trailing = bytes.clone();
234 trailing.push(0);
235 assert!(MetricResidual::from_state(&trailing).is_err());
236 for dimension in [0_u64, u64::MAX, u64::MAX / 16] {
237 let mut invalid = bytes.clone();
238 invalid[..8].copy_from_slice(&dimension.to_le_bytes());
239 assert!(MetricResidual::from_state(&invalid).is_err());
240 }
241 let mut invalid = bytes.clone();
242 invalid[8] = 255;
243 assert!(MetricResidual::from_state(&invalid).is_err());
244 let mut invalid = bytes;
245 invalid[25..33].copy_from_slice(&1.0_f64.to_le_bytes());
246 assert!(MetricResidual::from_state(&invalid).is_err());
247 assert!(MetricResidual::state_size(usize::MAX).is_err());
248 }
249}