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.
- OpenPNM-1.1/MANIFEST.in +2 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__FickianDiffusion__.py +67 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__FourierConduction__.py +63 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__GenericAlgorithm__.py +235 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__GenericLinearTransport__.py +641 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolationForImbibition__.py +703 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolationTimed__.py +702 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__InvasionPercolation__.py +156 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__OhmicConduction__.py +64 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__OrdinaryPercolation__.py +402 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__StokesFlow__.py +64 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__Tortuosity__.py +91 -0
- OpenPNM-1.1/OpenPNM/Algorithms/__init__.py +48 -0
- OpenPNM-1.1/OpenPNM/Base/__Controller__.py +480 -0
- OpenPNM-1.1/OpenPNM/Base/__Core__.py +1522 -0
- OpenPNM-1.1/OpenPNM/Base/__ModelsDict__.py +345 -0
- OpenPNM-1.1/OpenPNM/Base/__Tools__.py +72 -0
- OpenPNM-1.1/OpenPNM/Base/__init__.py +32 -0
- OpenPNM-1.1/OpenPNM/Geometry/__Boundary__.py +80 -0
- OpenPNM-1.1/OpenPNM/Geometry/__Cube_and_Cuboid__.py +64 -0
- OpenPNM-1.1/OpenPNM/Geometry/__GenericGeometry__.py +106 -0
- OpenPNM-1.1/OpenPNM/Geometry/__SGL10__.py +67 -0
- OpenPNM-1.1/OpenPNM/Geometry/__Stick_and_Ball__.py +68 -0
- OpenPNM-1.1/OpenPNM/Geometry/__TestGeometry__.py +51 -0
- OpenPNM-1.1/OpenPNM/Geometry/__Toray090__.py +68 -0
- OpenPNM-1.1/OpenPNM/Geometry/__Voronoi__.py +98 -0
- OpenPNM-1.1/OpenPNM/Geometry/__init__.py +47 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/__init__.py +33 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_area.py +27 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_centroid.py +35 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_diameter.py +127 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_misc.py +55 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_seed.py +212 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_surface_area.py +28 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_vertices.py +19 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/pore_volume.py +133 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_area.py +47 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_centroid.py +80 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_diameter.py +106 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_length.py +95 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_misc.py +42 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_normal.py +31 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_offset_vertices.py +191 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_perimeter.py +26 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_seed.py +12 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_shape_factor.py +37 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_surface_area.py +44 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_vector.py +27 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_vertices.py +19 -0
- OpenPNM-1.1/OpenPNM/Geometry/models/throat_volume.py +45 -0
- OpenPNM-1.1/OpenPNM/Network/__Cubic__.py +316 -0
- OpenPNM-1.1/OpenPNM/Network/__DelaunayCubic__.py +127 -0
- OpenPNM-1.1/OpenPNM/Network/__Delaunay__.py +600 -0
- OpenPNM-1.1/OpenPNM/Network/__GenericNetwork__.py +1184 -0
- OpenPNM-1.1/OpenPNM/Network/__MatFile__.py +331 -0
- OpenPNM-1.1/OpenPNM/Network/__TestNet__.py +109 -0
- OpenPNM-1.1/OpenPNM/Network/__init__.py +40 -0
- OpenPNM-1.1/OpenPNM/Network/models/__init__.py +12 -0
- OpenPNM-1.1/OpenPNM/Network/models/pore_topology.py +106 -0
- OpenPNM-1.1/OpenPNM/Phases/__Air__.py +63 -0
- OpenPNM-1.1/OpenPNM/Phases/__GenericPhase__.py +146 -0
- OpenPNM-1.1/OpenPNM/Phases/__Mercury__.py +71 -0
- OpenPNM-1.1/OpenPNM/Phases/__TestPhase__.py +46 -0
- OpenPNM-1.1/OpenPNM/Phases/__Water__.py +56 -0
- OpenPNM-1.1/OpenPNM/Phases/__init__.py +38 -0
- OpenPNM-1.1/OpenPNM/Phases/models/__init__.py +22 -0
- OpenPNM-1.1/OpenPNM/Phases/models/contact_angle.py +34 -0
- OpenPNM-1.1/OpenPNM/Phases/models/density.py +81 -0
- OpenPNM-1.1/OpenPNM/Phases/models/diffusivity.py +95 -0
- OpenPNM-1.1/OpenPNM/Phases/models/electrical_conductivity.py +10 -0
- OpenPNM-1.1/OpenPNM/Phases/models/misc.py +125 -0
- OpenPNM-1.1/OpenPNM/Phases/models/molar_density.py +69 -0
- OpenPNM-1.1/OpenPNM/Phases/models/molar_mass.py +31 -0
- OpenPNM-1.1/OpenPNM/Phases/models/surface_tension.py +104 -0
- OpenPNM-1.1/OpenPNM/Phases/models/thermal_conductivity.py +98 -0
- OpenPNM-1.1/OpenPNM/Phases/models/vapor_pressure.py +69 -0
- OpenPNM-1.1/OpenPNM/Phases/models/viscosity.py +103 -0
- OpenPNM-1.1/OpenPNM/Physics/__GenericPhysics__.py +111 -0
- OpenPNM-1.1/OpenPNM/Physics/__Standard__.py +51 -0
- OpenPNM-1.1/OpenPNM/Physics/__TestPhysics__.py +50 -0
- OpenPNM-1.1/OpenPNM/Physics/__init__.py +30 -0
- OpenPNM-1.1/OpenPNM/Physics/models/__init__.py +18 -0
- OpenPNM-1.1/OpenPNM/Physics/models/capillary_pressure.py +122 -0
- OpenPNM-1.1/OpenPNM/Physics/models/diffusive_conductance.py +82 -0
- OpenPNM-1.1/OpenPNM/Physics/models/electrical_conductance.py +59 -0
- OpenPNM-1.1/OpenPNM/Physics/models/generic_source_term.py +564 -0
- OpenPNM-1.1/OpenPNM/Physics/models/hydraulic_conductance.py +76 -0
- OpenPNM-1.1/OpenPNM/Physics/models/multiphase.py +133 -0
- OpenPNM-1.1/OpenPNM/Physics/models/thermal_conductance.py +67 -0
- OpenPNM-1.1/OpenPNM/Postprocessing/Graphics.py +251 -0
- OpenPNM-1.1/OpenPNM/Postprocessing/Plots.py +369 -0
- OpenPNM-1.1/OpenPNM/Postprocessing/__init__.py +10 -0
- OpenPNM-1.1/OpenPNM/Utilities/IO.py +277 -0
- OpenPNM-1.1/OpenPNM/Utilities/Shortcuts.py +17 -0
- OpenPNM-1.1/OpenPNM/Utilities/__init__.py +16 -0
- OpenPNM-1.1/OpenPNM/Utilities/misc.py +226 -0
- OpenPNM-1.1/OpenPNM/Utilities/transformations.py +1923 -0
- OpenPNM-1.1/OpenPNM/Utilities/vertexops.py +824 -0
- OpenPNM-1.1/OpenPNM/__init__.py +56 -0
- OpenPNM-1.1/OpenPNM.egg-info/PKG-INFO +11 -0
- OpenPNM-1.1/OpenPNM.egg-info/SOURCES.txt +107 -0
- OpenPNM-1.1/OpenPNM.egg-info/dependency_links.txt +1 -0
- OpenPNM-1.1/OpenPNM.egg-info/requires.txt +1 -0
- OpenPNM-1.1/OpenPNM.egg-info/top_level.txt +1 -0
- OpenPNM-1.1/PKG-INFO +11 -0
- OpenPNM-1.1/README.txt +88 -0
- OpenPNM-1.1/setup.cfg +7 -0
- OpenPNM-1.1/setup.py +39 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_centroid
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
from OpenPNM.Utilities import transformations as tr
|
|
9
|
+
from OpenPNM.Utilities import vertexops as vo
|
|
10
|
+
from scipy.spatial import ConvexHull
|
|
11
|
+
|
|
12
|
+
def voronoi(geometry,
|
|
13
|
+
**kwargs):
|
|
14
|
+
r"""
|
|
15
|
+
Calculate the geometrical centroid of the throat from the voronoi vertices.
|
|
16
|
+
"""
|
|
17
|
+
verts = geometry['throat.vertices']
|
|
18
|
+
offset_verts = geometry['throat.offset_vertices']
|
|
19
|
+
value = _sp.ndarray([len(verts),3])
|
|
20
|
+
for i in range(len(verts)):
|
|
21
|
+
if len(offset_verts[i]) > 2:
|
|
22
|
+
value[i] = _sp.array([offset_verts[i][:,0].mean(),offset_verts[i][:,1].mean(),offset_verts[i][:,2].mean()])
|
|
23
|
+
elif len(verts[i]) > 2:
|
|
24
|
+
value[i] = _sp.array([verts[i][:,0].mean(),verts[i][:,1].mean(),verts[i][:,2].mean()])
|
|
25
|
+
else:
|
|
26
|
+
value[i] = _sp.array([0,0,0])
|
|
27
|
+
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
def centre_of_mass(geometry,
|
|
31
|
+
**kwargs):
|
|
32
|
+
r"""
|
|
33
|
+
Calculate the centre of mass of the throat from the voronoi vertices.
|
|
34
|
+
"""
|
|
35
|
+
Nt = geometry.num_throats()
|
|
36
|
+
outer_verts = geometry['throat.vertices']
|
|
37
|
+
offset_verts = geometry['throat.offset_vertices']
|
|
38
|
+
normal = geometry['throat.normal']
|
|
39
|
+
z_axis = [0,0,1]
|
|
40
|
+
value = _sp.ndarray([Nt,3])
|
|
41
|
+
for i in range(Nt):
|
|
42
|
+
if len(offset_verts[i]) > 2:
|
|
43
|
+
verts = offset_verts[i]
|
|
44
|
+
elif len(outer_verts[i]) > 2:
|
|
45
|
+
verts = outer_verts[i]
|
|
46
|
+
else:
|
|
47
|
+
verts = []
|
|
48
|
+
if len(verts) > 0:
|
|
49
|
+
" For boundaries some facets will already be aligned with the axis - if this is the case a rotation is unnecessary and could also cause problems "
|
|
50
|
+
angle = tr.angle_between_vectors(normal[i],z_axis)
|
|
51
|
+
if (angle==0.0)or(angle==_sp.pi):
|
|
52
|
+
"We are already aligned"
|
|
53
|
+
rotate_input = False
|
|
54
|
+
facet = verts
|
|
55
|
+
else:
|
|
56
|
+
rotate_input = True
|
|
57
|
+
M = tr.rotation_matrix(tr.angle_between_vectors(normal[i],z_axis),tr.vector_product(normal[i],z_axis))
|
|
58
|
+
facet = _sp.dot(verts,M[:3,:3].T)
|
|
59
|
+
"Now we have a rotated facet aligned with the z axis - make 2D"
|
|
60
|
+
facet_2D = _sp.column_stack((facet[:,0],facet[:,1]))
|
|
61
|
+
z = _sp.unique(_sp.around(facet[:,2],10))
|
|
62
|
+
if len(z) == 1:
|
|
63
|
+
"We need the vertices arranged in order so perform a convex hull"
|
|
64
|
+
hull = ConvexHull(facet_2D)
|
|
65
|
+
ordered_facet_2D = facet_2D[hull.vertices]
|
|
66
|
+
"Call the routine to calculate an area wighted centroid from the 2D polygon"
|
|
67
|
+
COM_2D = vo.PolyWeightedCentroid2D(ordered_facet_2D)
|
|
68
|
+
COM_3D = _sp.hstack((COM_2D,z))
|
|
69
|
+
"If we performed a rotation we need to rotate back"
|
|
70
|
+
if (rotate_input):
|
|
71
|
+
MI = tr.inverse_matrix(M)
|
|
72
|
+
" Unrotate the offset coordinates using the inverse of the original rotation matrix"
|
|
73
|
+
value[i] = _sp.dot(COM_3D,MI[:3,:3].T)
|
|
74
|
+
else:
|
|
75
|
+
value[i] = COM_3D
|
|
76
|
+
else:
|
|
77
|
+
print("Rotation Failed: "+str(_sp.unique(facet[:,2])))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
return value
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_diameter
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
import OpenPNM.Utilities.transformations as tr
|
|
11
|
+
from scipy.spatial import ConvexHull
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cylinder(geometry,
|
|
16
|
+
tsd_name,tsd_shape,tsd_loc,tsd_scale,
|
|
17
|
+
throat_seed='throat.seed',
|
|
18
|
+
tsd_offset=0,
|
|
19
|
+
**kwargs):
|
|
20
|
+
r"""
|
|
21
|
+
Calculate throat diameter from seeds for a cylindrical throat
|
|
22
|
+
"""
|
|
23
|
+
import scipy.stats as spst
|
|
24
|
+
prob_fn = getattr(spst,tsd_name)
|
|
25
|
+
P = prob_fn(tsd_shape,loc=tsd_loc,scale=tsd_scale)
|
|
26
|
+
value=P.ppf(geometry[throat_seed])+tsd_offset
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
def voronoi(geometry,
|
|
30
|
+
throat_area='throat.area',
|
|
31
|
+
**kwargs):
|
|
32
|
+
r"""
|
|
33
|
+
Calculate throat diameter from analysis of Voronoi facets
|
|
34
|
+
Equivalent circular diameter from voronoi area
|
|
35
|
+
Could do better here and work out minimum diameter from verts
|
|
36
|
+
"""
|
|
37
|
+
areas = geometry[throat_area]
|
|
38
|
+
value = 2*_sp.sqrt(areas/_sp.pi)#64 bit sqrt doesn't work!
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
def incircle(geometry,
|
|
42
|
+
**kwargs):
|
|
43
|
+
r"""
|
|
44
|
+
Calculate the incircle diameter by linear programming and the simplex search algorithm using the offset vertices
|
|
45
|
+
generated by the voronoi diagram offsetting routine
|
|
46
|
+
"""
|
|
47
|
+
import warnings
|
|
48
|
+
try:
|
|
49
|
+
import pulp as pu
|
|
50
|
+
Nt = geometry.num_throats()
|
|
51
|
+
verts = geometry['throat.offset_vertices']
|
|
52
|
+
normals = geometry['throat.normal']
|
|
53
|
+
value = _sp.zeros(Nt)
|
|
54
|
+
for i in range(Nt):
|
|
55
|
+
if len(verts[i]) > 2:
|
|
56
|
+
pts = tr.rotate_and_chop(verts[i],normals[i],[0,0,1])
|
|
57
|
+
"Work out central point to use as initial guess"
|
|
58
|
+
C = np.mean(pts,axis=0)
|
|
59
|
+
"Compute convex hull to find points lying on the hull in order"
|
|
60
|
+
hull = ConvexHull(pts, qhull_options='QJ Pp')
|
|
61
|
+
"For each simplex making up the hull collect the end points"
|
|
62
|
+
A = pts[hull.vertices]
|
|
63
|
+
B = pts[np.roll(hull.vertices,-1)]
|
|
64
|
+
I = np.array([[0,1],[-1,0]])
|
|
65
|
+
"Normal of the simplices"
|
|
66
|
+
N = np.dot((B-A),I)
|
|
67
|
+
#L = np.sqrt(np.sum(np.square(N),axis=1))
|
|
68
|
+
"Normalize the normal vector"
|
|
69
|
+
L = np.linalg.norm(N,axis=1)
|
|
70
|
+
F = np.vstack((L,L)).T
|
|
71
|
+
N /= F
|
|
72
|
+
"Mid-points of the simplex"
|
|
73
|
+
M = (B+A)/2
|
|
74
|
+
#C = np.mean(A)
|
|
75
|
+
"If normals point out of hull change sign to point in"
|
|
76
|
+
pointing_out = (np.sum((M-C)*N,axis=1)>0)
|
|
77
|
+
N[pointing_out]*= -1
|
|
78
|
+
"Define Linear Program Variables"
|
|
79
|
+
"The centre of the incircle adjustment"
|
|
80
|
+
cx = pu.LpVariable("cx",None,None,pu.LpContinuous)
|
|
81
|
+
cy = pu.LpVariable("cy",None,None,pu.LpContinuous)
|
|
82
|
+
"Radius of the incircle"
|
|
83
|
+
R = pu.LpVariable("R",0,None,pu.LpContinuous)
|
|
84
|
+
"Slack variables for shortest distance between centre and simplices"
|
|
85
|
+
S = pu.LpVariable.dict("SlackVariable",range(len(A)),0,None,pu.LpContinuous)
|
|
86
|
+
"Set up LP problem"
|
|
87
|
+
prob = pu.LpProblem("FindInRadius",pu.LpMaximize)
|
|
88
|
+
"Objective Function"
|
|
89
|
+
prob += R
|
|
90
|
+
for j in range(len(A)):
|
|
91
|
+
" Ni.(C-Ai)-Si = 0"
|
|
92
|
+
prob += N[j][0]*(C[0]+cx) + N[j][1]*(C[1]+cy) - N[j][0]*A[j][0] - N[j][1]*A[j][1] - S[j] == 0
|
|
93
|
+
"Si >= R"
|
|
94
|
+
prob += S[j] >= R
|
|
95
|
+
"Solve the LP"
|
|
96
|
+
with warnings.catch_warnings():
|
|
97
|
+
warnings.simplefilter("ignore")
|
|
98
|
+
prob.solve()
|
|
99
|
+
"As the radius is the objective function we can get it from the objective or as R.value()"
|
|
100
|
+
value[i] = 2*R.value()
|
|
101
|
+
else:
|
|
102
|
+
value[i] = 0.0
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
except ImportError:
|
|
106
|
+
print("Cannot use incircle method without installing pulp package")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_length
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
|
|
9
|
+
def straight(network,
|
|
10
|
+
geometry,
|
|
11
|
+
pore_diameter='pore.diameter',
|
|
12
|
+
L_negative = 1e-9,
|
|
13
|
+
**kwargs):
|
|
14
|
+
r"""
|
|
15
|
+
Calculate throat length
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
L_negative : float
|
|
20
|
+
The default throat length to use when negative lengths are found. The
|
|
21
|
+
default is 1 nm. To accept negative throat lengths, set this value to
|
|
22
|
+
``None``.
|
|
23
|
+
"""
|
|
24
|
+
#Initialize throat_property['length']
|
|
25
|
+
throats = network.throats(geometry.name)
|
|
26
|
+
pore1 = network['throat.conns'][:,0]
|
|
27
|
+
pore2 = network['throat.conns'][:,1]
|
|
28
|
+
C1 = network['pore.coords'][pore1]
|
|
29
|
+
C2 = network['pore.coords'][pore2]
|
|
30
|
+
E = _sp.sqrt(_sp.sum((C1-C2)**2,axis=1)) #Euclidean distance between pores
|
|
31
|
+
D1 = network[pore_diameter][pore1]
|
|
32
|
+
D2 = network[pore_diameter][pore2]
|
|
33
|
+
value = E-(D1+D2)/2.
|
|
34
|
+
value = value[throats]
|
|
35
|
+
if _sp.any(value<0) and (L_negative is not None):
|
|
36
|
+
print('Negative throat lengths are calculated. Arbitrary positive length assigned: '+str(L_negative))
|
|
37
|
+
Ts = _sp.where(value<0)[0]
|
|
38
|
+
value[Ts] = L_negative
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
def voronoi(network,
|
|
42
|
+
geometry,
|
|
43
|
+
**kwargs):
|
|
44
|
+
r"""
|
|
45
|
+
Calculate the centre to centre distance from centroid of pore1 to centroid of throat to centroid of pore2
|
|
46
|
+
This is tricky as connections are defined at network level but centroids are stored on geometry.
|
|
47
|
+
The pore and throat map relates the geometry index to the network index but we must look up the index of the map
|
|
48
|
+
to go back to geometry index of the connected pores.
|
|
49
|
+
This will probably break down when a throat connects two different geometries
|
|
50
|
+
"""
|
|
51
|
+
throats = geometry.map_throats(network,geometry.throats())
|
|
52
|
+
connections = network['throat.conns'][throats]
|
|
53
|
+
net_pore1 = connections[:,0]
|
|
54
|
+
net_pore2 = connections[:,1]
|
|
55
|
+
pore_centroids = network['pore.centroid']
|
|
56
|
+
throat_centroids = network['throat.centroid'][throats]
|
|
57
|
+
v1 = throat_centroids-pore_centroids[net_pore1]
|
|
58
|
+
v2 = throat_centroids-pore_centroids[net_pore2]
|
|
59
|
+
value = _sp.ndarray(len(connections))
|
|
60
|
+
for i in range(len(connections)):
|
|
61
|
+
value[i] = _sp.linalg.norm(v1[i])+_sp.linalg.norm(v2[i])
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def c2c(network,
|
|
65
|
+
geometry,
|
|
66
|
+
**kwargs):
|
|
67
|
+
r"""
|
|
68
|
+
Calculate throat length
|
|
69
|
+
"""
|
|
70
|
+
#Initialize throat_property['length']
|
|
71
|
+
throats = network.throats(geometry.name)
|
|
72
|
+
pore1 = network['throat.conns'][:,0]
|
|
73
|
+
pore2 = network['throat.conns'][:,1]
|
|
74
|
+
C1 = network['pore.coords'][pore1]
|
|
75
|
+
C2 = network['pore.coords'][pore2]
|
|
76
|
+
E = _sp.sqrt(_sp.sum((C1-C2)**2,axis=1)) #Euclidean distance between pores
|
|
77
|
+
value = E
|
|
78
|
+
value = value[throats]
|
|
79
|
+
if _sp.any(value<0):
|
|
80
|
+
geometry._logger.warning('Negative throat lengths are calculated. Arbitrary positive length assigned (1e9 meters)')
|
|
81
|
+
Ts = _sp.where(value<0)[0]
|
|
82
|
+
value[Ts] = 1e-9
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
def constant(network,
|
|
86
|
+
geometry,
|
|
87
|
+
const,
|
|
88
|
+
**kwargs):
|
|
89
|
+
r"""
|
|
90
|
+
Calculate throat length
|
|
91
|
+
"""
|
|
92
|
+
#Initialize throat_property['length']
|
|
93
|
+
throats = network.throats(geometry.name)
|
|
94
|
+
value = _sp.ones(len(throats))*const
|
|
95
|
+
return value
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
throat_misc -- Miscillaneous and generic functions to apply to throats
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
|
|
9
|
+
def random(geometry,
|
|
10
|
+
seed=None,
|
|
11
|
+
num_range=[0,1],
|
|
12
|
+
**kwargs):
|
|
13
|
+
r"""
|
|
14
|
+
Assign random number to throats
|
|
15
|
+
note: should this be called 'poisson'?
|
|
16
|
+
"""
|
|
17
|
+
range_size = num_range[1]-num_range[0]
|
|
18
|
+
range_min = num_range[0]
|
|
19
|
+
_sp.random.seed(seed)
|
|
20
|
+
value = _sp.random.rand(geometry.num_throats(),)
|
|
21
|
+
value = value*range_size + range_min
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def neighbor(geometry,
|
|
26
|
+
network,
|
|
27
|
+
pore_prop='pore.seed',
|
|
28
|
+
mode='min',
|
|
29
|
+
**kwargs):
|
|
30
|
+
r"""
|
|
31
|
+
Adopt a value based on the neighboring pores
|
|
32
|
+
"""
|
|
33
|
+
throats = network.throats(geometry.name)
|
|
34
|
+
P12 = network.find_connected_pores(throats)
|
|
35
|
+
pvalues = network[pore_prop][P12]
|
|
36
|
+
if mode == 'min':
|
|
37
|
+
value = _sp.amin(pvalues,axis=1)
|
|
38
|
+
if mode == 'max':
|
|
39
|
+
value = _sp.amax(pvalues,axis=1)
|
|
40
|
+
if mode == 'mean':
|
|
41
|
+
value = _sp.mean(pvalues,axis=1)
|
|
42
|
+
return value
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
throat_normal -- calculate from the normal vector to the throat vertices
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as sp
|
|
8
|
+
from scipy.spatial import ConvexHull
|
|
9
|
+
|
|
10
|
+
def voronoi(network,
|
|
11
|
+
geometry,
|
|
12
|
+
**kwargs):
|
|
13
|
+
r"""
|
|
14
|
+
Update the throat normals from the voronoi vertices
|
|
15
|
+
"""
|
|
16
|
+
verts = geometry["throat.vertices"]
|
|
17
|
+
value = sp.ndarray([len(verts),3])
|
|
18
|
+
for i in range(len(verts)):
|
|
19
|
+
if len(sp.unique(verts[i][:,0]))==1:
|
|
20
|
+
verts_2d = sp.vstack((verts[i][:,1],verts[i][:,2])).T
|
|
21
|
+
elif len(sp.unique(verts[i][:,1]))==1:
|
|
22
|
+
verts_2d = sp.vstack((verts[i][:,0],verts[i][:,2])).T
|
|
23
|
+
else:
|
|
24
|
+
verts_2d = sp.vstack((verts[i][:,0],verts[i][:,1])).T
|
|
25
|
+
hull = ConvexHull(verts_2d, qhull_options='QJ Pp')
|
|
26
|
+
sorted_verts = verts[i][hull.vertices]
|
|
27
|
+
v1 = sorted_verts[1]-sorted_verts[0]
|
|
28
|
+
v2 = sorted_verts[-1]-sorted_verts[0]
|
|
29
|
+
value[i] = sp.cross(v1,v2)
|
|
30
|
+
|
|
31
|
+
return value
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
throat_offset_vertices -- Offeset throat vertices using a fibre radius parameter
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as sp
|
|
8
|
+
import OpenPNM.Utilities.vertexops as vo
|
|
9
|
+
import OpenPNM.Utilities.transformations as tr
|
|
10
|
+
|
|
11
|
+
def voronoi(network,
|
|
12
|
+
geometry,
|
|
13
|
+
offset,
|
|
14
|
+
**kwargs):
|
|
15
|
+
r"""
|
|
16
|
+
Offset the throat vertices effectively erroding the facet by the offset distance supplied
|
|
17
|
+
"""
|
|
18
|
+
Nt = geometry.num_throats()
|
|
19
|
+
area = sp.ndarray(Nt)
|
|
20
|
+
perimeter = sp.ndarray(Nt)
|
|
21
|
+
offset_verts = sp.ndarray(Nt,dtype=object)
|
|
22
|
+
offset_error = sp.ndarray(Nt)
|
|
23
|
+
throat_COM = sp.ndarray([Nt,3])
|
|
24
|
+
for i in range(Nt):
|
|
25
|
+
offset_rand = (sp.random.rand(1)-0.5)*offset
|
|
26
|
+
throat_verts=geometry["throat.vertices"][i]
|
|
27
|
+
throat_normal=geometry["throat.normal"][i]
|
|
28
|
+
area[i],perimeter[i],offset_verts[i],throat_COM[i],offset_error[i] = vo.get_throat_geom(throat_verts,throat_normal,offset)
|
|
29
|
+
|
|
30
|
+
for i in range(Nt):
|
|
31
|
+
if offset_error[i] > 0 and len(offset_verts[i]) > 0:
|
|
32
|
+
offset_verts[i]=[]
|
|
33
|
+
"Properties that depend on the offset vertices are the area, perimeter and the centroid or COM"
|
|
34
|
+
"To speed things up we could save them all now rather than processing them individually"
|
|
35
|
+
if kwargs["set_dependent"]==True:
|
|
36
|
+
geometry["throat.area"]=area
|
|
37
|
+
geometry["throat.perimeter"]=perimeter
|
|
38
|
+
geometry["throat.centroid"]=throat_COM
|
|
39
|
+
|
|
40
|
+
return offset_verts
|
|
41
|
+
|
|
42
|
+
def distance_transform(network,
|
|
43
|
+
geometry,
|
|
44
|
+
offset,
|
|
45
|
+
**kwargs):
|
|
46
|
+
r"""
|
|
47
|
+
Use the Voronoi vertices and perform image analysis to obtain throat properties
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import math
|
|
51
|
+
import numpy as np
|
|
52
|
+
from skimage.morphology import convex_hull_image
|
|
53
|
+
from skimage.measure import regionprops
|
|
54
|
+
from scipy import ndimage
|
|
55
|
+
|
|
56
|
+
Nt = geometry.num_throats()
|
|
57
|
+
area = sp.zeros(Nt)
|
|
58
|
+
perimeter = sp.zeros(Nt)
|
|
59
|
+
centroid = sp.zeros([Nt,3])
|
|
60
|
+
incentre = sp.zeros([Nt,3])
|
|
61
|
+
inradius = sp.zeros(Nt)
|
|
62
|
+
equiv_diameter = sp.zeros(Nt)
|
|
63
|
+
eroded_verts = sp.ndarray(Nt,dtype=object)
|
|
64
|
+
|
|
65
|
+
res=200
|
|
66
|
+
vertices = geometry["throat.vertices"]
|
|
67
|
+
normals = geometry["throat.normal"]
|
|
68
|
+
z_axis = [0,0,1]
|
|
69
|
+
|
|
70
|
+
for i in range(Nt):
|
|
71
|
+
" For boundaries some facets will already be aligned with the axis - if this is the case a rotation is unnecessary and could also cause problems "
|
|
72
|
+
angle = tr.angle_between_vectors(normals[i],z_axis)
|
|
73
|
+
if (angle==0.0)or(angle==np.pi):
|
|
74
|
+
"We are already aligned"
|
|
75
|
+
rotate_facet = False
|
|
76
|
+
facet = vertices[i]
|
|
77
|
+
else:
|
|
78
|
+
rotate_facet = True
|
|
79
|
+
M = tr.rotation_matrix(tr.angle_between_vectors(normals[i],z_axis),tr.vector_product(normals[i],z_axis))
|
|
80
|
+
facet = np.dot(vertices[i],M[:3,:3].T)
|
|
81
|
+
x = facet[:,0]
|
|
82
|
+
y = facet[:,1]
|
|
83
|
+
z = facet[:,2]
|
|
84
|
+
"Get points in 2d for image analysis"
|
|
85
|
+
pts = np.column_stack((x,y))
|
|
86
|
+
"translate points so min sits at the origin"
|
|
87
|
+
translation = [pts[:,0].min(),pts[:,1].min()]
|
|
88
|
+
pts -= translation
|
|
89
|
+
order = np.int(math.ceil(-np.log10(np.max(pts))))
|
|
90
|
+
"Normalise and scale the points so that largest span equals the resolution to save on memory and create clear image"
|
|
91
|
+
max_factor = np.max([pts[:,0].max(),pts[:,1].max()])
|
|
92
|
+
f = res/max_factor
|
|
93
|
+
"Scale the offset and define a circular structuring element with radius"
|
|
94
|
+
r = f*offset
|
|
95
|
+
"Only proceed if r is less than half the span of the image"
|
|
96
|
+
if r <= res/2:
|
|
97
|
+
pts *= f
|
|
98
|
+
minp1 = pts[:,0].min()
|
|
99
|
+
minp2 = pts[:,1].min()
|
|
100
|
+
maxp1 = pts[:,0].max()
|
|
101
|
+
maxp2 = pts[:,1].max()
|
|
102
|
+
img = np.zeros([np.int(math.ceil(maxp1-minp1)+1),np.int(math.ceil(maxp2-minp2)+1)])
|
|
103
|
+
int_pts = np.around(pts,0).astype(int)
|
|
104
|
+
for pt in int_pts:
|
|
105
|
+
img[pt[0]][pt[1]]=1
|
|
106
|
+
"Pad with zeros all the way around the edges"
|
|
107
|
+
img_pad = np.zeros([np.shape(img)[0]+2,np.shape(img)[1]+2])
|
|
108
|
+
img_pad[1:np.shape(img)[0]+1,1:np.shape(img)[1]+1]=img
|
|
109
|
+
|
|
110
|
+
"All points should lie on this plane but could be some rounding errors so use the order parameter"
|
|
111
|
+
z_plane = sp.unique(np.around(z,order+2))
|
|
112
|
+
if len(z_plane) > 1:
|
|
113
|
+
print("rotation for image analysis failed")
|
|
114
|
+
"Fill in the convex hull polygon"
|
|
115
|
+
convhullimg = convex_hull_image(img_pad)
|
|
116
|
+
"Perform a Distance Transform and black out points less than r to create binary erosion"
|
|
117
|
+
"This is faster than performing an erosion and dt can also be used later to find incircle"
|
|
118
|
+
eroded = ndimage.distance_transform_edt(convhullimg)
|
|
119
|
+
#eroded = dt.copy()
|
|
120
|
+
eroded[eroded<=r]=0
|
|
121
|
+
eroded[eroded>r]=1
|
|
122
|
+
"If we are left with less than 3 non-zero points then the throat is fully occluded"
|
|
123
|
+
if np.sum(eroded)>=3:
|
|
124
|
+
"Do some image analysis to extract the key properties"
|
|
125
|
+
regions = regionprops(eroded[1:np.shape(img)[0]+1,1:np.shape(img)[1]+1].astype(int))
|
|
126
|
+
if len(regions) == 1: # Change this to cope with genuine multi-region throats
|
|
127
|
+
for props in regions:
|
|
128
|
+
x0,y0 = props.centroid
|
|
129
|
+
equiv_diameter[i] = props.equivalent_diameter
|
|
130
|
+
area[i] = props.area
|
|
131
|
+
perimeter[i] = props.perimeter
|
|
132
|
+
coords = props.coords
|
|
133
|
+
"Undo the translation, scaling and truncation on the centroif"
|
|
134
|
+
centroid2d = [x0,y0]/f
|
|
135
|
+
centroid2d += (translation)
|
|
136
|
+
centroid3d = np.concatenate((centroid2d,z_plane))
|
|
137
|
+
"Distance transform the eroded facet to find the incentre and inradius"
|
|
138
|
+
#dt[dt>r] -= r
|
|
139
|
+
dt = ndimage.distance_transform_edt(eroded)
|
|
140
|
+
inx0,iny0 = np.asarray(np.unravel_index(dt.argmax(), dt.shape)).astype(float)
|
|
141
|
+
incentre2d=[inx0,iny0]
|
|
142
|
+
"Undo the translation, scaling and truncation on the incentre"
|
|
143
|
+
incentre2d /= f
|
|
144
|
+
incentre2d += (translation)
|
|
145
|
+
incentre3d = np.concatenate((incentre2d,z_plane))
|
|
146
|
+
"The offset vertices will be those in the coords that are closest to the originals"
|
|
147
|
+
offset_verts = []
|
|
148
|
+
for pt in int_pts:
|
|
149
|
+
vert = np.argmin(np.sum(np.square(coords-pt),axis=1))
|
|
150
|
+
if vert not in offset_verts:
|
|
151
|
+
offset_verts.append(vert)
|
|
152
|
+
"If we are left with less than 3 different vertices then the throat is fully occluded as we can't make a shape with non-zero area"
|
|
153
|
+
if len(offset_verts) >= 3:
|
|
154
|
+
offset_coords = coords[offset_verts].astype(float)
|
|
155
|
+
"Undo the translation, scaling and truncation on the offset_verts"
|
|
156
|
+
offset_coords /= f
|
|
157
|
+
offset_coords_3d = np.vstack((offset_coords[:,0]+translation[0],offset_coords[:,1]+translation[1],np.ones(len(offset_verts))*z_plane)).T
|
|
158
|
+
|
|
159
|
+
" Get matrix to un-rotate the co-ordinates back to the original orientation if we rotated in the first place"
|
|
160
|
+
if (rotate_facet):
|
|
161
|
+
MI = tr.inverse_matrix(M)
|
|
162
|
+
" Unrotate the offset coordinates "
|
|
163
|
+
incentre[i] = np.dot(incentre3d,MI[:3,:3].T)
|
|
164
|
+
centroid[i] = np.dot(centroid3d,MI[:3,:3].T)
|
|
165
|
+
eroded_verts[i] = np.dot(offset_coords_3d,MI[:3,:3].T)
|
|
166
|
+
|
|
167
|
+
else:
|
|
168
|
+
incentre[i] = incentre3d
|
|
169
|
+
centroid[i] = centroid3d
|
|
170
|
+
eroded_verts[i] = offset_coords_3d
|
|
171
|
+
|
|
172
|
+
inradius[i] = dt.max()
|
|
173
|
+
"Undo scaling on other parameters"
|
|
174
|
+
area[i] /= f*f
|
|
175
|
+
perimeter[i] /= f
|
|
176
|
+
equiv_diameter[i] /= f
|
|
177
|
+
inradius[i] /= f
|
|
178
|
+
else:
|
|
179
|
+
area[i]=0
|
|
180
|
+
perimeter[i]=0
|
|
181
|
+
equiv_diameter[i]=0
|
|
182
|
+
|
|
183
|
+
if kwargs["set_dependent"]==True:
|
|
184
|
+
geometry["throat.area"] = area
|
|
185
|
+
geometry["throat.perimeter"] = perimeter
|
|
186
|
+
geometry["throat.centroid"] = centroid
|
|
187
|
+
geometry["throat.diameter"] = equiv_diameter
|
|
188
|
+
geometry["throat.indiameter"] = inradius*2
|
|
189
|
+
geometry["throat.incentre"] = incentre
|
|
190
|
+
|
|
191
|
+
return eroded_verts
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_perimeter
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
import OpenPNM.Utilities.transformations as tr
|
|
9
|
+
import OpenPNM.Utilities.vertexops as vo
|
|
10
|
+
|
|
11
|
+
def voronoi(geometry,
|
|
12
|
+
**kwargs):
|
|
13
|
+
r"""
|
|
14
|
+
Use the Voronoi verts and throat normals to work out the perimeter
|
|
15
|
+
"""
|
|
16
|
+
Nt = geometry.num_throats()
|
|
17
|
+
verts = geometry['throat.offset_vertices']
|
|
18
|
+
normals = geometry['throat.normal']
|
|
19
|
+
perimeter = _sp.ndarray(Nt)
|
|
20
|
+
for i in range(Nt):
|
|
21
|
+
if len(verts[i]) > 2:
|
|
22
|
+
verts_2D = tr.rotate_and_chop(verts[i],normals[i],[0,0,1])
|
|
23
|
+
perimeter[i] = vo.PolyPerimeter2D(verts_2D)
|
|
24
|
+
else:
|
|
25
|
+
perimeter[i] = 0.0
|
|
26
|
+
return perimeter
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_shape_factor
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
|
|
9
|
+
def compactness(geometry,
|
|
10
|
+
throat_perimeter='throat.perimeter',
|
|
11
|
+
throat_area='throat.area',
|
|
12
|
+
**kwargs):
|
|
13
|
+
r"""
|
|
14
|
+
Mortensen et al. have shown that the Hagen-Poiseuille hydraluic resistance is linearly dependent on the compactness
|
|
15
|
+
Defined as perimeter^2/area.
|
|
16
|
+
The dependence is not universal as shapes with sharp corners provide more resistance than those that are more elliptical
|
|
17
|
+
Count the number of vertices and apply the right correction
|
|
18
|
+
"""
|
|
19
|
+
"Only apply to throats with an area"
|
|
20
|
+
ts = geometry.throats()[geometry[throat_area]>0]
|
|
21
|
+
P = geometry[throat_perimeter]
|
|
22
|
+
A = geometry[throat_area]
|
|
23
|
+
C = _sp.ones(geometry.num_throats())
|
|
24
|
+
C[ts] = P[ts]**2/A[ts]
|
|
25
|
+
verts = geometry["throat.offset_vertices"]
|
|
26
|
+
for i in range(len(verts)):
|
|
27
|
+
if len(verts[i]) == 3:
|
|
28
|
+
"Triangular Correction"
|
|
29
|
+
C[i] = C[i]*(25/17) + (40*_sp.sqrt(3)/17)
|
|
30
|
+
elif len(verts[i]) == 4:
|
|
31
|
+
"Rectangular Correction"
|
|
32
|
+
C[i] = C[i]*(22/7) + (65/3)
|
|
33
|
+
elif len(verts[i]) > 4:
|
|
34
|
+
"Approximate Elliptical Correction"
|
|
35
|
+
C[i] = C[i]*(8/3) + (8*_sp.pi/3)
|
|
36
|
+
|
|
37
|
+
return C
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
===============================================================================
|
|
3
|
+
Submodule -- throat_surface_area
|
|
4
|
+
===============================================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
import scipy as _sp
|
|
8
|
+
|
|
9
|
+
def cylinder(geometry,
|
|
10
|
+
throat_diameter='throat.diameter',
|
|
11
|
+
throat_length='throat.length',
|
|
12
|
+
**kwargs):
|
|
13
|
+
r"""
|
|
14
|
+
Calculate throat area for a cylindrical throat
|
|
15
|
+
"""
|
|
16
|
+
D = geometry[throat_diameter]
|
|
17
|
+
L = geometry[throat_length]
|
|
18
|
+
value = _sp.constants.pi*D*L
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
def cuboid(geometry,
|
|
22
|
+
throat_diameter='throat.diameter',
|
|
23
|
+
throat_length='throat.length',
|
|
24
|
+
**kwargs):
|
|
25
|
+
r"""
|
|
26
|
+
Calculate throat area for a cuboid throat
|
|
27
|
+
"""
|
|
28
|
+
D = geometry[throat_diameter]
|
|
29
|
+
L = geometry[throat_length]
|
|
30
|
+
value = 4*D*L
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
def extrusion(geometry,
|
|
34
|
+
throat_perimeter='throat.perimeter',
|
|
35
|
+
throat_length='throat.length',
|
|
36
|
+
**kwargs):
|
|
37
|
+
r"""
|
|
38
|
+
Calculate surface area from perimeter and length -
|
|
39
|
+
perimeter calculated when throat area is calculated so must be run in correct order
|
|
40
|
+
"""
|
|
41
|
+
P = geometry[throat_perimeter]
|
|
42
|
+
L = geometry[throat_length]
|
|
43
|
+
value = P*L
|
|
44
|
+
return value
|