Skip to main content

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