Skip to main content

fpm_rs/model/
forward.rs

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/// Reusable host buffers for allocation-free repeated forward evaluations.
14#[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
41/// Allocation-friendly CPU implementation of the image-plane FPM forward model.
42pub 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    /// Coherent low-resolution field for one illumination source.
115    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    /// Coherent field for a non-multiplexed frame. A coded frame with exactly
152    /// one source is represented by a field scaled by the square-root weight.
153    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    /// Predicts every measured frame into a contiguous
241    /// `[frame][row][column]` destination using independent worker scratch.
242    ///
243    /// Frame order and floating-point results within each frame are unchanged
244    /// by `worker_count`. Values larger than the frame count are capped; zero is
245    /// rejected. A worker count of one executes directly without spawning.
246    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    /// Allocation-owning convenience wrapper for [`Self::forward_intensity_stack_into`].
320    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}