{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Representation of thermodynamic states\n", "\n", "## Goal of this notebook\n", "\n", "- Learn about the `State` object, how to construct, and how to use it." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from feos.si import *\n", "from feos.pcsaft import *\n", "from feos.eos import *" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `State` object\n", "\n", "The `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$.\n", "\n", "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.\n", "\n", "There are **several ways to construct** `State` objects:\n", "\n", "1. Given the natural variables, $\\mathbf{N}, V, T$.\n", "2. Given a combination of other state variables, such as $\\mathbf{N}, p, T$ or $\\mathbf{N}, p, h$.\n", "3. At critical conditions.\n", "4. At phase equilibrium (this will generate multiple `State` objects, one for each phase).\n", "\n", "Constructor methods need the `EquationOfState` or `HelmholtzEnergyFunctional` object 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." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Equation of state object.\n", "parameters = PcSaftParameters.from_json(\n", " ['hexane'], \n", " '../parameters/pcsaft/gross2001.json'\n", ")\n", "pcsaft = EquationOfState.pcsaft(parameters)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The default constructor\n", "\n", "The default constructor, `State(...)`, takes a combination of input state variables. The first argument, however, is always the equation of state.\n", "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}$.\n", "The default constructor takes `SINumber` (and `SIArray1` for e.g. the partial densities) as input. \n", "If you want to learn more about dimensioned quantities, please take a look at the respective tutorial." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|temperature|density|\n", "|-|-|\n", "|300.15000 K|7.51820 kmol/m³|" ], "text/plain": [ "T = 300.15000 K, ρ = 7.51820 kmol/m³" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state_nvt = State(\n", " pcsaft, \n", " temperature=300.15*KELVIN, \n", " density=7.5182*KILO*MOL/METER**3, \n", " total_moles=100.0*MOL\n", ")\n", "state_nvt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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`.\n", "\n", "For example, we can create a `State` for given temperature and pressure (using the default amount of substance for a pure component).\n", "The density is thus determined iteratively and stored as a *field* which can be accessed via `state.density`. \n", "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "density : 7.518194138679665 kmol/m³\n", "pressure: 100.00000000009686 kPa\n" ] } ], "source": [ "state_npt = State(\n", " pcsaft, \n", " temperature=300.15*KELVIN, \n", " pressure=1.0*BAR\n", ")\n", "print('density : ', state_npt.density)\n", "print('pressure: ', state_npt.pressure())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the above case, specifying temperature and pressure may not yield the expected result.\n", "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.\n", "\n", "To control the initial values for the density iteration, you can use the `density_initialization` keyword.\n", "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." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mass density: 3.2263994087922345 kg/m³\n" ] } ], "source": [ "s_vapor = State(\n", " pcsaft, \n", " temperature=335.0*KELVIN, \n", " pressure=1.0*BAR, \n", " density_initialization='vapor'\n", ")\n", "print('mass density: ', s_vapor.mass_density())" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mass density: 616.3096597655958 kg/m³\n" ] } ], "source": [ "s_liquid = State(\n", " pcsaft, \n", " temperature=335.0*KELVIN, \n", " pressure=1.0*BAR, \n", " density_initialization='liquid'\n", ")\n", "print('mass density: ', s_liquid.mass_density())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mass density: 616.3096597655958 kg/m³\n" ] } ], "source": [ "s = State(\n", " pcsaft, \n", " temperature=335.0*KELVIN, \n", " pressure=1.0*BAR\n", ")\n", "print('mass density:', s.mass_density())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can run a *stability analysis* for each state using the `is_stable()` method." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vapor stable? False\n", "Liquid stable? True\n" ] } ], "source": [ "print('Vapor stable? ', s_vapor.is_stable())\n", "print('Liquid stable?', s_liquid.is_stable())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Stored information\n", "\n", "Once we create a `State` object, we can access its **fields** withouth further computations:\n", "\n", "- `density`: molar density of the thermodynamic state for the given substance(s)\n", "- `molefracs`: molar fractions for each substance\n", "- `moles`: amount of substance for each substance\n", "- `partial_density`: molar density for each substance\n", "- `temperature`: temperature\n", "- `total_moles`: total amount of substance\n", "- `volume`: volume\n", "\n", "For 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**:\n", "\n", "- `mass()`: mass for each substance\n", "- `mass_density()`: total mass density\n", "- `massfracs()`: mass fractions for each substance\n", "- `total_mass()`: total mass\n", "- `total_molar_weight()`: total molar weight" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "total moles: 200 mol\n", "total mass : 17.235400000000002 kg\n" ] } ], "source": [ "state = State(\n", " pcsaft, \n", " temperature=335.0*KELVIN, \n", " pressure=1.0*BAR, \n", " total_moles=200.0*MOL\n", ")\n", "print('total moles: ', state.total_moles)\n", "print('total mass : ', state.total_mass())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Computing properties\n", "\n", "Thermodynamic properties can be computed by invoking the appropriate **method**. For example, we can compute the total system pressure via the `pressure()` method." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "pressure: 99.99999999988452 kPa\n" ] } ], "source": [ "pressure = state.pressure()\n", "print('pressure: ', pressure)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "Some properties accept an optional `Contributions` object which we can use to compute specific contributions to the property.\n", "The `Contributions` object allows for four options:\n", "\n", "- `Contributions.IdealGas`: only the ideal gas contribution is considered (which is defined by the ideal gas model for the de Broglie wavelength)\n", "- `Contributions.ResidualNvt`: only the *residual* contributions to the Helmholtz energy with respect to an ideal gas for given $\\mathbf{N}, V, T$ are considered.\n", "- `Contributions.ResidualNpt`: only the *residual* contributions to the Helmholtz energy with respect to an ideal gas for given $\\mathbf{N}, p, T$ are considered.\n", "- `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." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "entropy (default) : -63.82223112938985 J/mol/K\n", "entropy (total) : -63.82223112938985 J/mol/K\n", "entropy (ideal gas) : -20.979592032157587 J/mol/K\n", "entropy (residual) : -42.84263909723226 J/mol/K\n", "entropy (residual p) : -86.86192380683525 J/mol/K\n" ] } ], "source": [ "print('entropy (default) :', state.molar_entropy())\n", "print('entropy (total) :', state.molar_entropy(Contributions.Total))\n", "print('entropy (ideal gas) :', state.molar_entropy(Contributions.IdealGas))\n", "print('entropy (residual) :', state.molar_entropy(Contributions.ResidualNvt))\n", "print('entropy (residual p) :', state.molar_entropy(Contributions.ResidualNpt))" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/latex": [ "$0\\,\\mathrm{\\frac{ J}{molK}}$" ], "text/plain": [ "0 J/mol/K" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state.molar_entropy() - (state.molar_entropy(Contributions.IdealGas) + state.molar_entropy(Contributions.ResidualNvt))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## `State` at critical conditions\n", "\n", "$\\text{FeO}_\\text{s}$ provides constructors for `State` objects at critical conditions as well in form of *static class methods*.\n", "\n", "- `State.critical_point(...)`: critial point of the system\n", "- `State.critical_point_pure(...)`: critical point for each substance in the system\n", "- `State.critical_point_binary_p(...)`: critical point for binary system, given pressure\n", "- `State.critical_point_binary_t(...)`: critical point for binary system, given temperature\n", "\n", "Optional keywords are:\n", "\n", "- `moles`: amount of substance for each component. For mixtures this is mandatory.\n", "- `initial_temperature` : initial value for temperature. Can be used to speed up / increase convergence.\n", "- `max_iter`: number of allowed iterations. Can be increased if convergence is an issue.\n", "- `tol`: tolerance for the solution\n", "- `verbosity`: a `Verbosity` object can be used to print information of the computation. Can be used if convergence is an issue." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|temperature|density|\n", "|-|-|\n", "|519.33427 K|2.65414 kmol/m³|" ], "text/plain": [ "T = 519.33427 K, ρ = 2.65414 kmol/m³" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "critical_point = State.critical_point(pcsaft)\n", "critical_point" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Critical conditions for hexane\n", "temperature : 519.3342707319016 K\n", "pressure : 3.5427176263083453 MPa\n", "density : 228.72574604854387 kg/m³\n" ] } ], "source": [ "print('Critical conditions for hexane')\n", "print('temperature :', critical_point.temperature)\n", "print('pressure :', critical_point.pressure())\n", "print('density :', critical_point.mass_density())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## `PhaseEquilibrium`: `State` objects at phase equilibrium\n", "\n", "Another 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.\n", "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.\n", "\n", "For pure substances, we can generate two states in equilibrium using the `PhaseEquilibrium.pure(...)` static method.\n", "The resulting object contains two (or more) `State` objects which we in this case can access via the `liquid` and `vapor` fields.\n", "As before, we can now compute properties using these objects." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "||temperature|density|\n", "|-|-|-|\n", "|phase 1|341.53511 K|36.63788 mol/m³|\n", "|phase 2|341.53511 K|7.07977 kmol/m³|\n" ], "text/plain": [ "phase 0: T = 341.53511 K, ρ = 36.63788 mol/m³\n", "phase 1: T = 341.53511 K, ρ = 7.07977 kmol/m³" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle = PhaseEquilibrium.pure(\n", " pcsaft, \n", " temperature_or_pressure=1.0*BAR\n", ")\n", "vle" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|temperature|density|\n", "|-|-|\n", "|341.53511 K|7.07977 kmol/m³|" ], "text/plain": [ "T = 341.53511 K, ρ = 7.07977 kmol/m³" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle.liquid" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "|temperature|density|\n", "|-|-|\n", "|341.53511 K|36.63788 mol/m³|" ], "text/plain": [ "T = 341.53511 K, ρ = 36.63788 mol/m³" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vle.vapor" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "enthalpy of vaporization (T = 341.53510965735256 K): 29.123040330216202 kJ/mol\n" ] } ], "source": [ "enthalpy_of_vaporization = vle.vapor.molar_enthalpy() - vle.liquid.molar_enthalpy()\n", "print(f'enthalpy of vaporization (T = {vle.vapor.temperature}): {enthalpy_of_vaporization}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Caching partial derivatives of the Helmholtz energy\n", "\n", "A `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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Contributions to the Helmholtz energy\n", "\n", "If 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). " ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "state = State(\n", " pcsaft, \n", " temperature=300.0*KELVIN, \n", " pressure=1.0*BAR, \n", " total_moles=25.0*MOL\n", ")" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('Ideal gas (QSPR)', 264.00707466668604 kJ),\n", " ('Hard Sphere', 549.608863830199 kJ),\n", " ('Hard Chain', -159.16321691198146 kJ),\n", " ('Dispersion', -750.1133884410586 kJ)]" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state.helmholtz_energy_contributions()" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('Ideal gas (QSPR)', 18.75674450579379 MPa),\n", " ('Hard Sphere', 304.57586929403857 MPa),\n", " ('Hard Chain', -63.0137117835539 MPa),\n", " ('Dispersion', -260.2189020162783 MPa)]" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state.pressure_contributions()" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[('Ideal gas (QSPR)', 13.054621772113414 kJ/mol),\n", " ('Hard Sphere', 62.48794000517027 kJ/mol),\n", " ('Hard Chain', -14.746316825114631 kJ/mol),\n", " ('Dispersion', -64.60937326973789 kJ/mol)]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state.chemical_potential_contributions(0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Dynamic properties via entropy scaling\n", "\n", "If an equation of state implements correlation functions for entropy scaling, it can be used to compute dynamic properties via entropy scaling. For more information, see the respective tutorial for entropy scaling." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## List of `State` methods and fields\n", "\n", "### Constructors\n", "\n", "- `critical_point`\n", "- `critical_point_binary_p`\n", "- `critical_point_binary_t`\n", "- `critical_point_pure`\n", "- `tp_flash`\n", "\n", "### Fields\n", "\n", "- `density`,\n", "- `molefracs`,\n", "- `moles`,\n", "- `partial_density`,\n", "- `temperature`,\n", "- `total_moles`,\n", "- `volume`\n", "\n", "### Stability analysis\n", "\n", "- `is_stable`,\n", "- `stability_analysis`\n", "\n", "### Thermodynamic properties\n", "\n", "- `c_p`,\n", "- `c_v`,\n", "- `chemical_potential`,\n", "- `chemical_potential_contributions`,\n", "- `compressibility`,\n", "- `d2p_drho2`, \n", "- `d2p_dv2`,\n", "- `dc_v_dt`,\n", "- `dln_phi_dnj`,\n", "- `dln_phi_dp`,\n", "- `dln_phi_dt`,\n", "- `dmu_dni`,\n", "- `dmu_dt`,\n", "- `dp_dni`,\n", "- `dp_drho`,\n", "- `dp_dt`,\n", "- `dp_dv`,\n", "- `ds_dt`,\n", "- `enthalpy`,\n", "- `entropy`,\n", "- `gibbs_energy`,\n", "- `helmholtz_energy`,\n", "- `helmholtz_energy_contributions`,\n", "- `internal_energy`,\n", "- `isentropic_compressibility`,\n", "- `isothermal_compressibility`,\n", "- `joule_thomson`,\n", "- `ln_phi`,\n", "- `ln_phi_pure_liquid`,\n", "- `ln_symmetric_activity_coefficient`,\n", "- `molar_enthalpy`,\n", "- `molar_entropy`,\n", "- `molar_gibbs_energy`,\n", "- `molar_helmholtz_energy`,\n", "- `molar_internal_energy`,\n", "- `partial_molar_volume`,\n", "- `partial_molar_enthalpy`,\n", "- `partial_molar_entropy`,\n", "- `pressure`,\n", "- `pressure_contributions`\n", "- `structure_factor`,\n", "- `thermodynamic_factor`\n", "\n", "\n", "### Mass related and mass specific properties\n", "\n", "- `specific_enthalpy`,\n", "- `specific_entropy`,\n", "- `specific_gibbs_energy`,\n", "- `specific_helmholtz_energy`,\n", "- `specific_internal_energy`,\n", "- `speed_of_sound`,\n", "- `thermodynamic_factor`,\n", "- `total_mass`,\n", "- `total_molar_weight`\n", "\n", "### Dynamic properties (entropy scaling)\n", "\n", "- `diffusion`,\n", "- `diffusion_reference`,\n", "- `ln_diffusion_reduced`,\n", "- `ln_thermal_conductivity_reduced`,\n", "- `ln_viscosity_reduced`,\n", "- `thermal_conductivity`,\n", "- `thermal_conductivity_reference`,\n", "- `viscosity`,\n", "- `viscosity_reference`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Concluding remkars\n", "\n", "Hopefully you found this example helpful. If you have comments, critique or feedback, please let us know and consider [opening an issue on github](https://github.com/feos-org/feos/issues)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" }, "vscode": { "interpreter": { "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" } } }, "nbformat": 4, "nbformat_minor": 4 }