1use ndarray::{ArrayView2, ArrayViewMut2};
2use num_complex::Complex64;
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::{
6 Result,
7 array_layout::{StandardView2, checked_len_2d},
8 error::Error,
9 experiment::{
10 AcquisitionPlan, Illumination, KVector, MultiplexingMatrix, Optics, ResolvedIllumination,
11 },
12};
13
14use super::{CropIndices, FourierCrop, FourierOffset, Pupil, Sampling};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ReconstructionShape {
24 Exact((usize, usize)),
26 Minimum,
29 Smooth,
32 PowerOfTwo,
34}
35
36#[derive(Clone, Copy, Debug)]
37struct GridShift {
38 integer: isize,
39 offset: f64,
40 lower: isize,
41 upper: isize,
42}
43
44#[derive(Clone, Copy, Debug)]
45pub(crate) struct CropDisplacementBounds {
46 minimum_row: isize,
47 maximum_row: isize,
48 minimum_column: isize,
49 maximum_column: isize,
50}
51
52#[derive(Clone, Debug, Serialize)]
58pub struct ImagePlaneModel {
59 pub(crate) k_vectors: Vec<KVector>,
61 pub(crate) pupil: Pupil,
62 pub(crate) crop_indices: CropIndices,
63 #[serde(default)]
65 pub(crate) subpixel_offsets: Option<Vec<FourierOffset>>,
66 pub(crate) sampling: Sampling,
67 pub(crate) image_shape: (usize, usize),
68 pub(crate) reconstruction_shape: (usize, usize),
69 pub(crate) frame_gains: Option<Vec<f64>>,
70 pub(crate) background: Option<Vec<f64>>,
71 pub(crate) multiplexing_matrix: Option<MultiplexingMatrix>,
73}
74
75impl<'de> Deserialize<'de> for ImagePlaneModel {
76 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
77 where
78 D: Deserializer<'de>,
79 {
80 use serde::de::Error as _;
81
82 #[derive(Deserialize)]
83 #[serde(deny_unknown_fields)]
84 struct Representation {
85 k_vectors: Vec<KVector>,
86 pupil: Pupil,
87 crop_indices: CropIndices,
88 #[serde(default)]
89 subpixel_offsets: Option<Vec<FourierOffset>>,
90 sampling: Sampling,
91 image_shape: (usize, usize),
92 reconstruction_shape: (usize, usize),
93 frame_gains: Option<Vec<f64>>,
94 background: Option<Vec<f64>>,
95 multiplexing_matrix: Option<MultiplexingMatrix>,
96 }
97
98 let representation = Representation::deserialize(deserializer)?;
99 let model = Self {
100 k_vectors: representation.k_vectors,
101 pupil: representation.pupil,
102 crop_indices: representation.crop_indices,
103 subpixel_offsets: representation.subpixel_offsets,
104 sampling: representation.sampling,
105 image_shape: representation.image_shape,
106 reconstruction_shape: representation.reconstruction_shape,
107 frame_gains: representation.frame_gains,
108 background: representation.background,
109 multiplexing_matrix: representation.multiplexing_matrix,
110 };
111 model.validate().map_err(D::Error::custom)?;
112 Ok(model)
113 }
114}
115
116impl ImagePlaneModel {
117 pub fn new(
123 k_vectors: Vec<KVector>,
124 pupil: Pupil,
125 crop_indices: CropIndices,
126 sampling: Sampling,
127 image_shape: (usize, usize),
128 reconstruction_shape: (usize, usize),
129 ) -> Result<Self> {
130 let model = Self {
131 k_vectors,
132 pupil,
133 crop_indices,
134 subpixel_offsets: None,
135 sampling,
136 image_shape,
137 reconstruction_shape,
138 frame_gains: None,
139 background: None,
140 multiplexing_matrix: None,
141 };
142 model.validate()?;
143 Ok(model)
144 }
145
146 pub fn from_experiment(
149 optics: &Optics,
150 illumination: &Illumination,
151 image_shape: (usize, usize),
152 reconstruction_shape: ReconstructionShape,
153 ) -> Result<Self> {
154 optics.validate()?;
155 let resolved = illumination.resolve(optics)?;
156 Self::compile(optics, &resolved, image_shape, reconstruction_shape)
157 }
158
159 pub fn suggest_reconstruction_shape(
162 optics: &Optics,
163 illumination: &Illumination,
164 image_shape: (usize, usize),
165 reconstruction_shape: ReconstructionShape,
166 ) -> Result<(usize, usize)> {
167 optics.validate()?;
168 let resolved = illumination.resolve(optics)?;
169 let bounds = Self::crop_displacement_bounds(optics, image_shape, resolved.k_vectors())?;
170 Self::resolve_reconstruction_shape(image_shape, reconstruction_shape, &[bounds])
171 }
172
173 pub fn compile(
178 optics: &Optics,
179 illumination: &ResolvedIllumination,
180 image_shape: (usize, usize),
181 reconstruction_shape: ReconstructionShape,
182 ) -> Result<Self> {
183 optics.validate()?;
184 let k_vectors = illumination.k_vectors().to_vec();
185 let bounds = Self::crop_displacement_bounds(optics, image_shape, &k_vectors)?;
186 let reconstruction_shape =
187 Self::resolve_reconstruction_shape(image_shape, reconstruction_shape, &[bounds])?;
188 let scale = reconstruction_shape.1 as f64 / image_shape.1 as f64;
189 let low_res_pixel_size = optics.object_pixel_size();
190 let mut sampling = Sampling::new(
191 low_res_pixel_size,
192 low_res_pixel_size / scale,
193 std::f64::consts::TAU / (image_shape.1 as f64 * low_res_pixel_size),
194 std::f64::consts::TAU / (image_shape.0 as f64 * low_res_pixel_size),
195 )?;
196 sampling.wavelength = Some(optics.wavelength_vacuum_m);
197 let maximum_illumination_na = k_vectors
198 .iter()
199 .map(|vector| {
200 vector.kx.hypot(vector.ky) * optics.wavelength_vacuum_m / std::f64::consts::TAU
201 })
202 .fold(0.0, f64::max);
203 sampling.synthetic_na = Some(optics.objective_na + maximum_illumination_na);
204 let pupil = Pupil::circular(image_shape, &sampling, optics)?;
205 let mut offsets = Vec::with_capacity(k_vectors.len());
206 let crops = k_vectors
207 .iter()
208 .map(|vector| {
209 let (crop, offset) =
210 Self::crop_for_k_vector(vector, &sampling, image_shape, reconstruction_shape)?;
211 offsets.push(offset);
212 Ok(crop)
213 })
214 .collect::<Result<Vec<_>>>()?;
215 let mut model = Self::new(
216 k_vectors,
217 pupil,
218 CropIndices::new(crops),
219 sampling,
220 image_shape,
221 reconstruction_shape,
222 )?;
223 model.frame_gains = Some(illumination.frame_gains());
224 let matrix = illumination.compiled_multiplexing_matrix();
225 let identity = matrix.len() == model.source_count()
226 && matrix
227 .iter()
228 .enumerate()
229 .all(|(source, row)| row.as_slice() == [(source, 1.0)]);
230 model.multiplexing_matrix = (!identity).then_some(matrix);
231 model.subpixel_offsets = Some(offsets);
232 model.validate()?;
233 Ok(model)
234 }
235
236 pub(crate) fn crop_displacement_bounds(
237 optics: &Optics,
238 image_shape: (usize, usize),
239 k_vectors: &[KVector],
240 ) -> Result<CropDisplacementBounds> {
241 optics.validate()?;
242 validate_image_shape(image_shape)?;
243 if k_vectors.is_empty() {
244 return Err(Error::InvalidModel(
245 "illumination must contain at least one frame".into(),
246 ));
247 }
248 let low_res_pixel_size = optics.object_pixel_size();
249 let dkx = std::f64::consts::TAU / (image_shape.1 as f64 * low_res_pixel_size);
250 let dky = std::f64::consts::TAU / (image_shape.0 as f64 * low_res_pixel_size);
251 let mut bounds = CropDisplacementBounds {
252 minimum_row: isize::MAX,
253 maximum_row: isize::MIN,
254 minimum_column: isize::MAX,
255 maximum_column: isize::MIN,
256 };
257 for vector in k_vectors {
258 let row = checked_grid_shift(vector.ky / dky)?;
259 let column = checked_grid_shift(vector.kx / dkx)?;
260 bounds.minimum_row = bounds.minimum_row.min(row.lower);
261 bounds.maximum_row = bounds.maximum_row.max(row.upper);
262 bounds.minimum_column = bounds.minimum_column.min(column.lower);
263 bounds.maximum_column = bounds.maximum_column.max(column.upper);
264 }
265 Ok(bounds)
266 }
267
268 pub(crate) fn resolve_reconstruction_shape(
269 image_shape: (usize, usize),
270 reconstruction_shape: ReconstructionShape,
271 bounds: &[CropDisplacementBounds],
272 ) -> Result<(usize, usize)> {
273 validate_image_shape(image_shape)?;
274 if bounds.is_empty() {
275 return Err(Error::InvalidModel(
276 "at least one set of illumination crop bounds is required".into(),
277 ));
278 }
279 match reconstruction_shape {
280 ReconstructionShape::Exact(shape) => {
281 validate_reconstruction_aspect(image_shape, shape)?;
282 if !shape_contains_bounds(image_shape, shape, bounds)? {
283 return Err(Error::InvalidShape(format!(
284 "reconstruction shape {shape:?} does not contain every illumination crop and subpixel interpolation stencil"
285 )));
286 }
287 Ok(shape)
288 }
289 policy => {
290 let divisor = greatest_common_divisor(image_shape.0, image_shape.1);
291 let aspect_height = image_shape.0 / divisor;
292 let aspect_width = image_shape.1 / divisor;
293 let maximum_multiplier =
294 (isize::MAX as usize / aspect_height).min(isize::MAX as usize / aspect_width);
295 let minimum_multiplier = minimum_fitting_multiplier(
296 image_shape,
297 (aspect_height, aspect_width),
298 divisor,
299 maximum_multiplier,
300 bounds,
301 )?;
302 let multiplier = match policy {
303 ReconstructionShape::Minimum => minimum_multiplier,
304 ReconstructionShape::Smooth => next_smooth_multiplier(minimum_multiplier)
305 .filter(|&value| value <= maximum_multiplier)
306 .ok_or_else(|| {
307 Error::InvalidShape(
308 "no supported 2/3/5/7-smooth reconstruction multiplier exists"
309 .into(),
310 )
311 })?,
312 ReconstructionShape::PowerOfTwo => minimum_multiplier
313 .checked_next_power_of_two()
314 .filter(|&value| value <= maximum_multiplier)
315 .ok_or_else(|| {
316 Error::InvalidShape(
317 "no supported power-of-two reconstruction multiplier exists".into(),
318 )
319 })?,
320 ReconstructionShape::Exact(shape) => {
321 return Err(Error::InvalidShape(format!(
322 "unexpected exact reconstruction shape {shape:?} during automatic sizing"
323 )));
324 }
325 };
326 candidate_shape((aspect_height, aspect_width), multiplier)
327 }
328 }
329 }
330
331 pub fn source_count(&self) -> usize {
333 self.k_vectors.len()
334 }
335
336 pub fn frame_count(&self) -> usize {
338 self.multiplexing_matrix
339 .as_ref()
340 .map_or_else(|| self.source_count(), Vec::len)
341 }
342
343 pub fn is_multiplexed(&self) -> bool {
345 self.multiplexing_matrix
346 .as_ref()
347 .is_some_and(|matrix| matrix.iter().any(|row| row.len() > 1))
348 }
349
350 pub fn k_vectors(&self) -> &[KVector] {
352 &self.k_vectors
353 }
354
355 pub fn pupil(&self) -> &Pupil {
357 &self.pupil
358 }
359
360 pub fn pupil_mut(&mut self) -> &mut Pupil {
362 &mut self.pupil
363 }
364
365 pub fn crop_indices(&self) -> &CropIndices {
367 &self.crop_indices
368 }
369
370 pub fn subpixel_offsets(&self) -> Option<&[FourierOffset]> {
372 self.subpixel_offsets.as_deref()
373 }
374
375 pub const fn sampling(&self) -> &Sampling {
377 &self.sampling
378 }
379
380 pub const fn image_shape(&self) -> (usize, usize) {
382 self.image_shape
383 }
384
385 pub const fn reconstruction_shape(&self) -> (usize, usize) {
387 self.reconstruction_shape
388 }
389
390 pub fn frame_gains(&self) -> Option<&[f64]> {
392 self.frame_gains.as_deref()
393 }
394
395 pub fn background(&self) -> Option<&[f64]> {
400 self.background.as_deref()
401 }
402
403 pub fn multiplexing_matrix(&self) -> Option<&MultiplexingMatrix> {
405 self.multiplexing_matrix.as_ref()
406 }
407
408 pub fn with_frame_gains(mut self, values: Option<Vec<f64>>) -> Result<Self> {
410 self.frame_gains = values;
411 self.validate()?;
412 Ok(self)
413 }
414
415 pub fn update_intensity_calibration(
422 &mut self,
423 source_power: &[f64],
424 acquisition: &AcquisitionPlan,
425 ) -> Result<()> {
426 if source_power.len() != self.source_count()
427 || source_power
428 .iter()
429 .any(|value| !value.is_finite() || *value < 0.0)
430 {
431 return Err(Error::InvalidParameter {
432 name: "source_power",
433 reason: format!(
434 "must contain {} finite non-negative values",
435 self.source_count()
436 ),
437 });
438 }
439 if acquisition.frame_count() != self.frame_count() {
440 return Err(Error::InvalidParameter {
441 name: "acquisition",
442 reason: format!(
443 "frame count {} differs from compiled frame count {}",
444 acquisition.frame_count(),
445 self.frame_count()
446 ),
447 });
448 }
449 let mut matrix = Vec::with_capacity(acquisition.frame_count());
450 let mut gains = Vec::with_capacity(acquisition.frame_count());
451 for frame in acquisition.frames() {
452 let mut row = Vec::with_capacity(frame.contributions.len());
453 for contribution in &frame.contributions {
454 let power = source_power.get(contribution.source).ok_or_else(|| {
455 Error::InvalidParameter {
456 name: "acquisition",
457 reason: format!(
458 "source index {} is outside {} compiled sources",
459 contribution.source,
460 self.source_count()
461 ),
462 }
463 })?;
464 let weight = contribution.intensity_weight * power;
465 if !weight.is_finite() || weight < 0.0 {
466 return Err(Error::InvalidParameter {
467 name: "acquisition",
468 reason: "compiled source weights must be finite and non-negative".into(),
469 });
470 }
471 if weight != 0.0 {
472 row.push((contribution.source, weight));
473 }
474 }
475 if row.is_empty() {
476 return Err(Error::InvalidParameter {
477 name: "acquisition",
478 reason: "every calibrated frame must retain positive source weight".into(),
479 });
480 }
481 matrix.push(row);
482 gains.push(frame.gain);
483 }
484 let identity = matrix.len() == self.source_count()
485 && matrix
486 .iter()
487 .enumerate()
488 .all(|(source, row)| row.as_slice() == [(source, 1.0)]);
489 let previous_matrix =
490 std::mem::replace(&mut self.multiplexing_matrix, (!identity).then_some(matrix));
491 let previous_gains = self.frame_gains.replace(gains);
492 if let Err(error) = self.validate() {
493 self.multiplexing_matrix = previous_matrix;
494 self.frame_gains = previous_gains;
495 return Err(error);
496 }
497 Ok(())
498 }
499
500 pub fn update_illumination_geometry(
506 &mut self,
507 optics: &Optics,
508 illumination: &Illumination,
509 ) -> Result<()> {
510 let resolved = illumination.resolve(optics)?;
511 if resolved.source_count() != self.source_count()
512 || resolved.frame_count() != self.frame_count()
513 {
514 return Err(Error::InvalidModel(
515 "calibrated illumination must preserve source and frame counts".into(),
516 ));
517 }
518 let k_vectors = resolved.k_vectors().to_vec();
519 let mut offsets = Vec::with_capacity(k_vectors.len());
520 let crops = k_vectors
521 .iter()
522 .map(|vector| {
523 let (crop, offset) = Self::crop_for_k_vector(
524 vector,
525 &self.sampling,
526 self.image_shape,
527 self.reconstruction_shape,
528 )?;
529 offsets.push(offset);
530 Ok(crop)
531 })
532 .collect::<Result<Vec<_>>>()?;
533
534 let previous_vectors = std::mem::replace(&mut self.k_vectors, k_vectors);
535 let previous_crops = std::mem::replace(&mut self.crop_indices, CropIndices::new(crops));
536 let previous_offsets = self.subpixel_offsets.replace(offsets);
537 let previous_synthetic_na = self.sampling.synthetic_na;
538 self.sampling.synthetic_na = Some(
539 optics.objective_na
540 + self
541 .k_vectors
542 .iter()
543 .map(|vector| {
544 vector.kx.hypot(vector.ky) * optics.wavelength_vacuum_m
545 / std::f64::consts::TAU
546 })
547 .fold(0.0, f64::max),
548 );
549 if let Err(error) =
550 self.update_intensity_calibration(resolved.source_power(), illumination.acquisition())
551 {
552 self.k_vectors = previous_vectors;
553 self.crop_indices = previous_crops;
554 self.subpixel_offsets = previous_offsets;
555 self.sampling.synthetic_na = previous_synthetic_na;
556 return Err(error);
557 }
558 self.validate()
559 }
560
561 pub fn replace_k_vectors(&mut self, values: Vec<KVector>) -> Result<()> {
567 let previous = std::mem::replace(&mut self.k_vectors, values);
568 if let Err(error) = self.validate() {
569 self.k_vectors = previous;
570 return Err(error);
571 }
572 Ok(())
573 }
574
575 pub fn with_background(mut self, values: Option<Vec<f64>>) -> Result<Self> {
577 self.background = values;
578 self.validate()?;
579 Ok(self)
580 }
581
582 pub fn with_multiplexing(mut self, matrix: MultiplexingMatrix) -> Result<Self> {
584 self.multiplexing_matrix = Some(matrix);
585 self.validate()?;
586 Ok(self)
587 }
588
589 pub fn with_subpixel_offsets(mut self, offsets: Vec<FourierOffset>) -> Result<Self> {
591 self.subpixel_offsets = Some(offsets);
592 self.validate()?;
593 Ok(self)
594 }
595
596 pub fn source_offset(&self, source: usize) -> Result<FourierOffset> {
598 self.crop_indices.get(source)?;
599 match &self.subpixel_offsets {
600 None => Ok(FourierOffset::default()),
601 Some(offsets) => offsets.get(source).copied().ok_or_else(|| {
602 Error::InvalidModel("subpixel offset count does not match source count".into())
603 }),
604 }
605 }
606
607 pub fn extract_patch(
612 &self,
613 object_spectrum: ArrayView2<'_, Complex64>,
614 source: usize,
615 destination: &mut [Complex64],
616 ) -> Result<()> {
617 let object_spectrum = StandardView2::try_from(object_spectrum)?;
618 self.extract_patch_standard(object_spectrum, source, destination)
619 }
620
621 pub(crate) fn extract_patch_standard(
622 &self,
623 object_spectrum: StandardView2<'_, Complex64>,
624 source: usize,
625 destination: &mut [Complex64],
626 ) -> Result<()> {
627 self.extract_patch_at_offset(
628 object_spectrum,
629 source,
630 self.source_offset(source)?,
631 destination,
632 )
633 }
634
635 pub(crate) fn extract_patch_at_offset(
636 &self,
637 object_spectrum: StandardView2<'_, Complex64>,
638 source: usize,
639 offset: FourierOffset,
640 destination: &mut [Complex64],
641 ) -> Result<()> {
642 if object_spectrum.dim() != self.reconstruction_shape {
643 return Err(Error::InvalidShape(format!(
644 "object spectrum shape {:?} does not match {:?}",
645 object_spectrum.dim(),
646 self.reconstruction_shape
647 )));
648 }
649 let crop = self.crop_indices.get(source)?;
650 crop.extract_subpixel_standard(object_spectrum, destination, offset)
651 }
652
653 pub fn insert_patch_adjoint(
655 &self,
656 destination: ArrayViewMut2<'_, Complex64>,
657 source: usize,
658 update: &[Complex64],
659 scale: f64,
660 ) -> Result<()> {
661 let destination = crate::array_layout::StandardViewMut2::try_from(destination)?;
662 self.insert_patch_adjoint_standard(destination, source, update, scale)
663 }
664
665 pub(crate) fn insert_patch_adjoint_standard(
666 &self,
667 destination: crate::array_layout::StandardViewMut2<'_, Complex64>,
668 source: usize,
669 update: &[Complex64],
670 scale: f64,
671 ) -> Result<()> {
672 self.insert_patch_adjoint_at_offset(
673 destination,
674 source,
675 update,
676 scale,
677 self.source_offset(source)?,
678 )
679 }
680
681 pub(crate) fn insert_patch_adjoint_at_offset(
682 &self,
683 mut destination: crate::array_layout::StandardViewMut2<'_, Complex64>,
684 source: usize,
685 update: &[Complex64],
686 scale: f64,
687 offset: FourierOffset,
688 ) -> Result<()> {
689 if destination.dim() != self.reconstruction_shape {
690 return Err(Error::InvalidShape(format!(
691 "object spectrum shape {:?} does not match {:?}",
692 destination.dim(),
693 self.reconstruction_shape
694 )));
695 }
696 let crop = self.crop_indices.get(source)?;
697 crop.insert_subpixel_adjoint_slice(
698 destination.as_slice_mut(),
699 self.reconstruction_shape,
700 update,
701 scale,
702 offset,
703 )
704 }
705
706 pub(crate) fn insert_patch_adjoint_slice_at_offset(
707 &self,
708 destination: &mut [Complex64],
709 source: usize,
710 update: &[Complex64],
711 scale: f64,
712 offset: FourierOffset,
713 ) -> Result<()> {
714 let crop = self.crop_indices.get(source)?;
715 crop.insert_subpixel_adjoint_slice(
716 destination,
717 self.reconstruction_shape,
718 update,
719 scale,
720 offset,
721 )
722 }
723
724 pub fn validate_source_offset(&self, source: usize, offset: FourierOffset) -> Result<()> {
726 self.crop_indices
727 .get(source)?
728 .validate_subpixel_inside(self.reconstruction_shape, offset)
729 }
730
731 pub(crate) fn crop_for_k_vector(
732 vector: &KVector,
733 sampling: &Sampling,
734 image_shape: (usize, usize),
735 reconstruction_shape: (usize, usize),
736 ) -> Result<(FourierCrop, FourierOffset)> {
737 let continuous_row = vector.ky / sampling.dky;
738 let continuous_column = vector.kx / sampling.dkx;
739 let row_shift = checked_grid_shift(continuous_row)?;
740 let column_shift = checked_grid_shift(continuous_column)?;
741 let reconstruction_center_row = isize::try_from(reconstruction_shape.0 / 2)
742 .map_err(|_| Error::InvalidShape("reconstruction height is too large".into()))?;
743 let reconstruction_center_column = isize::try_from(reconstruction_shape.1 / 2)
744 .map_err(|_| Error::InvalidShape("reconstruction width is too large".into()))?;
745 let image_half_height = isize::try_from(image_shape.0 / 2)
746 .map_err(|_| Error::InvalidShape("image height is too large".into()))?;
747 let image_half_width = isize::try_from(image_shape.1 / 2)
748 .map_err(|_| Error::InvalidShape("image width is too large".into()))?;
749 let start_row = reconstruction_center_row
750 .checked_sub(image_half_height)
751 .and_then(|value| value.checked_add(row_shift.integer));
752 let start_column = reconstruction_center_column
753 .checked_sub(image_half_width)
754 .and_then(|value| value.checked_add(column_shift.integer));
755 let (Some(start_row), Some(start_column)) = (start_row, start_column) else {
756 return Err(Error::InvalidModel(format!(
757 "illumination vector {vector:?} overflows the reconstruction grid"
758 )));
759 };
760 if start_row < 0 || start_column < 0 {
761 return Err(Error::InvalidModel(format!(
762 "illumination vector {vector:?} produces a crop outside the reconstruction grid"
763 )));
764 }
765 let crop = FourierCrop::new(
766 usize::try_from(start_row).map_err(|_| {
767 Error::InvalidModel("crop row cannot be represented as an index".into())
768 })?,
769 usize::try_from(start_column).map_err(|_| {
770 Error::InvalidModel("crop column cannot be represented as an index".into())
771 })?,
772 image_shape.0,
773 image_shape.1,
774 );
775 let offset = FourierOffset::new(row_shift.offset, column_shift.offset);
776 crop.validate_subpixel_inside(reconstruction_shape, offset)?;
777 Ok((crop, offset))
778 }
779
780 pub fn frame_gain(&self, frame: usize) -> Result<f64> {
782 if frame >= self.frame_count() {
783 return Err(Error::FrameOutOfRange {
784 index: frame,
785 frames: self.frame_count(),
786 });
787 }
788 Ok(self
789 .frame_gains
790 .as_ref()
791 .map_or(1.0, |values| values[frame]))
792 }
793
794 pub fn background_value(&self, frame: usize, pixel: usize) -> Result<f64> {
798 if frame >= self.frame_count() {
799 return Err(Error::FrameOutOfRange {
800 index: frame,
801 frames: self.frame_count(),
802 });
803 }
804 let image_len = checked_len_2d(self.image_shape)?;
805 if pixel >= image_len {
806 return Err(Error::InvalidParameter {
807 name: "pixel",
808 reason: format!("index {pixel} is outside an image with {image_len} pixels"),
809 });
810 }
811 Ok(self.background.as_ref().map_or(0.0, |values| {
812 values[if values.len() == image_len {
813 pixel
814 } else {
815 frame * image_len + pixel
816 }]
817 }))
818 }
819
820 pub fn validate(&self) -> Result<()> {
823 self.sampling.validate()?;
824 if self.k_vectors.is_empty() || self.source_count() != self.crop_indices.len() {
825 return Err(Error::InvalidModel(format!(
826 "{} source k-vectors and {} crops; counts must be equal and non-zero",
827 self.source_count(),
828 self.crop_indices.len()
829 )));
830 }
831 if self
832 .k_vectors
833 .iter()
834 .any(|vector| !vector.kx.is_finite() || !vector.ky.is_finite())
835 {
836 return Err(Error::InvalidModel(
837 "k-vectors must contain finite values".into(),
838 ));
839 }
840 if self.pupil.shape() != self.image_shape {
841 return Err(Error::InvalidModel(format!(
842 "pupil shape {:?} does not match image shape {:?}",
843 self.pupil.shape(),
844 self.image_shape
845 )));
846 }
847 if self.pupil.support.len() != self.pupil.values.len()
848 || self
849 .pupil
850 .values
851 .as_slice()
852 .iter()
853 .any(|value| !value.re.is_finite() || !value.im.is_finite())
854 {
855 return Err(Error::InvalidModel(
856 "pupil support or numeric values are invalid".into(),
857 ));
858 }
859 if self.reconstruction_shape.0 < self.image_shape.0
860 || self.reconstruction_shape.1 < self.image_shape.1
861 {
862 return Err(Error::InvalidModel(
863 "reconstruction shape must contain a low-resolution crop".into(),
864 ));
865 }
866 for crop in &self.crop_indices.crops {
867 if (crop.height, crop.width) != self.image_shape {
868 return Err(Error::InvalidModel(format!(
869 "crop shape {:?} does not match image shape {:?}",
870 (crop.height, crop.width),
871 self.image_shape
872 )));
873 }
874 crop.validate_inside(self.reconstruction_shape)?;
875 }
876 if let Some(offsets) = &self.subpixel_offsets {
877 if offsets.len() != self.source_count() {
878 return Err(Error::InvalidModel(
879 "subpixel offset count does not match source count".into(),
880 ));
881 }
882 for (crop, &offset) in self.crop_indices.crops.iter().zip(offsets) {
883 crop.validate_subpixel_inside(self.reconstruction_shape, offset)?;
884 }
885 }
886 if let Some(values) = &self.frame_gains {
887 if values.len() != self.frame_count() {
888 return Err(Error::InvalidModel(
889 "frame gain count does not match frame count".into(),
890 ));
891 }
892 if values
893 .iter()
894 .any(|value| !value.is_finite() || *value < 0.0)
895 {
896 return Err(Error::InvalidModel(
897 "frame gains must be finite and non-negative".into(),
898 ));
899 }
900 }
901 let image_len = checked_len_2d(self.image_shape)?;
902 let stack_len =
903 image_len
904 .checked_mul(self.frame_count())
905 .ok_or_else(|| Error::ShapeOverflow {
906 shape: vec![self.frame_count(), self.image_shape.0, self.image_shape.1],
907 })?;
908 if self
909 .background
910 .as_ref()
911 .is_some_and(|values| values.len() != image_len && values.len() != stack_len)
912 {
913 return Err(Error::InvalidModel(
914 "background must be one image or one image per frame".into(),
915 ));
916 }
917 if self
918 .background
919 .as_ref()
920 .is_some_and(|values| values.iter().any(|value| !value.is_finite()))
921 {
922 return Err(Error::InvalidModel(
923 "background values must be finite".into(),
924 ));
925 }
926 if let Some(matrix) = &self.multiplexing_matrix {
927 if matrix.is_empty() {
928 return Err(Error::InvalidModel(
929 "multiplexing matrix must contain at least one measured frame".into(),
930 ));
931 }
932 for (row_index, row) in matrix.iter().enumerate() {
933 let mut seen = vec![false; self.source_count()];
934 let invalid = row.is_empty()
935 || row.iter().any(|&(source, weight)| {
936 let duplicate = source < self.source_count() && seen[source];
937 if source < self.source_count() {
938 seen[source] = true;
939 }
940 source >= self.source_count()
941 || !weight.is_finite()
942 || weight < 0.0
943 || duplicate
944 });
945 if invalid {
946 return Err(Error::InvalidModel(format!(
947 "multiplexing row {row_index} is empty or contains a duplicate/invalid source or weight"
948 )));
949 }
950 }
951 }
952 Ok(())
953 }
954}
955
956fn checked_grid_shift(value: f64) -> Result<GridShift> {
957 if !value.is_finite() {
958 return Err(Error::InvalidModel(
959 "illumination shift must be finite".into(),
960 ));
961 }
962 let rounded = value.round();
963 if rounded < isize::MIN as f64 || rounded > isize::MAX as f64 {
964 return Err(Error::InvalidModel(
965 "illumination shift is outside the supported index range".into(),
966 ));
967 }
968 let integer = rounded as isize;
969 let offset = value - rounded;
970 let nearest_offset = offset.round();
971 let (lower, upper) = if (offset - nearest_offset).abs() <= 1e-12 {
972 let displacement = integer
973 .checked_add(nearest_offset as isize)
974 .ok_or_else(|| {
975 Error::InvalidModel(
976 "illumination shift is outside the supported index range".into(),
977 )
978 })?;
979 (displacement, displacement)
980 } else if offset > 0.0 {
981 (
982 integer,
983 integer.checked_add(1).ok_or_else(|| {
984 Error::InvalidModel(
985 "illumination shift is outside the supported index range".into(),
986 )
987 })?,
988 )
989 } else {
990 (
991 integer.checked_sub(1).ok_or_else(|| {
992 Error::InvalidModel(
993 "illumination shift is outside the supported index range".into(),
994 )
995 })?,
996 integer,
997 )
998 };
999 Ok(GridShift {
1000 integer,
1001 offset,
1002 lower,
1003 upper,
1004 })
1005}
1006
1007fn validate_image_shape(image_shape: (usize, usize)) -> Result<()> {
1008 if image_shape.0 == 0 || image_shape.1 == 0 {
1009 return Err(Error::InvalidShape(format!(
1010 "image shape {image_shape:?} must be non-zero"
1011 )));
1012 }
1013 if image_shape.0 > isize::MAX as usize || image_shape.1 > isize::MAX as usize {
1014 return Err(Error::InvalidShape(
1015 "image shape is outside the supported index range".into(),
1016 ));
1017 }
1018 Ok(())
1019}
1020
1021fn validate_reconstruction_aspect(
1022 image_shape: (usize, usize),
1023 reconstruction_shape: (usize, usize),
1024) -> Result<()> {
1025 if reconstruction_shape.0 < image_shape.0 || reconstruction_shape.1 < image_shape.1 {
1026 return Err(Error::InvalidShape(format!(
1027 "image shape {image_shape:?} must fit reconstruction shape {reconstruction_shape:?}"
1028 )));
1029 }
1030 if reconstruction_shape.0 > isize::MAX as usize || reconstruction_shape.1 > isize::MAX as usize
1031 {
1032 return Err(Error::InvalidShape(
1033 "reconstruction shape is outside the supported index range".into(),
1034 ));
1035 }
1036 let left = reconstruction_shape.0 as u128 * image_shape.1 as u128;
1037 let right = reconstruction_shape.1 as u128 * image_shape.0 as u128;
1038 if left != right {
1039 return Err(Error::InvalidShape(
1040 "reconstruction must use the same scale factor in both dimensions".into(),
1041 ));
1042 }
1043 Ok(())
1044}
1045
1046fn shape_contains_bounds(
1047 image_shape: (usize, usize),
1048 reconstruction_shape: (usize, usize),
1049 bounds: &[CropDisplacementBounds],
1050) -> Result<bool> {
1051 validate_reconstruction_aspect(image_shape, reconstruction_shape)?;
1052 for bound in bounds {
1053 if !axis_contains_bounds(
1054 image_shape.0,
1055 reconstruction_shape.0,
1056 bound.minimum_row,
1057 bound.maximum_row,
1058 )? || !axis_contains_bounds(
1059 image_shape.1,
1060 reconstruction_shape.1,
1061 bound.minimum_column,
1062 bound.maximum_column,
1063 )? {
1064 return Ok(false);
1065 }
1066 }
1067 Ok(true)
1068}
1069
1070fn axis_contains_bounds(
1071 image_length: usize,
1072 reconstruction_length: usize,
1073 minimum_displacement: isize,
1074 maximum_displacement: isize,
1075) -> Result<bool> {
1076 let centre = i128::try_from(reconstruction_length / 2)
1077 .map_err(|_| Error::InvalidShape("reconstruction dimension is too large".into()))?;
1078 let image_half = i128::try_from(image_length / 2)
1079 .map_err(|_| Error::InvalidShape("image dimension is too large".into()))?;
1080 let base = centre.checked_sub(image_half).ok_or_else(|| {
1081 Error::InvalidShape("reconstruction crop origin overflows the index range".into())
1082 })?;
1083 let lower = base
1084 .checked_add(minimum_displacement as i128)
1085 .ok_or_else(|| {
1086 Error::InvalidShape("reconstruction crop origin overflows the index range".into())
1087 })?;
1088 let upper = base
1089 .checked_add((image_length - 1) as i128)
1090 .and_then(|value| value.checked_add(maximum_displacement as i128))
1091 .ok_or_else(|| {
1092 Error::InvalidShape("reconstruction crop extent overflows the index range".into())
1093 })?;
1094 Ok(lower >= 0 && upper < reconstruction_length as i128)
1095}
1096
1097fn minimum_fitting_multiplier(
1098 image_shape: (usize, usize),
1099 aspect: (usize, usize),
1100 initial_multiplier: usize,
1101 maximum_multiplier: usize,
1102 bounds: &[CropDisplacementBounds],
1103) -> Result<usize> {
1104 let fits = |multiplier| -> Result<bool> {
1105 let shape = candidate_shape(aspect, multiplier)?;
1106 shape_contains_bounds(image_shape, shape, bounds)
1107 };
1108 if fits(initial_multiplier)? {
1109 return Ok(initial_multiplier);
1110 }
1111 let mut lower = initial_multiplier;
1112 let mut upper = initial_multiplier;
1113 loop {
1114 let doubled = upper.checked_mul(2).unwrap_or(maximum_multiplier);
1115 upper = doubled.min(maximum_multiplier);
1116 if upper == lower {
1117 return Err(Error::InvalidShape(
1118 "illumination crops require a reconstruction shape outside the supported index range"
1119 .into(),
1120 ));
1121 }
1122 if fits(upper)? {
1123 break;
1124 }
1125 lower = upper;
1126 }
1127 while lower + 1 < upper {
1128 let middle = lower + (upper - lower) / 2;
1129 if fits(middle)? {
1130 upper = middle;
1131 } else {
1132 lower = middle;
1133 }
1134 }
1135 Ok(upper)
1136}
1137
1138fn candidate_shape(aspect: (usize, usize), multiplier: usize) -> Result<(usize, usize)> {
1139 let height = aspect.0.checked_mul(multiplier).ok_or_else(|| {
1140 Error::InvalidShape("reconstruction height overflows the supported range".into())
1141 })?;
1142 let width = aspect.1.checked_mul(multiplier).ok_or_else(|| {
1143 Error::InvalidShape("reconstruction width overflows the supported range".into())
1144 })?;
1145 Ok((height, width))
1146}
1147
1148fn greatest_common_divisor(mut left: usize, mut right: usize) -> usize {
1149 while right != 0 {
1150 let remainder = left % right;
1151 left = right;
1152 right = remainder;
1153 }
1154 left
1155}
1156
1157fn next_smooth_multiplier(minimum: usize) -> Option<usize> {
1158 fn visit(value: usize, prime_index: usize, minimum: usize, best: &mut Option<usize>) {
1159 if value >= minimum {
1160 if best.is_none_or(|current| value < current) {
1161 *best = Some(value);
1162 }
1163 return;
1164 }
1165 const PRIMES: [usize; 4] = [2, 3, 5, 7];
1166 for (index, &prime) in PRIMES.iter().enumerate().skip(prime_index) {
1167 let Some(next) = value.checked_mul(prime) else {
1168 continue;
1169 };
1170 if best.is_none_or(|current| next < current) {
1171 visit(next, index, minimum, best);
1172 }
1173 }
1174 }
1175
1176 let mut best = None;
1177 visit(1, 0, minimum, &mut best);
1178 best
1179}