Repository: PMEAL/OpenPNM
Pull request #1705 (2020-10-17 19:15:23)

Expose set locations
This PR focuses on providing the user with more power to change physics and geometry associations.  

The hidden ``_set_locations`` method on the Subdomain class now has two public wrappers on the GenericGeometry class, ``add_locations`` and ``drop_locations``.  These both call the ``_set_locations`` method, and do a bit of extra clean-up.  Crucially, these methods will also change the locations of any associated physics objects.  If I remember correctly, we had hidden these methods because we didn't want users to start fiddling with locations on a geometry, since this would break the associated physics.  So we made it difficult to change these associations after the objects were made.  This new approach addresses this, since it keeps things correctly connected.

This also adds ``set_phase`` and ``set_geometry`` method to the GenericPhysics class.  Changing phase is straightfoward.  Changing geometry effectively changes which pores and throats the physics is associated with.  Each of the methods has several modes, including 'add', 'drop', and 'swap'.  

Unit tests were added for all new methods.


Patch:
diff --git a/openpnm/core/Base.py b/openpnm/core/Base.py
index 62affc798..7f9d43656 100644
--- a/openpnm/core/Base.py
+++ b/openpnm/core/Base.py
@@ -395,7 +395,7 @@ class Base(dict):
     def keys(self, element=None, mode=None, deep=False):
         r"""
         This subclass works exactly like ``keys`` when no arguments are passed,
-        but optionally accepts an ``element`` and/or a ``mode``, which filters
+        but optionally accepts an ``element`` and a ``mode``, which filters
         the output to only the requested keys.
 
         The default behavior is exactly equivalent to the normal ``keys``
diff --git a/openpnm/core/Subdomain.py b/openpnm/core/Subdomain.py
index d325b7c93..415c2cda7 100644
--- a/openpnm/core/Subdomain.py
+++ b/openpnm/core/Subdomain.py
@@ -10,20 +10,6 @@ class Subdomain(Base):
 
     Notes
     -----
-    The following table list the two methods added to Base by this subclass.
-
-    +---------------------+---------------------------------------------------+
-    | Methods             | Description                                       |
-    +=====================+===================================================+
-    | ``_add_locations``  | Specified which pores and throats the object      |
-    |                     | should be assigned to                             |
-    +---------------------+---------------------------------------------------+
-    | ``_drop_locations`` | Removes the object from the specified pores and   |
-    |                     | throats                                           |
-    +---------------------+---------------------------------------------------+
-    | ``_set_locations``  | The actual general method called by the above    |
-    +---------------------+---------------------------------------------------+
-
     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.
@@ -63,56 +49,6 @@ class Subdomain(Base):
                                 + hit + ' is already defined')
         super().__setitem__(key, value)
 
-    def _add_locations(self, pores=[], throats=[]):
-        r"""
-        Adds associations between an object and its boss object at the
-        given pore and/or throat locations.
-
-        Parameters
-        ----------
-        pores and throats : array_like
-            The pore and/or throat locations for which the association should
-            be added.  These indices are for the full domain.
-
-        Notes
-        -----
-        For *Physics* objects, the boss is the *Phase* with which it was
-        assigned, while for *Geometry* objects the boss is the *Network*.
-
-        """
-        boss = self.project.find_full_domain(self)
-        pores = boss._parse_indices(pores)
-        throats = boss._parse_indices(throats)
-        if len(pores) > 0:
-            self._set_locations(element='pore', indices=pores, mode='add')
-        if len(throats) > 0:
-            self._set_locations(element='throat', indices=throats, mode='add')
-
-    def _drop_locations(self, pores=[], throats=[]):
-        r"""
-        Removes association between an objectx and its boss object at the
-        given pore and/or throat locations.
-
-        Parameters
-        ----------
-        pores and throats : array_like
-            The pore and/or throat locations from which the association should
-            be removed.  These indices refer to the full domain.
-
-        Notes
-        -----
-        For *Physics* objects, the boss is the *Phase* with which it was
-        assigned, while for *Geometry* objects the boss is the *Network*.
-
-        """
-        boss = self.project.find_full_domain(self)
-        pores = boss._parse_indices(pores)
-        throats = boss._parse_indices(throats)
-        if len(pores) > 0:
-            self._set_locations(element='pore', indices=pores, mode='drop')
-        if len(throats) > 0:
-            self._set_locations(element='throat', indices=throats, mode='drop')
-
     def _set_locations(self, element, indices, mode):
         r"""
         This private method is called by ``set_locations`` and
