Skip to main content

goad/
settings.rs

1pub mod cli;
2pub mod constants;
3pub mod loading;
4pub mod validation;
5
6use nalgebra::Complex;
7use pyo3::prelude::*;
8#[cfg(feature = "stub-gen")]
9use pyo3_stub_gen::derive::*;
10use serde::{Deserialize, Serialize};
11use std::path::PathBuf;
12
13use crate::bins::{self, BinningScheme};
14use crate::diff::Mapping;
15use crate::orientation::Euler;
16use crate::orientation::*;
17use crate::zones::ZoneConfig;
18
19/// Provides a default empty zones vec.
20fn default_zones() -> Vec<ZoneConfig> {
21    vec![]
22}
23
24/// Configuration for output file generation
25#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
26pub struct OutputConfig {
27    /// Enable writing of settings.json configuration file
28    pub settings_json: bool,
29    /// Enable writing of 2D Mueller matrix files
30    pub mueller_2d: bool,
31    /// Enable writing of 1D integrated Mueller matrix files
32    pub mueller_1d: bool,
33    /// Enable writing of specific Mueller components
34    pub mueller_components: MuellerComponentConfig,
35}
36
37/// Configuration for Mueller matrix component outputs
38#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
39pub struct MuellerComponentConfig {
40    /// Enable total scattering component output
41    pub total: bool,
42    /// Enable beam component output
43    pub beam: bool,
44    /// Enable external diffraction component output
45    pub external: bool,
46}
47
48// Re-export constants, defaults, and loading functions for backward compatibility
49pub use self::constants::*;
50pub use self::loading::{load_config, load_config_with_cli, load_default_config};
51
52/// Runtime configuration for the application.
53#[cfg_attr(feature = "stub-gen", gen_stub_pyclass)]
54#[pyclass(module = "goad._goad")]
55#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
56pub struct Settings {
57    pub wavelength: f32,
58    pub beam_power_threshold: f32,
59    pub beam_area_threshold_fac: f32,
60    pub cutoff: f32,
61    pub medium_refr_index: Complex<f32>,
62    pub particle_refr_index: Vec<Complex<f32>>,
63    pub orientation: Orientation,
64    pub geom_name: String,
65    pub max_rec: i32,
66    pub max_tir: i32,
67    /// Zones for binning (new format). Takes precedence over `binning`.
68    #[serde(default = "default_zones")]
69    pub zones: Vec<ZoneConfig>,
70    /// Legacy binning field (deprecated, use `zones` instead).
71    /// If present and zones is empty, this will be converted to a single zone.
72    #[serde(default, skip_serializing)]
73    pub binning: Option<BinningScheme>,
74    pub seed: Option<u64>,
75    /// Problem scaling factor - scales the entire problem (geometry, wavelength, and beam area thresholds)
76    #[serde(default = "constants::default_scale_factor")]
77    pub scale: f32,
78    pub distortion: Option<f32>,
79    /// Per-axis geometry scaling [x, y, z] - scales only the geometry in each dimension
80    #[serde(default = "constants::default_geom_scale")]
81    pub geom_scale: Option<Vec<f32>>,
82    #[serde(default = "constants::default_directory")]
83    pub directory: PathBuf,
84    #[serde(default = "constants::default_fov_factor")]
85    pub fov_factor: Option<f32>,
86    pub mapping: Mapping,
87    #[serde(default = "constants::default_output_config")]
88    pub output: OutputConfig,
89    pub coherence: bool,
90    /// Suppress progress bars and status messages
91    #[serde(default = "constants::default_quiet")]
92    pub quiet: bool,
93}
94
95#[cfg_attr(feature = "stub-gen", gen_stub_pymethods)]
96#[pymethods]
97impl Settings {
98    #[new]
99    #[pyo3(signature = (
100        geom_path,
101        wavelength = DEFAULT_WAVELENGTH,
102        particle_refr_index_re = DEFAULT_PARTICLE_REFR_INDEX_RE,
103        particle_refr_index_im = DEFAULT_PARTICLE_REFR_INDEX_IM,
104        medium_refr_index_re = DEFAULT_MEDIUM_REFR_INDEX_RE,
105        medium_refr_index_im = DEFAULT_MEDIUM_REFR_INDEX_IM,
106        orientation = None,
107        zones = None,
108        beam_power_threshold = DEFAULT_BEAM_POWER_THRESHOLD,
109        beam_area_threshold_fac = DEFAULT_BEAM_AREA_THRESHOLD_FAC,
110        cutoff = DEFAULT_CUTOFF,
111        max_rec = DEFAULT_MAX_REC,
112        max_tir = DEFAULT_MAX_TIR,
113        scale = 1.0,
114        distortion = None,
115        directory = "goad_run",
116        mapping = DEFAULT_MAPPING,
117        coherence = DEFAULT_COHERENCE,
118        quiet = DEFAULT_QUIET,
119        seed = None,
120    ))]
121    fn py_new(
122        geom_path: String,
123        wavelength: f32,
124        particle_refr_index_re: f32,
125        particle_refr_index_im: f32,
126        medium_refr_index_re: f32,
127        medium_refr_index_im: f32,
128        orientation: Option<Orientation>,
129        zones: Option<Vec<ZoneConfig>>,
130        beam_power_threshold: f32,
131        beam_area_threshold_fac: f32,
132        cutoff: f32,
133        max_rec: i32,
134        max_tir: i32,
135        scale: f32,
136        distortion: Option<f32>,
137        directory: &str,
138        mapping: Mapping,
139        coherence: bool,
140        quiet: bool,
141        seed: Option<u64>,
142    ) -> PyResult<Self> {
143        // Input validation
144        if wavelength <= 0.0 {
145            return Err(pyo3::exceptions::PyValueError::new_err(format!(
146                "Wavelength must be positive, got: {}",
147                wavelength
148            )));
149        }
150
151        if !std::path::Path::new(&geom_path).exists() {
152            return Err(pyo3::exceptions::PyFileNotFoundError::new_err(format!(
153                "Geometry file not found: {}",
154                geom_path
155            )));
156        }
157
158        if cutoff < 0.0 || cutoff > 1.0 {
159            return Err(pyo3::exceptions::PyValueError::new_err(format!(
160                "Cutoff must be between 0 and 1, got: {}",
161                cutoff
162            )));
163        }
164
165        if max_rec < 0 {
166            return Err(pyo3::exceptions::PyValueError::new_err(format!(
167                "max_rec must be non-negative, got: {}",
168                max_rec
169            )));
170        }
171
172        if max_tir < 0 {
173            return Err(pyo3::exceptions::PyValueError::new_err(format!(
174                "max_tir must be non-negative, got: {}",
175                max_tir
176            )));
177        }
178        // Create default orientation if none provided (single Sobol orientation)
179        let orientation = orientation.unwrap_or_else(|| Orientation {
180            scheme: Scheme::Sobol { num_orients: 1 },
181            euler_convention: DEFAULT_EULER_ORDER,
182        });
183
184        // Use provided zones or create default (single full zone with interval binning)
185        let zones = zones.unwrap_or_else(|| {
186            vec![ZoneConfig::new(bins::Scheme::Interval {
187                thetas: vec![0.0, 5.0, 175.0, 179.0, 180.0],
188                theta_spacings: vec![0.1, 2.0, 0.5, 0.1],
189                phis: vec![0.0, 360.0],
190                phi_spacings: vec![7.5],
191            })]
192        });
193
194        let mut settings = Settings {
195            wavelength,
196            beam_power_threshold,
197            beam_area_threshold_fac,
198            cutoff,
199            medium_refr_index: Complex::new(medium_refr_index_re, medium_refr_index_im),
200            particle_refr_index: vec![Complex::new(particle_refr_index_re, particle_refr_index_im)],
201            orientation,
202            geom_name: geom_path,
203            max_rec,
204            max_tir,
205            zones,
206            binning: None,
207            seed,
208            scale,
209            distortion,
210            geom_scale: None,
211            directory: PathBuf::from(directory),
212            fov_factor: None,
213            mapping,
214            output: constants::default_output_config(),
215            coherence,
216            quiet,
217        };
218
219        validation::validate_config(&mut settings);
220
221        Ok(settings)
222    }
223
224    /// Set the euler angles
225    #[setter]
226    fn set_eulers(&mut self, euler: Vec<f32>) {
227        self.orientation = Orientation {
228            scheme: Scheme::Discrete {
229                eulers: vec![Euler::new(euler[0], euler[1], euler[2])],
230            },
231            euler_convention: EulerConvention::XYZ,
232        };
233    }
234
235    /// Get the euler angle, assuming the orientation scheme is discrete
236    #[getter]
237    fn get_eulers(&self) -> Vec<f32> {
238        match &self.orientation.scheme {
239            Scheme::Discrete { eulers } => vec![eulers[0].alpha, eulers[0].beta, eulers[0].gamma],
240            _ => vec![0.0, 0.0, 0.0],
241        }
242    }
243
244    /// Set the full orientation object
245    #[setter]
246    fn set_orientation(&mut self, orientation: Orientation) {
247        self.orientation = orientation;
248    }
249
250    /// Get the full orientation object
251    #[getter]
252    fn get_orientation(&self) -> Orientation {
253        self.orientation.clone()
254    }
255
256    /// Set the geometry file path
257    #[setter]
258    fn set_geom_path(&mut self, geom_path: String) {
259        self.geom_name = geom_path;
260    }
261
262    /// Get the geometry file path
263    #[getter]
264    fn get_geom_path(&self) -> String {
265        self.geom_name.clone()
266    }
267
268    /// Set the wavelength
269    #[setter]
270    fn set_wavelength(&mut self, wavelength: f32) {
271        self.wavelength = wavelength;
272    }
273
274    /// Get the wavelength
275    #[getter]
276    fn get_wavelength(&self) -> f32 {
277        self.wavelength
278    }
279
280    /// Set the particle refractive index (real part)
281    #[setter]
282    fn set_particle_refr_index_re(&mut self, re: f32) {
283        if !self.particle_refr_index.is_empty() {
284            self.particle_refr_index[0].re = re;
285        }
286    }
287
288    /// Get the particle refractive index (real part)
289    #[getter]
290    fn get_particle_refr_index_re(&self) -> f32 {
291        if !self.particle_refr_index.is_empty() {
292            self.particle_refr_index[0].re
293        } else {
294            0.0
295        }
296    }
297
298    /// Set the particle refractive index (imaginary part)
299    #[setter]
300    fn set_particle_refr_index_im(&mut self, im: f32) {
301        if !self.particle_refr_index.is_empty() {
302            self.particle_refr_index[0].im = im;
303        }
304    }
305
306    /// Get the particle refractive index (imaginary part)
307    #[getter]
308    fn get_particle_refr_index_im(&self) -> f32 {
309        if !self.particle_refr_index.is_empty() {
310            self.particle_refr_index[0].im
311        } else {
312            0.0
313        }
314    }
315
316    /// Set the medium refractive index (real part)
317    #[setter]
318    fn set_medium_refr_index_re(&mut self, re: f32) {
319        self.medium_refr_index.re = re;
320    }
321
322    /// Get the medium refractive index (real part)
323    #[getter]
324    fn get_medium_refr_index_re(&self) -> f32 {
325        self.medium_refr_index.re
326    }
327
328    /// Set the medium refractive index (imaginary part)
329    #[setter]
330    fn set_medium_refr_index_im(&mut self, im: f32) {
331        self.medium_refr_index.im = im;
332    }
333
334    /// Get the medium refractive index (imaginary part)
335    #[getter]
336    fn get_medium_refr_index_im(&self) -> f32 {
337        self.medium_refr_index.im
338    }
339
340    /// Set the beam power threshold
341    #[setter]
342    fn set_beam_power_threshold(&mut self, threshold: f32) {
343        self.beam_power_threshold = threshold;
344    }
345
346    /// Get the beam power threshold
347    #[getter]
348    fn get_beam_power_threshold(&self) -> f32 {
349        self.beam_power_threshold
350    }
351
352    /// Set the cutoff
353    #[setter]
354    fn set_cutoff(&mut self, cutoff: f32) {
355        self.cutoff = cutoff;
356    }
357
358    /// Get the cutoff
359    #[getter]
360    fn get_cutoff(&self) -> f32 {
361        self.cutoff
362    }
363
364    /// Set the max recursion depth
365    #[setter]
366    fn set_max_rec(&mut self, max_rec: i32) {
367        self.max_rec = max_rec;
368    }
369
370    /// Get the max recursion depth
371    #[getter]
372    fn get_max_rec(&self) -> i32 {
373        self.max_rec
374    }
375
376    /// Set the max TIR bounces
377    #[setter]
378    fn set_max_tir(&mut self, max_tir: i32) {
379        self.max_tir = max_tir;
380    }
381
382    /// Get the max TIR bounces
383    #[getter]
384    fn get_max_tir(&self) -> i32 {
385        self.max_tir
386    }
387
388    /// Set the zones configuration
389    #[setter]
390    fn set_zones(&mut self, zones: Vec<ZoneConfig>) {
391        self.zones = zones;
392    }
393
394    /// Get the zones configuration
395    #[getter]
396    fn get_zones(&self) -> Vec<ZoneConfig> {
397        self.zones.clone()
398    }
399
400    /// Set the per-axis geometry scaling [x, y, z]
401    #[setter]
402    fn set_geom_scale(&mut self, geom_scale: Option<Vec<f32>>) {
403        self.geom_scale = geom_scale;
404    }
405
406    /// Get the per-axis geometry scaling [x, y, z]
407    #[getter]
408    fn get_geom_scale(&self) -> Option<Vec<f32>> {
409        self.geom_scale.clone()
410    }
411
412    /// Set the seed for random number generation
413    #[setter]
414    fn set_seed(&mut self, seed: Option<u64>) {
415        self.seed = seed;
416    }
417
418    /// Get the seed for random number generation
419    #[getter]
420    fn get_seed(&self) -> Option<u64> {
421        self.seed
422    }
423
424    /// Set the distortion factor
425    #[setter]
426    fn set_distortion(&mut self, distortion: Option<f32>) {
427        self.distortion = distortion;
428    }
429
430    /// Get the distortion factor
431    #[getter]
432    fn get_distortion(&self) -> Option<f32> {
433        self.distortion
434    }
435
436    /// Set the field of view factor
437    #[setter]
438    fn set_fov_factor(&mut self, fov_factor: Option<f32>) {
439        self.fov_factor = fov_factor;
440    }
441
442    /// Get the field of view factor
443    #[getter]
444    fn get_fov_factor(&self) -> Option<f32> {
445        self.fov_factor
446    }
447
448    /// Set quiet mode (suppress progress bars)
449    #[setter]
450    fn set_quiet(&mut self, quiet: bool) {
451        self.quiet = quiet;
452    }
453
454    /// Get quiet mode
455    #[getter]
456    fn get_quiet(&self) -> bool {
457        self.quiet
458    }
459}
460
461impl Settings {
462    pub fn beam_area_threshold(&self) -> f32 {
463        self.wavelength * self.wavelength * self.beam_area_threshold_fac * self.scale.powi(2)
464    }
465}