Repository: PMEAL/OpenPNM

Issue:
Make main arguments mandatory for main objects
I think we should make main arguments, namely `network`, `geometry`, `phase`, `physics` (where applicable), mandatory for main objects. Here's what I propose:

- `network`: looks good as is
- `geometry`: `network`
- `phase`: `network`
- `physics`: `network`, `geometry`, `phase` (maybe even just `phase` and `geometry`?)
- `algorithm`: `network`, `phase` (or just `phase`?)


Discussion:

Patch:
diff --git a/example.py b/example.py
index c781347fe..d3e8746af 100644
--- a/example.py
+++ b/example.py
@@ -5,11 +5,10 @@ from openpnm.models.physics import source_terms
 # %% Initialization: create Workspace and project objects.
 ws = op.Workspace()
 ws.settings.loglevel = 50
-proj = ws.new_project()
 np.random.seed(9)
 
 # %% Create network, geometry, phase, and physics objects
-pn = op.network.Cubic(shape=[10, 10, 10], spacing=1e-4, project=proj)
+pn = op.network.Cubic(shape=[10, 10, 10], spacing=1e-4)
 geo = op.geometry.SpheresAndCylinders(network=pn, pores=pn.Ps, throats=pn.Ts)
 air = op.phases.Air(network=pn, name='air')
 water = op.phases.Water(network=pn, name='h2o')
@@ -33,7 +32,7 @@ sf.set_value_BC(pores=pn.pores('left'), values=101325)
 sf.run()
 water.update(sf.results())
 # calculate absolute permeability in x direction
-perm = op.metrics.AbsolutePermeability(network=pn, project=proj)
+perm = op.metrics.AbsolutePermeability(network=pn)
 K = perm.run()
 # K 7.51015925e-13
 # %% Perform reaction-diffusion simulation
@@ -60,8 +59,8 @@ fd.set_value_BC(pores=pn.pores('left'), values=1)
 fd.set_value_BC(pores=pn.pores('right'), values=0)
 fd.run()
 # calculate formation factor in x direction
-FF = op.metrics.FormationFactor(network=pn, project=proj)
+FF = op.metrics.FormationFactor(network=pn)
 F = FF.run()
 # F 20.53387084881872
 # %% Output network and the phases to a VTP file for visualization in Paraview
-proj.export_data(phases=[hg, air, water], filename='output.vtp')
+pn.project.export_data(phases=[hg, air, water], filename='output.vtp')
diff --git a/openpnm/algorithms/_generic_algorithm.py b/openpnm/algorithms/_generic_algorithm.py
index 0dc1908ab..3a6bf1a7c 100644
--- a/openpnm/algorithms/_generic_algorithm.py
+++ b/openpnm/algorithms/_generic_algorithm.py
@@ -1,5 +1,5 @@
 import numpy as np
-from openpnm.core import Base, LegacyMixin, LabelMixin
+from openpnm.core import Base, LegacyMixin
 from openpnm.utils import logging, Docorator, SettingsAttr
 logger = logging.getLogger(__name__)
 docstr = Docorator()
@@ -36,13 +36,11 @@ class GenericAlgorithm(Base, LegacyMixin):
 
     """
 
-    def __init__(self, settings=None, **kwargs):
+    def __init__(self, network, settings=None, **kwargs):
         self.settings = SettingsAttr(GenericAlgorithmSettings, settings)
-        super().__init__(settings=self.settings, **kwargs)
-
-        if self.project:
-            self['pore.all'] = np.ones(self.project.network.Np, dtype=bool)
-            self['throat.all'] = np.ones(self.project.network.Nt, dtype=bool)
+        super().__init__(network=network, settings=self.settings, **kwargs)
+        self['pore.all'] = np.ones(network.Np, dtype=bool)
+        self['throat.all'] = np.ones(network.Nt, dtype=bool)
 
     def results(self):
         r"""
diff --git a/openpnm/geometry/_generic.py b/openpnm/geometry/_generic.py
index d105ff3ba..f73c0a90b 100644
--- a/openpnm/geometry/_generic.py
+++ b/openpnm/geometry/_generic.py
@@ -68,20 +68,17 @@ class GenericGeometry(ParamMixin, Subdomain, ModelsMixin):
 
     """
 
-    def __init__(self, pores=[], throats=[], settings=None, **kwargs):
+    def __init__(self, network, pores=[], throats=[], settings=None, **kwargs):
         self.settings = SettingsAttr(GeometrySettings, settings)
-        super().__init__(settings=self.settings, **kwargs)
-
-        network = self.project.network
-        if network:
-            network[f'pore.{self.name}'] = False
-            network[f'throat.{self.name}'] = False
-            try:
-                self.set_locations(pores=pores, throats=throats, mode='add')
-            except Exception as e:
-                logger.error(f'{e}, instantiation cancelled')
-                network.project.purge_object(self)
-                raise e
+        super().__init__(network=network, settings=self.settings, **kwargs)
+        network[f'pore.{self.name}'] = False
+        network[f'throat.{self.name}'] = False
+        try:
+            self.set_locations(pores=pores, throats=throats, mode='add')
+        except Exception as e:
+            logger.error(f'{e}, instantiation cancelled')
+            network.project.purge_object(self)
+            raise e
 
     def _get_phys(self):
         """A shortcut to get a handle to the associated physics."""
diff --git a/openpnm/phases/_generic.py b/openpnm/phases/_generic.py
index a9c5537f1..d69ef8206 100644
--- a/openpnm/phases/_generic.py
+++ b/openpnm/phases/_generic.py
@@ -35,15 +35,13 @@ class GenericPhase(ParamMixin, Base, ModelsMixin, LabelMixin):
 
     """
 
