1use ndarray::{Array2, ArrayView2, ArrayViewMut2};
2use num_complex::Complex64;
3use rand::{Rng, SeedableRng, rngs::StdRng};
4use rand_distr::{Distribution, Normal};
5use std::path::Path;
6
7use crate::{
8 Result,
9 array_layout::{StandardArray2, checked_len_2d},
10 complex,
11 error::Error,
12 image_io::{GrayscaleScaling, load_grayscale},
13};
14
15#[derive(Clone, Debug)]
36pub struct SyntheticObject {
37 pub(crate) field: StandardArray2<Complex64>,
38 label: Option<String>,
39}
40
41impl SyntheticObject {
42 pub fn new(field: Array2<Complex64>) -> Result<Self> {
46 let field = StandardArray2::try_from(field)?;
47 validate_shape(field.dim())?;
48 if field
49 .as_slice()
50 .iter()
51 .any(|value| !value.re.is_finite() || !value.im.is_finite())
52 {
53 return Err(Error::InvalidParameter {
54 name: "field",
55 reason: "values must have finite real and imaginary components".into(),
56 });
57 }
58 Ok(Self { field, label: None })
59 }
60
61 pub fn from_amplitude_phase(
65 amplitude: ArrayView2<'_, f64>,
66 phase: ArrayView2<'_, f64>,
67 ) -> Result<Self> {
68 Self::new(complex::from_amplitude_phase(amplitude, phase)?)
69 }
70
71 pub fn amplitude_only(amplitude: Array2<f64>) -> Result<Self> {
73 let amplitude = StandardArray2::try_from(amplitude)?;
74 if amplitude
75 .as_slice()
76 .iter()
77 .any(|value| !value.is_finite() || *value < 0.0)
78 {
79 return Err(Error::InvalidParameter {
80 name: "amplitude",
81 reason: "values must be finite and non-negative".into(),
82 });
83 }
84 Self::from_values(
85 amplitude.dim(),
86 amplitude
87 .as_slice()
88 .iter()
89 .map(|&value| Complex64::new(value, 0.0))
90 .collect(),
91 )
92 }
93
94 pub fn phase_only(phase: Array2<f64>) -> Result<Self> {
96 let phase = StandardArray2::try_from(phase)?;
97 if phase.as_slice().iter().any(|value| !value.is_finite()) {
98 return Err(Error::InvalidParameter {
99 name: "phase",
100 reason: "values must be finite".into(),
101 });
102 }
103 Self::from_values(
104 phase.dim(),
105 phase
106 .as_slice()
107 .iter()
108 .map(|&value| Complex64::from_polar(1.0, value))
109 .collect(),
110 )
111 }
112
113 pub fn from_amplitude_image(path: impl AsRef<Path>) -> Result<Self> {
115 let amplitude = load_grayscale(path, GrayscaleScaling::Unit)?;
116 Self::amplitude_only(amplitude)
117 }
118
119 pub fn from_amplitude_phase_images(
122 amplitude_path: impl AsRef<Path>,
123 phase_path: impl AsRef<Path>,
124 phase_extent: f64,
125 ) -> Result<Self> {
126 if !phase_extent.is_finite() || phase_extent <= 0.0 {
127 return Err(Error::InvalidParameter {
128 name: "phase_extent",
129 reason: "must be finite and positive".into(),
130 });
131 }
132 let amplitude = load_grayscale(amplitude_path, GrayscaleScaling::Unit)?;
133 let normalized_phase = load_grayscale(phase_path, GrayscaleScaling::Unit)?;
134 if amplitude.dim() != normalized_phase.dim() {
135 return Err(Error::InvalidShape(format!(
136 "amplitude image shape {:?} differs from phase image shape {:?}",
137 amplitude.dim(),
138 normalized_phase.dim()
139 )));
140 }
141 let phase = normalized_phase.mapv(|value| (2.0 * value - 1.0) * phase_extent);
142 Self::from_amplitude_phase(amplitude.view(), phase.view())
143 }
144
145 pub fn constant(shape: (usize, usize), amplitude: f64, phase: f64) -> Result<Self> {
147 validate_shape(shape)?;
148 validate_amplitude(amplitude)?;
149 let length = checked_len_2d(shape)?;
150 Self::from_values(shape, vec![Complex64::from_polar(amplitude, phase); length])
151 }
152
153 pub fn phase_disk(shape: (usize, usize), radius_pixels: f64, phase_shift: f64) -> Result<Self> {
157 validate_shape(shape)?;
158 if !radius_pixels.is_finite() || radius_pixels <= 0.0 || !phase_shift.is_finite() {
159 return Err(Error::InvalidParameter {
160 name: "phase disk",
161 reason: "radius must be positive and phase must be finite".into(),
162 });
163 }
164 let center = ((shape.0 - 1) as f64 / 2.0, (shape.1 - 1) as f64 / 2.0);
165 let mut values = Vec::with_capacity(checked_len_2d(shape)?);
166 for row in 0..shape.0 {
167 for column in 0..shape.1 {
168 let radius = (row as f64 - center.0).hypot(column as f64 - center.1);
169 values.push(Complex64::from_polar(
170 1.0,
171 if radius <= radius_pixels {
172 phase_shift
173 } else {
174 0.0
175 },
176 ));
177 }
178 }
179 Self::from_values(shape, values)
180 }
181
182 pub fn siemens_star(shape: (usize, usize), spokes: usize) -> Result<Self> {
184 validate_shape(shape)?;
185 if spokes < 2 {
186 return Err(Error::InvalidParameter {
187 name: "spokes",
188 reason: "must be at least 2".into(),
189 });
190 }
191 let center = ((shape.0 - 1) as f64 / 2.0, (shape.1 - 1) as f64 / 2.0);
192 let maximum_radius = shape.0.min(shape.1) as f64 * 0.46;
193 let mut values = Vec::with_capacity(checked_len_2d(shape)?);
194 for row in 0..shape.0 {
195 for column in 0..shape.1 {
196 let y = row as f64 - center.0;
197 let x = column as f64 - center.1;
198 let radius = x.hypot(y);
199 let amplitude =
200 if radius <= maximum_radius && ((x.atan2(y) * spokes as f64).sin() >= 0.0) {
201 0.25
202 } else {
203 1.0
204 };
205 values.push(Complex64::new(amplitude, 0.0));
206 }
207 }
208 Self::from_values(shape, values)
209 }
210
211 pub fn resolution_target(shape: (usize, usize)) -> Result<Self> {
213 validate_shape(shape)?;
214 let mut values = vec![Complex64::new(1.0, 0.0); checked_len_2d(shape)?];
215 let groups = [2_usize, 3, 4, 6, 8];
216 for (group, &period) in groups.iter().enumerate() {
217 let top = group * shape.0 / groups.len();
218 let bottom = (group + 1) * shape.0 / groups.len();
219 for row in top..bottom {
220 for column in 0..shape.1 {
221 let dark = if group.is_multiple_of(2) {
222 (column / period).is_multiple_of(2)
223 } else {
224 ((row - top) / period).is_multiple_of(2)
225 };
226 if dark {
227 values[row * shape.1 + column] = Complex64::new(0.2, 0.0);
228 }
229 }
230 }
231 }
232 Self::from_values(shape, values)
233 }
234
235 pub fn random_phase(shape: (usize, usize), standard_deviation: f64, seed: u64) -> Result<Self> {
237 validate_shape(shape)?;
238 if !standard_deviation.is_finite() || standard_deviation < 0.0 {
239 return Err(Error::InvalidParameter {
240 name: "standard_deviation",
241 reason: "must be finite and non-negative".into(),
242 });
243 }
244 let mut rng = StdRng::seed_from_u64(seed);
245 let length = checked_len_2d(shape)?;
246 let values = if standard_deviation == 0.0 {
247 vec![Complex64::new(1.0, 0.0); length]
248 } else {
249 let distribution =
250 Normal::new(0.0, standard_deviation).map_err(|error| Error::InvalidParameter {
251 name: "standard_deviation",
252 reason: error.to_string(),
253 })?;
254 (0..length)
255 .map(|_| Complex64::from_polar(1.0, distribution.sample(&mut rng)))
256 .collect()
257 };
258 Self::from_values(shape, values)
259 }
260
261 pub fn particle_field(shape: (usize, usize), particles: usize, seed: u64) -> Result<Self> {
263 validate_shape(shape)?;
264 let mut values = vec![Complex64::new(1.0, 0.0); checked_len_2d(shape)?];
265 let mut rng = StdRng::seed_from_u64(seed);
266 for _ in 0..particles {
267 let row = rng.random_range(0..shape.0);
268 let column = rng.random_range(0..shape.1);
269 values[row * shape.1 + column] = Complex64::new(0.1, 0.0);
270 }
271 Self::from_values(shape, values)
272 }
273
274 pub fn mixed_test_pattern(shape: (usize, usize)) -> Result<Self> {
276 validate_shape(shape)?;
277 let center = ((shape.0 - 1) as f64 / 2.0, (shape.1 - 1) as f64 / 2.0);
278 let scale = shape.0.min(shape.1) as f64;
279 let mut values = Vec::with_capacity(checked_len_2d(shape)?);
280 for row in 0..shape.0 {
281 for column in 0..shape.1 {
282 let y = row as f64 - center.0;
283 let x = column as f64 - center.1;
284 let radius = x.hypot(y);
285 let amplitude = if (x.abs() < 0.12 * scale && y.abs() < 0.35 * scale)
286 || radius < 0.12 * scale
287 {
288 0.45
289 } else {
290 1.0
291 };
292 let phase = 0.8 * (-radius * radius / (0.08 * scale * scale)).exp()
293 + 0.25 * (std::f64::consts::TAU * x / scale).sin();
294 values.push(Complex64::from_polar(amplitude, phase));
295 }
296 }
297 Self::from_values(shape, values)
298 }
299
300 pub fn biological_like(shape: (usize, usize), features: usize, seed: u64) -> Result<Self> {
303 validate_shape(shape)?;
304 if features == 0 {
305 return Err(Error::InvalidParameter {
306 name: "features",
307 reason: "must be greater than zero".into(),
308 });
309 }
310 let mut rng = StdRng::seed_from_u64(seed);
311 let minimum_size = shape.0.min(shape.1) as f64;
312 let lower_sigma = (0.02 * minimum_size).max(1.0);
313 let upper_sigma = (0.12 * minimum_size).max(lower_sigma + 0.1);
314 let blobs: Vec<_> = (0..features)
315 .map(|_| {
316 (
317 rng.random_range(0.0..shape.0 as f64),
318 rng.random_range(0.0..shape.1 as f64),
319 rng.random_range(lower_sigma..upper_sigma),
320 rng.random_range(-0.4..1.0),
321 rng.random_range(0.0..0.25),
322 )
323 })
324 .collect();
325 let mut values = Vec::with_capacity(checked_len_2d(shape)?);
326 for row in 0..shape.0 {
327 for column in 0..shape.1 {
328 let mut phase = 0.0;
329 let mut absorption = 0.0;
330 for &(center_row, center_column, sigma, phase_strength, absorption_strength) in
331 &blobs
332 {
333 let squared_radius =
334 (row as f64 - center_row).powi(2) + (column as f64 - center_column).powi(2);
335 let profile = (-squared_radius / (2.0 * sigma * sigma)).exp();
336 phase += phase_strength * profile;
337 absorption += absorption_strength * profile;
338 }
339 values.push(Complex64::from_polar(
340 (1.0_f64 - absorption).max(0.1),
341 phase,
342 ));
343 }
344 }
345 Self::from_values(shape, values)
346 }
347
348 pub fn shape(&self) -> (usize, usize) {
350 self.field.dim()
351 }
352
353 pub fn field(&self) -> ArrayView2<'_, Complex64> {
355 self.field.ndarray_view()
356 }
357
358 pub fn field_mut(&mut self) -> ArrayViewMut2<'_, Complex64> {
360 self.field.ndarray_view_mut()
361 }
362
363 pub fn label(&self) -> Option<&str> {
365 self.label.as_deref()
366 }
367
368 pub fn with_label(mut self, label: impl Into<String>) -> Self {
370 self.label = Some(label.into());
371 self
372 }
373
374 fn from_values(shape: (usize, usize), values: Vec<Complex64>) -> Result<Self> {
375 Ok(Self {
376 field: StandardArray2::from_shape_vec(shape, values)?,
377 label: None,
378 })
379 }
380}
381
382impl TryFrom<Array2<Complex64>> for SyntheticObject {
383 type Error = Error;
384
385 fn try_from(field: Array2<Complex64>) -> Result<Self> {
386 Self::new(field)
387 }
388}
389
390fn validate_amplitude(amplitude: f64) -> Result<()> {
391 if !amplitude.is_finite() || amplitude < 0.0 {
392 Err(Error::InvalidParameter {
393 name: "amplitude",
394 reason: "must be finite and non-negative".into(),
395 })
396 } else {
397 Ok(())
398 }
399}
400
401fn validate_shape(shape: (usize, usize)) -> Result<()> {
402 if shape.0 == 0 || shape.1 == 0 {
403 Err(Error::InvalidShape(format!(
404 "synthetic object dimensions must be non-zero, got {shape:?}"
405 )))
406 } else {
407 Ok(())
408 }
409}