1use crate::constants::physical::{
13 ELECTRIC_CONSTANT, ELEMENTARY_CHARGE, MAGNETIC_CONSTANT, SPEED_OF_LIGHT,
14};
15
16use super::error::{PhysicsError, PhysicsResult};
17
18#[inline(always)]
20fn coulomb_constant() -> f64 {
21 1.0 / (4.0 * std::f64::consts::PI * ELECTRIC_CONSTANT)
22}
23
24pub fn coulomb_force(q1: f64, q2: f64, distance: f64) -> PhysicsResult<f64> {
43 if distance <= 0.0 {
44 return Err(PhysicsError::InvalidParameter {
45 param: "distance",
46 reason: format!("distance must be positive, got {distance}"),
47 });
48 }
49 Ok(coulomb_constant() * q1 * q2 / (distance * distance))
50}
51
52pub fn electric_potential(charge: f64, distance: f64) -> PhysicsResult<f64> {
65 if distance <= 0.0 {
66 return Err(PhysicsError::InvalidParameter {
67 param: "distance",
68 reason: format!("distance must be positive, got {distance}"),
69 });
70 }
71 Ok(coulomb_constant() * charge / distance)
72}
73
74pub fn electric_field(charge: f64, distance: f64) -> PhysicsResult<f64> {
89 if distance <= 0.0 {
90 return Err(PhysicsError::InvalidParameter {
91 param: "distance",
92 reason: format!("distance must be positive, got {distance}"),
93 });
94 }
95 Ok(coulomb_constant() * charge.abs() / (distance * distance))
96}
97
98pub fn electrostatic_potential_energy(q1: f64, q2: f64, distance: f64) -> PhysicsResult<f64> {
112 if distance <= 0.0 {
113 return Err(PhysicsError::InvalidParameter {
114 param: "distance",
115 reason: format!("distance must be positive, got {distance}"),
116 });
117 }
118 Ok(coulomb_constant() * q1 * q2 / distance)
119}
120
121pub fn capacitor_energy(capacitance: f64, voltage: f64) -> PhysicsResult<f64> {
134 if capacitance <= 0.0 {
135 return Err(PhysicsError::InvalidParameter {
136 param: "capacitance",
137 reason: format!("capacitance must be positive, got {capacitance}"),
138 });
139 }
140 Ok(0.5 * capacitance * voltage * voltage)
141}
142
143pub fn magnetic_force(
162 charge: f64,
163 velocity: f64,
164 b_field: f64,
165 angle_rad: f64,
166) -> PhysicsResult<f64> {
167 if velocity < 0.0 {
168 return Err(PhysicsError::InvalidParameter {
169 param: "velocity",
170 reason: format!("velocity must be non-negative, got {velocity}"),
171 });
172 }
173 if b_field < 0.0 {
174 return Err(PhysicsError::InvalidParameter {
175 param: "b_field",
176 reason: format!("magnetic field must be non-negative, got {b_field}"),
177 });
178 }
179 Ok(charge.abs() * velocity * b_field * angle_rad.sin().abs())
180}
181
182pub fn biot_savart_wire(current: f64, distance: f64) -> PhysicsResult<f64> {
196 if current < 0.0 {
197 return Err(PhysicsError::InvalidParameter {
198 param: "current",
199 reason: format!("current must be non-negative, got {current}"),
200 });
201 }
202 if distance <= 0.0 {
203 return Err(PhysicsError::InvalidParameter {
204 param: "distance",
205 reason: format!("distance must be positive, got {distance}"),
206 });
207 }
208 Ok(MAGNETIC_CONSTANT * current / (2.0 * std::f64::consts::PI * distance))
209}
210
211pub fn cyclotron_radius(mass: f64, velocity: f64, charge: f64, b_field: f64) -> PhysicsResult<f64> {
227 if mass <= 0.0 {
228 return Err(PhysicsError::InvalidParameter {
229 param: "mass",
230 reason: format!("mass must be positive, got {mass}"),
231 });
232 }
233 if velocity < 0.0 {
234 return Err(PhysicsError::InvalidParameter {
235 param: "velocity",
236 reason: format!("velocity must be non-negative, got {velocity}"),
237 });
238 }
239 if charge <= 0.0 {
240 return Err(PhysicsError::InvalidParameter {
241 param: "charge",
242 reason: format!("charge magnitude must be positive, got {charge}"),
243 });
244 }
245 if b_field <= 0.0 {
246 return Err(PhysicsError::InvalidParameter {
247 param: "b_field",
248 reason: format!("magnetic field must be positive, got {b_field}"),
249 });
250 }
251 Ok(mass * velocity / (charge * b_field))
252}
253
254pub fn lorentz_factor(velocity: f64) -> PhysicsResult<f64> {
269 if velocity < 0.0 {
270 return Err(PhysicsError::InvalidParameter {
271 param: "velocity",
272 reason: format!("velocity must be non-negative, got {velocity}"),
273 });
274 }
275 if velocity >= SPEED_OF_LIGHT {
276 return Err(PhysicsError::SuperluminalVelocity {
277 velocity,
278 c: SPEED_OF_LIGHT,
279 });
280 }
281 let beta = velocity / SPEED_OF_LIGHT;
282 Ok(1.0 / (1.0 - beta * beta).sqrt())
283}
284
285pub fn relativistic_energy(mass: f64, velocity: f64) -> PhysicsResult<f64> {
299 if mass <= 0.0 {
300 return Err(PhysicsError::InvalidParameter {
301 param: "mass",
302 reason: format!("mass must be positive, got {mass}"),
303 });
304 }
305 let gamma = lorentz_factor(velocity)?;
306 Ok(gamma * mass * SPEED_OF_LIGHT * SPEED_OF_LIGHT)
307}
308
309pub fn rest_energy(mass: f64) -> PhysicsResult<f64> {
319 if mass <= 0.0 {
320 return Err(PhysicsError::InvalidParameter {
321 param: "mass",
322 reason: format!("mass must be positive, got {mass}"),
323 });
324 }
325 Ok(mass * SPEED_OF_LIGHT * SPEED_OF_LIGHT)
326}
327
328pub fn relativistic_kinetic_energy(mass: f64, velocity: f64) -> PhysicsResult<f64> {
341 if mass <= 0.0 {
342 return Err(PhysicsError::InvalidParameter {
343 param: "mass",
344 reason: format!("mass must be positive, got {mass}"),
345 });
346 }
347 let gamma = lorentz_factor(velocity)?;
348 Ok((gamma - 1.0) * mass * SPEED_OF_LIGHT * SPEED_OF_LIGHT)
349}
350
351pub fn relativistic_momentum(mass: f64, velocity: f64) -> PhysicsResult<f64> {
363 if mass <= 0.0 {
364 return Err(PhysicsError::InvalidParameter {
365 param: "mass",
366 reason: format!("mass must be positive, got {mass}"),
367 });
368 }
369 let gamma = lorentz_factor(velocity)?;
370 Ok(gamma * mass * velocity)
371}
372
373pub fn relativistic_velocity_addition(u: f64, v: f64) -> PhysicsResult<f64> {
390 if u < 0.0 {
391 return Err(PhysicsError::InvalidParameter {
392 param: "u",
393 reason: format!("speed u must be non-negative, got {u}"),
394 });
395 }
396 if v < 0.0 {
397 return Err(PhysicsError::InvalidParameter {
398 param: "v",
399 reason: format!("speed v must be non-negative, got {v}"),
400 });
401 }
402 if u >= SPEED_OF_LIGHT {
403 return Err(PhysicsError::SuperluminalVelocity {
404 velocity: u,
405 c: SPEED_OF_LIGHT,
406 });
407 }
408 if v >= SPEED_OF_LIGHT {
409 return Err(PhysicsError::SuperluminalVelocity {
410 velocity: v,
411 c: SPEED_OF_LIGHT,
412 });
413 }
414 let c2 = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
415 Ok((u + v) / (1.0 + u * v / c2))
416}
417
418#[must_use]
422pub fn impedance_of_free_space() -> f64 {
423 MAGNETIC_CONSTANT * SPEED_OF_LIGHT
424}
425
426pub fn ohm_resistance(voltage: f64, current: f64) -> PhysicsResult<f64> {
437 if current == 0.0 {
438 return Err(PhysicsError::InvalidParameter {
439 param: "current",
440 reason: "current must be non-zero to compute resistance".to_string(),
441 });
442 }
443 Ok(voltage / current)
444}
445
446pub fn resistor_power(current: f64, resistance: f64) -> PhysicsResult<f64> {
457 if resistance <= 0.0 {
458 return Err(PhysicsError::InvalidParameter {
459 param: "resistance",
460 reason: format!("resistance must be positive, got {resistance}"),
461 });
462 }
463 Ok(current * current * resistance)
464}
465
466pub fn compton_wavelength_shift(angle_rad: f64) -> f64 {
478 use crate::constants::physical::{COMPTON_WAVELENGTH, ELECTRON_MASS};
479 let _ = ELECTRON_MASS; COMPTON_WAVELENGTH * (1.0 - angle_rad.cos())
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use std::f64::consts::PI;
488
489 const TOL: f64 = 1e-9;
490
491 #[test]
494 fn test_coulomb_force_two_elementary_charges() {
495 let r = 1e-10_f64;
497 let f = coulomb_force(ELEMENTARY_CHARGE, ELEMENTARY_CHARGE, r).expect("should succeed");
498 assert!(f > 0.0, "Like charges repel");
500 assert!((f - 23.07e-9).abs() < 1e-10, "F = {f:.4e} N");
501 }
502
503 #[test]
504 fn test_coulomb_force_opposite_charges_attract() {
505 let f =
506 coulomb_force(ELEMENTARY_CHARGE, -ELEMENTARY_CHARGE, 1e-10).expect("should succeed");
507 assert!(f < 0.0, "Opposite charges attract");
508 }
509
510 #[test]
511 fn test_coulomb_force_invalid() {
512 assert!(coulomb_force(1.0, 1.0, 0.0).is_err());
513 assert!(coulomb_force(1.0, 1.0, -1.0).is_err());
514 }
515
516 #[test]
517 fn test_electric_potential_sign() {
518 let vp = electric_potential(ELEMENTARY_CHARGE, 1.0).expect("should succeed");
519 let vn = electric_potential(-ELEMENTARY_CHARGE, 1.0).expect("should succeed");
520 assert!(vp > 0.0);
521 assert!(vn < 0.0);
522 assert!((vp + vn).abs() < TOL);
523 }
524
525 #[test]
526 fn test_electric_field_positive() {
527 let e = electric_field(-ELEMENTARY_CHARGE, 1.0).expect("should succeed");
528 assert!(e > 0.0, "electric_field returns magnitude");
529 }
530
531 #[test]
532 fn test_electrostatic_potential_energy() {
533 use crate::constants::physical::BOHR_RADIUS;
535 let u = electrostatic_potential_energy(ELEMENTARY_CHARGE, -ELEMENTARY_CHARGE, BOHR_RADIUS)
536 .expect("should succeed");
537 assert!(u < 0.0, "Opposite charges: negative PE");
539 assert!((u + 4.36e-18).abs() < 1e-20, "U = {u:.4e} J");
540 }
541
542 #[test]
543 fn test_capacitor_energy() {
544 let u = capacitor_energy(1e-6, 10.0).expect("should succeed");
546 assert!((u - 50e-6).abs() < 1e-15);
547 }
548
549 #[test]
552 fn test_magnetic_force_perpendicular() {
553 let f = magnetic_force(ELEMENTARY_CHARGE, 1e6, 1.0, PI / 2.0).expect("should succeed");
555 let expected = ELEMENTARY_CHARGE * 1e6 * 1.0;
556 assert!((f - expected).abs() < 1e-30);
557 }
558
559 #[test]
560 fn test_magnetic_force_parallel_zero() {
561 let f = magnetic_force(ELEMENTARY_CHARGE, 1e6, 1.0, 0.0).expect("should succeed");
563 assert!(f.abs() < TOL);
564 }
565
566 #[test]
567 fn test_biot_savart_wire() {
568 let b = biot_savart_wire(1.0, 1.0).expect("should succeed");
570 let expected = MAGNETIC_CONSTANT / (2.0 * PI);
571 assert!((b - expected).abs() < 1e-15);
572 }
573
574 #[test]
575 fn test_cyclotron_radius_proton() {
576 use crate::constants::physical::PROTON_MASS;
577 let r = cyclotron_radius(PROTON_MASS, 1e6, ELEMENTARY_CHARGE, 1.0).expect("should succeed");
579 let expected = PROTON_MASS * 1e6 / ELEMENTARY_CHARGE;
580 assert!((r - expected).abs() < 1e-15);
581 }
582
583 #[test]
586 fn test_lorentz_factor_zero_velocity() {
587 let gamma = lorentz_factor(0.0).expect("should succeed");
588 assert!((gamma - 1.0).abs() < TOL);
589 }
590
591 #[test]
592 fn test_lorentz_factor_high_velocity() {
593 let v = 0.99 * SPEED_OF_LIGHT;
595 let gamma = lorentz_factor(v).expect("should succeed");
596 assert!((gamma - 7.089).abs() < 0.001, "γ = {gamma:.4}");
597 }
598
599 #[test]
600 fn test_lorentz_factor_superluminal_fails() {
601 assert!(lorentz_factor(SPEED_OF_LIGHT).is_err());
602 assert!(lorentz_factor(SPEED_OF_LIGHT + 1.0).is_err());
603 }
604
605 #[test]
606 fn test_relativistic_energy_at_rest() {
607 let m = crate::constants::physical::ELECTRON_MASS;
609 let e_rel = relativistic_energy(m, 0.0).expect("should succeed");
610 let e_rest = rest_energy(m).expect("should succeed");
611 assert!((e_rel - e_rest).abs() < 1e-40);
612 }
613
614 #[test]
615 fn test_relativistic_kinetic_energy_low_v_matches_classical() {
616 let m = 1.0_f64;
623 let v = 1e7_f64; let k_rel = relativistic_kinetic_energy(m, v).expect("should succeed");
625 let p_rel = relativistic_momentum(m, v).expect("should succeed");
626 let mc2 = rest_energy(m).expect("should succeed");
627 let e_total = k_rel + mc2;
629 let lhs = e_total * e_total;
630 let rhs = (p_rel * SPEED_OF_LIGHT).powi(2) + mc2 * mc2;
631 let rel_err = (lhs - rhs).abs() / rhs;
632 assert!(
633 rel_err < 1e-12,
634 "Energy-momentum invariant violated: LHS={lhs:.8e}, RHS={rhs:.8e}, rel err={rel_err:.2e}"
635 );
636 let k_classical = 0.5 * m * v * v;
638 assert!(
639 k_rel > k_classical,
640 "Relativistic KE must exceed classical KE"
641 );
642 }
643
644 #[test]
645 fn test_velocity_addition_stays_subluminal() {
646 let v = 0.9 * SPEED_OF_LIGHT;
648 let w = relativistic_velocity_addition(v, v).expect("should succeed");
649 assert!(w < SPEED_OF_LIGHT, "w = {w:.6e} m/s must be < c");
650 let expected = (v + v) / (1.0 + v * v / (SPEED_OF_LIGHT * SPEED_OF_LIGHT));
652 assert!((w - expected).abs() < 1.0);
653 }
654
655 #[test]
656 fn test_compton_shift_ninety_degrees() {
657 use crate::constants::physical::COMPTON_WAVELENGTH;
658 let shift = compton_wavelength_shift(PI / 2.0);
659 assert!((shift - COMPTON_WAVELENGTH).abs() < 1e-25);
660 }
661
662 #[test]
663 fn test_impedance_of_free_space() {
664 let z0 = impedance_of_free_space();
665 assert!((z0 - 376.73).abs() < 0.01, "Z₀ = {z0:.2} Ω");
667 }
668
669 #[test]
670 fn test_ohm_resistance() {
671 let r = ohm_resistance(12.0, 3.0).expect("should succeed");
672 assert!((r - 4.0).abs() < TOL);
673 assert!(ohm_resistance(12.0, 0.0).is_err());
674 }
675}