1use std::{collections::BTreeMap, fs::File, io::BufWriter, path::Path};
2
3use image::{GrayImage, Luma};
4use ndarray::{Array2, ArrayView2};
5use num_complex::Complex64;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 Result,
10 array_layout::StandardArray2,
11 array_serde::Array2Data,
12 backend::FftDirection,
13 complex,
14 error::Error,
15 illumination_calibration::IlluminationCalibrationState,
16 model::{ImagePlaneModel, Pupil, ifftshift_copy},
17};
18
19use super::{ReconstructionState, ReconstructionTrace};
20
21#[derive(Clone, Debug, Default, Serialize, Deserialize)]
23pub struct RuntimeInfo {
24 pub elapsed_seconds: f64,
26 pub completed_iterations: usize,
28 pub stopped_early: bool,
30 pub algorithm: String,
32}
33
34#[derive(Clone, Debug)]
40pub struct ReconstructionResult {
41 pub object: Array2<Complex64>,
43 pub amplitude: Array2<f64>,
45 pub phase: Array2<f64>,
47 pub object_spectrum: Array2<Complex64>,
49 pub recovered_pupil: Pupil,
55 pub calibrated_illumination: Option<Vec<(f64, f64)>>,
57 pub recovered_frame_gains: Option<Vec<f64>>,
59 pub recovered_background: Option<Vec<f64>>,
61 pub physical_illumination_calibration: Option<IlluminationCalibrationState>,
63 pub calibrated_model: Option<ImagePlaneModel>,
65 pub trace: ReconstructionTrace,
67 pub scalar_diagnostics: BTreeMap<String, f64>,
69 pub runtime: RuntimeInfo,
71 pub metadata: BTreeMap<String, String>,
73}
74
75impl ReconstructionResult {
76 pub(crate) fn from_state(
77 state: &mut ReconstructionState,
78 trace: ReconstructionTrace,
79 runtime: RuntimeInfo,
80 ) -> Result<Self> {
81 let object = state_object(state)?;
82 let amplitude = complex::amplitude(object.view());
83 let phase = complex::phase(object.view());
84 let mut scalar_diagnostics = BTreeMap::new();
85 if let Some(objective) = trace.final_objective() {
86 scalar_diagnostics.insert("final_objective".into(), objective);
87 }
88 Ok(Self {
89 object,
90 amplitude,
91 phase,
92 object_spectrum: state.object_spectrum.clone().into_inner(),
93 recovered_pupil: state.pupil.clone(),
94 calibrated_illumination: state.illumination_corrections.clone(),
95 recovered_frame_gains: state.frame_gains.clone(),
96 recovered_background: state.background.clone(),
97 physical_illumination_calibration: state.physical_illumination_calibration.clone(),
98 calibrated_model: state.calibrated_model.clone(),
99 trace,
100 scalar_diagnostics,
101 runtime,
102 metadata: BTreeMap::new(),
103 })
104 }
105
106 pub fn save_amplitude(&self, path: impl AsRef<Path>) -> Result<()> {
108 save_grayscale(self.amplitude.view(), path, false)
109 }
110
111 pub fn save_phase(&self, path: impl AsRef<Path>) -> Result<()> {
113 save_grayscale(self.phase.view(), path, true)
114 }
115
116 pub fn save_complex_object(&self, path: impl AsRef<Path>) -> Result<()> {
118 let writer = BufWriter::new(File::create(path)?);
119 serde_json::to_writer(writer, &Array2Data::from_view(self.object.view()))?;
120 Ok(())
121 }
122
123 pub fn save_pupil(&self, path: impl AsRef<Path>) -> Result<()> {
125 let writer = BufWriter::new(File::create(path)?);
126 serde_json::to_writer(writer, &self.recovered_pupil)?;
127 Ok(())
128 }
129
130 pub fn save_trace_csv(&self, path: impl AsRef<Path>) -> Result<()> {
132 let mut writer = csv::Writer::from_path(path)?;
133 writer.write_record(["iteration", "objective", "elapsed_seconds"])?;
134 for record in &self.trace.iterations {
135 writer.serialize((record.iteration, record.objective, record.elapsed_seconds))?;
136 }
137 writer.flush()?;
138 Ok(())
139 }
140
141 #[cfg(feature = "parquet")]
142 pub fn write_bundle(
170 &self,
171 path: impl AsRef<Path>,
172 options: crate::reconstruction::BundleExportOptions,
173 ) -> Result<crate::reconstruction::ResultBundle> {
174 crate::tabular::parquet::write_result_bundle(self, path.as_ref(), options, None, None)
175 }
176
177 #[cfg(feature = "parquet")]
180 pub fn write_bundle_with_context(
181 &self,
182 path: impl AsRef<Path>,
183 options: crate::reconstruction::BundleExportOptions,
184 diagnostics: Option<&crate::diagnostics::ReconstructionDiagnostics>,
185 evaluation: Option<&crate::evaluation::ReconstructionEvaluation>,
186 ) -> Result<crate::reconstruction::ResultBundle> {
187 crate::tabular::parquet::write_result_bundle(
188 self,
189 path.as_ref(),
190 options,
191 diagnostics,
192 evaluation,
193 )
194 }
195
196 pub fn validate(&self) -> Result<()> {
199 let shape = self.object.dim();
200 if shape.0 == 0 || shape.1 == 0 {
201 return Err(Error::InvalidShape(
202 "result reconstruction arrays must be non-empty".into(),
203 ));
204 }
205 for (context, array_shape, strides, is_standard) in [
206 (
207 "reconstruction result object",
208 self.object.shape(),
209 self.object.strides(),
210 self.object.is_standard_layout(),
211 ),
212 (
213 "reconstruction result amplitude",
214 self.amplitude.shape(),
215 self.amplitude.strides(),
216 self.amplitude.is_standard_layout(),
217 ),
218 (
219 "reconstruction result phase",
220 self.phase.shape(),
221 self.phase.strides(),
222 self.phase.is_standard_layout(),
223 ),
224 (
225 "reconstruction result object spectrum",
226 self.object_spectrum.shape(),
227 self.object_spectrum.strides(),
228 self.object_spectrum.is_standard_layout(),
229 ),
230 ] {
231 if !is_standard {
232 return Err(Error::NonStandardLayout {
233 context,
234 shape: array_shape.to_vec(),
235 strides: strides.to_vec(),
236 });
237 }
238 }
239 if self.amplitude.dim() != shape
240 || self.phase.dim() != shape
241 || self.object_spectrum.dim() != shape
242 {
243 return Err(Error::InvalidShape(
244 "result object, amplitude, phase, and spectrum shapes must match".into(),
245 ));
246 }
247 if self.recovered_pupil.support.len() != self.recovered_pupil.values.len() {
248 return Err(Error::InvalidShape(
249 "result pupil support and values have different lengths".into(),
250 ));
251 }
252 if self
253 .calibrated_illumination
254 .as_ref()
255 .is_some_and(Vec::is_empty)
256 || self
257 .recovered_frame_gains
258 .as_ref()
259 .is_some_and(Vec::is_empty)
260 || self
261 .recovered_background
262 .as_ref()
263 .is_some_and(Vec::is_empty)
264 {
265 return Err(Error::InvalidShape(
266 "present result calibration arrays must be non-empty".into(),
267 ));
268 }
269 if let (Some(gains), Some(background)) =
270 (&self.recovered_frame_gains, &self.recovered_background)
271 && gains.len() != background.len()
272 {
273 return Err(Error::InvalidShape(
274 "result frame gains and background lengths must match".into(),
275 ));
276 }
277 if let Some(frame_count) = self
278 .metadata
279 .get("frame_count")
280 .and_then(|value| value.parse::<usize>().ok())
281 && self
282 .recovered_frame_gains
283 .as_ref()
284 .into_iter()
285 .chain(self.recovered_background.as_ref())
286 .any(|values| values.len() != frame_count)
287 {
288 return Err(Error::InvalidShape(
289 "result frame calibration length must match metadata frame_count".into(),
290 ));
291 }
292 if self
293 .object
294 .iter()
295 .chain(self.object_spectrum.iter())
296 .chain(self.recovered_pupil.values.as_slice())
297 .any(|value| !value.re.is_finite() || !value.im.is_finite())
298 || self
299 .amplitude
300 .iter()
301 .chain(self.phase.iter())
302 .any(|value| !value.is_finite())
303 {
304 return Err(Error::InvalidModel(
305 "result arrays contain non-finite values".into(),
306 ));
307 }
308 if self.calibrated_illumination.as_ref().is_some_and(|values| {
309 values
310 .iter()
311 .any(|&(row, column)| !row.is_finite() || !column.is_finite())
312 }) || self.recovered_frame_gains.as_ref().is_some_and(|values| {
313 values
314 .iter()
315 .any(|value| !value.is_finite() || *value <= 0.0)
316 }) || self
317 .recovered_background
318 .as_ref()
319 .is_some_and(|values| values.iter().any(|value| !value.is_finite()))
320 {
321 return Err(Error::InvalidModel(
322 "result calibration values are invalid".into(),
323 ));
324 }
325 if self.physical_illumination_calibration.is_some() != self.calibrated_model.is_some() {
326 return Err(Error::InvalidModel(
327 "result physical calibration and calibrated model must be present together".into(),
328 ));
329 }
330 if let Some(model) = &self.calibrated_model {
331 model.validate()?;
332 if model.reconstruction_shape() != shape
333 || model.pupil().shape() != self.recovered_pupil.shape()
334 {
335 return Err(Error::InvalidModel(
336 "result calibrated model shapes do not match reconstructed fields".into(),
337 ));
338 }
339 }
340 if let Some(calibration) = &self.physical_illumination_calibration {
341 calibration.validate()?;
342 }
343 if self
344 .scalar_diagnostics
345 .values()
346 .any(|value| !value.is_finite())
347 {
348 return Err(Error::InvalidModel(
349 "result diagnostics contain non-finite values".into(),
350 ));
351 }
352 if !self.runtime.elapsed_seconds.is_finite()
353 || self.runtime.elapsed_seconds < 0.0
354 || self.runtime.algorithm.is_empty()
355 || self.runtime.completed_iterations != self.trace.iterations.len()
356 || self
357 .trace
358 .iterations
359 .iter()
360 .enumerate()
361 .any(|(index, record)| {
362 record.iteration != index + 1
363 || !record.objective.is_finite()
364 || !record.elapsed_seconds.is_finite()
365 || record.elapsed_seconds < 0.0
366 })
367 || self
368 .trace
369 .iterations
370 .windows(2)
371 .any(|pair| pair[1].elapsed_seconds < pair[0].elapsed_seconds)
372 || self
373 .trace
374 .iterations
375 .last()
376 .is_some_and(|record| record.elapsed_seconds > self.runtime.elapsed_seconds)
377 {
378 return Err(Error::InvalidModel(
379 "result runtime and trace are inconsistent".into(),
380 ));
381 }
382 if self.trace.algorithm_metrics.iter().any(|record| {
383 record.iteration == 0
384 || record.iteration > self.runtime.completed_iterations
385 || record.namespace.is_empty()
386 || record.metric.is_empty()
387 || !record.value.is_finite()
388 }) {
389 return Err(Error::InvalidModel(
390 "result algorithm metrics are invalid".into(),
391 ));
392 }
393 Ok(())
394 }
395}
396
397pub(crate) fn state_object(state: &mut ReconstructionState) -> Result<Array2<Complex64>> {
398 if let Some(cached) = &state.object_real_space_cache {
399 return Ok(cached.clone().into_inner());
400 }
401 let shape = state.object_spectrum.dim();
402 let mut unshifted = vec![Complex64::default(); state.object_spectrum.len()];
403 ifftshift_copy(state.object_spectrum.as_slice(), &mut unshifted, shape);
404 state.backend.fft2(
405 &mut unshifted,
406 shape,
407 FftDirection::Inverse,
408 &mut state.scratch.column,
409 )?;
410 let object = StandardArray2::from_shape_vec(shape, unshifted)?;
411 state.object_real_space_cache = Some(object.clone());
412 Ok(object.into_inner())
413}
414
415pub(crate) fn save_grayscale(
416 values: ArrayView2<'_, f64>,
417 path: impl AsRef<Path>,
418 phase: bool,
419) -> Result<()> {
420 let range = if phase {
421 (-std::f64::consts::PI, std::f64::consts::PI)
422 } else {
423 let minimum = values.iter().copied().fold(f64::INFINITY, f64::min);
424 let maximum = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
425 (minimum, maximum)
426 };
427 save_grayscale_with_range(values, path, range)
428}
429
430pub(crate) fn save_signed_grayscale(
431 values: ArrayView2<'_, f64>,
432 path: impl AsRef<Path>,
433) -> Result<()> {
434 let maximum_absolute = values
435 .iter()
436 .map(|value| value.abs())
437 .fold(0.0, f64::max)
438 .max(f64::EPSILON);
439 save_grayscale_with_range(values, path, (-maximum_absolute, maximum_absolute))
440}
441
442fn save_grayscale_with_range(
443 values: ArrayView2<'_, f64>,
444 path: impl AsRef<Path>,
445 (minimum, maximum): (f64, f64),
446) -> Result<()> {
447 let width = u32::try_from(values.ncols()).map_err(|_| {
448 Error::InvalidShape("image width does not fit the PNG dimension type".into())
449 })?;
450 let height = u32::try_from(values.nrows()).map_err(|_| {
451 Error::InvalidShape("image height does not fit the PNG dimension type".into())
452 })?;
453 let range = (maximum - minimum).max(f64::EPSILON);
454 let mut image = GrayImage::new(width, height);
455 for row in 0..values.nrows() {
456 for column in 0..values.ncols() {
457 let normalized = ((values[(row, column)] - minimum) / range).clamp(0.0, 1.0);
458 image.put_pixel(
459 column as u32,
460 row as u32,
461 Luma([(normalized * 255.0).round() as u8]),
462 );
463 }
464 }
465 image.save(path)?;
466 Ok(())
467}
468
469impl Serialize for ReconstructionResult {
470 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
471 where
472 S: serde::Serializer,
473 {
474 #[derive(Serialize)]
475 struct Representation<'a> {
476 object: Array2Data<Complex64>,
477 amplitude: Array2Data<f64>,
478 phase: Array2Data<f64>,
479 object_spectrum: Array2Data<Complex64>,
480 recovered_pupil: &'a Pupil,
481 calibrated_illumination: &'a Option<Vec<(f64, f64)>>,
482 recovered_frame_gains: &'a Option<Vec<f64>>,
483 recovered_background: &'a Option<Vec<f64>>,
484 physical_illumination_calibration: &'a Option<IlluminationCalibrationState>,
485 calibrated_model: &'a Option<ImagePlaneModel>,
486 trace: &'a ReconstructionTrace,
487 scalar_diagnostics: &'a BTreeMap<String, f64>,
488 runtime: &'a RuntimeInfo,
489 metadata: &'a BTreeMap<String, String>,
490 }
491
492 Representation {
493 object: Array2Data::from_view(self.object.view()),
494 amplitude: Array2Data::from_view(self.amplitude.view()),
495 phase: Array2Data::from_view(self.phase.view()),
496 object_spectrum: Array2Data::from_view(self.object_spectrum.view()),
497 recovered_pupil: &self.recovered_pupil,
498 calibrated_illumination: &self.calibrated_illumination,
499 recovered_frame_gains: &self.recovered_frame_gains,
500 recovered_background: &self.recovered_background,
501 physical_illumination_calibration: &self.physical_illumination_calibration,
502 calibrated_model: &self.calibrated_model,
503 trace: &self.trace,
504 scalar_diagnostics: &self.scalar_diagnostics,
505 runtime: &self.runtime,
506 metadata: &self.metadata,
507 }
508 .serialize(serializer)
509 }
510}
511
512impl<'de> Deserialize<'de> for ReconstructionResult {
513 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
514 where
515 D: serde::Deserializer<'de>,
516 {
517 use serde::de::Error as _;
518
519 #[derive(Deserialize)]
520 #[serde(deny_unknown_fields)]
521 struct Representation {
522 object: Array2Data<Complex64>,
523 amplitude: Array2Data<f64>,
524 phase: Array2Data<f64>,
525 object_spectrum: Array2Data<Complex64>,
526 recovered_pupil: Pupil,
527 calibrated_illumination: Option<Vec<(f64, f64)>>,
528 recovered_frame_gains: Option<Vec<f64>>,
529 recovered_background: Option<Vec<f64>>,
530 physical_illumination_calibration: Option<IlluminationCalibrationState>,
531 calibrated_model: Option<ImagePlaneModel>,
532 trace: ReconstructionTrace,
533 scalar_diagnostics: BTreeMap<String, f64>,
534 runtime: RuntimeInfo,
535 metadata: BTreeMap<String, String>,
536 }
537
538 let representation = Representation::deserialize(deserializer)?;
539 let result = Self {
540 object: representation
541 .object
542 .into_array()
543 .map_err(D::Error::custom)?,
544 amplitude: representation
545 .amplitude
546 .into_array()
547 .map_err(D::Error::custom)?,
548 phase: representation
549 .phase
550 .into_array()
551 .map_err(D::Error::custom)?,
552 object_spectrum: representation
553 .object_spectrum
554 .into_array()
555 .map_err(D::Error::custom)?,
556 recovered_pupil: representation.recovered_pupil,
557 calibrated_illumination: representation.calibrated_illumination,
558 recovered_frame_gains: representation.recovered_frame_gains,
559 recovered_background: representation.recovered_background,
560 physical_illumination_calibration: representation.physical_illumination_calibration,
561 calibrated_model: representation.calibrated_model,
562 trace: representation.trace,
563 scalar_diagnostics: representation.scalar_diagnostics,
564 runtime: representation.runtime,
565 metadata: representation.metadata,
566 };
567 result.validate().map_err(D::Error::custom)?;
568 Ok(result)
569 }
570}