<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Geometry of Imported Networks
The Imported geometry class is used to store the geometrical properties of imported networks.  When importing an extracted network into OpenPNM using any of the io classes, all the geometrical and topological properties are  lumped together on the network object.  OpenPNM is generally designed such that geometrical properties are stored on a geometry object, so this class address this issue. The main function of the Imported class is to automatically strip the geometrical properties off of the network and transfer them onto itself.
What problem does the Imported class solve? Although OpenPNM can function with the geometrical properties on the network, a problem arises if the user wishes to add more pores to the network, such as boundary pores.  In this case, they will probably wish to add pore-scale models to calculate size information, say 'pore.volume'.  If they add this to the network, this model will overwrite the pre-existing 'pore.volume' values. The solution to this problem is an intrinsic part of OpenPNM
Step1:  Let's start by generating a random network using the Delaunay class.  This will repreent an imported network
Step2:  This network generator adds nicely defined boundary pores around the edges/faces of the network.  Let's remove these for the sake of this example
Step3:  This network does not have any geometrical properties on it when generated.  To mimic the situation of an imported network, let's manually enter some values for 'pore.diameter'.  We'll just assign random numbers to illustrate the point
Step4:  Now when we print the network we'll see all the topological data ('pore.coords' and 'throat.conns'), all the labels that were added by the generator (e.g. 'pore.left'), as well as the new geometry info we just added ('pore.diameter')
Step5:  OpenPNM was designed to work by assigning geomtrical information to Geometry objects.  The presence of 'pore.diameter' on the network can be a problem in some cases.  For instance, let's add some boundary pores to the left edge
Step6:  Visualizing this networks shows the newl added pores where we intended
Step7:  Now we have internal pores (red) and boundary pores (green).  We would like to assign geometrical information to the boundary pores that we just created.  This is typically done by creating a Geometry object, then either assigning numerical values or attaching a pore-scale model that calculates the values.  The problem is that OpenPNM prevents you from having 'pore.diameter' on the network AND a geometry object at the same time.
Step8:  Now we we try to assign 'pore.diameter', we'll get the following exception (The "try-except" structure is used for the purpose of this notebook example, but is not needed in an actual script)
Step9:  The solution is to remove the geometrical information from the network before adding the boundary pores, and place them on their own geometry.  In this example it is easy to transfer the 'pore.diameter' array, but in the case of a real extracted network there could be quite a few arrays to move.  OpenPNM has a facility for doing this
Step10:  Here we pass the network to the Imported geometry class.  This class literally removes all numerical data from the network to itself.  Everything is moved except topological info ('pore.coords' and 'throat.conns') and labels ('pore.left').
Step11:  Printing geo reveals that the 'pore.diameter' array has been transferred from the network automatically
Step12:  Now that the geometrical information is properly assigned to a geometry object, we can now use OpenPNM as intended.  Let's extend this network by adding a single new pore.
Step13:  The new pore can clearly be seen outside the top-right corner of the domain.
Step14:  We can now create a geometry just for this single pore and we will be free to add any properties we wish
Step15:  Note that the network has the ability to fetch the 'pore.diameter' array from the geometry sub-domain object and create a single full array containing the values from all the locations.  In the printout below we can see the value of 2.0 in the very last element, which is where new pores are added to the list.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
import openpnm as op
import matplotlib.pyplot as plt
ws = op.Workspace()
ws.settings['loglevel'] = 50  # Supress warnings, but see error messages
Explanation: Geometry of Imported Networks
The Imported geometry class is used to store the geometrical properties of imported networks.  When importing an extracted network into OpenPNM using any of the io classes, all the geometrical and topological properties are  lumped together on the network object.  OpenPNM is generally designed such that geometrical properties are stored on a geometry object, so this class address this issue. The main function of the Imported class is to automatically strip the geometrical properties off of the network and transfer them onto itself.
What problem does the Imported class solve? Although OpenPNM can function with the geometrical properties on the network, a problem arises if the user wishes to add more pores to the network, such as boundary pores.  In this case, they will probably wish to add pore-scale models to calculate size information, say 'pore.volume'.  If they add this to the network, this model will overwrite the pre-existing 'pore.volume' values. The solution to this problem is an intrinsic part of OpenPNM: create a separate geometry object to manage it's own 'pore.volume' model and values. However, this won't work!  OpenPNM will not allow an array called 'pore.volume' to exist on the network and a geometry object. The reason is that networks store values for every pore, so when adding new pores the network the 'pore.volume' array will increase to accommodate them.  If you attempt to put 'pore.volume' values on the geometry object, you're are essentially putting two values in those locations.  Therefore, the Imported class solves this problem by first transferring the 'pore.volume' array (and all other geometrical properties) from the network to itself.
End of explanation
np.random.seed(0)
pn = op.network.Delaunay(shape=[1, 1, 0], points=100)
Explanation: Let's start by generating a random network using the Delaunay class.  This will repreent an imported network:
End of explanation
op.topotools.trim(network=pn, pores=pn.pores('boundary'))
fig, ax = plt.subplots(1, 1, figsize=[5, 5])
op.topotools.plot_coordinates(network=pn, c='r', ax=ax)
op.topotools.plot_connections(network=pn, ax=ax)
Explanation: This network generator adds nicely defined boundary pores around the edges/faces of the network.  Let's remove these for the sake of this example:
End of explanation
pn['pore.diameter'] = np.random.rand(pn.Np)
Explanation: This network does not have any geometrical properties on it when generated.  To mimic the situation of an imported network, let's manually enter some values for 'pore.diameter'.  We'll just assign random numbers to illustrate the point:
End of explanation
print(pn)
Explanation: Now when we print the network we'll see all the topological data ('pore.coords' and 'throat.conns'), all the labels that were added by the generator (e.g. 'pore.left'), as well as the new geometry info we just added ('pore.diameter'):
End of explanation
Ps = pn['pore.surface']*(pn['pore.coords'][:, 0] < 0.1)
Ps = pn.toindices(Ps)
op.topotools.add_boundary_pores(network=pn, pores=Ps, 
                                move_to=[0, None, None], 
                                apply_label='left')
