Pore Scale Models#

Pore scale models are one of the more important facets of OpenPNM, but they can be a bit confusing at first, since they work ‘behind-the-scenes’.
They offer 3 main advantages:
  1. A large library of pre-written models is included and they can be mixed together and their parameters edited to get a desired overall result.

  2. They allow automatic regeneration of all dependent properties when something ‘upstream’ is changed.

  3. The pore-scale model machinery was designed to allow easy use of custom written code for cases where a prewritten model is not available.

The best way to explain their importance is via illustration.

Consider a diffusion simulation, where the diffusive conductance is defined as:

\[g_D = D_{AB}\frac{A}{L}\]

The diffusion coefficient can be predicted by the Fuller correlation:

\[D_{AB} = \frac{10^{-3}T^{1.75}(M_1^{-1} + M_2^{-1})^{0.5}}{P[(\Sigma_i V_{i,1})^{0.33} + (\Sigma_i V_{i,2})^{0.33}]^2}\]

Now say you want to re-run the diffusion simulation at different temperature. This would require recalculating \(D_{AB}\), followed by updating the diffusive conductance.

Using pore-scale models in OpenPNM allows for simple and reliable updating of these properties, for instance within a for-loop where temperature is being varied.

Using Existing Pore-Scale Models#

The first advantage listed above is that OpenPNM includes a library of pre-written model. In this example below we can will apply the Fuller model, without having to worry about mis-typing the equation.

[1]:
import numpy as np
np.random.seed(0)
import openpnm as op
%config InlineBackend.figure_formats = ['svg']
pn = op.network.Cubic(shape=[5, 5, 1], spacing=1e-4)
geo = op.geometry.SpheresAndCylinders(network=pn, pores=pn.Ps, throats=pn.Ts)

Now we need to define the gas phase diffusivity. We can fetch the fuller model from the models library to do this, and attach it to an empty phase object:

[2]:
air = op.phases.GenericPhase(network=pn)
f = op.models.phases.diffusivity.fuller
air.add_model(propname='pore.diffusivity',
              model=f,
              MA=0.032, MB=0.028, vA=16.6, vB=17.9)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 air = op.phases.GenericPhase(network=pn)
      2 f = op.models.phases.diffusivity.fuller
      3 air.add_model(propname='pore.diffusivity',
      4               model=f,
      5               MA=0.032, MB=0.028, vA=16.6, vB=17.9)

AttributeError: module 'openpnm' has no attribute 'phases'

Note that we had to supply the molecular weights (MA and MB) as well as the diffusion volumes (vA and vB). This model also requires knowing the temperature and pressure, but by default it will look in ‘pore.temperature’ and ‘pore.pressure’.

Next we need to define a physics object with the diffusive conductance, which is also available in the model libary:

[3]:
phys = op.physics.GenericPhysics(network=pn, phase=air, geometry=geo)
f = op.models.physics.diffusive_conductance.ordinary_diffusion
phys.add_model(propname='throat.diffusive_conductance',
               model=f)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [3], in <cell line: 1>()
----> 1 phys = op.physics.GenericPhysics(network=pn, phase=air, geometry=geo)
      2 f = op.models.physics.diffusive_conductance.ordinary_diffusion
      3 phys.add_model(propname='throat.diffusive_conductance',
      4                model=f)

NameError: name 'air' is not defined

Lastly we can run the Fickian diffusion simulation to get the diffusion rate across the domain:

[4]:
fd = op.algorithms.FickianDiffusion(network=pn, phase=air)
fd.set_value_BC(pores=pn.pores('left'), values=1)
fd.set_value_BC(pores=pn.pores('right'), values=0)
fd.run()
print(fd.rate(pores=pn.pores('left')))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [4], in <cell line: 1>()
----> 1 fd = op.algorithms.FickianDiffusion(network=pn, phase=air)
      2 fd.set_value_BC(pores=pn.pores('left'), values=1)
      3 fd.set_value_BC(pores=pn.pores('right'), values=0)

NameError: name 'air' is not defined

Updating parameter on an existing model#

It’s also easy to change parameters of a model since they are all stored on the object (air in this case), meaning you don’t have to reassign a new model get new parameters (although that would work). The models and their parameters are stored under the models attribute of each object. This is a dictionary with each model stored under the key match the propname to which is was assigned. For instance, to adjust the diffusion volumes of the Fuller model:

[5]:
print('Diffusivity before changing parameter:', air['pore.diffusivity'][0])
air.models['pore.diffusivity']['vA'] = 15.9
air.regenerate_models()
print('Diffusivity after:', air['pore.diffusivity'][0])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [5], in <cell line: 1>()
----> 1 print('Diffusivity before changing parameter:', air['pore.diffusivity'][0])
      2 air.models['pore.diffusivity']['vA'] = 15.9
      3 air.regenerate_models()

