1use std::{
9 fs::File,
10 io::{BufReader, BufWriter},
11 path::Path,
12};
13
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 Result,
18 array_layout::checked_len_2d,
19 error::Error,
20 experiment::{Illumination, Optics, ResolvedIllumination},
21 model::{ImagePlaneModel, ReconstructionShape},
22 simulation::{CameraModel, IlluminationAcquisitionErrors},
23};
24
25pub const CONFIGURATION_FORMAT_VERSION: u32 = 2;
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct ExperimentDescription {
32 pub optics: Optics,
34 pub illumination: Illumination,
36 pub optical_background: Option<Vec<f64>>,
38}
39
40impl ExperimentDescription {
41 pub fn new(optics: Optics, illumination: Illumination) -> Self {
43 Self {
44 optics,
45 illumination,
46 optical_background: None,
47 }
48 }
49
50 pub fn with_optical_background(mut self, background: Vec<f64>) -> Self {
55 self.optical_background = Some(background);
56 self
57 }
58
59 pub fn compile(
61 &self,
62 image_shape: (usize, usize),
63 reconstruction_shape: ReconstructionShape,
64 ) -> Result<ImagePlaneModel> {
65 let resolved = self.illumination.resolve(&self.optics)?;
66 self.compile_resolved(image_shape, reconstruction_shape, &resolved)
67 }
68
69 fn compile_resolved(
70 &self,
71 image_shape: (usize, usize),
72 reconstruction_shape: ReconstructionShape,
73 resolved: &ResolvedIllumination,
74 ) -> Result<ImagePlaneModel> {
75 let mut model =
76 ImagePlaneModel::compile(&self.optics, resolved, image_shape, reconstruction_shape)?;
77 model.background = self.optical_background.clone();
78 model.validate()?;
79 Ok(model)
80 }
81
82 pub fn validate(&self) -> Result<()> {
85 self.optics.validate()?;
86 self.illumination.resolve(&self.optics)?;
87 if self.optical_background.as_ref().is_some_and(|values| {
88 values
89 .iter()
90 .any(|value| !value.is_finite() || *value < 0.0)
91 }) {
92 return Err(Error::InvalidParameter {
93 name: "optical_background",
94 reason: "values must be finite and non-negative".into(),
95 });
96 }
97 Ok(())
98 }
99}
100
101#[derive(Clone, Debug, Serialize, Deserialize)]
103#[serde(deny_unknown_fields)]
104pub struct CompiledModelPair {
105 pub true_model: ImagePlaneModel,
107 pub reconstruction_model: ImagePlaneModel,
109}
110
111impl CompiledModelPair {
112 pub fn validate(&self) -> Result<()> {
114 self.true_model.validate()?;
115 self.reconstruction_model.validate()?;
116 if self.true_model.image_shape != self.reconstruction_model.image_shape
117 || self.true_model.reconstruction_shape
118 != self.reconstruction_model.reconstruction_shape
119 || self.true_model.frame_count() != self.reconstruction_model.frame_count()
120 {
121 return Err(Error::InvalidModel(
122 "compiled true and reconstruction models must have matching image, object, and frame dimensions"
123 .into(),
124 ));
125 }
126 Ok(())
127 }
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct SimulationConfiguration {
134 pub format_version: u32,
136 pub true_experiment: ExperimentDescription,
138 pub reconstruction_experiment: ExperimentDescription,
140 pub image_shape: (usize, usize),
142 pub reconstruction_shape: (usize, usize),
144 pub compiled_models: CompiledModelPair,
146 pub camera: Option<CameraModel>,
148 pub illumination_acquisition_errors: Option<IlluminationAcquisitionErrors>,
150 pub random_seed: u64,
152}
153
154impl SimulationConfiguration {
155 pub fn new(
158 true_experiment: ExperimentDescription,
159 reconstruction_experiment: ExperimentDescription,
160 image_shape: (usize, usize),
161 reconstruction_shape: ReconstructionShape,
162 ) -> Result<Self> {
163 let true_resolved = true_experiment
164 .illumination
165 .resolve(&true_experiment.optics)?;
166 let reconstruction_resolved = reconstruction_experiment
167 .illumination
168 .resolve(&reconstruction_experiment.optics)?;
169 let true_bounds = ImagePlaneModel::crop_displacement_bounds(
170 &true_experiment.optics,
171 image_shape,
172 true_resolved.k_vectors(),
173 )?;
174 let reconstruction_bounds = ImagePlaneModel::crop_displacement_bounds(
175 &reconstruction_experiment.optics,
176 image_shape,
177 reconstruction_resolved.k_vectors(),
178 )?;
179 let reconstruction_shape = ImagePlaneModel::resolve_reconstruction_shape(
180 image_shape,
181 reconstruction_shape,
182 &[true_bounds, reconstruction_bounds],
183 )?;
184 let compiled_models = CompiledModelPair {
185 true_model: true_experiment.compile_resolved(
186 image_shape,
187 ReconstructionShape::Exact(reconstruction_shape),
188 &true_resolved,
189 )?,
190 reconstruction_model: reconstruction_experiment.compile_resolved(
191 image_shape,
192 ReconstructionShape::Exact(reconstruction_shape),
193 &reconstruction_resolved,
194 )?,
195 };
196 let configuration = Self {
197 format_version: CONFIGURATION_FORMAT_VERSION,
198 true_experiment,
199 reconstruction_experiment,
200 image_shape,
201 reconstruction_shape,
202 compiled_models,
203 camera: None,
204 illumination_acquisition_errors: None,
205 random_seed: 0,
206 };
207 configuration.validate()?;
208 Ok(configuration)
209 }
210
211 pub fn with_camera(mut self, camera: CameraModel) -> Result<Self> {
213 self.camera = Some(camera);
214 self.validate()?;
215 Ok(self)
216 }
217
218 pub fn with_illumination_acquisition_errors(
220 mut self,
221 errors: IlluminationAcquisitionErrors,
222 ) -> Result<Self> {
223 self.illumination_acquisition_errors = Some(errors);
224 self.validate()?;
225 Ok(self)
226 }
227
228 pub fn with_random_seed(mut self, random_seed: u64) -> Self {
230 self.random_seed = random_seed;
231 self
232 }
233
234 pub fn reconstruction_model_for_counts(&self) -> Result<ImagePlaneModel> {
241 match &self.camera {
242 Some(camera) => camera
243 .compile_reconstruction_model(self.compiled_models.reconstruction_model.clone()),
244 None => Ok(self.compiled_models.reconstruction_model.clone()),
245 }
246 }
247
248 pub fn validate(&self) -> Result<()> {
251 if self.format_version != CONFIGURATION_FORMAT_VERSION {
252 return Err(Error::InvalidParameter {
253 name: "configuration format_version",
254 reason: format!(
255 "expected {CONFIGURATION_FORMAT_VERSION}, got {}",
256 self.format_version
257 ),
258 });
259 }
260 self.true_experiment.validate()?;
261 self.reconstruction_experiment.validate()?;
262 self.compiled_models.validate()?;
263 validate_compiled_description(
264 &self.true_experiment,
265 &self.compiled_models.true_model,
266 self.image_shape,
267 self.reconstruction_shape,
268 )?;
269 validate_compiled_description(
270 &self.reconstruction_experiment,
271 &self.compiled_models.reconstruction_model,
272 self.image_shape,
273 self.reconstruction_shape,
274 )?;
275 if let Some(camera) = &self.camera {
276 camera.validate_for_frame(checked_len_2d(self.image_shape)?)?;
277 }
278 if let Some(errors) = &self.illumination_acquisition_errors {
279 validate_acquisition_errors(errors, &self.compiled_models.true_model)?;
280 }
281 Ok(())
282 }
283
284 pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
286 self.validate()?;
287 let writer = BufWriter::new(File::create(path)?);
288 serde_json::to_writer_pretty(writer, self)?;
289 Ok(())
290 }
291
292 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
294 let reader = BufReader::new(File::open(path)?);
295 let configuration: Self = serde_json::from_reader(reader)?;
296 configuration.validate()?;
297 Ok(configuration)
298 }
299}
300
301fn validate_compiled_description(
302 description: &ExperimentDescription,
303 stored: &ImagePlaneModel,
304 image_shape: (usize, usize),
305 reconstruction_shape: (usize, usize),
306) -> Result<()> {
307 if stored.image_shape != image_shape || stored.reconstruction_shape != reconstruction_shape {
308 return Err(Error::InvalidModel(
309 "compiled model dimensions differ from configuration dimensions".into(),
310 ));
311 }
312 let expected = description.compile(
313 image_shape,
314 ReconstructionShape::Exact(reconstruction_shape),
315 )?;
316 if expected.frame_count() != stored.frame_count()
317 || expected.source_count() != stored.source_count()
318 || expected.crop_indices.crops != stored.crop_indices.crops
319 || expected.pupil.support != stored.pupil.support
320 || !complex_values_close(
321 expected.pupil.values.as_slice(),
322 stored.pupil.values.as_slice(),
323 )
324 || !vectors_close(&expected.k_vectors, &stored.k_vectors)
325 || !optional_values_close(
326 expected.frame_gains.as_deref(),
327 stored.frame_gains.as_deref(),
328 )
329 || !optional_values_close(expected.background.as_deref(), stored.background.as_deref())
330 || !multiplexing_close(
331 expected.multiplexing_matrix.as_deref(),
332 stored.multiplexing_matrix.as_deref(),
333 )
334 || !sampling_close(&expected.sampling, &stored.sampling)
335 || !offsets_close(
336 expected.subpixel_offsets.as_deref(),
337 stored.subpixel_offsets.as_deref(),
338 )
339 {
340 return Err(Error::InvalidModel(
341 "compiled model is inconsistent with its experiment description".into(),
342 ));
343 }
344 Ok(())
345}
346
347fn complex_values_close(expected: &[crate::Complex64], actual: &[crate::Complex64]) -> bool {
348 expected.len() == actual.len()
349 && expected.iter().zip(actual).all(|(expected, actual)| {
350 close(expected.re, actual.re) && close(expected.im, actual.im)
351 })
352}
353
354fn optional_values_close(expected: Option<&[f64]>, actual: Option<&[f64]>) -> bool {
355 match (expected, actual) {
356 (None, None) => true,
357 (Some(expected), Some(actual)) => {
358 expected.len() == actual.len()
359 && expected
360 .iter()
361 .zip(actual)
362 .all(|(&expected, &actual)| close(expected, actual))
363 }
364 _ => false,
365 }
366}
367
368fn multiplexing_close(
369 expected: Option<&[Vec<crate::experiment::SourceWeight>]>,
370 actual: Option<&[Vec<crate::experiment::SourceWeight>]>,
371) -> bool {
372 match (expected, actual) {
373 (None, None) => true,
374 (Some(expected), Some(actual)) => {
375 expected.len() == actual.len()
376 && expected.iter().zip(actual).all(|(expected, actual)| {
377 expected.len() == actual.len()
378 && expected.iter().zip(actual).all(
379 |(
380 &(expected_source, expected_weight),
381 &(actual_source, actual_weight),
382 )| {
383 expected_source == actual_source
384 && close(expected_weight, actual_weight)
385 },
386 )
387 })
388 }
389 _ => false,
390 }
391}
392
393fn sampling_close(expected: &crate::model::Sampling, actual: &crate::model::Sampling) -> bool {
394 expected.coordinate_convention == actual.coordinate_convention
395 && close(expected.low_res_pixel_size, actual.low_res_pixel_size)
396 && close(expected.high_res_pixel_size, actual.high_res_pixel_size)
397 && close(expected.dkx, actual.dkx)
398 && close(expected.dky, actual.dky)
399 && optional_scalar_close(expected.wavelength, actual.wavelength)
400 && optional_scalar_close(expected.synthetic_na, actual.synthetic_na)
401}
402
403fn optional_scalar_close(expected: Option<f64>, actual: Option<f64>) -> bool {
404 match (expected, actual) {
405 (None, None) => true,
406 (Some(expected), Some(actual)) => close(expected, actual),
407 _ => false,
408 }
409}
410
411fn vectors_close(
412 expected: &[crate::experiment::KVector],
413 actual: &[crate::experiment::KVector],
414) -> bool {
415 expected.len() == actual.len()
416 && expected.iter().zip(actual).all(|(expected, actual)| {
417 close(expected.kx, actual.kx) && close(expected.ky, actual.ky)
418 })
419}
420
421fn offsets_close(
422 expected: Option<&[crate::model::FourierOffset]>,
423 actual: Option<&[crate::model::FourierOffset]>,
424) -> bool {
425 match (expected, actual) {
426 (None, None) => true,
427 (Some(expected), Some(actual)) => {
428 expected.len() == actual.len()
429 && expected.iter().zip(actual).all(|(expected, actual)| {
430 close(expected.row, actual.row) && close(expected.column, actual.column)
431 })
432 }
433 _ => false,
434 }
435}
436
437fn close(expected: f64, actual: f64) -> bool {
438 (expected - actual).abs() <= 1e-12 * expected.abs().max(actual.abs()).max(1.0)
439}
440
441fn validate_acquisition_errors(
442 errors: &IlluminationAcquisitionErrors,
443 model: &ImagePlaneModel,
444) -> Result<()> {
445 if !errors.frame_gain_relative_std.is_finite() || errors.frame_gain_relative_std < 0.0 {
446 return Err(Error::InvalidParameter {
447 name: "frame_gain_relative_std",
448 reason: "must be finite and non-negative".into(),
449 });
450 }
451 let mut missing = errors.missing_frames.clone();
452 missing.sort_unstable();
453 if missing.iter().any(|&frame| frame >= model.frame_count())
454 || missing.windows(2).any(|pair| pair[0] == pair[1])
455 {
456 return Err(Error::InvalidParameter {
457 name: "missing_frames",
458 reason: format!(
459 "must contain unique frame indices below {}",
460 model.frame_count()
461 ),
462 });
463 }
464 if let Some(permutation) = &errors.source_permutation {
465 let mut sorted = permutation.clone();
466 sorted.sort_unstable();
467 if permutation.len() != model.source_count()
468 || sorted
469 .iter()
470 .enumerate()
471 .any(|(expected, &actual)| expected != actual)
472 {
473 return Err(Error::InvalidParameter {
474 name: "source_permutation",
475 reason: format!("must be a permutation of 0..{}", model.source_count()),
476 });
477 }
478 }
479 Ok(())
480}