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
Faults( parameters: datagenerator.Parameters.Parameters, unfaulted_depth_maps: numpy.ndarray, onlap_horizon_list: numpy.ndarray, geomodels: datagenerator.Geomodels.Geomodel, fan_horizon_list: numpy.ndarray, fan_thickness: numpy.ndarray)
 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.
def apply_faulting_to_geomodels_and_depth_maps(self) -> None:
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
def build_faulted_property_geomodels(self, facies: numpy.ndarray) -> 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
def create_qc_plots(self) -> 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
def generate_faults(self) -> numpy.ndarray:
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
def fault_parameters(self):
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.
def build_faults(self, fp: dict, verbose=False):
 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
def improve_depth_maps_post_faulting( self, unfaulted_geologic_age: numpy.ndarray, faulted_geologic_age: numpy.ndarray, onlap_clips: numpy.ndarray):
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
@staticmethod
def partial_faulting( depth_map_gaps_faulted, fault_plane_classification, faulted_depth_map, ii, jj, max_throw, origtime_cube, unfaulted_depth_map):
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
def get_displacement_vector( self, semi_axes: tuple, origin: tuple, throw: float, tilt, wb, index, fp):
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 :

def apply_xyz_displacement(self, traces) -> numpy.ndarray:
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)
def copy_and_divide_depth_maps_by_infill(self, zmaps) -> numpy.ndarray:
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
def rotate_3d_ellipsoid(self, x0, y0, z0, a, b, c, fraction) -> numpy.ndarray:
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_
@staticmethod
def apply_3d_rotation(inarray, array_shape, x0, y0, fraction):
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
@staticmethod
def get_fault_plane_sobel(test_ellipsoid):
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
def get_fault_centre(self, ellipsoid, wb_time_map, z_on_ellipse, index):
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
def xyz_dis(self, z_idx, throw, z_on_ellipse, ellipsoid, wb, index):
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
def fault_summary_plot( self, ff, z, throw, sigma, x, y, xy_dis_norm, ellipsoid, z_idx, xy_dis, index, alpha) -> None:
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
@staticmethod
def create_binary_segmentations_post_faulting(cube, segmentation_threshold):
2551    @staticmethod
2552    def create_binary_segmentations_post_faulting(cube, segmentation_threshold):
2553        cube[cube >= segmentation_threshold] = 1.0
2554        return cube
def reassign_channel_segment_encoding( self, faulted_age, floodplain_shale, channel_fill, shale_channel_drape, levee, crevasse, channel_flag_lut):
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
def find_zero_thickness_onlapping_layers(z, onlap_list):
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
def fix_zero_thickness_fan_layers(z, layer_number, thickness):
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):
def fix_zero_thickness_onlap_layers(faulted_depth_maps: numpy.ndarray, onlap_dict: dict) -> numpy.ndarray:
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