lib_ria/internal/
store.rs

1use std::collections::HashMap;
2
3use anyhow::{anyhow, Result};
4use serde::{Deserialize, Serialize};
5
6/// A flat, key-value store for material refractive index data.
7#[derive(Serialize, Deserialize, Debug)]
8pub struct Store {
9    inner: HashMap<String, Material>,
10}
11
12/// A single item in the store containing materials data.
13#[derive(Serialize, Deserialize, Debug)]
14pub struct Material {
15    pub shelf: String,
16    pub book: String,
17    pub page: String,
18    pub comments: String,
19    pub references: String,
20    pub data: Vec<DispersionData>,
21}
22
23#[derive(Debug)]
24enum DataType {
25    Real,
26    Imaginary,
27    Both,
28}
29
30/// The refractive index data associated with a material.
31#[derive(Serialize, Deserialize, Debug)]
32pub enum DispersionData {
33    TabulatedK {
34        data: Vec<[f64; 2]>,
35    },
36    TabulatedN {
37        data: Vec<[f64; 2]>,
38    },
39    TabulatedNK {
40        data: Vec<[f64; 3]>,
41    },
42
43    /// The Sellmeier formula.
44    Formula1 {
45        wavelength_range: [f64; 2],
46        c: Vec<f64>,
47    },
48
49    /// The Sellmeier-2 formula.
50    Formula2 {
51        wavelength_range: [f64; 2],
52        c: Vec<f64>,
53    },
54
55    /// Polynomial
56    Formula3 {
57        wavelength_range: [f64; 2],
58        c: Vec<f64>,
59    },
60
61    /// RefractiveIndex.INFO
62    Formula4 {
63        wavelength_range: [f64; 2],
64        c: Vec<f64>,
65    },
66
67    /// Cauchy
68    Formula5 {
69        wavelength_range: [f64; 2],
70        c: Vec<f64>,
71    },
72
73    /// Gases
74    Formula6 {
75        wavelength_range: [f64; 2],
76        c: Vec<f64>,
77    },
78
79    /// Herzberger
80    Formula7 {
81        wavelength_range: [f64; 2],
82        c: Vec<f64>,
83    },
84
85    /// Retro
86    Formula8 {
87        wavelength_range: [f64; 2],
88        c: Vec<f64>,
89    },
90
91    /// Exotic
92    Formula9 {
93        wavelength_range: [f64; 2],
94        c: Vec<f64>,
95    },
96}
97
98impl Store {
99    pub fn new(database: HashMap<String, Material>) -> Self {
100        Store { inner: database }
101    }
102
103    /// Returns the item from the store associated with the given key.
104    ///
105    /// # Arguments
106    /// - `key`: The key to look up in the store.
107    ///
108    /// # Returns
109    /// The item associated with the given key, if it exists.
110    pub fn get(&self, key: &str) -> Option<&Material> {
111        self.inner.get(key)
112    }
113
114    /// Inserts a new item into the store.
115    ///
116    /// # Arguments
117    /// - `key`: The key to associate with the item.
118    /// - `material`: The item to insert into the store.
119    pub fn insert(&mut self, key: String, material: Material) {
120        self.inner.insert(key, material);
121    }
122
123    /// Returns an iterator over the keys in the store.
124    pub fn keys(&self) -> impl Iterator<Item = &String> {
125        self.inner.keys()
126    }
127
128    /// Retains only the items in the store that satisfy the given predicate.
129    ///
130    /// # Arguments
131    /// - `predicate`: The predicate to use to retain items in the store. Any
132    ///   item for which the predicate returns `false` will be removed.
133    pub fn retain(&mut self, predicate: impl FnMut(&String, &mut Material) -> bool) {
134        self.inner.retain(predicate);
135    }
136
137    /// Removes the item associated with the given key from the store.
138    ///
139    /// # Arguments
140    /// - `key`: The key of the item to remove from the store.
141    pub fn remove(&mut self, key: &str) -> Option<Material> {
142        self.inner.remove(key)
143    }
144
145    /// Removes multiple items from the store.
146    ///
147    /// # Arguments
148    /// - `keys`: The keys of the items to remove from the store.
149    pub fn remove_many(&mut self, keys: &[String]) {
150        for key in keys {
151            self.inner.remove(key);
152        }
153    }
154}
155
156impl Default for Store {
157    fn default() -> Self {
158        let database = HashMap::new();
159        Self::new(database)
160    }
161}
162
163impl Material {
164    /// Computes the real part of the refractive index of the material at the
165    /// given wavelength.
166    ///
167    /// # Arguments
168    /// - `wavelength`: The wavelength at which to evaluate the refractive
169    ///   index.
170    ///
171    /// # Returns
172    /// The real part of the refractive index of the material at the given
173    /// wavelength.
174    ///
175    /// # Errors
176    /// - If no real data is found for the item.
177    /// - If the wavelength is outside the range of the real data.
178    pub fn n(&self, wavelength: f64) -> Result<f64> {
179        let data = self
180            .data
181            .iter()
182            .find(|d| matches!(d.data_type(), DataType::Real | DataType::Both));
183        let (n, _) = match data {
184            Some(data) => data.interpolate(wavelength)?,
185            None => return Err(anyhow!("No real data found for item.")),
186        };
187        Ok(n)
188    }
189
190    /// Computes the imaginary part of the refractive index of the material at
191    /// the given wavelength.
192    ///
193    /// # Arguments
194    /// - `wavelength`: The wavelength at which to evaluate the refractive
195    ///   index.
196    ///
197    /// # Returns
198    /// The imaginary part of the refractive index of the material at the given
199    /// wavelength.
200    ///
201    /// # Errors
202    /// - If the wavelength is outside the range of the imaginary data.
203    pub fn k(&self, wavelength: f64) -> Result<Option<f64>> {
204        let data = self
205            .data
206            .iter()
207            .find(|d| matches!(d.data_type(), DataType::Imaginary | DataType::Both));
208        match data {
209            Some(data) => Ok(data.interpolate(wavelength)?.1),
210            None => Ok(None),
211        }
212    }
213}
214
215impl DispersionData {
216    /// Computes the value of the dispersion curve at the given wavelength.
217    ///
218    /// # Arguments
219    /// - `wavelength`: The wavelength at which to evaluate the dispersion
220    ///   curve.
221    ///
222    /// # Returns
223    /// The value of the dispersion curve at the given wavelength. The first
224    /// value is the real part of the refractive index, and the second value
225    /// is the imaginary part of the refractive index.
226    ///
227    /// # Errors
228    /// - If the wavelength is outside the range of dispersion data.
229    pub fn interpolate(&self, wavelength: f64) -> Result<(f64, Option<f64>)> {
230        let n: f64 = match &self {
231            Self::Formula1 {
232                wavelength_range,
233                c,
234            } => {
235                // Sellmeier (preferred)
236                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
237                    return Err(anyhow!(
238                        "The requested wavelength is outside the range of the available dispersion data."
239                    ));
240                }
241                let mut sum = 0.0;
242                for i in (1..c.len()).step_by(2) {
243                    sum += c[i] * wavelength.powi(2) / (wavelength.powi(2) - c[i + 1].powi(2));
244                }
245                (1.0 + c[0] + sum).sqrt()
246            }
247            Self::Formula2 {
248                wavelength_range,
249                c,
250            } => {
251                // Sellmeier-2
252                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
253                    return Err(anyhow!(
254                        "The requested wavelength is outside the range of the available dispersion data."
255                    ));
256                }
257                let mut sum = 0.0;
258                for i in (1..c.len()).step_by(2) {
259                    sum += c[i] * wavelength.powi(2) / (wavelength.powi(2) - c[i + 1]);
260                }
261                (1.0 + c[0] + sum).sqrt()
262            }
263            Self::Formula3 {
264                wavelength_range,
265                c,
266            } => {
267                // Polynomial
268                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
269                    return Err(anyhow!(
270                        "The requested wavelength is outside the range of the available dispersion data."
271                    ));
272                }
273
274                let mut sum = 0.0;
275                for i in (1..c.len()).step_by(2) {
276                    sum += c[i] * wavelength.powf(c[i + 1]);
277                }
278                (c[0] + sum).sqrt()
279            }
280            Self::Formula4 {
281                wavelength_range,
282                c,
283            } => {
284                // RefractiveIndex.INFO
285                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
286                    return Err(anyhow!(
287                        "The requested wavelength is outside the range of the available dispersion data."
288                    ));
289                }
290
291                let mut sum = 0.0;
292                for i in (1..c.len()).step_by(4) {
293                    // Formula 4 is kind of wild.
294                    if i <= 9 {
295                        sum += c[i] * wavelength.powf(c[i + 1])
296                            / (wavelength.powi(2) - c[i + 2].powf(c[i + 3]));
297                    } else {
298                        sum += c[i] * wavelength.powf(c[i + 1]);
299                    }
300                }
301                (c[0] + sum).sqrt()
302            }
303            Self::Formula5 {
304                wavelength_range,
305                c,
306            } => {
307                // Cauchy
308                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
309                    return Err(anyhow!(
310                        "The requested wavelength is outside the range of the available dispersion data."
311                    ));
312                }
313
314                let mut sum = 0.0;
315                for i in (1..c.len()).step_by(2) {
316                    sum += c[i] * wavelength.powf(c[i + 1]);
317                }
318                c[0] + sum
319            }
320            Self::Formula6 {
321                wavelength_range,
322                c,
323            } => {
324                // Gases
325                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
326                    return Err(anyhow!(
327                        "The requested wavelength is outside the range of the available dispersion data."
328                    ));
329                }
330
331                let mut sum = 0.0;
332                for i in (1..c.len()).step_by(2) {
333                    sum += c[i] / (c[i + 1] - wavelength.powi(-2));
334                }
335                1.0 + c[0] + sum
336            }
337            Self::Formula7 {
338                wavelength_range,
339                c,
340            } => {
341                // Herzberger
342                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
343                    return Err(anyhow!(
344                        "The requested wavelength is outside the range of the available dispersion data."
345                    ));
346                }
347                let mut sum = 0.0;
348                for i in (3..c.len()).step_by(2) {
349                    sum += c[i] * wavelength.powi(i as i32 - 1);
350                }
351                c[0] + c[1] / (wavelength.powi(2) - 0.028)
352                    + c[2] / (wavelength.powi(2) - 0.028).powi(2)
353                    + sum
354            }
355            Self::Formula8 {
356                wavelength_range,
357                c,
358            } => {
359                // Retro
360                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
361                    return Err(anyhow!(
362                        "The requested wavelength is outside the range of the available dispersion data."
363                    ));
364                }
365
366                let sum = c[0]
367                    + c[1] * wavelength.powi(2) / (wavelength.powi(2) - c[2])
368                    + c[3] * wavelength.powi(2);
369                println!("sum: {}", sum);
370                ((2.0 * sum + 1.0) / (1.0 - sum)).sqrt()
371            }
372            Self::Formula9 {
373                wavelength_range,
374                c,
375            } => {
376                // Exotic
377                if wavelength < wavelength_range[0] || wavelength > wavelength_range[1] {
378                    return Err(anyhow!(
379                        "The requested wavelength is outside the range of the available dispersion data."
380                    ));
381                }
382
383                (c[0]
384                    + c[1] / (wavelength.powi(2) - c[2])
385                    + c[3] * (wavelength - c[4]) / ((wavelength - c[4]).powi(2) + c[5]))
386                    .sqrt()
387            }
388            _ => {
389                return Err(anyhow!("Tabulated dispersion data are not implemented."));
390            }
391        };
392
393        Ok((n, None))
394    }
395
396    /// Returns the type of data stored in the DispersionData.
397    ///
398    /// An item may have either one or two dispersion data sets. If there are
399    /// two, we have to infer from the type which is real and which is
400    /// imaginary.  We use the following rules to determine which is which:
401    ///
402    /// 1. If the DispersionData is TabulatedN, then it's the real part.
403    /// 2. If the DispersionData is TabulatedK, then it's the imaginary part.
404    /// 3. If the DispersionData is a formula, then it's the real part.
405    ///
406    /// If there is only one dispersion data set, then we use one additional
407    /// rule:
408    ///
409    /// 1. If the DispersionData is TabulatedNK, then it's both the real and
410    ///    imaginary parts.
411    fn data_type(&self) -> DataType {
412        match self {
413            self::DispersionData::TabulatedK { data: _ } => DataType::Imaginary,
414            self::DispersionData::TabulatedN { data: _ } => DataType::Real,
415            self::DispersionData::TabulatedNK { data: _ } => DataType::Both,
416            _ => DataType::Real,
417        }
418    }
419}
420
421#[cfg(test)]
422mod test {
423    use super::*;
424    use approx::assert_abs_diff_eq;
425
426    #[test]
427    fn test_interpolate_formula_1() {
428        // Water Ice at 150 K from refractiveindex.info
429        let data = DispersionData::Formula1 {
430            wavelength_range: [0.210, 0.757],
431            c: vec![0.0, 0.496, 0.071, 0.190, 0.134],
432        };
433        let (n, k) = data.interpolate(0.5876).unwrap();
434        assert_abs_diff_eq!(n, 1.3053, epsilon = 1e-4);
435        assert!(k.is_none());
436    }
437
438    #[test]
439    fn test_interpolate_formula_2() {
440        // N-BK7 from refractiveindex.info
441        let data = DispersionData::Formula2 {
442            wavelength_range: [0.3, 2.5],
443            c: vec![
444                0.0,
445                1.03961212,
446                0.00600069867,
447                0.231792344,
448                0.0200179144,
449                1.01046945,
450                103.560653,
451            ],
452        };
453
454        let (n, k) = data.interpolate(0.5876).unwrap();
455        assert_abs_diff_eq!(n, 1.51680, epsilon = 1e-5);
456        assert!(k.is_none());
457    }
458
459    #[test]
460    fn test_interpolate_formula_3() {
461        // Ohara BAH10 from refractiveindex.info
462        let data = DispersionData::Formula3 {
463            wavelength_range: [0.365, 0.9],
464            c: vec![
465                2.730459,
466                -0.01063385,
467                2.0,
468                0.01942756,
469                -2.0,
470                0.0008209873,
471                -4.0,
472                -5.210457e-05,
473                -6.0,
474                4.447534e-06,
475                -8.0,
476            ],
477        };
478        let (n, k) = data.interpolate(0.5876).unwrap();
479        assert_abs_diff_eq!(n, 1.6700, epsilon = 1e-4);
480        assert!(k.is_none());
481    }
482
483    #[test]
484    fn test_interpolate_formula_4() {
485        // CH4N20 Urea from refractiveindex.info
486        let data = DispersionData::Formula4 {
487            wavelength_range: [0.3, 1.06],
488            c: vec![2.1823, 0.0125, 0.0, 0.0300, 1.0, 0.0, 0.0, 0.0, 1.0],
489        };
490        let (n, k) = data.interpolate(0.5876).unwrap();
491        assert_abs_diff_eq!(n, 1.4906, epsilon = 1e-4);
492        assert!(k.is_none());
493    }
494
495    #[test]
496    fn test_interpolate_formula_5() {
497        // BK7 matching liquid from refractiveindex.info
498        let data = DispersionData::Formula5 {
499            wavelength_range: [0.31, 1.55],
500            c: vec![1.502787, 455872.4E-8, -2.0, 9.844856E-5, -4.0],
501        };
502        let (n, k) = data.interpolate(0.5876).unwrap();
503        assert_abs_diff_eq!(n, 1.5168, epsilon = 1e-4);
504        assert!(k.is_none());
505    }
506
507    #[test]
508    fn test_interpolate_formula_6() {
509        // H2 (Peck) in main shelf from refractiveindex.info
510        let data = DispersionData::Formula6 {
511            wavelength_range: [0.168, 1.6945],
512            c: vec![0.0, 0.0148956, 180.7, 0.0049037, 92.0],
513        };
514        let (n, k) = data.interpolate(0.5876).unwrap();
515        assert_abs_diff_eq!(n, 1.00013881, epsilon = 1e-8);
516        assert!(k.is_none());
517    }
518
519    #[test]
520    fn test_interpolate_formula_7() {
521        // Si (Edwards) in main shelf of refractiveindex.info
522        let data = DispersionData::Formula7 {
523            wavelength_range: [2.4373, 25.0],
524            c: vec![3.41983, 0.159906, -0.123109, 1.26878E-6, -1.95104E-9],
525        };
526        let (n, k) = data.interpolate(2.4373).unwrap();
527        assert_abs_diff_eq!(n, 3.4434, epsilon = 1e-4);
528        assert!(k.is_none());
529    }
530
531    #[test]
532    fn test_interpolate_formula_8() {
533        // TlCl (Schroter) in main shelf of refractiveindex.info
534        let data = DispersionData::Formula8 {
535            wavelength_range: [0.43, 0.66],
536            c: vec![0.47856, 0.07858, 0.08277, -0.00881],
537        };
538        let (n, k) = data.interpolate(0.5876).unwrap();
539        assert_abs_diff_eq!(n, 2.2636, epsilon = 1e-4);
540        assert!(k.is_none());
541    }
542
543    #[test]
544    fn test_interpolate_formula_9() {
545        // CH4N2O Urea (Rosker-e) from refractiveindex.info
546        let data = DispersionData::Formula9 {
547            wavelength_range: [0.3, 1.06],
548            c: vec![2.51527, 0.0240, 0.0300, 0.020, 1.52, 0.8771],
549        };
550        let (n, k) = data.interpolate(0.5876).unwrap();
551        assert_abs_diff_eq!(n, 1.6065, epsilon = 1e-4);
552        assert!(k.is_none());
553    }
554}