-    def __init__(self, settings=None, **kwargs):
+    def __init__(self, network, settings=None, **kwargs):
         self.settings = SettingsAttr(PhaseSettings, settings)
-        super().__init__(settings=self.settings, **kwargs)
+        super().__init__(network=network, settings=self.settings, **kwargs)
 
         # If project has a network object, adjust pore and throat array sizes
-        network = self.project.network
-        if network:
-            self['pore.all'] = ones((network.Np, ), dtype=bool)
-            self['throat.all'] = ones((network.Nt, ), dtype=bool)
+        self['pore.all'] = ones((network.Np, ), dtype=bool)
+        self['throat.all'] = ones((network.Nt, ), dtype=bool)
 
         # Set standard conditions on the fluid to get started
         self['pore.temperature'] = 298.0
diff --git a/openpnm/physics/_generic.py b/openpnm/physics/_generic.py
index 8f8627356..8e7fae1fb 100644
--- a/openpnm/physics/_generic.py
+++ b/openpnm/physics/_generic.py
@@ -38,27 +38,25 @@ class GenericPhysics(ParamMixin, Subdomain, ModelsMixin):
 
     """
 
-    def __init__(self, phase=None, geometry=None, pores=None, throats=None,
+    def __init__(self, network, phase=None, geometry=None, pores=None, throats=None,
                  settings=None, **kwargs):
         self.settings = SettingsAttr(PhysicsSettings, settings)
-        super().__init__(settings=self.settings, **kwargs)
-
-        network = self.project.network
-        if network:
-            if phase is not None:
-                self.set_phase(phase=phase, mode='add')
-            if geometry is None:
-                if (pores is None) and (throats is None):
-                    logger.warning('No Geometry provided, ' + self.name
-                                   + ' will not be associated with any locations')
-                else:
-                    self.set_locations(pores=pores, throats=throats, mode='add')
+        super().__init__(network=network, settings=self.settings, **kwargs)
+
+        if phase is not None:
+            self.set_phase(phase=phase, mode='add')
+        if geometry is None:
+            if (pores is None) and (throats is None):
+                logger.warning('No Geometry provided, ' + self.name
+                               + ' will not be associated with any locations')
+            else:
+                self.set_locations(pores=pores, throats=throats, mode='add')
+        else:
+            if phase is None:
+                logger.warning('Cannot associate with a geometry unless '
+                               + 'a phase is also given')
             else:
-                if phase is None:
-                    logger.warning('Cannot associate with a geometry unless '
-                                   + 'a phase is also given')
-                else:
-                    self.set_geometry(geometry=geometry)
+                self.set_geometry(geometry=geometry)
 
     def _set_phase(self, phase):
         if phase is None:
diff --git a/openpnm/utils/_project.py b/openpnm/utils/_project.py
index a61c35e23..59e736312 100644
--- a/openpnm/utils/_project.py
+++ b/openpnm/utils/_project.py
@@ -522,14 +522,14 @@ class Project(list):
         if objtype.startswith('net'):
             obj = openpnm.network.GenericNetwork(project=self, name=name)
         elif objtype.startswith('geo'):
-            obj = openpnm.geometry.GenericGeometry(project=self, name=name,
+            obj = openpnm.geometry.GenericGeometry(network=self.network, name=name,
                                                    pores=[], throats=[])
         elif objtype.startswith('pha'):
-            obj = openpnm.phases.GenericPhase(project=self, name=name)
+            obj = openpnm.phases.GenericPhase(network=self.network, name=name)
         elif objtype.startswith('phy'):
-            obj = openpnm.physics.GenericPhysics(project=self, name=name)
+            obj = openpnm.physics.GenericPhysics(network=self.network, name=name)
         elif objtype.startswith('alg'):
-            obj = openpnm.algorithms.GenericAlgorithm(project=self, name=name)
+            obj = openpnm.algorithms.GenericAlgorithm(network=self.network, name=name)
         else:
             obj = openpnm.core.Base(project=self, name=name)
         return obj


Test patch:
diff --git a/tests/unit/algorithms/GenericTransportTest.py b/tests/unit/algorithms/GenericTransportTest.py
index ced62c3fc..b6752a047 100644
--- a/tests/unit/algorithms/GenericTransportTest.py
+++ b/tests/unit/algorithms/GenericTransportTest.py
@@ -172,7 +172,7 @@ class GenericTransportTest:
         geom = op.geometry.SpheresAndCylinders(network=net, pores=net.Ps, throats=net.Ts)
         air = op.phases.Air(network=net)
         water = op.phases.Water(network=net)
-        m = op.phases.MultiPhase(phases=[air, water], project=net.project)
+        m = op.phases.MultiPhase(network=net, phases=[air, water])
         m.set_occupancy(phase=air, pores=[0, 1, 2])
         m.set_occupancy(phase=water, pores=[3, 4, 5])
         const = op.models.misc.constant
diff --git a/tests/unit/geometry/GenericGeometryTest.py b/tests/unit/geometry/GenericGeometryTest.py
index e4aa181c0..c8b6bb73c 100644
--- a/tests/unit/geometry/GenericGeometryTest.py
+++ b/tests/unit/geometry/GenericGeometryTest.py
@@ -1,4 +1,5 @@
 import openpnm as op
+import pytest
 
 
 class GenericGeometryTest:
@@ -14,6 +15,19 @@ class GenericGeometryTest:
         mgr = op.Workspace()
         mgr.clear()
 
+    def test_instantiate_without_network_errors(self):
+        with pytest.raises(TypeError):
+            op.geometry.SpheresAndCylinders()
+
+    def test_add_locations_after_instantiation(self):
+        net = op.network.Cubic(shape=[3, 3, 3])
+        geo = op.geometry.SpheresAndCylinders(network=net)
+        geo.set_locations(pores=net.Ps)
+        assert geo.Np == 27
+        assert geo.Nt == 0
+        geo.set_locations(throats=net.Ts)
+        assert geo.Nt == 54
+
     def test_plot_histogram(self):
         # Test default args
         self.geo.show_hist()
diff --git a/tests/unit/models/physics/DiffusiveConductanceTest.py b/tests/unit/models/physics/DiffusiveConductanceTest.py
index b987e0ea4..ed5c3e5ef 100644
--- a/tests/unit/models/physics/DiffusiveConductanceTest.py
+++ b/tests/unit/models/physics/DiffusiveConductanceTest.py
@@ -89,7 +89,7 @@ class DiffusiveConductanceTest:
         geom['throat.diffusive_size_factors'] = self.size_factors_dict
         air = op.phases.Air(network=net)
         water = op.phases.Water(network=net)
-        m = op.phases.MultiPhase(phases=[air, water], project=net.project)
+        m = op.phases.MultiPhase(network=net, phases=[air, water])
         m.set_occupancy(phase=air, pores=[0, 1, 2])
         m.set_occupancy(phase=water, pores=[3, 4, 5])
         const = op.models.misc.constant
diff --git a/tests/unit/phases/GenericPhaseTest.py b/tests/unit/phases/GenericPhaseTest.py
index 477f812f3..28625e394 100644
--- a/tests/unit/phases/GenericPhaseTest.py
+++ b/tests/unit/phases/GenericPhaseTest.py
@@ -1,5 +1,5 @@
 import openpnm as op
-import scipy as sp
+import pytest
 
 
 class GenericPhaseTest:
@@ -9,3 +9,18 @@ class GenericPhaseTest:
     def teardown_class(self):
         mgr = op.Workspace()
         mgr.clear()
+
+    def test_instantiate_without_network_fails(self):
+        with pytest.raises(TypeError):
+            op.phases.GenericPhase()
+
+
+if __name__ == '__main__':
+
+    t = GenericPhaseTest()
+    self = t
+    t.setup_class()
+    for item in t.__dir__():
+        if item.startswith('test'):
+            print('running test: '+item)
+            t.__getattribute__(item)()
diff --git a/tests/unit/physics/GenericPhysicsTest.py b/tests/unit/physics/GenericPhysicsTest.py
index b1ed95fde..1f677f130 100644
--- a/tests/unit/physics/GenericPhysicsTest.py
+++ b/tests/unit/physics/GenericPhysicsTest.py
@@ -50,6 +50,10 @@ class GenericPhysicsTest:
         with pytest.raises(Exception):
             _ = phys.project.find_geometry(phys)
 
+    def test_instantiate_without_network_fails(self):
+        with pytest.raises(TypeError):
+            op.physics.GenericPhysics()
+
     def test_instantiate_with_pores_and_throats(self):
         net = op.network.Cubic(shape=[3, 3, 3])
         _ = op.geometry.GenericGeometry(network=net,