Concise Overview of OpenPNM#

[1]:
import openpnm as op
%config InlineBackend.figure_formats = ['svg']
import numpy as np
import matplotlib.pyplot as plt
op.Workspace().settings['loglevel'] = 50

Creating a network#

OpenPNM includes several network “generators”, including the traditional cubic lattice that will be used here. Refer to Tutorials > Network for more information.

For this introduction we’ll start by just using a standard cubic network:

[2]:
Lx, Ly, Lz = 4, 6, 8
spacing = 1e-4
pn = op.network.Cubic(shape=[Lx, Ly, Lz], spacing=spacing)

Quick visualization of topology#

OpenPNM support Paraview for very detailed visualization, but it’s possible to get a quick view of a network for inspection within python.

Links to Related Notebooks

A detailed illustration of OpenPNM’s plotting functionality

FIXME: Export to Paraview is the preferred way to visualize a network

[3]:
fig, ax = plt.subplots()
op.topotools.plot_coordinates(network=pn, c='r', s=50, ax=ax)
op.topotools.plot_connections(network=pn, ax=ax)
[3]:
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x7fe0209570a0>
../../_images/examples_getting_started_concise_overview_of_openpnm_6_1.svg

Printing objects#

One of the most useful development or debugging tools is to print the network, so see a list of its properties:

[4]:
print(pn)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
openpnm.network.Cubic : net_01
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Properties                                    Valid Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.coords                                     192 / 192
2     throat.conns                                    472 / 472
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#     Labels                                        Assigned Locations
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     pore.all                                      192
2     pore.back                                     32
3     pore.bottom                                   24
4     pore.front                                    32
5     pore.internal                                 192
6     pore.left                                     48
7     pore.right                                    48
8     pore.surface                                  144
9     pore.top                                      24
10    throat.all                                    472
11    throat.internal                               472
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
Parameters                          Values
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Adding geometrical properties#

The network only stores topological information. Geometry objects are used to store information like pore and throat sizes. Each simulation should have a geometry object associated with it. We can create an empty geometry and add properties to it later:

[5]:
geo = op.geometry.GenericGeometry(network=pn, pores=pn.Ps, throats=pn.Ts)

Adding pore-scale models#

Pore-scale models are one of the most useful aspects of OpenPNM.

Links to Related Notebooks

In-depth discussion and illustration of pore-scale models

Below we’ll add a few to geo. OpenPNM includes a library of pre-written models to choose from. First assign a random seed to each pore

[6]:
geo.add_model(propname='pore.seed',
              model=op.models.geometry.pore_size.random,
              seed=0)

Next use the seed to find pore sizes from the cumulative pore size distribution. This process is described in detail here.

[7]:
geo.add_model(propname='pore.diameter',
              model=op.models.geometry.pore_size.weibull,
              scale=0.5e-4, shape=0.8, loc=1e-6)

We can also add models for pore volume, as well as throat diameter, length, and volume:

[8]:
geo.add_model(propname='pore.volume',
              model=op.models.geometry.pore_volume.sphere)
geo.add_model(propname='throat.diameter',
              model=op.models.geometry.throat_size.from_neighbor_pores,
              mode='min')
geo.add_model(propname='throat.length',
              model=op.models.geometry.throat_length.classic)
geo.add_model(propname='throat.volume',
              model=op.models.geometry.throat_volume.cylinder)
geo.add_model(propname='throat.hydraulic_size_factors',
              model=op.models.geometry.hydraulic_size_factors.spheres_and_cylinders)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [8], in <cell line: 6>()
      1 geo.add_model(propname='pore.volume',
      2               model=op.models.geometry.pore_volume.sphere)
      3 geo.add_model(propname='throat.diameter',
      4               model=op.models.geometry.throat_size.from_neighbor_pores,
      5               mode='min')
      6 geo.add_model(propname='throat.length',
----> 7               model=op.models.geometry.throat_length.classic)
      8 geo.add_model(propname='throat.volume',
      9               model=op.models.geometry.throat_volume.cylinder)
     10 geo.add_model(propname='throat.hydraulic_size_factors',
     11               model=op.models.geometry.hydraulic_size_factors.spheres_and_cylinders)

AttributeError: module 'openpnm.models.geometry.throat_length' has no attribute 'classic'

Adjusting pore-scale models#

All the models that have been added to an object are stored in the models attribute, under the same key as the property they calculate. The parameters of each model are also stored and can be adjusted

Links to Related Notebooks

