1use num_complex::Complex64;
2
3use crate::{
4 Result,
5 algorithms::objective::{LossType, point_loss},
6 backend::FftDirection,
7 diagnostics::StepDiagnostics,
8 error::Error,
9 measurements::MeasurementRead,
10 model::{FourierOffset, ImagePlaneModel, fftshift_copy, ifftshift_copy},
11 reconstruction::{
12 AdmmAuxiliaryState, AlgorithmAuxiliaryState, Batch, ReconstructionProblem,
13 ReconstructionState,
14 },
15};
16
17use super::ReconstructionAlgorithm;
18
19#[derive(Clone, Debug)]
52pub struct Admm {
53 pub iterations: usize,
55 pub object_step: f64,
57 pub penalty: f64,
60 pub dual_relaxation: f64,
62 pub batch_size: usize,
65 pub epsilon: f64,
67}
68
69impl Default for Admm {
70 fn default() -> Self {
71 Self {
72 iterations: 100,
73 object_step: 0.8,
74 penalty: 1.0,
75 dual_relaxation: 1.0,
76 batch_size: usize::MAX,
77 epsilon: 1e-10,
78 }
79 }
80}
81
82impl Admm {
83 pub fn iterations(mut self, iterations: usize) -> Self {
84 self.iterations = iterations;
85 self
86 }
87
88 pub fn object_step(mut self, step: f64) -> Self {
89 self.object_step = step;
90 self
91 }
92
93 pub fn penalty(mut self, penalty: f64) -> Self {
94 self.penalty = penalty;
95 self
96 }
97
98 pub fn dual_relaxation(mut self, relaxation: f64) -> Self {
99 self.dual_relaxation = relaxation;
100 self
101 }
102
103 pub fn batch_size(mut self, batch_size: usize) -> Self {
104 self.batch_size = batch_size;
105 self
106 }
107}
108
109impl ReconstructionAlgorithm for Admm {
110 fn validate(&self) -> Result<()> {
111 if !self.object_step.is_finite() || self.object_step <= 0.0 {
112 return Err(Error::InvalidParameter {
113 name: "object_step",
114 reason: "must be finite and positive".into(),
115 });
116 }
117 if !self.penalty.is_finite() || self.penalty <= 0.0 {
118 return Err(Error::InvalidParameter {
119 name: "penalty",
120 reason: "must be finite and positive".into(),
121 });
122 }
123 if !self.dual_relaxation.is_finite() || !(0.0..=2.0).contains(&self.dual_relaxation) {
124 return Err(Error::InvalidParameter {
125 name: "dual_relaxation",
126 reason: "must be finite and between zero and two".into(),
127 });
128 }
129 if self.batch_size == 0 || !self.epsilon.is_finite() || self.epsilon <= 0.0 {
130 return Err(Error::InvalidParameter {
131 name: "batch_size/epsilon",
132 reason: "batch size must be non-zero and epsilon finite and positive".into(),
133 });
134 }
135 Ok(())
136 }
137
138 fn step<M: MeasurementRead>(
139 &mut self,
140 problem: &ReconstructionProblem<M>,
141 state: &mut ReconstructionState,
142 batch: &Batch,
143 _iteration: usize,
144 ) -> Result<StepDiagnostics> {
145 let expected = admm_auxiliary_len(&problem.model)?;
146 let mut auxiliary = match state.algorithm_auxiliary.take() {
147 None => AdmmAuxiliaryState {
148 auxiliary_fields: vec![Complex64::default(); expected],
149 dual_fields: vec![Complex64::default(); expected],
150 },
151 Some(AlgorithmAuxiliaryState::Admm(auxiliary)) => auxiliary,
152 };
153 let result = self.step_with_auxiliary(problem, state, batch, &mut auxiliary);
154 state.algorithm_auxiliary = Some(AlgorithmAuxiliaryState::Admm(auxiliary));
155 result
156 }
157
158 fn iterations(&self) -> usize {
159 self.iterations
160 }
161
162 fn batch_size(&self) -> usize {
163 self.batch_size
164 }
165}
166
167impl Admm {
168 fn step_with_auxiliary<M: MeasurementRead>(
169 &self,
170 problem: &ReconstructionProblem<M>,
171 state: &mut ReconstructionState,
172 batch: &Batch,
173 auxiliary: &mut AdmmAuxiliaryState,
174 ) -> Result<StepDiagnostics> {
175 let model = &problem.model;
176 let shape = model.image_shape;
177 let image_len = shape.0 * shape.1;
178 let expected = admm_auxiliary_len(model)?;
179 if auxiliary.auxiliary_fields.len() != expected || auxiliary.dual_fields.len() != expected {
180 return Err(Error::InvalidModel(
181 "ADMM auxiliary state does not match the problem modes".into(),
182 ));
183 }
184 state
185 .scratch
186 .object_gradient
187 .resize(state.object_spectrum.len(), Complex64::default());
188 state.scratch.object_gradient.fill(Complex64::default());
189 let maximum_pupil_power = state
190 .pupil
191 .values
192 .as_slice()
193 .iter()
194 .map(|value| value.norm_sqr())
195 .fold(0.0, f64::max)
196 .max(self.epsilon);
197 let mut diagnostics = StepDiagnostics::default();
198 let mut active_frames = 0;
199
200 for &frame in &batch.indices {
201 let frame_weight = problem.measurements.frame_weight(frame)?;
202 if frame_weight == 0.0 {
203 diagnostics.push_frame(frame, 0.0, 0.0);
204 continue;
205 }
206 active_frames += 1;
207 let single_source = [(frame, 1.0)];
208 let sources = frame_sources(model, frame, &single_source);
209 let source_weight_sum: f64 = sources.iter().map(|&(_, weight)| weight).sum();
210 let mode_start = frame_mode_start(model, frame);
211 let multiplex_len = image_len * sources.len();
212 state
213 .scratch
214 .multiplex_fields
215 .resize(multiplex_len, Complex64::default());
216 state.scratch.multiplex_offsets.clear();
217 state.scratch.projected_field.fill(Complex64::default());
218
219 for (local_mode, &(source, source_weight)) in sources.iter().enumerate() {
222 let offset = state.effective_source_offset(model, source)?;
223 state.scratch.multiplex_offsets.push(offset);
224 compute_source_field(problem, state, source, offset)?;
225 let local_start = local_mode * image_len;
226 state.scratch.multiplex_fields[local_start..local_start + image_len]
227 .copy_from_slice(&state.scratch.field);
228 let auxiliary_start = (mode_start + local_mode) * image_len;
229 for pixel in 0..image_len {
230 let field = state.scratch.field[pixel];
231 let consensus = field + auxiliary.dual_fields[auxiliary_start + pixel];
232 state.scratch.projected_field[pixel].re += source_weight * field.norm_sqr();
233 state.scratch.projected_field[pixel].im += source_weight * consensus.norm_sqr();
234 }
235 }
236
237 let measured = problem.measurements.frame(frame)?;
238 let mask = problem.measurements.frame_mask(frame)?;
239 let gain = state
240 .frame_gains
241 .as_ref()
242 .map_or(1.0, |values| values[frame]);
243 if !gain.is_finite() || gain <= 0.0 {
244 return Err(Error::InvalidModel(format!(
245 "state frame {frame} has invalid gain {gain}"
246 )));
247 }
248 let mut frame_loss = 0.0;
249 let mut valid_pixels = 0;
250 for pixel in 0..image_len {
251 if mask.is_some_and(|values| values[pixel] == 0) {
252 continue;
253 }
254 valid_pixels += 1;
255 let background = background_value(state, frame, pixel, image_len);
256 let predicted = gain * state.scratch.projected_field[pixel].re + background;
257 frame_loss += point_loss(predicted, measured[pixel], LossType::AmplitudeMse);
258 }
259 if valid_pixels == 0 {
260 return Err(Error::InvalidMeasurements(format!(
261 "frame {frame} has no unmasked pixels"
262 )));
263 }
264 diagnostics.push_frame(frame, frame_loss / valid_pixels as f64, frame_weight);
265
266 for (local_mode, &(source, source_weight)) in sources.iter().enumerate() {
267 let local_start = local_mode * image_len;
268 state.scratch.field.copy_from_slice(
269 &state.scratch.multiplex_fields[local_start..local_start + image_len],
270 );
271 let auxiliary_start = (mode_start + local_mode) * image_len;
272 for pixel in 0..image_len {
273 let index = auxiliary_start + pixel;
274 if mask.is_some_and(|values| values[pixel] == 0) {
275 auxiliary.auxiliary_fields[index] = state.scratch.field[pixel];
276 auxiliary.dual_fields[index] = Complex64::default();
277 state.scratch.difference[pixel] = Complex64::default();
278 continue;
279 }
280 let background = background_value(state, frame, pixel, image_len);
281 let target = ((measured[pixel] - background) / gain).max(0.0).sqrt();
282 let consensus = state.scratch.field[pixel] + auxiliary.dual_fields[index];
283 let consensus_norm = state.scratch.projected_field[pixel].im;
284 let projected = if consensus_norm > self.epsilon {
285 consensus * (target / consensus_norm.sqrt())
286 } else {
287 Complex64::new(target / source_weight_sum.sqrt(), 0.0)
288 };
289 let auxiliary_value = (self.penalty * consensus + frame_weight * projected)
290 / (self.penalty + frame_weight);
291 diagnostics.push_admm_dual_change(
292 auxiliary_value - auxiliary.auxiliary_fields[index],
293 self.penalty,
294 );
295 auxiliary.auxiliary_fields[index] = auxiliary_value;
296 state.scratch.difference[pixel] =
297 auxiliary_value - auxiliary.dual_fields[index] - state.scratch.field[pixel];
298 }
299 state.backend.fft2(
300 &mut state.scratch.difference,
301 shape,
302 FftDirection::Forward,
303 &mut state.scratch.column,
304 )?;
305 fftshift_copy(
306 &state.scratch.difference,
307 &mut state.scratch.projected_spectrum,
308 shape,
309 );
310 for pixel in 0..image_len {
311 state.scratch.difference[pixel] = state.pupil.values.as_slice()[pixel].conj()
312 * state.scratch.projected_spectrum[pixel]
313 / (maximum_pupil_power + self.epsilon);
314 }
315 model.insert_patch_adjoint_slice_at_offset(
316 &mut state.scratch.object_gradient,
317 source,
318 &state.scratch.difference,
319 source_weight / source_weight_sum,
320 state.scratch.multiplex_offsets[local_mode],
321 )?;
322 }
323 }
324
325 if active_frames > 0 {
326 let step = self.object_step / active_frames as f64;
327 for (object, &update) in state
328 .object_spectrum
329 .as_mut_slice()
330 .iter_mut()
331 .zip(&state.scratch.object_gradient)
332 {
333 *object += step * update;
334 }
335 }
336
337 for &frame in &batch.indices {
338 if problem.measurements.frame_weight(frame)? == 0.0 {
339 continue;
340 }
341 let single_source = [(frame, 1.0)];
342 let sources = frame_sources(model, frame, &single_source);
343 let mode_start = frame_mode_start(model, frame);
344 let mask = problem.measurements.frame_mask(frame)?;
345 for (local_mode, &(source, _)) in sources.iter().enumerate() {
346 let offset = state.effective_source_offset(model, source)?;
347 compute_source_field(problem, state, source, offset)?;
348 let auxiliary_start = (mode_start + local_mode) * image_len;
349 for pixel in 0..image_len {
350 let index = auxiliary_start + pixel;
351 if mask.is_some_and(|values| values[pixel] == 0) {
352 auxiliary.auxiliary_fields[index] = state.scratch.field[pixel];
353 auxiliary.dual_fields[index] = Complex64::default();
354 } else {
355 let primal_residual =
356 state.scratch.field[pixel] - auxiliary.auxiliary_fields[index];
357 diagnostics.push_admm_primal_residual(primal_residual);
358 auxiliary.dual_fields[index] += self.dual_relaxation * primal_residual;
359 }
360 }
361 }
362 }
363 Ok(diagnostics)
364 }
365}
366
367pub(crate) fn admm_auxiliary_len(model: &ImagePlaneModel) -> Result<usize> {
368 let mode_count = model.multiplexing_matrix.as_ref().map_or_else(
369 || Ok(model.frame_count()),
370 |matrix| {
371 matrix.iter().try_fold(0_usize, |count, row| {
372 count
373 .checked_add(row.len())
374 .ok_or_else(|| Error::InvalidShape("ADMM source mode count overflows".into()))
375 })
376 },
377 )?;
378 (model.image_shape.0 * model.image_shape.1)
379 .checked_mul(mode_count)
380 .ok_or_else(|| Error::InvalidShape("ADMM auxiliary length overflows".into()))
381}
382
383fn frame_mode_start(model: &ImagePlaneModel, frame: usize) -> usize {
384 model
385 .multiplexing_matrix
386 .as_ref()
387 .map_or(frame, |matrix| matrix[..frame].iter().map(Vec::len).sum())
388}
389
390fn frame_sources<'a>(
391 model: &'a ImagePlaneModel,
392 frame: usize,
393 single_source: &'a [(usize, f64); 1],
394) -> &'a [(usize, f64)] {
395 model
396 .multiplexing_matrix
397 .as_ref()
398 .map_or(single_source, |matrix| matrix[frame].as_slice())
399}
400
401fn compute_source_field<M: MeasurementRead>(
402 problem: &ReconstructionProblem<M>,
403 state: &mut ReconstructionState,
404 source: usize,
405 offset: FourierOffset,
406) -> Result<()> {
407 let model = &problem.model;
408 let shape = model.image_shape;
409 model.extract_patch_at_offset(
410 &state.object_spectrum,
411 source,
412 offset,
413 &mut state.scratch.patch,
414 )?;
415 for pixel in 0..state.scratch.patch.len() {
416 state.scratch.exit_spectrum[pixel] =
417 state.scratch.patch[pixel] * state.pupil.values.as_slice()[pixel];
418 }
419 ifftshift_copy(
420 &state.scratch.exit_spectrum,
421 &mut state.scratch.field,
422 shape,
423 );
424 state.backend.fft2(
425 &mut state.scratch.field,
426 shape,
427 FftDirection::Inverse,
428 &mut state.scratch.column,
429 )
430}
431
432fn background_value(
433 state: &ReconstructionState,
434 frame: usize,
435 pixel: usize,
436 image_len: usize,
437) -> f64 {
438 state.background.as_ref().map_or(0.0, |values| {
439 values[if values.len() == image_len {
440 pixel
441 } else {
442 frame * image_len + pixel
443 }]
444 })
445}