Skip to main content

twine_thermo/flow/
work.rs

1use std::cmp::Ordering;
2
3use twine_core::constraint::{Constrained, ConstraintError, StrictlyPositive};
4use uom::{ConstZero, si::f64::Power};
5
6/// Represents work flow across a system boundary.
7///
8/// This enum represents flow direction relative to the system:
9///
10/// - `In`: Work flows into the system (positive contribution, work done on the system).
11/// - `Out`: Work flows out of the system (negative contribution, work done by the system).
12/// - `None`: No work flow occurs.
13///
14/// **Note:** Some thermodynamic texts define work as positive when **done by**
15/// the system (i.e., flowing *out*).
16/// This crate uses the opposite convention: **positive = into the system**,
17/// for consistency with [`HeatFlow`](crate::HeatFlow) and
18/// [`MassFlow`](crate::MassFlow) sign conventions.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum WorkFlow {
21    /// Work flowing into the system.
22    In(Constrained<Power, StrictlyPositive>),
23    /// Work flowing out of the system.
24    Out(Constrained<Power, StrictlyPositive>),
25    /// No work flow occurs.
26    None,
27}
28
29impl WorkFlow {
30    /// Creates a [`WorkFlow::In`] representing work flowing into the system.
31    ///
32    /// # Errors
33    ///
34    /// Returns a [`ConstraintError`] if `work_rate` is not strictly positive.
35    pub fn incoming(work_rate: Power) -> Result<Self, ConstraintError> {
36        Ok(Self::In(Constrained::new(work_rate)?))
37    }
38
39    /// Creates a [`WorkFlow::Out`] representing work flowing out of the system.
40    ///
41    /// # Errors
42    ///
43    /// Returns a [`ConstraintError`] if `work_rate` is not strictly positive.
44    pub fn outgoing(work_rate: Power) -> Result<Self, ConstraintError> {
45        Ok(Self::Out(Constrained::new(work_rate)?))
46    }
47
48    /// Creates a [`WorkFlow`] from a signed work flow rate.
49    ///
50    /// - Positive values indicate work flowing into the system.
51    /// - Negative values indicate work flowing out of the system.
52    /// - Zero indicates no work flow.
53    ///
54    /// # Errors
55    ///
56    /// Returns a [`ConstraintError::NotANumber`] if the value is not finite.
57    pub fn from_signed(work_rate: Power) -> Result<Self, ConstraintError> {
58        match work_rate.partial_cmp(&Power::ZERO) {
59            Some(Ordering::Greater) => Self::incoming(work_rate),
60            Some(Ordering::Less) => Self::outgoing(-work_rate),
61            Some(Ordering::Equal) => Ok(Self::None),
62            None => Err(ConstraintError::NotANumber),
63        }
64    }
65
66    /// Returns the signed work flow rate.
67    ///
68    /// - Positive for work flowing into the system.
69    /// - Negative for work flowing out of the system.
70    /// - Zero if no work flow.
71    #[must_use]
72    pub fn signed(&self) -> Power {
73        match self {
74            Self::In(work_rate) => work_rate.into_inner(),
75            Self::Out(work_rate) => -work_rate.into_inner(),
76            Self::None => Power::ZERO,
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    use approx::assert_relative_eq;
86    use uom::si::{f64::Power, power::watt};
87
88    #[test]
89    fn incoming_is_positive() {
90        let w_dot = Power::new::<watt>(150.0);
91        let flow = WorkFlow::incoming(w_dot).unwrap();
92        assert!(matches!(flow, WorkFlow::In(_)));
93        assert_relative_eq!(flow.signed().get::<watt>(), 150.0);
94    }
95
96    #[test]
97    fn outgoing_is_negative() {
98        let w_dot = Power::new::<watt>(250.0);
99        let flow = WorkFlow::outgoing(w_dot).unwrap();
100        assert!(matches!(flow, WorkFlow::Out(_)));
101        assert_relative_eq!(flow.signed().get::<watt>(), -250.0);
102    }
103
104    #[test]
105    fn none_is_zero() {
106        let flow = WorkFlow::None;
107        assert_relative_eq!(flow.signed().get::<watt>(), 0.0);
108    }
109
110    #[test]
111    fn from_signed_work_rate_classifies_correctly() {
112        let in_flow = WorkFlow::from_signed(Power::new::<watt>(75.0)).unwrap();
113        let out_flow = WorkFlow::from_signed(Power::new::<watt>(-50.0)).unwrap();
114        let none_flow = WorkFlow::from_signed(Power::new::<watt>(0.0)).unwrap();
115
116        assert!(matches!(in_flow, WorkFlow::In(_)));
117        assert!(matches!(out_flow, WorkFlow::Out(_)));
118        assert!(matches!(none_flow, WorkFlow::None));
119    }
120
121    #[test]
122    fn rejects_nan_input() {
123        let w_dot = Power::new::<watt>(f64::NAN);
124        let result = WorkFlow::from_signed(w_dot);
125        assert!(matches!(result, Err(ConstraintError::NotANumber)));
126    }
127
128    #[test]
129    fn rejects_negative_incoming() {
130        let w_dot = Power::new::<watt>(-1.0);
131        assert!(WorkFlow::incoming(w_dot).is_err());
132    }
133
134    #[test]
135    fn rejects_zero_incoming() {
136        let w_dot = Power::new::<watt>(0.0);
137        assert!(WorkFlow::incoming(w_dot).is_err());
138    }
139}