1use std::f64::consts::PI;
11
12use super::error::{PhysicsError, PhysicsResult};
13
14#[derive(Debug, Clone, PartialEq)]
18pub struct ProjectileResult {
19 pub max_height: f64,
21 pub range: f64,
23 pub time_of_flight: f64,
25 pub vx: f64,
27 pub vy0: f64,
29}
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct OscillatorResult {
34 pub angular_frequency: f64,
36 pub period: f64,
38 pub frequency: f64,
40}
41
42pub fn kinetic_energy(mass: f64, velocity: f64) -> PhysicsResult<f64> {
57 if mass <= 0.0 {
58 return Err(PhysicsError::InvalidParameter {
59 param: "mass",
60 reason: format!("mass must be positive, got {mass}"),
61 });
62 }
63 Ok(0.5 * mass * velocity * velocity)
64}
65
66pub fn potential_energy_gravity(mass: f64, height: f64, g: f64) -> PhysicsResult<f64> {
80 if mass <= 0.0 {
81 return Err(PhysicsError::InvalidParameter {
82 param: "mass",
83 reason: format!("mass must be positive, got {mass}"),
84 });
85 }
86 if g <= 0.0 {
87 return Err(PhysicsError::InvalidParameter {
88 param: "g",
89 reason: format!("gravitational acceleration must be positive, got {g}"),
90 });
91 }
92 Ok(mass * g * height)
93}
94
95pub fn momentum(mass: f64, velocity: f64) -> PhysicsResult<f64> {
106 if mass <= 0.0 {
107 return Err(PhysicsError::InvalidParameter {
108 param: "mass",
109 reason: format!("mass must be positive, got {mass}"),
110 });
111 }
112 Ok(mass * velocity)
113}
114
115pub fn angular_momentum(mass: f64, velocity: f64, radius: f64) -> PhysicsResult<f64> {
127 if mass <= 0.0 {
128 return Err(PhysicsError::InvalidParameter {
129 param: "mass",
130 reason: format!("mass must be positive, got {mass}"),
131 });
132 }
133 if radius <= 0.0 {
134 return Err(PhysicsError::InvalidParameter {
135 param: "radius",
136 reason: format!("radius must be positive, got {radius}"),
137 });
138 }
139 Ok(mass * velocity * radius)
140}
141
142pub fn centripetal_acceleration(velocity: f64, radius: f64) -> PhysicsResult<f64> {
153 if radius <= 0.0 {
154 return Err(PhysicsError::InvalidParameter {
155 param: "radius",
156 reason: format!("radius must be positive, got {radius}"),
157 });
158 }
159 Ok(velocity * velocity / radius)
160}
161
162pub fn projectile_motion(v0: f64, angle_deg: f64, g: f64) -> PhysicsResult<ProjectileResult> {
181 if v0 < 0.0 {
182 return Err(PhysicsError::InvalidParameter {
183 param: "v0",
184 reason: format!("initial speed must be non-negative, got {v0}"),
185 });
186 }
187 if g <= 0.0 {
188 return Err(PhysicsError::InvalidParameter {
189 param: "g",
190 reason: format!("gravitational acceleration must be positive, got {g}"),
191 });
192 }
193 if !(0.0..=90.0).contains(&angle_deg) {
194 return Err(PhysicsError::DomainError(format!(
195 "launch angle must be in [0, 90] degrees, got {angle_deg}"
196 )));
197 }
198
199 let angle_rad = angle_deg * PI / 180.0;
200 let vx = v0 * angle_rad.cos();
201 let vy0 = v0 * angle_rad.sin();
202
203 let time_of_flight = 2.0 * vy0 / g;
205 let max_height = vy0 * vy0 / (2.0 * g);
207 let range = vx * time_of_flight;
209
210 Ok(ProjectileResult {
211 max_height,
212 range,
213 time_of_flight,
214 vx,
215 vy0,
216 })
217}
218
219pub fn projectile_position(v0: f64, angle_deg: f64, g: f64, t: f64) -> PhysicsResult<(f64, f64)> {
235 if t < 0.0 {
236 return Err(PhysicsError::InvalidParameter {
237 param: "t",
238 reason: format!("time must be non-negative, got {t}"),
239 });
240 }
241 let proj = projectile_motion(v0, angle_deg, g)?;
242 let x = proj.vx * t;
243 let y = proj.vy0 * t - 0.5 * g * t * t;
244 Ok((x, y))
245}
246
247pub fn simple_harmonic_oscillator(
262 mass: f64,
263 spring_constant: f64,
264) -> PhysicsResult<OscillatorResult> {
265 if mass <= 0.0 {
266 return Err(PhysicsError::InvalidParameter {
267 param: "mass",
268 reason: format!("mass must be positive, got {mass}"),
269 });
270 }
271 if spring_constant <= 0.0 {
272 return Err(PhysicsError::InvalidParameter {
273 param: "spring_constant",
274 reason: format!("spring constant must be positive, got {spring_constant}"),
275 });
276 }
277 let angular_frequency = (spring_constant / mass).sqrt();
278 let period = 2.0 * PI / angular_frequency;
279 let frequency = angular_frequency / (2.0 * PI);
280 Ok(OscillatorResult {
281 angular_frequency,
282 period,
283 frequency,
284 })
285}
286
287pub fn sho_displacement(
302 amplitude: f64,
303 angular_freq: f64,
304 time: f64,
305 phase: f64,
306) -> PhysicsResult<f64> {
307 if amplitude < 0.0 {
308 return Err(PhysicsError::InvalidParameter {
309 param: "amplitude",
310 reason: format!("amplitude must be non-negative, got {amplitude}"),
311 });
312 }
313 if angular_freq <= 0.0 {
314 return Err(PhysicsError::InvalidParameter {
315 param: "angular_freq",
316 reason: format!("angular frequency must be positive, got {angular_freq}"),
317 });
318 }
319 if time < 0.0 {
320 return Err(PhysicsError::InvalidParameter {
321 param: "time",
322 reason: format!("time must be non-negative, got {time}"),
323 });
324 }
325 Ok(amplitude * (angular_freq * time + phase).cos())
326}
327
328pub fn gravitational_force(m1: f64, m2: f64, distance: f64) -> PhysicsResult<f64> {
342 use crate::constants::physical::GRAVITATIONAL_CONSTANT;
343 if m1 <= 0.0 {
344 return Err(PhysicsError::InvalidParameter {
345 param: "m1",
346 reason: format!("mass m1 must be positive, got {m1}"),
347 });
348 }
349 if m2 <= 0.0 {
350 return Err(PhysicsError::InvalidParameter {
351 param: "m2",
352 reason: format!("mass m2 must be positive, got {m2}"),
353 });
354 }
355 if distance <= 0.0 {
356 return Err(PhysicsError::InvalidParameter {
357 param: "distance",
358 reason: format!("distance must be positive, got {distance}"),
359 });
360 }
361 Ok(GRAVITATIONAL_CONSTANT * m1 * m2 / (distance * distance))
362}
363
364pub fn escape_velocity(mass: f64, radius: f64) -> PhysicsResult<f64> {
377 use crate::constants::physical::GRAVITATIONAL_CONSTANT;
378 if mass <= 0.0 {
379 return Err(PhysicsError::InvalidParameter {
380 param: "mass",
381 reason: format!("mass must be positive, got {mass}"),
382 });
383 }
384 if radius <= 0.0 {
385 return Err(PhysicsError::InvalidParameter {
386 param: "radius",
387 reason: format!("radius must be positive, got {radius}"),
388 });
389 }
390 Ok((2.0 * GRAVITATIONAL_CONSTANT * mass / radius).sqrt())
391}
392
393pub fn orbital_period(orbital_radius: f64, central_mass: f64) -> PhysicsResult<f64> {
406 use crate::constants::physical::GRAVITATIONAL_CONSTANT;
407 if orbital_radius <= 0.0 {
408 return Err(PhysicsError::InvalidParameter {
409 param: "orbital_radius",
410 reason: format!("orbital radius must be positive, got {orbital_radius}"),
411 });
412 }
413 if central_mass <= 0.0 {
414 return Err(PhysicsError::InvalidParameter {
415 param: "central_mass",
416 reason: format!("central mass must be positive, got {central_mass}"),
417 });
418 }
419 Ok(2.0 * PI * (orbital_radius.powi(3) / (GRAVITATIONAL_CONSTANT * central_mass)).sqrt())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 const TOL: f64 = 1e-9;
427
428 #[test]
429 fn test_kinetic_energy_basic() {
430 let ke = kinetic_energy(2.0, 3.0).expect("should succeed");
432 assert!((ke - 9.0).abs() < TOL, "KE = {ke}");
433 }
434
435 #[test]
436 fn test_kinetic_energy_zero_velocity() {
437 let ke = kinetic_energy(5.0, 0.0).expect("should succeed");
438 assert_eq!(ke, 0.0);
439 }
440
441 #[test]
442 fn test_kinetic_energy_invalid_mass() {
443 assert!(kinetic_energy(-1.0, 1.0).is_err());
444 assert!(kinetic_energy(0.0, 1.0).is_err());
445 }
446
447 #[test]
448 fn test_potential_energy_gravity() {
449 let pe = potential_energy_gravity(2.0, 10.0, 9.81).expect("should succeed");
451 assert!((pe - 196.2).abs() < 1e-10);
452 }
453
454 #[test]
455 fn test_potential_energy_negative_height() {
456 let pe = potential_energy_gravity(1.0, -5.0, 9.81).expect("should succeed");
458 assert!(pe < 0.0);
459 }
460
461 #[test]
462 fn test_momentum() {
463 let p = momentum(3.0, 4.0).expect("should succeed");
464 assert!((p - 12.0).abs() < TOL);
465 }
466
467 #[test]
468 fn test_angular_momentum() {
469 let l = angular_momentum(1.0, 2.0, 3.0).expect("should succeed");
471 assert!((l - 6.0).abs() < TOL);
472 }
473
474 #[test]
475 fn test_centripetal_acceleration() {
476 let a = centripetal_acceleration(2.0, 2.0).expect("should succeed");
478 assert!((a - 2.0).abs() < TOL);
479 }
480
481 #[test]
482 fn test_projectile_45_degrees() {
483 let v0 = 20.0;
485 let g = 9.81;
486 let result = projectile_motion(v0, 45.0, g).expect("should succeed");
487 let expected_range = v0 * v0 / g;
488 assert!(
489 (result.range - expected_range).abs() < 1e-10,
490 "range = {}, expected = {}",
491 result.range,
492 expected_range
493 );
494 }
495
496 #[test]
497 fn test_projectile_90_degrees_zero_range() {
498 let v0 = 10.0;
500 let g = 9.81;
501 let result = projectile_motion(v0, 90.0, g).expect("should succeed");
502 assert!(result.range.abs() < 1e-10, "range should be ~0");
503 let expected_height = v0 * v0 / (2.0 * g);
504 assert!((result.max_height - expected_height).abs() < 1e-10);
505 }
506
507 #[test]
508 fn test_projectile_invalid_angle() {
509 assert!(projectile_motion(10.0, 95.0, 9.81).is_err());
510 assert!(projectile_motion(10.0, -1.0, 9.81).is_err());
511 }
512
513 #[test]
514 fn test_simple_harmonic_oscillator() {
515 let result = simple_harmonic_oscillator(1.0, 4.0).expect("should succeed");
517 assert!((result.angular_frequency - 2.0).abs() < TOL);
518 assert!((result.period - PI).abs() < TOL);
519 assert!((result.frequency - 1.0 / PI).abs() < TOL);
520 }
521
522 #[test]
523 fn test_sho_displacement_at_zero() {
524 let d = sho_displacement(3.0, 2.0, 0.0, 0.0).expect("should succeed");
526 assert!((d - 3.0).abs() < TOL);
527 }
528
529 #[test]
530 fn test_escape_velocity_earth() {
531 use crate::constants::physical::{EARTH_MASS, EARTH_RADIUS};
532 let ve = escape_velocity(EARTH_MASS, EARTH_RADIUS).expect("should succeed");
533 assert!(
535 (ve - 11_186.0).abs() < 200.0,
536 "escape velocity = {ve:.1} m/s"
537 );
538 }
539
540 #[test]
541 fn test_gravitational_force_known() {
542 use crate::constants::physical::GRAVITATIONAL_CONSTANT;
543 let f = gravitational_force(1.0, 1.0, 1.0).expect("should succeed");
545 assert!((f - GRAVITATIONAL_CONSTANT).abs() < 1e-20);
546 }
547
548 #[test]
549 fn test_orbital_period_earth_approx() {
550 use crate::constants::physical::{EARTH_RADIUS, SOLAR_MASS};
551 use crate::constants::physical::ASTRONOMICAL_UNIT;
553 let period = orbital_period(ASTRONOMICAL_UNIT, SOLAR_MASS).expect("should succeed");
554 let one_year = 3.156e7_f64;
556 assert!(
557 (period - one_year).abs() / one_year < 0.01,
558 "orbital period = {period:.3e} s"
559 );
560 assert!(orbital_period(0.0, SOLAR_MASS).is_err());
562 assert!(orbital_period(EARTH_RADIUS, 0.0).is_err());
563 }
564}