datagenerator.Closures
1import os 2 3import numpy as np 4from datagenerator.Horizons import Horizons 5from datagenerator.Geomodels import Geomodel 6from datagenerator.Parameters import Parameters 7from skimage import morphology, measure 8from scipy.ndimage import minimum_filter, maximum_filter 9 10 11class Closures(Horizons, Geomodel, Parameters): 12 def __init__(self, parameters, faults, facies, onlap_horizon_list): 13 self.closure_dict = dict() 14 self.cfg = parameters 15 self.faults = faults 16 self.facies = facies 17 self.onlap_list = onlap_horizon_list 18 self.top_lith_facies = None 19 self.closure_vol_shape = self.faults.faulted_age_volume.shape 20 self.closure_segments = self.cfg.hdf_init( 21 "closure_segments", shape=self.closure_vol_shape 22 ) 23 self.oil_closures = self.cfg.hdf_init( 24 "oil_closures", shape=self.closure_vol_shape, dtype="uint8" 25 ) 26 self.gas_closures = self.cfg.hdf_init( 27 "gas_closures", shape=self.closure_vol_shape, dtype="uint8" 28 ) 29 self.brine_closures = self.cfg.hdf_init( 30 "brine_closures", shape=self.closure_vol_shape, dtype="uint8" 31 ) 32 self.simple_closures = self.cfg.hdf_init( 33 "simple_closures", shape=self.closure_vol_shape, dtype="uint8" 34 ) 35 self.strat_closures = self.cfg.hdf_init( 36 "strat_closures", shape=self.closure_vol_shape, dtype="uint8" 37 ) 38 self.fault_closures = self.cfg.hdf_init( 39 "fault_closures", shape=self.closure_vol_shape, dtype="uint8" 40 ) 41 self.hc_labels = self.cfg.hdf_init( 42 "hc_labels", shape=self.closure_vol_shape, dtype="uint8" 43 ) 44 45 self.all_closure_segments = self.cfg.hdf_init( 46 "all_closure_segments", shape=self.closure_vol_shape 47 ) 48 49 # Class attributes added from Intersect3D 50 self.wide_faults = self.cfg.hdf_init( 51 "wide_faults", shape=self.closure_vol_shape 52 ) 53 self.fat_faults = self.cfg.hdf_init("fat_faults", shape=self.closure_vol_shape) 54 self.onlaps_upward = self.cfg.hdf_init( 55 "onlaps_upward", shape=self.closure_vol_shape 56 ) 57 self.onlaps_downward = self.cfg.hdf_init( 58 "onlaps_downward", shape=self.closure_vol_shape 59 ) 60 61 # Faulted closures 62 self.faulted_closures_oil = self.cfg.hdf_init( 63 "faulted_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 64 ) 65 self.faulted_closures_gas = self.cfg.hdf_init( 66 "faulted_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 67 ) 68 self.faulted_closures_brine = self.cfg.hdf_init( 69 "faulted_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 70 ) 71 self.fault_closures_oil_segment_list = list() 72 self.fault_closures_gas_segment_list = list() 73 self.fault_closures_brine_segment_list = list() 74 self.n_fault_closures_oil = 0 75 self.n_fault_closures_gas = 0 76 self.n_fault_closures_brine = 0 77 78 self.faulted_all_closures = self.cfg.hdf_init( 79 "faulted_all_closures", shape=self.closure_vol_shape, dtype="uint8" 80 ) 81 self.fault_all_closures_segment_list = list() 82 self.n_fault_all_closures = 0 83 84 # Onlap closures 85 self.onlap_closures_oil = self.cfg.hdf_init( 86 "onlap_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 87 ) 88 self.onlap_closures_gas = self.cfg.hdf_init( 89 "onlap_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 90 ) 91 self.onlap_closures_brine = self.cfg.hdf_init( 92 "onlap_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 93 ) 94 self.onlap_closures_oil_segment_list = list() 95 self.onlap_closures_gas_segment_list = list() 96 self.onlap_closures_brine_segment_list = list() 97 self.n_onlap_closures_oil = 0 98 self.n_onlap_closures_gas = 0 99 self.n_onlap_closures_brine = 0 100 101 self.onlap_all_closures = self.cfg.hdf_init( 102 "onlap_all_closures", shape=self.closure_vol_shape, dtype="uint8" 103 ) 104 self.onlap_all_closures_segment_list = list() 105 self.n_onlap_all_closures_oil = 0 106 107 # Simple closures 108 self.simple_closures_oil = self.cfg.hdf_init( 109 "simple_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 110 ) 111 self.simple_closures_gas = self.cfg.hdf_init( 112 "simple_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 113 ) 114 self.simple_closures_brine = self.cfg.hdf_init( 115 "simple_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 116 ) 117 self.simple_closures_oil_segment_list = list() 118 self.simple_closures_gas_segment_list = list() 119 self.simple_closures_brine_segment_list = list() 120 self.n_4way_closures_oil = 0 121 self.n_4way_closures_gas = 0 122 self.n_4way_closures_brine = 0 123 124 self.simple_all_closures = self.cfg.hdf_init( 125 "simple_all_closures", shape=self.closure_vol_shape, dtype="uint8" 126 ) 127 self.simple_all_closures_segment_list = list() 128 self.n_4way_all_closures = 0 129 130 # False closures 131 self.false_closures_oil = self.cfg.hdf_init( 132 "false_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 133 ) 134 self.false_closures_gas = self.cfg.hdf_init( 135 "false_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 136 ) 137 self.false_closures_brine = self.cfg.hdf_init( 138 "false_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 139 ) 140 self.n_false_closures_oil = 0 141 self.n_false_closures_gas = 0 142 self.n_false_closures_brine = 0 143 144 self.false_all_closures = self.cfg.hdf_init( 145 "false_all_closures", shape=self.closure_vol_shape, dtype="uint8" 146 ) 147 self.n_false_all_closures = 0 148 149 if self.cfg.include_salt: 150 self.salt_closures = self.cfg.hdf_init( 151 "salt_closures", shape=self.closure_vol_shape, dtype="uint8" 152 ) 153 self.wide_salt = self.cfg.hdf_init( 154 "wide_salt", shape=self.closure_vol_shape 155 ) 156 self.salt_closures_oil = self.cfg.hdf_init( 157 "salt_bounded_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 158 ) 159 self.salt_closures_gas = self.cfg.hdf_init( 160 "salt_bounded_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 161 ) 162 self.salt_closures_brine = self.cfg.hdf_init( 163 "salt_bounded_closures_brine", 164 shape=self.closure_vol_shape, 165 dtype="uint8", 166 ) 167 self.salt_closures_oil_segment_list = list() 168 self.salt_closures_gas_segment_list = list() 169 self.salt_closures_brine_segment_list = list() 170 self.n_salt_closures_oil = 0 171 self.n_salt_closures_gas = 0 172 self.n_salt_closures_brine = 0 173 174 self.salt_all_closures = self.cfg.hdf_init( 175 "salt_bounded_all_closures", shape=self.closure_vol_shape, dtype="uint8" 176 ) 177 self.salt_all_closures_segment_list = list() 178 self.n_salt_all_closures = 0 179 180 def create_closure_labels_from_depth_maps( 181 self, depth_maps, depth_maps_infilled, max_col_height 182 ): 183 if self.cfg.verbose: 184 print("\n\t... inside insertClosureLabels3D ") 185 print( 186 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 187 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 188 ) 189 190 # create 3D cube to hold segmentation results 191 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 192 193 # create grids with grid indices 194 ii, jj = self.build_meshgrid() 195 196 # loop through horizons in 'depth_maps' 197 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 198 layers_with_closure = 0 199 200 avg_sand_thickness = list() 201 avg_shale_thickness = list() 202 avg_unit_thickness = list() 203 for ihorizon in range(depth_maps.shape[2] - 1): 204 avg_unit_thickness.append( 205 np.mean( 206 depth_maps_infilled[..., ihorizon + 1] 207 - depth_maps_infilled[..., ihorizon] 208 ) 209 ) 210 211 if self.top_lith_facies[ihorizon] > 0: 212 # If facies is not shale, calculate a closure map for the layer 213 if self.cfg.verbose: 214 print( 215 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 216 ) 217 avg_sand_thickness.append( 218 np.mean( 219 depth_maps_infilled[..., ihorizon + 1] 220 - depth_maps_infilled[..., ihorizon] 221 ) 222 ) 223 # compute a closure map 224 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 225 # - use thicknesses converted to samples instead of ft or ms 226 # - assumes that fault intersections are inserted in input map with value of 0. 227 # - assumes that input map values represent depth (i.e., bigger values are deeper) 228 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 229 top_structure_depth_map[ 230 np.isnan(top_structure_depth_map) 231 ] = 0.0 # replace nans with 0. 232 top_structure_depth_map /= float(self.cfg.digi) 233 if self.cfg.partial_voxels: 234 top_structure_depth_map -= ( 235 1.0 # account for voxels partially in layer 236 ) 237 base_structure_depth_map = depth_maps_infilled[ 238 :, :, ihorizon + 1 239 ].copy() 240 base_structure_depth_map[ 241 np.isnan(top_structure_depth_map) 242 ] = 0.0 # replace nans with 0. 243 base_structure_depth_map /= float(self.cfg.digi) 244 print( 245 " ...inside create_closure_labels_from_depth_maps... ihorizon, self.top_lith_facies[ihorizon] = ", 246 ihorizon, 247 self.top_lith_facies[ihorizon], 248 ) 249 # if there is non-zero thickness between top/base closure 250 if top_structure_depth_map.min() != top_structure_depth_map.max(): 251 max_column = max_col_height[ihorizon] / self.cfg.digi 252 if self.cfg.verbose: 253 print( 254 f" ...avg depth for layer {ihorizon}.", 255 top_structure_depth_map.mean(), 256 ) 257 if self.cfg.verbose: 258 print( 259 f" ...maximum column height for layer {ihorizon}.", 260 max_column, 261 ) 262 263 if ihorizon == 27000 or ihorizon == 1000: 264 closure_depth_map = _flood_fill( 265 top_structure_depth_map, 266 max_column_height=max_column, 267 verbose=True, 268 debug=True, 269 ) 270 else: 271 closure_depth_map = _flood_fill( 272 top_structure_depth_map, max_column_height=max_column 273 ) 274 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 275 closure_depth_map == 0 276 ] 277 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 278 closure_depth_map == 1 279 ] 280 closure_depth_map[ 281 closure_depth_map == 1e5 282 ] = top_structure_depth_map[closure_depth_map == 1e5] 283 # Select the maximum value between the top sand map and the flood-filled closure map 284 closure_depth_map = np.max( 285 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 286 ) 287 closure_depth_map = np.min( 288 np.dstack((closure_depth_map, base_structure_depth_map)), 289 axis=-1, 290 ) 291 if self.cfg.verbose: 292 print( 293 f"\n ... layer {ihorizon}," 294 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 295 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 296 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 297 ) 298 closure_thickness = closure_depth_map - top_structure_depth_map 299 closure_thickness_no_nan = closure_thickness[ 300 ~np.isnan(closure_thickness) 301 ] 302 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 303 if self.cfg.verbose: 304 print(f" ... layer {ihorizon}, max_closure {max_closure}") 305 306 # locate 3D zone in closure after checking that closures exist for this horizon 307 # if False in (top_structure_depth_map == closure_depth_map): 308 if max_closure > 0: 309 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 310 # put label in cube between top_structure_depth_map and closure_depth_map 311 top_structure_depth_map_integer = top_structure_depth_map 312 closure_depth_map_integer = closure_depth_map 313 314 if self.cfg.verbose: 315 closure_map_min = closure_depth_map_integer[ 316 closure_depth_map_integer > 0.1 317 ].min() 318 closure_map_max = closure_depth_map_integer[ 319 closure_depth_map_integer > 0.1 320 ].max() 321 print( 322 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 323 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 324 f" closure map min, max: {closure_map_min}, {closure_map_max}" 325 ) 326 327 slices_with_substitution = 0 328 print(" ... max_closure: {}".format(max_closure)) 329 for k in range( 330 max_closure + 1 331 ): # add one more sample than seemingly needed for round-off 332 # Subtract 2 from the closure cube shape since adding one later 333 horizon_slice = (k + top_structure_depth_map).clip( 334 0, closure_segments.shape[2] - 2 335 ) 336 sublayer_kk = horizon_slice[ 337 horizon_slice < closure_depth_map.astype("int") 338 ] 339 sublayer_ii = ii[ 340 horizon_slice < closure_depth_map.astype("int") 341 ] 342 sublayer_jj = jj[ 343 horizon_slice < closure_depth_map.astype("int") 344 ] 345 346 if sublayer_ii.size > 0: 347 slices_with_substitution += 1 348 349 i_indices = sublayer_ii 350 j_indices = sublayer_jj 351 k_indices = sublayer_kk + 1 352 353 try: 354 closure_segments[ 355 i_indices, j_indices, k_indices.astype("int") 356 ] += 1.0 357 voxel_change_count[ 358 i_indices, j_indices, k_indices.astype("int") 359 ] += 1 360 except IndexError: 361 print("\nIndex is out of bounds.") 362 print(f"\tclosure_segments: {closure_segments}") 363 print(f"\tvoxel_change_count: {voxel_change_count}") 364 print(f"\ti_indices: {i_indices}") 365 print(f"\tj_indices: {j_indices}") 366 print(f"\tk_indices: {k_indices.astype('int')}") 367 pass 368 369 if slices_with_substitution > 0: 370 layers_with_closure += 1 371 372 if self.cfg.verbose: 373 print( 374 " ... finished putting closures in closures_segments for layer ...", 375 ihorizon, 376 ) 377 378 else: 379 continue 380 else: 381 # Calculate shale unit thicknesses 382 avg_shale_thickness.append( 383 np.mean( 384 depth_maps_infilled[..., ihorizon + 1] 385 - depth_maps_infilled[..., ihorizon] 386 ) 387 ) 388 389 if len(avg_sand_thickness) == 0: 390 avg_sand_thickness = 0 391 self.cfg.write_to_logfile( 392 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 393 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 394 f"max: {np.max(avg_sand_thickness):.2f}" 395 ) 396 self.cfg.write_to_logfile( 397 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 398 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 399 f"max: {np.max(avg_shale_thickness):.2f}" 400 ) 401 self.cfg.write_to_logfile( 402 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 403 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 404 f"max: {np.max(avg_unit_thickness):.2f}" 405 ) 406 self.cfg.write_to_logfile( 407 msg=None, 408 mainkey="model_parameters", 409 subkey="sand_unit_thickness_combined_mean", 410 val=np.mean(avg_sand_thickness), 411 ) 412 self.cfg.write_to_logfile( 413 msg=None, 414 mainkey="model_parameters", 415 subkey="sand_unit_thickness_combined_std", 416 val=np.std(avg_sand_thickness), 417 ) 418 self.cfg.write_to_logfile( 419 msg=None, 420 mainkey="model_parameters", 421 subkey="sand_unit_thickness_combined_min", 422 val=np.min(avg_sand_thickness), 423 ) 424 self.cfg.write_to_logfile( 425 msg=None, 426 mainkey="model_parameters", 427 subkey="sand_unit_thickness_combined_max", 428 val=np.max(avg_sand_thickness), 429 ) 430 # 431 self.cfg.write_to_logfile( 432 msg=None, 433 mainkey="model_parameters", 434 subkey="shale_unit_thickness_combined_mean", 435 val=np.mean(avg_shale_thickness), 436 ) 437 self.cfg.write_to_logfile( 438 msg=None, 439 mainkey="model_parameters", 440 subkey="shale_unit_thickness_combined_std", 441 val=np.std(avg_shale_thickness), 442 ) 443 self.cfg.write_to_logfile( 444 msg=None, 445 mainkey="model_parameters", 446 subkey="shale_unit_thickness_combined_min", 447 val=np.min(avg_shale_thickness), 448 ) 449 self.cfg.write_to_logfile( 450 msg=None, 451 mainkey="model_parameters", 452 subkey="shale_unit_thickness_combined_max", 453 val=np.max(avg_shale_thickness), 454 ) 455 456 self.cfg.write_to_logfile( 457 msg=None, 458 mainkey="model_parameters", 459 subkey="overall_unit_thickness_combined_mean", 460 val=np.mean(avg_unit_thickness), 461 ) 462 self.cfg.write_to_logfile( 463 msg=None, 464 mainkey="model_parameters", 465 subkey="overall_unit_thickness_combined_std", 466 val=np.std(avg_unit_thickness), 467 ) 468 self.cfg.write_to_logfile( 469 msg=None, 470 mainkey="model_parameters", 471 subkey="overall_unit_thickness_combined_min", 472 val=np.min(avg_unit_thickness), 473 ) 474 self.cfg.write_to_logfile( 475 msg=None, 476 mainkey="model_parameters", 477 subkey="overall_unit_thickness_combined_max", 478 val=np.max(avg_unit_thickness), 479 ) 480 481 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 482 pct_non_zero = float(non_zero_pixels) / ( 483 closure_segments.shape[0] 484 * closure_segments.shape[1] 485 * closure_segments.shape[2] 486 ) 487 if self.cfg.verbose: 488 print( 489 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 490 closure_segments.min(), 491 closure_segments.mean(), 492 closure_segments.max(), 493 pct_non_zero, 494 ) 495 ) 496 497 print(f"\t... layers_with_closure {layers_with_closure}") 498 print("\t... finished putting closures in closure_segments ...\n") 499 500 if self.cfg.verbose: 501 print( 502 f"\n ...closure segments created. min: {closure_segments.min()}, " 503 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 504 f" voxel count: {closure_segments[closure_segments != 0].shape}" 505 ) 506 507 return closure_segments 508 509 def create_closure_labels_from_all_depth_maps( 510 self, depth_maps, depth_maps_infilled, max_col_height 511 ): 512 if self.cfg.verbose: 513 print("\n\t... inside insertClosureLabels3D ") 514 print( 515 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 516 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 517 ) 518 519 # create 3D cube to hold segmentation results 520 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 521 522 # create grids with grid indices 523 ii, jj = self.build_meshgrid() 524 525 # loop through horizons in 'depth_maps' 526 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 527 layers_with_closure = 0 528 529 avg_sand_thickness = list() 530 avg_shale_thickness = list() 531 avg_unit_thickness = list() 532 for ihorizon in range(depth_maps.shape[2] - 1): 533 avg_unit_thickness.append( 534 np.mean( 535 depth_maps_infilled[..., ihorizon + 1] 536 - depth_maps_infilled[..., ihorizon] 537 ) 538 ) 539 # calculate a closure map for the layer 540 if self.cfg.verbose: 541 print( 542 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 543 ) 544 545 # compute a closure map 546 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 547 # - use thicknesses converted to samples instead of ft or ms 548 # - assumes that fault intersections are inserted in input map with value of 0. 549 # - assumes that input map values represent depth (i.e., bigger values are deeper) 550 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 551 top_structure_depth_map[ 552 np.isnan(top_structure_depth_map) 553 ] = 0.0 # replace nans with 0. 554 top_structure_depth_map /= float(self.cfg.digi) 555 if self.cfg.partial_voxels: 556 top_structure_depth_map -= 1.0 # account for voxels partially in layer 557 base_structure_depth_map = depth_maps_infilled[:, :, ihorizon + 1].copy() 558 base_structure_depth_map[ 559 np.isnan(top_structure_depth_map) 560 ] = 0.0 # replace nans with 0. 561 base_structure_depth_map /= float(self.cfg.digi) 562 print( 563 " ...inside create_closure_labels_from_depth_maps... ihorizon = ", 564 ihorizon, 565 ) 566 # if there is non-zero thickness between top/base closure 567 if top_structure_depth_map.min() != top_structure_depth_map.max(): 568 max_column = max_col_height[ihorizon] / self.cfg.digi 569 if self.cfg.verbose: 570 print( 571 f" ...avg depth for layer {ihorizon}.", 572 top_structure_depth_map.mean(), 573 ) 574 if self.cfg.verbose: 575 print( 576 f" ...maximum column height for layer {ihorizon}.", max_column 577 ) 578 579 if ihorizon == 27000 or ihorizon == 1000: 580 closure_depth_map = _flood_fill( 581 top_structure_depth_map, 582 max_column_height=max_column, 583 verbose=True, 584 debug=True, 585 ) 586 else: 587 closure_depth_map = _flood_fill( 588 top_structure_depth_map, max_column_height=max_column 589 ) 590 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 591 closure_depth_map == 0 592 ] 593 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 594 closure_depth_map == 1 595 ] 596 closure_depth_map[closure_depth_map == 1e5] = top_structure_depth_map[ 597 closure_depth_map == 1e5 598 ] 599 # Select the maximum value between the top sand map and the flood-filled closure map 600 closure_depth_map = np.max( 601 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 602 ) 603 closure_depth_map = np.min( 604 np.dstack((closure_depth_map, base_structure_depth_map)), axis=-1 605 ) 606 if self.cfg.verbose: 607 print( 608 f"\n ... layer {ihorizon}," 609 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 610 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 611 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 612 ) 613 closure_thickness = closure_depth_map - top_structure_depth_map 614 closure_thickness_no_nan = closure_thickness[ 615 ~np.isnan(closure_thickness) 616 ] 617 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 618 if self.cfg.verbose: 619 print(f" ... layer {ihorizon}, max_closure {max_closure}") 620 621 # locate 3D zone in closure after checking that closures exist for this horizon 622 # if False in (top_structure_depth_map == closure_depth_map): 623 if max_closure > 0: 624 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 625 # put label in cube between top_structure_depth_map and closure_depth_map 626 top_structure_depth_map_integer = top_structure_depth_map 627 closure_depth_map_integer = closure_depth_map 628 629 if self.cfg.verbose: 630 closure_map_min = closure_depth_map_integer[ 631 closure_depth_map_integer > 0.1 632 ].min() 633 closure_map_max = closure_depth_map_integer[ 634 closure_depth_map_integer > 0.1 635 ].max() 636 print( 637 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 638 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 639 f" closure map min, max: {closure_map_min}, {closure_map_max}" 640 ) 641 642 slices_with_substitution = 0 643 print(" ... max_closure: {}".format(max_closure)) 644 for k in range( 645 max_closure + 1 646 ): # add one more sample than seemingly needed for round-off 647 # Subtract 2 from the closure cube shape since adding one later 648 horizon_slice = (k + top_structure_depth_map).clip( 649 0, closure_segments.shape[2] - 2 650 ) 651 sublayer_kk = horizon_slice[ 652 horizon_slice < closure_depth_map.astype("int") 653 ] 654 sublayer_ii = ii[ 655 horizon_slice < closure_depth_map.astype("int") 656 ] 657 sublayer_jj = jj[ 658 horizon_slice < closure_depth_map.astype("int") 659 ] 660 661 if sublayer_ii.size > 0: 662 slices_with_substitution += 1 663 664 i_indices = sublayer_ii 665 j_indices = sublayer_jj 666 k_indices = sublayer_kk + 1 667 668 try: 669 closure_segments[ 670 i_indices, j_indices, k_indices.astype("int") 671 ] += 1.0 672 voxel_change_count[ 673 i_indices, j_indices, k_indices.astype("int") 674 ] += 1 675 except IndexError: 676 print("\nIndex is out of bounds.") 677 print(f"\tclosure_segments: {closure_segments}") 678 print(f"\tvoxel_change_count: {voxel_change_count}") 679 print(f"\ti_indices: {i_indices}") 680 print(f"\tj_indices: {j_indices}") 681 print(f"\tk_indices: {k_indices.astype('int')}") 682 pass 683 684 if slices_with_substitution > 0: 685 layers_with_closure += 1 686 687 if self.cfg.verbose: 688 print( 689 " ... finished putting closures in closures_segments for layer ...", 690 ihorizon, 691 ) 692 693 else: 694 continue 695 696 if self.facies[ihorizon] == 1: 697 avg_sand_thickness.append( 698 np.mean( 699 depth_maps_infilled[..., ihorizon + 1] 700 - depth_maps_infilled[..., ihorizon] 701 ) 702 ) 703 elif self.facies[ihorizon] == 0: 704 # Calculate shale unit thicknesses 705 avg_shale_thickness.append( 706 np.mean( 707 depth_maps_infilled[..., ihorizon + 1] 708 - depth_maps_infilled[..., ihorizon] 709 ) 710 ) 711 712 # TODO handle case where avg_sand_thickness is zero-size array 713 try: 714 self.cfg.write_to_logfile( 715 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 716 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 717 f"max: {np.max(avg_sand_thickness):.2f}" 718 ) 719 except: 720 print("No sands in model") 721 self.cfg.write_to_logfile( 722 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 723 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 724 f"max: {np.max(avg_shale_thickness):.2f}" 725 ) 726 self.cfg.write_to_logfile( 727 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 728 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 729 f"max: {np.max(avg_unit_thickness):.2f}" 730 ) 731 732 self.cfg.write_to_logfile( 733 msg=None, 734 mainkey="model_parameters", 735 subkey="sand_unit_thickness_mean", 736 val=np.mean(avg_sand_thickness), 737 ) 738 self.cfg.write_to_logfile( 739 msg=None, 740 mainkey="model_parameters", 741 subkey="sand_unit_thickness_std", 742 val=np.std(avg_sand_thickness), 743 ) 744 self.cfg.write_to_logfile( 745 msg=None, 746 mainkey="model_parameters", 747 subkey="sand_unit_thickness_min", 748 val=np.min(avg_sand_thickness), 749 ) 750 self.cfg.write_to_logfile( 751 msg=None, 752 mainkey="model_parameters", 753 subkey="sand_unit_thickness_max", 754 val=np.max(avg_sand_thickness), 755 ) 756 # 757 self.cfg.write_to_logfile( 758 msg=None, 759 mainkey="model_parameters", 760 subkey="shale_unit_thickness_mean", 761 val=np.mean(avg_shale_thickness), 762 ) 763 self.cfg.write_to_logfile( 764 msg=None, 765 mainkey="model_parameters", 766 subkey="shale_unit_thickness_std", 767 val=np.std(avg_shale_thickness), 768 ) 769 self.cfg.write_to_logfile( 770 msg=None, 771 mainkey="model_parameters", 772 subkey="shale_unit_thickness_min", 773 val=np.min(avg_shale_thickness), 774 ) 775 self.cfg.write_to_logfile( 776 msg=None, 777 mainkey="model_parameters", 778 subkey="shale_unit_thickness_max", 779 val=np.max(avg_shale_thickness), 780 ) 781 782 self.cfg.write_to_logfile( 783 msg=None, 784 mainkey="model_parameters", 785 subkey="overall_unit_thickness_mean", 786 val=np.mean(avg_unit_thickness), 787 ) 788 self.cfg.write_to_logfile( 789 msg=None, 790 mainkey="model_parameters", 791 subkey="overall_unit_thickness_std", 792 val=np.std(avg_unit_thickness), 793 ) 794 self.cfg.write_to_logfile( 795 msg=None, 796 mainkey="model_parameters", 797 subkey="overall_unit_thickness_min", 798 val=np.min(avg_unit_thickness), 799 ) 800 self.cfg.write_to_logfile( 801 msg=None, 802 mainkey="model_parameters", 803 subkey="overall_unit_thickness_max", 804 val=np.max(avg_unit_thickness), 805 ) 806 807 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 808 pct_non_zero = float(non_zero_pixels) / ( 809 closure_segments.shape[0] 810 * closure_segments.shape[1] 811 * closure_segments.shape[2] 812 ) 813 if self.cfg.verbose: 814 print( 815 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 816 closure_segments.min(), 817 closure_segments.mean(), 818 closure_segments.max(), 819 pct_non_zero, 820 ) 821 ) 822 823 print(f"\t... layers_with_closure {layers_with_closure}") 824 print("\t... finished putting closures in closure_segments ...\n") 825 826 if self.cfg.verbose: 827 print( 828 f"\n ...closure segments created. min: {closure_segments.min()}, " 829 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 830 f" voxel count: {closure_segments[closure_segments != 0].shape}" 831 ) 832 833 return closure_segments 834 835 def find_top_lith_horizons(self): 836 """ 837 Find horizons which are the top of layers where the lithology changes 838 839 Combine layers of the same lithology and retain the top of these new layers for closure calculations. 840 """ 841 top_lith_indices = list(np.array(self.onlap_list) - 1) 842 for i, _ in enumerate(self.facies[:-1]): 843 if i == 0: 844 continue 845 print( 846 f"i: {i}, sand_layer_label[i-1]: {self.facies[i - 1]}," 847 f" sand_layer_label[i]: {self.facies[i]}" 848 ) 849 if self.facies[i] != self.facies[i - 1]: 850 top_lith_indices.append(i) 851 if self.cfg.verbose: 852 print( 853 " ... layer lith different than layer above it. i = {}".format( 854 i 855 ) 856 ) 857 top_lith_indices.sort() 858 if self.cfg.verbose: 859 print( 860 "\n ...layers selected for closure computations...\n", 861 top_lith_indices, 862 ) 863 self.top_lith_indices = np.array(top_lith_indices) 864 self.top_lith_facies = self.facies[top_lith_indices] 865 866 # return top_lith_indices 867 868 def create_closures(self): 869 if self.cfg.verbose: 870 print("\n\n ... create 3D labels for closure") 871 872 # Convert nan to 0's 873 old_depth_maps = np.nan_to_num(self.faults.faulted_depth_maps[:], copy=True) 874 old_depth_maps_gaps = np.nan_to_num( 875 self.faults.faulted_depth_maps_gaps[:], copy=True 876 ) 877 878 # Convert from samples to units 879 old_depth_maps_gaps = self.convert_map_from_samples_to_units( 880 old_depth_maps_gaps 881 ) 882 old_depth_maps = self.convert_map_from_samples_to_units(old_depth_maps) 883 884 # keep only horizons corresponding to top of layers where lithology changes 885 self.find_top_lith_horizons() 886 all_lith_indices = np.arange(old_depth_maps.shape[-1]) 887 import sys 888 889 print("All lith indices (last, then all):", self.facies[-1], all_lith_indices) 890 sys.stdout.flush() 891 892 depth_maps_gaps_top_lith = old_depth_maps_gaps[ 893 :, :, self.top_lith_indices 894 ].copy() 895 depth_maps_gaps_all_lith = old_depth_maps_gaps[:, :, all_lith_indices].copy() 896 depth_maps_top_lith = old_depth_maps[:, :, self.top_lith_indices].copy() 897 depth_maps_all_lith = old_depth_maps[:, :, all_lith_indices].copy() 898 max_column_heights = variable_max_column_height( 899 self.top_lith_indices, 900 self.faults.faulted_depth_maps_gaps.shape[-1], 901 self.cfg.max_column_height[0], 902 self.cfg.max_column_height[1], 903 ) 904 all_max_column_heights = variable_max_column_height( 905 all_lith_indices, 906 self.faults.faulted_depth_maps_gaps.shape[-1], 907 self.cfg.max_column_height[0], 908 self.cfg.max_column_height[1], 909 ) 910 911 if self.cfg.verbose: 912 print("\n ...facies for closure computations...\n", self.top_lith_facies) 913 print( 914 "\n ...max column heights for closure computations...\n", 915 max_column_heights, 916 ) 917 918 self.closure_segments[:] = self.create_closure_labels_from_depth_maps( 919 depth_maps_gaps_top_lith, depth_maps_top_lith, max_column_heights 920 ) 921 922 self.all_closure_segments[:] = self.create_closure_labels_from_all_depth_maps( 923 depth_maps_gaps_all_lith, depth_maps_all_lith, all_max_column_heights 924 ) 925 926 if self.cfg.verbose: 927 print( 928 " ...+++... number of nan's in depth_maps_gaps before insertClosureLabels3D ...+++... {}".format( 929 old_depth_maps_gaps[np.isnan(old_depth_maps_gaps)].shape 930 ) 931 ) 932 print( 933 " ...+++... number of nan's in depth_maps_gaps after insertClosureLabels3D ...+++... {}".format( 934 self.faults.faulted_depth_maps_gaps[ 935 np.isnan(self.faults.faulted_depth_maps_gaps) 936 ].shape 937 ) 938 ) 939 print( 940 " ...+++... number of nan's in depth_maps after insertClosureLabels3D ...+++... {}".format( 941 self.faults.faulted_depth_maps[ 942 np.isnan(self.faults.faulted_depth_maps) 943 ].shape 944 ) 945 ) 946 _closure_segments = self.closure_segments[:] 947 print( 948 " ...+++... number of closure voxels in self.closure_segments ...+++... {}".format( 949 _closure_segments[_closure_segments > 0.0].shape 950 ) 951 ) 952 del _closure_segments 953 954 labels_clean, self.closure_segments[:] = self.segment_closures( 955 self.closure_segments[:], remove_shale=True 956 ) 957 label_values, labels_clean = self.parse_label_values_and_counts(labels_clean) 958 959 labels_clean_all, self.all_closure_segments[:] = self.segment_closures( 960 self.all_closure_segments[:], remove_shale=False 961 ) 962 label_values_all, labels_clean_all = self.parse_label_values_and_counts( 963 labels_clean_all 964 ) 965 self.write_cube_to_disk(self.all_closure_segments[:], "all_closure_segments") 966 967 # Assign fluid types 968 ( 969 self.oil_closures[:], 970 self.gas_closures[:], 971 self.brine_closures[:], 972 ) = self.assign_fluid_types(label_values, labels_clean) 973 all_closures_final = (labels_clean_all != 0).astype("uint8") 974 975 # Identify closures by type (simple, faulted, onlap or salt bounded) 976 self.find_faulted_closures(label_values, labels_clean) 977 self.find_onlap_closures(label_values, labels_clean) 978 self.find_simple_closures(label_values, labels_clean) 979 self.find_false_closures(label_values, labels_clean) 980 981 self.find_faulted_all_closures(label_values_all, labels_clean_all) 982 self.find_onlap_all_closures(label_values_all, labels_clean_all) 983 self.find_simple_all_closures(label_values_all, labels_clean_all) 984 self.find_false_all_closures(label_values_all, labels_clean_all) 985 986 if self.cfg.include_salt: 987 self.find_salt_bounded_closures(label_values, labels_clean) 988 self.find_salt_bounded_all_closures(label_values_all, labels_clean_all) 989 990 # Remove false closures from oil & gas closure cubes 991 if self.n_false_closures_oil > 0: 992 print(f"Removing {self.n_false_closures_oil} false oil closures") 993 self.oil_closures[self.false_closures_oil == 1] = 0.0 994 if self.n_false_closures_gas > 0: 995 print(f"Removing {self.n_false_closures_gas} false gas closures") 996 self.gas_closures[self.false_closures_gas == 1] = 0.0 997 998 # Remove false closures from allclosure cube 999 if self.n_false_all_closures > 0: 1000 print(f"Removing {self.n_false_all_closures} false all closures") 1001 self.all_closure_segments[self.false_all_closures == 1] = 0.0 1002 1003 # Create a closure cube with voxel count as labels, and include closure type in decimal 1004 # e.g. simple closure of size 5000 = 5000.1 1005 # faulted closure of size 5000 = 5000.2 1006 # onlap closure of size 5000 = 5000.3 1007 # salt-bounded closure of size 5000 = 5000.4 1008 hc_closure_codes = np.zeros_like(self.gas_closures, dtype="float32") 1009 1010 # AZ: COULD RUN THESE CLOSURE SIZE FILTERS ON ALL_CLOSURES, IF DESIRED 1011 1012 if "simple" in self.cfg.closure_types: 1013 print("Filtering 4 Way Closures") 1014 ( 1015 self.simple_closures_oil[:], 1016 self.n_4way_closures_oil, 1017 ) = self.closure_size_filter( 1018 self.simple_closures_oil[:], 1019 self.cfg.closure_min_voxels_simple, 1020 self.n_4way_closures_oil, 1021 ) 1022 ( 1023 self.simple_closures_gas[:], 1024 self.n_4way_closures_gas, 1025 ) = self.closure_size_filter( 1026 self.simple_closures_gas[:], 1027 self.cfg.closure_min_voxels_simple, 1028 self.n_4way_closures_gas, 1029 ) 1030 1031 # Add simple closures to closure code cube 1032 hc_closures = ( 1033 self.simple_closures_oil[:] + self.simple_closures_gas[:] 1034 ).astype("float32") 1035 labels, num = measure.label( 1036 hc_closures, connectivity=2, background=0, return_num=True 1037 ) 1038 hc_closure_codes = self.parse_closure_codes( 1039 hc_closure_codes, labels, num, code=0.1 1040 ) 1041 else: # if closure type not in config, set HC closures to 0 1042 self.simple_closures_oil[:] *= 0 1043 self.simple_closures_gas[:] *= 0 1044 self.simple_all_closures[:] *= 0 1045 1046 self.oil_closures[self.simple_closures_oil[:] > 0.0] = 1.0 1047 self.oil_closures[self.simple_closures_oil[:] < 0.0] = 0.0 1048 self.gas_closures[self.simple_closures_gas[:] > 0.0] = 1.0 1049 self.gas_closures[self.simple_closures_gas[:] < 0.0] = 0.0 1050 1051 all_closures_final[self.simple_all_closures[:] > 0.0] = 1.0 1052 all_closures_final[self.simple_all_closures[:] < 0.0] = 0.0 1053 1054 if "faulted" in self.cfg.closure_types: 1055 print("Filtering 4 Way Closures") 1056 # Grow the faulted closures to the fault planes 1057 self.faulted_closures_oil[:] = self.grow_to_fault2( 1058 self.faulted_closures_oil[:] 1059 ) 1060 self.faulted_closures_gas[:] = self.grow_to_fault2( 1061 self.faulted_closures_gas[:] 1062 ) 1063 1064 ( 1065 self.faulted_closures_oil[:], 1066 self.n_fault_closures_oil, 1067 ) = self.closure_size_filter( 1068 self.faulted_closures_oil[:], 1069 self.cfg.closure_min_voxels_faulted, 1070 self.n_fault_closures_oil, 1071 ) 1072 ( 1073 self.faulted_closures_gas[:], 1074 self.n_fault_closures_gas, 1075 ) = self.closure_size_filter( 1076 self.faulted_closures_gas[:], 1077 self.cfg.closure_min_voxels_faulted, 1078 self.n_fault_closures_gas, 1079 ) 1080 1081 self.faulted_all_closures[:] = self.grow_to_fault2( 1082 self.faulted_all_closures[:], 1083 grow_only_sand_closures=False, 1084 remove_small_closures=False, 1085 ) 1086 1087 # Add faulted closures to closure code cube 1088 hc_closures = self.faulted_closures_oil[:] + self.faulted_closures_gas[:] 1089 labels, num = measure.label( 1090 hc_closures, connectivity=2, background=0, return_num=True 1091 ) 1092 hc_closure_codes = self.parse_closure_codes( 1093 hc_closure_codes, labels, num, code=0.2 1094 ) 1095 else: # if closure type not in config, set HC closures to 0 1096 self.faulted_closures_oil[:] *= 0 1097 self.faulted_closures_gas[:] *= 0 1098 self.faulted_all_closures[:] *= 0 1099 1100 self.oil_closures[self.faulted_closures_oil[:] > 0.0] = 1.0 1101 self.oil_closures[self.faulted_closures_oil[:] < 0.0] = 0.0 1102 self.gas_closures[self.faulted_closures_gas[:] > 0.0] = 1.0 1103 self.gas_closures[self.faulted_closures_gas[:] < 0.0] = 0.0 1104 1105 all_closures_final[self.faulted_all_closures[:] > 0.0] = 1.0 1106 all_closures_final[self.faulted_all_closures[:] < 0.0] = 0.0 1107 1108 if "onlap" in self.cfg.closure_types: 1109 print("Filtering Onlap Closures") 1110 ( 1111 self.onlap_closures_oil[:], 1112 self.n_onlap_closures_oil, 1113 ) = self.closure_size_filter( 1114 self.onlap_closures_oil[:], 1115 self.cfg.closure_min_voxels_onlap, 1116 self.n_onlap_closures_oil, 1117 ) 1118 ( 1119 self.onlap_closures_gas[:], 1120 self.n_onlap_closures_gas, 1121 ) = self.closure_size_filter( 1122 self.onlap_closures_gas[:], 1123 self.cfg.closure_min_voxels_onlap, 1124 self.n_onlap_closures_gas, 1125 ) 1126 1127 # Add faulted closures to closure code cube 1128 hc_closures = self.onlap_closures_oil[:] + self.onlap_closures_gas[:] 1129 labels, num = measure.label( 1130 hc_closures, connectivity=2, background=0, return_num=True 1131 ) 1132 hc_closure_codes = self.parse_closure_codes( 1133 hc_closure_codes, labels, num, code=0.3 1134 ) 1135 # labels = labels.astype('float32') 1136 # if num > 0: 1137 # for x in range(1, num + 1): 1138 # y = 0.3 + labels[labels == x].size 1139 # labels[labels == x] = y 1140 # hc_closure_codes += labels 1141 else: # if closure type not in config, set HC closures to 0 1142 self.onlap_closures_oil[:] *= 0 1143 self.onlap_closures_gas[:] *= 0 1144 self.onlap_all_closures[:] *= 0 1145 1146 self.oil_closures[self.onlap_closures_oil[:] > 0.0] = 1.0 1147 self.oil_closures[self.onlap_closures_oil[:] < 0.0] = 0.0 1148 self.gas_closures[self.onlap_closures_gas[:] > 0.0] = 1.0 1149 self.gas_closures[self.onlap_closures_gas[:] < 0.0] = 0.0 1150 all_closures_final[self.onlap_all_closures[:] > 0.0] = 1.0 1151 all_closures_final[self.onlap_all_closures[:] < 0.0] = 0.0 1152 1153 if self.cfg.include_salt: 1154 # Grow the salt-bounded closures to the salt body 1155 salt_closures_oil_grown = np.zeros_like(self.salt_closures_oil[:]) 1156 salt_closures_gas_grown = np.zeros_like(self.salt_closures_gas[:]) 1157 1158 if np.max(self.salt_closures_oil[:]) > 0.0: 1159 self.write_cube_to_disk( 1160 self.salt_closures_oil[:], "salt_closures_oil_initial" 1161 ) 1162 print( 1163 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1164 ) 1165 salt_closures_oil_grown = self.grow_to_salt(self.salt_closures_oil[:]) 1166 self.salt_closures_oil[:] = salt_closures_oil_grown 1167 print( 1168 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1169 ) 1170 if np.max(self.salt_closures_gas[:]) > 0.0: 1171 self.write_cube_to_disk( 1172 self.salt_closures_gas[:], "salt_closures_gas_initial" 1173 ) 1174 print( 1175 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 0].size}" 1176 ) 1177 salt_closures_gas_grown = self.grow_to_salt(self.salt_closures_gas[:]) 1178 self.salt_closures_gas[:] = salt_closures_gas_grown 1179 print( 1180 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 1].size}" 1181 ) 1182 if np.max(self.salt_all_closures[:]) > 0.0: 1183 self.write_cube_to_disk( 1184 self.salt_all_closures[:], "salt_all_closures_initial" 1185 ) # maybe remove later 1186 print( 1187 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 0].size}" 1188 ) 1189 salt_all_closures_grown = self.grow_to_salt(self.salt_all_closures[:]) 1190 self.salt_all_closures[:] = salt_all_closures_grown 1191 print( 1192 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 1].size}" 1193 ) 1194 else: 1195 salt_all_closures_grown = np.zeros_like(self.salt_all_closures) 1196 1197 if np.max(self.salt_closures_oil[:]) > 0.0: 1198 self.write_cube_to_disk( 1199 self.salt_closures_oil[:], "salt_closures_oil_grown" 1200 ) 1201 if np.max(self.salt_closures_gas[:]) > 0.0: 1202 self.write_cube_to_disk( 1203 self.salt_closures_gas[:], "salt_closures_gas_grown" 1204 ) 1205 if np.max(self.salt_all_closures[:]) > 0.0: 1206 self.write_cube_to_disk( 1207 self.salt_all_closures[:], "salt_all_closures_grown" 1208 ) # maybe remove later 1209 1210 ( 1211 self.salt_closures_oil[:], 1212 self.n_salt_closures_oil, 1213 ) = self.closure_size_filter( 1214 self.salt_closures_oil[:], 1215 self.cfg.closure_min_voxels, 1216 self.n_salt_closures_oil, 1217 ) 1218 ( 1219 self.salt_closures_gas[:], 1220 self.n_salt_closures_gas, 1221 ) = self.closure_size_filter( 1222 self.salt_closures_gas[:], 1223 self.cfg.closure_min_voxels, 1224 self.n_salt_closures_gas, 1225 ) 1226 1227 # Append salt-bounded closures to main closure cubes for oil and gas 1228 if np.max(salt_closures_oil_grown) > 0.0: 1229 self.oil_closures[salt_closures_oil_grown > 0.0] = 1.0 1230 self.oil_closures[salt_closures_oil_grown < 0.0] = 0.0 1231 if np.max(salt_closures_gas_grown) > 0.0: 1232 self.gas_closures[salt_closures_gas_grown > 0.0] = 1.0 1233 self.gas_closures[salt_closures_gas_grown < 0.0] = 0.0 1234 if np.max(salt_all_closures_grown) > 0.0: 1235 all_closures_final[salt_all_closures_grown > 0.0] = 1.0 1236 all_closures_final[salt_all_closures_grown < 0.0] = 0.0 1237 1238 # Add faulted closures to closure code cube 1239 hc_closures = self.salt_closures_oil[:] + self.salt_closures_gas[:] 1240 labels, num = measure.label( 1241 hc_closures, connectivity=2, background=0, return_num=True 1242 ) 1243 hc_closure_codes = self.parse_closure_codes( 1244 hc_closure_codes, labels, num, code=0.4 1245 ) 1246 1247 # Write hc_closure_codes to disk 1248 self.write_cube_to_disk(hc_closure_codes, "closure_segments_hc_voxelcount") 1249 1250 # Create closure volumes by type 1251 if self.simple_closures[:] is None: 1252 self.simple_closures[:] = self.simple_closures_oil[:].astype("uint8") 1253 else: 1254 self.simple_closures[:] += self.simple_closures_oil[:].astype("uint8") 1255 self.simple_closures[:] += self.simple_closures_gas[:].astype("uint8") 1256 self.simple_closures[:] += self.simple_closures_brine[:].astype("uint8") 1257 # Onlap closures 1258 if self.strat_closures is None: 1259 self.strat_closures[:] = self.onlap_closures_oil[:].astype("uint8") 1260 else: 1261 self.strat_closures[:] += self.onlap_closures_oil[:].astype("uint8") 1262 self.strat_closures[:] += self.onlap_closures_gas[:].astype("uint8") 1263 self.strat_closures[:] += self.onlap_closures_brine[:].astype("uint8") 1264 # Fault closures 1265 if self.fault_closures is None: 1266 self.fault_closures[:] = self.faulted_closures_oil[:].astype("uint8") 1267 else: 1268 self.fault_closures[:] += self.faulted_closures_oil[:].astype("uint8") 1269 self.fault_closures[:] += self.faulted_closures_gas[:].astype("uint8") 1270 self.fault_closures[:] += self.faulted_closures_brine[:].astype("uint8") 1271 1272 # Salt-bounded closures 1273 if self.cfg.include_salt: 1274 if self.salt_closures is None: 1275 self.salt_closures[:] = self.salt_closures_oil[:].astype("uint8") 1276 else: 1277 self.salt_closures[:] += self.salt_closures_oil[:].astype("uint8") 1278 self.salt_closures[:] += self.salt_closures_gas[:].astype("uint8") 1279 1280 # Convert closure cubes from int16 to uint8 for writing to disk 1281 self.closure_segments[:] = self.closure_segments[:].astype("uint8") 1282 1283 # add any oil/gas/brine closures into all_closures_final in case missed 1284 all_closures_final[:][self.oil_closures[:] > 0] = 1 1285 all_closures_final[:][self.gas_closures[:] > 0] = 1 1286 all_closures_final[:][self.gas_closures[:] > 0] = 1 1287 # Write all_closures_final to disk 1288 self.write_cube_to_disk(all_closures_final.astype("uint8"), "trap_label") 1289 1290 # add any oil/gas/brine closures into reservoir in case missed 1291 self.faults.reservoir[:][self.oil_closures[:] > 0] = 1 1292 self.faults.reservoir[:][self.gas_closures[:] > 0] = 1 1293 self.faults.reservoir[:][self.brine_closures[:] > 0] = 1 1294 # write reservoir_label to disk 1295 self.write_cube_to_disk( 1296 self.faults.reservoir[:].astype("uint8"), "reservoir_label" 1297 ) 1298 1299 if self.cfg.qc_plots: 1300 from datagenerator.util import plot_xsection 1301 from datagenerator.util import find_line_with_most_voxels 1302 1303 # visualize closures QC 1304 inline_index_cl = find_line_with_most_voxels( 1305 self.closure_segments, 0.5, self.cfg 1306 ) 1307 plot_xsection( 1308 volume=labels_clean, 1309 maps=self.faults.faulted_depth_maps_gaps, 1310 line_num=inline_index_cl, 1311 title="Example Trav through 3D model\nclosures after faulting", 1312 png_name="QC_plot__AfterFaulting_closure_segments.png", 1313 cmap="gist_ncar_r", 1314 cfg=self.cfg, 1315 ) 1316 1317 def closure_size_filter(self, closure_type, threshold, count): 1318 labels, num = measure.label( 1319 closure_type, connectivity=2, background=0, return_num=True 1320 ) 1321 if ( 1322 num > 0 1323 ): # TODO add whether smallest closure is below threshold constraint too 1324 s = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1325 labels = morphology.remove_small_objects(labels, threshold, connectivity=2) 1326 t = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1327 print( 1328 f"Closure sizes before filter: {s}\nThreshold: {threshold}\n" 1329 f"Closure sizes after filter: {t}" 1330 ) 1331 count = len(t) 1332 return labels, count 1333 1334 def closure_type_info_for_log(self): 1335 fluid_types = ["oil", "gas", "brine"] 1336 if "faulted" in self.cfg.closure_types: 1337 # Faulted closures 1338 for name, fluid, num in zip( 1339 fluid_types, 1340 [ 1341 self.faulted_closures_oil[:], 1342 self.faulted_closures_gas[:], 1343 self.faulted_closures_brine[:], 1344 ], 1345 [ 1346 self.n_fault_closures_oil, 1347 self.n_fault_closures_gas, 1348 self.n_fault_closures_brine, 1349 ], 1350 ): 1351 n_voxels = fluid[fluid[:] > 0.0].size 1352 msg = f"n_fault_closures_{name}: {num:03d}\n" 1353 msg += f"n_voxels_fault_closures_{name}: {n_voxels:08d}\n" 1354 print(msg) 1355 self.cfg.write_to_logfile(msg) 1356 self.cfg.write_to_logfile( 1357 msg=None, 1358 mainkey="model_parameters", 1359 subkey=f"n_fault_closures_{name}", 1360 val=num, 1361 ) 1362 self.cfg.write_to_logfile( 1363 msg=None, 1364 mainkey="model_parameters", 1365 subkey=f"n_voxels_fault_closures_{name}", 1366 val=n_voxels, 1367 ) 1368 closure_statistics = self.calculate_closure_statistics( 1369 fluid, f"Faulted {name.capitalize()}" 1370 ) 1371 if closure_statistics: 1372 print(closure_statistics) 1373 self.cfg.write_to_logfile(closure_statistics) 1374 1375 if "onlap" in self.cfg.closure_types: 1376 # Onlap Closures 1377 for name, fluid, num in zip( 1378 fluid_types, 1379 [ 1380 self.onlap_closures_oil[:], 1381 self.onlap_closures_gas[:], 1382 self.onlap_closures_brine[:], 1383 ], 1384 [ 1385 self.n_onlap_closures_oil, 1386 self.n_onlap_closures_gas, 1387 self.n_onlap_closures_brine, 1388 ], 1389 ): 1390 n_voxels = fluid[fluid[:] > 0.0].size 1391 msg = f"n_onlap_closures_{name}: {num:03d}\n" 1392 msg += f"n_voxels_onlap_closures_{name}: {n_voxels:08d}\n" 1393 print(msg) 1394 self.cfg.write_to_logfile(msg) 1395 self.cfg.write_to_logfile( 1396 msg=None, 1397 mainkey="model_parameters", 1398 subkey=f"n_onlap_closures_{name}", 1399 val=num, 1400 ) 1401 self.cfg.write_to_logfile( 1402 msg=None, 1403 mainkey="model_parameters", 1404 subkey=f"n_voxels_onlap_closures_{name}", 1405 val=n_voxels, 1406 ) 1407 closure_statistics = self.calculate_closure_statistics( 1408 fluid, f"Onlap {name.capitalize()}" 1409 ) 1410 if closure_statistics: 1411 print(closure_statistics) 1412 self.cfg.write_to_logfile(closure_statistics) 1413 1414 if "simple" in self.cfg.closure_types: 1415 # Simple Closures 1416 for name, fluid, num in zip( 1417 fluid_types, 1418 [ 1419 self.simple_closures_oil[:], 1420 self.simple_closures_gas[:], 1421 self.simple_closures_brine[:], 1422 ], 1423 [ 1424 self.n_4way_closures_oil, 1425 self.n_4way_closures_gas, 1426 self.n_4way_closures_brine, 1427 ], 1428 ): 1429 n_voxels = fluid[fluid[:] > 0.0].size 1430 msg = f"n_4way_closures_{name}: {num:03d}\n" 1431 msg += f"n_voxels_4way_closures_{name}: {n_voxels:08d}\n" 1432 print(msg) 1433 self.cfg.write_to_logfile(msg) 1434 self.cfg.write_to_logfile( 1435 msg=None, 1436 mainkey="model_parameters", 1437 subkey=f"n_4way_closures_{name}", 1438 val=num, 1439 ) 1440 self.cfg.write_to_logfile( 1441 msg=None, 1442 mainkey="model_parameters", 1443 subkey=f"n_voxels_4way_closures_{name}", 1444 val=n_voxels, 1445 ) 1446 closure_statistics = self.calculate_closure_statistics( 1447 fluid, f"4-Way {name.capitalize()}" 1448 ) 1449 if closure_statistics: 1450 print(closure_statistics) 1451 self.cfg.write_to_logfile(closure_statistics) 1452 1453 if self.cfg.include_salt: 1454 # Salt-Bounded Closures 1455 for name, fluid, num in zip( 1456 fluid_types, 1457 [ 1458 self.salt_closures_oil[:], 1459 self.salt_closures_gas[:], 1460 self.salt_closures_brine[:], 1461 ], 1462 [ 1463 self.n_salt_closures_oil, 1464 self.n_salt_closures_gas, 1465 self.n_salt_closures_brine, 1466 ], 1467 ): 1468 n_voxels = fluid[fluid[:] > 0.0].size 1469 msg = f"n_salt_closures_{name}: {num:03d}\n" 1470 msg += f"n_voxels_salt_closures_{name}: {n_voxels:08d}\n" 1471 print(msg) 1472 self.cfg.write_to_logfile(msg) 1473 self.cfg.write_to_logfile( 1474 msg=None, 1475 mainkey="model_parameters", 1476 subkey=f"n_salt_closures_{name}", 1477 val=num, 1478 ) 1479 self.cfg.write_to_logfile( 1480 msg=None, 1481 mainkey="model_parameters", 1482 subkey=f"n_voxels_salt_closures_{name}", 1483 val=n_voxels, 1484 ) 1485 closure_statistics = self.calculate_closure_statistics( 1486 fluid, f"Salt {name.capitalize()}" 1487 ) 1488 if closure_statistics: 1489 print(closure_statistics) 1490 self.cfg.write_to_logfile(closure_statistics) 1491 1492 def get_voxel_counts(self, closures): 1493 next_label = 0 1494 label_values = [0] 1495 label_counts = [closures[closures == 0].size] 1496 for i in range(closures.max() + 1): 1497 try: 1498 next_label = closures[closures > next_label].min() 1499 except (TypeError, ValueError): 1500 break 1501 label_values.append(next_label) 1502 label_counts.append(closures[closures == next_label].size) 1503 print( 1504 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1505 ) 1506 1507 print( 1508 f'{72 * "*"}\n\tNum Closures: {len(label_counts) - 1}\n\tVoxel counts\n{label_counts[1:]}\n{72 * "*"}' 1509 ) 1510 for vox_count in label_counts: 1511 if vox_count < self.cfg.closure_min_voxels: 1512 print(f"voxel_count: {vox_count}") 1513 1514 def populate_closure_dict(self, labels, fluid, seismic_nmf=None): 1515 clist = [] 1516 max_num = np.max(labels) 1517 if seismic_nmf is not None: 1518 # calculate ai_gi 1519 ai, gi = compute_ai_gi(self.cfg, seismic_nmf) 1520 for i in range(1, max_num + 1): 1521 _c = np.where(labels == i) 1522 cl = dict() 1523 cl["model_id"] = os.path.basename(self.cfg.work_subfolder) 1524 cl["fluid"] = fluid 1525 cl["n_voxels"] = len(_c[0]) 1526 # np.min() or x.min() returns type numpy.int64 which SQLITE cannot handle. Convert to int 1527 cl["x_min"] = int(np.min(_c[0])) 1528 cl["x_max"] = int(np.max(_c[0])) 1529 cl["y_min"] = int(np.min(_c[1])) 1530 cl["y_max"] = int(np.max(_c[1])) 1531 cl["z_min"] = int(np.min(_c[2])) 1532 cl["z_max"] = int(np.max(_c[2])) 1533 cl["zbml_min"] = np.min(self.faults.faulted_depth[_c]) 1534 cl["zbml_max"] = np.max(self.faults.faulted_depth[_c]) 1535 cl["zbml_avg"] = np.mean(self.faults.faulted_depth[_c]) 1536 cl["zbml_std"] = np.std(self.faults.faulted_depth[_c]) 1537 cl["zbml_25pct"] = np.percentile(self.faults.faulted_depth[_c], 25) 1538 cl["zbml_median"] = np.percentile(self.faults.faulted_depth[_c], 50) 1539 cl["zbml_75pct"] = np.percentile(self.faults.faulted_depth[_c], 75) 1540 cl["ng_min"] = np.min(self.faults.faulted_net_to_gross[_c]) 1541 cl["ng_max"] = np.max(self.faults.faulted_net_to_gross[_c]) 1542 cl["ng_avg"] = np.mean(self.faults.faulted_net_to_gross[_c]) 1543 cl["ng_std"] = np.std(self.faults.faulted_net_to_gross[_c]) 1544 cl["ng_25pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 25) 1545 cl["ng_median"] = np.median(self.faults.faulted_net_to_gross[_c]) 1546 cl["ng_75pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 75) 1547 # Check for intersections with faults, salt and onlaps for closure type 1548 cl["intersects_fault"] = False 1549 cl["intersects_onlap"] = False 1550 cl["intersects_salt"] = False 1551 if np.max(self.wide_faults[_c] > 0): 1552 cl["intersects_fault"] = True 1553 if np.max(self.onlaps_upward[_c] > 0): 1554 cl["intersects_onlap"] = True 1555 if self.cfg.include_salt and np.max(self.wide_salt[_c] > 0): 1556 cl["intersects_salt"] = True 1557 1558 if seismic_nmf is not None: 1559 # Using only the top of the closure, calculate seismic properties 1560 labels_copy = labels.copy() 1561 labels_copy[labels_copy != i] = 0 1562 top_closure = get_top_of_closure(labels_copy) 1563 near = seismic_nmf[0, ...][np.where(top_closure == 1)] 1564 cl["near_min"] = np.min(near) 1565 cl["near_max"] = np.max(near) 1566 cl["near_avg"] = np.mean(near) 1567 cl["near_std"] = np.std(near) 1568 cl["near_25pct"] = np.percentile(near, 25) 1569 cl["near_median"] = np.percentile(near, 50) 1570 cl["near_75pct"] = np.percentile(near, 75) 1571 mid = seismic_nmf[1, ...][np.where(top_closure == 1)] 1572 cl["mid_min"] = np.min(mid) 1573 cl["mid_max"] = np.max(mid) 1574 cl["mid_avg"] = np.mean(mid) 1575 cl["mid_std"] = np.std(mid) 1576 cl["mid_25pct"] = np.percentile(mid, 25) 1577 cl["mid_median"] = np.percentile(mid, 50) 1578 cl["mid_75pct"] = np.percentile(mid, 75) 1579 far = seismic_nmf[2, ...][np.where(top_closure == 1)] 1580 cl["far_min"] = np.min(far) 1581 cl["far_max"] = np.max(far) 1582 cl["far_avg"] = np.mean(far) 1583 cl["far_std"] = np.std(far) 1584 cl["far_25pct"] = np.percentile(far, 25) 1585 cl["far_median"] = np.percentile(far, 50) 1586 cl["far_75pct"] = np.percentile(far, 75) 1587 intercept = ai[np.where(top_closure == 1)] 1588 cl["intercept_min"] = np.min(intercept) 1589 cl["intercept_max"] = np.max(intercept) 1590 cl["intercept_avg"] = np.mean(intercept) 1591 cl["intercept_std"] = np.std(intercept) 1592 cl["intercept_25pct"] = np.percentile(intercept, 25) 1593 cl["intercept_median"] = np.percentile(intercept, 50) 1594 cl["intercept_75pct"] = np.percentile(intercept, 75) 1595 gradient = gi[np.where(top_closure == 1)] 1596 cl["gradient_min"] = np.min(gradient) 1597 cl["gradient_max"] = np.max(gradient) 1598 cl["gradient_avg"] = np.mean(gradient) 1599 cl["gradient_std"] = np.std(gradient) 1600 cl["gradient_25pct"] = np.percentile(gradient, 25) 1601 cl["gradient_median"] = np.percentile(gradient, 50) 1602 cl["gradient_75pct"] = np.percentile(gradient, 75) 1603 1604 clist.append(cl) 1605 1606 return clist 1607 1608 def write_closure_info_to_log(self, seismic_nmf=None): 1609 """store info about closure in log file""" 1610 top_sand_layers = [x for x in self.top_lith_indices if self.facies[x] == 1.0] 1611 self.cfg.write_to_logfile( 1612 msg=None, 1613 mainkey="model_parameters", 1614 subkey="top_sand_layers", 1615 val=top_sand_layers, 1616 ) 1617 o = measure.label(self.oil_closures[:], connectivity=2, background=0) 1618 g = measure.label(self.gas_closures[:], connectivity=2, background=0) 1619 b = measure.label(self.brine_closures[:], connectivity=2, background=0) 1620 oil_closures = self.populate_closure_dict(o, "oil", seismic_nmf) 1621 gas_closures = self.populate_closure_dict(g, "gas", seismic_nmf) 1622 brine_closures = self.populate_closure_dict(b, "brine", seismic_nmf) 1623 all_closures = oil_closures + gas_closures + brine_closures 1624 for i, c in enumerate(all_closures): 1625 self.cfg.sqldict[f"closure_{i+1}"] = c 1626 num_labels = np.max(o) + np.max(g) 1627 self.cfg.write_to_logfile( 1628 msg=None, 1629 mainkey="model_parameters", 1630 subkey="number_hc_closures", 1631 val=num_labels, 1632 ) 1633 # Add total number of closure voxels, with ratio of closure voxels given as a percentage 1634 closure_voxel_count = o[o > 0].size + g[g > 0].size 1635 closure_voxel_pct = closure_voxel_count / o.size 1636 self.cfg.write_to_logfile( 1637 msg=None, 1638 mainkey="model_parameters", 1639 subkey="closure_voxel_count", 1640 val=closure_voxel_count, 1641 ) 1642 self.cfg.write_to_logfile( 1643 msg=None, 1644 mainkey="model_parameters", 1645 subkey="closure_voxel_pct", 1646 val=closure_voxel_pct * 100, 1647 ) 1648 # Same for Brine 1649 _brine_voxels = b[b == 1].size 1650 _brine_voxels_pct = _brine_voxels / b.size 1651 self.cfg.write_to_logfile( 1652 msg=None, 1653 mainkey="model_parameters", 1654 subkey="closure_voxel_count_brine", 1655 val=_brine_voxels, 1656 ) 1657 self.cfg.write_to_logfile( 1658 msg=None, 1659 mainkey="model_parameters", 1660 subkey="closure_voxel_pct_brine", 1661 val=_brine_voxels_pct * 100, 1662 ) 1663 # Same for Oil 1664 _oil_voxels = o[o == 1].size 1665 _oil_voxels_pct = _oil_voxels / o.size 1666 self.cfg.write_to_logfile( 1667 msg=None, 1668 mainkey="model_parameters", 1669 subkey="closure_voxel_count_oil", 1670 val=_oil_voxels, 1671 ) 1672 self.cfg.write_to_logfile( 1673 msg=None, 1674 mainkey="model_parameters", 1675 subkey="closure_voxel_pct_oil", 1676 val=_oil_voxels_pct * 100, 1677 ) 1678 # Same for Gas 1679 _gas_voxels = g[g == 1].size 1680 _gas_voxels_pct = _gas_voxels / g.size 1681 self.cfg.write_to_logfile( 1682 msg=None, 1683 mainkey="model_parameters", 1684 subkey="closure_voxel_count_gas", 1685 val=_gas_voxels, 1686 ) 1687 self.cfg.write_to_logfile( 1688 msg=None, 1689 mainkey="model_parameters", 1690 subkey="closure_voxel_pct_gas", 1691 val=_gas_voxels_pct, 1692 ) 1693 # Write old logfile as well as the sql dict 1694 msg = f"layers for closure computation: {str(self.top_lith_indices)}\n" 1695 msg += f"Number of HC Closures : {num_labels}\n" 1696 msg += ( 1697 f"Closure voxel count: {closure_voxel_count} - " 1698 f"{closure_voxel_pct:5.2%}\n" 1699 ) 1700 msg += ( 1701 f"Closure voxel count: (brine) {_brine_voxels} - {_brine_voxels_pct:5.2%}\n" 1702 ) 1703 msg += f"Closure voxel count: (oil) {_oil_voxels} - {_oil_voxels_pct:5.2%}\n" 1704 msg += f"Closure voxel count: (gas) {_gas_voxels} - {_gas_voxels_pct:5.2%}\n" 1705 print(msg) 1706 for i in range(self.facies.shape[0]): 1707 if self.facies[i] == 1: 1708 msg += f" layers for closure computation: {i}, sand\n" 1709 else: 1710 msg += f" layers for closure computation: {i}, shale\n" 1711 self.cfg.write_to_logfile(msg) 1712 1713 def parse_label_values_and_counts(self, labels_clean): 1714 """parse label values and counts""" 1715 if self.cfg.verbose: 1716 print(" Inside parse_label_values_and_counts") 1717 next_label = 0 1718 label_values = [0] 1719 label_counts = [labels_clean[labels_clean == 0].size] 1720 for i in range(1, labels_clean.max() + 1): 1721 try: 1722 next_label = labels_clean[labels_clean > next_label].min() 1723 except (TypeError, ValueError): 1724 break 1725 label_values.append(next_label) 1726 label_counts.append(labels_clean[labels_clean == next_label].size) 1727 print( 1728 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1729 ) 1730 # force labels to use consecutive integer values 1731 for i, ilabel in enumerate(label_values): 1732 labels_clean[labels_clean == ilabel] = i 1733 label_values[i] = i 1734 # labels_clean = self.remove_small_objects(labels_clean) # already applied to labels_clean 1735 # Remove label_value 0 1736 label_values.remove(0) 1737 return label_values, labels_clean 1738 1739 def assign_fluid_types(self, label_values, labels_clean): 1740 """randomly assign oil or gas to closure""" 1741 print( 1742 " labels_clean.min(), labels_clean.max() = ", 1743 labels_clean.min(), 1744 labels_clean.max(), 1745 ) 1746 _brine_closures = (labels_clean * 0.0).astype("uint8") 1747 _oil_closures = (labels_clean * 0.0).astype("uint8") 1748 _gas_closures = (labels_clean * 0.0).astype("uint8") 1749 1750 fluid_type_code = np.random.randint(3, size=labels_clean.max() + 1) 1751 1752 _closure_segments = self.closure_segments[:] 1753 for i in range(1, labels_clean.max() + 1): 1754 voxel_count = labels_clean[labels_clean == i].size 1755 if voxel_count > 0: 1756 print(f"Voxel Count: {voxel_count}\tFluid type: {fluid_type_code[i]}") 1757 # not in closure = 0 1758 # closure with brine filled reservoir fluid_type_code = 1 1759 # closure with oil filled reservoir fluid_type_code = 2 1760 # closure with gas filled reservoir fluid_type_code = 3 1761 if i in label_values: 1762 if fluid_type_code[i] == 0: 1763 # brine: change labels_clean contents to fluid_type_code = 1 (same as background) 1764 _brine_closures[ 1765 np.logical_and(labels_clean == i, _closure_segments > 0) 1766 ] = 1 1767 elif fluid_type_code[i] == 1: 1768 # oil: change labels_clean contents to fluid_type_code = 2 1769 _oil_closures[labels_clean == i] = 1 1770 elif fluid_type_code[i] == 2: 1771 # gas: change labels_clean contents to fluid_type_code = 3 1772 _gas_closures[labels_clean == i] = 1 1773 return _oil_closures, _gas_closures, _brine_closures 1774 1775 def remove_small_objects(self, labels, min_filter=True): 1776 try: 1777 # Use the global minimum voxel size initially, before closure types are identified 1778 labels_clean = morphology.remove_small_objects( 1779 labels, self.cfg.closure_min_voxels 1780 ) 1781 if self.cfg.verbose: 1782 print("labels_clean succeeded.") 1783 print( 1784 " labels.min:{}, labels.max: {}".format(labels.min(), labels.max()) 1785 ) 1786 print( 1787 " labels_clean min:{}, labels_clean max: {}".format( 1788 labels_clean.min(), labels_clean.max() 1789 ) 1790 ) 1791 except Exception as e: 1792 print( 1793 f"Closures/create_closures: labels_clean (remove_small_objects) did not succeed: {e}" 1794 ) 1795 if min_filter: 1796 labels_clean = minimum_filter(labels, size=(3, 3, 3)) 1797 if self.cfg.verbose: 1798 print( 1799 " labels.min:{}, labels.max: {}".format( 1800 labels.min(), labels.max() 1801 ) 1802 ) 1803 print( 1804 " labels_clean min:{}, labels_clean max: {}".format( 1805 labels_clean.min(), labels_clean.max() 1806 ) 1807 ) 1808 return labels_clean 1809 1810 def segment_closures(self, _closure_segments, remove_shale=True): 1811 """Segment the closures so that they can be randomly filled with hydrocarbons""" 1812 1813 _closure_segments = np.clip(_closure_segments, 0.0, 1.0) 1814 # remove tiny clusters 1815 _closure_segments = minimum_filter( 1816 _closure_segments.astype("int16"), size=(3, 3, 1) 1817 ) 1818 _closure_segments = maximum_filter(_closure_segments, size=(3, 3, 1)) 1819 1820 if remove_shale: 1821 # restrict closures to sand (non-shale) voxels 1822 if self.faults.faulted_lithology.shape[2] == _closure_segments.shape[2]: 1823 sand_shale = self.faults.faulted_lithology[:].copy() 1824 else: 1825 sand_shale = self.faults.faulted_lithology[ 1826 :, :, :: self.cfg.infill_factor 1827 ].copy() 1828 _closure_segments[sand_shale <= 0.0] = 0 1829 del sand_shale 1830 labels = measure.label(_closure_segments, connectivity=2, background=0) 1831 1832 labels_clean = self.remove_small_objects(labels) 1833 return labels_clean, _closure_segments 1834 1835 def write_closure_volumes_to_disk(self): 1836 # Create files for closure volumes 1837 self.write_cube_to_disk(self.brine_closures[:], "closure_segments_brine") 1838 self.write_cube_to_disk(self.oil_closures[:], "closure_segments_oil") 1839 self.write_cube_to_disk(self.gas_closures[:], "closure_segments_gas") 1840 # Create combined HC cube by adding oil and gas closures 1841 self.hc_labels[:] = (self.oil_closures[:] + self.gas_closures[:]).astype( 1842 "uint8" 1843 ) 1844 self.write_cube_to_disk(self.hc_labels[:], "closure_segments_hc") 1845 1846 if self.cfg.model_qc_volumes: 1847 self.write_cube_to_disk(self.closure_segments, "closure_segments_raw_all") 1848 self.write_cube_to_disk(self.simple_closures, "closure_segments_simple") 1849 self.write_cube_to_disk(self.strat_closures, "closure_segments_strat") 1850 self.write_cube_to_disk(self.fault_closures, "closure_segments_fault") 1851 1852 # Triple check that no small closures exist in the final closure files 1853 for i, c in enumerate( 1854 [ 1855 self.oil_closures, 1856 self.gas_closures, 1857 self.simple_closures, 1858 self.strat_closures, 1859 self.fault_closures, 1860 ] 1861 ): 1862 _t = measure.label(c, connectivity=2, background=0) 1863 counts = [_t[_t == x].size for x in range(np.max(_t))] 1864 print(f"Final closure volume voxels sizes: {counts}") 1865 for n, x in enumerate(counts): 1866 if x < self.cfg.closure_min_voxels: 1867 print(f"Voxel count: {x}\t Count:{i}, index: {n}") 1868 1869 # Return the hydrocarbon closure labels so that augmentation can be applied to the data & labels 1870 # return self.oil_closures + self.gas_closures 1871 1872 def calculate_closure_statistics(self, in_array, closure_type): 1873 """ 1874 Calculate the size and location of isolated features in an array 1875 1876 :param in_array: ndarray. Input array to be labelled, where non-zero values are counted as features 1877 :param closure_type: string. Closure type label 1878 :param digi: int or float. To convert depth values from samples to units 1879 :return: string. Concatenated string of closure statistics to be written to log 1880 """ 1881 labelled_array, max_labels = measure.label( 1882 in_array, connectivity=2, return_num=True 1883 ) 1884 msg = "" 1885 for i in range(1, max_labels + 1): # start at 1 to avoid counting 0's 1886 trap = np.where(labelled_array == i) 1887 ranges = [([np.min(trap[x]), np.max(trap[x])]) for x, _ in enumerate(trap)] 1888 sizes = [x[1] - x[0] for x in ranges] 1889 n_voxels = labelled_array[labelled_array == i].size 1890 if sum(sizes) > 0: 1891 msg += ( 1892 f"{closure_type}\t" 1893 f"Num X,Y,Z Samples: {str(sizes).ljust(15)}\t" 1894 f"Num Voxels: {str(n_voxels).ljust(5)}\t" 1895 f"Track: {2000 + ranges[0][0]}-{2000 + ranges[0][1]}\t" 1896 f"Bin: {1000 + ranges[1][0]}-{1000 + ranges[1][1]}\t" 1897 f"Depth: {ranges[2][0] * self.cfg.digi}-{ranges[2][1] * self.cfg.digi + self.cfg.digi / 2}\n" 1898 ) 1899 return msg 1900 1901 def find_faulted_closures(self, closure_segment_list, closure_segments): 1902 self._dilate_faults() 1903 for iclosure in closure_segment_list: 1904 i, j, k = np.where(closure_segments == iclosure) 1905 faults_within_closure = self.wide_faults[i, j, k] 1906 if faults_within_closure.max() > 0: 1907 if self.oil_closures[i, j, k].max() > 0: 1908 # Faulted oil closure 1909 self.faulted_closures_oil[i, j, k] = 1.0 1910 self.n_fault_closures_oil += 1 1911 self.fault_closures_oil_segment_list.append(iclosure) 1912 elif self.gas_closures[i, j, k].max() > 0: 1913 # Faulted gas closure 1914 self.faulted_closures_gas[i, j, k] = 1.0 1915 self.n_fault_closures_gas += 1 1916 self.fault_closures_gas_segment_list.append(iclosure) 1917 elif self.brine_closures[i, j, k].max() > 0: 1918 # Faulted brine closure 1919 self.faulted_closures_brine[i, j, k] = 1.0 1920 self.n_fault_closures_brine += 1 1921 self.fault_closures_brine_segment_list.append(iclosure) 1922 else: 1923 print( 1924 "Closure is faulted but does not have oil, gas or brine assigned" 1925 ) 1926 1927 def find_onlap_closures(self, closure_segment_list, closure_segments): 1928 for iclosure in closure_segment_list: 1929 i, j, k = np.where(closure_segments == iclosure) 1930 onlaps_within_closure = self.onlaps_upward[i, j, k] 1931 if onlaps_within_closure.max() > 0: 1932 if self.oil_closures[i, j, k].max() > 0: 1933 self.onlap_closures_oil[i, j, k] = 1.0 1934 self.n_onlap_closures_oil += 1 1935 self.onlap_closures_oil_segment_list.append(iclosure) 1936 elif self.gas_closures[i, j, k].max() > 0: 1937 self.onlap_closures_gas[i, j, k] = 1.0 1938 self.n_onlap_closures_gas += 1 1939 self.onlap_closures_gas_segment_list.append(iclosure) 1940 elif self.brine_closures[i, j, k].max() > 0: 1941 self.onlap_closures_brine[i, j, k] = 1.0 1942 self.n_onlap_closures_brine += 1 1943 self.onlap_closures_brine_segment_list.append(iclosure) 1944 else: 1945 print( 1946 "Closure is onlap but does not have oil, gas or brine assigned" 1947 ) 1948 1949 def find_simple_closures(self, closure_segment_list, closure_segments): 1950 for iclosure in closure_segment_list: 1951 i, j, k = np.where(closure_segments == iclosure) 1952 faults_within_closure = self.wide_faults[i, j, k] 1953 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 1954 onlaps_within_closure = onlaps[i, j, k] 1955 oil_within_closure = self.oil_closures[i, j, k] 1956 gas_within_closure = self.gas_closures[i, j, k] 1957 brine_within_closure = self.brine_closures[i, j, k] 1958 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 1959 if oil_within_closure.max() > 0: 1960 self.simple_closures_oil[i, j, k] = 1.0 1961 self.n_4way_closures_oil += 1 1962 elif gas_within_closure.max() > 0: 1963 self.simple_closures_gas[i, j, k] = 1.0 1964 self.n_4way_closures_gas += 1 1965 elif brine_within_closure.max() > 0: 1966 self.simple_closures_brine[i, j, k] = 1.0 1967 self.n_4way_closures_brine += 1 1968 else: 1969 print( 1970 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 1971 ) 1972 1973 def find_false_closures(self, closure_segment_list, closure_segments): 1974 for iclosure in closure_segment_list: 1975 i, j, k = np.where(closure_segments == iclosure) 1976 faults_within_closure = self.fat_faults[i, j, k] 1977 onlaps_within_closure = self.onlaps_downward[i, j, k] 1978 for fluid, false, num in zip( 1979 [self.oil_closures, self.gas_closures, self.brine_closures], 1980 [ 1981 self.false_closures_oil, 1982 self.false_closures_gas, 1983 self.false_closures_brine, 1984 ], 1985 [ 1986 self.n_false_closures_oil, 1987 self.n_false_closures_gas, 1988 self.n_false_closures_brine, 1989 ], 1990 ): 1991 fluid_within_closure = fluid[i, j, k] 1992 if fluid_within_closure.max() > 0: 1993 if onlaps_within_closure.max() > 0: 1994 _faulted_closure_threshold = float( 1995 faults_within_closure[faults_within_closure > 0].size 1996 / fluid_within_closure[fluid_within_closure > 0].size 1997 ) 1998 _onlap_closure_threshold = float( 1999 onlaps_within_closure[onlaps_within_closure > 0].size 2000 / fluid_within_closure[fluid_within_closure > 0].size 2001 ) 2002 if ( 2003 _faulted_closure_threshold > 0.65 2004 and _onlap_closure_threshold > 0.65 2005 ): 2006 false[i, j, k] = 1 2007 num += 1 2008 2009 def find_salt_bounded_closures(self, closure_segment_list, closure_segments): 2010 self._dilate_salt() 2011 for iclosure in closure_segment_list: 2012 i, j, k = np.where(closure_segments == iclosure) 2013 salt_within_closure = self.wide_salt[i, j, k] 2014 if salt_within_closure.max() > 0: 2015 if self.oil_closures[i, j, k].max() > 0: 2016 # salt bounded oil closure 2017 self.salt_closures_oil[i, j, k] = 1.0 2018 self.n_salt_closures_oil += 1 2019 self.salt_closures_oil_segment_list.append(iclosure) 2020 elif self.gas_closures[i, j, k].max() > 0: 2021 # salt bounded gas closure 2022 self.salt_closures_gas[i, j, k] = 1.0 2023 self.n_salt_closures_gas += 1 2024 self.salt_closures_gas_segment_list.append(iclosure) 2025 elif self.brine_closures[i, j, k].max() > 0: 2026 # salt bounded brine closure 2027 self.salt_closures_brine[i, j, k] = 1.0 2028 self.n_salt_closures_brine += 1 2029 self.salt_closures_brine_segment_list.append(iclosure) 2030 else: 2031 print( 2032 "Closure is salt bounded but does not have oil, gas or brine assigned" 2033 ) 2034 2035 def find_faulted_all_closures(self, closure_segment_list, closure_segments): 2036 for iclosure in closure_segment_list: 2037 i, j, k = np.where(closure_segments == iclosure) 2038 faults_within_closure = self.wide_faults[i, j, k] 2039 if faults_within_closure.max() > 0: 2040 self.faulted_all_closures[i, j, k] = 1.0 2041 self.n_fault_all_closures += 1 2042 self.fault_all_closures_segment_list.append(iclosure) 2043 2044 def find_onlap_all_closures(self, closure_segment_list, closure_segments): 2045 for iclosure in closure_segment_list: 2046 i, j, k = np.where(closure_segments == iclosure) 2047 onlaps_within_closure = self.onlaps_upward[i, j, k] 2048 if onlaps_within_closure.max() > 0: 2049 self.onlap_all_closures[i, j, k] = 1.0 2050 self.n_onlap_all_closures += 1 2051 self.onlap_all_closures_segment_list.append(iclosure) 2052 2053 def find_simple_all_closures(self, closure_segment_list, closure_segments): 2054 for iclosure in closure_segment_list: 2055 i, j, k = np.where(closure_segments == iclosure) 2056 faults_within_closure = self.wide_faults[i, j, k] 2057 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 2058 onlaps_within_closure = onlaps[i, j, k] 2059 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2060 self.simple_all_closures[i, j, k] = 1.0 2061 self.n_4way_all_closures += 1 2062 2063 def find_false_all_closures(self, closure_segment_list, closure_segments): 2064 for iclosure in closure_segment_list: 2065 i, j, k = np.where(closure_segments == iclosure) 2066 faults_within_closure = self.fat_faults[i, j, k] 2067 onlaps_within_closure = self.onlaps_downward[i, j, k] 2068 if onlaps_within_closure.max() > 0: 2069 _faulted_closure_threshold = float( 2070 faults_within_closure[faults_within_closure > 0].size / i.size 2071 ) 2072 _onlap_closure_threshold = float( 2073 onlaps_within_closure[onlaps_within_closure > 0].size / i.size 2074 ) 2075 if ( 2076 _faulted_closure_threshold > 0.65 2077 and _onlap_closure_threshold > 0.65 2078 ): 2079 self.false_all_closures[i, j, k] = 1 2080 self.n_false_all_closures += 1 2081 2082 def find_salt_bounded_all_closures(self, closure_segment_list, closure_segments): 2083 self._dilate_salt() 2084 for iclosure in closure_segment_list: 2085 i, j, k = np.where(closure_segments == iclosure) 2086 salt_within_closure = self.wide_salt[i, j, k] 2087 if salt_within_closure.max() > 0: 2088 self.salt_all_closures[i, j, k] = 1.0 2089 self.n_salt_all_closures += 1 2090 self.salt_all_closures_segment_list.append(iclosure) 2091 2092 def _dilate_faults(self): 2093 thresholded_faults = self._threshold_volumes(self.faults.fault_planes[:]) 2094 self.wide_faults[:] = self.grow_lateral( 2095 thresholded_faults, iterations=9, dist=1 2096 ) 2097 self.fat_faults[:] = self.grow_lateral( 2098 thresholded_faults, iterations=21, dist=1 2099 ) 2100 if self.cfg.include_salt: 2101 # Treat the salt body as a fault to grow closures to boundary 2102 thresholded_salt = self._threshold_volumes( 2103 self.faults.salt_model.salt_segments[:] 2104 ) 2105 wide_salt = self.grow_lateral(thresholded_salt, iterations=9, dist=1) 2106 self.wide_salt[:] = wide_salt 2107 # Add salt to faults to cehck if growing the closure works 2108 self.wide_faults[:] += wide_salt 2109 2110 def _dilate_salt(self): 2111 thresholded_salt = self._threshold_volumes( 2112 self.faults.salt_model.salt_segments[:] 2113 ) 2114 wide_salt = self.grow_lateral(thresholded_salt, iterations=9, dist=1) 2115 self.wide_salt[:] = wide_salt 2116 2117 def _dilate_onlaps(self): 2118 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 2119 mask = np.zeros((1, 1, 3)) 2120 mask[0, 0, :2] = 1 2121 self.onlaps_upward[:] = morphology.binary_dilation(onlaps, mask) 2122 mask = np.zeros((1, 1, 3)) 2123 mask[0, 0, 1:] = 1 2124 self.onlaps_downward[:] = onlaps.copy() 2125 for _ in range(30): 2126 try: 2127 self.onlaps_downward[:] = morphology.binary_dilation( 2128 self.onlaps_downward[:], mask 2129 ) 2130 except: 2131 break 2132 2133 def grow_to_fault2( 2134 self, closures, grow_only_sand_closures=True, remove_small_closures=True 2135 ): 2136 # - grow closures laterally and up within layer and within fault block 2137 print( 2138 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2139 ) 2140 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2141 2142 # dilated_fault_closures = closures.copy() 2143 # n_faulted_closures = dilated_fault_closures.max() 2144 labels_clean = self.closure_segments[:].copy() 2145 labels_clean[closures == 0] = 0 2146 labels_clean_list = list(set(labels_clean.flatten())) 2147 labels_clean_list.remove(0) 2148 initial_closures = labels_clean.copy() 2149 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2150 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2151 2152 # TODO remove this once small closures are found and fixed 2153 voxel_sizes = [ 2154 self.closure_segments[self.closure_segments[:] == i].size 2155 for i in labels_clean_list 2156 ] 2157 for _v in voxel_sizes: 2158 print(f"Voxel_Sizes: {_v}") 2159 if _v < self.cfg.closure_min_voxels: 2160 print(_v) 2161 2162 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2163 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2164 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2165 _ng = self.faults.faulted_net_to_gross[:].copy() 2166 # Cannot solely use NG anymore since shales may have variable net to gross 2167 _lith = self.faults.faulted_lithology[:].copy() 2168 _age = self.faults.faulted_age_volume[:].copy() 2169 fault_throw = self.faults.max_fault_throw[:] 2170 2171 for il, i in enumerate(labels_clean_list): 2172 fault_blocks_list = list(set(fault_throw[labels_clean == i].flatten())) 2173 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2174 for jl, j in enumerate(fault_blocks_list): 2175 print( 2176 "\n\n ... label, throw = ", 2177 i, 2178 j, 2179 list(set(fault_throw[labels_clean == i].flatten())), 2180 labels_clean[labels_clean == i].size, 2181 fault_throw[fault_throw == j].size, 2182 fault_throw[ 2183 np.where((labels_clean == i) & (fault_throw[:] == j)) 2184 ].size, 2185 ) 2186 single_closure = labels_clean * 0.0 2187 size = single_closure[ 2188 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2189 ].size 2190 if size >= self.cfg.closure_min_voxels: 2191 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2192 single_closure[ 2193 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2194 ] = 1 2195 if single_closure[single_closure > 0].size == 0: 2196 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2197 labels_clean[np.where(labels_clean == i)] = 0 2198 continue 2199 avg_ng = _ng[single_closure == 1].mean() 2200 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2201 _ng_voxels = _ng[single_closure == 1] 2202 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2203 min_geo_age = _geo_age_voxels.min() - 0.5 2204 avg_geo_age = int(_geo_age_voxels.mean()) 2205 max_geo_age = _geo_age_voxels.max() + 0.5 2206 _depth_geobody_voxels = depth_cube[single_closure == 1] 2207 min_depth = _depth_geobody_voxels.min() 2208 max_depth = _depth_geobody_voxels.max() 2209 avg_throw = np.median(fault_throw[single_closure == 1]) 2210 2211 closure_boundary_cube = closures * 0.0 2212 if grow_only_sand_closures: 2213 lith_flag = _lith > 0.0 2214 else: 2215 lith_flag = _lith >= 0.0 2216 closure_boundary_cube[ 2217 np.where( 2218 lith_flag 2219 & (_age > min_geo_age) 2220 & (_age < max_geo_age) 2221 & (fault_throw == avg_throw) 2222 & (depth_cube <= max_depth) 2223 ) 2224 ] = 1.0 2225 print( 2226 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2227 closure_boundary_cube[closure_boundary_cube == 1].size, 2228 ) 2229 2230 n_voxel = single_closure[single_closure == 1].size 2231 2232 original_voxels = n_voxel + 0 2233 print( 2234 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2235 i, 2236 j, 2237 n_voxel, 2238 (min_geo_age, avg_geo_age, max_geo_age), 2239 (min_depth, max_depth), 2240 avg_ng, 2241 il, 2242 " / ", 2243 len(labels_clean_list), 2244 ) 2245 2246 grown_closure = single_closure.copy() 2247 grown_closure[depth_cube >= max_depth] = 0 2248 delta_voxel = 0 2249 previous_delta_voxel = 1e9 2250 converged = False 2251 for ii in range(15): 2252 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2253 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2254 # stay within layer, within age, within fault block, above HCWC 2255 grown_closure[closure_boundary_cube == 0.0] = 0.0 2256 single_closure = single_closure + grown_closure 2257 single_closure[single_closure > 0] = i 2258 new_n_voxel = single_closure[single_closure > 0].size 2259 previous_delta_voxel = delta_voxel + 0 2260 delta_voxel = new_n_voxel - n_voxel 2261 print( 2262 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2263 " delta_voxel>previous_delta_voxel = ", 2264 i, 2265 ii, 2266 new_n_voxel, 2267 delta_voxel, 2268 previous_delta_voxel, 2269 delta_voxel > previous_delta_voxel, 2270 ) 2271 if n_voxel == new_n_voxel: 2272 # finish bottom voxel layer near "HCWC" 2273 grown_closure = self.grow_downward( 2274 grown_closure, 1, dist=1, verbose=False 2275 ) 2276 # stay within layer, within age, within fault block, above HCWC 2277 grown_closure[closure_boundary_cube == 0.0] = 0.0 2278 single_closure = single_closure + grown_closure 2279 single_closure[single_closure > 0] = i 2280 converged = True 2281 break 2282 else: 2283 n_voxel = new_n_voxel 2284 previous_delta_voxel = delta_voxel + 0 2285 if converged is True: 2286 labels_clean[single_closure > 0] = i 2287 msg_postscript = " converged" 2288 else: 2289 labels_clean[labels_clean == i] = -i 2290 msg_postscript = " NOT converged" 2291 msg = ( 2292 f"closure_id: {int(i):04d}, fault_id: {int(j + .5):04d}, " 2293 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2294 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2295 ) 2296 print(msg + msg_postscript) 2297 self.cfg.write_to_logfile(msg + msg_postscript) 2298 2299 # Set small closures to 0 after growth 2300 # _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2301 # for x in np.unique(_grown_labels): 2302 # size = _grown_labels[_grown_labels == x].size 2303 # print(f'Size before editing: {size}') 2304 # if size < self.cfg.closure_min_voxels: 2305 # labels_clean[_grown_labels == x] = 0. 2306 # for x in np.unique(labels_clean): 2307 # size = labels_clean[labels_clean == x].size 2308 # print(f'Size after editing: {size}') 2309 2310 if remove_small_closures: 2311 _initial_labels = measure.label( 2312 initial_closures, connectivity=2, background=0 2313 ) 2314 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2315 for x in np.unique(_grown_labels): 2316 size_initial = _initial_labels[_initial_labels == x].size 2317 size_grown = _grown_labels[_grown_labels == x].size 2318 print(f"Size before editing: {size_initial}") 2319 print(f"Size after editing: {size_grown}") 2320 if size_grown < self.cfg.closure_min_voxels: 2321 print( 2322 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2323 ) 2324 labels_clean[_grown_labels == x] = 0.0 2325 return labels_clean 2326 2327 def grow_to_salt(self, closures): 2328 # - grow closures laterally and up within layer up to salt body 2329 print("\n\n ... grow_to_salt: grow closures laterally and up within layer ...") 2330 self.cfg.write_to_logfile("growing closures to salt body: grow_to_salt") 2331 2332 labels_clean = measure.label( 2333 self.closure_segments[:], connectivity=2, background=0 2334 ) 2335 labels_clean[closures == 0] = 0 2336 # labels_clean = self.closure_segments[:].copy() 2337 # labels_clean[closures == 0] = 0 2338 labels_clean_list = list(set(labels_clean.flatten())) 2339 labels_clean_list.remove(0) 2340 initial_closures = labels_clean.copy() 2341 print("\n ... grow_to_salt: n_salt_closures = ", len(labels_clean_list)) 2342 print(" ... grow_to_salt: salt_closures = ", labels_clean_list) 2343 2344 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2345 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2346 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2347 _ng = self.faults.faulted_net_to_gross[:].copy() 2348 _age = self.faults.faulted_age_volume[:].copy() 2349 salt = self.faults.salt_model.salt_segments[:] 2350 2351 for il, i in enumerate(labels_clean_list): 2352 salt_list = list(set(salt[labels_clean == i].flatten())) 2353 print(" ... grow_to_fault2: salt_list = ", salt_list) 2354 single_closure = labels_clean * 0.0 2355 size = single_closure[np.where(labels_clean == i)].size 2356 if size >= self.cfg.closure_min_voxels: 2357 print(f"Label: {i}, Voxel_Count: {size}") 2358 single_closure[np.where(labels_clean == i)] = 1 2359 if single_closure[single_closure > 0].size == 0: 2360 labels_clean[np.where(labels_clean == i)] = 0 2361 continue 2362 avg_ng = _ng[single_closure == 1].mean() 2363 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2364 _ng_voxels = _ng[single_closure == 1] 2365 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2366 min_geo_age = _geo_age_voxels.min() - 0.5 2367 avg_geo_age = int(_geo_age_voxels.mean()) 2368 max_geo_age = _geo_age_voxels.max() + 0.5 2369 _depth_geobody_voxels = depth_cube[single_closure == 1] 2370 min_depth = _depth_geobody_voxels.min() 2371 max_depth = _depth_geobody_voxels.max() 2372 # Define AOI where salt has been dilated 2373 # close_to_salt = np.zeros_like(salt) 2374 # close_to_salt[self.wide_salt[:] == 1] = 1.0 2375 # close_to_salt[salt == 1] = 0.0 2376 2377 closure_boundary_cube = closures * 0.0 2378 closure_boundary_cube[ 2379 np.where( 2380 (_ng > 0.3) 2381 & (_age > min_geo_age) # account for partial voxels 2382 & (_age < max_geo_age) 2383 & (salt == 0.0) 2384 & (depth_cube <= max_depth) 2385 ) 2386 ] = 1.0 2387 print( 2388 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2389 closure_boundary_cube[closure_boundary_cube == 1].size, 2390 ) 2391 2392 n_voxel = single_closure[single_closure == 1].size 2393 2394 original_voxels = n_voxel + 0 2395 print( 2396 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2397 i, 2398 n_voxel, 2399 (min_geo_age, avg_geo_age, max_geo_age), 2400 (min_depth, max_depth), 2401 avg_ng, 2402 il, 2403 " / ", 2404 len(labels_clean_list), 2405 ) 2406 2407 grown_closure = single_closure.copy() 2408 grown_closure[depth_cube >= max_depth] = 0 2409 delta_voxel = 0 2410 previous_delta_voxel = 1e9 2411 converged = False 2412 for ii in range(99): 2413 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2414 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2415 # stay within layer, within age, close to salt and above HCWC 2416 grown_closure[closure_boundary_cube == 0.0] = 0.0 2417 single_closure = single_closure + grown_closure 2418 single_closure[single_closure > 0] = i 2419 new_n_voxel = single_closure[single_closure > 0].size 2420 previous_delta_voxel = delta_voxel + 0 2421 delta_voxel = new_n_voxel - n_voxel 2422 print( 2423 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2424 " delta_voxel>previous_delta_voxel = ", 2425 i, 2426 ii, 2427 new_n_voxel, 2428 delta_voxel, 2429 previous_delta_voxel, 2430 delta_voxel > previous_delta_voxel, 2431 ) 2432 2433 # If grown voxel is touching the egde of survey, stop and remove closure 2434 _a, _b, _ = np.where(single_closure > 0) 2435 max_boundary_i = self.cfg.cube_shape[0] - 1 2436 max_boundary_j = self.cfg.cube_shape[1] - 1 2437 if ( 2438 np.min(_a) == 0 2439 or np.max(_a) == max_boundary_i 2440 or np.min(_b) == 0 2441 or np.max(_b) == max_boundary_j 2442 ): 2443 print("Boundary reached, removing closure") 2444 converged = False 2445 break 2446 2447 if n_voxel == new_n_voxel: 2448 # finish bottom voxel layer near HCWC 2449 grown_closure = self.grow_downward( 2450 grown_closure, 1, dist=1, verbose=False 2451 ) 2452 # stay within layer, within age, within fault block, above HCWC 2453 grown_closure[closure_boundary_cube == 0.0] = 0.0 2454 single_closure = single_closure + grown_closure 2455 single_closure[single_closure > 0] = i 2456 converged = True 2457 break 2458 else: 2459 n_voxel = new_n_voxel 2460 previous_delta_voxel = delta_voxel + 0 2461 if converged is True: 2462 labels_clean[single_closure > 0] = i 2463 msg_postscript = " converged" 2464 else: 2465 labels_clean[labels_clean == i] = -i 2466 msg_postscript = " NOT converged" 2467 msg = ( 2468 f"closure_id: {int(i):04d}, " 2469 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2470 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2471 ) 2472 print(msg + msg_postscript) 2473 self.cfg.write_to_logfile(msg + msg_postscript) 2474 2475 # Set small closures to 0 after growth 2476 _initial_labels = measure.label(initial_closures, connectivity=2, background=0) 2477 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2478 for x in np.unique(_grown_labels)[ 2479 1: 2480 ]: # ignore the first label of 0 (closures only) 2481 size_initial = _initial_labels[_initial_labels == x].size 2482 size_grown = _grown_labels[_grown_labels == x].size 2483 print(f"Size before editing: {size_initial}") 2484 print(f"Size after editing: {size_grown}") 2485 if size_grown < self.cfg.closure_min_voxels: 2486 print( 2487 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2488 ) 2489 labels_clean[_grown_labels == x] = 0.0 2490 2491 return labels_clean 2492 2493 @staticmethod 2494 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2495 from scipy.ndimage.morphology import grey_dilation 2496 2497 dist_size = 2 * dist + 1 2498 mask = np.zeros((dist_size, dist_size, 1)) 2499 mask[:, :, :] = 1 2500 _geobody = geobody.copy() 2501 if verbose: 2502 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2503 for k in range(iterations): 2504 try: 2505 _geobody = grey_dilation(_geobody, footprint=mask) 2506 if verbose: 2507 print( 2508 " ...grow_lateral: k, _geobody.shape = ", 2509 k, 2510 _geobody[_geobody > 0].shape, 2511 ) 2512 except: 2513 break 2514 return _geobody 2515 2516 @staticmethod 2517 def grow_upward(geobody, iterations, dist=1, verbose=False): 2518 from scipy.ndimage.morphology import grey_dilation 2519 2520 dist_size = 2 * dist + 1 2521 mask = np.zeros((1, 1, dist_size)) 2522 mask[0, 0, : dist + 1] = 1 2523 _geobody = geobody.copy() 2524 if verbose: 2525 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2526 for k in range(iterations): 2527 try: 2528 _geobody = grey_dilation(_geobody, footprint=mask) 2529 if verbose: 2530 print( 2531 " ...grow_upward: k, _geobody.shape = ", 2532 k, 2533 _geobody[_geobody > 0].shape, 2534 ) 2535 except: 2536 break 2537 return _geobody 2538 2539 @staticmethod 2540 def grow_downward(geobody, iterations, dist=1, verbose=False): 2541 from scipy.ndimage.morphology import grey_dilation 2542 2543 dist_size = 2 * dist + 1 2544 mask = np.zeros((1, 1, dist_size)) 2545 mask[0, 0, dist:] = 1 2546 _geobody = geobody.copy() 2547 if verbose: 2548 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2549 for k in range(iterations): 2550 try: 2551 _geobody = grey_dilation(_geobody, footprint=mask) 2552 if verbose: 2553 print( 2554 " ...grow_downward: k, _geobody.shape = ", 2555 k, 2556 _geobody[_geobody > 0].shape, 2557 ) 2558 except: 2559 break 2560 return _geobody 2561 2562 @staticmethod 2563 def _threshold_volumes(volume, threshold=0.5): 2564 volume[volume >= threshold] = 1.0 2565 volume[volume < threshold] = 0.0 2566 return volume 2567 2568 def parse_closure_codes(self, hc_closure_codes, labels, num, code=0.1): 2569 labels = labels.astype("float32") 2570 if num > 0: 2571 for x in range(1, num + 1): 2572 y = code + labels[labels == x].size 2573 labels[labels == x] = y 2574 hc_closure_codes += labels 2575 return hc_closure_codes 2576 2577 2578class Intersect3D(Closures): 2579 def __init__( 2580 self, 2581 faults, 2582 onlaps, 2583 oil_closures, 2584 gas_closures, 2585 brine_closures, 2586 closure_segment_list, 2587 closure_segments, 2588 parameters, 2589 ): 2590 self.closure_segment_list = closure_segment_list 2591 self.closure_segments = closure_segments 2592 self.cfg = parameters 2593 2594 self.fault_throw = faults.max_fault_throw 2595 self.geologic_age = faults.faulted_age_volume 2596 self.geomodel_ng = faults.faulted_net_to_gross 2597 self.faults = self._threshold_volumes(faults.fault_planes.copy()) 2598 self.onlaps = self._threshold_volumes(onlaps.copy()) 2599 self.oil_closures = self._threshold_volumes(oil_closures.copy()) 2600 self.gas_closures = self._threshold_volumes(gas_closures.copy()) 2601 self.brine_closures = self._threshold_volumes(brine_closures.copy()) 2602 2603 self.wide_faults = None 2604 self.fat_faults = None 2605 self.onlaps_upward = None 2606 self.onlaps_downward = None 2607 self._dilate_faults_and_onlaps() 2608 2609 # Outputs 2610 self.faulted_closures_oil = np.zeros_like(self.oil_closures) 2611 self.faulted_closures_gas = np.zeros_like(self.gas_closures) 2612 self.faulted_closures_brine = np.zeros_like(self.brine_closures) 2613 self.fault_closures_oil_segment_list = list() 2614 self.fault_closures_gas_segment_list = list() 2615 self.fault_closures_brine_segment_list = list() 2616 self.n_fault_closures_oil = 0 2617 self.n_fault_closures_gas = 0 2618 self.n_fault_closures_brine = 0 2619 2620 self.onlap_closures_oil = np.zeros_like(self.oil_closures) 2621 self.onlap_closures_gas = np.zeros_like(self.gas_closures) 2622 self.onlap_closures_brine = np.zeros_like(self.brine_closures) 2623 self.onlap_closures_oil_segment_list = list() 2624 self.onlap_closures_gas_segment_list = list() 2625 self.onlap_closures_brine_segment_list = list() 2626 self.n_onlap_closures_oil = 0 2627 self.n_onlap_closures_gas = 0 2628 self.n_onlap_closures_brine = 0 2629 2630 self.simple_closures_oil = np.zeros_like(self.oil_closures) 2631 self.simple_closures_gas = np.zeros_like(self.gas_closures) 2632 self.simple_closures_brine = np.zeros_like(self.brine_closures) 2633 self.n_4way_closures_oil = 0 2634 self.n_4way_closures_gas = 0 2635 self.n_4way_closures_brine = 0 2636 2637 self.false_closures_oil = np.zeros_like(self.oil_closures) 2638 self.false_closures_gas = np.zeros_like(self.gas_closures) 2639 self.false_closures_brine = np.zeros_like(self.brine_closures) 2640 self.n_false_closures_oil = 0 2641 self.n_false_closures_gas = 0 2642 self.n_false_closures_brine = 0 2643 2644 def find_faulted_closures(self): 2645 for iclosure in self.closure_segment_list: 2646 i, j, k = np.where(self.closure_segments == iclosure) 2647 faults_within_closure = self.wide_faults[i, j, k] 2648 if faults_within_closure.max() > 0: 2649 if self.oil_closures[i, j, k].max() > 0: 2650 # Faulted oil closure 2651 self.faulted_closures_oil[i, j, k] = 1.0 2652 self.n_fault_closures_oil += 1 2653 self.fault_closures_oil_segment_list.append(iclosure) 2654 elif self.gas_closures[i, j, k].max() > 0: 2655 # Faulted gas closure 2656 self.faulted_closures_gas[i, j, k] = 1.0 2657 self.n_fault_closures_gas += 1 2658 self.fault_closures_gas_segment_list.append(iclosure) 2659 elif self.brine_closures[i, j, k].max() > 0: 2660 # Faulted brine closure 2661 self.faulted_closures_brine[i, j, k] = 1.0 2662 self.n_fault_closures_brine += 1 2663 self.fault_closures_brine_segment_list.append(iclosure) 2664 else: 2665 print( 2666 "Closure is faulted but does not have oil, gas or brine assigned" 2667 ) 2668 2669 def find_onlap_closures(self): 2670 for iclosure in self.closure_segment_list: 2671 i, j, k = np.where(self.closure_segments == iclosure) 2672 onlaps_within_closure = self.onlaps_upward[i, j, k] 2673 if onlaps_within_closure.max() > 0: 2674 if self.oil_closures[i, j, k].max() > 0: 2675 self.onlap_closures_oil[i, j, k] = 1.0 2676 self.n_onlap_closures_oil += 1 2677 self.onlap_closures_oil_segment_list.append(iclosure) 2678 elif self.gas_closures[i, j, k].max() > 0: 2679 self.onlap_closures_gas[i, j, k] = 1.0 2680 self.n_onlap_closures_gas += 1 2681 self.onlap_closures_gas_segment_list.append(iclosure) 2682 elif self.brine_closures[i, j, k].max() > 0: 2683 self.onlap_closures_brine[i, j, k] = 1.0 2684 self.n_onlap_closures_brine += 1 2685 self.onlap_closures_brine_segment_list.append(iclosure) 2686 else: 2687 print( 2688 "Closure is onlap but does not have oil, gas or brine assigned" 2689 ) 2690 2691 def find_simple_closures(self): 2692 for iclosure in self.closure_segment_list: 2693 i, j, k = np.where(self.closure_segments == iclosure) 2694 faults_within_closure = self.wide_faults[i, j, k] 2695 onlaps_within_closure = self.onlaps[i, j, k] 2696 oil_within_closure = self.oil_closures[i, j, k] 2697 gas_within_closure = self.gas_closures[i, j, k] 2698 brine_within_closure = self.brine_closures[i, j, k] 2699 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2700 if oil_within_closure.max() > 0: 2701 self.simple_closures_oil[i, j, k] = 1.0 2702 self.n_4way_closures_oil += 1 2703 elif gas_within_closure.max() > 0: 2704 self.simple_closures_gas[i, j, k] = 1.0 2705 self.n_4way_closures_gas += 1 2706 elif brine_within_closure.max() > 0: 2707 self.simple_closures_brine[i, j, k] = 1.0 2708 self.n_4way_closures_brine += 1 2709 else: 2710 print( 2711 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 2712 ) 2713 2714 def find_false_closures(self): 2715 for iclosure in self.closure_segment_list: 2716 i, j, k = np.where(self.closure_segments == iclosure) 2717 faults_within_closure = self.fat_faults[i, j, k] 2718 onlaps_within_closure = self.onlaps_downward[i, j, k] 2719 for fluid, false, num in zip( 2720 [self.oil_closures, self.gas_closures, self.brine_closures], 2721 [ 2722 self.false_closures_oil, 2723 self.false_closures_gas, 2724 self.false_closures_brine, 2725 ], 2726 [ 2727 self.n_false_closures_oil, 2728 self.n_false_closures_gas, 2729 self.n_false_closures_brine, 2730 ], 2731 ): 2732 fluid_within_closure = fluid[i, j, k] 2733 if fluid_within_closure.max() > 0: 2734 if onlaps_within_closure.max() > 0: 2735 _faulted_closure_threshold = float( 2736 faults_within_closure[faults_within_closure > 0].size 2737 / fluid_within_closure[fluid_within_closure > 0].size 2738 ) 2739 _onlap_closure_threshold = float( 2740 onlaps_within_closure[onlaps_within_closure > 0].size 2741 / fluid_within_closure[fluid_within_closure > 0].size 2742 ) 2743 if ( 2744 _faulted_closure_threshold > 0.65 2745 and _onlap_closure_threshold > 0.65 2746 ): 2747 false[i, j, k] = 1 2748 num += 1 2749 2750 def grow_to_fault2(self, closures): 2751 # - grow closures laterally and up within layer and within fault block 2752 print( 2753 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2754 ) 2755 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2756 2757 dilated_fault_closures = closures.copy() 2758 n_faulted_closures = dilated_fault_closures.max() 2759 labels_clean = self.closure_segments.copy() 2760 labels_clean[closures == 0] = 0 2761 labels_clean_list = list(set(labels_clean.flatten())) 2762 labels_clean_list.remove(0) 2763 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2764 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2765 2766 # fixme remove this once small closures are found and fixed 2767 voxel_sizes = [ 2768 self.closure_segments[self.closure_segments == i].size 2769 for i in labels_clean_list 2770 ] 2771 for _v in voxel_sizes: 2772 print(f"Voxel_Sizes: {_v}") 2773 if _v < self.cfg.closure_min_voxels: 2774 print(_v) 2775 2776 depth_cube = np.zeros(self.geologic_age.shape, float) 2777 _depths = np.arange(self.geologic_age.shape[2]) 2778 depth_cube += _depths.reshape(1, 1, self.geologic_age.shape[2]) 2779 _ng = self.geomodel_ng.copy() 2780 _age = self.geologic_age.copy() 2781 2782 for il, i in enumerate(labels_clean_list): 2783 fault_blocks_list = list(set(self.fault_throw[labels_clean == i].flatten())) 2784 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2785 for jl, j in enumerate(fault_blocks_list): 2786 print( 2787 "\n\n ... label, throw = ", 2788 i, 2789 j, 2790 list(set(self.fault_throw[labels_clean == i].flatten())), 2791 labels_clean[labels_clean == i].size, 2792 self.fault_throw[self.fault_throw == j].size, 2793 self.fault_throw[ 2794 np.where((labels_clean == i) & (self.fault_throw == j)) 2795 ].size, 2796 ) 2797 single_closure = labels_clean * 0.0 2798 size = single_closure[ 2799 np.where( 2800 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2801 ) 2802 ].size 2803 if size >= self.cfg.closure_min_voxels: 2804 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2805 single_closure[ 2806 np.where( 2807 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2808 ) 2809 ] = 1 2810 if single_closure[single_closure > 0].size == 0: 2811 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2812 labels_clean[np.where(labels_clean == i)] = 0 2813 continue 2814 avg_ng = _ng[single_closure == 1].mean() 2815 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2816 _ng_voxels = _ng[single_closure == 1] 2817 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2818 min_geo_age = _geo_age_voxels.min() - 0.5 2819 avg_geo_age = int(_geo_age_voxels.mean()) 2820 max_geo_age = _geo_age_voxels.max() + 0.5 2821 _depth_geobody_voxels = depth_cube[single_closure == 1] 2822 min_depth = _depth_geobody_voxels.min() 2823 max_depth = _depth_geobody_voxels.max() 2824 avg_throw = np.median(self.fault_throw[single_closure == 1]) 2825 2826 closure_boundary_cube = closures * 0.0 2827 closure_boundary_cube[ 2828 np.where( 2829 (_ng > 0.0) 2830 & (_age > min_geo_age) 2831 & (_age < max_geo_age) 2832 & (self.fault_throw == avg_throw) 2833 & (depth_cube <= max_depth) 2834 ) 2835 ] = 1.0 2836 print( 2837 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2838 closure_boundary_cube[closure_boundary_cube == 1].size, 2839 ) 2840 2841 n_voxel = single_closure[single_closure == 1].size 2842 2843 original_voxels = n_voxel + 0 2844 print( 2845 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2846 i, 2847 j, 2848 n_voxel, 2849 (min_geo_age, avg_geo_age, max_geo_age), 2850 (min_depth, max_depth), 2851 avg_ng, 2852 il, 2853 " / ", 2854 len(labels_clean_list), 2855 ) 2856 2857 grown_closure = single_closure.copy() 2858 grown_closure[depth_cube >= max_depth] = 0 2859 delta_voxel = 0 2860 previous_delta_voxel = 1e9 2861 converged = False 2862 for ii in range(15): 2863 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2864 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2865 # stay within layer, within age, within fault block, above HCWC 2866 grown_closure[closure_boundary_cube == 0.0] = 0.0 2867 single_closure = single_closure + grown_closure 2868 single_closure[single_closure > 0] = i 2869 new_n_voxel = single_closure[single_closure > 0].size 2870 previous_delta_voxel = delta_voxel + 0 2871 delta_voxel = new_n_voxel - n_voxel 2872 print( 2873 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2874 " delta_voxel>previous_delta_voxel = ", 2875 i, 2876 ii, 2877 new_n_voxel, 2878 delta_voxel, 2879 previous_delta_voxel, 2880 delta_voxel > previous_delta_voxel, 2881 ) 2882 if n_voxel == new_n_voxel: 2883 # finish bottom voxel layer near "HCWC" 2884 grown_closure = self.grow_downward( 2885 grown_closure, 1, dist=1, verbose=False 2886 ) 2887 # stay within layer, within age, within fault block, above HCWC 2888 grown_closure[closure_boundary_cube == 0.0] = 0.0 2889 single_closure = single_closure + grown_closure 2890 single_closure[single_closure > 0] = i 2891 converged = True 2892 break 2893 else: 2894 n_voxel = new_n_voxel 2895 previous_delta_voxel = delta_voxel + 0 2896 if converged is True: 2897 labels_clean[single_closure > 0] = i 2898 msg_postscript = " converged" 2899 else: 2900 labels_clean[labels_clean == i] = -i 2901 msg_postscript = " NOT converged" 2902 msg = ( 2903 "closure_id: " 2904 + format(i, "4d") 2905 + ", fault_id: " 2906 + format(int(j + 0.5), "4d") 2907 + ", original_voxels: " 2908 + format(original_voxels, "11,.0f") 2909 + ", new_n_voxel: " 2910 + format(new_n_voxel, "11,.0f") 2911 + ", percent_growth: " 2912 + format(float(new_n_voxel) / original_voxels, "6.2f") 2913 ) 2914 print(msg + msg_postscript) 2915 self.cfg.write_to_logfile(msg + msg_postscript) 2916 2917 # Set small closures to 0 after growth 2918 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2919 for x in np.unique(_grown_labels): 2920 size = _grown_labels[_grown_labels == x].size 2921 print(f"Size before editing: {size}") 2922 if size < self.cfg.closure_min_voxels: 2923 labels_clean[_grown_labels == x] = 0.0 2924 for x in np.unique(labels_clean): 2925 size = labels_clean[labels_clean == x].size 2926 print(f"Size after editing: {size}") 2927 2928 return labels_clean 2929 2930 def _dilate_faults_and_onlaps(self): 2931 self.wide_faults = self.grow_lateral(self.faults, 9, dist=1, verbose=False) 2932 self.fat_faults = self.grow_lateral(self.faults, 21, dist=1, verbose=False) 2933 mask = np.zeros((1, 1, 3)) 2934 mask[0, 0, :2] = 1 2935 self.onlaps_upward = morphology.binary_dilation(self.onlaps, mask) 2936 mask = np.zeros((1, 1, 3)) 2937 mask[0, 0, 1:] = 1 2938 self.onlaps_downward = self.onlaps.copy() 2939 for k in range(30): 2940 try: 2941 self.onlaps_downward = morphology.binary_dilation( 2942 self.onlaps_downward, mask 2943 ) 2944 except: 2945 break 2946 2947 @staticmethod 2948 def _threshold_volumes(volume, threshold=0.5): 2949 volume[volume >= threshold] = 1.0 2950 volume[volume < threshold] = 0.0 2951 return volume 2952 2953 @staticmethod 2954 def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False): 2955 from scipy.ndimage import maximum_filter 2956 2957 hdist_size = 2 * hdist + 1 2958 vdist_size = 2 * vdist + 1 2959 mask = np.zeros((hdist_size, hdist_size, vdist_size)) 2960 mask[:, :, : vdist + 1] = 1 2961 _geobody = geobody.copy() 2962 if verbose: 2963 print( 2964 " ...grow_up_and_lateral: _geobody.shape = ", 2965 _geobody[_geobody > 0].shape, 2966 ) 2967 for k in range(iterations): 2968 try: 2969 _geobody = maximum_filter(_geobody, footprint=mask) 2970 if verbose: 2971 print( 2972 " ...grow_up_and_lateral: k, _geobody.shape = ", 2973 k, 2974 _geobody[_geobody > 0].shape, 2975 ) 2976 except: 2977 break 2978 return _geobody 2979 2980 @staticmethod 2981 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2982 from scipy.ndimage.morphology import grey_dilation 2983 2984 dist_size = 2 * dist + 1 2985 mask = np.zeros((dist_size, dist_size, 1)) 2986 mask[:, :, :] = 1 2987 _geobody = geobody.copy() 2988 if verbose: 2989 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2990 for k in range(iterations): 2991 try: 2992 _geobody = grey_dilation(_geobody, footprint=mask) 2993 if verbose: 2994 print( 2995 " ...grow_lateral: k, _geobody.shape = ", 2996 k, 2997 _geobody[_geobody > 0].shape, 2998 ) 2999 except: 3000 break 3001 return _geobody 3002 3003 @staticmethod 3004 def grow_upward(geobody, iterations, dist=1, verbose=False): 3005 from scipy.ndimage.morphology import grey_dilation 3006 3007 dist_size = 2 * dist + 1 3008 mask = np.zeros((1, 1, dist_size)) 3009 mask[0, 0, : dist + 1] = 1 3010 _geobody = geobody.copy() 3011 if verbose: 3012 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3013 for k in range(iterations): 3014 try: 3015 _geobody = grey_dilation(_geobody, footprint=mask) 3016 if verbose: 3017 print( 3018 " ...grow_upward: k, _geobody.shape = ", 3019 k, 3020 _geobody[_geobody > 0].shape, 3021 ) 3022 except: 3023 break 3024 return _geobody 3025 3026 @staticmethod 3027 def grow_downward(geobody, iterations, dist=1, verbose=False): 3028 from scipy.ndimage.morphology import grey_dilation 3029 3030 dist_size = 2 * dist + 1 3031 mask = np.zeros((1, 1, dist_size)) 3032 mask[0, 0, dist:] = 1 3033 _geobody = geobody.copy() 3034 if verbose: 3035 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3036 for k in range(iterations): 3037 try: 3038 _geobody = grey_dilation(_geobody, footprint=mask) 3039 if verbose: 3040 print( 3041 " ...grow_downward: k, _geobody.shape = ", 3042 k, 3043 _geobody[_geobody > 0].shape, 3044 ) 3045 except: 3046 break 3047 return _geobody 3048 3049 3050def variable_max_column_height(top_lith_idx, num_horizons, hmin=25, hmax=200): 3051 """ 3052 Create a 1-D array of maximum column heights using linear function in layer numbers 3053 Shallow closures will have small vertical closure heights 3054 Deep closures will have larger vertical closure heights 3055 3056 Would be better to use a pressure profile to determine maximum column heights at given depths 3057 3058 :param top_lith_idx: 1-D array of horizon numbers corresponding to top of layers where lithology changes 3059 :param num_horizons: Total number of horizons in model 3060 :param hmin: Minimum column height to use in linear function 3061 :param hmax: Maximum column height to use in linear function 3062 :return: 1-D array of column heights of closures 3063 """ 3064 # Use a linear function to determine max column height based on layer number 3065 column_heights = np.linspace(hmin, hmax, num=num_horizons) 3066 max_col_heights = column_heights[top_lith_idx] 3067 return max_col_heights 3068 3069 3070# Horizon Spill Point functions 3071def fill_to_spill(test_array, array_flags, empty_value=1.0e22, quiet=True): 3072 if not quiet: 3073 print(" ... start fillToSpill ......") 3074 temp_array = test_array.copy() 3075 flags = array_flags.copy() 3076 test_array_max = 2.0 * (temp_array[~np.isnan(temp_array)]).max() 3077 temp_array[array_flags == 255] = -empty_value 3078 flood_filled = test_array_max - flood_fill_heap( 3079 test_array_max - temp_array, empty_value=empty_value 3080 ) 3081 if not quiet: 3082 print(" ... finish fillToSpill ......") 3083 3084 flood_filled[array_flags != 1] = 0 3085 flood_filled[flood_filled == 1.0e5] = 0 3086 flags[flood_filled == empty_value] = 0 3087 3088 return flood_filled 3089 3090 3091def flood_fill_heap(test_array, empty_value=1.0e22, quiet=True): 3092 # from internet: http://arcgisandpython.blogspot.co.uk/2012/01/python-flood-fill-algorithm.html 3093 3094 import heapq 3095 from scipy import ndimage 3096 3097 input_array = np.copy(test_array) 3098 num_validPoints = ( 3099 test_array.flatten().shape[0] 3100 - input_array[np.isnan(input_array)].shape[0] 3101 - input_array[input_array > empty_value / 2].shape[0] 3102 ) 3103 if not quiet: 3104 print( 3105 " ... flood_fill_heap ... number of valid input horizon picks = ", 3106 num_validPoints, 3107 ) 3108 3109 validPoints = input_array[~np.isnan(input_array)] 3110 validPoints = validPoints[validPoints < empty_value / 2] 3111 validPoints = validPoints[validPoints < 1.0e5] 3112 validPoints = validPoints[validPoints > np.percentile(validPoints, 2)] 3113 3114 if len(validPoints) > 2: 3115 amin = validPoints.min() 3116 amax = validPoints.max() 3117 else: 3118 return test_array 3119 3120 if not quiet: 3121 print( 3122 " ... validPoints stats = ", 3123 validPoints.min(), 3124 np.median(validPoints), 3125 validPoints.mean(), 3126 validPoints.max(), 3127 ) 3128 print( 3129 " ... validPoints %tiles = ", 3130 np.percentile(validPoints, 0), 3131 np.percentile(validPoints, 1), 3132 np.percentile(validPoints, 5), 3133 np.percentile(validPoints, 10), 3134 np.percentile(validPoints, 25), 3135 np.percentile(validPoints, 50), 3136 np.percentile(validPoints, 75), 3137 np.percentile(validPoints, 90), 3138 np.percentile(validPoints, 95), 3139 np.percentile(validPoints, 99), 3140 np.percentile(validPoints, 100), 3141 ) 3142 from datagenerator.util import import_matplotlib 3143 3144 plt = import_matplotlib() 3145 plt.figure(5) 3146 plt.clf() 3147 plt.imshow(np.flipud(input_array), vmin=amin, vmax=amax, cmap="jet_r") 3148 plt.colorbar() 3149 plt.show() 3150 plt.savefig("flood_fill.png", format="png") 3151 plt.close() 3152 3153 print(" ... min & max for surface = ", amin, amax) 3154 3155 # set empty values and nan's to huge 3156 input_array[np.isnan(input_array)] = empty_value 3157 3158 # Set h_max to a value larger than the array maximum to ensure that the while loop will terminate 3159 h_max = np.max(input_array * 2.0) 3160 3161 # Build mask of cells with data not on the edge of the image 3162 # Use 3x3 square structuring element 3163 el = ndimage.generate_binary_structure(2, 2).astype(np.int) 3164 inside_mask = ndimage.binary_erosion(~np.isnan(input_array), structure=el) 3165 inside_mask[input_array == empty_value] = False 3166 edge_mask = np.invert(inside_mask) 3167 # Initialize output array as max value test_array except edges 3168 output_array = np.copy(input_array) 3169 output_array[inside_mask] = h_max 3170 3171 if not quiet: 3172 plt.figure(6) 3173 plt.clf() 3174 plt.imshow(np.flipud(input_array), cmap="jet_r") 3175 plt.colorbar() 3176 plt.show() 3177 plt.savefig("flood_fill2.png", format="png") 3178 plt.close() 3179 3180 # Build priority queue and place edge pixels into priority queue 3181 # Last value is flag to indicate if cell is an edge cell 3182 put = heapq.heappush 3183 get = heapq.heappop 3184 fill_heap = [ 3185 (output_array[t_row, t_col], int(t_row), int(t_col), 1) 3186 for t_row, t_col in np.transpose(np.where(edge_mask)) 3187 ] 3188 heapq.heapify(fill_heap) 3189 3190 # Iterate until priority queue is empty 3191 while 1: 3192 try: 3193 h_crt, t_row, t_col, edge_flag = get(fill_heap) 3194 except IndexError: 3195 break 3196 for n_row, n_col in [ 3197 ((t_row - 1), t_col), 3198 ((t_row + 1), t_col), 3199 (t_row, (t_col - 1)), 3200 (t_row, (t_col + 1)), 3201 ]: 3202 # Skip cell if outside array edges 3203 if edge_flag: 3204 try: 3205 if not inside_mask[n_row, n_col]: 3206 continue 3207 except IndexError: 3208 continue 3209 if output_array[n_row, n_col] == h_max: 3210 output_array[n_row, n_col] = max(h_crt, input_array[n_row, n_col]) 3211 put(fill_heap, (output_array[n_row, n_col], n_row, n_col, 0)) 3212 output_array[output_array == empty_value] = np.nan 3213 return output_array 3214 3215 3216def _flood_fill(horizon, max_column_height=20.0, verbose=False, debug=False): 3217 """Locate areas on horizon that are in structural closure. 3218 3219 # horizon: depth horizon as 2D numpy array. 3220 # - assume that fault intersections are inserted with value of 0. 3221 # - assume that values represent depth (i.e., bigger values are deeper)""" 3222 from scipy import ndimage 3223 3224 # copy input array 3225 temp_event = horizon.copy() 3226 3227 emptypicks = temp_event * 0.0 3228 emptypicks[temp_event < 1.0] = 1.0 3229 emptypicks_dilated = ndimage.grey_dilation( 3230 emptypicks, size=(3, 3), structure=np.ones((3, 3)) 3231 ) 3232 # dilation removes some of the event - turn this off to honour the input events exactly 3233 # Changed to avoid vertical closure-boundaries near faults 3234 # emptypicks_dilated = emptypicks 3235 if verbose: 3236 print( 3237 " emptypicks_dilated min,mean,max = ", 3238 emptypicks_dilated.min(), 3239 emptypicks_dilated.mean(), 3240 emptypicks_dilated.max(), 3241 ) 3242 3243 # create boundary around edges of 2D array 3244 temp_event[:, :3] = 0.0 3245 temp_event[:, -3:] = 0.0 3246 temp_event[:3, :] = 0.0 3247 temp_event[-3:, :] = 0.0 3248 3249 # replace pixels with value=0 with vertical 'wall' that is max_column_height deeper than nearby pixels 3250 temp_event[ 3251 np.logical_and(emptypicks_dilated == 2.0, temp_event != 0.0) 3252 ] += max_column_height 3253 temp_event[emptypicks == 1.0] += 0.0 3254 3255 # put deep point at map origin to 'collect' flood-fill run-off 3256 temp_event[0, 0] = 1.0e5 3257 3258 # create flags to indicate pick vs no-pick in 2D array 3259 flags = np.zeros((horizon.shape[0], horizon.shape[1]), "int") 3260 flags[temp_event > 0.0] = 1 3261 flags[0, 0] = 1 3262 3263 flood_filled = -fill_to_spill(-temp_event, flags) 3264 3265 # set pixels near fault gaps to empty 3266 flood_filled[np.logical_and(emptypicks_dilated == 2.0, temp_event != 0.0)] = 0.0 3267 3268 # limit closure heights to max_column_height 3269 # - Note that this typically causes under-filling of shallow 4-way closures 3270 if debug: 3271 import pdb 3272 3273 pdb.set_trace() 3274 ff = flood_filled.copy() 3275 diff = horizon - ff 3276 diff[flood_filled == 0.0] = 0.0 3277 diff[diff != 0.0] = 1.0 3278 3279 from skimage import morphology 3280 from skimage import measure 3281 3282 labels = measure.label(diff, connectivity=2, background=0) 3283 labels_clean = morphology.remove_small_objects(labels, 50) 3284 labels_clean_list = list(set(labels_clean.flatten())) 3285 labels_clean_list.sort() 3286 for i in labels_clean_list: 3287 if i == 0: 3288 continue 3289 trap_crest = -horizon[labels_clean == i].min() 3290 initial_size = horizon[labels_clean == i].size 3291 spill_depth_map = np.ones_like(horizon) * (trap_crest - max_column_height) 3292 spill_points = np.dstack((-flood_filled, spill_depth_map)) 3293 spill_point_map = spill_points.max(axis=-1) 3294 spill_point_map[spill_point_map == -100000.0] = 0.0 3295 spill_point_map[spill_point_map > 0.0] = 0.0 3296 flood_filled[labels_clean == i] = -spill_point_map[labels_clean == i] 3297 flood_filled[flood_filled < horizon] = horizon[flood_filled < horizon] 3298 final_size = horizon[ 3299 np.where((horizon - flood_filled != 0.0) & (labels_clean == i)) 3300 ].size 3301 print( 3302 " ...inside _flood_fill: i, initial_size, final_size = ", 3303 i, 3304 initial_size, 3305 final_size, 3306 ) 3307 del spill_depth_map 3308 del spill_points 3309 del spill_point_map 3310 del ff 3311 del diff 3312 del labels 3313 del labels_clean 3314 3315 return flood_filled 3316 3317 3318def get_top_of_closure(inarray, pad_up=0, pad_down=0): 3319 """Create a mask leaving only the top of a closure.""" 3320 mask = inarray != 0 3321 t = np.where(mask.any(axis=-1), mask.argmax(axis=-1), -1) 3322 xy = np.argwhere(t > 0) 3323 z = t[t > 0] 3324 outarray = np.zeros_like(inarray) 3325 for (x, y), z in zip(xy, z): 3326 zmin = z - pad_up 3327 zmax = z + pad_down + 1 3328 outarray[x, y, zmin:zmax] = 1 3329 return outarray 3330 3331 3332def lsq(x, y, axis=-1): 3333 ### 3334 ### compute the slope and intercept for an array with points to be fit 3335 ### in the last dimension. can be in other axis using the 'axis' parmameter. 3336 ### 3337 ### returns: 3338 ### - intercept 3339 ### - slope 3340 ### - pearson r (normalized cross-correlation coefficient) 3341 ### 3342 ### output will have dimensions of input with one less axis 3343 ### - (specified by axis parameter) 3344 ### 3345 3346 """ 3347 # compute x and y with mean removed 3348 x_zeromean = x * 1. 3349 x_zeromean -= x.mean(axis=axis).reshape(x.shape[0],x.shape[1],1) 3350 y_zeromean = y * 1. 3351 y_zeromean -= y.mean(axis=axis).reshape(y.shape[0],y.shape[1],1) 3352 """ 3353 3354 # compute pearsonr 3355 r = np.sum(x * y, axis=axis) - np.sum(x) * np.sum(y, axis=axis) / y.shape[axis] 3356 r /= np.sqrt( 3357 (np.sum(x ** 2, axis=axis) - np.sum(x, axis=axis) ** 2 / y.shape[axis]) 3358 * (np.sum(y ** 2, axis=axis) - np.sum(y, axis=axis) ** 2 / y.shape[axis]) 3359 ) 3360 3361 # compute slope 3362 slope = r * y.std(axis=axis) / x.std(axis=axis) 3363 3364 # compute intercept 3365 intercept = y.mean(axis=axis) - slope * x.mean(axis=axis) 3366 3367 return intercept, slope, r 3368 3369 3370def compute_ai_gi(parameters, seismic_data): 3371 """[summary] 3372 3373 Args: 3374 cfg (Parameter class object): Model Parameters 3375 seismic_data (np.array): Seismic data with shape n * x * y * z, 3376 where n is number of angle stacks 3377 """ 3378 inc_angles = np.array(parameters.incident_angles) 3379 inc_angles = np.sin(inc_angles * np.pi / 180.0) ** 2 3380 inc_angles = inc_angles.reshape(len(inc_angles), 1, 1, 1) 3381 3382 intercept, slope, _ = lsq(inc_angles, seismic_data, axis=0) 3383 3384 intercept[np.isnan(intercept)] = 0.0 3385 slope[np.isnan(slope)] = 0.0 3386 intercept[np.isinf(intercept)] = 0.0 3387 slope[np.isinf(slope)] = 0.0 3388 return intercept, slope
12class Closures(Horizons, Geomodel, Parameters): 13 def __init__(self, parameters, faults, facies, onlap_horizon_list): 14 self.closure_dict = dict() 15 self.cfg = parameters 16 self.faults = faults 17 self.facies = facies 18 self.onlap_list = onlap_horizon_list 19 self.top_lith_facies = None 20 self.closure_vol_shape = self.faults.faulted_age_volume.shape 21 self.closure_segments = self.cfg.hdf_init( 22 "closure_segments", shape=self.closure_vol_shape 23 ) 24 self.oil_closures = self.cfg.hdf_init( 25 "oil_closures", shape=self.closure_vol_shape, dtype="uint8" 26 ) 27 self.gas_closures = self.cfg.hdf_init( 28 "gas_closures", shape=self.closure_vol_shape, dtype="uint8" 29 ) 30 self.brine_closures = self.cfg.hdf_init( 31 "brine_closures", shape=self.closure_vol_shape, dtype="uint8" 32 ) 33 self.simple_closures = self.cfg.hdf_init( 34 "simple_closures", shape=self.closure_vol_shape, dtype="uint8" 35 ) 36 self.strat_closures = self.cfg.hdf_init( 37 "strat_closures", shape=self.closure_vol_shape, dtype="uint8" 38 ) 39 self.fault_closures = self.cfg.hdf_init( 40 "fault_closures", shape=self.closure_vol_shape, dtype="uint8" 41 ) 42 self.hc_labels = self.cfg.hdf_init( 43 "hc_labels", shape=self.closure_vol_shape, dtype="uint8" 44 ) 45 46 self.all_closure_segments = self.cfg.hdf_init( 47 "all_closure_segments", shape=self.closure_vol_shape 48 ) 49 50 # Class attributes added from Intersect3D 51 self.wide_faults = self.cfg.hdf_init( 52 "wide_faults", shape=self.closure_vol_shape 53 ) 54 self.fat_faults = self.cfg.hdf_init("fat_faults", shape=self.closure_vol_shape) 55 self.onlaps_upward = self.cfg.hdf_init( 56 "onlaps_upward", shape=self.closure_vol_shape 57 ) 58 self.onlaps_downward = self.cfg.hdf_init( 59 "onlaps_downward", shape=self.closure_vol_shape 60 ) 61 62 # Faulted closures 63 self.faulted_closures_oil = self.cfg.hdf_init( 64 "faulted_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 65 ) 66 self.faulted_closures_gas = self.cfg.hdf_init( 67 "faulted_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 68 ) 69 self.faulted_closures_brine = self.cfg.hdf_init( 70 "faulted_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 71 ) 72 self.fault_closures_oil_segment_list = list() 73 self.fault_closures_gas_segment_list = list() 74 self.fault_closures_brine_segment_list = list() 75 self.n_fault_closures_oil = 0 76 self.n_fault_closures_gas = 0 77 self.n_fault_closures_brine = 0 78 79 self.faulted_all_closures = self.cfg.hdf_init( 80 "faulted_all_closures", shape=self.closure_vol_shape, dtype="uint8" 81 ) 82 self.fault_all_closures_segment_list = list() 83 self.n_fault_all_closures = 0 84 85 # Onlap closures 86 self.onlap_closures_oil = self.cfg.hdf_init( 87 "onlap_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 88 ) 89 self.onlap_closures_gas = self.cfg.hdf_init( 90 "onlap_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 91 ) 92 self.onlap_closures_brine = self.cfg.hdf_init( 93 "onlap_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 94 ) 95 self.onlap_closures_oil_segment_list = list() 96 self.onlap_closures_gas_segment_list = list() 97 self.onlap_closures_brine_segment_list = list() 98 self.n_onlap_closures_oil = 0 99 self.n_onlap_closures_gas = 0 100 self.n_onlap_closures_brine = 0 101 102 self.onlap_all_closures = self.cfg.hdf_init( 103 "onlap_all_closures", shape=self.closure_vol_shape, dtype="uint8" 104 ) 105 self.onlap_all_closures_segment_list = list() 106 self.n_onlap_all_closures_oil = 0 107 108 # Simple closures 109 self.simple_closures_oil = self.cfg.hdf_init( 110 "simple_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 111 ) 112 self.simple_closures_gas = self.cfg.hdf_init( 113 "simple_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 114 ) 115 self.simple_closures_brine = self.cfg.hdf_init( 116 "simple_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 117 ) 118 self.simple_closures_oil_segment_list = list() 119 self.simple_closures_gas_segment_list = list() 120 self.simple_closures_brine_segment_list = list() 121 self.n_4way_closures_oil = 0 122 self.n_4way_closures_gas = 0 123 self.n_4way_closures_brine = 0 124 125 self.simple_all_closures = self.cfg.hdf_init( 126 "simple_all_closures", shape=self.closure_vol_shape, dtype="uint8" 127 ) 128 self.simple_all_closures_segment_list = list() 129 self.n_4way_all_closures = 0 130 131 # False closures 132 self.false_closures_oil = self.cfg.hdf_init( 133 "false_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 134 ) 135 self.false_closures_gas = self.cfg.hdf_init( 136 "false_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 137 ) 138 self.false_closures_brine = self.cfg.hdf_init( 139 "false_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 140 ) 141 self.n_false_closures_oil = 0 142 self.n_false_closures_gas = 0 143 self.n_false_closures_brine = 0 144 145 self.false_all_closures = self.cfg.hdf_init( 146 "false_all_closures", shape=self.closure_vol_shape, dtype="uint8" 147 ) 148 self.n_false_all_closures = 0 149 150 if self.cfg.include_salt: 151 self.salt_closures = self.cfg.hdf_init( 152 "salt_closures", shape=self.closure_vol_shape, dtype="uint8" 153 ) 154 self.wide_salt = self.cfg.hdf_init( 155 "wide_salt", shape=self.closure_vol_shape 156 ) 157 self.salt_closures_oil = self.cfg.hdf_init( 158 "salt_bounded_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 159 ) 160 self.salt_closures_gas = self.cfg.hdf_init( 161 "salt_bounded_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 162 ) 163 self.salt_closures_brine = self.cfg.hdf_init( 164 "salt_bounded_closures_brine", 165 shape=self.closure_vol_shape, 166 dtype="uint8", 167 ) 168 self.salt_closures_oil_segment_list = list() 169 self.salt_closures_gas_segment_list = list() 170 self.salt_closures_brine_segment_list = list() 171 self.n_salt_closures_oil = 0 172 self.n_salt_closures_gas = 0 173 self.n_salt_closures_brine = 0 174 175 self.salt_all_closures = self.cfg.hdf_init( 176 "salt_bounded_all_closures", shape=self.closure_vol_shape, dtype="uint8" 177 ) 178 self.salt_all_closures_segment_list = list() 179 self.n_salt_all_closures = 0 180 181 def create_closure_labels_from_depth_maps( 182 self, depth_maps, depth_maps_infilled, max_col_height 183 ): 184 if self.cfg.verbose: 185 print("\n\t... inside insertClosureLabels3D ") 186 print( 187 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 188 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 189 ) 190 191 # create 3D cube to hold segmentation results 192 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 193 194 # create grids with grid indices 195 ii, jj = self.build_meshgrid() 196 197 # loop through horizons in 'depth_maps' 198 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 199 layers_with_closure = 0 200 201 avg_sand_thickness = list() 202 avg_shale_thickness = list() 203 avg_unit_thickness = list() 204 for ihorizon in range(depth_maps.shape[2] - 1): 205 avg_unit_thickness.append( 206 np.mean( 207 depth_maps_infilled[..., ihorizon + 1] 208 - depth_maps_infilled[..., ihorizon] 209 ) 210 ) 211 212 if self.top_lith_facies[ihorizon] > 0: 213 # If facies is not shale, calculate a closure map for the layer 214 if self.cfg.verbose: 215 print( 216 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 217 ) 218 avg_sand_thickness.append( 219 np.mean( 220 depth_maps_infilled[..., ihorizon + 1] 221 - depth_maps_infilled[..., ihorizon] 222 ) 223 ) 224 # compute a closure map 225 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 226 # - use thicknesses converted to samples instead of ft or ms 227 # - assumes that fault intersections are inserted in input map with value of 0. 228 # - assumes that input map values represent depth (i.e., bigger values are deeper) 229 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 230 top_structure_depth_map[ 231 np.isnan(top_structure_depth_map) 232 ] = 0.0 # replace nans with 0. 233 top_structure_depth_map /= float(self.cfg.digi) 234 if self.cfg.partial_voxels: 235 top_structure_depth_map -= ( 236 1.0 # account for voxels partially in layer 237 ) 238 base_structure_depth_map = depth_maps_infilled[ 239 :, :, ihorizon + 1 240 ].copy() 241 base_structure_depth_map[ 242 np.isnan(top_structure_depth_map) 243 ] = 0.0 # replace nans with 0. 244 base_structure_depth_map /= float(self.cfg.digi) 245 print( 246 " ...inside create_closure_labels_from_depth_maps... ihorizon, self.top_lith_facies[ihorizon] = ", 247 ihorizon, 248 self.top_lith_facies[ihorizon], 249 ) 250 # if there is non-zero thickness between top/base closure 251 if top_structure_depth_map.min() != top_structure_depth_map.max(): 252 max_column = max_col_height[ihorizon] / self.cfg.digi 253 if self.cfg.verbose: 254 print( 255 f" ...avg depth for layer {ihorizon}.", 256 top_structure_depth_map.mean(), 257 ) 258 if self.cfg.verbose: 259 print( 260 f" ...maximum column height for layer {ihorizon}.", 261 max_column, 262 ) 263 264 if ihorizon == 27000 or ihorizon == 1000: 265 closure_depth_map = _flood_fill( 266 top_structure_depth_map, 267 max_column_height=max_column, 268 verbose=True, 269 debug=True, 270 ) 271 else: 272 closure_depth_map = _flood_fill( 273 top_structure_depth_map, max_column_height=max_column 274 ) 275 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 276 closure_depth_map == 0 277 ] 278 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 279 closure_depth_map == 1 280 ] 281 closure_depth_map[ 282 closure_depth_map == 1e5 283 ] = top_structure_depth_map[closure_depth_map == 1e5] 284 # Select the maximum value between the top sand map and the flood-filled closure map 285 closure_depth_map = np.max( 286 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 287 ) 288 closure_depth_map = np.min( 289 np.dstack((closure_depth_map, base_structure_depth_map)), 290 axis=-1, 291 ) 292 if self.cfg.verbose: 293 print( 294 f"\n ... layer {ihorizon}," 295 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 296 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 297 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 298 ) 299 closure_thickness = closure_depth_map - top_structure_depth_map 300 closure_thickness_no_nan = closure_thickness[ 301 ~np.isnan(closure_thickness) 302 ] 303 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 304 if self.cfg.verbose: 305 print(f" ... layer {ihorizon}, max_closure {max_closure}") 306 307 # locate 3D zone in closure after checking that closures exist for this horizon 308 # if False in (top_structure_depth_map == closure_depth_map): 309 if max_closure > 0: 310 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 311 # put label in cube between top_structure_depth_map and closure_depth_map 312 top_structure_depth_map_integer = top_structure_depth_map 313 closure_depth_map_integer = closure_depth_map 314 315 if self.cfg.verbose: 316 closure_map_min = closure_depth_map_integer[ 317 closure_depth_map_integer > 0.1 318 ].min() 319 closure_map_max = closure_depth_map_integer[ 320 closure_depth_map_integer > 0.1 321 ].max() 322 print( 323 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 324 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 325 f" closure map min, max: {closure_map_min}, {closure_map_max}" 326 ) 327 328 slices_with_substitution = 0 329 print(" ... max_closure: {}".format(max_closure)) 330 for k in range( 331 max_closure + 1 332 ): # add one more sample than seemingly needed for round-off 333 # Subtract 2 from the closure cube shape since adding one later 334 horizon_slice = (k + top_structure_depth_map).clip( 335 0, closure_segments.shape[2] - 2 336 ) 337 sublayer_kk = horizon_slice[ 338 horizon_slice < closure_depth_map.astype("int") 339 ] 340 sublayer_ii = ii[ 341 horizon_slice < closure_depth_map.astype("int") 342 ] 343 sublayer_jj = jj[ 344 horizon_slice < closure_depth_map.astype("int") 345 ] 346 347 if sublayer_ii.size > 0: 348 slices_with_substitution += 1 349 350 i_indices = sublayer_ii 351 j_indices = sublayer_jj 352 k_indices = sublayer_kk + 1 353 354 try: 355 closure_segments[ 356 i_indices, j_indices, k_indices.astype("int") 357 ] += 1.0 358 voxel_change_count[ 359 i_indices, j_indices, k_indices.astype("int") 360 ] += 1 361 except IndexError: 362 print("\nIndex is out of bounds.") 363 print(f"\tclosure_segments: {closure_segments}") 364 print(f"\tvoxel_change_count: {voxel_change_count}") 365 print(f"\ti_indices: {i_indices}") 366 print(f"\tj_indices: {j_indices}") 367 print(f"\tk_indices: {k_indices.astype('int')}") 368 pass 369 370 if slices_with_substitution > 0: 371 layers_with_closure += 1 372 373 if self.cfg.verbose: 374 print( 375 " ... finished putting closures in closures_segments for layer ...", 376 ihorizon, 377 ) 378 379 else: 380 continue 381 else: 382 # Calculate shale unit thicknesses 383 avg_shale_thickness.append( 384 np.mean( 385 depth_maps_infilled[..., ihorizon + 1] 386 - depth_maps_infilled[..., ihorizon] 387 ) 388 ) 389 390 if len(avg_sand_thickness) == 0: 391 avg_sand_thickness = 0 392 self.cfg.write_to_logfile( 393 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 394 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 395 f"max: {np.max(avg_sand_thickness):.2f}" 396 ) 397 self.cfg.write_to_logfile( 398 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 399 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 400 f"max: {np.max(avg_shale_thickness):.2f}" 401 ) 402 self.cfg.write_to_logfile( 403 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 404 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 405 f"max: {np.max(avg_unit_thickness):.2f}" 406 ) 407 self.cfg.write_to_logfile( 408 msg=None, 409 mainkey="model_parameters", 410 subkey="sand_unit_thickness_combined_mean", 411 val=np.mean(avg_sand_thickness), 412 ) 413 self.cfg.write_to_logfile( 414 msg=None, 415 mainkey="model_parameters", 416 subkey="sand_unit_thickness_combined_std", 417 val=np.std(avg_sand_thickness), 418 ) 419 self.cfg.write_to_logfile( 420 msg=None, 421 mainkey="model_parameters", 422 subkey="sand_unit_thickness_combined_min", 423 val=np.min(avg_sand_thickness), 424 ) 425 self.cfg.write_to_logfile( 426 msg=None, 427 mainkey="model_parameters", 428 subkey="sand_unit_thickness_combined_max", 429 val=np.max(avg_sand_thickness), 430 ) 431 # 432 self.cfg.write_to_logfile( 433 msg=None, 434 mainkey="model_parameters", 435 subkey="shale_unit_thickness_combined_mean", 436 val=np.mean(avg_shale_thickness), 437 ) 438 self.cfg.write_to_logfile( 439 msg=None, 440 mainkey="model_parameters", 441 subkey="shale_unit_thickness_combined_std", 442 val=np.std(avg_shale_thickness), 443 ) 444 self.cfg.write_to_logfile( 445 msg=None, 446 mainkey="model_parameters", 447 subkey="shale_unit_thickness_combined_min", 448 val=np.min(avg_shale_thickness), 449 ) 450 self.cfg.write_to_logfile( 451 msg=None, 452 mainkey="model_parameters", 453 subkey="shale_unit_thickness_combined_max", 454 val=np.max(avg_shale_thickness), 455 ) 456 457 self.cfg.write_to_logfile( 458 msg=None, 459 mainkey="model_parameters", 460 subkey="overall_unit_thickness_combined_mean", 461 val=np.mean(avg_unit_thickness), 462 ) 463 self.cfg.write_to_logfile( 464 msg=None, 465 mainkey="model_parameters", 466 subkey="overall_unit_thickness_combined_std", 467 val=np.std(avg_unit_thickness), 468 ) 469 self.cfg.write_to_logfile( 470 msg=None, 471 mainkey="model_parameters", 472 subkey="overall_unit_thickness_combined_min", 473 val=np.min(avg_unit_thickness), 474 ) 475 self.cfg.write_to_logfile( 476 msg=None, 477 mainkey="model_parameters", 478 subkey="overall_unit_thickness_combined_max", 479 val=np.max(avg_unit_thickness), 480 ) 481 482 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 483 pct_non_zero = float(non_zero_pixels) / ( 484 closure_segments.shape[0] 485 * closure_segments.shape[1] 486 * closure_segments.shape[2] 487 ) 488 if self.cfg.verbose: 489 print( 490 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 491 closure_segments.min(), 492 closure_segments.mean(), 493 closure_segments.max(), 494 pct_non_zero, 495 ) 496 ) 497 498 print(f"\t... layers_with_closure {layers_with_closure}") 499 print("\t... finished putting closures in closure_segments ...\n") 500 501 if self.cfg.verbose: 502 print( 503 f"\n ...closure segments created. min: {closure_segments.min()}, " 504 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 505 f" voxel count: {closure_segments[closure_segments != 0].shape}" 506 ) 507 508 return closure_segments 509 510 def create_closure_labels_from_all_depth_maps( 511 self, depth_maps, depth_maps_infilled, max_col_height 512 ): 513 if self.cfg.verbose: 514 print("\n\t... inside insertClosureLabels3D ") 515 print( 516 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 517 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 518 ) 519 520 # create 3D cube to hold segmentation results 521 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 522 523 # create grids with grid indices 524 ii, jj = self.build_meshgrid() 525 526 # loop through horizons in 'depth_maps' 527 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 528 layers_with_closure = 0 529 530 avg_sand_thickness = list() 531 avg_shale_thickness = list() 532 avg_unit_thickness = list() 533 for ihorizon in range(depth_maps.shape[2] - 1): 534 avg_unit_thickness.append( 535 np.mean( 536 depth_maps_infilled[..., ihorizon + 1] 537 - depth_maps_infilled[..., ihorizon] 538 ) 539 ) 540 # calculate a closure map for the layer 541 if self.cfg.verbose: 542 print( 543 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 544 ) 545 546 # compute a closure map 547 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 548 # - use thicknesses converted to samples instead of ft or ms 549 # - assumes that fault intersections are inserted in input map with value of 0. 550 # - assumes that input map values represent depth (i.e., bigger values are deeper) 551 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 552 top_structure_depth_map[ 553 np.isnan(top_structure_depth_map) 554 ] = 0.0 # replace nans with 0. 555 top_structure_depth_map /= float(self.cfg.digi) 556 if self.cfg.partial_voxels: 557 top_structure_depth_map -= 1.0 # account for voxels partially in layer 558 base_structure_depth_map = depth_maps_infilled[:, :, ihorizon + 1].copy() 559 base_structure_depth_map[ 560 np.isnan(top_structure_depth_map) 561 ] = 0.0 # replace nans with 0. 562 base_structure_depth_map /= float(self.cfg.digi) 563 print( 564 " ...inside create_closure_labels_from_depth_maps... ihorizon = ", 565 ihorizon, 566 ) 567 # if there is non-zero thickness between top/base closure 568 if top_structure_depth_map.min() != top_structure_depth_map.max(): 569 max_column = max_col_height[ihorizon] / self.cfg.digi 570 if self.cfg.verbose: 571 print( 572 f" ...avg depth for layer {ihorizon}.", 573 top_structure_depth_map.mean(), 574 ) 575 if self.cfg.verbose: 576 print( 577 f" ...maximum column height for layer {ihorizon}.", max_column 578 ) 579 580 if ihorizon == 27000 or ihorizon == 1000: 581 closure_depth_map = _flood_fill( 582 top_structure_depth_map, 583 max_column_height=max_column, 584 verbose=True, 585 debug=True, 586 ) 587 else: 588 closure_depth_map = _flood_fill( 589 top_structure_depth_map, max_column_height=max_column 590 ) 591 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 592 closure_depth_map == 0 593 ] 594 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 595 closure_depth_map == 1 596 ] 597 closure_depth_map[closure_depth_map == 1e5] = top_structure_depth_map[ 598 closure_depth_map == 1e5 599 ] 600 # Select the maximum value between the top sand map and the flood-filled closure map 601 closure_depth_map = np.max( 602 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 603 ) 604 closure_depth_map = np.min( 605 np.dstack((closure_depth_map, base_structure_depth_map)), axis=-1 606 ) 607 if self.cfg.verbose: 608 print( 609 f"\n ... layer {ihorizon}," 610 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 611 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 612 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 613 ) 614 closure_thickness = closure_depth_map - top_structure_depth_map 615 closure_thickness_no_nan = closure_thickness[ 616 ~np.isnan(closure_thickness) 617 ] 618 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 619 if self.cfg.verbose: 620 print(f" ... layer {ihorizon}, max_closure {max_closure}") 621 622 # locate 3D zone in closure after checking that closures exist for this horizon 623 # if False in (top_structure_depth_map == closure_depth_map): 624 if max_closure > 0: 625 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 626 # put label in cube between top_structure_depth_map and closure_depth_map 627 top_structure_depth_map_integer = top_structure_depth_map 628 closure_depth_map_integer = closure_depth_map 629 630 if self.cfg.verbose: 631 closure_map_min = closure_depth_map_integer[ 632 closure_depth_map_integer > 0.1 633 ].min() 634 closure_map_max = closure_depth_map_integer[ 635 closure_depth_map_integer > 0.1 636 ].max() 637 print( 638 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 639 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 640 f" closure map min, max: {closure_map_min}, {closure_map_max}" 641 ) 642 643 slices_with_substitution = 0 644 print(" ... max_closure: {}".format(max_closure)) 645 for k in range( 646 max_closure + 1 647 ): # add one more sample than seemingly needed for round-off 648 # Subtract 2 from the closure cube shape since adding one later 649 horizon_slice = (k + top_structure_depth_map).clip( 650 0, closure_segments.shape[2] - 2 651 ) 652 sublayer_kk = horizon_slice[ 653 horizon_slice < closure_depth_map.astype("int") 654 ] 655 sublayer_ii = ii[ 656 horizon_slice < closure_depth_map.astype("int") 657 ] 658 sublayer_jj = jj[ 659 horizon_slice < closure_depth_map.astype("int") 660 ] 661 662 if sublayer_ii.size > 0: 663 slices_with_substitution += 1 664 665 i_indices = sublayer_ii 666 j_indices = sublayer_jj 667 k_indices = sublayer_kk + 1 668 669 try: 670 closure_segments[ 671 i_indices, j_indices, k_indices.astype("int") 672 ] += 1.0 673 voxel_change_count[ 674 i_indices, j_indices, k_indices.astype("int") 675 ] += 1 676 except IndexError: 677 print("\nIndex is out of bounds.") 678 print(f"\tclosure_segments: {closure_segments}") 679 print(f"\tvoxel_change_count: {voxel_change_count}") 680 print(f"\ti_indices: {i_indices}") 681 print(f"\tj_indices: {j_indices}") 682 print(f"\tk_indices: {k_indices.astype('int')}") 683 pass 684 685 if slices_with_substitution > 0: 686 layers_with_closure += 1 687 688 if self.cfg.verbose: 689 print( 690 " ... finished putting closures in closures_segments for layer ...", 691 ihorizon, 692 ) 693 694 else: 695 continue 696 697 if self.facies[ihorizon] == 1: 698 avg_sand_thickness.append( 699 np.mean( 700 depth_maps_infilled[..., ihorizon + 1] 701 - depth_maps_infilled[..., ihorizon] 702 ) 703 ) 704 elif self.facies[ihorizon] == 0: 705 # Calculate shale unit thicknesses 706 avg_shale_thickness.append( 707 np.mean( 708 depth_maps_infilled[..., ihorizon + 1] 709 - depth_maps_infilled[..., ihorizon] 710 ) 711 ) 712 713 # TODO handle case where avg_sand_thickness is zero-size array 714 try: 715 self.cfg.write_to_logfile( 716 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 717 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 718 f"max: {np.max(avg_sand_thickness):.2f}" 719 ) 720 except: 721 print("No sands in model") 722 self.cfg.write_to_logfile( 723 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 724 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 725 f"max: {np.max(avg_shale_thickness):.2f}" 726 ) 727 self.cfg.write_to_logfile( 728 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 729 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 730 f"max: {np.max(avg_unit_thickness):.2f}" 731 ) 732 733 self.cfg.write_to_logfile( 734 msg=None, 735 mainkey="model_parameters", 736 subkey="sand_unit_thickness_mean", 737 val=np.mean(avg_sand_thickness), 738 ) 739 self.cfg.write_to_logfile( 740 msg=None, 741 mainkey="model_parameters", 742 subkey="sand_unit_thickness_std", 743 val=np.std(avg_sand_thickness), 744 ) 745 self.cfg.write_to_logfile( 746 msg=None, 747 mainkey="model_parameters", 748 subkey="sand_unit_thickness_min", 749 val=np.min(avg_sand_thickness), 750 ) 751 self.cfg.write_to_logfile( 752 msg=None, 753 mainkey="model_parameters", 754 subkey="sand_unit_thickness_max", 755 val=np.max(avg_sand_thickness), 756 ) 757 # 758 self.cfg.write_to_logfile( 759 msg=None, 760 mainkey="model_parameters", 761 subkey="shale_unit_thickness_mean", 762 val=np.mean(avg_shale_thickness), 763 ) 764 self.cfg.write_to_logfile( 765 msg=None, 766 mainkey="model_parameters", 767 subkey="shale_unit_thickness_std", 768 val=np.std(avg_shale_thickness), 769 ) 770 self.cfg.write_to_logfile( 771 msg=None, 772 mainkey="model_parameters", 773 subkey="shale_unit_thickness_min", 774 val=np.min(avg_shale_thickness), 775 ) 776 self.cfg.write_to_logfile( 777 msg=None, 778 mainkey="model_parameters", 779 subkey="shale_unit_thickness_max", 780 val=np.max(avg_shale_thickness), 781 ) 782 783 self.cfg.write_to_logfile( 784 msg=None, 785 mainkey="model_parameters", 786 subkey="overall_unit_thickness_mean", 787 val=np.mean(avg_unit_thickness), 788 ) 789 self.cfg.write_to_logfile( 790 msg=None, 791 mainkey="model_parameters", 792 subkey="overall_unit_thickness_std", 793 val=np.std(avg_unit_thickness), 794 ) 795 self.cfg.write_to_logfile( 796 msg=None, 797 mainkey="model_parameters", 798 subkey="overall_unit_thickness_min", 799 val=np.min(avg_unit_thickness), 800 ) 801 self.cfg.write_to_logfile( 802 msg=None, 803 mainkey="model_parameters", 804 subkey="overall_unit_thickness_max", 805 val=np.max(avg_unit_thickness), 806 ) 807 808 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 809 pct_non_zero = float(non_zero_pixels) / ( 810 closure_segments.shape[0] 811 * closure_segments.shape[1] 812 * closure_segments.shape[2] 813 ) 814 if self.cfg.verbose: 815 print( 816 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 817 closure_segments.min(), 818 closure_segments.mean(), 819 closure_segments.max(), 820 pct_non_zero, 821 ) 822 ) 823 824 print(f"\t... layers_with_closure {layers_with_closure}") 825 print("\t... finished putting closures in closure_segments ...\n") 826 827 if self.cfg.verbose: 828 print( 829 f"\n ...closure segments created. min: {closure_segments.min()}, " 830 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 831 f" voxel count: {closure_segments[closure_segments != 0].shape}" 832 ) 833 834 return closure_segments 835 836 def find_top_lith_horizons(self): 837 """ 838 Find horizons which are the top of layers where the lithology changes 839 840 Combine layers of the same lithology and retain the top of these new layers for closure calculations. 841 """ 842 top_lith_indices = list(np.array(self.onlap_list) - 1) 843 for i, _ in enumerate(self.facies[:-1]): 844 if i == 0: 845 continue 846 print( 847 f"i: {i}, sand_layer_label[i-1]: {self.facies[i - 1]}," 848 f" sand_layer_label[i]: {self.facies[i]}" 849 ) 850 if self.facies[i] != self.facies[i - 1]: 851 top_lith_indices.append(i) 852 if self.cfg.verbose: 853 print( 854 " ... layer lith different than layer above it. i = {}".format( 855 i 856 ) 857 ) 858 top_lith_indices.sort() 859 if self.cfg.verbose: 860 print( 861 "\n ...layers selected for closure computations...\n", 862 top_lith_indices, 863 ) 864 self.top_lith_indices = np.array(top_lith_indices) 865 self.top_lith_facies = self.facies[top_lith_indices] 866 867 # return top_lith_indices 868 869 def create_closures(self): 870 if self.cfg.verbose: 871 print("\n\n ... create 3D labels for closure") 872 873 # Convert nan to 0's 874 old_depth_maps = np.nan_to_num(self.faults.faulted_depth_maps[:], copy=True) 875 old_depth_maps_gaps = np.nan_to_num( 876 self.faults.faulted_depth_maps_gaps[:], copy=True 877 ) 878 879 # Convert from samples to units 880 old_depth_maps_gaps = self.convert_map_from_samples_to_units( 881 old_depth_maps_gaps 882 ) 883 old_depth_maps = self.convert_map_from_samples_to_units(old_depth_maps) 884 885 # keep only horizons corresponding to top of layers where lithology changes 886 self.find_top_lith_horizons() 887 all_lith_indices = np.arange(old_depth_maps.shape[-1]) 888 import sys 889 890 print("All lith indices (last, then all):", self.facies[-1], all_lith_indices) 891 sys.stdout.flush() 892 893 depth_maps_gaps_top_lith = old_depth_maps_gaps[ 894 :, :, self.top_lith_indices 895 ].copy() 896 depth_maps_gaps_all_lith = old_depth_maps_gaps[:, :, all_lith_indices].copy() 897 depth_maps_top_lith = old_depth_maps[:, :, self.top_lith_indices].copy() 898 depth_maps_all_lith = old_depth_maps[:, :, all_lith_indices].copy() 899 max_column_heights = variable_max_column_height( 900 self.top_lith_indices, 901 self.faults.faulted_depth_maps_gaps.shape[-1], 902 self.cfg.max_column_height[0], 903 self.cfg.max_column_height[1], 904 ) 905 all_max_column_heights = variable_max_column_height( 906 all_lith_indices, 907 self.faults.faulted_depth_maps_gaps.shape[-1], 908 self.cfg.max_column_height[0], 909 self.cfg.max_column_height[1], 910 ) 911 912 if self.cfg.verbose: 913 print("\n ...facies for closure computations...\n", self.top_lith_facies) 914 print( 915 "\n ...max column heights for closure computations...\n", 916 max_column_heights, 917 ) 918 919 self.closure_segments[:] = self.create_closure_labels_from_depth_maps( 920 depth_maps_gaps_top_lith, depth_maps_top_lith, max_column_heights 921 ) 922 923 self.all_closure_segments[:] = self.create_closure_labels_from_all_depth_maps( 924 depth_maps_gaps_all_lith, depth_maps_all_lith, all_max_column_heights 925 ) 926 927 if self.cfg.verbose: 928 print( 929 " ...+++... number of nan's in depth_maps_gaps before insertClosureLabels3D ...+++... {}".format( 930 old_depth_maps_gaps[np.isnan(old_depth_maps_gaps)].shape 931 ) 932 ) 933 print( 934 " ...+++... number of nan's in depth_maps_gaps after insertClosureLabels3D ...+++... {}".format( 935 self.faults.faulted_depth_maps_gaps[ 936 np.isnan(self.faults.faulted_depth_maps_gaps) 937 ].shape 938 ) 939 ) 940 print( 941 " ...+++... number of nan's in depth_maps after insertClosureLabels3D ...+++... {}".format( 942 self.faults.faulted_depth_maps[ 943 np.isnan(self.faults.faulted_depth_maps) 944 ].shape 945 ) 946 ) 947 _closure_segments = self.closure_segments[:] 948 print( 949 " ...+++... number of closure voxels in self.closure_segments ...+++... {}".format( 950 _closure_segments[_closure_segments > 0.0].shape 951 ) 952 ) 953 del _closure_segments 954 955 labels_clean, self.closure_segments[:] = self.segment_closures( 956 self.closure_segments[:], remove_shale=True 957 ) 958 label_values, labels_clean = self.parse_label_values_and_counts(labels_clean) 959 960 labels_clean_all, self.all_closure_segments[:] = self.segment_closures( 961 self.all_closure_segments[:], remove_shale=False 962 ) 963 label_values_all, labels_clean_all = self.parse_label_values_and_counts( 964 labels_clean_all 965 ) 966 self.write_cube_to_disk(self.all_closure_segments[:], "all_closure_segments") 967 968 # Assign fluid types 969 ( 970 self.oil_closures[:], 971 self.gas_closures[:], 972 self.brine_closures[:], 973 ) = self.assign_fluid_types(label_values, labels_clean) 974 all_closures_final = (labels_clean_all != 0).astype("uint8") 975 976 # Identify closures by type (simple, faulted, onlap or salt bounded) 977 self.find_faulted_closures(label_values, labels_clean) 978 self.find_onlap_closures(label_values, labels_clean) 979 self.find_simple_closures(label_values, labels_clean) 980 self.find_false_closures(label_values, labels_clean) 981 982 self.find_faulted_all_closures(label_values_all, labels_clean_all) 983 self.find_onlap_all_closures(label_values_all, labels_clean_all) 984 self.find_simple_all_closures(label_values_all, labels_clean_all) 985 self.find_false_all_closures(label_values_all, labels_clean_all) 986 987 if self.cfg.include_salt: 988 self.find_salt_bounded_closures(label_values, labels_clean) 989 self.find_salt_bounded_all_closures(label_values_all, labels_clean_all) 990 991 # Remove false closures from oil & gas closure cubes 992 if self.n_false_closures_oil > 0: 993 print(f"Removing {self.n_false_closures_oil} false oil closures") 994 self.oil_closures[self.false_closures_oil == 1] = 0.0 995 if self.n_false_closures_gas > 0: 996 print(f"Removing {self.n_false_closures_gas} false gas closures") 997 self.gas_closures[self.false_closures_gas == 1] = 0.0 998 999 # Remove false closures from allclosure cube 1000 if self.n_false_all_closures > 0: 1001 print(f"Removing {self.n_false_all_closures} false all closures") 1002 self.all_closure_segments[self.false_all_closures == 1] = 0.0 1003 1004 # Create a closure cube with voxel count as labels, and include closure type in decimal 1005 # e.g. simple closure of size 5000 = 5000.1 1006 # faulted closure of size 5000 = 5000.2 1007 # onlap closure of size 5000 = 5000.3 1008 # salt-bounded closure of size 5000 = 5000.4 1009 hc_closure_codes = np.zeros_like(self.gas_closures, dtype="float32") 1010 1011 # AZ: COULD RUN THESE CLOSURE SIZE FILTERS ON ALL_CLOSURES, IF DESIRED 1012 1013 if "simple" in self.cfg.closure_types: 1014 print("Filtering 4 Way Closures") 1015 ( 1016 self.simple_closures_oil[:], 1017 self.n_4way_closures_oil, 1018 ) = self.closure_size_filter( 1019 self.simple_closures_oil[:], 1020 self.cfg.closure_min_voxels_simple, 1021 self.n_4way_closures_oil, 1022 ) 1023 ( 1024 self.simple_closures_gas[:], 1025 self.n_4way_closures_gas, 1026 ) = self.closure_size_filter( 1027 self.simple_closures_gas[:], 1028 self.cfg.closure_min_voxels_simple, 1029 self.n_4way_closures_gas, 1030 ) 1031 1032 # Add simple closures to closure code cube 1033 hc_closures = ( 1034 self.simple_closures_oil[:] + self.simple_closures_gas[:] 1035 ).astype("float32") 1036 labels, num = measure.label( 1037 hc_closures, connectivity=2, background=0, return_num=True 1038 ) 1039 hc_closure_codes = self.parse_closure_codes( 1040 hc_closure_codes, labels, num, code=0.1 1041 ) 1042 else: # if closure type not in config, set HC closures to 0 1043 self.simple_closures_oil[:] *= 0 1044 self.simple_closures_gas[:] *= 0 1045 self.simple_all_closures[:] *= 0 1046 1047 self.oil_closures[self.simple_closures_oil[:] > 0.0] = 1.0 1048 self.oil_closures[self.simple_closures_oil[:] < 0.0] = 0.0 1049 self.gas_closures[self.simple_closures_gas[:] > 0.0] = 1.0 1050 self.gas_closures[self.simple_closures_gas[:] < 0.0] = 0.0 1051 1052 all_closures_final[self.simple_all_closures[:] > 0.0] = 1.0 1053 all_closures_final[self.simple_all_closures[:] < 0.0] = 0.0 1054 1055 if "faulted" in self.cfg.closure_types: 1056 print("Filtering 4 Way Closures") 1057 # Grow the faulted closures to the fault planes 1058 self.faulted_closures_oil[:] = self.grow_to_fault2( 1059 self.faulted_closures_oil[:] 1060 ) 1061 self.faulted_closures_gas[:] = self.grow_to_fault2( 1062 self.faulted_closures_gas[:] 1063 ) 1064 1065 ( 1066 self.faulted_closures_oil[:], 1067 self.n_fault_closures_oil, 1068 ) = self.closure_size_filter( 1069 self.faulted_closures_oil[:], 1070 self.cfg.closure_min_voxels_faulted, 1071 self.n_fault_closures_oil, 1072 ) 1073 ( 1074 self.faulted_closures_gas[:], 1075 self.n_fault_closures_gas, 1076 ) = self.closure_size_filter( 1077 self.faulted_closures_gas[:], 1078 self.cfg.closure_min_voxels_faulted, 1079 self.n_fault_closures_gas, 1080 ) 1081 1082 self.faulted_all_closures[:] = self.grow_to_fault2( 1083 self.faulted_all_closures[:], 1084 grow_only_sand_closures=False, 1085 remove_small_closures=False, 1086 ) 1087 1088 # Add faulted closures to closure code cube 1089 hc_closures = self.faulted_closures_oil[:] + self.faulted_closures_gas[:] 1090 labels, num = measure.label( 1091 hc_closures, connectivity=2, background=0, return_num=True 1092 ) 1093 hc_closure_codes = self.parse_closure_codes( 1094 hc_closure_codes, labels, num, code=0.2 1095 ) 1096 else: # if closure type not in config, set HC closures to 0 1097 self.faulted_closures_oil[:] *= 0 1098 self.faulted_closures_gas[:] *= 0 1099 self.faulted_all_closures[:] *= 0 1100 1101 self.oil_closures[self.faulted_closures_oil[:] > 0.0] = 1.0 1102 self.oil_closures[self.faulted_closures_oil[:] < 0.0] = 0.0 1103 self.gas_closures[self.faulted_closures_gas[:] > 0.0] = 1.0 1104 self.gas_closures[self.faulted_closures_gas[:] < 0.0] = 0.0 1105 1106 all_closures_final[self.faulted_all_closures[:] > 0.0] = 1.0 1107 all_closures_final[self.faulted_all_closures[:] < 0.0] = 0.0 1108 1109 if "onlap" in self.cfg.closure_types: 1110 print("Filtering Onlap Closures") 1111 ( 1112 self.onlap_closures_oil[:], 1113 self.n_onlap_closures_oil, 1114 ) = self.closure_size_filter( 1115 self.onlap_closures_oil[:], 1116 self.cfg.closure_min_voxels_onlap, 1117 self.n_onlap_closures_oil, 1118 ) 1119 ( 1120 self.onlap_closures_gas[:], 1121 self.n_onlap_closures_gas, 1122 ) = self.closure_size_filter( 1123 self.onlap_closures_gas[:], 1124 self.cfg.closure_min_voxels_onlap, 1125 self.n_onlap_closures_gas, 1126 ) 1127 1128 # Add faulted closures to closure code cube 1129 hc_closures = self.onlap_closures_oil[:] + self.onlap_closures_gas[:] 1130 labels, num = measure.label( 1131 hc_closures, connectivity=2, background=0, return_num=True 1132 ) 1133 hc_closure_codes = self.parse_closure_codes( 1134 hc_closure_codes, labels, num, code=0.3 1135 ) 1136 # labels = labels.astype('float32') 1137 # if num > 0: 1138 # for x in range(1, num + 1): 1139 # y = 0.3 + labels[labels == x].size 1140 # labels[labels == x] = y 1141 # hc_closure_codes += labels 1142 else: # if closure type not in config, set HC closures to 0 1143 self.onlap_closures_oil[:] *= 0 1144 self.onlap_closures_gas[:] *= 0 1145 self.onlap_all_closures[:] *= 0 1146 1147 self.oil_closures[self.onlap_closures_oil[:] > 0.0] = 1.0 1148 self.oil_closures[self.onlap_closures_oil[:] < 0.0] = 0.0 1149 self.gas_closures[self.onlap_closures_gas[:] > 0.0] = 1.0 1150 self.gas_closures[self.onlap_closures_gas[:] < 0.0] = 0.0 1151 all_closures_final[self.onlap_all_closures[:] > 0.0] = 1.0 1152 all_closures_final[self.onlap_all_closures[:] < 0.0] = 0.0 1153 1154 if self.cfg.include_salt: 1155 # Grow the salt-bounded closures to the salt body 1156 salt_closures_oil_grown = np.zeros_like(self.salt_closures_oil[:]) 1157 salt_closures_gas_grown = np.zeros_like(self.salt_closures_gas[:]) 1158 1159 if np.max(self.salt_closures_oil[:]) > 0.0: 1160 self.write_cube_to_disk( 1161 self.salt_closures_oil[:], "salt_closures_oil_initial" 1162 ) 1163 print( 1164 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1165 ) 1166 salt_closures_oil_grown = self.grow_to_salt(self.salt_closures_oil[:]) 1167 self.salt_closures_oil[:] = salt_closures_oil_grown 1168 print( 1169 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1170 ) 1171 if np.max(self.salt_closures_gas[:]) > 0.0: 1172 self.write_cube_to_disk( 1173 self.salt_closures_gas[:], "salt_closures_gas_initial" 1174 ) 1175 print( 1176 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 0].size}" 1177 ) 1178 salt_closures_gas_grown = self.grow_to_salt(self.salt_closures_gas[:]) 1179 self.salt_closures_gas[:] = salt_closures_gas_grown 1180 print( 1181 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 1].size}" 1182 ) 1183 if np.max(self.salt_all_closures[:]) > 0.0: 1184 self.write_cube_to_disk( 1185 self.salt_all_closures[:], "salt_all_closures_initial" 1186 ) # maybe remove later 1187 print( 1188 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 0].size}" 1189 ) 1190 salt_all_closures_grown = self.grow_to_salt(self.salt_all_closures[:]) 1191 self.salt_all_closures[:] = salt_all_closures_grown 1192 print( 1193 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 1].size}" 1194 ) 1195 else: 1196 salt_all_closures_grown = np.zeros_like(self.salt_all_closures) 1197 1198 if np.max(self.salt_closures_oil[:]) > 0.0: 1199 self.write_cube_to_disk( 1200 self.salt_closures_oil[:], "salt_closures_oil_grown" 1201 ) 1202 if np.max(self.salt_closures_gas[:]) > 0.0: 1203 self.write_cube_to_disk( 1204 self.salt_closures_gas[:], "salt_closures_gas_grown" 1205 ) 1206 if np.max(self.salt_all_closures[:]) > 0.0: 1207 self.write_cube_to_disk( 1208 self.salt_all_closures[:], "salt_all_closures_grown" 1209 ) # maybe remove later 1210 1211 ( 1212 self.salt_closures_oil[:], 1213 self.n_salt_closures_oil, 1214 ) = self.closure_size_filter( 1215 self.salt_closures_oil[:], 1216 self.cfg.closure_min_voxels, 1217 self.n_salt_closures_oil, 1218 ) 1219 ( 1220 self.salt_closures_gas[:], 1221 self.n_salt_closures_gas, 1222 ) = self.closure_size_filter( 1223 self.salt_closures_gas[:], 1224 self.cfg.closure_min_voxels, 1225 self.n_salt_closures_gas, 1226 ) 1227 1228 # Append salt-bounded closures to main closure cubes for oil and gas 1229 if np.max(salt_closures_oil_grown) > 0.0: 1230 self.oil_closures[salt_closures_oil_grown > 0.0] = 1.0 1231 self.oil_closures[salt_closures_oil_grown < 0.0] = 0.0 1232 if np.max(salt_closures_gas_grown) > 0.0: 1233 self.gas_closures[salt_closures_gas_grown > 0.0] = 1.0 1234 self.gas_closures[salt_closures_gas_grown < 0.0] = 0.0 1235 if np.max(salt_all_closures_grown) > 0.0: 1236 all_closures_final[salt_all_closures_grown > 0.0] = 1.0 1237 all_closures_final[salt_all_closures_grown < 0.0] = 0.0 1238 1239 # Add faulted closures to closure code cube 1240 hc_closures = self.salt_closures_oil[:] + self.salt_closures_gas[:] 1241 labels, num = measure.label( 1242 hc_closures, connectivity=2, background=0, return_num=True 1243 ) 1244 hc_closure_codes = self.parse_closure_codes( 1245 hc_closure_codes, labels, num, code=0.4 1246 ) 1247 1248 # Write hc_closure_codes to disk 1249 self.write_cube_to_disk(hc_closure_codes, "closure_segments_hc_voxelcount") 1250 1251 # Create closure volumes by type 1252 if self.simple_closures[:] is None: 1253 self.simple_closures[:] = self.simple_closures_oil[:].astype("uint8") 1254 else: 1255 self.simple_closures[:] += self.simple_closures_oil[:].astype("uint8") 1256 self.simple_closures[:] += self.simple_closures_gas[:].astype("uint8") 1257 self.simple_closures[:] += self.simple_closures_brine[:].astype("uint8") 1258 # Onlap closures 1259 if self.strat_closures is None: 1260 self.strat_closures[:] = self.onlap_closures_oil[:].astype("uint8") 1261 else: 1262 self.strat_closures[:] += self.onlap_closures_oil[:].astype("uint8") 1263 self.strat_closures[:] += self.onlap_closures_gas[:].astype("uint8") 1264 self.strat_closures[:] += self.onlap_closures_brine[:].astype("uint8") 1265 # Fault closures 1266 if self.fault_closures is None: 1267 self.fault_closures[:] = self.faulted_closures_oil[:].astype("uint8") 1268 else: 1269 self.fault_closures[:] += self.faulted_closures_oil[:].astype("uint8") 1270 self.fault_closures[:] += self.faulted_closures_gas[:].astype("uint8") 1271 self.fault_closures[:] += self.faulted_closures_brine[:].astype("uint8") 1272 1273 # Salt-bounded closures 1274 if self.cfg.include_salt: 1275 if self.salt_closures is None: 1276 self.salt_closures[:] = self.salt_closures_oil[:].astype("uint8") 1277 else: 1278 self.salt_closures[:] += self.salt_closures_oil[:].astype("uint8") 1279 self.salt_closures[:] += self.salt_closures_gas[:].astype("uint8") 1280 1281 # Convert closure cubes from int16 to uint8 for writing to disk 1282 self.closure_segments[:] = self.closure_segments[:].astype("uint8") 1283 1284 # add any oil/gas/brine closures into all_closures_final in case missed 1285 all_closures_final[:][self.oil_closures[:] > 0] = 1 1286 all_closures_final[:][self.gas_closures[:] > 0] = 1 1287 all_closures_final[:][self.gas_closures[:] > 0] = 1 1288 # Write all_closures_final to disk 1289 self.write_cube_to_disk(all_closures_final.astype("uint8"), "trap_label") 1290 1291 # add any oil/gas/brine closures into reservoir in case missed 1292 self.faults.reservoir[:][self.oil_closures[:] > 0] = 1 1293 self.faults.reservoir[:][self.gas_closures[:] > 0] = 1 1294 self.faults.reservoir[:][self.brine_closures[:] > 0] = 1 1295 # write reservoir_label to disk 1296 self.write_cube_to_disk( 1297 self.faults.reservoir[:].astype("uint8"), "reservoir_label" 1298 ) 1299 1300 if self.cfg.qc_plots: 1301 from datagenerator.util import plot_xsection 1302 from datagenerator.util import find_line_with_most_voxels 1303 1304 # visualize closures QC 1305 inline_index_cl = find_line_with_most_voxels( 1306 self.closure_segments, 0.5, self.cfg 1307 ) 1308 plot_xsection( 1309 volume=labels_clean, 1310 maps=self.faults.faulted_depth_maps_gaps, 1311 line_num=inline_index_cl, 1312 title="Example Trav through 3D model\nclosures after faulting", 1313 png_name="QC_plot__AfterFaulting_closure_segments.png", 1314 cmap="gist_ncar_r", 1315 cfg=self.cfg, 1316 ) 1317 1318 def closure_size_filter(self, closure_type, threshold, count): 1319 labels, num = measure.label( 1320 closure_type, connectivity=2, background=0, return_num=True 1321 ) 1322 if ( 1323 num > 0 1324 ): # TODO add whether smallest closure is below threshold constraint too 1325 s = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1326 labels = morphology.remove_small_objects(labels, threshold, connectivity=2) 1327 t = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1328 print( 1329 f"Closure sizes before filter: {s}\nThreshold: {threshold}\n" 1330 f"Closure sizes after filter: {t}" 1331 ) 1332 count = len(t) 1333 return labels, count 1334 1335 def closure_type_info_for_log(self): 1336 fluid_types = ["oil", "gas", "brine"] 1337 if "faulted" in self.cfg.closure_types: 1338 # Faulted closures 1339 for name, fluid, num in zip( 1340 fluid_types, 1341 [ 1342 self.faulted_closures_oil[:], 1343 self.faulted_closures_gas[:], 1344 self.faulted_closures_brine[:], 1345 ], 1346 [ 1347 self.n_fault_closures_oil, 1348 self.n_fault_closures_gas, 1349 self.n_fault_closures_brine, 1350 ], 1351 ): 1352 n_voxels = fluid[fluid[:] > 0.0].size 1353 msg = f"n_fault_closures_{name}: {num:03d}\n" 1354 msg += f"n_voxels_fault_closures_{name}: {n_voxels:08d}\n" 1355 print(msg) 1356 self.cfg.write_to_logfile(msg) 1357 self.cfg.write_to_logfile( 1358 msg=None, 1359 mainkey="model_parameters", 1360 subkey=f"n_fault_closures_{name}", 1361 val=num, 1362 ) 1363 self.cfg.write_to_logfile( 1364 msg=None, 1365 mainkey="model_parameters", 1366 subkey=f"n_voxels_fault_closures_{name}", 1367 val=n_voxels, 1368 ) 1369 closure_statistics = self.calculate_closure_statistics( 1370 fluid, f"Faulted {name.capitalize()}" 1371 ) 1372 if closure_statistics: 1373 print(closure_statistics) 1374 self.cfg.write_to_logfile(closure_statistics) 1375 1376 if "onlap" in self.cfg.closure_types: 1377 # Onlap Closures 1378 for name, fluid, num in zip( 1379 fluid_types, 1380 [ 1381 self.onlap_closures_oil[:], 1382 self.onlap_closures_gas[:], 1383 self.onlap_closures_brine[:], 1384 ], 1385 [ 1386 self.n_onlap_closures_oil, 1387 self.n_onlap_closures_gas, 1388 self.n_onlap_closures_brine, 1389 ], 1390 ): 1391 n_voxels = fluid[fluid[:] > 0.0].size 1392 msg = f"n_onlap_closures_{name}: {num:03d}\n" 1393 msg += f"n_voxels_onlap_closures_{name}: {n_voxels:08d}\n" 1394 print(msg) 1395 self.cfg.write_to_logfile(msg) 1396 self.cfg.write_to_logfile( 1397 msg=None, 1398 mainkey="model_parameters", 1399 subkey=f"n_onlap_closures_{name}", 1400 val=num, 1401 ) 1402 self.cfg.write_to_logfile( 1403 msg=None, 1404 mainkey="model_parameters", 1405 subkey=f"n_voxels_onlap_closures_{name}", 1406 val=n_voxels, 1407 ) 1408 closure_statistics = self.calculate_closure_statistics( 1409 fluid, f"Onlap {name.capitalize()}" 1410 ) 1411 if closure_statistics: 1412 print(closure_statistics) 1413 self.cfg.write_to_logfile(closure_statistics) 1414 1415 if "simple" in self.cfg.closure_types: 1416 # Simple Closures 1417 for name, fluid, num in zip( 1418 fluid_types, 1419 [ 1420 self.simple_closures_oil[:], 1421 self.simple_closures_gas[:], 1422 self.simple_closures_brine[:], 1423 ], 1424 [ 1425 self.n_4way_closures_oil, 1426 self.n_4way_closures_gas, 1427 self.n_4way_closures_brine, 1428 ], 1429 ): 1430 n_voxels = fluid[fluid[:] > 0.0].size 1431 msg = f"n_4way_closures_{name}: {num:03d}\n" 1432 msg += f"n_voxels_4way_closures_{name}: {n_voxels:08d}\n" 1433 print(msg) 1434 self.cfg.write_to_logfile(msg) 1435 self.cfg.write_to_logfile( 1436 msg=None, 1437 mainkey="model_parameters", 1438 subkey=f"n_4way_closures_{name}", 1439 val=num, 1440 ) 1441 self.cfg.write_to_logfile( 1442 msg=None, 1443 mainkey="model_parameters", 1444 subkey=f"n_voxels_4way_closures_{name}", 1445 val=n_voxels, 1446 ) 1447 closure_statistics = self.calculate_closure_statistics( 1448 fluid, f"4-Way {name.capitalize()}" 1449 ) 1450 if closure_statistics: 1451 print(closure_statistics) 1452 self.cfg.write_to_logfile(closure_statistics) 1453 1454 if self.cfg.include_salt: 1455 # Salt-Bounded Closures 1456 for name, fluid, num in zip( 1457 fluid_types, 1458 [ 1459 self.salt_closures_oil[:], 1460 self.salt_closures_gas[:], 1461 self.salt_closures_brine[:], 1462 ], 1463 [ 1464 self.n_salt_closures_oil, 1465 self.n_salt_closures_gas, 1466 self.n_salt_closures_brine, 1467 ], 1468 ): 1469 n_voxels = fluid[fluid[:] > 0.0].size 1470 msg = f"n_salt_closures_{name}: {num:03d}\n" 1471 msg += f"n_voxels_salt_closures_{name}: {n_voxels:08d}\n" 1472 print(msg) 1473 self.cfg.write_to_logfile(msg) 1474 self.cfg.write_to_logfile( 1475 msg=None, 1476 mainkey="model_parameters", 1477 subkey=f"n_salt_closures_{name}", 1478 val=num, 1479 ) 1480 self.cfg.write_to_logfile( 1481 msg=None, 1482 mainkey="model_parameters", 1483 subkey=f"n_voxels_salt_closures_{name}", 1484 val=n_voxels, 1485 ) 1486 closure_statistics = self.calculate_closure_statistics( 1487 fluid, f"Salt {name.capitalize()}" 1488 ) 1489 if closure_statistics: 1490 print(closure_statistics) 1491 self.cfg.write_to_logfile(closure_statistics) 1492 1493 def get_voxel_counts(self, closures): 1494 next_label = 0 1495 label_values = [0] 1496 label_counts = [closures[closures == 0].size] 1497 for i in range(closures.max() + 1): 1498 try: 1499 next_label = closures[closures > next_label].min() 1500 except (TypeError, ValueError): 1501 break 1502 label_values.append(next_label) 1503 label_counts.append(closures[closures == next_label].size) 1504 print( 1505 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1506 ) 1507 1508 print( 1509 f'{72 * "*"}\n\tNum Closures: {len(label_counts) - 1}\n\tVoxel counts\n{label_counts[1:]}\n{72 * "*"}' 1510 ) 1511 for vox_count in label_counts: 1512 if vox_count < self.cfg.closure_min_voxels: 1513 print(f"voxel_count: {vox_count}") 1514 1515 def populate_closure_dict(self, labels, fluid, seismic_nmf=None): 1516 clist = [] 1517 max_num = np.max(labels) 1518 if seismic_nmf is not None: 1519 # calculate ai_gi 1520 ai, gi = compute_ai_gi(self.cfg, seismic_nmf) 1521 for i in range(1, max_num + 1): 1522 _c = np.where(labels == i) 1523 cl = dict() 1524 cl["model_id"] = os.path.basename(self.cfg.work_subfolder) 1525 cl["fluid"] = fluid 1526 cl["n_voxels"] = len(_c[0]) 1527 # np.min() or x.min() returns type numpy.int64 which SQLITE cannot handle. Convert to int 1528 cl["x_min"] = int(np.min(_c[0])) 1529 cl["x_max"] = int(np.max(_c[0])) 1530 cl["y_min"] = int(np.min(_c[1])) 1531 cl["y_max"] = int(np.max(_c[1])) 1532 cl["z_min"] = int(np.min(_c[2])) 1533 cl["z_max"] = int(np.max(_c[2])) 1534 cl["zbml_min"] = np.min(self.faults.faulted_depth[_c]) 1535 cl["zbml_max"] = np.max(self.faults.faulted_depth[_c]) 1536 cl["zbml_avg"] = np.mean(self.faults.faulted_depth[_c]) 1537 cl["zbml_std"] = np.std(self.faults.faulted_depth[_c]) 1538 cl["zbml_25pct"] = np.percentile(self.faults.faulted_depth[_c], 25) 1539 cl["zbml_median"] = np.percentile(self.faults.faulted_depth[_c], 50) 1540 cl["zbml_75pct"] = np.percentile(self.faults.faulted_depth[_c], 75) 1541 cl["ng_min"] = np.min(self.faults.faulted_net_to_gross[_c]) 1542 cl["ng_max"] = np.max(self.faults.faulted_net_to_gross[_c]) 1543 cl["ng_avg"] = np.mean(self.faults.faulted_net_to_gross[_c]) 1544 cl["ng_std"] = np.std(self.faults.faulted_net_to_gross[_c]) 1545 cl["ng_25pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 25) 1546 cl["ng_median"] = np.median(self.faults.faulted_net_to_gross[_c]) 1547 cl["ng_75pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 75) 1548 # Check for intersections with faults, salt and onlaps for closure type 1549 cl["intersects_fault"] = False 1550 cl["intersects_onlap"] = False 1551 cl["intersects_salt"] = False 1552 if np.max(self.wide_faults[_c] > 0): 1553 cl["intersects_fault"] = True 1554 if np.max(self.onlaps_upward[_c] > 0): 1555 cl["intersects_onlap"] = True 1556 if self.cfg.include_salt and np.max(self.wide_salt[_c] > 0): 1557 cl["intersects_salt"] = True 1558 1559 if seismic_nmf is not None: 1560 # Using only the top of the closure, calculate seismic properties 1561 labels_copy = labels.copy() 1562 labels_copy[labels_copy != i] = 0 1563 top_closure = get_top_of_closure(labels_copy) 1564 near = seismic_nmf[0, ...][np.where(top_closure == 1)] 1565 cl["near_min"] = np.min(near) 1566 cl["near_max"] = np.max(near) 1567 cl["near_avg"] = np.mean(near) 1568 cl["near_std"] = np.std(near) 1569 cl["near_25pct"] = np.percentile(near, 25) 1570 cl["near_median"] = np.percentile(near, 50) 1571 cl["near_75pct"] = np.percentile(near, 75) 1572 mid = seismic_nmf[1, ...][np.where(top_closure == 1)] 1573 cl["mid_min"] = np.min(mid) 1574 cl["mid_max"] = np.max(mid) 1575 cl["mid_avg"] = np.mean(mid) 1576 cl["mid_std"] = np.std(mid) 1577 cl["mid_25pct"] = np.percentile(mid, 25) 1578 cl["mid_median"] = np.percentile(mid, 50) 1579 cl["mid_75pct"] = np.percentile(mid, 75) 1580 far = seismic_nmf[2, ...][np.where(top_closure == 1)] 1581 cl["far_min"] = np.min(far) 1582 cl["far_max"] = np.max(far) 1583 cl["far_avg"] = np.mean(far) 1584 cl["far_std"] = np.std(far) 1585 cl["far_25pct"] = np.percentile(far, 25) 1586 cl["far_median"] = np.percentile(far, 50) 1587 cl["far_75pct"] = np.percentile(far, 75) 1588 intercept = ai[np.where(top_closure == 1)] 1589 cl["intercept_min"] = np.min(intercept) 1590 cl["intercept_max"] = np.max(intercept) 1591 cl["intercept_avg"] = np.mean(intercept) 1592 cl["intercept_std"] = np.std(intercept) 1593 cl["intercept_25pct"] = np.percentile(intercept, 25) 1594 cl["intercept_median"] = np.percentile(intercept, 50) 1595 cl["intercept_75pct"] = np.percentile(intercept, 75) 1596 gradient = gi[np.where(top_closure == 1)] 1597 cl["gradient_min"] = np.min(gradient) 1598 cl["gradient_max"] = np.max(gradient) 1599 cl["gradient_avg"] = np.mean(gradient) 1600 cl["gradient_std"] = np.std(gradient) 1601 cl["gradient_25pct"] = np.percentile(gradient, 25) 1602 cl["gradient_median"] = np.percentile(gradient, 50) 1603 cl["gradient_75pct"] = np.percentile(gradient, 75) 1604 1605 clist.append(cl) 1606 1607 return clist 1608 1609 def write_closure_info_to_log(self, seismic_nmf=None): 1610 """store info about closure in log file""" 1611 top_sand_layers = [x for x in self.top_lith_indices if self.facies[x] == 1.0] 1612 self.cfg.write_to_logfile( 1613 msg=None, 1614 mainkey="model_parameters", 1615 subkey="top_sand_layers", 1616 val=top_sand_layers, 1617 ) 1618 o = measure.label(self.oil_closures[:], connectivity=2, background=0) 1619 g = measure.label(self.gas_closures[:], connectivity=2, background=0) 1620 b = measure.label(self.brine_closures[:], connectivity=2, background=0) 1621 oil_closures = self.populate_closure_dict(o, "oil", seismic_nmf) 1622 gas_closures = self.populate_closure_dict(g, "gas", seismic_nmf) 1623 brine_closures = self.populate_closure_dict(b, "brine", seismic_nmf) 1624 all_closures = oil_closures + gas_closures + brine_closures 1625 for i, c in enumerate(all_closures): 1626 self.cfg.sqldict[f"closure_{i+1}"] = c 1627 num_labels = np.max(o) + np.max(g) 1628 self.cfg.write_to_logfile( 1629 msg=None, 1630 mainkey="model_parameters", 1631 subkey="number_hc_closures", 1632 val=num_labels, 1633 ) 1634 # Add total number of closure voxels, with ratio of closure voxels given as a percentage 1635 closure_voxel_count = o[o > 0].size + g[g > 0].size 1636 closure_voxel_pct = closure_voxel_count / o.size 1637 self.cfg.write_to_logfile( 1638 msg=None, 1639 mainkey="model_parameters", 1640 subkey="closure_voxel_count", 1641 val=closure_voxel_count, 1642 ) 1643 self.cfg.write_to_logfile( 1644 msg=None, 1645 mainkey="model_parameters", 1646 subkey="closure_voxel_pct", 1647 val=closure_voxel_pct * 100, 1648 ) 1649 # Same for Brine 1650 _brine_voxels = b[b == 1].size 1651 _brine_voxels_pct = _brine_voxels / b.size 1652 self.cfg.write_to_logfile( 1653 msg=None, 1654 mainkey="model_parameters", 1655 subkey="closure_voxel_count_brine", 1656 val=_brine_voxels, 1657 ) 1658 self.cfg.write_to_logfile( 1659 msg=None, 1660 mainkey="model_parameters", 1661 subkey="closure_voxel_pct_brine", 1662 val=_brine_voxels_pct * 100, 1663 ) 1664 # Same for Oil 1665 _oil_voxels = o[o == 1].size 1666 _oil_voxels_pct = _oil_voxels / o.size 1667 self.cfg.write_to_logfile( 1668 msg=None, 1669 mainkey="model_parameters", 1670 subkey="closure_voxel_count_oil", 1671 val=_oil_voxels, 1672 ) 1673 self.cfg.write_to_logfile( 1674 msg=None, 1675 mainkey="model_parameters", 1676 subkey="closure_voxel_pct_oil", 1677 val=_oil_voxels_pct * 100, 1678 ) 1679 # Same for Gas 1680 _gas_voxels = g[g == 1].size 1681 _gas_voxels_pct = _gas_voxels / g.size 1682 self.cfg.write_to_logfile( 1683 msg=None, 1684 mainkey="model_parameters", 1685 subkey="closure_voxel_count_gas", 1686 val=_gas_voxels, 1687 ) 1688 self.cfg.write_to_logfile( 1689 msg=None, 1690 mainkey="model_parameters", 1691 subkey="closure_voxel_pct_gas", 1692 val=_gas_voxels_pct, 1693 ) 1694 # Write old logfile as well as the sql dict 1695 msg = f"layers for closure computation: {str(self.top_lith_indices)}\n" 1696 msg += f"Number of HC Closures : {num_labels}\n" 1697 msg += ( 1698 f"Closure voxel count: {closure_voxel_count} - " 1699 f"{closure_voxel_pct:5.2%}\n" 1700 ) 1701 msg += ( 1702 f"Closure voxel count: (brine) {_brine_voxels} - {_brine_voxels_pct:5.2%}\n" 1703 ) 1704 msg += f"Closure voxel count: (oil) {_oil_voxels} - {_oil_voxels_pct:5.2%}\n" 1705 msg += f"Closure voxel count: (gas) {_gas_voxels} - {_gas_voxels_pct:5.2%}\n" 1706 print(msg) 1707 for i in range(self.facies.shape[0]): 1708 if self.facies[i] == 1: 1709 msg += f" layers for closure computation: {i}, sand\n" 1710 else: 1711 msg += f" layers for closure computation: {i}, shale\n" 1712 self.cfg.write_to_logfile(msg) 1713 1714 def parse_label_values_and_counts(self, labels_clean): 1715 """parse label values and counts""" 1716 if self.cfg.verbose: 1717 print(" Inside parse_label_values_and_counts") 1718 next_label = 0 1719 label_values = [0] 1720 label_counts = [labels_clean[labels_clean == 0].size] 1721 for i in range(1, labels_clean.max() + 1): 1722 try: 1723 next_label = labels_clean[labels_clean > next_label].min() 1724 except (TypeError, ValueError): 1725 break 1726 label_values.append(next_label) 1727 label_counts.append(labels_clean[labels_clean == next_label].size) 1728 print( 1729 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1730 ) 1731 # force labels to use consecutive integer values 1732 for i, ilabel in enumerate(label_values): 1733 labels_clean[labels_clean == ilabel] = i 1734 label_values[i] = i 1735 # labels_clean = self.remove_small_objects(labels_clean) # already applied to labels_clean 1736 # Remove label_value 0 1737 label_values.remove(0) 1738 return label_values, labels_clean 1739 1740 def assign_fluid_types(self, label_values, labels_clean): 1741 """randomly assign oil or gas to closure""" 1742 print( 1743 " labels_clean.min(), labels_clean.max() = ", 1744 labels_clean.min(), 1745 labels_clean.max(), 1746 ) 1747 _brine_closures = (labels_clean * 0.0).astype("uint8") 1748 _oil_closures = (labels_clean * 0.0).astype("uint8") 1749 _gas_closures = (labels_clean * 0.0).astype("uint8") 1750 1751 fluid_type_code = np.random.randint(3, size=labels_clean.max() + 1) 1752 1753 _closure_segments = self.closure_segments[:] 1754 for i in range(1, labels_clean.max() + 1): 1755 voxel_count = labels_clean[labels_clean == i].size 1756 if voxel_count > 0: 1757 print(f"Voxel Count: {voxel_count}\tFluid type: {fluid_type_code[i]}") 1758 # not in closure = 0 1759 # closure with brine filled reservoir fluid_type_code = 1 1760 # closure with oil filled reservoir fluid_type_code = 2 1761 # closure with gas filled reservoir fluid_type_code = 3 1762 if i in label_values: 1763 if fluid_type_code[i] == 0: 1764 # brine: change labels_clean contents to fluid_type_code = 1 (same as background) 1765 _brine_closures[ 1766 np.logical_and(labels_clean == i, _closure_segments > 0) 1767 ] = 1 1768 elif fluid_type_code[i] == 1: 1769 # oil: change labels_clean contents to fluid_type_code = 2 1770 _oil_closures[labels_clean == i] = 1 1771 elif fluid_type_code[i] == 2: 1772 # gas: change labels_clean contents to fluid_type_code = 3 1773 _gas_closures[labels_clean == i] = 1 1774 return _oil_closures, _gas_closures, _brine_closures 1775 1776 def remove_small_objects(self, labels, min_filter=True): 1777 try: 1778 # Use the global minimum voxel size initially, before closure types are identified 1779 labels_clean = morphology.remove_small_objects( 1780 labels, self.cfg.closure_min_voxels 1781 ) 1782 if self.cfg.verbose: 1783 print("labels_clean succeeded.") 1784 print( 1785 " labels.min:{}, labels.max: {}".format(labels.min(), labels.max()) 1786 ) 1787 print( 1788 " labels_clean min:{}, labels_clean max: {}".format( 1789 labels_clean.min(), labels_clean.max() 1790 ) 1791 ) 1792 except Exception as e: 1793 print( 1794 f"Closures/create_closures: labels_clean (remove_small_objects) did not succeed: {e}" 1795 ) 1796 if min_filter: 1797 labels_clean = minimum_filter(labels, size=(3, 3, 3)) 1798 if self.cfg.verbose: 1799 print( 1800 " labels.min:{}, labels.max: {}".format( 1801 labels.min(), labels.max() 1802 ) 1803 ) 1804 print( 1805 " labels_clean min:{}, labels_clean max: {}".format( 1806 labels_clean.min(), labels_clean.max() 1807 ) 1808 ) 1809 return labels_clean 1810 1811 def segment_closures(self, _closure_segments, remove_shale=True): 1812 """Segment the closures so that they can be randomly filled with hydrocarbons""" 1813 1814 _closure_segments = np.clip(_closure_segments, 0.0, 1.0) 1815 # remove tiny clusters 1816 _closure_segments = minimum_filter( 1817 _closure_segments.astype("int16"), size=(3, 3, 1) 1818 ) 1819 _closure_segments = maximum_filter(_closure_segments, size=(3, 3, 1)) 1820 1821 if remove_shale: 1822 # restrict closures to sand (non-shale) voxels 1823 if self.faults.faulted_lithology.shape[2] == _closure_segments.shape[2]: 1824 sand_shale = self.faults.faulted_lithology[:].copy() 1825 else: 1826 sand_shale = self.faults.faulted_lithology[ 1827 :, :, :: self.cfg.infill_factor 1828 ].copy() 1829 _closure_segments[sand_shale <= 0.0] = 0 1830 del sand_shale 1831 labels = measure.label(_closure_segments, connectivity=2, background=0) 1832 1833 labels_clean = self.remove_small_objects(labels) 1834 return labels_clean, _closure_segments 1835 1836 def write_closure_volumes_to_disk(self): 1837 # Create files for closure volumes 1838 self.write_cube_to_disk(self.brine_closures[:], "closure_segments_brine") 1839 self.write_cube_to_disk(self.oil_closures[:], "closure_segments_oil") 1840 self.write_cube_to_disk(self.gas_closures[:], "closure_segments_gas") 1841 # Create combined HC cube by adding oil and gas closures 1842 self.hc_labels[:] = (self.oil_closures[:] + self.gas_closures[:]).astype( 1843 "uint8" 1844 ) 1845 self.write_cube_to_disk(self.hc_labels[:], "closure_segments_hc") 1846 1847 if self.cfg.model_qc_volumes: 1848 self.write_cube_to_disk(self.closure_segments, "closure_segments_raw_all") 1849 self.write_cube_to_disk(self.simple_closures, "closure_segments_simple") 1850 self.write_cube_to_disk(self.strat_closures, "closure_segments_strat") 1851 self.write_cube_to_disk(self.fault_closures, "closure_segments_fault") 1852 1853 # Triple check that no small closures exist in the final closure files 1854 for i, c in enumerate( 1855 [ 1856 self.oil_closures, 1857 self.gas_closures, 1858 self.simple_closures, 1859 self.strat_closures, 1860 self.fault_closures, 1861 ] 1862 ): 1863 _t = measure.label(c, connectivity=2, background=0) 1864 counts = [_t[_t == x].size for x in range(np.max(_t))] 1865 print(f"Final closure volume voxels sizes: {counts}") 1866 for n, x in enumerate(counts): 1867 if x < self.cfg.closure_min_voxels: 1868 print(f"Voxel count: {x}\t Count:{i}, index: {n}") 1869 1870 # Return the hydrocarbon closure labels so that augmentation can be applied to the data & labels 1871 # return self.oil_closures + self.gas_closures 1872 1873 def calculate_closure_statistics(self, in_array, closure_type): 1874 """ 1875 Calculate the size and location of isolated features in an array 1876 1877 :param in_array: ndarray. Input array to be labelled, where non-zero values are counted as features 1878 :param closure_type: string. Closure type label 1879 :param digi: int or float. To convert depth values from samples to units 1880 :return: string. Concatenated string of closure statistics to be written to log 1881 """ 1882 labelled_array, max_labels = measure.label( 1883 in_array, connectivity=2, return_num=True 1884 ) 1885 msg = "" 1886 for i in range(1, max_labels + 1): # start at 1 to avoid counting 0's 1887 trap = np.where(labelled_array == i) 1888 ranges = [([np.min(trap[x]), np.max(trap[x])]) for x, _ in enumerate(trap)] 1889 sizes = [x[1] - x[0] for x in ranges] 1890 n_voxels = labelled_array[labelled_array == i].size 1891 if sum(sizes) > 0: 1892 msg += ( 1893 f"{closure_type}\t" 1894 f"Num X,Y,Z Samples: {str(sizes).ljust(15)}\t" 1895 f"Num Voxels: {str(n_voxels).ljust(5)}\t" 1896 f"Track: {2000 + ranges[0][0]}-{2000 + ranges[0][1]}\t" 1897 f"Bin: {1000 + ranges[1][0]}-{1000 + ranges[1][1]}\t" 1898 f"Depth: {ranges[2][0] * self.cfg.digi}-{ranges[2][1] * self.cfg.digi + self.cfg.digi / 2}\n" 1899 ) 1900 return msg 1901 1902 def find_faulted_closures(self, closure_segment_list, closure_segments): 1903 self._dilate_faults() 1904 for iclosure in closure_segment_list: 1905 i, j, k = np.where(closure_segments == iclosure) 1906 faults_within_closure = self.wide_faults[i, j, k] 1907 if faults_within_closure.max() > 0: 1908 if self.oil_closures[i, j, k].max() > 0: 1909 # Faulted oil closure 1910 self.faulted_closures_oil[i, j, k] = 1.0 1911 self.n_fault_closures_oil += 1 1912 self.fault_closures_oil_segment_list.append(iclosure) 1913 elif self.gas_closures[i, j, k].max() > 0: 1914 # Faulted gas closure 1915 self.faulted_closures_gas[i, j, k] = 1.0 1916 self.n_fault_closures_gas += 1 1917 self.fault_closures_gas_segment_list.append(iclosure) 1918 elif self.brine_closures[i, j, k].max() > 0: 1919 # Faulted brine closure 1920 self.faulted_closures_brine[i, j, k] = 1.0 1921 self.n_fault_closures_brine += 1 1922 self.fault_closures_brine_segment_list.append(iclosure) 1923 else: 1924 print( 1925 "Closure is faulted but does not have oil, gas or brine assigned" 1926 ) 1927 1928 def find_onlap_closures(self, closure_segment_list, closure_segments): 1929 for iclosure in closure_segment_list: 1930 i, j, k = np.where(closure_segments == iclosure) 1931 onlaps_within_closure = self.onlaps_upward[i, j, k] 1932 if onlaps_within_closure.max() > 0: 1933 if self.oil_closures[i, j, k].max() > 0: 1934 self.onlap_closures_oil[i, j, k] = 1.0 1935 self.n_onlap_closures_oil += 1 1936 self.onlap_closures_oil_segment_list.append(iclosure) 1937 elif self.gas_closures[i, j, k].max() > 0: 1938 self.onlap_closures_gas[i, j, k] = 1.0 1939 self.n_onlap_closures_gas += 1 1940 self.onlap_closures_gas_segment_list.append(iclosure) 1941 elif self.brine_closures[i, j, k].max() > 0: 1942 self.onlap_closures_brine[i, j, k] = 1.0 1943 self.n_onlap_closures_brine += 1 1944 self.onlap_closures_brine_segment_list.append(iclosure) 1945 else: 1946 print( 1947 "Closure is onlap but does not have oil, gas or brine assigned" 1948 ) 1949 1950 def find_simple_closures(self, closure_segment_list, closure_segments): 1951 for iclosure in closure_segment_list: 1952 i, j, k = np.where(closure_segments == iclosure) 1953 faults_within_closure = self.wide_faults[i, j, k] 1954 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 1955 onlaps_within_closure = onlaps[i, j, k] 1956 oil_within_closure = self.oil_closures[i, j, k] 1957 gas_within_closure = self.gas_closures[i, j, k] 1958 brine_within_closure = self.brine_closures[i, j, k] 1959 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 1960 if oil_within_closure.max() > 0: 1961 self.simple_closures_oil[i, j, k] = 1.0 1962 self.n_4way_closures_oil += 1 1963 elif gas_within_closure.max() > 0: 1964 self.simple_closures_gas[i, j, k] = 1.0 1965 self.n_4way_closures_gas += 1 1966 elif brine_within_closure.max() > 0: 1967 self.simple_closures_brine[i, j, k] = 1.0 1968 self.n_4way_closures_brine += 1 1969 else: 1970 print( 1971 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 1972 ) 1973 1974 def find_false_closures(self, closure_segment_list, closure_segments): 1975 for iclosure in closure_segment_list: 1976 i, j, k = np.where(closure_segments == iclosure) 1977 faults_within_closure = self.fat_faults[i, j, k] 1978 onlaps_within_closure = self.onlaps_downward[i, j, k] 1979 for fluid, false, num in zip( 1980 [self.oil_closures, self.gas_closures, self.brine_closures], 1981 [ 1982 self.false_closures_oil, 1983 self.false_closures_gas, 1984 self.false_closures_brine, 1985 ], 1986 [ 1987 self.n_false_closures_oil, 1988 self.n_false_closures_gas, 1989 self.n_false_closures_brine, 1990 ], 1991 ): 1992 fluid_within_closure = fluid[i, j, k] 1993 if fluid_within_closure.max() > 0: 1994 if onlaps_within_closure.max() > 0: 1995 _faulted_closure_threshold = float( 1996 faults_within_closure[faults_within_closure > 0].size 1997 / fluid_within_closure[fluid_within_closure > 0].size 1998 ) 1999 _onlap_closure_threshold = float( 2000 onlaps_within_closure[onlaps_within_closure > 0].size 2001 / fluid_within_closure[fluid_within_closure > 0].size 2002 ) 2003 if ( 2004 _faulted_closure_threshold > 0.65 2005 and _onlap_closure_threshold > 0.65 2006 ): 2007 false[i, j, k] = 1 2008 num += 1 2009 2010 def find_salt_bounded_closures(self, closure_segment_list, closure_segments): 2011 self._dilate_salt() 2012 for iclosure in closure_segment_list: 2013 i, j, k = np.where(closure_segments == iclosure) 2014 salt_within_closure = self.wide_salt[i, j, k] 2015 if salt_within_closure.max() > 0: 2016 if self.oil_closures[i, j, k].max() > 0: 2017 # salt bounded oil closure 2018 self.salt_closures_oil[i, j, k] = 1.0 2019 self.n_salt_closures_oil += 1 2020 self.salt_closures_oil_segment_list.append(iclosure) 2021 elif self.gas_closures[i, j, k].max() > 0: 2022 # salt bounded gas closure 2023 self.salt_closures_gas[i, j, k] = 1.0 2024 self.n_salt_closures_gas += 1 2025 self.salt_closures_gas_segment_list.append(iclosure) 2026 elif self.brine_closures[i, j, k].max() > 0: 2027 # salt bounded brine closure 2028 self.salt_closures_brine[i, j, k] = 1.0 2029 self.n_salt_closures_brine += 1 2030 self.salt_closures_brine_segment_list.append(iclosure) 2031 else: 2032 print( 2033 "Closure is salt bounded but does not have oil, gas or brine assigned" 2034 ) 2035 2036 def find_faulted_all_closures(self, closure_segment_list, closure_segments): 2037 for iclosure in closure_segment_list: 2038 i, j, k = np.where(closure_segments == iclosure) 2039 faults_within_closure = self.wide_faults[i, j, k] 2040 if faults_within_closure.max() > 0: 2041 self.faulted_all_closures[i, j, k] = 1.0 2042 self.n_fault_all_closures += 1 2043 self.fault_all_closures_segment_list.append(iclosure) 2044 2045 def find_onlap_all_closures(self, closure_segment_list, closure_segments): 2046 for iclosure in closure_segment_list: 2047 i, j, k = np.where(closure_segments == iclosure) 2048 onlaps_within_closure = self.onlaps_upward[i, j, k] 2049 if onlaps_within_closure.max() > 0: 2050 self.onlap_all_closures[i, j, k] = 1.0 2051 self.n_onlap_all_closures += 1 2052 self.onlap_all_closures_segment_list.append(iclosure) 2053 2054 def find_simple_all_closures(self, closure_segment_list, closure_segments): 2055 for iclosure in closure_segment_list: 2056 i, j, k = np.where(closure_segments == iclosure) 2057 faults_within_closure = self.wide_faults[i, j, k] 2058 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 2059 onlaps_within_closure = onlaps[i, j, k] 2060 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2061 self.simple_all_closures[i, j, k] = 1.0 2062 self.n_4way_all_closures += 1 2063 2064 def find_false_all_closures(self, closure_segment_list, closure_segments): 2065 for iclosure in closure_segment_list: 2066 i, j, k = np.where(closure_segments == iclosure) 2067 faults_within_closure = self.fat_faults[i, j, k] 2068 onlaps_within_closure = self.onlaps_downward[i, j, k] 2069 if onlaps_within_closure.max() > 0: 2070 _faulted_closure_threshold = float( 2071 faults_within_closure[faults_within_closure > 0].size / i.size 2072 ) 2073 _onlap_closure_threshold = float( 2074 onlaps_within_closure[onlaps_within_closure > 0].size / i.size 2075 ) 2076 if ( 2077 _faulted_closure_threshold > 0.65 2078 and _onlap_closure_threshold > 0.65 2079 ): 2080 self.false_all_closures[i, j, k] = 1 2081 self.n_false_all_closures += 1 2082 2083 def find_salt_bounded_all_closures(self, closure_segment_list, closure_segments): 2084 self._dilate_salt() 2085 for iclosure in closure_segment_list: 2086 i, j, k = np.where(closure_segments == iclosure) 2087 salt_within_closure = self.wide_salt[i, j, k] 2088 if salt_within_closure.max() > 0: 2089 self.salt_all_closures[i, j, k] = 1.0 2090 self.n_salt_all_closures += 1 2091 self.salt_all_closures_segment_list.append(iclosure) 2092 2093 def _dilate_faults(self): 2094 thresholded_faults = self._threshold_volumes(self.faults.fault_planes[:]) 2095 self.wide_faults[:] = self.grow_lateral( 2096 thresholded_faults, iterations=9, dist=1 2097 ) 2098 self.fat_faults[:] = self.grow_lateral( 2099 thresholded_faults, iterations=21, dist=1 2100 ) 2101 if self.cfg.include_salt: 2102 # Treat the salt body as a fault to grow closures to boundary 2103 thresholded_salt = self._threshold_volumes( 2104 self.faults.salt_model.salt_segments[:] 2105 ) 2106 wide_salt = self.grow_lateral(thresholded_salt, iterations=9, dist=1) 2107 self.wide_salt[:] = wide_salt 2108 # Add salt to faults to cehck if growing the closure works 2109 self.wide_faults[:] += wide_salt 2110 2111 def _dilate_salt(self): 2112 thresholded_salt = self._threshold_volumes( 2113 self.faults.salt_model.salt_segments[:] 2114 ) 2115 wide_salt = self.grow_lateral(thresholded_salt, iterations=9, dist=1) 2116 self.wide_salt[:] = wide_salt 2117 2118 def _dilate_onlaps(self): 2119 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 2120 mask = np.zeros((1, 1, 3)) 2121 mask[0, 0, :2] = 1 2122 self.onlaps_upward[:] = morphology.binary_dilation(onlaps, mask) 2123 mask = np.zeros((1, 1, 3)) 2124 mask[0, 0, 1:] = 1 2125 self.onlaps_downward[:] = onlaps.copy() 2126 for _ in range(30): 2127 try: 2128 self.onlaps_downward[:] = morphology.binary_dilation( 2129 self.onlaps_downward[:], mask 2130 ) 2131 except: 2132 break 2133 2134 def grow_to_fault2( 2135 self, closures, grow_only_sand_closures=True, remove_small_closures=True 2136 ): 2137 # - grow closures laterally and up within layer and within fault block 2138 print( 2139 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2140 ) 2141 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2142 2143 # dilated_fault_closures = closures.copy() 2144 # n_faulted_closures = dilated_fault_closures.max() 2145 labels_clean = self.closure_segments[:].copy() 2146 labels_clean[closures == 0] = 0 2147 labels_clean_list = list(set(labels_clean.flatten())) 2148 labels_clean_list.remove(0) 2149 initial_closures = labels_clean.copy() 2150 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2151 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2152 2153 # TODO remove this once small closures are found and fixed 2154 voxel_sizes = [ 2155 self.closure_segments[self.closure_segments[:] == i].size 2156 for i in labels_clean_list 2157 ] 2158 for _v in voxel_sizes: 2159 print(f"Voxel_Sizes: {_v}") 2160 if _v < self.cfg.closure_min_voxels: 2161 print(_v) 2162 2163 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2164 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2165 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2166 _ng = self.faults.faulted_net_to_gross[:].copy() 2167 # Cannot solely use NG anymore since shales may have variable net to gross 2168 _lith = self.faults.faulted_lithology[:].copy() 2169 _age = self.faults.faulted_age_volume[:].copy() 2170 fault_throw = self.faults.max_fault_throw[:] 2171 2172 for il, i in enumerate(labels_clean_list): 2173 fault_blocks_list = list(set(fault_throw[labels_clean == i].flatten())) 2174 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2175 for jl, j in enumerate(fault_blocks_list): 2176 print( 2177 "\n\n ... label, throw = ", 2178 i, 2179 j, 2180 list(set(fault_throw[labels_clean == i].flatten())), 2181 labels_clean[labels_clean == i].size, 2182 fault_throw[fault_throw == j].size, 2183 fault_throw[ 2184 np.where((labels_clean == i) & (fault_throw[:] == j)) 2185 ].size, 2186 ) 2187 single_closure = labels_clean * 0.0 2188 size = single_closure[ 2189 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2190 ].size 2191 if size >= self.cfg.closure_min_voxels: 2192 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2193 single_closure[ 2194 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2195 ] = 1 2196 if single_closure[single_closure > 0].size == 0: 2197 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2198 labels_clean[np.where(labels_clean == i)] = 0 2199 continue 2200 avg_ng = _ng[single_closure == 1].mean() 2201 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2202 _ng_voxels = _ng[single_closure == 1] 2203 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2204 min_geo_age = _geo_age_voxels.min() - 0.5 2205 avg_geo_age = int(_geo_age_voxels.mean()) 2206 max_geo_age = _geo_age_voxels.max() + 0.5 2207 _depth_geobody_voxels = depth_cube[single_closure == 1] 2208 min_depth = _depth_geobody_voxels.min() 2209 max_depth = _depth_geobody_voxels.max() 2210 avg_throw = np.median(fault_throw[single_closure == 1]) 2211 2212 closure_boundary_cube = closures * 0.0 2213 if grow_only_sand_closures: 2214 lith_flag = _lith > 0.0 2215 else: 2216 lith_flag = _lith >= 0.0 2217 closure_boundary_cube[ 2218 np.where( 2219 lith_flag 2220 & (_age > min_geo_age) 2221 & (_age < max_geo_age) 2222 & (fault_throw == avg_throw) 2223 & (depth_cube <= max_depth) 2224 ) 2225 ] = 1.0 2226 print( 2227 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2228 closure_boundary_cube[closure_boundary_cube == 1].size, 2229 ) 2230 2231 n_voxel = single_closure[single_closure == 1].size 2232 2233 original_voxels = n_voxel + 0 2234 print( 2235 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2236 i, 2237 j, 2238 n_voxel, 2239 (min_geo_age, avg_geo_age, max_geo_age), 2240 (min_depth, max_depth), 2241 avg_ng, 2242 il, 2243 " / ", 2244 len(labels_clean_list), 2245 ) 2246 2247 grown_closure = single_closure.copy() 2248 grown_closure[depth_cube >= max_depth] = 0 2249 delta_voxel = 0 2250 previous_delta_voxel = 1e9 2251 converged = False 2252 for ii in range(15): 2253 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2254 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2255 # stay within layer, within age, within fault block, above HCWC 2256 grown_closure[closure_boundary_cube == 0.0] = 0.0 2257 single_closure = single_closure + grown_closure 2258 single_closure[single_closure > 0] = i 2259 new_n_voxel = single_closure[single_closure > 0].size 2260 previous_delta_voxel = delta_voxel + 0 2261 delta_voxel = new_n_voxel - n_voxel 2262 print( 2263 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2264 " delta_voxel>previous_delta_voxel = ", 2265 i, 2266 ii, 2267 new_n_voxel, 2268 delta_voxel, 2269 previous_delta_voxel, 2270 delta_voxel > previous_delta_voxel, 2271 ) 2272 if n_voxel == new_n_voxel: 2273 # finish bottom voxel layer near "HCWC" 2274 grown_closure = self.grow_downward( 2275 grown_closure, 1, dist=1, verbose=False 2276 ) 2277 # stay within layer, within age, within fault block, above HCWC 2278 grown_closure[closure_boundary_cube == 0.0] = 0.0 2279 single_closure = single_closure + grown_closure 2280 single_closure[single_closure > 0] = i 2281 converged = True 2282 break 2283 else: 2284 n_voxel = new_n_voxel 2285 previous_delta_voxel = delta_voxel + 0 2286 if converged is True: 2287 labels_clean[single_closure > 0] = i 2288 msg_postscript = " converged" 2289 else: 2290 labels_clean[labels_clean == i] = -i 2291 msg_postscript = " NOT converged" 2292 msg = ( 2293 f"closure_id: {int(i):04d}, fault_id: {int(j + .5):04d}, " 2294 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2295 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2296 ) 2297 print(msg + msg_postscript) 2298 self.cfg.write_to_logfile(msg + msg_postscript) 2299 2300 # Set small closures to 0 after growth 2301 # _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2302 # for x in np.unique(_grown_labels): 2303 # size = _grown_labels[_grown_labels == x].size 2304 # print(f'Size before editing: {size}') 2305 # if size < self.cfg.closure_min_voxels: 2306 # labels_clean[_grown_labels == x] = 0. 2307 # for x in np.unique(labels_clean): 2308 # size = labels_clean[labels_clean == x].size 2309 # print(f'Size after editing: {size}') 2310 2311 if remove_small_closures: 2312 _initial_labels = measure.label( 2313 initial_closures, connectivity=2, background=0 2314 ) 2315 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2316 for x in np.unique(_grown_labels): 2317 size_initial = _initial_labels[_initial_labels == x].size 2318 size_grown = _grown_labels[_grown_labels == x].size 2319 print(f"Size before editing: {size_initial}") 2320 print(f"Size after editing: {size_grown}") 2321 if size_grown < self.cfg.closure_min_voxels: 2322 print( 2323 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2324 ) 2325 labels_clean[_grown_labels == x] = 0.0 2326 return labels_clean 2327 2328 def grow_to_salt(self, closures): 2329 # - grow closures laterally and up within layer up to salt body 2330 print("\n\n ... grow_to_salt: grow closures laterally and up within layer ...") 2331 self.cfg.write_to_logfile("growing closures to salt body: grow_to_salt") 2332 2333 labels_clean = measure.label( 2334 self.closure_segments[:], connectivity=2, background=0 2335 ) 2336 labels_clean[closures == 0] = 0 2337 # labels_clean = self.closure_segments[:].copy() 2338 # labels_clean[closures == 0] = 0 2339 labels_clean_list = list(set(labels_clean.flatten())) 2340 labels_clean_list.remove(0) 2341 initial_closures = labels_clean.copy() 2342 print("\n ... grow_to_salt: n_salt_closures = ", len(labels_clean_list)) 2343 print(" ... grow_to_salt: salt_closures = ", labels_clean_list) 2344 2345 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2346 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2347 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2348 _ng = self.faults.faulted_net_to_gross[:].copy() 2349 _age = self.faults.faulted_age_volume[:].copy() 2350 salt = self.faults.salt_model.salt_segments[:] 2351 2352 for il, i in enumerate(labels_clean_list): 2353 salt_list = list(set(salt[labels_clean == i].flatten())) 2354 print(" ... grow_to_fault2: salt_list = ", salt_list) 2355 single_closure = labels_clean * 0.0 2356 size = single_closure[np.where(labels_clean == i)].size 2357 if size >= self.cfg.closure_min_voxels: 2358 print(f"Label: {i}, Voxel_Count: {size}") 2359 single_closure[np.where(labels_clean == i)] = 1 2360 if single_closure[single_closure > 0].size == 0: 2361 labels_clean[np.where(labels_clean == i)] = 0 2362 continue 2363 avg_ng = _ng[single_closure == 1].mean() 2364 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2365 _ng_voxels = _ng[single_closure == 1] 2366 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2367 min_geo_age = _geo_age_voxels.min() - 0.5 2368 avg_geo_age = int(_geo_age_voxels.mean()) 2369 max_geo_age = _geo_age_voxels.max() + 0.5 2370 _depth_geobody_voxels = depth_cube[single_closure == 1] 2371 min_depth = _depth_geobody_voxels.min() 2372 max_depth = _depth_geobody_voxels.max() 2373 # Define AOI where salt has been dilated 2374 # close_to_salt = np.zeros_like(salt) 2375 # close_to_salt[self.wide_salt[:] == 1] = 1.0 2376 # close_to_salt[salt == 1] = 0.0 2377 2378 closure_boundary_cube = closures * 0.0 2379 closure_boundary_cube[ 2380 np.where( 2381 (_ng > 0.3) 2382 & (_age > min_geo_age) # account for partial voxels 2383 & (_age < max_geo_age) 2384 & (salt == 0.0) 2385 & (depth_cube <= max_depth) 2386 ) 2387 ] = 1.0 2388 print( 2389 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2390 closure_boundary_cube[closure_boundary_cube == 1].size, 2391 ) 2392 2393 n_voxel = single_closure[single_closure == 1].size 2394 2395 original_voxels = n_voxel + 0 2396 print( 2397 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2398 i, 2399 n_voxel, 2400 (min_geo_age, avg_geo_age, max_geo_age), 2401 (min_depth, max_depth), 2402 avg_ng, 2403 il, 2404 " / ", 2405 len(labels_clean_list), 2406 ) 2407 2408 grown_closure = single_closure.copy() 2409 grown_closure[depth_cube >= max_depth] = 0 2410 delta_voxel = 0 2411 previous_delta_voxel = 1e9 2412 converged = False 2413 for ii in range(99): 2414 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2415 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2416 # stay within layer, within age, close to salt and above HCWC 2417 grown_closure[closure_boundary_cube == 0.0] = 0.0 2418 single_closure = single_closure + grown_closure 2419 single_closure[single_closure > 0] = i 2420 new_n_voxel = single_closure[single_closure > 0].size 2421 previous_delta_voxel = delta_voxel + 0 2422 delta_voxel = new_n_voxel - n_voxel 2423 print( 2424 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2425 " delta_voxel>previous_delta_voxel = ", 2426 i, 2427 ii, 2428 new_n_voxel, 2429 delta_voxel, 2430 previous_delta_voxel, 2431 delta_voxel > previous_delta_voxel, 2432 ) 2433 2434 # If grown voxel is touching the egde of survey, stop and remove closure 2435 _a, _b, _ = np.where(single_closure > 0) 2436 max_boundary_i = self.cfg.cube_shape[0] - 1 2437 max_boundary_j = self.cfg.cube_shape[1] - 1 2438 if ( 2439 np.min(_a) == 0 2440 or np.max(_a) == max_boundary_i 2441 or np.min(_b) == 0 2442 or np.max(_b) == max_boundary_j 2443 ): 2444 print("Boundary reached, removing closure") 2445 converged = False 2446 break 2447 2448 if n_voxel == new_n_voxel: 2449 # finish bottom voxel layer near HCWC 2450 grown_closure = self.grow_downward( 2451 grown_closure, 1, dist=1, verbose=False 2452 ) 2453 # stay within layer, within age, within fault block, above HCWC 2454 grown_closure[closure_boundary_cube == 0.0] = 0.0 2455 single_closure = single_closure + grown_closure 2456 single_closure[single_closure > 0] = i 2457 converged = True 2458 break 2459 else: 2460 n_voxel = new_n_voxel 2461 previous_delta_voxel = delta_voxel + 0 2462 if converged is True: 2463 labels_clean[single_closure > 0] = i 2464 msg_postscript = " converged" 2465 else: 2466 labels_clean[labels_clean == i] = -i 2467 msg_postscript = " NOT converged" 2468 msg = ( 2469 f"closure_id: {int(i):04d}, " 2470 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2471 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2472 ) 2473 print(msg + msg_postscript) 2474 self.cfg.write_to_logfile(msg + msg_postscript) 2475 2476 # Set small closures to 0 after growth 2477 _initial_labels = measure.label(initial_closures, connectivity=2, background=0) 2478 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2479 for x in np.unique(_grown_labels)[ 2480 1: 2481 ]: # ignore the first label of 0 (closures only) 2482 size_initial = _initial_labels[_initial_labels == x].size 2483 size_grown = _grown_labels[_grown_labels == x].size 2484 print(f"Size before editing: {size_initial}") 2485 print(f"Size after editing: {size_grown}") 2486 if size_grown < self.cfg.closure_min_voxels: 2487 print( 2488 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2489 ) 2490 labels_clean[_grown_labels == x] = 0.0 2491 2492 return labels_clean 2493 2494 @staticmethod 2495 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2496 from scipy.ndimage.morphology import grey_dilation 2497 2498 dist_size = 2 * dist + 1 2499 mask = np.zeros((dist_size, dist_size, 1)) 2500 mask[:, :, :] = 1 2501 _geobody = geobody.copy() 2502 if verbose: 2503 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2504 for k in range(iterations): 2505 try: 2506 _geobody = grey_dilation(_geobody, footprint=mask) 2507 if verbose: 2508 print( 2509 " ...grow_lateral: k, _geobody.shape = ", 2510 k, 2511 _geobody[_geobody > 0].shape, 2512 ) 2513 except: 2514 break 2515 return _geobody 2516 2517 @staticmethod 2518 def grow_upward(geobody, iterations, dist=1, verbose=False): 2519 from scipy.ndimage.morphology import grey_dilation 2520 2521 dist_size = 2 * dist + 1 2522 mask = np.zeros((1, 1, dist_size)) 2523 mask[0, 0, : dist + 1] = 1 2524 _geobody = geobody.copy() 2525 if verbose: 2526 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2527 for k in range(iterations): 2528 try: 2529 _geobody = grey_dilation(_geobody, footprint=mask) 2530 if verbose: 2531 print( 2532 " ...grow_upward: k, _geobody.shape = ", 2533 k, 2534 _geobody[_geobody > 0].shape, 2535 ) 2536 except: 2537 break 2538 return _geobody 2539 2540 @staticmethod 2541 def grow_downward(geobody, iterations, dist=1, verbose=False): 2542 from scipy.ndimage.morphology import grey_dilation 2543 2544 dist_size = 2 * dist + 1 2545 mask = np.zeros((1, 1, dist_size)) 2546 mask[0, 0, dist:] = 1 2547 _geobody = geobody.copy() 2548 if verbose: 2549 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2550 for k in range(iterations): 2551 try: 2552 _geobody = grey_dilation(_geobody, footprint=mask) 2553 if verbose: 2554 print( 2555 " ...grow_downward: k, _geobody.shape = ", 2556 k, 2557 _geobody[_geobody > 0].shape, 2558 ) 2559 except: 2560 break 2561 return _geobody 2562 2563 @staticmethod 2564 def _threshold_volumes(volume, threshold=0.5): 2565 volume[volume >= threshold] = 1.0 2566 volume[volume < threshold] = 0.0 2567 return volume 2568 2569 def parse_closure_codes(self, hc_closure_codes, labels, num, code=0.1): 2570 labels = labels.astype("float32") 2571 if num > 0: 2572 for x in range(1, num + 1): 2573 y = code + labels[labels == x].size 2574 labels[labels == x] = y 2575 hc_closure_codes += labels 2576 return hc_closure_codes
Geomodel
The class of the Geomodel object.
This class contains all the items that make up the Geologic model.
Parameters
- parameters (datagenerator.Parameters): Parameter object storing all model parameters.
- depth_maps (np.ndarray): A numpy array containing the depth maps.
- onlap_horizon_list (list): A list of the onlap horizons.
- facies (np.ndarray): The generated facies.
Returns
- None
13 def __init__(self, parameters, faults, facies, onlap_horizon_list): 14 self.closure_dict = dict() 15 self.cfg = parameters 16 self.faults = faults 17 self.facies = facies 18 self.onlap_list = onlap_horizon_list 19 self.top_lith_facies = None 20 self.closure_vol_shape = self.faults.faulted_age_volume.shape 21 self.closure_segments = self.cfg.hdf_init( 22 "closure_segments", shape=self.closure_vol_shape 23 ) 24 self.oil_closures = self.cfg.hdf_init( 25 "oil_closures", shape=self.closure_vol_shape, dtype="uint8" 26 ) 27 self.gas_closures = self.cfg.hdf_init( 28 "gas_closures", shape=self.closure_vol_shape, dtype="uint8" 29 ) 30 self.brine_closures = self.cfg.hdf_init( 31 "brine_closures", shape=self.closure_vol_shape, dtype="uint8" 32 ) 33 self.simple_closures = self.cfg.hdf_init( 34 "simple_closures", shape=self.closure_vol_shape, dtype="uint8" 35 ) 36 self.strat_closures = self.cfg.hdf_init( 37 "strat_closures", shape=self.closure_vol_shape, dtype="uint8" 38 ) 39 self.fault_closures = self.cfg.hdf_init( 40 "fault_closures", shape=self.closure_vol_shape, dtype="uint8" 41 ) 42 self.hc_labels = self.cfg.hdf_init( 43 "hc_labels", shape=self.closure_vol_shape, dtype="uint8" 44 ) 45 46 self.all_closure_segments = self.cfg.hdf_init( 47 "all_closure_segments", shape=self.closure_vol_shape 48 ) 49 50 # Class attributes added from Intersect3D 51 self.wide_faults = self.cfg.hdf_init( 52 "wide_faults", shape=self.closure_vol_shape 53 ) 54 self.fat_faults = self.cfg.hdf_init("fat_faults", shape=self.closure_vol_shape) 55 self.onlaps_upward = self.cfg.hdf_init( 56 "onlaps_upward", shape=self.closure_vol_shape 57 ) 58 self.onlaps_downward = self.cfg.hdf_init( 59 "onlaps_downward", shape=self.closure_vol_shape 60 ) 61 62 # Faulted closures 63 self.faulted_closures_oil = self.cfg.hdf_init( 64 "faulted_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 65 ) 66 self.faulted_closures_gas = self.cfg.hdf_init( 67 "faulted_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 68 ) 69 self.faulted_closures_brine = self.cfg.hdf_init( 70 "faulted_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 71 ) 72 self.fault_closures_oil_segment_list = list() 73 self.fault_closures_gas_segment_list = list() 74 self.fault_closures_brine_segment_list = list() 75 self.n_fault_closures_oil = 0 76 self.n_fault_closures_gas = 0 77 self.n_fault_closures_brine = 0 78 79 self.faulted_all_closures = self.cfg.hdf_init( 80 "faulted_all_closures", shape=self.closure_vol_shape, dtype="uint8" 81 ) 82 self.fault_all_closures_segment_list = list() 83 self.n_fault_all_closures = 0 84 85 # Onlap closures 86 self.onlap_closures_oil = self.cfg.hdf_init( 87 "onlap_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 88 ) 89 self.onlap_closures_gas = self.cfg.hdf_init( 90 "onlap_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 91 ) 92 self.onlap_closures_brine = self.cfg.hdf_init( 93 "onlap_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 94 ) 95 self.onlap_closures_oil_segment_list = list() 96 self.onlap_closures_gas_segment_list = list() 97 self.onlap_closures_brine_segment_list = list() 98 self.n_onlap_closures_oil = 0 99 self.n_onlap_closures_gas = 0 100 self.n_onlap_closures_brine = 0 101 102 self.onlap_all_closures = self.cfg.hdf_init( 103 "onlap_all_closures", shape=self.closure_vol_shape, dtype="uint8" 104 ) 105 self.onlap_all_closures_segment_list = list() 106 self.n_onlap_all_closures_oil = 0 107 108 # Simple closures 109 self.simple_closures_oil = self.cfg.hdf_init( 110 "simple_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 111 ) 112 self.simple_closures_gas = self.cfg.hdf_init( 113 "simple_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 114 ) 115 self.simple_closures_brine = self.cfg.hdf_init( 116 "simple_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 117 ) 118 self.simple_closures_oil_segment_list = list() 119 self.simple_closures_gas_segment_list = list() 120 self.simple_closures_brine_segment_list = list() 121 self.n_4way_closures_oil = 0 122 self.n_4way_closures_gas = 0 123 self.n_4way_closures_brine = 0 124 125 self.simple_all_closures = self.cfg.hdf_init( 126 "simple_all_closures", shape=self.closure_vol_shape, dtype="uint8" 127 ) 128 self.simple_all_closures_segment_list = list() 129 self.n_4way_all_closures = 0 130 131 # False closures 132 self.false_closures_oil = self.cfg.hdf_init( 133 "false_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 134 ) 135 self.false_closures_gas = self.cfg.hdf_init( 136 "false_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 137 ) 138 self.false_closures_brine = self.cfg.hdf_init( 139 "false_closures_brine", shape=self.closure_vol_shape, dtype="uint8" 140 ) 141 self.n_false_closures_oil = 0 142 self.n_false_closures_gas = 0 143 self.n_false_closures_brine = 0 144 145 self.false_all_closures = self.cfg.hdf_init( 146 "false_all_closures", shape=self.closure_vol_shape, dtype="uint8" 147 ) 148 self.n_false_all_closures = 0 149 150 if self.cfg.include_salt: 151 self.salt_closures = self.cfg.hdf_init( 152 "salt_closures", shape=self.closure_vol_shape, dtype="uint8" 153 ) 154 self.wide_salt = self.cfg.hdf_init( 155 "wide_salt", shape=self.closure_vol_shape 156 ) 157 self.salt_closures_oil = self.cfg.hdf_init( 158 "salt_bounded_closures_oil", shape=self.closure_vol_shape, dtype="uint8" 159 ) 160 self.salt_closures_gas = self.cfg.hdf_init( 161 "salt_bounded_closures_gas", shape=self.closure_vol_shape, dtype="uint8" 162 ) 163 self.salt_closures_brine = self.cfg.hdf_init( 164 "salt_bounded_closures_brine", 165 shape=self.closure_vol_shape, 166 dtype="uint8", 167 ) 168 self.salt_closures_oil_segment_list = list() 169 self.salt_closures_gas_segment_list = list() 170 self.salt_closures_brine_segment_list = list() 171 self.n_salt_closures_oil = 0 172 self.n_salt_closures_gas = 0 173 self.n_salt_closures_brine = 0 174 175 self.salt_all_closures = self.cfg.hdf_init( 176 "salt_bounded_all_closures", shape=self.closure_vol_shape, dtype="uint8" 177 ) 178 self.salt_all_closures_segment_list = list() 179 self.n_salt_all_closures = 0
__init__
Initializer for the Geomodel class.
Parameters
- parameters (datagenerator.Parameters): Parameter object storing all model parameters.
- depth_maps (np.ndarray): A numpy array containing the depth maps.
- onlap_horizon_list (list): A list of the onlap horizons.
- facies (np.ndarray): The generated facies.
181 def create_closure_labels_from_depth_maps( 182 self, depth_maps, depth_maps_infilled, max_col_height 183 ): 184 if self.cfg.verbose: 185 print("\n\t... inside insertClosureLabels3D ") 186 print( 187 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 188 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 189 ) 190 191 # create 3D cube to hold segmentation results 192 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 193 194 # create grids with grid indices 195 ii, jj = self.build_meshgrid() 196 197 # loop through horizons in 'depth_maps' 198 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 199 layers_with_closure = 0 200 201 avg_sand_thickness = list() 202 avg_shale_thickness = list() 203 avg_unit_thickness = list() 204 for ihorizon in range(depth_maps.shape[2] - 1): 205 avg_unit_thickness.append( 206 np.mean( 207 depth_maps_infilled[..., ihorizon + 1] 208 - depth_maps_infilled[..., ihorizon] 209 ) 210 ) 211 212 if self.top_lith_facies[ihorizon] > 0: 213 # If facies is not shale, calculate a closure map for the layer 214 if self.cfg.verbose: 215 print( 216 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 217 ) 218 avg_sand_thickness.append( 219 np.mean( 220 depth_maps_infilled[..., ihorizon + 1] 221 - depth_maps_infilled[..., ihorizon] 222 ) 223 ) 224 # compute a closure map 225 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 226 # - use thicknesses converted to samples instead of ft or ms 227 # - assumes that fault intersections are inserted in input map with value of 0. 228 # - assumes that input map values represent depth (i.e., bigger values are deeper) 229 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 230 top_structure_depth_map[ 231 np.isnan(top_structure_depth_map) 232 ] = 0.0 # replace nans with 0. 233 top_structure_depth_map /= float(self.cfg.digi) 234 if self.cfg.partial_voxels: 235 top_structure_depth_map -= ( 236 1.0 # account for voxels partially in layer 237 ) 238 base_structure_depth_map = depth_maps_infilled[ 239 :, :, ihorizon + 1 240 ].copy() 241 base_structure_depth_map[ 242 np.isnan(top_structure_depth_map) 243 ] = 0.0 # replace nans with 0. 244 base_structure_depth_map /= float(self.cfg.digi) 245 print( 246 " ...inside create_closure_labels_from_depth_maps... ihorizon, self.top_lith_facies[ihorizon] = ", 247 ihorizon, 248 self.top_lith_facies[ihorizon], 249 ) 250 # if there is non-zero thickness between top/base closure 251 if top_structure_depth_map.min() != top_structure_depth_map.max(): 252 max_column = max_col_height[ihorizon] / self.cfg.digi 253 if self.cfg.verbose: 254 print( 255 f" ...avg depth for layer {ihorizon}.", 256 top_structure_depth_map.mean(), 257 ) 258 if self.cfg.verbose: 259 print( 260 f" ...maximum column height for layer {ihorizon}.", 261 max_column, 262 ) 263 264 if ihorizon == 27000 or ihorizon == 1000: 265 closure_depth_map = _flood_fill( 266 top_structure_depth_map, 267 max_column_height=max_column, 268 verbose=True, 269 debug=True, 270 ) 271 else: 272 closure_depth_map = _flood_fill( 273 top_structure_depth_map, max_column_height=max_column 274 ) 275 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 276 closure_depth_map == 0 277 ] 278 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 279 closure_depth_map == 1 280 ] 281 closure_depth_map[ 282 closure_depth_map == 1e5 283 ] = top_structure_depth_map[closure_depth_map == 1e5] 284 # Select the maximum value between the top sand map and the flood-filled closure map 285 closure_depth_map = np.max( 286 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 287 ) 288 closure_depth_map = np.min( 289 np.dstack((closure_depth_map, base_structure_depth_map)), 290 axis=-1, 291 ) 292 if self.cfg.verbose: 293 print( 294 f"\n ... layer {ihorizon}," 295 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 296 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 297 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 298 ) 299 closure_thickness = closure_depth_map - top_structure_depth_map 300 closure_thickness_no_nan = closure_thickness[ 301 ~np.isnan(closure_thickness) 302 ] 303 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 304 if self.cfg.verbose: 305 print(f" ... layer {ihorizon}, max_closure {max_closure}") 306 307 # locate 3D zone in closure after checking that closures exist for this horizon 308 # if False in (top_structure_depth_map == closure_depth_map): 309 if max_closure > 0: 310 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 311 # put label in cube between top_structure_depth_map and closure_depth_map 312 top_structure_depth_map_integer = top_structure_depth_map 313 closure_depth_map_integer = closure_depth_map 314 315 if self.cfg.verbose: 316 closure_map_min = closure_depth_map_integer[ 317 closure_depth_map_integer > 0.1 318 ].min() 319 closure_map_max = closure_depth_map_integer[ 320 closure_depth_map_integer > 0.1 321 ].max() 322 print( 323 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 324 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 325 f" closure map min, max: {closure_map_min}, {closure_map_max}" 326 ) 327 328 slices_with_substitution = 0 329 print(" ... max_closure: {}".format(max_closure)) 330 for k in range( 331 max_closure + 1 332 ): # add one more sample than seemingly needed for round-off 333 # Subtract 2 from the closure cube shape since adding one later 334 horizon_slice = (k + top_structure_depth_map).clip( 335 0, closure_segments.shape[2] - 2 336 ) 337 sublayer_kk = horizon_slice[ 338 horizon_slice < closure_depth_map.astype("int") 339 ] 340 sublayer_ii = ii[ 341 horizon_slice < closure_depth_map.astype("int") 342 ] 343 sublayer_jj = jj[ 344 horizon_slice < closure_depth_map.astype("int") 345 ] 346 347 if sublayer_ii.size > 0: 348 slices_with_substitution += 1 349 350 i_indices = sublayer_ii 351 j_indices = sublayer_jj 352 k_indices = sublayer_kk + 1 353 354 try: 355 closure_segments[ 356 i_indices, j_indices, k_indices.astype("int") 357 ] += 1.0 358 voxel_change_count[ 359 i_indices, j_indices, k_indices.astype("int") 360 ] += 1 361 except IndexError: 362 print("\nIndex is out of bounds.") 363 print(f"\tclosure_segments: {closure_segments}") 364 print(f"\tvoxel_change_count: {voxel_change_count}") 365 print(f"\ti_indices: {i_indices}") 366 print(f"\tj_indices: {j_indices}") 367 print(f"\tk_indices: {k_indices.astype('int')}") 368 pass 369 370 if slices_with_substitution > 0: 371 layers_with_closure += 1 372 373 if self.cfg.verbose: 374 print( 375 " ... finished putting closures in closures_segments for layer ...", 376 ihorizon, 377 ) 378 379 else: 380 continue 381 else: 382 # Calculate shale unit thicknesses 383 avg_shale_thickness.append( 384 np.mean( 385 depth_maps_infilled[..., ihorizon + 1] 386 - depth_maps_infilled[..., ihorizon] 387 ) 388 ) 389 390 if len(avg_sand_thickness) == 0: 391 avg_sand_thickness = 0 392 self.cfg.write_to_logfile( 393 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 394 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 395 f"max: {np.max(avg_sand_thickness):.2f}" 396 ) 397 self.cfg.write_to_logfile( 398 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 399 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 400 f"max: {np.max(avg_shale_thickness):.2f}" 401 ) 402 self.cfg.write_to_logfile( 403 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 404 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 405 f"max: {np.max(avg_unit_thickness):.2f}" 406 ) 407 self.cfg.write_to_logfile( 408 msg=None, 409 mainkey="model_parameters", 410 subkey="sand_unit_thickness_combined_mean", 411 val=np.mean(avg_sand_thickness), 412 ) 413 self.cfg.write_to_logfile( 414 msg=None, 415 mainkey="model_parameters", 416 subkey="sand_unit_thickness_combined_std", 417 val=np.std(avg_sand_thickness), 418 ) 419 self.cfg.write_to_logfile( 420 msg=None, 421 mainkey="model_parameters", 422 subkey="sand_unit_thickness_combined_min", 423 val=np.min(avg_sand_thickness), 424 ) 425 self.cfg.write_to_logfile( 426 msg=None, 427 mainkey="model_parameters", 428 subkey="sand_unit_thickness_combined_max", 429 val=np.max(avg_sand_thickness), 430 ) 431 # 432 self.cfg.write_to_logfile( 433 msg=None, 434 mainkey="model_parameters", 435 subkey="shale_unit_thickness_combined_mean", 436 val=np.mean(avg_shale_thickness), 437 ) 438 self.cfg.write_to_logfile( 439 msg=None, 440 mainkey="model_parameters", 441 subkey="shale_unit_thickness_combined_std", 442 val=np.std(avg_shale_thickness), 443 ) 444 self.cfg.write_to_logfile( 445 msg=None, 446 mainkey="model_parameters", 447 subkey="shale_unit_thickness_combined_min", 448 val=np.min(avg_shale_thickness), 449 ) 450 self.cfg.write_to_logfile( 451 msg=None, 452 mainkey="model_parameters", 453 subkey="shale_unit_thickness_combined_max", 454 val=np.max(avg_shale_thickness), 455 ) 456 457 self.cfg.write_to_logfile( 458 msg=None, 459 mainkey="model_parameters", 460 subkey="overall_unit_thickness_combined_mean", 461 val=np.mean(avg_unit_thickness), 462 ) 463 self.cfg.write_to_logfile( 464 msg=None, 465 mainkey="model_parameters", 466 subkey="overall_unit_thickness_combined_std", 467 val=np.std(avg_unit_thickness), 468 ) 469 self.cfg.write_to_logfile( 470 msg=None, 471 mainkey="model_parameters", 472 subkey="overall_unit_thickness_combined_min", 473 val=np.min(avg_unit_thickness), 474 ) 475 self.cfg.write_to_logfile( 476 msg=None, 477 mainkey="model_parameters", 478 subkey="overall_unit_thickness_combined_max", 479 val=np.max(avg_unit_thickness), 480 ) 481 482 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 483 pct_non_zero = float(non_zero_pixels) / ( 484 closure_segments.shape[0] 485 * closure_segments.shape[1] 486 * closure_segments.shape[2] 487 ) 488 if self.cfg.verbose: 489 print( 490 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 491 closure_segments.min(), 492 closure_segments.mean(), 493 closure_segments.max(), 494 pct_non_zero, 495 ) 496 ) 497 498 print(f"\t... layers_with_closure {layers_with_closure}") 499 print("\t... finished putting closures in closure_segments ...\n") 500 501 if self.cfg.verbose: 502 print( 503 f"\n ...closure segments created. min: {closure_segments.min()}, " 504 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 505 f" voxel count: {closure_segments[closure_segments != 0].shape}" 506 ) 507 508 return closure_segments
510 def create_closure_labels_from_all_depth_maps( 511 self, depth_maps, depth_maps_infilled, max_col_height 512 ): 513 if self.cfg.verbose: 514 print("\n\t... inside insertClosureLabels3D ") 515 print( 516 f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f}," 517 f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}" 518 ) 519 520 # create 3D cube to hold segmentation results 521 closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32") 522 523 # create grids with grid indices 524 ii, jj = self.build_meshgrid() 525 526 # loop through horizons in 'depth_maps' 527 voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8) 528 layers_with_closure = 0 529 530 avg_sand_thickness = list() 531 avg_shale_thickness = list() 532 avg_unit_thickness = list() 533 for ihorizon in range(depth_maps.shape[2] - 1): 534 avg_unit_thickness.append( 535 np.mean( 536 depth_maps_infilled[..., ihorizon + 1] 537 - depth_maps_infilled[..., ihorizon] 538 ) 539 ) 540 # calculate a closure map for the layer 541 if self.cfg.verbose: 542 print( 543 f"\n...closure voxels computation for layer {ihorizon} in horizon list." 544 ) 545 546 # compute a closure map 547 # - identical to top structure map when not in closure, 'max flooding' depth when in closure 548 # - use thicknesses converted to samples instead of ft or ms 549 # - assumes that fault intersections are inserted in input map with value of 0. 550 # - assumes that input map values represent depth (i.e., bigger values are deeper) 551 top_structure_depth_map = depth_maps[:, :, ihorizon].copy() 552 top_structure_depth_map[ 553 np.isnan(top_structure_depth_map) 554 ] = 0.0 # replace nans with 0. 555 top_structure_depth_map /= float(self.cfg.digi) 556 if self.cfg.partial_voxels: 557 top_structure_depth_map -= 1.0 # account for voxels partially in layer 558 base_structure_depth_map = depth_maps_infilled[:, :, ihorizon + 1].copy() 559 base_structure_depth_map[ 560 np.isnan(top_structure_depth_map) 561 ] = 0.0 # replace nans with 0. 562 base_structure_depth_map /= float(self.cfg.digi) 563 print( 564 " ...inside create_closure_labels_from_depth_maps... ihorizon = ", 565 ihorizon, 566 ) 567 # if there is non-zero thickness between top/base closure 568 if top_structure_depth_map.min() != top_structure_depth_map.max(): 569 max_column = max_col_height[ihorizon] / self.cfg.digi 570 if self.cfg.verbose: 571 print( 572 f" ...avg depth for layer {ihorizon}.", 573 top_structure_depth_map.mean(), 574 ) 575 if self.cfg.verbose: 576 print( 577 f" ...maximum column height for layer {ihorizon}.", max_column 578 ) 579 580 if ihorizon == 27000 or ihorizon == 1000: 581 closure_depth_map = _flood_fill( 582 top_structure_depth_map, 583 max_column_height=max_column, 584 verbose=True, 585 debug=True, 586 ) 587 else: 588 closure_depth_map = _flood_fill( 589 top_structure_depth_map, max_column_height=max_column 590 ) 591 closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[ 592 closure_depth_map == 0 593 ] 594 closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[ 595 closure_depth_map == 1 596 ] 597 closure_depth_map[closure_depth_map == 1e5] = top_structure_depth_map[ 598 closure_depth_map == 1e5 599 ] 600 # Select the maximum value between the top sand map and the flood-filled closure map 601 closure_depth_map = np.max( 602 np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1 603 ) 604 closure_depth_map = np.min( 605 np.dstack((closure_depth_map, base_structure_depth_map)), axis=-1 606 ) 607 if self.cfg.verbose: 608 print( 609 f"\n ... layer {ihorizon}," 610 f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f}," 611 f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max" 612 f" {closure_depth_map.min():.2f} {closure_depth_map.max()}" 613 ) 614 closure_thickness = closure_depth_map - top_structure_depth_map 615 closure_thickness_no_nan = closure_thickness[ 616 ~np.isnan(closure_thickness) 617 ] 618 max_closure = int(np.around(closure_thickness_no_nan.max(), 0)) 619 if self.cfg.verbose: 620 print(f" ... layer {ihorizon}, max_closure {max_closure}") 621 622 # locate 3D zone in closure after checking that closures exist for this horizon 623 # if False in (top_structure_depth_map == closure_depth_map): 624 if max_closure > 0: 625 # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map 626 # put label in cube between top_structure_depth_map and closure_depth_map 627 top_structure_depth_map_integer = top_structure_depth_map 628 closure_depth_map_integer = closure_depth_map 629 630 if self.cfg.verbose: 631 closure_map_min = closure_depth_map_integer[ 632 closure_depth_map_integer > 0.1 633 ].min() 634 closure_map_max = closure_depth_map_integer[ 635 closure_depth_map_integer > 0.1 636 ].max() 637 print( 638 f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, " 639 f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()}," 640 f" closure map min, max: {closure_map_min}, {closure_map_max}" 641 ) 642 643 slices_with_substitution = 0 644 print(" ... max_closure: {}".format(max_closure)) 645 for k in range( 646 max_closure + 1 647 ): # add one more sample than seemingly needed for round-off 648 # Subtract 2 from the closure cube shape since adding one later 649 horizon_slice = (k + top_structure_depth_map).clip( 650 0, closure_segments.shape[2] - 2 651 ) 652 sublayer_kk = horizon_slice[ 653 horizon_slice < closure_depth_map.astype("int") 654 ] 655 sublayer_ii = ii[ 656 horizon_slice < closure_depth_map.astype("int") 657 ] 658 sublayer_jj = jj[ 659 horizon_slice < closure_depth_map.astype("int") 660 ] 661 662 if sublayer_ii.size > 0: 663 slices_with_substitution += 1 664 665 i_indices = sublayer_ii 666 j_indices = sublayer_jj 667 k_indices = sublayer_kk + 1 668 669 try: 670 closure_segments[ 671 i_indices, j_indices, k_indices.astype("int") 672 ] += 1.0 673 voxel_change_count[ 674 i_indices, j_indices, k_indices.astype("int") 675 ] += 1 676 except IndexError: 677 print("\nIndex is out of bounds.") 678 print(f"\tclosure_segments: {closure_segments}") 679 print(f"\tvoxel_change_count: {voxel_change_count}") 680 print(f"\ti_indices: {i_indices}") 681 print(f"\tj_indices: {j_indices}") 682 print(f"\tk_indices: {k_indices.astype('int')}") 683 pass 684 685 if slices_with_substitution > 0: 686 layers_with_closure += 1 687 688 if self.cfg.verbose: 689 print( 690 " ... finished putting closures in closures_segments for layer ...", 691 ihorizon, 692 ) 693 694 else: 695 continue 696 697 if self.facies[ihorizon] == 1: 698 avg_sand_thickness.append( 699 np.mean( 700 depth_maps_infilled[..., ihorizon + 1] 701 - depth_maps_infilled[..., ihorizon] 702 ) 703 ) 704 elif self.facies[ihorizon] == 0: 705 # Calculate shale unit thicknesses 706 avg_shale_thickness.append( 707 np.mean( 708 depth_maps_infilled[..., ihorizon + 1] 709 - depth_maps_infilled[..., ihorizon] 710 ) 711 ) 712 713 # TODO handle case where avg_sand_thickness is zero-size array 714 try: 715 self.cfg.write_to_logfile( 716 f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, " 717 f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, " 718 f"max: {np.max(avg_sand_thickness):.2f}" 719 ) 720 except: 721 print("No sands in model") 722 self.cfg.write_to_logfile( 723 f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, " 724 f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, " 725 f"max: {np.max(avg_shale_thickness):.2f}" 726 ) 727 self.cfg.write_to_logfile( 728 f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, " 729 f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, " 730 f"max: {np.max(avg_unit_thickness):.2f}" 731 ) 732 733 self.cfg.write_to_logfile( 734 msg=None, 735 mainkey="model_parameters", 736 subkey="sand_unit_thickness_mean", 737 val=np.mean(avg_sand_thickness), 738 ) 739 self.cfg.write_to_logfile( 740 msg=None, 741 mainkey="model_parameters", 742 subkey="sand_unit_thickness_std", 743 val=np.std(avg_sand_thickness), 744 ) 745 self.cfg.write_to_logfile( 746 msg=None, 747 mainkey="model_parameters", 748 subkey="sand_unit_thickness_min", 749 val=np.min(avg_sand_thickness), 750 ) 751 self.cfg.write_to_logfile( 752 msg=None, 753 mainkey="model_parameters", 754 subkey="sand_unit_thickness_max", 755 val=np.max(avg_sand_thickness), 756 ) 757 # 758 self.cfg.write_to_logfile( 759 msg=None, 760 mainkey="model_parameters", 761 subkey="shale_unit_thickness_mean", 762 val=np.mean(avg_shale_thickness), 763 ) 764 self.cfg.write_to_logfile( 765 msg=None, 766 mainkey="model_parameters", 767 subkey="shale_unit_thickness_std", 768 val=np.std(avg_shale_thickness), 769 ) 770 self.cfg.write_to_logfile( 771 msg=None, 772 mainkey="model_parameters", 773 subkey="shale_unit_thickness_min", 774 val=np.min(avg_shale_thickness), 775 ) 776 self.cfg.write_to_logfile( 777 msg=None, 778 mainkey="model_parameters", 779 subkey="shale_unit_thickness_max", 780 val=np.max(avg_shale_thickness), 781 ) 782 783 self.cfg.write_to_logfile( 784 msg=None, 785 mainkey="model_parameters", 786 subkey="overall_unit_thickness_mean", 787 val=np.mean(avg_unit_thickness), 788 ) 789 self.cfg.write_to_logfile( 790 msg=None, 791 mainkey="model_parameters", 792 subkey="overall_unit_thickness_std", 793 val=np.std(avg_unit_thickness), 794 ) 795 self.cfg.write_to_logfile( 796 msg=None, 797 mainkey="model_parameters", 798 subkey="overall_unit_thickness_min", 799 val=np.min(avg_unit_thickness), 800 ) 801 self.cfg.write_to_logfile( 802 msg=None, 803 mainkey="model_parameters", 804 subkey="overall_unit_thickness_max", 805 val=np.max(avg_unit_thickness), 806 ) 807 808 non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0] 809 pct_non_zero = float(non_zero_pixels) / ( 810 closure_segments.shape[0] 811 * closure_segments.shape[1] 812 * closure_segments.shape[2] 813 ) 814 if self.cfg.verbose: 815 print( 816 " ...closure_segments min {}, mean {}, max {}, % non-zero {}".format( 817 closure_segments.min(), 818 closure_segments.mean(), 819 closure_segments.max(), 820 pct_non_zero, 821 ) 822 ) 823 824 print(f"\t... layers_with_closure {layers_with_closure}") 825 print("\t... finished putting closures in closure_segments ...\n") 826 827 if self.cfg.verbose: 828 print( 829 f"\n ...closure segments created. min: {closure_segments.min()}, " 830 f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}" 831 f" voxel count: {closure_segments[closure_segments != 0].shape}" 832 ) 833 834 return closure_segments
836 def find_top_lith_horizons(self): 837 """ 838 Find horizons which are the top of layers where the lithology changes 839 840 Combine layers of the same lithology and retain the top of these new layers for closure calculations. 841 """ 842 top_lith_indices = list(np.array(self.onlap_list) - 1) 843 for i, _ in enumerate(self.facies[:-1]): 844 if i == 0: 845 continue 846 print( 847 f"i: {i}, sand_layer_label[i-1]: {self.facies[i - 1]}," 848 f" sand_layer_label[i]: {self.facies[i]}" 849 ) 850 if self.facies[i] != self.facies[i - 1]: 851 top_lith_indices.append(i) 852 if self.cfg.verbose: 853 print( 854 " ... layer lith different than layer above it. i = {}".format( 855 i 856 ) 857 ) 858 top_lith_indices.sort() 859 if self.cfg.verbose: 860 print( 861 "\n ...layers selected for closure computations...\n", 862 top_lith_indices, 863 ) 864 self.top_lith_indices = np.array(top_lith_indices) 865 self.top_lith_facies = self.facies[top_lith_indices] 866 867 # return top_lith_indices
Find horizons which are the top of layers where the lithology changes
Combine layers of the same lithology and retain the top of these new layers for closure calculations.
869 def create_closures(self): 870 if self.cfg.verbose: 871 print("\n\n ... create 3D labels for closure") 872 873 # Convert nan to 0's 874 old_depth_maps = np.nan_to_num(self.faults.faulted_depth_maps[:], copy=True) 875 old_depth_maps_gaps = np.nan_to_num( 876 self.faults.faulted_depth_maps_gaps[:], copy=True 877 ) 878 879 # Convert from samples to units 880 old_depth_maps_gaps = self.convert_map_from_samples_to_units( 881 old_depth_maps_gaps 882 ) 883 old_depth_maps = self.convert_map_from_samples_to_units(old_depth_maps) 884 885 # keep only horizons corresponding to top of layers where lithology changes 886 self.find_top_lith_horizons() 887 all_lith_indices = np.arange(old_depth_maps.shape[-1]) 888 import sys 889 890 print("All lith indices (last, then all):", self.facies[-1], all_lith_indices) 891 sys.stdout.flush() 892 893 depth_maps_gaps_top_lith = old_depth_maps_gaps[ 894 :, :, self.top_lith_indices 895 ].copy() 896 depth_maps_gaps_all_lith = old_depth_maps_gaps[:, :, all_lith_indices].copy() 897 depth_maps_top_lith = old_depth_maps[:, :, self.top_lith_indices].copy() 898 depth_maps_all_lith = old_depth_maps[:, :, all_lith_indices].copy() 899 max_column_heights = variable_max_column_height( 900 self.top_lith_indices, 901 self.faults.faulted_depth_maps_gaps.shape[-1], 902 self.cfg.max_column_height[0], 903 self.cfg.max_column_height[1], 904 ) 905 all_max_column_heights = variable_max_column_height( 906 all_lith_indices, 907 self.faults.faulted_depth_maps_gaps.shape[-1], 908 self.cfg.max_column_height[0], 909 self.cfg.max_column_height[1], 910 ) 911 912 if self.cfg.verbose: 913 print("\n ...facies for closure computations...\n", self.top_lith_facies) 914 print( 915 "\n ...max column heights for closure computations...\n", 916 max_column_heights, 917 ) 918 919 self.closure_segments[:] = self.create_closure_labels_from_depth_maps( 920 depth_maps_gaps_top_lith, depth_maps_top_lith, max_column_heights 921 ) 922 923 self.all_closure_segments[:] = self.create_closure_labels_from_all_depth_maps( 924 depth_maps_gaps_all_lith, depth_maps_all_lith, all_max_column_heights 925 ) 926 927 if self.cfg.verbose: 928 print( 929 " ...+++... number of nan's in depth_maps_gaps before insertClosureLabels3D ...+++... {}".format( 930 old_depth_maps_gaps[np.isnan(old_depth_maps_gaps)].shape 931 ) 932 ) 933 print( 934 " ...+++... number of nan's in depth_maps_gaps after insertClosureLabels3D ...+++... {}".format( 935 self.faults.faulted_depth_maps_gaps[ 936 np.isnan(self.faults.faulted_depth_maps_gaps) 937 ].shape 938 ) 939 ) 940 print( 941 " ...+++... number of nan's in depth_maps after insertClosureLabels3D ...+++... {}".format( 942 self.faults.faulted_depth_maps[ 943 np.isnan(self.faults.faulted_depth_maps) 944 ].shape 945 ) 946 ) 947 _closure_segments = self.closure_segments[:] 948 print( 949 " ...+++... number of closure voxels in self.closure_segments ...+++... {}".format( 950 _closure_segments[_closure_segments > 0.0].shape 951 ) 952 ) 953 del _closure_segments 954 955 labels_clean, self.closure_segments[:] = self.segment_closures( 956 self.closure_segments[:], remove_shale=True 957 ) 958 label_values, labels_clean = self.parse_label_values_and_counts(labels_clean) 959 960 labels_clean_all, self.all_closure_segments[:] = self.segment_closures( 961 self.all_closure_segments[:], remove_shale=False 962 ) 963 label_values_all, labels_clean_all = self.parse_label_values_and_counts( 964 labels_clean_all 965 ) 966 self.write_cube_to_disk(self.all_closure_segments[:], "all_closure_segments") 967 968 # Assign fluid types 969 ( 970 self.oil_closures[:], 971 self.gas_closures[:], 972 self.brine_closures[:], 973 ) = self.assign_fluid_types(label_values, labels_clean) 974 all_closures_final = (labels_clean_all != 0).astype("uint8") 975 976 # Identify closures by type (simple, faulted, onlap or salt bounded) 977 self.find_faulted_closures(label_values, labels_clean) 978 self.find_onlap_closures(label_values, labels_clean) 979 self.find_simple_closures(label_values, labels_clean) 980 self.find_false_closures(label_values, labels_clean) 981 982 self.find_faulted_all_closures(label_values_all, labels_clean_all) 983 self.find_onlap_all_closures(label_values_all, labels_clean_all) 984 self.find_simple_all_closures(label_values_all, labels_clean_all) 985 self.find_false_all_closures(label_values_all, labels_clean_all) 986 987 if self.cfg.include_salt: 988 self.find_salt_bounded_closures(label_values, labels_clean) 989 self.find_salt_bounded_all_closures(label_values_all, labels_clean_all) 990 991 # Remove false closures from oil & gas closure cubes 992 if self.n_false_closures_oil > 0: 993 print(f"Removing {self.n_false_closures_oil} false oil closures") 994 self.oil_closures[self.false_closures_oil == 1] = 0.0 995 if self.n_false_closures_gas > 0: 996 print(f"Removing {self.n_false_closures_gas} false gas closures") 997 self.gas_closures[self.false_closures_gas == 1] = 0.0 998 999 # Remove false closures from allclosure cube 1000 if self.n_false_all_closures > 0: 1001 print(f"Removing {self.n_false_all_closures} false all closures") 1002 self.all_closure_segments[self.false_all_closures == 1] = 0.0 1003 1004 # Create a closure cube with voxel count as labels, and include closure type in decimal 1005 # e.g. simple closure of size 5000 = 5000.1 1006 # faulted closure of size 5000 = 5000.2 1007 # onlap closure of size 5000 = 5000.3 1008 # salt-bounded closure of size 5000 = 5000.4 1009 hc_closure_codes = np.zeros_like(self.gas_closures, dtype="float32") 1010 1011 # AZ: COULD RUN THESE CLOSURE SIZE FILTERS ON ALL_CLOSURES, IF DESIRED 1012 1013 if "simple" in self.cfg.closure_types: 1014 print("Filtering 4 Way Closures") 1015 ( 1016 self.simple_closures_oil[:], 1017 self.n_4way_closures_oil, 1018 ) = self.closure_size_filter( 1019 self.simple_closures_oil[:], 1020 self.cfg.closure_min_voxels_simple, 1021 self.n_4way_closures_oil, 1022 ) 1023 ( 1024 self.simple_closures_gas[:], 1025 self.n_4way_closures_gas, 1026 ) = self.closure_size_filter( 1027 self.simple_closures_gas[:], 1028 self.cfg.closure_min_voxels_simple, 1029 self.n_4way_closures_gas, 1030 ) 1031 1032 # Add simple closures to closure code cube 1033 hc_closures = ( 1034 self.simple_closures_oil[:] + self.simple_closures_gas[:] 1035 ).astype("float32") 1036 labels, num = measure.label( 1037 hc_closures, connectivity=2, background=0, return_num=True 1038 ) 1039 hc_closure_codes = self.parse_closure_codes( 1040 hc_closure_codes, labels, num, code=0.1 1041 ) 1042 else: # if closure type not in config, set HC closures to 0 1043 self.simple_closures_oil[:] *= 0 1044 self.simple_closures_gas[:] *= 0 1045 self.simple_all_closures[:] *= 0 1046 1047 self.oil_closures[self.simple_closures_oil[:] > 0.0] = 1.0 1048 self.oil_closures[self.simple_closures_oil[:] < 0.0] = 0.0 1049 self.gas_closures[self.simple_closures_gas[:] > 0.0] = 1.0 1050 self.gas_closures[self.simple_closures_gas[:] < 0.0] = 0.0 1051 1052 all_closures_final[self.simple_all_closures[:] > 0.0] = 1.0 1053 all_closures_final[self.simple_all_closures[:] < 0.0] = 0.0 1054 1055 if "faulted" in self.cfg.closure_types: 1056 print("Filtering 4 Way Closures") 1057 # Grow the faulted closures to the fault planes 1058 self.faulted_closures_oil[:] = self.grow_to_fault2( 1059 self.faulted_closures_oil[:] 1060 ) 1061 self.faulted_closures_gas[:] = self.grow_to_fault2( 1062 self.faulted_closures_gas[:] 1063 ) 1064 1065 ( 1066 self.faulted_closures_oil[:], 1067 self.n_fault_closures_oil, 1068 ) = self.closure_size_filter( 1069 self.faulted_closures_oil[:], 1070 self.cfg.closure_min_voxels_faulted, 1071 self.n_fault_closures_oil, 1072 ) 1073 ( 1074 self.faulted_closures_gas[:], 1075 self.n_fault_closures_gas, 1076 ) = self.closure_size_filter( 1077 self.faulted_closures_gas[:], 1078 self.cfg.closure_min_voxels_faulted, 1079 self.n_fault_closures_gas, 1080 ) 1081 1082 self.faulted_all_closures[:] = self.grow_to_fault2( 1083 self.faulted_all_closures[:], 1084 grow_only_sand_closures=False, 1085 remove_small_closures=False, 1086 ) 1087 1088 # Add faulted closures to closure code cube 1089 hc_closures = self.faulted_closures_oil[:] + self.faulted_closures_gas[:] 1090 labels, num = measure.label( 1091 hc_closures, connectivity=2, background=0, return_num=True 1092 ) 1093 hc_closure_codes = self.parse_closure_codes( 1094 hc_closure_codes, labels, num, code=0.2 1095 ) 1096 else: # if closure type not in config, set HC closures to 0 1097 self.faulted_closures_oil[:] *= 0 1098 self.faulted_closures_gas[:] *= 0 1099 self.faulted_all_closures[:] *= 0 1100 1101 self.oil_closures[self.faulted_closures_oil[:] > 0.0] = 1.0 1102 self.oil_closures[self.faulted_closures_oil[:] < 0.0] = 0.0 1103 self.gas_closures[self.faulted_closures_gas[:] > 0.0] = 1.0 1104 self.gas_closures[self.faulted_closures_gas[:] < 0.0] = 0.0 1105 1106 all_closures_final[self.faulted_all_closures[:] > 0.0] = 1.0 1107 all_closures_final[self.faulted_all_closures[:] < 0.0] = 0.0 1108 1109 if "onlap" in self.cfg.closure_types: 1110 print("Filtering Onlap Closures") 1111 ( 1112 self.onlap_closures_oil[:], 1113 self.n_onlap_closures_oil, 1114 ) = self.closure_size_filter( 1115 self.onlap_closures_oil[:], 1116 self.cfg.closure_min_voxels_onlap, 1117 self.n_onlap_closures_oil, 1118 ) 1119 ( 1120 self.onlap_closures_gas[:], 1121 self.n_onlap_closures_gas, 1122 ) = self.closure_size_filter( 1123 self.onlap_closures_gas[:], 1124 self.cfg.closure_min_voxels_onlap, 1125 self.n_onlap_closures_gas, 1126 ) 1127 1128 # Add faulted closures to closure code cube 1129 hc_closures = self.onlap_closures_oil[:] + self.onlap_closures_gas[:] 1130 labels, num = measure.label( 1131 hc_closures, connectivity=2, background=0, return_num=True 1132 ) 1133 hc_closure_codes = self.parse_closure_codes( 1134 hc_closure_codes, labels, num, code=0.3 1135 ) 1136 # labels = labels.astype('float32') 1137 # if num > 0: 1138 # for x in range(1, num + 1): 1139 # y = 0.3 + labels[labels == x].size 1140 # labels[labels == x] = y 1141 # hc_closure_codes += labels 1142 else: # if closure type not in config, set HC closures to 0 1143 self.onlap_closures_oil[:] *= 0 1144 self.onlap_closures_gas[:] *= 0 1145 self.onlap_all_closures[:] *= 0 1146 1147 self.oil_closures[self.onlap_closures_oil[:] > 0.0] = 1.0 1148 self.oil_closures[self.onlap_closures_oil[:] < 0.0] = 0.0 1149 self.gas_closures[self.onlap_closures_gas[:] > 0.0] = 1.0 1150 self.gas_closures[self.onlap_closures_gas[:] < 0.0] = 0.0 1151 all_closures_final[self.onlap_all_closures[:] > 0.0] = 1.0 1152 all_closures_final[self.onlap_all_closures[:] < 0.0] = 0.0 1153 1154 if self.cfg.include_salt: 1155 # Grow the salt-bounded closures to the salt body 1156 salt_closures_oil_grown = np.zeros_like(self.salt_closures_oil[:]) 1157 salt_closures_gas_grown = np.zeros_like(self.salt_closures_gas[:]) 1158 1159 if np.max(self.salt_closures_oil[:]) > 0.0: 1160 self.write_cube_to_disk( 1161 self.salt_closures_oil[:], "salt_closures_oil_initial" 1162 ) 1163 print( 1164 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1165 ) 1166 salt_closures_oil_grown = self.grow_to_salt(self.salt_closures_oil[:]) 1167 self.salt_closures_oil[:] = salt_closures_oil_grown 1168 print( 1169 f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}" 1170 ) 1171 if np.max(self.salt_closures_gas[:]) > 0.0: 1172 self.write_cube_to_disk( 1173 self.salt_closures_gas[:], "salt_closures_gas_initial" 1174 ) 1175 print( 1176 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 0].size}" 1177 ) 1178 salt_closures_gas_grown = self.grow_to_salt(self.salt_closures_gas[:]) 1179 self.salt_closures_gas[:] = salt_closures_gas_grown 1180 print( 1181 f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 1].size}" 1182 ) 1183 if np.max(self.salt_all_closures[:]) > 0.0: 1184 self.write_cube_to_disk( 1185 self.salt_all_closures[:], "salt_all_closures_initial" 1186 ) # maybe remove later 1187 print( 1188 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 0].size}" 1189 ) 1190 salt_all_closures_grown = self.grow_to_salt(self.salt_all_closures[:]) 1191 self.salt_all_closures[:] = salt_all_closures_grown 1192 print( 1193 f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 1].size}" 1194 ) 1195 else: 1196 salt_all_closures_grown = np.zeros_like(self.salt_all_closures) 1197 1198 if np.max(self.salt_closures_oil[:]) > 0.0: 1199 self.write_cube_to_disk( 1200 self.salt_closures_oil[:], "salt_closures_oil_grown" 1201 ) 1202 if np.max(self.salt_closures_gas[:]) > 0.0: 1203 self.write_cube_to_disk( 1204 self.salt_closures_gas[:], "salt_closures_gas_grown" 1205 ) 1206 if np.max(self.salt_all_closures[:]) > 0.0: 1207 self.write_cube_to_disk( 1208 self.salt_all_closures[:], "salt_all_closures_grown" 1209 ) # maybe remove later 1210 1211 ( 1212 self.salt_closures_oil[:], 1213 self.n_salt_closures_oil, 1214 ) = self.closure_size_filter( 1215 self.salt_closures_oil[:], 1216 self.cfg.closure_min_voxels, 1217 self.n_salt_closures_oil, 1218 ) 1219 ( 1220 self.salt_closures_gas[:], 1221 self.n_salt_closures_gas, 1222 ) = self.closure_size_filter( 1223 self.salt_closures_gas[:], 1224 self.cfg.closure_min_voxels, 1225 self.n_salt_closures_gas, 1226 ) 1227 1228 # Append salt-bounded closures to main closure cubes for oil and gas 1229 if np.max(salt_closures_oil_grown) > 0.0: 1230 self.oil_closures[salt_closures_oil_grown > 0.0] = 1.0 1231 self.oil_closures[salt_closures_oil_grown < 0.0] = 0.0 1232 if np.max(salt_closures_gas_grown) > 0.0: 1233 self.gas_closures[salt_closures_gas_grown > 0.0] = 1.0 1234 self.gas_closures[salt_closures_gas_grown < 0.0] = 0.0 1235 if np.max(salt_all_closures_grown) > 0.0: 1236 all_closures_final[salt_all_closures_grown > 0.0] = 1.0 1237 all_closures_final[salt_all_closures_grown < 0.0] = 0.0 1238 1239 # Add faulted closures to closure code cube 1240 hc_closures = self.salt_closures_oil[:] + self.salt_closures_gas[:] 1241 labels, num = measure.label( 1242 hc_closures, connectivity=2, background=0, return_num=True 1243 ) 1244 hc_closure_codes = self.parse_closure_codes( 1245 hc_closure_codes, labels, num, code=0.4 1246 ) 1247 1248 # Write hc_closure_codes to disk 1249 self.write_cube_to_disk(hc_closure_codes, "closure_segments_hc_voxelcount") 1250 1251 # Create closure volumes by type 1252 if self.simple_closures[:] is None: 1253 self.simple_closures[:] = self.simple_closures_oil[:].astype("uint8") 1254 else: 1255 self.simple_closures[:] += self.simple_closures_oil[:].astype("uint8") 1256 self.simple_closures[:] += self.simple_closures_gas[:].astype("uint8") 1257 self.simple_closures[:] += self.simple_closures_brine[:].astype("uint8") 1258 # Onlap closures 1259 if self.strat_closures is None: 1260 self.strat_closures[:] = self.onlap_closures_oil[:].astype("uint8") 1261 else: 1262 self.strat_closures[:] += self.onlap_closures_oil[:].astype("uint8") 1263 self.strat_closures[:] += self.onlap_closures_gas[:].astype("uint8") 1264 self.strat_closures[:] += self.onlap_closures_brine[:].astype("uint8") 1265 # Fault closures 1266 if self.fault_closures is None: 1267 self.fault_closures[:] = self.faulted_closures_oil[:].astype("uint8") 1268 else: 1269 self.fault_closures[:] += self.faulted_closures_oil[:].astype("uint8") 1270 self.fault_closures[:] += self.faulted_closures_gas[:].astype("uint8") 1271 self.fault_closures[:] += self.faulted_closures_brine[:].astype("uint8") 1272 1273 # Salt-bounded closures 1274 if self.cfg.include_salt: 1275 if self.salt_closures is None: 1276 self.salt_closures[:] = self.salt_closures_oil[:].astype("uint8") 1277 else: 1278 self.salt_closures[:] += self.salt_closures_oil[:].astype("uint8") 1279 self.salt_closures[:] += self.salt_closures_gas[:].astype("uint8") 1280 1281 # Convert closure cubes from int16 to uint8 for writing to disk 1282 self.closure_segments[:] = self.closure_segments[:].astype("uint8") 1283 1284 # add any oil/gas/brine closures into all_closures_final in case missed 1285 all_closures_final[:][self.oil_closures[:] > 0] = 1 1286 all_closures_final[:][self.gas_closures[:] > 0] = 1 1287 all_closures_final[:][self.gas_closures[:] > 0] = 1 1288 # Write all_closures_final to disk 1289 self.write_cube_to_disk(all_closures_final.astype("uint8"), "trap_label") 1290 1291 # add any oil/gas/brine closures into reservoir in case missed 1292 self.faults.reservoir[:][self.oil_closures[:] > 0] = 1 1293 self.faults.reservoir[:][self.gas_closures[:] > 0] = 1 1294 self.faults.reservoir[:][self.brine_closures[:] > 0] = 1 1295 # write reservoir_label to disk 1296 self.write_cube_to_disk( 1297 self.faults.reservoir[:].astype("uint8"), "reservoir_label" 1298 ) 1299 1300 if self.cfg.qc_plots: 1301 from datagenerator.util import plot_xsection 1302 from datagenerator.util import find_line_with_most_voxels 1303 1304 # visualize closures QC 1305 inline_index_cl = find_line_with_most_voxels( 1306 self.closure_segments, 0.5, self.cfg 1307 ) 1308 plot_xsection( 1309 volume=labels_clean, 1310 maps=self.faults.faulted_depth_maps_gaps, 1311 line_num=inline_index_cl, 1312 title="Example Trav through 3D model\nclosures after faulting", 1313 png_name="QC_plot__AfterFaulting_closure_segments.png", 1314 cmap="gist_ncar_r", 1315 cfg=self.cfg, 1316 )
1318 def closure_size_filter(self, closure_type, threshold, count): 1319 labels, num = measure.label( 1320 closure_type, connectivity=2, background=0, return_num=True 1321 ) 1322 if ( 1323 num > 0 1324 ): # TODO add whether smallest closure is below threshold constraint too 1325 s = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1326 labels = morphology.remove_small_objects(labels, threshold, connectivity=2) 1327 t = [labels[labels == x].size for x in range(1, 1 + np.max(labels))] 1328 print( 1329 f"Closure sizes before filter: {s}\nThreshold: {threshold}\n" 1330 f"Closure sizes after filter: {t}" 1331 ) 1332 count = len(t) 1333 return labels, count
1335 def closure_type_info_for_log(self): 1336 fluid_types = ["oil", "gas", "brine"] 1337 if "faulted" in self.cfg.closure_types: 1338 # Faulted closures 1339 for name, fluid, num in zip( 1340 fluid_types, 1341 [ 1342 self.faulted_closures_oil[:], 1343 self.faulted_closures_gas[:], 1344 self.faulted_closures_brine[:], 1345 ], 1346 [ 1347 self.n_fault_closures_oil, 1348 self.n_fault_closures_gas, 1349 self.n_fault_closures_brine, 1350 ], 1351 ): 1352 n_voxels = fluid[fluid[:] > 0.0].size 1353 msg = f"n_fault_closures_{name}: {num:03d}\n" 1354 msg += f"n_voxels_fault_closures_{name}: {n_voxels:08d}\n" 1355 print(msg) 1356 self.cfg.write_to_logfile(msg) 1357 self.cfg.write_to_logfile( 1358 msg=None, 1359 mainkey="model_parameters", 1360 subkey=f"n_fault_closures_{name}", 1361 val=num, 1362 ) 1363 self.cfg.write_to_logfile( 1364 msg=None, 1365 mainkey="model_parameters", 1366 subkey=f"n_voxels_fault_closures_{name}", 1367 val=n_voxels, 1368 ) 1369 closure_statistics = self.calculate_closure_statistics( 1370 fluid, f"Faulted {name.capitalize()}" 1371 ) 1372 if closure_statistics: 1373 print(closure_statistics) 1374 self.cfg.write_to_logfile(closure_statistics) 1375 1376 if "onlap" in self.cfg.closure_types: 1377 # Onlap Closures 1378 for name, fluid, num in zip( 1379 fluid_types, 1380 [ 1381 self.onlap_closures_oil[:], 1382 self.onlap_closures_gas[:], 1383 self.onlap_closures_brine[:], 1384 ], 1385 [ 1386 self.n_onlap_closures_oil, 1387 self.n_onlap_closures_gas, 1388 self.n_onlap_closures_brine, 1389 ], 1390 ): 1391 n_voxels = fluid[fluid[:] > 0.0].size 1392 msg = f"n_onlap_closures_{name}: {num:03d}\n" 1393 msg += f"n_voxels_onlap_closures_{name}: {n_voxels:08d}\n" 1394 print(msg) 1395 self.cfg.write_to_logfile(msg) 1396 self.cfg.write_to_logfile( 1397 msg=None, 1398 mainkey="model_parameters", 1399 subkey=f"n_onlap_closures_{name}", 1400 val=num, 1401 ) 1402 self.cfg.write_to_logfile( 1403 msg=None, 1404 mainkey="model_parameters", 1405 subkey=f"n_voxels_onlap_closures_{name}", 1406 val=n_voxels, 1407 ) 1408 closure_statistics = self.calculate_closure_statistics( 1409 fluid, f"Onlap {name.capitalize()}" 1410 ) 1411 if closure_statistics: 1412 print(closure_statistics) 1413 self.cfg.write_to_logfile(closure_statistics) 1414 1415 if "simple" in self.cfg.closure_types: 1416 # Simple Closures 1417 for name, fluid, num in zip( 1418 fluid_types, 1419 [ 1420 self.simple_closures_oil[:], 1421 self.simple_closures_gas[:], 1422 self.simple_closures_brine[:], 1423 ], 1424 [ 1425 self.n_4way_closures_oil, 1426 self.n_4way_closures_gas, 1427 self.n_4way_closures_brine, 1428 ], 1429 ): 1430 n_voxels = fluid[fluid[:] > 0.0].size 1431 msg = f"n_4way_closures_{name}: {num:03d}\n" 1432 msg += f"n_voxels_4way_closures_{name}: {n_voxels:08d}\n" 1433 print(msg) 1434 self.cfg.write_to_logfile(msg) 1435 self.cfg.write_to_logfile( 1436 msg=None, 1437 mainkey="model_parameters", 1438 subkey=f"n_4way_closures_{name}", 1439 val=num, 1440 ) 1441 self.cfg.write_to_logfile( 1442 msg=None, 1443 mainkey="model_parameters", 1444 subkey=f"n_voxels_4way_closures_{name}", 1445 val=n_voxels, 1446 ) 1447 closure_statistics = self.calculate_closure_statistics( 1448 fluid, f"4-Way {name.capitalize()}" 1449 ) 1450 if closure_statistics: 1451 print(closure_statistics) 1452 self.cfg.write_to_logfile(closure_statistics) 1453 1454 if self.cfg.include_salt: 1455 # Salt-Bounded Closures 1456 for name, fluid, num in zip( 1457 fluid_types, 1458 [ 1459 self.salt_closures_oil[:], 1460 self.salt_closures_gas[:], 1461 self.salt_closures_brine[:], 1462 ], 1463 [ 1464 self.n_salt_closures_oil, 1465 self.n_salt_closures_gas, 1466 self.n_salt_closures_brine, 1467 ], 1468 ): 1469 n_voxels = fluid[fluid[:] > 0.0].size 1470 msg = f"n_salt_closures_{name}: {num:03d}\n" 1471 msg += f"n_voxels_salt_closures_{name}: {n_voxels:08d}\n" 1472 print(msg) 1473 self.cfg.write_to_logfile(msg) 1474 self.cfg.write_to_logfile( 1475 msg=None, 1476 mainkey="model_parameters", 1477 subkey=f"n_salt_closures_{name}", 1478 val=num, 1479 ) 1480 self.cfg.write_to_logfile( 1481 msg=None, 1482 mainkey="model_parameters", 1483 subkey=f"n_voxels_salt_closures_{name}", 1484 val=n_voxels, 1485 ) 1486 closure_statistics = self.calculate_closure_statistics( 1487 fluid, f"Salt {name.capitalize()}" 1488 ) 1489 if closure_statistics: 1490 print(closure_statistics) 1491 self.cfg.write_to_logfile(closure_statistics)
1493 def get_voxel_counts(self, closures): 1494 next_label = 0 1495 label_values = [0] 1496 label_counts = [closures[closures == 0].size] 1497 for i in range(closures.max() + 1): 1498 try: 1499 next_label = closures[closures > next_label].min() 1500 except (TypeError, ValueError): 1501 break 1502 label_values.append(next_label) 1503 label_counts.append(closures[closures == next_label].size) 1504 print( 1505 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1506 ) 1507 1508 print( 1509 f'{72 * "*"}\n\tNum Closures: {len(label_counts) - 1}\n\tVoxel counts\n{label_counts[1:]}\n{72 * "*"}' 1510 ) 1511 for vox_count in label_counts: 1512 if vox_count < self.cfg.closure_min_voxels: 1513 print(f"voxel_count: {vox_count}")
1515 def populate_closure_dict(self, labels, fluid, seismic_nmf=None): 1516 clist = [] 1517 max_num = np.max(labels) 1518 if seismic_nmf is not None: 1519 # calculate ai_gi 1520 ai, gi = compute_ai_gi(self.cfg, seismic_nmf) 1521 for i in range(1, max_num + 1): 1522 _c = np.where(labels == i) 1523 cl = dict() 1524 cl["model_id"] = os.path.basename(self.cfg.work_subfolder) 1525 cl["fluid"] = fluid 1526 cl["n_voxels"] = len(_c[0]) 1527 # np.min() or x.min() returns type numpy.int64 which SQLITE cannot handle. Convert to int 1528 cl["x_min"] = int(np.min(_c[0])) 1529 cl["x_max"] = int(np.max(_c[0])) 1530 cl["y_min"] = int(np.min(_c[1])) 1531 cl["y_max"] = int(np.max(_c[1])) 1532 cl["z_min"] = int(np.min(_c[2])) 1533 cl["z_max"] = int(np.max(_c[2])) 1534 cl["zbml_min"] = np.min(self.faults.faulted_depth[_c]) 1535 cl["zbml_max"] = np.max(self.faults.faulted_depth[_c]) 1536 cl["zbml_avg"] = np.mean(self.faults.faulted_depth[_c]) 1537 cl["zbml_std"] = np.std(self.faults.faulted_depth[_c]) 1538 cl["zbml_25pct"] = np.percentile(self.faults.faulted_depth[_c], 25) 1539 cl["zbml_median"] = np.percentile(self.faults.faulted_depth[_c], 50) 1540 cl["zbml_75pct"] = np.percentile(self.faults.faulted_depth[_c], 75) 1541 cl["ng_min"] = np.min(self.faults.faulted_net_to_gross[_c]) 1542 cl["ng_max"] = np.max(self.faults.faulted_net_to_gross[_c]) 1543 cl["ng_avg"] = np.mean(self.faults.faulted_net_to_gross[_c]) 1544 cl["ng_std"] = np.std(self.faults.faulted_net_to_gross[_c]) 1545 cl["ng_25pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 25) 1546 cl["ng_median"] = np.median(self.faults.faulted_net_to_gross[_c]) 1547 cl["ng_75pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 75) 1548 # Check for intersections with faults, salt and onlaps for closure type 1549 cl["intersects_fault"] = False 1550 cl["intersects_onlap"] = False 1551 cl["intersects_salt"] = False 1552 if np.max(self.wide_faults[_c] > 0): 1553 cl["intersects_fault"] = True 1554 if np.max(self.onlaps_upward[_c] > 0): 1555 cl["intersects_onlap"] = True 1556 if self.cfg.include_salt and np.max(self.wide_salt[_c] > 0): 1557 cl["intersects_salt"] = True 1558 1559 if seismic_nmf is not None: 1560 # Using only the top of the closure, calculate seismic properties 1561 labels_copy = labels.copy() 1562 labels_copy[labels_copy != i] = 0 1563 top_closure = get_top_of_closure(labels_copy) 1564 near = seismic_nmf[0, ...][np.where(top_closure == 1)] 1565 cl["near_min"] = np.min(near) 1566 cl["near_max"] = np.max(near) 1567 cl["near_avg"] = np.mean(near) 1568 cl["near_std"] = np.std(near) 1569 cl["near_25pct"] = np.percentile(near, 25) 1570 cl["near_median"] = np.percentile(near, 50) 1571 cl["near_75pct"] = np.percentile(near, 75) 1572 mid = seismic_nmf[1, ...][np.where(top_closure == 1)] 1573 cl["mid_min"] = np.min(mid) 1574 cl["mid_max"] = np.max(mid) 1575 cl["mid_avg"] = np.mean(mid) 1576 cl["mid_std"] = np.std(mid) 1577 cl["mid_25pct"] = np.percentile(mid, 25) 1578 cl["mid_median"] = np.percentile(mid, 50) 1579 cl["mid_75pct"] = np.percentile(mid, 75) 1580 far = seismic_nmf[2, ...][np.where(top_closure == 1)] 1581 cl["far_min"] = np.min(far) 1582 cl["far_max"] = np.max(far) 1583 cl["far_avg"] = np.mean(far) 1584 cl["far_std"] = np.std(far) 1585 cl["far_25pct"] = np.percentile(far, 25) 1586 cl["far_median"] = np.percentile(far, 50) 1587 cl["far_75pct"] = np.percentile(far, 75) 1588 intercept = ai[np.where(top_closure == 1)] 1589 cl["intercept_min"] = np.min(intercept) 1590 cl["intercept_max"] = np.max(intercept) 1591 cl["intercept_avg"] = np.mean(intercept) 1592 cl["intercept_std"] = np.std(intercept) 1593 cl["intercept_25pct"] = np.percentile(intercept, 25) 1594 cl["intercept_median"] = np.percentile(intercept, 50) 1595 cl["intercept_75pct"] = np.percentile(intercept, 75) 1596 gradient = gi[np.where(top_closure == 1)] 1597 cl["gradient_min"] = np.min(gradient) 1598 cl["gradient_max"] = np.max(gradient) 1599 cl["gradient_avg"] = np.mean(gradient) 1600 cl["gradient_std"] = np.std(gradient) 1601 cl["gradient_25pct"] = np.percentile(gradient, 25) 1602 cl["gradient_median"] = np.percentile(gradient, 50) 1603 cl["gradient_75pct"] = np.percentile(gradient, 75) 1604 1605 clist.append(cl) 1606 1607 return clist
1609 def write_closure_info_to_log(self, seismic_nmf=None): 1610 """store info about closure in log file""" 1611 top_sand_layers = [x for x in self.top_lith_indices if self.facies[x] == 1.0] 1612 self.cfg.write_to_logfile( 1613 msg=None, 1614 mainkey="model_parameters", 1615 subkey="top_sand_layers", 1616 val=top_sand_layers, 1617 ) 1618 o = measure.label(self.oil_closures[:], connectivity=2, background=0) 1619 g = measure.label(self.gas_closures[:], connectivity=2, background=0) 1620 b = measure.label(self.brine_closures[:], connectivity=2, background=0) 1621 oil_closures = self.populate_closure_dict(o, "oil", seismic_nmf) 1622 gas_closures = self.populate_closure_dict(g, "gas", seismic_nmf) 1623 brine_closures = self.populate_closure_dict(b, "brine", seismic_nmf) 1624 all_closures = oil_closures + gas_closures + brine_closures 1625 for i, c in enumerate(all_closures): 1626 self.cfg.sqldict[f"closure_{i+1}"] = c 1627 num_labels = np.max(o) + np.max(g) 1628 self.cfg.write_to_logfile( 1629 msg=None, 1630 mainkey="model_parameters", 1631 subkey="number_hc_closures", 1632 val=num_labels, 1633 ) 1634 # Add total number of closure voxels, with ratio of closure voxels given as a percentage 1635 closure_voxel_count = o[o > 0].size + g[g > 0].size 1636 closure_voxel_pct = closure_voxel_count / o.size 1637 self.cfg.write_to_logfile( 1638 msg=None, 1639 mainkey="model_parameters", 1640 subkey="closure_voxel_count", 1641 val=closure_voxel_count, 1642 ) 1643 self.cfg.write_to_logfile( 1644 msg=None, 1645 mainkey="model_parameters", 1646 subkey="closure_voxel_pct", 1647 val=closure_voxel_pct * 100, 1648 ) 1649 # Same for Brine 1650 _brine_voxels = b[b == 1].size 1651 _brine_voxels_pct = _brine_voxels / b.size 1652 self.cfg.write_to_logfile( 1653 msg=None, 1654 mainkey="model_parameters", 1655 subkey="closure_voxel_count_brine", 1656 val=_brine_voxels, 1657 ) 1658 self.cfg.write_to_logfile( 1659 msg=None, 1660 mainkey="model_parameters", 1661 subkey="closure_voxel_pct_brine", 1662 val=_brine_voxels_pct * 100, 1663 ) 1664 # Same for Oil 1665 _oil_voxels = o[o == 1].size 1666 _oil_voxels_pct = _oil_voxels / o.size 1667 self.cfg.write_to_logfile( 1668 msg=None, 1669 mainkey="model_parameters", 1670 subkey="closure_voxel_count_oil", 1671 val=_oil_voxels, 1672 ) 1673 self.cfg.write_to_logfile( 1674 msg=None, 1675 mainkey="model_parameters", 1676 subkey="closure_voxel_pct_oil", 1677 val=_oil_voxels_pct * 100, 1678 ) 1679 # Same for Gas 1680 _gas_voxels = g[g == 1].size 1681 _gas_voxels_pct = _gas_voxels / g.size 1682 self.cfg.write_to_logfile( 1683 msg=None, 1684 mainkey="model_parameters", 1685 subkey="closure_voxel_count_gas", 1686 val=_gas_voxels, 1687 ) 1688 self.cfg.write_to_logfile( 1689 msg=None, 1690 mainkey="model_parameters", 1691 subkey="closure_voxel_pct_gas", 1692 val=_gas_voxels_pct, 1693 ) 1694 # Write old logfile as well as the sql dict 1695 msg = f"layers for closure computation: {str(self.top_lith_indices)}\n" 1696 msg += f"Number of HC Closures : {num_labels}\n" 1697 msg += ( 1698 f"Closure voxel count: {closure_voxel_count} - " 1699 f"{closure_voxel_pct:5.2%}\n" 1700 ) 1701 msg += ( 1702 f"Closure voxel count: (brine) {_brine_voxels} - {_brine_voxels_pct:5.2%}\n" 1703 ) 1704 msg += f"Closure voxel count: (oil) {_oil_voxels} - {_oil_voxels_pct:5.2%}\n" 1705 msg += f"Closure voxel count: (gas) {_gas_voxels} - {_gas_voxels_pct:5.2%}\n" 1706 print(msg) 1707 for i in range(self.facies.shape[0]): 1708 if self.facies[i] == 1: 1709 msg += f" layers for closure computation: {i}, sand\n" 1710 else: 1711 msg += f" layers for closure computation: {i}, shale\n" 1712 self.cfg.write_to_logfile(msg)
store info about closure in log file
1714 def parse_label_values_and_counts(self, labels_clean): 1715 """parse label values and counts""" 1716 if self.cfg.verbose: 1717 print(" Inside parse_label_values_and_counts") 1718 next_label = 0 1719 label_values = [0] 1720 label_counts = [labels_clean[labels_clean == 0].size] 1721 for i in range(1, labels_clean.max() + 1): 1722 try: 1723 next_label = labels_clean[labels_clean > next_label].min() 1724 except (TypeError, ValueError): 1725 break 1726 label_values.append(next_label) 1727 label_counts.append(labels_clean[labels_clean == next_label].size) 1728 print( 1729 f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}" 1730 ) 1731 # force labels to use consecutive integer values 1732 for i, ilabel in enumerate(label_values): 1733 labels_clean[labels_clean == ilabel] = i 1734 label_values[i] = i 1735 # labels_clean = self.remove_small_objects(labels_clean) # already applied to labels_clean 1736 # Remove label_value 0 1737 label_values.remove(0) 1738 return label_values, labels_clean
parse label values and counts
1740 def assign_fluid_types(self, label_values, labels_clean): 1741 """randomly assign oil or gas to closure""" 1742 print( 1743 " labels_clean.min(), labels_clean.max() = ", 1744 labels_clean.min(), 1745 labels_clean.max(), 1746 ) 1747 _brine_closures = (labels_clean * 0.0).astype("uint8") 1748 _oil_closures = (labels_clean * 0.0).astype("uint8") 1749 _gas_closures = (labels_clean * 0.0).astype("uint8") 1750 1751 fluid_type_code = np.random.randint(3, size=labels_clean.max() + 1) 1752 1753 _closure_segments = self.closure_segments[:] 1754 for i in range(1, labels_clean.max() + 1): 1755 voxel_count = labels_clean[labels_clean == i].size 1756 if voxel_count > 0: 1757 print(f"Voxel Count: {voxel_count}\tFluid type: {fluid_type_code[i]}") 1758 # not in closure = 0 1759 # closure with brine filled reservoir fluid_type_code = 1 1760 # closure with oil filled reservoir fluid_type_code = 2 1761 # closure with gas filled reservoir fluid_type_code = 3 1762 if i in label_values: 1763 if fluid_type_code[i] == 0: 1764 # brine: change labels_clean contents to fluid_type_code = 1 (same as background) 1765 _brine_closures[ 1766 np.logical_and(labels_clean == i, _closure_segments > 0) 1767 ] = 1 1768 elif fluid_type_code[i] == 1: 1769 # oil: change labels_clean contents to fluid_type_code = 2 1770 _oil_closures[labels_clean == i] = 1 1771 elif fluid_type_code[i] == 2: 1772 # gas: change labels_clean contents to fluid_type_code = 3 1773 _gas_closures[labels_clean == i] = 1 1774 return _oil_closures, _gas_closures, _brine_closures
randomly assign oil or gas to closure
1776 def remove_small_objects(self, labels, min_filter=True): 1777 try: 1778 # Use the global minimum voxel size initially, before closure types are identified 1779 labels_clean = morphology.remove_small_objects( 1780 labels, self.cfg.closure_min_voxels 1781 ) 1782 if self.cfg.verbose: 1783 print("labels_clean succeeded.") 1784 print( 1785 " labels.min:{}, labels.max: {}".format(labels.min(), labels.max()) 1786 ) 1787 print( 1788 " labels_clean min:{}, labels_clean max: {}".format( 1789 labels_clean.min(), labels_clean.max() 1790 ) 1791 ) 1792 except Exception as e: 1793 print( 1794 f"Closures/create_closures: labels_clean (remove_small_objects) did not succeed: {e}" 1795 ) 1796 if min_filter: 1797 labels_clean = minimum_filter(labels, size=(3, 3, 3)) 1798 if self.cfg.verbose: 1799 print( 1800 " labels.min:{}, labels.max: {}".format( 1801 labels.min(), labels.max() 1802 ) 1803 ) 1804 print( 1805 " labels_clean min:{}, labels_clean max: {}".format( 1806 labels_clean.min(), labels_clean.max() 1807 ) 1808 ) 1809 return labels_clean
1811 def segment_closures(self, _closure_segments, remove_shale=True): 1812 """Segment the closures so that they can be randomly filled with hydrocarbons""" 1813 1814 _closure_segments = np.clip(_closure_segments, 0.0, 1.0) 1815 # remove tiny clusters 1816 _closure_segments = minimum_filter( 1817 _closure_segments.astype("int16"), size=(3, 3, 1) 1818 ) 1819 _closure_segments = maximum_filter(_closure_segments, size=(3, 3, 1)) 1820 1821 if remove_shale: 1822 # restrict closures to sand (non-shale) voxels 1823 if self.faults.faulted_lithology.shape[2] == _closure_segments.shape[2]: 1824 sand_shale = self.faults.faulted_lithology[:].copy() 1825 else: 1826 sand_shale = self.faults.faulted_lithology[ 1827 :, :, :: self.cfg.infill_factor 1828 ].copy() 1829 _closure_segments[sand_shale <= 0.0] = 0 1830 del sand_shale 1831 labels = measure.label(_closure_segments, connectivity=2, background=0) 1832 1833 labels_clean = self.remove_small_objects(labels) 1834 return labels_clean, _closure_segments
Segment the closures so that they can be randomly filled with hydrocarbons
1836 def write_closure_volumes_to_disk(self): 1837 # Create files for closure volumes 1838 self.write_cube_to_disk(self.brine_closures[:], "closure_segments_brine") 1839 self.write_cube_to_disk(self.oil_closures[:], "closure_segments_oil") 1840 self.write_cube_to_disk(self.gas_closures[:], "closure_segments_gas") 1841 # Create combined HC cube by adding oil and gas closures 1842 self.hc_labels[:] = (self.oil_closures[:] + self.gas_closures[:]).astype( 1843 "uint8" 1844 ) 1845 self.write_cube_to_disk(self.hc_labels[:], "closure_segments_hc") 1846 1847 if self.cfg.model_qc_volumes: 1848 self.write_cube_to_disk(self.closure_segments, "closure_segments_raw_all") 1849 self.write_cube_to_disk(self.simple_closures, "closure_segments_simple") 1850 self.write_cube_to_disk(self.strat_closures, "closure_segments_strat") 1851 self.write_cube_to_disk(self.fault_closures, "closure_segments_fault") 1852 1853 # Triple check that no small closures exist in the final closure files 1854 for i, c in enumerate( 1855 [ 1856 self.oil_closures, 1857 self.gas_closures, 1858 self.simple_closures, 1859 self.strat_closures, 1860 self.fault_closures, 1861 ] 1862 ): 1863 _t = measure.label(c, connectivity=2, background=0) 1864 counts = [_t[_t == x].size for x in range(np.max(_t))] 1865 print(f"Final closure volume voxels sizes: {counts}") 1866 for n, x in enumerate(counts): 1867 if x < self.cfg.closure_min_voxels: 1868 print(f"Voxel count: {x}\t Count:{i}, index: {n}") 1869 1870 # Return the hydrocarbon closure labels so that augmentation can be applied to the data & labels 1871 # return self.oil_closures + self.gas_closures
1873 def calculate_closure_statistics(self, in_array, closure_type): 1874 """ 1875 Calculate the size and location of isolated features in an array 1876 1877 :param in_array: ndarray. Input array to be labelled, where non-zero values are counted as features 1878 :param closure_type: string. Closure type label 1879 :param digi: int or float. To convert depth values from samples to units 1880 :return: string. Concatenated string of closure statistics to be written to log 1881 """ 1882 labelled_array, max_labels = measure.label( 1883 in_array, connectivity=2, return_num=True 1884 ) 1885 msg = "" 1886 for i in range(1, max_labels + 1): # start at 1 to avoid counting 0's 1887 trap = np.where(labelled_array == i) 1888 ranges = [([np.min(trap[x]), np.max(trap[x])]) for x, _ in enumerate(trap)] 1889 sizes = [x[1] - x[0] for x in ranges] 1890 n_voxels = labelled_array[labelled_array == i].size 1891 if sum(sizes) > 0: 1892 msg += ( 1893 f"{closure_type}\t" 1894 f"Num X,Y,Z Samples: {str(sizes).ljust(15)}\t" 1895 f"Num Voxels: {str(n_voxels).ljust(5)}\t" 1896 f"Track: {2000 + ranges[0][0]}-{2000 + ranges[0][1]}\t" 1897 f"Bin: {1000 + ranges[1][0]}-{1000 + ranges[1][1]}\t" 1898 f"Depth: {ranges[2][0] * self.cfg.digi}-{ranges[2][1] * self.cfg.digi + self.cfg.digi / 2}\n" 1899 ) 1900 return msg
Calculate the size and location of isolated features in an array
Parameters
- in_array: ndarray. Input array to be labelled, where non-zero values are counted as features
- closure_type: string. Closure type label
- digi: int or float. To convert depth values from samples to units
Returns
string. Concatenated string of closure statistics to be written to log
1902 def find_faulted_closures(self, closure_segment_list, closure_segments): 1903 self._dilate_faults() 1904 for iclosure in closure_segment_list: 1905 i, j, k = np.where(closure_segments == iclosure) 1906 faults_within_closure = self.wide_faults[i, j, k] 1907 if faults_within_closure.max() > 0: 1908 if self.oil_closures[i, j, k].max() > 0: 1909 # Faulted oil closure 1910 self.faulted_closures_oil[i, j, k] = 1.0 1911 self.n_fault_closures_oil += 1 1912 self.fault_closures_oil_segment_list.append(iclosure) 1913 elif self.gas_closures[i, j, k].max() > 0: 1914 # Faulted gas closure 1915 self.faulted_closures_gas[i, j, k] = 1.0 1916 self.n_fault_closures_gas += 1 1917 self.fault_closures_gas_segment_list.append(iclosure) 1918 elif self.brine_closures[i, j, k].max() > 0: 1919 # Faulted brine closure 1920 self.faulted_closures_brine[i, j, k] = 1.0 1921 self.n_fault_closures_brine += 1 1922 self.fault_closures_brine_segment_list.append(iclosure) 1923 else: 1924 print( 1925 "Closure is faulted but does not have oil, gas or brine assigned" 1926 )
1928 def find_onlap_closures(self, closure_segment_list, closure_segments): 1929 for iclosure in closure_segment_list: 1930 i, j, k = np.where(closure_segments == iclosure) 1931 onlaps_within_closure = self.onlaps_upward[i, j, k] 1932 if onlaps_within_closure.max() > 0: 1933 if self.oil_closures[i, j, k].max() > 0: 1934 self.onlap_closures_oil[i, j, k] = 1.0 1935 self.n_onlap_closures_oil += 1 1936 self.onlap_closures_oil_segment_list.append(iclosure) 1937 elif self.gas_closures[i, j, k].max() > 0: 1938 self.onlap_closures_gas[i, j, k] = 1.0 1939 self.n_onlap_closures_gas += 1 1940 self.onlap_closures_gas_segment_list.append(iclosure) 1941 elif self.brine_closures[i, j, k].max() > 0: 1942 self.onlap_closures_brine[i, j, k] = 1.0 1943 self.n_onlap_closures_brine += 1 1944 self.onlap_closures_brine_segment_list.append(iclosure) 1945 else: 1946 print( 1947 "Closure is onlap but does not have oil, gas or brine assigned" 1948 )
1950 def find_simple_closures(self, closure_segment_list, closure_segments): 1951 for iclosure in closure_segment_list: 1952 i, j, k = np.where(closure_segments == iclosure) 1953 faults_within_closure = self.wide_faults[i, j, k] 1954 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 1955 onlaps_within_closure = onlaps[i, j, k] 1956 oil_within_closure = self.oil_closures[i, j, k] 1957 gas_within_closure = self.gas_closures[i, j, k] 1958 brine_within_closure = self.brine_closures[i, j, k] 1959 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 1960 if oil_within_closure.max() > 0: 1961 self.simple_closures_oil[i, j, k] = 1.0 1962 self.n_4way_closures_oil += 1 1963 elif gas_within_closure.max() > 0: 1964 self.simple_closures_gas[i, j, k] = 1.0 1965 self.n_4way_closures_gas += 1 1966 elif brine_within_closure.max() > 0: 1967 self.simple_closures_brine[i, j, k] = 1.0 1968 self.n_4way_closures_brine += 1 1969 else: 1970 print( 1971 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 1972 )
1974 def find_false_closures(self, closure_segment_list, closure_segments): 1975 for iclosure in closure_segment_list: 1976 i, j, k = np.where(closure_segments == iclosure) 1977 faults_within_closure = self.fat_faults[i, j, k] 1978 onlaps_within_closure = self.onlaps_downward[i, j, k] 1979 for fluid, false, num in zip( 1980 [self.oil_closures, self.gas_closures, self.brine_closures], 1981 [ 1982 self.false_closures_oil, 1983 self.false_closures_gas, 1984 self.false_closures_brine, 1985 ], 1986 [ 1987 self.n_false_closures_oil, 1988 self.n_false_closures_gas, 1989 self.n_false_closures_brine, 1990 ], 1991 ): 1992 fluid_within_closure = fluid[i, j, k] 1993 if fluid_within_closure.max() > 0: 1994 if onlaps_within_closure.max() > 0: 1995 _faulted_closure_threshold = float( 1996 faults_within_closure[faults_within_closure > 0].size 1997 / fluid_within_closure[fluid_within_closure > 0].size 1998 ) 1999 _onlap_closure_threshold = float( 2000 onlaps_within_closure[onlaps_within_closure > 0].size 2001 / fluid_within_closure[fluid_within_closure > 0].size 2002 ) 2003 if ( 2004 _faulted_closure_threshold > 0.65 2005 and _onlap_closure_threshold > 0.65 2006 ): 2007 false[i, j, k] = 1 2008 num += 1
2010 def find_salt_bounded_closures(self, closure_segment_list, closure_segments): 2011 self._dilate_salt() 2012 for iclosure in closure_segment_list: 2013 i, j, k = np.where(closure_segments == iclosure) 2014 salt_within_closure = self.wide_salt[i, j, k] 2015 if salt_within_closure.max() > 0: 2016 if self.oil_closures[i, j, k].max() > 0: 2017 # salt bounded oil closure 2018 self.salt_closures_oil[i, j, k] = 1.0 2019 self.n_salt_closures_oil += 1 2020 self.salt_closures_oil_segment_list.append(iclosure) 2021 elif self.gas_closures[i, j, k].max() > 0: 2022 # salt bounded gas closure 2023 self.salt_closures_gas[i, j, k] = 1.0 2024 self.n_salt_closures_gas += 1 2025 self.salt_closures_gas_segment_list.append(iclosure) 2026 elif self.brine_closures[i, j, k].max() > 0: 2027 # salt bounded brine closure 2028 self.salt_closures_brine[i, j, k] = 1.0 2029 self.n_salt_closures_brine += 1 2030 self.salt_closures_brine_segment_list.append(iclosure) 2031 else: 2032 print( 2033 "Closure is salt bounded but does not have oil, gas or brine assigned" 2034 )
2036 def find_faulted_all_closures(self, closure_segment_list, closure_segments): 2037 for iclosure in closure_segment_list: 2038 i, j, k = np.where(closure_segments == iclosure) 2039 faults_within_closure = self.wide_faults[i, j, k] 2040 if faults_within_closure.max() > 0: 2041 self.faulted_all_closures[i, j, k] = 1.0 2042 self.n_fault_all_closures += 1 2043 self.fault_all_closures_segment_list.append(iclosure)
2045 def find_onlap_all_closures(self, closure_segment_list, closure_segments): 2046 for iclosure in closure_segment_list: 2047 i, j, k = np.where(closure_segments == iclosure) 2048 onlaps_within_closure = self.onlaps_upward[i, j, k] 2049 if onlaps_within_closure.max() > 0: 2050 self.onlap_all_closures[i, j, k] = 1.0 2051 self.n_onlap_all_closures += 1 2052 self.onlap_all_closures_segment_list.append(iclosure)
2054 def find_simple_all_closures(self, closure_segment_list, closure_segments): 2055 for iclosure in closure_segment_list: 2056 i, j, k = np.where(closure_segments == iclosure) 2057 faults_within_closure = self.wide_faults[i, j, k] 2058 onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:]) 2059 onlaps_within_closure = onlaps[i, j, k] 2060 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2061 self.simple_all_closures[i, j, k] = 1.0 2062 self.n_4way_all_closures += 1
2064 def find_false_all_closures(self, closure_segment_list, closure_segments): 2065 for iclosure in closure_segment_list: 2066 i, j, k = np.where(closure_segments == iclosure) 2067 faults_within_closure = self.fat_faults[i, j, k] 2068 onlaps_within_closure = self.onlaps_downward[i, j, k] 2069 if onlaps_within_closure.max() > 0: 2070 _faulted_closure_threshold = float( 2071 faults_within_closure[faults_within_closure > 0].size / i.size 2072 ) 2073 _onlap_closure_threshold = float( 2074 onlaps_within_closure[onlaps_within_closure > 0].size / i.size 2075 ) 2076 if ( 2077 _faulted_closure_threshold > 0.65 2078 and _onlap_closure_threshold > 0.65 2079 ): 2080 self.false_all_closures[i, j, k] = 1 2081 self.n_false_all_closures += 1
2083 def find_salt_bounded_all_closures(self, closure_segment_list, closure_segments): 2084 self._dilate_salt() 2085 for iclosure in closure_segment_list: 2086 i, j, k = np.where(closure_segments == iclosure) 2087 salt_within_closure = self.wide_salt[i, j, k] 2088 if salt_within_closure.max() > 0: 2089 self.salt_all_closures[i, j, k] = 1.0 2090 self.n_salt_all_closures += 1 2091 self.salt_all_closures_segment_list.append(iclosure)
2134 def grow_to_fault2( 2135 self, closures, grow_only_sand_closures=True, remove_small_closures=True 2136 ): 2137 # - grow closures laterally and up within layer and within fault block 2138 print( 2139 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2140 ) 2141 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2142 2143 # dilated_fault_closures = closures.copy() 2144 # n_faulted_closures = dilated_fault_closures.max() 2145 labels_clean = self.closure_segments[:].copy() 2146 labels_clean[closures == 0] = 0 2147 labels_clean_list = list(set(labels_clean.flatten())) 2148 labels_clean_list.remove(0) 2149 initial_closures = labels_clean.copy() 2150 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2151 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2152 2153 # TODO remove this once small closures are found and fixed 2154 voxel_sizes = [ 2155 self.closure_segments[self.closure_segments[:] == i].size 2156 for i in labels_clean_list 2157 ] 2158 for _v in voxel_sizes: 2159 print(f"Voxel_Sizes: {_v}") 2160 if _v < self.cfg.closure_min_voxels: 2161 print(_v) 2162 2163 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2164 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2165 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2166 _ng = self.faults.faulted_net_to_gross[:].copy() 2167 # Cannot solely use NG anymore since shales may have variable net to gross 2168 _lith = self.faults.faulted_lithology[:].copy() 2169 _age = self.faults.faulted_age_volume[:].copy() 2170 fault_throw = self.faults.max_fault_throw[:] 2171 2172 for il, i in enumerate(labels_clean_list): 2173 fault_blocks_list = list(set(fault_throw[labels_clean == i].flatten())) 2174 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2175 for jl, j in enumerate(fault_blocks_list): 2176 print( 2177 "\n\n ... label, throw = ", 2178 i, 2179 j, 2180 list(set(fault_throw[labels_clean == i].flatten())), 2181 labels_clean[labels_clean == i].size, 2182 fault_throw[fault_throw == j].size, 2183 fault_throw[ 2184 np.where((labels_clean == i) & (fault_throw[:] == j)) 2185 ].size, 2186 ) 2187 single_closure = labels_clean * 0.0 2188 size = single_closure[ 2189 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2190 ].size 2191 if size >= self.cfg.closure_min_voxels: 2192 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2193 single_closure[ 2194 np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25)) 2195 ] = 1 2196 if single_closure[single_closure > 0].size == 0: 2197 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2198 labels_clean[np.where(labels_clean == i)] = 0 2199 continue 2200 avg_ng = _ng[single_closure == 1].mean() 2201 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2202 _ng_voxels = _ng[single_closure == 1] 2203 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2204 min_geo_age = _geo_age_voxels.min() - 0.5 2205 avg_geo_age = int(_geo_age_voxels.mean()) 2206 max_geo_age = _geo_age_voxels.max() + 0.5 2207 _depth_geobody_voxels = depth_cube[single_closure == 1] 2208 min_depth = _depth_geobody_voxels.min() 2209 max_depth = _depth_geobody_voxels.max() 2210 avg_throw = np.median(fault_throw[single_closure == 1]) 2211 2212 closure_boundary_cube = closures * 0.0 2213 if grow_only_sand_closures: 2214 lith_flag = _lith > 0.0 2215 else: 2216 lith_flag = _lith >= 0.0 2217 closure_boundary_cube[ 2218 np.where( 2219 lith_flag 2220 & (_age > min_geo_age) 2221 & (_age < max_geo_age) 2222 & (fault_throw == avg_throw) 2223 & (depth_cube <= max_depth) 2224 ) 2225 ] = 1.0 2226 print( 2227 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2228 closure_boundary_cube[closure_boundary_cube == 1].size, 2229 ) 2230 2231 n_voxel = single_closure[single_closure == 1].size 2232 2233 original_voxels = n_voxel + 0 2234 print( 2235 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2236 i, 2237 j, 2238 n_voxel, 2239 (min_geo_age, avg_geo_age, max_geo_age), 2240 (min_depth, max_depth), 2241 avg_ng, 2242 il, 2243 " / ", 2244 len(labels_clean_list), 2245 ) 2246 2247 grown_closure = single_closure.copy() 2248 grown_closure[depth_cube >= max_depth] = 0 2249 delta_voxel = 0 2250 previous_delta_voxel = 1e9 2251 converged = False 2252 for ii in range(15): 2253 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2254 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2255 # stay within layer, within age, within fault block, above HCWC 2256 grown_closure[closure_boundary_cube == 0.0] = 0.0 2257 single_closure = single_closure + grown_closure 2258 single_closure[single_closure > 0] = i 2259 new_n_voxel = single_closure[single_closure > 0].size 2260 previous_delta_voxel = delta_voxel + 0 2261 delta_voxel = new_n_voxel - n_voxel 2262 print( 2263 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2264 " delta_voxel>previous_delta_voxel = ", 2265 i, 2266 ii, 2267 new_n_voxel, 2268 delta_voxel, 2269 previous_delta_voxel, 2270 delta_voxel > previous_delta_voxel, 2271 ) 2272 if n_voxel == new_n_voxel: 2273 # finish bottom voxel layer near "HCWC" 2274 grown_closure = self.grow_downward( 2275 grown_closure, 1, dist=1, verbose=False 2276 ) 2277 # stay within layer, within age, within fault block, above HCWC 2278 grown_closure[closure_boundary_cube == 0.0] = 0.0 2279 single_closure = single_closure + grown_closure 2280 single_closure[single_closure > 0] = i 2281 converged = True 2282 break 2283 else: 2284 n_voxel = new_n_voxel 2285 previous_delta_voxel = delta_voxel + 0 2286 if converged is True: 2287 labels_clean[single_closure > 0] = i 2288 msg_postscript = " converged" 2289 else: 2290 labels_clean[labels_clean == i] = -i 2291 msg_postscript = " NOT converged" 2292 msg = ( 2293 f"closure_id: {int(i):04d}, fault_id: {int(j + .5):04d}, " 2294 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2295 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2296 ) 2297 print(msg + msg_postscript) 2298 self.cfg.write_to_logfile(msg + msg_postscript) 2299 2300 # Set small closures to 0 after growth 2301 # _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2302 # for x in np.unique(_grown_labels): 2303 # size = _grown_labels[_grown_labels == x].size 2304 # print(f'Size before editing: {size}') 2305 # if size < self.cfg.closure_min_voxels: 2306 # labels_clean[_grown_labels == x] = 0. 2307 # for x in np.unique(labels_clean): 2308 # size = labels_clean[labels_clean == x].size 2309 # print(f'Size after editing: {size}') 2310 2311 if remove_small_closures: 2312 _initial_labels = measure.label( 2313 initial_closures, connectivity=2, background=0 2314 ) 2315 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2316 for x in np.unique(_grown_labels): 2317 size_initial = _initial_labels[_initial_labels == x].size 2318 size_grown = _grown_labels[_grown_labels == x].size 2319 print(f"Size before editing: {size_initial}") 2320 print(f"Size after editing: {size_grown}") 2321 if size_grown < self.cfg.closure_min_voxels: 2322 print( 2323 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2324 ) 2325 labels_clean[_grown_labels == x] = 0.0 2326 return labels_clean
2328 def grow_to_salt(self, closures): 2329 # - grow closures laterally and up within layer up to salt body 2330 print("\n\n ... grow_to_salt: grow closures laterally and up within layer ...") 2331 self.cfg.write_to_logfile("growing closures to salt body: grow_to_salt") 2332 2333 labels_clean = measure.label( 2334 self.closure_segments[:], connectivity=2, background=0 2335 ) 2336 labels_clean[closures == 0] = 0 2337 # labels_clean = self.closure_segments[:].copy() 2338 # labels_clean[closures == 0] = 0 2339 labels_clean_list = list(set(labels_clean.flatten())) 2340 labels_clean_list.remove(0) 2341 initial_closures = labels_clean.copy() 2342 print("\n ... grow_to_salt: n_salt_closures = ", len(labels_clean_list)) 2343 print(" ... grow_to_salt: salt_closures = ", labels_clean_list) 2344 2345 depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float) 2346 _depths = np.arange(self.faults.faulted_age_volume.shape[2]) 2347 depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2]) 2348 _ng = self.faults.faulted_net_to_gross[:].copy() 2349 _age = self.faults.faulted_age_volume[:].copy() 2350 salt = self.faults.salt_model.salt_segments[:] 2351 2352 for il, i in enumerate(labels_clean_list): 2353 salt_list = list(set(salt[labels_clean == i].flatten())) 2354 print(" ... grow_to_fault2: salt_list = ", salt_list) 2355 single_closure = labels_clean * 0.0 2356 size = single_closure[np.where(labels_clean == i)].size 2357 if size >= self.cfg.closure_min_voxels: 2358 print(f"Label: {i}, Voxel_Count: {size}") 2359 single_closure[np.where(labels_clean == i)] = 1 2360 if single_closure[single_closure > 0].size == 0: 2361 labels_clean[np.where(labels_clean == i)] = 0 2362 continue 2363 avg_ng = _ng[single_closure == 1].mean() 2364 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2365 _ng_voxels = _ng[single_closure == 1] 2366 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2367 min_geo_age = _geo_age_voxels.min() - 0.5 2368 avg_geo_age = int(_geo_age_voxels.mean()) 2369 max_geo_age = _geo_age_voxels.max() + 0.5 2370 _depth_geobody_voxels = depth_cube[single_closure == 1] 2371 min_depth = _depth_geobody_voxels.min() 2372 max_depth = _depth_geobody_voxels.max() 2373 # Define AOI where salt has been dilated 2374 # close_to_salt = np.zeros_like(salt) 2375 # close_to_salt[self.wide_salt[:] == 1] = 1.0 2376 # close_to_salt[salt == 1] = 0.0 2377 2378 closure_boundary_cube = closures * 0.0 2379 closure_boundary_cube[ 2380 np.where( 2381 (_ng > 0.3) 2382 & (_age > min_geo_age) # account for partial voxels 2383 & (_age < max_geo_age) 2384 & (salt == 0.0) 2385 & (depth_cube <= max_depth) 2386 ) 2387 ] = 1.0 2388 print( 2389 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2390 closure_boundary_cube[closure_boundary_cube == 1].size, 2391 ) 2392 2393 n_voxel = single_closure[single_closure == 1].size 2394 2395 original_voxels = n_voxel + 0 2396 print( 2397 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2398 i, 2399 n_voxel, 2400 (min_geo_age, avg_geo_age, max_geo_age), 2401 (min_depth, max_depth), 2402 avg_ng, 2403 il, 2404 " / ", 2405 len(labels_clean_list), 2406 ) 2407 2408 grown_closure = single_closure.copy() 2409 grown_closure[depth_cube >= max_depth] = 0 2410 delta_voxel = 0 2411 previous_delta_voxel = 1e9 2412 converged = False 2413 for ii in range(99): 2414 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2415 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2416 # stay within layer, within age, close to salt and above HCWC 2417 grown_closure[closure_boundary_cube == 0.0] = 0.0 2418 single_closure = single_closure + grown_closure 2419 single_closure[single_closure > 0] = i 2420 new_n_voxel = single_closure[single_closure > 0].size 2421 previous_delta_voxel = delta_voxel + 0 2422 delta_voxel = new_n_voxel - n_voxel 2423 print( 2424 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2425 " delta_voxel>previous_delta_voxel = ", 2426 i, 2427 ii, 2428 new_n_voxel, 2429 delta_voxel, 2430 previous_delta_voxel, 2431 delta_voxel > previous_delta_voxel, 2432 ) 2433 2434 # If grown voxel is touching the egde of survey, stop and remove closure 2435 _a, _b, _ = np.where(single_closure > 0) 2436 max_boundary_i = self.cfg.cube_shape[0] - 1 2437 max_boundary_j = self.cfg.cube_shape[1] - 1 2438 if ( 2439 np.min(_a) == 0 2440 or np.max(_a) == max_boundary_i 2441 or np.min(_b) == 0 2442 or np.max(_b) == max_boundary_j 2443 ): 2444 print("Boundary reached, removing closure") 2445 converged = False 2446 break 2447 2448 if n_voxel == new_n_voxel: 2449 # finish bottom voxel layer near HCWC 2450 grown_closure = self.grow_downward( 2451 grown_closure, 1, dist=1, verbose=False 2452 ) 2453 # stay within layer, within age, within fault block, above HCWC 2454 grown_closure[closure_boundary_cube == 0.0] = 0.0 2455 single_closure = single_closure + grown_closure 2456 single_closure[single_closure > 0] = i 2457 converged = True 2458 break 2459 else: 2460 n_voxel = new_n_voxel 2461 previous_delta_voxel = delta_voxel + 0 2462 if converged is True: 2463 labels_clean[single_closure > 0] = i 2464 msg_postscript = " converged" 2465 else: 2466 labels_clean[labels_clean == i] = -i 2467 msg_postscript = " NOT converged" 2468 msg = ( 2469 f"closure_id: {int(i):04d}, " 2470 + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, " 2471 + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}" 2472 ) 2473 print(msg + msg_postscript) 2474 self.cfg.write_to_logfile(msg + msg_postscript) 2475 2476 # Set small closures to 0 after growth 2477 _initial_labels = measure.label(initial_closures, connectivity=2, background=0) 2478 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2479 for x in np.unique(_grown_labels)[ 2480 1: 2481 ]: # ignore the first label of 0 (closures only) 2482 size_initial = _initial_labels[_initial_labels == x].size 2483 size_grown = _grown_labels[_grown_labels == x].size 2484 print(f"Size before editing: {size_initial}") 2485 print(f"Size after editing: {size_grown}") 2486 if size_grown < self.cfg.closure_min_voxels: 2487 print( 2488 f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed" 2489 ) 2490 labels_clean[_grown_labels == x] = 0.0 2491 2492 return labels_clean
2494 @staticmethod 2495 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2496 from scipy.ndimage.morphology import grey_dilation 2497 2498 dist_size = 2 * dist + 1 2499 mask = np.zeros((dist_size, dist_size, 1)) 2500 mask[:, :, :] = 1 2501 _geobody = geobody.copy() 2502 if verbose: 2503 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2504 for k in range(iterations): 2505 try: 2506 _geobody = grey_dilation(_geobody, footprint=mask) 2507 if verbose: 2508 print( 2509 " ...grow_lateral: k, _geobody.shape = ", 2510 k, 2511 _geobody[_geobody > 0].shape, 2512 ) 2513 except: 2514 break 2515 return _geobody
2517 @staticmethod 2518 def grow_upward(geobody, iterations, dist=1, verbose=False): 2519 from scipy.ndimage.morphology import grey_dilation 2520 2521 dist_size = 2 * dist + 1 2522 mask = np.zeros((1, 1, dist_size)) 2523 mask[0, 0, : dist + 1] = 1 2524 _geobody = geobody.copy() 2525 if verbose: 2526 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2527 for k in range(iterations): 2528 try: 2529 _geobody = grey_dilation(_geobody, footprint=mask) 2530 if verbose: 2531 print( 2532 " ...grow_upward: k, _geobody.shape = ", 2533 k, 2534 _geobody[_geobody > 0].shape, 2535 ) 2536 except: 2537 break 2538 return _geobody
2540 @staticmethod 2541 def grow_downward(geobody, iterations, dist=1, verbose=False): 2542 from scipy.ndimage.morphology import grey_dilation 2543 2544 dist_size = 2 * dist + 1 2545 mask = np.zeros((1, 1, dist_size)) 2546 mask[0, 0, dist:] = 1 2547 _geobody = geobody.copy() 2548 if verbose: 2549 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 2550 for k in range(iterations): 2551 try: 2552 _geobody = grey_dilation(_geobody, footprint=mask) 2553 if verbose: 2554 print( 2555 " ...grow_downward: k, _geobody.shape = ", 2556 k, 2557 _geobody[_geobody > 0].shape, 2558 ) 2559 except: 2560 break 2561 return _geobody
Inherited Members
2579class Intersect3D(Closures): 2580 def __init__( 2581 self, 2582 faults, 2583 onlaps, 2584 oil_closures, 2585 gas_closures, 2586 brine_closures, 2587 closure_segment_list, 2588 closure_segments, 2589 parameters, 2590 ): 2591 self.closure_segment_list = closure_segment_list 2592 self.closure_segments = closure_segments 2593 self.cfg = parameters 2594 2595 self.fault_throw = faults.max_fault_throw 2596 self.geologic_age = faults.faulted_age_volume 2597 self.geomodel_ng = faults.faulted_net_to_gross 2598 self.faults = self._threshold_volumes(faults.fault_planes.copy()) 2599 self.onlaps = self._threshold_volumes(onlaps.copy()) 2600 self.oil_closures = self._threshold_volumes(oil_closures.copy()) 2601 self.gas_closures = self._threshold_volumes(gas_closures.copy()) 2602 self.brine_closures = self._threshold_volumes(brine_closures.copy()) 2603 2604 self.wide_faults = None 2605 self.fat_faults = None 2606 self.onlaps_upward = None 2607 self.onlaps_downward = None 2608 self._dilate_faults_and_onlaps() 2609 2610 # Outputs 2611 self.faulted_closures_oil = np.zeros_like(self.oil_closures) 2612 self.faulted_closures_gas = np.zeros_like(self.gas_closures) 2613 self.faulted_closures_brine = np.zeros_like(self.brine_closures) 2614 self.fault_closures_oil_segment_list = list() 2615 self.fault_closures_gas_segment_list = list() 2616 self.fault_closures_brine_segment_list = list() 2617 self.n_fault_closures_oil = 0 2618 self.n_fault_closures_gas = 0 2619 self.n_fault_closures_brine = 0 2620 2621 self.onlap_closures_oil = np.zeros_like(self.oil_closures) 2622 self.onlap_closures_gas = np.zeros_like(self.gas_closures) 2623 self.onlap_closures_brine = np.zeros_like(self.brine_closures) 2624 self.onlap_closures_oil_segment_list = list() 2625 self.onlap_closures_gas_segment_list = list() 2626 self.onlap_closures_brine_segment_list = list() 2627 self.n_onlap_closures_oil = 0 2628 self.n_onlap_closures_gas = 0 2629 self.n_onlap_closures_brine = 0 2630 2631 self.simple_closures_oil = np.zeros_like(self.oil_closures) 2632 self.simple_closures_gas = np.zeros_like(self.gas_closures) 2633 self.simple_closures_brine = np.zeros_like(self.brine_closures) 2634 self.n_4way_closures_oil = 0 2635 self.n_4way_closures_gas = 0 2636 self.n_4way_closures_brine = 0 2637 2638 self.false_closures_oil = np.zeros_like(self.oil_closures) 2639 self.false_closures_gas = np.zeros_like(self.gas_closures) 2640 self.false_closures_brine = np.zeros_like(self.brine_closures) 2641 self.n_false_closures_oil = 0 2642 self.n_false_closures_gas = 0 2643 self.n_false_closures_brine = 0 2644 2645 def find_faulted_closures(self): 2646 for iclosure in self.closure_segment_list: 2647 i, j, k = np.where(self.closure_segments == iclosure) 2648 faults_within_closure = self.wide_faults[i, j, k] 2649 if faults_within_closure.max() > 0: 2650 if self.oil_closures[i, j, k].max() > 0: 2651 # Faulted oil closure 2652 self.faulted_closures_oil[i, j, k] = 1.0 2653 self.n_fault_closures_oil += 1 2654 self.fault_closures_oil_segment_list.append(iclosure) 2655 elif self.gas_closures[i, j, k].max() > 0: 2656 # Faulted gas closure 2657 self.faulted_closures_gas[i, j, k] = 1.0 2658 self.n_fault_closures_gas += 1 2659 self.fault_closures_gas_segment_list.append(iclosure) 2660 elif self.brine_closures[i, j, k].max() > 0: 2661 # Faulted brine closure 2662 self.faulted_closures_brine[i, j, k] = 1.0 2663 self.n_fault_closures_brine += 1 2664 self.fault_closures_brine_segment_list.append(iclosure) 2665 else: 2666 print( 2667 "Closure is faulted but does not have oil, gas or brine assigned" 2668 ) 2669 2670 def find_onlap_closures(self): 2671 for iclosure in self.closure_segment_list: 2672 i, j, k = np.where(self.closure_segments == iclosure) 2673 onlaps_within_closure = self.onlaps_upward[i, j, k] 2674 if onlaps_within_closure.max() > 0: 2675 if self.oil_closures[i, j, k].max() > 0: 2676 self.onlap_closures_oil[i, j, k] = 1.0 2677 self.n_onlap_closures_oil += 1 2678 self.onlap_closures_oil_segment_list.append(iclosure) 2679 elif self.gas_closures[i, j, k].max() > 0: 2680 self.onlap_closures_gas[i, j, k] = 1.0 2681 self.n_onlap_closures_gas += 1 2682 self.onlap_closures_gas_segment_list.append(iclosure) 2683 elif self.brine_closures[i, j, k].max() > 0: 2684 self.onlap_closures_brine[i, j, k] = 1.0 2685 self.n_onlap_closures_brine += 1 2686 self.onlap_closures_brine_segment_list.append(iclosure) 2687 else: 2688 print( 2689 "Closure is onlap but does not have oil, gas or brine assigned" 2690 ) 2691 2692 def find_simple_closures(self): 2693 for iclosure in self.closure_segment_list: 2694 i, j, k = np.where(self.closure_segments == iclosure) 2695 faults_within_closure = self.wide_faults[i, j, k] 2696 onlaps_within_closure = self.onlaps[i, j, k] 2697 oil_within_closure = self.oil_closures[i, j, k] 2698 gas_within_closure = self.gas_closures[i, j, k] 2699 brine_within_closure = self.brine_closures[i, j, k] 2700 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2701 if oil_within_closure.max() > 0: 2702 self.simple_closures_oil[i, j, k] = 1.0 2703 self.n_4way_closures_oil += 1 2704 elif gas_within_closure.max() > 0: 2705 self.simple_closures_gas[i, j, k] = 1.0 2706 self.n_4way_closures_gas += 1 2707 elif brine_within_closure.max() > 0: 2708 self.simple_closures_brine[i, j, k] = 1.0 2709 self.n_4way_closures_brine += 1 2710 else: 2711 print( 2712 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 2713 ) 2714 2715 def find_false_closures(self): 2716 for iclosure in self.closure_segment_list: 2717 i, j, k = np.where(self.closure_segments == iclosure) 2718 faults_within_closure = self.fat_faults[i, j, k] 2719 onlaps_within_closure = self.onlaps_downward[i, j, k] 2720 for fluid, false, num in zip( 2721 [self.oil_closures, self.gas_closures, self.brine_closures], 2722 [ 2723 self.false_closures_oil, 2724 self.false_closures_gas, 2725 self.false_closures_brine, 2726 ], 2727 [ 2728 self.n_false_closures_oil, 2729 self.n_false_closures_gas, 2730 self.n_false_closures_brine, 2731 ], 2732 ): 2733 fluid_within_closure = fluid[i, j, k] 2734 if fluid_within_closure.max() > 0: 2735 if onlaps_within_closure.max() > 0: 2736 _faulted_closure_threshold = float( 2737 faults_within_closure[faults_within_closure > 0].size 2738 / fluid_within_closure[fluid_within_closure > 0].size 2739 ) 2740 _onlap_closure_threshold = float( 2741 onlaps_within_closure[onlaps_within_closure > 0].size 2742 / fluid_within_closure[fluid_within_closure > 0].size 2743 ) 2744 if ( 2745 _faulted_closure_threshold > 0.65 2746 and _onlap_closure_threshold > 0.65 2747 ): 2748 false[i, j, k] = 1 2749 num += 1 2750 2751 def grow_to_fault2(self, closures): 2752 # - grow closures laterally and up within layer and within fault block 2753 print( 2754 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2755 ) 2756 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2757 2758 dilated_fault_closures = closures.copy() 2759 n_faulted_closures = dilated_fault_closures.max() 2760 labels_clean = self.closure_segments.copy() 2761 labels_clean[closures == 0] = 0 2762 labels_clean_list = list(set(labels_clean.flatten())) 2763 labels_clean_list.remove(0) 2764 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2765 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2766 2767 # fixme remove this once small closures are found and fixed 2768 voxel_sizes = [ 2769 self.closure_segments[self.closure_segments == i].size 2770 for i in labels_clean_list 2771 ] 2772 for _v in voxel_sizes: 2773 print(f"Voxel_Sizes: {_v}") 2774 if _v < self.cfg.closure_min_voxels: 2775 print(_v) 2776 2777 depth_cube = np.zeros(self.geologic_age.shape, float) 2778 _depths = np.arange(self.geologic_age.shape[2]) 2779 depth_cube += _depths.reshape(1, 1, self.geologic_age.shape[2]) 2780 _ng = self.geomodel_ng.copy() 2781 _age = self.geologic_age.copy() 2782 2783 for il, i in enumerate(labels_clean_list): 2784 fault_blocks_list = list(set(self.fault_throw[labels_clean == i].flatten())) 2785 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2786 for jl, j in enumerate(fault_blocks_list): 2787 print( 2788 "\n\n ... label, throw = ", 2789 i, 2790 j, 2791 list(set(self.fault_throw[labels_clean == i].flatten())), 2792 labels_clean[labels_clean == i].size, 2793 self.fault_throw[self.fault_throw == j].size, 2794 self.fault_throw[ 2795 np.where((labels_clean == i) & (self.fault_throw == j)) 2796 ].size, 2797 ) 2798 single_closure = labels_clean * 0.0 2799 size = single_closure[ 2800 np.where( 2801 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2802 ) 2803 ].size 2804 if size >= self.cfg.closure_min_voxels: 2805 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2806 single_closure[ 2807 np.where( 2808 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2809 ) 2810 ] = 1 2811 if single_closure[single_closure > 0].size == 0: 2812 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2813 labels_clean[np.where(labels_clean == i)] = 0 2814 continue 2815 avg_ng = _ng[single_closure == 1].mean() 2816 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2817 _ng_voxels = _ng[single_closure == 1] 2818 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2819 min_geo_age = _geo_age_voxels.min() - 0.5 2820 avg_geo_age = int(_geo_age_voxels.mean()) 2821 max_geo_age = _geo_age_voxels.max() + 0.5 2822 _depth_geobody_voxels = depth_cube[single_closure == 1] 2823 min_depth = _depth_geobody_voxels.min() 2824 max_depth = _depth_geobody_voxels.max() 2825 avg_throw = np.median(self.fault_throw[single_closure == 1]) 2826 2827 closure_boundary_cube = closures * 0.0 2828 closure_boundary_cube[ 2829 np.where( 2830 (_ng > 0.0) 2831 & (_age > min_geo_age) 2832 & (_age < max_geo_age) 2833 & (self.fault_throw == avg_throw) 2834 & (depth_cube <= max_depth) 2835 ) 2836 ] = 1.0 2837 print( 2838 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2839 closure_boundary_cube[closure_boundary_cube == 1].size, 2840 ) 2841 2842 n_voxel = single_closure[single_closure == 1].size 2843 2844 original_voxels = n_voxel + 0 2845 print( 2846 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2847 i, 2848 j, 2849 n_voxel, 2850 (min_geo_age, avg_geo_age, max_geo_age), 2851 (min_depth, max_depth), 2852 avg_ng, 2853 il, 2854 " / ", 2855 len(labels_clean_list), 2856 ) 2857 2858 grown_closure = single_closure.copy() 2859 grown_closure[depth_cube >= max_depth] = 0 2860 delta_voxel = 0 2861 previous_delta_voxel = 1e9 2862 converged = False 2863 for ii in range(15): 2864 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2865 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2866 # stay within layer, within age, within fault block, above HCWC 2867 grown_closure[closure_boundary_cube == 0.0] = 0.0 2868 single_closure = single_closure + grown_closure 2869 single_closure[single_closure > 0] = i 2870 new_n_voxel = single_closure[single_closure > 0].size 2871 previous_delta_voxel = delta_voxel + 0 2872 delta_voxel = new_n_voxel - n_voxel 2873 print( 2874 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2875 " delta_voxel>previous_delta_voxel = ", 2876 i, 2877 ii, 2878 new_n_voxel, 2879 delta_voxel, 2880 previous_delta_voxel, 2881 delta_voxel > previous_delta_voxel, 2882 ) 2883 if n_voxel == new_n_voxel: 2884 # finish bottom voxel layer near "HCWC" 2885 grown_closure = self.grow_downward( 2886 grown_closure, 1, dist=1, verbose=False 2887 ) 2888 # stay within layer, within age, within fault block, above HCWC 2889 grown_closure[closure_boundary_cube == 0.0] = 0.0 2890 single_closure = single_closure + grown_closure 2891 single_closure[single_closure > 0] = i 2892 converged = True 2893 break 2894 else: 2895 n_voxel = new_n_voxel 2896 previous_delta_voxel = delta_voxel + 0 2897 if converged is True: 2898 labels_clean[single_closure > 0] = i 2899 msg_postscript = " converged" 2900 else: 2901 labels_clean[labels_clean == i] = -i 2902 msg_postscript = " NOT converged" 2903 msg = ( 2904 "closure_id: " 2905 + format(i, "4d") 2906 + ", fault_id: " 2907 + format(int(j + 0.5), "4d") 2908 + ", original_voxels: " 2909 + format(original_voxels, "11,.0f") 2910 + ", new_n_voxel: " 2911 + format(new_n_voxel, "11,.0f") 2912 + ", percent_growth: " 2913 + format(float(new_n_voxel) / original_voxels, "6.2f") 2914 ) 2915 print(msg + msg_postscript) 2916 self.cfg.write_to_logfile(msg + msg_postscript) 2917 2918 # Set small closures to 0 after growth 2919 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2920 for x in np.unique(_grown_labels): 2921 size = _grown_labels[_grown_labels == x].size 2922 print(f"Size before editing: {size}") 2923 if size < self.cfg.closure_min_voxels: 2924 labels_clean[_grown_labels == x] = 0.0 2925 for x in np.unique(labels_clean): 2926 size = labels_clean[labels_clean == x].size 2927 print(f"Size after editing: {size}") 2928 2929 return labels_clean 2930 2931 def _dilate_faults_and_onlaps(self): 2932 self.wide_faults = self.grow_lateral(self.faults, 9, dist=1, verbose=False) 2933 self.fat_faults = self.grow_lateral(self.faults, 21, dist=1, verbose=False) 2934 mask = np.zeros((1, 1, 3)) 2935 mask[0, 0, :2] = 1 2936 self.onlaps_upward = morphology.binary_dilation(self.onlaps, mask) 2937 mask = np.zeros((1, 1, 3)) 2938 mask[0, 0, 1:] = 1 2939 self.onlaps_downward = self.onlaps.copy() 2940 for k in range(30): 2941 try: 2942 self.onlaps_downward = morphology.binary_dilation( 2943 self.onlaps_downward, mask 2944 ) 2945 except: 2946 break 2947 2948 @staticmethod 2949 def _threshold_volumes(volume, threshold=0.5): 2950 volume[volume >= threshold] = 1.0 2951 volume[volume < threshold] = 0.0 2952 return volume 2953 2954 @staticmethod 2955 def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False): 2956 from scipy.ndimage import maximum_filter 2957 2958 hdist_size = 2 * hdist + 1 2959 vdist_size = 2 * vdist + 1 2960 mask = np.zeros((hdist_size, hdist_size, vdist_size)) 2961 mask[:, :, : vdist + 1] = 1 2962 _geobody = geobody.copy() 2963 if verbose: 2964 print( 2965 " ...grow_up_and_lateral: _geobody.shape = ", 2966 _geobody[_geobody > 0].shape, 2967 ) 2968 for k in range(iterations): 2969 try: 2970 _geobody = maximum_filter(_geobody, footprint=mask) 2971 if verbose: 2972 print( 2973 " ...grow_up_and_lateral: k, _geobody.shape = ", 2974 k, 2975 _geobody[_geobody > 0].shape, 2976 ) 2977 except: 2978 break 2979 return _geobody 2980 2981 @staticmethod 2982 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2983 from scipy.ndimage.morphology import grey_dilation 2984 2985 dist_size = 2 * dist + 1 2986 mask = np.zeros((dist_size, dist_size, 1)) 2987 mask[:, :, :] = 1 2988 _geobody = geobody.copy() 2989 if verbose: 2990 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2991 for k in range(iterations): 2992 try: 2993 _geobody = grey_dilation(_geobody, footprint=mask) 2994 if verbose: 2995 print( 2996 " ...grow_lateral: k, _geobody.shape = ", 2997 k, 2998 _geobody[_geobody > 0].shape, 2999 ) 3000 except: 3001 break 3002 return _geobody 3003 3004 @staticmethod 3005 def grow_upward(geobody, iterations, dist=1, verbose=False): 3006 from scipy.ndimage.morphology import grey_dilation 3007 3008 dist_size = 2 * dist + 1 3009 mask = np.zeros((1, 1, dist_size)) 3010 mask[0, 0, : dist + 1] = 1 3011 _geobody = geobody.copy() 3012 if verbose: 3013 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3014 for k in range(iterations): 3015 try: 3016 _geobody = grey_dilation(_geobody, footprint=mask) 3017 if verbose: 3018 print( 3019 " ...grow_upward: k, _geobody.shape = ", 3020 k, 3021 _geobody[_geobody > 0].shape, 3022 ) 3023 except: 3024 break 3025 return _geobody 3026 3027 @staticmethod 3028 def grow_downward(geobody, iterations, dist=1, verbose=False): 3029 from scipy.ndimage.morphology import grey_dilation 3030 3031 dist_size = 2 * dist + 1 3032 mask = np.zeros((1, 1, dist_size)) 3033 mask[0, 0, dist:] = 1 3034 _geobody = geobody.copy() 3035 if verbose: 3036 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3037 for k in range(iterations): 3038 try: 3039 _geobody = grey_dilation(_geobody, footprint=mask) 3040 if verbose: 3041 print( 3042 " ...grow_downward: k, _geobody.shape = ", 3043 k, 3044 _geobody[_geobody > 0].shape, 3045 ) 3046 except: 3047 break 3048 return _geobody
Geomodel
The class of the Geomodel object.
This class contains all the items that make up the Geologic model.
Parameters
- parameters (datagenerator.Parameters): Parameter object storing all model parameters.
- depth_maps (np.ndarray): A numpy array containing the depth maps.
- onlap_horizon_list (list): A list of the onlap horizons.
- facies (np.ndarray): The generated facies.
Returns
- None
2580 def __init__( 2581 self, 2582 faults, 2583 onlaps, 2584 oil_closures, 2585 gas_closures, 2586 brine_closures, 2587 closure_segment_list, 2588 closure_segments, 2589 parameters, 2590 ): 2591 self.closure_segment_list = closure_segment_list 2592 self.closure_segments = closure_segments 2593 self.cfg = parameters 2594 2595 self.fault_throw = faults.max_fault_throw 2596 self.geologic_age = faults.faulted_age_volume 2597 self.geomodel_ng = faults.faulted_net_to_gross 2598 self.faults = self._threshold_volumes(faults.fault_planes.copy()) 2599 self.onlaps = self._threshold_volumes(onlaps.copy()) 2600 self.oil_closures = self._threshold_volumes(oil_closures.copy()) 2601 self.gas_closures = self._threshold_volumes(gas_closures.copy()) 2602 self.brine_closures = self._threshold_volumes(brine_closures.copy()) 2603 2604 self.wide_faults = None 2605 self.fat_faults = None 2606 self.onlaps_upward = None 2607 self.onlaps_downward = None 2608 self._dilate_faults_and_onlaps() 2609 2610 # Outputs 2611 self.faulted_closures_oil = np.zeros_like(self.oil_closures) 2612 self.faulted_closures_gas = np.zeros_like(self.gas_closures) 2613 self.faulted_closures_brine = np.zeros_like(self.brine_closures) 2614 self.fault_closures_oil_segment_list = list() 2615 self.fault_closures_gas_segment_list = list() 2616 self.fault_closures_brine_segment_list = list() 2617 self.n_fault_closures_oil = 0 2618 self.n_fault_closures_gas = 0 2619 self.n_fault_closures_brine = 0 2620 2621 self.onlap_closures_oil = np.zeros_like(self.oil_closures) 2622 self.onlap_closures_gas = np.zeros_like(self.gas_closures) 2623 self.onlap_closures_brine = np.zeros_like(self.brine_closures) 2624 self.onlap_closures_oil_segment_list = list() 2625 self.onlap_closures_gas_segment_list = list() 2626 self.onlap_closures_brine_segment_list = list() 2627 self.n_onlap_closures_oil = 0 2628 self.n_onlap_closures_gas = 0 2629 self.n_onlap_closures_brine = 0 2630 2631 self.simple_closures_oil = np.zeros_like(self.oil_closures) 2632 self.simple_closures_gas = np.zeros_like(self.gas_closures) 2633 self.simple_closures_brine = np.zeros_like(self.brine_closures) 2634 self.n_4way_closures_oil = 0 2635 self.n_4way_closures_gas = 0 2636 self.n_4way_closures_brine = 0 2637 2638 self.false_closures_oil = np.zeros_like(self.oil_closures) 2639 self.false_closures_gas = np.zeros_like(self.gas_closures) 2640 self.false_closures_brine = np.zeros_like(self.brine_closures) 2641 self.n_false_closures_oil = 0 2642 self.n_false_closures_gas = 0 2643 self.n_false_closures_brine = 0
__init__
Initializer for the Geomodel class.
Parameters
- parameters (datagenerator.Parameters): Parameter object storing all model parameters.
- depth_maps (np.ndarray): A numpy array containing the depth maps.
- onlap_horizon_list (list): A list of the onlap horizons.
- facies (np.ndarray): The generated facies.
2645 def find_faulted_closures(self): 2646 for iclosure in self.closure_segment_list: 2647 i, j, k = np.where(self.closure_segments == iclosure) 2648 faults_within_closure = self.wide_faults[i, j, k] 2649 if faults_within_closure.max() > 0: 2650 if self.oil_closures[i, j, k].max() > 0: 2651 # Faulted oil closure 2652 self.faulted_closures_oil[i, j, k] = 1.0 2653 self.n_fault_closures_oil += 1 2654 self.fault_closures_oil_segment_list.append(iclosure) 2655 elif self.gas_closures[i, j, k].max() > 0: 2656 # Faulted gas closure 2657 self.faulted_closures_gas[i, j, k] = 1.0 2658 self.n_fault_closures_gas += 1 2659 self.fault_closures_gas_segment_list.append(iclosure) 2660 elif self.brine_closures[i, j, k].max() > 0: 2661 # Faulted brine closure 2662 self.faulted_closures_brine[i, j, k] = 1.0 2663 self.n_fault_closures_brine += 1 2664 self.fault_closures_brine_segment_list.append(iclosure) 2665 else: 2666 print( 2667 "Closure is faulted but does not have oil, gas or brine assigned" 2668 )
2670 def find_onlap_closures(self): 2671 for iclosure in self.closure_segment_list: 2672 i, j, k = np.where(self.closure_segments == iclosure) 2673 onlaps_within_closure = self.onlaps_upward[i, j, k] 2674 if onlaps_within_closure.max() > 0: 2675 if self.oil_closures[i, j, k].max() > 0: 2676 self.onlap_closures_oil[i, j, k] = 1.0 2677 self.n_onlap_closures_oil += 1 2678 self.onlap_closures_oil_segment_list.append(iclosure) 2679 elif self.gas_closures[i, j, k].max() > 0: 2680 self.onlap_closures_gas[i, j, k] = 1.0 2681 self.n_onlap_closures_gas += 1 2682 self.onlap_closures_gas_segment_list.append(iclosure) 2683 elif self.brine_closures[i, j, k].max() > 0: 2684 self.onlap_closures_brine[i, j, k] = 1.0 2685 self.n_onlap_closures_brine += 1 2686 self.onlap_closures_brine_segment_list.append(iclosure) 2687 else: 2688 print( 2689 "Closure is onlap but does not have oil, gas or brine assigned" 2690 )
2692 def find_simple_closures(self): 2693 for iclosure in self.closure_segment_list: 2694 i, j, k = np.where(self.closure_segments == iclosure) 2695 faults_within_closure = self.wide_faults[i, j, k] 2696 onlaps_within_closure = self.onlaps[i, j, k] 2697 oil_within_closure = self.oil_closures[i, j, k] 2698 gas_within_closure = self.gas_closures[i, j, k] 2699 brine_within_closure = self.brine_closures[i, j, k] 2700 if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0: 2701 if oil_within_closure.max() > 0: 2702 self.simple_closures_oil[i, j, k] = 1.0 2703 self.n_4way_closures_oil += 1 2704 elif gas_within_closure.max() > 0: 2705 self.simple_closures_gas[i, j, k] = 1.0 2706 self.n_4way_closures_gas += 1 2707 elif brine_within_closure.max() > 0: 2708 self.simple_closures_brine[i, j, k] = 1.0 2709 self.n_4way_closures_brine += 1 2710 else: 2711 print( 2712 "Closure is not faulted or onlap but does not have oil, gas or brine assigned" 2713 )
2715 def find_false_closures(self): 2716 for iclosure in self.closure_segment_list: 2717 i, j, k = np.where(self.closure_segments == iclosure) 2718 faults_within_closure = self.fat_faults[i, j, k] 2719 onlaps_within_closure = self.onlaps_downward[i, j, k] 2720 for fluid, false, num in zip( 2721 [self.oil_closures, self.gas_closures, self.brine_closures], 2722 [ 2723 self.false_closures_oil, 2724 self.false_closures_gas, 2725 self.false_closures_brine, 2726 ], 2727 [ 2728 self.n_false_closures_oil, 2729 self.n_false_closures_gas, 2730 self.n_false_closures_brine, 2731 ], 2732 ): 2733 fluid_within_closure = fluid[i, j, k] 2734 if fluid_within_closure.max() > 0: 2735 if onlaps_within_closure.max() > 0: 2736 _faulted_closure_threshold = float( 2737 faults_within_closure[faults_within_closure > 0].size 2738 / fluid_within_closure[fluid_within_closure > 0].size 2739 ) 2740 _onlap_closure_threshold = float( 2741 onlaps_within_closure[onlaps_within_closure > 0].size 2742 / fluid_within_closure[fluid_within_closure > 0].size 2743 ) 2744 if ( 2745 _faulted_closure_threshold > 0.65 2746 and _onlap_closure_threshold > 0.65 2747 ): 2748 false[i, j, k] = 1 2749 num += 1
2751 def grow_to_fault2(self, closures): 2752 # - grow closures laterally and up within layer and within fault block 2753 print( 2754 "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..." 2755 ) 2756 self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2") 2757 2758 dilated_fault_closures = closures.copy() 2759 n_faulted_closures = dilated_fault_closures.max() 2760 labels_clean = self.closure_segments.copy() 2761 labels_clean[closures == 0] = 0 2762 labels_clean_list = list(set(labels_clean.flatten())) 2763 labels_clean_list.remove(0) 2764 print("\n ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list)) 2765 print(" ... grow_to_fault2: faulted_closures = ", labels_clean_list) 2766 2767 # fixme remove this once small closures are found and fixed 2768 voxel_sizes = [ 2769 self.closure_segments[self.closure_segments == i].size 2770 for i in labels_clean_list 2771 ] 2772 for _v in voxel_sizes: 2773 print(f"Voxel_Sizes: {_v}") 2774 if _v < self.cfg.closure_min_voxels: 2775 print(_v) 2776 2777 depth_cube = np.zeros(self.geologic_age.shape, float) 2778 _depths = np.arange(self.geologic_age.shape[2]) 2779 depth_cube += _depths.reshape(1, 1, self.geologic_age.shape[2]) 2780 _ng = self.geomodel_ng.copy() 2781 _age = self.geologic_age.copy() 2782 2783 for il, i in enumerate(labels_clean_list): 2784 fault_blocks_list = list(set(self.fault_throw[labels_clean == i].flatten())) 2785 print(" ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list) 2786 for jl, j in enumerate(fault_blocks_list): 2787 print( 2788 "\n\n ... label, throw = ", 2789 i, 2790 j, 2791 list(set(self.fault_throw[labels_clean == i].flatten())), 2792 labels_clean[labels_clean == i].size, 2793 self.fault_throw[self.fault_throw == j].size, 2794 self.fault_throw[ 2795 np.where((labels_clean == i) & (self.fault_throw == j)) 2796 ].size, 2797 ) 2798 single_closure = labels_clean * 0.0 2799 size = single_closure[ 2800 np.where( 2801 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2802 ) 2803 ].size 2804 if size >= self.cfg.closure_min_voxels: 2805 print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}") 2806 single_closure[ 2807 np.where( 2808 (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25) 2809 ) 2810 ] = 1 2811 if single_closure[single_closure > 0].size == 0: 2812 # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0 2813 labels_clean[np.where(labels_clean == i)] = 0 2814 continue 2815 avg_ng = _ng[single_closure == 1].mean() 2816 _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int") 2817 _ng_voxels = _ng[single_closure == 1] 2818 _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0] 2819 min_geo_age = _geo_age_voxels.min() - 0.5 2820 avg_geo_age = int(_geo_age_voxels.mean()) 2821 max_geo_age = _geo_age_voxels.max() + 0.5 2822 _depth_geobody_voxels = depth_cube[single_closure == 1] 2823 min_depth = _depth_geobody_voxels.min() 2824 max_depth = _depth_geobody_voxels.max() 2825 avg_throw = np.median(self.fault_throw[single_closure == 1]) 2826 2827 closure_boundary_cube = closures * 0.0 2828 closure_boundary_cube[ 2829 np.where( 2830 (_ng > 0.0) 2831 & (_age > min_geo_age) 2832 & (_age < max_geo_age) 2833 & (self.fault_throw == avg_throw) 2834 & (depth_cube <= max_depth) 2835 ) 2836 ] = 1.0 2837 print( 2838 "\n ... grow_to_fault2: closure_boundary_cube voxels = ", 2839 closure_boundary_cube[closure_boundary_cube == 1].size, 2840 ) 2841 2842 n_voxel = single_closure[single_closure == 1].size 2843 2844 original_voxels = n_voxel + 0 2845 print( 2846 "\n ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ", 2847 i, 2848 j, 2849 n_voxel, 2850 (min_geo_age, avg_geo_age, max_geo_age), 2851 (min_depth, max_depth), 2852 avg_ng, 2853 il, 2854 " / ", 2855 len(labels_clean_list), 2856 ) 2857 2858 grown_closure = single_closure.copy() 2859 grown_closure[depth_cube >= max_depth] = 0 2860 delta_voxel = 0 2861 previous_delta_voxel = 1e9 2862 converged = False 2863 for ii in range(15): 2864 grown_closure = self.grow_lateral(grown_closure, 1, dist=2) 2865 grown_closure = self.grow_upward(grown_closure, 1, dist=1) 2866 # stay within layer, within age, within fault block, above HCWC 2867 grown_closure[closure_boundary_cube == 0.0] = 0.0 2868 single_closure = single_closure + grown_closure 2869 single_closure[single_closure > 0] = i 2870 new_n_voxel = single_closure[single_closure > 0].size 2871 previous_delta_voxel = delta_voxel + 0 2872 delta_voxel = new_n_voxel - n_voxel 2873 print( 2874 " ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel," 2875 " delta_voxel>previous_delta_voxel = ", 2876 i, 2877 ii, 2878 new_n_voxel, 2879 delta_voxel, 2880 previous_delta_voxel, 2881 delta_voxel > previous_delta_voxel, 2882 ) 2883 if n_voxel == new_n_voxel: 2884 # finish bottom voxel layer near "HCWC" 2885 grown_closure = self.grow_downward( 2886 grown_closure, 1, dist=1, verbose=False 2887 ) 2888 # stay within layer, within age, within fault block, above HCWC 2889 grown_closure[closure_boundary_cube == 0.0] = 0.0 2890 single_closure = single_closure + grown_closure 2891 single_closure[single_closure > 0] = i 2892 converged = True 2893 break 2894 else: 2895 n_voxel = new_n_voxel 2896 previous_delta_voxel = delta_voxel + 0 2897 if converged is True: 2898 labels_clean[single_closure > 0] = i 2899 msg_postscript = " converged" 2900 else: 2901 labels_clean[labels_clean == i] = -i 2902 msg_postscript = " NOT converged" 2903 msg = ( 2904 "closure_id: " 2905 + format(i, "4d") 2906 + ", fault_id: " 2907 + format(int(j + 0.5), "4d") 2908 + ", original_voxels: " 2909 + format(original_voxels, "11,.0f") 2910 + ", new_n_voxel: " 2911 + format(new_n_voxel, "11,.0f") 2912 + ", percent_growth: " 2913 + format(float(new_n_voxel) / original_voxels, "6.2f") 2914 ) 2915 print(msg + msg_postscript) 2916 self.cfg.write_to_logfile(msg + msg_postscript) 2917 2918 # Set small closures to 0 after growth 2919 _grown_labels = measure.label(labels_clean, connectivity=2, background=0) 2920 for x in np.unique(_grown_labels): 2921 size = _grown_labels[_grown_labels == x].size 2922 print(f"Size before editing: {size}") 2923 if size < self.cfg.closure_min_voxels: 2924 labels_clean[_grown_labels == x] = 0.0 2925 for x in np.unique(labels_clean): 2926 size = labels_clean[labels_clean == x].size 2927 print(f"Size after editing: {size}") 2928 2929 return labels_clean
2954 @staticmethod 2955 def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False): 2956 from scipy.ndimage import maximum_filter 2957 2958 hdist_size = 2 * hdist + 1 2959 vdist_size = 2 * vdist + 1 2960 mask = np.zeros((hdist_size, hdist_size, vdist_size)) 2961 mask[:, :, : vdist + 1] = 1 2962 _geobody = geobody.copy() 2963 if verbose: 2964 print( 2965 " ...grow_up_and_lateral: _geobody.shape = ", 2966 _geobody[_geobody > 0].shape, 2967 ) 2968 for k in range(iterations): 2969 try: 2970 _geobody = maximum_filter(_geobody, footprint=mask) 2971 if verbose: 2972 print( 2973 " ...grow_up_and_lateral: k, _geobody.shape = ", 2974 k, 2975 _geobody[_geobody > 0].shape, 2976 ) 2977 except: 2978 break 2979 return _geobody
2981 @staticmethod 2982 def grow_lateral(geobody, iterations, dist=1, verbose=False): 2983 from scipy.ndimage.morphology import grey_dilation 2984 2985 dist_size = 2 * dist + 1 2986 mask = np.zeros((dist_size, dist_size, 1)) 2987 mask[:, :, :] = 1 2988 _geobody = geobody.copy() 2989 if verbose: 2990 print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape) 2991 for k in range(iterations): 2992 try: 2993 _geobody = grey_dilation(_geobody, footprint=mask) 2994 if verbose: 2995 print( 2996 " ...grow_lateral: k, _geobody.shape = ", 2997 k, 2998 _geobody[_geobody > 0].shape, 2999 ) 3000 except: 3001 break 3002 return _geobody
3004 @staticmethod 3005 def grow_upward(geobody, iterations, dist=1, verbose=False): 3006 from scipy.ndimage.morphology import grey_dilation 3007 3008 dist_size = 2 * dist + 1 3009 mask = np.zeros((1, 1, dist_size)) 3010 mask[0, 0, : dist + 1] = 1 3011 _geobody = geobody.copy() 3012 if verbose: 3013 print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3014 for k in range(iterations): 3015 try: 3016 _geobody = grey_dilation(_geobody, footprint=mask) 3017 if verbose: 3018 print( 3019 " ...grow_upward: k, _geobody.shape = ", 3020 k, 3021 _geobody[_geobody > 0].shape, 3022 ) 3023 except: 3024 break 3025 return _geobody
3027 @staticmethod 3028 def grow_downward(geobody, iterations, dist=1, verbose=False): 3029 from scipy.ndimage.morphology import grey_dilation 3030 3031 dist_size = 2 * dist + 1 3032 mask = np.zeros((1, 1, dist_size)) 3033 mask[0, 0, dist:] = 1 3034 _geobody = geobody.copy() 3035 if verbose: 3036 print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape) 3037 for k in range(iterations): 3038 try: 3039 _geobody = grey_dilation(_geobody, footprint=mask) 3040 if verbose: 3041 print( 3042 " ...grow_downward: k, _geobody.shape = ", 3043 k, 3044 _geobody[_geobody > 0].shape, 3045 ) 3046 except: 3047 break 3048 return _geobody
Inherited Members
- Closures
- create_closure_labels_from_depth_maps
- create_closure_labels_from_all_depth_maps
- find_top_lith_horizons
- create_closures
- closure_size_filter
- closure_type_info_for_log
- get_voxel_counts
- populate_closure_dict
- write_closure_info_to_log
- parse_label_values_and_counts
- assign_fluid_types
- remove_small_objects
- segment_closures
- write_closure_volumes_to_disk
- calculate_closure_statistics
- find_salt_bounded_closures
- find_faulted_all_closures
- find_onlap_all_closures
- find_simple_all_closures
- find_false_all_closures
- find_salt_bounded_all_closures
- grow_to_salt
- parse_closure_codes
- datagenerator.Horizons.Horizons
- insert_feature_into_horizon_stack
- insert_seafloor
- create_random_net_over_gross_map
- fit_plane_lsq
- eval_plane
- halton
- rotate_point
- convert_map_from_samples_to_units
- write_maps_to_disk
- write_onlap_episodes
- write_fan_horizons
3051def variable_max_column_height(top_lith_idx, num_horizons, hmin=25, hmax=200): 3052 """ 3053 Create a 1-D array of maximum column heights using linear function in layer numbers 3054 Shallow closures will have small vertical closure heights 3055 Deep closures will have larger vertical closure heights 3056 3057 Would be better to use a pressure profile to determine maximum column heights at given depths 3058 3059 :param top_lith_idx: 1-D array of horizon numbers corresponding to top of layers where lithology changes 3060 :param num_horizons: Total number of horizons in model 3061 :param hmin: Minimum column height to use in linear function 3062 :param hmax: Maximum column height to use in linear function 3063 :return: 1-D array of column heights of closures 3064 """ 3065 # Use a linear function to determine max column height based on layer number 3066 column_heights = np.linspace(hmin, hmax, num=num_horizons) 3067 max_col_heights = column_heights[top_lith_idx] 3068 return max_col_heights
Create a 1-D array of maximum column heights using linear function in layer numbers Shallow closures will have small vertical closure heights Deep closures will have larger vertical closure heights
Would be better to use a pressure profile to determine maximum column heights at given depths
Parameters
- top_lith_idx: 1-D array of horizon numbers corresponding to top of layers where lithology changes
- num_horizons: Total number of horizons in model
- hmin: Minimum column height to use in linear function
- hmax: Maximum column height to use in linear function
Returns
1-D array of column heights of closures
3072def fill_to_spill(test_array, array_flags, empty_value=1.0e22, quiet=True): 3073 if not quiet: 3074 print(" ... start fillToSpill ......") 3075 temp_array = test_array.copy() 3076 flags = array_flags.copy() 3077 test_array_max = 2.0 * (temp_array[~np.isnan(temp_array)]).max() 3078 temp_array[array_flags == 255] = -empty_value 3079 flood_filled = test_array_max - flood_fill_heap( 3080 test_array_max - temp_array, empty_value=empty_value 3081 ) 3082 if not quiet: 3083 print(" ... finish fillToSpill ......") 3084 3085 flood_filled[array_flags != 1] = 0 3086 flood_filled[flood_filled == 1.0e5] = 0 3087 flags[flood_filled == empty_value] = 0 3088 3089 return flood_filled
3092def flood_fill_heap(test_array, empty_value=1.0e22, quiet=True): 3093 # from internet: http://arcgisandpython.blogspot.co.uk/2012/01/python-flood-fill-algorithm.html 3094 3095 import heapq 3096 from scipy import ndimage 3097 3098 input_array = np.copy(test_array) 3099 num_validPoints = ( 3100 test_array.flatten().shape[0] 3101 - input_array[np.isnan(input_array)].shape[0] 3102 - input_array[input_array > empty_value / 2].shape[0] 3103 ) 3104 if not quiet: 3105 print( 3106 " ... flood_fill_heap ... number of valid input horizon picks = ", 3107 num_validPoints, 3108 ) 3109 3110 validPoints = input_array[~np.isnan(input_array)] 3111 validPoints = validPoints[validPoints < empty_value / 2] 3112 validPoints = validPoints[validPoints < 1.0e5] 3113 validPoints = validPoints[validPoints > np.percentile(validPoints, 2)] 3114 3115 if len(validPoints) > 2: 3116 amin = validPoints.min() 3117 amax = validPoints.max() 3118 else: 3119 return test_array 3120 3121 if not quiet: 3122 print( 3123 " ... validPoints stats = ", 3124 validPoints.min(), 3125 np.median(validPoints), 3126 validPoints.mean(), 3127 validPoints.max(), 3128 ) 3129 print( 3130 " ... validPoints %tiles = ", 3131 np.percentile(validPoints, 0), 3132 np.percentile(validPoints, 1), 3133 np.percentile(validPoints, 5), 3134 np.percentile(validPoints, 10), 3135 np.percentile(validPoints, 25), 3136 np.percentile(validPoints, 50), 3137 np.percentile(validPoints, 75), 3138 np.percentile(validPoints, 90), 3139 np.percentile(validPoints, 95), 3140 np.percentile(validPoints, 99), 3141 np.percentile(validPoints, 100), 3142 ) 3143 from datagenerator.util import import_matplotlib 3144 3145 plt = import_matplotlib() 3146 plt.figure(5) 3147 plt.clf() 3148 plt.imshow(np.flipud(input_array), vmin=amin, vmax=amax, cmap="jet_r") 3149 plt.colorbar() 3150 plt.show() 3151 plt.savefig("flood_fill.png", format="png") 3152 plt.close() 3153 3154 print(" ... min & max for surface = ", amin, amax) 3155 3156 # set empty values and nan's to huge 3157 input_array[np.isnan(input_array)] = empty_value 3158 3159 # Set h_max to a value larger than the array maximum to ensure that the while loop will terminate 3160 h_max = np.max(input_array * 2.0) 3161 3162 # Build mask of cells with data not on the edge of the image 3163 # Use 3x3 square structuring element 3164 el = ndimage.generate_binary_structure(2, 2).astype(np.int) 3165 inside_mask = ndimage.binary_erosion(~np.isnan(input_array), structure=el) 3166 inside_mask[input_array == empty_value] = False 3167 edge_mask = np.invert(inside_mask) 3168 # Initialize output array as max value test_array except edges 3169 output_array = np.copy(input_array) 3170 output_array[inside_mask] = h_max 3171 3172 if not quiet: 3173 plt.figure(6) 3174 plt.clf() 3175 plt.imshow(np.flipud(input_array), cmap="jet_r") 3176 plt.colorbar() 3177 plt.show() 3178 plt.savefig("flood_fill2.png", format="png") 3179 plt.close() 3180 3181 # Build priority queue and place edge pixels into priority queue 3182 # Last value is flag to indicate if cell is an edge cell 3183 put = heapq.heappush 3184 get = heapq.heappop 3185 fill_heap = [ 3186 (output_array[t_row, t_col], int(t_row), int(t_col), 1) 3187 for t_row, t_col in np.transpose(np.where(edge_mask)) 3188 ] 3189 heapq.heapify(fill_heap) 3190 3191 # Iterate until priority queue is empty 3192 while 1: 3193 try: 3194 h_crt, t_row, t_col, edge_flag = get(fill_heap) 3195 except IndexError: 3196 break 3197 for n_row, n_col in [ 3198 ((t_row - 1), t_col), 3199 ((t_row + 1), t_col), 3200 (t_row, (t_col - 1)), 3201 (t_row, (t_col + 1)), 3202 ]: 3203 # Skip cell if outside array edges 3204 if edge_flag: 3205 try: 3206 if not inside_mask[n_row, n_col]: 3207 continue 3208 except IndexError: 3209 continue 3210 if output_array[n_row, n_col] == h_max: 3211 output_array[n_row, n_col] = max(h_crt, input_array[n_row, n_col]) 3212 put(fill_heap, (output_array[n_row, n_col], n_row, n_col, 0)) 3213 output_array[output_array == empty_value] = np.nan 3214 return output_array
3319def get_top_of_closure(inarray, pad_up=0, pad_down=0): 3320 """Create a mask leaving only the top of a closure.""" 3321 mask = inarray != 0 3322 t = np.where(mask.any(axis=-1), mask.argmax(axis=-1), -1) 3323 xy = np.argwhere(t > 0) 3324 z = t[t > 0] 3325 outarray = np.zeros_like(inarray) 3326 for (x, y), z in zip(xy, z): 3327 zmin = z - pad_up 3328 zmax = z + pad_down + 1 3329 outarray[x, y, zmin:zmax] = 1 3330 return outarray
Create a mask leaving only the top of a closure.
3333def lsq(x, y, axis=-1): 3334 ### 3335 ### compute the slope and intercept for an array with points to be fit 3336 ### in the last dimension. can be in other axis using the 'axis' parmameter. 3337 ### 3338 ### returns: 3339 ### - intercept 3340 ### - slope 3341 ### - pearson r (normalized cross-correlation coefficient) 3342 ### 3343 ### output will have dimensions of input with one less axis 3344 ### - (specified by axis parameter) 3345 ### 3346 3347 """ 3348 # compute x and y with mean removed 3349 x_zeromean = x * 1. 3350 x_zeromean -= x.mean(axis=axis).reshape(x.shape[0],x.shape[1],1) 3351 y_zeromean = y * 1. 3352 y_zeromean -= y.mean(axis=axis).reshape(y.shape[0],y.shape[1],1) 3353 """ 3354 3355 # compute pearsonr 3356 r = np.sum(x * y, axis=axis) - np.sum(x) * np.sum(y, axis=axis) / y.shape[axis] 3357 r /= np.sqrt( 3358 (np.sum(x ** 2, axis=axis) - np.sum(x, axis=axis) ** 2 / y.shape[axis]) 3359 * (np.sum(y ** 2, axis=axis) - np.sum(y, axis=axis) ** 2 / y.shape[axis]) 3360 ) 3361 3362 # compute slope 3363 slope = r * y.std(axis=axis) / x.std(axis=axis) 3364 3365 # compute intercept 3366 intercept = y.mean(axis=axis) - slope * x.mean(axis=axis) 3367 3368 return intercept, slope, r
compute x and y with mean removed
x_zeromean = x * 1. x_zeromean -= x.mean(axis=axis).reshape(x.shape[0],x.shape[1],1) y_zeromean = y * 1. y_zeromean -= y.mean(axis=axis).reshape(y.shape[0],y.shape[1],1)
3371def compute_ai_gi(parameters, seismic_data): 3372 """[summary] 3373 3374 Args: 3375 cfg (Parameter class object): Model Parameters 3376 seismic_data (np.array): Seismic data with shape n * x * y * z, 3377 where n is number of angle stacks 3378 """ 3379 inc_angles = np.array(parameters.incident_angles) 3380 inc_angles = np.sin(inc_angles * np.pi / 180.0) ** 2 3381 inc_angles = inc_angles.reshape(len(inc_angles), 1, 1, 1) 3382 3383 intercept, slope, _ = lsq(inc_angles, seismic_data, axis=0) 3384 3385 intercept[np.isnan(intercept)] = 0.0 3386 slope[np.isnan(slope)] = 0.0 3387 intercept[np.isinf(intercept)] = 0.0 3388 slope[np.isinf(slope)] = 0.0 3389 return intercept, slope
[summary]
Args: cfg (Parameter class object): Model Parameters seismic_data (np.array): Seismic data with shape n * x * y * z, where n is number of angle stacks