Skip to main content

use_torque/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Torque-specific scalar helpers.
5
6/// Re-exports for ergonomic glob imports.
7pub 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/// Computes torque from a force and lever arm.
18///
19/// Formula: `τ = F * r`
20///
21/// Returns `None` when either input is not finite or when the computed result is not finite.
22/// Negative forces and lever arms are allowed.
23///
24/// # Examples
25///
26/// ```rust
27/// use use_torque::torque;
28///
29/// assert_eq!(torque(10.0, 2.0), Some(20.0));
30/// ```
31#[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/// Computes torque when the applied force meets the lever arm at an angle in radians.
41///
42/// Formula: `τ = r * F * sin(theta)`
43///
44/// Returns `None` when any input is not finite or when the computed result is not finite.
45/// Negative forces and lever arms are allowed.
46///
47/// # Examples
48///
49/// ```rust
50/// use core::f64::consts::FRAC_PI_2;
51/// use use_torque::torque_at_angle;
52///
53/// let applied = torque_at_angle(10.0, 2.0, FRAC_PI_2).unwrap();
54///
55/// assert!((applied - 20.0).abs() < 1.0e-12);
56/// ```
57#[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/// Computes torque when the applied force meets the lever arm at an angle in degrees.
67///
68/// This helper converts degrees to radians internally and delegates to [`torque_at_angle`].
69#[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/// Computes the force required to produce a known torque at a lever arm.
79///
80/// Formula: `F = τ / r`
81///
82/// Returns `None` when `lever_arm` is zero, when either input is not finite, or when the
83/// computed result is not finite.
84///
85/// # Examples
86///
87/// ```rust
88/// use use_torque::force_from_torque;
89///
90/// assert_eq!(force_from_torque(20.0, 2.0), Some(10.0));
91/// ```
92#[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/// Computes the lever arm required to produce a known torque from a force.
102///
103/// Formula: `r = τ / F`
104///
105/// Returns `None` when `force` is zero, when either input is not finite, or when the computed
106/// result is not finite.
107#[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/// Computes the component of a force that acts perpendicular to a lever arm.
117///
118/// Formula: `F_perp = F * sin(theta)`
119///
120/// Returns `None` when either input is not finite or when the computed result is not finite.
121/// Negative forces are allowed.
122#[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/// Computes the perpendicular force component from an angle given in degrees.
132///
133/// This helper converts degrees to radians internally and delegates to
134/// [`perpendicular_force_component`].
135#[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/// Computes the perpendicular moment arm from a lever arm and angle in radians.
145///
146/// Formula: `r_perp = r * sin(theta)`
147///
148/// Returns `None` when either input is not finite or when the computed result is not finite.
149/// Negative lever arms are allowed.
150#[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/// Computes the perpendicular moment arm from an angle given in degrees.
160///
161/// This helper converts degrees to radians internally and delegates to [`moment_arm`].
162#[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/// Computes the sum of a slice of torque values.
172///
173/// Returns `Some(0.0)` for an empty slice, `None` when any input is not finite, or `None` when
174/// the computed result is not finite.
175///
176/// # Examples
177///
178/// ```rust
179/// use use_torque::net_torque;
180///
181/// assert_eq!(net_torque(&[10.0, -4.0, 2.0]), Some(8.0));
182/// ```
183#[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/// Converts force and lever-arm pairs into torque values.
193///
194/// Returns `Some(vec![])` for an empty slice or `None` when any pair is invalid.
195#[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/// Computes the net torque for force and lever-arm pairs.
204///
205/// This helper delegates to [`torques_from_force_lever_pairs`] and then to [`net_torque`].
206#[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/// Checks whether a torque system is in rotational equilibrium.
214///
215/// Returns `None` when `tolerance` is negative or not finite, when any torque is not finite, or
216/// when the net torque cannot be computed as a finite value.
217///
218/// # Examples
219///
220/// ```rust
221/// use use_torque::is_rotational_equilibrium;
222///
223/// assert_eq!(is_rotational_equilibrium(&[10.0, -10.0], 1.0e-6), Some(true));
224/// ```
225#[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/// Computes the balancing force needed at a lever arm to cancel a known torque.
237///
238/// Formula: `F_balance = -τ_known / r`
239///
240/// Returns `None` when `lever_arm` is zero, when either input is not finite, or when the
241/// computed result is not finite.
242#[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/// Computes the balancing lever arm needed for a force to cancel a known torque.
252///
253/// Formula: `r_balance = -τ_known / F`
254///
255/// Returns `None` when `force` is zero, when either input is not finite, or when the computed
256/// result is not finite.
257#[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/// Computes angular acceleration from torque and moment of inertia.
267///
268/// Formula: `α = τ / I`
269///
270/// Returns `None` when `moment_of_inertia` is less than or equal to zero, when either input is
271/// not finite, or when the computed result is not finite. Broader angular-motion helpers belong
272/// in a future or separate `use-rotation` crate.
273///
274/// # Examples
275///
276/// ```rust
277/// use use_torque::angular_acceleration_from_torque;
278///
279/// assert_eq!(angular_acceleration_from_torque(20.0, 4.0), Some(5.0));
280/// ```
281#[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/// Computes the moment of inertia for a point mass.
294///
295/// Formula: `I = m * r²`
296///
297/// Returns `None` when `mass` or `radius` is negative, when either input is not finite, or when
298/// the computed result is not finite.
299#[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/// Computes the moment of inertia of a uniform rod about its center.
309///
310/// Formula: `I = (1 / 12) * m * L²`
311///
312/// Returns `None` when `mass` or `length` is negative, when either input is not finite, or when
313/// the computed result is not finite.
314#[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/// Computes the moment of inertia of a uniform rod about one end.
324///
325/// Formula: `I = (1 / 3) * m * L²`
326///
327/// Returns `None` when `mass` or `length` is negative, when either input is not finite, or when
328/// the computed result is not finite.
329#[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/// A force applied at a lever arm.
339#[derive(Debug, Clone, Copy, PartialEq)]
340pub struct LeverForce {
341    /// Applied force in newtons.
342    pub force: f64,
343    /// Lever arm in meters.
344    pub lever_arm: f64,
345}
346
347impl LeverForce {
348    /// Creates a lever-force pair from finite values.
349    ///
350    /// Returns `None` when either input is not finite.
351    #[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    /// Computes torque for this force and lever arm.
361    ///
362    /// # Examples
363    ///
364    /// ```rust
365    /// use use_torque::LeverForce;
366    ///
367    /// let lever = LeverForce::new(10.0, 2.0).unwrap();
368    ///
369    /// assert_eq!(lever.torque(), Some(20.0));
370    /// ```
371    #[must_use]
372    pub fn torque(&self) -> Option<f64> {
373        torque(self.force, self.lever_arm)
374    }
375
376    /// Computes torque for this force and lever arm at an angle in radians.
377    #[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/// A collection of torque values that can be analyzed as a system.
384#[derive(Debug, Clone, PartialEq)]
385pub struct TorqueSystem {
386    /// Torque values in newton-meters.
387    pub torques: Vec<f64>,
388}
389
390impl TorqueSystem {
391    /// Creates a torque system from finite torque values.
392    ///
393    /// Returns `None` when any torque is not finite.
394    #[must_use]
395    pub fn new(torques: Vec<f64>) -> Option<Self> {
396        all_finite(&torques).then_some(Self { torques })
397    }
398
399    /// Computes the system's net torque.
400    ///
401    /// # Examples
402    ///
403    /// ```rust
404    /// use use_torque::TorqueSystem;
405    ///
406    /// let system = TorqueSystem::new(vec![10.0, -4.0, 2.0]).unwrap();
407    ///
408    /// assert_eq!(system.net_torque(), Some(8.0));
409    /// ```
410    #[must_use]
411    pub fn net_torque(&self) -> Option<f64> {
412        net_torque(&self.torques)
413    }
414
415    /// Checks whether the system is in rotational equilibrium for the provided tolerance.
416    #[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}