openpnm 1.0.0__zip

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. OpenPNM-1.1/MANIFEST.in +2 -0
  2. OpenPNM-1.1/OpenPNM/Algorithms/__FickianDiffusion__.py +67 -0
  3. OpenPNM-1.1/OpenPNM/Algorithms/__FourierConduction__.py +63 -0
  4. OpenPNM-1.1/OpenPNM/Algorithms/__GenericAlgorithm__.py +235 -0
  5. OpenPNM-1.1/OpenPNM/Algorithms/__GenericLinearTransport__.py +641 -0
  6. OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolationForImbibition__.py +703 -0
  7. OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolationTimed__.py +702 -0
  8. OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolation__.py +156 -0
  9. OpenPNM-1.1/OpenPNM/Algorithms/__OhmicConduction__.py +64 -0
  10. OpenPNM-1.1/OpenPNM/Algorithms/__OrdinaryPercolation__.py +402 -0
  11. OpenPNM-1.1/OpenPNM/Algorithms/__StokesFlow__.py +64 -0
  12. OpenPNM-1.1/OpenPNM/Algorithms/__Tortuosity__.py +91 -0
  13. OpenPNM-1.1/OpenPNM/Algorithms/__init__.py +48 -0
  14. OpenPNM-1.1/OpenPNM/Base/__Controller__.py +480 -0
  15. OpenPNM-1.1/OpenPNM/Base/__Core__.py +1522 -0
  16. OpenPNM-1.1/OpenPNM/Base/__ModelsDict__.py +345 -0
  17. OpenPNM-1.1/OpenPNM/Base/__Tools__.py +72 -0
  18. OpenPNM-1.1/OpenPNM/Base/__init__.py +32 -0
  19. OpenPNM-1.1/OpenPNM/Geometry/__Boundary__.py +80 -0
  20. OpenPNM-1.1/OpenPNM/Geometry/__Cube_and_Cuboid__.py +64 -0
  21. OpenPNM-1.1/OpenPNM/Geometry/__GenericGeometry__.py +106 -0
  22. OpenPNM-1.1/OpenPNM/Geometry/__SGL10__.py +67 -0
  23. OpenPNM-1.1/OpenPNM/Geometry/__Stick_and_Ball__.py +68 -0
  24. OpenPNM-1.1/OpenPNM/Geometry/__TestGeometry__.py +51 -0
  25. OpenPNM-1.1/OpenPNM/Geometry/__Toray090__.py +68 -0
  26. OpenPNM-1.1/OpenPNM/Geometry/__Voronoi__.py +98 -0
  27. OpenPNM-1.1/OpenPNM/Geometry/__init__.py +47 -0
  28. OpenPNM-1.1/OpenPNM/Geometry/models/__init__.py +33 -0
  29. OpenPNM-1.1/OpenPNM/Geometry/models/pore_area.py +27 -0
  30. OpenPNM-1.1/OpenPNM/Geometry/models/pore_centroid.py +35 -0
  31. OpenPNM-1.1/OpenPNM/Geometry/models/pore_diameter.py +127 -0
  32. OpenPNM-1.1/OpenPNM/Geometry/models/pore_misc.py +55 -0
  33. OpenPNM-1.1/OpenPNM/Geometry/models/pore_seed.py +212 -0
  34. OpenPNM-1.1/OpenPNM/Geometry/models/pore_surface_area.py +28 -0
  35. OpenPNM-1.1/OpenPNM/Geometry/models/pore_vertices.py +19 -0
  36. OpenPNM-1.1/OpenPNM/Geometry/models/pore_volume.py +133 -0
  37. OpenPNM-1.1/OpenPNM/Geometry/models/throat_area.py +47 -0
  38. OpenPNM-1.1/OpenPNM/Geometry/models/throat_centroid.py +80 -0
  39. OpenPNM-1.1/OpenPNM/Geometry/models/throat_diameter.py +106 -0
  40. OpenPNM-1.1/OpenPNM/Geometry/models/throat_length.py +95 -0
  41. OpenPNM-1.1/OpenPNM/Geometry/models/throat_misc.py +42 -0
  42. OpenPNM-1.1/OpenPNM/Geometry/models/throat_normal.py +31 -0
  43. OpenPNM-1.1/OpenPNM/Geometry/models/throat_offset_vertices.py +191 -0
  44. OpenPNM-1.1/OpenPNM/Geometry/models/throat_perimeter.py +26 -0
  45. OpenPNM-1.1/OpenPNM/Geometry/models/throat_seed.py +12 -0
  46. OpenPNM-1.1/OpenPNM/Geometry/models/throat_shape_factor.py +37 -0
  47. OpenPNM-1.1/OpenPNM/Geometry/models/throat_surface_area.py +44 -0
  48. OpenPNM-1.1/OpenPNM/Geometry/models/throat_vector.py +27 -0
  49. OpenPNM-1.1/OpenPNM/Geometry/models/throat_vertices.py +19 -0
  50. OpenPNM-1.1/OpenPNM/Geometry/models/throat_volume.py +45 -0
  51. OpenPNM-1.1/OpenPNM/Network/__Cubic__.py +316 -0
  52. OpenPNM-1.1/OpenPNM/Network/__DelaunayCubic__.py +127 -0
  53. OpenPNM-1.1/OpenPNM/Network/__Delaunay__.py +600 -0
  54. OpenPNM-1.1/OpenPNM/Network/__GenericNetwork__.py +1184 -0
  55. OpenPNM-1.1/OpenPNM/Network/__MatFile__.py +331 -0
  56. OpenPNM-1.1/OpenPNM/Network/__TestNet__.py +109 -0
  57. OpenPNM-1.1/OpenPNM/Network/__init__.py +40 -0
  58. OpenPNM-1.1/OpenPNM/Network/models/__init__.py +12 -0
  59. OpenPNM-1.1/OpenPNM/Network/models/pore_topology.py +106 -0
  60. OpenPNM-1.1/OpenPNM/Phases/__Air__.py +63 -0
  61. OpenPNM-1.1/OpenPNM/Phases/__GenericPhase__.py +146 -0
  62. OpenPNM-1.1/OpenPNM/Phases/__Mercury__.py +71 -0
  63. OpenPNM-1.1/OpenPNM/Phases/__TestPhase__.py +46 -0
  64. OpenPNM-1.1/OpenPNM/Phases/__Water__.py +56 -0
  65. OpenPNM-1.1/OpenPNM/Phases/__init__.py +38 -0
  66. OpenPNM-1.1/OpenPNM/Phases/models/__init__.py +22 -0
  67. OpenPNM-1.1/OpenPNM/Phases/models/contact_angle.py +34 -0
  68. OpenPNM-1.1/OpenPNM/Phases/models/density.py +81 -0
  69. OpenPNM-1.1/OpenPNM/Phases/models/diffusivity.py +95 -0
  70. OpenPNM-1.1/OpenPNM/Phases/models/electrical_conductivity.py +10 -0
  71. OpenPNM-1.1/OpenPNM/Phases/models/misc.py +125 -0
  72. OpenPNM-1.1/OpenPNM/Phases/models/molar_density.py +69 -0
  73. OpenPNM-1.1/OpenPNM/Phases/models/molar_mass.py +31 -0
  74. OpenPNM-1.1/OpenPNM/Phases/models/surface_tension.py +104 -0
  75. OpenPNM-1.1/OpenPNM/Phases/models/thermal_conductivity.py +98 -0
  76. OpenPNM-1.1/OpenPNM/Phases/models/vapor_pressure.py +69 -0
  77. OpenPNM-1.1/OpenPNM/Phases/models/viscosity.py +103 -0
  78. OpenPNM-1.1/OpenPNM/Physics/__GenericPhysics__.py +111 -0
  79. OpenPNM-1.1/OpenPNM/Physics/__Standard__.py +51 -0
  80. OpenPNM-1.1/OpenPNM/Physics/__TestPhysics__.py +50 -0
  81. OpenPNM-1.1/OpenPNM/Physics/__init__.py +30 -0
  82. OpenPNM-1.1/OpenPNM/Physics/models/__init__.py +18 -0
  83. OpenPNM-1.1/OpenPNM/Physics/models/capillary_pressure.py +122 -0
  84. OpenPNM-1.1/OpenPNM/Physics/models/diffusive_conductance.py +82 -0
  85. OpenPNM-1.1/OpenPNM/Physics/models/electrical_conductance.py +59 -0
  86. OpenPNM-1.1/OpenPNM/Physics/models/generic_source_term.py +564 -0
  87. OpenPNM-1.1/OpenPNM/Physics/models/hydraulic_conductance.py +76 -0
  88. OpenPNM-1.1/OpenPNM/Physics/models/multiphase.py +133 -0
  89. OpenPNM-1.1/OpenPNM/Physics/models/thermal_conductance.py +67 -0
  90. OpenPNM-1.1/OpenPNM/Postprocessing/Graphics.py +251 -0
  91. OpenPNM-1.1/OpenPNM/Postprocessing/Plots.py +369 -0
  92. OpenPNM-1.1/OpenPNM/Postprocessing/__init__.py +10 -0
  93. OpenPNM-1.1/OpenPNM/Utilities/IO.py +277 -0
  94. OpenPNM-1.1/OpenPNM/Utilities/Shortcuts.py +17 -0
  95. OpenPNM-1.1/OpenPNM/Utilities/__init__.py +16 -0
  96. OpenPNM-1.1/OpenPNM/Utilities/misc.py +226 -0
  97. OpenPNM-1.1/OpenPNM/Utilities/transformations.py +1923 -0
  98. OpenPNM-1.1/OpenPNM/Utilities/vertexops.py +824 -0
  99. OpenPNM-1.1/OpenPNM/__init__.py +56 -0
  100. OpenPNM-1.1/OpenPNM.egg-info/PKG-INFO +11 -0
  101. OpenPNM-1.1/OpenPNM.egg-info/SOURCES.txt +107 -0
  102. OpenPNM-1.1/OpenPNM.egg-info/dependency_links.txt +1 -0
  103. OpenPNM-1.1/OpenPNM.egg-info/requires.txt +1 -0
  104. OpenPNM-1.1/OpenPNM.egg-info/top_level.txt +1 -0
  105. OpenPNM-1.1/PKG-INFO +11 -0
  106. OpenPNM-1.1/README.txt +88 -0
  107. OpenPNM-1.1/setup.cfg +7 -0
  108. OpenPNM-1.1/setup.py +39 -0
