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 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