twine_components/turbomachinery/
work.rs

1//! Work types for turbomachinery models.
2//!
3//! Work in turbomachinery can be reported with different sign conventions.
4//! Twine turbomachinery components instead expose work as a non-negative quantity,
5//! with direction encoded in the type.
6//!
7//! - [`CompressionWork`] represents the shaft work input required by a compressor.
8//! - [`ExpansionWork`] represents the shaft work output produced by a turbine.
9
10use twine_core::constraint::{Constrained, ConstraintError, NonNegative};
11use twine_thermo::units::SpecificEnthalpy;
12
13/// Specific shaft work for compression.
14///
15/// The inner value is a [`SpecificEnthalpy`] that is guaranteed to be non-negative.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct CompressionWork(SpecificEnthalpy);
18
19impl CompressionWork {
20    /// Returns zero compression work.
21    #[must_use]
22    pub fn zero() -> Self {
23        Self::from_constrained(NonNegative::zero())
24    }
25
26    /// Constructs a [`CompressionWork`] if `work >= 0`.
27    ///
28    /// # Errors
29    ///
30    /// Returns a [`ConstraintError`] if `work` is negative or not comparable (e.g., NaN).
31    pub fn new(work: SpecificEnthalpy) -> Result<Self, ConstraintError> {
32        let work = NonNegative::new(work)?;
33        Ok(Self::from_constrained(work))
34    }
35
36    /// Creates a new [`CompressionWork`] from a pre-validated non-negative work value.
37    #[must_use]
38    pub fn from_constrained(work: Constrained<SpecificEnthalpy, NonNegative>) -> Self {
39        Self(work.into_inner())
40    }
41
42    /// Returns the underlying specific work quantity.
43    #[must_use]
44    pub fn quantity(&self) -> SpecificEnthalpy {
45        self.0
46    }
47}
48
49/// Specific shaft work for expansion.
50///
51/// The inner value is a [`SpecificEnthalpy`] that is guaranteed to be non-negative.
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct ExpansionWork(SpecificEnthalpy);
54
55impl ExpansionWork {
56    /// Returns zero expansion work.
57    #[must_use]
58    pub fn zero() -> Self {
59        Self::from_constrained(NonNegative::zero())
60    }
61
62    /// Constructs an [`ExpansionWork`] if `work >= 0`.
63    ///
64    /// # Errors
65    ///
66    /// Returns a [`ConstraintError`] if `work` is negative or not comparable (e.g., NaN).
67    pub fn new(work: SpecificEnthalpy) -> Result<Self, ConstraintError> {
68        let work = NonNegative::new(work)?;
69        Ok(Self::from_constrained(work))
70    }
71
72    /// Creates a new [`ExpansionWork`] from a pre-validated non-negative work value.
73    #[must_use]
74    pub fn from_constrained(work: Constrained<SpecificEnthalpy, NonNegative>) -> Self {
75        Self(work.into_inner())
76    }
77
78    /// Returns the underlying specific work quantity.
79    #[must_use]
80    pub fn quantity(&self) -> SpecificEnthalpy {
81        self.0
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    use twine_core::constraint::NonNegative;
90
91    use crate::turbomachinery::test_utils::enth_si;
92
93    #[test]
94    fn compression_work_rejects_negative() {
95        let negative = enth_si(-1.0);
96        assert!(CompressionWork::new(negative).is_err());
97    }
98
99    #[test]
100    fn compression_work_from_constrained_quantity_roundtrip() {
101        let value = enth_si(42.0);
102        let constrained = NonNegative::new(value).unwrap();
103        let work = CompressionWork::from_constrained(constrained);
104        assert_eq!(work.quantity(), value);
105    }
106
107    #[test]
108    fn expansion_work_rejects_negative() {
109        let negative = enth_si(-1.0);
110        assert!(ExpansionWork::new(negative).is_err());
111    }
112
113    #[test]
114    fn expansion_work_from_constrained() {
115        let value = enth_si(5.0);
116        let constrained = NonNegative::new(value).unwrap();
117        let work = ExpansionWork::from_constrained(constrained);
118        assert_eq!(work.quantity(), value);
119    }
120}