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 and Water

In the second part we will cover topics relating to mixtures, including:

  • Creating individual Species objects from the CAS number

  • Combining 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 0x7f1bccad6610>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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.00055239 0.00049776 0.00072744 0.00070669]

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.00055239 0.00049776 0.00072744 0.00070669]
After: [0.00051967 0.00046666 0.00069911 0.00067736]

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.00055239 0.00049776 0.00072744 0.00070669]

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 0x7f1bcc5e58a0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a2a200>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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.” to a model

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 0x7f1bc9a59030>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a59260>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a5b6f0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a5a930>,
 'nitrogen': nitrogen : <openpnm.phase.StandardGas at 0x7f1bc9a5ae30>}

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 0x7f1bc9a5ae30>}

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 0x7f1bc9a5a930>}

But there is a specific method for this:

air.add_comp(n2, mole_fraction=0.79)
air.components
{'oxygen': oxygen : <openpnm.phase.StandardGas at 0x7f1bc9a5a930>,
 'nitrogen': nitrogen : <openpnm.phase.StandardGas at 0x7f1bc9a5ae30>}

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 0x7f1bc9a5b6f0>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a5a930>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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 0x7f1bc9a5ae30>
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
  #  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.