Adjusting the pore size distribution

[9]:
print(geo.models)
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#   Property Name                       Parameter                 Value
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1   pore.seed                           model:                    random
                                        seed:                     0
                                        num_range:                [0, 1]
                                        regeneration mode:        normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
2   pore.diameter                       model:                    weibull
                                        scale:                    5e-05
                                        shape:                    0.8
                                        loc:                      1e-06
                                        seeds:                    pore.seed
                                        regeneration mode:        normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
3   pore.volume                         model:                    sphere
                                        pore_diameter:            pore.diameter
                                        regeneration mode:        normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
4   throat.diameter                     model:                    from_neighbor_pores
                                        mode:                     min
                                        prop:                     pore.diameter
                                        regeneration mode:        normal
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
[10]:
geo.models['pore.diameter']['shape'] = 1.5

This change is not reflected in the ‘pore.diameter’ values until regenerate_models is run. This process also automatically updates all the values that depend on it like ‘pore.volume’.

[11]:
geo.regenerate_models()

Inspecting histograms of properties#

Ideally, matplotlib should be used to make nice plots of pore size histograms and such, but each OpenPNM object has the ability to plot histograms for quick inspections:

[12]:
geo.show_hist(['pore.diameter', 'throat.diameter', 'throat.length'])
../../_images/examples_getting_started_concise_overview_of_openpnm_24_0.svg

Creating phases with thermophysical properties#

It’s possible to create a blank phase with no properties or pore-scale models.

[13]:
water = op.phases.GenericPhase(network=pn)
print(water)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [13], in <cell line: 1>()
----> 1 water = op.phases.GenericPhase(network=pn)
      2 print(water)

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

Adding static data#

The phase objec created above is empty of all physical properties. We could add pore-scale models if desired, as done for geo, but we can also add a static value directly.

[14]:
water['pore.viscosity'] = 0.001
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [14], in <cell line: 1>()
----> 1 water['pore.viscosity'] = 0.001

NameError: name 'water' is not defined

These are several important features evidient in the above line.

Links to Related Notebooks

Overview of data storage in OpenPNM

Using pre-defined classes that include pore-scale models#

OpenPNM includes a small selection of pre-defined phases which include common pore-scale model and preset parameters for describing their properties.

[15]:
hg = op.phases.Mercury(network=pn)
print(hg)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [15], in <cell line: 1>()
----> 1 hg = op.phases.Mercury(network=pn)
      2 print(hg)

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

Defining physics#

The last step of the ‘setup’ of an OpenPNM simulation involves defining which pore-scale physics to use in algorithms. For capillary pressure curves, for instance, we need to calcuate the entry pressure of each throat. This is often doen using the Washburn equation.

These pore-scale models are applied to a physics object.

[16]:
phys = op.physics.GenericPhysics(network=pn, phase=hg, geometry=geo)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [16], in <cell line: 1>()
----> 1 phys = op.physics.GenericPhysics(network=pn, phase=hg, geometry=geo)

NameError: name 'hg' is not defined
[17]:
phys.add_model(propname='throat.entry_pressure',
               model=op.models.physics.capillary_pressure.washburn)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [17], in <cell line: 1>()
----> 1 phys.add_model(propname='throat.entry_pressure',
      2                model=op.models.physics.capillary_pressure.washburn)

NameError: name 'phys' is not defined

Overview of the simulation setup#

Physics objects require know to which phase and geometry they belong. This is because physics models always combine both geometrical and thermophysical information. In other words, when calculating a physics model, data must be pulled from both an associated phase (e.g. surface tension) and the corresponding geometry (e.g. pore size).

This relationship is summarized by ‘the grid’. This shows the relationship between various objects, with physics object laying at the intersection of a phase and a geometry.

Links to Related Notebooks

The role of the Workspace and Projects is explained further here

The ‘grid’ is particularly useful when several subdomains have been defined as demonstrated here

[18]:
print(pn.project.grid)
+--------+
| net_01 |
+--------+
| geo_01 |
+--------+

Objects can have names#

The above table may be unhelpful if there are a lot of objects in the project. This can be remedied by giving them unique names.

[19]:
hg.name = 'mercury'
water.name = 'water'
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [19], in <cell line: 1>()
----> 1 hg.name = 'mercury'
      2 water.name = 'water'

NameError: name 'hg' is not defined
[20]:
print(pn.project.grid)
+--------+
| net_01 |
+--------+
| geo_01 |
+--------+

Running a simulation#