diff --git a/openpnm/geometry/GenericGeometry.py b/openpnm/geometry/GenericGeometry.py
index 248428717..9a22843a4 100644
--- a/openpnm/geometry/GenericGeometry.py
+++ b/openpnm/geometry/GenericGeometry.py
@@ -1,3 +1,4 @@
+import numpy as np
 from openpnm.core import Subdomain, ModelsMixin
 from openpnm.utils import Workspace, logging
 logger = logging.getLogger(__name__)
@@ -91,7 +92,61 @@ class GenericGeometry(Subdomain, ModelsMixin):
             network['pore.'+self.name] = False
             network['throat.'+self.name] = False
             try:
-                self._add_locations(pores=pores, throats=throats)
+                self.add_locations(pores=pores, throats=throats)
             except Exception as e:
                 network.project.purge_object(self)
                 logger.error(str(e) +  ', instantiation cancelled')
+
+    def add_locations(self, pores=[], throats=[]):
+        r"""
+        Adds associations between this geometry and the given pore and/or
+        throat locations.
+
+        Parameters
+        ----------
+        pores and throats : array_like
+            The pore and/or throat locations for which the association should
+            be added.  These indices are for the full domain.
+
+        Notes
+        -----
+        If a physics object is associated with this geometry, then its
+        pore and/or throat associations are also changed.
+        """
+        pores = self.network._parse_indices(pores)
+        throats = self.network._parse_indices(throats)
+        objects = self.project.find_physics(self)
+        objects.append(self)
+        for obj in objects:
+            if len(pores) > 0:
+                obj._set_locations(element='pore', indices=pores, mode='add')
+            if len(throats) > 0:
+                obj._set_locations(element='throat', indices=throats, mode='add')
+
+
+    def drop_locations(self, pores=[], throats=[]):
+        r"""
+        Removes association between this geometry and the given pore and/or
+        throat locations.
+
+        Parameters
+        ----------
+        pores and throats : array_like
+            The pore and/or throat locations from which the association should
+            be removed.  These indices refer to the full domain.
+
+        Notes
+        -----
+        If a physics object is associated with this geometry, then its
+        pore and/or throat associations are also changed.
+
+        """
+        pores = self.network._parse_indices(pores)
+        throats = self.network._parse_indices(throats)
+        objects = self.project.find_physics(self)
+        objects.append(self)
+        for obj in objects:
+            if len(pores) > 0:
+                obj._set_locations(element='pore', indices=pores, mode='drop')
+            if len(throats) > 0:
+                obj._set_locations(element='throat', indices=throats, mode='drop')
diff --git a/openpnm/io/Dict.py b/openpnm/io/Dict.py
index 5a6e1c57d..d118b424c 100644
--- a/openpnm/io/Dict.py
+++ b/openpnm/io/Dict.py
@@ -254,6 +254,9 @@ class Dict(GenericIO):
 
     @classmethod
     def save(cls, *args, **kwargs):
+        r"""
+        This method is being deprecated.  Use ``export_data`` instead.
+        """
         cls.export_data(*args, **kwargs)
 
     @classmethod
diff --git a/openpnm/physics/GenericPhysics.py b/openpnm/physics/GenericPhysics.py
index 53bd0ac55..0da03823a 100644
--- a/openpnm/physics/GenericPhysics.py
+++ b/openpnm/physics/GenericPhysics.py
@@ -1,3 +1,4 @@
+import numpy as np
 from openpnm.core import Subdomain, ModelsMixin
 from openpnm.utils import Workspace, logging
 logger = logging.getLogger(__name__)
@@ -26,7 +27,7 @@ class GenericPhysics(Subdomain, ModelsMixin):
     name : str, optional
         A unique string name to identify the Physics object, typically same as
         instance name but can be anything.  If left blank, and name will be
-        generated that include the class name and a random string.
+        generated that includes the class name and an integer index.
 
     """
 
@@ -66,26 +67,104 @@ class GenericPhysics(Subdomain, ModelsMixin):
                 else:
                     self.set_geometry(geometry=geometry)
 
-    def set_phase(self, phase, mode='add'):
+    def set_phase(self, phase=None, mode='swap'):
         r"""
