atm_refraction/
environment.rs

1use crate::air::{air_index, d_air_index, Atmosphere};
2use crate::{flat, spherical, Path, PathStepper, RayState, RayStateDerivative};
3
4/// The shape of the simulated Earth
5#[derive(Clone, Copy)]
6#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
7pub enum EarthShape {
8    Spherical { radius: f64 },
9    Flat,
10}
11
12/// Structure storing the shape of the underlying world and the atmospheric model.
13#[derive(Clone)]
14#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
15pub struct Environment {
16    pub shape: EarthShape,
17    pub atmosphere: Atmosphere,
18    #[cfg_attr(feature = "serialization", serde(default = "default_wavelength"))]
19    pub wavelength: f64,
20}
21
22#[cfg(feature = "serialization")]
23fn default_wavelength() -> f64 {
24    530e-9
25}
26
27impl Environment {
28    /// Returns the refractive index of the air at the given altitude.
29    pub fn n(&self, h: f64) -> f64 {
30        let pressure = self.atmosphere.pressure(h);
31        let temperature = self.atmosphere.temperature(h);
32        let rh = self.atmosphere.humidity(h);
33        air_index(self.wavelength, pressure, temperature, rh)
34    }
35
36    /// Returns the derivative of the refractive index of the air with respect to the altitude, at
37    /// the given altitude
38    pub fn dn(&self, h: f64) -> f64 {
39        let pressure = self.atmosphere.pressure(h);
40        let temperature = self.atmosphere.temperature(h);
41        let rh = self.atmosphere.humidity(h);
42        let dp = self.atmosphere.dpressure(h);
43        let dt = self.atmosphere.dtemperature(h);
44        let drh = self.atmosphere.dhumidity(h);
45        d_air_index(self.wavelength, pressure, temperature, rh, dp, dt, drh)
46    }
47
48    /// Returns Some(radius in meters) if the planet model is spherical, or None if it's flat.
49    pub fn radius(&self) -> Option<f64> {
50        match self.shape {
51            EarthShape::Spherical { radius } => Some(radius),
52            EarthShape::Flat => None,
53        }
54    }
55
56    pub(crate) fn calc_derivative_spherical(&self, state: &RayState) -> RayStateDerivative {
57        let radius = self.radius().unwrap();
58        let dh = state.dh * radius;
59        let h = state.h;
60
61        let nr = self.n(h);
62        let dnr = self.dn(h);
63
64        let r = h + radius;
65        let d2h = dh * dh * dnr / nr + r * r * dnr / nr + 2.0 * dh * dh / r + r;
66
67        RayStateDerivative {
68            dx: 1.0,
69            dh: state.dh,
70            d2h: d2h / radius / radius,
71        }
72    }
73
74    pub(crate) fn calc_derivative_flat(&self, state: &RayState) -> RayStateDerivative {
75        let dh = state.dh;
76        let h = state.h;
77
78        let nr = self.n(h);
79        let dnr = self.dn(h);
80
81        let d2h = dnr / nr * (1.0 + dh * dh);
82
83        RayStateDerivative { dx: 1.0, dh, d2h }
84    }
85
86    /// Returns an object representing a light path.
87    ///
88    /// The path is defined by 3 parameters:
89    /// * `start_h` - the starting altitude of the path in meters
90    /// * `start_ang` - the initial angle in radians between the path and the horizontal plane;
91    /// -π/2 is down, 0 is horizontal, π/2 is up
92    /// * `straight` - `true` if the path should be a straight line, `false` if it should be a ray
93    /// affected by the atmosphere
94    pub fn cast_ray<'a>(
95        &'a self,
96        start_h: f64,
97        start_ang: f64,
98        straight: bool,
99    ) -> Box<dyn Path<'a> + 'a> {
100        match (straight, self.shape) {
101            (true, EarthShape::Flat) => Box::new(flat::Line::from_h_ang(start_h, start_ang)),
102            (true, EarthShape::Spherical { .. }) => {
103                Box::new(spherical::Line::from_h_ang(self, start_h, start_ang))
104            }
105            (false, EarthShape::Flat) => Box::new(flat::Ray::from_h_ang(self, start_h, start_ang)),
106            (false, EarthShape::Spherical { .. }) => {
107                Box::new(spherical::Ray::from_h_ang(self, start_h, start_ang))
108            }
109        }
110    }
111
112    /// Returns an object representing a light path.
113    ///
114    /// The path is defined by 3 parameters:
115    /// * `start_h` - the starting altitude of the path in meters
116    /// * `start_ang` - the initial angle in radians between the path and the horizontal plane;
117    /// -π/2 is down, 0 is horizontal, π/2 is up
118    /// * `straight` - `true` if the path should be a straight line, `false` if it should be a ray
119    /// affected by the atmosphere
120    pub fn cast_ray_stepper<'a>(
121        &'a self,
122        start_h: f64,
123        start_ang: f64,
124        straight: bool,
125    ) -> Box<dyn PathStepper<Item = RayState> + 'a> {
126        match (straight, self.shape) {
127            (true, EarthShape::Flat) => {
128                flat::Line::from_h_ang(start_h, start_ang).into_path_stepper()
129            }
130            (true, EarthShape::Spherical { .. }) => {
131                spherical::Line::from_h_ang(self, start_h, start_ang).into_path_stepper()
132            }
133            (false, EarthShape::Flat) => {
134                flat::Ray::from_h_ang(self, start_h, start_ang).into_path_stepper()
135            }
136            (false, EarthShape::Spherical { .. }) => {
137                spherical::Ray::from_h_ang(self, start_h, start_ang).into_path_stepper()
138            }
139        }
140    }
141
142    /// Returns an object representing a light path.
143    ///
144    /// Instead of using the initial angle, this method chooses a ray that will hit a given target.
145    /// The target is defined as distance and altitude.
146    ///
147    /// * `start_h` - the initial altitude of the path in meters
148    /// * `tgt_h` - the altitude of the target point in meters
149    /// * `tgt_dist` - the distance of the target point from the initial point, in meters
150    /// * `straight` - `true` if the path should be a straight line, `false` if it should be a ray
151    /// affected by the atmosphere
152    ///
153    /// The ray is calculated by performing a binary search on the initial angle.
154    pub fn cast_ray_target<'a>(
155        &'a self,
156        start_h: f64,
157        tgt_h: f64,
158        tgt_dist: f64,
159        straight: bool,
160    ) -> Box<dyn Path<'a> + 'a> {
161        if straight {
162            match self.shape {
163                EarthShape::Flat => {
164                    Box::new(flat::Line::from_two_points(start_h, 0.0, tgt_h, tgt_dist))
165                }
166                EarthShape::Spherical { radius } => Box::new(spherical::Line::from_two_points(
167                    self,
168                    start_h,
169                    0.0,
170                    tgt_h,
171                    tgt_dist / radius,
172                )),
173            }
174        } else {
175            let (mut min_ang, mut max_ang) = (-1.5, 1.5);
176            let epsilon = 1e-9;
177
178            while max_ang - min_ang > epsilon {
179                let cur_ang = 0.5 * (min_ang + max_ang);
180                let ray = self.cast_ray(start_h, cur_ang, straight);
181                let h = ray.h_at_dist(tgt_dist);
182                if h > tgt_h {
183                    max_ang = cur_ang;
184                } else {
185                    min_ang = cur_ang;
186                }
187            }
188
189            self.cast_ray(start_h, 0.5 * (min_ang + max_ang), straight)
190        }
191    }
192}