cherry_rs/views/ray_trace_3d/
rays.rs

1use anyhow::{bail, Result};
2use serde::{Deserialize, Serialize};
3
4use crate::core::{
5    math::vec3::Vec3,
6    sequential_model::{Step, Surface},
7    Float, PI,
8};
9
10/// Tolerance for convergence of the Newton-Raphson method in integer mutliples
11/// of the machine epsilon
12const TOL: Float = Float::EPSILON;
13
14/// A single ray to be traced through an optical system.
15///
16/// # Attributes
17/// - pos: Position of the ray
18/// - dir: Direction of the ray (direction cosines)
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Ray {
21    pos: Vec3,
22    dir: Vec3,
23    terminated: bool,
24    field_id: usize,
25}
26
27impl Ray {
28    pub fn new(pos: Vec3, dir: Vec3, field_id: usize) -> Result<Self> {
29        if !dir.is_unit() {
30            bail!("Ray direction must be a unit vector");
31        }
32        Ok(Self {
33            pos,
34            dir,
35            terminated: false,
36            field_id,
37        })
38    }
39
40    /// Finds the intersection point of a ray with a surface and the surface
41    /// normal at that point.
42    ///
43    /// If no intersection is found, then this function returns an error.
44    ///
45    /// # Arguments
46    /// - surf: Surface to intersect with
47    /// - max_iter: Maximum number of iterations for the Newton-Raphson method
48    pub fn intersect(&self, surf: &Surface, max_iter: usize) -> Result<(Vec3, Vec3)> {
49        // Initial guess for the intersection point
50        let mut s_1 = 0.0;
51
52        // Find the distance along the ray to the z=0 plane; use this as the initial
53        // value for s
54        let mut s = -self.pos.z() / self.dir.m();
55
56        let mut p: Vec3;
57        let mut sag: Float;
58        let mut norm: Vec3;
59        for ctr in 0..max_iter {
60            // Compute the current estimate of the intersection point from the distance s
61            p = self.pos + self.dir * s;
62
63            // Update the distance s using the Newton-Raphson method
64            (sag, norm) = surf.sag_norm(p);
65            s -= (p.z() - sag) / norm.dot(self.dir);
66
67            // Check for convergence by comparing the current and previous values of s
68            if (s - s_1).abs() / Float::max(s, s_1) < TOL {
69                break;
70            }
71
72            if ctr == max_iter - 1 {
73                bail!("Ray intersection did not converge");
74            }
75
76            s_1 = s;
77        }
78
79        // Compute the final intersection point and surface normal
80        p = self.pos + self.dir * s;
81        (_, norm) = surf.sag_norm(p);
82
83        Ok((p, norm))
84    }
85
86    // Redirect the ray by computing the direction cosines of the ray after
87    // interaction with a surface.
88    //
89    // This function accepts the surface normal at the intersection point as an
90    // argument to avoid recomputing it.
91    pub fn redirect(&mut self, step: &Step, norm: Vec3) {
92        // Do not match on the wildcard "_" to ensure that this function is updated when
93        // new surfaces are added
94        let (gap_0, surf, gap_1) = step;
95        let n_0 = gap_0.refractive_index.n();
96        let n_1 = if let Some(gap_1) = gap_1 {
97            gap_1.refractive_index.n()
98        } else {
99            n_0
100        };
101
102        match surf {
103            // Refracting surfaces
104            //Surface::Conic(_) | Surface::Toric(_) => {
105            Surface::Conic(_) => {
106                let mu = n_0 / n_1;
107                let cos_theta_1 = self.dir.dot(norm);
108                let term_1 = norm * (1.0 - mu * mu * (1.0 - cos_theta_1 * cos_theta_1)).sqrt();
109                let term_2 = (self.dir - norm * cos_theta_1) * mu;
110
111                self.dir = term_1 + term_2;
112            }
113            // No-op surfaces
114            Surface::Image(_) => {}
115            Surface::Object(_) => {}
116            Surface::Probe(_) => {}
117            Surface::Stop(_) => {}
118        }
119    }
120
121    /// Displace a ray to the given location.
122    pub fn displace(&mut self, pos: Vec3) {
123        self.pos = pos;
124    }
125
126    /// Transform a ray into the local coordinate system of a surface from the
127    /// global system.
128    pub fn transform(&mut self, surf: &Surface) {
129        self.pos = surf.rot_mat() * (self.pos - surf.pos());
130        self.dir = surf.rot_mat() * self.dir;
131    }
132
133    /// Transform a ray from the local coordinate system of a surface into the
134    /// global system.
135    pub fn i_transform(&mut self, surf: &Surface) {
136        self.pos = surf.rot_mat().transpose() * (self.pos + surf.pos());
137        self.dir = surf.rot_mat().transpose() * self.dir;
138    }
139
140    pub fn terminate(&mut self) {
141        self.terminated = true;
142    }
143
144    #[inline]
145    pub fn is_terminated(&self) -> bool {
146        self.terminated
147    }
148
149    /// Create a fan of uniformly spaced rays in a given z-plane at an angle phi
150    /// to the z-axis.
151    ///
152    /// The vectors have endpoints at an angle theta with respect to the x-axis
153    /// and extend from distances -r to r from the point (0, 0, z). The rays
154    /// are at an angle phi from the z-axis.
155    ///
156    /// # Arguments
157    /// - n: Number of vectors to create
158    /// - r: Radial span of vector endpoints from [-r, r]
159    /// - theta: Angle of vectors with respect to x, radians
160    /// - z: z-coordinate of endpoints
161    /// - phi: Angle of vectors with respect to z, the optics axis, radians
162    /// - radial_offset_x: Offset the radial position of the vectors by this
163    ///   amount in x
164    /// - radial_offset_y: Offset the radial position of the vectors by this
165    ///   amount in y
166    #[allow(clippy::too_many_arguments)]
167    pub fn fan(
168        n: usize,
169        r: Float,
170        theta: Float,
171        z: Float,
172        phi: Float,
173        radial_offset_x: Float,
174        radial_offset_y: Float,
175        field_id: usize,
176    ) -> Vec<Ray> {
177        let pos = Vec3::fan(n, r, theta, z, radial_offset_x, radial_offset_y);
178        let dir: Vec<Vec3> = pos
179            .iter()
180            .map(|_| {
181                let l = phi.sin() * theta.cos();
182                let m = phi.sin() * theta.sin();
183                let n = phi.cos();
184                Vec3::new(l, m, n)
185            })
186            .collect();
187
188        pos.iter()
189            .zip(dir.iter())
190            .map(|(p, d)| Ray::new(*p, *d, field_id).unwrap())
191            .collect()
192    }
193
194    /// Create a square grid of uniformly spaced rays within a circle in a given
195    /// z-plane.
196    ///
197    /// # Arguments
198    /// - `radius`: Radius of the circle
199    /// - `spacing`: Spacing between rays
200    /// - `z`: z-coordinate of endpoints
201    /// - `phi`: Angle of vectors with respect to z, the optics axis, radians
202    /// - radial_offset_x: Offset the radial position of the vectors by this
203    ///   amount in x
204    /// - radial_offset_y: Offset the radial position of the vectors by this
205    ///   amount in y
206    /// - `field_id`: Field ID of the rays.
207    pub(crate) fn sq_grid_in_circ(
208        radius: Float,
209        spacing: Float,
210        z: Float,
211        phi: Float,
212        radial_offset_x: Float,
213        radial_offset_y: Float,
214        field_id: usize,
215    ) -> Vec<Ray> {
216        let theta = PI / 2.0; // TODO: For now rays are rotated about x only
217
218        let pos: Vec<Vec3> =
219            Vec3::sq_grid_in_circ(radius, spacing, z, radial_offset_x, radial_offset_y);
220        let dir: Vec<Vec3> = pos
221            .iter()
222            .map(|_| {
223                let l = phi.sin() * theta.cos();
224                let m = phi.sin() * theta.sin();
225                let n = phi.cos();
226                Vec3::new(l, m, n)
227            })
228            .collect();
229
230        pos.iter()
231            .zip(dir.iter())
232            .map(|(p, d)| Ray::new(*p, *d, field_id).unwrap())
233            .collect()
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use crate::specs::surfaces::{SurfaceSpec, SurfaceType};
240
241    use super::*;
242    // Test the constructor of Ray
243    #[test]
244    fn test_rays_new() {
245        let pos = Vec3::new(0.0, 0.0, 0.0);
246        let dir = Vec3::new(0.0, 0.0, 1.0);
247        let field_id = 0;
248
249        let rays = Ray::new(pos, dir, field_id);
250
251        assert!(rays.is_ok());
252    }
253
254    // Test the constructor of Ray with a non-unit direction vector
255    #[test]
256    fn test_rays_new_non_unit_dir() {
257        let pos = Vec3::new(0.0, 0.0, 0.0);
258        let dir = Vec3::new(0.0, 0.0, 2.0);
259        let field_id = 0;
260
261        let rays = Ray::new(pos, dir, field_id);
262
263        assert!(rays.is_err());
264    }
265
266    // Test the intersection of a ray with a flat surface
267    #[test]
268    fn test_ray_intersection_flat_surface() {
269        let field_id = 0;
270        let pos = Vec3::new(0.0, 0.0, 0.0);
271        let ray = Ray::new(
272            Vec3::new(0.0, 0.0, -1.0),
273            Vec3::new(0.0, 0.0, 1.0),
274            field_id,
275        )
276        .unwrap();
277        let surf_spec = SurfaceSpec::Conic {
278            semi_diameter: 4.0,
279            radius_of_curvature: Float::INFINITY,
280            conic_constant: 0.0,
281            surf_type: SurfaceType::Refracting,
282        };
283        let surf = Surface::from_spec(&surf_spec, pos);
284        let max_iter = 1000;
285
286        let (p, _) = ray.intersect(&surf, max_iter).unwrap();
287
288        assert_eq!(p, Vec3::new(0.0, 0.0, 0.0));
289    }
290
291    // Test the intersection of a ray with a circular surface
292    #[test]
293    fn test_ray_intersection_conic() {
294        let field_id = 0;
295        let pos = Vec3::new(0.0, 0.0, 0.0);
296
297        // Ray starts at z = -1.0 and travels at 45 degrees to the optics axis
298        let l = 0.7071;
299        let m = ((1.0 as Float) - 0.7071 * 0.7071).sqrt();
300        let ray = Ray::new(Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, l, m), field_id).unwrap();
301
302        // Surface has radius of curvature -1.0 and conic constant 0.0, i.e. a circle
303        let surf_spec = SurfaceSpec::Conic {
304            semi_diameter: 4.0,
305            radius_of_curvature: -1.0,
306            conic_constant: 0.0,
307            surf_type: SurfaceType::Refracting,
308        };
309        let surf = Surface::from_spec(&surf_spec, pos);
310        let max_iter = 1000;
311
312        let (p, _) = ray.intersect(&surf, max_iter).unwrap();
313
314        assert!((p.x() - 0.0).abs() < 1e-4);
315        assert!((p.y() - (PI / 4.0).sin()).abs() < 1e-4);
316        assert!((p.z() - ((PI / 4.0).cos() - 1.0)).abs() < 1e-4);
317    }
318}