Skip to main content

feos_core/state/
composition.rs

1use crate::equation_of_state::Residual;
2use crate::{FeosError, FeosResult};
3use nalgebra::allocator::Allocator;
4use nalgebra::{DefaultAllocator, Dim, Dyn, OVector, U1, U2, dvector, vector};
5use num_dual::{DualNum, DualStruct};
6use quantity::Moles;
7
8/// Trait to generalize over different input types for the composition of
9/// a state.
10///
11/// The trait is implemented for the following data types:
12///
13/// |components|input|total_moles?|comment|
14/// |:-:|-|-|-|
15/// |1|`()`|-||
16/// |1|`Moles`|✅|
17/// |2|`f64`|-|
18/// |N|`OVector<f64,N>`|-|
19/// |N|`&OVector<f64,N>`|-|
20/// |N|`OVector<f64,N-1>`|-|`Dyn` only|
21/// |N|`&OVector<f64,N-1>`|-|`Dyn` only|
22/// |N|`Moles<OVector<f64,N>>`|✅|
23/// |N|`&Moles<OVector<f64,N>>`|✅|
24pub trait Composition<D: DualNum<f64> + Copy, N: Dim>
25where
26    DefaultAllocator: Allocator<N>,
27{
28    /// Convert the composition into molefracs and total moles if possible.
29    #[expect(clippy::type_complexity)]
30    fn into_molefracs<E: Residual<N, D>>(
31        self,
32        eos: &E,
33    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)>;
34}
35
36// trivial implementations
37impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for (OVector<D, N>, Moles<D>)
38where
39    DefaultAllocator: Allocator<N>,
40{
41    fn into_molefracs<E: Residual<N, D>>(
42        self,
43        _: &E,
44    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
45        Ok((self.0, Some(self.1)))
46    }
47}
48
49impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for (OVector<D, N>, Option<Moles<D>>)
50where
51    DefaultAllocator: Allocator<N>,
52{
53    fn into_molefracs<E: Residual<N, D>>(
54        self,
55        _: &E,
56    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
57        Ok((self.0, self.1))
58    }
59}
60
61// a pure component needs no specification
62impl<D: DualNum<f64> + Copy> Composition<D, U1> for () {
63    fn into_molefracs<E: Residual<U1, D>>(
64        self,
65        _: &E,
66    ) -> FeosResult<(OVector<D, U1>, Option<Moles<D>>)> {
67        Ok(((vector![D::one()]), None))
68    }
69}
70impl<D: DualNum<f64> + Copy> Composition<D, Dyn> for () {
71    fn into_molefracs<E: Residual<Dyn, D>>(
72        self,
73        eos: &E,
74    ) -> FeosResult<(OVector<D, Dyn>, Option<Moles<D>>)> {
75        if eos.components() == 1 {
76            Ok(((dvector![D::one()]), None))
77        } else {
78            Err(FeosError::UndeterminedState(
79                "The composition needs to be specified for a system with more than one component."
80                    .into(),
81            ))
82        }
83    }
84}
85
86// a binary mixture can be specified by a scalar (x1)
87impl<D: DualNum<f64> + Copy> Composition<D, U2> for D {
88    fn into_molefracs<E: Residual<U2, D>>(
89        self,
90        _: &E,
91    ) -> FeosResult<(OVector<D, U2>, Option<Moles<D>>)> {
92        Ok(((vector![self, -self + 1.0]), None))
93    }
94}
95
96// this cannot be implemented generically for D due to mising specialization
97impl Composition<f64, Dyn> for f64 {
98    fn into_molefracs<E: Residual>(
99        self,
100        eos: &E,
101    ) -> FeosResult<(OVector<f64, Dyn>, Option<Moles>)> {
102        if eos.components() == 2 {
103            Ok(((dvector![self, 1.0 - self]), None))
104        } else {
105            Err(FeosError::UndeterminedState(format!(
106                "A scalar ({}) can only be used to specify a binary mixture!",
107                self
108            )))
109        }
110    }
111}
112
113// a pure component can be specified by the total mole number
114impl<D: DualNum<f64> + Copy> Composition<D, U1> for Moles<D> {
115    fn into_molefracs<E: Residual<U1, D>>(
116        self,
117        _: &E,
118    ) -> FeosResult<(OVector<D, U1>, Option<Moles<D>>)> {
119        Ok(((vector![D::one()]), Some(self)))
120    }
121}
122
123impl<D: DualNum<f64> + Copy> Composition<D, Dyn> for Moles<D> {
124    fn into_molefracs<E: Residual<Dyn, D>>(
125        self,
126        eos: &E,
127    ) -> FeosResult<(OVector<D, Dyn>, Option<Moles<D>>)> {
128        if eos.components() == 1 {
129            Ok(((dvector![D::one()]), Some(self)))
130        } else {
131            Err(FeosError::UndeterminedState(format!(
132                "A single mole number ({}) can only be used to specify a pure component!",
133                self.re()
134            )))
135        }
136    }
137}
138
139// the mixture can be specified by its molefractions
140//
141// for a dynamic number of components, it is also possible to specify only the
142// N-1 first components
143impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for OVector<D, N>
144where
145    DefaultAllocator: Allocator<N>,
146{
147    fn into_molefracs<E: Residual<N, D>>(
148        self,
149        eos: &E,
150    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
151        (&self).into_molefracs(eos)
152    }
153}
154
155impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for &OVector<D, N>
156where
157    DefaultAllocator: Allocator<N>,
158{
159    fn into_molefracs<E: Residual<N, D>>(
160        self,
161        eos: &E,
162    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
163        let sum = self.sum();
164        if eos.components() == self.len() {
165            Ok(((self.clone() / sum), None))
166        } else if eos.components() == self.len() + 1 {
167            let mut x = OVector::zeros_generic(N::from_usize(eos.components()), U1);
168            for i in 0..self.len() {
169                x[i] = self[i];
170            }
171            x[self.len()] = -sum + 1.0;
172            Ok(((x), None))
173        } else {
174            Err(FeosError::UndeterminedState(format!(
175                "The length of the composition vector ({}) does not match the number of components ({})!",
176                self.len(),
177                eos.components()
178            )))
179        }
180    }
181}
182
183// the mixture can be specified by its moles
184impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for Moles<OVector<D, N>>
185where
186    DefaultAllocator: Allocator<N>,
187{
188    fn into_molefracs<E: Residual<N, D>>(
189        self,
190        eos: &E,
191    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
192        (&self).into_molefracs(eos)
193    }
194}
195
196impl<D: DualNum<f64> + Copy, N: Dim> Composition<D, N> for &Moles<OVector<D, N>>
197where
198    DefaultAllocator: Allocator<N>,
199{
200    fn into_molefracs<E: Residual<N, D>>(
201        self,
202        eos: &E,
203    ) -> FeosResult<(OVector<D, N>, Option<Moles<D>>)> {
204        if eos.components() == self.len() {
205            let total_moles = self.sum();
206            Ok(((self.convert_to(total_moles)), Some(total_moles)))
207        } else {
208            Err(FeosError::UndeterminedState(format!(
209                "The length of the composition vector ({}) does not match the number of components ({})!",
210                self.len(),
211                eos.components()
212            )))
213        }
214    }
215}