OpenPNM includes a large assortment of pre-written algorithms for simulating various physical processes.

Performing a porosimetry simulation#

It is common to simulate the capillary pressure curve of a material since this is usually measured experimentally so a comparison can be made.

[21]:
mip = op.algorithms.Porosimetry(network=pn, phase=hg)
mip.set_inlets(pores=pn.pores('left'))
mip.run()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [21], in <cell line: 1>()
----> 1 mip = op.algorithms.Porosimetry(network=pn, phase=hg)
      2 mip.set_inlets(pores=pn.pores('left'))
      3 mip.run()

AttributeError: module 'openpnm.algorithms' has no attribute 'Porosimetry'
[22]:
fig = mip.plot_intrusion_curve()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [22], in <cell line: 1>()
----> 1 fig = mip.plot_intrusion_curve()

NameError: name 'mip' is not defined

Using labels to find pore indices#

The above simulation used pn.pores('left') during the call to set_inlets. All OpenPNM objects have a pores and a throats method that retrieves pore/throat indices if a certain label has been applied to them.

Links to Related Notebooks

Labels are discussed at length here

Many common labels are added to networks during creation, usually indicating faces like ‘left’ and ‘right’, which are useful for applying boundary conditions.

Running a fluid flow simulation#

Another type of simulation is to determine permeabilit of a network. This is done using the StokesFlow algorithm.

Before proceeding though, we must remember that we did not define a pore-scale hydraulic conductance model. It is possible to assign a physics model to a phase (and geometry models to the network), but you should really know what your doing. This is illustrated below:

[23]:
water.add_model(propname='throat.hydraulic_conductance',
                model=op.models.physics.hydraulic_conductance.hagen_poiseuille)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [23], in <cell line: 1>()
----> 1 water.add_model(propname='throat.hydraulic_conductance',
      2                 model=op.models.physics.hydraulic_conductance.hagen_poiseuille)

NameError: name 'water' is not defined
[24]:
sf = op.algorithms.StokesFlow(network=pn, phase=water)
sf.set_value_BC(pores=pn.pores('left'), values=1)
sf.set_value_BC(pores=pn.pores('right'), values=0)
sf.run()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [24], in <cell line: 1>()
----> 1 sf = op.algorithms.StokesFlow(network=pn, phase=water)
      2 sf.set_value_BC(pores=pn.pores('left'), values=1)
      3 sf.set_value_BC(pores=pn.pores('right'), values=0)

NameError: name 'water' is not defined

Altering the settings of a simulation#

All OpenPNM objects have a settings attribute, which is dictionary containing a variety of information about how the algorithm is run.

[25]:
print(sf.settings)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [25], in <cell line: 1>()
----> 1 print(sf.settings)

NameError: name 'sf' is not defined

Estimating permeability coefficient of a network#

Now that the fluid flow simulation has been run, its possible to determine the network’s permeability by applying Darcy’s law.

\[Q = \frac{K A}{\mu L}\Delta P\]

We know \(A\) and \(L\) from the network dimension, and \(\mu\) was specified as $ 0.001 Pa:nbsphinx-math:cdot `s$ since we’re using ``water` as the phase. We also applied the \(\Delta P\) as (1-0). Therefore finding \(K\) can be done algebraically once \(Q\) is known.

Transport algorithms possess a rate method that calculates the rate through a set of pores. Find the rate through one of the boundary faces can be done as:

[26]:
Q = sf.rate(pores=pn.pores('left'))
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [26], in <cell line: 1>()
----> 1 Q = sf.rate(pores=pn.pores('left'))

NameError: name 'sf' is not defined

We also know:

[27]:
A = Lx*Ly*spacing**2
L = Lz*spacing
mu = 0.001
dP = 1

Therefore, the permeability coefficient for this network is:

[28]:
K = Q*mu*L/A/dP
print(K)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [28], in <cell line: 1>()
----> 1 K = Q*mu*L/A/dP
      2 print(K)

NameError: name 'Q' is not defined

Returning results to the phase object#

Upon running the fluid flow simulation above, the pressure in each pore is solved for. These results are not automatically pushed back to the associated phase, and the user must explicity do so as follows:

[29]:
water.update(sf.results())
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [29], in <cell line: 1>()
----> 1 water.update(sf.results())

NameError: name 'water' is not defined

Outputting or saving simualtion results#

Once the simulation is complete it can be either output to Paraview as described here (FIXME) or saved to one of several file formats for post-processing.