Creating Phases#
All simulations in OpenPNM eventually require knowing the physical and thermodynamic properties of the fluids and phases. OpenPNM provides a framework for computing these values in the phase
submodule, along with a basic set of functions for predicting the properties of pure phases and mixtures. Since we can’t possibly support an exhaustive physical property library, our policy is to provide a set of reasonable default functions for use as first approximations. There are a number of other packages that can be used when more exact values are required, such as chemicals
and cantera
, which can be used within the framework of OpenPNM.
In the first part of this notebook, we will cover:
Assigning constant values for fixed properties
Generating values using built-in pore-scale models
Exploring the dependency handler
Creating custom pore-scale models
Specific classes for common fluids like
Air
andWater
In the second part we will cover topics relating to mixtures, including:
Creating individual
Species
objects from the CAS numberCombining components into a
Mixture
Utilizing the built-in classes for first-approximations of pure gas and liquid properties, and for mixture properties
Leveraging external packages for making better estimates of pure component and mixture properties
Part 1: The Phase
class#
If your simulation is simple, then a basic Phase
object may be sufficient. It has no predefined models for computing anything, so you have to either assign known values directly (e.g water['pore.viscosity'] = 0.001
) or define models that will compute the values you need. The models can be taken from the openpnm.models.phase
library, or you can write your own. We will cover all three options below.
import openpnm as op
import numpy as np
op.visualization.set_mpl_style()
All simulations start by defining/creating a network. The Demo
class creates a simple cubic network with an assortment of useful geometrical properties included:
pn = op.network.Demo(shape=[2, 2, 1])
Once the network is defined, it is passed to the Phase
class as an argument. This allows phase
to know about the geometrical and topological features of the network:
phase1 = op.phase.Phase(network=pn)
print(phase1)
══════════════════════════════════════════════════════════════════════════════
phase_01 : <openpnm.phase.Phase at 0x7f030a363e20>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Direct assignment of a constant value#
The basic Phase
class creates a (nearly) empty object with only standard temperature and pressure assigned to each pore. In order to use this object for simulations it needs some additional information. For instance, to compute the permeability of pn
we will need the viscosity. So, let’s assign a known value directly for liquid water:
phase1['pore.viscosity'] = 0.001 # Pa.s
Tip
Scalar values are broadcast to a full ndarray
When assigning a scalar value to a dictionary key it gets assigned to every pore (or throat). The result of the above assignment can be seen below.
phase1['pore.viscosity']
array([0.001, 0.001, 0.001, 0.001])
Using a built-in model#
Perhaps you would like to run a simulation in the presence of a temperature gradient, and viscosity is a strong function of temperature. Instead of assigning a constant viscosity, in this case it is better to assign a pore-scale model that OpenPNM will run to compute the viscosity in each pore.
The models
library in OpenPNM contains some general models which can be used, such as polynomials or linear lines. A 4th order polynomial can be fit to experimental data of viscosity vs temperature yielding the following coefficients:
a4 = 5.8543E-11
a3 = -7.6756E-08
a2 = 3.7831E-05
a1 = -8.3156E-03
a0 = 6.8898E-01
These can be used in the the op.models.misc.polynomial
model as follows:
print('Before:', phase1['pore.viscosity'])
f = op.models.misc.polynomial
phase1.add_model(propname='pore.viscosity',
model=f,
a = (a0, a1, a2, a3, a4),
prop='pore.temperature')
print('After:', phase1['pore.viscosity'])
Before: [0.001 0.001 0.001 0.001]
After: [0.00091476 0.00091476 0.00091476 0.00091476]
Caution
Models are run when added
When assigning a pore-scale model using the add_model
function, it is automatically run, so it either overwrites any values present in phase1['pore.viscosity']
or creates a new array in that location. To prevent this behavior, for instance if a model needs other information to be computed before it’s run, add regen_mode='deferred'
to the argument list of the add_model
method.
The most important benefit of using a pore-scale model is that it can re-compute each model if one of its dependent properties has changed. To illustrate the point, let’s set the temperature to a random number between 300 and 350 K, the rerun the model at the new temperatures:
print('Before:', phase1['pore.viscosity'])
phase1['pore.temperature'] = 300.0 + np.random.rand(pn.Np)*50
phase1.regenerate_models()
print('After:', phase1['pore.viscosity'])
Before: [0.00091476 0.00091476 0.00091476 0.00091476]
After: [0.00070813 0.00052388 0.00052282 0.00048111]
Using a water-specific model#
Because water is so common, OpenPNM has some available functions for its properties. For instance, there is no need to look-up experimental data and fit an n-th order polynomial, as this is already provided:
print('Before:', phase1['pore.viscosity'])
f = op.models.phase.viscosity.water_correlation
phase1.add_model(propname='pore.viscosity',
model=f)
print('After:', phase1['pore.viscosity'])
Before: [0.00070813 0.00052388 0.00052282 0.00048111]
After: [0.00067887 0.0004919 0.00049088 0.00045045]
Tip
Overwriting existing models
Assigning a new model with propname='pore.viscostiy'
will overwrite the existing model that is stored in pn.models['pore.viscosity@<domain>']
, which in this case was the custom model we defined above.
See also
Predefined Fluids
OpenPNM also has specific models for air and mercury, which are also common.
Writing your own custom model#
This subject is explained in detail in another tutorial, but the basic outline is as follows. Creating a custom model, containing any functionality needed, is as simple as defining a new python function:
def custom_mu(phase, temperature='pore.temperature'):
T = phase[temperature]
a4 = 5.8543E-11
a3 = -7.6756E-08
a2 = 3.7831E-05
a1 = -8.3156E-03
a0 = 6.8898E-01
mu = a0 + a1*T + a2*T**2 + a3*T**3 + a4*T**4
return mu
phase1.add_model(propname='pore.viscosity',
model=custom_mu)
print(phase1['pore.viscosity'])
[0.00070813 0.00052388 0.00052282 0.00048111]
The values here are the same as above since this function is just a reimplementation of the n-th order polynomial fit
Specific Classes for Common Fluids#
Air, water, and mercury are used commonly enough that OpenPNM not only has pore-scale models for their properties (i.e. op.models.viscosity.water_correlation
), but we have also created pre-defined classes with all the appropriate models already attached:
water = op.phase.Water(network=pn)
print(water)
══════════════════════════════════════════════════════════════════════════════
phase_02 : <openpnm.phase.Water at 0x7f030a380ea0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
4 pore.contact_angle 4 / 4
5 pore.density 4 / 4
6 pore.molar_density 4 / 4
7 pore.surface_tension 4 / 4
8 pore.thermal_conductivity 4 / 4
9 pore.vapor_pressure 4 / 4
10 pore.viscosity 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
As can be seen in the above print-out, a variety of things have been computed, most of which are coming from a water-specific pore-scale model. These can be viewed with:
print(water.models)
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Property Name Parameter Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 pore.contact_angle@all model: constant
value: 110.0
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.density@all model: water_correlation
T: pore.temperature
salinity: pore.salinity
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
3 pore.molar_density@all model: mass_to_molar
MW: param.molecular_weight
rho: pore.density
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
4 pore.surface_tension@all model: water_correlation
T: pore.temperature
salinity: pore.salinity
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
5 pore.thermal_conductivity@all model: water_correlation
T: pore.temperature
salinity: pore.salinity
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
6 pore.vapor_pressure@all model: liquid_pure_antoine
T: pore.temperature
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
7 pore.viscosity@all model: water_correlation
T: pore.temperature
salinity: pore.salinity
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
As can be seen, many of these models include T='pore.temperature'
as a parameter, which means that these models will fetch the current value of water['pore.temperature']
when running, thereby using the most current value of temperature.
Note
**Dependency checking **
Close inspection of the above printout reveals that 'pore.molar_density'
takes 'pore.density'
as an argument. OpenPNM uses a graph-based dependency handler to ensure that 'pore.density'
is computed before 'pore.molar_denstiy'
. This happens automatically when regenerate_model
is called. In some cases an error will occur if these dependencies form cycles which cannot be resolved. This can be fixed by changing the name of one of the “upstream” models, for instance.
Part 2: Using Species and Mixtures#
In some cases a user may wish to compute the thermophysical properties of unusual species, or even mixtures of species. There are packages whose entire purpose is computing these values, so OpenPNM does not attempt to reproduce this functionality. However, it is needed often enough that OpenPNM has some base level support for making ‘first-approximation’ estimates of species properties, and also has a defined API for dealing with mixtures of individual species.
Caution
Mixtures might be an unnecessary complication for you
In most cases you probably do not need to use the Mixture
class. For instance, air is a gas mixture with a well-known set of properties. This is sufficient for computing things like permeability. On the other hand, if you have a 4 component gas mixture at a variety of compositions, and the properties are unknown, then perhaps the Mixture
feature is the right choice.
Pure Species#
Mixtures were introduced as a beta feature in V2, but are now part of V3. To create a Mixture
we start by creating several Species
objects. Let’s make air, consisting of oxygen and nitrogen.
o2 = op.phase.Species(network=pn, species='oxygen')
print(o2)
══════════════════════════════════════════════════════════════════════════════
phase_03 : <openpnm.phase.Species at 0x7f030760d760>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
See also
OpenPNM uses the chemicals and thermo packages behind the scenes
The species
argument is looked up using the thermo
package which contains numerous synonyms for each species. This is argument is actually passed to thermo.Chemical
, which does the database look-up to fetch all the needed chemical properties.
As can be seen above the Species
class does not compute any properties of the given species, BUT it does contain a host of thermodynamic properties in the params
attribute:
print(o2.params)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Parameters Value
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
CAS 7782-44-7
common_name oxygen
charge 0
formula O2
boiling_temperature 90.1878078805
melting_temperature 54.36
triple_point_temperature 54.361
triple_point_pressure 146.277647044
dipole_moment 0.0
LJ_diameter 3.29728
LJ_energy 1.6520845934e-21
surface_tension_Tb 0.013145681055529814
molar_volume_Tb 2.8039990920723867e-05
molecular_weight 31.9988
critical_temperature 154.581
critical_pressure 5043000.0
critical_volume 7.33675715334e-05
critical_compressibilty_factor 0.28787424687871216
acentric_factor 0.0222
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Tip
The “param” prefix and “params” attribute
You can access the values of these parameters either from the params
attribute or using the dictionary lookup of the main object which will dispatch the query to the params
attribute.
o2.params['CAS']
'7782-44-7'
o2['param.CAS']
'7782-44-7'
Writing also works.
o2['param.foo'] = 'bar'
o2.params['foo']
'bar'
These parameters are used in the various property estimation methods. For instance, to compute the viscosity of oxygen, OpenPNM provides a function that implements the model of Stiel and Thodos (openpnm.models.phase.viscosity.gas_pure_ls
):
f = op.models.phase.viscosity.gas_pure_st
o2.add_model(propname='pore.viscosity',
model=f)
print(o2['pore.viscosity'])
[2.0102393e-05 2.0102393e-05 2.0102393e-05 2.0102393e-05]
This function requires several pieces of thermodynamics information, such as the critical temperature and critical pressure. You can see all the arguments below
print(o2.models)
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Property Name Parameter Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 pore.viscosity@all model: gas_pure_st
T: pore.temperature
Tc: param.critical_temperature
Pc: param.critical_pressure
MW: param.molecular_weight
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
The above shows that the temperature of the phase is fetched as 'pore.temperature'
, while all the rest are retrieved from the params
attribute. To further illustrate this behavior, we could write the critical temperature A['pore.critical_temperature']
and also overwrite the default argument:
o2.models['pore.viscosity@all']['Tc'] = 'pore.critical_temperature'
o2['pore.critical_temperature'] = o2['param.critical_temperature']
print(o2.models)
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Property Name Parameter Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 pore.viscosity@all model: gas_pure_st
T: pore.temperature
Tc: pore.critical_temperature
Pc: param.critical_pressure
MW: param.molecular_weight
regeneration mode: normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Now when we regenerate the model it will fetch the critical temperature values for each pore individual, but will work as expected:
o2.regenerate_models()
print(o2['pore.viscosity'])
[2.0102393e-05 2.0102393e-05 2.0102393e-05 2.0102393e-05]
Tip
Passing “param.
The ability to fetch items from the ``params`` attribute via the standard dictionary look-up was added specifically to support the above behavior. Within the function the following line is called:
Tcrit = phase[Tc]
So if Tc='pore.critical_temperature'
is supplied as the argument, then OpenPNM will look for values in the main dictionary (i.e. phase['pore.critical_temperature']
. If however, Tc='param.critical_temperature'
is specified, then OpenPNM will look in the params
attribute, becuase phase['param.critical_temperature']
is equivalent to phase.params['critical_temperature']
.
Gas and Liquid Species#
OpenPNM has a suite of functions for computing the properties of pure phases, but these functions differ for gases and liquids. For this reason we offer two classes for gas and liquid with the appropriate models already defined. These are referred to as StandardLiquid
and StandardGas
to indicate that the models being used are the standard selection which provide a first-approximation:
o2 = op.phase.StandardGas(network=pn, species='o2')
h2o = op.phase.StandardLiquid(network=pn, species='h2o')
These objects are populated with their respective thermodynamic properties, as illustrated above, but the more interesting feature is that an assortment of pore-scale models are also added, which compute the properties:
print(o2)
══════════════════════════════════════════════════════════════════════════════
phase_04 : <openpnm.phase.StandardGas at 0x7f0307638c20>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
4 pore.heat_capacity_gas 4 / 4
5 pore.heat_capacity 4 / 4
6 pore.thermal_conductivity 4 / 4
7 pore.viscosity 4 / 4
8 pore.density 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
print(h2o)
══════════════════════════════════════════════════════════════════════════════
phase_05 : <openpnm.phase.StandardLiquid at 0x7f030760f1a0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
4 pore.heat_capacity_gas 4 / 4
5 pore.heat_capacity 4 / 4
6 pore.thermal_conductivity 4 / 4
7 pore.vapor_pressure 4 / 4
8 pore.viscosity 4 / 4
9 pore.density 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
The functions that are used to compute the properties are taken from the chemicals
package. OpenPNM has reimplemented a minimal selection of these using direct numpy
vectorization. You can see which models are chosen by printing the models
attribute, and you can read the docstring of each function for more information.
print(h2o.models)
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Property Name Parameter Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1 pore.density@all model: liquid_pure_COSTALD
T: pore.temperature
Tc: param.critical_temperature
Vc: param.critical_volume
omega: param.acentric_factor
MW: param.molecular_weight
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.heat_capacity_gas@all model: gas_pure_TRC
T: pore.temperature
a: []
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
3 pore.heat_capacity@all model: liquid_pure_rp
T: pore.temperature
Tc: param.critical_temperature
omega: param.acentric_factor
Cpg: pore.heat_capacity_gas
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
4 pore.thermal_conductivity@all model: liquid_pure_gismr
T: pore.temperature
MW: param.molecular_weight
Tb: param.boiling_temperature
Pc: param.critical_pressure
omega: param.acentric_factor
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
5 pore.viscosity@all model: liquid_pure_ls
T: pore.temperature
MW: param.molecular_weight
Tc: param.critical_temperature
Pc: param.critical_pressure
omega: param.acentric_factor
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
6 pore.vapor_pressure@all model: liquid_pure_antoine
T: pore.temperature
regeneration mode: deferred
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Gas and Liquid Mixtures#
To create a Mixture
we must first specify the individual Species
object. Below we use the StandardGas
class, which is the basic Species
class, but with an assortment of models already attached for computing the component properties:
o2 = op.phase.StandardGas(network=pn, species='o2', name='oxygen')
n2 = op.phase.StandardGas(network=pn, species='n2', name='nitrogen')
air = op.phase.StandardGasMixture(network=pn, components=[o2, n2])
Before using this mixture we must first specify the compositions:
air.y(o2, 0.21)
air.y(n2, 0.79)
Now that the compositions are known, we can run the models:
air.regenerate_models()
print(air)
══════════════════════════════════════════════════════════════════════════════
mixture_01 : <openpnm.phase.StandardGasMixture at 0x7f030763b420>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
4 pore.mole_fraction.oxygen 4 / 4
5 pore.mole_fraction.nitrogen 4 / 4
6 pore.heat_capacity 4 / 4
7 pore.thermal_conductivity 4 / 4
8 pore.viscosity 4 / 4
9 pore.density 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
The above properties are for the Mixture
, which is a blend of the properties of the individual Species
. As demonstrated above where Species
properties were all functions of temperature, the Mixture
class considers the composition of each species in the mixture as well. If we change the composition of the components, and rerun the models, the mixture properties will change:
print('Before:', air['pore.viscosity'])
air.y(o2, 0.8)
air.y(n2, 0.2)
air.regenerate_models()
print('After:', air['pore.viscosity'])
Before: [1.78599451e-05 1.78599451e-05 1.78599451e-05 1.78599451e-05]
After: [2.01895979e-05 2.01895979e-05 2.01895979e-05 2.01895979e-05]
Tip
Retrieving sub-dicts
The mole fractions of both species can be retrieved using the dict
lookup without specifying which component. OpenPNM will return a dictionary of both components with their names as they keys.
print(air['pore.mole_fraction'])
{'oxygen': array([0.8, 0.8, 0.8, 0.8]), 'nitrogen': array([0.2, 0.2, 0.2, 0.2])}
This also means you can index into the returned dictionary using the names:
print(air['pore.mole_fraction'][o2.name])
[0.8 0.8 0.8 0.8]
Alternatively, the returned dictionary can be used to get a list of components that are currently part of the mixture:
air['pore.mole_fraction'].keys()
dict_keys(['oxygen', 'nitrogen'])
The individual species can be retrieved using the components
attribute which returns a dictionary with component names as the keys and handles to the actual objects as values:
air.components
{'oxygen': oxygen : <openpnm.phase.StandardGas at 0x7f030763a2a0>,
'nitrogen': nitrogen : <openpnm.phase.StandardGas at 0x7f030763a2f0>}
Getting and Setting Compositions#
The x
method on LiquidMixture
, and y
on GasMixture
can also be used to retrieve the mole fraction of one or both components:
air.y()
{'oxygen': array([0.8, 0.8, 0.8, 0.8]),
'nitrogen': array([0.2, 0.2, 0.2, 0.2])}
air.y(o2.name)
array([0.8, 0.8, 0.8, 0.8])
Exploring Features of the Mixture Objects#
Adding and Removing Species#
You can remove a species:
air.remove_comp(o2.name)
air.components
{'nitrogen': nitrogen : <openpnm.phase.StandardGas at 0x7f030763a2f0>}
Note
How a Component is Defined
A species is considered a component of a mixture if and only if 'pore.mole_fraction.<species.name>'
appears in the mixture dictionary. Adding and removing the corresponding array from the dictionary is literally how the components are defined.
del air['pore.mole_fraction.' + n2.name]
air.components
{}
They can be readded in the same way:
air['pore.mole_fraction.' + o2.name] = 0.21
air.components
{'oxygen': oxygen : <openpnm.phase.StandardGas at 0x7f030763a2a0>}
But there is a specific method for this:
air.add_comp(n2, mole_fraction=0.79)
air.components
{'oxygen': oxygen : <openpnm.phase.StandardGas at 0x7f030763a2a0>,
'nitrogen': nitrogen : <openpnm.phase.StandardGas at 0x7f030763a2f0>}
The info
Attribute#
The info
attribute reports all the existing properties on the mixture (similar to print
) but also of each of the components:
air.info
══════════════════════════════════════════════════════════════════════════════
mixture_01 : <openpnm.phase.StandardGasMixture at 0x7f030763b420>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature 4 / 4
3 pore.pressure 4 / 4
4 pore.heat_capacity 4 / 4
5 pore.thermal_conductivity 4 / 4
6 pore.viscosity 4 / 4
7 pore.density 4 / 4
8 pore.mole_fraction.oxygen 4 / 4
9 pore.mole_fraction.nitrogen 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all 4
3 throat.all 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Component Phases
══════════════════════════════════════════════════════════════════════════════
oxygen : <openpnm.phase.StandardGas at 0x7f030763a2a0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature.oxygen 4 / 4
3 pore.pressure.oxygen 4 / 4
4 pore.heat_capacity_gas.oxygen 4 / 4
5 pore.heat_capacity.oxygen 4 / 4
6 pore.thermal_conductivity.oxygen 4 / 4
7 pore.viscosity.oxygen 4 / 4
8 pore.density.oxygen 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all.oxygen 4
3 throat.all.oxygen 4
══════════════════════════════════════════════════════════════════════════════
nitrogen : <openpnm.phase.StandardGas at 0x7f030763a2f0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Properties Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.temperature.nitrogen 4 / 4
3 pore.pressure.nitrogen 4 / 4
4 pore.heat_capacity_gas.nitrogen 4 / 4
5 pore.heat_capacity.nitrogen 4 / 4
6 pore.thermal_conductivity.nitrogen 4 / 4
7 pore.viscosity.nitrogen 4 / 4
8 pore.density.nitrogen 4 / 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Labels Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2 pore.all.nitrogen 4
3 throat.all.nitrogen 4
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Get component mole fractions#
You’ll notice that the mixture object has arrays called 'pore.mole_fraction.<compname>'
for each component. The dictionary look-up in OpenPNM will return a subdictionary if the given key is just 'pore.mole_fraction'
.
air['pore.mole_fraction']
{'oxygen': array([0.21, 0.21, 0.21, 0.21]),
'nitrogen': array([0.79, 0.79, 0.79, 0.79])}
The components
Attribute#
This attribute returns a dictionary with each of the components accessible by their name:
d = air.components
print(d.keys())
dict_keys(['oxygen', 'nitrogen'])
Using get_comp_vals
#
Since the mixture is made from several components, it is often desired to get the values of a specific property from each component. This method provides a convenient way to do this:
mus = air.get_comp_vals('pore.viscosity')
print(mus)
{'oxygen': array([2.09391681e-05, 2.09391681e-05, 2.09391681e-05, 2.09391681e-05]), 'nitrogen': array([1.6985128e-05, 1.6985128e-05, 1.6985128e-05, 1.6985128e-05])}
It is also possible to retrieve the properties of a component by asking the mixture and appending the component name, as follows:
air['pore.viscosity.' + o2.name]
array([2.09391681e-05, 2.09391681e-05, 2.09391681e-05, 2.09391681e-05])
In reality there is no array on air
with the name 'pore.viscosity.phase_01'
, but failure to find this array is what actually triggers the look-up of the array from o2
. This is a convenient feature that is added using some ‘syntactic sugar’ behind the scenes in Python.
Checking consistency with check_mixture_health
#
You can also check the health of the mixture, such as whether all the mole fractions add to 1.0 each pore:
print(air.check_mixture_health())
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Key Value
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
mole_fraction_too_low []
mole_fraction_too_high []
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
air.y(o2.name, 0.1)
print(air.check_mixture_health())
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Key Value
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
mole_fraction_too_low (4,)
mole_fraction_too_high []
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Retrieving Species Properties#
Lastly, the properties of the individual species can be retrieved from the mixture as follows:
air.get_comp_vals('pore.viscosity')
{'oxygen': array([2.09391681e-05, 2.09391681e-05, 2.09391681e-05, 2.09391681e-05]),
'nitrogen': array([1.6985128e-05, 1.6985128e-05, 1.6985128e-05, 1.6985128e-05])}
Using the wildcard (*
) syntax#
One more feature that has been added is the ability to fetch requested property arrays from all the components by replacing the component name with the universal wildcard symbol: *
as follows:
air['pore.viscosity.*']
{'oxygen': array([2.09391681e-05, 2.09391681e-05, 2.09391681e-05, 2.09391681e-05]),
'nitrogen': array([1.6985128e-05, 1.6985128e-05, 1.6985128e-05, 1.6985128e-05])}
Note that this returns exactly the same dictionary as the get_comp_vals
method (in fact this function gets called behind the scenes), but this feature is offered for more than just convenience. The main reason for supporting this feature is so that pore-scale models can be instructed to fetch the needed arrays for computing the mixture properties. This is demonstrated in the following simple example of a custom mixture model:
def mole_fraction_weighting(phase, propname):
xs = phase['pore.mole_fraction']
ys = phase[propname] # This is the key step
z = 0.0
for i in xs.keys():
z += xs[i]*ys[i]
return z
vals = mole_fraction_weighting(phase=air, propname='pore.viscosity.*')
print(vals)
[1.55121679e-05 1.55121679e-05 1.55121679e-05 1.55121679e-05]
The use of the '.*'
as the suffix of the propname
argument is crucial here. As can be seen in the definition of mole_fraction_weighting
, the call to phase[propname]
passes 'pore.viscsoity.*'
directly to the dictionary lookup of values from phase
, and this in turn triggers the retrieval of the 'pore.viscosity'
values from each component.
If we were to pass 'pore.viscosity'
then the function would throw an error since the call to phase['pore.viscosity']
would return a single numpy array of viscosity values of the mixture (or not find any values at all).
Note
Component Look-ups
In summary, air['pore.viscosity']
will fetch the viscosity of the mixture, while air['pore.viscosity.oxygen']
will retrieve the values of viscosity from o2
, and air['pore.viscosity.*']
will fetch the viscosity from all the components and return them in a dictionary with each component name as the key.