1use std::{collections::BTreeSet, sync::Arc, time::Instant};
2
3use crate::{
4 Result,
5 algorithms::ReconstructionAlgorithm,
6 backend::Backend,
7 callbacks::{Callback, CallbackAction, CallbackHook, StepContext},
8 complex,
9 diagnostics::{
10 DiagnosticRequest, Diagnostics, FrameDiagnosticRecord, IterationRecord,
11 RawFrameStatisticsRecord, ReconstructionHistory, StepDiagnostics,
12 },
13 error::Error,
14 measurements::MeasurementRead,
15 metrics::intensity::{compare_intensity_masked, intensity_statistics},
16 model::ForwardModel,
17};
18
19use super::{
20 Batch, ReconstructionCheckpoint, ReconstructionProblem, ReconstructionResult,
21 ReconstructionState, RunOptions, RuntimeInfo, state_object,
22};
23
24pub struct Runner<A> {
25 algorithm: A,
26 options: RunOptions,
27 callbacks: Vec<Box<dyn Callback>>,
28 initial_checkpoint: Option<ReconstructionCheckpoint>,
29 backend: Option<Arc<dyn Backend>>,
30}
31
32impl<A: ReconstructionAlgorithm> Runner<A> {
33 pub fn new(algorithm: A, options: RunOptions) -> Self {
34 Self {
35 algorithm,
36 options,
37 callbacks: Vec::new(),
38 initial_checkpoint: None,
39 backend: None,
40 }
41 }
42
43 pub fn with_callback(mut self, callback: Box<dyn Callback>) -> Self {
44 self.callbacks.push(callback);
45 self
46 }
47
48 pub fn with_callbacks(mut self, callbacks: Vec<Box<dyn Callback>>) -> Self {
49 self.callbacks.extend(callbacks);
50 self
51 }
52
53 pub fn resume_from(mut self, checkpoint: ReconstructionCheckpoint) -> Self {
54 self.initial_checkpoint = Some(checkpoint);
55 self
56 }
57
58 pub fn with_backend(mut self, backend: Arc<dyn Backend>) -> Self {
59 self.backend = Some(backend);
60 self
61 }
62
63 pub fn run<M: MeasurementRead>(
64 mut self,
65 problem: &ReconstructionProblem<M>,
66 ) -> Result<ReconstructionResult> {
67 self.algorithm.validate()?;
68 problem.validate()?;
69 self.algorithm.validate_problem(problem)?;
70 if self.options.batch_size == 0 {
71 return Err(Error::InvalidParameter {
72 name: "batch_size",
73 reason: "must be greater than zero".into(),
74 });
75 }
76 let started = Instant::now();
77 let algorithm_name = std::any::type_name::<A>()
78 .rsplit("::")
79 .next()
80 .unwrap_or("reconstruction algorithm")
81 .to_owned();
82 let (mut state, mut history, starting_iteration) =
83 if let Some(checkpoint) = &self.initial_checkpoint {
84 (
85 if let Some(backend) = &self.backend {
86 ReconstructionState::from_checkpoint_with_backend(
87 problem,
88 checkpoint,
89 backend.clone(),
90 )?
91 } else {
92 ReconstructionState::from_checkpoint(problem, checkpoint)?
93 },
94 checkpoint.history.clone(),
95 checkpoint.completed_iterations,
96 )
97 } else {
98 (
99 if let Some(backend) = &self.backend {
100 self.algorithm
101 .initialize_with_backend(problem, backend.clone())?
102 } else {
103 self.algorithm.initialize(problem)?
104 },
105 ReconstructionHistory::default(),
106 0,
107 )
108 };
109 let previous_elapsed = history
110 .iterations
111 .last()
112 .map_or(0.0, |record| record.elapsed_seconds);
113 let start_requests =
114 callback_requests(&self.callbacks, CallbackHook::Start, starting_iteration);
115 let start_diagnostics = build_diagnostics(
116 problem,
117 &mut state,
118 &start_requests,
119 None,
120 None,
121 starting_iteration,
122 )?;
123 let start_context = StepContext {
124 iteration: starting_iteration,
125 frame_index: None,
126 batch_index: None,
127 state: &state,
128 diagnostics: &start_diagnostics,
129 history: &history,
130 model: &problem.model,
131 problem_name: problem.name.as_deref(),
132 };
133 let mut stopped_early = false;
134 for callback in &mut self.callbacks {
135 if callback.on_start(&start_context)? == CallbackAction::Stop {
136 stopped_early = true;
137 }
138 }
139 if !stopped_early {
140 for zero_based_iteration in starting_iteration..self.options.max_iterations {
141 let current_iteration = zero_based_iteration + 1;
142 let frame_requests =
143 callback_requests(&self.callbacks, CallbackHook::FrameEnd, current_iteration);
144 let order = self
145 .options
146 .schedule
147 .order_for_problem(problem, zero_based_iteration)?;
148 let mut iteration_step = StepDiagnostics::default();
149 for (batch_index, indices) in order.chunks(self.options.batch_size).enumerate() {
150 let batch = Batch::new(indices.to_vec(), batch_index);
151 let batch_step =
152 self.algorithm
153 .step(problem, &mut state, &batch, zero_based_iteration)?;
154 state.object_real_space_cache = None;
155 if self.options.enable_frame_callbacks {
156 let batch_loss = batch_step.mean_loss();
157 let mut diagnostics = build_diagnostics(
158 problem,
159 &mut state,
160 &frame_requests,
161 batch_loss,
162 Some(&batch_step),
163 current_iteration,
164 )?;
165 for &frame in &batch.indices {
170 if frame_requests.contains(&DiagnosticRequest::Loss) {
171 diagnostics.loss = batch_step.per_frame_loss.get(&frame).copied();
172 }
173 let context = StepContext {
174 iteration: current_iteration,
175 frame_index: Some(frame),
176 batch_index: Some(batch_index),
177 state: &state,
178 diagnostics: &diagnostics,
179 history: &history,
180 model: &problem.model,
181 problem_name: problem.name.as_deref(),
182 };
183 for callback in &mut self.callbacks {
184 if callback.on_frame_end(&context)? == CallbackAction::Stop {
185 stopped_early = true;
186 }
187 }
188 if stopped_early {
189 break;
190 }
191 }
192 }
193 iteration_step.merge(batch_step);
194 if stopped_early {
195 break;
196 }
197 }
198 let loss = iteration_step.mean_loss().unwrap_or(f64::NAN);
199 history.iterations.push(IterationRecord {
200 iteration: current_iteration,
201 loss,
202 elapsed_seconds: previous_elapsed + started.elapsed().as_secs_f64(),
203 admm_primal_residual_rms: iteration_step.admm_primal_residual_rms(),
204 admm_dual_residual_rms: iteration_step.admm_dual_residual_rms(),
205 });
206 let iteration_requests = callback_requests(
207 &self.callbacks,
208 CallbackHook::IterationEnd,
209 current_iteration,
210 );
211 let diagnostics = build_diagnostics(
212 problem,
213 &mut state,
214 &iteration_requests,
215 Some(loss),
216 Some(&iteration_step),
217 current_iteration,
218 )?;
219 let context = StepContext {
220 iteration: current_iteration,
221 frame_index: None,
222 batch_index: None,
223 state: &state,
224 diagnostics: &diagnostics,
225 history: &history,
226 model: &problem.model,
227 problem_name: problem.name.as_deref(),
228 };
229 for callback in &mut self.callbacks {
230 if callback.on_iteration_end(&context)? == CallbackAction::Stop {
231 stopped_early = true;
232 }
233 }
234 if stopped_early {
235 break;
236 }
237 }
238 }
239
240 let runtime = RuntimeInfo {
241 elapsed_seconds: previous_elapsed + started.elapsed().as_secs_f64(),
242 completed_iterations: history
243 .iterations
244 .last()
245 .map_or(starting_iteration, |record| record.iteration),
246 stopped_early,
247 algorithm: algorithm_name,
248 };
249 let mut result = ReconstructionResult::from_state(&mut state, history, runtime)?;
250 if let Some(name) = &problem.name {
251 result.metadata.insert("problem_name".into(), name.clone());
252 }
253 for callback in &mut self.callbacks {
254 callback.on_finish(&result)?;
255 }
256 Ok(result)
257 }
258}
259
260fn callback_requests(
261 callbacks: &[Box<dyn Callback>],
262 hook: CallbackHook,
263 iteration: usize,
264) -> BTreeSet<DiagnosticRequest> {
265 callbacks
266 .iter()
267 .flat_map(|callback| callback.requires_for(hook, iteration))
268 .collect()
269}
270
271fn build_diagnostics<M: MeasurementRead>(
272 problem: &ReconstructionProblem<M>,
273 state: &mut ReconstructionState,
274 requests: &BTreeSet<DiagnosticRequest>,
275 natural_loss: Option<f64>,
276 step: Option<&StepDiagnostics>,
277 iteration: usize,
278) -> Result<Diagnostics> {
279 let mut diagnostics = Diagnostics::default();
280 if requests.contains(&DiagnosticRequest::Loss) {
281 diagnostics.loss = natural_loss;
282 }
283 if requests.contains(&DiagnosticRequest::RawFrameStats) {
284 let mut values = Vec::with_capacity(problem.model.frame_count());
285 for frame in 0..problem.model.frame_count() {
286 let measured = problem.measurements.frame(frame)?;
287 values.push(RawFrameStatisticsRecord {
288 frame_index: frame,
289 metrics: intensity_statistics(&measured, None)?,
290 });
291 }
292 diagnostics.raw_frame_stats = Some(values);
293 }
294 if requests.contains(&DiagnosticRequest::PerFrameError)
295 && let Some(step) = step
296 {
297 let mut values = vec![f64::NAN; problem.model.frame_count()];
298 for (&frame, &loss) in &step.per_frame_loss {
299 values[frame] = loss;
300 }
301 diagnostics.per_frame_error = Some(values);
302 }
303 if requests.contains(&DiagnosticRequest::FrameSummaries)
304 || requests.contains(&DiagnosticRequest::ResidualImages)
305 || (requests.contains(&DiagnosticRequest::PerFrameError) && step.is_none())
306 {
307 let diagnostic_model = model_with_state_calibration(problem, state)?;
308 let forward = ForwardModel::with_backend(&diagnostic_model, state.backend.clone())?;
309 let mut workspace = forward.workspace();
310 let mut predicted = vec![0.0; problem.model.image_shape.0 * problem.model.image_shape.1];
311 let mut frame_diagnostics = Vec::with_capacity(problem.model.frame_count());
312 let mut residual_images = if requests.contains(&DiagnosticRequest::ResidualImages) {
313 Some(Vec::with_capacity(problem.model.frame_count()))
314 } else {
315 None
316 };
317 let mut per_frame_error =
318 if requests.contains(&DiagnosticRequest::PerFrameError) && step.is_none() {
319 Some(Vec::with_capacity(problem.model.frame_count()))
320 } else {
321 None
322 };
323 for frame in 0..problem.model.frame_count() {
324 forward.forward_intensity_into(
325 &state.object_spectrum,
326 &state.pupil,
327 frame,
328 &mut workspace,
329 &mut predicted,
330 )?;
331 let measured = problem.measurements.frame(frame)?;
332 let mask = problem.measurements.frame_mask(frame)?;
333 let metadata = problem
334 .measurements
335 .frame_metadata()
336 .get(frame)
337 .cloned()
338 .unwrap_or_else(|| crate::measurements::FrameMetadata::new(frame));
339 if let Some(values) = per_frame_error.as_mut() {
340 let mut loss_sum = 0.0;
341 let mut valid_pixels = 0;
342 for pixel in 0..predicted.len() {
343 if mask.is_none_or(|values| values[pixel] != 0) {
344 let residual =
345 predicted[pixel].max(0.0).sqrt() - measured[pixel].max(0.0).sqrt();
346 loss_sum += residual * residual;
347 valid_pixels += 1;
348 }
349 }
350 values.push(if valid_pixels == 0 {
351 0.0
352 } else {
353 loss_sum / valid_pixels as f64
354 });
355 }
356 if let Some(images) = residual_images.as_mut() {
357 let mut residual: Vec<_> = predicted
358 .iter()
359 .zip(measured.iter())
360 .map(|(&predicted, &measured)| predicted - measured)
361 .collect();
362 if let Some(mask) = mask {
363 for (value, &valid) in residual.iter_mut().zip(mask) {
364 if valid == 0 {
365 *value = 0.0;
366 }
367 }
368 }
369 images.push(crate::Array2::from_vec(
370 problem.model.image_shape,
371 residual,
372 )?);
373 }
374 if requests.contains(&DiagnosticRequest::FrameSummaries) {
375 let metrics = compare_intensity_masked(&measured, &predicted, mask, None)?;
376 frame_diagnostics.push(FrameDiagnosticRecord {
377 iteration: Some(iteration),
378 frame_index: frame,
379 illumination_index: metadata.illumination_index.unwrap_or(frame),
380 metrics,
381 });
382 }
383 }
384 if diagnostics.per_frame_error.is_none() {
385 diagnostics.per_frame_error = per_frame_error;
386 }
387 diagnostics.frame_diagnostics = Some(frame_diagnostics);
388 if let Some(images) = residual_images {
389 diagnostics.residual_images = Some(images);
390 }
391 }
392 if requests.contains(&DiagnosticRequest::ObjectAmplitude)
393 || requests.contains(&DiagnosticRequest::ObjectPhase)
394 {
395 let object = state_object(state)?;
396 if requests.contains(&DiagnosticRequest::ObjectAmplitude) {
397 diagnostics.object_amplitude = Some(complex::amplitude(&object));
398 }
399 if requests.contains(&DiagnosticRequest::ObjectPhase) {
400 diagnostics.object_phase = Some(complex::phase(&object));
401 }
402 }
403 if requests.contains(&DiagnosticRequest::Pupil) {
404 diagnostics.pupil_amplitude = Some(complex::amplitude(&state.pupil.values));
405 diagnostics.pupil_phase = Some(complex::phase(&state.pupil.values));
406 }
407 Ok(diagnostics)
408}
409
410fn model_with_state_calibration<M: MeasurementRead>(
411 problem: &ReconstructionProblem<M>,
412 state: &ReconstructionState,
413) -> Result<crate::model::ImagePlaneModel> {
414 let mut model = problem.model.clone();
415 model.frame_gains = state.frame_gains.clone();
416 model.background = state.background.clone();
417 if state.illumination_corrections.is_some() {
418 model.subpixel_offsets = Some(
419 (0..model.source_count())
420 .map(|source| state.effective_source_offset(&problem.model, source))
421 .collect::<Result<Vec<_>>>()?,
422 );
423 }
424 model.validate()?;
425 Ok(model)
426}