Skip to main content

use_statics/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small statics helpers.
5
6/// Re-exports for ergonomic glob imports.
7pub mod prelude;
8
9fn finite(value: f64) -> Option<f64> {
10    if value.is_finite() {
11        Some(if value == 0.0 { 0.0 } else { value })
12    } else {
13        None
14    }
15}
16
17fn finite_pair(x: f64, y: f64) -> Option<(f64, f64)> {
18    Some((finite(x)?, finite(y)?))
19}
20
21fn all_finite(values: &[f64]) -> bool {
22    values.iter().all(|value| value.is_finite())
23}
24
25fn nonnegative_finite(value: f64) -> bool {
26    value.is_finite() && value >= 0.0
27}
28
29fn positive_finite(value: f64) -> bool {
30    value.is_finite() && value > 0.0
31}
32
33/// Computes the net force from a list of scalar forces.
34///
35/// Returns `Some(0.0)` for an empty slice and `None` when any input or the computed result is not
36/// finite.
37///
38/// # Examples
39///
40/// ```rust
41/// use use_statics::net_force_1d;
42///
43/// assert_eq!(net_force_1d(&[10.0, -4.0, -6.0]), Some(0.0));
44/// ```
45#[must_use]
46pub fn net_force_1d(forces: &[f64]) -> Option<f64> {
47    if !all_finite(forces) {
48        return None;
49    }
50
51    finite(forces.iter().copied().sum())
52}
53
54/// Computes the net force from a list of planar force components.
55///
56/// Each tuple is interpreted as `(force_x, force_y)`.
57///
58/// # Examples
59///
60/// ```rust
61/// use use_statics::net_force_2d;
62///
63/// assert_eq!(net_force_2d(&[(3.0, 4.0), (-1.0, 2.0)]), Some((2.0, 6.0)));
64/// ```
65#[must_use]
66pub fn net_force_2d(forces: &[(f64, f64)]) -> Option<(f64, f64)> {
67    let (sum_x, sum_y) =
68        forces
69            .iter()
70            .try_fold((0.0, 0.0), |(sum_x, sum_y), (force_x, force_y)| {
71                if !force_x.is_finite() || !force_y.is_finite() {
72                    return None;
73                }
74
75                Some((sum_x + force_x, sum_y + force_y))
76            })?;
77
78    finite_pair(sum_x, sum_y)
79}
80
81/// Computes the magnitude of a planar force vector.
82///
83/// Formula: `F = sqrt(Fx² + Fy²)`.
84#[must_use]
85pub fn force_magnitude(force_x: f64, force_y: f64) -> Option<f64> {
86    if !force_x.is_finite() || !force_y.is_finite() {
87        return None;
88    }
89
90    finite(force_x.hypot(force_y))
91}
92
93/// Computes the planar angle of a force vector in radians.
94///
95/// This helper uses `atan2(force_y, force_x)`.
96#[must_use]
97pub fn force_angle_radians(force_x: f64, force_y: f64) -> Option<f64> {
98    if !force_x.is_finite() || !force_y.is_finite() {
99        return None;
100    }
101
102    finite(force_y.atan2(force_x))
103}
104
105/// Checks whether a 1D force system is in translational equilibrium.
106///
107/// Returns `None` when `tolerance` is negative or not finite, or when the net force cannot be
108/// computed as a finite value.
109#[must_use]
110pub fn is_translational_equilibrium_1d(forces: &[f64], tolerance: f64) -> Option<bool> {
111    if !nonnegative_finite(tolerance) {
112        return None;
113    }
114
115    Some(net_force_1d(forces)?.abs() <= tolerance)
116}
117
118/// Checks whether a 2D force system is in translational equilibrium.
119///
120/// Translational equilibrium requires the net force in both axes to be within the provided
121/// tolerance.
122#[must_use]
123pub fn is_translational_equilibrium_2d(forces: &[(f64, f64)], tolerance: f64) -> Option<bool> {
124    if !nonnegative_finite(tolerance) {
125        return None;
126    }
127
128    let (net_x, net_y) = net_force_2d(forces)?;
129
130    Some(net_x.abs() <= tolerance && net_y.abs() <= tolerance)
131}
132
133/// Computes the scalar `z`-moment of a planar force about a chosen point.
134///
135/// Formula: `M = x * Fy - y * Fx`.
136/// Positive values are counterclockwise by convention.
137///
138/// # Examples
139///
140/// ```rust
141/// use use_statics::moment_2d;
142///
143/// assert_eq!(moment_2d(2.0, 0.0, 0.0, 10.0), Some(20.0));
144/// ```
145#[must_use]
146pub fn moment_2d(position_x: f64, position_y: f64, force_x: f64, force_y: f64) -> Option<f64> {
147    if !position_x.is_finite()
148        || !position_y.is_finite()
149        || !force_x.is_finite()
150        || !force_y.is_finite()
151    {
152        return None;
153    }
154
155    finite(position_x.mul_add(force_y, -(position_y * force_x)))
156}
157
158/// Computes a moment from a signed force and a signed moment arm.
159///
160/// Formula: `M = F * d`.
161#[must_use]
162pub fn moment_from_force_and_arm(force: f64, moment_arm: f64) -> Option<f64> {
163    if !force.is_finite() || !moment_arm.is_finite() {
164        return None;
165    }
166
167    finite(force * moment_arm)
168}
169
170/// Computes the net moment from a slice of scalar moment values.
171#[must_use]
172pub fn net_moment(moments: &[f64]) -> Option<f64> {
173    if !all_finite(moments) {
174        return None;
175    }
176
177    finite(moments.iter().copied().sum())
178}
179
180/// Computes the net moment from planar point forces.
181///
182/// Each tuple is interpreted as `(position_x, position_y, force_x, force_y)`.
183#[must_use]
184pub fn net_moment_2d(force_positions: &[(f64, f64, f64, f64)]) -> Option<f64> {
185    let total = force_positions.iter().try_fold(
186        0.0,
187        |total, (position_x, position_y, force_x, force_y)| {
188            Some(total + moment_2d(*position_x, *position_y, *force_x, *force_y)?)
189        },
190    )?;
191
192    finite(total)
193}
194
195/// Checks whether a moment system is in rotational equilibrium.
196#[must_use]
197pub fn is_rotational_equilibrium(moments: &[f64], tolerance: f64) -> Option<bool> {
198    if !nonnegative_finite(tolerance) {
199        return None;
200    }
201
202    Some(net_moment(moments)?.abs() <= tolerance)
203}
204
205/// Checks whether a planar static system satisfies both translational and rotational equilibrium.
206///
207/// # Examples
208///
209/// ```rust
210/// use use_statics::is_static_equilibrium_2d;
211///
212/// assert_eq!(
213///     is_static_equilibrium_2d(&[(1.0, 2.0), (-1.0, -2.0)], &[10.0, -10.0], 0.0),
214///     Some(true)
215/// );
216/// ```
217#[must_use]
218pub fn is_static_equilibrium_2d(
219    forces: &[(f64, f64)],
220    moments: &[f64],
221    tolerance: f64,
222) -> Option<bool> {
223    Some(
224        is_translational_equilibrium_2d(forces, tolerance)?
225            && is_rotational_equilibrium(moments, tolerance)?,
226    )
227}
228
229/// Computes the maximum available static friction.
230///
231/// Formula: `f_s,max = μ_s * N`.
232///
233/// # Examples
234///
235/// ```rust
236/// use use_statics::maximum_static_friction;
237///
238/// assert_eq!(maximum_static_friction(0.5, 100.0), Some(50.0));
239/// ```
240#[must_use]
241pub fn maximum_static_friction(coefficient_static_friction: f64, normal_force: f64) -> Option<f64> {
242    if !nonnegative_finite(coefficient_static_friction) || !nonnegative_finite(normal_force) {
243        return None;
244    }
245
246    finite(coefficient_static_friction * normal_force)
247}
248
249/// Computes the static friction magnitude required to hold horizontal equilibrium.
250#[must_use]
251pub fn required_static_friction(applied_horizontal_force: f64) -> Option<f64> {
252    if !applied_horizontal_force.is_finite() {
253        return None;
254    }
255
256    finite(applied_horizontal_force.abs())
257}
258
259/// Checks whether static friction can hold a body at rest.
260#[must_use]
261pub fn can_static_friction_hold(
262    applied_horizontal_force: f64,
263    coefficient_static_friction: f64,
264    normal_force: f64,
265) -> Option<bool> {
266    Some(
267        required_static_friction(applied_horizontal_force)?
268            <= maximum_static_friction(coefficient_static_friction, normal_force)?,
269    )
270}
271
272/// Computes the normal force on a horizontal surface.
273///
274/// Formula: `N = m * g`.
275#[must_use]
276pub fn normal_force_horizontal_surface(mass: f64, gravitational_acceleration: f64) -> Option<f64> {
277    if !nonnegative_finite(mass) || !nonnegative_finite(gravitational_acceleration) {
278        return None;
279    }
280
281    finite(mass * gravitational_acceleration)
282}
283
284/// Computes the normal force on an incline.
285///
286/// Formula: `N = m * g * cos(theta)`.
287///
288/// # Examples
289///
290/// ```rust
291/// use use_statics::normal_force_incline;
292///
293/// let Some(normal_force) = normal_force_incline(10.0, 9.80665, 0.0) else {
294///     panic!("valid incline inputs should produce a normal force");
295/// };
296///
297/// assert!((normal_force - 98.0665).abs() < 1.0e-12);
298/// ```
299#[must_use]
300pub fn normal_force_incline(
301    mass: f64,
302    gravitational_acceleration: f64,
303    angle_radians: f64,
304) -> Option<f64> {
305    if !nonnegative_finite(mass)
306        || !nonnegative_finite(gravitational_acceleration)
307        || !angle_radians.is_finite()
308    {
309        return None;
310    }
311
312    let normal_force = mass * gravitational_acceleration * angle_radians.cos();
313
314    if normal_force < 0.0 {
315        return None;
316    }
317
318    finite(normal_force)
319}
320
321/// Computes the downslope component of weight on an incline.
322///
323/// Formula: `F_parallel = m * g * sin(theta)`.
324#[must_use]
325pub fn downslope_force_incline(
326    mass: f64,
327    gravitational_acceleration: f64,
328    angle_radians: f64,
329) -> Option<f64> {
330    if !nonnegative_finite(mass)
331        || !nonnegative_finite(gravitational_acceleration)
332        || !angle_radians.is_finite()
333    {
334        return None;
335    }
336
337    finite(mass * gravitational_acceleration * angle_radians.sin())
338}
339
340/// Computes the minimum static friction coefficient needed to prevent sliding on an incline.
341///
342/// Formula: `μ_s,min = tan(theta)`.
343#[must_use]
344pub fn minimum_static_friction_coefficient_for_incline(angle_radians: f64) -> Option<f64> {
345    if !nonnegative_finite(angle_radians) {
346        return None;
347    }
348
349    let coefficient = finite(angle_radians.tan())?;
350
351    (coefficient >= 0.0).then_some(coefficient)
352}
353
354/// Computes support reactions for a simply supported beam with one point load.
355///
356/// Formulae:
357/// - `R_left = P * (L - a) / L`
358/// - `R_right = P * a / L`
359///
360/// # Examples
361///
362/// ```rust
363/// use use_statics::simply_supported_point_load_reactions;
364///
365/// assert_eq!(
366///     simply_supported_point_load_reactions(10.0, 100.0, 5.0),
367///     Some((50.0, 50.0))
368/// );
369/// ```
370#[must_use]
371pub fn simply_supported_point_load_reactions(
372    span: f64,
373    load: f64,
374    load_position_from_left: f64,
375) -> Option<(f64, f64)> {
376    if !positive_finite(span)
377        || !nonnegative_finite(load)
378        || !load_position_from_left.is_finite()
379        || !(0.0..=span).contains(&load_position_from_left)
380    {
381        return None;
382    }
383
384    let left_reaction = load * (span - load_position_from_left) / span;
385    let right_reaction = load * load_position_from_left / span;
386
387    finite_pair(left_reaction, right_reaction)
388}
389
390/// Computes support reactions for a simply supported beam with a uniform load.
391#[must_use]
392pub fn simply_supported_uniform_load_reactions(
393    span: f64,
394    load_per_length: f64,
395) -> Option<(f64, f64)> {
396    if !positive_finite(span) || !nonnegative_finite(load_per_length) {
397        return None;
398    }
399
400    let total_load = load_per_length * span;
401    let reaction = finite(total_load / 2.0)?;
402
403    Some((reaction, reaction))
404}
405
406/// Fixed-end reaction data for a cantilever with a free-end point load.
407#[derive(Debug, Clone, Copy, PartialEq)]
408pub struct CantileverReaction {
409    /// The vertical reaction at the fixed support.
410    pub vertical_reaction: f64,
411    /// The fixed-end moment at the support.
412    pub fixed_end_moment: f64,
413}
414
415/// Computes the fixed-end reaction for a cantilever with a downward load at the free end.
416///
417/// # Examples
418///
419/// ```rust
420/// use use_statics::{CantileverReaction, cantilever_end_point_load_reaction};
421///
422/// assert_eq!(
423///     cantilever_end_point_load_reaction(10.0, 100.0),
424///     Some(CantileverReaction {
425///         vertical_reaction: 100.0,
426///         fixed_end_moment: 1000.0,
427///     })
428/// );
429/// ```
430#[must_use]
431pub fn cantilever_end_point_load_reaction(span: f64, load: f64) -> Option<CantileverReaction> {
432    if !positive_finite(span) || !nonnegative_finite(load) {
433        return None;
434    }
435
436    Some(CantileverReaction {
437        vertical_reaction: finite(load)?,
438        fixed_end_moment: finite(load * span)?,
439    })
440}
441
442/// A validated planar force value.
443#[derive(Debug, Clone, Copy, PartialEq)]
444pub struct Force2D {
445    /// Horizontal force component.
446    pub x: f64,
447    /// Vertical force component.
448    pub y: f64,
449}
450
451impl Force2D {
452    /// Creates a new planar force when both components are finite.
453    #[must_use]
454    pub fn new(x: f64, y: f64) -> Option<Self> {
455        Some(Self {
456            x: finite(x)?,
457            y: finite(y)?,
458        })
459    }
460
461    /// Returns the magnitude of this force.
462    ///
463    /// # Examples
464    ///
465    /// ```rust
466    /// use use_statics::Force2D;
467    ///
468    /// assert_eq!(Force2D::new(3.0, 4.0).and_then(|force| force.magnitude()), Some(5.0));
469    /// ```
470    #[must_use]
471    pub fn magnitude(&self) -> Option<f64> {
472        force_magnitude(self.x, self.y)
473    }
474
475    /// Returns the planar angle of this force in radians.
476    #[must_use]
477    pub fn angle_radians(&self) -> Option<f64> {
478        force_angle_radians(self.x, self.y)
479    }
480}
481
482/// A planar force applied at a position relative to a chosen moment point.
483#[derive(Debug, Clone, Copy, PartialEq)]
484pub struct PointForce2D {
485    /// Horizontal position from the chosen origin.
486    pub position_x: f64,
487    /// Vertical position from the chosen origin.
488    pub position_y: f64,
489    /// Horizontal force component.
490    pub force_x: f64,
491    /// Vertical force component.
492    pub force_y: f64,
493}
494
495impl PointForce2D {
496    /// Creates a new point force when all values are finite.
497    #[must_use]
498    pub fn new(position_x: f64, position_y: f64, force_x: f64, force_y: f64) -> Option<Self> {
499        Some(Self {
500            position_x: finite(position_x)?,
501            position_y: finite(position_y)?,
502            force_x: finite(force_x)?,
503            force_y: finite(force_y)?,
504        })
505    }
506
507    /// Returns the moment of this force about the origin.
508    #[must_use]
509    pub fn moment_about_origin(&self) -> Option<f64> {
510        moment_2d(self.position_x, self.position_y, self.force_x, self.force_y)
511    }
512}
513
514/// A simple planar static system made of force vectors and scalar moments.
515#[derive(Debug, Clone, PartialEq)]
516pub struct StaticSystem2D {
517    /// The planar forces acting on the system.
518    pub forces: Vec<Force2D>,
519    /// Free moments acting on the system.
520    pub moments: Vec<f64>,
521}
522
523impl StaticSystem2D {
524    /// Creates a validated static system.
525    #[must_use]
526    pub fn new(forces: Vec<Force2D>, moments: Vec<f64>) -> Option<Self> {
527        if !forces
528            .iter()
529            .all(|force| force.x.is_finite() && force.y.is_finite())
530            || !all_finite(&moments)
531        {
532            return None;
533        }
534
535        Some(Self { forces, moments })
536    }
537
538    fn force_components(&self) -> Vec<(f64, f64)> {
539        self.forces.iter().map(|force| (force.x, force.y)).collect()
540    }
541
542    /// Returns the net force in the system.
543    #[must_use]
544    pub fn net_force(&self) -> Option<(f64, f64)> {
545        net_force_2d(&self.force_components())
546    }
547
548    /// Returns the net free moment in the system.
549    #[must_use]
550    pub fn net_moment(&self) -> Option<f64> {
551        net_moment(&self.moments)
552    }
553
554    /// Checks whether the system is in static equilibrium.
555    ///
556    /// # Examples
557    ///
558    /// ```rust
559    /// use use_statics::{Force2D, StaticSystem2D};
560    ///
561    /// let Some(system) = StaticSystem2D::new(
562    ///     vec![
563    ///         Force2D::new(1.0, 2.0).unwrap(),
564    ///         Force2D::new(-1.0, -2.0).unwrap(),
565    ///     ],
566    ///     vec![10.0, -10.0],
567    /// ) else {
568    ///     panic!("valid system should construct");
569    /// };
570    ///
571    /// assert_eq!(system.is_equilibrium(0.0), Some(true));
572    /// ```
573    #[must_use]
574    pub fn is_equilibrium(&self, tolerance: f64) -> Option<bool> {
575        is_static_equilibrium_2d(&self.force_components(), &self.moments, tolerance)
576    }
577}
578
579#[cfg(test)]
580#[allow(clippy::float_cmp)]
581mod tests {
582    use core::f64;
583    use core::f64::consts::FRAC_PI_4;
584
585    use super::{
586        CantileverReaction, Force2D, PointForce2D, StaticSystem2D, can_static_friction_hold,
587        cantilever_end_point_load_reaction, downslope_force_incline, force_angle_radians,
588        force_magnitude, is_rotational_equilibrium, is_static_equilibrium_2d,
589        is_translational_equilibrium_1d, is_translational_equilibrium_2d, maximum_static_friction,
590        minimum_static_friction_coefficient_for_incline, moment_2d, moment_from_force_and_arm,
591        net_force_1d, net_force_2d, net_moment, net_moment_2d, normal_force_horizontal_surface,
592        normal_force_incline, required_static_friction, simply_supported_point_load_reactions,
593        simply_supported_uniform_load_reactions,
594    };
595
596    fn approx_eq(left: f64, right: f64, tolerance: f64) {
597        assert!(
598            (left - right).abs() <= tolerance,
599            "expected {left} to be within {tolerance} of {right}"
600        );
601    }
602
603    #[test]
604    fn force_helpers_cover_required_cases() {
605        assert_eq!(net_force_1d(&[10.0, -4.0, -6.0]), Some(0.0));
606        assert_eq!(net_force_1d(&[]), Some(0.0));
607        assert_eq!(net_force_1d(&[10.0, f64::NAN]), None);
608
609        assert_eq!(net_force_2d(&[(1.0, 2.0), (-1.0, -2.0)]), Some((0.0, 0.0)));
610        assert_eq!(net_force_2d(&[]), Some((0.0, 0.0)));
611
612        assert_eq!(force_magnitude(3.0, 4.0), Some(5.0));
613        assert_eq!(force_angle_radians(1.0, 0.0), Some(0.0));
614    }
615
616    #[test]
617    fn translational_equilibrium_helpers_cover_required_cases() {
618        assert_eq!(
619            is_translational_equilibrium_1d(&[10.0, -10.0], 0.0),
620            Some(true)
621        );
622        assert_eq!(
623            is_translational_equilibrium_1d(&[10.0, -9.0], 0.0),
624            Some(false)
625        );
626        assert_eq!(is_translational_equilibrium_1d(&[10.0, -10.0], -1.0), None);
627
628        assert_eq!(
629            is_translational_equilibrium_2d(&[(1.0, 2.0), (-1.0, -2.0)], 0.0),
630            Some(true)
631        );
632    }
633
634    #[test]
635    fn moment_helpers_cover_required_cases() {
636        assert_eq!(moment_2d(2.0, 0.0, 0.0, 10.0), Some(20.0));
637        assert_eq!(moment_2d(0.0, 2.0, 10.0, 0.0), Some(-20.0));
638
639        assert_eq!(moment_from_force_and_arm(10.0, 2.0), Some(20.0));
640        assert_eq!(moment_from_force_and_arm(-10.0, 2.0), Some(-20.0));
641
642        assert_eq!(net_moment(&[10.0, -4.0, -6.0]), Some(0.0));
643        assert_eq!(net_moment(&[]), Some(0.0));
644
645        assert_eq!(
646            net_moment_2d(&[(2.0, 0.0, 0.0, 10.0), (0.0, 2.0, 10.0, 0.0)]),
647            Some(0.0)
648        );
649    }
650
651    #[test]
652    fn rotational_equilibrium_helpers_cover_required_cases() {
653        assert_eq!(is_rotational_equilibrium(&[10.0, -10.0], 0.0), Some(true));
654        assert_eq!(is_rotational_equilibrium(&[10.0, -9.0], 0.0), Some(false));
655        assert_eq!(
656            is_static_equilibrium_2d(&[(1.0, 2.0), (-1.0, -2.0)], &[10.0, -10.0], 0.0),
657            Some(true)
658        );
659    }
660
661    #[test]
662    fn static_friction_helpers_cover_required_cases() {
663        assert_eq!(maximum_static_friction(0.5, 100.0), Some(50.0));
664        assert_eq!(maximum_static_friction(-0.5, 100.0), None);
665
666        assert_eq!(required_static_friction(-20.0), Some(20.0));
667        assert_eq!(can_static_friction_hold(20.0, 0.5, 100.0), Some(true));
668        assert_eq!(can_static_friction_hold(60.0, 0.5, 100.0), Some(false));
669
670        let Some(normal_force) = normal_force_horizontal_surface(10.0, 9.80665) else {
671            panic!("valid horizontal surface inputs should produce a normal force");
672        };
673        approx_eq(normal_force, 98.0665, 1.0e-12);
674        assert_eq!(normal_force_horizontal_surface(-10.0, 9.80665), None);
675    }
676
677    #[test]
678    fn incline_helpers_cover_required_cases() {
679        let Some(normal_force) = normal_force_incline(10.0, 9.80665, 0.0) else {
680            panic!("valid incline inputs should produce a normal force");
681        };
682        approx_eq(normal_force, 98.0665, 1.0e-12);
683
684        let Some(downslope_force) = downslope_force_incline(10.0, 9.80665, 0.0) else {
685            panic!("valid incline inputs should produce a downslope force");
686        };
687        approx_eq(downslope_force, 0.0, 1.0e-12);
688
689        let Some(coefficient) = minimum_static_friction_coefficient_for_incline(FRAC_PI_4) else {
690            panic!("forty-five degrees should produce a finite coefficient");
691        };
692        approx_eq(coefficient, 1.0, 1.0e-12);
693        assert_eq!(minimum_static_friction_coefficient_for_incline(-1.0), None);
694    }
695
696    #[test]
697    fn support_reaction_helpers_cover_required_cases() {
698        assert_eq!(
699            simply_supported_point_load_reactions(10.0, 100.0, 5.0),
700            Some((50.0, 50.0))
701        );
702        assert_eq!(
703            simply_supported_point_load_reactions(10.0, 100.0, 0.0),
704            Some((100.0, 0.0))
705        );
706        assert_eq!(
707            simply_supported_point_load_reactions(10.0, 100.0, 10.0),
708            Some((0.0, 100.0))
709        );
710        assert_eq!(
711            simply_supported_point_load_reactions(10.0, 100.0, 11.0),
712            None
713        );
714        assert_eq!(simply_supported_point_load_reactions(0.0, 100.0, 0.0), None);
715
716        assert_eq!(
717            simply_supported_uniform_load_reactions(10.0, 20.0),
718            Some((100.0, 100.0))
719        );
720        assert_eq!(simply_supported_uniform_load_reactions(10.0, -20.0), None);
721
722        assert_eq!(
723            cantilever_end_point_load_reaction(10.0, 100.0),
724            Some(CantileverReaction {
725                vertical_reaction: 100.0,
726                fixed_end_moment: 1000.0,
727            })
728        );
729        assert_eq!(cantilever_end_point_load_reaction(0.0, 100.0), None);
730    }
731
732    #[test]
733    fn structs_cover_required_cases() {
734        let Some(force) = Force2D::new(3.0, 4.0) else {
735            panic!("valid force should construct");
736        };
737        assert_eq!(force.magnitude(), Some(5.0));
738        assert_eq!(Force2D::new(f64::NAN, 4.0), None);
739
740        let Some(point_force) = PointForce2D::new(2.0, 0.0, 0.0, 10.0) else {
741            panic!("valid point force should construct");
742        };
743        assert_eq!(point_force.moment_about_origin(), Some(20.0));
744        assert_eq!(PointForce2D::new(f64::NAN, 0.0, 0.0, 10.0), None);
745    }
746
747    #[test]
748    fn static_system_covers_required_cases() {
749        let Some(force_a) = Force2D::new(1.0, 2.0) else {
750            panic!("valid force should construct");
751        };
752        let Some(force_b) = Force2D::new(-1.0, -2.0) else {
753            panic!("valid force should construct");
754        };
755        let Some(system) = StaticSystem2D::new(vec![force_a, force_b], vec![10.0, -10.0]) else {
756            panic!("valid system should construct");
757        };
758
759        assert_eq!(system.net_force(), Some((0.0, 0.0)));
760        assert_eq!(system.net_moment(), Some(0.0));
761        assert_eq!(system.is_equilibrium(0.0), Some(true));
762
763        let Some(valid_force) = Force2D::new(1.0, 2.0) else {
764            panic!("valid force should construct");
765        };
766        assert_eq!(StaticSystem2D::new(vec![valid_force], vec![f64::NAN]), None);
767    }
768}