1use num_complex::Complex64;
2use std::thread;
3
4use crate::{
5 Result,
6 algorithms::objective::{LossType, point_loss},
7 backend::FftDirection,
8 diagnostics::StepDiagnostics,
9 error::Error,
10 measurements::MeasurementRead,
11 model::{FourierOffset, fftshift_copy, ifftshift_copy},
12 reconstruction::{Batch, ReconstructionProblem, ReconstructionState},
13};
14
15use super::{
16 ReconstructionAlgorithm,
17 regularization::{apply_complex_tv_step, apply_quadratic_smoothing_step},
18};
19
20#[derive(Clone, Debug)]
45pub struct GradientDescent {
46 pub iterations: usize,
48 pub object_step: f64,
50 pub batch_size: usize,
52 pub epsilon: f64,
54 pub loss_type: LossType,
56 pub recover_illumination: bool,
58 pub illumination_step: f64,
60 pub illumination_finite_difference: f64,
62 pub maximum_illumination_correction: f64,
64 pub recover_pupil: bool,
66 pub pupil_step: f64,
68 pub constrain_pupil_support: bool,
70 pub object_tv_weight: f64,
73 pub object_tv_epsilon: f64,
75 pub pupil_smoothing_weight: f64,
78 pub parallel_workers: usize,
80}
81
82impl Default for GradientDescent {
83 fn default() -> Self {
84 Self {
85 iterations: 100,
86 object_step: 0.5,
87 batch_size: 1,
88 epsilon: 1e-10,
89 loss_type: LossType::AmplitudeMse,
90 recover_illumination: false,
91 illumination_step: 0.1,
92 illumination_finite_difference: 0.05,
93 maximum_illumination_correction: 1.0,
94 recover_pupil: false,
95 pupil_step: 0.05,
96 constrain_pupil_support: true,
97 object_tv_weight: 0.0,
98 object_tv_epsilon: 1e-6,
99 pupil_smoothing_weight: 0.0,
100 parallel_workers: std::thread::available_parallelism().map_or(1, |count| count.get()),
101 }
102 }
103}
104
105impl GradientDescent {
106 pub fn iterations(mut self, iterations: usize) -> Self {
107 self.iterations = iterations;
108 self
109 }
110
111 pub fn object_step(mut self, step: f64) -> Self {
112 self.object_step = step;
113 self
114 }
115
116 pub fn batch_size(mut self, batch_size: usize) -> Self {
117 self.batch_size = batch_size;
118 self
119 }
120
121 pub fn loss_type(mut self, loss_type: LossType) -> Self {
122 self.loss_type = loss_type;
123 self
124 }
125
126 pub fn recover_illumination(mut self, recover: bool) -> Self {
127 self.recover_illumination = recover;
128 self
129 }
130
131 pub fn illumination_step(mut self, step: f64) -> Self {
132 self.illumination_step = step;
133 self
134 }
135
136 pub fn illumination_finite_difference(mut self, distance: f64) -> Self {
137 self.illumination_finite_difference = distance;
138 self
139 }
140
141 pub fn illumination_bounds(mut self, maximum_absolute_correction: f64) -> Self {
142 self.maximum_illumination_correction = maximum_absolute_correction;
143 self
144 }
145
146 pub fn recover_pupil(mut self, recover: bool) -> Self {
147 self.recover_pupil = recover;
148 self
149 }
150
151 pub fn pupil_step(mut self, step: f64) -> Self {
152 self.pupil_step = step;
153 self
154 }
155
156 pub fn constrain_pupil_support(mut self, constrain: bool) -> Self {
157 self.constrain_pupil_support = constrain;
158 self
159 }
160
161 pub fn object_tv(mut self, weight: f64) -> Self {
162 self.object_tv_weight = weight;
163 self
164 }
165
166 pub fn object_tv_epsilon(mut self, epsilon: f64) -> Self {
167 self.object_tv_epsilon = epsilon;
168 self
169 }
170
171 pub fn pupil_smoothing(mut self, weight: f64) -> Self {
172 self.pupil_smoothing_weight = weight;
173 self
174 }
175
176 pub fn parallel_workers(mut self, workers: usize) -> Self {
179 self.parallel_workers = workers;
180 self
181 }
182}
183
184impl ReconstructionAlgorithm for GradientDescent {
185 fn validate(&self) -> Result<()> {
186 if !self.object_step.is_finite() || self.object_step <= 0.0 {
187 return Err(Error::InvalidParameter {
188 name: "object_step",
189 reason: "must be finite and positive".into(),
190 });
191 }
192 if !self.epsilon.is_finite() || self.epsilon <= 0.0 {
193 return Err(Error::InvalidParameter {
194 name: "epsilon",
195 reason: "must be finite and positive".into(),
196 });
197 }
198 if self.batch_size == 0 {
199 return Err(Error::InvalidParameter {
200 name: "batch_size",
201 reason: "must be greater than zero".into(),
202 });
203 }
204 if !self.illumination_step.is_finite() || self.illumination_step <= 0.0 {
205 return Err(Error::InvalidParameter {
206 name: "illumination_step",
207 reason: "must be finite and positive".into(),
208 });
209 }
210 if !self.illumination_finite_difference.is_finite()
211 || self.illumination_finite_difference <= 0.0
212 {
213 return Err(Error::InvalidParameter {
214 name: "illumination_finite_difference",
215 reason: "must be finite and positive".into(),
216 });
217 }
218 if !self.maximum_illumination_correction.is_finite()
219 || self.maximum_illumination_correction <= 0.0
220 {
221 return Err(Error::InvalidParameter {
222 name: "maximum_illumination_correction",
223 reason: "must be finite and positive".into(),
224 });
225 }
226 if !self.pupil_step.is_finite() || self.pupil_step < 0.0 {
227 return Err(Error::InvalidParameter {
228 name: "pupil_step",
229 reason: "must be finite and non-negative".into(),
230 });
231 }
232 if !self.object_tv_weight.is_finite() || self.object_tv_weight < 0.0 {
233 return Err(Error::InvalidParameter {
234 name: "object_tv_weight",
235 reason: "must be finite and non-negative".into(),
236 });
237 }
238 if !self.object_tv_epsilon.is_finite() || self.object_tv_epsilon <= 0.0 {
239 return Err(Error::InvalidParameter {
240 name: "object_tv_epsilon",
241 reason: "must be finite and positive".into(),
242 });
243 }
244 if !self.pupil_smoothing_weight.is_finite() || self.pupil_smoothing_weight < 0.0 {
245 return Err(Error::InvalidParameter {
246 name: "pupil_smoothing_weight",
247 reason: "must be finite and non-negative".into(),
248 });
249 }
250 if self.pupil_smoothing_weight > 0.0 && !self.recover_pupil {
251 return Err(Error::InvalidParameter {
252 name: "pupil_smoothing_weight",
253 reason: "requires pupil recovery to be enabled".into(),
254 });
255 }
256 if self.parallel_workers == 0 {
257 return Err(Error::InvalidParameter {
258 name: "parallel_workers",
259 reason: "must be greater than zero".into(),
260 });
261 }
262 Ok(())
263 }
264
265 fn step<M: MeasurementRead>(
266 &mut self,
267 problem: &ReconstructionProblem<M>,
268 state: &mut ReconstructionState,
269 batch: &Batch,
270 iteration: usize,
271 ) -> Result<StepDiagnostics> {
272 if self.parallel_workers > 1 && batch.indices.len() > 1 {
273 return self.parallel_step(problem, state, batch, iteration);
274 }
275 let model = &problem.model;
276 let shape = model.image_shape;
277 let image_len = shape.0 * shape.1;
278 let maximum_pupil_power = state
279 .pupil
280 .values
281 .as_slice()
282 .iter()
283 .map(|value| value.norm_sqr())
284 .fold(0.0, f64::max)
285 .max(self.epsilon);
286 let mut diagnostics = StepDiagnostics::default();
287 let mut active_frames = 0;
288 if self.recover_illumination {
289 prepare_illumination_accumulators(model, state)?;
290 }
291 state
292 .scratch
293 .object_gradient
294 .resize(state.object_spectrum.len(), Complex64::default());
295 state.scratch.object_gradient.fill(Complex64::default());
296 if self.recover_pupil {
297 state.scratch.pupil_gradient.fill(Complex64::default());
298 }
299
300 for &frame in &batch.indices {
301 let frame_weight = problem.measurements.frame_weight(frame)?;
302 if frame_weight == 0.0 {
303 diagnostics.push_frame(frame, 0.0, 0.0);
304 continue;
305 }
306 active_frames += 1;
307 let single_source = [(frame, 1.0)];
308 let sources = model
309 .multiplexing_matrix
310 .as_ref()
311 .map_or(single_source.as_slice(), |matrix| matrix[frame].as_slice());
312
313 state.scratch.projected_field.fill(Complex64::default());
314 for &(source, source_weight) in sources {
315 let offset = state.effective_source_offset(model, source)?;
316 compute_source_field(problem, state, source, offset)?;
317 for (predicted, field) in state
318 .scratch
319 .projected_field
320 .iter_mut()
321 .zip(&state.scratch.field)
322 {
323 predicted.re += source_weight * field.norm_sqr();
324 }
325 }
326
327 let measured = problem.measurements.frame(frame)?;
328 let mask = problem.measurements.frame_mask(frame)?;
329 let gain = state
330 .frame_gains
331 .as_ref()
332 .map_or(1.0, |values| values[frame]);
333 if !gain.is_finite() || gain <= 0.0 {
334 return Err(Error::InvalidModel(format!(
335 "state frame {frame} has invalid gain {gain}"
336 )));
337 }
338 let mut frame_loss = 0.0;
339 let mut valid_pixels = 0;
340 for pixel in 0..image_len {
341 if mask.is_some_and(|values| values[pixel] == 0) {
342 state.scratch.projected_field[pixel] = Complex64::default();
343 continue;
344 }
345 valid_pixels += 1;
346 let background = background_value(state, frame, pixel, image_len);
347 let intrinsic_prediction = state.scratch.projected_field[pixel].re.max(0.0);
348 let target_intensity = ((measured[pixel] - background) / gain).max(0.0);
349 frame_loss += point_loss(intrinsic_prediction, target_intensity, self.loss_type);
350 state.scratch.projected_field[pixel] = Complex64::new(
351 descent_factor(
352 intrinsic_prediction,
353 target_intensity,
354 self.loss_type,
355 self.epsilon,
356 ),
357 intrinsic_prediction,
358 );
359 }
360 if valid_pixels == 0 {
361 return Err(Error::InvalidMeasurements(format!(
362 "frame {frame} has no unmasked pixels"
363 )));
364 }
365 let valid_pixel_scale = image_len as f64 / valid_pixels as f64;
368 diagnostics.push_frame(frame, frame_loss / valid_pixels as f64, frame_weight);
369
370 for &(source, source_weight) in sources {
371 let offset = state.effective_source_offset(model, source)?;
372 compute_source_field(problem, state, source, offset)?;
373 if self.recover_illumination {
374 for (reference, field) in state
375 .scratch
376 .calibration_reference
377 .iter_mut()
378 .zip(&state.scratch.field)
379 {
380 *reference = field.norm_sqr();
381 }
382 }
383 for pixel in 0..image_len {
384 state.scratch.field[pixel] *=
385 source_weight * state.scratch.projected_field[pixel].re;
386 }
387 state.backend.fft2(
388 &mut state.scratch.field,
389 shape,
390 FftDirection::Forward,
391 &mut state.scratch.column,
392 )?;
393 fftshift_copy(
394 &state.scratch.field,
395 &mut state.scratch.projected_spectrum,
396 shape,
397 );
398 if self.recover_pupil {
399 let maximum_object_power = state
400 .scratch
401 .patch
402 .iter()
403 .map(|value| value.norm_sqr())
404 .fold(0.0, f64::max)
405 .max(self.epsilon);
406 for pixel in 0..image_len {
407 state.scratch.pupil_gradient[pixel] += frame_weight
408 * valid_pixel_scale
409 * state.scratch.patch[pixel].conj()
410 * state.scratch.projected_spectrum[pixel]
411 / (maximum_object_power + self.epsilon);
412 }
413 }
414 for pixel in 0..image_len {
415 state.scratch.difference[pixel] = state.pupil.values.as_slice()[pixel].conj()
416 * state.scratch.projected_spectrum[pixel]
417 / (maximum_pupil_power + self.epsilon);
418 }
419 model.insert_patch_adjoint_slice_at_offset(
420 &mut state.scratch.object_gradient,
421 source,
422 &state.scratch.difference,
423 frame_weight * valid_pixel_scale,
424 offset,
425 )?;
426 if self.recover_illumination {
427 let gradient = illumination_gradient(
428 problem,
429 state,
430 source,
431 offset,
432 IlluminationGradientConfiguration {
433 source_weight,
434 valid_pixels,
435 distance: self.illumination_finite_difference,
436 loss_type: self.loss_type,
437 epsilon: self.epsilon,
438 },
439 )?;
440 state.scratch.illumination_gradient[source].0 +=
441 frame_weight * gradient.row.gradient;
442 state.scratch.illumination_gradient[source].1 +=
443 frame_weight * gradient.column.gradient;
444 state.scratch.illumination_curvature[source].0 +=
445 frame_weight * gradient.row.curvature;
446 state.scratch.illumination_curvature[source].1 +=
447 frame_weight * gradient.column.curvature;
448 state.scratch.illumination_weight[source] += frame_weight;
449 }
450 }
451 }
452 if active_frames > 0 {
453 let step = self.object_step / active_frames as f64;
454 for (object, &gradient) in state
455 .object_spectrum
456 .as_mut_slice()
457 .iter_mut()
458 .zip(&state.scratch.object_gradient)
459 {
460 *object -= step * gradient;
461 }
462 if self.recover_pupil {
463 let pupil_step = self.pupil_step / active_frames as f64;
464 for (pupil, &gradient) in state
465 .pupil
466 .values
467 .as_mut_slice()
468 .iter_mut()
469 .zip(&state.scratch.pupil_gradient)
470 {
471 *pupil -= pupil_step * gradient;
472 if !pupil.re.is_finite() || !pupil.im.is_finite() {
473 return Err(Error::Numerical(
474 "pupil update produced a non-finite value".into(),
475 ));
476 }
477 }
478 }
479 }
480 let batch_fraction = batch.indices.len() as f64 / model.frame_count() as f64;
481 if self.object_tv_weight > 0.0 {
482 apply_object_tv(
483 state,
484 model.reconstruction_shape,
485 batch_fraction * self.object_tv_weight,
486 self.object_tv_epsilon,
487 )?;
488 }
489 if self.recover_pupil && self.pupil_smoothing_weight > 0.0 {
490 apply_quadratic_smoothing_step(
491 state.pupil.values.as_mut_slice(),
492 model.image_shape,
493 batch_fraction * self.pupil_smoothing_weight,
494 &mut state.scratch.pupil_gradient,
495 )?;
496 }
497 if self.recover_pupil && self.constrain_pupil_support {
498 state.pupil.apply_support();
499 }
500 if self.recover_illumination {
501 self.apply_illumination_update(model, state)?;
502 }
503 Ok(diagnostics)
504 }
505
506 fn iterations(&self) -> usize {
507 self.iterations
508 }
509
510 fn batch_size(&self) -> usize {
511 self.batch_size
512 }
513}
514
515struct ParallelWorkerResult {
516 position: usize,
517 object_delta: Vec<Complex64>,
518 pupil_delta: Vec<Complex64>,
519 illumination_gradient: Vec<(f64, f64)>,
520 illumination_curvature: Vec<(f64, f64)>,
521 illumination_weight: Vec<f64>,
522 diagnostics: StepDiagnostics,
523 active_frames: usize,
524}
525
526impl GradientDescent {
527 fn parallel_step<M: MeasurementRead>(
528 &self,
529 problem: &ReconstructionProblem<M>,
530 state: &mut ReconstructionState,
531 batch: &Batch,
532 iteration: usize,
533 ) -> Result<StepDiagnostics> {
534 let worker_count = self.parallel_workers.min(batch.indices.len());
535 if self.recover_illumination {
536 prepare_illumination_accumulators(&problem.model, state)?;
537 }
538 let base_state = state.clone();
539 let mut results = thread::scope(|scope| -> Result<Vec<ParallelWorkerResult>> {
540 let base_chunk_len = batch.indices.len() / worker_count;
541 let remainder = batch.indices.len() % worker_count;
542 let handles: Vec<_> = (0..worker_count)
543 .map(|worker| {
544 let start = worker * base_chunk_len + worker.min(remainder);
545 let length = base_chunk_len + usize::from(worker < remainder);
546 let frames = &batch.indices[start..start + length];
547 let base_state = &base_state;
548 scope.spawn(move || -> Result<ParallelWorkerResult> {
549 let mut local_state = base_state.clone();
550 let mut local_algorithm = self.clone();
551 local_algorithm.parallel_workers = 1;
552 local_algorithm.object_tv_weight = 0.0;
553 local_algorithm.pupil_smoothing_weight = 0.0;
554 local_algorithm.constrain_pupil_support = false;
555 let mut output = ParallelWorkerResult {
556 position: worker,
557 object_delta: vec![
558 Complex64::default();
559 base_state.object_spectrum.len()
560 ],
561 pupil_delta: if self.recover_pupil {
562 vec![Complex64::default(); base_state.pupil.values.len()]
563 } else {
564 Vec::new()
565 },
566 illumination_gradient: if self.recover_illumination {
567 vec![(0.0, 0.0); problem.model.source_count()]
568 } else {
569 Vec::new()
570 },
571 illumination_curvature: if self.recover_illumination {
572 vec![(0.0, 0.0); problem.model.source_count()]
573 } else {
574 Vec::new()
575 },
576 illumination_weight: if self.recover_illumination {
577 vec![0.0; problem.model.source_count()]
578 } else {
579 Vec::new()
580 },
581 diagnostics: StepDiagnostics::default(),
582 active_frames: 0,
583 };
584 for &frame in frames {
585 local_state
586 .object_spectrum
587 .as_mut_slice()
588 .copy_from_slice(base_state.object_spectrum.as_slice());
589 if self.recover_pupil {
590 local_state
591 .pupil
592 .values
593 .as_mut_slice()
594 .copy_from_slice(base_state.pupil.values.as_slice());
595 }
596 if self.recover_illumination {
597 local_state
598 .illumination_corrections
599 .clone_from(&base_state.illumination_corrections);
600 }
601 let diagnostics = local_algorithm.step(
602 problem,
603 &mut local_state,
604 &Batch::single(frame),
605 iteration,
606 )?;
607 if diagnostics.weight_sum > 0.0 {
608 output.active_frames += 1;
609 for ((sum, &value), &initial) in output
610 .object_delta
611 .iter_mut()
612 .zip(local_state.object_spectrum.as_slice())
613 .zip(base_state.object_spectrum.as_slice())
614 {
615 *sum += value - initial;
616 }
617 if self.recover_pupil {
618 for ((sum, &value), &initial) in output
619 .pupil_delta
620 .iter_mut()
621 .zip(local_state.pupil.values.as_slice())
622 .zip(base_state.pupil.values.as_slice())
623 {
624 *sum += value - initial;
625 }
626 }
627 if self.recover_illumination {
628 for source in 0..problem.model.source_count() {
629 output.illumination_gradient[source].0 +=
630 local_state.scratch.illumination_gradient[source].0;
631 output.illumination_gradient[source].1 +=
632 local_state.scratch.illumination_gradient[source].1;
633 output.illumination_curvature[source].0 +=
634 local_state.scratch.illumination_curvature[source].0;
635 output.illumination_curvature[source].1 +=
636 local_state.scratch.illumination_curvature[source].1;
637 output.illumination_weight[source] +=
638 local_state.scratch.illumination_weight[source];
639 }
640 }
641 }
642 output.diagnostics.merge(diagnostics);
643 }
644 Ok(output)
645 })
646 })
647 .collect();
648 let mut output = Vec::with_capacity(handles.len());
649 for handle in handles {
650 output.push(
651 handle.join().map_err(|_| {
652 Error::Numerical("parallel gradient worker panicked".into())
653 })??,
654 );
655 }
656 Ok(output)
657 })?;
658 results.sort_by_key(|result| result.position);
659
660 state
661 .scratch
662 .object_gradient
663 .resize(state.object_spectrum.len(), Complex64::default());
664 state.scratch.object_gradient.fill(Complex64::default());
665 if self.recover_pupil {
666 state.scratch.pupil_gradient.fill(Complex64::default());
667 }
668 if self.recover_illumination {
669 state.scratch.illumination_gradient.fill((0.0, 0.0));
670 state.scratch.illumination_curvature.fill((0.0, 0.0));
671 state.scratch.illumination_weight.fill(0.0);
672 }
673 let mut diagnostics = StepDiagnostics::default();
674 let mut active_frames = 0;
675 for result in results {
676 active_frames += result.active_frames;
677 for (sum, &value) in state
678 .scratch
679 .object_gradient
680 .iter_mut()
681 .zip(&result.object_delta)
682 {
683 *sum += value;
684 }
685 if self.recover_pupil {
686 for (sum, &value) in state
687 .scratch
688 .pupil_gradient
689 .iter_mut()
690 .zip(&result.pupil_delta)
691 {
692 *sum += value;
693 }
694 }
695 if self.recover_illumination {
696 for source in 0..problem.model.source_count() {
697 state.scratch.illumination_gradient[source].0 +=
698 result.illumination_gradient[source].0;
699 state.scratch.illumination_gradient[source].1 +=
700 result.illumination_gradient[source].1;
701 state.scratch.illumination_curvature[source].0 +=
702 result.illumination_curvature[source].0;
703 state.scratch.illumination_curvature[source].1 +=
704 result.illumination_curvature[source].1;
705 state.scratch.illumination_weight[source] += result.illumination_weight[source];
706 }
707 }
708 diagnostics.merge(result.diagnostics);
709 }
710 if active_frames > 0 {
711 let normalization = active_frames as f64;
712 for (object, &sum) in state
713 .object_spectrum
714 .as_mut_slice()
715 .iter_mut()
716 .zip(&state.scratch.object_gradient)
717 {
718 *object += sum / normalization;
719 }
720 if self.recover_pupil {
721 for (pupil, &sum) in state
722 .pupil
723 .values
724 .as_mut_slice()
725 .iter_mut()
726 .zip(&state.scratch.pupil_gradient)
727 {
728 *pupil += sum / normalization;
729 if !pupil.re.is_finite() || !pupil.im.is_finite() {
730 return Err(Error::Numerical(
731 "parallel pupil update produced a non-finite value".into(),
732 ));
733 }
734 }
735 }
736 }
737
738 let model = &problem.model;
739 let batch_fraction = batch.indices.len() as f64 / model.frame_count() as f64;
740 if self.object_tv_weight > 0.0 {
741 apply_object_tv(
742 state,
743 model.reconstruction_shape,
744 batch_fraction * self.object_tv_weight,
745 self.object_tv_epsilon,
746 )?;
747 }
748 if self.recover_pupil && self.pupil_smoothing_weight > 0.0 {
749 apply_quadratic_smoothing_step(
750 state.pupil.values.as_mut_slice(),
751 model.image_shape,
752 batch_fraction * self.pupil_smoothing_weight,
753 &mut state.scratch.pupil_gradient,
754 )?;
755 }
756 if self.recover_pupil && self.constrain_pupil_support {
757 state.pupil.apply_support();
758 }
759 if self.recover_illumination {
760 self.apply_illumination_update(model, state)?;
761 }
762 Ok(diagnostics)
763 }
764
765 fn apply_illumination_update(
766 &self,
767 model: &crate::model::ImagePlaneModel,
768 state: &mut ReconstructionState,
769 ) -> Result<()> {
770 let corrections = state.illumination_corrections.as_mut().ok_or_else(|| {
771 Error::InvalidModel("illumination corrections were not initialized".into())
772 })?;
773 for (source, correction) in corrections.iter_mut().enumerate() {
774 let weight = state.scratch.illumination_weight[source];
775 if weight == 0.0 {
776 continue;
777 }
778 let gradient = state.scratch.illumination_gradient[source];
779 let curvature = state.scratch.illumination_curvature[source];
780 if !gradient.0.is_finite()
781 || !gradient.1.is_finite()
782 || !curvature.0.is_finite()
783 || !curvature.1.is_finite()
784 || curvature.0 < 0.0
785 || curvature.1 < 0.0
786 {
787 return Err(Error::Numerical(format!(
788 "illumination gradient or curvature for source {source} is invalid"
789 )));
790 }
791 let candidate = (
792 (correction.0 - self.illumination_step * gradient.0 / (curvature.0 + self.epsilon))
793 .clamp(
794 -self.maximum_illumination_correction,
795 self.maximum_illumination_correction,
796 ),
797 (correction.1 - self.illumination_step * gradient.1 / (curvature.1 + self.epsilon))
798 .clamp(
799 -self.maximum_illumination_correction,
800 self.maximum_illumination_correction,
801 ),
802 );
803 let base = model.source_offset(source)?;
804 let effective = FourierOffset::new(base.row + candidate.0, base.column + candidate.1);
805 if model.validate_source_offset(source, effective).is_ok() {
806 *correction = candidate;
807 }
808 }
809 Ok(())
810 }
811}
812
813fn prepare_illumination_accumulators(
814 model: &crate::model::ImagePlaneModel,
815 state: &mut ReconstructionState,
816) -> Result<()> {
817 match &state.illumination_corrections {
818 None => {
819 state.illumination_corrections = Some(vec![(0.0, 0.0); model.source_count()]);
820 }
821 Some(corrections) if corrections.len() != model.source_count() => {
822 return Err(Error::InvalidModel(
823 "illumination correction count does not match source count".into(),
824 ));
825 }
826 Some(_) => {}
827 }
828 state
829 .scratch
830 .illumination_gradient
831 .resize(model.source_count(), (0.0, 0.0));
832 state.scratch.illumination_gradient.fill((0.0, 0.0));
833 state
834 .scratch
835 .illumination_curvature
836 .resize(model.source_count(), (0.0, 0.0));
837 state.scratch.illumination_curvature.fill((0.0, 0.0));
838 state
839 .scratch
840 .illumination_weight
841 .resize(model.source_count(), 0.0);
842 state.scratch.illumination_weight.fill(0.0);
843 Ok(())
844}
845
846fn apply_object_tv(
847 state: &mut ReconstructionState,
848 shape: (usize, usize),
849 weight: f64,
850 epsilon: f64,
851) -> Result<()> {
852 state
853 .scratch
854 .regularization_field
855 .resize(state.object_spectrum.len(), Complex64::default());
856 ifftshift_copy(
857 state.object_spectrum.as_slice(),
858 &mut state.scratch.regularization_field,
859 shape,
860 );
861 state.backend.fft2(
862 &mut state.scratch.regularization_field,
863 shape,
864 FftDirection::Inverse,
865 &mut state.scratch.column,
866 )?;
867 apply_complex_tv_step(
868 &mut state.scratch.regularization_field,
869 shape,
870 weight,
871 epsilon,
872 &mut state.scratch.object_gradient,
873 )?;
874 state.backend.fft2(
875 &mut state.scratch.regularization_field,
876 shape,
877 FftDirection::Forward,
878 &mut state.scratch.column,
879 )?;
880 fftshift_copy(
881 &state.scratch.regularization_field,
882 state.object_spectrum.as_mut_slice(),
883 shape,
884 );
885 Ok(())
886}
887
888fn descent_factor(predicted: f64, measured: f64, loss_type: LossType, epsilon: f64) -> f64 {
889 match loss_type {
890 LossType::AmplitudeMse => 1.0 - measured.max(0.0).sqrt() / predicted.max(epsilon).sqrt(),
891 LossType::IntensityMse => 2.0 * (predicted - measured),
892 LossType::PoissonNegativeLogLikelihood => 1.0 - measured.max(0.0) / predicted.max(epsilon),
893 LossType::HuberAmplitude => {
894 let predicted_amplitude = predicted.max(epsilon).sqrt();
895 let residual = predicted_amplitude - measured.max(0.0).sqrt();
896 residual.clamp(-1.0, 1.0) / (2.0 * predicted_amplitude)
897 }
898 }
899}
900
901fn compute_source_field<M: MeasurementRead>(
902 problem: &ReconstructionProblem<M>,
903 state: &mut ReconstructionState,
904 source: usize,
905 offset: FourierOffset,
906) -> Result<()> {
907 let model = &problem.model;
908 let shape = model.image_shape;
909 model.extract_patch_at_offset(
910 &state.object_spectrum,
911 source,
912 offset,
913 &mut state.scratch.patch,
914 )?;
915 for pixel in 0..state.scratch.patch.len() {
916 state.scratch.exit_spectrum[pixel] =
917 state.scratch.patch[pixel] * state.pupil.values.as_slice()[pixel];
918 }
919 ifftshift_copy(
920 &state.scratch.exit_spectrum,
921 &mut state.scratch.field,
922 shape,
923 );
924 state.backend.fft2(
925 &mut state.scratch.field,
926 shape,
927 FftDirection::Inverse,
928 &mut state.scratch.column,
929 )
930}
931
932#[derive(Clone, Copy)]
933struct IlluminationGradientConfiguration {
934 source_weight: f64,
935 valid_pixels: usize,
936 distance: f64,
937 loss_type: LossType,
938 epsilon: f64,
939}
940
941#[derive(Clone, Copy, Default)]
942struct AxisGradient {
943 gradient: f64,
944 curvature: f64,
945}
946
947#[derive(Clone, Copy, Default)]
948struct IlluminationGradient {
949 row: AxisGradient,
950 column: AxisGradient,
951}
952
953fn illumination_gradient<M: MeasurementRead>(
954 problem: &ReconstructionProblem<M>,
955 state: &mut ReconstructionState,
956 source: usize,
957 offset: FourierOffset,
958 configuration: IlluminationGradientConfiguration,
959) -> Result<IlluminationGradient> {
960 let row = illumination_axis_gradient(
961 problem,
962 state,
963 source,
964 offset,
965 FourierOffset::new(configuration.distance, 0.0),
966 configuration,
967 )?;
968 let column = illumination_axis_gradient(
969 problem,
970 state,
971 source,
972 offset,
973 FourierOffset::new(0.0, configuration.distance),
974 configuration,
975 )?;
976 let pixels = configuration.valid_pixels as f64;
977 Ok(IlluminationGradient {
978 row: AxisGradient {
979 gradient: row.gradient / pixels,
980 curvature: row.curvature / pixels,
981 },
982 column: AxisGradient {
983 gradient: column.gradient / pixels,
984 curvature: column.curvature / pixels,
985 },
986 })
987}
988
989fn illumination_axis_gradient<M: MeasurementRead>(
990 problem: &ReconstructionProblem<M>,
991 state: &mut ReconstructionState,
992 source: usize,
993 offset: FourierOffset,
994 displacement: FourierOffset,
995 configuration: IlluminationGradientConfiguration,
996) -> Result<AxisGradient> {
997 let model = &problem.model;
998 let distance = displacement.row.abs() + displacement.column.abs();
999 let plus = FourierOffset::new(
1000 offset.row + displacement.row,
1001 offset.column + displacement.column,
1002 );
1003 let minus = FourierOffset::new(
1004 offset.row - displacement.row,
1005 offset.column - displacement.column,
1006 );
1007 let plus_valid = model.validate_source_offset(source, plus).is_ok();
1008 let minus_valid = model.validate_source_offset(source, minus).is_ok();
1009 if !plus_valid && !minus_valid {
1010 return Ok(AxisGradient::default());
1011 }
1012 if plus_valid {
1013 compute_source_field(problem, state, source, plus)?;
1014 for (candidate, field) in state
1015 .scratch
1016 .difference
1017 .iter_mut()
1018 .zip(&state.scratch.field)
1019 {
1020 candidate.re = field.norm_sqr();
1021 }
1022 }
1023 if minus_valid {
1024 compute_source_field(problem, state, source, minus)?;
1025 }
1026 let mut gradient = 0.0;
1027 let mut curvature = 0.0;
1028 for pixel in 0..state.scratch.field.len() {
1029 let derivative = match (plus_valid, minus_valid) {
1030 (true, true) => {
1031 (state.scratch.difference[pixel].re - state.scratch.field[pixel].norm_sqr())
1032 / (2.0 * distance)
1033 }
1034 (true, false) => {
1035 (state.scratch.difference[pixel].re - state.scratch.calibration_reference[pixel])
1036 / distance
1037 }
1038 (false, true) => {
1039 (state.scratch.calibration_reference[pixel] - state.scratch.field[pixel].norm_sqr())
1040 / distance
1041 }
1042 (false, false) => 0.0,
1043 };
1044 let intensity_derivative = configuration.source_weight * derivative;
1045 let loss_derivative = state.scratch.projected_field[pixel].re;
1046 let predicted = state.scratch.projected_field[pixel].im;
1047 gradient += loss_derivative * intensity_derivative;
1048 curvature += descent_curvature(
1049 predicted,
1050 loss_derivative,
1051 configuration.loss_type,
1052 configuration.epsilon,
1053 ) * intensity_derivative
1054 * intensity_derivative;
1055 }
1056 Ok(AxisGradient {
1057 gradient,
1058 curvature,
1059 })
1060}
1061
1062fn descent_curvature(
1063 predicted: f64,
1064 descent_factor: f64,
1065 loss_type: LossType,
1066 epsilon: f64,
1067) -> f64 {
1068 let mean = predicted.max(epsilon);
1069 match loss_type {
1070 LossType::AmplitudeMse => 0.5 / mean,
1071 LossType::IntensityMse => 2.0,
1072 LossType::PoissonNegativeLogLikelihood => 1.0 / mean,
1073 LossType::HuberAmplitude => {
1074 let clipped_residual = descent_factor * 2.0 * mean.sqrt();
1075 if clipped_residual.abs() < 1.0 {
1076 0.25 / mean
1077 } else {
1078 epsilon
1079 }
1080 }
1081 }
1082}
1083
1084fn background_value(
1085 state: &ReconstructionState,
1086 frame: usize,
1087 pixel: usize,
1088 image_len: usize,
1089) -> f64 {
1090 state.background.as_ref().map_or(0.0, |values| {
1091 values[if values.len() == image_len {
1092 pixel
1093 } else {
1094 frame * image_len + pixel
1095 }]
1096 })
1097}