Skip to main content

deep_causality_physics/photonics/
ray.rs

1/*
2 * SPDX-License-Identifier: MIT
3 * Copyright (c) 2023 - 2026. The DeepCausality Authors and Contributors. All Rights Reserved.
4 */
5
6use crate::photonics::quantities::{AbcdMatrix, OpticalPower, RayAngle, RayHeight};
7
8use crate::{IndexOfRefraction, PhysicsError};
9use deep_causality_tensor::{CausalTensor, Tensor};
10
11/// Applies an ABCD matrix to a ray vector.
12///
13/// $$ \begin{pmatrix} y_{out} \\ \theta_{out} \end{pmatrix} = \begin{pmatrix} A & B \\ C & D \end{pmatrix} \begin{pmatrix} y_{in} \\ \theta_{in} \end{pmatrix} $$
14///
15/// The ray transfer matrix (ABCD matrix) describes the optical system in the paraxial approximation.
16/// The vector consists of the ray height $y$ and angle $\theta$.
17///
18/// # Arguments
19/// *   `matrix` - The $2 \times 2$ ray transfer matrix (ABCD).
20/// *   `height` - Input ray height $y$.
21/// *   `angle` - Input ray angle $\theta$.
22///
23/// # Returns
24/// *   `Result<(RayHeight, RayAngle), PhysicsError>` - The output ray height and angle.
25pub fn ray_transfer_kernel(
26    matrix: &AbcdMatrix,
27    height: RayHeight,
28    angle: RayAngle,
29) -> Result<(RayHeight, RayAngle), PhysicsError> {
30    let m = matrix.inner();
31    if m.shape() != [2, 2] {
32        return Err(PhysicsError::DimensionMismatch(
33            "ABCD matrix must be 2x2".into(),
34        ));
35    }
36
37    let y_in = height.value();
38    let theta_in = angle.value();
39
40    // Create input column vector [y, theta]
41    let input_vec = CausalTensor::new(vec![y_in, theta_in], vec![2, 1])?;
42
43    // Matrix multiplication: [y_out, theta_out] = M * [y_in, theta_in]
44    let output_vec = m.matmul(&input_vec)?; // output_vec will be [2, 1]
45
46    let y_out = output_vec.data()[0];
47    let theta_out = output_vec.data()[1];
48
49    Ok((RayHeight::new(y_out)?, RayAngle::new(theta_out)?))
50}
51
52/// Calculates refracted angle using Snell's Law or returns a Critical Angle error.
53///
54/// $$ n_1 \sin \theta_1 = n_2 \sin \theta_2 $$
55///
56/// # Arguments
57/// *   `n1` - Refractive index of medium 1.
58/// *   `n2` - Refractive index of medium 2.
59/// *   `theta1` - Angle of incidence (relative to normal).
60///
61/// # Returns
62/// *   `Result<RayAngle, PhysicsError>` - Angle of refraction.
63///
64/// # Errors
65/// *   `PhysicalInvariantBroken` - If total internal reflection occurs ($\sin \theta_2 > 1$).
66pub fn snells_law_kernel(
67    n1: IndexOfRefraction,
68    n2: IndexOfRefraction,
69    theta1: RayAngle,
70) -> Result<RayAngle, PhysicsError> {
71    let n1_val = n1.value();
72    let n2_val = n2.value();
73    let theta1_val = theta1.value();
74
75    // sin(theta2) = (n1 / n2) * sin(theta1)
76    let sin_theta2 = (n1_val / n2_val) * theta1_val.sin();
77
78    if sin_theta2.abs() > 1.0 {
79        return Err(PhysicsError::PhysicalInvariantBroken(
80            "Total Internal Reflection: sin(theta2) > 1".into(),
81        ));
82    }
83
84    let theta2 = sin_theta2.asin();
85    RayAngle::new(theta2)
86}
87
88/// Calculates optical power and focal length using the Lens Maker's Equation.
89///
90/// $$ P = (n - 1) \left( \frac{1}{R_1} - \frac{1}{R_2} \right) $$
91///
92/// Uses the sign convention where:
93/// *   Light travels from left to right.
94/// *   Radius of curvature $R$ is positive if the center of curvature is to the right of the surface.
95/// *   Therefore, for a biconvex lens, $R_1 > 0$ (front surface convex) and $R_2 < 0$ (back surface convex).
96///
97/// # Arguments
98/// *   `n` - Refractive index of the lens material (assuming ambient index is 1).
99/// *   `r1` - Radius of curvature of the first surface.
100/// *   `r2` - Radius of curvature of the second surface.
101///
102/// # Returns
103/// *   `Result<OpticalPower, PhysicsError>` - Optical power in Diopters.
104pub fn lens_maker_kernel(
105    n: IndexOfRefraction,
106    r1: f64,
107    r2: f64,
108) -> Result<OpticalPower, PhysicsError> {
109    // P = (n - 1) * (1/R1 - 1/R2)
110    let n_val = n.value();
111
112    if r1 == 0.0 || r2 == 0.0 {
113        return Err(PhysicsError::Singularity(
114            "Radius of curvature cannot be zero".into(),
115        ));
116    }
117
118    let power = (n_val - 1.0) * ((1.0 / r1) - (1.0 / r2));
119    OpticalPower::new(power)
120}