cmflow 0.0.0__py3-none-any.whl
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.
- cmflow/__init__.py +0 -0
- cmflow/conceptual_models.py +631 -0
- cmflow/geom_3dface_utils.py +409 -0
- cmflow/geom_surface_utils.py +457 -0
- cmflow/t2data_utils.py +458 -0
- cmflow/test_conceptual_models.py +285 -0
- cmflow/test_geom_3dface_utils.py +113 -0
- cmflow/test_geom_surface_utils.py +65 -0
- cmflow-0.0.0.dist-info/METADATA +140 -0
- cmflow-0.0.0.dist-info/RECORD +12 -0
- cmflow-0.0.0.dist-info/WHEEL +5 -0
- cmflow-0.0.0.dist-info/top_level.txt +1 -0
cmflow/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
from mulgrids import *
|
|
2
|
+
from t2data import *
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
# for blocky CM
|
|
7
|
+
from shapely.geometry import Polygon
|
|
8
|
+
from shapely.geometry import LineString
|
|
9
|
+
|
|
10
|
+
# for faults CM
|
|
11
|
+
from cmflow.geom_3dface_utils import Face3D
|
|
12
|
+
from cmflow.geom_surface_utils import get_columns_intersect_polygon
|
|
13
|
+
from cmflow.geom_surface_utils import geo_column_polygon
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
START = [time.time()]
|
|
19
|
+
def print_wall_time(msg, loop_stop=False, total=False):
|
|
20
|
+
t = time.time()
|
|
21
|
+
if total:
|
|
22
|
+
print(msg, ': %f seconds' % (t - START[0]))
|
|
23
|
+
else:
|
|
24
|
+
print(msg, ': %f seconds' % (t - START[-1]))
|
|
25
|
+
if loop_stop:
|
|
26
|
+
START.append(t)
|
|
27
|
+
|
|
28
|
+
class LeapfrogGM(object):
|
|
29
|
+
def __init__(self, geometry=''):
|
|
30
|
+
super(LeapfrogGM, self).__init__()
|
|
31
|
+
self.litholist = []
|
|
32
|
+
self.blocklitho = {}
|
|
33
|
+
self.import_from = '' # optional, if imported from leapfrog
|
|
34
|
+
self.geometry = geometry # optional, matching mulgrid geometry file
|
|
35
|
+
|
|
36
|
+
def import_leapfrog_csv(self, filename, report=False):
|
|
37
|
+
""" load geology info from Leapfrog's 'Generate rock types' feature. The
|
|
38
|
+
.csv file from Leapfrog usually starts with a table of 'LithoCode,Lithology'
|
|
39
|
+
then a longer table of 'BlockName,LithoCode'. Both of these will be
|
|
40
|
+
retuened as dictionaries. """
|
|
41
|
+
import csv
|
|
42
|
+
f = open(filename,'r')
|
|
43
|
+
allrows = csv.reader(f)
|
|
44
|
+
lithocodes, self.litholist = {}, []
|
|
45
|
+
self.blocklitho = {}
|
|
46
|
+
# TODO: this file reading is ugly, need work
|
|
47
|
+
read_litho_name, read_block_litho = False, False
|
|
48
|
+
for row in allrows:
|
|
49
|
+
if len(row) == 1: continue
|
|
50
|
+
if len(row) == 3:
|
|
51
|
+
if row[0] == '#' and row[1] == 'LithoCode':
|
|
52
|
+
read_litho_name = True
|
|
53
|
+
continue
|
|
54
|
+
if len(row) == 2:
|
|
55
|
+
if row[0] == 'BlockName' and row[1] == 'LithoCode':
|
|
56
|
+
read_litho_name = False
|
|
57
|
+
read_block_litho = True
|
|
58
|
+
continue
|
|
59
|
+
if read_litho_name:
|
|
60
|
+
if not row[0] == '#': raise Exception
|
|
61
|
+
self.litholist.append(row[2])
|
|
62
|
+
lithocodes[int(row[1])] = len(self.litholist)-1
|
|
63
|
+
if read_block_litho:
|
|
64
|
+
if len(row) != 2: break
|
|
65
|
+
self.blocklitho[row[0]] = lithocodes[int(row[1])]
|
|
66
|
+
f.close()
|
|
67
|
+
self.import_from = filename
|
|
68
|
+
if report:
|
|
69
|
+
print('%8s Lithology found.' % len(self.litholist))
|
|
70
|
+
print('%8s Blocks allocated.' % len(self.blocklitho))
|
|
71
|
+
|
|
72
|
+
def write(self, filename):
|
|
73
|
+
with open(filename, 'w') as f:
|
|
74
|
+
data = {
|
|
75
|
+
"comments": [
|
|
76
|
+
"Simplified Leapfrog Geological Model",
|
|
77
|
+
"zones: a list of zone names",
|
|
78
|
+
],
|
|
79
|
+
"import_from" : self.import_from,
|
|
80
|
+
"geometry" : self.geometry,
|
|
81
|
+
"zones": self.litholist,
|
|
82
|
+
"blocks": self.blocklitho,
|
|
83
|
+
}
|
|
84
|
+
json.dump(data, f, indent=4, sort_keys=True)
|
|
85
|
+
|
|
86
|
+
def read(self, filename):
|
|
87
|
+
with open(filename, 'r') as f:
|
|
88
|
+
data = json.load(f)
|
|
89
|
+
self.import_from = data['import_from']
|
|
90
|
+
self.geometry = data['geometry']
|
|
91
|
+
self.litholist = data['zones']
|
|
92
|
+
self.blocklitho = data['blocks']
|
|
93
|
+
|
|
94
|
+
class CM(object):
|
|
95
|
+
def populate_model(self, np_dtype):
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
def calc_bmstats(self, geo, np_dtype=np.single):
|
|
99
|
+
stats, zones = self.populate_model(geo, np_dtype)
|
|
100
|
+
bms = BMStats(geo=geo, stats=stats, zones=zones)
|
|
101
|
+
return bms
|
|
102
|
+
|
|
103
|
+
class CM_Blocky(CM):
|
|
104
|
+
""" A nceptual Model object is a model that represents a space by a list of
|
|
105
|
+
zones (usually exists as a blocky model, each block has a *rocktype*). """
|
|
106
|
+
def __init__(self, geo, grid):
|
|
107
|
+
""" initialise CM model by load mulgrid geometry and t2grid objects """
|
|
108
|
+
super(CM_Blocky, self).__init__()
|
|
109
|
+
|
|
110
|
+
if isinstance(geo, str):
|
|
111
|
+
self.geo = mulgrid(geo)
|
|
112
|
+
elif isinstance(geo, mulgrid):
|
|
113
|
+
self.geo = geo
|
|
114
|
+
else:
|
|
115
|
+
raise Exception("Unable to load mulgrid geometry file")
|
|
116
|
+
|
|
117
|
+
if isinstance(grid, str):
|
|
118
|
+
self._load_from_t2grid(t2data(grid).grid)
|
|
119
|
+
elif isinstance(grid, t2data):
|
|
120
|
+
self._load_from_t2grid(grid.grid)
|
|
121
|
+
elif isinstance(grid, t2grid):
|
|
122
|
+
self._load_from_t2grid(grid)
|
|
123
|
+
elif isinstance(grid, LeapfrogGM):
|
|
124
|
+
# if LeapfrogGM object
|
|
125
|
+
self.zones = grid.litholist
|
|
126
|
+
self.block = grid.blocklitho
|
|
127
|
+
elif grid is None:
|
|
128
|
+
# customised cm
|
|
129
|
+
# !!! NOTE, .populate_model() might not work
|
|
130
|
+
self.zones = []
|
|
131
|
+
self.block = {}
|
|
132
|
+
else:
|
|
133
|
+
raise Exception("Unable to load t2grid or LeapfrogGM object")
|
|
134
|
+
|
|
135
|
+
# caching objects, so can call populate multiple times efficiently
|
|
136
|
+
self._inter_areas, self._bm_col_ccis = None, None
|
|
137
|
+
self._inter_lengths, self._bm_lay_clis = None, None
|
|
138
|
+
self._bm_idx = None
|
|
139
|
+
|
|
140
|
+
self.num_zones = len(self.zones)
|
|
141
|
+
|
|
142
|
+
def _load_from_t2grid(self, grid):
|
|
143
|
+
print_wall_time('_load_from_t2grid()', loop_stop=True)
|
|
144
|
+
self.zones = [r.name for r in grid.rocktypelist]
|
|
145
|
+
print_wall_time(' created zones from t2grid', loop_stop=True)
|
|
146
|
+
self.block = {b: grid.rocktypelist.index(grid.block[b].rocktype) for b in self.geo.block_name_list}
|
|
147
|
+
print_wall_time(' created block zone dict (old method)', loop_stop=True)
|
|
148
|
+
|
|
149
|
+
def column_intersect_area(self, geo):
|
|
150
|
+
""" return an two-d array of (absolute) area of column intersections
|
|
151
|
+
between the model column and CM columns.
|
|
152
|
+
|
|
153
|
+
'bm_col_ccis' is a list the same length/order as geo.block_name_list, each
|
|
154
|
+
element is a (varying length) list of cm columns that intersects bm
|
|
155
|
+
column.
|
|
156
|
+
"""
|
|
157
|
+
from rtree import index
|
|
158
|
+
if self._inter_areas is not None and self._bm_col_ccis is not None:
|
|
159
|
+
return self._inter_areas, self._bm_col_ccis
|
|
160
|
+
|
|
161
|
+
bm_polys = [Polygon([n.pos for n in c.node]) for c in geo.columnlist]
|
|
162
|
+
print_wall_time(' constructed all %i BM polys' % geo.num_columns, loop_stop=True)
|
|
163
|
+
|
|
164
|
+
# CM usually has larger number of columns and is regular, so I should
|
|
165
|
+
# probably do RTree on CM grid
|
|
166
|
+
cm_idx = index.Index()
|
|
167
|
+
cm_polys = [Polygon([n.pos for n in c.node]) for c in self.geo.columnlist]
|
|
168
|
+
print_wall_time(' constructed all %i CM polys' % self.geo.num_columns, loop_stop=True)
|
|
169
|
+
for i,poly in enumerate(cm_polys):
|
|
170
|
+
cm_idx.insert(i, poly.bounds)
|
|
171
|
+
print_wall_time(' constructed CM polys RTree', loop_stop=True)
|
|
172
|
+
|
|
173
|
+
bm_col_ccis = [] # list of CM col indices that intersects BM columns
|
|
174
|
+
areas = np.zeros((geo.num_columns, self.geo.num_columns))
|
|
175
|
+
for i,bm_poly in enumerate(bm_polys):
|
|
176
|
+
bm_col_ccis.append([])
|
|
177
|
+
for j in cm_idx.intersection(bm_poly.bounds):
|
|
178
|
+
areas[i,j] = bm_poly.intersection(cm_polys[j]).area
|
|
179
|
+
if areas[i,j] > 0.0:
|
|
180
|
+
bm_col_ccis[-1].append(j)
|
|
181
|
+
print_wall_time(' finished creating column intersection array', loop_stop=True)
|
|
182
|
+
|
|
183
|
+
self._inter_areas, self._bm_col_ccis = areas, bm_col_ccis
|
|
184
|
+
return areas, bm_col_ccis
|
|
185
|
+
|
|
186
|
+
def layer_intersect_length(self, geo):
|
|
187
|
+
if self._inter_lengths is not None and self._bm_lay_clis is not None:
|
|
188
|
+
return self._inter_lengths, self._bm_lay_clis
|
|
189
|
+
|
|
190
|
+
bm_lines = [LineString([(lay.bottom,0), (lay.top,0)]) for lay in geo.layerlist]
|
|
191
|
+
cm_lines = [LineString([(lay.bottom,0), (lay.top,0)]) for lay in self.geo.layerlist]
|
|
192
|
+
bm_lay_clis = [] # list of CM lay indices that intersects BM layers
|
|
193
|
+
lengths = np.zeros((geo.num_layers, self.geo.num_layers))
|
|
194
|
+
for i,bm_line in enumerate(bm_lines):
|
|
195
|
+
bm_lay_clis.append([])
|
|
196
|
+
for j,cm_line in enumerate(cm_lines):
|
|
197
|
+
lengths[i,j] = bm_line.intersection(cm_line).length
|
|
198
|
+
if lengths[i,j] > 0.0:
|
|
199
|
+
bm_lay_clis[-1].append(j)
|
|
200
|
+
|
|
201
|
+
self._inter_lengths, self._bm_lay_clis = lengths, bm_lay_clis
|
|
202
|
+
return lengths, bm_lay_clis
|
|
203
|
+
|
|
204
|
+
def populate_model(self, geo, np_dtype=np.single):
|
|
205
|
+
""" This is the core of the CM processing, fill-in the stats array.
|
|
206
|
+
|
|
207
|
+
stats array rows are of base model blocks, and columns of the zones from
|
|
208
|
+
CM. Each cell is the portion of the block occupied by the zone. In
|
|
209
|
+
most cases, the total of each row should be 1.0.
|
|
210
|
+
"""
|
|
211
|
+
inter_areas, bm_col_ccis = self.column_intersect_area(geo)
|
|
212
|
+
print_wall_time(' column_intersect_area() finished: ',
|
|
213
|
+
loop_stop=True)
|
|
214
|
+
inter_lengths, bm_lay_clis = self.layer_intersect_length(geo)
|
|
215
|
+
print_wall_time(' layer_intersect_length() finished: ',
|
|
216
|
+
loop_stop=True)
|
|
217
|
+
|
|
218
|
+
stats = np.zeros((geo.num_blocks, self.num_zones), dtype=np_dtype)
|
|
219
|
+
print_wall_time(' created stats array %i x %i' % (geo.num_blocks, self.num_zones), loop_stop=True)
|
|
220
|
+
|
|
221
|
+
def setup_block_name_index_fast(geo):
|
|
222
|
+
""" based on mulgrid.setup_block_name_index()
|
|
223
|
+
|
|
224
|
+
Note atmosphere blocks may not have proper column or layer index,
|
|
225
|
+
None would be used in place.
|
|
226
|
+
"""
|
|
227
|
+
block_ij_list = [] # (coli, layj)
|
|
228
|
+
if geo.num_layers > 0:
|
|
229
|
+
if geo.atmosphere_type == 0: # one atmosphere block
|
|
230
|
+
# bn = geo.block_name(geo.layerlist[0].name, geo.atmosphere_column_name)
|
|
231
|
+
block_ij_list.append((None, None))
|
|
232
|
+
elif geo.atmosphere_type == 1: # one atmosphere block per column
|
|
233
|
+
for i,col in enumerate(geo.columnlist):
|
|
234
|
+
# bn = geo.block_name(geo.layerlist[0].name, col.name)
|
|
235
|
+
block_ij_list.append((i, None))
|
|
236
|
+
for j,lay in enumerate(geo.layerlist[1:]):
|
|
237
|
+
for i,col in [(ii,col) for ii,col in enumerate(geo.columnlist) if col.surface > lay.bottom]:
|
|
238
|
+
# bn = geo.block_name(lay.name, col.name)
|
|
239
|
+
block_ij_list.append((i, j+1))
|
|
240
|
+
return block_ij_list
|
|
241
|
+
|
|
242
|
+
# cm_idx = setup_block_name_index_fast(self.geo)
|
|
243
|
+
# print_wall_time('created col/lay idx for CM (new method)', loop_stop=True)
|
|
244
|
+
if self._bm_idx is None:
|
|
245
|
+
self._bm_idx = setup_block_name_index_fast(geo)
|
|
246
|
+
print_wall_time(' created col/lay idx for BM (new method)', loop_stop=True)
|
|
247
|
+
|
|
248
|
+
### calculating and fillinf stats here
|
|
249
|
+
for ii,(bm_ci,bm_li) in enumerate(self._bm_idx):
|
|
250
|
+
if bm_ci is None or bm_li is None:
|
|
251
|
+
# atmosphere blocks, skip
|
|
252
|
+
continue
|
|
253
|
+
bvol = geo.block_volume(geo.layerlist[bm_li], geo.columnlist[bm_ci])
|
|
254
|
+
# only do check columns and layers that actually intersect current BM block
|
|
255
|
+
for cm_ci in bm_col_ccis[bm_ci]:
|
|
256
|
+
for cm_li in bm_lay_clis[bm_li]:
|
|
257
|
+
ivol = inter_areas[bm_ci,cm_ci] * inter_lengths[bm_li,cm_li]
|
|
258
|
+
if ivol > 0.0:
|
|
259
|
+
cb = self.geo.block_name(self.geo.layerlist[cm_li].name,
|
|
260
|
+
self.geo.columnlist[cm_ci].name)
|
|
261
|
+
if cb not in self.block:
|
|
262
|
+
continue
|
|
263
|
+
zi = self.block[cb]
|
|
264
|
+
# zi = self.block[self.geo.block_name_index(cb)]
|
|
265
|
+
stats[ii,zi] = stats[ii,zi] + ivol / bvol
|
|
266
|
+
print_wall_time(' Finished calculating/filling stats', loop_stop=True)
|
|
267
|
+
return stats, self.zones
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class CM_Prism(CM):
|
|
271
|
+
def __init__(self, name, polygon, ztop, zbottom):
|
|
272
|
+
""" create a conceptual mode
|
|
273
|
+
"""
|
|
274
|
+
super(CM_Prism, self).__init__()
|
|
275
|
+
self.name = name
|
|
276
|
+
self.polygon = polygon
|
|
277
|
+
self.ztop, self.zbottom = ztop, zbottom
|
|
278
|
+
|
|
279
|
+
def column_intersect_area(self, bm_geo):
|
|
280
|
+
bm_polys = geo_column_polygon(bm_geo)
|
|
281
|
+
areas = np.zeros(bm_geo.num_columns)
|
|
282
|
+
for i,bm_poly in enumerate(bm_polys):
|
|
283
|
+
areas[i] = self.polygon.intersection(bm_poly).area
|
|
284
|
+
print_wall_time(' finished creating column intersection array', loop_stop=True)
|
|
285
|
+
return areas
|
|
286
|
+
|
|
287
|
+
def layer_intersect_length(self, bm_geo):
|
|
288
|
+
bm_lines = [LineString([(lay.bottom,0), (lay.top,0)]) for lay in bm_geo.layerlist]
|
|
289
|
+
cm_line = LineString([(self.ztop,0), (self.zbottom,0)])
|
|
290
|
+
lengths = np.zeros(bm_geo.num_layers)
|
|
291
|
+
for i,bm_line in enumerate(bm_lines):
|
|
292
|
+
lengths[i] = bm_line.intersection(cm_line).length
|
|
293
|
+
return lengths
|
|
294
|
+
|
|
295
|
+
def populate_model(self, bm_geo, np_dtype=np.single):
|
|
296
|
+
""" This is the core of the CM processing, fill-in the stats array.
|
|
297
|
+
|
|
298
|
+
stats array rows are of base model blocks, and columns of the zones from
|
|
299
|
+
CM. Each cell is the portion of the block occupied by the zone. In
|
|
300
|
+
most cases, the total of each row should be 1.0.
|
|
301
|
+
"""
|
|
302
|
+
def setup_block_name_index_fast(bm_geo):
|
|
303
|
+
""" based on mulgrid.setup_block_name_index()
|
|
304
|
+
|
|
305
|
+
Note atmosphere blocks may not have proper column or layer index,
|
|
306
|
+
None would be used in place.
|
|
307
|
+
"""
|
|
308
|
+
block_ij_list = [] # (coli, layj)
|
|
309
|
+
if bm_geo.num_layers > 0:
|
|
310
|
+
if bm_geo.atmosphere_type == 0: # one atmosphere block
|
|
311
|
+
# bn = bm_geo.block_name(bm_geo.layerlist[0].name, bm_geo.atmosphere_column_name)
|
|
312
|
+
block_ij_list.append((None, None))
|
|
313
|
+
elif bm_geo.atmosphere_type == 1: # one atmosphere block per column
|
|
314
|
+
for i,col in enumerate(bm_geo.columnlist):
|
|
315
|
+
# bn = bm_geo.block_name(bm_geo.layerlist[0].name, col.name)
|
|
316
|
+
block_ij_list.append((i, None))
|
|
317
|
+
for j,lay in enumerate(bm_geo.layerlist[1:]):
|
|
318
|
+
for i,col in [(ii,col) for ii,col in enumerate(bm_geo.columnlist) if col.surface > lay.bottom]:
|
|
319
|
+
# bn = bm_geo.block_name(lay.name, col.name)
|
|
320
|
+
block_ij_list.append((i, j+1))
|
|
321
|
+
return block_ij_list
|
|
322
|
+
|
|
323
|
+
inter_areas = self.column_intersect_area(bm_geo)
|
|
324
|
+
print_wall_time(' column_intersect_area() finished: ', loop_stop=True)
|
|
325
|
+
inter_lengths = self.layer_intersect_length(bm_geo)
|
|
326
|
+
print_wall_time(' layer_intersect_length() finished: ', loop_stop=True)
|
|
327
|
+
|
|
328
|
+
stats = np.zeros((bm_geo.num_blocks, 1), dtype=np_dtype)
|
|
329
|
+
|
|
330
|
+
bm_idx = setup_block_name_index_fast(bm_geo)
|
|
331
|
+
print_wall_time(' created col/lay idx for BM (new method)', loop_stop=True)
|
|
332
|
+
|
|
333
|
+
### calculating and fill-in stats here
|
|
334
|
+
for ii,(bm_ci,bm_li) in enumerate(bm_idx):
|
|
335
|
+
if bm_ci is None or bm_li is None:
|
|
336
|
+
# atmosphere blocks, skip
|
|
337
|
+
continue
|
|
338
|
+
bvol = bm_geo.block_volume(bm_geo.layerlist[bm_li], bm_geo.columnlist[bm_ci])
|
|
339
|
+
ivol = inter_areas[bm_ci] * inter_lengths[bm_li]
|
|
340
|
+
if ivol > 0.0:
|
|
341
|
+
stats[ii,0] = stats[ii,0] + ivol / bvol
|
|
342
|
+
print_wall_time(' Finished calculating/filling stats', loop_stop=True)
|
|
343
|
+
return stats, [self.name]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class CM_Faults(CM):
|
|
347
|
+
""" Conceptual Model of faults as 3D surface (Face3D *.ts objects)
|
|
348
|
+
|
|
349
|
+
NOTE this simplements the simple way of getting blocks crossed by faults.
|
|
350
|
+
Instead of 3D Face cutting across 3D blocks/elements, I simply let 3D Face
|
|
351
|
+
cuts across layer centre plane.
|
|
352
|
+
|
|
353
|
+
if dilation is specified as a positive float, then the line will be dilated
|
|
354
|
+
(.buffer) with the specified amount. This changes the behaviour of stats,
|
|
355
|
+
instead of two possible values of 0.0/1.0 for normal fault line case, this
|
|
356
|
+
will return stats with intersection area ratio as other CMs.
|
|
357
|
+
"""
|
|
358
|
+
def __init__(self, faults=None, dilation=None):
|
|
359
|
+
import os.path
|
|
360
|
+
super(CM_Faults, self).__init__()
|
|
361
|
+
self.zones = []
|
|
362
|
+
self.fault = {}
|
|
363
|
+
self.dilation = None
|
|
364
|
+
if isinstance(dilation, float):
|
|
365
|
+
if dilation > 0.0:
|
|
366
|
+
self.dilation = dilation
|
|
367
|
+
print('Fault uses dilation')
|
|
368
|
+
if faults is None:
|
|
369
|
+
pass
|
|
370
|
+
elif isinstance(faults, list):
|
|
371
|
+
# a list of *.ts files to load
|
|
372
|
+
for filename in faults:
|
|
373
|
+
fault = Face3D()
|
|
374
|
+
fault.read(filename)
|
|
375
|
+
zonename = os.path.splitext(filename)[0]
|
|
376
|
+
self.zones.append(zonename)
|
|
377
|
+
self.fault[zonename] = fault
|
|
378
|
+
elif isinstance(faults, dict):
|
|
379
|
+
# dictionary of Face3D objects
|
|
380
|
+
for zonename in sorted(faults.keys()):
|
|
381
|
+
self.zones.append(zonename)
|
|
382
|
+
self.fault = faults
|
|
383
|
+
else:
|
|
384
|
+
raise Exception
|
|
385
|
+
self.num_zones = len(self.zones)
|
|
386
|
+
|
|
387
|
+
def populate_model(self, geo, np_dtype=np.single):
|
|
388
|
+
|
|
389
|
+
def column_intersect_area(dilated_line, bm_geo):
|
|
390
|
+
bm_polys = geo_column_polygon(bm_geo)
|
|
391
|
+
areas = np.zeros(bm_geo.num_columns)
|
|
392
|
+
for i,bm_poly in enumerate(bm_polys):
|
|
393
|
+
areas[i] = dilated_line.intersection(bm_poly).area
|
|
394
|
+
print_wall_time(' finished creating column intersection array', loop_stop=True)
|
|
395
|
+
return areas
|
|
396
|
+
|
|
397
|
+
def column_polygons(geo):
|
|
398
|
+
# CM usually has larger number of columns and is regular, so I should
|
|
399
|
+
# probably do RTree on CM grid
|
|
400
|
+
from rtree import index
|
|
401
|
+
column_idx = index.Index()
|
|
402
|
+
column_polys = [Polygon([n.pos for n in c.node]) for c in geo.columnlist]
|
|
403
|
+
print_wall_time(' constructed all %i column polygons' % geo.num_columns, loop_stop=True)
|
|
404
|
+
for i,poly in enumerate(column_polys):
|
|
405
|
+
column_idx.insert(i, poly.bounds)
|
|
406
|
+
print_wall_time(' constructed column polygons RTree', loop_stop=True)
|
|
407
|
+
return column_polys, column_idx
|
|
408
|
+
|
|
409
|
+
def setup_block_name_index_fast(geo):
|
|
410
|
+
""" based on mulgrid.setup_block_name_index()
|
|
411
|
+
|
|
412
|
+
Note atmosphere blocks may not have proper column or layer index,
|
|
413
|
+
None would be used in place.
|
|
414
|
+
"""
|
|
415
|
+
block_ij_idx = {} # { (coli, layj): block index }
|
|
416
|
+
bi = 0
|
|
417
|
+
if geo.num_layers > 0:
|
|
418
|
+
if geo.atmosphere_type == 0: # one atmosphere block
|
|
419
|
+
# bn = geo.block_name(geo.layerlist[0].name, geo.atmosphere_column_name)
|
|
420
|
+
block_ij_idx[(None, 0)] = bi
|
|
421
|
+
bi += 1
|
|
422
|
+
elif geo.atmosphere_type == 1: # one atmosphere block per column
|
|
423
|
+
for i,col in enumerate(geo.columnlist):
|
|
424
|
+
# bn = geo.block_name(geo.layerlist[0].name, col.name)
|
|
425
|
+
block_ij_idx[(i, 0)] = bi
|
|
426
|
+
bi += 1
|
|
427
|
+
for j,lay in enumerate(geo.layerlist[1:]):
|
|
428
|
+
for i,col in [(ii,col) for ii,col in enumerate(geo.columnlist) if col.surface > lay.bottom]:
|
|
429
|
+
# bn = geo.block_name(lay.name, col.name)
|
|
430
|
+
block_ij_idx[(i, j+1)] = bi
|
|
431
|
+
bi += 1
|
|
432
|
+
return block_ij_idx
|
|
433
|
+
|
|
434
|
+
stats = np.zeros((geo.num_blocks, self.num_zones), dtype=np_dtype)
|
|
435
|
+
col_polys, col_idx = column_polygons(geo)
|
|
436
|
+
block_ij_idx = setup_block_name_index_fast(geo)
|
|
437
|
+
print_wall_time(' setup_block_name_index_fast()', loop_stop=True)
|
|
438
|
+
for jj,lay in enumerate(geo.layerlist):
|
|
439
|
+
count = 0 # intersected blocks count, per layer
|
|
440
|
+
z = lay.centre
|
|
441
|
+
for fi,fname in enumerate(self.zones):
|
|
442
|
+
fault = self.fault[fname]
|
|
443
|
+
fault.set_cutting_plane((0.0,0.0,z), (0.0,0.0,1.0))
|
|
444
|
+
pts = fault.search_line()
|
|
445
|
+
if len(pts) < 2:
|
|
446
|
+
# not enough points to construct LineString, layer not
|
|
447
|
+
# cutting through the fault Face
|
|
448
|
+
print(" skipping layer %i '%s' with fault '%s'" % (jj, lay.name, fname))
|
|
449
|
+
continue
|
|
450
|
+
line = LineString([tuple(pt[:2]) for pt in pts])
|
|
451
|
+
if self.dilation is not None:
|
|
452
|
+
line = line.buffer(self.dilation)
|
|
453
|
+
for ii in col_idx.intersection(line.bounds):
|
|
454
|
+
iarea = line.intersection(col_polys[ii]).area
|
|
455
|
+
if iarea > 0.0:
|
|
456
|
+
if (ii,jj) in block_ij_idx:
|
|
457
|
+
bi = block_ij_idx[(ii,jj)]
|
|
458
|
+
c = geo.column_name(geo.block_name_list[bi])
|
|
459
|
+
carea = geo.column[c].area
|
|
460
|
+
stats[bi,fi] = stats[bi,fi] + iarea / carea
|
|
461
|
+
count += 1
|
|
462
|
+
else:
|
|
463
|
+
for ii in col_idx.intersection(line.bounds):
|
|
464
|
+
if line.intersection(col_polys[ii]).length > 0.0:
|
|
465
|
+
if (ii,jj) in block_ij_idx:
|
|
466
|
+
bi = block_ij_idx[(ii,jj)]
|
|
467
|
+
stats[bi,fi] = 1.0
|
|
468
|
+
count += 1
|
|
469
|
+
# print 'found ', geo.block_name_list[bi]
|
|
470
|
+
print_wall_time(' finished layer %i, found %i blocks' % (jj, count), loop_stop=True)
|
|
471
|
+
return stats, self.zones
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class BMStatsError(Exception):
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
class BMStats(object):
|
|
478
|
+
""" Base Model Stats, mainly numpy arrays with rows corresponding to mulgrid
|
|
479
|
+
blocks, and columns corresponding to zones. Each is a value, usually
|
|
480
|
+
between 0.0 and 1.0. Often 1.0 is indicating that particular block is fully
|
|
481
|
+
within the zone.
|
|
482
|
+
|
|
483
|
+
.stats numpy array (n,m), n = num of model blocks, m = num of zones
|
|
484
|
+
.zones list of zone names (str)
|
|
485
|
+
.zonestats dict of stats column by zone names
|
|
486
|
+
.cellstats dict of stats row by block name
|
|
487
|
+
"""
|
|
488
|
+
def __init__(self, filename=None, geo=None, stats=None, zones=None,
|
|
489
|
+
np_dtype=np.single):
|
|
490
|
+
"""
|
|
491
|
+
Usage:
|
|
492
|
+
# will load: .json .npy and geometry file
|
|
493
|
+
bms = BMStats('abc.json')
|
|
494
|
+
|
|
495
|
+
# geo already loaded, "geometry" in abc.json no longer matter
|
|
496
|
+
bms = BMStats('abc.json', geo=geo)
|
|
497
|
+
|
|
498
|
+
# new empty BMStats, using pre-loaded geo
|
|
499
|
+
geo = mulgrid('xyz.dat')
|
|
500
|
+
bms = BMStats(geo=geo)
|
|
501
|
+
"""
|
|
502
|
+
# load from file
|
|
503
|
+
if filename is not None:
|
|
504
|
+
if geo is None:
|
|
505
|
+
self.load(filename)
|
|
506
|
+
else:
|
|
507
|
+
# if geo is specified (pre-loaded)
|
|
508
|
+
self.geo = geo
|
|
509
|
+
self.load(filename, load_geo=False)
|
|
510
|
+
return
|
|
511
|
+
# new/empty
|
|
512
|
+
self.geo, self.stats, self.zones = geo, stats, zones
|
|
513
|
+
if geo is None:
|
|
514
|
+
raise BMStatsError("New/empty BMStats requires a valid mulgrid geometry passed in as 'geo'")
|
|
515
|
+
n = geo.num_blocks
|
|
516
|
+
if zones is None:
|
|
517
|
+
self.zones = []
|
|
518
|
+
if stats is None:
|
|
519
|
+
self.stats = np.zeros((n, len(self.zones)), dtype=np_dtype) # 'empty' array, ready to concatenate etc
|
|
520
|
+
self._reindex()
|
|
521
|
+
|
|
522
|
+
def __repr__(self):
|
|
523
|
+
r = []
|
|
524
|
+
sh = self.stats.shape
|
|
525
|
+
r.append('BMStats with {} zones and {} cells'.format(sh[1], sh[0]))
|
|
526
|
+
if self.filename:
|
|
527
|
+
r.append(", file '{}'".format(self.filename))
|
|
528
|
+
return ''.join(r)
|
|
529
|
+
|
|
530
|
+
def _reindex(self):
|
|
531
|
+
if self.stats.shape != (self.geo.num_blocks, len(self.zones)):
|
|
532
|
+
raise BMStatsError('.stats shape {} mismatches (.geo.num_blocks, len(.zones)) {}'.format(self.stats.shape, (self.geo.num_blocks, len(self.zones))))
|
|
533
|
+
self.zonestats = {z:self.stats[:,i] for i,z in enumerate(self.zones)}
|
|
534
|
+
self.cellstats = {b:self.stats[i,:] for i,b in enumerate(self.geo.block_name_list)}
|
|
535
|
+
|
|
536
|
+
def save(self, filename):
|
|
537
|
+
import os.path
|
|
538
|
+
root, ext = os.path.splitext(filename)
|
|
539
|
+
npy_filename = root + '.npy'
|
|
540
|
+
np.save(npy_filename, self.stats)
|
|
541
|
+
# the .npy file will be expected in the same directory, so strip dir
|
|
542
|
+
npy_filename = os.path.split(npy_filename)[1]
|
|
543
|
+
data = {
|
|
544
|
+
"comments": [
|
|
545
|
+
"BM geometry: %s" % self.geo.filename,
|
|
546
|
+
],
|
|
547
|
+
"stats": npy_filename,
|
|
548
|
+
"zones": self.zones,
|
|
549
|
+
"geometry": None,
|
|
550
|
+
}
|
|
551
|
+
if self.geo is not None:
|
|
552
|
+
data["geometry"] = self.geo.filename
|
|
553
|
+
with open(filename, 'w') as f:
|
|
554
|
+
json.dump(data, f, indent=True, sort_keys=True)
|
|
555
|
+
self.filename = filename
|
|
556
|
+
|
|
557
|
+
def load(self, filename, load_geo=True):
|
|
558
|
+
import os.path
|
|
559
|
+
with open(filename, 'r') as f:
|
|
560
|
+
data = json.load(f)
|
|
561
|
+
if load_geo:
|
|
562
|
+
self.geo = mulgrid(data['geometry'])
|
|
563
|
+
self.zones = data['zones']
|
|
564
|
+
# .npy file expected relative to the json file
|
|
565
|
+
npy_filename = os.path.join(os.path.split(filename)[0], data["stats"])
|
|
566
|
+
self.stats = np.load(npy_filename)
|
|
567
|
+
n = self.stats.shape[0]
|
|
568
|
+
if n != self.geo.num_blocks:
|
|
569
|
+
msg1 = 'Loaded BMStats has different number of blocks to geometry file.'
|
|
570
|
+
msg2 = 'BMStats (%i) != Geometry (%i)' % (n, self.geo.num_blocks)
|
|
571
|
+
raise BMStatsError('\n'.join([msg1, msg2]))
|
|
572
|
+
self.filename = filename
|
|
573
|
+
self._reindex()
|
|
574
|
+
|
|
575
|
+
def add_zone(self, zone, stats):
|
|
576
|
+
""" append a single zone, with stats an array (n,1) n is number of
|
|
577
|
+
blocks
|
|
578
|
+
"""
|
|
579
|
+
if zone in self.zones:
|
|
580
|
+
raise BMStatsError('zone {} already in BMStats.zones'.format(zone))
|
|
581
|
+
self.zones.append(zone)
|
|
582
|
+
stats = np.reshape(np.array(stats), (self.stats.shape[0], 1))
|
|
583
|
+
self.stats = np.concatenate((self.stats, stats), axis=1)
|
|
584
|
+
self._reindex()
|
|
585
|
+
|
|
586
|
+
def add_stats(self, stats, zones):
|
|
587
|
+
for i,zz in enumerate(zones):
|
|
588
|
+
ss = stats[:,i:i+1]
|
|
589
|
+
if zz in self.zones:
|
|
590
|
+
ii = self.zones.index(zz)
|
|
591
|
+
self.stats[:,ii] = self.stats[:,ii] + ss[:,0]
|
|
592
|
+
else:
|
|
593
|
+
self.stats = np.concatenate((self.stats, ss), axis=1)
|
|
594
|
+
self.zones.append(zz)
|
|
595
|
+
self._reindex()
|
|
596
|
+
|
|
597
|
+
def add_cm(self, cm):
|
|
598
|
+
stats, zones = cm.populate_model(self.geo)
|
|
599
|
+
self.add_stats(stats, zones)
|
|
600
|
+
|
|
601
|
+
def blocks_in_zone(self, zone, indices=False):
|
|
602
|
+
""" Returns a tuple of (blocks, ratios). blocks is a list of all blocks
|
|
603
|
+
intersect the zone and ratios are portions of each respective block's
|
|
604
|
+
intersection with the zone. If indices is set to True, block indices
|
|
605
|
+
are returned instead of block names.
|
|
606
|
+
|
|
607
|
+
It is assumed self.geo is a laoded mulgrid object.
|
|
608
|
+
|
|
609
|
+
Ratios calculation assumes the whole zone is fully within the BM.
|
|
610
|
+
"""
|
|
611
|
+
nis = np.nonzero(self.zonestats[zone])[0]
|
|
612
|
+
nvs, nbs = [], []
|
|
613
|
+
for i in nis:
|
|
614
|
+
b_name = self.geo.block_name_list[i]
|
|
615
|
+
b_lay = self.geo.layer[self.geo.layer_name(b_name)]
|
|
616
|
+
b_col = self.geo.column[self.geo.column_name(b_name)]
|
|
617
|
+
nvs.append(self.geo.block_volume(b_lay, b_col) * self.zonestats[zone][i])
|
|
618
|
+
nbs.append(b_name)
|
|
619
|
+
nrs = [v/sum(nvs) for v in nvs]
|
|
620
|
+
if indices:
|
|
621
|
+
return nis, nrs
|
|
622
|
+
else:
|
|
623
|
+
return nbs, nrs
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
if __name__ == '__main__':
|
|
628
|
+
# test_cm_blocky_full()
|
|
629
|
+
# test_cm_fault_full()
|
|
630
|
+
test_zonestats_small()
|
|
631
|
+
pass
|