# Overall Design¶

## The Grid¶

OpenPNM separates different types of data between 5 object types: Network, Geometry, Phase, Physics, and Algorithms. Each of these are described in more detail below, but their names hopefully indicate what sort of data or roles are assigned to each.

The main motivation for this division of data between objects is encompassed by the following table, known as the Grid, and explained below:

 Network Phase 1 Phase 2 Phase 3 Geometry 1 Physics 1 Physics 2 Physics 3 Geometry 2 Physics 4 Physics 5 Physics 6

This Grid represents a single Project. Each Project has one Network, which has Np pores and Nt throats. The Network’s main role is to house the pore coordinates and throat connection data. Because there is only one Network, it occupies the special corner location in the Grid.

A Project can have many Phases, and since each Phase has a different value for a given property (e.g. density or viscosity) a unique object is required for each one. Each Phase represents a new column in the Grid, where each column has unique values of thermo-physical properties. Phases can exist everywhere, anywhere, or nowhere in a given domain, and can redistribute during a simulation. As such, Phase properties are calculated everywhere, so they are associated with all pores and throats in the domain.

In some cases, the domain may have multiple distinct regions, such as a two-layered electrode, or multi-modal pore size distributions such as a hierarchical rock. Since Geometry objects are responsible for calculating the pore and throat sizes, it is necessary to have multiple objects for these cases (e.g. different parameters for the distribution functions). Each Geometry represents a new row in the Grid, where each row has unique values of geometrical properties. Each row also represents a subset of the total pores and throats in the domain, since each pore and throat can only be assigned to one Geometry. Thus Geometry objects have their own values of Np and Nt, corresponding to the subset of pores and throats they are in charge of.

Finally, Physics objects exist at the intersection of a row and a column. This represents the fact that a Physics object calculates values that require size information and thermo-physical properties. For example, the Hagan-Poiseuille model for hydraulic conductance requires throat diameter and length, as well as viscosity. Each Physics object is associated with a specific Phase, from which it retrieves thermo-physical property data, and a specific Geometry from which it retries geometrical information. Physics objects, because they are associated one-to-one with a Geometry, also apply to a subset of pores and throats, hence have their own values of Np and Nt.

With this Grid arrangement in mind, we can now dive into an explanation of each object and it’s particular abilities.

## Object Inheritance Structure¶

OpenPNM consists of 5 main object types: Network, Phases, Geometries, Physics, and Algorithms. The inheritance structure of each of these objects is shown in the diagram below. Each of these objects is a subclass of the Base class, described in more detail in the next section. Some objects are applied only to subdomains rather than the entire domains, so these inherit from the Subdomain class, which is itself a subclass of Base. Finally, some of these objects also have the ability to store pore-scale models added via the ModelsMixin mixin class.

## The Base Class¶

All the objects in OpenPNM are subclasses of a single Base class, which is a subclass of the Python Dictionary (dict). So before explaining each of the specific OpenPNM subclasses, the Base class should be covered.

class openpnm.core.Base(Np=0, Nt=0, name=None, project=None)[source]

Contains methods for working with the data in the OpenPNM dict objects

Parameters: Np (int, default is 0) – The total number of pores to be assigned to the object Nt (int, default is 0) – The total number of throats to be assigned to the object name (string, optional) – The unique name of the object. If not given one will be generated. project (OpenPNM Project object, optional) – The Project with which the object should be assigned. If not supplied then a new Project is created

Notes

This Base class is used as the template for all other OpenPNM objects, including Networks, Geometries, Phases, Physics, and Algorithms. This class is a subclass of the standard dict so has the usual methods such as pop and keys, and has extra methods for working specifically with OpenPNM data. These are outlined briefly in the following table:

Method or Attribute Functionality
props List of keys containing numerical arrays
labels List of key containing boolean arrays

pores

throats

Returns pore / throat indices that have given labels
Ps, Ts Indices for ALL pores and throats on object

num_pores ,

num_throats

Counts the number of pores or throats with a given label
Np, Nt Total number of pores and throats on the object
tomask Converts a list of pore or throat indices to a boolean mask
toindices Converts a boolean mask to pore or throat indices

map_pores ,

map_throats

Given indices on object B returns corresponding indices on object A
interleave_data Fetches data from associated objects into a single array
interpolate_data Given pore or throat data, interpolate the other
filter_by_label Given indices find those with specific labels
show_hist Method for quickly plotting histograms of data
check_data_health Ensures all data arrays are valid and complete

In addition to the above methods, there are a few attributes which provide access to useful items:

Attribute Functionality
name The string name of the object, unique to each Project
settings A dictionary containing various setting values
project A handle to the Project containing the object

Examples

It is possible to create an instance of Base, although it is not very useful except for demonstration purposes as done here.

>>> import openpnm as op
>>> obj = op.core.Base(Np=4, Nt=5)


Now query the object for its basic properties:

>>> obj.Np, obj.Nt  # Number of pores and throats
(4, 5)


Add a label to the object, as a boolean with True where the label applies:

>>> obj['pore.new_label'] = [ True, False, False, True]


See list of available labels and confirm new_label was added:

>>> print(obj.labels())
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     : pore.all
2     : pore.new_label
3     : throat.all
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――


Use the label to fetch pores where it was applied:

>>> Ps = obj.pores('new_label')
>>> print(Ps)
[0 3]


Find the number of pores with label:

>>> print(obj.num_pores('new_label'))
2


Convert between indices and boolean mask

>>> mask = obj.tomask(throats=[0, 2, 4])
[ True False  True False  True]
>>> inds = obj.toindices(mask)
>>> print(inds)
[0 2 4]


### Settings¶

