<jupyter_start><jupyter_text>Representation of thermodynamic states Goal of this notebook- Learn about the `State` object, how to construct, and how to use them. Important objects from $\text{FeO}_\text{s}$In the following we need:- SI units via the `feos.si` module,- `PcSaftParameters` and `Contributions` objects from the `feos.pcsaft` module, and- `PcSaft`, `State`, and `PhaseEquilibrium` objects from `feos.pcsaft.eos` modules.- Furthermore, we use `numpy` and `pandas` for arrays and `DataFrames`, respectively,- and `matplotlib` and `seaborn` for visualization.<jupyter_code>from feos.si import *
from feos.pcsaft import Contributions, PcSaftParameters
from feos.pcsaft.eos import PcSaft, State, PhaseEquilibrium<jupyter_output><empty_output><jupyter_text>The `State` objectThe `State` object is the most important object type in $\text{FeO}_\text{s}$. It defines a thermodynamic state in the natural variables of the Helmholtz energy - the amount of substance of each component, $\mathbf{N}$, the volume, $V$, and the temperature, $T$.Once a `State` object is constructed, we can calculate thermodynamic properties. Internally, $\text{FeO}_\text{s}$ transforms the state variables to generalized hyper dual numbers (see the separate tutorial on the topic of dual numbers) with which partial derivatives of the Helmholtz energy are computed.There are **several ways to construct** `State` objects:1. Given the natural variables, $\mathbf{N}, V, T$.2. Given a combination of other state variables, such as $\mathbf{N}, p, T$ or $\mathbf{N}, p, h$.3. At critical conditions.4. At phase equilibrium (this will generate multiple `State` objects, one for each phase).Constructor methods need an equation of state object (here `PcSaft`) as input, since, except for given $\mathbf{N}, V, T$, the density and/or temperature has to be iteratively determined for which derivatives of the Helmholtz energy with respect to volume and temperature are utilized.<jupyter_code># Equation of state object.
parameters = PcSaftParameters.from_json(['hexane'], '../parameters/pcsaft/gross2001.json')
pcsaft = PcSaft(parameters)<jupyter_output><empty_output><jupyter_text>The default constructorThe default constructor, `State(...)`, takes a combination of input state variables. The first argument, however, is always the equation of state.For all cases, if we do not define the amount of substance, it is set to the inverse of Avogradro's number, $N_\text{AV}^{-1}$.The default constructor takes `SINumber` (and `SIArray1` for e.g. the partial densities) as input. If you want to learn more about dimensioned quantities, please take a look at the respective tutorial.<jupyter_code>state_nvt = State(pcsaft, temperature=300.15*KELVIN, density=7.5182*KILO*MOL/METER**3, total_moles=100.0*MOL)
state_nvt<jupyter_output><empty_output><jupyter_text>Since internally, only $\mathbf{N}, V$, and $T$ are stored, all other properties have to be computed even if they were used to create the `State`.For example, we can create a `State` for given temperature and pressure (using the default amount of substance for a pure component).The density is thus determined iteratively and stored as a *field* which can be accessed via `state.density`. If we are interested in the pressure of the state, we have to call the `state.pressure()` *method* which computes the pressure as partial derivative even though we used the pressure to create the `State` in the first place.<jupyter_code>state_npt = State(pcsaft, temperature=300.15*KELVIN, pressure=1.0*BAR)
print('density : ', state_npt.density)
print('pressure: ', state_npt.pressure())<jupyter_output>density :  7.518194138679666 kmol/m³
pressure:  99.99999999998137 kPa<jupyter_text>In the above case, specifying temperature and pressure may not yield the expected result.Consider thermodynamic conditions near phase equilibrium. The resulting density (for given tempreature and pressure) can be that of a meta-stable liquid or vapor phase depending on the initial density for the iteration.To control the initial values for the density iteration, you can use the `density_initialization` keyword.Below, we create two `State` objects for the same temperature and pressure with different initial densities denoted by the `vapor` and `liquid` keywords for `density_initialization`. Alternatively, a starting density (as `SINumber`) can be provided.<jupyter_code>s_vapor = State(pcsaft, temperature=335.0*KELVIN, pressure=1.0*BAR, density_initialization='vapor')
print('mass density: ', s_vapor.mass_density())
s_liquid = State(pcsaft, temperature=335.0*KELVIN, pressure=1.0*BAR, density_initialization='liquid')
print('mass density: ', s_liquid.mass_density())<jupyter_output>mass density:  616.3096597655963 kg/m³<jupyter_text>If no value for `density_initialization` is provided, both a low and high density is used to as starting point for the iteration and only the *stable* phase is returned.<jupyter_code>s = State(pcsaft, temperature=335.0*KELVIN, pressure=1.0*BAR)
print('mass density:', s.mass_density())<jupyter_output>mass density: 616.3096597655963 kg/m³<jupyter_text>You can run a *stability analysis* for each state using the `is_stable()` method.<jupyter_code>print('Vapor stable? ', s_vapor.is_stable())
print('Liquid stable?', s_liquid.is_stable())<jupyter_output>Vapor stable?  False
Liquid stable? True<jupyter_text>--- Stored informationOnce we create a `State` object, we can access its **fields** withouth further computations:- `density`: molar density of the thermodynamic state for the given substance(s)- `molefracs`: molar fractions for each substance- `moles`: amount of substance for each substance- `partial_density`: molar density for each substance- `temperature`: temperature- `total_moles`: total amount of substance- `volume`: volumeFor an equation of state that *implements a molar weight* (i.e. stores the molar weight in the parameter set), mass specific properties are also available as **methods**:- `mass()`: mass for each substance- `mass_density()`: total mass density- `massfracs()`: mass fractions for each substance- `total_mass()`: total mass- `total_molar_weight()`: total molar weight<jupyter_code>state = State(pcsaft, temperature=335.0*KELVIN, pressure=1.0*BAR, total_moles=200.0*MOL)
print('total moles: ', state.total_moles)
print('total mass : ', state.total_mass())<jupyter_output>total moles:  200  mol
total mass :  17.235400000000002 kg<jupyter_text>--- Computing propertiesThermodynamic properties can be computed by invoking the appropriate **method**. For example, we can compute the total system pressure via the `pressure()` method.<jupyter_code>pressure = state.pressure()
print('pressure: ', pressure)<jupyter_output>pressure:  100.00000000000372 kPa<jupyter_text>For a full list of possible thermodyanmic properties, please refer to the API documentation of the `State` object or take a look at the very bottom of this notebook.Some properties accept an optional `Contributions` object which we can use to compute specific contributions to the property.The `Contributions` object allows for four options:- `Contributions.IdealGas`: only the ideal gas contribution is considered (which is defined by the ideal gas model for the de Broglie wavelength)- `Contributions.Residual`: only the *residual* contributions to the Helmholtz energy with respect to an ideal gas for given $\mathbf{N}, V, T$ are considered.- `Contributions.ResidualP`: only the *residual* contributions to the Helmholtz energy with respect to an ideal gas for given $\mathbf{N}, p, T$ are considered.- `Contributions.Total`: all contributions to the Helmholtz energy (and thus the property of interest) are considered, i.e. ideal gas plus residual. This is the **default** for most properties if no argument is provided. Please refer to the method documentation if you are not sure about the contributions used.<jupyter_code>print('entropy (default)    :', state.molar_entropy())
print('entropy (total)      :', state.molar_entropy(Contributions.Total))
print('entropy (ideal gas)  :', state.molar_entropy(Contributions.IdealGas))
print('entropy (residual)   :', state.molar_entropy(Contributions.Residual))
print('entropy (residual p) :', state.molar_entropy(Contributions.ResidualP))
state.molar_entropy() - (state.molar_entropy(Contributions.IdealGas) + state.molar_entropy(Contributions.Residual))<jupyter_output><empty_output><jupyter_text>--- `State` at critical conditions$\text{FeO}_\text{s}$ provides constructors for `State` objects at critical conditions as well in form of *static class methods*.- `State.critical_point(...)`: critial point of the system- `State.critical_point_pure(...)`: critical point for each substance in the system- `State.critical_point_binary_p(...)`: critical point for binary system, given pressure- `State.critical_point_binary_t(...)`: critical point for binary system, given temperatureOptional keywords are:- `moles`: amount of substance for each component. For mixtures this is mandatory.- `initial_temperature` : initial value for temperature. Can be used to speed up / increase convergence.- `max_iter`: number of allowed iterations. Can be increased if convergence is an issue.- `tol`: tolerance for the solution- `verbosity`: a `Verbosity` object can be used to print information of the computation. Can be used if convergence is an issue.<jupyter_code>critical_point = State.critical_point(pcsaft)
critical_point
print('Critical conditions for hexane')
print('temperature :', critical_point.temperature)
print('pressure    :', critical_point.pressure())
print('density     :', critical_point.mass_density())<jupyter_output>Critical conditions for hexane
temperature : 519.3342707319018 K
pressure    : 3.542717626308359 MPa
density     : 228.72574604854444 kg/m³<jupyter_text>--- `PhaseEquilibrium`: `State` objects at phase equilibriumAnother common use case for equations of state is the computation of phase equilibria. In $\text{FeO}_\text{s}$, we can generate multiple `State` objects that are in equilibrium using a `PhaseEquilibrium` object.We will not discuss `PhaseEquilibrium` objects in detail in this notebook but merely consider it as another way to generate `State` objects. Please refer to the tutorial about phase diagrams if you want to learn more.For pure substances, we can generate two states in equilibrium using either the `PhaseEquilibrium.pure_p(...)` or the `PhaseEquilibrium.pure_t(...)` static method.The resulting object contains two (or more) `State` objects which we in this case can access via the `liquid` and `vapor` fields.As before, we can now compute properties using these objects.<jupyter_code>vle = PhaseEquilibrium.pure_p(pcsaft, pressure=1.0*BAR)
vle
vle.liquid
vle.vapor
enthalpy_of_vaporization = vle.vapor.molar_enthalpy() - vle.liquid.molar_enthalpy()
print(f'enthalpy of vaporization (T = {vle.vapor.temperature}): {enthalpy_of_vaporization}')<jupyter_output>enthalpy of vaporization (T = 341.53510965735273 K): 29.12304033021623 kJ/mol<jupyter_text>--- Caching partial derivatives of the Helmholtz energyA `State` object caches partial derivatives of the Helmholtz energy. If efficiency is a concern, you might want to consider the order in which you compute properties for a given state. If a method is called multiple times, only the first call will invoke a computation while additional calls will pull prior results from the cache. --- Contributions to the Helmholtz energyIf you are interested in developing an equation of state, you might find the `..._contributions()` methods of a `State` object useful. These methods return the contributions to a property which can be insightful (or a useful debugging tool).<jupyter_code>state = State(pcsaft, temperature=300.0*KELVIN, pressure=1.0*BAR, total_moles=25.0*MOL)
state.helmholtz_energy_contributions()
state.pressure_contributions()
state.chemical_potential_contributions(0)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09282417121275703, 'fraction_numerical': 0.02140159462861939, 'mean_word_length': 6.059241706161138, 'pattern_counts': {'":': 0, '<': 45, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 3, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '524088', 'license': 'Apache-2.0', 'n_tokens_mistral': 3603, 'n_tokens_neox': 3259, 'n_words': 1338, 'path': 'docs/examples/eos/pcsaft/pcsaft_state.ipynb', 'repo_name': 'feos-org/feos', 'size': '11915'}