Repository: PMEAL/OpenPNM
Pull request #1291 (2019-05-30 02:45:16)

Bug fixes


Patch:
diff --git a/docs/getting_started/examples.rst b/docs/getting_started/examples.rst
index 70a4ce08c..d01c2ba87 100644
--- a/docs/getting_started/examples.rst
+++ b/docs/getting_started/examples.rst
@@ -6,9 +6,9 @@ Examples
 
 
 --------------------------------------------------------------------------------
-OpenPNM-Examples Repository on Github
+OpenPNM Examples on Github
 --------------------------------------------------------------------------------
 
-Examples of performing specific tasks in OpenPNM are given on a separate Github repo.  The advantage of this is that example files can be tested using continuous integration so we can alerted if an example has a typo or flaw.  Github also renders Jupyter Notebooks, so all the examples are given as notebooks which can be downloaded and altered if desired.
+Examples of performing specific tasks in OpenPNM are given in a folder on the main Github repo.  The example files are tested using continuous integration so we can alerted if an example has a typo or flaw.  Github also renders Jupyter Notebooks, so all the examples are given as notebooks which can be downloaded and altered if desired.
 
-The repo can be found `here <https://www.github.com/PMEAL/OpenPNM-Examples>`_.
+The repo can be found `here <https://www.github.com/PMEAL/OpenPNM/tree/master/examples>`_.
diff --git a/examples/README.md b/examples/README.md
index 38b330eb6..fd56750cd 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,6 +1,6 @@
 [![Travis](https://travis-ci.org/PMEAL/OpenPNM.svg?branch=master)](https://travis-ci.org/PMEAL/OpenPNM)
 
-# OpenPNM-Examples
+# OpenPNM Examples
 
 A collection of scripts illustrating the use of OpenPNM.  The files in these folders are written in MarkDown, which allows for an useful blend of formatted text and highlighted code.  You can browse the available files by clicking through the web interface of the repository seen above.
 > **Note:** All the examples in this repository are subject to automated testing, so if the build badge above says passing then all of these examples should work.  All code within these files is in code-blocks and preceded by the python prompt symbol (>>>).  This is annoying for cutting-and-pasting, but is necessary if we want to run automated tests on the code.
diff --git a/examples/tutorials/01 - Intro to OpenPNM - Basics.ipynb b/examples/tutorials/01 - Intro to OpenPNM - Basics.ipynb
index 3a3ad86a1..26a3c2bd3 100644
--- a/examples/tutorials/01 - Intro to OpenPNM - Basics.ipynb
+++ b/examples/tutorials/01 - Intro to OpenPNM - Basics.ipynb
@@ -279,7 +279,7 @@
     "\n",
     "![](http://i.imgur.com/ScdydO9l.png)\n",
     "\n",
-    "This image was creating using [Paraview](http://www.paraview.org), using the instructions given here: [Example in the OpenPNM-Example collection](https://github.com/PMEAL/OpenPNM-Examples/blob/master/IO_and_Visualization/paraview.md)"
+    "This image was creating using [Paraview](http://www.paraview.org), using the instructions given [here](https://openpnm.readthedocs.io/en/master/getting_started/visualize_in_paraview.html)"
    ]
   },
   {
diff --git a/openpnm/core/Base.py b/openpnm/core/Base.py
index e6359ef4c..57aba04ef 100644
--- a/openpnm/core/Base.py
+++ b/openpnm/core/Base.py
@@ -249,7 +249,7 @@ class Base(dict):
         element, prop = key.split('.', 1)
         if key in self.keys():
             # Get values if present on self
-            vals = self.get(key)
+            vals = super().__getitem__(key)
         elif key in self.keys(mode='all', deep=True):
             # Interleave values from geom if found there
             vals = self.interleave_data(key)
@@ -265,6 +265,12 @@ class Base(dict):
             keys = self.keys(mode='all', deep=True)
             vals.update({k: self.interleave_data(k) for k in keys
                          if k.startswith(key + '.')})
+        # The following code, if activated, attempts to run models when
+        # missing data is requested from the dictionary.  The works fine,
+        # but breaks the general way openpnm behaviors.
+        # elif hasattr(self, 'models') and key in self.models:
+        #     self.regenerate_models(key)
+        #     vals = super().__getitem__(key)
         else:
             raise KeyError(key)
         return vals
@@ -1544,27 +1550,41 @@ class Base(dict):
         temp = sp.size(super(Base, self).__getitem__(element+'.all'))
         return temp
 
-    def show_hist(self, props=[], bins=20, **kwargs):
+    def show_hist(self,
+                  props=['pore.diameter', 'throat.diameter', 'throat.length'],
+                  bins=20, fontsize=22, **kwargs):
         r"""
         Show a quick plot of key property distributions.
 
         Parameters
         ----------
         props : string or list of strings
-            The pore and/or throat properties to be plotted as histograms
+            The pore and/or throat properties to be plotted as histograms.  By
+            default this function will show 'pore.diameter', 'throat.diameter',
+            and 'throat.length'.
 
         bins : int or array_like
             The number of bins to use when generating the histogram.  If an
             array is given they are used as the bin spacing instead.
 
+        fontsize : int
+            Sets the font size temporarily.  The default size of matplotlib is
+            10, which is too small for many screenn.  This function has a
+            default of 22, which does not overwrite the matplotlib setting.
+            Note that you can override matplotlib setting globally with
+            ``matplotlib.rcParams['font.size'] = 22``.
+
         Notes
         -----
         Other keyword arguments are passed to the ``matplotlib.pyplot.hist``
         function.
         """
+        temp = plt.rcParams['font.size']
+        plt.rcParams['font.size'] = fontsize
         if type(props) is str:
             props = [props]
         N = len(props)
+        color = plt.cm.tab10(range(10))
         if N == 1:
             r = 1
             c = 1
@@ -1574,25 +1594,24 @@ class Base(dict):
         else:
             r = int(sp.ceil(N**0.5))
             c = int(sp.floor(N**0.5))
-
         for i in range(len(props)):
             plt.subplot(r, c, i+1)
-            plt.hist(self[props[i]], bins=bins, **kwargs)
-
-    def check_data_health(self, props=[], element=None):
+            try:
+                # Update kwargs with some default values
+                if 'edgecolor' not in kwargs.keys():
+                    kwargs.update({'edgecolor': 'k'})
+                if 'facecolor' not in kwargs:
+                    kwargs.update({'facecolor': color[sp.mod(i, 10)]})
+                plt.hist(self[props[i]], bins=bins, **kwargs)
+            except KeyError:
+                pass
+            plt.xlabel(props[i])
+        plt.rcParams['font.size'] = temp
+
+    def check_data_health(self):
         r"""
         Check the health of pore and throat data arrays.
 
-        Parameters
-        ----------
-        element : string, optional
-            Can be either 'pore' or 'throat', which will limit the checks to
-            only those data arrays.
-
-        props : list of pore (or throat) properties, optional
-            If given, will limit the health checks to only the specfied
-            properties.  Also useful for checking existance.
-
         Returns
         -------
         Returns a HealthDict object which a basic dictionary with an added
@@ -1607,20 +1626,7 @@ class Base(dict):
         >>> h.health
         True
         """
-        health = HealthDict()
-        if props == []:
-            props = self.props(element)
-        else:
-            if type(props) == str:
-                props = [props]
-        for item in props:
-            health[item] = []
-            if self[item].dtype == 'O':
-                health[item] = 'No checks on object'
-            elif sp.sum(sp.isnan(self[item])) > 0:
-                health[item] = 'Has NaNs'
-            elif sp.shape(self[item])[0] != self._count(item.split('.')[0]):
-                health[item] = 'Wrong Length'
+        health = self.project.check_data_health(obj=self)
         return health
 
     def _parse_indices(self, indices):
diff --git a/openpnm/core/ModelsMixin.py b/openpnm/core/ModelsMixin.py
index b75173316..fa853fd70 100644
--- a/openpnm/core/ModelsMixin.py
+++ b/openpnm/core/ModelsMixin.py
@@ -112,6 +112,37 @@ class ModelsDict(PrintableDict):
         return '\n'.join(lines)
 
 
+class ModelWrapper(dict):
+    r"""
+    This class is used to hold individual models and provide some extra
+    functionality, such as pretty-printing.
+    """
+    @property
+    def propname(self):
+        for proj in ws.values():
+            for obj in proj:
+                if hasattr(obj, 'models'):
+                    for key, mod in obj.models.items():
+                        if mod is self:
+                            return key
+
+    def __str__(self):
+        horizontal_rule = '―' * 78
+        lines = [horizontal_rule]
+        strg = '{0:<25s} {2:<25s} {2}'
+        lines.append(strg.format('Property Name', 'Parameter', 'Value'))
+        lines.append(horizontal_rule)
+        temp = self.copy()
+        regen_mode = temp.pop('regen_mode', None)
+        model = str(temp.pop('model')).split(' ')[1]
+        lines.append(strg.format(self.propname, 'model:', model))
+        for param in temp.keys():
+            lines.append(strg.format('', param+':', temp[param]))
+        lines.append(strg.format('', 'regeneration mode:', regen_mode))
+        lines.append(horizontal_rule)
+        return '\n'.join(lines)
+
+
 class ModelsMixin():
     r"""
     This class is meant to be combined by the Base class in multiple
@@ -214,9 +245,9 @@ class ModelsMixin():
             for k, v in zip(keys, vals):  # Put defaults into kwargs
                 if k not in kwargs:  # Skip if argument was given in kwargs
                     kwargs.update({k: v})
-        self.models[propname] = kwargs  # Store all keyword argumnents in model
+        self.models[propname] = ModelWrapper(kwargs)  # Store all kwargs
         # Regenerate model values if necessary
-        if regen_mode not in ['deferred']:
+        if regen_mode not in ['deferred', 'explicit']:
             self._regen(propname)
 
     def regenerate_models(self, propnames=None, exclude=[], deep=False):
@@ -251,8 +282,9 @@ class ModelsMixin():
         if propnames is None:  # If no props given, then regenerate them all
             propnames = self.models.dependency_list()
             # If some props are to be excluded, remove them from list
-            if len(exclude) > 0:
-                propnames = [i for i in propnames if i not in exclude]
+            exclude.extend([k for k, v in self.models.items()
+                            if v['regen_mode'] == 'explicit'])
+            propnames = [i for i in propnames if i not in exclude]
         # Re-order given propnames according to dependency tree
         self_models = self.models.dependency_list()
         propnames = [i for i in self_models if i in propnames]
diff --git a/openpnm/models/__init__.py b/openpnm/models/__init__.py
index b3655e492..b7ae35d84 100644
--- a/openpnm/models/__init__.py
+++ b/openpnm/models/__init__.py
@@ -26,10 +26,6 @@ is demonstrated below:
 ...              model=op.models.misc.random,
 ...              element='pore',
 ...              num_range=[0.1, 0.9])
->>> pn.add_model(propname='pore.diameter',
-...              model=op.models.geometry.pore_size.normal,
-...              seeds='pore.seed',
-...              loc=0.5, scale=0.1)
 
 Upon being added to the object the models are run.  The resulting data is
 placed into the object using the ``propname`` as a key.  Inspecting the
@@ -38,9 +34,8 @@ object reveals that these two data items are indeed present.
 >>> print(pn.props())
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 1     : pore.coords
-2     : pore.diameter
-3     : pore.seed
-4     : throat.conns
+2     : pore.seed
+3     : throat.conns
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 
 The actual models and their respective parameters are also stored on the
@@ -51,18 +46,20 @@ as the ``propnames``.  This can also be inspected:
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
 #   Property Name             Parameter                 Value
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
-1   pore.seed                 model:                    random
+1   pore.coordination_number  model:                    coordination_number
+                              regeneration mode:        explicit
+――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+2   pore.seed                 model:                    random
                               element:                  pore
                               num_range:                [0.1, 0.9]
                               seed:                     None
                               regeneration mode:        normal
 ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
-2   pore.diameter             model:                    normal
-                              seeds:                    pore.seed
-                              loc:                      0.5
-                              scale:                    0.1
-                              regeneration mode:        normal
-――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+
+Note that the model 'pore.coordination_number' is added to all GenericNetworks
+upon instantiation, but not run.  This is why the model appears in the
+``models`` attribute but the data does not yet appear when the ``props`` method
+is called.
 
 Finally, any of the models parameters can be edited by reaching into the
 ``models`` dictionary as follows:
@@ -72,14 +69,14 @@ Finally, any of the models parameters can be edited by reaching into the
 The ``'pore.seed'`` values must be regenerated for this new parameter to take
 effect, and the ``'pore.diameter'`` model must also be regenerated to utilized
 the new seeds.  This is accomplished using ``regenerate_models`` which
-automatically ensures that models are called in the correct order (seeds before
-diameters):
+automatically ensures that models are called in the correct order.
 
 >>> pn.regenerate_models()
 
 """
 
 from . import misc
+from . import topology
 from . import geometry
 from . import phases
 from . import physics
diff --git a/openpnm/models/misc/__init__.py b/openpnm/models/misc/__init__.py
index 034ee8dc7..1b97469b1 100644
--- a/openpnm/models/misc/__init__.py
+++ b/openpnm/models/misc/__init__.py
@@ -13,5 +13,5 @@ generating an array of random numbers.
 from .statistical_distributions import generic_distribution, normal, weibull, random
 from .simple_equations import linear, polynomial, generic_function
 from .basic_math import constant, product, scaled, clip, normalize, summation
-from .basic_math import fraction
+from .basic_math import fraction, invert
 from .neighbor_lookups import from_neighbor_throats, from_neighbor_pores
diff --git a/openpnm/models/misc/basic_math.py b/openpnm/models/misc/basic_math.py
index ac9da674c..5c74b7fcf 100644
--- a/openpnm/models/misc/basic_math.py
+++ b/openpnm/models/misc/basic_math.py
@@ -12,6 +12,24 @@ from openpnm.utils import logging
 logger = logging.getLogger(__name__)
 
 
+def invert(target, prop):
+    r"""
+    Inverts the given array
+
+    Parameters
+    ----------
+    target : OpenPNM Object
+        The object which this model is associated with. This controls the
+        length of the calculated array, and also provides access to other
+        necessary properties.
+
+    prop : string
+        Dictionary key pointing the values to invert
+
+    """
+    return 1.0/target[prop]
+
+
 def fraction(target, numerator, denominator):
     r"""
     Calculates the ratio between two values
diff --git a/openpnm/models/topology/__init__.py b/openpnm/models/topology/__init__.py
new file mode 100644
index 000000000..c036560a4
--- /dev/null
+++ b/openpnm/models/topology/__init__.py
@@ -0,0 +1,12 @@
+r"""
+
+**openpnm.models.network**
+
+----
+
+This submodule contains models for calculating topological properties of
+networks
+
+"""
+
+from .topology import coordination_number
diff --git a/openpnm/models/topology/topology.py b/openpnm/models/topology/topology.py
new file mode 100644
index 000000000..e1d6f50c7
--- /dev/null
+++ b/openpnm/models/topology/topology.py
@@ -0,0 +1,16 @@
+r"""
+
+.. autofunction:: openpnm.models.topology.coordination_number
+
+"""
+from openpnm.utils import logging
+logger = logging.getLogger(__name__)
+
+
+def coordination_number(target):
+    r"""
+    """
+    network = target.network
+    N = network.num_neighbors(pores=network.Ps, flatten=False)
+    vals = N[network.pores(target.name)]
+    return vals
diff --git a/openpnm/network/GenericNetwork.py b/openpnm/network/GenericNetwork.py
index 99a2842c3..6f895010c 100644
--- a/openpnm/network/GenericNetwork.py
+++ b/openpnm/network/GenericNetwork.py
@@ -6,6 +6,7 @@ import scipy.sparse.csgraph as csg
 from openpnm.core import Base, ModelsMixin
 from openpnm import topotools
 from openpnm.utils import HealthDict, Workspace, logging
+import openpnm.models.topology as tm
 logger = logging.getLogger(__name__)
 ws = Workspace()
 
@@ -128,6 +129,9 @@ class GenericNetwork(Base, ModelsMixin):
         # Initialize adjacency and incidence matrix dictionaries
         self._im = {}
         self._am = {}
+        self.add_model(propname='pore.coordination_number',
+                       model=tm.coordination_number,
+                       regen_mode='explicit')
 
     def __setitem__(self, key, value):
         if key == 'throat.conns':
@@ -892,67 +896,6 @@ class GenericNetwork(Base, ModelsMixin):
         - This is just a 'check' and does not 'fix' the problems it finds
         """
 
-        health = HealthDict()
-        health['disconnected_clusters'] = []
-        health['isolated_pores'] = []
-        health['trim_pores'] = []
-        health['duplicate_throats'] = []
-        health['bidirectional_throats'] = []
-        health['headless_throats'] = []
-        health['looped_throats'] = []
-
-        # Check for headless throats
-        hits = sp.where(self['throat.conns'] > self.Np - 1)[0]
-        if sp.size(hits) > 0:
-            health['headless_throats'] = sp.unique(hits)
-            return health
-
-        # Check for throats that loop back onto the same pore
-        P12 = self['throat.conns']
-        hits = sp.where(P12[:, 0] == P12[:, 1])[0]
-        if sp.size(hits) > 0:
-            health['looped_throats'] = hits
-
-        # Check for individual isolated pores
-        Ps = self.num_neighbors(self.pores())
-        if sp.sum(Ps == 0) > 0:
-            health['isolated_pores'] = sp.where(Ps == 0)[0]
-
-        # Check for separated clusters of pores
-        temp = []
-        am = self.create_adjacency_matrix(fmt='coo', triu=True)
-        Cs = csg.connected_components(am, directed=False)[1]
-        if sp.unique(Cs).size > 1:
-            for i in sp.unique(Cs):
-                temp.append(sp.where(Cs == i)[0])
-            b = sp.array([len(item) for item in temp])
-            c = sp.argsort(b)[::-1]
-            for i in range(0, len(c)):
-                health['disconnected_clusters'].append(temp[c[i]])
-                if i > 0:
-                    health['trim_pores'].extend(temp[c[i]])
-
-        # Check for duplicate throats
-        am = self.create_adjacency_matrix(fmt='csr', triu=True).tocoo()
-        hits = sp.where(am.data > 1)[0]
-        if len(hits):
-            mergeTs = []
-            hits = sp.vstack((am.row[hits], am.col[hits])).T
-            ihits = hits[:, 0] + 1j*hits[:, 1]
-            conns = self['throat.conns']
-            iconns = conns[:, 0] + 1j*conns[:, 1]  # Convert to imaginary
-            for item in ihits:
-                mergeTs.append(sp.where(iconns == item)[0])
-            health['duplicate_throats'] = mergeTs
-
-        # Check for bidirectional throats
-        adjmat = self.create_adjacency_matrix(fmt='coo')
-        num_full = adjmat.sum()
-        temp = sprs.triu(adjmat, k=1)
-        num_upper = temp.sum()
-        if num_full > num_upper:
-            biTs = sp.where(self['throat.conns'][:, 0] >
-                            self['throat.conns'][:, 1])[0]
-            health['bidirectional_throats'] = biTs.tolist()
+        health = self.project.check_network_health()
 
         return health
diff --git a/openpnm/phases/mixtures/GenericMixture.py b/openpnm/phases/mixtures/GenericMixture.py
index b223ccf42..2e4e06734 100644
--- a/openpnm/phases/mixtures/GenericMixture.py
+++ b/openpnm/phases/mixtures/GenericMixture.py
@@ -170,9 +170,9 @@ class GenericMixture(GenericPhase):
             self[element+'.mole_fraction.'+component] = 1 - mol_frac
             # [self[element+'.concentration.'+component] = (1 - mol_frac)*density
         else:
-            raise Exception('Insufficient concentration values found ' +
-                            'for component species, must specify ' +
-                            str(abs(n_spec + 1)) + ' additional values')
+            raise Exception('Insufficient concentration values found for ' +
+                            'component species, cannot calculate mole ' +
+                            'fractions')
 
     def set_concentration(self, component, values=[]):
         r"""
diff --git a/openpnm/topotools/topotools.py b/openpnm/topotools/topotools.py
index c05143f44..5e24b1d10 100644
--- a/openpnm/topotools/topotools.py
+++ b/openpnm/topotools/topotools.py
@@ -989,8 +989,8 @@ def trim(network, pores=[], throats=[]):
     296
 
     '''
-    pores = sp.array(pores, ndmin=1)
-    throats = sp.array(throats, ndmin=1)
+    pores = network._parse_indices(pores)
+    throats = network._parse_indices(throats)
     Pkeep = sp.copy(network['pore.all'])
     Tkeep = sp.copy(network['throat.all'])
     if sp.size(pores) > 0:
@@ -1282,8 +1282,16 @@ def find_surface_pores(network, markers=None, label='surface'):
             z = r*sp.cos(phi)
             markers = sp.vstack((x, y, z)).T
     else:
-        pass
-        # markers = sp.atleast_2d(markers)
+        if sum(dims) == 1:
+            pass
+        if sum(dims) == 2:
+            markers = sp.atleast_2d(markers)
+            if markers.shape[1] != 2:
+                raise Exception('Network appears planar, so markers must be 2D')
+        if sum(dims) == 3:
+            markers = sp.atleast_2d(markers)
+            if markers.shape[1] != 3:
+                raise Exception('Markers must be 3D for this network')
     pts = sp.vstack((coords, markers))
     tri = sptl.Delaunay(pts, incremental=False)
     (indices, indptr) = tri.vertex_neighbor_vertices
@@ -1347,6 +1355,7 @@ def clone_pores(network, pores, labels=['clone'], mode='parents'):
 
     if type(labels) == str:
         labels = [labels]
+    network._parse_indices(pores)
     Np = network.Np
     Nt = network.Nt
     # Clone pores
@@ -2769,7 +2778,8 @@ def _bond_percolation(network, tmask):
     return (p_clusters, t_clusters)
 
 
-def add_boundary_pores(network, pores, offset, apply_label='boundary'):
+def add_boundary_pores(network, pores, offset=None, move_to=None,
+                       apply_label='boundary'):
     r"""
     This method uses ``clone_pores`` to clone the input pores, then shifts
     them the specified amount and direction, then applies the given label.
@@ -2782,7 +2792,14 @@ def add_boundary_pores(network, pores, offset, apply_label='boundary'):
 
     offset : 3 x 1 array
         The distance in vector form which the cloned boundary pores should
-        be offset.
+        be offset.  Either this, or ``move_to`` must be specified.
+
+    move_to : 3 x 1 array
+        The location to move the boundary pores to.  A value of ``None``
+        indicates that no translation should be applied in that axis.  For
+        instance, ``[None, None, 0]`` indicates that the boundary pores should
+        moved along the z-axis to the specified location.  Either this or
+        ``offset`` must be specified.
 
     apply_label : string
         This label is applied to the boundary pores.  Default is
@@ -2812,8 +2829,14 @@ def add_boundary_pores(network, pores, offset, apply_label='boundary'):
     del network['pore.clone']
     newTs = network.throats('clone')
     del network['throat.clone']
-    # Offset the cloned pores
-    network['pore.coords'][newPs] += offset
+    if offset is not None:  # Offset the cloned pores
+        network['pore.coords'][newPs] += offset
+    if move_to is not None:  # Move the cloned pores
+        for i, d in enumerate(move_to):
+            if d is not None:
+                temp = network['pore.coords'][newPs]
+                temp[:, i] = d
+                network['pore.coords'][newPs] = temp
     # Apply labels to boundary pores (trim leading 'pores' if present)
     label = apply_label.split('.')[-1]
     plabel = 'pore.' + label
diff --git a/openpnm/utils/Project.py b/openpnm/utils/Project.py
index 6bbff3eef..f9d3ee431 100644
--- a/openpnm/utils/Project.py
+++ b/openpnm/utils/Project.py
@@ -3,7 +3,8 @@ import h5py
 import numpy as np
 import openpnm
 from copy import deepcopy
-from openpnm.utils import SettingsDict, HealthDict, Workspace
+from openpnm.utils import SettingsDict, HealthDict, Workspace, logging
+logger = logging.getLogger(__name__)
 ws = Workspace()
 
 
@@ -764,6 +765,9 @@ class Project(list):
             health['undefined_pores'] = np.where(Ptemp == 0)[0].tolist()
             health['overlapping_throats'] = np.where(Ttemp > 1)[0].tolist()
             health['undefined_throats'] = np.where(Ttemp == 0)[0].tolist()
+        else:
+            health['undefined_pores'] = self.network.Ps
+            health['undefined_throats'] = self.network.Ts
         return health
 
     def check_physics_health(self, phase):
@@ -806,6 +810,132 @@ class Project(list):
             health['undefined_throats'] = np.where(Ttemp == 0)[0].tolist()
         return health
 
+    def check_data_health(self, obj):
+        r"""
+        Check the health of pore and throat data arrays.
+
+        Parameters
+        ----------
+        obj : OpenPNM object
+            A handle of the object to be checked
+
+        Returns
+        -------
+        health : dict
+            Returns a HealthDict object which a basic dictionary with an added
+            ``health`` attribute that is True is all entries in the dict are
+            deemed healthy (empty lists), or False otherwise.
+
+        """
+        health = HealthDict()
+        for item in obj.props():
+            health[item] = []
+            if obj[item].dtype == 'O':
+                health[item] = 'No checks on object'
+            elif np.sum(np.isnan(obj[item])) > 0:
+                health[item] = 'Has NaNs'
+            elif np.shape(obj[item])[0] != obj._count(item.split('.')[0]):
+                health[item] = 'Wrong Length'
+        return health
+
+    def check_network_health(self):
+        r"""
+        This method check the network topological health by checking for:
+
+            (1) Isolated pores
+            (2) Islands or isolated clusters of pores
+            (3) Duplicate throats
+            (4) Bidirectional throats (ie. symmetrical adjacency matrix)
+            (5) Headless throats
+
+        Returns
+        -------
+        health : dict
+            A dictionary containing the offending pores or throat numbers under
+            each named key.
+
+        Notes
+        -----
+        It also returns a list of which pores and throats should be trimmed
+        from the network to restore health.  This list is a suggestion only,
+        and is based on keeping the largest cluster and trimming the others.
+
+        Notes
+        -----
+        - Does not yet check for duplicate pores
+        - Does not yet suggest which throats to remove
+        - This is just a 'check' and does not 'fix' the problems it finds
+        """
+        import scipy.sparse.csgraph as csg
+        import scipy.sparse as sprs
+
+        health = HealthDict()
+        health['disconnected_clusters'] = []
+        health['isolated_pores'] = []
+        health['trim_pores'] = []
+        health['duplicate_throats'] = []
+        health['bidirectional_throats'] = []
+        health['headless_throats'] = []
+        health['looped_throats'] = []
+
+        net = self.network
+
+        # Check for headless throats
+        hits = np.where(net['throat.conns'] > net.Np - 1)[0]
+        if np.size(hits) > 0:
+            health['headless_throats'] = np.unique(hits)
+            return health
+
+        # Check for throats that loop back onto the same pore
+        P12 = net['throat.conns']
+        hits = np.where(P12[:, 0] == P12[:, 1])[0]
+        if np.size(hits) > 0:
+            health['looped_throats'] = hits
+
+        # Check for individual isolated pores
+        Ps = net.num_neighbors(net.pores())
+        if np.sum(Ps == 0) > 0:
+            health['isolated_pores'] = np.where(Ps == 0)[0]
+
+        # Check for separated clusters of pores
+        temp = []
+        am = net.create_adjacency_matrix(fmt='coo', triu=True)
+        Cs = csg.connected_components(am, directed=False)[1]
+        if np.unique(Cs).size > 1:
+            for i in np.unique(Cs):
+                temp.append(np.where(Cs == i)[0])
+            b = np.array([len(item) for item in temp])
+            c = np.argsort(b)[::-1]
+            for i in range(0, len(c)):
+                health['disconnected_clusters'].append(temp[c[i]])
+                if i > 0:
+                    health['trim_pores'].extend(temp[c[i]])
+
+        # Check for duplicate throats
+        am = net.create_adjacency_matrix(fmt='csr', triu=True).tocoo()
+        hits = np.where(am.data > 1)[0]
+        if len(hits):
+            mergeTs = []
+            hits = np.vstack((am.row[hits], am.col[hits])).T
+            ihits = hits[:, 0] + 1j*hits[:, 1]
+            conns = net['throat.conns']
+            iconns = conns[:, 0] + 1j*conns[:, 1]  # Convert to imaginary
+            for item in ihits:
+                mergeTs.append(np.where(iconns == item)[0])
+            health['duplicate_throats'] = mergeTs
+
+        # Check for bidirectional throats
+        adjmat = net.create_adjacency_matrix(fmt='coo')
+        num_full = adjmat.sum()
+        temp = sprs.triu(adjmat, k=1)
+        num_upper = temp.sum()
+        if num_full > num_upper:
+            biTs = np.where(net['throat.conns'][:, 0] >
+                            net['throat.conns'][:, 1])[0]
+            health['bidirectional_throats'] = biTs.tolist()
+
+        return health
+
     def _regenerate_models(self, objs=[], propnames=[]):
         r"""
         Can be used to regenerate models across all objects in the project.
diff --git a/readme.md b/readme.md
index d0055b94e..fe2ec698c 100644
--- a/readme.md
+++ b/readme.md
@@ -50,7 +50,7 @@ The drainage curve can be visualized with `MIP.plot_intrusion_curve()` giving so
 
 ![](https://i.imgur.com/1C2uXt9m.png)
 
-A collection of examples is available as a separate Github repository: [OpenPNM-Examples](https://www.github.com/PMEAL/OpenPNM-Examples)
+A collection of examples is available in the *examples* folder of this repository: [Examples](https://www.github.com/PMEAL/OpenPNM/tree/master/examples)
 
 ## Release Management and Versioning
 
diff --git a/setup.py b/setup.py
index e5fb2f10b..9d79fbb81 100644
--- a/setup.py
+++ b/setup.py
@@ -40,6 +40,7 @@ setup(
         'openpnm.utils',
         'openpnm.io',
         'openpnm.models',
+        'openpnm.models.topology',
         'openpnm.models.misc',
         'openpnm.models.geometry',
         'openpnm.models.phases',


Test patch:
diff --git a/tests/unit/models/misc/MiscTest.py b/tests/unit/models/misc/MiscTest.py
index 7a946a6f8..1d17e15ab 100644
--- a/tests/unit/models/misc/MiscTest.py
+++ b/tests/unit/models/misc/MiscTest.py
@@ -212,6 +212,15 @@ class MiscTest:
             test[i] = net['pore.rand2'][pore] == T_max
         assert sp.all(test)
 
+    def test_invert(self):
+        net = op.network.Cubic(shape=[5, 5, 5])
+        net['pore.diameter'] = 2.0
+        net.add_model(propname='pore.entry_pressure',
+                      prop='pore.diameter',
+                      model=mods.basic_math.invert)
+        assert net['pore.entry_pressure'][0] == 0.5
+
+
 if __name__ == '__main__':
 
     t = MiscTest()
diff --git a/tests/unit/topotools/TopotoolsTest.py b/tests/unit/topotools/TopotoolsTest.py
index 0ae5445f6..470862a93 100644
--- a/tests/unit/topotools/TopotoolsTest.py
+++ b/tests/unit/topotools/TopotoolsTest.py
@@ -2,6 +2,7 @@ import openpnm as op
 import numpy as np
 from numpy.testing import assert_allclose
 from openpnm import topotools
+import pytest
 
 
 class TopotoolsTest:
@@ -50,7 +51,7 @@ class TopotoolsTest:
         assert net.num_pores('top') == 9
         assert net.num_pores('bottom') == 9
 
-    def test_find_surface_pores(self):
+    def test_find_surface_pores_default_markers(self):
         from skimage.morphology import ball
         net = op.network.CubicTemplate(template=ball(3), spacing=1)
         net.clear(mode='labels')
@@ -58,6 +59,34 @@ class TopotoolsTest:
         topotools.find_surface_pores(network=net)
         assert net.num_pores('surface') == 66
 
+    def test_find_surface_pores_custom_markers_2d(self):
+        net = op.network.Cubic(shape=[4, 4, 1], spacing=1)
+        net.clear(mode='labels')
+        assert net.labels() == ['pore.all', 'throat.all']
+        markers = [[-1, 2], [2, -1], [2, 5], [5, 2]]
+        topotools.find_surface_pores(network=net, markers=markers)
+        assert net.num_pores('surface') == 12
+        markers = [[-1], [2], [2], [5]]
+        with pytest.raises(Exception):
+            topotools.find_surface_pores(network=net, markers=markers)
+        markers = [[-1, 2, 0], [2, -1, 0], [2, 5, 0], [5, 2, 0]]
+        with pytest.raises(Exception):
+            topotools.find_surface_pores(network=net, markers=markers)
+
+    def test_find_surface_pores_custom_markers_3d(self):
+        net = op.network.Cubic(shape=[4, 4, 4], spacing=1)
+        net.clear(mode='labels')
+        assert net.labels() == ['pore.all', 'throat.all']
+        markers = [[-1, 2, 2], [2, -1, 2], [2, 5, 2], [5, 2, 2]]
+        topotools.find_surface_pores(network=net, markers=markers)
+        assert net.num_pores('surface') == 48
+        markers = [[-1], [2], [2], [5]]
+        with pytest.raises(Exception):
+            topotools.find_surface_pores(network=net, markers=markers)
+        markers = [[-1, 2], [2, -1], [2, 5], [5, 2]]
+        with pytest.raises(Exception):
+            topotools.find_surface_pores(network=net, markers=markers)
+
     def test_find_pore_to_pore_distance(self):
         net = op.network.Cubic(shape=[3, 3, 3], connectivity=6)
         dm = topotools.find_pore_to_pore_distance(network=net,