+        Sets the association between this physics and a phase.
+
+        Parameters
+        ----------
+        phase : OpenPNM Phase object
+            If mode is 'add' or 'swap', this must be specified so that
+            associations can be recorded in the phase dictionary.  If the
+            mode is 'drop', this is not needed since the existing association
+            can be used to find it.
+        mode : str
+            Options are:
+
+            'swap' - Associations will be made with the new phase, and
+            the pore and throat locations from the current phase will be
+            transferred to the new one.
+
+            'drop' - Associations with the existing phase will be removed.
+
+            'add' - If the physics does not presently have an associated
+            phase, this will create associations, but no pore or throat
+            locations will assigned.  This must be done using the
+            ``set_geometry`` method.
+
+        Notes
+        -----
+        In all cases the property data will be deleted since it will not
+        be relevant to the new phase, so the ``regenerate_models`` method
+        must be run.
+
         """
-        if phase not in self.project:
-            raise Exception(self.name + ' not in same project as given phase')
-        if mode == 'add':
-            phase['pore.'+self.name] = False
-            phase['throat.'+self.name] = False
-        elif mode == 'remove':
+        if mode in ['add', 'swap']:
+            if phase not in self.project:
+                raise Exception(self.name + ' not in same project as given phase')
+            try:
+                old_phase = self.project.find_phase(self)
+                phase['pore.'+self.name] = old_phase['pore.'+self.name]
+                phase['throat.'+self.name] = old_phase['throat.'+self.name]
+                old_phase.pop('pore.'+self.name, None)
+                old_phase.pop('throat.'+self.name, None)
+                self.clear()
+            except Exception as e:
+                logger.debug(e)
+                phase['pore.'+self.name] = False
+                phase['throat.'+self.name] = False
+        elif mode in ['remove', 'drop']:
+            self.update({'pore.all': np.array([], dtype=bool)})
+            self.update({'throat.all': np.array([], dtype=bool)})
+            phase = self.project.find_phase(self)
             phase.pop('pore.'+self.name, None)
             phase.pop('throat.'+self.name, None)
+            self.clear()
         else:
             raise Exception('mode ' + mode + ' not understood')
 
-    def set_geometry(self, geometry):
+    def set_geometry(self, geometry=None, mode='add'):
         r"""
+        Sets the association between this physics and a geometry (i.e. a
+        set of pores and throats that define a subdomain)
+
+        Parameters
+        ----------
+        geometry : OpenPNM Geometry object
+            The geometry defining the pores and throats to which this physics
+            should be attached
+        mode : str
+            Options are:
+
+            'swap' - Associations will be made with the new geometry, and
+            the pore and throat locations from the current geometry will be
+            transferred to the new one.
+
+            'drop' - Associations with the current geometry will be removed.
+
+            'add' - If the physics does not presently have an associated
+            geometry, this will create associations.
+
         """
-        if geometry not in self.project:
-            raise Exception(self.name + ' not in same project as given geometry')
-        network = self.network
-        Ps = network.pores(geometry.name)
-        Ts = network.throats(geometry.name)
-        self._add_locations(pores=Ps, throats=Ts)
+        if mode in ['add', 'swap']:
+            if geometry not in self.project:
+                raise Exception(self.name + ' not in same project as given geometry')
+            try:
+                old_geometry = self.project.find_geometry(self)
+                Ps = self.network.pores(old_geometry.name)
+                Ts = self.network.throats(old_geometry.name)
+                self._set_locations(element='pore', indices=Ps, mode='drop')
+                self._set_locations(element='throat', indices=Ts, mode='drop')
+            except Exception as e:
+                logger.debug(e)
+            Ps = self.network.pores(geometry.name)
+            Ts = self.network.throats(geometry.name)
+            self._set_locations(element='pore', indices=Ps, mode='add')
+            self._set_locations(element='throat', indices=Ts, mode='add')
+        if mode in ['remove', 'drop']:
+            phase = self.project.find_phase(self)
+            phase['pore.'+self.name] = False
+            phase['throat.'+self.name] = False
+            self.update({'pore.all': np.array([], dtype=bool)})
+            self.update({'throat.all': np.array([], dtype=bool)})
+            self.clear()
diff --git a/openpnm/utils/Grid.py b/openpnm/utils/Grid.py
index 2d9d79988..286122cf8 100644
--- a/openpnm/utils/Grid.py
+++ b/openpnm/utils/Grid.py
@@ -226,6 +226,8 @@ class Tableist():
             The number of rows to insert.  The default is 1.
 
         """
