1#![allow(dead_code)]
16#![allow(clippy::too_many_arguments)]
17
18use std::f64::consts::PI;
19
20pub const C0: f64 = 2.997_924_58e8;
26
27pub const EPS0: f64 = 8.854_187_817e-12;
29
30pub const MU0: f64 = 1.256_637_061_4e-6;
32
33pub const Z0: f64 = 376.730_313_668;
35
36pub const E_CHARGE: f64 = 1.602_176_634e-19;
38
39pub const E_MASS: f64 = 9.109_383_701_5e-31;
41
42pub const KB: f64 = 1.380_649e-23;
44
45pub const HBAR: f64 = 1.054_571_817e-34;
47
48#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Tensor3x3 {
58 pub data: [[f64; 3]; 3],
60}
61
62impl Tensor3x3 {
63 pub fn diagonal(d: f64) -> Self {
65 let mut t = Self::zero();
66 t.data[0][0] = d;
67 t.data[1][1] = d;
68 t.data[2][2] = d;
69 t
70 }
71
72 pub fn zero() -> Self {
74 Self {
75 data: [[0.0; 3]; 3],
76 }
77 }
78
79 pub fn from_diag(d0: f64, d1: f64, d2: f64) -> Self {
81 let mut t = Self::zero();
82 t.data[0][0] = d0;
83 t.data[1][1] = d1;
84 t.data[2][2] = d2;
85 t
86 }
87
88 #[allow(clippy::needless_range_loop)]
90 pub fn mul_vec(&self, x: [f64; 3]) -> [f64; 3] {
91 let mut y = [0.0_f64; 3];
92 for i in 0..3 {
93 for j in 0..3 {
94 y[i] += self.data[i][j] * x[j];
95 }
96 }
97 y
98 }
99
100 pub fn trace(&self) -> f64 {
102 self.data[0][0] + self.data[1][1] + self.data[2][2]
103 }
104
105 pub fn transpose(&self) -> Self {
107 let mut t = Self::zero();
108 for i in 0..3 {
109 for j in 0..3 {
110 t.data[i][j] = self.data[j][i];
111 }
112 }
113 t
114 }
115
116 pub fn is_symmetric(&self, eps: f64) -> bool {
118 for i in 0..3 {
119 for j in 0..3 {
120 if (self.data[i][j] - self.data[j][i]).abs() > eps {
121 return false;
122 }
123 }
124 }
125 true
126 }
127}
128
129#[derive(Debug, Clone)]
135pub struct ElectromagneticMaterial {
136 pub name: String,
138 pub permittivity: Tensor3x3,
140 pub permeability: Tensor3x3,
142 pub conductivity: Tensor3x3,
144 pub loss_tangent: f64,
146 pub ref_freq: f64,
148}
149
150impl ElectromagneticMaterial {
151 pub fn dielectric(name: &str, eps_r: f64, loss_tangent: f64, ref_freq: f64) -> Self {
153 Self {
154 name: name.to_owned(),
155 permittivity: Tensor3x3::diagonal(eps_r),
156 permeability: Tensor3x3::diagonal(1.0),
157 conductivity: Tensor3x3::zero(),
158 loss_tangent,
159 ref_freq,
160 }
161 }
162
163 pub fn conductor(name: &str, sigma: f64) -> Self {
165 Self {
166 name: name.to_owned(),
167 permittivity: Tensor3x3::diagonal(1.0),
168 permeability: Tensor3x3::diagonal(1.0),
169 conductivity: Tensor3x3::diagonal(sigma),
170 loss_tangent: 0.0,
171 ref_freq: 1.0e9,
172 }
173 }
174
175 pub fn eps_r_scalar(&self) -> f64 {
177 self.permittivity.trace() / 3.0
178 }
179
180 pub fn mu_r_scalar(&self) -> f64 {
182 self.permeability.trace() / 3.0
183 }
184
185 pub fn intrinsic_impedance(&self) -> f64 {
187 let eps = self.eps_r_scalar().max(1e-30);
188 let mu = self.mu_r_scalar().max(1e-30);
189 Z0 * (mu / eps).sqrt()
190 }
191
192 pub fn phase_velocity(&self) -> f64 {
194 let eps = self.eps_r_scalar().max(1e-30);
195 let mu = self.mu_r_scalar().max(1e-30);
196 C0 / (eps * mu).sqrt()
197 }
198
199 pub fn skin_depth(&self, f: f64) -> f64 {
201 let sigma = self.conductivity.trace() / 3.0;
202 let mu = self.mu_r_scalar();
203 if sigma < 1e-30 || f < 1e-30 {
204 return f64::INFINITY;
205 }
206 1.0 / (PI * f * mu * MU0 * sigma).sqrt()
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum MetamaterialClass {
217 DoublePosive,
219 DoubleNegative,
221 EpsilonNegative,
223 MuNegative,
225 EpsilonNearZero,
227 MuNearZero,
229}
230
231#[derive(Debug, Clone)]
234pub struct Metamaterial {
235 pub name: String,
237 pub eps_r_real: f64,
239 pub eps_r_imag: f64,
241 pub mu_r_real: f64,
243 pub mu_r_imag: f64,
245 pub freq: f64,
247}
248
249impl Metamaterial {
250 pub fn classify(&self) -> MetamaterialClass {
252 let enz_thresh = 0.01;
253 let mnz_thresh = 0.01;
254 if self.eps_r_real.abs() < enz_thresh {
255 return MetamaterialClass::EpsilonNearZero;
256 }
257 if self.mu_r_real.abs() < mnz_thresh {
258 return MetamaterialClass::MuNearZero;
259 }
260 match (self.eps_r_real < 0.0, self.mu_r_real < 0.0) {
261 (true, true) => MetamaterialClass::DoubleNegative,
262 (true, false) => MetamaterialClass::EpsilonNegative,
263 (false, true) => MetamaterialClass::MuNegative,
264 (false, false) => MetamaterialClass::DoublePosive,
265 }
266 }
267
268 pub fn refractive_index(&self) -> f64 {
272 let n2 = self.eps_r_real * self.mu_r_real;
273 if n2 >= 0.0 {
274 if self.eps_r_real < 0.0 && self.mu_r_real < 0.0 {
276 -(n2.sqrt())
277 } else {
278 n2.sqrt()
279 }
280 } else {
281 -((-n2).sqrt())
283 }
284 }
285
286 pub fn group_velocity_approx(&self) -> f64 {
289 let n = self.refractive_index();
290 if n.abs() < 1e-30 { C0 } else { C0 / n }
291 }
292}
293
294#[derive(Debug, Clone, Copy)]
303pub struct DrudeModel {
304 pub eps_inf: f64,
306 pub omega_p: f64,
308 pub gamma: f64,
310}
311
312impl DrudeModel {
313 pub fn new(eps_inf: f64, omega_p: f64, gamma: f64) -> Self {
315 Self {
316 eps_inf,
317 omega_p,
318 gamma,
319 }
320 }
321
322 pub fn gold() -> Self {
324 Self {
325 eps_inf: 9.5,
326 omega_p: 1.37e16, gamma: 1.05e14, }
329 }
330
331 pub fn silver() -> Self {
333 Self {
334 eps_inf: 5.0,
335 omega_p: 1.39e16,
336 gamma: 2.73e13,
337 }
338 }
339
340 pub fn eps_real(&self, omega: f64) -> f64 {
342 self.eps_inf - self.omega_p * self.omega_p / (omega * omega + self.gamma * self.gamma)
343 }
344
345 pub fn eps_imag(&self, omega: f64) -> f64 {
347 self.omega_p * self.omega_p * self.gamma
348 / (omega * (omega * omega + self.gamma * self.gamma))
349 }
350
351 pub fn dc_conductivity(&self) -> f64 {
353 EPS0 * self.omega_p * self.omega_p / self.gamma
354 }
355
356 pub fn skin_depth(&self, omega: f64) -> f64 {
358 let eps_im = self.eps_imag(omega);
359 if eps_im < 1e-30 {
360 return f64::INFINITY;
361 }
362 C0 / (omega * (eps_im / 2.0).sqrt())
364 }
365
366 pub fn plasma_freq_hz(&self) -> f64 {
368 self.omega_p / (2.0 * PI)
369 }
370}
371
372#[derive(Debug, Clone, Copy)]
380pub struct LorentzOscillator {
381 pub eps_inf: f64,
383 pub delta_eps: f64,
385 pub omega0: f64,
387 pub gamma: f64,
389}
390
391impl LorentzOscillator {
392 pub fn new(eps_inf: f64, delta_eps: f64, omega0: f64, gamma: f64) -> Self {
394 Self {
395 eps_inf,
396 delta_eps,
397 omega0,
398 gamma,
399 }
400 }
401
402 pub fn eps_real(&self, omega: f64) -> f64 {
404 let d = (self.omega0 * self.omega0 - omega * omega).powi(2) + (self.gamma * omega).powi(2);
405 self.eps_inf
406 + self.delta_eps
407 * self.omega0
408 * self.omega0
409 * (self.omega0 * self.omega0 - omega * omega)
410 / d
411 }
412
413 pub fn eps_imag(&self, omega: f64) -> f64 {
415 let d = (self.omega0 * self.omega0 - omega * omega).powi(2) + (self.gamma * omega).powi(2);
416 self.delta_eps * self.omega0 * self.omega0 * self.gamma * omega / d
417 }
418
419 pub fn eps_static(&self) -> f64 {
421 self.eps_inf + self.delta_eps
422 }
423
424 pub fn eps_imag_at_resonance(&self) -> f64 {
426 self.eps_imag(self.omega0)
427 }
428}
429
430#[derive(Debug, Clone)]
440pub struct SellmeierModel {
441 pub b: Vec<f64>,
443 pub c: Vec<f64>,
445}
446
447impl SellmeierModel {
448 pub fn new(b: Vec<f64>, c: Vec<f64>) -> Self {
450 assert_eq!(b.len(), c.len(), "Sellmeier B and C must have equal length");
451 Self { b, c }
452 }
453
454 pub fn bk7() -> Self {
456 Self::new(
457 vec![1.039_612_12, 0.231_792_344, 1.010_469_45],
458 vec![0.006_000_699_5, 0.020_017_914, 103.560_653],
459 )
460 }
461
462 pub fn fused_silica() -> Self {
464 Self::new(
465 vec![0.696_166_3, 0.407_942_6, 0.897_479_4],
466 vec![0.004_679_148_6, 0.013_512_063, 97.934_002_5],
467 )
468 }
469
470 pub fn refractive_index(&self, lambda_um: f64) -> f64 {
472 let l2 = lambda_um * lambda_um;
473 let n2 = 1.0
474 + self
475 .b
476 .iter()
477 .zip(self.c.iter())
478 .map(|(&bi, &ci)| bi * l2 / (l2 - ci))
479 .sum::<f64>();
480 n2.max(0.0).sqrt()
481 }
482
483 pub fn group_index(&self, lambda_um: f64) -> f64 {
485 let dl = 1e-5_f64;
486 let n1 = self.refractive_index(lambda_um - dl);
487 let n2 = self.refractive_index(lambda_um + dl);
488 let dn_dl = (n2 - n1) / (2.0 * dl);
489 self.refractive_index(lambda_um) - lambda_um * dn_dl
490 }
491
492 pub fn gvd(&self, lambda_um: f64) -> f64 {
494 let dl = 1e-4_f64;
495 let n0 = self.refractive_index(lambda_um);
496 let np = self.refractive_index(lambda_um + dl);
497 let nm = self.refractive_index(lambda_um - dl);
498 let d2n = (np - 2.0 * n0 + nm) / (dl * dl);
499 let lambda_m = lambda_um * 1e-6;
500 lambda_m.powi(3) / (2.0 * PI * C0 * C0) * d2n
501 }
502}
503
504#[derive(Debug, Clone, Copy)]
510pub struct FaradayRotation {
511 pub verdet_constant: f64,
513 pub b_field: f64,
515 pub length: f64,
517}
518
519impl FaradayRotation {
520 pub fn new(verdet_constant: f64, b_field: f64, length: f64) -> Self {
522 Self {
523 verdet_constant,
524 b_field,
525 length,
526 }
527 }
528
529 pub fn rotation_angle(&self) -> f64 {
531 self.verdet_constant * self.b_field * self.length
532 }
533
534 pub fn rotation_angle_deg(&self) -> f64 {
536 self.rotation_angle().to_degrees()
537 }
538
539 pub fn tgg_verdet_1064nm() -> f64 {
541 -40.0 }
543
544 pub fn eps_xy_estimate(&self, n: f64) -> f64 {
549 let theta = self.rotation_angle();
552 let lambda_approx = 1.064e-6_f64;
553 let delta_n = theta * lambda_approx / (PI * self.length.max(1e-30));
554 2.0 * n * delta_n
555 }
556}
557
558#[derive(Debug, Clone, Copy)]
564pub struct Superconductor {
565 pub tc: f64,
567 pub lambda_london_0: f64,
569 pub xi0: f64,
571}
572
573impl Superconductor {
574 pub fn new(tc: f64, lambda_london_0: f64, xi0: f64) -> Self {
576 Self {
577 tc,
578 lambda_london_0,
579 xi0,
580 }
581 }
582
583 pub fn ybco() -> Self {
585 Self {
586 tc: 92.0,
587 lambda_london_0: 140e-9,
588 xi0: 1.2e-9,
589 }
590 }
591
592 pub fn niobium() -> Self {
594 Self {
595 tc: 9.26,
596 lambda_london_0: 39e-9,
597 xi0: 38e-9,
598 }
599 }
600
601 pub fn penetration_depth(&self, temp_k: f64) -> f64 {
604 if temp_k >= self.tc {
605 return f64::INFINITY; }
607 let t_ratio = temp_k / self.tc;
608 self.lambda_london_0 / (1.0 - t_ratio.powi(4)).sqrt()
609 }
610
611 pub fn gl_parameter(&self, temp_k: f64) -> f64 {
615 self.penetration_depth(temp_k) / self.xi0
616 }
617
618 pub fn surface_resistance(&self, freq: f64, sigma_normal: f64, temp_k: f64) -> f64 {
621 let omega = 2.0 * PI * freq;
622 let lam = self.penetration_depth(temp_k);
623 if lam.is_infinite() {
624 return f64::INFINITY;
625 }
626 MU0 * MU0 * sigma_normal * omega * omega * lam.powi(3) / 2.0
627 }
628
629 pub fn is_superconducting(&self, temp_k: f64) -> bool {
631 temp_k < self.tc
632 }
633
634 pub fn hc1_estimate(&self, temp_k: f64) -> f64 {
636 let lam = self.penetration_depth(temp_k);
637 if lam.is_infinite() {
638 return 0.0;
639 }
640 let phi0 = 2.067_833_848e-15_f64; let kappa = self.gl_parameter(temp_k);
643 if kappa <= 1.0 {
644 return 0.0;
645 }
646 phi0 * kappa.ln() / (4.0 * PI * MU0 * lam * lam)
647 }
648}
649
650#[derive(Debug, Clone, Copy)]
659pub struct FerroelectricPE {
660 pub p_sat: f64,
662 pub e_coercive: f64,
664 pub e0: f64,
666}
667
668impl FerroelectricPE {
669 pub fn new(p_sat: f64, e_coercive: f64, e0: f64) -> Self {
671 Self {
672 p_sat,
673 e_coercive,
674 e0,
675 }
676 }
677
678 pub fn batio3() -> Self {
680 Self {
681 p_sat: 0.26,
682 e_coercive: 1.0e5,
683 e0: 5.0e4,
684 }
685 }
686
687 pub fn polarisation_ascending(&self, e_field: f64) -> f64 {
689 self.p_sat * ((e_field - self.e_coercive) / self.e0).tanh()
690 }
691
692 pub fn polarisation_descending(&self, e_field: f64) -> f64 {
694 self.p_sat * ((e_field + self.e_coercive) / self.e0).tanh()
695 }
696
697 pub fn remnant_polarisation(&self) -> f64 {
699 self.polarisation_ascending(0.0).abs()
700 }
701
702 pub fn hysteresis_energy_loss(&self, e_max: f64, n_steps: usize) -> f64 {
706 let de = 2.0 * e_max / n_steps as f64;
707 let mut loss = 0.0_f64;
708 let mut e = -e_max;
710 let mut p_prev = self.polarisation_ascending(e);
711 for _ in 0..n_steps {
712 e += de;
713 let p = self.polarisation_ascending(e);
714 loss += e * (p - p_prev);
715 p_prev = p;
716 }
717 e = e_max;
719 p_prev = self.polarisation_descending(e);
720 for _ in 0..n_steps {
721 e -= de;
722 let p = self.polarisation_descending(e);
723 loss -= e * (p - p_prev); p_prev = p;
725 }
726 loss.abs()
727 }
728}
729
730#[derive(Debug, Clone, Copy)]
739pub struct JilesAthertonParams {
740 pub m_sat: f64,
742 pub a: f64,
744 pub alpha: f64,
746 pub k: f64,
748 pub c: f64,
750}
751
752impl JilesAthertonParams {
753 pub fn new(m_sat: f64, a: f64, alpha: f64, k: f64, c: f64) -> Self {
755 Self {
756 m_sat,
757 a,
758 alpha,
759 k,
760 c,
761 }
762 }
763
764 pub fn soft_iron() -> Self {
766 Self {
767 m_sat: 1.6e6,
768 a: 470.0,
769 alpha: 1.0e-4,
770 k: 400.0,
771 c: 0.1,
772 }
773 }
774
775 fn langevin(x: f64) -> f64 {
777 if x.abs() < 1e-6 {
778 x / 3.0
779 } else {
780 1.0 / x.tanh() - 1.0 / x
781 }
782 }
783
784 pub fn h_effective(&self, h: f64, m: f64) -> f64 {
786 h + self.alpha * m
787 }
788
789 pub fn anhysteretic(&self, h: f64, m: f64) -> f64 {
791 let h_eff = self.h_effective(h, m);
792 self.m_sat * Self::langevin(h_eff / self.a)
793 }
794
795 pub fn compute_bh_loop(&self, h_max: f64, n_steps: usize) -> (Vec<f64>, Vec<f64>) {
799 let mut h_vals = Vec::with_capacity(2 * n_steps);
800 let mut m_vals = Vec::with_capacity(2 * n_steps);
801
802 let dh = 2.0 * h_max / n_steps as f64;
803 let mut m = 0.0_f64;
804
805 let mut h = -h_max;
807 for _ in 0..n_steps {
808 h_vals.push(h);
809 m_vals.push(m);
810 let m_an = self.anhysteretic(h, m);
811 let denom = (1.0 - self.c) * self.k;
812 let dm_dh = if denom.abs() < 1e-30 {
813 0.0
814 } else {
815 (m_an - m) / denom + self.c * (m_an - m) / self.a
816 };
817 m += dm_dh * dh;
818 m = m.clamp(-self.m_sat, self.m_sat);
819 h += dh;
820 }
821 h = h_max;
823 for _ in 0..n_steps {
824 h_vals.push(h);
825 m_vals.push(m);
826 let m_an = self.anhysteretic(h, m);
827 let denom = (1.0 - self.c) * self.k;
828 let dm_dh = if denom.abs() < 1e-30 {
829 0.0
830 } else {
831 (m_an - m) / denom + self.c * (m_an - m) / self.a
832 };
833 m -= dm_dh * dh;
834 m = m.clamp(-self.m_sat, self.m_sat);
835 h -= dh;
836 }
837
838 (h_vals, m_vals)
839 }
840
841 pub fn coercive_field(&self, h_max: f64, n_steps: usize) -> f64 {
843 let (h_vals, m_vals) = self.compute_bh_loop(h_max, n_steps);
844 let mut hc = 0.0_f64;
846 let n = h_vals.len();
847 for i in 1..n {
848 if m_vals[i - 1] * m_vals[i] < 0.0 {
849 hc = h_vals[i].abs();
850 break;
851 }
852 }
853 hc
854 }
855}
856
857pub fn b_from_jiles_atherton(_ja: &JilesAthertonParams, h: f64, m: f64) -> f64 {
859 MU0 * (h + m)
860}
861
862#[derive(Debug, Clone, Copy)]
871pub struct ShieldingEffectiveness {
872 pub mu_r: f64,
874 pub sigma: f64,
876 pub thickness: f64,
878}
879
880impl ShieldingEffectiveness {
881 pub fn new(mu_r: f64, sigma: f64, thickness: f64) -> Self {
883 Self {
884 mu_r,
885 sigma,
886 thickness,
887 }
888 }
889
890 pub fn copper(thickness: f64) -> Self {
892 Self {
893 mu_r: 1.0,
894 sigma: 5.8e7,
895 thickness,
896 }
897 }
898
899 pub fn mild_steel(thickness: f64) -> Self {
901 Self {
902 mu_r: 200.0,
903 sigma: 1.0e7,
904 thickness,
905 }
906 }
907
908 pub fn skin_depth(&self, f: f64) -> f64 {
910 1.0 / (PI * f * self.mu_r * MU0 * self.sigma).sqrt()
911 }
912
913 pub fn absorption_loss_db(&self, f: f64) -> f64 {
915 let delta = self.skin_depth(f);
916 if delta < 1e-30 {
917 return f64::INFINITY;
918 }
919 20.0 * (self.thickness / delta) * std::f64::consts::LOG10_E
920 }
921
922 pub fn reflection_loss_db(&self, f: f64) -> f64 {
926 168.0 + 10.0 * (self.sigma / (self.mu_r * f)).log10()
927 }
928
929 pub fn re_reflection_correction_db(&self, f: f64) -> f64 {
931 let a = self.absorption_loss_db(f);
932 if a > 10.0 {
933 return 0.0;
934 }
935 let ratio = 10.0_f64.powf(-a / 10.0);
937 20.0 * (1.0 - ratio).abs().log10()
938 }
939
940 pub fn total_se_db(&self, f: f64) -> f64 {
942 self.reflection_loss_db(f)
943 + self.absorption_loss_db(f)
944 + self.re_reflection_correction_db(f)
945 }
946}
947
948pub fn fr4() -> ElectromagneticMaterial {
954 ElectromagneticMaterial::dielectric("FR4", 4.5, 0.02, 1.0e9)
955}
956
957pub fn ptfe() -> ElectromagneticMaterial {
959 ElectromagneticMaterial::dielectric("PTFE", 2.1, 0.0002, 1.0e9)
960}
961
962pub fn water_2_45ghz() -> ElectromagneticMaterial {
964 ElectromagneticMaterial::dielectric("Water (2.45 GHz)", 80.0, 0.157, 2.45e9)
965}
966
967pub fn copper_em() -> ElectromagneticMaterial {
969 ElectromagneticMaterial::conductor("Copper", 5.8e7)
970}
971
972#[cfg(test)]
977mod tests {
978 use super::*;
979
980 const EPS: f64 = 1e-9;
981
982 #[test]
985 fn test_tensor_diagonal_trace() {
986 let t = Tensor3x3::diagonal(3.0);
987 assert!((t.trace() - 9.0).abs() < EPS);
988 }
989
990 #[test]
991 fn test_tensor_from_diag_mul_vec() {
992 let t = Tensor3x3::from_diag(2.0, 3.0, 4.0);
993 let y = t.mul_vec([1.0, 1.0, 1.0]);
994 assert!((y[0] - 2.0).abs() < EPS);
995 assert!((y[1] - 3.0).abs() < EPS);
996 assert!((y[2] - 4.0).abs() < EPS);
997 }
998
999 #[test]
1000 fn test_tensor_transpose_diagonal_unchanged() {
1001 let t = Tensor3x3::diagonal(5.0);
1002 let tr = t.transpose();
1003 for i in 0..3 {
1004 for j in 0..3 {
1005 assert!((t.data[i][j] - tr.data[i][j]).abs() < EPS);
1006 }
1007 }
1008 }
1009
1010 #[test]
1011 fn test_tensor_is_symmetric() {
1012 let t = Tensor3x3::diagonal(1.0);
1013 assert!(t.is_symmetric(1e-12));
1014 }
1015
1016 #[test]
1017 fn test_tensor_zero_trace() {
1018 let t = Tensor3x3::zero();
1019 assert_eq!(t.trace(), 0.0);
1020 }
1021
1022 #[test]
1025 fn test_em_material_eps_r_scalar() {
1026 let m = ElectromagneticMaterial::dielectric("test", 4.0, 0.01, 1e9);
1027 assert!((m.eps_r_scalar() - 4.0).abs() < EPS);
1028 }
1029
1030 #[test]
1031 fn test_em_material_intrinsic_impedance_free_space() {
1032 let m = ElectromagneticMaterial::dielectric("vacuum", 1.0, 0.0, 1e9);
1034 assert!((m.intrinsic_impedance() - Z0).abs() < 1e-6);
1035 }
1036
1037 #[test]
1038 fn test_em_material_phase_velocity_free_space() {
1039 let m = ElectromagneticMaterial::dielectric("vacuum", 1.0, 0.0, 1e9);
1040 assert!((m.phase_velocity() - C0).abs() < 1.0); }
1042
1043 #[test]
1044 fn test_em_material_skin_depth_conductor() {
1045 let m = copper_em();
1046 let d = m.skin_depth(1.0e9); assert!(d > 0.0 && d < 1e-3, "skin depth = {d}");
1048 }
1049
1050 #[test]
1051 fn test_em_material_insulator_infinite_skin_depth() {
1052 let m = ptfe();
1053 let d = m.skin_depth(1.0e9);
1054 assert!(d.is_infinite());
1055 }
1056
1057 #[test]
1060 fn test_metamaterial_double_negative() {
1061 let m = Metamaterial {
1062 name: "DNG".to_owned(),
1063 eps_r_real: -1.0,
1064 eps_r_imag: 0.01,
1065 mu_r_real: -1.0,
1066 mu_r_imag: 0.01,
1067 freq: 1.0e10,
1068 };
1069 assert_eq!(m.classify(), MetamaterialClass::DoubleNegative);
1070 assert!(m.refractive_index() < 0.0);
1071 }
1072
1073 #[test]
1074 fn test_metamaterial_enz() {
1075 let m = Metamaterial {
1076 name: "ENZ".to_owned(),
1077 eps_r_real: 0.005,
1078 eps_r_imag: 0.001,
1079 mu_r_real: 1.0,
1080 mu_r_imag: 0.0,
1081 freq: 1.0e12,
1082 };
1083 assert_eq!(m.classify(), MetamaterialClass::EpsilonNearZero);
1084 }
1085
1086 #[test]
1087 fn test_metamaterial_eps_negative() {
1088 let m = Metamaterial {
1089 name: "ENG".to_owned(),
1090 eps_r_real: -2.0,
1091 eps_r_imag: 0.1,
1092 mu_r_real: 1.0,
1093 mu_r_imag: 0.0,
1094 freq: 1.0e10,
1095 };
1096 assert_eq!(m.classify(), MetamaterialClass::EpsilonNegative);
1097 }
1098
1099 #[test]
1102 fn test_drude_gold_eps_negative_visible() {
1103 let d = DrudeModel::gold();
1104 let omega = 2.0 * PI * 500.0e12;
1106 assert!(
1107 d.eps_real(omega) < 0.0,
1108 "gold eps_real should be negative in visible"
1109 );
1110 }
1111
1112 #[test]
1113 fn test_drude_dc_conductivity_positive() {
1114 let d = DrudeModel::silver();
1115 assert!(d.dc_conductivity() > 0.0);
1116 }
1117
1118 #[test]
1119 fn test_drude_plasma_freq_hz() {
1120 let d = DrudeModel::gold();
1121 let fp = d.plasma_freq_hz();
1122 assert!(fp > 1.0e14, "plasma freq should be >100 THz for gold: {fp}");
1123 }
1124
1125 #[test]
1126 fn test_drude_skin_depth_positive() {
1127 let d = DrudeModel::silver();
1128 let omega = 2.0 * PI * 1.0e15;
1129 let delta = d.skin_depth(omega);
1130 assert!(delta > 0.0);
1131 }
1132
1133 #[test]
1136 fn test_lorentz_static_eps() {
1137 let l = LorentzOscillator::new(2.0, 3.0, 1.0e15, 1.0e13);
1138 assert!((l.eps_static() - 5.0).abs() < EPS);
1139 }
1140
1141 #[test]
1142 fn test_lorentz_eps_imag_positive() {
1143 let l = LorentzOscillator::new(2.0, 3.0, 1.0e15, 1.0e13);
1144 assert!(l.eps_imag(1.0e15) > 0.0);
1145 }
1146
1147 #[test]
1148 fn test_lorentz_eps_real_below_resonance() {
1149 let l = LorentzOscillator::new(2.0, 3.0, 1.0e15, 1.0e13);
1151 let er_low = l.eps_real(1.0e10); assert!((er_low - l.eps_static()).abs() < 0.1);
1153 }
1154
1155 #[test]
1156 fn test_lorentz_resonance_peak() {
1157 let l = LorentzOscillator::new(2.0, 3.0, 1.0e15, 1.0e13);
1158 let peak = l.eps_imag_at_resonance();
1159 assert!(peak > 0.0);
1160 }
1161
1162 #[test]
1165 fn test_sellmeier_bk7_visible() {
1166 let s = SellmeierModel::bk7();
1167 let n = s.refractive_index(0.587); assert!(n > 1.5 && n < 1.6, "BK7 n at 587 nm = {n}");
1169 }
1170
1171 #[test]
1172 fn test_sellmeier_fused_silica() {
1173 let s = SellmeierModel::fused_silica();
1174 let n = s.refractive_index(1.0); assert!(n > 1.44 && n < 1.46, "SiO2 n at 1 μm = {n}");
1176 }
1177
1178 #[test]
1179 fn test_sellmeier_group_index_greater_than_phase_index() {
1180 let s = SellmeierModel::bk7();
1181 let n = s.refractive_index(0.8);
1182 let ng = s.group_index(0.8);
1183 assert!(ng > n, "n_g={ng}, n={n}");
1185 }
1186
1187 #[test]
1188 fn test_sellmeier_gvd_finite() {
1189 let s = SellmeierModel::fused_silica();
1190 let gvd = s.gvd(1.3); assert!(gvd.is_finite());
1192 }
1193
1194 #[test]
1197 fn test_faraday_rotation_angle() {
1198 let f = FaradayRotation::new(100.0, 1.0, 0.01); let theta = f.rotation_angle_deg();
1200 assert!((theta - 100.0_f64.to_degrees() * 0.01).abs() < 1e-6);
1201 }
1202
1203 #[test]
1204 fn test_faraday_rotation_zero_field() {
1205 let f = FaradayRotation::new(100.0, 0.0, 0.01);
1206 assert!((f.rotation_angle()).abs() < EPS);
1207 }
1208
1209 #[test]
1210 fn test_faraday_rotation_eps_xy_finite() {
1211 let f = FaradayRotation::new(40.0, 0.5, 0.05);
1212 let eps_xy = f.eps_xy_estimate(2.3);
1213 assert!(eps_xy.is_finite());
1214 }
1215
1216 #[test]
1219 fn test_superconductor_below_tc() {
1220 let s = Superconductor::ybco();
1221 assert!(s.is_superconducting(77.0));
1222 assert!(!s.is_superconducting(100.0));
1223 }
1224
1225 #[test]
1226 fn test_superconductor_penetration_depth_increases_with_temp() {
1227 let s = Superconductor::niobium();
1228 let d_low = s.penetration_depth(1.0);
1229 let d_high = s.penetration_depth(8.0);
1230 assert!(d_high > d_low, "λ should increase with temperature");
1231 }
1232
1233 #[test]
1234 fn test_superconductor_gl_parameter_type_ii() {
1235 let s = Superconductor::ybco();
1236 let kappa = s.gl_parameter(77.0);
1237 assert!(
1238 kappa > std::f64::consts::FRAC_1_SQRT_2,
1239 "YBCO should be type-II (κ={kappa})"
1240 );
1241 }
1242
1243 #[test]
1244 fn test_superconductor_hc1_positive() {
1245 let s = Superconductor::ybco();
1246 let hc1 = s.hc1_estimate(77.0);
1247 assert!(hc1 > 0.0);
1248 }
1249
1250 #[test]
1253 fn test_ferroelectric_remnant_polarisation_positive() {
1254 let fe = FerroelectricPE::batio3();
1255 assert!(fe.remnant_polarisation() >= 0.0);
1256 }
1257
1258 #[test]
1259 fn test_ferroelectric_saturation_at_large_field() {
1260 let fe = FerroelectricPE::batio3();
1261 let p_big = fe.polarisation_ascending(1.0e7);
1262 assert!(
1263 (p_big - fe.p_sat).abs() < 1e-4 * fe.p_sat,
1264 "should be near saturation at large E"
1265 );
1266 }
1267
1268 #[test]
1269 fn test_ferroelectric_hysteresis_energy_positive() {
1270 let fe = FerroelectricPE::batio3();
1271 let e_loss = fe.hysteresis_energy_loss(5.0e5, 100);
1272 assert!(e_loss >= 0.0);
1273 }
1274
1275 #[test]
1276 fn test_ferroelectric_ascending_descending_differ() {
1277 let fe = FerroelectricPE::batio3();
1278 let pa = fe.polarisation_ascending(0.0);
1279 let pd = fe.polarisation_descending(0.0);
1280 assert!(
1281 (pa - pd).abs() > 1e-10,
1282 "ascending and descending should differ at E=0"
1283 );
1284 }
1285
1286 #[test]
1289 fn test_ja_langevin_small_x() {
1290 let ja = JilesAthertonParams::soft_iron();
1291 let man = ja.anhysteretic(0.0, 0.0);
1293 assert!(man.abs() < 1e-10, "M_an(0) should be ~0, got {man}");
1294 }
1295
1296 #[test]
1297 fn test_ja_compute_bh_loop_length() {
1298 let ja = JilesAthertonParams::soft_iron();
1299 let (h, m) = ja.compute_bh_loop(2000.0, 50);
1300 assert_eq!(h.len(), 100);
1301 assert_eq!(m.len(), 100);
1302 }
1303
1304 #[test]
1305 fn test_ja_magnetisation_bounded() {
1306 let ja = JilesAthertonParams::soft_iron();
1307 let (_h, m) = ja.compute_bh_loop(5000.0, 100);
1308 for mi in &m {
1309 assert!(mi.abs() <= ja.m_sat * 1.01, "M exceeds M_sat: {mi}");
1310 }
1311 }
1312
1313 #[test]
1314 fn test_ja_coercive_field_positive() {
1315 let ja = JilesAthertonParams::soft_iron();
1316 let hc = ja.coercive_field(2000.0, 100);
1317 assert!(hc >= 0.0);
1318 }
1319
1320 #[test]
1321 fn test_b_from_ja_positive_at_positive_h() {
1322 let ja = JilesAthertonParams::soft_iron();
1323 let b = b_from_jiles_atherton(&ja, 1000.0, 0.5e6);
1324 assert!(b > 0.0);
1325 }
1326
1327 #[test]
1330 fn test_shielding_skin_depth_copper() {
1331 let s = ShieldingEffectiveness::copper(1e-3);
1332 let d = s.skin_depth(1.0e6); assert!(d > 0.0 && d < 1e-3, "copper skin depth at 1 MHz: {d}");
1334 }
1335
1336 #[test]
1337 fn test_shielding_absorption_loss_increases_with_freq() {
1338 let s = ShieldingEffectiveness::copper(1e-3);
1339 let a1 = s.absorption_loss_db(1.0e6);
1340 let a2 = s.absorption_loss_db(1.0e9);
1341 assert!(a2 > a1, "absorption loss should increase with frequency");
1342 }
1343
1344 #[test]
1345 fn test_shielding_total_se_positive() {
1346 let s = ShieldingEffectiveness::mild_steel(2e-3);
1347 let se = s.total_se_db(1.0e6);
1348 assert!(se > 0.0, "SE should be positive: {se}");
1349 }
1350
1351 #[test]
1352 fn test_shielding_reflection_loss_copper_1mhz() {
1353 let s = ShieldingEffectiveness::copper(1e-3);
1354 let r = s.reflection_loss_db(1.0e6);
1355 assert!(r > 100.0, "R = {r} dB");
1357 }
1358
1359 #[test]
1360 fn test_fr4_eps_r() {
1361 let m = fr4();
1362 assert!((m.eps_r_scalar() - 4.5).abs() < EPS);
1363 }
1364
1365 #[test]
1366 fn test_water_high_permittivity() {
1367 let m = water_2_45ghz();
1368 assert!(m.eps_r_scalar() > 70.0);
1369 }
1370}