Repository: PMEAL/OpenPNM
Pull request #671 (2017-01-22 03:11:27)

Network import returns a Geometry
Adjusting the IO classes to return a Geometry along with the Network


Patch:
diff --git a/OpenPNM/Utilities/IO.py b/OpenPNM/Utilities/IO.py
index 5c6c1775c..c4b6753ef 100644
--- a/OpenPNM/Utilities/IO.py
+++ b/OpenPNM/Utilities/IO.py
@@ -12,7 +12,110 @@ logger = logging.getLogger(__name__)
 mgr = OpenPNM.Base.Workspace()
 
 
-class VTK():
+class GenericIO():
+
+    @classmethod
+    def save(cls):
+        raise NotImplementedError("The \'save\' method for this class " +
+                                  "does not exist yet")
+
+    @classmethod
+    def load(cls):
+        raise NotImplementedError("The \'load\' method for this class " +
+                                  "does not exist yet")
+
+    @staticmethod
+    def split_geometry(network):
+        r"""
+        This method accepts an OpenPNM Network object and removes all geometry
+        related pore and throat properties, (basically all values other than
+        ```'pore.coords'``` and ```throat.conns```), and places them on a
+        GenericGeometry object.  Any labels on the Network are left intact.
+
+        Parameters
+        ----------
+        network : OpenPNM Network Object
+            The Network that possesses the geometrical values
+
+        Returns
+        -------
+        geometry : OpenPNM Geometry Object
+            The new GenericGeometry object that was created to contain the
+            geometrical pore and throat properties.
+
+        """
+        geom = OpenPNM.Geometry.GenericGeometry(network=network,
+                                                pores=network.Ps,
+                                                throats=network.Ts)
+        for item in network.props():
+            if item not in ['pore.coords', 'throat.conns']:
+                geom.update({item: network.pop(item)})
+        return geom
+
+    @classmethod
+    def _update_network(cls, network, net, return_geometry=False):
+        # Infer Np and Nt from length of given prop arrays in file
+        for element in ['pore', 'throat']:
+            N = [_sp.shape(net[i])[0] for i in net.keys() if i.startswith(element)]
+            if N:
+                N = _sp.array(N)
+                if _sp.all(N == N[0]):
+                    if (network._count(element) == N[0]) \
+                            or (network._count(element) == 0):
+                        network.update({element+'.all': _sp.ones((N[0],),
+                                                                 dtype=bool)})
+                        net.pop(element+'.all', None)
+                    else:
+                        raise Exception('Length of '+element+' data in file' +
+                                        ' does not match network')
+                else:
+                    raise Exception(element+' data in file have inconsistent' +
+                                    ' lengths')
+        # Add data on dummy net to actual network
+        for item in net.keys():
+            # Try to infer array types and change if necessary
+            # Chcek for booleans disguised and 1's and 0's
+            num0s = _sp.sum(net[item] == 0)
+            num1s = _sp.sum(net[item] == 1)
+            if (num1s + num0s) == _sp.shape(net[item])[0]:
+                net[item] = net[item].astype(bool)
+            # Write data to network object
+            if item not in network:
+                network.update({item: net[item]})
+            else:
+                logger.warning('\''+item+'\' already present')
+        if return_geometry:
+            geometry = cls.split_geometry(network)
+            network = (network, geometry)
+        return network
+
+    @classmethod
+    def _write_file(cls, filename, ext):
+        ext = ext.replace('.', '').lower()
+        if ext not in ['csv', 'yaml', 'mat', 'vtp', 'dat']:
+            raise Exception(ext+' is not a supported file extension')
+        filename = filename.rstrip('.'+ext)
+        filename = filename+'.'+ext
+        try:
+            logger.warning(filename+' already exists, contents will be ' +
+                           'overwritten')
+            f = open(filename, mode='w')
+        except:
+            f = open(filename, mode='x')
+        return f
+
+    @classmethod
+    def _read_file(cls, filename, ext):
+        ext = ext.replace('.', '').lower()
+        if ext not in ['csv', 'yaml', 'mat', 'vtp', 'dat']:
+            raise Exception(ext+' is not a supported file extension')
+        if not filename.endswith('.'+ext):
+            filename = filename+'.'+ext
+        f = open(filename, mode='r')
+        return f
+
+
+class VTK(GenericIO):
     r"""
     Class for writing a Vtp file to be read by ParaView
 
@@ -36,8 +139,8 @@ class VTK():
     </VTKFile>
     '''.strip()
 
-    @staticmethod
-    def save(network, filename='', phases=[], legacy=True):
+    @classmethod
+    def save(cls, network, filename='', phases=[], legacy=True):
         r"""
         Save network and phase data to a single vtp file for visualizing in
         Paraview
