1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
5
6pub mod prelude;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct RotatingBody {
11 pub moment_of_inertia: f64,
12 pub angular_velocity: f64,
13}
14
15impl RotatingBody {
16 #[must_use]
18 pub const fn new(moment_of_inertia: f64, angular_velocity: f64) -> Option<Self> {
19 if !moment_of_inertia.is_finite()
20 || moment_of_inertia < 0.0
21 || !angular_velocity.is_finite()
22 {
23 return None;
24 }
25
26 Some(Self {
27 moment_of_inertia,
28 angular_velocity,
29 })
30 }
31
32 #[must_use]
34 pub fn angular_momentum(&self) -> Option<f64> {
35 angular_momentum(self.moment_of_inertia, self.angular_velocity)
36 }
37
38 #[must_use]
50 pub fn rotational_kinetic_energy(&self) -> Option<f64> {
51 rotational_kinetic_energy(self.moment_of_inertia, self.angular_velocity)
52 }
53
54 #[must_use]
56 pub fn angular_acceleration_from_torque(&self, torque: f64) -> Option<f64> {
57 angular_acceleration_from_torque(torque, self.moment_of_inertia)
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct AngularState {
64 pub angular_position: f64,
65 pub angular_velocity: f64,
66}
67
68impl AngularState {
69 #[must_use]
71 pub const fn new(angular_position: f64, angular_velocity: f64) -> Option<Self> {
72 if !angular_position.is_finite() || !angular_velocity.is_finite() {
73 return None;
74 }
75
76 Some(Self {
77 angular_position,
78 angular_velocity,
79 })
80 }
81
82 #[must_use]
103 pub fn advanced_by_constant_acceleration(
104 &self,
105 angular_acceleration: f64,
106 time: f64,
107 ) -> Option<Self> {
108 let displacement = angular_displacement(self.angular_velocity, angular_acceleration, time)?;
109 let angular_velocity =
110 final_angular_velocity(self.angular_velocity, angular_acceleration, time)?;
111 let angular_position = finite_result(self.angular_position + displacement)?;
112
113 Some(Self {
114 angular_position,
115 angular_velocity,
116 })
117 }
118}
119
120#[must_use]
124pub fn radians_from_degrees(degrees: f64) -> Option<f64> {
125 if !degrees.is_finite() {
126 return None;
127 }
128
129 finite_result(degrees.to_radians())
130}
131
132#[must_use]
136pub fn degrees_from_radians(radians: f64) -> Option<f64> {
137 if !radians.is_finite() {
138 return None;
139 }
140
141 finite_result(radians.to_degrees())
142}
143
144#[must_use]
148pub fn revolutions_from_radians(radians: f64) -> Option<f64> {
149 if !radians.is_finite() {
150 return None;
151 }
152
153 finite_result(radians / TAU)
154}
155
156#[must_use]
160pub fn radians_from_revolutions(revolutions: f64) -> Option<f64> {
161 if !revolutions.is_finite() {
162 return None;
163 }
164
165 finite_result(revolutions * TAU)
166}
167
168#[must_use]
181pub fn angular_velocity(angular_displacement: f64, time: f64) -> Option<f64> {
182 if !angular_displacement.is_finite() || !is_positive_finite(time) {
183 return None;
184 }
185
186 finite_result(angular_displacement / time)
187}
188
189#[must_use]
202pub fn angular_acceleration(
203 initial_angular_velocity: f64,
204 final_angular_velocity: f64,
205 time: f64,
206) -> Option<f64> {
207 if !initial_angular_velocity.is_finite()
208 || !final_angular_velocity.is_finite()
209 || !is_positive_finite(time)
210 {
211 return None;
212 }
213
214 finite_result((final_angular_velocity - initial_angular_velocity) / time)
215}
216
217#[must_use]
222pub fn final_angular_velocity(
223 initial_angular_velocity: f64,
224 angular_acceleration: f64,
225 time: f64,
226) -> Option<f64> {
227 if !initial_angular_velocity.is_finite()
228 || !angular_acceleration.is_finite()
229 || !is_nonnegative_finite(time)
230 {
231 return None;
232 }
233
234 finite_result(angular_acceleration.mul_add(time, initial_angular_velocity))
235}
236
237#[must_use]
242pub fn angular_displacement(
243 initial_angular_velocity: f64,
244 angular_acceleration: f64,
245 time: f64,
246) -> Option<f64> {
247 if !initial_angular_velocity.is_finite()
248 || !angular_acceleration.is_finite()
249 || !is_nonnegative_finite(time)
250 {
251 return None;
252 }
253
254 let acceleration_term = 0.5 * angular_acceleration * time * time;
255
256 finite_result(initial_angular_velocity.mul_add(time, acceleration_term))
257}
258
259#[must_use]
264pub fn final_angular_velocity_squared(
265 initial_angular_velocity: f64,
266 angular_acceleration: f64,
267 angular_displacement: f64,
268) -> Option<f64> {
269 if !initial_angular_velocity.is_finite()
270 || !angular_acceleration.is_finite()
271 || !angular_displacement.is_finite()
272 {
273 return None;
274 }
275
276 let squared = initial_angular_velocity.mul_add(
277 initial_angular_velocity,
278 2.0 * angular_acceleration * angular_displacement,
279 );
280
281 if !squared.is_finite() || squared < 0.0 {
282 return None;
283 }
284
285 Some(squared)
286}
287
288#[must_use]
293pub fn final_angular_velocity_from_displacement(
294 initial_angular_velocity: f64,
295 angular_acceleration: f64,
296 angular_displacement: f64,
297) -> Option<f64> {
298 let squared = final_angular_velocity_squared(
299 initial_angular_velocity,
300 angular_acceleration,
301 angular_displacement,
302 )?;
303
304 finite_result(squared.sqrt())
305}
306
307#[must_use]
320pub fn tangential_speed(angular_velocity: f64, radius: f64) -> Option<f64> {
321 if !angular_velocity.is_finite() || !is_nonnegative_finite(radius) {
322 return None;
323 }
324
325 finite_result(angular_velocity * radius)
326}
327
328#[must_use]
333pub fn angular_velocity_from_tangential_speed(tangential_speed: f64, radius: f64) -> Option<f64> {
334 if !tangential_speed.is_finite() || !is_positive_finite(radius) {
335 return None;
336 }
337
338 finite_result(tangential_speed / radius)
339}
340
341#[must_use]
346pub fn tangential_acceleration(angular_acceleration: f64, radius: f64) -> Option<f64> {
347 if !angular_acceleration.is_finite() || !is_nonnegative_finite(radius) {
348 return None;
349 }
350
351 finite_result(angular_acceleration * radius)
352}
353
354#[must_use]
367pub fn centripetal_acceleration_from_angular_velocity(
368 angular_velocity: f64,
369 radius: f64,
370) -> Option<f64> {
371 if !angular_velocity.is_finite() || !is_nonnegative_finite(radius) {
372 return None;
373 }
374
375 let acceleration = angular_velocity * angular_velocity * radius;
376 if acceleration < 0.0 {
377 return None;
378 }
379
380 finite_result(acceleration)
381}
382
383#[must_use]
388pub fn centripetal_acceleration_from_tangential_speed(
389 tangential_speed: f64,
390 radius: f64,
391) -> Option<f64> {
392 if !tangential_speed.is_finite() || !is_positive_finite(radius) {
393 return None;
394 }
395
396 let acceleration = tangential_speed * tangential_speed / radius;
397 if acceleration < 0.0 {
398 return None;
399 }
400
401 finite_result(acceleration)
402}
403
404#[must_use]
409pub fn point_mass_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
410 scaled_square_measure(mass, radius, 1.0)
411}
412
413#[must_use]
426pub fn solid_disk_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
427 scaled_square_measure(mass, radius, 0.5)
428}
429
430#[must_use]
435pub fn thin_ring_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
436 point_mass_moment_of_inertia(mass, radius)
437}
438
439#[must_use]
444pub fn solid_sphere_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
445 scaled_square_measure(mass, radius, 2.0 / 5.0)
446}
447
448#[must_use]
453pub fn hollow_sphere_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
454 scaled_square_measure(mass, radius, 2.0 / 3.0)
455}
456
457#[must_use]
462pub fn rod_moment_of_inertia_about_center(mass: f64, length: f64) -> Option<f64> {
463 scaled_square_measure(mass, length, 1.0 / 12.0)
464}
465
466#[must_use]
471pub fn rod_moment_of_inertia_about_end(mass: f64, length: f64) -> Option<f64> {
472 scaled_square_measure(mass, length, 1.0 / 3.0)
473}
474
475#[must_use]
488pub fn angular_momentum(moment_of_inertia: f64, angular_velocity: f64) -> Option<f64> {
489 if !is_nonnegative_finite(moment_of_inertia) || !angular_velocity.is_finite() {
490 return None;
491 }
492
493 finite_result(moment_of_inertia * angular_velocity)
494}
495
496#[must_use]
501pub fn angular_velocity_from_angular_momentum(
502 angular_momentum: f64,
503 moment_of_inertia: f64,
504) -> Option<f64> {
505 if !angular_momentum.is_finite() || !is_positive_finite(moment_of_inertia) {
506 return None;
507 }
508
509 finite_result(angular_momentum / moment_of_inertia)
510}
511
512#[must_use]
525pub fn rotational_kinetic_energy(moment_of_inertia: f64, angular_velocity: f64) -> Option<f64> {
526 if !is_nonnegative_finite(moment_of_inertia) || !angular_velocity.is_finite() {
527 return None;
528 }
529
530 let energy = 0.5 * moment_of_inertia * angular_velocity * angular_velocity;
531 if energy < 0.0 {
532 return None;
533 }
534
535 finite_result(energy)
536}
537
538#[must_use]
544pub fn angular_velocity_from_rotational_kinetic_energy(
545 rotational_kinetic_energy: f64,
546 moment_of_inertia: f64,
547) -> Option<f64> {
548 if !is_nonnegative_finite(rotational_kinetic_energy) || !is_positive_finite(moment_of_inertia) {
549 return None;
550 }
551
552 let squared = 2.0 * rotational_kinetic_energy / moment_of_inertia;
553 if !squared.is_finite() || squared < 0.0 {
554 return None;
555 }
556
557 finite_result(squared.sqrt())
558}
559
560#[must_use]
568pub fn angular_acceleration_from_torque(torque: f64, moment_of_inertia: f64) -> Option<f64> {
569 if !torque.is_finite() || !is_positive_finite(moment_of_inertia) {
570 return None;
571 }
572
573 finite_result(torque / moment_of_inertia)
574}
575
576fn scaled_square_measure(primary: f64, measure: f64, factor: f64) -> Option<f64> {
577 if !is_nonnegative_finite(primary) || !is_nonnegative_finite(measure) {
578 return None;
579 }
580
581 finite_result(factor * primary * measure * measure)
582}
583
584fn is_nonnegative_finite(value: f64) -> bool {
585 value.is_finite() && value >= 0.0
586}
587
588fn is_positive_finite(value: f64) -> bool {
589 value.is_finite() && value > 0.0
590}
591
592fn finite_result(value: f64) -> Option<f64> {
593 value.is_finite().then_some(value)
594}
595
596#[cfg(test)]
597#[allow(clippy::float_cmp)]
598mod tests {
599 use super::{
600 AngularState, RotatingBody, angular_acceleration, angular_acceleration_from_torque,
601 angular_displacement, angular_momentum, angular_velocity,
602 angular_velocity_from_angular_momentum, angular_velocity_from_rotational_kinetic_energy,
603 angular_velocity_from_tangential_speed, centripetal_acceleration_from_angular_velocity,
604 centripetal_acceleration_from_tangential_speed, degrees_from_radians,
605 final_angular_velocity, final_angular_velocity_from_displacement,
606 final_angular_velocity_squared, hollow_sphere_moment_of_inertia,
607 point_mass_moment_of_inertia, radians_from_degrees, radians_from_revolutions,
608 revolutions_from_radians, rod_moment_of_inertia_about_center,
609 rod_moment_of_inertia_about_end, rotational_kinetic_energy, solid_disk_moment_of_inertia,
610 solid_sphere_moment_of_inertia, tangential_acceleration, tangential_speed,
611 thin_ring_moment_of_inertia,
612 };
613 use core::f64::consts::{PI, TAU};
614
615 const EPSILON: f64 = 1.0e-12;
616
617 fn assert_approx_eq(left: f64, right: f64) {
618 assert!(
619 (left - right).abs() <= EPSILON,
620 "left={left}, right={right}"
621 );
622 }
623
624 fn assert_some_approx_eq(value: Option<f64>, expected: f64) {
625 assert_approx_eq(value.expect("expected Some value"), expected);
626 }
627
628 #[test]
629 fn angular_conversions_cover_common_values() {
630 assert_some_approx_eq(radians_from_degrees(180.0), PI);
631 assert_some_approx_eq(degrees_from_radians(PI), 180.0);
632 assert_some_approx_eq(revolutions_from_radians(2.0 * PI), 1.0);
633 assert_some_approx_eq(radians_from_revolutions(1.0), TAU);
634 }
635
636 #[test]
637 fn angular_velocity_requires_positive_time() {
638 assert_eq!(angular_velocity(10.0, 2.0), Some(5.0));
639 assert_eq!(angular_velocity(10.0, 0.0), None);
640 }
641
642 #[test]
643 fn angular_acceleration_requires_positive_time() {
644 assert_eq!(angular_acceleration(2.0, 10.0, 4.0), Some(2.0));
645 assert_eq!(angular_acceleration(2.0, 10.0, 0.0), None);
646 }
647
648 #[test]
649 fn final_angular_velocity_requires_nonnegative_time() {
650 assert_eq!(final_angular_velocity(2.0, 3.0, 4.0), Some(14.0));
651 assert_eq!(final_angular_velocity(2.0, 3.0, -1.0), None);
652 }
653
654 #[test]
655 fn angular_displacement_requires_nonnegative_time() {
656 assert_eq!(angular_displacement(2.0, 3.0, 4.0), Some(32.0));
657 assert_eq!(angular_displacement(2.0, 3.0, -1.0), None);
658 }
659
660 #[test]
661 fn displacement_based_kinematics_cover_common_values() {
662 assert_eq!(final_angular_velocity_squared(2.0, 3.0, 4.0), Some(28.0));
663 assert_some_approx_eq(
664 final_angular_velocity_from_displacement(2.0, 3.0, 4.0),
665 28.0_f64.sqrt(),
666 );
667 }
668
669 #[test]
670 fn tangential_and_centripetal_relations_cover_common_values() {
671 assert_eq!(tangential_speed(3.0, 2.0), Some(6.0));
672 assert_eq!(tangential_speed(3.0, -2.0), None);
673 assert_eq!(angular_velocity_from_tangential_speed(6.0, 2.0), Some(3.0));
674 assert_eq!(angular_velocity_from_tangential_speed(6.0, 0.0), None);
675 assert_eq!(tangential_acceleration(3.0, 2.0), Some(6.0));
676 assert_eq!(
677 centripetal_acceleration_from_angular_velocity(3.0, 2.0),
678 Some(18.0)
679 );
680 assert_eq!(
681 centripetal_acceleration_from_tangential_speed(6.0, 2.0),
682 Some(18.0)
683 );
684 }
685
686 #[test]
687 fn moment_of_inertia_helpers_cover_common_shapes() {
688 assert_eq!(point_mass_moment_of_inertia(2.0, 3.0), Some(18.0));
689 assert_eq!(solid_disk_moment_of_inertia(2.0, 3.0), Some(9.0));
690 assert_eq!(thin_ring_moment_of_inertia(2.0, 3.0), Some(18.0));
691 assert_eq!(solid_sphere_moment_of_inertia(5.0, 2.0), Some(8.0));
692 assert_eq!(hollow_sphere_moment_of_inertia(3.0, 2.0), Some(8.0));
693 assert_eq!(rod_moment_of_inertia_about_center(12.0, 2.0), Some(4.0));
694 assert_eq!(rod_moment_of_inertia_about_end(3.0, 2.0), Some(4.0));
695 }
696
697 #[test]
698 fn angular_momentum_helpers_cover_common_values() {
699 assert_eq!(angular_momentum(4.0, 5.0), Some(20.0));
700 assert_eq!(angular_momentum(-4.0, 5.0), None);
701 assert_eq!(angular_velocity_from_angular_momentum(20.0, 4.0), Some(5.0));
702 assert_eq!(angular_velocity_from_angular_momentum(20.0, 0.0), None);
703 }
704
705 #[test]
706 fn rotational_energy_helpers_cover_common_values() {
707 assert_eq!(rotational_kinetic_energy(4.0, 5.0), Some(50.0));
708 assert_eq!(rotational_kinetic_energy(-4.0, 5.0), None);
709 assert_eq!(
710 angular_velocity_from_rotational_kinetic_energy(50.0, 4.0),
711 Some(5.0)
712 );
713 assert_eq!(
714 angular_velocity_from_rotational_kinetic_energy(-50.0, 4.0),
715 None
716 );
717 }
718
719 #[test]
720 fn angular_acceleration_from_torque_requires_positive_inertia() {
721 assert_eq!(angular_acceleration_from_torque(20.0, 4.0), Some(5.0));
722 assert_eq!(angular_acceleration_from_torque(20.0, 0.0), None);
723 }
724
725 #[test]
726 fn rotating_body_delegates_to_public_functions() {
727 let body = RotatingBody::new(4.0, 5.0).expect("expected valid rotating body");
728
729 assert_eq!(body.angular_momentum(), Some(20.0));
730 assert_eq!(body.rotational_kinetic_energy(), Some(50.0));
731 assert_eq!(RotatingBody::new(-4.0, 5.0), None);
732 }
733
734 #[test]
735 fn angular_state_advances_with_constant_acceleration() {
736 let next = AngularState::new(1.0, 2.0)
737 .expect("expected valid angular state")
738 .advanced_by_constant_acceleration(3.0, 4.0)
739 .expect("expected advanced state");
740
741 assert_eq!(
742 next,
743 AngularState {
744 angular_position: 33.0,
745 angular_velocity: 14.0,
746 }
747 );
748 assert_eq!(AngularState::new(f64::NAN, 2.0), None);
749 }
750}