1use num_complex::Complex64;
2use std::{sync::Arc, thread};
3
4use crate::{
5 Array2, Result,
6 algorithms::objective::LossType,
7 backend::{Backend, CpuBackend, FftDirection},
8 error::Error,
9};
10
11use super::{ImagePlaneModel, Pupil};
12
13#[derive(Clone, Debug)]
15pub struct ForwardWorkspace {
16 patch: Vec<Complex64>,
17 centered_exit: Vec<Complex64>,
18 field: Vec<Complex64>,
19 column: Vec<Complex64>,
20}
21
22impl ForwardWorkspace {
23 fn new(model: &ImagePlaneModel) -> Self {
24 let image_len = model.image_shape.0 * model.image_shape.1;
25 Self {
26 patch: vec![Complex64::default(); image_len],
27 centered_exit: vec![Complex64::default(); image_len],
28 field: vec![Complex64::default(); image_len],
29 column: vec![
30 Complex64::default();
31 model.image_shape.0.max(model.reconstruction_shape.0)
32 ],
33 }
34 }
35
36 pub fn field(&self) -> &[Complex64] {
37 &self.field
38 }
39}
40
41pub struct ForwardModel<'a> {
43 model: &'a ImagePlaneModel,
44 backend: Arc<dyn Backend>,
45}
46
47impl<'a> ForwardModel<'a> {
48 pub fn new(model: &'a ImagePlaneModel) -> Result<Self> {
49 let backend: Arc<dyn Backend> = Arc::new(CpuBackend::new(
50 model.image_shape,
51 model.reconstruction_shape,
52 )?);
53 Self::with_backend(model, backend)
54 }
55
56 pub fn with_backend(model: &'a ImagePlaneModel, backend: Arc<dyn Backend>) -> Result<Self> {
57 model.validate()?;
58 Ok(Self { model, backend })
59 }
60
61 pub fn model(&self) -> &ImagePlaneModel {
62 self.model
63 }
64
65 pub fn workspace(&self) -> ForwardWorkspace {
66 ForwardWorkspace::new(self.model)
67 }
68
69 pub fn extract_patch(
70 &self,
71 object_spectrum: &Array2<Complex64>,
72 source: usize,
73 ) -> Result<Array2<Complex64>> {
74 let mut values =
75 vec![Complex64::default(); self.model.image_shape.0 * self.model.image_shape.1];
76 self.model
77 .extract_patch(object_spectrum, source, &mut values)?;
78 Array2::from_vec(self.model.image_shape, values)
79 }
80
81 pub fn insert_patch_update(
82 &self,
83 object_spectrum: &mut Array2<Complex64>,
84 source: usize,
85 update: &[Complex64],
86 scale: f64,
87 ) -> Result<()> {
88 self.model
89 .insert_patch_adjoint(object_spectrum, source, update, scale)
90 }
91
92 pub fn apply_pupil(
93 &self,
94 patch: &[Complex64],
95 pupil: &Pupil,
96 destination: &mut [Complex64],
97 ) -> Result<()> {
98 let len = self.model.image_shape.0 * self.model.image_shape.1;
99 if patch.len() != len || destination.len() != len || pupil.values.len() != len {
100 return Err(Error::InvalidShape(
101 "patch, pupil, and destination lengths must match image shape".into(),
102 ));
103 }
104 for ((destination, &patch), &pupil) in destination
105 .iter_mut()
106 .zip(patch)
107 .zip(pupil.values.as_slice())
108 {
109 *destination = patch * pupil;
110 }
111 Ok(())
112 }
113
114 pub fn forward_source_field(
116 &self,
117 object_spectrum: &Array2<Complex64>,
118 pupil: &Pupil,
119 source: usize,
120 ) -> Result<Array2<Complex64>> {
121 let mut workspace = self.workspace();
122 self.forward_source_field_into(object_spectrum, pupil, source, &mut workspace)?;
123 Array2::from_vec(self.model.image_shape, workspace.field)
124 }
125
126 pub fn forward_source_field_into<'b>(
127 &self,
128 object_spectrum: &Array2<Complex64>,
129 pupil: &Pupil,
130 source: usize,
131 workspace: &'b mut ForwardWorkspace,
132 ) -> Result<&'b [Complex64]> {
133 self.validate_workspace(workspace)?;
134 self.model
135 .extract_patch(object_spectrum, source, &mut workspace.patch)?;
136 self.apply_pupil(&workspace.patch, pupil, &mut workspace.centered_exit)?;
137 ifftshift_copy(
138 &workspace.centered_exit,
139 &mut workspace.field,
140 self.model.image_shape,
141 );
142 self.backend.fft2(
143 &mut workspace.field,
144 self.model.image_shape,
145 FftDirection::Inverse,
146 &mut workspace.column,
147 )?;
148 Ok(&workspace.field)
149 }
150
151 pub fn forward_field(
154 &self,
155 object_spectrum: &Array2<Complex64>,
156 pupil: &Pupil,
157 frame: usize,
158 ) -> Result<Array2<Complex64>> {
159 if frame >= self.model.frame_count() {
160 return Err(Error::FrameOutOfRange {
161 index: frame,
162 frames: self.model.frame_count(),
163 });
164 }
165 if let Some(matrix) = &self.model.multiplexing_matrix {
166 let row = &matrix[frame];
167 if row.len() != 1 {
168 return Err(Error::Unsupported(
169 "a multiplexed frame has no single coherent field".into(),
170 ));
171 }
172 let (source, weight) = row[0];
173 let mut field = self.forward_source_field(object_spectrum, pupil, source)?;
174 let field_scale = weight.sqrt();
175 for value in field.as_mut_slice() {
176 *value *= field_scale;
177 }
178 Ok(field)
179 } else {
180 self.forward_source_field(object_spectrum, pupil, frame)
181 }
182 }
183
184 pub fn forward_intensity(
185 &self,
186 object_spectrum: &Array2<Complex64>,
187 pupil: &Pupil,
188 frame: usize,
189 ) -> Result<Array2<f64>> {
190 let mut workspace = self.workspace();
191 let mut values = vec![0.0; self.model.image_shape.0 * self.model.image_shape.1];
192 self.forward_intensity_into(object_spectrum, pupil, frame, &mut workspace, &mut values)?;
193 Array2::from_vec(self.model.image_shape, values)
194 }
195
196 pub fn forward_intensity_into(
197 &self,
198 object_spectrum: &Array2<Complex64>,
199 pupil: &Pupil,
200 frame: usize,
201 workspace: &mut ForwardWorkspace,
202 destination: &mut [f64],
203 ) -> Result<()> {
204 if frame >= self.model.frame_count() {
205 return Err(Error::FrameOutOfRange {
206 index: frame,
207 frames: self.model.frame_count(),
208 });
209 }
210 let gain = self.frame_gain(frame)?;
211 let image_len = self.model.image_shape.0 * self.model.image_shape.1;
212 if destination.len() != image_len {
213 return Err(Error::LengthMismatch {
214 actual: destination.len(),
215 expected: image_len,
216 shape: self.model.image_shape,
217 });
218 }
219 destination.fill(0.0);
220 if let Some(matrix) = &self.model.multiplexing_matrix {
221 for &(source, weight) in &matrix[frame] {
222 let field =
223 self.forward_source_field_into(object_spectrum, pupil, source, workspace)?;
224 for (intensity, value) in destination.iter_mut().zip(field) {
225 *intensity += weight * value.norm_sqr();
226 }
227 }
228 } else {
229 let field = self.forward_source_field_into(object_spectrum, pupil, frame, workspace)?;
230 for (intensity, value) in destination.iter_mut().zip(field) {
231 *intensity = value.norm_sqr();
232 }
233 }
234 for (pixel, value) in destination.iter_mut().enumerate() {
235 *value = gain * *value + self.background(frame, pixel, image_len);
236 }
237 Ok(())
238 }
239
240 pub fn forward_intensity_stack_into(
247 &self,
248 object_spectrum: &Array2<Complex64>,
249 pupil: &Pupil,
250 destination: &mut [f64],
251 worker_count: usize,
252 ) -> Result<()> {
253 if worker_count == 0 {
254 return Err(Error::InvalidParameter {
255 name: "worker_count",
256 reason: "must be greater than zero".into(),
257 });
258 }
259 let image_len = self.model.image_shape.0 * self.model.image_shape.1;
260 let expected = image_len
261 .checked_mul(self.model.frame_count())
262 .ok_or_else(|| Error::InvalidShape("forward stack length overflows".into()))?;
263 if destination.len() != expected {
264 return Err(Error::LengthMismatch {
265 actual: destination.len(),
266 expected,
267 shape: (self.model.frame_count(), image_len),
268 });
269 }
270 let workers = worker_count.min(self.model.frame_count());
271 if workers == 1 {
272 let mut workspace = self.workspace();
273 for (frame, frame_destination) in destination.chunks_exact_mut(image_len).enumerate() {
274 self.forward_intensity_into(
275 object_spectrum,
276 pupil,
277 frame,
278 &mut workspace,
279 frame_destination,
280 )?;
281 }
282 return Ok(());
283 }
284
285 let frames_per_worker = self.model.frame_count().div_ceil(workers);
286 let values_per_worker = frames_per_worker * image_len;
287 thread::scope(|scope| {
288 let handles: Vec<_> = destination
289 .chunks_mut(values_per_worker)
290 .enumerate()
291 .map(|(chunk_index, output)| {
292 let first_frame = chunk_index * frames_per_worker;
293 scope.spawn(move || -> Result<()> {
294 let mut workspace = self.workspace();
295 for (local_frame, frame_destination) in
296 output.chunks_exact_mut(image_len).enumerate()
297 {
298 self.forward_intensity_into(
299 object_spectrum,
300 pupil,
301 first_frame + local_frame,
302 &mut workspace,
303 frame_destination,
304 )?;
305 }
306 Ok(())
307 })
308 })
309 .collect();
310 for handle in handles {
311 handle
312 .join()
313 .map_err(|_| Error::Numerical("parallel forward worker panicked".into()))??;
314 }
315 Ok(())
316 })
317 }
318
319 pub fn forward_intensity_stack(
321 &self,
322 object_spectrum: &Array2<Complex64>,
323 pupil: &Pupil,
324 worker_count: usize,
325 ) -> Result<Vec<f64>> {
326 let image_len = self.model.image_shape.0 * self.model.image_shape.1;
327 let mut destination = vec![0.0; image_len * self.model.frame_count()];
328 self.forward_intensity_stack_into(object_spectrum, pupil, &mut destination, worker_count)?;
329 Ok(destination)
330 }
331
332 pub fn residual(
333 &self,
334 object_spectrum: &Array2<Complex64>,
335 pupil: &Pupil,
336 frame: usize,
337 measured: &[f64],
338 ) -> Result<Vec<f64>> {
339 let predicted = self.forward_intensity(object_spectrum, pupil, frame)?;
340 if measured.len() != predicted.len() {
341 return Err(Error::InvalidShape(
342 "measurement does not match predicted frame".into(),
343 ));
344 }
345 Ok(predicted
346 .as_slice()
347 .iter()
348 .zip(measured)
349 .map(|(&predicted, &measured)| predicted - measured)
350 .collect())
351 }
352
353 pub fn amplitude_projection(
354 &self,
355 field: &[Complex64],
356 measured_intensity: &[f64],
357 frame: usize,
358 epsilon: f64,
359 ) -> Result<Vec<Complex64>> {
360 if field.len() != measured_intensity.len() {
361 return Err(Error::InvalidShape(
362 "field and measured image lengths differ".into(),
363 ));
364 }
365 if frame >= self.model.frame_count() {
366 return Err(Error::FrameOutOfRange {
367 index: frame,
368 frames: self.model.frame_count(),
369 });
370 }
371 if !epsilon.is_finite() || epsilon <= 0.0 {
372 return Err(Error::InvalidParameter {
373 name: "epsilon",
374 reason: "must be finite and positive".into(),
375 });
376 }
377 if self
378 .model
379 .multiplexing_matrix
380 .as_ref()
381 .is_some_and(|matrix| matrix[frame].len() != 1)
382 {
383 return Err(Error::Unsupported(
384 "amplitude projection is not defined for an incoherent multiplexed field".into(),
385 ));
386 }
387 let gain = self.frame_gain(frame)?;
388 let image_len = field.len();
389 Ok(field
390 .iter()
391 .zip(measured_intensity)
392 .enumerate()
393 .map(|(pixel, (&value, &measurement))| {
394 let corrected =
395 ((measurement - self.background(frame, pixel, image_len)) / gain).max(0.0);
396 let target = corrected.sqrt();
397 let magnitude = value.norm();
398 if magnitude > epsilon {
399 value * (target / magnitude)
400 } else {
401 Complex64::new(target, 0.0)
402 }
403 })
404 .collect())
405 }
406
407 pub fn frame_loss(
408 &self,
409 object_spectrum: &Array2<Complex64>,
410 pupil: &Pupil,
411 frame: usize,
412 measured: &[f64],
413 loss_type: LossType,
414 ) -> Result<f64> {
415 let predicted = self.forward_intensity(object_spectrum, pupil, frame)?;
416 crate::algorithms::objective::loss(predicted.as_slice(), measured, loss_type)
417 }
418
419 fn frame_gain(&self, frame: usize) -> Result<f64> {
420 let gain = self
421 .model
422 .frame_gains
423 .as_ref()
424 .map_or(1.0, |gains| gains[frame]);
425 if !gain.is_finite() || gain <= 0.0 {
426 return Err(Error::InvalidModel(format!(
427 "frame {frame} has invalid gain {gain}"
428 )));
429 }
430 Ok(gain)
431 }
432
433 fn background(&self, frame: usize, pixel: usize, image_len: usize) -> f64 {
434 self.model.background.as_ref().map_or(0.0, |values| {
435 values[if values.len() == image_len {
436 pixel
437 } else {
438 frame * image_len + pixel
439 }]
440 })
441 }
442
443 fn validate_workspace(&self, workspace: &ForwardWorkspace) -> Result<()> {
444 let image_len = self.model.image_shape.0 * self.model.image_shape.1;
445 if workspace.patch.len() != image_len
446 || workspace.centered_exit.len() != image_len
447 || workspace.field.len() != image_len
448 || workspace.column.len() < self.model.image_shape.0
449 {
450 return Err(Error::InvalidShape(
451 "forward workspace does not match the model image shape".into(),
452 ));
453 }
454 Ok(())
455 }
456}
457
458pub(crate) fn fftshift_copy(
459 source: &[Complex64],
460 destination: &mut [Complex64],
461 shape: (usize, usize),
462) {
463 shift_copy(source, destination, shape, shape.0 / 2, shape.1 / 2);
464}
465
466pub(crate) fn ifftshift_copy(
467 source: &[Complex64],
468 destination: &mut [Complex64],
469 shape: (usize, usize),
470) {
471 shift_copy(
472 source,
473 destination,
474 shape,
475 shape.0.div_ceil(2),
476 shape.1.div_ceil(2),
477 );
478}
479
480fn shift_copy(
481 source: &[Complex64],
482 destination: &mut [Complex64],
483 shape: (usize, usize),
484 row_shift: usize,
485 column_shift: usize,
486) {
487 debug_assert_eq!(source.len(), shape.0 * shape.1);
488 debug_assert_eq!(destination.len(), source.len());
489 for row in 0..shape.0 {
490 for column in 0..shape.1 {
491 let destination_row = (row + row_shift) % shape.0;
492 let destination_column = (column + column_shift) % shape.1;
493 destination[destination_row * shape.1 + destination_column] =
494 source[row * shape.1 + column];
495 }
496 }
497}