Explanation: OpenPNM was designed to work by assigning geomtrical information to Geometry objects.  The presence of 'pore.diameter' on the network can be a problem in some cases.  For instance, let's add some boundary pores to the left edge:
End of explanation
fig, ax = plt.subplots(figsize=[7, 7])
ax = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left', mode='not'), c='r', ax=ax)
ax = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left'), c='g', ax=ax)
ax = op.topotools.plot_connections(network=pn, ax=ax)
Explanation: Visualizing this networks shows the newl added pores where we intended:
End of explanation
Ps = pn.pores('left')
Ts = pn.find_neighbor_throats(pores=Ps)
geo_bndry = op.geometry.GenericGeometry(network=pn, pores=Ps, throats=Ts)
Explanation: Now we have internal pores (red) and boundary pores (green).  We would like to assign geometrical information to the boundary pores that we just created.  This is typically done by creating a Geometry object, then either assigning numerical values or attaching a pore-scale model that calculates the values.  The problem is that OpenPNM prevents you from having 'pore.diameter' on the network AND a geometry object at the same time.
End of explanation
try:
    geo_bndry['pore.diameter'] = 0
except Exception as e:
    print(e)
Explanation: Now we we try to assign 'pore.diameter', we'll get the following exception (The "try-except" structure is used for the purpose of this notebook example, but is not needed in an actual script):
End of explanation
pn = op.network.Delaunay(shape=[1, 1, 0], points=100)
pn['pore.diameter'] = np.random.rand(pn.Np)
Explanation: The solution is to remove the geometrical information from the network before adding the boundary pores, and place them on their own geometry.  In this example it is easy to transfer the 'pore.diameter' array, but in the case of a real extracted network there could be quite a few arrays to move.  OpenPNM has a facility for doing this: the Imported geometry class.
Using the Imported Geometry Class
Let's create a network and add a geometric properties again, this time before adding boundary pores.
End of explanation
geo = op.geometry.Imported(network=pn)
Explanation: Here we pass the network to the Imported geometry class.  This class literally removes all numerical data from the network to itself.  Everything is moved except topological info ('pore.coords' and 'throat.conns') and labels ('pore.left').
End of explanation
print(geo)
Explanation: Printing geo reveals that the 'pore.diameter' array has been transferred from the network automatically:
End of explanation
op.topotools.extend(network=pn, pore_coords = [[1.2, 1.2, 0]], labels='new')
Explanation: Now that the geometrical information is properly assigned to a geometry object, we can now use OpenPNM as intended.  Let's extend this network by adding a single new pore.
End of explanation
fig, ax = plt.subplots(figsize=[7, 7])
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left', mode='not'), c='r', ax=ax)
fig = op.topotools.plot_coordinates(network=pn, pores=pn.pores('left'), c='g', ax=ax)
fig = op.topotools.plot_connections(network=pn, ax=ax)
Explanation: The new pore can clearly be seen outside the top-right corner of the domain.
End of explanation
geo2 = op.geometry.GenericGeometry(network=pn, pores=pn.pores('new'))
geo2['pore.diameter'] = 2.0
print(geo2)
Explanation: We can now create a geometry just for this single pore and we will be free to add any properties we wish:
End of explanation
print(pn['pore.diameter'])
Explanation: Note that the network has the ability to fetch the 'pore.diameter' array from the geometry sub-domain object and create a single full array containing the values from all the locations.  In the printout below we can see the value of 2.0 in the very last element, which is where new pores are added to the list.
End of explanation
<END_TASK>