All Base objects in OpenPNM have a settings attribute which is a dictionary that stores information that dicates an objects behavior. This is particularly useful for Algorithm objects, where information is stores such as the convergence tolerance, maximum number of iterations, and so on. The list of settings on any objects can be nicly printed with print(obj.settings). Settinsg can be changed by hand (object.settings['setting_x'] = 1). Most of the Algorithm object possess a setup method, which accepts arguments that are then stored in the settings dictionary. These setup methods are helpful because their documentation explains what each settings means or controls.

## Networks¶

The GenericNetwork class add more methods to the Base class than any other type in OpenPNM. These added methods are all related to the querying of topological information such as finding neighboring throats, or nearby pores. The table below gives a high level overview of these methods. For a deeper discussion of the topological data format used by OpenPNM (and thus how these queries are performed) refer to Representing Topology.

class openpnm.network.GenericNetwork(conns=None, coords=None, project=None, settings={}, **kwargs)[source]

This generic class contains the main functionality used by all networks

Parameters: coords (array_like) – An Np-by-3 array of [x, y, z] coordinates for each pore. conns (array_like) – An Nt-by-2 array of [head, tail] connections between pores.

Notes

The GenericNetwork class houses a number of methods used for querying and managing the network’s spatial and topological information. The following table gives a very short overview of the methods added those already found on the openpnm.core.Base class.

Method or Attribute Functionality
create_adjacency_matrix Create an adjacency matrix using given weights in a specified format
create_incidence_matrix Create an incidence matrix using given weights in a specified format
get_adjacency_matrix Retrieve an existing adjacency matrix in the specified format (from am)
get_incidence_matrix Retrieve an existing incidence matrix in the specified format (from im)
am Returns the adjacency matrix in COO format
im Returns the incidence matrix in COO format
find_neighbor_pores For a given set of pores, find all neighboring pores
find_neighbor_throats For a given set of pores, find all neighboring throats
find_connecting_throat For each pair of throats find the pores they connect
find_connected_pores For each throat, find the pores which it connects
num_neighbors For a given set of pores find the number of neighbors for each
find_nearby_pores For a given set of pores, find pores that are within a certain distance
check_network_health Check the topology for any problems such as isolated pores

Examples

>>> import openpnm as op


Create some pore coordinates and connections manually and assign to a GenericNetwork instance. Consider a linear network of 4 pores and 3 throats:

0 ―― 1 ―― 3 ―― 2

>>> coords = [[0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0]]
>>> conns = [[0, 1], [1, 3], [2, 3]]
>>> pn = op.network.GenericNetwork(conns=conns, coords=coords)


Networks have two required properties: ‘pore.coords’ and ‘throat.conns’. These arrays indicate the spatial location of each pore, and which pores are connected to which. Without these the Network object cannot function.

>>> print(pn.props())
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1     : pore.coords
2     : throat.conns
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――


The GenericNetwork class has several methods for querying the topology.

>>> Ps = pn.find_neighbor_pores(pores=1)
>>> print(Ps)
[0 3]
>>> Ts = pn.find_neighbor_throats(pores=[0, 1])
>>> print(Ts)
[0 1]
>>> print(pn.num_neighbors(2))
[1]


All of the topological queries are accomplished by inspecting the adjacency and incidence matrices. They are created on demand, and are stored for future use to save construction time.

## Phases and the ModelsMixin¶

The GenericPhase class is very simple subclass of The Base Class. The subclass itself adds no additional methods beyond those of Base, but it uses multiple inheritance, so inherits 3 methods from ModelsMixin, and an added attribute called models which is a ModelsDict object that stores the models and their respective parameters.

class openpnm.phases.GenericPhase(network=None, project=None, settings={}, **kwargs)[source]

This generic class is meant as a starter for custom Phase objects

This class produces a blank-slate object with no pore-scale models for calculating any thermophysical properties. Users must add models and specify parameters for all the properties they require.

Parameters: network (openpnm Network object) – The network to which this Phase should be attached name (str, optional) – A unique string name to identify the Phase object, typically same as instance name but can be anything.

Examples

Create a new empty phase:

>>> import openpnm as op
>>> pn = op.network.Cubic([10, 10, 10])
>>> phase = op.phases.GenericPhase(network=pn)


And add a model:

>>> phase.add_model(propname='pore.molar_density',
...                 model=op.models.phases.molar_density.ideal_gas)


Now confirm that the model was added and data was calculated. The models attribute can be printed:

>>> print(phase.models)
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
#   Property Name             Parameter                 Value
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
1   pore.molar_density        model:                    ideal_gas
pressure:                 pore.pressure
temperature:              pore.temperature
regeneration mode:        normal
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――


And the Phase itself has a nice printout using print(phase).

## Subdomains: Geometry and Physics¶

Geometry and Physics objects are the only two object types can be assigned to a subset of the full domain. This ability is included in the Subdomain class which is a child of the normal Base class. The only functionality added to Subdomain is the ability to set and remove which locations (pores and throats) the object is assigned to.

class openpnm.core.Subdomain(Np=0, Nt=0, name=None, project=None)[source]

This subclass of the Base class provides the ability assign the object to specific locations (pores and throats) in the domain. This class is subclassed by GenericGeometry and GenericPhysics.

Notes

The following table list the two methods added to Base by this subclass.

The Project object has two methods, check_geometry_health and check_physics_health that look to make sure all locations are assigned to one and only one Geometry and/or Physics.

## Algorithms¶

Like the other classes discussed above, the GenericAlgorithm class inherits from Base, but because every algorithm is a bit different and they tend to be more complicated, discussion of the details is reserved for its own section Algorithms.