Skip to content

Conversation

@prehner
Copy link
Contributor

@prehner prehner commented Dec 14, 2025

This is something I wanted to improve on since running into some issues in phase equilibrium algorithms. The core ideas:

  • extensive properties should not appear in the evaluation of the equations of state (this was already changed in the last release)
  • extensive properties should only be evaluatable if the state was initialized extensively
  • there are many ways to specify the composition of a mixture, either intensively, or extensively. Interfaces become much easier and flexible to use if we are more generic here.

I actually tried to do this at compile-time, which worked and actually helped a lot identifying all problems that would only be visible in run-time now. However, having yet another generic parameter to ultimately avoid some errors in edge cases is simply not worth it.

The key for evaluating extensive properties is

impl<E, N: Dim, D: DualNum<f64> + Copy> State<E, N, D>
where
    DefaultAllocator: Allocator<N>,
{
    /// Total moles $N=\sum_iN_i$
    pub fn total_moles(&self) -> Moles<D> {
        self.total_moles.expect("Extensive properties can only be evaluated for states that are initialized with extensive properties!")
    }
}

All state methods for extensive properties have to call this method at which point they will panic if the size of the state is not known. This is a hard error (a panic even), because it is a fundamentally wrong usage on the user side. Some previous fields of State are now getters instead (volume, moles, partial_density).

The field total_moles: Option<Moles<D>> is set depending on the inputs with which the state is created. This is done via the
Composition and FullComposition traits.

pub trait Composition<D: DualNum<f64> + Copy, N: Dim>
where
    DefaultAllocator: Allocator<N>,
{
    fn into_molefracs<E: Residual<N, D>>(self, eos: &E) -> (OVector<D, N>, Option<Moles<D>>);
    fn density(&self) -> Option<Density<D>> {
        None
    }
}

pub trait FullComposition<D: DualNum<f64> + Copy, N: Dim>: Composition<D, N>
where
    DefaultAllocator: Allocator<N>,
{
    fn into_moles<E: Residual<N, D>>(self, eos: &E) -> (OVector<D, N>, Moles<D>);
}

state creations that need the full composition (e.g., tp flash or TVN) can require the FullComposition trait. As mentioned before compile-time checks for this only apply to the creation of the state.

The Composition trait is implemented for the following structs:

components input full? density? comment
1 () - -
1 Moles -
2 f64 - -
N OVector<f64,N> - -
N &OVector<f64,N> - -
N OVector<f64,N-1> - - Dyn only
N &OVector<f64,N-1> - - Dyn only
N Moles<OVector<f64,N>> -
N &Moles<OVector<f64,N>> -
N Density<OVector<f64,N>> -

Composition::density is only needed in some specific cases to avoid overspecifying the density of a state.

This gives the opportunity to organize the state creator methods a bit. The following methods are now implemented:

    // every constructor goes through this private function
    fn _new(
        eos: &E,
        temperature: Temperature<D>,
        density: Density<D>,
        molefracs: OVector<D, N>,
        total_moles: Option<Moles<D>>,
    ) -> FeosResult<Self>;

    // the basic extensive constructor
    pub fn new_nvt<X: FullComposition<D, N>>(
        eos: &E,
        temperature: Temperature<D>,
        volume: Volume<D>,
        composition: X,
    ) -> FeosResult<Self>;

    // the basic intensive constructor
    pub fn new<X: Composition<D, N>>(
        eos: &E,
        temperature: Temperature<D>,
        density: Density<D>,
        composition: X,
    ) -> FeosResult<Self>;

    // some small helper functions for special cases
    pub fn new_density(
        eos: &E,
        temperature: Temperature<D>,
        partial_density: Density<OVector<D, N>>,
    ) -> FeosResult<Self>;
    pub fn new_pure(eos: &E, temperature: Temperature<D>, density: Density<D>) -> FeosResult<Self>;

    // the pressure constructors
    pub fn new_npt<X: Composition<D, N>>(
        eos: &E,
        temperature: Temperature<D>,
        pressure: Pressure<D>,
        composition: X,
        density_initialization: Option<DensityInitialization>,
    ) -> FeosResult<Self>;
    pub fn new_tpvx(
        eos: &E,
        temperature: Temperature<D>,
        pressure: Pressure<D>,
        volume: Volume<D>,
        molefracs: OVector<D, N>,
        density_initialization: Option<DensityInitialization>,
    ) -> FeosResult<Self>;
    
    // the caloric constructors (unchanged)
    pub fn new_nph(...)
    pub fn new_nth(...)
    pub fn new_nps(...)
    pub fn new_nts(...)
    pub fn new_nvu(...)
    
    // the builder constructors
    pub fn build<X: Composition<D, N>>(
        eos: &E,
        temperature: Temperature<D>,
        volume: Option<Volume<D>>,
        density: Option<Density<D>>,
        composition: X,
        pressure: Option<Pressure<D>>,
        density_initialization: Option<DensityInitialization>,
    ) -> FeosResult<Self>;
    pub fn build_full<X: Composition<D, N> + Clone>(
        eos: &E,
        temperature: Option<Temperature<D>>,
        volume: Option<Volume<D>>,
        density: Option<Density<D>>,
        composition: X,
        pressure: Option<Pressure<D>>,
        molar_enthalpy: Option<MolarEnergy<D>>,
        molar_entropy: Option<MolarEntropy<D>>,
        molar_internal_energy: Option<MolarEnergy<D>>,
        density_initialization: Option<DensityInitialization>,
        initial_temperature: Option<Temperature<D>>,
    ) -> FeosResult<Self>;

With a lot of the logic being moved to the new traits, the use cases for the StateBuilder dwindle and I suggest to remove it.

@prehner prehner force-pushed the extensive_properties branch from 3569089 to 421a92e Compare January 7, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants