1use std::f64::consts::PI;
8
9#[derive(Debug, Clone, PartialEq)]
15pub enum OpticsError {
16 TotalInternalReflection,
18 InvalidParameter(String),
20 NoImage,
22}
23
24impl std::fmt::Display for OpticsError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 OpticsError::TotalInternalReflection => write!(f, "total internal reflection"),
28 OpticsError::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
29 OpticsError::NoImage => write!(f, "no real image formed"),
30 }
31 }
32}
33
34impl std::error::Error for OpticsError {}
35
36pub type OpticsResult<T> = Result<T, OpticsError>;
37
38#[derive(Debug, Clone)]
44pub struct OpticalSurface {
45 pub n1: f64,
47 pub n2: f64,
49}
50
51#[derive(Debug, Clone)]
53pub struct RayTraceResult {
54 pub incidence_angles: Vec<f64>,
56 pub refraction_angles: Vec<f64>,
58 pub final_angle: f64,
60}
61
62#[derive(Debug, Clone)]
68pub struct FresnelResult {
69 pub rs: f64,
71 pub rp: f64,
73 pub ts: f64,
75 pub tp: f64,
77}
78
79#[derive(Debug, Clone)]
85pub struct ThinLensResult {
86 pub image_distance: f64,
88 pub magnification: f64,
90}
91
92#[derive(Debug, Clone)]
98pub struct DiffractionResult {
99 pub max_order: i32,
101 pub maxima: Vec<(i32, f64)>,
103 pub resolving_power: f64,
105}
106
107pub fn snells_law(n1: f64, n2: f64, theta_i: f64) -> OpticsResult<f64> {
118 if n1 <= 0.0 || n2 <= 0.0 {
119 return Err(OpticsError::InvalidParameter(
120 "refractive indices must be positive".to_string(),
121 ));
122 }
123 let sin_t = n1 * theta_i.sin() / n2;
124 if sin_t.abs() > 1.0 {
125 return Err(OpticsError::TotalInternalReflection);
126 }
127 Ok(sin_t.asin())
128}
129
130pub fn fresnel(n1: f64, n2: f64, theta_i: f64) -> OpticsResult<FresnelResult> {
134 let theta_t = snells_law(n1, n2, theta_i)?;
135
136 let cos_i = theta_i.cos();
137 let cos_t = theta_t.cos();
138
139 let rs_num = n1 * cos_i - n2 * cos_t;
141 let rs_den = n1 * cos_i + n2 * cos_t;
142 let rp_num = n2 * cos_i - n1 * cos_t;
143 let rp_den = n2 * cos_i + n1 * cos_t;
144
145 let rs = if rs_den.abs() < 1e-15 {
146 0.0
147 } else {
148 (rs_num / rs_den).powi(2)
149 };
150 let rp = if rp_den.abs() < 1e-15 {
151 0.0
152 } else {
153 (rp_num / rp_den).powi(2)
154 };
155
156 Ok(FresnelResult {
157 rs,
158 rp,
159 ts: 1.0 - rs,
160 tp: 1.0 - rp,
161 })
162}
163
164pub fn thin_lens(focal_length: f64, object_distance: f64) -> OpticsResult<ThinLensResult> {
169 if focal_length.abs() < 1e-15 {
170 return Err(OpticsError::InvalidParameter(
171 "focal length cannot be zero".to_string(),
172 ));
173 }
174 let inv_di = 1.0 / focal_length - 1.0 / object_distance;
175 if inv_di.abs() < 1e-15 {
176 return Err(OpticsError::NoImage);
177 }
178 let image_distance = 1.0 / inv_di;
179 let magnification = -image_distance / object_distance;
180
181 Ok(ThinLensResult {
182 image_distance,
183 magnification,
184 })
185}
186
187pub fn trace_ray(surfaces: &[OpticalSurface], initial_angle: f64) -> OpticsResult<RayTraceResult> {
191 let mut incidence_angles = Vec::with_capacity(surfaces.len());
192 let mut refraction_angles = Vec::with_capacity(surfaces.len());
193
194 let mut current_angle = initial_angle;
195
196 for surface in surfaces {
197 incidence_angles.push(current_angle);
198 let refracted = snells_law(surface.n1, surface.n2, current_angle)?;
199 refraction_angles.push(refracted);
200 current_angle = refracted;
201 }
202
203 Ok(RayTraceResult {
204 incidence_angles,
205 refraction_angles,
206 final_angle: current_angle,
207 })
208}
209
210pub fn diffraction_grating(
216 d: f64,
217 wavelength: f64,
218 n_slits: usize,
219) -> OpticsResult<DiffractionResult> {
220 if d <= 0.0 || wavelength <= 0.0 {
221 return Err(OpticsError::InvalidParameter(
222 "grating spacing and wavelength must be positive".to_string(),
223 ));
224 }
225
226 let max_order = (d / wavelength).floor() as i32;
227 let mut maxima = Vec::new();
228
229 for m in -max_order..=max_order {
230 let sin_theta = (m as f64) * wavelength / d;
231 if sin_theta.abs() <= 1.0 {
232 maxima.push((m, sin_theta.asin()));
233 }
234 }
235
236 let resolving_power = max_order as f64 * n_slits as f64;
237
238 Ok(DiffractionResult {
239 max_order,
240 maxima,
241 resolving_power,
242 })
243}
244
245pub fn brewster_angle(n1: f64, n2: f64) -> OpticsResult<f64> {
250 if n1 <= 0.0 || n2 <= 0.0 {
251 return Err(OpticsError::InvalidParameter(
252 "refractive indices must be positive".to_string(),
253 ));
254 }
255 Ok((n2 / n1).atan())
256}
257
258pub fn critical_angle(n1: f64, n2: f64) -> OpticsResult<f64> {
262 if n1 <= 0.0 || n2 <= 0.0 {
263 return Err(OpticsError::InvalidParameter(
264 "refractive indices must be positive".to_string(),
265 ));
266 }
267 if n1 <= n2 {
268 return Err(OpticsError::InvalidParameter(
269 "n1 must be greater than n2 for total internal reflection".to_string(),
270 ));
271 }
272 let ratio = n2 / n1;
273 Ok(ratio.asin())
274}
275
276pub fn optical_path_length(refractive_index: f64, physical_length: f64) -> OpticsResult<f64> {
280 if refractive_index <= 0.0 {
281 return Err(OpticsError::InvalidParameter(
282 "refractive index must be positive".to_string(),
283 ));
284 }
285 if physical_length < 0.0 {
286 return Err(OpticsError::InvalidParameter(
287 "physical length cannot be negative".to_string(),
288 ));
289 }
290 Ok(refractive_index * physical_length)
291}
292
293pub fn prism_minimum_deviation(n: f64, apex_angle: f64) -> OpticsResult<f64> {
301 if n <= 0.0 {
302 return Err(OpticsError::InvalidParameter(
303 "refractive index must be positive".to_string(),
304 ));
305 }
306 if apex_angle <= 0.0 || apex_angle >= PI {
307 return Err(OpticsError::InvalidParameter(
308 "apex angle must be in (0, pi)".to_string(),
309 ));
310 }
311
312 let sin_half = (apex_angle / 2.0).sin();
313 let arg = n * sin_half;
314 if arg.abs() > 1.0 {
315 return Err(OpticsError::TotalInternalReflection);
316 }
317 let d_min = 2.0 * arg.asin() - apex_angle;
318 Ok(d_min)
319}
320
321pub fn numerical_aperture(n_core: f64, n_cladding: f64) -> OpticsResult<f64> {
325 if n_core <= 0.0 || n_cladding <= 0.0 {
326 return Err(OpticsError::InvalidParameter(
327 "refractive indices must be positive".to_string(),
328 ));
329 }
330 if n_core < n_cladding {
331 return Err(OpticsError::InvalidParameter(
332 "core index must be >= cladding index".to_string(),
333 ));
334 }
335 let na_sq = n_core * n_core - n_cladding * n_cladding;
336 Ok(na_sq.sqrt())
337}
338
339#[cfg(test)]
344mod tests {
345 use super::*;
346
347 const DEG: f64 = PI / 180.0;
348
349 #[test]
352 fn test_snells_law_normal_incidence() {
353 let theta_t = snells_law(1.0, 1.5, 0.0).expect("should not fail");
354 assert!(theta_t.abs() < 1e-10, "normal incidence → 0 refraction");
355 }
356
357 #[test]
358 fn test_snells_law_air_to_glass() {
359 let theta_t = snells_law(1.0, 1.5, 30.0 * DEG).expect("should work");
360 let expected = (1.0_f64 / 3.0).asin();
362 assert!((theta_t - expected).abs() < 1e-10);
363 }
364
365 #[test]
366 fn test_snells_law_total_internal_reflection() {
367 let result = snells_law(1.5, 1.0, 50.0 * DEG);
369 assert!(result.is_err());
370 }
371
372 #[test]
373 fn test_snells_law_negative_n() {
374 let result = snells_law(-1.0, 1.5, 0.0);
375 assert!(result.is_err());
376 }
377
378 #[test]
379 fn test_snells_law_symmetry() {
380 let theta_i = 30.0 * DEG;
382 let theta_t = snells_law(1.0, 1.5, theta_i).expect("ok");
383 let theta_back = snells_law(1.5, 1.0, theta_t).expect("ok");
384 assert!((theta_back - theta_i).abs() < 1e-10);
385 }
386
387 #[test]
390 fn test_fresnel_normal_incidence() {
391 let f = fresnel(1.0, 1.5, 0.0).expect("ok");
392 assert!((f.rs - f.rp).abs() < 1e-10);
394 }
395
396 #[test]
397 fn test_fresnel_energy_conservation() {
398 let f = fresnel(1.0, 1.5, 30.0 * DEG).expect("ok");
399 assert!((f.rs + f.ts - 1.0).abs() < 1e-10);
400 assert!((f.rp + f.tp - 1.0).abs() < 1e-10);
401 }
402
403 #[test]
404 fn test_fresnel_same_medium() {
405 let f = fresnel(1.5, 1.5, 20.0 * DEG).expect("ok");
406 assert!(f.rs < 1e-10);
407 assert!(f.rp < 1e-10);
408 }
409
410 #[test]
411 fn test_fresnel_total_internal_reflection() {
412 let result = fresnel(1.5, 1.0, 50.0 * DEG);
413 assert!(result.is_err());
414 }
415
416 #[test]
419 fn test_thin_lens_converging() {
420 let r = thin_lens(10.0, 20.0).expect("ok");
421 assert!((r.image_distance - 20.0).abs() < 1e-10);
423 assert!((r.magnification - (-1.0)).abs() < 1e-10);
425 }
426
427 #[test]
428 fn test_thin_lens_object_at_2f() {
429 let f = 5.0;
430 let d_o = 2.0 * f;
431 let r = thin_lens(f, d_o).expect("ok");
432 assert!((r.image_distance - d_o).abs() < 1e-10);
433 }
434
435 #[test]
436 fn test_thin_lens_zero_focal() {
437 let result = thin_lens(0.0, 10.0);
438 assert!(result.is_err());
439 }
440
441 #[test]
442 fn test_thin_lens_virtual_image() {
443 let r = thin_lens(10.0, 5.0).expect("ok");
445 assert!(r.image_distance < 0.0);
446 assert!(r.magnification > 0.0); }
448
449 #[test]
452 fn test_trace_ray_single_surface() {
453 let surfaces = vec![OpticalSurface { n1: 1.0, n2: 1.5 }];
454 let result = trace_ray(&surfaces, 30.0 * DEG).expect("ok");
455 assert_eq!(result.incidence_angles.len(), 1);
456 assert_eq!(result.refraction_angles.len(), 1);
457 }
458
459 #[test]
460 fn test_trace_ray_two_surfaces_roundtrip() {
461 let surfaces = vec![
463 OpticalSurface { n1: 1.0, n2: 1.5 },
464 OpticalSurface { n1: 1.5, n2: 1.0 },
465 ];
466 let initial = 20.0 * DEG;
467 let result = trace_ray(&surfaces, initial).expect("ok");
468 assert!((result.final_angle - initial).abs() < 1e-10);
469 }
470
471 #[test]
472 fn test_trace_ray_empty() {
473 let result = trace_ray(&[], 0.0).expect("ok");
474 assert!(result.incidence_angles.is_empty());
475 assert_eq!(result.final_angle, 0.0);
476 }
477
478 #[test]
481 fn test_diffraction_grating_zeroth_order() {
482 let result = diffraction_grating(1e-6, 500e-9, 100).expect("ok");
483 assert!(result
485 .maxima
486 .iter()
487 .any(|(m, a)| *m == 0 && a.abs() < 1e-10));
488 }
489
490 #[test]
491 fn test_diffraction_grating_max_order() {
492 let d = 2e-6;
493 let wl = 500e-9;
494 let result = diffraction_grating(d, wl, 50).expect("ok");
495 assert_eq!(result.max_order, 4);
497 }
498
499 #[test]
500 fn test_diffraction_grating_resolving_power() {
501 let result = diffraction_grating(1e-6, 500e-9, 100).expect("ok");
502 assert!(result.resolving_power > 0.0);
503 }
504
505 #[test]
506 fn test_diffraction_grating_invalid() {
507 assert!(diffraction_grating(0.0, 500e-9, 10).is_err());
508 assert!(diffraction_grating(1e-6, -1.0, 10).is_err());
509 }
510
511 #[test]
514 fn test_brewster_angle_air_glass() {
515 let theta_b = brewster_angle(1.0, 1.5).expect("ok");
516 let expected = (1.5_f64).atan();
517 assert!((theta_b - expected).abs() < 1e-10);
518 }
519
520 #[test]
521 fn test_brewster_angle_complement_critical() {
522 let theta_b = brewster_angle(1.5, 1.0).expect("ok");
524 let theta_c = critical_angle(1.5, 1.0).expect("ok");
525 assert!(theta_b + theta_c < PI / 2.0 + 0.1);
526 }
527
528 #[test]
529 fn test_brewster_angle_invalid() {
530 assert!(brewster_angle(-1.0, 1.5).is_err());
531 }
532
533 #[test]
536 fn test_critical_angle_glass_air() {
537 let theta_c = critical_angle(1.5, 1.0).expect("ok");
538 let expected = (1.0_f64 / 1.5).asin();
539 assert!((theta_c - expected).abs() < 1e-10);
540 }
541
542 #[test]
543 fn test_critical_angle_n1_less_than_n2() {
544 assert!(critical_angle(1.0, 1.5).is_err());
546 }
547
548 #[test]
549 fn test_critical_angle_equal_indices() {
550 assert!(critical_angle(1.5, 1.5).is_err());
551 }
552
553 #[test]
556 fn test_optical_path_length_vacuum() {
557 let opl = optical_path_length(1.0, 10.0).expect("ok");
558 assert!((opl - 10.0).abs() < 1e-10);
559 }
560
561 #[test]
562 fn test_optical_path_length_glass() {
563 let opl = optical_path_length(1.5, 10.0).expect("ok");
564 assert!((opl - 15.0).abs() < 1e-10);
565 }
566
567 #[test]
568 fn test_optical_path_length_zero_length() {
569 let opl = optical_path_length(1.5, 0.0).expect("ok");
570 assert!(opl.abs() < 1e-10);
571 }
572
573 #[test]
574 fn test_optical_path_length_invalid_n() {
575 assert!(optical_path_length(0.0, 10.0).is_err());
576 }
577
578 #[test]
579 fn test_optical_path_length_negative_length() {
580 assert!(optical_path_length(1.5, -1.0).is_err());
581 }
582
583 #[test]
586 fn test_prism_min_deviation_equilateral() {
587 let a = 60.0 * DEG;
589 let d = prism_minimum_deviation(1.5, a).expect("ok");
590 let expected = 2.0 * (1.5 * (a / 2.0).sin()).asin() - a;
592 assert!((d - expected).abs() < 1e-10);
593 }
594
595 #[test]
596 fn test_prism_min_deviation_positive() {
597 let d = prism_minimum_deviation(1.5, 60.0 * DEG).expect("ok");
598 assert!(d > 0.0);
599 }
600
601 #[test]
602 fn test_prism_min_deviation_invalid_n() {
603 assert!(prism_minimum_deviation(0.0, 60.0 * DEG).is_err());
604 }
605
606 #[test]
607 fn test_prism_min_deviation_invalid_angle() {
608 assert!(prism_minimum_deviation(1.5, 0.0).is_err());
609 assert!(prism_minimum_deviation(1.5, PI).is_err());
610 }
611
612 #[test]
615 fn test_numerical_aperture_fiber() {
616 let na = numerical_aperture(1.5, 1.45).expect("ok");
617 let expected = ((1.5f64).powi(2) - (1.45f64).powi(2)).sqrt();
618 assert!((na - expected).abs() < 1e-10);
619 }
620
621 #[test]
622 fn test_numerical_aperture_same_index() {
623 let na = numerical_aperture(1.5, 1.5).expect("ok");
624 assert!(na.abs() < 1e-10);
625 }
626
627 #[test]
628 fn test_numerical_aperture_invalid() {
629 assert!(numerical_aperture(1.0, 1.5).is_err()); assert!(numerical_aperture(-1.0, 0.5).is_err());
631 }
632
633 #[test]
636 fn test_optics_error_display_tir() {
637 let e = OpticsError::TotalInternalReflection;
638 assert!(format!("{e}").contains("total internal reflection"));
639 }
640
641 #[test]
642 fn test_optics_error_display_invalid() {
643 let e = OpticsError::InvalidParameter("test".to_string());
644 assert!(format!("{e}").contains("test"));
645 }
646
647 #[test]
648 fn test_optics_error_display_no_image() {
649 let e = OpticsError::NoImage;
650 assert!(format!("{e}").contains("no real image"));
651 }
652
653 #[test]
656 fn test_ray_trace_result_fields() {
657 let surfaces = vec![OpticalSurface { n1: 1.0, n2: 1.5 }];
658 let r = trace_ray(&surfaces, 10.0 * DEG).expect("ok");
659 assert_eq!(r.incidence_angles.len(), 1);
660 assert_eq!(r.refraction_angles.len(), 1);
661 assert!(r.final_angle < 10.0 * DEG); }
663
664 #[test]
665 fn test_diffraction_result_maxima_symmetric() {
666 let r = diffraction_grating(1e-6, 500e-9, 10).expect("ok");
667 for &(m, angle) in &r.maxima {
669 if m > 0 {
670 let neg = r.maxima.iter().find(|&&(mm, _)| mm == -m);
671 assert!(neg.is_some());
672 let neg_angle = neg.map(|&(_, a)| a).unwrap_or(0.0);
674 assert!((angle + neg_angle).abs() < 1e-10);
675 }
676 }
677 }
678
679 #[test]
680 fn test_thin_lens_result_fields() {
681 let r = thin_lens(10.0, 30.0).expect("ok");
682 assert!(r.image_distance > 0.0);
683 assert!(r.magnification < 0.0); }
685}