@@ -138,8 +241,8 @@ class VTK():
             # consider adding header: '<?xml version="1.0"?>\n'+
             f.write(string)
 
-    @staticmethod
-    def load(filename, network=None):
+    @classmethod
+    def load(cls, filename, network=None, return_geometry=False):
         r"""
         Read in pore and throat data from a saved VTK file.
 
@@ -153,9 +256,21 @@ class VTK():
             The Network object onto which the data should be loaded.  If no
             Network is supplied than one will be created and returned.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
         Returns
         -------
         If no Network object is supplied then one will be created and returned.
+
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
         """
         net = {}
 
@@ -189,7 +304,8 @@ class VTK():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
         return network
 
     @staticmethod
@@ -225,7 +341,7 @@ class VTK():
         return array
 
 
-class Statoil():
+class Statoil(GenericIO):
     r"""
     This class is for loading data stored in the 'Statoil' file format.  More
     specifically, this file format is used by the network extraction code of
@@ -238,8 +354,9 @@ class Statoil():
     specific property.  Headers are not provided in the files, so one must
     refer to various theses and documents to interpret their meaning.
     """
-    @staticmethod
-    def load(path, prefix, network=None):
+
+    @classmethod
+    def load(cls, path, prefix, network=None, return_geometry=False):
         r"""
         Load data from the \'dat\' files located in specified folder.
 
@@ -256,6 +373,22 @@ class Statoil():
             If given then the data will be loaded on it and returned.  If not
             given, a Network will be created and return.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
+        Returns
+        -------
+        If no Network object is supplied then one will be created and returned.
+
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
+
         """
         net = {}
 
@@ -263,7 +396,7 @@ class Statoil():
         # Parse the link1 file
         for item in ['link1']:
             filename = _os.path.join(path, prefix+'_'+item+'.dat')