+        if num == 0:
+            return
         header = [self.blank for i in self._grid.table_data[0]]
         if row:
             self._grid.table_data.insert(row, header)
@@ -247,6 +249,8 @@ class Tableist():
             The number of columns to insert.  The default is 1.
 
         """
+        if num == 0:
+            return
         for row in self._grid.table_data:
             if col:
                 row.insert(col, self.blank)
diff --git a/openpnm/utils/Project.py b/openpnm/utils/Project.py
index d4423ce2d..4786a30c9 100644
--- a/openpnm/utils/Project.py
+++ b/openpnm/utils/Project.py
@@ -312,39 +312,35 @@ class Project(list):
 
         """
 
-        if geometry and phase:
+        if geometry is not None and phase is not None:
             physics = self.find_physics(geometry=geometry)
             phases = list(self.phases().values())
             phys = physics[phases.index(phase)]
             return phys
-
-        if geometry:
+        elif geometry is not None and phase is None:
             result = []
             net = self.network
             geoPs = net['pore.'+geometry.name]
             geoTs = net['throat.'+geometry.name]
             for phase in self.phases().values():
                 physics = self.find_physics(phase=phase)
-                temp = None
                 for phys in physics:
                     Ps = phase.map_pores(pores=phys.Ps, origin=phys)
                     physPs = phase.tomask(pores=Ps)
                     Ts = phase.map_throats(throats=phys.Ts, origin=phys)
                     physTs = phase.tomask(throats=Ts)
                     if np.all(geoPs == physPs) and np.all(geoTs == physTs):
-                        temp = phys
-                result.append(temp)
+                        result.append(phys)
             return result
-
-        if phase:
+        elif geometry is None and phase is not None:
             names = set(self.physics().keys())
             keys = set([item.split('.')[-1] for item in phase.keys()])
             hits = names.intersection(keys)
             phys = [self.physics().get(i, None) for i in hits]
             return phys