@@ -0,0 +1,1184 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ===============================================================================
4
+ GenericNetwork: Abstract class to construct pore networks
5
+ ===============================================================================
6
+
7
+ """
8
+ import scipy as sp
9
+ import scipy.sparse as sprs
10
+ import scipy.spatial as sptl
11
+ import OpenPNM.Utilities.misc as misc
12
+ from OpenPNM.Base import Core, Controller, Tools, logging
13
+ logger = logging.getLogger(__name__)
14
+ ctrl = Controller()
15
+
16
+ class GenericNetwork(Core):
17
+ r"""
18
+ GenericNetwork - Base class to construct pore networks
19
+
20
+ Parameters
21
+ ----------
22
+ name : string
23
+ Unique name for Network object
24
+
25
+ """
26
+ def __init__(self,**kwargs):
27
+ r"""
28
+ Initialize Network
29
+ """
30
+ super(GenericNetwork,self).__init__(**kwargs)
31
+ logger.name = self.name
32
+
33
+ #Initialize adjacency and incidence matrix dictionaries
34
+ self._incidence_matrix = {}
35
+ self._adjacency_matrix = {}
36
+
37
+ def __setitem__(self,prop,value):
38
+ for geom in self._geometries:
39
+ if (prop in geom.keys()) and ('all' not in prop.split('.')):
40
+ logger.error(prop+' is already defined in at least one associated Geometry object')
41
+ return
42
+ super(GenericNetwork,self).__setitem__(prop,value)
43
+
44
+ def __getitem__(self,key):
45
+ if key.split('.')[-1] == self.name:
46
+ element = key.split('.')[0]
47
+ return self[element+'.all']
48
+ if key not in self.keys():
49
+ logger.debug(key+' not on Network, constructing data from Geometries')
50
+ return self._interleave_data(key,self.geometries())
51
+ else:
52
+ return super(GenericNetwork,self).__getitem__(key)
53
+
54
+ def _set_net(self,network):
55
+ pass
56
+ def _get_net(self):
57
+ return self
58
+ _net = property(fset=_set_net,fget=_get_net)
59
+
60
+ #--------------------------------------------------------------------------
61
+ '''Graph Theory and Network Query Methods'''
62
+ #--------------------------------------------------------------------------
63
+ def create_adjacency_matrix(self,data=None,sprsfmt='coo',dropzeros=True,sym=True):
64
+ r"""
65
+ Generates a weighted adjacency matrix in the desired sparse format
66
+
67
+ Parameters
68
+ ----------
69
+ data : array_like, optional
70
+ An array containing the throat values to enter into the matrix (in
71
+ graph theory these are known as the 'weights'). If omitted, ones
72
+ are used to create a standard adjacency matrix representing
73
+ connectivity only.
74
+
75
+ sprsfmt : string, optional
76
+ The sparse storage format to return. Options are:
77
+
78
+ * 'coo' : (default) This is the native format of OpenPNM data
79
+
80
+ * 'lil' : Enables row-wise slice of data
81
+
82
+ * 'csr' : Favored by most linear algebra routines
83
+
84
+ dropzeros : boolean, optional
85
+ Remove 0 elements from the values, instead of creating 0-weighted
86
+ links, the default is True.
87
+
88
+ sym : Boolean, optional
89
+ Makes the matrix symmetric about the diagonal, the default is true.
90
+
91
+ Returns
92
+ -------
93
+ Returns an adjacency matrix in the specified Scipy sparse format
94
+
95
+ Examples
96
+ --------
97
+ >>> import OpenPNM
98
+ >>> pn = OpenPNM.Network.TestNet()
99
+ >>> vals = sp.rand(pn.num_throats(),) < 0.5
100
+ >>> temp = pn.create_adjacency_matrix(data=vals,sprsfmt='csr')
101
+
102
+ """
103
+ logger.debug('create_adjacency_matrix: Start of method')
104
+ Np = self.num_pores()
105
+ Nt = self.num_throats()
106
+
107
+ #Check if provided data is valid
108
+ if data is None:
109
+ data = sp.ones((self.num_throats(),))
110
+ elif sp.shape(data)[0] != Nt:
111
+ raise Exception('Received dataset of incorrect length')
112
+
113
+ #Clear any zero-weighted connections
114
+ if dropzeros:
115
+ ind = data>0
116
+ else:
117
+ ind = sp.ones_like(data,dtype=bool)
118
+
119
+ #Get connectivity info from network
120
+ conn = self['throat.conns'][ind]
121
+ row = conn[:,0]
122
+ col = conn[:,1]
123
+ data = data[ind]
124
+
125
+ if sym: #Append row & col to each other, and data to itself
126
+ row = sp.append(row,conn[:,1])
127
+ col = sp.append(col,conn[:,0])
128
+ data = sp.append(data,data)
129
+
130
+ #Generate sparse adjacency matrix in 'coo' format
131
+ temp = sprs.coo_matrix((data,(row,col)),(Np,Np))
132
+
133
+ #Convert to requested format
134
+ if sprsfmt == 'coo':
135
+ pass #temp is already in coo format
136
+ if sprsfmt == 'csr':
137
+ temp = temp.tocsr()
138
+ if sprsfmt == 'lil':
139
+ temp = temp.tolil()
140
+ logger.debug('create_adjacency_matrix: End of method')
141
+ return temp
142
+
143
+ def create_incidence_matrix(self,data=None,sprsfmt='coo',dropzeros=True):
144
+ r"""
145
+ Creates an incidence matrix filled with supplied throat values
146
+
147
+ Parameters
148
+ ----------
149
+ data : array_like, optional
150
+ An array containing the throat values to enter into the matrix (In
151
+ graph theory these are known as the 'weights'). If omitted, ones
152
+ are used to create a standard incidence matrix representing
153
+ connectivity only.
154
+
155
+ sprsfmt : string, optional
156
+ The sparse storage format to return. Options are:
157
+
158
+ * 'coo' : (default) This is the native format of OpenPNMs data
159
+
160
+ * 'lil' : Enables row-wise slice of data
161
+
162
+ * 'csr' : Favored by most linear algebra routines
163
+
164
+ dropzeros : Boolean, optional
165
+ Remove 0 elements from values, instead of creating 0-weighted
166
+ links, the default is True.
167
+
168
+ Returns
169
+ -------
170
+ An incidence matrix (a cousin to the adjacency matrix, useful for
171
+ finding throats of given a pore)
172
+
173
+ Examples
174
+ --------
175
+ >>> import OpenPNM
176
+ >>> pn = OpenPNM.Network.TestNet()
177
+ >>> vals = sp.rand(pn.num_throats(),) < 0.5
178
+ >>> temp = pn.create_incidence_matrix(data=vals,sprsfmt='csr')
179
+ """
180
+ logger.debug('create_incidence_matrix: Start of method')
181
+
182
+ Nt = self.num_throats()
183
+ Np = self.num_pores()
184
+
185
+ #Check if provided data is valid
186
+ if data is None:
187
+ data = sp.ones((self.num_throats(),))
188
+ elif sp.shape(data)[0] != Nt:
189
+ raise Exception('Received dataset of incorrect length')
190
+
191
+ if dropzeros:
192
+ ind = data > 0
193
+ else:
194
+ ind = sp.ones_like(data, dtype=bool)
195
+
196
+ conn = self['throat.conns'][ind]
197
+ row = conn[:,0]
198
+ row = sp.append(row,conn[:,1])
199
+ col = self.throats('all')[ind]
200
+ col = sp.append(col,col)
201
+ data = sp.append(data[ind],data[ind])
202
+
203
+ temp = sprs.coo.coo_matrix((data,(row,col)),(Np,Nt))
204
+
205
+ #Convert to requested format
206
+ if sprsfmt == 'coo':
207
+ pass #temp is already in coo format
208
+ if sprsfmt == 'csr':
209
+ temp = temp.tocsr()
210
+ if sprsfmt == 'lil':
211
+ temp = temp.tolil()
212
+ logger.debug('create_incidence_matrix: End of method')
213
+ return temp
214
+
215
+ def find_connected_pores(self,throats=[],flatten=False):
216
+ r"""
217
+ Return a list of pores connected to the given list of throats
218
+
219
+ Parameters
220
+ ----------
221
+ throats : array_like
222
+ List of throats numbers
223
+
224
+ flatten : boolean, optional
225
+ If flatten is True (default) a 1D array of unique pore numbers
226
+ is returned. If flatten is False each location in the the returned
227
+ array contains a sub-arras of neighboring pores for each input
228
+ throat, in the order they were sent.
229
+
230
+ Returns
231
+ -------
232
+ 1D array (if flatten is True) or ndarray of arrays (if flatten is False)
233
+
234
+ Examples
235
+ --------
236
+ >>> import OpenPNM
237
+ >>> pn = OpenPNM.Network.TestNet()
238
+ >>> pn.find_connected_pores(throats=[0,1])
239
+ array([[0, 1],
240
+ [0, 5]])
241
+ >>> pn.find_connected_pores(throats=[0,1],flatten=True)
242
+ array([0, 1, 5])
243
+ """
244
+ Ps = self['throat.conns'][throats]
245
+ #Ps = [sp.asarray(x) for x in Ps if x]
246
+ if flatten:
247
+ Ps = sp.unique(sp.hstack(Ps))
248
+ return Ps
249
+
250
+ def find_connecting_throat(self,P1,P2):
251
+ r"""
252
+ Return the throat number connecting pairs of pores
253
+
254
+ Parameters
255
+ ----------
256
+ P1 , P2 : array_like
257
+ The pore numbers whose throats are sought. These can be vectors
258
+ of pore numbers, but must be the same length
259
+
260
+ Returns
261
+ -------
262
+ Tnum : list of list of int
263
+ Returns throat number(s), or empty array if pores are not connected
264
+
265
+ Examples
266
+ --------
267
+ >>> import OpenPNM
268
+ >>> pn = OpenPNM.Network.TestNet()
269
+ >>> pn.find_connecting_throat([0,1,2],[2,2,2])
270
+ [[], [3], []]
271
+
272
+ TODO: This now works on 'vector' inputs, but is not actually vectorized
273
+ in the Numpy sense, so could be slow with large P1,P2 inputs
274
+ """
275
+ Ts1 = self.find_neighbor_throats(P1,flatten=False)
276
+ Ts2 = self.find_neighbor_throats(P2,flatten=False)
277
+ Ts = []
278
+ if sp.shape(P1) == ():
279
+ P1 = [P1]
280
+ P2 = [P2]
281
+ for row in range(0,len(P1)):
282
+ if P1[row] == P2[row]:
283
+ throat = []
284
+ else:
285
+ throat = sp.intersect1d(Ts1[row],Ts2[row]).tolist()
286
+ Ts.insert(0,throat)
287
+ Ts.reverse()
288
+ return Ts
289
+
290
+ def find_neighbor_pores(self,pores,mode='union',flatten=True,excl_self=True):
291
+ r"""
292
+ Returns a list of pores neighboring the given pore(s)
293
+
294
+ Parameters
295
+ ----------
296
+ pores : array_like
297
+ ID numbers of pores whose neighbors are sought.
298
+ flatten : boolean, optional
299
+ If flatten is True a 1D array of unique pore ID numbers is
300
+ returned. If flatten is False the returned array contains arrays
301
+ of neighboring pores for each input pore, in the order they were
302
+ sent.
303
+ excl_self : bool, optional (Default is False)
304
+ If this is True then the input pores are not included in the
305
+ returned list. This option only applies when input pores
306
+ are in fact neighbors to each other, otherwise they are not
307
+ part of the returned list anyway.
308
+ mode : string, optional
309
+ Specifies which neighbors should be returned. The options are:
310
+
311
+ * 'union' : All neighbors of the input pores
312
+
313
+ * 'intersection' : Only neighbors shared by all input pores
314
+
315
+ * 'not_intersection' : Only neighbors not shared by any input pores
316
+
317
+ Returns
318
+ -------
319
+ neighborPs : 1D array (if flatten is True) or ndarray of ndarrays (if
320
+ flatten if False)
321
+
322
+ Examples
323
+ --------
324
+ >>> import OpenPNM
325
+ >>> pn = OpenPNM.Network.TestNet()
326
+ >>> pn.find_neighbor_pores(pores=[0,2])
327
+ array([ 1, 3, 5, 7, 25, 27])
328
+ >>> pn.find_neighbor_pores(pores=[0,1]) #Find all neighbors, excluding selves
329
+ array([ 2, 5, 6, 25, 26])
330
+ >>> pn.find_neighbor_pores(pores=[0,1],mode='union',excl_self=False) #Find all neighbors, including selves
331
+ array([ 0, 1, 2, 5, 6, 25, 26])
332
+ >>> pn.find_neighbor_pores(pores=[0,2],flatten=False)
333
+ array([array([ 1, 5, 25]), array([ 1, 3, 7, 27])], dtype=object)
334
+ >>> pn.find_neighbor_pores(pores=[0,2],mode='intersection') #Find only common neighbors
335
+ array([1])
336
+ >>> pn.find_neighbor_pores(pores=[0,2],mode='not_intersection') #Exclude common neighbors
337
+ array([ 3, 5, 7, 25, 27])
338
+ """
339
+ pores = sp.array(pores,ndmin=1)
340
+ try:
341
+ neighborPs = self._adjacency_matrix['lil'].rows[[pores]]
342
+ except:
343
+ temp = self.create_adjacency_matrix(sprsfmt='lil')
344
+ self._adjacency_matrix['lil'] = temp
345
+ neighborPs = self._adjacency_matrix['lil'].rows[[pores]]
346
+ if [sp.asarray(x) for x in neighborPs if x] == []:
347
+ return sp.array([],ndmin=1)
348
+ if flatten:
349
+ #All the empty lists must be removed to maintain data type after hstack (numpy bug?)
350
+ neighborPs = [sp.asarray(x) for x in neighborPs if x]
351
+ neighborPs = sp.hstack(neighborPs)
352
+ neighborPs = sp.concatenate((neighborPs,pores))
353
+ #Remove references to input pores and duplicates
354
+ if mode == 'not_intersection':
355
+ neighborPs = sp.array(sp.unique(sp.where(sp.bincount(neighborPs)==1)[0]),dtype=int)
356
+ elif mode == 'union':
357
+ neighborPs = sp.array(sp.unique(neighborPs),int)
358
+ elif mode == 'intersection':
359
+ neighborPs = sp.array(sp.unique(sp.where(sp.bincount(neighborPs)>1)[0]),dtype=int)
360
+ if excl_self:
361
+ neighborPs = neighborPs[~sp.in1d(neighborPs,pores)]
362
+ else:
363
+ for i in range(0,sp.size(pores)):
364
+ neighborPs[i] = sp.array(neighborPs[i],dtype=int)
365
+ return sp.array(neighborPs,ndmin=1)
366
+
367
+ def find_neighbor_throats(self,pores,mode='union',flatten=True):
368
+ r"""
369
+ Returns a list of throats neighboring the given pore(s)
370
+
371
+ Parameters
372
+ ----------
373
+ pores : array_like
374
+ Indices of pores whose neighbors are sought
375
+ flatten : boolean, optional
376
+ If flatten is True (default) a 1D array of unique throat ID numbers
377
+ is returned. If flatten is False the returned array contains arrays
378
+ of neighboring throat ID numbers for each input pore, in the order
379
+ they were sent.
380
+ mode : string, optional
381
+ Specifies which neighbors should be returned. The options are:
382
+
383
+ * 'union' : All neighbors of the input pores
384
+
385
+ * 'intersection' : Only neighbors shared by all input pores
386
+
387
+ * 'not_intersection' : Only neighbors not shared by any input pores
388
+
389
+ Returns
390
+ -------
391
+ neighborTs : 1D array (if flatten is True) or ndarray of arrays (if
392
+ flatten if False)
393
+
394
+ Examples
395
+ --------
396
+ >>> import OpenPNM
397
+ >>> pn = OpenPNM.Network.TestNet()
398
+ >>> pn.find_neighbor_throats(pores=[0,1])
399
+ array([0, 1, 2, 3, 4, 5])
400
+ >>> pn.find_neighbor_throats(pores=[0,1],flatten=False)
401
+ array([array([0, 1, 2]), array([0, 3, 4, 5])], dtype=object)
402
+ """
403
+ #Test for existence of incidence matrix
404
+ try:
405
+ neighborTs = self._incidence_matrix['lil'].rows[[pores]]
406
+ except:
407
+ temp = self.create_incidence_matrix(sprsfmt='lil')
408
+ self._incidence_matrix['lil'] = temp
409
+ neighborTs = self._incidence_matrix['lil'].rows[[pores]]
410
+ if [sp.asarray(x) for x in neighborTs if x] == []:
411
+ return sp.array([],ndmin=1)
412
+ if flatten:
413
+ #All the empty lists must be removed to maintain data type after hstack (numpy bug?)
414
+ neighborTs = [sp.asarray(x) for x in neighborTs if x]
415
+ neighborTs = sp.hstack(neighborTs)
416
+ #Remove references to input pores and duplicates
417
+ if mode == 'not_intersection':
418
+ neighborTs = sp.unique(sp.where(sp.bincount(neighborTs)==1)[0])
419
+ elif mode == 'union':
420
+ neighborTs = sp.unique(neighborTs)
421
+ elif mode == 'intersection':
422
+ neighborTs = sp.unique(sp.where(sp.bincount(neighborTs)>1)[0])
423
+ else:
424
+ for i in range(0,sp.size(pores)):
425
+ neighborTs[i] = sp.array(neighborTs[i])
426
+ return sp.array(neighborTs,ndmin=1)
427
+
428
+ def num_neighbors(self,pores,flatten=False):
429
+ r"""
430
+ Returns an ndarray containing the number of neigbhor pores for each
431
+ element in pores
432
+
433
+ Parameters
434
+ ----------
435
+ pores : array_like
436
+ Pores whose neighbors are to be counted
437
+ flatten : boolean (optional)
438
+ If False (default) the number pore neighbors for each input are
439
+ returned as an array. If True the sum total number of unique
440
+ neighbors is counted, not including the input pores even if they
441
+ neighbor each other.
442
+
443
+ Returns
444
+ -------
445
+ num_neighbors : 1D array with number of neighbors in each element
446
+
447
+ Examples
448
+ --------
449
+ >>> import OpenPNM
450
+ >>> pn = OpenPNM.Network.TestNet()
451
+ >>> pn.num_neighbors(pores=[0,1],flatten=False)
452
+ array([3, 4])
453
+ >>> pn.num_neighbors(pores=[0,1],flatten=True) # Sum excludes pores 0 & 1
454
+ 5
455
+ >>> pn.num_neighbors(pores=[0,2],flatten=True) # Sum includes pore 1, but not 0 & 2
456
+ 6
457
+ """
458
+
459
+ #Count number of neighbors
460
+ if flatten:
461
+ neighborPs = self.find_neighbor_pores(pores,flatten=True,mode='union',excl_self=True)
462
+ num = sp.shape(neighborPs)[0]
463
+ else:
464
+ neighborPs = self.find_neighbor_pores(pores,flatten=False)
465
+ num = sp.zeros(sp.shape(neighborPs),dtype=int)
466
+ for i in range(0,sp.shape(num)[0]):
467
+ num[i] = sp.size(neighborPs[i])
468
+ return num
469
+
470
+ def find_interface_throats(self,labels=[]):
471
+ r'''
472
+ Finds the throats that join two pore labels.
473
+
474
+ Parameters
475
+ ----------
476
+ labels : list of strings
477
+ The labels of the two pore groups whose interface is sought
478
+
479
+ Returns
480
+ -------
481
+ An array of throat numbers that connect the given pore groups
482
+
483
+ Notes
484
+ -----
485
+ This method is meant to find interfaces between TWO groups, regions or
486
+ clusters of pores (as defined by their label). If the input labels
487
+ overlap or are not adjacent, an empty array is returned.
488
+
489
+ Examples
490
+ --------
491
+ >>> import OpenPNM
492
+ >>> pn = OpenPNM.Network.TestNet()
493
+ >>> pn['pore.domain1'] = False
494
+ >>> pn['pore.domain2'] = False
495
+ >>> pn['pore.domain1'][[0,1,2]] = True
496
+ >>> pn['pore.domain2'][[5,6,7]] = True
497
+ >>> pn.find_interface_throats(labels=['domain1','domain2'])
498
+ array([1, 4, 7])
499
+ '''
500
+ Tind = sp.array([],ndmin=1)
501
+ if sp.shape(labels)[0] != 2:
502
+ logger.error('Exactly two labels must be given')
503
+ pass
504
+ else:
505
+ P1 = self.pores(labels=labels[0])
506
+ P2 = self.pores(labels=labels[1])
507
+ #Check if labels overlap
508
+ if sp.sum(sp.in1d(P1,P2)) > 0:
509
+ logger.error('Some labels overlap, iterface cannot be found')
510
+ pass
511
+ else:
512
+ T1 = self.find_neighbor_throats(P1)
513
+ T2 = self.find_neighbor_throats(P2)
514
+ Tmask = sp.in1d(T1,T2)
515
+ Tind = T1[Tmask]
516
+ return Tind
517
+
518
+ def find_clusters(self,mask=[]):
519
+ r'''
520
+ Identify connected clusters of pores in the network.
521
+
522
+ Parameters
523
+ ----------
524
+ mask : array_like, boolean
525
+ A list of active nodes. This method will automatically search
526
+ for clusters based on site or bond connectivity depending on
527
+ wheather the received mask is Np or Nt long.
528
+
529
+ Returns
530
+ -------
531
+ clusters : array_like
532
+ An Np long list of clusters numbers
533
+
534
+ '''
535
+ if sp.shape(mask)[0] == self.num_throats():
536
+ #Convert to boolean mask if not already
537
+ temp = sp.zeros((self.num_throats(),),dtype=bool)
538
+ temp[mask] = True
539
+ elif sp.shape(mask)[0] == self.num_pores():
540
+ conns = self.find_connected_pores(throats=self.throats())
541
+ conns[:,0] = mask[conns[:,0]]
542
+ conns[:,1] = mask[conns[:,1]]
543
+ temp = sp.array(conns[:,0]*conns[:,1],dtype=bool)
544
+ else:
545
+ raise Exception('Mask received was neither Nt nor Np long')
546
+ temp = self.create_adjacency_matrix(data=temp, sprsfmt='csr', dropzeros=True)
547
+ clusters = sprs.csgraph.connected_components(csgraph=temp,directed=False)[1]
548
+ return clusters
549
+
550
+ def _find_nearest_pores(self,pores,distance=0):
551
+ r'''
552
+ Still a work in progress, but will be useful for finding spatially
553
+ near, but not topologically connected pores.
554
+ '''
555
+ kd = sptl.cKDTree(self['pore.coords'])
556
+ if distance == 0:
557
+ pass
558
+ elif distance > 0:
559
+ Pn = kd.query_ball_point(self['pore.coords'][pores])[1]
560
+ return Pn
561
+
562
+ #--------------------------------------------------------------------------
563
+ '''Network Manipulation Methods'''
564
+ #--------------------------------------------------------------------------
565
+ def clone_pores(self,pores,apply_label=['clone'],mode='parents'):
566
+ r'''
567
+ Clones the specified pores and adds them to the network
568
+
569
+ Parameters
570
+ ----------
571
+ pores : array_like
572
+ List of pores to clone
573
+ apply_labels : string, or list of strings
574
+ The labels to apply to the clones, default is 'clone'
575
+ mode : string
576
+ Controls the connections between parents and clones. Options are:
577
+
578
+ - 'parents': (Default) Each clone is connected only to its parent
579
+ - 'siblings': Clones are only connected to each other in the same manner as parents were connected
580
+ - 'isolated': No connections between parents or siblings
581
+ '''
582
+ if (self._geometries != []):
583
+ logger.warning('Network has active Geometries, new pores must be assigned a Geometry')
584
+ if (self._phases != []):
585
+ raise Exception('Network has active Phases, cannot proceed')
586
+
587
+ logger.debug('Cloning pores')
588
+ apply_label = list(apply_label)
589
+ #Clone pores
590
+ Np = self.num_pores()
591
+ Nt = self.num_throats()
592
+ parents = sp.array(pores,ndmin=1)
593
+ pcurrent = self['pore.coords']
594
+ pclone = pcurrent[pores,:]
595
+ pnew = sp.concatenate((pcurrent,pclone),axis=0)
596
+ Npnew = sp.shape(pnew)[0]
597
+ clones = sp.arange(Np,Npnew)
598
+ #Add clone labels to network
599
+ for item in apply_label:
600
+ if ('pore.'+item) not in self.keys():
601
+ self['pore.'+item] = False
602
+ if ('throat.'+item) not in self.keys():
603
+ self['throat.'+item] = False
604
+ #Add connections between parents and clones
605
+ if mode == 'parents':
606
+ tclone = sp.vstack((parents,clones)).T
607
+ self.extend(pore_coords=pclone,throat_conns=tclone)
608
+ if mode == 'siblings':
609
+ ts = self.find_neighbor_throats(pores=pores,mode='intersection')
610
+ tclone = self['throat.conns'][ts] + self.num_pores()
611
+ self.extend(pore_coords=pclone,throat_conns=tclone)
612
+ if mode == 'isolated':
613
+ self.extend(pore_coords=pclone)
614
+ #Apply provided labels to cloned pores
615
+ for item in apply_label:
616
+ self['pore.'+item][self.pores('all')>=Np] = True
617
+ self['throat.'+item][self.throats('all')>=Nt] = True
618
+
619
+ # Any existing adjacency and incidence matrices will be invalid
620
+ self._update_network()
621
+
622
+ def extend(self,pore_coords=[],throat_conns=[],labels=[]):
623
+ r'''
624
+ Add individual pores (or throats) to the network from a list of coords
625
+ or conns.
626
+
627
+ Parameters
628
+ ----------
629
+ pore_coords : array_like
630
+ The coordinates of the pores to add
631
+ throat_conns : array_like
632
+ The throat connections to add
633
+ labels : string, or list of strings, optional
634
+ A list of labels to apply to the new pores and throats
635
+
636
+ Notes
637
+ -----
638
+ This needs to be enhanced so that it increases the size of all pore
639
+ and throat props and labels on ALL associated objects. At the moment
640
+ if throws an error is there are ANY associated objects.
641
+
642
+ '''
643
+ if (self._geometries != []):
644
+ logger.warning('Network has active Geometries, new pores must be assigned a Geometry')
645
+ if (self._phases != []):
646
+ raise Exception('Network has active Phases, cannot proceed')
647
+
648
+ logger.debug('Extending network')
649
+ Np_old = self.num_pores()
650
+ Nt_old = self.num_throats()
651
+ Np = Np_old + int(sp.size(pore_coords)/3)
652
+ Nt = Nt_old + int(sp.size(throat_conns)/2)
653
+ #Adjust 'all' labels
654
+ del self['pore.all'], self['throat.all']
655
+ self['pore.all'] = sp.ones((Np,),dtype=bool)
656
+ self['throat.all'] = sp.ones((Nt,),dtype=bool)
657
+ #Add coords and conns
658
+ if pore_coords != []:
659
+ coords = sp.vstack((self['pore.coords'],pore_coords))
660
+ self['pore.coords'] = coords
661
+ if throat_conns != []:
662
+ conns = sp.vstack((self['throat.conns'],throat_conns))
663
+ self['throat.conns'] = conns
664
+ for item in self.keys():
665
+ if item.split('.')[1] not in ['coords','conns','all']:
666
+ if item.split('.')[0] == 'pore':
667
+ N = Np
668
+ else:
669
+ N = Nt
670
+ if self[item].dtype == bool:
671
+ temp = self[item]
672
+ self[item] = sp.zeros((N,),dtype=bool)
673
+ self[item][temp] = True
674
+ elif self[item].dtype == object:
675
+ temp = self[item]
676
+ self[item] = sp.ndarray((N,),dtype=object)
677
+ self[item][sp.arange(0,sp.shape(temp)[0])] = temp
678
+ else:
679
+ temp = self[item]
680
+ try:
681
+ self[item] = sp.ones((N,sp.shape(temp)[1]),dtype=float)*sp.nan
682
+ except:
683
+ self[item] = sp.ones((N,),dtype=float)*sp.nan
684
+ self[item][sp.arange(0,sp.shape(temp)[0])] = temp
685
+ #Apply labels, if supplied
686
+ if labels != []:
687
+ #Convert labels to list if necessary
688
+ if type(labels) is str:
689
+ labels = [labels]
690
+ for label in labels:
691
+ #Remove pore or throat from label, if present
692
+ label = label.split('.')[-1]
693
+ if pore_coords != []:
694
+ Ps = sp.r_[Np_old:Np]
695
+ if 'pore.'+label not in self.labels():
696
+ self['pore.'+label] = False
697
+ self['pore.'+label][Ps] = True
698
+ if throat_conns != []:
699
+ Ts = sp.r_[Nt_old:Nt]
700
+ if 'throat.'+label not in self.labels():
701
+ self['throat.'+label] = False
702
+ self['throat.'+label][Ts] = True
703
+
704
+ self._update_network()
705
+
706
+ def trim(self, pores=[], throats=[]):
707
+ '''
708
+ Remove pores (or throats) from the network.
709
+
710
+ Parameters
711
+ ----------
712
+ pores (or throats) : array_like
713
+ A boolean mask of length Np (or Nt) or a list of indices of the
714
+ pores (or throats) to be removed.
715
+
716
+ Notes
717
+ -----
718
+ Trimming only adjusts Phase, Geometry, and Physics objects. Trimming a
719
+ Network that has already been used to run simulations will break those
720
+ simulation objects.
721
+
722
+ Examples
723
+ --------
724
+ >>> import OpenPNM
725
+ >>> pn = OpenPNM.Network.TestNet()
726
+ >>> pn.Np
727
+ 125
728
+ >>> pn.Nt
729
+ 300
730
+ >>> pn.trim(pores=[1])
731
+ >>> pn.Np
732
+ 124
733
+ >>> pn.Nt
734
+ 296
735
+
736
+ '''
737
+ for net in self.controller.networks():
738
+ if net._parent is self:
739
+ raise Exception('This Network has been cloned, cannot trim')
740
+
741
+ if len(pores) > 0:
742
+ pores = sp.array(pores,ndmin=1)
743
+ Pkeep = sp.ones((self.num_pores(),),dtype=bool)
744
+ Pkeep[pores] = False
745
+ Tkeep = sp.ones((self.num_throats(),),dtype=bool)
746
+ Ts = self.find_neighbor_throats(pores)
747
+ if len(Ts)>0:
748
+ Tkeep[Ts] = False
749
+ elif len(throats) > 0:
750
+ throats = sp.array(throats,ndmin=1)
751
+ Tkeep = sp.ones((self.num_throats(),),dtype=bool)
752
+ Tkeep[throats] = False
753
+ Pkeep = self['pore.all'].copy()
754
+ else:
755
+ logger.warning('No pores or throats recieved')
756
+ return
757
+
758
+ # Trim all associated objects
759
+ for item in self._geometries+self._physics+self._phases:
760
+ Pnet = self['pore.'+item.name]*Pkeep
761
+ Tnet = self['throat.'+item.name]*Tkeep
762
+ temp = self.map_pores(pores=sp.where(Pnet)[0],target=item,return_mapping=True)
763
+ Ps = temp['target']
764
+ temp = self.map_throats(throats=sp.where(Tnet)[0],target=item,return_mapping=True)
765
+ Ts = temp['target']
766
+ # Then resize 'all
767
+ item.update({'pore.all' : sp.ones((sp.sum(Pnet),),dtype=bool)})
768
+ item.update({'throat.all' : sp.ones((sp.sum(Tnet),),dtype=bool)})
769
+ # Overwrite remaining data and info
770
+ for key in list(item.keys()):
771
+ if key.split('.')[1] not in ['all']:
772
+ temp = item.pop(key)
773
+ if key.split('.')[0] == 'throat':
774
+ logger.debug('Trimming {a} from {b}'.format(a=key,b=item.name))
775
+ item[key] = temp[Ts]
776
+ if key.split('.')[0] == 'pore':
777
+ logger.debug('Trimming {a} from {b}'.format(a=key,b=item.name))
778
+ item[key] = temp[Ps]
779
+
780
+ #Remap throat connections
781
+ Pmap = sp.ones((self.Np,),dtype=int)*-1
782
+ Pmap[Pkeep] = sp.arange(0,sp.sum(Pkeep))
783
+ tpore1 = self['throat.conns'][:,0]
784
+ tpore2 = self['throat.conns'][:,1]
785
+ Tnew1 = Pmap[tpore1[Tkeep]]
786
+ Tnew2 = Pmap[tpore2[Tkeep]]
787
+ #Write 'all' label specifically
788
+ self.update({'throat.all' : sp.ones((sp.sum(Tkeep),),dtype=bool)})
789
+ self.update({'pore.all' : sp.ones((sp.sum(Pkeep),),dtype=bool)})
790
+ # Write throat connections specifically
791
+ self.update({'throat.conns' : sp.vstack((Tnew1,Tnew2)).T})
792
+ # Overwrite remaining data and info
793
+ for item in list(self.keys()):
794
+ if item.split('.')[-1] not in ['conns','all']:
795
+ temp = self.pop(item)
796
+ if item.split('.')[0] == 'throat':
797
+ logger.debug('Trimming {a} from {b}'.format(a=item,b=self.name))
798
+ self[item] = temp[Tkeep]
799
+ if item.split('.')[0] == 'pore':
800
+ logger.debug('Trimming {a} from {b}'.format(a=item,b=self.name))
801
+ self[item] = temp[Pkeep]
802
+
803
+ #Reset network graphs
804
+ self._update_network(mode='regenerate')
805
+
806
+ #Check Network health
807
+ health = self.check_network_health()
808
+ if health['trim_pores'] != []:
809
+ logger.warning('Isolated pores exist! Run check_network_health to ID which pores to remove.')
810
+ pass
811
+
812
+ def stitch(self,donor,pores_1,pores_2,method='delaunay',len_max=sp.inf,label_suffix=''):
813
+ r'''
814
+ Stitches a second a network to the current network.
815
+
816
+ Parameters
817
+ ----------
818
+ donor : OpenPNM Network Object
819
+ The Network to stitch on to the current Network
820
+
821
+ pores_1 : array_like
822
+ The pores on the current Network
823
+
824
+ pores_2 : array_like
825
+ The pores on the donor Network
826
+
827
+ label_suffix : string or None
828
+ Some text to append to each label in the donor Network before
829
+ inserting them into the recipient. The default is to append no
830
+ text, but a common option would be to append the donor Network's
831
+ name. To insert none of the donor labels, use None.
832
+
833
+ len_max : float
834
+ Set a length limit on length of new throats
835
+
836
+ method : string (default = 'delaunay')
837
+ The method to use when making pore to pore connections. Options are:
838
+
839
+ - 'delaunay' : Use a Delaunay tessellation
840
+ - 'nearest' : Connects each pore on the receptor network to its nearest pore on the donor network
841
+
842
+ Notes
843
+ -----
844
+ Before stitching it is necessary to translate the pore coordinates of
845
+ one of the Networks so that it is positioned correctly relative to the
846
+ other.
847
+
848
+ Examples
849
+ --------
850
+ >>> import OpenPNM
851
+ >>> pn = OpenPNM.Network.TestNet()
852
+ >>> pn2 = OpenPNM.Network.TestNet()
853
+ >>> [pn.Np, pn.Nt]
854
+ [125, 300]
855
+ >>> [pn2.Np, pn2.Nt]
856
+ [125, 300]
857
+ >>> pn2['pore.coords'][:,2] += 5.0 # Translate pn2 up 5 units in the Z-direction
858
+ >>> pn.stitch(donor=pn2,pores_1=pn.pores('top'),pores_2=pn2.pores('bottom'),len_max=1.0)
859
+ >>> [pn.Np, pn.Nt]
860
+ [250, 625]
861
+
862
+ '''
863
+ # Ensure Networks have no associated objects yet
864
+ if (len(self._simulation()) > 1) or (len(donor._simulation()) > 1):
865
+ raise Exception('Cannot stitch a Network with active sibling objects')
866
+ # Get the initial number of pores and throats
867
+ N_init = {}
868
+ N_init['pore'] = self.Np
869
+ N_init['throat'] = self.Nt
870
+ if method == 'delaunay':
871
+ P1 = pores_1
872
+ P2 = pores_2 + N_init['pore'] # Increment pores on donor
873
+ P = sp.hstack((P1,P2))
874
+ C1 = self['pore.coords'][pores_1]
875
+ C2 = donor['pore.coords'][pores_2]
876
+ C = sp.vstack((C1,C2))
877
+ T = sp.spatial.Delaunay(C)
878
+ a = T.simplices
879
+ b = []
880
+ for i in range(0,4): # Turn simplices into pairs of connections
881
+ for j in range(0,4):
882
+ if i != j:
883
+ b.append(sp.vstack((a[:,i],a[:,j])).T)
884
+ c = sp.vstack((b)) # Convert list b to numpy array
885
+ #Remove thraot connections to self
886
+ K1 = sp.in1d(P[c][:,0],P1)*sp.in1d(P[c][:,1],P2)
887
+ K2 = sp.in1d(P[c][:,0],P2)*sp.in1d(P[c][:,1],P1)
888
+ K = sp.where(K1 + K2)[0]
889
+ #Remove duplicate throat connections
890
+ i = P[c][K][:,0]
891
+ j = P[c][K][:,1]
892
+ v = sp.ones_like(i)
893
+ N = self.Np + donor.Np
894
+ adjmat = sprs.coo.coo_matrix((v,(i,j)),shape=(N,N))
895
+ #Convert to csr and back to coo to remove duplicates
896
+ adjmat = adjmat.tocsr()
897
+ adjmat = adjmat.tocoo()
898
+ #Remove lower triangular to remove bidirectional
899
+ adjmat = sprs.triu(adjmat,k=1,format="coo")
900
+ #Convert adjmat into 'throat.conns'
901
+ conns = sp.vstack((adjmat.row, adjmat.col)).T
902
+ if method == 'nearest':
903
+ P1 = pores_1
904
+ P2 = pores_2 + N_init['pore'] # Increment pores on donor
905
+ P = sp.hstack((P1,P2))
906
+ C1 = self['pore.coords'][pores_1]
907
+ C2 = donor['pore.coords'][pores_2]
908
+ D = sp.spatial.distance.cdist(C1,C2)
909
+ [P1_ind,P2_ind] = sp.where(D<=len_max)
910
+ conns = sp.vstack((P1[P1_ind],P2[P2_ind])).T
911
+
912
+ #Enter donor's pores into the Network
913
+ self.extend(pore_coords=donor['pore.coords'])
914
+
915
+ #Enter donor's throats into the Network
916
+ self.extend(throat_conns=donor['throat.conns']+N_init['pore'])
917
+
918
+ #Trim throats that are longer then given len_max
919
+ C1 = self['pore.coords'][conns[:,0]]
920
+ C2 = self['pore.coords'][conns[:,1]]
921
+ L = sp.sum((C1 - C2)**2,axis=1)**0.5
922
+ # conns = conns[L<=len_max]
923
+
924
+ #Add donor labels to recipient network
925
+ if label_suffix != None:
926
+ if label_suffix != '':
927
+ label_suffix = '_'+label_suffix
928
+ for label in donor.labels():
929
+ element = label.split('.')[0]
930
+ locations = sp.where(self._get_indices(element)>=N_init[element])[0]
931
+ try:
932
+ self[label+label_suffix]
933
+ except:
934
+ self[label+label_suffix] = False
935
+ self[label+label_suffix][locations] = donor[label]
936
+
937
+ #Add the new stitch throats to the Network
938
+ self.extend(throat_conns=conns,labels='stitched')
939
+
940
+ # Remove donor from Controller, if present
941
+ # This check allows for the reuse of a donor Network multiple times
942
+ if donor in ctrl.values():
943
+ ctrl.purge_object(donor)
944
+
945
+ def check_network_health(self):
946
+ r'''
947
+ This method check the network topological health by checking for:
948
+
949
+ (1) Isolated pores
950
+ (2) Islands or isolated clusters of pores
951
+ (3) Duplicate throats
952
+ (4) Bidirectional throats (ie. symmetrical adjacency matrix)
953
+
954
+ Returns
955
+ -------
956
+ A dictionary containing the offending pores or throat numbers under
957
+ each named key.
958
+
959
+ It also returns a list of which pores and throats should be trimmed
960
+ from the network to restore health. This list is a suggestion only,
961
+ and is based on keeping the largest cluster and trimming the others.
962
+
963
+ Notes
964
+ -----
965
+ - Does not yet check for duplicate pores
966
+ - Does not yet suggest which throats to remove
967
+ - This is just a 'check' method and does not 'fix' the problems it finds
968
+ '''
969
+
970
+ health = Tools.HealthDict()
971
+ health['disconnected_clusters'] = []
972
+ health['isolated_pores'] = []
973
+ health['trim_pores'] = []
974
+ health['duplicate_throats'] = []
975
+ health['bidirectional_throats'] = []
976
+
977
+ #Check for individual isolated pores
978
+ Ps = self.num_neighbors(self.pores())
979
+ if sp.sum(Ps==0) > 0:
980
+ logger.warning(str(sp.sum(Ps==0))+' pores have no neighbors')
981
+ health['isolated_pores'] = sp.where(Ps==0)[0]
982
+
983
+ #Check for separated clusters of pores
984
+ temp = []
985
+ Cs = self.find_clusters(self.tomask(throats=self.throats('all')))
986
+ if sp.shape(sp.unique(Cs))[0] > 1:
987
+ logger.warning('Isolated clusters exist in the network')
988
+ for i in sp.unique(Cs):
989
+ temp.append(sp.where(Cs==i)[0])
990
+ b = sp.array([len(item) for item in temp])
991
+ c = sp.argsort(b)[::-1]
992
+ for i in range(0,len(c)):
993
+ health['disconnected_clusters'].append(temp[c[i]])
994
+ if i > 0:
995
+ health['trim_pores'].extend(temp[c[i]])
996
+
997
+ #Check for duplicate throats
998
+ i = self['throat.conns'][:,0]
999
+ j = self['throat.conns'][:,1]
1000
+ v = sp.array(self['throat.all'],dtype=int)
1001
+ Np = self.num_pores()
1002
+ adjmat = sprs.coo_matrix((v,(i,j)),[Np,Np])
1003
+ temp = adjmat.tocsr() # Convert to CSR to combine duplicates
1004
+ temp = adjmat.tocoo() # And back to COO
1005
+ mergedTs = sp.where(temp.data>1)
1006
+ Ps12 = sp.vstack((temp.row[mergedTs], temp.col[mergedTs])).T
1007
+ dupTs = []
1008
+ for i in range(0,sp.shape(Ps12)[0]):
1009
+ dupTs.append(self.find_connecting_throat(Ps12[i,0],Ps12[i,1]).tolist)
1010
+ health['duplicate_throats'] = dupTs
1011
+
1012
+ #Check for bidirectional throats
1013
+ num_full = adjmat.sum()
1014
+ temp = sprs.triu(adjmat,k=1)
1015
+ num_upper = temp.sum()
1016
+ if num_full > num_upper:
1017
+ health['bidirectional_throats'] = str(num_full-num_upper)+' detected!'
1018
+
1019
+ #Check for coincident pores
1020
+ # temp = misc.dist(self['pore.coords'],self['pore.coords'])
1021
+ # temp = sp.triu(temp,k=1) # Remove lower triangular of matrix
1022
+ # temp = sp.where(temp==0) # Find 0 values in distance matrix
1023
+ # dupPs = sp.where(temp[1]>temp[0])[0] # Find 0 values above diagonal
1024
+ # health['duplicate_pores'] = dupPs
1025
+
1026
+ return health
1027
+
1028
+ def check_geometry_health(self):
1029
+ r'''
1030
+ Perform a check to find pores with overlapping or undefined Geometries
1031
+ '''
1032
+ geoms = self.geometries()
1033
+ Ptemp = sp.zeros((self.Np,))
1034
+ Ttemp = sp.zeros((self.Nt,))
1035
+ for item in geoms:
1036
+ Pind = self['pore.'+item]
1037
+ Tind = self['throat.'+item]
1038
+ Ptemp[Pind] = Ptemp[Pind] + 1
1039
+ Ttemp[Tind] = Ttemp[Tind] + 1
1040
+ health = Tools.HealthDict()
1041
+ health['overlapping_pores'] = sp.where(Ptemp>1)[0].tolist()
1042
+ health['undefined_pores'] = sp.where(Ptemp==0)[0].tolist()
1043
+ health['overlapping_throats'] = sp.where(Ttemp>1)[0].tolist()
1044
+ health['undefined_throats'] = sp.where(Ttemp==0)[0].tolist()
1045
+ return health
1046
+
1047
+ def _update_network(self,mode='clear'):
1048
+ r'''
1049
+ Regenerates the adjacency and incidence matrices
1050
+
1051
+ Parameters
1052
+ ----------
1053
+ mode : string
1054
+ Controls the extent of the update. Options are:
1055
+
1056
+ - 'clear' : Removes exsiting adjacency and incidence matrices
1057
+ - 'regenerate' : Removes the existing matrices and regenerates new ones.
1058
+
1059
+ Notes
1060
+ -----
1061
+ The 'regenerate' mode is more time consuming, so repeated calls to
1062
+ this function (ie. during network merges, and adding boundaries)
1063
+ should use the 'clear' mode. The other methods that require these
1064
+ matrices will generate them as needed, so this pushes the 'generation'
1065
+ time to 'on demand'.
1066
+ '''
1067
+ logger.debug('Resetting adjacency and incidence matrices')
1068
+ self._adjacency_matrix['coo'] = {}
1069
+ self._adjacency_matrix['csr'] = {}
1070
+ self._adjacency_matrix['lil'] = {}
1071
+ self._incidence_matrix['coo'] = {}
1072
+ self._incidence_matrix['csr'] = {}
1073
+ self._incidence_matrix['lil'] = {}
1074
+
1075
+ if mode == 'regenerate':
1076
+ self._adjacency_matrix['coo'] = self.create_adjacency_matrix(sprsfmt='coo')
1077
+ self._adjacency_matrix['csr'] = self.create_adjacency_matrix(sprsfmt='csr')
1078
+ self._adjacency_matrix['lil'] = self.create_adjacency_matrix(sprsfmt='lil')
1079
+ self._incidence_matrix['coo'] = self.create_incidence_matrix(sprsfmt='coo')
1080
+ self._incidence_matrix['csr'] = self.create_incidence_matrix(sprsfmt='csr')
1081
+ self._incidence_matrix['lil'] = self.create_incidence_matrix(sprsfmt='lil')
1082
+
1083
+ #--------------------------------------------------------------------------
1084
+ '''Domain Geometry Methods'''
1085
+ #--------------------------------------------------------------------------
1086
+ def domain_bulk_volume(self):
1087
+ r'''
1088
+ '''
1089
+ raise NotImplementedError()
1090
+
1091
+ def domain_pore_volume(self):
1092
+ r'''
1093
+ '''
1094
+ raise NotImplementedError()
1095
+
1096
+ def domain_length(self,face_1,face_2):
1097
+ r'''
1098
+ Calculate the distance between two faces of the network
1099
+
1100
+ Parameters
1101
+ ----------
1102
+ face_1 and face_2 : array_like
1103
+ Lists of pores belonging to opposite faces of the network
1104
+
1105
+ Returns
1106
+ -------
1107
+ The length of the domain in the specified direction
1108
+
1109
+ Notes
1110
+ -----
1111
+ - Does not yet check if input faces are perpendicular to each other
1112
+ '''
1113
+ #Ensure given points are coplanar before proceeding
1114
+ if misc.iscoplanar(self['pore.coords'][face_1]) and misc.iscoplanar(self['pore.coords'][face_2]):
1115
+ #Find distance between given faces
1116
+ x = self['pore.coords'][face_1]
1117
+ y = self['pore.coords'][face_2]
1118
+ Ds = misc.dist(x,y)
1119
+ L = sp.median(sp.amin(Ds,axis=0))
1120
+ else:
1121
+ logger.warning('The supplied pores are not coplanar. Length will be approximate.')
1122
+ f1 = self['pore.coords'][face_1]
1123
+ f2 = self['pore.coords'][face_2]
1124
+ distavg = [0,0,0]
1125
+ distavg[0] = sp.absolute(sp.average(f1[:,0]) - sp.average(f2[:,0]))
1126
+ distavg[1] = sp.absolute(sp.average(f1[:,1]) - sp.average(f2[:,1]))
1127
+ distavg[2] = sp.absolute(sp.average(f1[:,2]) - sp.average(f2[:,2]))
1128
+ L = max(distavg)
1129
+ return L
1130
+
1131
+
1132
+ def domain_area(self,face):
1133
+ r'''
1134
+ Calculate the area of a given network face
1135
+
1136
+ Parameters
1137
+ ----------
1138
+ face : array_like
1139
+ List of pores of pore defining the face of interest
1140
+
1141
+ Returns
1142
+ -------
1143
+ The area of the specified face
1144
+ '''
1145
+ coords = self['pore.coords'][face]
1146
+ rads = self['pore.diameter'][face]/2.
1147
+ # calculate the area of the 3 principle faces of the bounding cuboid
1148
+ dx = max(coords[:,0]+rads) - min(coords[:,0]-rads)
1149
+ dy = max(coords[:,1]+rads) - min(coords[:,1]-rads)
1150
+ dz = max(coords[:,2]+rads) - min(coords[:,2]-rads)
1151
+ yz = dy*dz # x normal
1152
+ xz = dx*dz # y normal
1153
+ xy = dx*dy # z normal
1154
+ # find the directions parallel to the plane
1155
+ directions = sp.where([yz,xz,xy]!=max([yz,xz,xy]))[0]
1156
+ try:
1157
+ # now, use the whole network to do the area calculation
1158
+ coords = self['pore.coords']
1159
+ rads = self['pore.diameter']/2.
1160
+ d0 = (max(coords[:,directions[0]]+rads) - min(coords[:,directions[0]]-rads))
1161
+ d1 = (max(coords[:,directions[1]]+rads) - min(coords[:,directions[1]]-rads))
1162
+ A = d0*d1
1163
+ except:
1164
+ # if that fails, use the max face area of the bounding cuboid
1165
+ A = max([yz,xz,xy])
1166
+ if not misc.iscoplanar(self['pore.coords'][face]):
1167
+ logger.warning('The supplied pores are not coplanar. Area will be approximate')
1168
+ pass
1169
+ return A
1170
+
1171
+ if __name__ == '__main__':
1172
+ #Run doc tests
1173
+ import doctest
1174
+ doctest.testmod(verbose=True)
1175
+
1176
+
1177
+
1178
+
1179
+
1180
+
1181
+
1182
+
1183
+
1184
+