-            with _read_file(filename=filename, ext='dat') as f:
+            with cls._read_file(filename=filename, ext='dat') as f:
                 link1 = _pd.read_table(filepath_or_buffer=f,
                                        header=None,
                                        skiprows=1,
@@ -283,7 +416,7 @@ class Statoil():
         # Parse the link2 file
         for item in ['link2']:
             filename = _os.path.join(path, prefix+'_'+item+'.dat')
-            with _read_file(filename=filename, ext='dat') as f:
+            with cls._read_file(filename=filename, ext='dat') as f:
                 link2 = _pd.read_table(filepath_or_buffer=f,
                                        header=None,
                                        sep=' ',
@@ -301,7 +434,7 @@ class Statoil():
         # Parse the node1 file
         for item in ['node1']:
             filename = _os.path.join(path, prefix+'_'+item+'.dat')
-            with _read_file(filename=filename, ext='dat') as f:
+            with cls._read_file(filename=filename, ext='dat') as f:
                 row_0 = f.readline().split()
                 num_lines = int(row_0[0])
                 array = _sp.ndarray([num_lines, 6])
@@ -320,7 +453,7 @@ class Statoil():
         # Parse the node1 file
         for item in ['node2']:
             filename = _os.path.join(path, prefix+'_'+item+'.dat')
-            with _read_file(filename=filename, ext='dat') as f:
+            with cls._read_file(filename=filename, ext='dat') as f:
                 node2 = _pd.read_table(filepath_or_buffer=f,
                                        header=None,
                                        sep=' ',
@@ -336,7 +469,8 @@ class Statoil():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
 
         # Use OpenPNM Tools to clean up network
         # Trim throats connected to 'inlet' or 'outlet' reservoirs
@@ -357,7 +491,7 @@ class Statoil():
         return network
 
 
-class MAT():
+class MAT(GenericIO):
     r"""
     Class for reading and writing OpenPNM data to a Matlab 'mat' file
 
@@ -376,8 +510,8 @@ class MAT():
     OpenPNM.
     """
 
-    @staticmethod
-    def save(network, filename='', phases=[]):
+    @classmethod
+    def save(cls, network, filename='', phases=[]):
         r"""
         Write Network to a Mat file for exporting to Matlab.
 
@@ -409,8 +543,8 @@ class MAT():
 
         _sp.io.savemat(file_name=filename, mdict=pnMatlab)
 
-    @staticmethod
-    def load(filename, network=None):
+    @classmethod
+    def load(cls, filename, network=None, return_geometry=False):
         r"""
         Loads data onto the given network from an appropriately formatted
         'mat' file (i.e. MatLAB output).
@@ -425,10 +559,22 @@ class MAT():
             The Network object onto which the data should be loaded.  If no
             Network is supplied than one will be created and returned.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
         Returns
         -------
         If no Network object is supplied then one will be created and returned.
 
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
+
         """
         net = {}
 
@@ -459,7 +605,8 @@ class MAT():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
         return network
 
 
@@ -538,7 +685,7 @@ class Pandas():
         return data
 
 
-class CSV():
+class CSV(GenericIO):
     r"""
     This class is used for reading and writing CSV files containing pore and
     throat property data.  This class uses Pandas for transferring data from
@@ -567,8 +714,8 @@ class CSV():
     indicates where the label applies and FALSE otherwise.
     """
 
-    @staticmethod
-    def save(network, filename='', phases=[]):
+    @classmethod
+    def save(cls, network, filename='', phases=[]):
         r"""
         Save all the pore and throat property data on the Network (and
         optionally on any Phases objects) to CSV files.
@@ -601,11 +748,11 @@ class CSV():
         # Write to file
         if filename == '':
             filename = network.name
-        with _write_file(filename=filename, ext='csv') as f:
+        with cls._write_file(filename=filename, ext='csv') as f:
             b.to_csv(f, index=False)
 
-    @staticmethod
-    def load(filename, network=None):
+    @classmethod
+    def load(cls, filename, network=None, return_geometry=False):
         r"""
         Opens a 'csv' file, reads in the data, and adds it to the **Network**
 
@@ -615,14 +762,31 @@ class CSV():
             The name of the file containing the data to import.  The formatting
             of this file is outlined below.
 
+        network : OpenPNM Network Object
+            The Network object onto which the data should be loaded.  If no
+            Network is supplied than one will be created and returned.
+
+
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
         Returns
         -------
         If no Network object is supplied then one will be created and returned.
 
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
+
         """
         net = {}
 
-        with _read_file(filename=filename, ext='csv') as f:
+        with cls._read_file(filename=filename, ext='csv') as f:
             a = _pd.read_table(filepath_or_buffer=f,
                                sep=',',
                                skipinitialspace=True,
@@ -653,11 +817,12 @@ class CSV():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
         return network
 
 
-class NetworkX():
+class NetworkX(GenericIO):
     r"""
     This class is meant specifcally for exchanging data with NetworkX, which
     is a common tool for dealing with network structures.  A network object
@@ -688,13 +853,8 @@ class NetworkX():
     and is extracted by OpenPNM.
     """
 
-    @staticmethod
-    def save():
-        # TODO: This would be a great place for a new developer to contribute
-        raise NotImplementedError
-
-    @staticmethod
-    def load(filename, network=None):
+    @classmethod
+    def load(cls, filename, network=None, return_geometry=False):
         r"""
         Add data to an OpenPNM Network from a NetworkX generated YAML file.
 
@@ -708,15 +868,27 @@ class NetworkX():
             Network is supplied then an empty Import Network is created and
             returned.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
         Returns
         -------
         If no Network object is supplied then one will be created and returned.
 
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
+
         """
         net = {}
 
         # Open file and read first line, to prevent NetworkX instantiation
-        with _read_file(filename=filename, ext='yaml') as f:
+        with cls._read_file(filename=filename, ext='yaml') as f:
             line = f.readline()
             if line.startswith('!!python/object:networkx.classes.graph.Graph'):
                 a = _yaml.safe_load(f)
@@ -782,31 +954,23 @@ class NetworkX():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
         return network
 
 
-class iMorph():
+class iMorph(GenericIO):
     r"""
     Combines two output files from the iMorph program to build a pore network.
-    throats_cellsThroatsGraph_Nodes.txt - stores nodes shape and type information
+    throats_cellsThroatsGraph_Nodes.txt - stores node shape and type information
     throats_cellsThroatsGraph.txt - stores node connectivity
     """
 
-    @staticmethod
-    def save():
-        r"""
-        iMorph requires several additional files generation from processing
-        the image making it impossible to truely save a network in this format.
-        """
-        raise NotImplementedError('Not a valid output format')
-
-    @staticmethod
-    def load(path,
+    @classmethod
+    def load(cls, path,
              node_file="throats_cellsThroatsGraph_Nodes.txt",
              graph_file="throats_cellsThroatsGraph.txt",
-             network=None,
-             voxel_size=None):
+             network=None, voxel_size=None, return_geometry=False):
         r"""
         Loads network data from an iMorph processed image stack
 
@@ -814,23 +978,39 @@ class iMorph():
         ----------
         path : string
             The path of the folder where the subfiles are held
+
         node_file : string
             The file that describes the pores and throats, the
             default iMorph name is: throats_cellsThroatsGraph_Nodes.txt
+
         graph_file : string
             The file that describes the connectivity of the network, the
             default iMorph name is: throats_cellsThroatsGraph.txt
+
         network : OpenPNM Network Object
             The OpenPNM Network onto which the data should be loaded.  If no
             network is supplied then an empty import network is created and
             returned.
+
         voxel_size : float
             Allows the user to define a voxel size different than what is
             contained in the node_file. The value must be in meters.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
         Returns
         -------
         If no Network object is supplied then one will be created and returned.
+
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
         """
         #
         node_file = _os.path.join(path, node_file)
@@ -855,14 +1035,14 @@ class iMorph():
                 if 'pore.'+vals[2] not in network.labels():
                     network['pore.'+vals[2]] = False
                 network['pore.'+vals[2]][int(vals[0])] = True
-        #
+
         if voxel_size is None:
             voxel_size = vox_size * 1.0E-6  # file stores value in microns
 
         if voxel_size < 0:
             raise(Exception('Error - Voxel size must be specfied in ' +
                             'the Nodes file or as a keyword argument.'))
-        #
+
         # parsing the graph file
         with open(graph_file, 'r') as file:
             # Define expected properties
@@ -894,7 +1074,7 @@ class iMorph():
                 line = file.readline()
             # Scan file to get to connectivity data
             scrap_lines.append(file.readline())  # Skip line
-            # Create sparse lil array for incremental constrution of adjacency matrix
+            # Create sparse lil array incrementally build adjacency matrix
             lil = _sp.sparse.lil_matrix((Np, Np), dtype=int)
             while True:
                 vals = _sp.fromstring(file.readline(), sep='\t', dtype=int)
@@ -902,17 +1082,17 @@ class iMorph():
                     break
                 lil.rows[vals[0]] = vals[2:]
                 lil.data[vals[0]] = _sp.ones(vals[1])
-        #
+
         # fixing any negative volumes or distances so they are 1 voxel/micron
         network['pore.volume'][_sp.where(network['pore.volume'] < 0)[0]] = 1.0
         network['pore.radius'][_sp.where(network['pore.radius'] < 0)[0]] = 1.0
         network['pore.dmax'][_sp.where(network['pore.dmax'] < 0)[0]] = 1.0
-        #
+
         # Add adjacency matrix to OpenPNM network
         conns = _sp.sparse.triu(lil, k=1, format='coo')
         network.update({'throat.all': _sp.ones(len(conns.col), dtype=bool)})
         network['throat.conns'] = _sp.vstack([conns.row, conns.col]).T
-        #
+
         network['pore.to_trim'] = False
         network['pore.to_trim'][network.pores('*throat')] = True
         Ts = network.pores('to_trim')
@@ -924,7 +1104,7 @@ class iMorph():
             network['throat.'+item][network.throats('new_conns')] = \
                 network['pore.'+item][Ts]
         network.trim(pores=Ts)
-        #
+
         # setting up boundary pores
         x_coord, y_coord, z_coord = _sp.hsplit(network['pore.coords'], 3)
         network['pore.front_boundary'] = _sp.ravel(x_coord == 0)
@@ -933,7 +1113,7 @@ class iMorph():
         network['pore.right_boundary'] = _sp.ravel(y_coord == ymax)
         network['pore.bottom_boundary'] = _sp.ravel(z_coord == 0)
         network['pore.top_boundary'] = _sp.ravel(z_coord == zmax)
-        #
+
         # removing any pores that got classified as a boundary pore but
         # weren't labled a border_cell_face
         ps = _sp.where(~_sp.in1d(network.pores('*_boundary'),
@@ -944,7 +1124,7 @@ class iMorph():
         # setting internal label
         network['pore.internal'] = False
         network['pore.internal'][network.pores('*_boundary', mode='not')] = True
-        #
+
         # adding props to border cell face throats and from pores
         Ts = _sp.where(network['throat.conns'][:, 1] >
                        network.pores('border_cell_face')[0] - 1)[0]
@@ -953,7 +1133,7 @@ class iMorph():
             item = item.split('.')[1]
             network['throat.'+item][Ts] = network['pore.'+item][faces]
         network['pore.volume'][faces] = 0.0
-        #
+
         # applying unit conversions
         # TODO: Determine if radius and dmax are indeed microns and not voxels
         network['pore.coords'] = network['pore.coords'] * 1e-6
@@ -968,10 +1148,13 @@ class iMorph():
         # checking network health to generate warnings for the user
         network.health_dict = network.check_network_health()
         logger.info('Network health stored as network.health_dict')
+        if return_geometry:
+            geometry = cls.fetch_geometry(network)
+            network = (network, geometry)
         return network
 
 
-class MARock():
+class MARock(GenericIO):
     r"""
     3DMA-Rock is a network extraction algorithm developed by Brent Lindquist
     and his group [1].  It uses Medial Axis thinning to find the skeleton of
@@ -983,15 +1166,9 @@ class MARock():
     of rock pore structure in 3-D computed microtomography images." SUNY Stony
     Brook (2005).
     """
-    @staticmethod
-    def save():
-        r"""
-        TODO: This might be useful for code that uses 3DMA-Rock files
-        """
-        raise NotImplemented()
 
-    @staticmethod
-    def load(path, network=None, voxel_size=1):
+    @classmethod
+    def load(cls, path, network=None, voxel_size=1, return_geometry=False):
         r"""
         Load data from a 3DMA-Rock extracted network.  This format consists of
         two files: 'rockname.np2th' and 'rockname.th2pn'.  They should be
@@ -1015,6 +1192,22 @@ class MARock():
             scale the voxel counts to actual dimension. It is recommended that
             this value be in SI units [m] to work well with OpenPNM.
 
+        return_geometry : Boolean
+            If True, then all geometrical related properties are removed from
+            the Network object and added to a GenericGeometry object.  In this
+            case the method returns a tuple containing (network, geometry). If
+            False (default) then the returned Network will contain all
+            properties that were in the original file.  In this case, the user
+            can call the ```split_geometry``` method explicitly to perform the
+            separation.
+
+        Returns
+        -------
+        If no Network object is supplied then one will be created and returned.
+
+        If return_geometry is True, then a tuple is returned containing both
+        the network and a geometry object.
+
         """
 
         net = {}
@@ -1080,70 +1273,11 @@ class MARock():
 
         if network is None:
             network = OpenPNM.Network.GenericNetwork()
-        network = _update_network(network=network, net=net)
+        network = cls._update_network(network=network, net=net,
+                                      return_geometry=return_geometry)
 
         # Trim headless throats before returning
         ind = _sp.where(network['throat.conns'][:, 0] == -1)[0]
         network.trim(throats=ind)
 
         return network
-
-
-def _update_network(network, net):
-    # Infer Np and Nt from length of given prop arrays in file
-    for element in ['pore', 'throat']:
-        N = [_sp.shape(net[i])[0] for i in net.keys() if i.startswith(element)]
-        if N:
-            N = _sp.array(N)
-            if _sp.all(N == N[0]):
-                if (network._count(element) == N[0]) \
-                        or (network._count(element) == 0):
-                    network.update({element+'.all': _sp.ones((N[0],),
-                                                             dtype=bool)})
-                    net.pop(element+'.all', None)
-                else:
-                    raise Exception('Length of '+element+' data in file ' +
-                                    'does not match network')
-            else:
-                raise Exception(element+' data in file have inconsistent ' +
-                                'lengths')
-
-    # Add data on dummy net to actual network
-    for item in net.keys():
-        # Try to infer array types and change if necessary
-        # Chcek for booleans disguised and 1's and 0's
-        num0s = _sp.sum(net[item] == 0)
-        num1s = _sp.sum(net[item] == 1)
-        if (num1s + num0s) == _sp.shape(net[item])[0]:
-            net[item] = net[item].astype(bool)
-        # Write data to network object
-        if item not in network:
-            network.update({item: net[item]})
-        else:
-            logger.warning('\''+item+'\' already present')
-    return network
-
-
-def _write_file(filename, ext):
-    ext = ext.replace('.', '').lower()
-    if ext not in ['csv', 'yaml', 'mat', 'vtp', 'dat']:
-        raise Exception(ext+' is not a supported file extension')
-    filename = filename.rstrip('.'+ext)
-    filename = filename+'.'+ext
-    try:
-        logger.warning(filename+' already exists, contents will be ' +
-                       'overwritten')
-        f = open(filename, mode='w')
-    except:
-        f = open(filename, mode='x')
-    return f
-
-
-def _read_file(filename, ext):
-    ext = ext.replace('.', '').lower()
-    if ext not in ['csv', 'yaml', 'mat', 'vtp', 'dat']:
-        raise Exception(ext+' is not a supported file extension')
-    if not filename.endswith('.'+ext):
-        filename = filename+'.'+ext
-    f = open(filename, mode='r')
-    return f


Test patch:
diff --git a/test/unit/Utilities/IOTest.py b/test/unit/Utilities/IOTest.py
index dec82b0a6..f615b4e35 100644
--- a/test/unit/Utilities/IOTest.py
+++ b/test/unit/Utilities/IOTest.py
@@ -2,6 +2,7 @@ import OpenPNM as op
 import scipy as sp
 import os
 import OpenPNM.Utilities.IO as io
+import pytest
 
 
 class IOTest:
@@ -16,6 +17,28 @@ class IOTest:
                                            pores=self.net.Ps,
                                            throats=self.net.Ts)
 
+    def test_generic_load(self):
+        with pytest.raises(NotImplementedError):
+            op.Utilities.IO.GenericIO.load()
+
+    def test_generic_save(self):
+        with pytest.raises(NotImplementedError):
+            op.Utilities.IO.GenericIO.save()
+
+    def test_generic_split_geometry(self):
+        fname = os.path.join(FIXTURE_DIR, 'test_load_csv_no_phases')
+        net = io.CSV.load(fname+'.csv')
+        Nprops = len(net.props())
+        geom = io.CSV.split_geometry(network=net)
+        assert len(net.props()) == 2
+        assert len(geom.props()) == (Nprops - 2)
+
+    def test_generic_return_geometry(self):
+        fname = os.path.join(FIXTURE_DIR, 'test_load_csv_no_phases')
+        net = io.CSV.load(fname+'.csv', return_geometry=True)
+        assert len(net) == 2
+        assert type(net) == tuple
+
     def test_load_statoil(self):
         path = os.path.join(FIXTURE_DIR, 'ICL-SandPack(F42A)')
         net = io.Statoil.load(path=path, prefix='F42A')