1use std::{
2 collections::BTreeMap,
3 fs::File,
4 io::{BufReader, BufWriter},
5 path::Path,
6};
7
8use image::{GrayImage, Luma};
9use num_complex::Complex64;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 Array2, Result,
14 backend::FftDirection,
15 complex,
16 diagnostics::ReconstructionHistory,
17 error::Error,
18 model::{Pupil, ifftshift_copy},
19};
20
21use super::ReconstructionState;
22
23pub const RESULT_BUNDLE_FORMAT_VERSION: u32 = 1;
24
25#[derive(Clone, Debug, Default, Serialize, Deserialize)]
26pub struct RuntimeInfo {
27 pub elapsed_seconds: f64,
28 pub completed_iterations: usize,
29 pub stopped_early: bool,
30 pub algorithm: String,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct ReconstructionResult {
35 pub object: Array2<Complex64>,
36 pub amplitude: Array2<f64>,
37 pub phase: Array2<f64>,
38 pub object_spectrum: Array2<Complex64>,
39 pub recovered_pupil: Pupil,
40 pub calibrated_illumination: Option<Vec<(f64, f64)>>,
42 pub recovered_frame_gains: Option<Vec<f64>>,
43 pub recovered_background: Option<Vec<f64>>,
44 pub history: ReconstructionHistory,
45 pub diagnostics: BTreeMap<String, f64>,
46 pub runtime: RuntimeInfo,
47 pub metadata: BTreeMap<String, String>,
48}
49
50#[derive(Serialize, Deserialize)]
51struct ReconstructionResultBundle {
52 format_version: u32,
53 result: ReconstructionResult,
54}
55
56impl ReconstructionResult {
57 pub(crate) fn from_state(
58 state: &mut ReconstructionState,
59 history: ReconstructionHistory,
60 runtime: RuntimeInfo,
61 ) -> Result<Self> {
62 let object = state_object(state)?;
63 let amplitude = complex::amplitude(&object);
64 let phase = complex::phase(&object);
65 let mut diagnostics = BTreeMap::new();
66 if let Some(loss) = history.final_loss() {
67 diagnostics.insert("final_loss".into(), loss);
68 }
69 if let Some(record) = history.iterations.last() {
70 if let Some(residual) = record.admm_primal_residual_rms {
71 diagnostics.insert("final_admm_primal_residual_rms".into(), residual);
72 }
73 if let Some(residual) = record.admm_dual_residual_rms {
74 diagnostics.insert("final_admm_dual_residual_rms".into(), residual);
75 }
76 }
77 Ok(Self {
78 object,
79 amplitude,
80 phase,
81 object_spectrum: state.object_spectrum.clone(),
82 recovered_pupil: state.pupil.clone(),
83 calibrated_illumination: state.illumination_corrections.clone(),
84 recovered_frame_gains: state.frame_gains.clone(),
85 recovered_background: state.background.clone(),
86 history,
87 diagnostics,
88 runtime,
89 metadata: BTreeMap::new(),
90 })
91 }
92
93 pub fn save_amplitude(&self, path: impl AsRef<Path>) -> Result<()> {
94 save_grayscale(&self.amplitude, path, false)
95 }
96
97 pub fn save_phase(&self, path: impl AsRef<Path>) -> Result<()> {
98 save_grayscale(&self.phase, path, true)
99 }
100
101 pub fn save_complex_object(&self, path: impl AsRef<Path>) -> Result<()> {
102 let writer = BufWriter::new(File::create(path)?);
103 serde_json::to_writer(writer, &self.object)?;
104 Ok(())
105 }
106
107 pub fn save_pupil(&self, path: impl AsRef<Path>) -> Result<()> {
108 let writer = BufWriter::new(File::create(path)?);
109 serde_json::to_writer(writer, &self.recovered_pupil)?;
110 Ok(())
111 }
112
113 pub fn save_loss_csv(&self, path: impl AsRef<Path>) -> Result<()> {
114 let mut writer = csv::Writer::from_path(path)?;
115 writer.write_record([
116 "iteration",
117 "loss",
118 "elapsed_seconds",
119 "admm_primal_residual_rms",
120 "admm_dual_residual_rms",
121 ])?;
122 for record in &self.history.iterations {
123 writer.serialize((
124 record.iteration,
125 record.loss,
126 record.elapsed_seconds,
127 record.admm_primal_residual_rms,
128 record.admm_dual_residual_rms,
129 ))?;
130 }
131 writer.flush()?;
132 Ok(())
133 }
134
135 pub fn save_bundle(&self, path: impl AsRef<Path>) -> Result<()> {
138 self.validate()?;
139 let writer = BufWriter::new(File::create(path)?);
140 serde_json::to_writer(
141 writer,
142 &ReconstructionResultBundle {
143 format_version: RESULT_BUNDLE_FORMAT_VERSION,
144 result: self.clone(),
145 },
146 )?;
147 Ok(())
148 }
149
150 pub fn load_bundle(path: impl AsRef<Path>) -> Result<Self> {
151 let reader = BufReader::new(File::open(path)?);
152 let bundle: ReconstructionResultBundle = serde_json::from_reader(reader)?;
153 if bundle.format_version != RESULT_BUNDLE_FORMAT_VERSION {
154 return Err(Error::InvalidParameter {
155 name: "result bundle format_version",
156 reason: format!(
157 "expected {RESULT_BUNDLE_FORMAT_VERSION}, got {}",
158 bundle.format_version
159 ),
160 });
161 }
162 bundle.result.validate()?;
163 Ok(bundle.result)
164 }
165
166 pub fn validate(&self) -> Result<()> {
167 let shape = self.object.shape();
168 if self.amplitude.shape() != shape
169 || self.phase.shape() != shape
170 || self.object_spectrum.shape() != shape
171 {
172 return Err(Error::InvalidShape(
173 "result object, amplitude, phase, and spectrum shapes must match".into(),
174 ));
175 }
176 if self.recovered_pupil.support.len() != self.recovered_pupil.values.len() {
177 return Err(Error::InvalidShape(
178 "result pupil support and values have different lengths".into(),
179 ));
180 }
181 if self
182 .object
183 .as_slice()
184 .iter()
185 .chain(self.object_spectrum.as_slice())
186 .chain(self.recovered_pupil.values.as_slice())
187 .any(|value| !value.re.is_finite() || !value.im.is_finite())
188 || self
189 .amplitude
190 .as_slice()
191 .iter()
192 .chain(self.phase.as_slice())
193 .any(|value| !value.is_finite())
194 {
195 return Err(Error::InvalidModel(
196 "result arrays contain non-finite values".into(),
197 ));
198 }
199 if self.calibrated_illumination.as_ref().is_some_and(|values| {
200 values
201 .iter()
202 .any(|&(row, column)| !row.is_finite() || !column.is_finite())
203 }) || self.recovered_frame_gains.as_ref().is_some_and(|values| {
204 values
205 .iter()
206 .any(|value| !value.is_finite() || *value <= 0.0)
207 }) || self
208 .recovered_background
209 .as_ref()
210 .is_some_and(|values| values.iter().any(|value| !value.is_finite()))
211 {
212 return Err(Error::InvalidModel(
213 "result calibration values are invalid".into(),
214 ));
215 }
216 if self.diagnostics.values().any(|value| !value.is_finite()) {
217 return Err(Error::InvalidModel(
218 "result diagnostics contain non-finite values".into(),
219 ));
220 }
221 if !self.runtime.elapsed_seconds.is_finite()
222 || self.runtime.elapsed_seconds < 0.0
223 || self.runtime.algorithm.is_empty()
224 || self.runtime.completed_iterations != self.history.iterations.len()
225 || self
226 .history
227 .iterations
228 .iter()
229 .enumerate()
230 .any(|(index, record)| {
231 record.iteration != index + 1
232 || !record.loss.is_finite()
233 || !record.elapsed_seconds.is_finite()
234 || record.elapsed_seconds < 0.0
235 || record
236 .admm_primal_residual_rms
237 .is_some_and(|value| !value.is_finite() || value < 0.0)
238 || record
239 .admm_dual_residual_rms
240 .is_some_and(|value| !value.is_finite() || value < 0.0)
241 })
242 || self
243 .history
244 .iterations
245 .windows(2)
246 .any(|pair| pair[1].elapsed_seconds < pair[0].elapsed_seconds)
247 || self
248 .history
249 .iterations
250 .last()
251 .is_some_and(|record| record.elapsed_seconds > self.runtime.elapsed_seconds)
252 {
253 return Err(Error::InvalidModel(
254 "result runtime and history are inconsistent".into(),
255 ));
256 }
257 Ok(())
258 }
259}
260
261pub(crate) fn state_object(state: &mut ReconstructionState) -> Result<Array2<Complex64>> {
262 if let Some(cached) = &state.object_real_space_cache {
263 return Ok(cached.clone());
264 }
265 let shape = state.object_spectrum.shape();
266 let mut unshifted = vec![Complex64::default(); state.object_spectrum.len()];
267 ifftshift_copy(state.object_spectrum.as_slice(), &mut unshifted, shape);
268 state.backend.fft2(
269 &mut unshifted,
270 shape,
271 FftDirection::Inverse,
272 &mut state.scratch.column,
273 )?;
274 let object = Array2::from_vec(shape, unshifted)?;
275 state.object_real_space_cache = Some(object.clone());
276 Ok(object)
277}
278
279pub(crate) fn save_grayscale(
280 values: &Array2<f64>,
281 path: impl AsRef<Path>,
282 phase: bool,
283) -> Result<()> {
284 let range = if phase {
285 (-std::f64::consts::PI, std::f64::consts::PI)
286 } else {
287 let minimum = values
288 .as_slice()
289 .iter()
290 .copied()
291 .fold(f64::INFINITY, f64::min);
292 let maximum = values
293 .as_slice()
294 .iter()
295 .copied()
296 .fold(f64::NEG_INFINITY, f64::max);
297 (minimum, maximum)
298 };
299 save_grayscale_with_range(values, path, range)
300}
301
302pub(crate) fn save_signed_grayscale(values: &Array2<f64>, path: impl AsRef<Path>) -> Result<()> {
303 let maximum_absolute = values
304 .as_slice()
305 .iter()
306 .map(|value| value.abs())
307 .fold(0.0, f64::max)
308 .max(f64::EPSILON);
309 save_grayscale_with_range(values, path, (-maximum_absolute, maximum_absolute))
310}
311
312fn save_grayscale_with_range(
313 values: &Array2<f64>,
314 path: impl AsRef<Path>,
315 (minimum, maximum): (f64, f64),
316) -> Result<()> {
317 let width = u32::try_from(values.width()).map_err(|_| {
318 Error::InvalidShape("image width does not fit the PNG dimension type".into())
319 })?;
320 let height = u32::try_from(values.height()).map_err(|_| {
321 Error::InvalidShape("image height does not fit the PNG dimension type".into())
322 })?;
323 let range = (maximum - minimum).max(f64::EPSILON);
324 let mut image = GrayImage::new(width, height);
325 for row in 0..values.height() {
326 for column in 0..values.width() {
327 let normalized = ((values[(row, column)] - minimum) / range).clamp(0.0, 1.0);
328 image.put_pixel(
329 column as u32,
330 row as u32,
331 Luma([(normalized * 255.0).round() as u8]),
332 );
333 }
334 }
335 image.save(path)?;
336 Ok(())
337}