-
-        phys = list(self.physics().values())
-        return phys
+        else:
+            phys = list(self.physics().values())
+            return phys
 
     def find_full_domain(self, obj):
         r"""


Test patch:
diff --git a/tests/unit/core/SubdomainTest.py b/tests/unit/core/SubdomainTest.py
index 7a9aa784d..7de3d971a 100644
--- a/tests/unit/core/SubdomainTest.py
+++ b/tests/unit/core/SubdomainTest.py
@@ -37,43 +37,16 @@ class SubdomainTest:
     def test_drop_locations_from_geom_successively_with_single_geometry(self):
         assert self.geo.Np == 27
         assert self.geo.Nt == 54
-        self.geo._drop_locations(pores=[0, 1, 2], throats=[0, 1, 2])
+        self.geo.drop_locations(pores=[0, 1, 2], throats=[0, 1, 2])
         assert self.geo.Np == 24
         assert self.geo.Nt == 51
-        self.geo._drop_locations(pores=[3, 4], throats=[3, 4])
+        self.geo.drop_locations(pores=[3, 4], throats=[3, 4])
         assert self.geo.Np == 22
         assert self.geo.Nt == 49
-        self.geo._add_locations(pores=[0, 1, 2, 3, 4], throats=[0, 1, 2, 3, 4])
+        self.geo.add_locations(pores=[0, 1, 2, 3, 4], throats=[0, 1, 2, 3, 4])
         assert self.geo.Np == 27
         assert self.geo.Nt == 54
 
-    def test_drop_locations_from_physics_successively_with_two_physics(self):
-        assert self.phys1.Np == 27
-        assert self.phys1.Nt == 54
-        self.phys1._drop_locations(pores=[0, 1], throats=[0, 1])
-        assert self.phys1.Np == 25
-        assert self.phys1.Nt == 52
-        self.phys1._drop_locations(pores=[3, 4], throats=[3, 4])
-        assert self.phys1.Np == 23
-        assert self.phys1.Nt == 50
-        self.phys1._add_locations(pores=[0, 1, 3, 4], throats=[0, 1, 3, 4])
-        assert self.phys1.Np == 27
-        assert self.phys1.Nt == 54
-
-    def test_drop_locations_all_but_not_complete(self):
-        assert self.phys1.Np == 27
-        assert self.phys1.Nt == 54
-        assert 'pore.'+self.phys1.name in self.phase1.keys()
-        assert 'throat.'+self.phys1.name in self.phase1.keys()
-        self.phys1._drop_locations(pores=self.net.Ps)
-        assert 'pore.'+self.phys1.name in self.phase1.keys()
-        assert self.phase1.num_pores(self.phys1.name) == 0
-        assert 'throat.'+self.phys1.name in self.phase1.keys()
-        self.phys1._drop_locations(throats=self.net.Ts)
-        assert 'throat.'+self.phys1.name in self.phase1.keys()
-        assert self.phase1.num_throats(self.phys1.name) == 0
-        self.phys1._add_locations(pores=self.net.Ps, throats=self.net.Ts)
-
     def test_interleaving_missing_objects(self):
         pn = op.network.Cubic(shape=[3, 1, 1])
         geo1 = op.geometry.GenericGeometry(network=pn, pores=[0], throats=[0])
@@ -226,11 +199,11 @@ class SubdomainTest:
         Ps = net.pores('bottom')
         geom2 = op.geometry.GenericGeometry(network=net, pores=Ps)
         with pytest.raises(KeyError):
-            net['pore.blah']
+            _ = net['pore.blah']
         with pytest.raises(KeyError):
-            geom1['pore.blah']
+            _ = geom1['pore.blah']
         with pytest.raises(KeyError):
-            geom2['pore.blah']
+            _ = geom2['pore.blah']
 
     def test_interleave_data_float_missing_geometry(self):
         net = op.network.Cubic(shape=[2, 2, 2])
@@ -283,7 +256,7 @@ class SubdomainTest:
         phys['pore.blah'] = True
         assert np.sum(air['pore.blah']) == phys.Np
 
-    def test_writting_subdict_names_across_subdomains(self):
+    def test_writing_subdict_names_across_subdomains(self):
         ws = op.Workspace()
         proj = ws.new_project()
 
diff --git a/tests/unit/io/PandasTest.py b/tests/unit/io/PandasTest.py
index a7be23692..a3c58fa41 100644
--- a/tests/unit/io/PandasTest.py
+++ b/tests/unit/io/PandasTest.py
@@ -62,19 +62,18 @@ class PandasTest:
     def test_to_dataframe_not_joined(self):
         df = Pandas.to_dataframe(network=self.net, phases=[self.phase_1],
                                  join=False)
-        assert len(df.pore.keys()) == 22
-        assert len(df.throat.keys()) == 13
+        assert len(df.pore.keys()) == 23
+        assert len(df.throat.keys()) == 14
 
     def test_to_dataframe_joined(self):
         df = Pandas.to_dataframe(network=self.net, phases=[self.phase_1],
                                  join=True)
-        assert len(df.keys()) == 35
+        assert len(df.keys()) == 37
 
 
 if __name__ == '__main__':
-    # All the tests in this file can be run with 'playing' this file
     t = PandasTest()
-    self = t  # For interacting with the tests at the command line
+    self = t
     t.setup_class()
     for item in t.__dir__():
         if item.startswith('test'):
diff --git a/tests/unit/physics/GenericPhysicsTest.py b/tests/unit/physics/GenericPhysicsTest.py
index 768007b4f..08fdaa930 100644
--- a/tests/unit/physics/GenericPhysicsTest.py
+++ b/tests/unit/physics/GenericPhysicsTest.py
@@ -19,6 +19,8 @@ class GenericPhysicsTest:
         phys = op.physics.GenericPhysics(network=self.net,
                                          phase=phase,
                                          geometry=self.geo)
+        assert phys.Np == self.geo.Np
+        assert phys.Nt == self.geo.Nt
 
     def test_instantiate_with_phase_only(self):
         phase = op.phases.GenericPhase(network=self.net)
@@ -27,7 +29,7 @@ class GenericPhysicsTest:
         assert phys.project is not None
         assert phys.project.find_phase(phys) is phase
         with pytest.raises(Exception):
-            phys.project.find_geometry(phys)
+            _ = phys.project.find_geometry(phys)
 
     def test_instantiate_with_geometry_only(self):
         phase = op.phases.GenericPhase(network=self.net)
@@ -44,9 +46,9 @@ class GenericPhysicsTest:
         phys = op.physics.GenericPhysics(network=self.net)
         assert phys.project is not None
         with pytest.raises(Exception):
-            phys.project.find_phase(phys) is phase
+            _ = phys.project.find_phase(phys) is phase
         with pytest.raises(Exception):
-            phys.project.find_geometry(phys)
+            _ = phys.project.find_geometry(phys)
 
     def test_set_phase_afer_instantiation(self):
         phys = op.physics.GenericPhysics(network=self.net)
@@ -71,10 +73,74 @@ class GenericPhysicsTest:
         phase = op.phases.GenericPhase(network=self.net)
         phys = op.physics.GenericPhysics(network=self.net)
         with pytest.raises(Exception):
-            phys.set_phase(phase=phase2)
+            _ = phys.set_phase(phase=phase2)
         phys.set_phase(phase=phase)
         with pytest.raises(Exception):
-            phys.set_geometry(geometry=geo2)
+            _ = phys.set_geometry(geometry=geo2)
+
+    def test_swap_phase(self):
+        net = op.network.Cubic(shape=[3, 3, 3])
+        geo = op.geometry.GenericGeometry(network=net, pores=net.Ps,
+                                          throats=net.Ts)
+        phase1 = op.phases.GenericPhase(network=net)
+        phase2 = op.phases.GenericPhase(network=net)
+        phys = op.physics.GenericPhysics(network=net, phase=phase1,
+                                         geometry=geo)
+        phys.set_phase(phase=phase2, mode='swap')
+        assert phys.Np == 27
+        assert phys.Nt == 54
+
+    def test_drop_add_phase(self):
+        net = op.network.Cubic(shape=[3, 3, 3])
+        geo = op.geometry.GenericGeometry(network=net, pores=net.Ps,
+                                          throats=net.Ts)
+        phase1 = op.phases.GenericPhase(network=net)
+        phase2 = op.phases.GenericPhase(network=net)
+        phys = op.physics.GenericPhysics(network=net, phase=phase1,
+                                         geometry=geo)
+        phys.set_phase(mode='drop')
+        assert phys.Np == 0
+        assert phys.Nt == 0
+        phys.set_phase(phase=phase2, mode='add')
+        assert phys.Np == 0
+        assert phys.Nt == 0
+
+    def test_drop_add_geometry(self):
+        net = op.network.Cubic(shape=[3, 3, 3])
+        geo = op.geometry.GenericGeometry(network=net, pores=net.Ps,
+                                          throats=net.Ts)
+        phase = op.phases.GenericPhase(network=net)
+        phys = op.physics.GenericPhysics(network=net, phase=phase,
+                                         geometry=geo)
+        assert phys.Np == 27
+        assert phys.Nt == 54
+        phys.set_geometry(mode='drop')
+        assert phys.Np == 0
+        assert phys.Nt == 0
+        phys.set_geometry(geometry=geo, mode='add')
+        assert phys.Np == 27
+        assert phys.Nt == 54
+        phys.set_phase(mode='drop')
+        assert phys.Np == 0
+        assert phys.Nt == 0
+        with pytest.raises(Exception):
+            phys.set_geometry(geometry=geo, mode='add')
+
+    def test_drop_geo_add_phys(self):
+        net = op.network.Cubic(shape=[3, 3, 3])
+        geo = op.geometry.GenericGeometry(network=net, pores=net.Ps,
+                                          throats=net.Ts)
+        phase = op.phases.GenericPhase(network=net)
+        phys = op.physics.GenericPhysics(network=net, phase=phase,
+                                         geometry=geo)
+        geo.drop_locations(pores=[0])
+        geo2 = op.geometry.GenericGeometry(network=net, pores=[0])
+        phys2 = op.physics.GenericPhysics(network=net, phase=phase,
+                                          geometry=geo2)
+        assert geo.Np == 26
+        assert phys.Np == 26
+        assert geo2.Np == 1
+        assert phys2.Np == 1
 
 
 if __name__ == '__main__':