NameError: name 'air' is not defined

Replacing an existing model with another#

Let’s say for some reason that the Fuller model is not suitable. It’s easy to go ‘shopping’ in the models library to retrieve a new model and replace the existing one. In the cell below we grab the Chapman-Enskog model and simply assign it to the same propname that the Fuller model was previously.

[6]:
f = op.models.phases.diffusivity.chapman_enskog
air.add_model(propname='pore.diffusivity',
              model=f, MA=0.0032, MB=0.0028, sigma_AB=3.467, omega_AB=4.1e-6)
print('Diffusivity after:', air['pore.diffusivity'][0])
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [6], in <cell line: 1>()
----> 1 f = op.models.phases.diffusivity.chapman_enskog
      2 air.add_model(propname='pore.diffusivity',
      3               model=f, MA=0.0032, MB=0.0028, sigma_AB=3.467, omega_AB=4.1e-6)
      4 print('Diffusivity after:', air['pore.diffusivity'][0])

AttributeError: module 'openpnm.models' has no attribute 'phases'

Note that we don’t need to explicitly call regenerate_models since this occurs automatically when a model is added. We do however, have to regenerate phys object so it calculates the diffusive conductance with the new diffusivity:

[7]:
phys.regenerate_models()
fd.reset()
fd.run()
print(fd.rate(pores=pn.pores('left')))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [7], in <cell line: 1>()
----> 1 phys.regenerate_models()
      2 fd.reset()
      3 fd.run()

NameError: name 'phys' is not defined

Changing dependent properties#

Now consider that you want to find the diffusion rate at higher temperature. This requires recalculating the diffusion coefficient on air, then updating the diffusive conductivity on phys, and finally re-running the simulation. Using pore-scale models this can be done as follows:

[8]:
print('Diffusivity before changing temperaure:', air['pore.diffusivity'][0])
air['pore.temperature'] = 353.0
air.regenerate_models()
print('Diffusivity after:', air['pore.diffusivity'][0])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [8], in <cell line: 1>()
----> 1 print('Diffusivity before changing temperaure:', air['pore.diffusivity'][0])
      2 air['pore.temperature'] = 353.0
      3 air.regenerate_models()

NameError: name 'air' is not defined

We can see that the diffusivity increased with temperature as expected with the Chapman-Enskog model. We can also propagate this change to the diffusive conductance:

[9]:
phys.regenerate_models()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [9], in <cell line: 1>()
----> 1 phys.regenerate_models()

NameError: name 'phys' is not defined

And lastly we can recalculate the diffusion rate:

[10]:
fd.reset()
fd.run()
print(fd.rate(pores=pn.pores('left')))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [10], in <cell line: 1>()
----> 1 fd.reset()
      2 fd.run()
      3 print(fd.rate(pores=pn.pores('left')))

NameError: name 'fd' is not defined

Creating Custom Models#

Lastly, let’s illustrate the ease with which a custom pore-scale model can be defined and used. Let’s create a very basic (and incorrect) model:

[11]:
def new_diffusivity(target, A, B,
                    temperature='pore.temperature',
                    pressure='pore.pressure'):
    T = target[temperature]
    P = target[pressure]
    DAB = A*T**3/(P*B)
    return DAB

There are a few key points to note in the above code.

  1. Every model must accept a target argument since the regenerate_models mechanism assumes it is present. The target is the object to which the model will be attached. It allows for the looking up of necessary properties that should already be defined, like temperature and pressure. Even if you don’t use target within the function it is still required by the pore-scale model mechanism. If it’s presence annoys you, you can put a **kwargs at the end of the argument list to accept all arguments that you don’t explicitly need.

  2. The input parameters should not be arrays (like an Np-long list of temperature values). Instead you should pass the dictionary key of the values on the target. This allows the model to lookup the latest values for each property when regenerate_models is called. This also enables openpnm to store the model parameters as short strings rather than large arrays.

  3. The function should return either a scalar value or an array of Np or Nt length. In the above case it returns a DAB value for each pore, depending on its local temperature and pressure in the pore. However, if the temperature were set to 'throat.temperature' and pressure to 'throat.pressure', then the above function would return a DAB value for each throat and it could be used to calculate 'throat.diffusivity'.

  4. This function can be placed at the top of the script in which it is used, or it can be placed in a separate file and imported into the script with from my_models import new_diffusivity.

Let’s add this model to our air phase and inspect the new values:

[12]:
air.add_model(propname='pore.diffusivity',
              model=new_diffusivity,
              A=1e-6, B=21)
print(air['pore.diffusivity'])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [12], in <cell line: 1>()
----> 1 air.add_model(propname='pore.diffusivity',
      2               model=new_diffusivity,
      3               A=1e-6, B=21)
      4 print(air['pore.diffusivity'])

NameError: name 'air' is not defined