datagenerator.Faults
1import os 2import numpy as np 3from tqdm import tqdm 4from scipy.ndimage import maximum_filter, binary_dilation 5from datagenerator.Horizons import Horizons 6from datagenerator.Geomodels import Geomodel 7from datagenerator.Parameters import Parameters 8from datagenerator.util import write_data_to_hdf, plot_3D_faults_plot 9from skimage import measure 10 11 12class Faults(Horizons, Geomodel): 13 """ 14 Faults Class 15 ------------ 16 Describes the class for faulting the model. 17 18 Parameters 19 ---------- 20 Horizons : datagenerator.Horizons 21 The Horizons class used to build the faults. 22 Geomodel : data_generator.Geomodels 23 The Geomodel class used to build the faults. 24 25 Returns 26 ------- 27 None 28 """ 29 def __init__( 30 self, 31 parameters: Parameters, 32 unfaulted_depth_maps: np.ndarray, 33 onlap_horizon_list: np.ndarray, 34 geomodels: Geomodel, 35 fan_horizon_list: np.ndarray, 36 fan_thickness: np.ndarray 37 ): 38 """__init__ 39 40 Initializes the Faults class.. 41 42 Parameters 43 ---------- 44 parameters : Parameters 45 The parameters class. 46 unfaulted_depth_maps : np.ndarray 47 The depth maps to be faulted. 48 onlap_horizon_list : np.ndarray 49 The onlap horizon list. 50 geomodels : Geomodel 51 The geomodels class. 52 fan_horizon_list : np.ndarray 53 The fan horizon list. 54 fan_thickness : np.ndarray 55 The fan thickness list. 56 """ 57 self.cfg = parameters 58 # Horizons 59 self.unfaulted_depth_maps = unfaulted_depth_maps 60 self.onlap_horizon_list = onlap_horizon_list 61 self.fan_horizon_list = fan_horizon_list 62 self.fan_thickness = fan_thickness 63 self.faulted_depth_maps = self.cfg.hdf_init( 64 "faulted_depth_maps", shape=unfaulted_depth_maps.shape 65 ) 66 self.faulted_depth_maps_gaps = self.cfg.hdf_init( 67 "faulted_depth_maps_gaps", shape=unfaulted_depth_maps.shape 68 ) 69 # Volumes 70 cube_shape = geomodels.geologic_age[:].shape 71 self.vols = geomodels 72 self.faulted_age_volume = self.cfg.hdf_init( 73 "faulted_age_volume", shape=cube_shape 74 ) 75 self.faulted_net_to_gross = self.cfg.hdf_init( 76 "faulted_net_to_gross", shape=cube_shape 77 ) 78 self.faulted_lithology = self.cfg.hdf_init( 79 "faulted_lithology", shape=cube_shape 80 ) 81 self.reservoir = self.cfg.hdf_init("reservoir", shape=cube_shape) 82 self.faulted_depth = self.cfg.hdf_init("faulted_depth", shape=cube_shape) 83 self.faulted_onlap_segments = self.cfg.hdf_init( 84 "faulted_onlap_segments", shape=cube_shape 85 ) 86 self.fault_planes = self.cfg.hdf_init("fault_planes", shape=cube_shape) 87 self.displacement_vectors = self.cfg.hdf_init( 88 "displacement_vectors", shape=cube_shape 89 ) 90 self.sum_map_displacements = self.cfg.hdf_init( 91 "sum_map_displacements", shape=cube_shape 92 ) 93 self.fault_intersections = self.cfg.hdf_init( 94 "fault_intersections", shape=cube_shape 95 ) 96 self.fault_plane_throw = self.cfg.hdf_init( 97 "fault_plane_throw", shape=cube_shape 98 ) 99 self.max_fault_throw = self.cfg.hdf_init("max_fault_throw", shape=cube_shape) 100 self.fault_plane_azimuth = self.cfg.hdf_init( 101 "fault_plane_azimuth", shape=cube_shape 102 ) 103 # Salt 104 self.salt_model = None 105 106 def apply_faulting_to_geomodels_and_depth_maps(self) -> None: 107 """ 108 Apply faulting to horizons and cubes 109 ------------------------------------ 110 Generates random faults and applies faulting to horizons and cubes. 111 112 The method does the following: 113 114 * Generate faults and sum the displacements 115 * Apply faulting to horizons 116 * Write faulted depth maps to disk 117 * Write faulted depth maps with gaps at faults to disk 118 * Write onlapping horizons to disk 119 * Apply faulting to geomodels 120 * Make segmentation results conform to binary values after 121 faulting and interpolation. 122 * Write cubes to file (if qc_volumes turned on in config.json) 123 124 (If segmentation is not reset to binary, the multiple 125 interpolations for successive faults destroys the crisp 126 localization of the labels. Subjective observation suggests 127 that slightly different thresholds for different 128 features provide superior results) 129 130 Parameters 131 ---------- 132 None 133 134 Returns 135 ------- 136 None 137 """ 138 # Make a dictionary of zero-thickness onlapping layers before faulting 139 onlap_clip_dict = find_zero_thickness_onlapping_layers( 140 self.unfaulted_depth_maps, self.onlap_horizon_list 141 ) 142 _ = self.generate_faults() 143 144 # Apply faulting to age model, net_to_gross cube & onlap segments 145 self.faulted_age_volume[:] = self.apply_xyz_displacement( 146 self.vols.geologic_age[:] 147 ).astype("float") 148 self.faulted_onlap_segments[:] = self.apply_xyz_displacement( 149 self.vols.onlap_segments[:] 150 ) 151 152 # Improve the depth maps post faulting by 153 # re-interpolating across faulted age model 154 ( 155 self.faulted_depth_maps[:], 156 self.faulted_depth_maps_gaps[:], 157 ) = self.improve_depth_maps_post_faulting( 158 self.vols.geologic_age[:], self.faulted_age_volume[:], onlap_clip_dict 159 ) 160 161 if self.cfg.include_salt: 162 from datagenerator.Salt import SaltModel 163 164 self.salt_model = SaltModel(self.cfg) 165 self.salt_model.compute_salt_body_segmentation() 166 ( 167 self.faulted_depth_maps[:], 168 self.faulted_depth_maps_gaps[:], 169 ) = self.salt_model.update_depth_maps_with_salt_segments_drag() 170 171 # # Write the faulted maps to disk 172 self.write_maps_to_disk( 173 self.faulted_depth_maps[:] * self.cfg.digi, "depth_maps" 174 ) 175 self.write_maps_to_disk( 176 self.faulted_depth_maps_gaps[:] * self.cfg.digi, "depth_maps_gaps" 177 ) 178 self.write_onlap_episodes( 179 self.onlap_horizon_list[:], 180 self.faulted_depth_maps_gaps[:], 181 self.faulted_depth_maps[:], 182 ) 183 if np.any(self.fan_horizon_list): 184 self.write_fan_horizons( 185 self.fan_horizon_list, self.faulted_depth_maps[:] * 4.0 186 ) 187 188 if self.cfg.hdf_store: 189 # Write faulted maps to hdf 190 for n, d in zip( 191 ["depth_maps", "depth_maps_gaps"], 192 [ 193 self.faulted_depth_maps[:] * self.cfg.digi, 194 self.faulted_depth_maps_gaps[:] * self.cfg.digi, 195 ], 196 ): 197 write_data_to_hdf(n, d, self.cfg.hdf_master) 198 199 # Create faulted binary segmentation volumes 200 _fault_planes = self.fault_planes[:] 201 self.fault_planes[:] = self.create_binary_segmentations_post_faulting( 202 _fault_planes, 0.45 203 ) 204 del _fault_planes 205 _fault_intersections = self.fault_intersections[:] 206 self.fault_intersections[:] = self.create_binary_segmentations_post_faulting( 207 _fault_intersections, 0.45 208 ) 209 del _fault_intersections 210 _faulted_onlap_segments = self.faulted_onlap_segments[:] 211 self.faulted_onlap_segments[:] = self.create_binary_segmentations_post_faulting( 212 _faulted_onlap_segments, 0.45 213 ) 214 del _faulted_onlap_segments 215 if self.cfg.include_channels: 216 self.vols.floodplain_shale = self.apply_xyz_displacement( 217 self.vols.floodplain_shale 218 ) 219 self.vols.channel_fill = self.apply_xyz_displacement(self.vols.channel_fill) 220 self.vols.shale_channel_drape = self.apply_xyz_displacement( 221 self.vols.shale_channel_drape 222 ) 223 self.vols.levee = self.apply_xyz_displacement(self.vols.levee) 224 self.vols.crevasse = self.apply_xyz_displacement(self.vols.crevasse) 225 226 ( 227 self.vols.channel_segments, 228 self.vols.geologic_age, 229 ) = self.reassign_channel_segment_encoding( 230 self.vols.geologic_age, 231 self.vols.floodplain_shale, 232 self.vols.channel_fill, 233 self.vols.shale_channel_drape, 234 self.vols.levee, 235 self.vols.crevasse, 236 self.maps.channels, 237 ) 238 if self.cfg.model_qc_volumes: 239 self.vols.write_cube_to_disk( 240 self.vols.channel_segments, "channel_segments" 241 ) 242 243 if self.cfg.model_qc_volumes: 244 # Output files if qc volumes required 245 self.vols.write_cube_to_disk(self.faulted_age_volume[:], "geologic_age") 246 self.vols.write_cube_to_disk( 247 self.faulted_onlap_segments[:], "onlap_segments" 248 ) 249 self.vols.write_cube_to_disk(self.fault_planes[:], "fault_segments") 250 self.vols.write_cube_to_disk( 251 self.fault_intersections[:], "fault_intersection_segments" 252 ) 253 self.vols.write_cube_to_disk( 254 self.fault_plane_throw[:], "fault_segments_throw" 255 ) 256 self.vols.write_cube_to_disk( 257 self.fault_plane_azimuth[:], "fault_segments_azimuth" 258 ) 259 if self.cfg.hdf_store: 260 # Write faulted age, onlap and fault segment cubes to hdf 261 for n, d in zip( 262 [ 263 "geologic_age_faulted", 264 "onlap_segments", 265 "fault_segments", 266 "fault_intersection_segments", 267 "fault_segments_throw", 268 "fault_segments_azimuth", 269 ], 270 [ 271 self.faulted_age_volume, 272 self.faulted_onlap_segments, 273 self.fault_planes, 274 self.fault_intersections, 275 self.fault_plane_throw, 276 self.fault_plane_azimuth, 277 ], 278 ): 279 write_data_to_hdf(n, d, self.cfg.hdf_master) 280 281 if self.cfg.qc_plots: 282 self.create_qc_plots() 283 try: 284 # Create 3D qc plot 285 plot_3D_faults_plot(self.cfg, self) 286 except ValueError: 287 self.cfg.write_to_logfile("3D Fault Plotting Failed") 288 289 def build_faulted_property_geomodels( 290 self, 291 facies: np.ndarray 292 ) -> None: 293 """ 294 Build Faulted Property Geomodels 295 ------------ 296 Generates faulted property geomodels. 297 298 **The method does the following:** 299 300 Use faulted geologic_age cube, depth_maps and facies 301 to create geomodel properties (depth, lith) 302 303 - lithology 304 - net_to_gross (to create effective sand layers) 305 - depth below mudline 306 - randomised depth below mudline (to 307 randomise the rock properties per layer) 308 309 Parameters 310 ---------- 311 facies : np.ndarray 312 The Horizons class used to build the faults. 313 314 Returns 315 ------- 316 None 317 """ 318 work_cube_lith = ( 319 np.ones_like(self.faulted_age_volume) * -1 320 ) # initialise lith cube to water 321 work_cube_sealed = np.zeros_like(self.faulted_age_volume) 322 work_cube_net_to_gross = np.zeros_like(self.faulted_age_volume) 323 work_cube_depth = np.zeros_like(self.faulted_age_volume) 324 # Also create a randomised depth cube for generating randomised rock properties 325 # final dimension's shape is based on number of possible list types 326 # currently one of ['seawater', 'shale', 'sand'] 327 # n_lith = len(['shale', 'sand']) 328 cube_shape = self.faulted_age_volume.shape 329 # randomised_depth = np.zeros(cube_shape, 'float32') 330 331 ii, jj = self.build_meshgrid() 332 333 # Loop over layers in reverse order, start at base 334 previous_depth_map = self.faulted_depth_maps[:, :, -1] 335 if self.cfg.partial_voxels: 336 # add .5 to consider partial voxels from half above and half below 337 previous_depth_map += 0.5 338 339 for i in range(self.faulted_depth_maps.shape[2] - 2, 0, -1): 340 # Apply a random depth shift within the range as provided in config file 341 # (provided in metres, so converted to samples here) 342 343 current_depth_map = self.faulted_depth_maps[:, :, i] 344 if self.cfg.partial_voxels: 345 current_depth_map += 0.5 346 347 # compute maps with indices of top map and base map to include partial voxels 348 top_map_index = current_depth_map.copy().astype("int") 349 base_map_index = ( 350 self.faulted_depth_maps[:, :, i + 1].copy().astype("int") + 1 351 ) 352 353 # compute thickness over which to iterate 354 thickness_map = base_map_index - top_map_index 355 thickness_map_max = thickness_map.max() 356 357 tvdml_map = previous_depth_map - self.faulted_depth_maps[:, :, 0] 358 # Net to Gross Map for layer 359 if not self.cfg.variable_shale_ng: 360 ng_map = np.zeros( 361 shape=( 362 self.faulted_depth_maps.shape[0], 363 self.faulted_depth_maps.shape[1], 364 ) 365 ) 366 else: 367 if ( 368 facies[i] == 0.0 369 ): # if shale layer, make non-zero N/G map for layer using a low average net to gross 370 ng_map = self.create_random_net_over_gross_map( 371 avg=(0.0, 0.2), stdev=(0.001, 0.01), octave=3 372 ) 373 if facies[i] == 1.0: # if sand layer, make non-zero N/G map for layer 374 ng_map = self.create_random_net_over_gross_map() 375 376 for k in range(thickness_map_max + 1): 377 378 if self.cfg.partial_voxels: 379 # compute fraction of voxel containing layer 380 top_map = np.max( 381 np.dstack( 382 (current_depth_map, top_map_index.astype("float32") + k) 383 ), 384 axis=-1, 385 ) 386 top_map = np.min(np.dstack((top_map, previous_depth_map)), axis=-1) 387 base_map = np.min( 388 np.dstack( 389 ( 390 previous_depth_map, 391 top_map_index.astype("float32") + k + 1, 392 ) 393 ), 394 axis=-1, 395 ) 396 fraction_of_voxel = np.clip(base_map - top_map, 0.0, 1.0) 397 valid_k = np.where( 398 (fraction_of_voxel > 0.0) 399 & ((top_map_index + k).astype("int") < cube_shape[2]), 400 1, 401 0, 402 ) 403 404 # put layer properties in the cube for each case 405 sublayer_ii = ii[valid_k == 1] 406 sublayer_jj = jj[valid_k == 1] 407 else: 408 sublayer_ii = ii[thickness_map > k] 409 sublayer_jj = jj[thickness_map > k] 410 411 if sublayer_ii.shape[0] > 0: 412 if self.cfg.partial_voxels: 413 sublayer_depth_map = (top_map_index + k).astype("int")[ 414 valid_k == 1 415 ] 416 sublayer_depth_map_int = np.clip(sublayer_depth_map, 0, None) 417 sublayer_ng_map = ng_map[valid_k == 1] 418 sublayer_tvdml_map = tvdml_map[valid_k == 1] 419 sublayer_fraction = fraction_of_voxel[valid_k == 1] 420 421 # Lithology cube 422 input_cube = work_cube_lith[ 423 sublayer_ii, sublayer_jj, sublayer_depth_map_int 424 ] 425 values = facies[i] * sublayer_fraction 426 input_cube[input_cube == -1.0] = ( 427 values[input_cube == -1.0] * 1.0 428 ) 429 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 430 work_cube_lith[ 431 sublayer_ii, sublayer_jj, sublayer_depth_map_int 432 ] = (input_cube * 1.0) 433 del input_cube 434 del values 435 436 input_cube = work_cube_sealed[ 437 sublayer_ii, sublayer_jj, sublayer_depth_map_int 438 ] 439 values = (1 - facies[i - 1]) * sublayer_fraction 440 input_cube[input_cube == -1.0] = ( 441 values[input_cube == -1.0] * 1.0 442 ) 443 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 444 work_cube_sealed[ 445 sublayer_ii, sublayer_jj, sublayer_depth_map_int 446 ] = (input_cube * 1.0) 447 del input_cube 448 del values 449 450 # Depth cube 451 work_cube_depth[ 452 sublayer_ii, sublayer_jj, sublayer_depth_map_int 453 ] += (sublayer_tvdml_map * sublayer_fraction) 454 # Net to Gross cube 455 work_cube_net_to_gross[ 456 sublayer_ii, sublayer_jj, sublayer_depth_map_int 457 ] += (sublayer_ng_map * sublayer_fraction) 458 459 # Randomised Depth Cube 460 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += \ 461 # (sublayer_tvdml_map + random_z_perturbation) * sublayer_fraction 462 463 else: 464 sublayer_depth_map_int = ( 465 0.5 466 + np.clip( 467 previous_depth_map[thickness_map > k], 468 0, 469 self.vols.geologic_age.shape[2] - 1, 470 ) 471 ).astype("int") - k 472 sublayer_tvdml_map = tvdml_map[thickness_map > k] 473 sublayer_ng_map = ng_map[thickness_map > k] 474 475 work_cube_lith[ 476 sublayer_ii, sublayer_jj, sublayer_depth_map_int 477 ] = facies[i] 478 work_cube_sealed[ 479 sublayer_ii, sublayer_jj, sublayer_depth_map_int 480 ] = (1 - facies[i - 1]) 481 482 work_cube_depth[ 483 sublayer_ii, sublayer_jj, sublayer_depth_map_int 484 ] = sublayer_tvdml_map 485 work_cube_net_to_gross[ 486 sublayer_ii, sublayer_jj, sublayer_depth_map_int 487 ] += sublayer_ng_map 488 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += (sublayer_tvdml_map + random_z_perturbation) 489 490 # replace previous depth map for next iteration 491 previous_depth_map = current_depth_map.copy() 492 493 if self.cfg.verbose: 494 print("\n\n ... After infilling ...") 495 self.write_cube_to_disk(work_cube_sealed.astype("uint8"), "sealed_label") 496 497 # Clip cubes and convert from samples to units 498 work_cube_lith = np.clip(work_cube_lith, -1.0, 1.0) # clip lith to [-1, +1] 499 work_cube_net_to_gross = np.clip( 500 work_cube_net_to_gross, 0, 1.0 501 ) # clip n/g to [0, 1] 502 work_cube_depth = np.clip(work_cube_depth, a_min=0, a_max=None) 503 work_cube_depth *= self.cfg.digi 504 505 if self.cfg.include_salt: 506 # Update age model after horizons have been modified by salt inclusion 507 self.faulted_age_volume[ 508 : 509 ] = self.create_geologic_age_3d_from_infilled_horizons( 510 self.faulted_depth_maps[:] * 10.0 511 ) 512 # Set lith code for salt 513 work_cube_lith[self.salt_model.salt_segments[:] > 0.0] = 2.0 514 # Fix deepest part of facies in case salt inclusion has shifted base horizon 515 # This can leave default (water) facies codes at the base 516 last_50_samples = self.cfg.cube_shape[-1] - 50 517 work_cube_lith[..., last_50_samples:][ 518 work_cube_lith[..., last_50_samples:] == -1.0 519 ] = 0.0 520 521 if self.cfg.qc_plots: 522 from datagenerator.util import plot_xsection 523 import matplotlib as mpl 524 525 line_number = int( 526 work_cube_lith.shape[0] / 2 527 ) # pick centre line for all plots 528 529 if self.cfg.include_salt and np.max(work_cube_lith[line_number, ...]) > 1: 530 lith_cmap = mpl.colors.ListedColormap( 531 ["blue", "saddlebrown", "gold", "grey"] 532 ) 533 else: 534 lith_cmap = mpl.colors.ListedColormap(["blue", "saddlebrown", "gold"]) 535 plot_xsection( 536 work_cube_lith, 537 self.faulted_depth_maps[:], 538 line_num=line_number, 539 title="Example Trav through 3D model\nLithology", 540 png_name="QC_plot__AfterFaulting_lithology.png", 541 cfg=self.cfg, 542 cmap=lith_cmap, 543 ) 544 plot_xsection( 545 work_cube_depth, 546 self.faulted_depth_maps, 547 line_num=line_number, 548 title="Example Trav through 3D model\nDepth Below Mudline", 549 png_name="QC_plot__AfterFaulting_depth_bml.png", 550 cfg=self.cfg, 551 cmap="cubehelix_r", 552 ) 553 self.faulted_lithology[:] = work_cube_lith 554 self.faulted_net_to_gross[:] = work_cube_net_to_gross 555 self.faulted_depth[:] = work_cube_depth 556 # self.randomised_depth[:] = randomised_depth 557 558 # Write the % sand in model to logfile 559 sand_fraction = ( 560 work_cube_lith[work_cube_lith == 1].size 561 / work_cube_lith[work_cube_lith >= 0].size 562 ) 563 self.cfg.write_to_logfile( 564 f"Sand voxel % in model {100 * sand_fraction:.1f}%", 565 mainkey="model_parameters", 566 subkey="sand_voxel_pct", 567 val=100 * sand_fraction, 568 ) 569 570 if self.cfg.hdf_store: 571 for n, d in zip( 572 ["lithology", "net_to_gross", "depth"], 573 [ 574 self.faulted_lithology[:], 575 self.faulted_net_to_gross[:], 576 self.faulted_depth[:], 577 ], 578 ): 579 write_data_to_hdf(n, d, self.cfg.hdf_master) 580 581 # Save out reservoir volume for XAI-NBDT 582 reservoir = (work_cube_lith == 1) * 1.0 583 reservoir_dilated = binary_dilation(reservoir) 584 self.reservoir[:] = reservoir_dilated 585 586 if self.cfg.model_qc_volumes: 587 self.write_cube_to_disk(self.faulted_lithology[:], "faulted_lithology") 588 self.write_cube_to_disk( 589 self.faulted_net_to_gross[:], "faulted_net_to_gross" 590 ) 591 self.write_cube_to_disk(self.faulted_depth[:], "faulted_depth") 592 self.write_cube_to_disk(self.faulted_age_volume[:], "faulted_age") 593 if self.cfg.include_salt: 594 self.write_cube_to_disk( 595 self.salt_model.salt_segments[..., : self.cfg.cube_shape[2]].astype( 596 "uint8" 597 ), 598 "salt", 599 ) 600 601 def create_qc_plots(self) -> None: 602 """ 603 Create QC Plots 604 --------------- 605 Creates QC Plots of faulted models and histograms of 606 voxels which are not in layers. 607 608 Parameters 609 ---------- 610 None 611 612 Returns 613 ------- 614 None 615 """ 616 from datagenerator.util import ( 617 find_line_with_most_voxels, 618 plot_voxels_not_in_regular_layers, 619 plot_xsection, 620 ) 621 622 # analyze voxel values not in regular layers 623 plot_voxels_not_in_regular_layers( 624 volume=self.faulted_age_volume[:], 625 threshold=0.0, 626 cfg=self.cfg, 627 title="Example Trav through 3D model\n" 628 + "histogram of layers after faulting, before inserting channel facies", 629 png_name="QC_plot__Channels__histogram_FaultedLayersNoChannels.png", 630 ) 631 try: # if channel_segments exists 632 inline_index_channels = find_line_with_most_voxels( 633 self.vols.channel_segments, 0.0, self.cfg 634 ) 635 plot_xsection( 636 self.vols.channel_segments, 637 self.faulted_depth_maps, 638 inline_index_channels, 639 cfg=self.cfg, 640 title="Example Trav through 3D model\nchannel_segments after faulting", 641 png_name="QC_plot__AfterFaulting_channel_segments.png", 642 ) 643 title = "Example Trav through 3D model\nLayers Filled With Layer Number / ChannelsAdded / Faulted" 644 png_name = "QC_plot__LayersFilledWithLayerNumber_ChannelsAdded_Faulted.png" 645 except (NameError, AttributeError): # channel_segments does not exist 646 inline_index_channels = int(self.faulted_age_volume.shape[0] / 2) 647 title = "Example Trav through 3D model\nLayers Filled With Layer Number / Faulted" 648 png_name = "QC_plot__LayersFilledWithLayerNumber_Faulted.png" 649 plot_xsection( 650 self.faulted_age_volume[:], 651 self.faulted_depth_maps[:], 652 inline_index_channels, 653 title, 654 png_name, 655 self.cfg, 656 ) 657 658 def generate_faults(self) -> np.ndarray: 659 """ 660 Generate Faults 661 --------------- 662 Generates faults in the model. 663 664 Parameters 665 ---------- 666 None 667 668 Returns 669 ------- 670 displacements_classification : np.ndarray 671 Array of fault displacement classifications 672 """ 673 if self.cfg.verbose: 674 print(f" ... create {self.cfg.number_faults} faults") 675 fault_params = self.fault_parameters() 676 677 # Write fault parameters to logfile 678 self.cfg.write_to_logfile( 679 f"Fault_mode: {self.cfg.fmode}", 680 mainkey="model_parameters", 681 subkey="fault_mode", 682 val=self.cfg.fmode, 683 ) 684 self.cfg.write_to_logfile( 685 f"Noise_level: {self.cfg.fnoise}", 686 mainkey="model_parameters", 687 subkey="noise_level", 688 val=self.cfg.fnoise, 689 ) 690 691 # Build faults, and sum displacements 692 displacements_classification, hockeys = self.build_faults(fault_params) 693 if self.cfg.model_qc_volumes: 694 # Write max fault throw cube to disk 695 self.vols.write_cube_to_disk(self.max_fault_throw[:], "max_fault_throw") 696 self.cfg.write_to_logfile( 697 f"Hockey_Sticks generated: {sum(hockeys)}", 698 mainkey="model_parameters", 699 subkey="hockey_sticks_generated", 700 val=sum(hockeys), 701 ) 702 703 self.cfg.write_to_logfile( 704 f"Fault Info: a, b, c, x0, y0, z0, throw/infill_factor, shear_zone_width," 705 " gouge_pctile, tilt_pct*100" 706 ) 707 for i in range(self.cfg.number_faults): 708 self.cfg.write_to_logfile( 709 f"Fault_{i + 1}: {fault_params['a'][i]:.2f}, {fault_params['b'][i]:.2f}, {fault_params['c'][i]:.2f}," 710 f" {fault_params['x0'][i]:>7.2f}, {fault_params['y0'][i]:>7.2f}, {fault_params['z0'][i]:>8.2f}," 711 f" {fault_params['throw'][i] / self.cfg.infill_factor:>6.2f}, {fault_params['tilt_pct'][i] * 100:.2f}" 712 ) 713 714 self.cfg.write_to_logfile( 715 msg=None, 716 mainkey=f"fault_{i + 1}", 717 subkey="model_id", 718 val=os.path.basename(self.cfg.work_subfolder), 719 ) 720 for _subkey_name in [ 721 "a", 722 "b", 723 "c", 724 "x0", 725 "y0", 726 "z0", 727 "throw", 728 "tilt_pct", 729 "shear_zone_width", 730 "gouge_pctile", 731 ]: 732 _val = fault_params[_subkey_name][i] 733 self.cfg.write_to_logfile( 734 msg=None, mainkey=f"fault_{i + 1}", subkey=_subkey_name, val=_val 735 ) 736 737 return displacements_classification 738 739 def fault_parameters(self): 740 """ 741 Get Fault Parameters 742 --------------- 743 Returns the fault parameters. 744 745 Factory design pattern used to select fault parameters 746 747 Parameters 748 ---------- 749 None 750 751 Returns 752 ------- 753 fault_mode : dict 754 Dictionary containing the fault parameters. 755 """ 756 fault_mode = self._get_fault_mode() 757 return fault_mode() 758 759 def build_faults(self, fp: dict, verbose=False): 760 """ 761 Build Faults 762 --------------- 763 Creates faults in the model. 764 765 Parameters 766 ---------- 767 fp : dict 768 Dictionary containing the fault parameters. 769 verbose : bool 770 The level of verbosity to use. 771 772 Returns 773 ------- 774 dis_class : np.ndarray 775 Array of fault displacement classifications 776 hockey_sticks : list 777 List of hockey sticks 778 """ 779 def apply_faulting(traces, stretch_times, verbose=False): 780 """ 781 Apply Faulting 782 -------------- 783 Applies faulting to the traces. 784 785 The method does the following: 786 787 Apply stretching and squeezing previously applied to the input cube 788 vertically to give all depths the same number of extrema. 789 This is intended to be a proxy for making the 790 dominant frequency the same everywhere. 791 Variables: 792 - traces - input, previously stretched/squeezed trace(s) 793 - stretch_times - input, LUT for stretching/squeezing trace(s), 794 range is (0,number samples in last dimension of 'traces') 795 - unstretch_traces - output, un-stretched/un-squeezed trace(s) 796 797 Parameters 798 ---------- 799 traces : np.ndarray 800 Previously stretched/squeezed trace(s). 801 stretch_times : np.ndarray 802 A look up table for stretching and squeezing the traces. 803 verbose : bool, optional 804 The level of verbosity, by default False 805 806 Returns 807 ------- 808 np.ndarray 809 The un-stretched/un-squeezed trace(s). 810 """ 811 unstretch_traces = np.zeros_like(traces) 812 origtime = np.arange(traces.shape[-1]) 813 814 if verbose: 815 print("\t ... Cube parameters going into interpolation") 816 print(f"\t ... Origtime shape = {len(origtime)}") 817 print(f"\t ... stretch_times_effects shape = {stretch_times.shape}") 818 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 819 print(f"\t ... traces shape = {traces.shape}") 820 821 for i in range(traces.shape[0]): 822 for j in range(traces.shape[1]): 823 if traces[i, j, :].min() != traces[i, j, :].max(): 824 unstretch_traces[i, j, :] = np.interp( 825 stretch_times[i, j, :], origtime, traces[i, j, :] 826 ) 827 else: 828 unstretch_traces[i, j, :] = traces[i, j, :] 829 return unstretch_traces 830 831 print("\n\n . starting 'build_faults'.") 832 print(" ... self.cfg.verbose = " + str(self.cfg.verbose)) 833 cube_shape = np.array(self.cfg.cube_shape) 834 cube_shape[-1] += self.cfg.pad_samples 835 samples_in_cube = self.vols.geologic_age[:].size 836 wb = self.copy_and_divide_depth_maps_by_infill( 837 self.unfaulted_depth_maps[..., 0] 838 ) 839 840 sum_displacements = np.zeros_like(self.vols.geologic_age[:]) 841 displacements_class = np.zeros_like(self.vols.geologic_age[:]) 842 hockey_sticks = [] 843 fault_voxel_count_list = [] 844 number_fault_intersections = 0 845 846 depth_maps_faulted_infilled = \ 847 self.copy_and_divide_depth_maps_by_infill( 848 self.unfaulted_depth_maps[:] 849 ) 850 depth_maps_gaps = self.copy_and_divide_depth_maps_by_infill( 851 self.unfaulted_depth_maps[:] 852 ) 853 854 # Create depth indices cube (moved from inside loop) 855 faulted_depths = np.zeros_like(self.vols.geologic_age[:]) 856 for k in range(faulted_depths.shape[-1]): 857 faulted_depths[:, :, k] = k 858 unfaulted_depths = faulted_depths * 1.0 859 _faulted_depths = ( 860 unfaulted_depths * 1.0 861 ) # in case there are 0 faults, prepare _faulted_depths here 862 863 for ifault in tqdm(range(self.cfg.number_faults)): 864 semi_axes = [ 865 fp["a"][ifault], 866 fp["b"][ifault], 867 fp["c"][ifault] / self.cfg.infill_factor ** 2, 868 ] 869 origin = [ 870 fp["x0"][ifault], 871 fp["y0"][ifault], 872 fp["z0"][ifault] / self.cfg.infill_factor, 873 ] 874 throw = fp["throw"][ifault] / self.cfg.infill_factor 875 tilt = fp["tilt_pct"][ifault] 876 877 print(f"\n\n ... inserting fault {ifault} with throw {throw:.2f}") 878 print( 879 f" ... fault ellipsoid semi-axes (a, b, c): {np.sqrt(semi_axes[0]):.2f}, " 880 f"{np.sqrt(semi_axes[1]):.2f}, {np.sqrt(semi_axes[2]):.2f}" 881 ) 882 print( 883 f" ... fault ellipsoid origin (x, y, z): {origin[0]:.2f}, {origin[1]:.2f}, {origin[2]:.2f}" 884 ) 885 print(f" ... tilt_pct: {tilt * 100:.2f}") 886 z_base = origin[2] * np.sqrt(semi_axes[2]) 887 print( 888 f" ...z for bottom of ellipsoid at depth (samples) = {np.around(z_base, 0)}" 889 ) 890 print(f" ...shape of output_cube = {self.vols.geologic_age.shape}") 891 print( 892 f" ...infill_factor, pad_samples = {self.cfg.infill_factor}, {self.cfg.pad_samples}" 893 ) 894 895 # add empty arrays for shear_zone_width and gouge_pctile to fault params dictionary 896 fp["shear_zone_width"] = np.zeros(self.cfg.number_faults) 897 fp["gouge_pctile"] = np.zeros(self.cfg.number_faults) 898 ( 899 displacement, 900 displacement_classification, 901 interpolation, 902 hockey_stick, 903 fault_segm, 904 ellipsoid, 905 fp, 906 ) = self.get_displacement_vector( 907 semi_axes, origin, throw, tilt, wb, ifault, fp 908 ) 909 910 if verbose: 911 print(" ... hockey_stick = " + str(hockey_stick)) 912 print( 913 " ... Is displacement the same as displacement_classification? " 914 + str(np.all(displacement == displacement_classification)) 915 ) 916 print( 917 " ... Sample count where displacement differs from displacement_classification? " 918 + str( 919 displacement[displacement != displacement_classification].size 920 ) 921 ) 922 print( 923 " ... percent of samples where displacement differs from displacement_classification = " 924 + format( 925 float( 926 displacement[ 927 displacement != displacement_classification 928 ].size 929 ) 930 / samples_in_cube, 931 "5.1%", 932 ) 933 ) 934 try: 935 print( 936 " ... (displacement differs from displacement_classification).mean() " 937 + str( 938 displacement[ 939 displacement != displacement_classification 940 ].mean() 941 ) 942 ) 943 print( 944 " ... (displacement differs from displacement_classification).max() " 945 + str( 946 displacement[ 947 displacement != displacement_classification 948 ].max() 949 ) 950 ) 951 except: 952 pass 953 954 print( 955 " ...displacement_classification.min() = " 956 + ", " 957 + str(displacement_classification.min()) 958 ) 959 print( 960 " ...displacement_classification.mean() = " 961 + ", " 962 + str(displacement_classification.mean()) 963 ) 964 print( 965 " ...displacement_classification.max() = " 966 + ", " 967 + str(displacement_classification.max()) 968 ) 969 970 print(" ...displacement.min() = " + ", " + str(displacement.min())) 971 print(" ...displacement.mean() = " + ", " + str(displacement.mean())) 972 print(" ...displacement.max() = " + ", " + str(displacement.max())) 973 if fault_segm[fault_segm > 0.0].size > 0: 974 print( 975 " ...displacement[fault_segm >0].min() = " 976 + ", " 977 + str(displacement[fault_segm > 0].min()) 978 ) 979 print( 980 " ...displacement[fault_segm >0] P10 = " 981 + ", " 982 + str(np.percentile(displacement[fault_segm > 0], 10)) 983 ) 984 print( 985 " ...displacement[fault_segm >0] P25 = " 986 + ", " 987 + str(np.percentile(displacement[fault_segm > 0], 25)) 988 ) 989 print( 990 " ...displacement[fault_segm >0].mean() = " 991 + ", " 992 + str(displacement[fault_segm > 0].mean()) 993 ) 994 print( 995 " ...displacement[fault_segm >0] P75 = " 996 + ", " 997 + str(np.percentile(displacement[fault_segm > 0], 75)) 998 ) 999 print( 1000 " ...displacement[fault_segm >0] P90 = " 1001 + ", " 1002 + str(np.percentile(displacement[fault_segm > 0], 90)) 1003 ) 1004 print( 1005 " ...displacement[fault_segm >0].max() = " 1006 + ", " 1007 + str(displacement[fault_segm > 0].max()) 1008 ) 1009 1010 inline = self.cfg.cube_shape[0] // 2 1011 1012 # limit labels to portions of fault plane with throw above threshold 1013 throw_threshold_samples = 1.0 1014 footprint = np.ones((3, 3, 1)) 1015 fp_i, fp_j, fp_k = np.where( 1016 (fault_segm > 0.25) 1017 & (displacement_classification > throw_threshold_samples) 1018 ) 1019 fault_segm = np.zeros_like(fault_segm) 1020 fault_plane_displacement = np.zeros_like(fault_segm) 1021 fault_segm[fp_i, fp_j, fp_k] = 1.0 1022 fault_plane_displacement[fp_i, fp_j, fp_k] = ( 1023 displacement_classification[fp_i, fp_j, fp_k] * 1.0 1024 ) 1025 1026 fault_voxel_count_list.append(fault_segm[fault_segm > 0.5].size) 1027 1028 # create blended version of displacement that accounts for simulation of fault drag 1029 drag_factor = 0.5 1030 displacement = ( 1031 1.0 - drag_factor 1032 ) * displacement + drag_factor * displacement_classification 1033 1034 # project fault depth on 2D map surface 1035 fault_plane_map = np.zeros_like(wb) 1036 depth_indices = np.arange(ellipsoid.shape[-1]) 1037 for ii in range(fault_plane_map.shape[0]): 1038 for jj in range(fault_plane_map.shape[1]): 1039 fault_plane_map[ii, jj] = np.interp( 1040 1.0, ellipsoid[ii, jj, :], depth_indices 1041 ) 1042 fault_plane_map = fault_plane_map.clip(0, ellipsoid.shape[2] - 1) 1043 1044 # compute fault azimuth (relative to i,j,k indices, not North) 1045 dx, dy = np.gradient(fault_plane_map) 1046 strike_angle = np.arctan2(dy, dx) * 180.0 / np.pi # 2D 1047 del dx 1048 del dy 1049 strike_angle = np.zeros_like(fault_segm) + strike_angle.reshape( 1050 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1051 ) 1052 strike_angle[fault_segm < 0.5] = 200.0 1053 1054 # - compute faulted depth as max of fault plane or faulted depth 1055 fault_plane = np.zeros_like(displacement) + fault_plane_map.reshape( 1056 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1057 ) 1058 1059 if ifault == 0: 1060 print(" .... set _unfaulted_depths to array with all zeros...") 1061 _faulted_depths = unfaulted_depths * 1.0 1062 self.fault_plane_azimuth[:] = strike_angle * 1.0 1063 1064 print(" ... interpolation = " + str(interpolation)) 1065 1066 if ( 1067 interpolation 1068 ): # i.e. if the fault should be considered, apply the displacements and append fault plane 1069 faulted_depths2 = np.zeros( 1070 (ellipsoid.shape[0], ellipsoid.shape[1], ellipsoid.shape[2], 2), 1071 "float", 1072 ) 1073 faulted_depths2[:, :, :, 0] = displacement * 1.0 1074 faulted_depths2[:, :, :, 1] = fault_plane - faulted_depths 1075 faulted_depths2[:, :, :, 1][ellipsoid > 1] = 0.0 1076 map_displacement_vector = np.min(faulted_depths2, axis=-1).clip( 1077 0.0, None 1078 ) 1079 del faulted_depths2 1080 map_displacement_vector[ellipsoid > 1] = 0.0 1081 1082 self.sum_map_displacements[:] += map_displacement_vector 1083 displacements_class += displacement_classification 1084 # Set displacements outside ellipsoid to 0 1085 displacement[ellipsoid > 1] = 0 1086 1087 sum_displacements += displacement 1088 1089 # apply fault to depth_cube 1090 if ifault == 0: 1091 print(" .... set _unfaulted_depths to array with all zeros...") 1092 _faulted_depths = unfaulted_depths * 1.0 1093 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1094 0, ellipsoid.shape[-1] - 1 1095 ) 1096 _faulted_depths = apply_faulting( 1097 _faulted_depths, adjusted_faulted_depths 1098 ) 1099 _mft = self.max_fault_throw[:] 1100 _mft[ellipsoid <= 1.0] = throw 1101 self.max_fault_throw[:] = _mft 1102 del _mft 1103 self.fault_planes[:] = fault_segm * 1.0 1104 _fault_intersections = self.fault_intersections[:] 1105 previous_intersection_voxel_count = _fault_intersections[ 1106 _fault_intersections > 1.1 1107 ].size 1108 _fault_intersections = self.fault_planes[:] * 1.0 1109 if ( 1110 _fault_intersections[_fault_intersections > 1.1].size 1111 > previous_intersection_voxel_count 1112 ): 1113 number_fault_intersections += 1 1114 self.fault_intersections[:] = _fault_intersections 1115 self.fault_plane_throw[:] = fault_plane_displacement * 1.0 1116 self.fault_plane_azimuth[:] = strike_angle * 1.0 1117 else: 1118 print( 1119 " .... update _unfaulted_depths array using faulted depths..." 1120 ) 1121 try: 1122 print( 1123 " .... (before) _faulted_depths.mean() = " 1124 + str(_faulted_depths.mean()) 1125 ) 1126 except: 1127 _faulted_depths = unfaulted_depths * 1.0 1128 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1129 0, ellipsoid.shape[-1] - 1 1130 ) 1131 _faulted_depths = apply_faulting( 1132 _faulted_depths, adjusted_faulted_depths 1133 ) 1134 1135 _fault_planes = apply_faulting( 1136 self.fault_planes[:], adjusted_faulted_depths 1137 ) 1138 if ( 1139 _fault_planes[ 1140 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1141 ].size 1142 > 0 1143 ): 1144 _fault_planes[ 1145 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1146 ] = 0.0 1147 if ( 1148 _fault_planes[ 1149 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1150 ].size 1151 > 0 1152 ): 1153 _fault_planes[ 1154 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1155 ] = 1.0 1156 self.fault_planes[:] = _fault_planes 1157 del _fault_planes 1158 1159 _fault_intersections = apply_faulting( 1160 self.fault_intersections[:], adjusted_faulted_depths 1161 ) 1162 if ( 1163 _fault_intersections[ 1164 np.logical_and( 1165 1.25 < _fault_intersections, _fault_intersections < 2.0 1166 ) 1167 ].size 1168 > 0 1169 ): 1170 _fault_intersections[ 1171 np.logical_and( 1172 1.25 < _fault_intersections, _fault_intersections < 2.0 1173 ) 1174 ] = 2.0 1175 self.fault_intersections[:] = _fault_intersections 1176 del _fault_intersections 1177 1178 self.max_fault_throw[:] = apply_faulting( 1179 self.max_fault_throw[:], adjusted_faulted_depths 1180 ) 1181 self.fault_plane_throw[:] = apply_faulting( 1182 self.fault_plane_throw[:], adjusted_faulted_depths 1183 ) 1184 self.fault_plane_azimuth[:] = apply_faulting( 1185 self.fault_plane_azimuth[:], adjusted_faulted_depths 1186 ) 1187 1188 self.fault_planes[:] += fault_segm 1189 self.fault_intersections[:] += fault_segm 1190 _mft = self.max_fault_throw[:] 1191 _mft[ellipsoid <= 1.0] += throw 1192 self.max_fault_throw[:] = _mft 1193 del _mft 1194 1195 if verbose: 1196 print( 1197 " ... fault_plane_displacement[fault_plane_displacement > 0.].size = " 1198 + str( 1199 fault_plane_displacement[ 1200 fault_plane_displacement > 0.0 1201 ].size 1202 ) 1203 ) 1204 self.fault_plane_throw[ 1205 fault_plane_displacement > 0.0 1206 ] = fault_plane_displacement[fault_plane_displacement > 0.0] 1207 if verbose: 1208 print( 1209 " ... self.fault_plane_throw[self.fault_plane_throw > 0.].size = " 1210 + str( 1211 self.fault_plane_throw[ 1212 self.fault_plane_throw > 0.0 1213 ].size 1214 ) 1215 ) 1216 self.fault_plane_azimuth[fault_segm > 0.9] = ( 1217 strike_angle[fault_segm > 0.9] * 1.0 1218 ) 1219 1220 if verbose: 1221 print( 1222 " .... (after) _faulted_depths.mean() = " 1223 + str(_faulted_depths.mean()) 1224 ) 1225 1226 # fix interpolated values in max_fault_throw 1227 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1228 self.max_fault_throw, return_counts=True 1229 ) 1230 max_fault_throw_list = max_fault_throw_list[ 1231 max_fault_throw_list_counts > 500 1232 ] 1233 if verbose: 1234 print( 1235 "\n ...max_fault_throw_list = " 1236 + str(max_fault_throw_list) 1237 + ", " 1238 + str(max_fault_throw_list_counts) 1239 ) 1240 mfts = self.max_fault_throw.shape 1241 self.cfg.hdf_remove_node_list("max_fault_throw_4d_diff") 1242 self.cfg.hdf_remove_node_list("max_fault_throw_4d") 1243 max_fault_throw_4d_diff = self.cfg.hdf_init( 1244 "max_fault_throw_4d_diff", 1245 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1246 ) 1247 max_fault_throw_4d = self.cfg.hdf_init( 1248 "max_fault_throw_4d", 1249 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1250 ) 1251 if verbose: 1252 print( 1253 "\n ...max_fault_throw_4d.shape = " 1254 + ", " 1255 + str(max_fault_throw_4d.shape) 1256 ) 1257 _max_fault_throw_4d_diff = max_fault_throw_4d_diff[:] 1258 _max_fault_throw_4d = max_fault_throw_4d[:] 1259 for imft, mft in enumerate(max_fault_throw_list): 1260 print( 1261 " ... imft, mft, max_fault_throw_4d_diff[:,:,:,imft].shape = " 1262 + str( 1263 ( 1264 imft, 1265 mft, 1266 max_fault_throw_4d_diff[:, :, :, imft].shape, 1267 ) 1268 ) 1269 ) 1270 _max_fault_throw_4d_diff[:, :, :, imft] = np.abs( 1271 self.max_fault_throw[:, :, :] - mft 1272 ) 1273 _max_fault_throw_4d[:, :, :, imft] = mft 1274 max_fault_throw_4d[:] = _max_fault_throw_4d 1275 max_fault_throw_4d_diff[:] = _max_fault_throw_4d_diff 1276 if verbose: 1277 print( 1278 " ...np.argmin(max_fault_throw_4d_diff, axis=-1).shape = " 1279 + ", " 1280 + str(np.argmin(max_fault_throw_4d_diff, axis=-1).shape) 1281 ) 1282 indices_nearest_throw = np.argmin(max_fault_throw_4d_diff, axis=-1) 1283 if verbose: 1284 print( 1285 "\n ...indices_nearest_throw.shape = " 1286 + ", " 1287 + str(indices_nearest_throw.shape) 1288 ) 1289 _max_fault_throw = self.max_fault_throw[:] 1290 for imft, mft in enumerate(max_fault_throw_list): 1291 _max_fault_throw[indices_nearest_throw == imft] = mft 1292 self.max_fault_throw[:] = _max_fault_throw 1293 1294 del _max_fault_throw 1295 del adjusted_faulted_depths 1296 1297 if verbose: 1298 print( 1299 " ...fault_segm[fault_segm>0.].size = " 1300 + ", " 1301 + str(fault_segm[fault_segm > 0.0].size) 1302 ) 1303 print(" ...fault_segm.min() = " + ", " + str(fault_segm.min())) 1304 print(" ...fault_segm.max() = " + ", " + str(fault_segm.max())) 1305 print( 1306 " ...self.fault_planes.max() = " 1307 + ", " 1308 + str(self.fault_planes.max()) 1309 ) 1310 print( 1311 " ...self.fault_intersections.max() = " 1312 + ", " 1313 + str(self.fault_intersections.max()) 1314 ) 1315 1316 print( 1317 " ...list of unique values in self.max_fault_throw = " 1318 + ", " 1319 + str(np.unique(self.max_fault_throw)) 1320 ) 1321 1322 # TODO: remove this block after qc/tests complete 1323 from datagenerator.util import import_matplotlib 1324 plt = import_matplotlib() 1325 1326 plt.close(35) 1327 plt.figure(35, figsize=(15, 10)) 1328 plt.clf() 1329 plt.imshow(_faulted_depths[inline, :, :].T, aspect="auto", cmap="prism") 1330 plt.tight_layout() 1331 plt.savefig( 1332 os.path.join( 1333 self.cfg.work_subfolder, f"faulted_depths_{ifault:02d}.png" 1334 ), 1335 format="png", 1336 ) 1337 1338 plt.close(35) 1339 plt.figure(36, figsize=(15, 10)) 1340 plt.clf() 1341 plt.imshow(displacement[inline, :, :].T, aspect="auto", cmap="jet") 1342 plt.tight_layout() 1343 plt.savefig( 1344 os.path.join( 1345 self.cfg.work_subfolder, f"displacement_{ifault:02d}.png" 1346 ), 1347 format="png", 1348 ) 1349 1350 # TODO: remove this block after qc/tests complete 1351 _plotarray = self.fault_planes[inline, :, :].copy() 1352 _plotarray[_plotarray == 0.0] = np.nan 1353 plt.close(37) 1354 plt.figure(37, figsize=(15, 10)) 1355 plt.clf() 1356 plt.imshow(_plotarray.T, aspect="auto", cmap="gist_ncar") 1357 plt.tight_layout() 1358 plt.savefig( 1359 os.path.join(self.cfg.work_subfolder, f"fault_{ifault:02d}.png"), 1360 format="png", 1361 ) 1362 plt.close(37) 1363 plt.figure(36) 1364 plt.imshow(_plotarray.T, aspect="auto", cmap="gray", alpha=0.6) 1365 plt.tight_layout() 1366 1367 plt.savefig( 1368 os.path.join( 1369 self.cfg.work_subfolder, 1370 f"displacement_fault_overlay_{ifault:02d}.png", 1371 ), 1372 format="png", 1373 ) 1374 plt.close(36) 1375 1376 hockey_sticks.append(hockey_stick) 1377 print(" ...hockey_sticks = " + ", " + str(hockey_sticks)) 1378 1379 # print final count 1380 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1381 self.max_fault_throw, return_counts=True 1382 ) 1383 max_fault_throw_list = max_fault_throw_list[max_fault_throw_list_counts > 100] 1384 if verbose: 1385 print( 1386 "\n ... ** final ** max_fault_throw_list = " 1387 + str(max_fault_throw_list) 1388 + ", " 1389 + str(max_fault_throw_list_counts) 1390 ) 1391 1392 # create fault intersections 1393 _fault_intersections = self.fault_planes[:] * 1.0 1394 # self.fault_intersections = self.fault_planes * 1. 1395 if verbose: 1396 print(" ... line 592:") 1397 print( 1398 " ...self.fault_intersections.max() = " 1399 + ", " 1400 + str(_fault_intersections.max()) 1401 ) 1402 print( 1403 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1404 + ", " 1405 + str(_fault_intersections[_fault_intersections > 0.0].size) 1406 ) 1407 1408 _fault_intersections[_fault_intersections <= 1.0] = 0.0 1409 _fault_intersections[_fault_intersections > 1.1] = 1.0 1410 if verbose: 1411 print(" ... line 597:") 1412 print( 1413 " ...self.fault_intersections.max() = " 1414 + ", " 1415 + str(_fault_intersections.max()) 1416 ) 1417 print( 1418 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1419 + ", " 1420 + str(_fault_intersections[_fault_intersections > 0.0].size) 1421 ) 1422 1423 # make 2nd count of number of intersections between faults. write result to logfile. 1424 from datetime import datetime 1425 1426 start_time = datetime.now() 1427 number_fault_intersections = max( 1428 number_fault_intersections, 1429 measure.label(_fault_intersections, background=0).max(), 1430 ) 1431 print( 1432 f" ... elapsed time for skimage.label = {(datetime.now() - start_time)}" 1433 ) 1434 print(" ... number_fault_intersections = " + str(number_fault_intersections)) 1435 1436 # dilate intersection values 1437 # - window size of (5,5,15) is arbitrary. Should be based on isolating sections 1438 # of fault planes on real seismic 1439 _fault_intersections = maximum_filter(_fault_intersections, size=(7, 7, 17)) 1440 1441 # Fault intersection segments > 1 at intersecting faults. Clip to 1 1442 _fault_intersections[_fault_intersections > 0.05] = 1.0 1443 _fault_intersections[_fault_intersections != 1.0] = 0.0 1444 if verbose: 1445 print(" ... line 607:") 1446 print( 1447 " ...self.fault_intersections.max() = " 1448 + ", " 1449 + str(_fault_intersections.max()) 1450 ) 1451 print( 1452 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1453 + ", " 1454 + str(_fault_intersections[_fault_intersections > 0.0].size) 1455 ) 1456 1457 # Fault segments = 1 at fault > 1 at intersections. Clip intersecting fault voxels to 1 1458 _fault_planes = self.fault_planes[:] 1459 _fault_planes[_fault_planes > 0.05] = 1.0 1460 _fault_planes[_fault_planes != 1.0] = 0.0 1461 1462 # make fault azi 3D and only retain voxels in fault plane 1463 1464 for k, v in zip( 1465 [ 1466 "n_voxels_faults", 1467 "n_voxels_fault_intersections", 1468 "number_fault_intersections", 1469 "fault_voxel_count_list", 1470 "hockey_sticks", 1471 ], 1472 [ 1473 _fault_planes[_fault_planes > 0.5].size, 1474 _fault_intersections[_fault_intersections > 0.5].size, 1475 number_fault_intersections, 1476 fault_voxel_count_list, 1477 hockey_sticks, 1478 ], 1479 ): 1480 self.cfg.write_to_logfile(f"{k}: {v}") 1481 1482 self.cfg.write_to_logfile( 1483 msg=None, mainkey="model_parameters", subkey=k, val=v 1484 ) 1485 1486 dis_class = _faulted_depths * 1 1487 self.fault_intersections[:] = _fault_intersections 1488 del _fault_intersections 1489 self.fault_planes[:] = _fault_planes 1490 del _fault_planes 1491 self.displacement_vectors[:] = _faulted_depths * 1.0 1492 1493 # TODO: check if next line of code modifies 'displacement' properly 1494 self.sum_map_displacements[:] = _faulted_depths * 1.0 1495 1496 # Save faulted maps 1497 self.faulted_depth_maps[:] = depth_maps_faulted_infilled 1498 self.faulted_depth_maps_gaps[:] = depth_maps_gaps 1499 1500 # perform faulting for fault_segments and fault_intersections 1501 return dis_class, hockey_sticks 1502 1503 def _qc_plot_check_faulted_horizons_match_fault_segments( 1504 self, faulted_depth_maps, faulted_geologic_age 1505 ): 1506 """_qc_plot_check_faulted_horizons_match_fault_segments _summary_ 1507 1508 Parameters 1509 ---------- 1510 faulted_depth_maps : np.ndarray 1511 The faluted depth maps 1512 faulted_geologic_age : _type_ 1513 _description_ 1514 """ 1515 import os 1516 from datagenerator.util import import_matplotlib 1517 1518 plt = import_matplotlib() 1519 1520 plt.figure(1, figsize=(15, 10)) 1521 plt.clf() 1522 voxel_count_max = 0 1523 inline = self.cfg.cube_shape[0] // 2 1524 for i in range(0, self.fault_planes.shape[1], 10): 1525 voxel_count = self.fault_planes[:, i, :] 1526 voxel_count = voxel_count[voxel_count > 0.5].size 1527 if voxel_count > voxel_count_max: 1528 inline = i 1529 voxel_count_max = voxel_count 1530 # inline=50 1531 plotdata = (faulted_geologic_age[:, inline, :]).T 1532 plotdata_overlay = (self.fault_planes[:, inline, :]).T 1533 plotdata[plotdata_overlay > 0.9] = plotdata.max() 1534 plt.imshow(plotdata, cmap="prism", aspect="auto") 1535 plt.title( 1536 "QC plot: faulted layers with horizons and fault_segments overlain" 1537 + "\nInline " 1538 + str(inline) 1539 ) 1540 plt.colorbar() 1541 for i in range(0, faulted_depth_maps.shape[2], 10): 1542 plt.plot( 1543 range(self.cfg.cube_shape[0]), 1544 faulted_depth_maps[:, inline], 1545 "k-", 1546 lw=0.25, 1547 ) 1548 plot_name = os.path.join( 1549 self.cfg.work_subfolder, "QC_horizons_from_geologic_age_isovalues.png" 1550 ) 1551 plt.savefig(plot_name, format="png") 1552 plt.close() 1553 # 1554 plt.figure(1, figsize=(15, 10)) 1555 plt.clf() 1556 voxel_count_max = 0 1557 iz = faulted_geologic_age.shape[-1] // 2 1558 for i in range(0, self.fault_planes.shape[-1], 50): 1559 voxel_count = self.fault_planes[:, :, i] 1560 voxel_count = voxel_count[voxel_count > 0.5].size 1561 if voxel_count > voxel_count_max: 1562 iz = i 1563 voxel_count_max = voxel_count 1564 1565 plotdata = faulted_geologic_age[:, :, iz].copy() 1566 plotdata2 = self.fault_planes[:, :, iz].copy() 1567 plotdata[plotdata2 > 0.05] = 0.0 1568 plt.subplot(1, 2, 1) 1569 plt.title(str(iz)) 1570 plt.imshow(plotdata.T, cmap="prism", aspect="auto") 1571 plt.subplot(1, 2, 2) 1572 plt.imshow(plotdata2.T, cmap="jet", aspect="auto") 1573 plt.colorbar() 1574 plot_name = os.path.join( 1575 self.cfg.work_subfolder, "QC_fault_segments_on_geologic_age_timeslice.png" 1576 ) 1577 plt.savefig(plot_name, format="png") 1578 plt.close() 1579 1580 def improve_depth_maps_post_faulting( 1581 self, 1582 unfaulted_geologic_age: np.ndarray, 1583 faulted_geologic_age: np.ndarray, 1584 onlap_clips: np.ndarray 1585 ): 1586 """ 1587 Re-interpolates the depth maps using the faulted geologic age cube 1588 1589 Parameters 1590 ---------- 1591 unfaulted_geologic_age : np.ndarray 1592 The unfaulted geologic age cube 1593 faulted_geologic_age : np.ndarray 1594 The faulted geologic age cube 1595 onlap_clips : np.ndarray 1596 The onlap clips 1597 1598 Returns 1599 ------- 1600 depth_maps : np.ndarray 1601 The improved depth maps 1602 depth_maps_gaps : np.ndarray 1603 The improved depth maps with gaps 1604 """ 1605 faulted_depth_maps = np.zeros_like(self.faulted_depth_maps) 1606 origtime = np.arange(self.faulted_depth_maps.shape[-1]) 1607 for i in range(self.faulted_depth_maps.shape[0]): 1608 for j in range(self.faulted_depth_maps.shape[1]): 1609 if ( 1610 faulted_geologic_age[i, j, :].min() 1611 != faulted_geologic_age[i, j, :].max() 1612 ): 1613 faulted_depth_maps[i, j, :] = np.interp( 1614 origtime, 1615 faulted_geologic_age[i, j, :], 1616 np.arange(faulted_geologic_age.shape[-1]).astype("float"), 1617 ) 1618 else: 1619 faulted_depth_maps[i, j, :] = unfaulted_geologic_age[i, j, :] 1620 # Waterbottom horizon has been set to 0. Re-insert this from the original depth_maps array 1621 if np.count_nonzero(faulted_depth_maps[:, :, 0]) == 0: 1622 faulted_depth_maps[:, :, 0] = self.faulted_depth_maps[:, :, 0] * 1.0 1623 1624 # Shift re-interpolated horizons to replace first horizon (of 0's) with the second, etc 1625 zmaps = np.zeros_like(faulted_depth_maps) 1626 zmaps[..., :-1] = faulted_depth_maps[..., 1:] 1627 # Fix the deepest re-interpolated horizon by adding a constant thickness to the shallower horizon 1628 zmaps[..., -1] = self.faulted_depth_maps[..., -1] + 10 1629 # Clip this last horizon to the one above 1630 thickness_map = zmaps[..., -1] - zmaps[..., -2] 1631 zmaps[..., -1][np.where(thickness_map <= 0.0)] = zmaps[..., -2][ 1632 np.where(thickness_map <= 0.0) 1633 ] 1634 faulted_depth_maps = zmaps.copy() 1635 1636 if self.cfg.qc_plots: 1637 self._qc_plot_check_faulted_horizons_match_fault_segments( 1638 faulted_depth_maps, faulted_geologic_age 1639 ) 1640 1641 # Re-apply old gaps to improved depth_maps 1642 zmaps_imp = faulted_depth_maps.copy() 1643 merged = zmaps_imp.copy() 1644 _depth_maps_gaps_improved = merged.copy() 1645 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1646 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1647 1648 # for zero-thickness layers, set depth_maps_gaps to nan 1649 for i in range(depth_maps_gaps.shape[-1] - 1): 1650 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1651 # set thicknesses < zero to NaN. Use NaNs in thickness_map to 0 to avoid runtime warning when indexing 1652 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1653 1654 # restore zero thickness from faulted horizons to improved (interpolated) depth maps 1655 ii, jj = np.meshgrid( 1656 range(self.cfg.cube_shape[0]), 1657 range(self.cfg.cube_shape[1]), 1658 sparse=False, 1659 indexing="ij", 1660 ) 1661 merged = zmaps_imp.copy() 1662 1663 # create temporary copy of fault_segments with dilation 1664 from scipy.ndimage.morphology import grey_dilation 1665 1666 _dilated_fault_planes = grey_dilation(self.fault_planes, size=(3, 3, 1)) 1667 1668 _onlap_segments = self.vols.onlap_segments[:] 1669 for ihor in range(depth_maps_gaps.shape[-1] - 1, 2, -1): 1670 # filter upper horizon being used for thickness if shallower events onlap it, except at faults 1671 improved_zmap_thickness = merged[:, :, ihor] - merged[:, :, ihor - 1] 1672 depth_map_int = ((merged[:, :, ihor]).astype(int)).clip( 1673 0, _onlap_segments.shape[-1] - 1 1674 ) 1675 improved_map_onlap_segments = _onlap_segments[ii, jj, depth_map_int] + 0.0 1676 improved_map_fault_segments = ( 1677 _dilated_fault_planes[ii, jj, depth_map_int] + 0.0 1678 ) 1679 # remember that self.maps.depth_maps has horizons with direct fault application 1680 faulted_infilled_map_thickness = ( 1681 self.faulted_depth_maps[:, :, ihor] 1682 - self.faulted_depth_maps[:, :, ihor - 1] 1683 ) 1684 improved_zmap_thickness[ 1685 np.where( 1686 (faulted_infilled_map_thickness <= 0.0) 1687 & (improved_map_onlap_segments > 0.0) 1688 & (improved_map_fault_segments == 0.0) 1689 ) 1690 ] = 0.0 1691 print( 1692 " ... ihor, improved_map_onlap_segments[improved_map_onlap_segments>0.].shape," 1693 " improved_zmap_thickness[improved_zmap_thickness==0].shape = ", 1694 ihor, 1695 improved_map_onlap_segments[improved_map_onlap_segments > 0.0].shape, 1696 improved_zmap_thickness[improved_zmap_thickness == 0].shape, 1697 ) 1698 merged[:, :, ihor - 1] = merged[:, :, ihor] - improved_zmap_thickness 1699 1700 if np.any(self.fan_horizon_list): 1701 # Clip fans downwards when thickness map is zero 1702 for count, layer in enumerate(self.fan_horizon_list): 1703 merged = fix_zero_thickness_fan_layers( 1704 merged, layer, self.fan_thickness[count] 1705 ) 1706 1707 # Re-apply clipping to onlapping layers post faulting 1708 merged = fix_zero_thickness_onlap_layers(merged, onlap_clips) 1709 1710 del _dilated_fault_planes 1711 1712 # Re-apply gaps to improved depth_maps 1713 _depth_maps_gaps_improved = merged.copy() 1714 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1715 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1716 1717 # for zero-thickness layers, set depth_maps_gaps to nan 1718 for i in range(depth_maps_gaps.shape[-1] - 1): 1719 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1720 # set nans in thickness_map to 0 to avoid runtime warning 1721 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1722 1723 return merged, depth_maps_gaps 1724 1725 @staticmethod 1726 def partial_faulting( 1727 depth_map_gaps_faulted, 1728 fault_plane_classification, 1729 faulted_depth_map, 1730 ii, 1731 jj, 1732 max_throw, 1733 origtime_cube, 1734 unfaulted_depth_map, 1735 ): 1736 """ 1737 Partial faulting 1738 ---------------- 1739 1740 Executes partial faluting. 1741 1742 The docstring of this function is a work in progress. 1743 1744 Parameters 1745 ---------- 1746 depth_map_gaps_faulted : np.ndarray 1747 The depth map. 1748 fault_plane_classification : np.ndarray 1749 Fault plane classifications. 1750 faulted_depth_map : _type_ 1751 The faulted depth map. 1752 ii : int 1753 The i position 1754 jj : int 1755 The j position 1756 max_throw : float 1757 The maximum amount of throw for the faults. 1758 origtime_cube : np.ndarray 1759 Original time cube. 1760 unfaulted_depth_map : np.ndarray 1761 Unfaulted depth map. 1762 1763 Returns 1764 ------- 1765 faulted_depth_map : np.ndarray 1766 The faulted depth map 1767 depth_map_gaps_faulted : np.ndarray 1768 Depth map with gaps filled 1769 """ 1770 for ithrow in range(1, int(max_throw) + 1): 1771 # infilled 1772 partial_faulting_map = unfaulted_depth_map + ithrow 1773 partial_faulting_map_ii_jj = ( 1774 partial_faulting_map[ii, jj] 1775 .astype("int") 1776 .clip(0, origtime_cube.shape[2] - 1) 1777 ) 1778 partial_faulting_on_horizon = fault_plane_classification[ 1779 ii, jj, partial_faulting_map_ii_jj 1780 ][..., 0] 1781 origtime_on_horizon = origtime_cube[ii, jj, partial_faulting_map_ii_jj][ 1782 ..., 0 1783 ] 1784 faulted_depth_map[partial_faulting_on_horizon == 1] = np.dstack( 1785 (origtime_on_horizon, partial_faulting_map) 1786 ).min(axis=-1)[partial_faulting_on_horizon == 1] 1787 # gaps 1788 depth_map_gaps_faulted[partial_faulting_on_horizon == 1] = np.nan 1789 return faulted_depth_map, depth_map_gaps_faulted 1790 1791 def get_displacement_vector( 1792 self, 1793 semi_axes: tuple, 1794 origin: tuple, 1795 throw: float, 1796 tilt, 1797 wb, 1798 index, 1799 fp 1800 ): 1801 """ 1802 Gets a displacement vector. 1803 1804 Parameters 1805 ---------- 1806 semi_axes : tuple 1807 The semi axes. 1808 origin : tuple 1809 The origin. 1810 throw : float 1811 The throw of th fault to use. 1812 tilt : float 1813 The tilt of the fault. 1814 1815 Returns 1816 ------- 1817 stretch_times : np.ndarray 1818 The stretch times. 1819 stretch_times_classification : np.ndarray 1820 Stretch times classification. 1821 interpolation : bool 1822 Whether or not to interpolate. 1823 hockey_stick : int 1824 The hockey stick. 1825 fault_segments : np.ndarray 1826 1827 ellipsoid : 1828 fp : 1829 """ 1830 a, b, c = semi_axes 1831 x0, y0, z0 = origin 1832 1833 random_shear_zone_width = ( 1834 np.around(np.random.uniform(low=0.75, high=1.5) * 200, -2) / 200 1835 ) 1836 if random_shear_zone_width == 0: 1837 random_gouge_pctile = 100 1838 else: 1839 # clip amplitudes inside shear_zone with this percentile of total (100 implies doing nothing) 1840 random_gouge_pctile = np.random.triangular(left=10, mode=50, right=100) 1841 # Store the random values 1842 fp["shear_zone_width"][index] = random_shear_zone_width 1843 fp["gouge_pctile"][index] = random_gouge_pctile 1844 1845 if self.cfg.verbose: 1846 print(f" ...shear_zone_width (samples) = {random_shear_zone_width}") 1847 print(f" ...gouge_pctile (percent*100) = {random_gouge_pctile}") 1848 print(f" .... output_cube.shape = {self.vols.geologic_age.shape}") 1849 _p = ( 1850 np.arange(self.vols.geologic_age.shape[2]) * self.cfg.infill_factor 1851 ).shape 1852 print( 1853 f" .... (np.arange(output_cube.shape[2])*infill_factor).shape = {_p}" 1854 ) 1855 1856 ellipsoid = self.rotate_3d_ellipsoid(x0, y0, z0, a, b, c, tilt) 1857 fault_segments = self.get_fault_plane_sobel(ellipsoid) 1858 z_idx = self.get_fault_centre(ellipsoid, wb, fault_segments, index) 1859 1860 # Initialise return objects in case z_idx size == 0 1861 interpolation = False 1862 hockey_stick = 0 1863 # displacement_cube = None 1864 if np.size(z_idx) != 0: 1865 print(" ... Computing fault depth at max displacement") 1866 print(" ... depth at max displacement = {}".format(z_idx[2])) 1867 down = float(ellipsoid[ellipsoid < 1.0].size) / np.prod( 1868 self.vols.geologic_age[:].shape 1869 ) 1870 """ 1871 down = np.int16(len(np.where(ellipsoid < 1.)[0]) / 1.0 * (self.vols.geologic_age.shape[2] * 1872 self.vols.geologic_age.shape[1] * 1873 self.vols.geologic_age.shape[0])) 1874 print(" ... This fault has {!s} %% of downthrown samples".format(down)) 1875 """ 1876 print( 1877 " ... This fault has " 1878 + format(down, "5.1%") 1879 + " of downthrown samples" 1880 ) 1881 1882 ( 1883 stretch_times, 1884 stretch_times_classification, 1885 interpolation, 1886 hockey_stick, 1887 ) = self.xyz_dis(z_idx, throw, fault_segments, ellipsoid, wb, index) 1888 else: 1889 print(" ... Ellipsoid larger than cube no fault inserted") 1890 stretch_times = np.ones_like(ellipsoid) 1891 stretch_times_classification = np.ones_like(self.vols.geologic_age[:]) 1892 1893 max_fault_throw = self.max_fault_throw[:] 1894 max_fault_throw[ellipsoid < 1.0] += int(throw) 1895 self.max_fault_throw[:] = max_fault_throw 1896 1897 return ( 1898 stretch_times, 1899 stretch_times_classification, 1900 interpolation, 1901 hockey_stick, 1902 fault_segments, 1903 ellipsoid, 1904 fp, 1905 ) 1906 1907 def apply_xyz_displacement(self, traces) -> np.ndarray: 1908 """ 1909 Applies XYZ Displacement. 1910 1911 Apply stretching and squeezing previously applied to the input cube 1912 vertically to give all depths the same number of extrema. 1913 1914 This is intended to be a proxy for making the 1915 dominant frequency the same everywhere. 1916 1917 Parameters 1918 ---------- 1919 traces : np.ndarray 1920 Previously stretched/squeezed trace(s) 1921 1922 Returns 1923 ------- 1924 unstretch_traces: np.ndarray 1925 Un-stretched/un-squeezed trace(s) 1926 """ 1927 unstretch_traces = np.zeros_like(traces) 1928 origtime = np.arange(traces.shape[-1]) 1929 1930 print("\t ... Cube parameters going into interpolation") 1931 print(f"\t ... Origtime shape = {len(origtime)}") 1932 print( 1933 f"\t ... stretch_times_effects shape = {self.displacement_vectors.shape}" 1934 ) 1935 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 1936 print(f"\t ... traces shape = {traces.shape}") 1937 1938 for i in range(traces.shape[0]): 1939 for j in range(traces.shape[1]): 1940 if traces[i, j, :].min() != traces[i, j, :].max(): 1941 unstretch_traces[i, j, :] = np.interp( 1942 self.displacement_vectors[i, j, :], origtime, traces[i, j, :] 1943 ) 1944 else: 1945 unstretch_traces[i, j, :] = traces[i, j, :] 1946 return unstretch_traces 1947 1948 def copy_and_divide_depth_maps_by_infill(self, zmaps) -> np.ndarray: 1949 """ 1950 Copy and divide depth maps by infill factor 1951 ------------------------------------------- 1952 1953 Copies and divides depth maps by infill factor. 1954 1955 Parameters 1956 ---------- 1957 zmaps : np.array 1958 The depth maps to copy and divide. 1959 1960 Returns 1961 ------- 1962 np.ndarray 1963 The result of the division 1964 """ 1965 return zmaps / self.cfg.infill_factor 1966 1967 def rotate_3d_ellipsoid( 1968 self, x0, y0, z0, a, b, c, fraction 1969 ) -> np.ndarray: 1970 """ 1971 Rotate a 3D ellipsoid 1972 --------------------- 1973 1974 Parameters 1975 ---------- 1976 x0 : _type_ 1977 _description_ 1978 y0 : _type_ 1979 _description_ 1980 z0 : _type_ 1981 _description_ 1982 a : _type_ 1983 _description_ 1984 b : _type_ 1985 _description_ 1986 c : _type_ 1987 _description_ 1988 fraction : _type_ 1989 _description_ 1990 """ 1991 def f(x1, y1, z1, x_0, y_0, z_0, a1, b1, c1): 1992 return ( 1993 ((x1 - x_0) ** 2) / a1 + ((y1 - y_0) ** 2) / b1 + ((z1 - z_0) ** 2) / c1 1994 ) 1995 1996 x = np.arange(self.vols.geologic_age.shape[0]).astype("float") 1997 y = np.arange(self.vols.geologic_age.shape[1]).astype("float") 1998 z = np.arange(self.vols.geologic_age.shape[2]).astype("float") 1999 2000 xx, yy, zz = np.meshgrid(x, y, z, indexing="ij", sparse=False) 2001 2002 xyz = ( 2003 np.vstack((xx.flatten(), yy.flatten(), zz.flatten())) 2004 .swapaxes(0, 1) 2005 .astype("float") 2006 ) 2007 2008 xyz_rotated = self.apply_3d_rotation( 2009 xyz, self.vols.geologic_age.shape, x0, y0, fraction 2010 ) 2011 2012 xx_rotated = xyz_rotated[:, 0].reshape(self.vols.geologic_age.shape) 2013 yy_rotated = xyz_rotated[:, 1].reshape(self.vols.geologic_age.shape) 2014 zz_rotated = xyz_rotated[:, 2].reshape(self.vols.geologic_age.shape) 2015 2016 ellipsoid = f(xx_rotated, yy_rotated, zz_rotated, x0, y0, z0, a, b, c).reshape( 2017 self.vols.geologic_age.shape 2018 ) 2019 2020 return ellipsoid 2021 2022 @staticmethod 2023 def apply_3d_rotation(inarray, array_shape, x0, y0, fraction): 2024 from math import sqrt, sin, cos, atan2 2025 from numpy import cross, eye 2026 2027 # expm3 deprecated, changed to 'more robust' expm (TM) 2028 from scipy.linalg import expm, norm 2029 2030 def m(axis, angle): 2031 return expm(cross(eye(3), axis / norm(axis) * angle)) 2032 2033 theta = atan2( 2034 fraction 2035 * sqrt((x0 - array_shape[0] / 2) ** 2 + (y0 - array_shape[1] / 2) ** 2), 2036 array_shape[2], 2037 ) 2038 dip_angle = atan2(y0 - array_shape[1] / 2, x0 - array_shape[0] / 2) 2039 2040 strike_unitvector = np.array( 2041 (sin(np.pi - dip_angle), cos(np.pi - dip_angle), 0.0) 2042 ) 2043 m0 = m(strike_unitvector, theta) 2044 2045 outarray = np.dot(m0, inarray.T).T 2046 2047 return outarray 2048 2049 @staticmethod 2050 def get_fault_plane_sobel(test_ellipsoid): 2051 from scipy.ndimage import sobel 2052 from scipy.ndimage import maximum_filter 2053 2054 test_ellipsoid[test_ellipsoid <= 1.0] = 0.0 2055 inside = np.zeros_like(test_ellipsoid) 2056 inside[test_ellipsoid <= 1.0] = 1.0 2057 # method 2 2058 edge = ( 2059 np.abs(sobel(inside, axis=0)) 2060 + np.abs(sobel(inside, axis=1)) 2061 + np.abs(sobel(inside, axis=-1)) 2062 ) 2063 edge_max = maximum_filter(edge, size=(5, 5, 5)) 2064 edge_max[edge_max == 0.0] = 1e6 2065 fault_segments = edge / edge_max 2066 fault_segments[np.isnan(fault_segments)] = 0.0 2067 fault_segments[fault_segments < 0.5] = 0.0 2068 fault_segments[fault_segments > 0.5] = 1.0 2069 return fault_segments 2070 2071 def get_fault_centre(self, ellipsoid, wb_time_map, z_on_ellipse, index): 2072 def find_nearest(array, value): 2073 idx = (np.abs(array - value)).argmin() 2074 return array[idx] 2075 2076 def intersec(ell, thresh, x, y, z): 2077 abc = np.where( 2078 (ell[x, y, z] < np.float32(1 + thresh)) 2079 & (ell[x, y, z] > np.float32(1 - thresh)) 2080 ) 2081 if np.size(abc[0]) != 0: 2082 direction = find_nearest(abc[0], 1) 2083 else: 2084 direction = 9999 2085 print( 2086 " ... computing intersection points between ellipsoid and cube, raise error if none found" 2087 ) 2088 xdir_min = intersec(ell, thresh, x, y[0], z[0]) 2089 xdir_max = intersec(ell, thresh, x, y[-1], z[0]) 2090 ydir_min = intersec(ell, thresh, x[0], y, z[0]) 2091 ydir_max = intersec(ell, thresh, x[-1], y, z[0]) 2092 2093 print(" ... xdir_min coord = ", xdir_min, y[0], z[0]) 2094 print(" ... xdir_max coord = ", xdir_max, y[-1], z[0]) 2095 print(" ... ydir_min coord = ", y[0], ydir_min, z[0]) 2096 print(" ... ydir_max coord = ", y[-1], ydir_max, z[0]) 2097 return direction 2098 2099 def get_middle_z(ellipse, wb_map, idx, verbose=False): 2100 # Retrieve indices and cube of point on the ellipsoid and under the sea bed 2101 2102 random_idx = [] 2103 do_it = True 2104 origtime = np.array(range(ellipse.shape[-1])) 2105 wb_time_cube = np.reshape( 2106 wb_map, (wb_map.shape[0], wb_map.shape[1], 1) 2107 ) * np.ones_like(origtime) 2108 abc = np.where((z_on_ellipse == 1) & ((wb_time_cube - origtime) <= 0)) 2109 xyz = np.vstack(abc) 2110 if verbose: 2111 print(" ... xyz.shape = ", xyz.shape) 2112 if np.size(abc[0]) != 0: 2113 xyz_xyz = np.array([]) 2114 threshold_center = 5 2115 while xyz_xyz.size == 0: 2116 if threshold_center < z_on_ellipse.shape[0]: 2117 z_middle = np.where( 2118 np.abs(xyz[2] - int((xyz[2].min() + xyz[2].max()) / 2)) 2119 < threshold_center 2120 ) 2121 xyz_z = xyz[:, z_middle[0]].copy() 2122 if verbose: 2123 print(" ... xyz_z.shape = ", xyz_z.shape) 2124 if xyz_z.size != 0: 2125 x_middle = np.where( 2126 np.abs( 2127 xyz_z[0] 2128 - int((xyz_z[0].min() + xyz_z[0].max()) / 2) 2129 ) 2130 < threshold_center 2131 ) 2132 xyz_xz = xyz_z[:, x_middle[0]].copy() 2133 if verbose: 2134 print(" ... xyz_xz.shape = ", xyz_xz.shape) 2135 if xyz_xz.size != 0: 2136 y_middle = np.where( 2137 np.abs( 2138 xyz_xz[1] 2139 - int((xyz_xz[1].min() + xyz_xz[1].max()) / 2) 2140 ) 2141 < threshold_center 2142 ) 2143 xyz_xyz = xyz_xz[:, y_middle[0]].copy() 2144 if verbose: 2145 print(" ... xyz_xyz.shape = ", xyz_xyz.shape) 2146 if verbose: 2147 print(" ... z for upper intersection = ", xyz[2].min()) 2148 print(" ... z for lower intersection = ", xyz[2].max()) 2149 print(" ... threshold_center used = ", threshold_center) 2150 threshold_center += 5 2151 else: 2152 print(" ... Break the loop, could not find a suitable point") 2153 random_idx = [] 2154 do_it = False 2155 break 2156 if do_it: 2157 from scipy import random 2158 2159 random_idx = xyz_xyz[:, random.choice(xyz_xyz.shape[1])] 2160 print( 2161 " ... Computing fault middle to hang max displacement function" 2162 ) 2163 print(" ... x idx for max displacement = ", random_idx[0]) 2164 print(" ... y idx for max displacement = ", random_idx[1]) 2165 print(" ... z idx for max displacement = ", random_idx[2]) 2166 print( 2167 " ... ellipsoid value = ", 2168 ellipse[random_idx[0], random_idx[1], random_idx[2]], 2169 ) 2170 else: 2171 print( 2172 " ... Empty intersection between fault and cube, assign d-max at cube lower corner" 2173 ) 2174 2175 return random_idx 2176 2177 z_idx = get_middle_z(ellipsoid, wb_time_map, index) 2178 return z_idx 2179 2180 def xyz_dis(self, z_idx, throw, z_on_ellipse, ellipsoid, wb, index): 2181 from scipy import interpolate, signal 2182 from math import atan2, degrees 2183 from scipy.stats import multivariate_normal 2184 from scipy.ndimage.interpolation import rotate 2185 2186 cube_shape = self.vols.geologic_age.shape 2187 2188 def u_gaussian(d_max, sig, shape, points): 2189 from scipy.signal.windows import general_gaussian 2190 2191 return d_max * general_gaussian(points, shape, np.float32(sig)) 2192 2193 # Choose random values sigma, p and coef 2194 sigma = np.random.uniform(low=10 * throw - 50, high=300) 2195 p = np.random.uniform(low=1.5, high=5) 2196 coef = np.random.uniform(1.3, 1.5) 2197 2198 # Fault plane 2199 infill_factor = 0.5 2200 origtime = np.arange(cube_shape[-1]) 2201 z = np.arange(cube_shape[2]).astype("int") 2202 # Define Gaussian max throw and roll it to chosen z 2203 # Gaussian should be defined on at least 5 sigma on each side 2204 roll_int = int(10 * sigma) 2205 g = u_gaussian(throw, sigma, p, cube_shape[2] + 2 * roll_int) 2206 # Pad signal by 10*sigma before rolling 2207 g_padded_rolled = np.roll(g, np.int32(z_idx[2] + g.argmax() + roll_int)) 2208 count = 0 2209 wb_x = np.where(z_on_ellipse == 1)[0] 2210 wb_y = np.where(z_on_ellipse == 1)[1] 2211 print(" ... Taper fault so it doesn't reach seabed") 2212 print(f" ... Sea floor max = {wb[wb_x, wb_y].max()}") 2213 # Shift Z throw so that id doesn't cross sea bed 2214 while g_padded_rolled[int(roll_int + wb[wb_x, wb_y].max())] > 1: 2215 g_padded_rolled = np.roll(g_padded_rolled, 5) 2216 count += 5 2217 # Break loop if can't find spot 2218 if count > cube_shape[2] - wb[wb_x, wb_y].max(): 2219 print(" ... Too many rolled sample, seafloor will not have 0 throw") 2220 break 2221 print(f" ... Vertical throw shifted by {str(count)} samples") 2222 g_centered = g_padded_rolled[roll_int : cube_shape[2] + roll_int] 2223 2224 ff = interpolate.interp1d(z, g_centered) 2225 z_shift = ff 2226 print(" ... Computing Gaussian distribution function") 2227 print(f" ... Max displacement = {int(throw)}") 2228 print(f" ... Sigma = {int(sigma)}") 2229 print(f" ... P = {int(p)}") 2230 2231 low_fault_throw = 5 2232 high_fault_throw = 35 2233 # Parameters to set ratio of 1.4 seems to be optimal for a 1500x1500 grid 2234 mu_x = 0 2235 mu_y = 0 2236 2237 # Use throw to get length 2238 throw_range = np.arange(low_fault_throw, high_fault_throw, 1) 2239 # Max throw vs length relationship 2240 fault_length = np.power(0.0013 * throw_range, 1.3258) 2241 # Max throw == 16000 2242 scale_factor = 16000 / fault_length[-1] 2243 # Coef random selection moved to top of function 2244 variance_x = scale_factor * fault_length[np.where(throw_range == int(throw))] 2245 variance_y = variance_x * coef 2246 # Do the same for the drag zone area 2247 fault_length_drag = fault_length / 10000 2248 variance_x_drag = ( 2249 scale_factor * fault_length_drag[np.where(throw_range == int(throw))] 2250 ) 2251 variance_y_drag = variance_x_drag * coef 2252 # Rotation from z_idx to center 2253 alpha = atan2(z_idx[1] - cube_shape[1] / 2, z_idx[0] - cube_shape[0] / 2) 2254 print(f" ... Variance_x, Variance_y = {variance_x} {variance_y}") 2255 print( 2256 f" ... Angle between max displacement point tangent plane and cube = {int(degrees(alpha))} Degrees" 2257 ) 2258 print(f" ... Max displacement point at x,y,z = {z_idx}") 2259 2260 # Create grid and multivariate normal 2261 x = np.linspace( 2262 -int(cube_shape[0] + 1.5 * cube_shape[0]), 2263 int(cube_shape[0] + 1.5 * cube_shape[0]), 2264 2 * int(cube_shape[0] + 1.5 * cube_shape[0]), 2265 ) 2266 y = np.linspace( 2267 -int(cube_shape[1] + 1.5 * cube_shape[1]), 2268 int(cube_shape[1] + 1.5 * cube_shape[1]), 2269 2 * int(cube_shape[1] + 1.5 * cube_shape[1]), 2270 ) 2271 _x, _y = np.meshgrid(x, y) 2272 pos = np.empty(_x.shape + (2,)) 2273 pos[:, :, 0] = _x 2274 pos[:, :, 1] = _y 2275 rv = multivariate_normal([mu_x, mu_y], [[variance_x, 0], [0, variance_y]]) 2276 rv_drag = multivariate_normal( 2277 [mu_x, mu_y], [[variance_x_drag, 0], [0, variance_y_drag]] 2278 ) 2279 # Scale up by mu order of magnitude and swap axes 2280 xy_dis = 10000 * (rv.pdf(pos)) 2281 xy_dis_drag = 10000 * (rv_drag.pdf(pos)) 2282 2283 # Normalize 2284 xy_dis = xy_dis / np.amax(xy_dis.flatten()) 2285 xy_dis_drag = (xy_dis_drag / np.amax(xy_dis_drag.flatten())) * 0.99 2286 2287 # Rotate plane by alpha 2288 x = np.linspace(0, cube_shape[0], cube_shape[0]) 2289 y = np.linspace(0, cube_shape[1], cube_shape[1]) 2290 _x, _y = np.meshgrid(x, y) 2291 xy_dis_rotated = np.zeros_like(xy_dis) 2292 xy_dis_drag_rotated = np.zeros_like(xy_dis) 2293 rotate( 2294 xy_dis_drag, 2295 degrees(alpha), 2296 reshape=False, 2297 output=xy_dis_drag_rotated, 2298 mode="nearest", 2299 ) 2300 rotate( 2301 xy_dis, degrees(alpha), reshape=False, output=xy_dis_rotated, mode="nearest" 2302 ) 2303 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2304 xy_dis = xy_dis[ 2305 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2306 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2307 ].copy() 2308 xy_dis_drag = xy_dis_drag[ 2309 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2310 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2311 ].copy() 2312 2313 # taper edges of xy_dis_drag in 2d to avoid edge artifacts in fft 2314 print(" ... xy_dis_drag.shape = " + str(xy_dis_drag.shape)) 2315 print( 2316 " ... xy_dis_drag[xy_dis_drag>0.].size = " 2317 + str(xy_dis_drag[xy_dis_drag > 0.0].size) 2318 ) 2319 print( 2320 " ... xy_dis_drag.shape min/mean/max= " 2321 + str((xy_dis_drag.min(), xy_dis_drag.mean(), xy_dis_drag.max())) 2322 ) 2323 try: 2324 self.plot_counter += 1 2325 except: 2326 self.plot_counter = 0 2327 2328 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2329 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2330 2331 # Normalize 2332 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2333 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2334 # Normalize 2335 new_matrix = xy_dis_rotated 2336 xy_dis_norm = xy_dis 2337 x_center = np.where(new_matrix == new_matrix.max())[0][0] 2338 y_center = np.where(new_matrix == new_matrix.max())[1][0] 2339 2340 # Pad event and move it to maximum depth location 2341 x_pad = 0 2342 y_pad = 0 2343 x_roll = z_idx[0] - x_center 2344 y_roll = z_idx[1] - y_center 2345 print(f" ... padding x,y and roll x,y = {x_pad} {y_pad} {x_roll} {y_roll}") 2346 print( 2347 f" ... Max displacement point before rotation and adding of padding x,y = {x_center} {y_center}" 2348 ) 2349 new_matrix = np.lib.pad( 2350 new_matrix, ((abs(x_pad), abs(x_pad)), (abs(y_pad), abs(y_pad))), "edge" 2351 ) 2352 new_matrix = np.roll(new_matrix, int(x_roll), axis=0) 2353 new_matrix = np.roll(new_matrix, int(y_roll), axis=1) 2354 new_matrix = new_matrix[ 2355 abs(x_pad) : cube_shape[0] + abs(x_pad), 2356 abs(y_pad) : cube_shape[1] + abs(y_pad), 2357 ] 2358 # Check that fault center is at the right place 2359 x_center = np.where(new_matrix == new_matrix.max())[0] 2360 y_center = np.where(new_matrix == new_matrix.max())[1] 2361 print( 2362 f" ... Max displacement point after rotation and removal of padding x,y = {x_center} {y_center}" 2363 ) 2364 print(f" ... z_idx = {z_idx[0]}, {z_idx[1]}") 2365 print( 2366 f" ... Difference from origin z_idx = {x_center - z_idx[0]}, {y_center - z_idx[1]}" 2367 ) 2368 2369 # Build cube of lateral variable displacement 2370 xy_dis = new_matrix.reshape(cube_shape[0], cube_shape[1], 1) 2371 # Get enlarged fault plane 2372 bb = np.zeros_like(ellipsoid) 2373 for j in range(bb.shape[-1]): 2374 bb[:, :, j] = j 2375 stretch_times_effects_drag = bb - xy_dis * z_shift(range(cube_shape[2])) 2376 fault_plane_classification = np.where( 2377 (z_on_ellipse == 1) 2378 & ((origtime - stretch_times_effects_drag) > infill_factor), 2379 1, 2380 0, 2381 ) 2382 hockey_stick = 0 2383 # Define as 0's, to be updated if necessary 2384 fault_plane_classification_drag = np.zeros_like(z_on_ellipse) 2385 if throw >= 0.85 * high_fault_throw: 2386 hockey_stick = 1 2387 # Check for non zero fault_plane_classification, to avoid division by 0 2388 if np.count_nonzero(fault_plane_classification) > 0: 2389 # Generate Hockey sticks by convolving small xy displacement with fault segment 2390 fault_plane_classification_drag = signal.fftconvolve( 2391 fault_plane_classification, 2392 np.reshape(xy_dis_drag, (cube_shape[0], cube_shape[1], 1)), 2393 mode="same", 2394 ) 2395 fault_plane_classification_drag = ( 2396 fault_plane_classification_drag 2397 / np.amax(fault_plane_classification_drag.flatten()) 2398 ) 2399 2400 xy_dis = xy_dis * np.ones_like(self.vols.geologic_age) 2401 xy_dis_classification = xy_dis.copy() 2402 interpolation = True 2403 2404 xy_dis = xy_dis - fault_plane_classification_drag 2405 xy_dis = np.where(xy_dis < 0, 0, xy_dis) 2406 stretch_times = xy_dis * z_shift(range(cube_shape[2])) 2407 stretch_times_classification = xy_dis_classification * z_shift( 2408 range(cube_shape[2]) 2409 ) 2410 2411 if self.cfg.qc_plots: 2412 self.fault_summary_plot( 2413 ff, 2414 z, 2415 throw, 2416 sigma, 2417 _x, 2418 _y, 2419 xy_dis_norm, 2420 ellipsoid, 2421 z_idx, 2422 xy_dis, 2423 index, 2424 alpha, 2425 ) 2426 2427 return stretch_times, stretch_times_classification, interpolation, hockey_stick 2428 2429 def fault_summary_plot( 2430 self, 2431 ff, 2432 z, 2433 throw, 2434 sigma, 2435 x, 2436 y, 2437 xy_dis_norm, 2438 ellipsoid, 2439 z_idx, 2440 xy_dis, 2441 index, 2442 alpha, 2443 ) -> None: 2444 """ 2445 Fault Summary Plot 2446 ------------------ 2447 2448 Generates a fault summary plot. 2449 2450 Parameters 2451 ---------- 2452 ff : _type_ 2453 _description_ 2454 z : _type_ 2455 _description_ 2456 throw : _type_ 2457 _description_ 2458 sigma : _type_ 2459 _description_ 2460 x : _type_ 2461 _description_ 2462 y : _type_ 2463 _description_ 2464 xy_dis_norm : _type_ 2465 _description_ 2466 ellipsoid : _type_ 2467 _description_ 2468 z_idx : _type_ 2469 _description_ 2470 xy_dis : _type_ 2471 _description_ 2472 index : _type_ 2473 _description_ 2474 alpha : _type_ 2475 _description_ 2476 2477 Returns 2478 ------- 2479 None 2480 """ 2481 import os 2482 from math import degrees 2483 from datagenerator.util import import_matplotlib 2484 2485 plt = import_matplotlib() 2486 # Import axes3d, required to create plot with projection='3d' below. DO NOT REMOVE! 2487 from mpl_toolkits.mplot3d import axes3d 2488 from mpl_toolkits.axes_grid1 import make_axes_locatable 2489 2490 # Make summary picture 2491 fig, axes = plt.subplots(nrows=2, ncols=2) 2492 ax0, ax1, ax2, ax3 = axes.flatten() 2493 fig.set_size_inches(10, 8) 2494 # PLot Z displacement 2495 ax0.plot(ff(z)) 2496 ax0.set_title("Fault with throw %s and sigma %s" % (throw, sigma)) 2497 ax0.set_xlabel("Z axis") 2498 ax0.set_ylabel("Throw") 2499 # Plot 3D XY displacement 2500 ax1.axis("off") 2501 ax1 = fig.add_subplot(222, projection="3d") 2502 ax1.plot_surface(x, y, xy_dis_norm, cmap="Spectral", linewidth=0) 2503 ax1.set_xlabel("X axis") 2504 ax1.set_ylabel("Y axis") 2505 ax1.set_zlabel("Throw fraction") 2506 ax1.set_title("3D XY displacement") 2507 # Ellipsoid location 2508 weights = ellipsoid[:, :, z_idx[2]] 2509 # Plot un-rotated XY displacement 2510 cax2 = ax2.imshow(np.rot90(xy_dis_norm, 3)) 2511 # Levels for imshow contour 2512 levels = np.arange(0, 1.1, 0.1) 2513 # Plot contour 2514 ax2.contour(np.rot90(xy_dis_norm, 3), levels, colors="k", linestyles="-") 2515 divider = make_axes_locatable(ax2) 2516 cax4 = divider.append_axes("right", size="5%", pad=0.05) 2517 cbar2 = fig.colorbar(cax2, cax=cax4) 2518 ax2.set_xlabel("X axis") 2519 ax2.set_ylabel("Y axis") 2520 ax2.set_title("2D projection of XY displacement unrotated") 2521 ############################################################ 2522 # Plot rotated displacement and contour 2523 cax3 = ax3.imshow(np.rot90(xy_dis[:, :, z_idx[2]], 3)) 2524 ax3.contour( 2525 np.rot90(xy_dis[:, :, z_idx[2]], 3), levels, colors="k", linestyles="-" 2526 ) 2527 # Add ellipsoid shape 2528 ax3.contour(np.rot90(weights, 3), levels=[1], colors="r", linestyles="-") 2529 divider = make_axes_locatable(ax3) 2530 cax5 = divider.append_axes("right", size="5%", pad=0.05) 2531 cbar3 = fig.colorbar(cax3, cax=cax5) 2532 ax3.set_xlabel("X axis") 2533 ax3.set_ylabel("Y axis") 2534 ax3.set_title( 2535 "2D projection of XY displacement rotated by %s degrees" 2536 % (int(degrees(alpha))) 2537 ) 2538 plt.suptitle( 2539 "XYZ displacement parameters for fault Nr %s" % str(index), 2540 fontweight="bold", 2541 ) 2542 fig.tight_layout() 2543 fig.subplots_adjust(top=0.94) 2544 image_path = os.path.join( 2545 self.cfg.work_subfolder, "QC_plot__Fault_%s.png" % str(index) 2546 ) 2547 plt.savefig(image_path, format="png") 2548 plt.close(fig) 2549 2550 @staticmethod 2551 def create_binary_segmentations_post_faulting(cube, segmentation_threshold): 2552 cube[cube >= segmentation_threshold] = 1.0 2553 return cube 2554 2555 def reassign_channel_segment_encoding( 2556 self, 2557 faulted_age, 2558 floodplain_shale, 2559 channel_fill, 2560 shale_channel_drape, 2561 levee, 2562 crevasse, 2563 channel_flag_lut, 2564 ): 2565 # Generate list of horizons with channel episodes 2566 channel_segments = np.zeros_like(floodplain_shale) 2567 channel_horizons_list = list() 2568 for i in range(self.faulted_depth_maps.shape[2] - 7): 2569 if channel_flag_lut[i] == 1: 2570 channel_horizons_list.append(i) 2571 channel_horizons_list.append(999) 2572 2573 # Re-assign correct channel segments encoding 2574 for i, iLayer in enumerate(channel_horizons_list[:-1]): 2575 if iLayer != 0 and iLayer < self.faulted_depth_maps.shape[-1]: 2576 # loop through channel facies 2577 # --- 0 is floodplain shale 2578 # --- 1 is channel fill (sand) 2579 # --- 2 is shale channel drape 2580 # --- 3 is levee (mid quality sand) 2581 # --- 4 is crevasse (low quality sand) 2582 for j in range(1000, 4001, 1000): 2583 print( 2584 " ... re-assign channel segments after faulting: i, iLayer, j = ", 2585 i, 2586 iLayer, 2587 j, 2588 ) 2589 channel_facies_code = j + iLayer 2590 layers_mask = np.where( 2591 ( 2592 (faulted_age >= iLayer) 2593 & (faulted_age < channel_horizons_list[i + 1]) 2594 ), 2595 1, 2596 0, 2597 ) 2598 if j == 1000: 2599 channel_segments[ 2600 np.logical_and(layers_mask == 1, channel_fill == 1) 2601 ] = channel_facies_code 2602 faulted_age[ 2603 np.logical_and(layers_mask == 1, channel_fill == 1) 2604 ] = channel_facies_code 2605 elif j == 2000: 2606 channel_segments[ 2607 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2608 ] = channel_facies_code 2609 faulted_age[ 2610 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2611 ] = channel_facies_code 2612 elif j == 3000: 2613 channel_segments[ 2614 np.logical_and(layers_mask == 1, levee == 1) 2615 ] = channel_facies_code 2616 faulted_age[ 2617 np.logical_and(layers_mask == 1, levee == 1) 2618 ] = channel_facies_code 2619 elif j == 4000: 2620 channel_segments[ 2621 np.logical_and(layers_mask == 1, crevasse == 1) 2622 ] = channel_facies_code 2623 faulted_age[ 2624 np.logical_and(layers_mask == 1, crevasse == 1) 2625 ] = channel_facies_code 2626 print(" ... finished Re-assign correct channel segments") 2627 return channel_segments, faulted_age 2628 2629 def _get_fault_mode(self): 2630 if self.cfg.mode == 0: 2631 return self._fault_params_random 2632 elif self.cfg.mode == 1.0: 2633 if self.cfg.clustering == 0: 2634 return self._fault_params_self_branching 2635 elif self.cfg.clustering == 1: 2636 return self._fault_params_stairs 2637 elif self.cfg.clustering == 2: 2638 return self._fault_params_relay_ramps 2639 else: 2640 raise ValueError(self.cfg.clustering) 2641 2642 elif self.cfg.mode == 2.0: 2643 return self._fault_params_horst_graben 2644 else: 2645 raise ValueError(self.cfg.mode) 2646 2647 def _fault_params_random(self): 2648 print(f" ... {self.cfg.number_faults} faults will be inserted randomly") 2649 2650 # Fault origin 2651 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2652 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2653 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2654 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2655 2656 # Principal semi-axes location of fault ellipsoid 2657 a = np.random.uniform(100, 600, self.cfg.number_faults) ** 2 2658 b = np.random.uniform(100, 600, self.cfg.number_faults) ** 2 2659 x0 = np.random.uniform( 2660 x0_min - np.sqrt(a), np.sqrt(a) + x0_max, self.cfg.number_faults 2661 ) 2662 y0 = np.random.uniform( 2663 y0_min - np.sqrt(b), np.sqrt(b) + y0_max, self.cfg.number_faults 2664 ) 2665 z0 = np.random.uniform( 2666 -self.cfg.cube_shape[2] * 2.0, 2667 -self.cfg.cube_shape[2] * 6.0, 2668 self.cfg.number_faults, 2669 ) 2670 _c0 = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0 2671 _c1 = _c0 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2672 c = np.random.uniform(_c0, _c1, self.cfg.number_faults) ** 2 2673 tilt_pct = np.random.uniform(0.1, 0.75, self.cfg.number_faults) 2674 throw_lut = np.random.uniform( 2675 low=self.cfg.low_fault_throw, 2676 high=self.cfg.high_fault_throw, 2677 size=self.cfg.number_faults, 2678 ) 2679 2680 fault_param_dict = { 2681 "a": a, 2682 "b": b, 2683 "c": c, 2684 "x0": x0, 2685 "y0": y0, 2686 "z0": z0, 2687 "tilt_pct": tilt_pct, 2688 "throw": throw_lut, 2689 } 2690 2691 return fault_param_dict 2692 2693 def _fault_params_self_branching(self): 2694 print( 2695 f" ... {self.cfg.number_faults} faults will be inserted in clustered mode with" 2696 ) 2697 print(" ... Self branching") 2698 x0_min = 0 2699 x0_max = int(self.cfg.cube_shape[0]) 2700 y0_min = 0 2701 y0_max = int(self.cfg.cube_shape[1]) 2702 # Self Branching mode means that faults are inserted side by side with no separation. 2703 number_of_branches = int(self.cfg.number_faults / 3) + int( 2704 self.cfg.number_faults % 3 > 0 2705 ) 2706 2707 for i in range(number_of_branches): 2708 # Initialize first fault center, offset center between each branch 2709 print(" ... Computing branch number ", i) 2710 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2711 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2712 if a_ini > b_ini: 2713 while np.sqrt(b_ini) < self.cfg.cube_shape[1]: 2714 print(" ... Recomputing b_ini for better branching") 2715 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2716 x0_ini = np.array(np.random.uniform(x0_min, x0_max)) 2717 range_1 = list(range(int(y0_min - np.sqrt(b_ini)), 0)) 2718 range_2 = list(range(int(self.cfg.cube_shape[1]))) 2719 y0_ini = np.array( 2720 np.random.choice(range_1 + range_2, int(np.sqrt(b_ini) + y0_max)) 2721 ) 2722 side = "x" 2723 else: 2724 while np.sqrt(a_ini) < self.cfg.cube_shape[0]: 2725 print(" ... Recomputing a_ini for better branching") 2726 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2727 range_1 = list(range(int(x0_min - np.sqrt(a_ini)), 0)) 2728 range_2 = list(range(int(self.cfg.cube_shape[0]))) 2729 x0_ini = np.array( 2730 np.array( 2731 np.random.choice( 2732 range_1 + range_2, int(np.sqrt(a_ini) + x0_max) 2733 ) 2734 ) 2735 ) 2736 y0_ini = np.array(np.random.uniform(y0_min, y0_max)) 2737 side = "y" 2738 # Compute the rest of the initial parameters normally 2739 z0_ini = np.array( 2740 np.random.uniform( 2741 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2742 ) 2743 ) 2744 2745 _c0_ini = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini 2746 _c1_ini = _c0_ini + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2747 c_ini = np.array(np.random.uniform(_c0_ini, _c1_ini) ** 2) 2748 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2749 # direction = np.random.choice([-1, 1]) 2750 throw_lut_ini = np.array( 2751 np.random.uniform( 2752 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 2753 ) 2754 ) 2755 if i == 0: 2756 # Initialize return parameters 2757 a = a_ini.copy() 2758 b = b_ini.copy() 2759 c = c_ini.copy() 2760 x0 = x0_ini.copy() 2761 y0 = y0_ini.copy() 2762 z0 = z0_ini.copy() 2763 tilt_pct = tilt_pct_ini.copy() 2764 throw_lut = throw_lut_ini.copy() 2765 else: 2766 a = np.append(a, a_ini) 2767 b = np.append(b, b_ini) 2768 c = np.append(c, c_ini) 2769 x0 = np.append(x0, x0_ini) 2770 y0 = np.append(y0, y0_ini) 2771 z0 = np.append(z0, z0_ini) 2772 tilt_pct = np.append(tilt_pct, tilt_pct_ini) 2773 throw_lut = np.append(throw_lut, throw_lut_ini) 2774 # Construct fault in branch 2775 if i < number_of_branches - 1 or self.cfg.number_faults % 3 == 0: 2776 fault_in_branch = 2 2777 else: 2778 fault_in_branch = self.cfg.number_faults % 3 2779 print(" ... Branch along axis ", side) 2780 2781 direction = 1 2782 if side == "x": 2783 if np.all(y0_ini > np.abs(self.cfg.cube_shape[1] - y0_ini)): 2784 print(" ... Building from right to left") 2785 direction = -1 2786 else: 2787 print(" ... Building from left to right") 2788 else: 2789 if np.all(x0_ini > np.abs(self.cfg.cube_shape[0] - x0_ini)): 2790 print(" ... Building from left to right") 2791 direction = -1 2792 else: 2793 print(" ... Building from right to left") 2794 move = 1 2795 for j in range( 2796 i * number_of_branches + 1, fault_in_branch + i * number_of_branches + 1 2797 ): 2798 print(" ... Computing fault number ", j) 2799 # Allow 20% deviation from initial fault parameter 2800 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 2801 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 2802 c_ramp = c_ini.copy() 2803 if side == "x": 2804 x0_ramp = x0_ini + direction * move * int( 2805 self.cfg.cube_shape[0] / 3.0 2806 ) 2807 y0_ramp = np.random.uniform(0.9, 1.1) * y0_ini 2808 else: 2809 y0_ramp = y0_ini + direction * move * int( 2810 self.cfg.cube_shape[0] / 3.0 2811 ) 2812 x0_ramp = np.random.uniform(0.9, 1.1) * x0_ini 2813 z0_ramp = z0_ini.copy() 2814 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 2815 # Add to existing 2816 throw_lut_ramp = np.random.uniform( 2817 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 2818 ) 2819 throw_lut = np.append(throw_lut, throw_lut_ramp) 2820 a = np.append(a, a_ramp) 2821 b = np.append(b, b_ramp) 2822 c = np.append(c, c_ramp) 2823 x0 = np.append(x0, x0_ramp) 2824 y0 = np.append(y0, y0_ramp) 2825 z0 = np.append(z0, z0_ramp) 2826 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 2827 move += 1 2828 2829 fault_param_dict = { 2830 "a": a, 2831 "b": b, 2832 "c": c, 2833 "x0": x0, 2834 "y0": y0, 2835 "z0": z0, 2836 "tilt_pct": tilt_pct, 2837 "throw": throw_lut, 2838 } 2839 return fault_param_dict 2840 2841 def _fault_params_stairs(self): 2842 print(" ... Stairs like feature") 2843 # Initialize value for first fault 2844 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2845 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2846 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2847 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2848 2849 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2850 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2851 x0_ini = np.array( 2852 np.random.uniform(x0_min - np.sqrt(a_ini), np.sqrt(a_ini) + x0_max) 2853 ) 2854 y0_ini = np.array( 2855 np.random.uniform(y0_min - np.sqrt(b_ini), np.sqrt(b_ini) + y0_max) 2856 ) 2857 z0_ini = np.array( 2858 np.random.uniform( 2859 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2860 ) 2861 ) 2862 2863 _c0_ini = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini 2864 _c1_ini = _c0_ini + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2865 c_ini = np.array(np.random.uniform(_c0_ini, _c1_ini) ** 2) 2866 2867 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2868 throw_lut = np.random.uniform( 2869 self.cfg.low_fault_throw, self.cfg.high_fault_throw, self.cfg.number_faults 2870 ) 2871 direction = np.random.choice([-1, 1]) 2872 separation_x = np.random.randint(1, 4) 2873 separation_y = np.random.randint(1, 4) 2874 # Initialize return parameters 2875 a = a_ini.copy() 2876 b = b_ini.copy() 2877 c = c_ini.copy() 2878 x0 = x0_ini.copy() 2879 y0 = y0_ini.copy() 2880 z0 = z0_ini.copy() 2881 x0_prec = x0_ini 2882 y0_prec = y0_ini 2883 tilt_pct = tilt_pct_ini.copy() 2884 for i in range(self.cfg.number_faults - 1): 2885 a_ramp = a_ini.copy() * np.random.uniform(0.8, 1.2) 2886 b_ramp = b_ini.copy() * np.random.uniform(0.8, 1.2) 2887 c_ramp = c_ini.copy() * np.random.uniform(0.8, 1.2) 2888 direction = np.random.choice([-1, 1]) 2889 x0_ramp = ( 2890 x0_prec + separation_x * direction * x0_ini / self.cfg.number_faults 2891 ) 2892 y0_ramp = ( 2893 y0_prec + separation_y * direction * y0_ini / self.cfg.number_faults 2894 ) 2895 z0_ramp = z0_ini.copy() 2896 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 2897 x0_prec = x0_ramp 2898 y0_prec = y0_ramp 2899 # Add to existing 2900 a = np.append(a, a_ramp) 2901 b = np.append(b, b_ramp) 2902 c = np.append(c, c_ramp) 2903 x0 = np.append(x0, x0_ramp) 2904 y0 = np.append(y0, y0_ramp) 2905 z0 = np.append(z0, z0_ramp) 2906 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 2907 2908 fault_param_dict = { 2909 "a": a, 2910 "b": b, 2911 "c": c, 2912 "x0": x0, 2913 "y0": y0, 2914 "z0": z0, 2915 "tilt_pct": tilt_pct, 2916 "throw": throw_lut, 2917 } 2918 return fault_param_dict 2919 2920 def _fault_params_relay_ramps(self): 2921 print(" ... relay ramps") 2922 # Initialize value for first fault 2923 # Maximum of 3 fault per ramp 2924 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2925 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2926 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2927 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2928 number_of_branches = int(self.cfg.number_faults / 3) + int( 2929 self.cfg.number_faults % 3 > 0 2930 ) 2931 2932 for i in range(number_of_branches): 2933 # Initialize first fault center, offset center between each branch 2934 print(" ... Computing branch number ", i) 2935 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2936 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2937 x0_ini = np.array( 2938 np.random.uniform(x0_min - np.sqrt(a_ini) / 2, np.sqrt(a_ini) + x0_max) 2939 / 2 2940 ) 2941 y0_ini = np.array( 2942 np.random.uniform(y0_min - np.sqrt(b_ini) / 2, np.sqrt(b_ini) + y0_max) 2943 / 2 2944 ) 2945 # Compute the rest of the initial parameters normally 2946 z0_ini = np.array( 2947 np.random.uniform( 2948 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2949 ) 2950 ) 2951 c_ini = np.array( 2952 np.random.uniform( 2953 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini, 2954 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 2955 - z0_ini 2956 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0, 2957 ) 2958 ** 2 2959 ) 2960 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2961 throw_lut = np.random.uniform( 2962 self.cfg.low_fault_throw, 2963 self.cfg.high_fault_throw, 2964 self.cfg.number_faults, 2965 ) 2966 # Initialize return parameters 2967 if i == 0: 2968 a = a_ini.copy() 2969 b = b_ini.copy() 2970 c = c_ini.copy() 2971 x0 = x0_ini.copy() 2972 y0 = y0_ini.copy() 2973 z0 = z0_ini.copy() 2974 tilt_pct = tilt_pct_ini.copy() 2975 else: 2976 a = np.append(a, a_ini) 2977 b = np.append(b, b_ini) 2978 c = np.append(c, c_ini) 2979 x0 = np.append(x0, x0_ini) 2980 y0 = np.append(y0, y0_ini) 2981 z0 = np.append(z0, z0_ini) 2982 tilt_pct = np.append(tilt_pct, tilt_pct_ini) 2983 # Construct fault in branch 2984 if i < number_of_branches - 1 or self.cfg.number_faults % 3 == 0: 2985 fault_in_branche = 2 2986 else: 2987 fault_in_branche = self.cfg.number_faults % 3 2988 2989 direction = np.random.choice([-1, 1]) 2990 move = 1 2991 x0_prec = x0_ini 2992 y0_prec = y0_ini 2993 for j in range( 2994 i * number_of_branches + 1, 2995 fault_in_branche + i * number_of_branches + 1, 2996 ): 2997 print(" ... Computing fault number ", j) 2998 # Allow 20% deviation from initial fault parameter 2999 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3000 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3001 c_ramp = c_ini.copy() 3002 direction = np.random.choice([-1, 1]) 3003 x0_ramp = x0_prec * np.random.uniform(0.8, 1.2) + direction * x0_ini / ( 3004 fault_in_branche 3005 ) 3006 y0_ramp = y0_prec * np.random.uniform(0.8, 1.2) + direction * y0_ini / ( 3007 fault_in_branche 3008 ) 3009 z0_ramp = z0_ini.copy() 3010 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3011 # Add to existing 3012 throw_LUT_ramp = np.random.uniform( 3013 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 3014 ) 3015 throw_lut = np.append(throw_lut, throw_LUT_ramp) 3016 a = np.append(a, a_ramp) 3017 b = np.append(b, b_ramp) 3018 c = np.append(c, c_ramp) 3019 x0 = np.append(x0, x0_ramp) 3020 y0 = np.append(y0, y0_ramp) 3021 z0 = np.append(z0, z0_ramp) 3022 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3023 x0_prec = x0_ramp 3024 y0_prec = y0_ramp 3025 move += 1 3026 3027 fault_param_dict = { 3028 "a": a, 3029 "b": b, 3030 "c": c, 3031 "x0": x0, 3032 "y0": y0, 3033 "z0": z0, 3034 "tilt_pct": tilt_pct, 3035 "throw": throw_lut, 3036 } 3037 return fault_param_dict 3038 3039 def _fault_params_horst_graben(self): 3040 print( 3041 " ... %s faults will be inserted as Horst and Graben" 3042 % (self.cfg.number_faults) 3043 ) 3044 # Initialize value for first fault 3045 x0_min = int(self.cfg.cube_shape[0] / 2.0) 3046 x0_max = int(self.cfg.cube_shape[0]) 3047 y0_min = int(self.cfg.cube_shape[1] / 2.0) 3048 y0_max = int(self.cfg.cube_shape[1]) 3049 3050 b_ini = np.array(np.random.uniform(100, 600) ** 2) 3051 a_ini = np.array(np.random.uniform(100, 600) ** 2) 3052 if a_ini > b_ini: 3053 side = "x" 3054 while np.sqrt(b_ini) < self.cfg.cube_shape[1]: 3055 print(" ... Recomputing b_ini for better branching") 3056 b_ini = np.array(np.random.uniform(100, 600) ** 2) 3057 x0_ini = np.array(np.random.uniform(0, self.cfg.cube_shape[0])) 3058 # Compute so that first is near center 3059 range_1 = list( 3060 range(int(y0_min - np.sqrt(b_ini)), int(y0_max - np.sqrt(b_ini))) 3061 ) 3062 range_2 = list( 3063 range(int(y0_min + np.sqrt(b_ini)), int(y0_max + np.sqrt(b_ini))) 3064 ) 3065 y0_ini = np.array(np.random.choice(range_1 + range_2)) 3066 else: 3067 side = "y" 3068 while np.sqrt(a_ini) < self.cfg.cube_shape[0]: 3069 print(" ... Recomputing a_ini for better branching") 3070 a_ini = np.array(np.random.uniform(100, 600) ** 2) 3071 # compute so that first is near center 3072 range_1 = list( 3073 range(int(x0_min - np.sqrt(a_ini)), int(x0_max - np.sqrt(a_ini))) 3074 ) 3075 range_2 = list( 3076 range(int(x0_min + np.sqrt(a_ini)), int(x0_max + np.sqrt(a_ini))) 3077 ) 3078 x0_ini = np.array(np.random.choice(range_1 + range_2)) 3079 y0_ini = np.array(np.random.uniform(0, self.cfg.cube_shape[1])) 3080 z0_ini = np.array( 3081 np.random.uniform( 3082 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 3083 ) 3084 ) 3085 c_ini = np.array( 3086 np.random.uniform( 3087 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini, 3088 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 3089 - z0_ini 3090 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 2.0, 3091 ) 3092 ** 2 3093 ) 3094 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 3095 throw_lut = np.random.uniform( 3096 self.cfg.low_fault_throw, self.cfg.high_fault_throw, self.cfg.number_faults 3097 ) 3098 # Initialize return parameters 3099 a = a_ini.copy() 3100 b = b_ini.copy() 3101 c = c_ini.copy() 3102 x0 = x0_ini.copy() 3103 y0 = y0_ini.copy() 3104 z0 = z0_ini.copy() 3105 3106 tilt_pct = tilt_pct_ini.copy() 3107 direction = "odd" 3108 mod = ["new"] 3109 3110 x0_even = x0_ini 3111 y0_even = y0_ini 3112 if side == "x": 3113 # X only moves marginally 3114 y0_odd = int(self.cfg.cube_shape[1]) - y0_even 3115 x0_odd = x0_even 3116 direction_sign = np.sign(y0_ini) 3117 direction_sidex = 0 3118 direction_sidey = 1 3119 else: 3120 # Y only moves marginally 3121 x0_odd = int(self.cfg.cube_shape[0]) - x0_even 3122 y0_odd = y0_even 3123 direction_sign = np.sign(x0_ini) 3124 direction_sidex = 1 3125 direction_sidey = 0 3126 3127 for i in range(self.cfg.number_faults - 1): 3128 if direction == "odd": 3129 # Put the next point as a mirror shifted by maximal shift and go backward 3130 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3131 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3132 c_ramp = c_ini.copy() 3133 x0_ramp = ( 3134 -1 3135 * direction_sign 3136 * direction_sidex 3137 * int(self.cfg.cube_shape[0]) 3138 * (i - 1) 3139 / self.cfg.number_faults 3140 ) + x0_odd * np.random.uniform(0.8, 1.2) 3141 y0_ramp = ( 3142 -1 3143 * direction_sign 3144 * direction_sidey 3145 * int(self.cfg.cube_shape[0]) 3146 * (i - 1) 3147 / self.cfg.number_faults 3148 ) + y0_odd * np.random.uniform(0.8, 1.2) 3149 z0_ramp = z0_ini.copy() 3150 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3151 x0_prec = x0_ramp 3152 y0_prec = y0_ramp 3153 # Add to existing 3154 a = np.append(a, a_ramp) 3155 b = np.append(b, b_ramp) 3156 c = np.append(c, c_ramp) 3157 x0 = np.append(x0, x0_ramp) 3158 y0 = np.append(y0, y0_ramp) 3159 z0 = np.append(z0, z0_ramp) 3160 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3161 direction = "even" 3162 mod.append("old") 3163 elif direction == "even": 3164 # Put next to ini 3165 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3166 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3167 c_ramp = c_ini.copy() 3168 x0_ramp = direction_sign * direction_sidex * int( 3169 self.cfg.cube_shape[0] 3170 ) * (i - 1) / self.cfg.number_faults + x0_even * np.random.uniform( 3171 0.8, 1.2 3172 ) 3173 y0_ramp = direction_sign * direction_sidey * int( 3174 self.cfg.cube_shape[0] 3175 ) * (i - 1) / self.cfg.number_faults + y0_even * np.random.uniform( 3176 0.8, 1.2 3177 ) 3178 z0_ramp = z0_ini.copy() 3179 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3180 # Add to existing 3181 a = np.append(a, a_ramp) 3182 b = np.append(b, b_ramp) 3183 c = np.append(c, c_ramp) 3184 x0 = np.append(x0, x0_ramp) 3185 y0 = np.append(y0, y0_ramp) 3186 z0 = np.append(z0, z0_ramp) 3187 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3188 direction = "odd" 3189 mod.append("new") 3190 3191 fault_param_dict = { 3192 "a": a, 3193 "b": b, 3194 "c": c, 3195 "x0": x0, 3196 "y0": y0, 3197 "z0": z0, 3198 "tilt_pct": tilt_pct, 3199 "throw": throw_lut, 3200 } 3201 return fault_param_dict 3202 3203 3204def find_zero_thickness_onlapping_layers(z, onlap_list): 3205 onlap_zero_z = dict() 3206 for layer in onlap_list: 3207 for x in range(layer, 1, -1): 3208 thickness = z[..., layer] - z[..., x - 1] 3209 zeros = np.where(thickness == 0.0) 3210 if zeros[0].size > 0: 3211 onlap_zero_z[f"{layer},{x-1}"] = zeros 3212 return onlap_zero_z 3213 3214 3215def fix_zero_thickness_fan_layers(z, layer_number, thickness): 3216 """ 3217 Clip fan layers to horizon below the fan layer in areas where the fan thickness is zero 3218 3219 Parameters 3220 ---------- 3221 z : ndarray - depth maps 3222 layer_number : 1d array - horizon numbers which contain fans 3223 thickness : tuple of ndarrays - original thickness maps of the fans 3224 3225 Returns 3226 ------- 3227 zmaps : ndarray - depth maps with fan layers clipped to lower horizons where thicknesses is zero 3228 """ 3229 zmaps = z.copy() 3230 zmaps[..., layer_number][np.where(thickness == 0.0)] = zmaps[..., layer_number + 1][ 3231 np.where(thickness == 0.0) 3232 ] 3233 return zmaps 3234 3235 3236def fix_zero_thickness_onlap_layers( 3237 faulted_depth_maps: np.ndarray, 3238 onlap_dict: dict 3239) -> np.ndarray: 3240 """fix_zero_thickness_onlap_layers _summary_ 3241 3242 Parameters 3243 ---------- 3244 faulted_depth_maps : np.ndarray 3245 The depth maps with faults 3246 onlap_dict : dict 3247 Onlaps dictionary 3248 3249 Returns 3250 ------- 3251 zmaps : np.ndarray 3252 Fixed depth maps 3253 """ 3254 zmaps = faulted_depth_maps.copy() 3255 for layers, idx in onlap_dict.items(): 3256 onlap_layer = int(str(layers).split(",")[0]) 3257 layer_to_clip = int(str(layers).split(",")[1]) 3258 zmaps[..., layer_to_clip][idx] = zmaps[..., onlap_layer][idx] 3259 3260 return zmaps
13class Faults(Horizons, Geomodel): 14 """ 15 Faults Class 16 ------------ 17 Describes the class for faulting the model. 18 19 Parameters 20 ---------- 21 Horizons : datagenerator.Horizons 22 The Horizons class used to build the faults. 23 Geomodel : data_generator.Geomodels 24 The Geomodel class used to build the faults. 25 26 Returns 27 ------- 28 None 29 """ 30 def __init__( 31 self, 32 parameters: Parameters, 33 unfaulted_depth_maps: np.ndarray, 34 onlap_horizon_list: np.ndarray, 35 geomodels: Geomodel, 36 fan_horizon_list: np.ndarray, 37 fan_thickness: np.ndarray 38 ): 39 """__init__ 40 41 Initializes the Faults class.. 42 43 Parameters 44 ---------- 45 parameters : Parameters 46 The parameters class. 47 unfaulted_depth_maps : np.ndarray 48 The depth maps to be faulted. 49 onlap_horizon_list : np.ndarray 50 The onlap horizon list. 51 geomodels : Geomodel 52 The geomodels class. 53 fan_horizon_list : np.ndarray 54 The fan horizon list. 55 fan_thickness : np.ndarray 56 The fan thickness list. 57 """ 58 self.cfg = parameters 59 # Horizons 60 self.unfaulted_depth_maps = unfaulted_depth_maps 61 self.onlap_horizon_list = onlap_horizon_list 62 self.fan_horizon_list = fan_horizon_list 63 self.fan_thickness = fan_thickness 64 self.faulted_depth_maps = self.cfg.hdf_init( 65 "faulted_depth_maps", shape=unfaulted_depth_maps.shape 66 ) 67 self.faulted_depth_maps_gaps = self.cfg.hdf_init( 68 "faulted_depth_maps_gaps", shape=unfaulted_depth_maps.shape 69 ) 70 # Volumes 71 cube_shape = geomodels.geologic_age[:].shape 72 self.vols = geomodels 73 self.faulted_age_volume = self.cfg.hdf_init( 74 "faulted_age_volume", shape=cube_shape 75 ) 76 self.faulted_net_to_gross = self.cfg.hdf_init( 77 "faulted_net_to_gross", shape=cube_shape 78 ) 79 self.faulted_lithology = self.cfg.hdf_init( 80 "faulted_lithology", shape=cube_shape 81 ) 82 self.reservoir = self.cfg.hdf_init("reservoir", shape=cube_shape) 83 self.faulted_depth = self.cfg.hdf_init("faulted_depth", shape=cube_shape) 84 self.faulted_onlap_segments = self.cfg.hdf_init( 85 "faulted_onlap_segments", shape=cube_shape 86 ) 87 self.fault_planes = self.cfg.hdf_init("fault_planes", shape=cube_shape) 88 self.displacement_vectors = self.cfg.hdf_init( 89 "displacement_vectors", shape=cube_shape 90 ) 91 self.sum_map_displacements = self.cfg.hdf_init( 92 "sum_map_displacements", shape=cube_shape 93 ) 94 self.fault_intersections = self.cfg.hdf_init( 95 "fault_intersections", shape=cube_shape 96 ) 97 self.fault_plane_throw = self.cfg.hdf_init( 98 "fault_plane_throw", shape=cube_shape 99 ) 100 self.max_fault_throw = self.cfg.hdf_init("max_fault_throw", shape=cube_shape) 101 self.fault_plane_azimuth = self.cfg.hdf_init( 102 "fault_plane_azimuth", shape=cube_shape 103 ) 104 # Salt 105 self.salt_model = None 106 107 def apply_faulting_to_geomodels_and_depth_maps(self) -> None: 108 """ 109 Apply faulting to horizons and cubes 110 ------------------------------------ 111 Generates random faults and applies faulting to horizons and cubes. 112 113 The method does the following: 114 115 * Generate faults and sum the displacements 116 * Apply faulting to horizons 117 * Write faulted depth maps to disk 118 * Write faulted depth maps with gaps at faults to disk 119 * Write onlapping horizons to disk 120 * Apply faulting to geomodels 121 * Make segmentation results conform to binary values after 122 faulting and interpolation. 123 * Write cubes to file (if qc_volumes turned on in config.json) 124 125 (If segmentation is not reset to binary, the multiple 126 interpolations for successive faults destroys the crisp 127 localization of the labels. Subjective observation suggests 128 that slightly different thresholds for different 129 features provide superior results) 130 131 Parameters 132 ---------- 133 None 134 135 Returns 136 ------- 137 None 138 """ 139 # Make a dictionary of zero-thickness onlapping layers before faulting 140 onlap_clip_dict = find_zero_thickness_onlapping_layers( 141 self.unfaulted_depth_maps, self.onlap_horizon_list 142 ) 143 _ = self.generate_faults() 144 145 # Apply faulting to age model, net_to_gross cube & onlap segments 146 self.faulted_age_volume[:] = self.apply_xyz_displacement( 147 self.vols.geologic_age[:] 148 ).astype("float") 149 self.faulted_onlap_segments[:] = self.apply_xyz_displacement( 150 self.vols.onlap_segments[:] 151 ) 152 153 # Improve the depth maps post faulting by 154 # re-interpolating across faulted age model 155 ( 156 self.faulted_depth_maps[:], 157 self.faulted_depth_maps_gaps[:], 158 ) = self.improve_depth_maps_post_faulting( 159 self.vols.geologic_age[:], self.faulted_age_volume[:], onlap_clip_dict 160 ) 161 162 if self.cfg.include_salt: 163 from datagenerator.Salt import SaltModel 164 165 self.salt_model = SaltModel(self.cfg) 166 self.salt_model.compute_salt_body_segmentation() 167 ( 168 self.faulted_depth_maps[:], 169 self.faulted_depth_maps_gaps[:], 170 ) = self.salt_model.update_depth_maps_with_salt_segments_drag() 171 172 # # Write the faulted maps to disk 173 self.write_maps_to_disk( 174 self.faulted_depth_maps[:] * self.cfg.digi, "depth_maps" 175 ) 176 self.write_maps_to_disk( 177 self.faulted_depth_maps_gaps[:] * self.cfg.digi, "depth_maps_gaps" 178 ) 179 self.write_onlap_episodes( 180 self.onlap_horizon_list[:], 181 self.faulted_depth_maps_gaps[:], 182 self.faulted_depth_maps[:], 183 ) 184 if np.any(self.fan_horizon_list): 185 self.write_fan_horizons( 186 self.fan_horizon_list, self.faulted_depth_maps[:] * 4.0 187 ) 188 189 if self.cfg.hdf_store: 190 # Write faulted maps to hdf 191 for n, d in zip( 192 ["depth_maps", "depth_maps_gaps"], 193 [ 194 self.faulted_depth_maps[:] * self.cfg.digi, 195 self.faulted_depth_maps_gaps[:] * self.cfg.digi, 196 ], 197 ): 198 write_data_to_hdf(n, d, self.cfg.hdf_master) 199 200 # Create faulted binary segmentation volumes 201 _fault_planes = self.fault_planes[:] 202 self.fault_planes[:] = self.create_binary_segmentations_post_faulting( 203 _fault_planes, 0.45 204 ) 205 del _fault_planes 206 _fault_intersections = self.fault_intersections[:] 207 self.fault_intersections[:] = self.create_binary_segmentations_post_faulting( 208 _fault_intersections, 0.45 209 ) 210 del _fault_intersections 211 _faulted_onlap_segments = self.faulted_onlap_segments[:] 212 self.faulted_onlap_segments[:] = self.create_binary_segmentations_post_faulting( 213 _faulted_onlap_segments, 0.45 214 ) 215 del _faulted_onlap_segments 216 if self.cfg.include_channels: 217 self.vols.floodplain_shale = self.apply_xyz_displacement( 218 self.vols.floodplain_shale 219 ) 220 self.vols.channel_fill = self.apply_xyz_displacement(self.vols.channel_fill) 221 self.vols.shale_channel_drape = self.apply_xyz_displacement( 222 self.vols.shale_channel_drape 223 ) 224 self.vols.levee = self.apply_xyz_displacement(self.vols.levee) 225 self.vols.crevasse = self.apply_xyz_displacement(self.vols.crevasse) 226 227 ( 228 self.vols.channel_segments, 229 self.vols.geologic_age, 230 ) = self.reassign_channel_segment_encoding( 231 self.vols.geologic_age, 232 self.vols.floodplain_shale, 233 self.vols.channel_fill, 234 self.vols.shale_channel_drape, 235 self.vols.levee, 236 self.vols.crevasse, 237 self.maps.channels, 238 ) 239 if self.cfg.model_qc_volumes: 240 self.vols.write_cube_to_disk( 241 self.vols.channel_segments, "channel_segments" 242 ) 243 244 if self.cfg.model_qc_volumes: 245 # Output files if qc volumes required 246 self.vols.write_cube_to_disk(self.faulted_age_volume[:], "geologic_age") 247 self.vols.write_cube_to_disk( 248 self.faulted_onlap_segments[:], "onlap_segments" 249 ) 250 self.vols.write_cube_to_disk(self.fault_planes[:], "fault_segments") 251 self.vols.write_cube_to_disk( 252 self.fault_intersections[:], "fault_intersection_segments" 253 ) 254 self.vols.write_cube_to_disk( 255 self.fault_plane_throw[:], "fault_segments_throw" 256 ) 257 self.vols.write_cube_to_disk( 258 self.fault_plane_azimuth[:], "fault_segments_azimuth" 259 ) 260 if self.cfg.hdf_store: 261 # Write faulted age, onlap and fault segment cubes to hdf 262 for n, d in zip( 263 [ 264 "geologic_age_faulted", 265 "onlap_segments", 266 "fault_segments", 267 "fault_intersection_segments", 268 "fault_segments_throw", 269 "fault_segments_azimuth", 270 ], 271 [ 272 self.faulted_age_volume, 273 self.faulted_onlap_segments, 274 self.fault_planes, 275 self.fault_intersections, 276 self.fault_plane_throw, 277 self.fault_plane_azimuth, 278 ], 279 ): 280 write_data_to_hdf(n, d, self.cfg.hdf_master) 281 282 if self.cfg.qc_plots: 283 self.create_qc_plots() 284 try: 285 # Create 3D qc plot 286 plot_3D_faults_plot(self.cfg, self) 287 except ValueError: 288 self.cfg.write_to_logfile("3D Fault Plotting Failed") 289 290 def build_faulted_property_geomodels( 291 self, 292 facies: np.ndarray 293 ) -> None: 294 """ 295 Build Faulted Property Geomodels 296 ------------ 297 Generates faulted property geomodels. 298 299 **The method does the following:** 300 301 Use faulted geologic_age cube, depth_maps and facies 302 to create geomodel properties (depth, lith) 303 304 - lithology 305 - net_to_gross (to create effective sand layers) 306 - depth below mudline 307 - randomised depth below mudline (to 308 randomise the rock properties per layer) 309 310 Parameters 311 ---------- 312 facies : np.ndarray 313 The Horizons class used to build the faults. 314 315 Returns 316 ------- 317 None 318 """ 319 work_cube_lith = ( 320 np.ones_like(self.faulted_age_volume) * -1 321 ) # initialise lith cube to water 322 work_cube_sealed = np.zeros_like(self.faulted_age_volume) 323 work_cube_net_to_gross = np.zeros_like(self.faulted_age_volume) 324 work_cube_depth = np.zeros_like(self.faulted_age_volume) 325 # Also create a randomised depth cube for generating randomised rock properties 326 # final dimension's shape is based on number of possible list types 327 # currently one of ['seawater', 'shale', 'sand'] 328 # n_lith = len(['shale', 'sand']) 329 cube_shape = self.faulted_age_volume.shape 330 # randomised_depth = np.zeros(cube_shape, 'float32') 331 332 ii, jj = self.build_meshgrid() 333 334 # Loop over layers in reverse order, start at base 335 previous_depth_map = self.faulted_depth_maps[:, :, -1] 336 if self.cfg.partial_voxels: 337 # add .5 to consider partial voxels from half above and half below 338 previous_depth_map += 0.5 339 340 for i in range(self.faulted_depth_maps.shape[2] - 2, 0, -1): 341 # Apply a random depth shift within the range as provided in config file 342 # (provided in metres, so converted to samples here) 343 344 current_depth_map = self.faulted_depth_maps[:, :, i] 345 if self.cfg.partial_voxels: 346 current_depth_map += 0.5 347 348 # compute maps with indices of top map and base map to include partial voxels 349 top_map_index = current_depth_map.copy().astype("int") 350 base_map_index = ( 351 self.faulted_depth_maps[:, :, i + 1].copy().astype("int") + 1 352 ) 353 354 # compute thickness over which to iterate 355 thickness_map = base_map_index - top_map_index 356 thickness_map_max = thickness_map.max() 357 358 tvdml_map = previous_depth_map - self.faulted_depth_maps[:, :, 0] 359 # Net to Gross Map for layer 360 if not self.cfg.variable_shale_ng: 361 ng_map = np.zeros( 362 shape=( 363 self.faulted_depth_maps.shape[0], 364 self.faulted_depth_maps.shape[1], 365 ) 366 ) 367 else: 368 if ( 369 facies[i] == 0.0 370 ): # if shale layer, make non-zero N/G map for layer using a low average net to gross 371 ng_map = self.create_random_net_over_gross_map( 372 avg=(0.0, 0.2), stdev=(0.001, 0.01), octave=3 373 ) 374 if facies[i] == 1.0: # if sand layer, make non-zero N/G map for layer 375 ng_map = self.create_random_net_over_gross_map() 376 377 for k in range(thickness_map_max + 1): 378 379 if self.cfg.partial_voxels: 380 # compute fraction of voxel containing layer 381 top_map = np.max( 382 np.dstack( 383 (current_depth_map, top_map_index.astype("float32") + k) 384 ), 385 axis=-1, 386 ) 387 top_map = np.min(np.dstack((top_map, previous_depth_map)), axis=-1) 388 base_map = np.min( 389 np.dstack( 390 ( 391 previous_depth_map, 392 top_map_index.astype("float32") + k + 1, 393 ) 394 ), 395 axis=-1, 396 ) 397 fraction_of_voxel = np.clip(base_map - top_map, 0.0, 1.0) 398 valid_k = np.where( 399 (fraction_of_voxel > 0.0) 400 & ((top_map_index + k).astype("int") < cube_shape[2]), 401 1, 402 0, 403 ) 404 405 # put layer properties in the cube for each case 406 sublayer_ii = ii[valid_k == 1] 407 sublayer_jj = jj[valid_k == 1] 408 else: 409 sublayer_ii = ii[thickness_map > k] 410 sublayer_jj = jj[thickness_map > k] 411 412 if sublayer_ii.shape[0] > 0: 413 if self.cfg.partial_voxels: 414 sublayer_depth_map = (top_map_index + k).astype("int")[ 415 valid_k == 1 416 ] 417 sublayer_depth_map_int = np.clip(sublayer_depth_map, 0, None) 418 sublayer_ng_map = ng_map[valid_k == 1] 419 sublayer_tvdml_map = tvdml_map[valid_k == 1] 420 sublayer_fraction = fraction_of_voxel[valid_k == 1] 421 422 # Lithology cube 423 input_cube = work_cube_lith[ 424 sublayer_ii, sublayer_jj, sublayer_depth_map_int 425 ] 426 values = facies[i] * sublayer_fraction 427 input_cube[input_cube == -1.0] = ( 428 values[input_cube == -1.0] * 1.0 429 ) 430 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 431 work_cube_lith[ 432 sublayer_ii, sublayer_jj, sublayer_depth_map_int 433 ] = (input_cube * 1.0) 434 del input_cube 435 del values 436 437 input_cube = work_cube_sealed[ 438 sublayer_ii, sublayer_jj, sublayer_depth_map_int 439 ] 440 values = (1 - facies[i - 1]) * sublayer_fraction 441 input_cube[input_cube == -1.0] = ( 442 values[input_cube == -1.0] * 1.0 443 ) 444 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 445 work_cube_sealed[ 446 sublayer_ii, sublayer_jj, sublayer_depth_map_int 447 ] = (input_cube * 1.0) 448 del input_cube 449 del values 450 451 # Depth cube 452 work_cube_depth[ 453 sublayer_ii, sublayer_jj, sublayer_depth_map_int 454 ] += (sublayer_tvdml_map * sublayer_fraction) 455 # Net to Gross cube 456 work_cube_net_to_gross[ 457 sublayer_ii, sublayer_jj, sublayer_depth_map_int 458 ] += (sublayer_ng_map * sublayer_fraction) 459 460 # Randomised Depth Cube 461 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += \ 462 # (sublayer_tvdml_map + random_z_perturbation) * sublayer_fraction 463 464 else: 465 sublayer_depth_map_int = ( 466 0.5 467 + np.clip( 468 previous_depth_map[thickness_map > k], 469 0, 470 self.vols.geologic_age.shape[2] - 1, 471 ) 472 ).astype("int") - k 473 sublayer_tvdml_map = tvdml_map[thickness_map > k] 474 sublayer_ng_map = ng_map[thickness_map > k] 475 476 work_cube_lith[ 477 sublayer_ii, sublayer_jj, sublayer_depth_map_int 478 ] = facies[i] 479 work_cube_sealed[ 480 sublayer_ii, sublayer_jj, sublayer_depth_map_int 481 ] = (1 - facies[i - 1]) 482 483 work_cube_depth[ 484 sublayer_ii, sublayer_jj, sublayer_depth_map_int 485 ] = sublayer_tvdml_map 486 work_cube_net_to_gross[ 487 sublayer_ii, sublayer_jj, sublayer_depth_map_int 488 ] += sublayer_ng_map 489 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += (sublayer_tvdml_map + random_z_perturbation) 490 491 # replace previous depth map for next iteration 492 previous_depth_map = current_depth_map.copy() 493 494 if self.cfg.verbose: 495 print("\n\n ... After infilling ...") 496 self.write_cube_to_disk(work_cube_sealed.astype("uint8"), "sealed_label") 497 498 # Clip cubes and convert from samples to units 499 work_cube_lith = np.clip(work_cube_lith, -1.0, 1.0) # clip lith to [-1, +1] 500 work_cube_net_to_gross = np.clip( 501 work_cube_net_to_gross, 0, 1.0 502 ) # clip n/g to [0, 1] 503 work_cube_depth = np.clip(work_cube_depth, a_min=0, a_max=None) 504 work_cube_depth *= self.cfg.digi 505 506 if self.cfg.include_salt: 507 # Update age model after horizons have been modified by salt inclusion 508 self.faulted_age_volume[ 509 : 510 ] = self.create_geologic_age_3d_from_infilled_horizons( 511 self.faulted_depth_maps[:] * 10.0 512 ) 513 # Set lith code for salt 514 work_cube_lith[self.salt_model.salt_segments[:] > 0.0] = 2.0 515 # Fix deepest part of facies in case salt inclusion has shifted base horizon 516 # This can leave default (water) facies codes at the base 517 last_50_samples = self.cfg.cube_shape[-1] - 50 518 work_cube_lith[..., last_50_samples:][ 519 work_cube_lith[..., last_50_samples:] == -1.0 520 ] = 0.0 521 522 if self.cfg.qc_plots: 523 from datagenerator.util import plot_xsection 524 import matplotlib as mpl 525 526 line_number = int( 527 work_cube_lith.shape[0] / 2 528 ) # pick centre line for all plots 529 530 if self.cfg.include_salt and np.max(work_cube_lith[line_number, ...]) > 1: 531 lith_cmap = mpl.colors.ListedColormap( 532 ["blue", "saddlebrown", "gold", "grey"] 533 ) 534 else: 535 lith_cmap = mpl.colors.ListedColormap(["blue", "saddlebrown", "gold"]) 536 plot_xsection( 537 work_cube_lith, 538 self.faulted_depth_maps[:], 539 line_num=line_number, 540 title="Example Trav through 3D model\nLithology", 541 png_name="QC_plot__AfterFaulting_lithology.png", 542 cfg=self.cfg, 543 cmap=lith_cmap, 544 ) 545 plot_xsection( 546 work_cube_depth, 547 self.faulted_depth_maps, 548 line_num=line_number, 549 title="Example Trav through 3D model\nDepth Below Mudline", 550 png_name="QC_plot__AfterFaulting_depth_bml.png", 551 cfg=self.cfg, 552 cmap="cubehelix_r", 553 ) 554 self.faulted_lithology[:] = work_cube_lith 555 self.faulted_net_to_gross[:] = work_cube_net_to_gross 556 self.faulted_depth[:] = work_cube_depth 557 # self.randomised_depth[:] = randomised_depth 558 559 # Write the % sand in model to logfile 560 sand_fraction = ( 561 work_cube_lith[work_cube_lith == 1].size 562 / work_cube_lith[work_cube_lith >= 0].size 563 ) 564 self.cfg.write_to_logfile( 565 f"Sand voxel % in model {100 * sand_fraction:.1f}%", 566 mainkey="model_parameters", 567 subkey="sand_voxel_pct", 568 val=100 * sand_fraction, 569 ) 570 571 if self.cfg.hdf_store: 572 for n, d in zip( 573 ["lithology", "net_to_gross", "depth"], 574 [ 575 self.faulted_lithology[:], 576 self.faulted_net_to_gross[:], 577 self.faulted_depth[:], 578 ], 579 ): 580 write_data_to_hdf(n, d, self.cfg.hdf_master) 581 582 # Save out reservoir volume for XAI-NBDT 583 reservoir = (work_cube_lith == 1) * 1.0 584 reservoir_dilated = binary_dilation(reservoir) 585 self.reservoir[:] = reservoir_dilated 586 587 if self.cfg.model_qc_volumes: 588 self.write_cube_to_disk(self.faulted_lithology[:], "faulted_lithology") 589 self.write_cube_to_disk( 590 self.faulted_net_to_gross[:], "faulted_net_to_gross" 591 ) 592 self.write_cube_to_disk(self.faulted_depth[:], "faulted_depth") 593 self.write_cube_to_disk(self.faulted_age_volume[:], "faulted_age") 594 if self.cfg.include_salt: 595 self.write_cube_to_disk( 596 self.salt_model.salt_segments[..., : self.cfg.cube_shape[2]].astype( 597 "uint8" 598 ), 599 "salt", 600 ) 601 602 def create_qc_plots(self) -> None: 603 """ 604 Create QC Plots 605 --------------- 606 Creates QC Plots of faulted models and histograms of 607 voxels which are not in layers. 608 609 Parameters 610 ---------- 611 None 612 613 Returns 614 ------- 615 None 616 """ 617 from datagenerator.util import ( 618 find_line_with_most_voxels, 619 plot_voxels_not_in_regular_layers, 620 plot_xsection, 621 ) 622 623 # analyze voxel values not in regular layers 624 plot_voxels_not_in_regular_layers( 625 volume=self.faulted_age_volume[:], 626 threshold=0.0, 627 cfg=self.cfg, 628 title="Example Trav through 3D model\n" 629 + "histogram of layers after faulting, before inserting channel facies", 630 png_name="QC_plot__Channels__histogram_FaultedLayersNoChannels.png", 631 ) 632 try: # if channel_segments exists 633 inline_index_channels = find_line_with_most_voxels( 634 self.vols.channel_segments, 0.0, self.cfg 635 ) 636 plot_xsection( 637 self.vols.channel_segments, 638 self.faulted_depth_maps, 639 inline_index_channels, 640 cfg=self.cfg, 641 title="Example Trav through 3D model\nchannel_segments after faulting", 642 png_name="QC_plot__AfterFaulting_channel_segments.png", 643 ) 644 title = "Example Trav through 3D model\nLayers Filled With Layer Number / ChannelsAdded / Faulted" 645 png_name = "QC_plot__LayersFilledWithLayerNumber_ChannelsAdded_Faulted.png" 646 except (NameError, AttributeError): # channel_segments does not exist 647 inline_index_channels = int(self.faulted_age_volume.shape[0] / 2) 648 title = "Example Trav through 3D model\nLayers Filled With Layer Number / Faulted" 649 png_name = "QC_plot__LayersFilledWithLayerNumber_Faulted.png" 650 plot_xsection( 651 self.faulted_age_volume[:], 652 self.faulted_depth_maps[:], 653 inline_index_channels, 654 title, 655 png_name, 656 self.cfg, 657 ) 658 659 def generate_faults(self) -> np.ndarray: 660 """ 661 Generate Faults 662 --------------- 663 Generates faults in the model. 664 665 Parameters 666 ---------- 667 None 668 669 Returns 670 ------- 671 displacements_classification : np.ndarray 672 Array of fault displacement classifications 673 """ 674 if self.cfg.verbose: 675 print(f" ... create {self.cfg.number_faults} faults") 676 fault_params = self.fault_parameters() 677 678 # Write fault parameters to logfile 679 self.cfg.write_to_logfile( 680 f"Fault_mode: {self.cfg.fmode}", 681 mainkey="model_parameters", 682 subkey="fault_mode", 683 val=self.cfg.fmode, 684 ) 685 self.cfg.write_to_logfile( 686 f"Noise_level: {self.cfg.fnoise}", 687 mainkey="model_parameters", 688 subkey="noise_level", 689 val=self.cfg.fnoise, 690 ) 691 692 # Build faults, and sum displacements 693 displacements_classification, hockeys = self.build_faults(fault_params) 694 if self.cfg.model_qc_volumes: 695 # Write max fault throw cube to disk 696 self.vols.write_cube_to_disk(self.max_fault_throw[:], "max_fault_throw") 697 self.cfg.write_to_logfile( 698 f"Hockey_Sticks generated: {sum(hockeys)}", 699 mainkey="model_parameters", 700 subkey="hockey_sticks_generated", 701 val=sum(hockeys), 702 ) 703 704 self.cfg.write_to_logfile( 705 f"Fault Info: a, b, c, x0, y0, z0, throw/infill_factor, shear_zone_width," 706 " gouge_pctile, tilt_pct*100" 707 ) 708 for i in range(self.cfg.number_faults): 709 self.cfg.write_to_logfile( 710 f"Fault_{i + 1}: {fault_params['a'][i]:.2f}, {fault_params['b'][i]:.2f}, {fault_params['c'][i]:.2f}," 711 f" {fault_params['x0'][i]:>7.2f}, {fault_params['y0'][i]:>7.2f}, {fault_params['z0'][i]:>8.2f}," 712 f" {fault_params['throw'][i] / self.cfg.infill_factor:>6.2f}, {fault_params['tilt_pct'][i] * 100:.2f}" 713 ) 714 715 self.cfg.write_to_logfile( 716 msg=None, 717 mainkey=f"fault_{i + 1}", 718 subkey="model_id", 719 val=os.path.basename(self.cfg.work_subfolder), 720 ) 721 for _subkey_name in [ 722 "a", 723 "b", 724 "c", 725 "x0", 726 "y0", 727 "z0", 728 "throw", 729 "tilt_pct", 730 "shear_zone_width", 731 "gouge_pctile", 732 ]: 733 _val = fault_params[_subkey_name][i] 734 self.cfg.write_to_logfile( 735 msg=None, mainkey=f"fault_{i + 1}", subkey=_subkey_name, val=_val 736 ) 737 738 return displacements_classification 739 740 def fault_parameters(self): 741 """ 742 Get Fault Parameters 743 --------------- 744 Returns the fault parameters. 745 746 Factory design pattern used to select fault parameters 747 748 Parameters 749 ---------- 750 None 751 752 Returns 753 ------- 754 fault_mode : dict 755 Dictionary containing the fault parameters. 756 """ 757 fault_mode = self._get_fault_mode() 758 return fault_mode() 759 760 def build_faults(self, fp: dict, verbose=False): 761 """ 762 Build Faults 763 --------------- 764 Creates faults in the model. 765 766 Parameters 767 ---------- 768 fp : dict 769 Dictionary containing the fault parameters. 770 verbose : bool 771 The level of verbosity to use. 772 773 Returns 774 ------- 775 dis_class : np.ndarray 776 Array of fault displacement classifications 777 hockey_sticks : list 778 List of hockey sticks 779 """ 780 def apply_faulting(traces, stretch_times, verbose=False): 781 """ 782 Apply Faulting 783 -------------- 784 Applies faulting to the traces. 785 786 The method does the following: 787 788 Apply stretching and squeezing previously applied to the input cube 789 vertically to give all depths the same number of extrema. 790 This is intended to be a proxy for making the 791 dominant frequency the same everywhere. 792 Variables: 793 - traces - input, previously stretched/squeezed trace(s) 794 - stretch_times - input, LUT for stretching/squeezing trace(s), 795 range is (0,number samples in last dimension of 'traces') 796 - unstretch_traces - output, un-stretched/un-squeezed trace(s) 797 798 Parameters 799 ---------- 800 traces : np.ndarray 801 Previously stretched/squeezed trace(s). 802 stretch_times : np.ndarray 803 A look up table for stretching and squeezing the traces. 804 verbose : bool, optional 805 The level of verbosity, by default False 806 807 Returns 808 ------- 809 np.ndarray 810 The un-stretched/un-squeezed trace(s). 811 """ 812 unstretch_traces = np.zeros_like(traces) 813 origtime = np.arange(traces.shape[-1]) 814 815 if verbose: 816 print("\t ... Cube parameters going into interpolation") 817 print(f"\t ... Origtime shape = {len(origtime)}") 818 print(f"\t ... stretch_times_effects shape = {stretch_times.shape}") 819 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 820 print(f"\t ... traces shape = {traces.shape}") 821 822 for i in range(traces.shape[0]): 823 for j in range(traces.shape[1]): 824 if traces[i, j, :].min() != traces[i, j, :].max(): 825 unstretch_traces[i, j, :] = np.interp( 826 stretch_times[i, j, :], origtime, traces[i, j, :] 827 ) 828 else: 829 unstretch_traces[i, j, :] = traces[i, j, :] 830 return unstretch_traces 831 832 print("\n\n . starting 'build_faults'.") 833 print(" ... self.cfg.verbose = " + str(self.cfg.verbose)) 834 cube_shape = np.array(self.cfg.cube_shape) 835 cube_shape[-1] += self.cfg.pad_samples 836 samples_in_cube = self.vols.geologic_age[:].size 837 wb = self.copy_and_divide_depth_maps_by_infill( 838 self.unfaulted_depth_maps[..., 0] 839 ) 840 841 sum_displacements = np.zeros_like(self.vols.geologic_age[:]) 842 displacements_class = np.zeros_like(self.vols.geologic_age[:]) 843 hockey_sticks = [] 844 fault_voxel_count_list = [] 845 number_fault_intersections = 0 846 847 depth_maps_faulted_infilled = \ 848 self.copy_and_divide_depth_maps_by_infill( 849 self.unfaulted_depth_maps[:] 850 ) 851 depth_maps_gaps = self.copy_and_divide_depth_maps_by_infill( 852 self.unfaulted_depth_maps[:] 853 ) 854 855 # Create depth indices cube (moved from inside loop) 856 faulted_depths = np.zeros_like(self.vols.geologic_age[:]) 857 for k in range(faulted_depths.shape[-1]): 858 faulted_depths[:, :, k] = k 859 unfaulted_depths = faulted_depths * 1.0 860 _faulted_depths = ( 861 unfaulted_depths * 1.0 862 ) # in case there are 0 faults, prepare _faulted_depths here 863 864 for ifault in tqdm(range(self.cfg.number_faults)): 865 semi_axes = [ 866 fp["a"][ifault], 867 fp["b"][ifault], 868 fp["c"][ifault] / self.cfg.infill_factor ** 2, 869 ] 870 origin = [ 871 fp["x0"][ifault], 872 fp["y0"][ifault], 873 fp["z0"][ifault] / self.cfg.infill_factor, 874 ] 875 throw = fp["throw"][ifault] / self.cfg.infill_factor 876 tilt = fp["tilt_pct"][ifault] 877 878 print(f"\n\n ... inserting fault {ifault} with throw {throw:.2f}") 879 print( 880 f" ... fault ellipsoid semi-axes (a, b, c): {np.sqrt(semi_axes[0]):.2f}, " 881 f"{np.sqrt(semi_axes[1]):.2f}, {np.sqrt(semi_axes[2]):.2f}" 882 ) 883 print( 884 f" ... fault ellipsoid origin (x, y, z): {origin[0]:.2f}, {origin[1]:.2f}, {origin[2]:.2f}" 885 ) 886 print(f" ... tilt_pct: {tilt * 100:.2f}") 887 z_base = origin[2] * np.sqrt(semi_axes[2]) 888 print( 889 f" ...z for bottom of ellipsoid at depth (samples) = {np.around(z_base, 0)}" 890 ) 891 print(f" ...shape of output_cube = {self.vols.geologic_age.shape}") 892 print( 893 f" ...infill_factor, pad_samples = {self.cfg.infill_factor}, {self.cfg.pad_samples}" 894 ) 895 896 # add empty arrays for shear_zone_width and gouge_pctile to fault params dictionary 897 fp["shear_zone_width"] = np.zeros(self.cfg.number_faults) 898 fp["gouge_pctile"] = np.zeros(self.cfg.number_faults) 899 ( 900 displacement, 901 displacement_classification, 902 interpolation, 903 hockey_stick, 904 fault_segm, 905 ellipsoid, 906 fp, 907 ) = self.get_displacement_vector( 908 semi_axes, origin, throw, tilt, wb, ifault, fp 909 ) 910 911 if verbose: 912 print(" ... hockey_stick = " + str(hockey_stick)) 913 print( 914 " ... Is displacement the same as displacement_classification? " 915 + str(np.all(displacement == displacement_classification)) 916 ) 917 print( 918 " ... Sample count where displacement differs from displacement_classification? " 919 + str( 920 displacement[displacement != displacement_classification].size 921 ) 922 ) 923 print( 924 " ... percent of samples where displacement differs from displacement_classification = " 925 + format( 926 float( 927 displacement[ 928 displacement != displacement_classification 929 ].size 930 ) 931 / samples_in_cube, 932 "5.1%", 933 ) 934 ) 935 try: 936 print( 937 " ... (displacement differs from displacement_classification).mean() " 938 + str( 939 displacement[ 940 displacement != displacement_classification 941 ].mean() 942 ) 943 ) 944 print( 945 " ... (displacement differs from displacement_classification).max() " 946 + str( 947 displacement[ 948 displacement != displacement_classification 949 ].max() 950 ) 951 ) 952 except: 953 pass 954 955 print( 956 " ...displacement_classification.min() = " 957 + ", " 958 + str(displacement_classification.min()) 959 ) 960 print( 961 " ...displacement_classification.mean() = " 962 + ", " 963 + str(displacement_classification.mean()) 964 ) 965 print( 966 " ...displacement_classification.max() = " 967 + ", " 968 + str(displacement_classification.max()) 969 ) 970 971 print(" ...displacement.min() = " + ", " + str(displacement.min())) 972 print(" ...displacement.mean() = " + ", " + str(displacement.mean())) 973 print(" ...displacement.max() = " + ", " + str(displacement.max())) 974 if fault_segm[fault_segm > 0.0].size > 0: 975 print( 976 " ...displacement[fault_segm >0].min() = " 977 + ", " 978 + str(displacement[fault_segm > 0].min()) 979 ) 980 print( 981 " ...displacement[fault_segm >0] P10 = " 982 + ", " 983 + str(np.percentile(displacement[fault_segm > 0], 10)) 984 ) 985 print( 986 " ...displacement[fault_segm >0] P25 = " 987 + ", " 988 + str(np.percentile(displacement[fault_segm > 0], 25)) 989 ) 990 print( 991 " ...displacement[fault_segm >0].mean() = " 992 + ", " 993 + str(displacement[fault_segm > 0].mean()) 994 ) 995 print( 996 " ...displacement[fault_segm >0] P75 = " 997 + ", " 998 + str(np.percentile(displacement[fault_segm > 0], 75)) 999 ) 1000 print( 1001 " ...displacement[fault_segm >0] P90 = " 1002 + ", " 1003 + str(np.percentile(displacement[fault_segm > 0], 90)) 1004 ) 1005 print( 1006 " ...displacement[fault_segm >0].max() = " 1007 + ", " 1008 + str(displacement[fault_segm > 0].max()) 1009 ) 1010 1011 inline = self.cfg.cube_shape[0] // 2 1012 1013 # limit labels to portions of fault plane with throw above threshold 1014 throw_threshold_samples = 1.0 1015 footprint = np.ones((3, 3, 1)) 1016 fp_i, fp_j, fp_k = np.where( 1017 (fault_segm > 0.25) 1018 & (displacement_classification > throw_threshold_samples) 1019 ) 1020 fault_segm = np.zeros_like(fault_segm) 1021 fault_plane_displacement = np.zeros_like(fault_segm) 1022 fault_segm[fp_i, fp_j, fp_k] = 1.0 1023 fault_plane_displacement[fp_i, fp_j, fp_k] = ( 1024 displacement_classification[fp_i, fp_j, fp_k] * 1.0 1025 ) 1026 1027 fault_voxel_count_list.append(fault_segm[fault_segm > 0.5].size) 1028 1029 # create blended version of displacement that accounts for simulation of fault drag 1030 drag_factor = 0.5 1031 displacement = ( 1032 1.0 - drag_factor 1033 ) * displacement + drag_factor * displacement_classification 1034 1035 # project fault depth on 2D map surface 1036 fault_plane_map = np.zeros_like(wb) 1037 depth_indices = np.arange(ellipsoid.shape[-1]) 1038 for ii in range(fault_plane_map.shape[0]): 1039 for jj in range(fault_plane_map.shape[1]): 1040 fault_plane_map[ii, jj] = np.interp( 1041 1.0, ellipsoid[ii, jj, :], depth_indices 1042 ) 1043 fault_plane_map = fault_plane_map.clip(0, ellipsoid.shape[2] - 1) 1044 1045 # compute fault azimuth (relative to i,j,k indices, not North) 1046 dx, dy = np.gradient(fault_plane_map) 1047 strike_angle = np.arctan2(dy, dx) * 180.0 / np.pi # 2D 1048 del dx 1049 del dy 1050 strike_angle = np.zeros_like(fault_segm) + strike_angle.reshape( 1051 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1052 ) 1053 strike_angle[fault_segm < 0.5] = 200.0 1054 1055 # - compute faulted depth as max of fault plane or faulted depth 1056 fault_plane = np.zeros_like(displacement) + fault_plane_map.reshape( 1057 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1058 ) 1059 1060 if ifault == 0: 1061 print(" .... set _unfaulted_depths to array with all zeros...") 1062 _faulted_depths = unfaulted_depths * 1.0 1063 self.fault_plane_azimuth[:] = strike_angle * 1.0 1064 1065 print(" ... interpolation = " + str(interpolation)) 1066 1067 if ( 1068 interpolation 1069 ): # i.e. if the fault should be considered, apply the displacements and append fault plane 1070 faulted_depths2 = np.zeros( 1071 (ellipsoid.shape[0], ellipsoid.shape[1], ellipsoid.shape[2], 2), 1072 "float", 1073 ) 1074 faulted_depths2[:, :, :, 0] = displacement * 1.0 1075 faulted_depths2[:, :, :, 1] = fault_plane - faulted_depths 1076 faulted_depths2[:, :, :, 1][ellipsoid > 1] = 0.0 1077 map_displacement_vector = np.min(faulted_depths2, axis=-1).clip( 1078 0.0, None 1079 ) 1080 del faulted_depths2 1081 map_displacement_vector[ellipsoid > 1] = 0.0 1082 1083 self.sum_map_displacements[:] += map_displacement_vector 1084 displacements_class += displacement_classification 1085 # Set displacements outside ellipsoid to 0 1086 displacement[ellipsoid > 1] = 0 1087 1088 sum_displacements += displacement 1089 1090 # apply fault to depth_cube 1091 if ifault == 0: 1092 print(" .... set _unfaulted_depths to array with all zeros...") 1093 _faulted_depths = unfaulted_depths * 1.0 1094 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1095 0, ellipsoid.shape[-1] - 1 1096 ) 1097 _faulted_depths = apply_faulting( 1098 _faulted_depths, adjusted_faulted_depths 1099 ) 1100 _mft = self.max_fault_throw[:] 1101 _mft[ellipsoid <= 1.0] = throw 1102 self.max_fault_throw[:] = _mft 1103 del _mft 1104 self.fault_planes[:] = fault_segm * 1.0 1105 _fault_intersections = self.fault_intersections[:] 1106 previous_intersection_voxel_count = _fault_intersections[ 1107 _fault_intersections > 1.1 1108 ].size 1109 _fault_intersections = self.fault_planes[:] * 1.0 1110 if ( 1111 _fault_intersections[_fault_intersections > 1.1].size 1112 > previous_intersection_voxel_count 1113 ): 1114 number_fault_intersections += 1 1115 self.fault_intersections[:] = _fault_intersections 1116 self.fault_plane_throw[:] = fault_plane_displacement * 1.0 1117 self.fault_plane_azimuth[:] = strike_angle * 1.0 1118 else: 1119 print( 1120 " .... update _unfaulted_depths array using faulted depths..." 1121 ) 1122 try: 1123 print( 1124 " .... (before) _faulted_depths.mean() = " 1125 + str(_faulted_depths.mean()) 1126 ) 1127 except: 1128 _faulted_depths = unfaulted_depths * 1.0 1129 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1130 0, ellipsoid.shape[-1] - 1 1131 ) 1132 _faulted_depths = apply_faulting( 1133 _faulted_depths, adjusted_faulted_depths 1134 ) 1135 1136 _fault_planes = apply_faulting( 1137 self.fault_planes[:], adjusted_faulted_depths 1138 ) 1139 if ( 1140 _fault_planes[ 1141 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1142 ].size 1143 > 0 1144 ): 1145 _fault_planes[ 1146 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1147 ] = 0.0 1148 if ( 1149 _fault_planes[ 1150 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1151 ].size 1152 > 0 1153 ): 1154 _fault_planes[ 1155 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1156 ] = 1.0 1157 self.fault_planes[:] = _fault_planes 1158 del _fault_planes 1159 1160 _fault_intersections = apply_faulting( 1161 self.fault_intersections[:], adjusted_faulted_depths 1162 ) 1163 if ( 1164 _fault_intersections[ 1165 np.logical_and( 1166 1.25 < _fault_intersections, _fault_intersections < 2.0 1167 ) 1168 ].size 1169 > 0 1170 ): 1171 _fault_intersections[ 1172 np.logical_and( 1173 1.25 < _fault_intersections, _fault_intersections < 2.0 1174 ) 1175 ] = 2.0 1176 self.fault_intersections[:] = _fault_intersections 1177 del _fault_intersections 1178 1179 self.max_fault_throw[:] = apply_faulting( 1180 self.max_fault_throw[:], adjusted_faulted_depths 1181 ) 1182 self.fault_plane_throw[:] = apply_faulting( 1183 self.fault_plane_throw[:], adjusted_faulted_depths 1184 ) 1185 self.fault_plane_azimuth[:] = apply_faulting( 1186 self.fault_plane_azimuth[:], adjusted_faulted_depths 1187 ) 1188 1189 self.fault_planes[:] += fault_segm 1190 self.fault_intersections[:] += fault_segm 1191 _mft = self.max_fault_throw[:] 1192 _mft[ellipsoid <= 1.0] += throw 1193 self.max_fault_throw[:] = _mft 1194 del _mft 1195 1196 if verbose: 1197 print( 1198 " ... fault_plane_displacement[fault_plane_displacement > 0.].size = " 1199 + str( 1200 fault_plane_displacement[ 1201 fault_plane_displacement > 0.0 1202 ].size 1203 ) 1204 ) 1205 self.fault_plane_throw[ 1206 fault_plane_displacement > 0.0 1207 ] = fault_plane_displacement[fault_plane_displacement > 0.0] 1208 if verbose: 1209 print( 1210 " ... self.fault_plane_throw[self.fault_plane_throw > 0.].size = " 1211 + str( 1212 self.fault_plane_throw[ 1213 self.fault_plane_throw > 0.0 1214 ].size 1215 ) 1216 ) 1217 self.fault_plane_azimuth[fault_segm > 0.9] = ( 1218 strike_angle[fault_segm > 0.9] * 1.0 1219 ) 1220 1221 if verbose: 1222 print( 1223 " .... (after) _faulted_depths.mean() = " 1224 + str(_faulted_depths.mean()) 1225 ) 1226 1227 # fix interpolated values in max_fault_throw 1228 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1229 self.max_fault_throw, return_counts=True 1230 ) 1231 max_fault_throw_list = max_fault_throw_list[ 1232 max_fault_throw_list_counts > 500 1233 ] 1234 if verbose: 1235 print( 1236 "\n ...max_fault_throw_list = " 1237 + str(max_fault_throw_list) 1238 + ", " 1239 + str(max_fault_throw_list_counts) 1240 ) 1241 mfts = self.max_fault_throw.shape 1242 self.cfg.hdf_remove_node_list("max_fault_throw_4d_diff") 1243 self.cfg.hdf_remove_node_list("max_fault_throw_4d") 1244 max_fault_throw_4d_diff = self.cfg.hdf_init( 1245 "max_fault_throw_4d_diff", 1246 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1247 ) 1248 max_fault_throw_4d = self.cfg.hdf_init( 1249 "max_fault_throw_4d", 1250 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1251 ) 1252 if verbose: 1253 print( 1254 "\n ...max_fault_throw_4d.shape = " 1255 + ", " 1256 + str(max_fault_throw_4d.shape) 1257 ) 1258 _max_fault_throw_4d_diff = max_fault_throw_4d_diff[:] 1259 _max_fault_throw_4d = max_fault_throw_4d[:] 1260 for imft, mft in enumerate(max_fault_throw_list): 1261 print( 1262 " ... imft, mft, max_fault_throw_4d_diff[:,:,:,imft].shape = " 1263 + str( 1264 ( 1265 imft, 1266 mft, 1267 max_fault_throw_4d_diff[:, :, :, imft].shape, 1268 ) 1269 ) 1270 ) 1271 _max_fault_throw_4d_diff[:, :, :, imft] = np.abs( 1272 self.max_fault_throw[:, :, :] - mft 1273 ) 1274 _max_fault_throw_4d[:, :, :, imft] = mft 1275 max_fault_throw_4d[:] = _max_fault_throw_4d 1276 max_fault_throw_4d_diff[:] = _max_fault_throw_4d_diff 1277 if verbose: 1278 print( 1279 " ...np.argmin(max_fault_throw_4d_diff, axis=-1).shape = " 1280 + ", " 1281 + str(np.argmin(max_fault_throw_4d_diff, axis=-1).shape) 1282 ) 1283 indices_nearest_throw = np.argmin(max_fault_throw_4d_diff, axis=-1) 1284 if verbose: 1285 print( 1286 "\n ...indices_nearest_throw.shape = " 1287 + ", " 1288 + str(indices_nearest_throw.shape) 1289 ) 1290 _max_fault_throw = self.max_fault_throw[:] 1291 for imft, mft in enumerate(max_fault_throw_list): 1292 _max_fault_throw[indices_nearest_throw == imft] = mft 1293 self.max_fault_throw[:] = _max_fault_throw 1294 1295 del _max_fault_throw 1296 del adjusted_faulted_depths 1297 1298 if verbose: 1299 print( 1300 " ...fault_segm[fault_segm>0.].size = " 1301 + ", " 1302 + str(fault_segm[fault_segm > 0.0].size) 1303 ) 1304 print(" ...fault_segm.min() = " + ", " + str(fault_segm.min())) 1305 print(" ...fault_segm.max() = " + ", " + str(fault_segm.max())) 1306 print( 1307 " ...self.fault_planes.max() = " 1308 + ", " 1309 + str(self.fault_planes.max()) 1310 ) 1311 print( 1312 " ...self.fault_intersections.max() = " 1313 + ", " 1314 + str(self.fault_intersections.max()) 1315 ) 1316 1317 print( 1318 " ...list of unique values in self.max_fault_throw = " 1319 + ", " 1320 + str(np.unique(self.max_fault_throw)) 1321 ) 1322 1323 # TODO: remove this block after qc/tests complete 1324 from datagenerator.util import import_matplotlib 1325 plt = import_matplotlib() 1326 1327 plt.close(35) 1328 plt.figure(35, figsize=(15, 10)) 1329 plt.clf() 1330 plt.imshow(_faulted_depths[inline, :, :].T, aspect="auto", cmap="prism") 1331 plt.tight_layout() 1332 plt.savefig( 1333 os.path.join( 1334 self.cfg.work_subfolder, f"faulted_depths_{ifault:02d}.png" 1335 ), 1336 format="png", 1337 ) 1338 1339 plt.close(35) 1340 plt.figure(36, figsize=(15, 10)) 1341 plt.clf() 1342 plt.imshow(displacement[inline, :, :].T, aspect="auto", cmap="jet") 1343 plt.tight_layout() 1344 plt.savefig( 1345 os.path.join( 1346 self.cfg.work_subfolder, f"displacement_{ifault:02d}.png" 1347 ), 1348 format="png", 1349 ) 1350 1351 # TODO: remove this block after qc/tests complete 1352 _plotarray = self.fault_planes[inline, :, :].copy() 1353 _plotarray[_plotarray == 0.0] = np.nan 1354 plt.close(37) 1355 plt.figure(37, figsize=(15, 10)) 1356 plt.clf() 1357 plt.imshow(_plotarray.T, aspect="auto", cmap="gist_ncar") 1358 plt.tight_layout() 1359 plt.savefig( 1360 os.path.join(self.cfg.work_subfolder, f"fault_{ifault:02d}.png"), 1361 format="png", 1362 ) 1363 plt.close(37) 1364 plt.figure(36) 1365 plt.imshow(_plotarray.T, aspect="auto", cmap="gray", alpha=0.6) 1366 plt.tight_layout() 1367 1368 plt.savefig( 1369 os.path.join( 1370 self.cfg.work_subfolder, 1371 f"displacement_fault_overlay_{ifault:02d}.png", 1372 ), 1373 format="png", 1374 ) 1375 plt.close(36) 1376 1377 hockey_sticks.append(hockey_stick) 1378 print(" ...hockey_sticks = " + ", " + str(hockey_sticks)) 1379 1380 # print final count 1381 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1382 self.max_fault_throw, return_counts=True 1383 ) 1384 max_fault_throw_list = max_fault_throw_list[max_fault_throw_list_counts > 100] 1385 if verbose: 1386 print( 1387 "\n ... ** final ** max_fault_throw_list = " 1388 + str(max_fault_throw_list) 1389 + ", " 1390 + str(max_fault_throw_list_counts) 1391 ) 1392 1393 # create fault intersections 1394 _fault_intersections = self.fault_planes[:] * 1.0 1395 # self.fault_intersections = self.fault_planes * 1. 1396 if verbose: 1397 print(" ... line 592:") 1398 print( 1399 " ...self.fault_intersections.max() = " 1400 + ", " 1401 + str(_fault_intersections.max()) 1402 ) 1403 print( 1404 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1405 + ", " 1406 + str(_fault_intersections[_fault_intersections > 0.0].size) 1407 ) 1408 1409 _fault_intersections[_fault_intersections <= 1.0] = 0.0 1410 _fault_intersections[_fault_intersections > 1.1] = 1.0 1411 if verbose: 1412 print(" ... line 597:") 1413 print( 1414 " ...self.fault_intersections.max() = " 1415 + ", " 1416 + str(_fault_intersections.max()) 1417 ) 1418 print( 1419 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1420 + ", " 1421 + str(_fault_intersections[_fault_intersections > 0.0].size) 1422 ) 1423 1424 # make 2nd count of number of intersections between faults. write result to logfile. 1425 from datetime import datetime 1426 1427 start_time = datetime.now() 1428 number_fault_intersections = max( 1429 number_fault_intersections, 1430 measure.label(_fault_intersections, background=0).max(), 1431 ) 1432 print( 1433 f" ... elapsed time for skimage.label = {(datetime.now() - start_time)}" 1434 ) 1435 print(" ... number_fault_intersections = " + str(number_fault_intersections)) 1436 1437 # dilate intersection values 1438 # - window size of (5,5,15) is arbitrary. Should be based on isolating sections 1439 # of fault planes on real seismic 1440 _fault_intersections = maximum_filter(_fault_intersections, size=(7, 7, 17)) 1441 1442 # Fault intersection segments > 1 at intersecting faults. Clip to 1 1443 _fault_intersections[_fault_intersections > 0.05] = 1.0 1444 _fault_intersections[_fault_intersections != 1.0] = 0.0 1445 if verbose: 1446 print(" ... line 607:") 1447 print( 1448 " ...self.fault_intersections.max() = " 1449 + ", " 1450 + str(_fault_intersections.max()) 1451 ) 1452 print( 1453 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1454 + ", " 1455 + str(_fault_intersections[_fault_intersections > 0.0].size) 1456 ) 1457 1458 # Fault segments = 1 at fault > 1 at intersections. Clip intersecting fault voxels to 1 1459 _fault_planes = self.fault_planes[:] 1460 _fault_planes[_fault_planes > 0.05] = 1.0 1461 _fault_planes[_fault_planes != 1.0] = 0.0 1462 1463 # make fault azi 3D and only retain voxels in fault plane 1464 1465 for k, v in zip( 1466 [ 1467 "n_voxels_faults", 1468 "n_voxels_fault_intersections", 1469 "number_fault_intersections", 1470 "fault_voxel_count_list", 1471 "hockey_sticks", 1472 ], 1473 [ 1474 _fault_planes[_fault_planes > 0.5].size, 1475 _fault_intersections[_fault_intersections > 0.5].size, 1476 number_fault_intersections, 1477 fault_voxel_count_list, 1478 hockey_sticks, 1479 ], 1480 ): 1481 self.cfg.write_to_logfile(f"{k}: {v}") 1482 1483 self.cfg.write_to_logfile( 1484 msg=None, mainkey="model_parameters", subkey=k, val=v 1485 ) 1486 1487 dis_class = _faulted_depths * 1 1488 self.fault_intersections[:] = _fault_intersections 1489 del _fault_intersections 1490 self.fault_planes[:] = _fault_planes 1491 del _fault_planes 1492 self.displacement_vectors[:] = _faulted_depths * 1.0 1493 1494 # TODO: check if next line of code modifies 'displacement' properly 1495 self.sum_map_displacements[:] = _faulted_depths * 1.0 1496 1497 # Save faulted maps 1498 self.faulted_depth_maps[:] = depth_maps_faulted_infilled 1499 self.faulted_depth_maps_gaps[:] = depth_maps_gaps 1500 1501 # perform faulting for fault_segments and fault_intersections 1502 return dis_class, hockey_sticks 1503 1504 def _qc_plot_check_faulted_horizons_match_fault_segments( 1505 self, faulted_depth_maps, faulted_geologic_age 1506 ): 1507 """_qc_plot_check_faulted_horizons_match_fault_segments _summary_ 1508 1509 Parameters 1510 ---------- 1511 faulted_depth_maps : np.ndarray 1512 The faluted depth maps 1513 faulted_geologic_age : _type_ 1514 _description_ 1515 """ 1516 import os 1517 from datagenerator.util import import_matplotlib 1518 1519 plt = import_matplotlib() 1520 1521 plt.figure(1, figsize=(15, 10)) 1522 plt.clf() 1523 voxel_count_max = 0 1524 inline = self.cfg.cube_shape[0] // 2 1525 for i in range(0, self.fault_planes.shape[1], 10): 1526 voxel_count = self.fault_planes[:, i, :] 1527 voxel_count = voxel_count[voxel_count > 0.5].size 1528 if voxel_count > voxel_count_max: 1529 inline = i 1530 voxel_count_max = voxel_count 1531 # inline=50 1532 plotdata = (faulted_geologic_age[:, inline, :]).T 1533 plotdata_overlay = (self.fault_planes[:, inline, :]).T 1534 plotdata[plotdata_overlay > 0.9] = plotdata.max() 1535 plt.imshow(plotdata, cmap="prism", aspect="auto") 1536 plt.title( 1537 "QC plot: faulted layers with horizons and fault_segments overlain" 1538 + "\nInline " 1539 + str(inline) 1540 ) 1541 plt.colorbar() 1542 for i in range(0, faulted_depth_maps.shape[2], 10): 1543 plt.plot( 1544 range(self.cfg.cube_shape[0]), 1545 faulted_depth_maps[:, inline], 1546 "k-", 1547 lw=0.25, 1548 ) 1549 plot_name = os.path.join( 1550 self.cfg.work_subfolder, "QC_horizons_from_geologic_age_isovalues.png" 1551 ) 1552 plt.savefig(plot_name, format="png") 1553 plt.close() 1554 # 1555 plt.figure(1, figsize=(15, 10)) 1556 plt.clf() 1557 voxel_count_max = 0 1558 iz = faulted_geologic_age.shape[-1] // 2 1559 for i in range(0, self.fault_planes.shape[-1], 50): 1560 voxel_count = self.fault_planes[:, :, i] 1561 voxel_count = voxel_count[voxel_count > 0.5].size 1562 if voxel_count > voxel_count_max: 1563 iz = i 1564 voxel_count_max = voxel_count 1565 1566 plotdata = faulted_geologic_age[:, :, iz].copy() 1567 plotdata2 = self.fault_planes[:, :, iz].copy() 1568 plotdata[plotdata2 > 0.05] = 0.0 1569 plt.subplot(1, 2, 1) 1570 plt.title(str(iz)) 1571 plt.imshow(plotdata.T, cmap="prism", aspect="auto") 1572 plt.subplot(1, 2, 2) 1573 plt.imshow(plotdata2.T, cmap="jet", aspect="auto") 1574 plt.colorbar() 1575 plot_name = os.path.join( 1576 self.cfg.work_subfolder, "QC_fault_segments_on_geologic_age_timeslice.png" 1577 ) 1578 plt.savefig(plot_name, format="png") 1579 plt.close() 1580 1581 def improve_depth_maps_post_faulting( 1582 self, 1583 unfaulted_geologic_age: np.ndarray, 1584 faulted_geologic_age: np.ndarray, 1585 onlap_clips: np.ndarray 1586 ): 1587 """ 1588 Re-interpolates the depth maps using the faulted geologic age cube 1589 1590 Parameters 1591 ---------- 1592 unfaulted_geologic_age : np.ndarray 1593 The unfaulted geologic age cube 1594 faulted_geologic_age : np.ndarray 1595 The faulted geologic age cube 1596 onlap_clips : np.ndarray 1597 The onlap clips 1598 1599 Returns 1600 ------- 1601 depth_maps : np.ndarray 1602 The improved depth maps 1603 depth_maps_gaps : np.ndarray 1604 The improved depth maps with gaps 1605 """ 1606 faulted_depth_maps = np.zeros_like(self.faulted_depth_maps) 1607 origtime = np.arange(self.faulted_depth_maps.shape[-1]) 1608 for i in range(self.faulted_depth_maps.shape[0]): 1609 for j in range(self.faulted_depth_maps.shape[1]): 1610 if ( 1611 faulted_geologic_age[i, j, :].min() 1612 != faulted_geologic_age[i, j, :].max() 1613 ): 1614 faulted_depth_maps[i, j, :] = np.interp( 1615 origtime, 1616 faulted_geologic_age[i, j, :], 1617 np.arange(faulted_geologic_age.shape[-1]).astype("float"), 1618 ) 1619 else: 1620 faulted_depth_maps[i, j, :] = unfaulted_geologic_age[i, j, :] 1621 # Waterbottom horizon has been set to 0. Re-insert this from the original depth_maps array 1622 if np.count_nonzero(faulted_depth_maps[:, :, 0]) == 0: 1623 faulted_depth_maps[:, :, 0] = self.faulted_depth_maps[:, :, 0] * 1.0 1624 1625 # Shift re-interpolated horizons to replace first horizon (of 0's) with the second, etc 1626 zmaps = np.zeros_like(faulted_depth_maps) 1627 zmaps[..., :-1] = faulted_depth_maps[..., 1:] 1628 # Fix the deepest re-interpolated horizon by adding a constant thickness to the shallower horizon 1629 zmaps[..., -1] = self.faulted_depth_maps[..., -1] + 10 1630 # Clip this last horizon to the one above 1631 thickness_map = zmaps[..., -1] - zmaps[..., -2] 1632 zmaps[..., -1][np.where(thickness_map <= 0.0)] = zmaps[..., -2][ 1633 np.where(thickness_map <= 0.0) 1634 ] 1635 faulted_depth_maps = zmaps.copy() 1636 1637 if self.cfg.qc_plots: 1638 self._qc_plot_check_faulted_horizons_match_fault_segments( 1639 faulted_depth_maps, faulted_geologic_age 1640 ) 1641 1642 # Re-apply old gaps to improved depth_maps 1643 zmaps_imp = faulted_depth_maps.copy() 1644 merged = zmaps_imp.copy() 1645 _depth_maps_gaps_improved = merged.copy() 1646 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1647 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1648 1649 # for zero-thickness layers, set depth_maps_gaps to nan 1650 for i in range(depth_maps_gaps.shape[-1] - 1): 1651 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1652 # set thicknesses < zero to NaN. Use NaNs in thickness_map to 0 to avoid runtime warning when indexing 1653 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1654 1655 # restore zero thickness from faulted horizons to improved (interpolated) depth maps 1656 ii, jj = np.meshgrid( 1657 range(self.cfg.cube_shape[0]), 1658 range(self.cfg.cube_shape[1]), 1659 sparse=False, 1660 indexing="ij", 1661 ) 1662 merged = zmaps_imp.copy() 1663 1664 # create temporary copy of fault_segments with dilation 1665 from scipy.ndimage.morphology import grey_dilation 1666 1667 _dilated_fault_planes = grey_dilation(self.fault_planes, size=(3, 3, 1)) 1668 1669 _onlap_segments = self.vols.onlap_segments[:] 1670 for ihor in range(depth_maps_gaps.shape[-1] - 1, 2, -1): 1671 # filter upper horizon being used for thickness if shallower events onlap it, except at faults 1672 improved_zmap_thickness = merged[:, :, ihor] - merged[:, :, ihor - 1] 1673 depth_map_int = ((merged[:, :, ihor]).astype(int)).clip( 1674 0, _onlap_segments.shape[-1] - 1 1675 ) 1676 improved_map_onlap_segments = _onlap_segments[ii, jj, depth_map_int] + 0.0 1677 improved_map_fault_segments = ( 1678 _dilated_fault_planes[ii, jj, depth_map_int] + 0.0 1679 ) 1680 # remember that self.maps.depth_maps has horizons with direct fault application 1681 faulted_infilled_map_thickness = ( 1682 self.faulted_depth_maps[:, :, ihor] 1683 - self.faulted_depth_maps[:, :, ihor - 1] 1684 ) 1685 improved_zmap_thickness[ 1686 np.where( 1687 (faulted_infilled_map_thickness <= 0.0) 1688 & (improved_map_onlap_segments > 0.0) 1689 & (improved_map_fault_segments == 0.0) 1690 ) 1691 ] = 0.0 1692 print( 1693 " ... ihor, improved_map_onlap_segments[improved_map_onlap_segments>0.].shape," 1694 " improved_zmap_thickness[improved_zmap_thickness==0].shape = ", 1695 ihor, 1696 improved_map_onlap_segments[improved_map_onlap_segments > 0.0].shape, 1697 improved_zmap_thickness[improved_zmap_thickness == 0].shape, 1698 ) 1699 merged[:, :, ihor - 1] = merged[:, :, ihor] - improved_zmap_thickness 1700 1701 if np.any(self.fan_horizon_list): 1702 # Clip fans downwards when thickness map is zero 1703 for count, layer in enumerate(self.fan_horizon_list): 1704 merged = fix_zero_thickness_fan_layers( 1705 merged, layer, self.fan_thickness[count] 1706 ) 1707 1708 # Re-apply clipping to onlapping layers post faulting 1709 merged = fix_zero_thickness_onlap_layers(merged, onlap_clips) 1710 1711 del _dilated_fault_planes 1712 1713 # Re-apply gaps to improved depth_maps 1714 _depth_maps_gaps_improved = merged.copy() 1715 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1716 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1717 1718 # for zero-thickness layers, set depth_maps_gaps to nan 1719 for i in range(depth_maps_gaps.shape[-1] - 1): 1720 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1721 # set nans in thickness_map to 0 to avoid runtime warning 1722 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1723 1724 return merged, depth_maps_gaps 1725 1726 @staticmethod 1727 def partial_faulting( 1728 depth_map_gaps_faulted, 1729 fault_plane_classification, 1730 faulted_depth_map, 1731 ii, 1732 jj, 1733 max_throw, 1734 origtime_cube, 1735 unfaulted_depth_map, 1736 ): 1737 """ 1738 Partial faulting 1739 ---------------- 1740 1741 Executes partial faluting. 1742 1743 The docstring of this function is a work in progress. 1744 1745 Parameters 1746 ---------- 1747 depth_map_gaps_faulted : np.ndarray 1748 The depth map. 1749 fault_plane_classification : np.ndarray 1750 Fault plane classifications. 1751 faulted_depth_map : _type_ 1752 The faulted depth map. 1753 ii : int 1754 The i position 1755 jj : int 1756 The j position 1757 max_throw : float 1758 The maximum amount of throw for the faults. 1759 origtime_cube : np.ndarray 1760 Original time cube. 1761 unfaulted_depth_map : np.ndarray 1762 Unfaulted depth map. 1763 1764 Returns 1765 ------- 1766 faulted_depth_map : np.ndarray 1767 The faulted depth map 1768 depth_map_gaps_faulted : np.ndarray 1769 Depth map with gaps filled 1770 """ 1771 for ithrow in range(1, int(max_throw) + 1): 1772 # infilled 1773 partial_faulting_map = unfaulted_depth_map + ithrow 1774 partial_faulting_map_ii_jj = ( 1775 partial_faulting_map[ii, jj] 1776 .astype("int") 1777 .clip(0, origtime_cube.shape[2] - 1) 1778 ) 1779 partial_faulting_on_horizon = fault_plane_classification[ 1780 ii, jj, partial_faulting_map_ii_jj 1781 ][..., 0] 1782 origtime_on_horizon = origtime_cube[ii, jj, partial_faulting_map_ii_jj][ 1783 ..., 0 1784 ] 1785 faulted_depth_map[partial_faulting_on_horizon == 1] = np.dstack( 1786 (origtime_on_horizon, partial_faulting_map) 1787 ).min(axis=-1)[partial_faulting_on_horizon == 1] 1788 # gaps 1789 depth_map_gaps_faulted[partial_faulting_on_horizon == 1] = np.nan 1790 return faulted_depth_map, depth_map_gaps_faulted 1791 1792 def get_displacement_vector( 1793 self, 1794 semi_axes: tuple, 1795 origin: tuple, 1796 throw: float, 1797 tilt, 1798 wb, 1799 index, 1800 fp 1801 ): 1802 """ 1803 Gets a displacement vector. 1804 1805 Parameters 1806 ---------- 1807 semi_axes : tuple 1808 The semi axes. 1809 origin : tuple 1810 The origin. 1811 throw : float 1812 The throw of th fault to use. 1813 tilt : float 1814 The tilt of the fault. 1815 1816 Returns 1817 ------- 1818 stretch_times : np.ndarray 1819 The stretch times. 1820 stretch_times_classification : np.ndarray 1821 Stretch times classification. 1822 interpolation : bool 1823 Whether or not to interpolate. 1824 hockey_stick : int 1825 The hockey stick. 1826 fault_segments : np.ndarray 1827 1828 ellipsoid : 1829 fp : 1830 """ 1831 a, b, c = semi_axes 1832 x0, y0, z0 = origin 1833 1834 random_shear_zone_width = ( 1835 np.around(np.random.uniform(low=0.75, high=1.5) * 200, -2) / 200 1836 ) 1837 if random_shear_zone_width == 0: 1838 random_gouge_pctile = 100 1839 else: 1840 # clip amplitudes inside shear_zone with this percentile of total (100 implies doing nothing) 1841 random_gouge_pctile = np.random.triangular(left=10, mode=50, right=100) 1842 # Store the random values 1843 fp["shear_zone_width"][index] = random_shear_zone_width 1844 fp["gouge_pctile"][index] = random_gouge_pctile 1845 1846 if self.cfg.verbose: 1847 print(f" ...shear_zone_width (samples) = {random_shear_zone_width}") 1848 print(f" ...gouge_pctile (percent*100) = {random_gouge_pctile}") 1849 print(f" .... output_cube.shape = {self.vols.geologic_age.shape}") 1850 _p = ( 1851 np.arange(self.vols.geologic_age.shape[2]) * self.cfg.infill_factor 1852 ).shape 1853 print( 1854 f" .... (np.arange(output_cube.shape[2])*infill_factor).shape = {_p}" 1855 ) 1856 1857 ellipsoid = self.rotate_3d_ellipsoid(x0, y0, z0, a, b, c, tilt) 1858 fault_segments = self.get_fault_plane_sobel(ellipsoid) 1859 z_idx = self.get_fault_centre(ellipsoid, wb, fault_segments, index) 1860 1861 # Initialise return objects in case z_idx size == 0 1862 interpolation = False 1863 hockey_stick = 0 1864 # displacement_cube = None 1865 if np.size(z_idx) != 0: 1866 print(" ... Computing fault depth at max displacement") 1867 print(" ... depth at max displacement = {}".format(z_idx[2])) 1868 down = float(ellipsoid[ellipsoid < 1.0].size) / np.prod( 1869 self.vols.geologic_age[:].shape 1870 ) 1871 """ 1872 down = np.int16(len(np.where(ellipsoid < 1.)[0]) / 1.0 * (self.vols.geologic_age.shape[2] * 1873 self.vols.geologic_age.shape[1] * 1874 self.vols.geologic_age.shape[0])) 1875 print(" ... This fault has {!s} %% of downthrown samples".format(down)) 1876 """ 1877 print( 1878 " ... This fault has " 1879 + format(down, "5.1%") 1880 + " of downthrown samples" 1881 ) 1882 1883 ( 1884 stretch_times, 1885 stretch_times_classification, 1886 interpolation, 1887 hockey_stick, 1888 ) = self.xyz_dis(z_idx, throw, fault_segments, ellipsoid, wb, index) 1889 else: 1890 print(" ... Ellipsoid larger than cube no fault inserted") 1891 stretch_times = np.ones_like(ellipsoid) 1892 stretch_times_classification = np.ones_like(self.vols.geologic_age[:]) 1893 1894 max_fault_throw = self.max_fault_throw[:] 1895 max_fault_throw[ellipsoid < 1.0] += int(throw) 1896 self.max_fault_throw[:] = max_fault_throw 1897 1898 return ( 1899 stretch_times, 1900 stretch_times_classification, 1901 interpolation, 1902 hockey_stick, 1903 fault_segments, 1904 ellipsoid, 1905 fp, 1906 ) 1907 1908 def apply_xyz_displacement(self, traces) -> np.ndarray: 1909 """ 1910 Applies XYZ Displacement. 1911 1912 Apply stretching and squeezing previously applied to the input cube 1913 vertically to give all depths the same number of extrema. 1914 1915 This is intended to be a proxy for making the 1916 dominant frequency the same everywhere. 1917 1918 Parameters 1919 ---------- 1920 traces : np.ndarray 1921 Previously stretched/squeezed trace(s) 1922 1923 Returns 1924 ------- 1925 unstretch_traces: np.ndarray 1926 Un-stretched/un-squeezed trace(s) 1927 """ 1928 unstretch_traces = np.zeros_like(traces) 1929 origtime = np.arange(traces.shape[-1]) 1930 1931 print("\t ... Cube parameters going into interpolation") 1932 print(f"\t ... Origtime shape = {len(origtime)}") 1933 print( 1934 f"\t ... stretch_times_effects shape = {self.displacement_vectors.shape}" 1935 ) 1936 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 1937 print(f"\t ... traces shape = {traces.shape}") 1938 1939 for i in range(traces.shape[0]): 1940 for j in range(traces.shape[1]): 1941 if traces[i, j, :].min() != traces[i, j, :].max(): 1942 unstretch_traces[i, j, :] = np.interp( 1943 self.displacement_vectors[i, j, :], origtime, traces[i, j, :] 1944 ) 1945 else: 1946 unstretch_traces[i, j, :] = traces[i, j, :] 1947 return unstretch_traces 1948 1949 def copy_and_divide_depth_maps_by_infill(self, zmaps) -> np.ndarray: 1950 """ 1951 Copy and divide depth maps by infill factor 1952 ------------------------------------------- 1953 1954 Copies and divides depth maps by infill factor. 1955 1956 Parameters 1957 ---------- 1958 zmaps : np.array 1959 The depth maps to copy and divide. 1960 1961 Returns 1962 ------- 1963 np.ndarray 1964 The result of the division 1965 """ 1966 return zmaps / self.cfg.infill_factor 1967 1968 def rotate_3d_ellipsoid( 1969 self, x0, y0, z0, a, b, c, fraction 1970 ) -> np.ndarray: 1971 """ 1972 Rotate a 3D ellipsoid 1973 --------------------- 1974 1975 Parameters 1976 ---------- 1977 x0 : _type_ 1978 _description_ 1979 y0 : _type_ 1980 _description_ 1981 z0 : _type_ 1982 _description_ 1983 a : _type_ 1984 _description_ 1985 b : _type_ 1986 _description_ 1987 c : _type_ 1988 _description_ 1989 fraction : _type_ 1990 _description_ 1991 """ 1992 def f(x1, y1, z1, x_0, y_0, z_0, a1, b1, c1): 1993 return ( 1994 ((x1 - x_0) ** 2) / a1 + ((y1 - y_0) ** 2) / b1 + ((z1 - z_0) ** 2) / c1 1995 ) 1996 1997 x = np.arange(self.vols.geologic_age.shape[0]).astype("float") 1998 y = np.arange(self.vols.geologic_age.shape[1]).astype("float") 1999 z = np.arange(self.vols.geologic_age.shape[2]).astype("float") 2000 2001 xx, yy, zz = np.meshgrid(x, y, z, indexing="ij", sparse=False) 2002 2003 xyz = ( 2004 np.vstack((xx.flatten(), yy.flatten(), zz.flatten())) 2005 .swapaxes(0, 1) 2006 .astype("float") 2007 ) 2008 2009 xyz_rotated = self.apply_3d_rotation( 2010 xyz, self.vols.geologic_age.shape, x0, y0, fraction 2011 ) 2012 2013 xx_rotated = xyz_rotated[:, 0].reshape(self.vols.geologic_age.shape) 2014 yy_rotated = xyz_rotated[:, 1].reshape(self.vols.geologic_age.shape) 2015 zz_rotated = xyz_rotated[:, 2].reshape(self.vols.geologic_age.shape) 2016 2017 ellipsoid = f(xx_rotated, yy_rotated, zz_rotated, x0, y0, z0, a, b, c).reshape( 2018 self.vols.geologic_age.shape 2019 ) 2020 2021 return ellipsoid 2022 2023 @staticmethod 2024 def apply_3d_rotation(inarray, array_shape, x0, y0, fraction): 2025 from math import sqrt, sin, cos, atan2 2026 from numpy import cross, eye 2027 2028 # expm3 deprecated, changed to 'more robust' expm (TM) 2029 from scipy.linalg import expm, norm 2030 2031 def m(axis, angle): 2032 return expm(cross(eye(3), axis / norm(axis) * angle)) 2033 2034 theta = atan2( 2035 fraction 2036 * sqrt((x0 - array_shape[0] / 2) ** 2 + (y0 - array_shape[1] / 2) ** 2), 2037 array_shape[2], 2038 ) 2039 dip_angle = atan2(y0 - array_shape[1] / 2, x0 - array_shape[0] / 2) 2040 2041 strike_unitvector = np.array( 2042 (sin(np.pi - dip_angle), cos(np.pi - dip_angle), 0.0) 2043 ) 2044 m0 = m(strike_unitvector, theta) 2045 2046 outarray = np.dot(m0, inarray.T).T 2047 2048 return outarray 2049 2050 @staticmethod 2051 def get_fault_plane_sobel(test_ellipsoid): 2052 from scipy.ndimage import sobel 2053 from scipy.ndimage import maximum_filter 2054 2055 test_ellipsoid[test_ellipsoid <= 1.0] = 0.0 2056 inside = np.zeros_like(test_ellipsoid) 2057 inside[test_ellipsoid <= 1.0] = 1.0 2058 # method 2 2059 edge = ( 2060 np.abs(sobel(inside, axis=0)) 2061 + np.abs(sobel(inside, axis=1)) 2062 + np.abs(sobel(inside, axis=-1)) 2063 ) 2064 edge_max = maximum_filter(edge, size=(5, 5, 5)) 2065 edge_max[edge_max == 0.0] = 1e6 2066 fault_segments = edge / edge_max 2067 fault_segments[np.isnan(fault_segments)] = 0.0 2068 fault_segments[fault_segments < 0.5] = 0.0 2069 fault_segments[fault_segments > 0.5] = 1.0 2070 return fault_segments 2071 2072 def get_fault_centre(self, ellipsoid, wb_time_map, z_on_ellipse, index): 2073 def find_nearest(array, value): 2074 idx = (np.abs(array - value)).argmin() 2075 return array[idx] 2076 2077 def intersec(ell, thresh, x, y, z): 2078 abc = np.where( 2079 (ell[x, y, z] < np.float32(1 + thresh)) 2080 & (ell[x, y, z] > np.float32(1 - thresh)) 2081 ) 2082 if np.size(abc[0]) != 0: 2083 direction = find_nearest(abc[0], 1) 2084 else: 2085 direction = 9999 2086 print( 2087 " ... computing intersection points between ellipsoid and cube, raise error if none found" 2088 ) 2089 xdir_min = intersec(ell, thresh, x, y[0], z[0]) 2090 xdir_max = intersec(ell, thresh, x, y[-1], z[0]) 2091 ydir_min = intersec(ell, thresh, x[0], y, z[0]) 2092 ydir_max = intersec(ell, thresh, x[-1], y, z[0]) 2093 2094 print(" ... xdir_min coord = ", xdir_min, y[0], z[0]) 2095 print(" ... xdir_max coord = ", xdir_max, y[-1], z[0]) 2096 print(" ... ydir_min coord = ", y[0], ydir_min, z[0]) 2097 print(" ... ydir_max coord = ", y[-1], ydir_max, z[0]) 2098 return direction 2099 2100 def get_middle_z(ellipse, wb_map, idx, verbose=False): 2101 # Retrieve indices and cube of point on the ellipsoid and under the sea bed 2102 2103 random_idx = [] 2104 do_it = True 2105 origtime = np.array(range(ellipse.shape[-1])) 2106 wb_time_cube = np.reshape( 2107 wb_map, (wb_map.shape[0], wb_map.shape[1], 1) 2108 ) * np.ones_like(origtime) 2109 abc = np.where((z_on_ellipse == 1) & ((wb_time_cube - origtime) <= 0)) 2110 xyz = np.vstack(abc) 2111 if verbose: 2112 print(" ... xyz.shape = ", xyz.shape) 2113 if np.size(abc[0]) != 0: 2114 xyz_xyz = np.array([]) 2115 threshold_center = 5 2116 while xyz_xyz.size == 0: 2117 if threshold_center < z_on_ellipse.shape[0]: 2118 z_middle = np.where( 2119 np.abs(xyz[2] - int((xyz[2].min() + xyz[2].max()) / 2)) 2120 < threshold_center 2121 ) 2122 xyz_z = xyz[:, z_middle[0]].copy() 2123 if verbose: 2124 print(" ... xyz_z.shape = ", xyz_z.shape) 2125 if xyz_z.size != 0: 2126 x_middle = np.where( 2127 np.abs( 2128 xyz_z[0] 2129 - int((xyz_z[0].min() + xyz_z[0].max()) / 2) 2130 ) 2131 < threshold_center 2132 ) 2133 xyz_xz = xyz_z[:, x_middle[0]].copy() 2134 if verbose: 2135 print(" ... xyz_xz.shape = ", xyz_xz.shape) 2136 if xyz_xz.size != 0: 2137 y_middle = np.where( 2138 np.abs( 2139 xyz_xz[1] 2140 - int((xyz_xz[1].min() + xyz_xz[1].max()) / 2) 2141 ) 2142 < threshold_center 2143 ) 2144 xyz_xyz = xyz_xz[:, y_middle[0]].copy() 2145 if verbose: 2146 print(" ... xyz_xyz.shape = ", xyz_xyz.shape) 2147 if verbose: 2148 print(" ... z for upper intersection = ", xyz[2].min()) 2149 print(" ... z for lower intersection = ", xyz[2].max()) 2150 print(" ... threshold_center used = ", threshold_center) 2151 threshold_center += 5 2152 else: 2153 print(" ... Break the loop, could not find a suitable point") 2154 random_idx = [] 2155 do_it = False 2156 break 2157 if do_it: 2158 from scipy import random 2159 2160 random_idx = xyz_xyz[:, random.choice(xyz_xyz.shape[1])] 2161 print( 2162 " ... Computing fault middle to hang max displacement function" 2163 ) 2164 print(" ... x idx for max displacement = ", random_idx[0]) 2165 print(" ... y idx for max displacement = ", random_idx[1]) 2166 print(" ... z idx for max displacement = ", random_idx[2]) 2167 print( 2168 " ... ellipsoid value = ", 2169 ellipse[random_idx[0], random_idx[1], random_idx[2]], 2170 ) 2171 else: 2172 print( 2173 " ... Empty intersection between fault and cube, assign d-max at cube lower corner" 2174 ) 2175 2176 return random_idx 2177 2178 z_idx = get_middle_z(ellipsoid, wb_time_map, index) 2179 return z_idx 2180 2181 def xyz_dis(self, z_idx, throw, z_on_ellipse, ellipsoid, wb, index): 2182 from scipy import interpolate, signal 2183 from math import atan2, degrees 2184 from scipy.stats import multivariate_normal 2185 from scipy.ndimage.interpolation import rotate 2186 2187 cube_shape = self.vols.geologic_age.shape 2188 2189 def u_gaussian(d_max, sig, shape, points): 2190 from scipy.signal.windows import general_gaussian 2191 2192 return d_max * general_gaussian(points, shape, np.float32(sig)) 2193 2194 # Choose random values sigma, p and coef 2195 sigma = np.random.uniform(low=10 * throw - 50, high=300) 2196 p = np.random.uniform(low=1.5, high=5) 2197 coef = np.random.uniform(1.3, 1.5) 2198 2199 # Fault plane 2200 infill_factor = 0.5 2201 origtime = np.arange(cube_shape[-1]) 2202 z = np.arange(cube_shape[2]).astype("int") 2203 # Define Gaussian max throw and roll it to chosen z 2204 # Gaussian should be defined on at least 5 sigma on each side 2205 roll_int = int(10 * sigma) 2206 g = u_gaussian(throw, sigma, p, cube_shape[2] + 2 * roll_int) 2207 # Pad signal by 10*sigma before rolling 2208 g_padded_rolled = np.roll(g, np.int32(z_idx[2] + g.argmax() + roll_int)) 2209 count = 0 2210 wb_x = np.where(z_on_ellipse == 1)[0] 2211 wb_y = np.where(z_on_ellipse == 1)[1] 2212 print(" ... Taper fault so it doesn't reach seabed") 2213 print(f" ... Sea floor max = {wb[wb_x, wb_y].max()}") 2214 # Shift Z throw so that id doesn't cross sea bed 2215 while g_padded_rolled[int(roll_int + wb[wb_x, wb_y].max())] > 1: 2216 g_padded_rolled = np.roll(g_padded_rolled, 5) 2217 count += 5 2218 # Break loop if can't find spot 2219 if count > cube_shape[2] - wb[wb_x, wb_y].max(): 2220 print(" ... Too many rolled sample, seafloor will not have 0 throw") 2221 break 2222 print(f" ... Vertical throw shifted by {str(count)} samples") 2223 g_centered = g_padded_rolled[roll_int : cube_shape[2] + roll_int] 2224 2225 ff = interpolate.interp1d(z, g_centered) 2226 z_shift = ff 2227 print(" ... Computing Gaussian distribution function") 2228 print(f" ... Max displacement = {int(throw)}") 2229 print(f" ... Sigma = {int(sigma)}") 2230 print(f" ... P = {int(p)}") 2231 2232 low_fault_throw = 5 2233 high_fault_throw = 35 2234 # Parameters to set ratio of 1.4 seems to be optimal for a 1500x1500 grid 2235 mu_x = 0 2236 mu_y = 0 2237 2238 # Use throw to get length 2239 throw_range = np.arange(low_fault_throw, high_fault_throw, 1) 2240 # Max throw vs length relationship 2241 fault_length = np.power(0.0013 * throw_range, 1.3258) 2242 # Max throw == 16000 2243 scale_factor = 16000 / fault_length[-1] 2244 # Coef random selection moved to top of function 2245 variance_x = scale_factor * fault_length[np.where(throw_range == int(throw))] 2246 variance_y = variance_x * coef 2247 # Do the same for the drag zone area 2248 fault_length_drag = fault_length / 10000 2249 variance_x_drag = ( 2250 scale_factor * fault_length_drag[np.where(throw_range == int(throw))] 2251 ) 2252 variance_y_drag = variance_x_drag * coef 2253 # Rotation from z_idx to center 2254 alpha = atan2(z_idx[1] - cube_shape[1] / 2, z_idx[0] - cube_shape[0] / 2) 2255 print(f" ... Variance_x, Variance_y = {variance_x} {variance_y}") 2256 print( 2257 f" ... Angle between max displacement point tangent plane and cube = {int(degrees(alpha))} Degrees" 2258 ) 2259 print(f" ... Max displacement point at x,y,z = {z_idx}") 2260 2261 # Create grid and multivariate normal 2262 x = np.linspace( 2263 -int(cube_shape[0] + 1.5 * cube_shape[0]), 2264 int(cube_shape[0] + 1.5 * cube_shape[0]), 2265 2 * int(cube_shape[0] + 1.5 * cube_shape[0]), 2266 ) 2267 y = np.linspace( 2268 -int(cube_shape[1] + 1.5 * cube_shape[1]), 2269 int(cube_shape[1] + 1.5 * cube_shape[1]), 2270 2 * int(cube_shape[1] + 1.5 * cube_shape[1]), 2271 ) 2272 _x, _y = np.meshgrid(x, y) 2273 pos = np.empty(_x.shape + (2,)) 2274 pos[:, :, 0] = _x 2275 pos[:, :, 1] = _y 2276 rv = multivariate_normal([mu_x, mu_y], [[variance_x, 0], [0, variance_y]]) 2277 rv_drag = multivariate_normal( 2278 [mu_x, mu_y], [[variance_x_drag, 0], [0, variance_y_drag]] 2279 ) 2280 # Scale up by mu order of magnitude and swap axes 2281 xy_dis = 10000 * (rv.pdf(pos)) 2282 xy_dis_drag = 10000 * (rv_drag.pdf(pos)) 2283 2284 # Normalize 2285 xy_dis = xy_dis / np.amax(xy_dis.flatten()) 2286 xy_dis_drag = (xy_dis_drag / np.amax(xy_dis_drag.flatten())) * 0.99 2287 2288 # Rotate plane by alpha 2289 x = np.linspace(0, cube_shape[0], cube_shape[0]) 2290 y = np.linspace(0, cube_shape[1], cube_shape[1]) 2291 _x, _y = np.meshgrid(x, y) 2292 xy_dis_rotated = np.zeros_like(xy_dis) 2293 xy_dis_drag_rotated = np.zeros_like(xy_dis) 2294 rotate( 2295 xy_dis_drag, 2296 degrees(alpha), 2297 reshape=False, 2298 output=xy_dis_drag_rotated, 2299 mode="nearest", 2300 ) 2301 rotate( 2302 xy_dis, degrees(alpha), reshape=False, output=xy_dis_rotated, mode="nearest" 2303 ) 2304 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2305 xy_dis = xy_dis[ 2306 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2307 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2308 ].copy() 2309 xy_dis_drag = xy_dis_drag[ 2310 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2311 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2312 ].copy() 2313 2314 # taper edges of xy_dis_drag in 2d to avoid edge artifacts in fft 2315 print(" ... xy_dis_drag.shape = " + str(xy_dis_drag.shape)) 2316 print( 2317 " ... xy_dis_drag[xy_dis_drag>0.].size = " 2318 + str(xy_dis_drag[xy_dis_drag > 0.0].size) 2319 ) 2320 print( 2321 " ... xy_dis_drag.shape min/mean/max= " 2322 + str((xy_dis_drag.min(), xy_dis_drag.mean(), xy_dis_drag.max())) 2323 ) 2324 try: 2325 self.plot_counter += 1 2326 except: 2327 self.plot_counter = 0 2328 2329 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2330 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2331 2332 # Normalize 2333 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2334 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2335 # Normalize 2336 new_matrix = xy_dis_rotated 2337 xy_dis_norm = xy_dis 2338 x_center = np.where(new_matrix == new_matrix.max())[0][0] 2339 y_center = np.where(new_matrix == new_matrix.max())[1][0] 2340 2341 # Pad event and move it to maximum depth location 2342 x_pad = 0 2343 y_pad = 0 2344 x_roll = z_idx[0] - x_center 2345 y_roll = z_idx[1] - y_center 2346 print(f" ... padding x,y and roll x,y = {x_pad} {y_pad} {x_roll} {y_roll}") 2347 print( 2348 f" ... Max displacement point before rotation and adding of padding x,y = {x_center} {y_center}" 2349 ) 2350 new_matrix = np.lib.pad( 2351 new_matrix, ((abs(x_pad), abs(x_pad)), (abs(y_pad), abs(y_pad))), "edge" 2352 ) 2353 new_matrix = np.roll(new_matrix, int(x_roll), axis=0) 2354 new_matrix = np.roll(new_matrix, int(y_roll), axis=1) 2355 new_matrix = new_matrix[ 2356 abs(x_pad) : cube_shape[0] + abs(x_pad), 2357 abs(y_pad) : cube_shape[1] + abs(y_pad), 2358 ] 2359 # Check that fault center is at the right place 2360 x_center = np.where(new_matrix == new_matrix.max())[0] 2361 y_center = np.where(new_matrix == new_matrix.max())[1] 2362 print( 2363 f" ... Max displacement point after rotation and removal of padding x,y = {x_center} {y_center}" 2364 ) 2365 print(f" ... z_idx = {z_idx[0]}, {z_idx[1]}") 2366 print( 2367 f" ... Difference from origin z_idx = {x_center - z_idx[0]}, {y_center - z_idx[1]}" 2368 ) 2369 2370 # Build cube of lateral variable displacement 2371 xy_dis = new_matrix.reshape(cube_shape[0], cube_shape[1], 1) 2372 # Get enlarged fault plane 2373 bb = np.zeros_like(ellipsoid) 2374 for j in range(bb.shape[-1]): 2375 bb[:, :, j] = j 2376 stretch_times_effects_drag = bb - xy_dis * z_shift(range(cube_shape[2])) 2377 fault_plane_classification = np.where( 2378 (z_on_ellipse == 1) 2379 & ((origtime - stretch_times_effects_drag) > infill_factor), 2380 1, 2381 0, 2382 ) 2383 hockey_stick = 0 2384 # Define as 0's, to be updated if necessary 2385 fault_plane_classification_drag = np.zeros_like(z_on_ellipse) 2386 if throw >= 0.85 * high_fault_throw: 2387 hockey_stick = 1 2388 # Check for non zero fault_plane_classification, to avoid division by 0 2389 if np.count_nonzero(fault_plane_classification) > 0: 2390 # Generate Hockey sticks by convolving small xy displacement with fault segment 2391 fault_plane_classification_drag = signal.fftconvolve( 2392 fault_plane_classification, 2393 np.reshape(xy_dis_drag, (cube_shape[0], cube_shape[1], 1)), 2394 mode="same", 2395 ) 2396 fault_plane_classification_drag = ( 2397 fault_plane_classification_drag 2398 / np.amax(fault_plane_classification_drag.flatten()) 2399 ) 2400 2401 xy_dis = xy_dis * np.ones_like(self.vols.geologic_age) 2402 xy_dis_classification = xy_dis.copy() 2403 interpolation = True 2404 2405 xy_dis = xy_dis - fault_plane_classification_drag 2406 xy_dis = np.where(xy_dis < 0, 0, xy_dis) 2407 stretch_times = xy_dis * z_shift(range(cube_shape[2])) 2408 stretch_times_classification = xy_dis_classification * z_shift( 2409 range(cube_shape[2]) 2410 ) 2411 2412 if self.cfg.qc_plots: 2413 self.fault_summary_plot( 2414 ff, 2415 z, 2416 throw, 2417 sigma, 2418 _x, 2419 _y, 2420 xy_dis_norm, 2421 ellipsoid, 2422 z_idx, 2423 xy_dis, 2424 index, 2425 alpha, 2426 ) 2427 2428 return stretch_times, stretch_times_classification, interpolation, hockey_stick 2429 2430 def fault_summary_plot( 2431 self, 2432 ff, 2433 z, 2434 throw, 2435 sigma, 2436 x, 2437 y, 2438 xy_dis_norm, 2439 ellipsoid, 2440 z_idx, 2441 xy_dis, 2442 index, 2443 alpha, 2444 ) -> None: 2445 """ 2446 Fault Summary Plot 2447 ------------------ 2448 2449 Generates a fault summary plot. 2450 2451 Parameters 2452 ---------- 2453 ff : _type_ 2454 _description_ 2455 z : _type_ 2456 _description_ 2457 throw : _type_ 2458 _description_ 2459 sigma : _type_ 2460 _description_ 2461 x : _type_ 2462 _description_ 2463 y : _type_ 2464 _description_ 2465 xy_dis_norm : _type_ 2466 _description_ 2467 ellipsoid : _type_ 2468 _description_ 2469 z_idx : _type_ 2470 _description_ 2471 xy_dis : _type_ 2472 _description_ 2473 index : _type_ 2474 _description_ 2475 alpha : _type_ 2476 _description_ 2477 2478 Returns 2479 ------- 2480 None 2481 """ 2482 import os 2483 from math import degrees 2484 from datagenerator.util import import_matplotlib 2485 2486 plt = import_matplotlib() 2487 # Import axes3d, required to create plot with projection='3d' below. DO NOT REMOVE! 2488 from mpl_toolkits.mplot3d import axes3d 2489 from mpl_toolkits.axes_grid1 import make_axes_locatable 2490 2491 # Make summary picture 2492 fig, axes = plt.subplots(nrows=2, ncols=2) 2493 ax0, ax1, ax2, ax3 = axes.flatten() 2494 fig.set_size_inches(10, 8) 2495 # PLot Z displacement 2496 ax0.plot(ff(z)) 2497 ax0.set_title("Fault with throw %s and sigma %s" % (throw, sigma)) 2498 ax0.set_xlabel("Z axis") 2499 ax0.set_ylabel("Throw") 2500 # Plot 3D XY displacement 2501 ax1.axis("off") 2502 ax1 = fig.add_subplot(222, projection="3d") 2503 ax1.plot_surface(x, y, xy_dis_norm, cmap="Spectral", linewidth=0) 2504 ax1.set_xlabel("X axis") 2505 ax1.set_ylabel("Y axis") 2506 ax1.set_zlabel("Throw fraction") 2507 ax1.set_title("3D XY displacement") 2508 # Ellipsoid location 2509 weights = ellipsoid[:, :, z_idx[2]] 2510 # Plot un-rotated XY displacement 2511 cax2 = ax2.imshow(np.rot90(xy_dis_norm, 3)) 2512 # Levels for imshow contour 2513 levels = np.arange(0, 1.1, 0.1) 2514 # Plot contour 2515 ax2.contour(np.rot90(xy_dis_norm, 3), levels, colors="k", linestyles="-") 2516 divider = make_axes_locatable(ax2) 2517 cax4 = divider.append_axes("right", size="5%", pad=0.05) 2518 cbar2 = fig.colorbar(cax2, cax=cax4) 2519 ax2.set_xlabel("X axis") 2520 ax2.set_ylabel("Y axis") 2521 ax2.set_title("2D projection of XY displacement unrotated") 2522 ############################################################ 2523 # Plot rotated displacement and contour 2524 cax3 = ax3.imshow(np.rot90(xy_dis[:, :, z_idx[2]], 3)) 2525 ax3.contour( 2526 np.rot90(xy_dis[:, :, z_idx[2]], 3), levels, colors="k", linestyles="-" 2527 ) 2528 # Add ellipsoid shape 2529 ax3.contour(np.rot90(weights, 3), levels=[1], colors="r", linestyles="-") 2530 divider = make_axes_locatable(ax3) 2531 cax5 = divider.append_axes("right", size="5%", pad=0.05) 2532 cbar3 = fig.colorbar(cax3, cax=cax5) 2533 ax3.set_xlabel("X axis") 2534 ax3.set_ylabel("Y axis") 2535 ax3.set_title( 2536 "2D projection of XY displacement rotated by %s degrees" 2537 % (int(degrees(alpha))) 2538 ) 2539 plt.suptitle( 2540 "XYZ displacement parameters for fault Nr %s" % str(index), 2541 fontweight="bold", 2542 ) 2543 fig.tight_layout() 2544 fig.subplots_adjust(top=0.94) 2545 image_path = os.path.join( 2546 self.cfg.work_subfolder, "QC_plot__Fault_%s.png" % str(index) 2547 ) 2548 plt.savefig(image_path, format="png") 2549 plt.close(fig) 2550 2551 @staticmethod 2552 def create_binary_segmentations_post_faulting(cube, segmentation_threshold): 2553 cube[cube >= segmentation_threshold] = 1.0 2554 return cube 2555 2556 def reassign_channel_segment_encoding( 2557 self, 2558 faulted_age, 2559 floodplain_shale, 2560 channel_fill, 2561 shale_channel_drape, 2562 levee, 2563 crevasse, 2564 channel_flag_lut, 2565 ): 2566 # Generate list of horizons with channel episodes 2567 channel_segments = np.zeros_like(floodplain_shale) 2568 channel_horizons_list = list() 2569 for i in range(self.faulted_depth_maps.shape[2] - 7): 2570 if channel_flag_lut[i] == 1: 2571 channel_horizons_list.append(i) 2572 channel_horizons_list.append(999) 2573 2574 # Re-assign correct channel segments encoding 2575 for i, iLayer in enumerate(channel_horizons_list[:-1]): 2576 if iLayer != 0 and iLayer < self.faulted_depth_maps.shape[-1]: 2577 # loop through channel facies 2578 # --- 0 is floodplain shale 2579 # --- 1 is channel fill (sand) 2580 # --- 2 is shale channel drape 2581 # --- 3 is levee (mid quality sand) 2582 # --- 4 is crevasse (low quality sand) 2583 for j in range(1000, 4001, 1000): 2584 print( 2585 " ... re-assign channel segments after faulting: i, iLayer, j = ", 2586 i, 2587 iLayer, 2588 j, 2589 ) 2590 channel_facies_code = j + iLayer 2591 layers_mask = np.where( 2592 ( 2593 (faulted_age >= iLayer) 2594 & (faulted_age < channel_horizons_list[i + 1]) 2595 ), 2596 1, 2597 0, 2598 ) 2599 if j == 1000: 2600 channel_segments[ 2601 np.logical_and(layers_mask == 1, channel_fill == 1) 2602 ] = channel_facies_code 2603 faulted_age[ 2604 np.logical_and(layers_mask == 1, channel_fill == 1) 2605 ] = channel_facies_code 2606 elif j == 2000: 2607 channel_segments[ 2608 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2609 ] = channel_facies_code 2610 faulted_age[ 2611 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2612 ] = channel_facies_code 2613 elif j == 3000: 2614 channel_segments[ 2615 np.logical_and(layers_mask == 1, levee == 1) 2616 ] = channel_facies_code 2617 faulted_age[ 2618 np.logical_and(layers_mask == 1, levee == 1) 2619 ] = channel_facies_code 2620 elif j == 4000: 2621 channel_segments[ 2622 np.logical_and(layers_mask == 1, crevasse == 1) 2623 ] = channel_facies_code 2624 faulted_age[ 2625 np.logical_and(layers_mask == 1, crevasse == 1) 2626 ] = channel_facies_code 2627 print(" ... finished Re-assign correct channel segments") 2628 return channel_segments, faulted_age 2629 2630 def _get_fault_mode(self): 2631 if self.cfg.mode == 0: 2632 return self._fault_params_random 2633 elif self.cfg.mode == 1.0: 2634 if self.cfg.clustering == 0: 2635 return self._fault_params_self_branching 2636 elif self.cfg.clustering == 1: 2637 return self._fault_params_stairs 2638 elif self.cfg.clustering == 2: 2639 return self._fault_params_relay_ramps 2640 else: 2641 raise ValueError(self.cfg.clustering) 2642 2643 elif self.cfg.mode == 2.0: 2644 return self._fault_params_horst_graben 2645 else: 2646 raise ValueError(self.cfg.mode) 2647 2648 def _fault_params_random(self): 2649 print(f" ... {self.cfg.number_faults} faults will be inserted randomly") 2650 2651 # Fault origin 2652 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2653 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2654 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2655 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2656 2657 # Principal semi-axes location of fault ellipsoid 2658 a = np.random.uniform(100, 600, self.cfg.number_faults) ** 2 2659 b = np.random.uniform(100, 600, self.cfg.number_faults) ** 2 2660 x0 = np.random.uniform( 2661 x0_min - np.sqrt(a), np.sqrt(a) + x0_max, self.cfg.number_faults 2662 ) 2663 y0 = np.random.uniform( 2664 y0_min - np.sqrt(b), np.sqrt(b) + y0_max, self.cfg.number_faults 2665 ) 2666 z0 = np.random.uniform( 2667 -self.cfg.cube_shape[2] * 2.0, 2668 -self.cfg.cube_shape[2] * 6.0, 2669 self.cfg.number_faults, 2670 ) 2671 _c0 = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0 2672 _c1 = _c0 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2673 c = np.random.uniform(_c0, _c1, self.cfg.number_faults) ** 2 2674 tilt_pct = np.random.uniform(0.1, 0.75, self.cfg.number_faults) 2675 throw_lut = np.random.uniform( 2676 low=self.cfg.low_fault_throw, 2677 high=self.cfg.high_fault_throw, 2678 size=self.cfg.number_faults, 2679 ) 2680 2681 fault_param_dict = { 2682 "a": a, 2683 "b": b, 2684 "c": c, 2685 "x0": x0, 2686 "y0": y0, 2687 "z0": z0, 2688 "tilt_pct": tilt_pct, 2689 "throw": throw_lut, 2690 } 2691 2692 return fault_param_dict 2693 2694 def _fault_params_self_branching(self): 2695 print( 2696 f" ... {self.cfg.number_faults} faults will be inserted in clustered mode with" 2697 ) 2698 print(" ... Self branching") 2699 x0_min = 0 2700 x0_max = int(self.cfg.cube_shape[0]) 2701 y0_min = 0 2702 y0_max = int(self.cfg.cube_shape[1]) 2703 # Self Branching mode means that faults are inserted side by side with no separation. 2704 number_of_branches = int(self.cfg.number_faults / 3) + int( 2705 self.cfg.number_faults % 3 > 0 2706 ) 2707 2708 for i in range(number_of_branches): 2709 # Initialize first fault center, offset center between each branch 2710 print(" ... Computing branch number ", i) 2711 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2712 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2713 if a_ini > b_ini: 2714 while np.sqrt(b_ini) < self.cfg.cube_shape[1]: 2715 print(" ... Recomputing b_ini for better branching") 2716 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2717 x0_ini = np.array(np.random.uniform(x0_min, x0_max)) 2718 range_1 = list(range(int(y0_min - np.sqrt(b_ini)), 0)) 2719 range_2 = list(range(int(self.cfg.cube_shape[1]))) 2720 y0_ini = np.array( 2721 np.random.choice(range_1 + range_2, int(np.sqrt(b_ini) + y0_max)) 2722 ) 2723 side = "x" 2724 else: 2725 while np.sqrt(a_ini) < self.cfg.cube_shape[0]: 2726 print(" ... Recomputing a_ini for better branching") 2727 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2728 range_1 = list(range(int(x0_min - np.sqrt(a_ini)), 0)) 2729 range_2 = list(range(int(self.cfg.cube_shape[0]))) 2730 x0_ini = np.array( 2731 np.array( 2732 np.random.choice( 2733 range_1 + range_2, int(np.sqrt(a_ini) + x0_max) 2734 ) 2735 ) 2736 ) 2737 y0_ini = np.array(np.random.uniform(y0_min, y0_max)) 2738 side = "y" 2739 # Compute the rest of the initial parameters normally 2740 z0_ini = np.array( 2741 np.random.uniform( 2742 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2743 ) 2744 ) 2745 2746 _c0_ini = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini 2747 _c1_ini = _c0_ini + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2748 c_ini = np.array(np.random.uniform(_c0_ini, _c1_ini) ** 2) 2749 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2750 # direction = np.random.choice([-1, 1]) 2751 throw_lut_ini = np.array( 2752 np.random.uniform( 2753 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 2754 ) 2755 ) 2756 if i == 0: 2757 # Initialize return parameters 2758 a = a_ini.copy() 2759 b = b_ini.copy() 2760 c = c_ini.copy() 2761 x0 = x0_ini.copy() 2762 y0 = y0_ini.copy() 2763 z0 = z0_ini.copy() 2764 tilt_pct = tilt_pct_ini.copy() 2765 throw_lut = throw_lut_ini.copy() 2766 else: 2767 a = np.append(a, a_ini) 2768 b = np.append(b, b_ini) 2769 c = np.append(c, c_ini) 2770 x0 = np.append(x0, x0_ini) 2771 y0 = np.append(y0, y0_ini) 2772 z0 = np.append(z0, z0_ini) 2773 tilt_pct = np.append(tilt_pct, tilt_pct_ini) 2774 throw_lut = np.append(throw_lut, throw_lut_ini) 2775 # Construct fault in branch 2776 if i < number_of_branches - 1 or self.cfg.number_faults % 3 == 0: 2777 fault_in_branch = 2 2778 else: 2779 fault_in_branch = self.cfg.number_faults % 3 2780 print(" ... Branch along axis ", side) 2781 2782 direction = 1 2783 if side == "x": 2784 if np.all(y0_ini > np.abs(self.cfg.cube_shape[1] - y0_ini)): 2785 print(" ... Building from right to left") 2786 direction = -1 2787 else: 2788 print(" ... Building from left to right") 2789 else: 2790 if np.all(x0_ini > np.abs(self.cfg.cube_shape[0] - x0_ini)): 2791 print(" ... Building from left to right") 2792 direction = -1 2793 else: 2794 print(" ... Building from right to left") 2795 move = 1 2796 for j in range( 2797 i * number_of_branches + 1, fault_in_branch + i * number_of_branches + 1 2798 ): 2799 print(" ... Computing fault number ", j) 2800 # Allow 20% deviation from initial fault parameter 2801 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 2802 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 2803 c_ramp = c_ini.copy() 2804 if side == "x": 2805 x0_ramp = x0_ini + direction * move * int( 2806 self.cfg.cube_shape[0] / 3.0 2807 ) 2808 y0_ramp = np.random.uniform(0.9, 1.1) * y0_ini 2809 else: 2810 y0_ramp = y0_ini + direction * move * int( 2811 self.cfg.cube_shape[0] / 3.0 2812 ) 2813 x0_ramp = np.random.uniform(0.9, 1.1) * x0_ini 2814 z0_ramp = z0_ini.copy() 2815 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 2816 # Add to existing 2817 throw_lut_ramp = np.random.uniform( 2818 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 2819 ) 2820 throw_lut = np.append(throw_lut, throw_lut_ramp) 2821 a = np.append(a, a_ramp) 2822 b = np.append(b, b_ramp) 2823 c = np.append(c, c_ramp) 2824 x0 = np.append(x0, x0_ramp) 2825 y0 = np.append(y0, y0_ramp) 2826 z0 = np.append(z0, z0_ramp) 2827 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 2828 move += 1 2829 2830 fault_param_dict = { 2831 "a": a, 2832 "b": b, 2833 "c": c, 2834 "x0": x0, 2835 "y0": y0, 2836 "z0": z0, 2837 "tilt_pct": tilt_pct, 2838 "throw": throw_lut, 2839 } 2840 return fault_param_dict 2841 2842 def _fault_params_stairs(self): 2843 print(" ... Stairs like feature") 2844 # Initialize value for first fault 2845 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2846 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2847 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2848 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2849 2850 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2851 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2852 x0_ini = np.array( 2853 np.random.uniform(x0_min - np.sqrt(a_ini), np.sqrt(a_ini) + x0_max) 2854 ) 2855 y0_ini = np.array( 2856 np.random.uniform(y0_min - np.sqrt(b_ini), np.sqrt(b_ini) + y0_max) 2857 ) 2858 z0_ini = np.array( 2859 np.random.uniform( 2860 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2861 ) 2862 ) 2863 2864 _c0_ini = self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini 2865 _c1_ini = _c0_ini + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0 2866 c_ini = np.array(np.random.uniform(_c0_ini, _c1_ini) ** 2) 2867 2868 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2869 throw_lut = np.random.uniform( 2870 self.cfg.low_fault_throw, self.cfg.high_fault_throw, self.cfg.number_faults 2871 ) 2872 direction = np.random.choice([-1, 1]) 2873 separation_x = np.random.randint(1, 4) 2874 separation_y = np.random.randint(1, 4) 2875 # Initialize return parameters 2876 a = a_ini.copy() 2877 b = b_ini.copy() 2878 c = c_ini.copy() 2879 x0 = x0_ini.copy() 2880 y0 = y0_ini.copy() 2881 z0 = z0_ini.copy() 2882 x0_prec = x0_ini 2883 y0_prec = y0_ini 2884 tilt_pct = tilt_pct_ini.copy() 2885 for i in range(self.cfg.number_faults - 1): 2886 a_ramp = a_ini.copy() * np.random.uniform(0.8, 1.2) 2887 b_ramp = b_ini.copy() * np.random.uniform(0.8, 1.2) 2888 c_ramp = c_ini.copy() * np.random.uniform(0.8, 1.2) 2889 direction = np.random.choice([-1, 1]) 2890 x0_ramp = ( 2891 x0_prec + separation_x * direction * x0_ini / self.cfg.number_faults 2892 ) 2893 y0_ramp = ( 2894 y0_prec + separation_y * direction * y0_ini / self.cfg.number_faults 2895 ) 2896 z0_ramp = z0_ini.copy() 2897 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 2898 x0_prec = x0_ramp 2899 y0_prec = y0_ramp 2900 # Add to existing 2901 a = np.append(a, a_ramp) 2902 b = np.append(b, b_ramp) 2903 c = np.append(c, c_ramp) 2904 x0 = np.append(x0, x0_ramp) 2905 y0 = np.append(y0, y0_ramp) 2906 z0 = np.append(z0, z0_ramp) 2907 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 2908 2909 fault_param_dict = { 2910 "a": a, 2911 "b": b, 2912 "c": c, 2913 "x0": x0, 2914 "y0": y0, 2915 "z0": z0, 2916 "tilt_pct": tilt_pct, 2917 "throw": throw_lut, 2918 } 2919 return fault_param_dict 2920 2921 def _fault_params_relay_ramps(self): 2922 print(" ... relay ramps") 2923 # Initialize value for first fault 2924 # Maximum of 3 fault per ramp 2925 x0_min = int(self.cfg.cube_shape[0] / 4.0) 2926 x0_max = int(self.cfg.cube_shape[0] / 2.0) 2927 y0_min = int(self.cfg.cube_shape[1] / 4.0) 2928 y0_max = int(self.cfg.cube_shape[1] / 2.0) 2929 number_of_branches = int(self.cfg.number_faults / 3) + int( 2930 self.cfg.number_faults % 3 > 0 2931 ) 2932 2933 for i in range(number_of_branches): 2934 # Initialize first fault center, offset center between each branch 2935 print(" ... Computing branch number ", i) 2936 b_ini = np.array(np.random.uniform(100, 600) ** 2) 2937 a_ini = np.array(np.random.uniform(100, 600) ** 2) 2938 x0_ini = np.array( 2939 np.random.uniform(x0_min - np.sqrt(a_ini) / 2, np.sqrt(a_ini) + x0_max) 2940 / 2 2941 ) 2942 y0_ini = np.array( 2943 np.random.uniform(y0_min - np.sqrt(b_ini) / 2, np.sqrt(b_ini) + y0_max) 2944 / 2 2945 ) 2946 # Compute the rest of the initial parameters normally 2947 z0_ini = np.array( 2948 np.random.uniform( 2949 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 2950 ) 2951 ) 2952 c_ini = np.array( 2953 np.random.uniform( 2954 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini, 2955 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 2956 - z0_ini 2957 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 4.0, 2958 ) 2959 ** 2 2960 ) 2961 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 2962 throw_lut = np.random.uniform( 2963 self.cfg.low_fault_throw, 2964 self.cfg.high_fault_throw, 2965 self.cfg.number_faults, 2966 ) 2967 # Initialize return parameters 2968 if i == 0: 2969 a = a_ini.copy() 2970 b = b_ini.copy() 2971 c = c_ini.copy() 2972 x0 = x0_ini.copy() 2973 y0 = y0_ini.copy() 2974 z0 = z0_ini.copy() 2975 tilt_pct = tilt_pct_ini.copy() 2976 else: 2977 a = np.append(a, a_ini) 2978 b = np.append(b, b_ini) 2979 c = np.append(c, c_ini) 2980 x0 = np.append(x0, x0_ini) 2981 y0 = np.append(y0, y0_ini) 2982 z0 = np.append(z0, z0_ini) 2983 tilt_pct = np.append(tilt_pct, tilt_pct_ini) 2984 # Construct fault in branch 2985 if i < number_of_branches - 1 or self.cfg.number_faults % 3 == 0: 2986 fault_in_branche = 2 2987 else: 2988 fault_in_branche = self.cfg.number_faults % 3 2989 2990 direction = np.random.choice([-1, 1]) 2991 move = 1 2992 x0_prec = x0_ini 2993 y0_prec = y0_ini 2994 for j in range( 2995 i * number_of_branches + 1, 2996 fault_in_branche + i * number_of_branches + 1, 2997 ): 2998 print(" ... Computing fault number ", j) 2999 # Allow 20% deviation from initial fault parameter 3000 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3001 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3002 c_ramp = c_ini.copy() 3003 direction = np.random.choice([-1, 1]) 3004 x0_ramp = x0_prec * np.random.uniform(0.8, 1.2) + direction * x0_ini / ( 3005 fault_in_branche 3006 ) 3007 y0_ramp = y0_prec * np.random.uniform(0.8, 1.2) + direction * y0_ini / ( 3008 fault_in_branche 3009 ) 3010 z0_ramp = z0_ini.copy() 3011 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3012 # Add to existing 3013 throw_LUT_ramp = np.random.uniform( 3014 low=self.cfg.low_fault_throw, high=self.cfg.high_fault_throw 3015 ) 3016 throw_lut = np.append(throw_lut, throw_LUT_ramp) 3017 a = np.append(a, a_ramp) 3018 b = np.append(b, b_ramp) 3019 c = np.append(c, c_ramp) 3020 x0 = np.append(x0, x0_ramp) 3021 y0 = np.append(y0, y0_ramp) 3022 z0 = np.append(z0, z0_ramp) 3023 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3024 x0_prec = x0_ramp 3025 y0_prec = y0_ramp 3026 move += 1 3027 3028 fault_param_dict = { 3029 "a": a, 3030 "b": b, 3031 "c": c, 3032 "x0": x0, 3033 "y0": y0, 3034 "z0": z0, 3035 "tilt_pct": tilt_pct, 3036 "throw": throw_lut, 3037 } 3038 return fault_param_dict 3039 3040 def _fault_params_horst_graben(self): 3041 print( 3042 " ... %s faults will be inserted as Horst and Graben" 3043 % (self.cfg.number_faults) 3044 ) 3045 # Initialize value for first fault 3046 x0_min = int(self.cfg.cube_shape[0] / 2.0) 3047 x0_max = int(self.cfg.cube_shape[0]) 3048 y0_min = int(self.cfg.cube_shape[1] / 2.0) 3049 y0_max = int(self.cfg.cube_shape[1]) 3050 3051 b_ini = np.array(np.random.uniform(100, 600) ** 2) 3052 a_ini = np.array(np.random.uniform(100, 600) ** 2) 3053 if a_ini > b_ini: 3054 side = "x" 3055 while np.sqrt(b_ini) < self.cfg.cube_shape[1]: 3056 print(" ... Recomputing b_ini for better branching") 3057 b_ini = np.array(np.random.uniform(100, 600) ** 2) 3058 x0_ini = np.array(np.random.uniform(0, self.cfg.cube_shape[0])) 3059 # Compute so that first is near center 3060 range_1 = list( 3061 range(int(y0_min - np.sqrt(b_ini)), int(y0_max - np.sqrt(b_ini))) 3062 ) 3063 range_2 = list( 3064 range(int(y0_min + np.sqrt(b_ini)), int(y0_max + np.sqrt(b_ini))) 3065 ) 3066 y0_ini = np.array(np.random.choice(range_1 + range_2)) 3067 else: 3068 side = "y" 3069 while np.sqrt(a_ini) < self.cfg.cube_shape[0]: 3070 print(" ... Recomputing a_ini for better branching") 3071 a_ini = np.array(np.random.uniform(100, 600) ** 2) 3072 # compute so that first is near center 3073 range_1 = list( 3074 range(int(x0_min - np.sqrt(a_ini)), int(x0_max - np.sqrt(a_ini))) 3075 ) 3076 range_2 = list( 3077 range(int(x0_min + np.sqrt(a_ini)), int(x0_max + np.sqrt(a_ini))) 3078 ) 3079 x0_ini = np.array(np.random.choice(range_1 + range_2)) 3080 y0_ini = np.array(np.random.uniform(0, self.cfg.cube_shape[1])) 3081 z0_ini = np.array( 3082 np.random.uniform( 3083 -self.cfg.cube_shape[2] * 2.0, -self.cfg.cube_shape[2] * 6.0 3084 ) 3085 ) 3086 c_ini = np.array( 3087 np.random.uniform( 3088 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 - z0_ini, 3089 self.cfg.cube_shape[2] * self.cfg.infill_factor * 4.0 3090 - z0_ini 3091 + self.cfg.cube_shape[2] * self.cfg.infill_factor / 2.0, 3092 ) 3093 ** 2 3094 ) 3095 tilt_pct_ini = np.array(np.random.uniform(0.1, 0.75)) 3096 throw_lut = np.random.uniform( 3097 self.cfg.low_fault_throw, self.cfg.high_fault_throw, self.cfg.number_faults 3098 ) 3099 # Initialize return parameters 3100 a = a_ini.copy() 3101 b = b_ini.copy() 3102 c = c_ini.copy() 3103 x0 = x0_ini.copy() 3104 y0 = y0_ini.copy() 3105 z0 = z0_ini.copy() 3106 3107 tilt_pct = tilt_pct_ini.copy() 3108 direction = "odd" 3109 mod = ["new"] 3110 3111 x0_even = x0_ini 3112 y0_even = y0_ini 3113 if side == "x": 3114 # X only moves marginally 3115 y0_odd = int(self.cfg.cube_shape[1]) - y0_even 3116 x0_odd = x0_even 3117 direction_sign = np.sign(y0_ini) 3118 direction_sidex = 0 3119 direction_sidey = 1 3120 else: 3121 # Y only moves marginally 3122 x0_odd = int(self.cfg.cube_shape[0]) - x0_even 3123 y0_odd = y0_even 3124 direction_sign = np.sign(x0_ini) 3125 direction_sidex = 1 3126 direction_sidey = 0 3127 3128 for i in range(self.cfg.number_faults - 1): 3129 if direction == "odd": 3130 # Put the next point as a mirror shifted by maximal shift and go backward 3131 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3132 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3133 c_ramp = c_ini.copy() 3134 x0_ramp = ( 3135 -1 3136 * direction_sign 3137 * direction_sidex 3138 * int(self.cfg.cube_shape[0]) 3139 * (i - 1) 3140 / self.cfg.number_faults 3141 ) + x0_odd * np.random.uniform(0.8, 1.2) 3142 y0_ramp = ( 3143 -1 3144 * direction_sign 3145 * direction_sidey 3146 * int(self.cfg.cube_shape[0]) 3147 * (i - 1) 3148 / self.cfg.number_faults 3149 ) + y0_odd * np.random.uniform(0.8, 1.2) 3150 z0_ramp = z0_ini.copy() 3151 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3152 x0_prec = x0_ramp 3153 y0_prec = y0_ramp 3154 # Add to existing 3155 a = np.append(a, a_ramp) 3156 b = np.append(b, b_ramp) 3157 c = np.append(c, c_ramp) 3158 x0 = np.append(x0, x0_ramp) 3159 y0 = np.append(y0, y0_ramp) 3160 z0 = np.append(z0, z0_ramp) 3161 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3162 direction = "even" 3163 mod.append("old") 3164 elif direction == "even": 3165 # Put next to ini 3166 a_ramp = np.random.uniform(0.8, 1.2) * a_ini.copy() 3167 b_ramp = np.random.uniform(0.8, 1.2) * b_ini.copy() 3168 c_ramp = c_ini.copy() 3169 x0_ramp = direction_sign * direction_sidex * int( 3170 self.cfg.cube_shape[0] 3171 ) * (i - 1) / self.cfg.number_faults + x0_even * np.random.uniform( 3172 0.8, 1.2 3173 ) 3174 y0_ramp = direction_sign * direction_sidey * int( 3175 self.cfg.cube_shape[0] 3176 ) * (i - 1) / self.cfg.number_faults + y0_even * np.random.uniform( 3177 0.8, 1.2 3178 ) 3179 z0_ramp = z0_ini.copy() 3180 tilt_pct_ramp = tilt_pct_ini * np.random.uniform(0.85, 1.15) 3181 # Add to existing 3182 a = np.append(a, a_ramp) 3183 b = np.append(b, b_ramp) 3184 c = np.append(c, c_ramp) 3185 x0 = np.append(x0, x0_ramp) 3186 y0 = np.append(y0, y0_ramp) 3187 z0 = np.append(z0, z0_ramp) 3188 tilt_pct = np.append(tilt_pct, tilt_pct_ramp) 3189 direction = "odd" 3190 mod.append("new") 3191 3192 fault_param_dict = { 3193 "a": a, 3194 "b": b, 3195 "c": c, 3196 "x0": x0, 3197 "y0": y0, 3198 "z0": z0, 3199 "tilt_pct": tilt_pct, 3200 "throw": throw_lut, 3201 } 3202 return fault_param_dict
Faults Class
Describes the class for faulting the model.
Parameters
- Horizons (datagenerator.Horizons): The Horizons class used to build the faults.
- Geomodel (data_generator.Geomodels): The Geomodel class used to build the faults.
Returns
- None
30 def __init__( 31 self, 32 parameters: Parameters, 33 unfaulted_depth_maps: np.ndarray, 34 onlap_horizon_list: np.ndarray, 35 geomodels: Geomodel, 36 fan_horizon_list: np.ndarray, 37 fan_thickness: np.ndarray 38 ): 39 """__init__ 40 41 Initializes the Faults class.. 42 43 Parameters 44 ---------- 45 parameters : Parameters 46 The parameters class. 47 unfaulted_depth_maps : np.ndarray 48 The depth maps to be faulted. 49 onlap_horizon_list : np.ndarray 50 The onlap horizon list. 51 geomodels : Geomodel 52 The geomodels class. 53 fan_horizon_list : np.ndarray 54 The fan horizon list. 55 fan_thickness : np.ndarray 56 The fan thickness list. 57 """ 58 self.cfg = parameters 59 # Horizons 60 self.unfaulted_depth_maps = unfaulted_depth_maps 61 self.onlap_horizon_list = onlap_horizon_list 62 self.fan_horizon_list = fan_horizon_list 63 self.fan_thickness = fan_thickness 64 self.faulted_depth_maps = self.cfg.hdf_init( 65 "faulted_depth_maps", shape=unfaulted_depth_maps.shape 66 ) 67 self.faulted_depth_maps_gaps = self.cfg.hdf_init( 68 "faulted_depth_maps_gaps", shape=unfaulted_depth_maps.shape 69 ) 70 # Volumes 71 cube_shape = geomodels.geologic_age[:].shape 72 self.vols = geomodels 73 self.faulted_age_volume = self.cfg.hdf_init( 74 "faulted_age_volume", shape=cube_shape 75 ) 76 self.faulted_net_to_gross = self.cfg.hdf_init( 77 "faulted_net_to_gross", shape=cube_shape 78 ) 79 self.faulted_lithology = self.cfg.hdf_init( 80 "faulted_lithology", shape=cube_shape 81 ) 82 self.reservoir = self.cfg.hdf_init("reservoir", shape=cube_shape) 83 self.faulted_depth = self.cfg.hdf_init("faulted_depth", shape=cube_shape) 84 self.faulted_onlap_segments = self.cfg.hdf_init( 85 "faulted_onlap_segments", shape=cube_shape 86 ) 87 self.fault_planes = self.cfg.hdf_init("fault_planes", shape=cube_shape) 88 self.displacement_vectors = self.cfg.hdf_init( 89 "displacement_vectors", shape=cube_shape 90 ) 91 self.sum_map_displacements = self.cfg.hdf_init( 92 "sum_map_displacements", shape=cube_shape 93 ) 94 self.fault_intersections = self.cfg.hdf_init( 95 "fault_intersections", shape=cube_shape 96 ) 97 self.fault_plane_throw = self.cfg.hdf_init( 98 "fault_plane_throw", shape=cube_shape 99 ) 100 self.max_fault_throw = self.cfg.hdf_init("max_fault_throw", shape=cube_shape) 101 self.fault_plane_azimuth = self.cfg.hdf_init( 102 "fault_plane_azimuth", shape=cube_shape 103 ) 104 # Salt 105 self.salt_model = None
__init__
Initializes the Faults class..
Parameters
- parameters (Parameters): The parameters class.
- unfaulted_depth_maps (np.ndarray): The depth maps to be faulted.
- onlap_horizon_list (np.ndarray): The onlap horizon list.
- geomodels (Geomodel): The geomodels class.
- fan_horizon_list (np.ndarray): The fan horizon list.
- fan_thickness (np.ndarray): The fan thickness list.
107 def apply_faulting_to_geomodels_and_depth_maps(self) -> None: 108 """ 109 Apply faulting to horizons and cubes 110 ------------------------------------ 111 Generates random faults and applies faulting to horizons and cubes. 112 113 The method does the following: 114 115 * Generate faults and sum the displacements 116 * Apply faulting to horizons 117 * Write faulted depth maps to disk 118 * Write faulted depth maps with gaps at faults to disk 119 * Write onlapping horizons to disk 120 * Apply faulting to geomodels 121 * Make segmentation results conform to binary values after 122 faulting and interpolation. 123 * Write cubes to file (if qc_volumes turned on in config.json) 124 125 (If segmentation is not reset to binary, the multiple 126 interpolations for successive faults destroys the crisp 127 localization of the labels. Subjective observation suggests 128 that slightly different thresholds for different 129 features provide superior results) 130 131 Parameters 132 ---------- 133 None 134 135 Returns 136 ------- 137 None 138 """ 139 # Make a dictionary of zero-thickness onlapping layers before faulting 140 onlap_clip_dict = find_zero_thickness_onlapping_layers( 141 self.unfaulted_depth_maps, self.onlap_horizon_list 142 ) 143 _ = self.generate_faults() 144 145 # Apply faulting to age model, net_to_gross cube & onlap segments 146 self.faulted_age_volume[:] = self.apply_xyz_displacement( 147 self.vols.geologic_age[:] 148 ).astype("float") 149 self.faulted_onlap_segments[:] = self.apply_xyz_displacement( 150 self.vols.onlap_segments[:] 151 ) 152 153 # Improve the depth maps post faulting by 154 # re-interpolating across faulted age model 155 ( 156 self.faulted_depth_maps[:], 157 self.faulted_depth_maps_gaps[:], 158 ) = self.improve_depth_maps_post_faulting( 159 self.vols.geologic_age[:], self.faulted_age_volume[:], onlap_clip_dict 160 ) 161 162 if self.cfg.include_salt: 163 from datagenerator.Salt import SaltModel 164 165 self.salt_model = SaltModel(self.cfg) 166 self.salt_model.compute_salt_body_segmentation() 167 ( 168 self.faulted_depth_maps[:], 169 self.faulted_depth_maps_gaps[:], 170 ) = self.salt_model.update_depth_maps_with_salt_segments_drag() 171 172 # # Write the faulted maps to disk 173 self.write_maps_to_disk( 174 self.faulted_depth_maps[:] * self.cfg.digi, "depth_maps" 175 ) 176 self.write_maps_to_disk( 177 self.faulted_depth_maps_gaps[:] * self.cfg.digi, "depth_maps_gaps" 178 ) 179 self.write_onlap_episodes( 180 self.onlap_horizon_list[:], 181 self.faulted_depth_maps_gaps[:], 182 self.faulted_depth_maps[:], 183 ) 184 if np.any(self.fan_horizon_list): 185 self.write_fan_horizons( 186 self.fan_horizon_list, self.faulted_depth_maps[:] * 4.0 187 ) 188 189 if self.cfg.hdf_store: 190 # Write faulted maps to hdf 191 for n, d in zip( 192 ["depth_maps", "depth_maps_gaps"], 193 [ 194 self.faulted_depth_maps[:] * self.cfg.digi, 195 self.faulted_depth_maps_gaps[:] * self.cfg.digi, 196 ], 197 ): 198 write_data_to_hdf(n, d, self.cfg.hdf_master) 199 200 # Create faulted binary segmentation volumes 201 _fault_planes = self.fault_planes[:] 202 self.fault_planes[:] = self.create_binary_segmentations_post_faulting( 203 _fault_planes, 0.45 204 ) 205 del _fault_planes 206 _fault_intersections = self.fault_intersections[:] 207 self.fault_intersections[:] = self.create_binary_segmentations_post_faulting( 208 _fault_intersections, 0.45 209 ) 210 del _fault_intersections 211 _faulted_onlap_segments = self.faulted_onlap_segments[:] 212 self.faulted_onlap_segments[:] = self.create_binary_segmentations_post_faulting( 213 _faulted_onlap_segments, 0.45 214 ) 215 del _faulted_onlap_segments 216 if self.cfg.include_channels: 217 self.vols.floodplain_shale = self.apply_xyz_displacement( 218 self.vols.floodplain_shale 219 ) 220 self.vols.channel_fill = self.apply_xyz_displacement(self.vols.channel_fill) 221 self.vols.shale_channel_drape = self.apply_xyz_displacement( 222 self.vols.shale_channel_drape 223 ) 224 self.vols.levee = self.apply_xyz_displacement(self.vols.levee) 225 self.vols.crevasse = self.apply_xyz_displacement(self.vols.crevasse) 226 227 ( 228 self.vols.channel_segments, 229 self.vols.geologic_age, 230 ) = self.reassign_channel_segment_encoding( 231 self.vols.geologic_age, 232 self.vols.floodplain_shale, 233 self.vols.channel_fill, 234 self.vols.shale_channel_drape, 235 self.vols.levee, 236 self.vols.crevasse, 237 self.maps.channels, 238 ) 239 if self.cfg.model_qc_volumes: 240 self.vols.write_cube_to_disk( 241 self.vols.channel_segments, "channel_segments" 242 ) 243 244 if self.cfg.model_qc_volumes: 245 # Output files if qc volumes required 246 self.vols.write_cube_to_disk(self.faulted_age_volume[:], "geologic_age") 247 self.vols.write_cube_to_disk( 248 self.faulted_onlap_segments[:], "onlap_segments" 249 ) 250 self.vols.write_cube_to_disk(self.fault_planes[:], "fault_segments") 251 self.vols.write_cube_to_disk( 252 self.fault_intersections[:], "fault_intersection_segments" 253 ) 254 self.vols.write_cube_to_disk( 255 self.fault_plane_throw[:], "fault_segments_throw" 256 ) 257 self.vols.write_cube_to_disk( 258 self.fault_plane_azimuth[:], "fault_segments_azimuth" 259 ) 260 if self.cfg.hdf_store: 261 # Write faulted age, onlap and fault segment cubes to hdf 262 for n, d in zip( 263 [ 264 "geologic_age_faulted", 265 "onlap_segments", 266 "fault_segments", 267 "fault_intersection_segments", 268 "fault_segments_throw", 269 "fault_segments_azimuth", 270 ], 271 [ 272 self.faulted_age_volume, 273 self.faulted_onlap_segments, 274 self.fault_planes, 275 self.fault_intersections, 276 self.fault_plane_throw, 277 self.fault_plane_azimuth, 278 ], 279 ): 280 write_data_to_hdf(n, d, self.cfg.hdf_master) 281 282 if self.cfg.qc_plots: 283 self.create_qc_plots() 284 try: 285 # Create 3D qc plot 286 plot_3D_faults_plot(self.cfg, self) 287 except ValueError: 288 self.cfg.write_to_logfile("3D Fault Plotting Failed")
Apply faulting to horizons and cubes
Generates random faults and applies faulting to horizons and cubes.
The method does the following:
- Generate faults and sum the displacements
- Apply faulting to horizons
- Write faulted depth maps to disk
- Write faulted depth maps with gaps at faults to disk
- Write onlapping horizons to disk
- Apply faulting to geomodels
- Make segmentation results conform to binary values after faulting and interpolation.
- Write cubes to file (if qc_volumes turned on in config.json)
(If segmentation is not reset to binary, the multiple interpolations for successive faults destroys the crisp localization of the labels. Subjective observation suggests that slightly different thresholds for different features provide superior results)
Parameters
- None
Returns
- None
290 def build_faulted_property_geomodels( 291 self, 292 facies: np.ndarray 293 ) -> None: 294 """ 295 Build Faulted Property Geomodels 296 ------------ 297 Generates faulted property geomodels. 298 299 **The method does the following:** 300 301 Use faulted geologic_age cube, depth_maps and facies 302 to create geomodel properties (depth, lith) 303 304 - lithology 305 - net_to_gross (to create effective sand layers) 306 - depth below mudline 307 - randomised depth below mudline (to 308 randomise the rock properties per layer) 309 310 Parameters 311 ---------- 312 facies : np.ndarray 313 The Horizons class used to build the faults. 314 315 Returns 316 ------- 317 None 318 """ 319 work_cube_lith = ( 320 np.ones_like(self.faulted_age_volume) * -1 321 ) # initialise lith cube to water 322 work_cube_sealed = np.zeros_like(self.faulted_age_volume) 323 work_cube_net_to_gross = np.zeros_like(self.faulted_age_volume) 324 work_cube_depth = np.zeros_like(self.faulted_age_volume) 325 # Also create a randomised depth cube for generating randomised rock properties 326 # final dimension's shape is based on number of possible list types 327 # currently one of ['seawater', 'shale', 'sand'] 328 # n_lith = len(['shale', 'sand']) 329 cube_shape = self.faulted_age_volume.shape 330 # randomised_depth = np.zeros(cube_shape, 'float32') 331 332 ii, jj = self.build_meshgrid() 333 334 # Loop over layers in reverse order, start at base 335 previous_depth_map = self.faulted_depth_maps[:, :, -1] 336 if self.cfg.partial_voxels: 337 # add .5 to consider partial voxels from half above and half below 338 previous_depth_map += 0.5 339 340 for i in range(self.faulted_depth_maps.shape[2] - 2, 0, -1): 341 # Apply a random depth shift within the range as provided in config file 342 # (provided in metres, so converted to samples here) 343 344 current_depth_map = self.faulted_depth_maps[:, :, i] 345 if self.cfg.partial_voxels: 346 current_depth_map += 0.5 347 348 # compute maps with indices of top map and base map to include partial voxels 349 top_map_index = current_depth_map.copy().astype("int") 350 base_map_index = ( 351 self.faulted_depth_maps[:, :, i + 1].copy().astype("int") + 1 352 ) 353 354 # compute thickness over which to iterate 355 thickness_map = base_map_index - top_map_index 356 thickness_map_max = thickness_map.max() 357 358 tvdml_map = previous_depth_map - self.faulted_depth_maps[:, :, 0] 359 # Net to Gross Map for layer 360 if not self.cfg.variable_shale_ng: 361 ng_map = np.zeros( 362 shape=( 363 self.faulted_depth_maps.shape[0], 364 self.faulted_depth_maps.shape[1], 365 ) 366 ) 367 else: 368 if ( 369 facies[i] == 0.0 370 ): # if shale layer, make non-zero N/G map for layer using a low average net to gross 371 ng_map = self.create_random_net_over_gross_map( 372 avg=(0.0, 0.2), stdev=(0.001, 0.01), octave=3 373 ) 374 if facies[i] == 1.0: # if sand layer, make non-zero N/G map for layer 375 ng_map = self.create_random_net_over_gross_map() 376 377 for k in range(thickness_map_max + 1): 378 379 if self.cfg.partial_voxels: 380 # compute fraction of voxel containing layer 381 top_map = np.max( 382 np.dstack( 383 (current_depth_map, top_map_index.astype("float32") + k) 384 ), 385 axis=-1, 386 ) 387 top_map = np.min(np.dstack((top_map, previous_depth_map)), axis=-1) 388 base_map = np.min( 389 np.dstack( 390 ( 391 previous_depth_map, 392 top_map_index.astype("float32") + k + 1, 393 ) 394 ), 395 axis=-1, 396 ) 397 fraction_of_voxel = np.clip(base_map - top_map, 0.0, 1.0) 398 valid_k = np.where( 399 (fraction_of_voxel > 0.0) 400 & ((top_map_index + k).astype("int") < cube_shape[2]), 401 1, 402 0, 403 ) 404 405 # put layer properties in the cube for each case 406 sublayer_ii = ii[valid_k == 1] 407 sublayer_jj = jj[valid_k == 1] 408 else: 409 sublayer_ii = ii[thickness_map > k] 410 sublayer_jj = jj[thickness_map > k] 411 412 if sublayer_ii.shape[0] > 0: 413 if self.cfg.partial_voxels: 414 sublayer_depth_map = (top_map_index + k).astype("int")[ 415 valid_k == 1 416 ] 417 sublayer_depth_map_int = np.clip(sublayer_depth_map, 0, None) 418 sublayer_ng_map = ng_map[valid_k == 1] 419 sublayer_tvdml_map = tvdml_map[valid_k == 1] 420 sublayer_fraction = fraction_of_voxel[valid_k == 1] 421 422 # Lithology cube 423 input_cube = work_cube_lith[ 424 sublayer_ii, sublayer_jj, sublayer_depth_map_int 425 ] 426 values = facies[i] * sublayer_fraction 427 input_cube[input_cube == -1.0] = ( 428 values[input_cube == -1.0] * 1.0 429 ) 430 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 431 work_cube_lith[ 432 sublayer_ii, sublayer_jj, sublayer_depth_map_int 433 ] = (input_cube * 1.0) 434 del input_cube 435 del values 436 437 input_cube = work_cube_sealed[ 438 sublayer_ii, sublayer_jj, sublayer_depth_map_int 439 ] 440 values = (1 - facies[i - 1]) * sublayer_fraction 441 input_cube[input_cube == -1.0] = ( 442 values[input_cube == -1.0] * 1.0 443 ) 444 input_cube[input_cube != -1.0] += values[input_cube != -1.0] 445 work_cube_sealed[ 446 sublayer_ii, sublayer_jj, sublayer_depth_map_int 447 ] = (input_cube * 1.0) 448 del input_cube 449 del values 450 451 # Depth cube 452 work_cube_depth[ 453 sublayer_ii, sublayer_jj, sublayer_depth_map_int 454 ] += (sublayer_tvdml_map * sublayer_fraction) 455 # Net to Gross cube 456 work_cube_net_to_gross[ 457 sublayer_ii, sublayer_jj, sublayer_depth_map_int 458 ] += (sublayer_ng_map * sublayer_fraction) 459 460 # Randomised Depth Cube 461 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += \ 462 # (sublayer_tvdml_map + random_z_perturbation) * sublayer_fraction 463 464 else: 465 sublayer_depth_map_int = ( 466 0.5 467 + np.clip( 468 previous_depth_map[thickness_map > k], 469 0, 470 self.vols.geologic_age.shape[2] - 1, 471 ) 472 ).astype("int") - k 473 sublayer_tvdml_map = tvdml_map[thickness_map > k] 474 sublayer_ng_map = ng_map[thickness_map > k] 475 476 work_cube_lith[ 477 sublayer_ii, sublayer_jj, sublayer_depth_map_int 478 ] = facies[i] 479 work_cube_sealed[ 480 sublayer_ii, sublayer_jj, sublayer_depth_map_int 481 ] = (1 - facies[i - 1]) 482 483 work_cube_depth[ 484 sublayer_ii, sublayer_jj, sublayer_depth_map_int 485 ] = sublayer_tvdml_map 486 work_cube_net_to_gross[ 487 sublayer_ii, sublayer_jj, sublayer_depth_map_int 488 ] += sublayer_ng_map 489 # randomised_depth[sublayer_ii, sublayer_jj, sublayer_depth_map_int] += (sublayer_tvdml_map + random_z_perturbation) 490 491 # replace previous depth map for next iteration 492 previous_depth_map = current_depth_map.copy() 493 494 if self.cfg.verbose: 495 print("\n\n ... After infilling ...") 496 self.write_cube_to_disk(work_cube_sealed.astype("uint8"), "sealed_label") 497 498 # Clip cubes and convert from samples to units 499 work_cube_lith = np.clip(work_cube_lith, -1.0, 1.0) # clip lith to [-1, +1] 500 work_cube_net_to_gross = np.clip( 501 work_cube_net_to_gross, 0, 1.0 502 ) # clip n/g to [0, 1] 503 work_cube_depth = np.clip(work_cube_depth, a_min=0, a_max=None) 504 work_cube_depth *= self.cfg.digi 505 506 if self.cfg.include_salt: 507 # Update age model after horizons have been modified by salt inclusion 508 self.faulted_age_volume[ 509 : 510 ] = self.create_geologic_age_3d_from_infilled_horizons( 511 self.faulted_depth_maps[:] * 10.0 512 ) 513 # Set lith code for salt 514 work_cube_lith[self.salt_model.salt_segments[:] > 0.0] = 2.0 515 # Fix deepest part of facies in case salt inclusion has shifted base horizon 516 # This can leave default (water) facies codes at the base 517 last_50_samples = self.cfg.cube_shape[-1] - 50 518 work_cube_lith[..., last_50_samples:][ 519 work_cube_lith[..., last_50_samples:] == -1.0 520 ] = 0.0 521 522 if self.cfg.qc_plots: 523 from datagenerator.util import plot_xsection 524 import matplotlib as mpl 525 526 line_number = int( 527 work_cube_lith.shape[0] / 2 528 ) # pick centre line for all plots 529 530 if self.cfg.include_salt and np.max(work_cube_lith[line_number, ...]) > 1: 531 lith_cmap = mpl.colors.ListedColormap( 532 ["blue", "saddlebrown", "gold", "grey"] 533 ) 534 else: 535 lith_cmap = mpl.colors.ListedColormap(["blue", "saddlebrown", "gold"]) 536 plot_xsection( 537 work_cube_lith, 538 self.faulted_depth_maps[:], 539 line_num=line_number, 540 title="Example Trav through 3D model\nLithology", 541 png_name="QC_plot__AfterFaulting_lithology.png", 542 cfg=self.cfg, 543 cmap=lith_cmap, 544 ) 545 plot_xsection( 546 work_cube_depth, 547 self.faulted_depth_maps, 548 line_num=line_number, 549 title="Example Trav through 3D model\nDepth Below Mudline", 550 png_name="QC_plot__AfterFaulting_depth_bml.png", 551 cfg=self.cfg, 552 cmap="cubehelix_r", 553 ) 554 self.faulted_lithology[:] = work_cube_lith 555 self.faulted_net_to_gross[:] = work_cube_net_to_gross 556 self.faulted_depth[:] = work_cube_depth 557 # self.randomised_depth[:] = randomised_depth 558 559 # Write the % sand in model to logfile 560 sand_fraction = ( 561 work_cube_lith[work_cube_lith == 1].size 562 / work_cube_lith[work_cube_lith >= 0].size 563 ) 564 self.cfg.write_to_logfile( 565 f"Sand voxel % in model {100 * sand_fraction:.1f}%", 566 mainkey="model_parameters", 567 subkey="sand_voxel_pct", 568 val=100 * sand_fraction, 569 ) 570 571 if self.cfg.hdf_store: 572 for n, d in zip( 573 ["lithology", "net_to_gross", "depth"], 574 [ 575 self.faulted_lithology[:], 576 self.faulted_net_to_gross[:], 577 self.faulted_depth[:], 578 ], 579 ): 580 write_data_to_hdf(n, d, self.cfg.hdf_master) 581 582 # Save out reservoir volume for XAI-NBDT 583 reservoir = (work_cube_lith == 1) * 1.0 584 reservoir_dilated = binary_dilation(reservoir) 585 self.reservoir[:] = reservoir_dilated 586 587 if self.cfg.model_qc_volumes: 588 self.write_cube_to_disk(self.faulted_lithology[:], "faulted_lithology") 589 self.write_cube_to_disk( 590 self.faulted_net_to_gross[:], "faulted_net_to_gross" 591 ) 592 self.write_cube_to_disk(self.faulted_depth[:], "faulted_depth") 593 self.write_cube_to_disk(self.faulted_age_volume[:], "faulted_age") 594 if self.cfg.include_salt: 595 self.write_cube_to_disk( 596 self.salt_model.salt_segments[..., : self.cfg.cube_shape[2]].astype( 597 "uint8" 598 ), 599 "salt", 600 )
Build Faulted Property Geomodels
Generates faulted property geomodels.
The method does the following:
Use faulted geologic_age cube, depth_maps and facies to create geomodel properties (depth, lith)
- lithology
- net_to_gross (to create effective sand layers)
- depth below mudline
- randomised depth below mudline (to randomise the rock properties per layer)
Parameters
- facies (np.ndarray): The Horizons class used to build the faults.
Returns
- None
602 def create_qc_plots(self) -> None: 603 """ 604 Create QC Plots 605 --------------- 606 Creates QC Plots of faulted models and histograms of 607 voxels which are not in layers. 608 609 Parameters 610 ---------- 611 None 612 613 Returns 614 ------- 615 None 616 """ 617 from datagenerator.util import ( 618 find_line_with_most_voxels, 619 plot_voxels_not_in_regular_layers, 620 plot_xsection, 621 ) 622 623 # analyze voxel values not in regular layers 624 plot_voxels_not_in_regular_layers( 625 volume=self.faulted_age_volume[:], 626 threshold=0.0, 627 cfg=self.cfg, 628 title="Example Trav through 3D model\n" 629 + "histogram of layers after faulting, before inserting channel facies", 630 png_name="QC_plot__Channels__histogram_FaultedLayersNoChannels.png", 631 ) 632 try: # if channel_segments exists 633 inline_index_channels = find_line_with_most_voxels( 634 self.vols.channel_segments, 0.0, self.cfg 635 ) 636 plot_xsection( 637 self.vols.channel_segments, 638 self.faulted_depth_maps, 639 inline_index_channels, 640 cfg=self.cfg, 641 title="Example Trav through 3D model\nchannel_segments after faulting", 642 png_name="QC_plot__AfterFaulting_channel_segments.png", 643 ) 644 title = "Example Trav through 3D model\nLayers Filled With Layer Number / ChannelsAdded / Faulted" 645 png_name = "QC_plot__LayersFilledWithLayerNumber_ChannelsAdded_Faulted.png" 646 except (NameError, AttributeError): # channel_segments does not exist 647 inline_index_channels = int(self.faulted_age_volume.shape[0] / 2) 648 title = "Example Trav through 3D model\nLayers Filled With Layer Number / Faulted" 649 png_name = "QC_plot__LayersFilledWithLayerNumber_Faulted.png" 650 plot_xsection( 651 self.faulted_age_volume[:], 652 self.faulted_depth_maps[:], 653 inline_index_channels, 654 title, 655 png_name, 656 self.cfg, 657 )
Create QC Plots
Creates QC Plots of faulted models and histograms of voxels which are not in layers.
Parameters
- None
Returns
- None
659 def generate_faults(self) -> np.ndarray: 660 """ 661 Generate Faults 662 --------------- 663 Generates faults in the model. 664 665 Parameters 666 ---------- 667 None 668 669 Returns 670 ------- 671 displacements_classification : np.ndarray 672 Array of fault displacement classifications 673 """ 674 if self.cfg.verbose: 675 print(f" ... create {self.cfg.number_faults} faults") 676 fault_params = self.fault_parameters() 677 678 # Write fault parameters to logfile 679 self.cfg.write_to_logfile( 680 f"Fault_mode: {self.cfg.fmode}", 681 mainkey="model_parameters", 682 subkey="fault_mode", 683 val=self.cfg.fmode, 684 ) 685 self.cfg.write_to_logfile( 686 f"Noise_level: {self.cfg.fnoise}", 687 mainkey="model_parameters", 688 subkey="noise_level", 689 val=self.cfg.fnoise, 690 ) 691 692 # Build faults, and sum displacements 693 displacements_classification, hockeys = self.build_faults(fault_params) 694 if self.cfg.model_qc_volumes: 695 # Write max fault throw cube to disk 696 self.vols.write_cube_to_disk(self.max_fault_throw[:], "max_fault_throw") 697 self.cfg.write_to_logfile( 698 f"Hockey_Sticks generated: {sum(hockeys)}", 699 mainkey="model_parameters", 700 subkey="hockey_sticks_generated", 701 val=sum(hockeys), 702 ) 703 704 self.cfg.write_to_logfile( 705 f"Fault Info: a, b, c, x0, y0, z0, throw/infill_factor, shear_zone_width," 706 " gouge_pctile, tilt_pct*100" 707 ) 708 for i in range(self.cfg.number_faults): 709 self.cfg.write_to_logfile( 710 f"Fault_{i + 1}: {fault_params['a'][i]:.2f}, {fault_params['b'][i]:.2f}, {fault_params['c'][i]:.2f}," 711 f" {fault_params['x0'][i]:>7.2f}, {fault_params['y0'][i]:>7.2f}, {fault_params['z0'][i]:>8.2f}," 712 f" {fault_params['throw'][i] / self.cfg.infill_factor:>6.2f}, {fault_params['tilt_pct'][i] * 100:.2f}" 713 ) 714 715 self.cfg.write_to_logfile( 716 msg=None, 717 mainkey=f"fault_{i + 1}", 718 subkey="model_id", 719 val=os.path.basename(self.cfg.work_subfolder), 720 ) 721 for _subkey_name in [ 722 "a", 723 "b", 724 "c", 725 "x0", 726 "y0", 727 "z0", 728 "throw", 729 "tilt_pct", 730 "shear_zone_width", 731 "gouge_pctile", 732 ]: 733 _val = fault_params[_subkey_name][i] 734 self.cfg.write_to_logfile( 735 msg=None, mainkey=f"fault_{i + 1}", subkey=_subkey_name, val=_val 736 ) 737 738 return displacements_classification
Generate Faults
Generates faults in the model.
Parameters
- None
Returns
- displacements_classification (np.ndarray): Array of fault displacement classifications
740 def fault_parameters(self): 741 """ 742 Get Fault Parameters 743 --------------- 744 Returns the fault parameters. 745 746 Factory design pattern used to select fault parameters 747 748 Parameters 749 ---------- 750 None 751 752 Returns 753 ------- 754 fault_mode : dict 755 Dictionary containing the fault parameters. 756 """ 757 fault_mode = self._get_fault_mode() 758 return fault_mode()
Get Fault Parameters
Returns the fault parameters.
Factory design pattern used to select fault parameters
Parameters
- None
Returns
- fault_mode (dict): Dictionary containing the fault parameters.
760 def build_faults(self, fp: dict, verbose=False): 761 """ 762 Build Faults 763 --------------- 764 Creates faults in the model. 765 766 Parameters 767 ---------- 768 fp : dict 769 Dictionary containing the fault parameters. 770 verbose : bool 771 The level of verbosity to use. 772 773 Returns 774 ------- 775 dis_class : np.ndarray 776 Array of fault displacement classifications 777 hockey_sticks : list 778 List of hockey sticks 779 """ 780 def apply_faulting(traces, stretch_times, verbose=False): 781 """ 782 Apply Faulting 783 -------------- 784 Applies faulting to the traces. 785 786 The method does the following: 787 788 Apply stretching and squeezing previously applied to the input cube 789 vertically to give all depths the same number of extrema. 790 This is intended to be a proxy for making the 791 dominant frequency the same everywhere. 792 Variables: 793 - traces - input, previously stretched/squeezed trace(s) 794 - stretch_times - input, LUT for stretching/squeezing trace(s), 795 range is (0,number samples in last dimension of 'traces') 796 - unstretch_traces - output, un-stretched/un-squeezed trace(s) 797 798 Parameters 799 ---------- 800 traces : np.ndarray 801 Previously stretched/squeezed trace(s). 802 stretch_times : np.ndarray 803 A look up table for stretching and squeezing the traces. 804 verbose : bool, optional 805 The level of verbosity, by default False 806 807 Returns 808 ------- 809 np.ndarray 810 The un-stretched/un-squeezed trace(s). 811 """ 812 unstretch_traces = np.zeros_like(traces) 813 origtime = np.arange(traces.shape[-1]) 814 815 if verbose: 816 print("\t ... Cube parameters going into interpolation") 817 print(f"\t ... Origtime shape = {len(origtime)}") 818 print(f"\t ... stretch_times_effects shape = {stretch_times.shape}") 819 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 820 print(f"\t ... traces shape = {traces.shape}") 821 822 for i in range(traces.shape[0]): 823 for j in range(traces.shape[1]): 824 if traces[i, j, :].min() != traces[i, j, :].max(): 825 unstretch_traces[i, j, :] = np.interp( 826 stretch_times[i, j, :], origtime, traces[i, j, :] 827 ) 828 else: 829 unstretch_traces[i, j, :] = traces[i, j, :] 830 return unstretch_traces 831 832 print("\n\n . starting 'build_faults'.") 833 print(" ... self.cfg.verbose = " + str(self.cfg.verbose)) 834 cube_shape = np.array(self.cfg.cube_shape) 835 cube_shape[-1] += self.cfg.pad_samples 836 samples_in_cube = self.vols.geologic_age[:].size 837 wb = self.copy_and_divide_depth_maps_by_infill( 838 self.unfaulted_depth_maps[..., 0] 839 ) 840 841 sum_displacements = np.zeros_like(self.vols.geologic_age[:]) 842 displacements_class = np.zeros_like(self.vols.geologic_age[:]) 843 hockey_sticks = [] 844 fault_voxel_count_list = [] 845 number_fault_intersections = 0 846 847 depth_maps_faulted_infilled = \ 848 self.copy_and_divide_depth_maps_by_infill( 849 self.unfaulted_depth_maps[:] 850 ) 851 depth_maps_gaps = self.copy_and_divide_depth_maps_by_infill( 852 self.unfaulted_depth_maps[:] 853 ) 854 855 # Create depth indices cube (moved from inside loop) 856 faulted_depths = np.zeros_like(self.vols.geologic_age[:]) 857 for k in range(faulted_depths.shape[-1]): 858 faulted_depths[:, :, k] = k 859 unfaulted_depths = faulted_depths * 1.0 860 _faulted_depths = ( 861 unfaulted_depths * 1.0 862 ) # in case there are 0 faults, prepare _faulted_depths here 863 864 for ifault in tqdm(range(self.cfg.number_faults)): 865 semi_axes = [ 866 fp["a"][ifault], 867 fp["b"][ifault], 868 fp["c"][ifault] / self.cfg.infill_factor ** 2, 869 ] 870 origin = [ 871 fp["x0"][ifault], 872 fp["y0"][ifault], 873 fp["z0"][ifault] / self.cfg.infill_factor, 874 ] 875 throw = fp["throw"][ifault] / self.cfg.infill_factor 876 tilt = fp["tilt_pct"][ifault] 877 878 print(f"\n\n ... inserting fault {ifault} with throw {throw:.2f}") 879 print( 880 f" ... fault ellipsoid semi-axes (a, b, c): {np.sqrt(semi_axes[0]):.2f}, " 881 f"{np.sqrt(semi_axes[1]):.2f}, {np.sqrt(semi_axes[2]):.2f}" 882 ) 883 print( 884 f" ... fault ellipsoid origin (x, y, z): {origin[0]:.2f}, {origin[1]:.2f}, {origin[2]:.2f}" 885 ) 886 print(f" ... tilt_pct: {tilt * 100:.2f}") 887 z_base = origin[2] * np.sqrt(semi_axes[2]) 888 print( 889 f" ...z for bottom of ellipsoid at depth (samples) = {np.around(z_base, 0)}" 890 ) 891 print(f" ...shape of output_cube = {self.vols.geologic_age.shape}") 892 print( 893 f" ...infill_factor, pad_samples = {self.cfg.infill_factor}, {self.cfg.pad_samples}" 894 ) 895 896 # add empty arrays for shear_zone_width and gouge_pctile to fault params dictionary 897 fp["shear_zone_width"] = np.zeros(self.cfg.number_faults) 898 fp["gouge_pctile"] = np.zeros(self.cfg.number_faults) 899 ( 900 displacement, 901 displacement_classification, 902 interpolation, 903 hockey_stick, 904 fault_segm, 905 ellipsoid, 906 fp, 907 ) = self.get_displacement_vector( 908 semi_axes, origin, throw, tilt, wb, ifault, fp 909 ) 910 911 if verbose: 912 print(" ... hockey_stick = " + str(hockey_stick)) 913 print( 914 " ... Is displacement the same as displacement_classification? " 915 + str(np.all(displacement == displacement_classification)) 916 ) 917 print( 918 " ... Sample count where displacement differs from displacement_classification? " 919 + str( 920 displacement[displacement != displacement_classification].size 921 ) 922 ) 923 print( 924 " ... percent of samples where displacement differs from displacement_classification = " 925 + format( 926 float( 927 displacement[ 928 displacement != displacement_classification 929 ].size 930 ) 931 / samples_in_cube, 932 "5.1%", 933 ) 934 ) 935 try: 936 print( 937 " ... (displacement differs from displacement_classification).mean() " 938 + str( 939 displacement[ 940 displacement != displacement_classification 941 ].mean() 942 ) 943 ) 944 print( 945 " ... (displacement differs from displacement_classification).max() " 946 + str( 947 displacement[ 948 displacement != displacement_classification 949 ].max() 950 ) 951 ) 952 except: 953 pass 954 955 print( 956 " ...displacement_classification.min() = " 957 + ", " 958 + str(displacement_classification.min()) 959 ) 960 print( 961 " ...displacement_classification.mean() = " 962 + ", " 963 + str(displacement_classification.mean()) 964 ) 965 print( 966 " ...displacement_classification.max() = " 967 + ", " 968 + str(displacement_classification.max()) 969 ) 970 971 print(" ...displacement.min() = " + ", " + str(displacement.min())) 972 print(" ...displacement.mean() = " + ", " + str(displacement.mean())) 973 print(" ...displacement.max() = " + ", " + str(displacement.max())) 974 if fault_segm[fault_segm > 0.0].size > 0: 975 print( 976 " ...displacement[fault_segm >0].min() = " 977 + ", " 978 + str(displacement[fault_segm > 0].min()) 979 ) 980 print( 981 " ...displacement[fault_segm >0] P10 = " 982 + ", " 983 + str(np.percentile(displacement[fault_segm > 0], 10)) 984 ) 985 print( 986 " ...displacement[fault_segm >0] P25 = " 987 + ", " 988 + str(np.percentile(displacement[fault_segm > 0], 25)) 989 ) 990 print( 991 " ...displacement[fault_segm >0].mean() = " 992 + ", " 993 + str(displacement[fault_segm > 0].mean()) 994 ) 995 print( 996 " ...displacement[fault_segm >0] P75 = " 997 + ", " 998 + str(np.percentile(displacement[fault_segm > 0], 75)) 999 ) 1000 print( 1001 " ...displacement[fault_segm >0] P90 = " 1002 + ", " 1003 + str(np.percentile(displacement[fault_segm > 0], 90)) 1004 ) 1005 print( 1006 " ...displacement[fault_segm >0].max() = " 1007 + ", " 1008 + str(displacement[fault_segm > 0].max()) 1009 ) 1010 1011 inline = self.cfg.cube_shape[0] // 2 1012 1013 # limit labels to portions of fault plane with throw above threshold 1014 throw_threshold_samples = 1.0 1015 footprint = np.ones((3, 3, 1)) 1016 fp_i, fp_j, fp_k = np.where( 1017 (fault_segm > 0.25) 1018 & (displacement_classification > throw_threshold_samples) 1019 ) 1020 fault_segm = np.zeros_like(fault_segm) 1021 fault_plane_displacement = np.zeros_like(fault_segm) 1022 fault_segm[fp_i, fp_j, fp_k] = 1.0 1023 fault_plane_displacement[fp_i, fp_j, fp_k] = ( 1024 displacement_classification[fp_i, fp_j, fp_k] * 1.0 1025 ) 1026 1027 fault_voxel_count_list.append(fault_segm[fault_segm > 0.5].size) 1028 1029 # create blended version of displacement that accounts for simulation of fault drag 1030 drag_factor = 0.5 1031 displacement = ( 1032 1.0 - drag_factor 1033 ) * displacement + drag_factor * displacement_classification 1034 1035 # project fault depth on 2D map surface 1036 fault_plane_map = np.zeros_like(wb) 1037 depth_indices = np.arange(ellipsoid.shape[-1]) 1038 for ii in range(fault_plane_map.shape[0]): 1039 for jj in range(fault_plane_map.shape[1]): 1040 fault_plane_map[ii, jj] = np.interp( 1041 1.0, ellipsoid[ii, jj, :], depth_indices 1042 ) 1043 fault_plane_map = fault_plane_map.clip(0, ellipsoid.shape[2] - 1) 1044 1045 # compute fault azimuth (relative to i,j,k indices, not North) 1046 dx, dy = np.gradient(fault_plane_map) 1047 strike_angle = np.arctan2(dy, dx) * 180.0 / np.pi # 2D 1048 del dx 1049 del dy 1050 strike_angle = np.zeros_like(fault_segm) + strike_angle.reshape( 1051 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1052 ) 1053 strike_angle[fault_segm < 0.5] = 200.0 1054 1055 # - compute faulted depth as max of fault plane or faulted depth 1056 fault_plane = np.zeros_like(displacement) + fault_plane_map.reshape( 1057 fault_plane_map.shape[0], fault_plane_map.shape[1], 1 1058 ) 1059 1060 if ifault == 0: 1061 print(" .... set _unfaulted_depths to array with all zeros...") 1062 _faulted_depths = unfaulted_depths * 1.0 1063 self.fault_plane_azimuth[:] = strike_angle * 1.0 1064 1065 print(" ... interpolation = " + str(interpolation)) 1066 1067 if ( 1068 interpolation 1069 ): # i.e. if the fault should be considered, apply the displacements and append fault plane 1070 faulted_depths2 = np.zeros( 1071 (ellipsoid.shape[0], ellipsoid.shape[1], ellipsoid.shape[2], 2), 1072 "float", 1073 ) 1074 faulted_depths2[:, :, :, 0] = displacement * 1.0 1075 faulted_depths2[:, :, :, 1] = fault_plane - faulted_depths 1076 faulted_depths2[:, :, :, 1][ellipsoid > 1] = 0.0 1077 map_displacement_vector = np.min(faulted_depths2, axis=-1).clip( 1078 0.0, None 1079 ) 1080 del faulted_depths2 1081 map_displacement_vector[ellipsoid > 1] = 0.0 1082 1083 self.sum_map_displacements[:] += map_displacement_vector 1084 displacements_class += displacement_classification 1085 # Set displacements outside ellipsoid to 0 1086 displacement[ellipsoid > 1] = 0 1087 1088 sum_displacements += displacement 1089 1090 # apply fault to depth_cube 1091 if ifault == 0: 1092 print(" .... set _unfaulted_depths to array with all zeros...") 1093 _faulted_depths = unfaulted_depths * 1.0 1094 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1095 0, ellipsoid.shape[-1] - 1 1096 ) 1097 _faulted_depths = apply_faulting( 1098 _faulted_depths, adjusted_faulted_depths 1099 ) 1100 _mft = self.max_fault_throw[:] 1101 _mft[ellipsoid <= 1.0] = throw 1102 self.max_fault_throw[:] = _mft 1103 del _mft 1104 self.fault_planes[:] = fault_segm * 1.0 1105 _fault_intersections = self.fault_intersections[:] 1106 previous_intersection_voxel_count = _fault_intersections[ 1107 _fault_intersections > 1.1 1108 ].size 1109 _fault_intersections = self.fault_planes[:] * 1.0 1110 if ( 1111 _fault_intersections[_fault_intersections > 1.1].size 1112 > previous_intersection_voxel_count 1113 ): 1114 number_fault_intersections += 1 1115 self.fault_intersections[:] = _fault_intersections 1116 self.fault_plane_throw[:] = fault_plane_displacement * 1.0 1117 self.fault_plane_azimuth[:] = strike_angle * 1.0 1118 else: 1119 print( 1120 " .... update _unfaulted_depths array using faulted depths..." 1121 ) 1122 try: 1123 print( 1124 " .... (before) _faulted_depths.mean() = " 1125 + str(_faulted_depths.mean()) 1126 ) 1127 except: 1128 _faulted_depths = unfaulted_depths * 1.0 1129 adjusted_faulted_depths = (unfaulted_depths - displacement).clip( 1130 0, ellipsoid.shape[-1] - 1 1131 ) 1132 _faulted_depths = apply_faulting( 1133 _faulted_depths, adjusted_faulted_depths 1134 ) 1135 1136 _fault_planes = apply_faulting( 1137 self.fault_planes[:], adjusted_faulted_depths 1138 ) 1139 if ( 1140 _fault_planes[ 1141 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1142 ].size 1143 > 0 1144 ): 1145 _fault_planes[ 1146 np.logical_and(0.0 < _fault_planes, _fault_planes < 0.25) 1147 ] = 0.0 1148 if ( 1149 _fault_planes[ 1150 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1151 ].size 1152 > 0 1153 ): 1154 _fault_planes[ 1155 np.logical_and(0.25 < _fault_planes, _fault_planes < 1.0) 1156 ] = 1.0 1157 self.fault_planes[:] = _fault_planes 1158 del _fault_planes 1159 1160 _fault_intersections = apply_faulting( 1161 self.fault_intersections[:], adjusted_faulted_depths 1162 ) 1163 if ( 1164 _fault_intersections[ 1165 np.logical_and( 1166 1.25 < _fault_intersections, _fault_intersections < 2.0 1167 ) 1168 ].size 1169 > 0 1170 ): 1171 _fault_intersections[ 1172 np.logical_and( 1173 1.25 < _fault_intersections, _fault_intersections < 2.0 1174 ) 1175 ] = 2.0 1176 self.fault_intersections[:] = _fault_intersections 1177 del _fault_intersections 1178 1179 self.max_fault_throw[:] = apply_faulting( 1180 self.max_fault_throw[:], adjusted_faulted_depths 1181 ) 1182 self.fault_plane_throw[:] = apply_faulting( 1183 self.fault_plane_throw[:], adjusted_faulted_depths 1184 ) 1185 self.fault_plane_azimuth[:] = apply_faulting( 1186 self.fault_plane_azimuth[:], adjusted_faulted_depths 1187 ) 1188 1189 self.fault_planes[:] += fault_segm 1190 self.fault_intersections[:] += fault_segm 1191 _mft = self.max_fault_throw[:] 1192 _mft[ellipsoid <= 1.0] += throw 1193 self.max_fault_throw[:] = _mft 1194 del _mft 1195 1196 if verbose: 1197 print( 1198 " ... fault_plane_displacement[fault_plane_displacement > 0.].size = " 1199 + str( 1200 fault_plane_displacement[ 1201 fault_plane_displacement > 0.0 1202 ].size 1203 ) 1204 ) 1205 self.fault_plane_throw[ 1206 fault_plane_displacement > 0.0 1207 ] = fault_plane_displacement[fault_plane_displacement > 0.0] 1208 if verbose: 1209 print( 1210 " ... self.fault_plane_throw[self.fault_plane_throw > 0.].size = " 1211 + str( 1212 self.fault_plane_throw[ 1213 self.fault_plane_throw > 0.0 1214 ].size 1215 ) 1216 ) 1217 self.fault_plane_azimuth[fault_segm > 0.9] = ( 1218 strike_angle[fault_segm > 0.9] * 1.0 1219 ) 1220 1221 if verbose: 1222 print( 1223 " .... (after) _faulted_depths.mean() = " 1224 + str(_faulted_depths.mean()) 1225 ) 1226 1227 # fix interpolated values in max_fault_throw 1228 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1229 self.max_fault_throw, return_counts=True 1230 ) 1231 max_fault_throw_list = max_fault_throw_list[ 1232 max_fault_throw_list_counts > 500 1233 ] 1234 if verbose: 1235 print( 1236 "\n ...max_fault_throw_list = " 1237 + str(max_fault_throw_list) 1238 + ", " 1239 + str(max_fault_throw_list_counts) 1240 ) 1241 mfts = self.max_fault_throw.shape 1242 self.cfg.hdf_remove_node_list("max_fault_throw_4d_diff") 1243 self.cfg.hdf_remove_node_list("max_fault_throw_4d") 1244 max_fault_throw_4d_diff = self.cfg.hdf_init( 1245 "max_fault_throw_4d_diff", 1246 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1247 ) 1248 max_fault_throw_4d = self.cfg.hdf_init( 1249 "max_fault_throw_4d", 1250 shape=(mfts[0], mfts[1], mfts[2], max_fault_throw_list.size), 1251 ) 1252 if verbose: 1253 print( 1254 "\n ...max_fault_throw_4d.shape = " 1255 + ", " 1256 + str(max_fault_throw_4d.shape) 1257 ) 1258 _max_fault_throw_4d_diff = max_fault_throw_4d_diff[:] 1259 _max_fault_throw_4d = max_fault_throw_4d[:] 1260 for imft, mft in enumerate(max_fault_throw_list): 1261 print( 1262 " ... imft, mft, max_fault_throw_4d_diff[:,:,:,imft].shape = " 1263 + str( 1264 ( 1265 imft, 1266 mft, 1267 max_fault_throw_4d_diff[:, :, :, imft].shape, 1268 ) 1269 ) 1270 ) 1271 _max_fault_throw_4d_diff[:, :, :, imft] = np.abs( 1272 self.max_fault_throw[:, :, :] - mft 1273 ) 1274 _max_fault_throw_4d[:, :, :, imft] = mft 1275 max_fault_throw_4d[:] = _max_fault_throw_4d 1276 max_fault_throw_4d_diff[:] = _max_fault_throw_4d_diff 1277 if verbose: 1278 print( 1279 " ...np.argmin(max_fault_throw_4d_diff, axis=-1).shape = " 1280 + ", " 1281 + str(np.argmin(max_fault_throw_4d_diff, axis=-1).shape) 1282 ) 1283 indices_nearest_throw = np.argmin(max_fault_throw_4d_diff, axis=-1) 1284 if verbose: 1285 print( 1286 "\n ...indices_nearest_throw.shape = " 1287 + ", " 1288 + str(indices_nearest_throw.shape) 1289 ) 1290 _max_fault_throw = self.max_fault_throw[:] 1291 for imft, mft in enumerate(max_fault_throw_list): 1292 _max_fault_throw[indices_nearest_throw == imft] = mft 1293 self.max_fault_throw[:] = _max_fault_throw 1294 1295 del _max_fault_throw 1296 del adjusted_faulted_depths 1297 1298 if verbose: 1299 print( 1300 " ...fault_segm[fault_segm>0.].size = " 1301 + ", " 1302 + str(fault_segm[fault_segm > 0.0].size) 1303 ) 1304 print(" ...fault_segm.min() = " + ", " + str(fault_segm.min())) 1305 print(" ...fault_segm.max() = " + ", " + str(fault_segm.max())) 1306 print( 1307 " ...self.fault_planes.max() = " 1308 + ", " 1309 + str(self.fault_planes.max()) 1310 ) 1311 print( 1312 " ...self.fault_intersections.max() = " 1313 + ", " 1314 + str(self.fault_intersections.max()) 1315 ) 1316 1317 print( 1318 " ...list of unique values in self.max_fault_throw = " 1319 + ", " 1320 + str(np.unique(self.max_fault_throw)) 1321 ) 1322 1323 # TODO: remove this block after qc/tests complete 1324 from datagenerator.util import import_matplotlib 1325 plt = import_matplotlib() 1326 1327 plt.close(35) 1328 plt.figure(35, figsize=(15, 10)) 1329 plt.clf() 1330 plt.imshow(_faulted_depths[inline, :, :].T, aspect="auto", cmap="prism") 1331 plt.tight_layout() 1332 plt.savefig( 1333 os.path.join( 1334 self.cfg.work_subfolder, f"faulted_depths_{ifault:02d}.png" 1335 ), 1336 format="png", 1337 ) 1338 1339 plt.close(35) 1340 plt.figure(36, figsize=(15, 10)) 1341 plt.clf() 1342 plt.imshow(displacement[inline, :, :].T, aspect="auto", cmap="jet") 1343 plt.tight_layout() 1344 plt.savefig( 1345 os.path.join( 1346 self.cfg.work_subfolder, f"displacement_{ifault:02d}.png" 1347 ), 1348 format="png", 1349 ) 1350 1351 # TODO: remove this block after qc/tests complete 1352 _plotarray = self.fault_planes[inline, :, :].copy() 1353 _plotarray[_plotarray == 0.0] = np.nan 1354 plt.close(37) 1355 plt.figure(37, figsize=(15, 10)) 1356 plt.clf() 1357 plt.imshow(_plotarray.T, aspect="auto", cmap="gist_ncar") 1358 plt.tight_layout() 1359 plt.savefig( 1360 os.path.join(self.cfg.work_subfolder, f"fault_{ifault:02d}.png"), 1361 format="png", 1362 ) 1363 plt.close(37) 1364 plt.figure(36) 1365 plt.imshow(_plotarray.T, aspect="auto", cmap="gray", alpha=0.6) 1366 plt.tight_layout() 1367 1368 plt.savefig( 1369 os.path.join( 1370 self.cfg.work_subfolder, 1371 f"displacement_fault_overlay_{ifault:02d}.png", 1372 ), 1373 format="png", 1374 ) 1375 plt.close(36) 1376 1377 hockey_sticks.append(hockey_stick) 1378 print(" ...hockey_sticks = " + ", " + str(hockey_sticks)) 1379 1380 # print final count 1381 max_fault_throw_list, max_fault_throw_list_counts = np.unique( 1382 self.max_fault_throw, return_counts=True 1383 ) 1384 max_fault_throw_list = max_fault_throw_list[max_fault_throw_list_counts > 100] 1385 if verbose: 1386 print( 1387 "\n ... ** final ** max_fault_throw_list = " 1388 + str(max_fault_throw_list) 1389 + ", " 1390 + str(max_fault_throw_list_counts) 1391 ) 1392 1393 # create fault intersections 1394 _fault_intersections = self.fault_planes[:] * 1.0 1395 # self.fault_intersections = self.fault_planes * 1. 1396 if verbose: 1397 print(" ... line 592:") 1398 print( 1399 " ...self.fault_intersections.max() = " 1400 + ", " 1401 + str(_fault_intersections.max()) 1402 ) 1403 print( 1404 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1405 + ", " 1406 + str(_fault_intersections[_fault_intersections > 0.0].size) 1407 ) 1408 1409 _fault_intersections[_fault_intersections <= 1.0] = 0.0 1410 _fault_intersections[_fault_intersections > 1.1] = 1.0 1411 if verbose: 1412 print(" ... line 597:") 1413 print( 1414 " ...self.fault_intersections.max() = " 1415 + ", " 1416 + str(_fault_intersections.max()) 1417 ) 1418 print( 1419 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1420 + ", " 1421 + str(_fault_intersections[_fault_intersections > 0.0].size) 1422 ) 1423 1424 # make 2nd count of number of intersections between faults. write result to logfile. 1425 from datetime import datetime 1426 1427 start_time = datetime.now() 1428 number_fault_intersections = max( 1429 number_fault_intersections, 1430 measure.label(_fault_intersections, background=0).max(), 1431 ) 1432 print( 1433 f" ... elapsed time for skimage.label = {(datetime.now() - start_time)}" 1434 ) 1435 print(" ... number_fault_intersections = " + str(number_fault_intersections)) 1436 1437 # dilate intersection values 1438 # - window size of (5,5,15) is arbitrary. Should be based on isolating sections 1439 # of fault planes on real seismic 1440 _fault_intersections = maximum_filter(_fault_intersections, size=(7, 7, 17)) 1441 1442 # Fault intersection segments > 1 at intersecting faults. Clip to 1 1443 _fault_intersections[_fault_intersections > 0.05] = 1.0 1444 _fault_intersections[_fault_intersections != 1.0] = 0.0 1445 if verbose: 1446 print(" ... line 607:") 1447 print( 1448 " ...self.fault_intersections.max() = " 1449 + ", " 1450 + str(_fault_intersections.max()) 1451 ) 1452 print( 1453 " ...self.fault_intersections[self.fault_intersections>0.].size = " 1454 + ", " 1455 + str(_fault_intersections[_fault_intersections > 0.0].size) 1456 ) 1457 1458 # Fault segments = 1 at fault > 1 at intersections. Clip intersecting fault voxels to 1 1459 _fault_planes = self.fault_planes[:] 1460 _fault_planes[_fault_planes > 0.05] = 1.0 1461 _fault_planes[_fault_planes != 1.0] = 0.0 1462 1463 # make fault azi 3D and only retain voxels in fault plane 1464 1465 for k, v in zip( 1466 [ 1467 "n_voxels_faults", 1468 "n_voxels_fault_intersections", 1469 "number_fault_intersections", 1470 "fault_voxel_count_list", 1471 "hockey_sticks", 1472 ], 1473 [ 1474 _fault_planes[_fault_planes > 0.5].size, 1475 _fault_intersections[_fault_intersections > 0.5].size, 1476 number_fault_intersections, 1477 fault_voxel_count_list, 1478 hockey_sticks, 1479 ], 1480 ): 1481 self.cfg.write_to_logfile(f"{k}: {v}") 1482 1483 self.cfg.write_to_logfile( 1484 msg=None, mainkey="model_parameters", subkey=k, val=v 1485 ) 1486 1487 dis_class = _faulted_depths * 1 1488 self.fault_intersections[:] = _fault_intersections 1489 del _fault_intersections 1490 self.fault_planes[:] = _fault_planes 1491 del _fault_planes 1492 self.displacement_vectors[:] = _faulted_depths * 1.0 1493 1494 # TODO: check if next line of code modifies 'displacement' properly 1495 self.sum_map_displacements[:] = _faulted_depths * 1.0 1496 1497 # Save faulted maps 1498 self.faulted_depth_maps[:] = depth_maps_faulted_infilled 1499 self.faulted_depth_maps_gaps[:] = depth_maps_gaps 1500 1501 # perform faulting for fault_segments and fault_intersections 1502 return dis_class, hockey_sticks
Build Faults
Creates faults in the model.
Parameters
- fp (dict): Dictionary containing the fault parameters.
- verbose (bool): The level of verbosity to use.
Returns
- dis_class (np.ndarray): Array of fault displacement classifications
- hockey_sticks (list): List of hockey sticks
1581 def improve_depth_maps_post_faulting( 1582 self, 1583 unfaulted_geologic_age: np.ndarray, 1584 faulted_geologic_age: np.ndarray, 1585 onlap_clips: np.ndarray 1586 ): 1587 """ 1588 Re-interpolates the depth maps using the faulted geologic age cube 1589 1590 Parameters 1591 ---------- 1592 unfaulted_geologic_age : np.ndarray 1593 The unfaulted geologic age cube 1594 faulted_geologic_age : np.ndarray 1595 The faulted geologic age cube 1596 onlap_clips : np.ndarray 1597 The onlap clips 1598 1599 Returns 1600 ------- 1601 depth_maps : np.ndarray 1602 The improved depth maps 1603 depth_maps_gaps : np.ndarray 1604 The improved depth maps with gaps 1605 """ 1606 faulted_depth_maps = np.zeros_like(self.faulted_depth_maps) 1607 origtime = np.arange(self.faulted_depth_maps.shape[-1]) 1608 for i in range(self.faulted_depth_maps.shape[0]): 1609 for j in range(self.faulted_depth_maps.shape[1]): 1610 if ( 1611 faulted_geologic_age[i, j, :].min() 1612 != faulted_geologic_age[i, j, :].max() 1613 ): 1614 faulted_depth_maps[i, j, :] = np.interp( 1615 origtime, 1616 faulted_geologic_age[i, j, :], 1617 np.arange(faulted_geologic_age.shape[-1]).astype("float"), 1618 ) 1619 else: 1620 faulted_depth_maps[i, j, :] = unfaulted_geologic_age[i, j, :] 1621 # Waterbottom horizon has been set to 0. Re-insert this from the original depth_maps array 1622 if np.count_nonzero(faulted_depth_maps[:, :, 0]) == 0: 1623 faulted_depth_maps[:, :, 0] = self.faulted_depth_maps[:, :, 0] * 1.0 1624 1625 # Shift re-interpolated horizons to replace first horizon (of 0's) with the second, etc 1626 zmaps = np.zeros_like(faulted_depth_maps) 1627 zmaps[..., :-1] = faulted_depth_maps[..., 1:] 1628 # Fix the deepest re-interpolated horizon by adding a constant thickness to the shallower horizon 1629 zmaps[..., -1] = self.faulted_depth_maps[..., -1] + 10 1630 # Clip this last horizon to the one above 1631 thickness_map = zmaps[..., -1] - zmaps[..., -2] 1632 zmaps[..., -1][np.where(thickness_map <= 0.0)] = zmaps[..., -2][ 1633 np.where(thickness_map <= 0.0) 1634 ] 1635 faulted_depth_maps = zmaps.copy() 1636 1637 if self.cfg.qc_plots: 1638 self._qc_plot_check_faulted_horizons_match_fault_segments( 1639 faulted_depth_maps, faulted_geologic_age 1640 ) 1641 1642 # Re-apply old gaps to improved depth_maps 1643 zmaps_imp = faulted_depth_maps.copy() 1644 merged = zmaps_imp.copy() 1645 _depth_maps_gaps_improved = merged.copy() 1646 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1647 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1648 1649 # for zero-thickness layers, set depth_maps_gaps to nan 1650 for i in range(depth_maps_gaps.shape[-1] - 1): 1651 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1652 # set thicknesses < zero to NaN. Use NaNs in thickness_map to 0 to avoid runtime warning when indexing 1653 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1654 1655 # restore zero thickness from faulted horizons to improved (interpolated) depth maps 1656 ii, jj = np.meshgrid( 1657 range(self.cfg.cube_shape[0]), 1658 range(self.cfg.cube_shape[1]), 1659 sparse=False, 1660 indexing="ij", 1661 ) 1662 merged = zmaps_imp.copy() 1663 1664 # create temporary copy of fault_segments with dilation 1665 from scipy.ndimage.morphology import grey_dilation 1666 1667 _dilated_fault_planes = grey_dilation(self.fault_planes, size=(3, 3, 1)) 1668 1669 _onlap_segments = self.vols.onlap_segments[:] 1670 for ihor in range(depth_maps_gaps.shape[-1] - 1, 2, -1): 1671 # filter upper horizon being used for thickness if shallower events onlap it, except at faults 1672 improved_zmap_thickness = merged[:, :, ihor] - merged[:, :, ihor - 1] 1673 depth_map_int = ((merged[:, :, ihor]).astype(int)).clip( 1674 0, _onlap_segments.shape[-1] - 1 1675 ) 1676 improved_map_onlap_segments = _onlap_segments[ii, jj, depth_map_int] + 0.0 1677 improved_map_fault_segments = ( 1678 _dilated_fault_planes[ii, jj, depth_map_int] + 0.0 1679 ) 1680 # remember that self.maps.depth_maps has horizons with direct fault application 1681 faulted_infilled_map_thickness = ( 1682 self.faulted_depth_maps[:, :, ihor] 1683 - self.faulted_depth_maps[:, :, ihor - 1] 1684 ) 1685 improved_zmap_thickness[ 1686 np.where( 1687 (faulted_infilled_map_thickness <= 0.0) 1688 & (improved_map_onlap_segments > 0.0) 1689 & (improved_map_fault_segments == 0.0) 1690 ) 1691 ] = 0.0 1692 print( 1693 " ... ihor, improved_map_onlap_segments[improved_map_onlap_segments>0.].shape," 1694 " improved_zmap_thickness[improved_zmap_thickness==0].shape = ", 1695 ihor, 1696 improved_map_onlap_segments[improved_map_onlap_segments > 0.0].shape, 1697 improved_zmap_thickness[improved_zmap_thickness == 0].shape, 1698 ) 1699 merged[:, :, ihor - 1] = merged[:, :, ihor] - improved_zmap_thickness 1700 1701 if np.any(self.fan_horizon_list): 1702 # Clip fans downwards when thickness map is zero 1703 for count, layer in enumerate(self.fan_horizon_list): 1704 merged = fix_zero_thickness_fan_layers( 1705 merged, layer, self.fan_thickness[count] 1706 ) 1707 1708 # Re-apply clipping to onlapping layers post faulting 1709 merged = fix_zero_thickness_onlap_layers(merged, onlap_clips) 1710 1711 del _dilated_fault_planes 1712 1713 # Re-apply gaps to improved depth_maps 1714 _depth_maps_gaps_improved = merged.copy() 1715 _depth_maps_gaps_improved[np.isnan(self.faulted_depth_maps_gaps)] = np.nan 1716 depth_maps_gaps = _depth_maps_gaps_improved.copy() 1717 1718 # for zero-thickness layers, set depth_maps_gaps to nan 1719 for i in range(depth_maps_gaps.shape[-1] - 1): 1720 thickness_map = depth_maps_gaps[:, :, i + 1] - depth_maps_gaps[:, :, i] 1721 # set nans in thickness_map to 0 to avoid runtime warning 1722 depth_maps_gaps[:, :, i][np.nan_to_num(thickness_map) <= 0.0] = np.nan 1723 1724 return merged, depth_maps_gaps
Re-interpolates the depth maps using the faulted geologic age cube
Parameters
- unfaulted_geologic_age (np.ndarray): The unfaulted geologic age cube
- faulted_geologic_age (np.ndarray): The faulted geologic age cube
- onlap_clips (np.ndarray): The onlap clips
Returns
- depth_maps (np.ndarray): The improved depth maps
- depth_maps_gaps (np.ndarray): The improved depth maps with gaps
1726 @staticmethod 1727 def partial_faulting( 1728 depth_map_gaps_faulted, 1729 fault_plane_classification, 1730 faulted_depth_map, 1731 ii, 1732 jj, 1733 max_throw, 1734 origtime_cube, 1735 unfaulted_depth_map, 1736 ): 1737 """ 1738 Partial faulting 1739 ---------------- 1740 1741 Executes partial faluting. 1742 1743 The docstring of this function is a work in progress. 1744 1745 Parameters 1746 ---------- 1747 depth_map_gaps_faulted : np.ndarray 1748 The depth map. 1749 fault_plane_classification : np.ndarray 1750 Fault plane classifications. 1751 faulted_depth_map : _type_ 1752 The faulted depth map. 1753 ii : int 1754 The i position 1755 jj : int 1756 The j position 1757 max_throw : float 1758 The maximum amount of throw for the faults. 1759 origtime_cube : np.ndarray 1760 Original time cube. 1761 unfaulted_depth_map : np.ndarray 1762 Unfaulted depth map. 1763 1764 Returns 1765 ------- 1766 faulted_depth_map : np.ndarray 1767 The faulted depth map 1768 depth_map_gaps_faulted : np.ndarray 1769 Depth map with gaps filled 1770 """ 1771 for ithrow in range(1, int(max_throw) + 1): 1772 # infilled 1773 partial_faulting_map = unfaulted_depth_map + ithrow 1774 partial_faulting_map_ii_jj = ( 1775 partial_faulting_map[ii, jj] 1776 .astype("int") 1777 .clip(0, origtime_cube.shape[2] - 1) 1778 ) 1779 partial_faulting_on_horizon = fault_plane_classification[ 1780 ii, jj, partial_faulting_map_ii_jj 1781 ][..., 0] 1782 origtime_on_horizon = origtime_cube[ii, jj, partial_faulting_map_ii_jj][ 1783 ..., 0 1784 ] 1785 faulted_depth_map[partial_faulting_on_horizon == 1] = np.dstack( 1786 (origtime_on_horizon, partial_faulting_map) 1787 ).min(axis=-1)[partial_faulting_on_horizon == 1] 1788 # gaps 1789 depth_map_gaps_faulted[partial_faulting_on_horizon == 1] = np.nan 1790 return faulted_depth_map, depth_map_gaps_faulted
Partial faulting
Executes partial faluting.
The docstring of this function is a work in progress.
Parameters
- depth_map_gaps_faulted (np.ndarray): The depth map.
- fault_plane_classification (np.ndarray): Fault plane classifications.
- faulted_depth_map (_type_): The faulted depth map.
- ii (int): The i position
- jj (int): The j position
- max_throw (float): The maximum amount of throw for the faults.
- origtime_cube (np.ndarray): Original time cube.
- unfaulted_depth_map (np.ndarray): Unfaulted depth map.
Returns
- faulted_depth_map (np.ndarray): The faulted depth map
- depth_map_gaps_faulted (np.ndarray): Depth map with gaps filled
1792 def get_displacement_vector( 1793 self, 1794 semi_axes: tuple, 1795 origin: tuple, 1796 throw: float, 1797 tilt, 1798 wb, 1799 index, 1800 fp 1801 ): 1802 """ 1803 Gets a displacement vector. 1804 1805 Parameters 1806 ---------- 1807 semi_axes : tuple 1808 The semi axes. 1809 origin : tuple 1810 The origin. 1811 throw : float 1812 The throw of th fault to use. 1813 tilt : float 1814 The tilt of the fault. 1815 1816 Returns 1817 ------- 1818 stretch_times : np.ndarray 1819 The stretch times. 1820 stretch_times_classification : np.ndarray 1821 Stretch times classification. 1822 interpolation : bool 1823 Whether or not to interpolate. 1824 hockey_stick : int 1825 The hockey stick. 1826 fault_segments : np.ndarray 1827 1828 ellipsoid : 1829 fp : 1830 """ 1831 a, b, c = semi_axes 1832 x0, y0, z0 = origin 1833 1834 random_shear_zone_width = ( 1835 np.around(np.random.uniform(low=0.75, high=1.5) * 200, -2) / 200 1836 ) 1837 if random_shear_zone_width == 0: 1838 random_gouge_pctile = 100 1839 else: 1840 # clip amplitudes inside shear_zone with this percentile of total (100 implies doing nothing) 1841 random_gouge_pctile = np.random.triangular(left=10, mode=50, right=100) 1842 # Store the random values 1843 fp["shear_zone_width"][index] = random_shear_zone_width 1844 fp["gouge_pctile"][index] = random_gouge_pctile 1845 1846 if self.cfg.verbose: 1847 print(f" ...shear_zone_width (samples) = {random_shear_zone_width}") 1848 print(f" ...gouge_pctile (percent*100) = {random_gouge_pctile}") 1849 print(f" .... output_cube.shape = {self.vols.geologic_age.shape}") 1850 _p = ( 1851 np.arange(self.vols.geologic_age.shape[2]) * self.cfg.infill_factor 1852 ).shape 1853 print( 1854 f" .... (np.arange(output_cube.shape[2])*infill_factor).shape = {_p}" 1855 ) 1856 1857 ellipsoid = self.rotate_3d_ellipsoid(x0, y0, z0, a, b, c, tilt) 1858 fault_segments = self.get_fault_plane_sobel(ellipsoid) 1859 z_idx = self.get_fault_centre(ellipsoid, wb, fault_segments, index) 1860 1861 # Initialise return objects in case z_idx size == 0 1862 interpolation = False 1863 hockey_stick = 0 1864 # displacement_cube = None 1865 if np.size(z_idx) != 0: 1866 print(" ... Computing fault depth at max displacement") 1867 print(" ... depth at max displacement = {}".format(z_idx[2])) 1868 down = float(ellipsoid[ellipsoid < 1.0].size) / np.prod( 1869 self.vols.geologic_age[:].shape 1870 ) 1871 """ 1872 down = np.int16(len(np.where(ellipsoid < 1.)[0]) / 1.0 * (self.vols.geologic_age.shape[2] * 1873 self.vols.geologic_age.shape[1] * 1874 self.vols.geologic_age.shape[0])) 1875 print(" ... This fault has {!s} %% of downthrown samples".format(down)) 1876 """ 1877 print( 1878 " ... This fault has " 1879 + format(down, "5.1%") 1880 + " of downthrown samples" 1881 ) 1882 1883 ( 1884 stretch_times, 1885 stretch_times_classification, 1886 interpolation, 1887 hockey_stick, 1888 ) = self.xyz_dis(z_idx, throw, fault_segments, ellipsoid, wb, index) 1889 else: 1890 print(" ... Ellipsoid larger than cube no fault inserted") 1891 stretch_times = np.ones_like(ellipsoid) 1892 stretch_times_classification = np.ones_like(self.vols.geologic_age[:]) 1893 1894 max_fault_throw = self.max_fault_throw[:] 1895 max_fault_throw[ellipsoid < 1.0] += int(throw) 1896 self.max_fault_throw[:] = max_fault_throw 1897 1898 return ( 1899 stretch_times, 1900 stretch_times_classification, 1901 interpolation, 1902 hockey_stick, 1903 fault_segments, 1904 ellipsoid, 1905 fp, 1906 )
Gets a displacement vector.
Parameters
- semi_axes (tuple): The semi axes.
- origin (tuple): The origin.
- throw (float): The throw of th fault to use.
- tilt (float): The tilt of the fault.
Returns
- stretch_times (np.ndarray): The stretch times.
- stretch_times_classification (np.ndarray): Stretch times classification.
- interpolation (bool): Whether or not to interpolate.
- hockey_stick (int): The hockey stick.
fault_segments (np.ndarray):
ellipsoid ():
fp :
1908 def apply_xyz_displacement(self, traces) -> np.ndarray: 1909 """ 1910 Applies XYZ Displacement. 1911 1912 Apply stretching and squeezing previously applied to the input cube 1913 vertically to give all depths the same number of extrema. 1914 1915 This is intended to be a proxy for making the 1916 dominant frequency the same everywhere. 1917 1918 Parameters 1919 ---------- 1920 traces : np.ndarray 1921 Previously stretched/squeezed trace(s) 1922 1923 Returns 1924 ------- 1925 unstretch_traces: np.ndarray 1926 Un-stretched/un-squeezed trace(s) 1927 """ 1928 unstretch_traces = np.zeros_like(traces) 1929 origtime = np.arange(traces.shape[-1]) 1930 1931 print("\t ... Cube parameters going into interpolation") 1932 print(f"\t ... Origtime shape = {len(origtime)}") 1933 print( 1934 f"\t ... stretch_times_effects shape = {self.displacement_vectors.shape}" 1935 ) 1936 print(f"\t ... unstretch_times shape = {unstretch_traces.shape}") 1937 print(f"\t ... traces shape = {traces.shape}") 1938 1939 for i in range(traces.shape[0]): 1940 for j in range(traces.shape[1]): 1941 if traces[i, j, :].min() != traces[i, j, :].max(): 1942 unstretch_traces[i, j, :] = np.interp( 1943 self.displacement_vectors[i, j, :], origtime, traces[i, j, :] 1944 ) 1945 else: 1946 unstretch_traces[i, j, :] = traces[i, j, :] 1947 return unstretch_traces
Applies XYZ Displacement.
Apply stretching and squeezing previously applied to the input cube vertically to give all depths the same number of extrema.
This is intended to be a proxy for making the dominant frequency the same everywhere.
Parameters
- traces (np.ndarray): Previously stretched/squeezed trace(s)
Returns
- unstretch_traces (np.ndarray): Un-stretched/un-squeezed trace(s)
1949 def copy_and_divide_depth_maps_by_infill(self, zmaps) -> np.ndarray: 1950 """ 1951 Copy and divide depth maps by infill factor 1952 ------------------------------------------- 1953 1954 Copies and divides depth maps by infill factor. 1955 1956 Parameters 1957 ---------- 1958 zmaps : np.array 1959 The depth maps to copy and divide. 1960 1961 Returns 1962 ------- 1963 np.ndarray 1964 The result of the division 1965 """ 1966 return zmaps / self.cfg.infill_factor
Copy and divide depth maps by infill factor
Copies and divides depth maps by infill factor.
Parameters
- zmaps (np.array): The depth maps to copy and divide.
Returns
- np.ndarray: The result of the division
1968 def rotate_3d_ellipsoid( 1969 self, x0, y0, z0, a, b, c, fraction 1970 ) -> np.ndarray: 1971 """ 1972 Rotate a 3D ellipsoid 1973 --------------------- 1974 1975 Parameters 1976 ---------- 1977 x0 : _type_ 1978 _description_ 1979 y0 : _type_ 1980 _description_ 1981 z0 : _type_ 1982 _description_ 1983 a : _type_ 1984 _description_ 1985 b : _type_ 1986 _description_ 1987 c : _type_ 1988 _description_ 1989 fraction : _type_ 1990 _description_ 1991 """ 1992 def f(x1, y1, z1, x_0, y_0, z_0, a1, b1, c1): 1993 return ( 1994 ((x1 - x_0) ** 2) / a1 + ((y1 - y_0) ** 2) / b1 + ((z1 - z_0) ** 2) / c1 1995 ) 1996 1997 x = np.arange(self.vols.geologic_age.shape[0]).astype("float") 1998 y = np.arange(self.vols.geologic_age.shape[1]).astype("float") 1999 z = np.arange(self.vols.geologic_age.shape[2]).astype("float") 2000 2001 xx, yy, zz = np.meshgrid(x, y, z, indexing="ij", sparse=False) 2002 2003 xyz = ( 2004 np.vstack((xx.flatten(), yy.flatten(), zz.flatten())) 2005 .swapaxes(0, 1) 2006 .astype("float") 2007 ) 2008 2009 xyz_rotated = self.apply_3d_rotation( 2010 xyz, self.vols.geologic_age.shape, x0, y0, fraction 2011 ) 2012 2013 xx_rotated = xyz_rotated[:, 0].reshape(self.vols.geologic_age.shape) 2014 yy_rotated = xyz_rotated[:, 1].reshape(self.vols.geologic_age.shape) 2015 zz_rotated = xyz_rotated[:, 2].reshape(self.vols.geologic_age.shape) 2016 2017 ellipsoid = f(xx_rotated, yy_rotated, zz_rotated, x0, y0, z0, a, b, c).reshape( 2018 self.vols.geologic_age.shape 2019 ) 2020 2021 return ellipsoid
Rotate a 3D ellipsoid
Parameters
- x0 (_type_): _description_
- y0 (_type_): _description_
- z0 (_type_): _description_
- a (_type_): _description_
- b (_type_): _description_
- c (_type_): _description_
- fraction (_type_): _description_
2023 @staticmethod 2024 def apply_3d_rotation(inarray, array_shape, x0, y0, fraction): 2025 from math import sqrt, sin, cos, atan2 2026 from numpy import cross, eye 2027 2028 # expm3 deprecated, changed to 'more robust' expm (TM) 2029 from scipy.linalg import expm, norm 2030 2031 def m(axis, angle): 2032 return expm(cross(eye(3), axis / norm(axis) * angle)) 2033 2034 theta = atan2( 2035 fraction 2036 * sqrt((x0 - array_shape[0] / 2) ** 2 + (y0 - array_shape[1] / 2) ** 2), 2037 array_shape[2], 2038 ) 2039 dip_angle = atan2(y0 - array_shape[1] / 2, x0 - array_shape[0] / 2) 2040 2041 strike_unitvector = np.array( 2042 (sin(np.pi - dip_angle), cos(np.pi - dip_angle), 0.0) 2043 ) 2044 m0 = m(strike_unitvector, theta) 2045 2046 outarray = np.dot(m0, inarray.T).T 2047 2048 return outarray
2050 @staticmethod 2051 def get_fault_plane_sobel(test_ellipsoid): 2052 from scipy.ndimage import sobel 2053 from scipy.ndimage import maximum_filter 2054 2055 test_ellipsoid[test_ellipsoid <= 1.0] = 0.0 2056 inside = np.zeros_like(test_ellipsoid) 2057 inside[test_ellipsoid <= 1.0] = 1.0 2058 # method 2 2059 edge = ( 2060 np.abs(sobel(inside, axis=0)) 2061 + np.abs(sobel(inside, axis=1)) 2062 + np.abs(sobel(inside, axis=-1)) 2063 ) 2064 edge_max = maximum_filter(edge, size=(5, 5, 5)) 2065 edge_max[edge_max == 0.0] = 1e6 2066 fault_segments = edge / edge_max 2067 fault_segments[np.isnan(fault_segments)] = 0.0 2068 fault_segments[fault_segments < 0.5] = 0.0 2069 fault_segments[fault_segments > 0.5] = 1.0 2070 return fault_segments
2072 def get_fault_centre(self, ellipsoid, wb_time_map, z_on_ellipse, index): 2073 def find_nearest(array, value): 2074 idx = (np.abs(array - value)).argmin() 2075 return array[idx] 2076 2077 def intersec(ell, thresh, x, y, z): 2078 abc = np.where( 2079 (ell[x, y, z] < np.float32(1 + thresh)) 2080 & (ell[x, y, z] > np.float32(1 - thresh)) 2081 ) 2082 if np.size(abc[0]) != 0: 2083 direction = find_nearest(abc[0], 1) 2084 else: 2085 direction = 9999 2086 print( 2087 " ... computing intersection points between ellipsoid and cube, raise error if none found" 2088 ) 2089 xdir_min = intersec(ell, thresh, x, y[0], z[0]) 2090 xdir_max = intersec(ell, thresh, x, y[-1], z[0]) 2091 ydir_min = intersec(ell, thresh, x[0], y, z[0]) 2092 ydir_max = intersec(ell, thresh, x[-1], y, z[0]) 2093 2094 print(" ... xdir_min coord = ", xdir_min, y[0], z[0]) 2095 print(" ... xdir_max coord = ", xdir_max, y[-1], z[0]) 2096 print(" ... ydir_min coord = ", y[0], ydir_min, z[0]) 2097 print(" ... ydir_max coord = ", y[-1], ydir_max, z[0]) 2098 return direction 2099 2100 def get_middle_z(ellipse, wb_map, idx, verbose=False): 2101 # Retrieve indices and cube of point on the ellipsoid and under the sea bed 2102 2103 random_idx = [] 2104 do_it = True 2105 origtime = np.array(range(ellipse.shape[-1])) 2106 wb_time_cube = np.reshape( 2107 wb_map, (wb_map.shape[0], wb_map.shape[1], 1) 2108 ) * np.ones_like(origtime) 2109 abc = np.where((z_on_ellipse == 1) & ((wb_time_cube - origtime) <= 0)) 2110 xyz = np.vstack(abc) 2111 if verbose: 2112 print(" ... xyz.shape = ", xyz.shape) 2113 if np.size(abc[0]) != 0: 2114 xyz_xyz = np.array([]) 2115 threshold_center = 5 2116 while xyz_xyz.size == 0: 2117 if threshold_center < z_on_ellipse.shape[0]: 2118 z_middle = np.where( 2119 np.abs(xyz[2] - int((xyz[2].min() + xyz[2].max()) / 2)) 2120 < threshold_center 2121 ) 2122 xyz_z = xyz[:, z_middle[0]].copy() 2123 if verbose: 2124 print(" ... xyz_z.shape = ", xyz_z.shape) 2125 if xyz_z.size != 0: 2126 x_middle = np.where( 2127 np.abs( 2128 xyz_z[0] 2129 - int((xyz_z[0].min() + xyz_z[0].max()) / 2) 2130 ) 2131 < threshold_center 2132 ) 2133 xyz_xz = xyz_z[:, x_middle[0]].copy() 2134 if verbose: 2135 print(" ... xyz_xz.shape = ", xyz_xz.shape) 2136 if xyz_xz.size != 0: 2137 y_middle = np.where( 2138 np.abs( 2139 xyz_xz[1] 2140 - int((xyz_xz[1].min() + xyz_xz[1].max()) / 2) 2141 ) 2142 < threshold_center 2143 ) 2144 xyz_xyz = xyz_xz[:, y_middle[0]].copy() 2145 if verbose: 2146 print(" ... xyz_xyz.shape = ", xyz_xyz.shape) 2147 if verbose: 2148 print(" ... z for upper intersection = ", xyz[2].min()) 2149 print(" ... z for lower intersection = ", xyz[2].max()) 2150 print(" ... threshold_center used = ", threshold_center) 2151 threshold_center += 5 2152 else: 2153 print(" ... Break the loop, could not find a suitable point") 2154 random_idx = [] 2155 do_it = False 2156 break 2157 if do_it: 2158 from scipy import random 2159 2160 random_idx = xyz_xyz[:, random.choice(xyz_xyz.shape[1])] 2161 print( 2162 " ... Computing fault middle to hang max displacement function" 2163 ) 2164 print(" ... x idx for max displacement = ", random_idx[0]) 2165 print(" ... y idx for max displacement = ", random_idx[1]) 2166 print(" ... z idx for max displacement = ", random_idx[2]) 2167 print( 2168 " ... ellipsoid value = ", 2169 ellipse[random_idx[0], random_idx[1], random_idx[2]], 2170 ) 2171 else: 2172 print( 2173 " ... Empty intersection between fault and cube, assign d-max at cube lower corner" 2174 ) 2175 2176 return random_idx 2177 2178 z_idx = get_middle_z(ellipsoid, wb_time_map, index) 2179 return z_idx
2181 def xyz_dis(self, z_idx, throw, z_on_ellipse, ellipsoid, wb, index): 2182 from scipy import interpolate, signal 2183 from math import atan2, degrees 2184 from scipy.stats import multivariate_normal 2185 from scipy.ndimage.interpolation import rotate 2186 2187 cube_shape = self.vols.geologic_age.shape 2188 2189 def u_gaussian(d_max, sig, shape, points): 2190 from scipy.signal.windows import general_gaussian 2191 2192 return d_max * general_gaussian(points, shape, np.float32(sig)) 2193 2194 # Choose random values sigma, p and coef 2195 sigma = np.random.uniform(low=10 * throw - 50, high=300) 2196 p = np.random.uniform(low=1.5, high=5) 2197 coef = np.random.uniform(1.3, 1.5) 2198 2199 # Fault plane 2200 infill_factor = 0.5 2201 origtime = np.arange(cube_shape[-1]) 2202 z = np.arange(cube_shape[2]).astype("int") 2203 # Define Gaussian max throw and roll it to chosen z 2204 # Gaussian should be defined on at least 5 sigma on each side 2205 roll_int = int(10 * sigma) 2206 g = u_gaussian(throw, sigma, p, cube_shape[2] + 2 * roll_int) 2207 # Pad signal by 10*sigma before rolling 2208 g_padded_rolled = np.roll(g, np.int32(z_idx[2] + g.argmax() + roll_int)) 2209 count = 0 2210 wb_x = np.where(z_on_ellipse == 1)[0] 2211 wb_y = np.where(z_on_ellipse == 1)[1] 2212 print(" ... Taper fault so it doesn't reach seabed") 2213 print(f" ... Sea floor max = {wb[wb_x, wb_y].max()}") 2214 # Shift Z throw so that id doesn't cross sea bed 2215 while g_padded_rolled[int(roll_int + wb[wb_x, wb_y].max())] > 1: 2216 g_padded_rolled = np.roll(g_padded_rolled, 5) 2217 count += 5 2218 # Break loop if can't find spot 2219 if count > cube_shape[2] - wb[wb_x, wb_y].max(): 2220 print(" ... Too many rolled sample, seafloor will not have 0 throw") 2221 break 2222 print(f" ... Vertical throw shifted by {str(count)} samples") 2223 g_centered = g_padded_rolled[roll_int : cube_shape[2] + roll_int] 2224 2225 ff = interpolate.interp1d(z, g_centered) 2226 z_shift = ff 2227 print(" ... Computing Gaussian distribution function") 2228 print(f" ... Max displacement = {int(throw)}") 2229 print(f" ... Sigma = {int(sigma)}") 2230 print(f" ... P = {int(p)}") 2231 2232 low_fault_throw = 5 2233 high_fault_throw = 35 2234 # Parameters to set ratio of 1.4 seems to be optimal for a 1500x1500 grid 2235 mu_x = 0 2236 mu_y = 0 2237 2238 # Use throw to get length 2239 throw_range = np.arange(low_fault_throw, high_fault_throw, 1) 2240 # Max throw vs length relationship 2241 fault_length = np.power(0.0013 * throw_range, 1.3258) 2242 # Max throw == 16000 2243 scale_factor = 16000 / fault_length[-1] 2244 # Coef random selection moved to top of function 2245 variance_x = scale_factor * fault_length[np.where(throw_range == int(throw))] 2246 variance_y = variance_x * coef 2247 # Do the same for the drag zone area 2248 fault_length_drag = fault_length / 10000 2249 variance_x_drag = ( 2250 scale_factor * fault_length_drag[np.where(throw_range == int(throw))] 2251 ) 2252 variance_y_drag = variance_x_drag * coef 2253 # Rotation from z_idx to center 2254 alpha = atan2(z_idx[1] - cube_shape[1] / 2, z_idx[0] - cube_shape[0] / 2) 2255 print(f" ... Variance_x, Variance_y = {variance_x} {variance_y}") 2256 print( 2257 f" ... Angle between max displacement point tangent plane and cube = {int(degrees(alpha))} Degrees" 2258 ) 2259 print(f" ... Max displacement point at x,y,z = {z_idx}") 2260 2261 # Create grid and multivariate normal 2262 x = np.linspace( 2263 -int(cube_shape[0] + 1.5 * cube_shape[0]), 2264 int(cube_shape[0] + 1.5 * cube_shape[0]), 2265 2 * int(cube_shape[0] + 1.5 * cube_shape[0]), 2266 ) 2267 y = np.linspace( 2268 -int(cube_shape[1] + 1.5 * cube_shape[1]), 2269 int(cube_shape[1] + 1.5 * cube_shape[1]), 2270 2 * int(cube_shape[1] + 1.5 * cube_shape[1]), 2271 ) 2272 _x, _y = np.meshgrid(x, y) 2273 pos = np.empty(_x.shape + (2,)) 2274 pos[:, :, 0] = _x 2275 pos[:, :, 1] = _y 2276 rv = multivariate_normal([mu_x, mu_y], [[variance_x, 0], [0, variance_y]]) 2277 rv_drag = multivariate_normal( 2278 [mu_x, mu_y], [[variance_x_drag, 0], [0, variance_y_drag]] 2279 ) 2280 # Scale up by mu order of magnitude and swap axes 2281 xy_dis = 10000 * (rv.pdf(pos)) 2282 xy_dis_drag = 10000 * (rv_drag.pdf(pos)) 2283 2284 # Normalize 2285 xy_dis = xy_dis / np.amax(xy_dis.flatten()) 2286 xy_dis_drag = (xy_dis_drag / np.amax(xy_dis_drag.flatten())) * 0.99 2287 2288 # Rotate plane by alpha 2289 x = np.linspace(0, cube_shape[0], cube_shape[0]) 2290 y = np.linspace(0, cube_shape[1], cube_shape[1]) 2291 _x, _y = np.meshgrid(x, y) 2292 xy_dis_rotated = np.zeros_like(xy_dis) 2293 xy_dis_drag_rotated = np.zeros_like(xy_dis) 2294 rotate( 2295 xy_dis_drag, 2296 degrees(alpha), 2297 reshape=False, 2298 output=xy_dis_drag_rotated, 2299 mode="nearest", 2300 ) 2301 rotate( 2302 xy_dis, degrees(alpha), reshape=False, output=xy_dis_rotated, mode="nearest" 2303 ) 2304 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2305 xy_dis = xy_dis[ 2306 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2307 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2308 ].copy() 2309 xy_dis_drag = xy_dis_drag[ 2310 int(cube_shape[0]) * 2 : cube_shape[0] + int(cube_shape[0]) * 2, 2311 int(cube_shape[1]) * 2 : cube_shape[1] + int(cube_shape[1]) * 2, 2312 ].copy() 2313 2314 # taper edges of xy_dis_drag in 2d to avoid edge artifacts in fft 2315 print(" ... xy_dis_drag.shape = " + str(xy_dis_drag.shape)) 2316 print( 2317 " ... xy_dis_drag[xy_dis_drag>0.].size = " 2318 + str(xy_dis_drag[xy_dis_drag > 0.0].size) 2319 ) 2320 print( 2321 " ... xy_dis_drag.shape min/mean/max= " 2322 + str((xy_dis_drag.min(), xy_dis_drag.mean(), xy_dis_drag.max())) 2323 ) 2324 try: 2325 self.plot_counter += 1 2326 except: 2327 self.plot_counter = 0 2328 2329 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2330 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2331 2332 # Normalize 2333 # xy_dis_rotated = rotate(xy_dis, degrees(alpha), mode='constant') 2334 print(" ...", xy_dis_rotated.shape, xy_dis.shape) 2335 # Normalize 2336 new_matrix = xy_dis_rotated 2337 xy_dis_norm = xy_dis 2338 x_center = np.where(new_matrix == new_matrix.max())[0][0] 2339 y_center = np.where(new_matrix == new_matrix.max())[1][0] 2340 2341 # Pad event and move it to maximum depth location 2342 x_pad = 0 2343 y_pad = 0 2344 x_roll = z_idx[0] - x_center 2345 y_roll = z_idx[1] - y_center 2346 print(f" ... padding x,y and roll x,y = {x_pad} {y_pad} {x_roll} {y_roll}") 2347 print( 2348 f" ... Max displacement point before rotation and adding of padding x,y = {x_center} {y_center}" 2349 ) 2350 new_matrix = np.lib.pad( 2351 new_matrix, ((abs(x_pad), abs(x_pad)), (abs(y_pad), abs(y_pad))), "edge" 2352 ) 2353 new_matrix = np.roll(new_matrix, int(x_roll), axis=0) 2354 new_matrix = np.roll(new_matrix, int(y_roll), axis=1) 2355 new_matrix = new_matrix[ 2356 abs(x_pad) : cube_shape[0] + abs(x_pad), 2357 abs(y_pad) : cube_shape[1] + abs(y_pad), 2358 ] 2359 # Check that fault center is at the right place 2360 x_center = np.where(new_matrix == new_matrix.max())[0] 2361 y_center = np.where(new_matrix == new_matrix.max())[1] 2362 print( 2363 f" ... Max displacement point after rotation and removal of padding x,y = {x_center} {y_center}" 2364 ) 2365 print(f" ... z_idx = {z_idx[0]}, {z_idx[1]}") 2366 print( 2367 f" ... Difference from origin z_idx = {x_center - z_idx[0]}, {y_center - z_idx[1]}" 2368 ) 2369 2370 # Build cube of lateral variable displacement 2371 xy_dis = new_matrix.reshape(cube_shape[0], cube_shape[1], 1) 2372 # Get enlarged fault plane 2373 bb = np.zeros_like(ellipsoid) 2374 for j in range(bb.shape[-1]): 2375 bb[:, :, j] = j 2376 stretch_times_effects_drag = bb - xy_dis * z_shift(range(cube_shape[2])) 2377 fault_plane_classification = np.where( 2378 (z_on_ellipse == 1) 2379 & ((origtime - stretch_times_effects_drag) > infill_factor), 2380 1, 2381 0, 2382 ) 2383 hockey_stick = 0 2384 # Define as 0's, to be updated if necessary 2385 fault_plane_classification_drag = np.zeros_like(z_on_ellipse) 2386 if throw >= 0.85 * high_fault_throw: 2387 hockey_stick = 1 2388 # Check for non zero fault_plane_classification, to avoid division by 0 2389 if np.count_nonzero(fault_plane_classification) > 0: 2390 # Generate Hockey sticks by convolving small xy displacement with fault segment 2391 fault_plane_classification_drag = signal.fftconvolve( 2392 fault_plane_classification, 2393 np.reshape(xy_dis_drag, (cube_shape[0], cube_shape[1], 1)), 2394 mode="same", 2395 ) 2396 fault_plane_classification_drag = ( 2397 fault_plane_classification_drag 2398 / np.amax(fault_plane_classification_drag.flatten()) 2399 ) 2400 2401 xy_dis = xy_dis * np.ones_like(self.vols.geologic_age) 2402 xy_dis_classification = xy_dis.copy() 2403 interpolation = True 2404 2405 xy_dis = xy_dis - fault_plane_classification_drag 2406 xy_dis = np.where(xy_dis < 0, 0, xy_dis) 2407 stretch_times = xy_dis * z_shift(range(cube_shape[2])) 2408 stretch_times_classification = xy_dis_classification * z_shift( 2409 range(cube_shape[2]) 2410 ) 2411 2412 if self.cfg.qc_plots: 2413 self.fault_summary_plot( 2414 ff, 2415 z, 2416 throw, 2417 sigma, 2418 _x, 2419 _y, 2420 xy_dis_norm, 2421 ellipsoid, 2422 z_idx, 2423 xy_dis, 2424 index, 2425 alpha, 2426 ) 2427 2428 return stretch_times, stretch_times_classification, interpolation, hockey_stick
2430 def fault_summary_plot( 2431 self, 2432 ff, 2433 z, 2434 throw, 2435 sigma, 2436 x, 2437 y, 2438 xy_dis_norm, 2439 ellipsoid, 2440 z_idx, 2441 xy_dis, 2442 index, 2443 alpha, 2444 ) -> None: 2445 """ 2446 Fault Summary Plot 2447 ------------------ 2448 2449 Generates a fault summary plot. 2450 2451 Parameters 2452 ---------- 2453 ff : _type_ 2454 _description_ 2455 z : _type_ 2456 _description_ 2457 throw : _type_ 2458 _description_ 2459 sigma : _type_ 2460 _description_ 2461 x : _type_ 2462 _description_ 2463 y : _type_ 2464 _description_ 2465 xy_dis_norm : _type_ 2466 _description_ 2467 ellipsoid : _type_ 2468 _description_ 2469 z_idx : _type_ 2470 _description_ 2471 xy_dis : _type_ 2472 _description_ 2473 index : _type_ 2474 _description_ 2475 alpha : _type_ 2476 _description_ 2477 2478 Returns 2479 ------- 2480 None 2481 """ 2482 import os 2483 from math import degrees 2484 from datagenerator.util import import_matplotlib 2485 2486 plt = import_matplotlib() 2487 # Import axes3d, required to create plot with projection='3d' below. DO NOT REMOVE! 2488 from mpl_toolkits.mplot3d import axes3d 2489 from mpl_toolkits.axes_grid1 import make_axes_locatable 2490 2491 # Make summary picture 2492 fig, axes = plt.subplots(nrows=2, ncols=2) 2493 ax0, ax1, ax2, ax3 = axes.flatten() 2494 fig.set_size_inches(10, 8) 2495 # PLot Z displacement 2496 ax0.plot(ff(z)) 2497 ax0.set_title("Fault with throw %s and sigma %s" % (throw, sigma)) 2498 ax0.set_xlabel("Z axis") 2499 ax0.set_ylabel("Throw") 2500 # Plot 3D XY displacement 2501 ax1.axis("off") 2502 ax1 = fig.add_subplot(222, projection="3d") 2503 ax1.plot_surface(x, y, xy_dis_norm, cmap="Spectral", linewidth=0) 2504 ax1.set_xlabel("X axis") 2505 ax1.set_ylabel("Y axis") 2506 ax1.set_zlabel("Throw fraction") 2507 ax1.set_title("3D XY displacement") 2508 # Ellipsoid location 2509 weights = ellipsoid[:, :, z_idx[2]] 2510 # Plot un-rotated XY displacement 2511 cax2 = ax2.imshow(np.rot90(xy_dis_norm, 3)) 2512 # Levels for imshow contour 2513 levels = np.arange(0, 1.1, 0.1) 2514 # Plot contour 2515 ax2.contour(np.rot90(xy_dis_norm, 3), levels, colors="k", linestyles="-") 2516 divider = make_axes_locatable(ax2) 2517 cax4 = divider.append_axes("right", size="5%", pad=0.05) 2518 cbar2 = fig.colorbar(cax2, cax=cax4) 2519 ax2.set_xlabel("X axis") 2520 ax2.set_ylabel("Y axis") 2521 ax2.set_title("2D projection of XY displacement unrotated") 2522 ############################################################ 2523 # Plot rotated displacement and contour 2524 cax3 = ax3.imshow(np.rot90(xy_dis[:, :, z_idx[2]], 3)) 2525 ax3.contour( 2526 np.rot90(xy_dis[:, :, z_idx[2]], 3), levels, colors="k", linestyles="-" 2527 ) 2528 # Add ellipsoid shape 2529 ax3.contour(np.rot90(weights, 3), levels=[1], colors="r", linestyles="-") 2530 divider = make_axes_locatable(ax3) 2531 cax5 = divider.append_axes("right", size="5%", pad=0.05) 2532 cbar3 = fig.colorbar(cax3, cax=cax5) 2533 ax3.set_xlabel("X axis") 2534 ax3.set_ylabel("Y axis") 2535 ax3.set_title( 2536 "2D projection of XY displacement rotated by %s degrees" 2537 % (int(degrees(alpha))) 2538 ) 2539 plt.suptitle( 2540 "XYZ displacement parameters for fault Nr %s" % str(index), 2541 fontweight="bold", 2542 ) 2543 fig.tight_layout() 2544 fig.subplots_adjust(top=0.94) 2545 image_path = os.path.join( 2546 self.cfg.work_subfolder, "QC_plot__Fault_%s.png" % str(index) 2547 ) 2548 plt.savefig(image_path, format="png") 2549 plt.close(fig)
Fault Summary Plot
Generates a fault summary plot.
Parameters
- ff (_type_): _description_
- z (_type_): _description_
- throw (_type_): _description_
- sigma (_type_): _description_
- x (_type_): _description_
- y (_type_): _description_
- xy_dis_norm (_type_): _description_
- ellipsoid (_type_): _description_
- z_idx (_type_): _description_
- xy_dis (_type_): _description_
- index (_type_): _description_
- alpha (_type_): _description_
Returns
- None
2556 def reassign_channel_segment_encoding( 2557 self, 2558 faulted_age, 2559 floodplain_shale, 2560 channel_fill, 2561 shale_channel_drape, 2562 levee, 2563 crevasse, 2564 channel_flag_lut, 2565 ): 2566 # Generate list of horizons with channel episodes 2567 channel_segments = np.zeros_like(floodplain_shale) 2568 channel_horizons_list = list() 2569 for i in range(self.faulted_depth_maps.shape[2] - 7): 2570 if channel_flag_lut[i] == 1: 2571 channel_horizons_list.append(i) 2572 channel_horizons_list.append(999) 2573 2574 # Re-assign correct channel segments encoding 2575 for i, iLayer in enumerate(channel_horizons_list[:-1]): 2576 if iLayer != 0 and iLayer < self.faulted_depth_maps.shape[-1]: 2577 # loop through channel facies 2578 # --- 0 is floodplain shale 2579 # --- 1 is channel fill (sand) 2580 # --- 2 is shale channel drape 2581 # --- 3 is levee (mid quality sand) 2582 # --- 4 is crevasse (low quality sand) 2583 for j in range(1000, 4001, 1000): 2584 print( 2585 " ... re-assign channel segments after faulting: i, iLayer, j = ", 2586 i, 2587 iLayer, 2588 j, 2589 ) 2590 channel_facies_code = j + iLayer 2591 layers_mask = np.where( 2592 ( 2593 (faulted_age >= iLayer) 2594 & (faulted_age < channel_horizons_list[i + 1]) 2595 ), 2596 1, 2597 0, 2598 ) 2599 if j == 1000: 2600 channel_segments[ 2601 np.logical_and(layers_mask == 1, channel_fill == 1) 2602 ] = channel_facies_code 2603 faulted_age[ 2604 np.logical_and(layers_mask == 1, channel_fill == 1) 2605 ] = channel_facies_code 2606 elif j == 2000: 2607 channel_segments[ 2608 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2609 ] = channel_facies_code 2610 faulted_age[ 2611 np.logical_and(layers_mask == 1, shale_channel_drape == 1) 2612 ] = channel_facies_code 2613 elif j == 3000: 2614 channel_segments[ 2615 np.logical_and(layers_mask == 1, levee == 1) 2616 ] = channel_facies_code 2617 faulted_age[ 2618 np.logical_and(layers_mask == 1, levee == 1) 2619 ] = channel_facies_code 2620 elif j == 4000: 2621 channel_segments[ 2622 np.logical_and(layers_mask == 1, crevasse == 1) 2623 ] = channel_facies_code 2624 faulted_age[ 2625 np.logical_and(layers_mask == 1, crevasse == 1) 2626 ] = channel_facies_code 2627 print(" ... finished Re-assign correct channel segments") 2628 return channel_segments, faulted_age
Inherited Members
3205def find_zero_thickness_onlapping_layers(z, onlap_list): 3206 onlap_zero_z = dict() 3207 for layer in onlap_list: 3208 for x in range(layer, 1, -1): 3209 thickness = z[..., layer] - z[..., x - 1] 3210 zeros = np.where(thickness == 0.0) 3211 if zeros[0].size > 0: 3212 onlap_zero_z[f"{layer},{x-1}"] = zeros 3213 return onlap_zero_z
3216def fix_zero_thickness_fan_layers(z, layer_number, thickness): 3217 """ 3218 Clip fan layers to horizon below the fan layer in areas where the fan thickness is zero 3219 3220 Parameters 3221 ---------- 3222 z : ndarray - depth maps 3223 layer_number : 1d array - horizon numbers which contain fans 3224 thickness : tuple of ndarrays - original thickness maps of the fans 3225 3226 Returns 3227 ------- 3228 zmaps : ndarray - depth maps with fan layers clipped to lower horizons where thicknesses is zero 3229 """ 3230 zmaps = z.copy() 3231 zmaps[..., layer_number][np.where(thickness == 0.0)] = zmaps[..., layer_number + 1][ 3232 np.where(thickness == 0.0) 3233 ] 3234 return zmaps
Clip fan layers to horizon below the fan layer in areas where the fan thickness is zero
Parameters
z (ndarray - depth maps):
layer_number (1d array - horizon numbers which contain fans):
thickness (tuple of ndarrays - original thickness maps of the fans):
Returns
- zmaps (ndarray - depth maps with fan layers clipped to lower horizons where thicknesses is zero):
3237def fix_zero_thickness_onlap_layers( 3238 faulted_depth_maps: np.ndarray, 3239 onlap_dict: dict 3240) -> np.ndarray: 3241 """fix_zero_thickness_onlap_layers _summary_ 3242 3243 Parameters 3244 ---------- 3245 faulted_depth_maps : np.ndarray 3246 The depth maps with faults 3247 onlap_dict : dict 3248 Onlaps dictionary 3249 3250 Returns 3251 ------- 3252 zmaps : np.ndarray 3253 Fixed depth maps 3254 """ 3255 zmaps = faulted_depth_maps.copy() 3256 for layers, idx in onlap_dict.items(): 3257 onlap_layer = int(str(layers).split(",")[0]) 3258 layer_to_clip = int(str(layers).split(",")[1]) 3259 zmaps[..., layer_to_clip][idx] = zmaps[..., onlap_layer][idx] 3260 3261 return zmaps
fix_zero_thickness_onlap_layers _summary_
Parameters
- faulted_depth_maps (np.ndarray): The depth maps with faults
- onlap_dict (dict): Onlaps dictionary
Returns
- zmaps (np.ndarray): Fixed depth maps