1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
8
9fn finite(value: f64) -> Option<f64> {
10 value.is_finite().then_some(value)
11}
12
13fn all_finite(values: &[f64]) -> bool {
14 values.iter().all(|value| value.is_finite())
15}
16
17#[must_use]
32pub fn torque(force: f64, lever_arm: f64) -> Option<f64> {
33 if !force.is_finite() || !lever_arm.is_finite() {
34 return None;
35 }
36
37 finite(force * lever_arm)
38}
39
40#[must_use]
58pub fn torque_at_angle(force: f64, lever_arm: f64, angle_radians: f64) -> Option<f64> {
59 if !force.is_finite() || !lever_arm.is_finite() || !angle_radians.is_finite() {
60 return None;
61 }
62
63 finite(lever_arm * force * angle_radians.sin())
64}
65
66#[must_use]
70pub fn torque_at_angle_degrees(force: f64, lever_arm: f64, angle_degrees: f64) -> Option<f64> {
71 if !angle_degrees.is_finite() {
72 return None;
73 }
74
75 torque_at_angle(force, lever_arm, angle_degrees.to_radians())
76}
77
78#[must_use]
93pub fn force_from_torque(applied_torque: f64, lever_arm: f64) -> Option<f64> {
94 if !applied_torque.is_finite() || !lever_arm.is_finite() || lever_arm == 0.0 {
95 return None;
96 }
97
98 finite(applied_torque / lever_arm)
99}
100
101#[must_use]
108pub fn lever_arm_from_torque(applied_torque: f64, force: f64) -> Option<f64> {
109 if !applied_torque.is_finite() || !force.is_finite() || force == 0.0 {
110 return None;
111 }
112
113 finite(applied_torque / force)
114}
115
116#[must_use]
123pub fn perpendicular_force_component(force: f64, angle_radians: f64) -> Option<f64> {
124 if !force.is_finite() || !angle_radians.is_finite() {
125 return None;
126 }
127
128 finite(force * angle_radians.sin())
129}
130
131#[must_use]
136pub fn perpendicular_force_component_degrees(force: f64, angle_degrees: f64) -> Option<f64> {
137 if !angle_degrees.is_finite() {
138 return None;
139 }
140
141 perpendicular_force_component(force, angle_degrees.to_radians())
142}
143
144#[must_use]
151pub fn moment_arm(lever_arm: f64, angle_radians: f64) -> Option<f64> {
152 if !lever_arm.is_finite() || !angle_radians.is_finite() {
153 return None;
154 }
155
156 finite(lever_arm * angle_radians.sin())
157}
158
159#[must_use]
163pub fn moment_arm_degrees(lever_arm: f64, angle_degrees: f64) -> Option<f64> {
164 if !angle_degrees.is_finite() {
165 return None;
166 }
167
168 moment_arm(lever_arm, angle_degrees.to_radians())
169}
170
171#[must_use]
184pub fn net_torque(torques: &[f64]) -> Option<f64> {
185 if !all_finite(torques) {
186 return None;
187 }
188
189 finite(torques.iter().copied().sum())
190}
191
192#[must_use]
196pub fn torques_from_force_lever_pairs(pairs: &[(f64, f64)]) -> Option<Vec<f64>> {
197 pairs
198 .iter()
199 .map(|(force, lever_arm)| torque(*force, *lever_arm))
200 .collect()
201}
202
203#[must_use]
207pub fn net_torque_from_force_lever_pairs(pairs: &[(f64, f64)]) -> Option<f64> {
208 let torques = torques_from_force_lever_pairs(pairs)?;
209
210 net_torque(&torques)
211}
212
213#[must_use]
226pub fn is_rotational_equilibrium(torques: &[f64], tolerance: f64) -> Option<bool> {
227 if !tolerance.is_finite() || tolerance < 0.0 {
228 return None;
229 }
230
231 let total = net_torque(torques)?;
232
233 Some(total.abs() <= tolerance)
234}
235
236#[must_use]
243pub fn balancing_force(known_torque: f64, lever_arm: f64) -> Option<f64> {
244 if !known_torque.is_finite() || !lever_arm.is_finite() || lever_arm == 0.0 {
245 return None;
246 }
247
248 finite(-known_torque / lever_arm)
249}
250
251#[must_use]
258pub fn balancing_lever_arm(known_torque: f64, force: f64) -> Option<f64> {
259 if !known_torque.is_finite() || !force.is_finite() || force == 0.0 {
260 return None;
261 }
262
263 finite(-known_torque / force)
264}
265
266#[must_use]
282pub fn angular_acceleration_from_torque(
283 applied_torque: f64,
284 moment_of_inertia: f64,
285) -> Option<f64> {
286 if !applied_torque.is_finite() || !moment_of_inertia.is_finite() || moment_of_inertia <= 0.0 {
287 return None;
288 }
289
290 finite(applied_torque / moment_of_inertia)
291}
292
293#[must_use]
300pub fn point_mass_moment_of_inertia(mass: f64, radius: f64) -> Option<f64> {
301 if !mass.is_finite() || !radius.is_finite() || mass < 0.0 || radius < 0.0 {
302 return None;
303 }
304
305 finite(mass * radius.powi(2))
306}
307
308#[must_use]
315pub fn rod_moment_of_inertia_about_center(mass: f64, length: f64) -> Option<f64> {
316 if !mass.is_finite() || !length.is_finite() || mass < 0.0 || length < 0.0 {
317 return None;
318 }
319
320 finite((mass * length.powi(2)) / 12.0)
321}
322
323#[must_use]
330pub fn rod_moment_of_inertia_about_end(mass: f64, length: f64) -> Option<f64> {
331 if !mass.is_finite() || !length.is_finite() || mass < 0.0 || length < 0.0 {
332 return None;
333 }
334
335 finite((mass * length.powi(2)) / 3.0)
336}
337
338#[derive(Debug, Clone, Copy, PartialEq)]
340pub struct LeverForce {
341 pub force: f64,
343 pub lever_arm: f64,
345}
346
347impl LeverForce {
348 #[must_use]
352 pub const fn new(force: f64, lever_arm: f64) -> Option<Self> {
353 if !force.is_finite() || !lever_arm.is_finite() {
354 return None;
355 }
356
357 Some(Self { force, lever_arm })
358 }
359
360 #[must_use]
372 pub fn torque(&self) -> Option<f64> {
373 torque(self.force, self.lever_arm)
374 }
375
376 #[must_use]
378 pub fn torque_at_angle(&self, angle_radians: f64) -> Option<f64> {
379 torque_at_angle(self.force, self.lever_arm, angle_radians)
380 }
381}
382
383#[derive(Debug, Clone, PartialEq)]
385pub struct TorqueSystem {
386 pub torques: Vec<f64>,
388}
389
390impl TorqueSystem {
391 #[must_use]
395 pub fn new(torques: Vec<f64>) -> Option<Self> {
396 all_finite(&torques).then_some(Self { torques })
397 }
398
399 #[must_use]
411 pub fn net_torque(&self) -> Option<f64> {
412 net_torque(&self.torques)
413 }
414
415 #[must_use]
417 pub fn is_equilibrium(&self, tolerance: f64) -> Option<bool> {
418 is_rotational_equilibrium(&self.torques, tolerance)
419 }
420}
421
422#[cfg(test)]
423#[allow(clippy::float_cmp)]
424mod tests {
425 use core::f64;
426 use core::f64::consts::FRAC_PI_2;
427
428 use super::{
429 LeverForce, TorqueSystem, angular_acceleration_from_torque, balancing_force,
430 balancing_lever_arm, force_from_torque, is_rotational_equilibrium, lever_arm_from_torque,
431 moment_arm, moment_arm_degrees, net_torque, net_torque_from_force_lever_pairs,
432 perpendicular_force_component, perpendicular_force_component_degrees,
433 point_mass_moment_of_inertia, rod_moment_of_inertia_about_center,
434 rod_moment_of_inertia_about_end, torque, torque_at_angle, torque_at_angle_degrees,
435 torques_from_force_lever_pairs,
436 };
437
438 fn approx_eq(left: f64, right: f64, tolerance: f64) {
439 let delta = (left - right).abs();
440
441 assert!(
442 delta <= tolerance,
443 "left={left} right={right} delta={delta} tolerance={tolerance}"
444 );
445 }
446
447 #[test]
448 fn torque_helpers_handle_signed_inputs() {
449 assert_eq!(torque(10.0, 2.0), Some(20.0));
450 assert_eq!(torque(-10.0, 2.0), Some(-20.0));
451 assert_eq!(torque(10.0, -2.0), Some(-20.0));
452 }
453
454 #[test]
455 fn torque_angle_helpers_match_expected_values() {
456 approx_eq(
457 torque_at_angle(10.0, 2.0, FRAC_PI_2).unwrap(),
458 20.0,
459 1.0e-12,
460 );
461 approx_eq(torque_at_angle(10.0, 2.0, 0.0).unwrap(), 0.0, 1.0e-12);
462 approx_eq(
463 torque_at_angle_degrees(10.0, 2.0, 90.0).unwrap(),
464 20.0,
465 1.0e-12,
466 );
467 approx_eq(
468 torque_at_angle_degrees(10.0, 2.0, 30.0).unwrap(),
469 10.0,
470 1.0e-12,
471 );
472 }
473
474 #[test]
475 fn inverse_helpers_validate_denominators() {
476 assert_eq!(force_from_torque(20.0, 2.0), Some(10.0));
477 assert_eq!(force_from_torque(20.0, 0.0), None);
478 assert_eq!(lever_arm_from_torque(20.0, 10.0), Some(2.0));
479 assert_eq!(lever_arm_from_torque(20.0, 0.0), None);
480 }
481
482 #[test]
483 fn perpendicular_helpers_compute_expected_components() {
484 approx_eq(
485 perpendicular_force_component(10.0, FRAC_PI_2).unwrap(),
486 10.0,
487 1.0e-12,
488 );
489 approx_eq(
490 perpendicular_force_component_degrees(10.0, 30.0).unwrap(),
491 5.0,
492 1.0e-12,
493 );
494 approx_eq(moment_arm(2.0, FRAC_PI_2).unwrap(), 2.0, 1.0e-12);
495 approx_eq(moment_arm_degrees(2.0, 30.0).unwrap(), 1.0, 1.0e-12);
496 }
497
498 #[test]
499 fn net_torque_helpers_sum_systems_and_pairs() {
500 assert_eq!(net_torque(&[10.0, -4.0, 2.0]), Some(8.0));
501 assert_eq!(net_torque(&[]), Some(0.0));
502 assert_eq!(net_torque(&[10.0, f64::NAN]), None);
503 assert_eq!(
504 torques_from_force_lever_pairs(&[(10.0, 2.0), (-3.0, 4.0)]),
505 Some(vec![20.0, -12.0])
506 );
507 assert_eq!(
508 net_torque_from_force_lever_pairs(&[(10.0, 2.0), (-3.0, 4.0)]),
509 Some(8.0)
510 );
511 }
512
513 #[test]
514 fn rotational_equilibrium_helpers_balance_known_torques() {
515 assert_eq!(
516 is_rotational_equilibrium(&[10.0, -10.0], 0.000_001),
517 Some(true)
518 );
519 assert_eq!(
520 is_rotational_equilibrium(&[10.0, -9.0], 0.000_001),
521 Some(false)
522 );
523 assert_eq!(is_rotational_equilibrium(&[10.0, -10.0], -1.0), None);
524 assert_eq!(balancing_force(20.0, 2.0), Some(-10.0));
525 assert_eq!(balancing_force(20.0, 0.0), None);
526 assert_eq!(balancing_lever_arm(20.0, 10.0), Some(-2.0));
527 assert_eq!(balancing_lever_arm(20.0, 0.0), None);
528 }
529
530 #[test]
531 fn angular_acceleration_validates_positive_inertia() {
532 assert_eq!(angular_acceleration_from_torque(20.0, 4.0), Some(5.0));
533 assert_eq!(angular_acceleration_from_torque(20.0, 0.0), None);
534 }
535
536 #[test]
537 fn moment_of_inertia_helpers_cover_basic_shapes() {
538 assert_eq!(point_mass_moment_of_inertia(2.0, 3.0), Some(18.0));
539 assert_eq!(point_mass_moment_of_inertia(-2.0, 3.0), None);
540 assert_eq!(point_mass_moment_of_inertia(2.0, -3.0), None);
541 assert_eq!(rod_moment_of_inertia_about_center(12.0, 2.0), Some(4.0));
542 assert_eq!(rod_moment_of_inertia_about_end(3.0, 2.0), Some(4.0));
543 }
544
545 #[test]
546 fn lever_force_validates_and_delegates() {
547 let lever = LeverForce::new(10.0, 2.0).unwrap();
548
549 assert_eq!(lever.torque(), Some(20.0));
550 approx_eq(lever.torque_at_angle(FRAC_PI_2).unwrap(), 20.0, 1.0e-12);
551 assert_eq!(LeverForce::new(f64::NAN, 2.0), None);
552 }
553
554 #[test]
555 fn torque_system_validates_and_delegates() {
556 let system = TorqueSystem::new(vec![10.0, -4.0, 2.0]).unwrap();
557
558 assert_eq!(system.net_torque(), Some(8.0));
559 assert_eq!(TorqueSystem::new(vec![10.0, f64::NAN]), None);
560 assert_eq!(
561 TorqueSystem::new(vec![10.0, -10.0])
562 .unwrap()
563 .is_equilibrium(0.000_001),
564 Some(true)
565 );
566 }
567}