Skip to content

Comments

More rigorous treatment of extensive states#330

Merged
prehner merged 2 commits intodevelopmentfrom
extensive_properties
Feb 14, 2026
Merged

More rigorous treatment of extensive states#330
prehner merged 2 commits intodevelopmentfrom
extensive_properties

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.

total_moles is an Option now

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) -> FeosResult<Moles<D>> {
        self.total_moles.ok_or(FeosError::IntensiveState)
    }
}

All state methods for extensive properties have to call this method and therefore have a Result as return value. This has almost no impact on phase equilibrium solvers because those should not depend on the size of the system (there is a small inconsistency in the stability analysis here, which I wasn't able to fix immediately). 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 trait.

The Composition trait

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>>);
}

state creations that need the full composition (only NVT and NVU) will err if the composition does not provide the total moles.

The Composition trait is implemented for the following structs:

components input total_moles? 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>>

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: Composition<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.

Changes to PhaseEquilibrium

To be more rigorous in the handling of extensive multi-phase states, the PhaseEquilibrium struct was adapted accordingly:

pub struct PhaseEquilibrium<E, const P: usize, N: Dim = Dyn, D: DualNum<f64> + Copy = f64>
where
    DefaultAllocator: Allocator<N>,
{
    states: [State<E, N, D>; P],
    pub phase_fractions: [D; P],
    total_moles: Option<Moles<D>>,
}

The addition of phase_fractions enables the calculation of properties like molar enthalpies or entropies without relying on the total moles in each state. To evaluate extensive properties, the total moles of the entire multi-phase state are stored separately analogously to how it is implemented in State. The total_moles within the states should not be touched anymore when the states are part of a PhaseEquilibrium, however, there is currently nothing stopping people from simply doing so. (This would be possible to enforce, if the extensivity of the states were done on the type level, but as mentioned above, that is a bit unnecessarily complex)

@prehner prehner force-pushed the extensive_properties branch from 3569089 to 421a92e Compare January 7, 2026 08:17
@prehner prehner force-pushed the extensive_properties branch from 7bca649 to 76a6c90 Compare January 25, 2026 16:30
@prehner prehner changed the base branch from main to development January 25, 2026 16:32
@prehner prehner force-pushed the extensive_properties branch from 7cb43ee to 0831756 Compare January 25, 2026 17:02
@prehner prehner force-pushed the development branch 2 times, most recently from 13d6ac6 to 711b625 Compare January 26, 2026 15:37
@prehner prehner force-pushed the extensive_properties branch 6 times, most recently from 2b9f8e4 to ad2ae46 Compare January 28, 2026 19:33
@prehner prehner force-pushed the extensive_properties branch from ad2ae46 to 7a5d4ff Compare February 12, 2026 07:43
@prehner prehner force-pushed the extensive_properties branch 3 times, most recently from 89bf0bd to 03ea575 Compare February 13, 2026 14:54
@prehner prehner marked this pull request as ready for review February 13, 2026 15:20
@prehner prehner force-pushed the extensive_properties branch from 03ea575 to 8234c31 Compare February 13, 2026 15:24
@prehner prehner force-pushed the extensive_properties branch from 8234c31 to 6577305 Compare February 14, 2026 08:53
@prehner prehner merged commit cf544cc into development Feb 14, 2026
17 checks passed
@prehner prehner deleted the extensive_properties branch February 14, 2026 08:54
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.

1 participant