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