1use num_complex::Complex64;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 Array2, Result,
6 error::Error,
7 experiment::{IlluminationSource, KVector, MultiplexingMatrix, Optics},
8};
9
10use super::{CropIndices, FourierCrop, FourierOffset, Pupil, Sampling};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum ReconstructionShape {
20 Exact((usize, usize)),
22 Minimum,
25 Smooth,
28 PowerOfTwo,
30}
31
32#[derive(Clone, Copy, Debug)]
33struct GridShift {
34 integer: isize,
35 offset: f64,
36 lower: isize,
37 upper: isize,
38}
39
40#[derive(Clone, Copy, Debug)]
41pub(crate) struct CropDisplacementBounds {
42 minimum_row: isize,
43 maximum_row: isize,
44 minimum_column: isize,
45 maximum_column: isize,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct ImagePlaneModel {
51 pub k_vectors: Vec<KVector>,
53 pub pupil: Pupil,
54 pub crop_indices: CropIndices,
55 #[serde(default)]
57 pub subpixel_offsets: Option<Vec<FourierOffset>>,
58 pub sampling: Sampling,
59 pub image_shape: (usize, usize),
60 pub reconstruction_shape: (usize, usize),
61 pub frame_gains: Option<Vec<f64>>,
62 pub background: Option<Vec<f64>>,
63 pub multiplexing_matrix: Option<MultiplexingMatrix>,
65}
66
67impl ImagePlaneModel {
68 pub fn new(
69 k_vectors: Vec<KVector>,
70 pupil: Pupil,
71 crop_indices: CropIndices,
72 sampling: Sampling,
73 image_shape: (usize, usize),
74 reconstruction_shape: (usize, usize),
75 ) -> Result<Self> {
76 let model = Self {
77 k_vectors,
78 pupil,
79 crop_indices,
80 subpixel_offsets: None,
81 sampling,
82 image_shape,
83 reconstruction_shape,
84 frame_gains: None,
85 background: None,
86 multiplexing_matrix: None,
87 };
88 model.validate()?;
89 Ok(model)
90 }
91
92 pub fn from_experiment<I: IlluminationSource>(
95 optics: &Optics,
96 illumination: &I,
97 image_shape: (usize, usize),
98 reconstruction_shape: ReconstructionShape,
99 ) -> Result<Self> {
100 optics.validate()?;
101 let k_vectors = illumination.k_vectors(optics)?;
102 Self::from_experiment_with_k_vectors(
103 optics,
104 illumination,
105 k_vectors,
106 image_shape,
107 reconstruction_shape,
108 )
109 }
110
111 pub fn suggest_reconstruction_shape<I: IlluminationSource>(
114 optics: &Optics,
115 illumination: &I,
116 image_shape: (usize, usize),
117 reconstruction_shape: ReconstructionShape,
118 ) -> Result<(usize, usize)> {
119 optics.validate()?;
120 let k_vectors = illumination.k_vectors(optics)?;
121 let bounds = Self::crop_displacement_bounds(optics, image_shape, &k_vectors)?;
122 Self::resolve_reconstruction_shape(image_shape, reconstruction_shape, &[bounds])
123 }
124
125 pub(crate) fn from_experiment_with_k_vectors<I: IlluminationSource>(
126 optics: &Optics,
127 illumination: &I,
128 k_vectors: Vec<KVector>,
129 image_shape: (usize, usize),
130 reconstruction_shape: ReconstructionShape,
131 ) -> Result<Self> {
132 optics.validate()?;
133 let bounds = Self::crop_displacement_bounds(optics, image_shape, &k_vectors)?;
134 let reconstruction_shape =
135 Self::resolve_reconstruction_shape(image_shape, reconstruction_shape, &[bounds])?;
136 let scale = reconstruction_shape.1 as f64 / image_shape.1 as f64;
137 let low_res_pixel_size = optics.object_pixel_size();
138 let mut sampling = Sampling::new(
139 low_res_pixel_size,
140 low_res_pixel_size / scale,
141 std::f64::consts::TAU / (image_shape.1 as f64 * low_res_pixel_size),
142 std::f64::consts::TAU / (image_shape.0 as f64 * low_res_pixel_size),
143 )?;
144 sampling.wavelength = Some(optics.wavelength);
145 let maximum_illumination_na = k_vectors
146 .iter()
147 .map(|vector| vector.kx.hypot(vector.ky) * optics.wavelength / std::f64::consts::TAU)
148 .fold(0.0, f64::max);
149 sampling.synthetic_na = Some(optics.objective_na + maximum_illumination_na);
150 let pupil = Pupil::circular(image_shape, &sampling, optics)?;
151 let mut offsets = Vec::with_capacity(k_vectors.len());
152 let crops = k_vectors
153 .iter()
154 .map(|vector| {
155 let (crop, offset) =
156 Self::crop_for_k_vector(vector, &sampling, image_shape, reconstruction_shape)?;
157 offsets.push(offset);
158 Ok(crop)
159 })
160 .collect::<Result<Vec<_>>>()?;
161 let mut model = Self::new(
162 k_vectors,
163 pupil,
164 CropIndices::new(crops),
165 sampling,
166 image_shape,
167 reconstruction_shape,
168 )?;
169 model.frame_gains = illumination.frame_gains()?;
170 model.multiplexing_matrix = illumination.multiplexing_matrix()?;
171 model.subpixel_offsets = Some(offsets);
172 model.validate()?;
173 Ok(model)
174 }
175
176 pub(crate) fn crop_displacement_bounds(
177 optics: &Optics,
178 image_shape: (usize, usize),
179 k_vectors: &[KVector],
180 ) -> Result<CropDisplacementBounds> {
181 optics.validate()?;
182 validate_image_shape(image_shape)?;
183 if k_vectors.is_empty() {
184 return Err(Error::InvalidModel(
185 "illumination must contain at least one frame".into(),
186 ));
187 }
188 let low_res_pixel_size = optics.object_pixel_size();
189 let dkx = std::f64::consts::TAU / (image_shape.1 as f64 * low_res_pixel_size);
190 let dky = std::f64::consts::TAU / (image_shape.0 as f64 * low_res_pixel_size);
191 let mut bounds = CropDisplacementBounds {
192 minimum_row: isize::MAX,
193 maximum_row: isize::MIN,
194 minimum_column: isize::MAX,
195 maximum_column: isize::MIN,
196 };
197 for vector in k_vectors {
198 let row = checked_grid_shift(vector.ky / dky)?;
199 let column = checked_grid_shift(vector.kx / dkx)?;
200 bounds.minimum_row = bounds.minimum_row.min(row.lower);
201 bounds.maximum_row = bounds.maximum_row.max(row.upper);
202 bounds.minimum_column = bounds.minimum_column.min(column.lower);
203 bounds.maximum_column = bounds.maximum_column.max(column.upper);
204 }
205 Ok(bounds)
206 }
207
208 pub(crate) fn resolve_reconstruction_shape(
209 image_shape: (usize, usize),
210 reconstruction_shape: ReconstructionShape,
211 bounds: &[CropDisplacementBounds],
212 ) -> Result<(usize, usize)> {
213 validate_image_shape(image_shape)?;
214 if bounds.is_empty() {
215 return Err(Error::InvalidModel(
216 "at least one set of illumination crop bounds is required".into(),
217 ));
218 }
219 match reconstruction_shape {
220 ReconstructionShape::Exact(shape) => {
221 validate_reconstruction_aspect(image_shape, shape)?;
222 if !shape_contains_bounds(image_shape, shape, bounds)? {
223 return Err(Error::InvalidShape(format!(
224 "reconstruction shape {shape:?} does not contain every illumination crop and subpixel interpolation stencil"
225 )));
226 }
227 Ok(shape)
228 }
229 policy => {
230 let divisor = greatest_common_divisor(image_shape.0, image_shape.1);
231 let aspect_height = image_shape.0 / divisor;
232 let aspect_width = image_shape.1 / divisor;
233 let maximum_multiplier =
234 (isize::MAX as usize / aspect_height).min(isize::MAX as usize / aspect_width);
235 let minimum_multiplier = minimum_fitting_multiplier(
236 image_shape,
237 (aspect_height, aspect_width),
238 divisor,
239 maximum_multiplier,
240 bounds,
241 )?;
242 let multiplier = match policy {
243 ReconstructionShape::Minimum => minimum_multiplier,
244 ReconstructionShape::Smooth => next_smooth_multiplier(minimum_multiplier)
245 .filter(|&value| value <= maximum_multiplier)
246 .ok_or_else(|| {
247 Error::InvalidShape(
248 "no supported 2/3/5/7-smooth reconstruction multiplier exists"
249 .into(),
250 )
251 })?,
252 ReconstructionShape::PowerOfTwo => minimum_multiplier
253 .checked_next_power_of_two()
254 .filter(|&value| value <= maximum_multiplier)
255 .ok_or_else(|| {
256 Error::InvalidShape(
257 "no supported power-of-two reconstruction multiplier exists".into(),
258 )
259 })?,
260 ReconstructionShape::Exact(_) => unreachable!(),
261 };
262 candidate_shape((aspect_height, aspect_width), multiplier)
263 }
264 }
265 }
266
267 pub fn source_count(&self) -> usize {
268 self.k_vectors.len()
269 }
270
271 pub fn frame_count(&self) -> usize {
272 self.multiplexing_matrix
273 .as_ref()
274 .map_or_else(|| self.source_count(), Vec::len)
275 }
276
277 pub fn is_multiplexed(&self) -> bool {
278 self.multiplexing_matrix.is_some()
279 }
280
281 pub fn with_multiplexing(mut self, matrix: MultiplexingMatrix) -> Result<Self> {
282 self.multiplexing_matrix = Some(matrix);
283 self.validate()?;
284 Ok(self)
285 }
286
287 pub fn with_subpixel_offsets(mut self, offsets: Vec<FourierOffset>) -> Result<Self> {
288 self.subpixel_offsets = Some(offsets);
289 self.validate()?;
290 Ok(self)
291 }
292
293 pub fn source_offset(&self, source: usize) -> Result<FourierOffset> {
294 self.crop_indices.get(source)?;
295 match &self.subpixel_offsets {
296 None => Ok(FourierOffset::default()),
297 Some(offsets) => offsets.get(source).copied().ok_or_else(|| {
298 Error::InvalidModel("subpixel offset count does not match source count".into())
299 }),
300 }
301 }
302
303 pub fn extract_patch(
304 &self,
305 object_spectrum: &Array2<Complex64>,
306 source: usize,
307 destination: &mut [Complex64],
308 ) -> Result<()> {
309 self.extract_patch_at_offset(
310 object_spectrum,
311 source,
312 self.source_offset(source)?,
313 destination,
314 )
315 }
316
317 pub fn extract_patch_at_offset(
318 &self,
319 object_spectrum: &Array2<Complex64>,
320 source: usize,
321 offset: FourierOffset,
322 destination: &mut [Complex64],
323 ) -> Result<()> {
324 if object_spectrum.shape() != self.reconstruction_shape {
325 return Err(Error::InvalidShape(format!(
326 "object spectrum shape {:?} does not match {:?}",
327 object_spectrum.shape(),
328 self.reconstruction_shape
329 )));
330 }
331 let crop = self.crop_indices.get(source)?;
332 crop.extract_subpixel(object_spectrum, destination, offset)
333 }
334
335 pub fn insert_patch_adjoint(
336 &self,
337 destination: &mut Array2<Complex64>,
338 source: usize,
339 update: &[Complex64],
340 scale: f64,
341 ) -> Result<()> {
342 self.insert_patch_adjoint_at_offset(
343 destination,
344 source,
345 update,
346 scale,
347 self.source_offset(source)?,
348 )
349 }
350
351 pub fn insert_patch_adjoint_at_offset(
352 &self,
353 destination: &mut Array2<Complex64>,
354 source: usize,
355 update: &[Complex64],
356 scale: f64,
357 offset: FourierOffset,
358 ) -> Result<()> {
359 if destination.shape() != self.reconstruction_shape {
360 return Err(Error::InvalidShape(format!(
361 "object spectrum shape {:?} does not match {:?}",
362 destination.shape(),
363 self.reconstruction_shape
364 )));
365 }
366 let crop = self.crop_indices.get(source)?;
367 crop.insert_subpixel_adjoint(destination, update, scale, offset)
368 }
369
370 pub(crate) fn insert_patch_adjoint_slice_at_offset(
371 &self,
372 destination: &mut [Complex64],
373 source: usize,
374 update: &[Complex64],
375 scale: f64,
376 offset: FourierOffset,
377 ) -> Result<()> {
378 let crop = self.crop_indices.get(source)?;
379 crop.insert_subpixel_adjoint_slice(
380 destination,
381 self.reconstruction_shape,
382 update,
383 scale,
384 offset,
385 )
386 }
387
388 pub fn validate_source_offset(&self, source: usize, offset: FourierOffset) -> Result<()> {
389 self.crop_indices
390 .get(source)?
391 .validate_subpixel_inside(self.reconstruction_shape, offset)
392 }
393
394 pub(crate) fn crop_for_k_vector(
395 vector: &KVector,
396 sampling: &Sampling,
397 image_shape: (usize, usize),
398 reconstruction_shape: (usize, usize),
399 ) -> Result<(FourierCrop, FourierOffset)> {
400 let continuous_row = vector.ky / sampling.dky;
401 let continuous_column = vector.kx / sampling.dkx;
402 let row_shift = checked_grid_shift(continuous_row)?;
403 let column_shift = checked_grid_shift(continuous_column)?;
404 let reconstruction_center_row = isize::try_from(reconstruction_shape.0 / 2)
405 .map_err(|_| Error::InvalidShape("reconstruction height is too large".into()))?;
406 let reconstruction_center_column = isize::try_from(reconstruction_shape.1 / 2)
407 .map_err(|_| Error::InvalidShape("reconstruction width is too large".into()))?;
408 let image_half_height = isize::try_from(image_shape.0 / 2)
409 .map_err(|_| Error::InvalidShape("image height is too large".into()))?;
410 let image_half_width = isize::try_from(image_shape.1 / 2)
411 .map_err(|_| Error::InvalidShape("image width is too large".into()))?;
412 let start_row = reconstruction_center_row
413 .checked_sub(image_half_height)
414 .and_then(|value| value.checked_add(row_shift.integer));
415 let start_column = reconstruction_center_column
416 .checked_sub(image_half_width)
417 .and_then(|value| value.checked_add(column_shift.integer));
418 let (Some(start_row), Some(start_column)) = (start_row, start_column) else {
419 return Err(Error::InvalidModel(format!(
420 "illumination vector {vector:?} overflows the reconstruction grid"
421 )));
422 };
423 if start_row < 0 || start_column < 0 {
424 return Err(Error::InvalidModel(format!(
425 "illumination vector {vector:?} produces a crop outside the reconstruction grid"
426 )));
427 }
428 let crop = FourierCrop::new(
429 usize::try_from(start_row).map_err(|_| {
430 Error::InvalidModel("crop row cannot be represented as an index".into())
431 })?,
432 usize::try_from(start_column).map_err(|_| {
433 Error::InvalidModel("crop column cannot be represented as an index".into())
434 })?,
435 image_shape.0,
436 image_shape.1,
437 );
438 let offset = FourierOffset::new(row_shift.offset, column_shift.offset);
439 crop.validate_subpixel_inside(reconstruction_shape, offset)?;
440 Ok((crop, offset))
441 }
442
443 pub fn frame_gain(&self, frame: usize) -> Result<f64> {
444 if frame >= self.frame_count() {
445 return Err(Error::FrameOutOfRange {
446 index: frame,
447 frames: self.frame_count(),
448 });
449 }
450 Ok(self
451 .frame_gains
452 .as_ref()
453 .map_or(1.0, |values| values[frame]))
454 }
455
456 pub fn background_value(&self, frame: usize, pixel: usize) -> Result<f64> {
457 if frame >= self.frame_count() {
458 return Err(Error::FrameOutOfRange {
459 index: frame,
460 frames: self.frame_count(),
461 });
462 }
463 let image_len = self.image_shape.0 * self.image_shape.1;
464 if pixel >= image_len {
465 return Err(Error::InvalidParameter {
466 name: "pixel",
467 reason: format!("index {pixel} is outside an image with {image_len} pixels"),
468 });
469 }
470 Ok(self.background.as_ref().map_or(0.0, |values| {
471 values[if values.len() == image_len {
472 pixel
473 } else {
474 frame * image_len + pixel
475 }]
476 }))
477 }
478
479 pub fn validate(&self) -> Result<()> {
480 self.sampling.validate()?;
481 if self.k_vectors.is_empty() || self.source_count() != self.crop_indices.len() {
482 return Err(Error::InvalidModel(format!(
483 "{} source k-vectors and {} crops; counts must be equal and non-zero",
484 self.source_count(),
485 self.crop_indices.len()
486 )));
487 }
488 if self
489 .k_vectors
490 .iter()
491 .any(|vector| !vector.kx.is_finite() || !vector.ky.is_finite())
492 {
493 return Err(Error::InvalidModel(
494 "k-vectors must contain finite values".into(),
495 ));
496 }
497 if self.pupil.shape() != self.image_shape {
498 return Err(Error::InvalidModel(format!(
499 "pupil shape {:?} does not match image shape {:?}",
500 self.pupil.shape(),
501 self.image_shape
502 )));
503 }
504 if self.pupil.support.len() != self.pupil.values.len()
505 || self
506 .pupil
507 .values
508 .as_slice()
509 .iter()
510 .any(|value| !value.re.is_finite() || !value.im.is_finite())
511 {
512 return Err(Error::InvalidModel(
513 "pupil support or numeric values are invalid".into(),
514 ));
515 }
516 if self.reconstruction_shape.0 < self.image_shape.0
517 || self.reconstruction_shape.1 < self.image_shape.1
518 {
519 return Err(Error::InvalidModel(
520 "reconstruction shape must contain a low-resolution crop".into(),
521 ));
522 }
523 for crop in &self.crop_indices.crops {
524 if (crop.height, crop.width) != self.image_shape {
525 return Err(Error::InvalidModel(format!(
526 "crop shape {:?} does not match image shape {:?}",
527 (crop.height, crop.width),
528 self.image_shape
529 )));
530 }
531 crop.validate_inside(self.reconstruction_shape)?;
532 }
533 if let Some(offsets) = &self.subpixel_offsets {
534 if offsets.len() != self.source_count() {
535 return Err(Error::InvalidModel(
536 "subpixel offset count does not match source count".into(),
537 ));
538 }
539 for (crop, &offset) in self.crop_indices.crops.iter().zip(offsets) {
540 crop.validate_subpixel_inside(self.reconstruction_shape, offset)?;
541 }
542 }
543 if let Some(values) = &self.frame_gains {
544 if values.len() != self.frame_count() {
545 return Err(Error::InvalidModel(
546 "frame gain count does not match frame count".into(),
547 ));
548 }
549 if values
550 .iter()
551 .any(|value| !value.is_finite() || *value <= 0.0)
552 {
553 return Err(Error::InvalidModel(
554 "frame gains must be finite and positive".into(),
555 ));
556 }
557 }
558 let image_len = self.image_shape.0 * self.image_shape.1;
559 if self.background.as_ref().is_some_and(|values| {
560 values.len() != image_len && values.len() != image_len * self.frame_count()
561 }) {
562 return Err(Error::InvalidModel(
563 "background must be one image or one image per frame".into(),
564 ));
565 }
566 if self
567 .background
568 .as_ref()
569 .is_some_and(|values| values.iter().any(|value| !value.is_finite()))
570 {
571 return Err(Error::InvalidModel(
572 "background values must be finite".into(),
573 ));
574 }
575 if let Some(matrix) = &self.multiplexing_matrix {
576 if matrix.is_empty() {
577 return Err(Error::InvalidModel(
578 "multiplexing matrix must contain at least one measured frame".into(),
579 ));
580 }
581 for (row_index, row) in matrix.iter().enumerate() {
582 let mut seen = vec![false; self.source_count()];
583 let invalid = row.is_empty()
584 || row.iter().any(|&(source, weight)| {
585 let duplicate = source < self.source_count() && seen[source];
586 if source < self.source_count() {
587 seen[source] = true;
588 }
589 source >= self.source_count()
590 || !weight.is_finite()
591 || weight <= 0.0
592 || duplicate
593 });
594 if invalid {
595 return Err(Error::InvalidModel(format!(
596 "multiplexing row {row_index} is empty or contains a duplicate/invalid source or weight"
597 )));
598 }
599 }
600 }
601 Ok(())
602 }
603}
604
605fn checked_grid_shift(value: f64) -> Result<GridShift> {
606 if !value.is_finite() {
607 return Err(Error::InvalidModel(
608 "illumination shift must be finite".into(),
609 ));
610 }
611 let rounded = value.round();
612 if rounded < isize::MIN as f64 || rounded > isize::MAX as f64 {
613 return Err(Error::InvalidModel(
614 "illumination shift is outside the supported index range".into(),
615 ));
616 }
617 let integer = rounded as isize;
618 let offset = value - rounded;
619 let nearest_offset = offset.round();
620 let (lower, upper) = if (offset - nearest_offset).abs() <= 1e-12 {
621 let displacement = integer
622 .checked_add(nearest_offset as isize)
623 .ok_or_else(|| {
624 Error::InvalidModel(
625 "illumination shift is outside the supported index range".into(),
626 )
627 })?;
628 (displacement, displacement)
629 } else if offset > 0.0 {
630 (
631 integer,
632 integer.checked_add(1).ok_or_else(|| {
633 Error::InvalidModel(
634 "illumination shift is outside the supported index range".into(),
635 )
636 })?,
637 )
638 } else {
639 (
640 integer.checked_sub(1).ok_or_else(|| {
641 Error::InvalidModel(
642 "illumination shift is outside the supported index range".into(),
643 )
644 })?,
645 integer,
646 )
647 };
648 Ok(GridShift {
649 integer,
650 offset,
651 lower,
652 upper,
653 })
654}
655
656fn validate_image_shape(image_shape: (usize, usize)) -> Result<()> {
657 if image_shape.0 == 0 || image_shape.1 == 0 {
658 return Err(Error::InvalidShape(format!(
659 "image shape {image_shape:?} must be non-zero"
660 )));
661 }
662 if image_shape.0 > isize::MAX as usize || image_shape.1 > isize::MAX as usize {
663 return Err(Error::InvalidShape(
664 "image shape is outside the supported index range".into(),
665 ));
666 }
667 Ok(())
668}
669
670fn validate_reconstruction_aspect(
671 image_shape: (usize, usize),
672 reconstruction_shape: (usize, usize),
673) -> Result<()> {
674 if reconstruction_shape.0 < image_shape.0 || reconstruction_shape.1 < image_shape.1 {
675 return Err(Error::InvalidShape(format!(
676 "image shape {image_shape:?} must fit reconstruction shape {reconstruction_shape:?}"
677 )));
678 }
679 if reconstruction_shape.0 > isize::MAX as usize || reconstruction_shape.1 > isize::MAX as usize
680 {
681 return Err(Error::InvalidShape(
682 "reconstruction shape is outside the supported index range".into(),
683 ));
684 }
685 let left = reconstruction_shape.0 as u128 * image_shape.1 as u128;
686 let right = reconstruction_shape.1 as u128 * image_shape.0 as u128;
687 if left != right {
688 return Err(Error::InvalidShape(
689 "reconstruction must use the same scale factor in both dimensions".into(),
690 ));
691 }
692 Ok(())
693}
694
695fn shape_contains_bounds(
696 image_shape: (usize, usize),
697 reconstruction_shape: (usize, usize),
698 bounds: &[CropDisplacementBounds],
699) -> Result<bool> {
700 validate_reconstruction_aspect(image_shape, reconstruction_shape)?;
701 for bound in bounds {
702 if !axis_contains_bounds(
703 image_shape.0,
704 reconstruction_shape.0,
705 bound.minimum_row,
706 bound.maximum_row,
707 )? || !axis_contains_bounds(
708 image_shape.1,
709 reconstruction_shape.1,
710 bound.minimum_column,
711 bound.maximum_column,
712 )? {
713 return Ok(false);
714 }
715 }
716 Ok(true)
717}
718
719fn axis_contains_bounds(
720 image_length: usize,
721 reconstruction_length: usize,
722 minimum_displacement: isize,
723 maximum_displacement: isize,
724) -> Result<bool> {
725 let centre = i128::try_from(reconstruction_length / 2)
726 .map_err(|_| Error::InvalidShape("reconstruction dimension is too large".into()))?;
727 let image_half = i128::try_from(image_length / 2)
728 .map_err(|_| Error::InvalidShape("image dimension is too large".into()))?;
729 let base = centre.checked_sub(image_half).ok_or_else(|| {
730 Error::InvalidShape("reconstruction crop origin overflows the index range".into())
731 })?;
732 let lower = base
733 .checked_add(minimum_displacement as i128)
734 .ok_or_else(|| {
735 Error::InvalidShape("reconstruction crop origin overflows the index range".into())
736 })?;
737 let upper = base
738 .checked_add((image_length - 1) as i128)
739 .and_then(|value| value.checked_add(maximum_displacement as i128))
740 .ok_or_else(|| {
741 Error::InvalidShape("reconstruction crop extent overflows the index range".into())
742 })?;
743 Ok(lower >= 0 && upper < reconstruction_length as i128)
744}
745
746fn minimum_fitting_multiplier(
747 image_shape: (usize, usize),
748 aspect: (usize, usize),
749 initial_multiplier: usize,
750 maximum_multiplier: usize,
751 bounds: &[CropDisplacementBounds],
752) -> Result<usize> {
753 let fits = |multiplier| -> Result<bool> {
754 let shape = candidate_shape(aspect, multiplier)?;
755 shape_contains_bounds(image_shape, shape, bounds)
756 };
757 if fits(initial_multiplier)? {
758 return Ok(initial_multiplier);
759 }
760 let mut lower = initial_multiplier;
761 let mut upper = initial_multiplier;
762 loop {
763 let doubled = upper.checked_mul(2).unwrap_or(maximum_multiplier);
764 upper = doubled.min(maximum_multiplier);
765 if upper == lower {
766 return Err(Error::InvalidShape(
767 "illumination crops require a reconstruction shape outside the supported index range"
768 .into(),
769 ));
770 }
771 if fits(upper)? {
772 break;
773 }
774 lower = upper;
775 }
776 while lower + 1 < upper {
777 let middle = lower + (upper - lower) / 2;
778 if fits(middle)? {
779 upper = middle;
780 } else {
781 lower = middle;
782 }
783 }
784 Ok(upper)
785}
786
787fn candidate_shape(aspect: (usize, usize), multiplier: usize) -> Result<(usize, usize)> {
788 let height = aspect.0.checked_mul(multiplier).ok_or_else(|| {
789 Error::InvalidShape("reconstruction height overflows the supported range".into())
790 })?;
791 let width = aspect.1.checked_mul(multiplier).ok_or_else(|| {
792 Error::InvalidShape("reconstruction width overflows the supported range".into())
793 })?;
794 Ok((height, width))
795}
796
797fn greatest_common_divisor(mut left: usize, mut right: usize) -> usize {
798 while right != 0 {
799 let remainder = left % right;
800 left = right;
801 right = remainder;
802 }
803 left
804}
805
806fn next_smooth_multiplier(minimum: usize) -> Option<usize> {
807 fn visit(value: usize, prime_index: usize, minimum: usize, best: &mut Option<usize>) {
808 if value >= minimum {
809 if best.is_none_or(|current| value < current) {
810 *best = Some(value);
811 }
812 return;
813 }
814 const PRIMES: [usize; 4] = [2, 3, 5, 7];
815 for (index, &prime) in PRIMES.iter().enumerate().skip(prime_index) {
816 let Some(next) = value.checked_mul(prime) else {
817 continue;
818 };
819 if best.is_none_or(|current| next < current) {
820 visit(next, index, minimum, best);
821 }
822 }
823 }
824
825 let mut best = None;
826 visit(1, 0, minimum, &mut best);
827 best
828}