datagenerator.Seismic

   1import os
   2import itertools
   3from multiprocessing import Pool
   4import numpy as np
   5
   6from tqdm import trange
   7from datagenerator.histogram_equalizer import normalize_seismic
   8from datagenerator.Geomodels import Geomodel
   9from datagenerator.util import write_data_to_hdf
  10from datagenerator.wavelets import generate_wavelet, plot_wavelets
  11from rockphysics.RockPropertyModels import select_rpm, RockProperties, EndMemberMixing
  12
  13
  14class SeismicVolume(Geomodel):
  15    """
  16    SeismicVolume Class
  17    -------------------
  18
  19    This class initializes the seismic volume that contains everything.
  20    """
  21    def __init__(self, parameters, faults, closures) -> None:
  22        """
  23        __init__
  24        --------
  25
  26        The initialization function.
  27
  28        Parameters
  29        ----------
  30        parameters : datagenerator.Parameters
  31            The parameters object of the project.
  32        faults : np.ndarray
  33            The faults array.
  34        closures : np.ndarray
  35            The closures array.
  36        
  37        Returns
  38        -------
  39        None
  40        """
  41        self.cfg = parameters
  42        self.faults = faults
  43        self.traps = closures
  44
  45        if self.cfg.model_qc_volumes:
  46            # Add 0 and 45 degree angles to the list of user-input angles
  47            self.angles = tuple(sorted(set((0,) + self.cfg.incident_angles + (45,))))
  48        else:
  49            self.angles = self.cfg.incident_angles
  50        if self.cfg.verbose:
  51            print(f"Angles: {self.angles}")
  52
  53        self.first_random_lyr = 20  # do not randomise shallow layers
  54
  55        self.rho = self.cfg.hdf_init(
  56            "rho", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  57        )
  58        self.vp = self.cfg.hdf_init(
  59            "vp", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  60        )
  61        self.vs = self.cfg.hdf_init(
  62            "vs", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  63        )
  64        self.rho_ff = self.cfg.hdf_init(
  65            "rho_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  66        )
  67        self.vp_ff = self.cfg.hdf_init(
  68            "vp_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  69        )
  70        self.vs_ff = self.cfg.hdf_init(
  71            "vs_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  72        )
  73
  74        seis_shape = (
  75            len(self.angles),
  76            *self.cfg.h5file.root.ModelData.faulted_depth.shape,
  77        )
  78        rfc_shape = (seis_shape[0], seis_shape[1], seis_shape[2], seis_shape[3] - 1)
  79        self.rfc_raw = self.cfg.hdf_init("rfc_raw", shape=rfc_shape)
  80        self.rfc_noise_added = self.cfg.hdf_init("rfc_noise_added", shape=rfc_shape)
  81
  82    def build_elastic_properties(self, mixing_method="inv_vel"):
  83        """
  84        Build Elastic Properties
  85        ------------------------
  86
  87        Creates Rho, Vp, Vs volumes using depth trends.
  88
  89        Parameters
  90        ----------
  91        mixing_method : str, optional
  92            Method for mixing Velocities. Defaults to "inv_vel".
  93
  94        """
  95        rpm = select_rpm(self.cfg)
  96        self.build_property_models_randomised_depth(
  97            rpm,
  98            mixing_method=mixing_method
  99        )
 100
 101    def bandlimit_volumes_wavelets(self, n_wavelets=10):
 102        # Pre-prepared n, m, f filter_specs from npy file
 103        fs_nr = np.load(self.cfg.wavelets[0])
 104        fs_md = np.load(self.cfg.wavelets[1])
 105        fs_fr = np.load(self.cfg.wavelets[2])
 106
 107        for wavelet_count in range(n_wavelets):
 108            low_freq_pctile = np.random.uniform(0, 100)
 109            mid_freq_pctile = np.random.uniform(0, 100)
 110            high_freq_pctile = np.random.uniform(0, 100)
 111            low_med_high_percentiles = (
 112                low_freq_pctile,
 113                mid_freq_pctile,
 114                high_freq_pctile,
 115            )
 116            print(f"low_med_high_percentiles: {low_med_high_percentiles}")
 117            f_nr, a_nr, w_nr = generate_wavelet(fs_nr, low_med_high_percentiles)
 118            f_md, a_md, w_md = generate_wavelet(fs_md, low_med_high_percentiles)
 119            f_fr, a_fr, w_fr = generate_wavelet(fs_fr, low_med_high_percentiles)
 120            _f = (f_nr, f_md, f_fr)
 121            _a = (a_nr, a_md, a_fr)
 122            _w = (w_nr, w_md, w_fr)
 123            labels = ["Near", "Mid", "Far"]
 124            pngname = os.path.join(
 125                self.cfg.work_subfolder, f"random_wavelets_{wavelet_count}.png"
 126            )
 127            plot_wavelets(_f, _a, _w, labels, pngname)
 128
 129            rfc_bandlimited_wavelet = np.zeros_like(self.rfc_noise_added)
 130            if self.cfg.model_qc_volumes:
 131                wavelets = [
 132                    w_nr,
 133                    w_nr,
 134                    w_md,
 135                    w_fr,
 136                    w_fr,
 137                ]  # use near for 0 and far for 45 deg cubes
 138            else:
 139                wavelets = [w_nr, w_md, w_fr]
 140
 141            for idx in range(rfc_bandlimited_wavelet.shape[0]):
 142                rfc_bandlimited_wavelet[idx, ...] = apply_wavelet(
 143                    self.rfc_noise_added[idx, ...], wavelets[idx]
 144                )
 145            if self.cfg.lateral_filter_size > 1:
 146                # Apply lateral smoothing filter
 147                rfc_bandlimited_wavelet = self.apply_lateral_filter(
 148                    rfc_bandlimited_wavelet
 149                )
 150            cumsum_wavelet_noise_added = self.apply_cumsum(rfc_bandlimited_wavelet)
 151            _ = self.write_final_cubes_to_disk(
 152                rfc_bandlimited_wavelet, f"seismicCubes_RFC_Wavelet_{wavelet_count}_"
 153            )
 154            _ = self.write_final_cubes_to_disk(
 155                cumsum_wavelet_noise_added,
 156                f"seismicCubes_cumsum_Wavelet_{wavelet_count}_",
 157            )
 158
 159    def postprocess_rfc_cubes(
 160        self, rfc_data, filename_suffix="", bb=False, stack=False
 161    ):
 162        """Postprocess the RFC cubes."""
 163        if bb:
 164            bandlimited = self.apply_bandlimits(rfc_data, (4.0, 90.0))
 165        else:
 166            bandlimited = self.apply_bandlimits(rfc_data)
 167        if self.cfg.lateral_filter_size > 1:
 168            bandlimited = self.apply_lateral_filter(bandlimited)
 169        if filename_suffix == "":
 170            fname = "seismicCubes_RFC_"
 171        else:
 172            fname = f"seismicCubes_RFC_{filename_suffix}_"
 173        _ = self.write_final_cubes_to_disk(bandlimited, fname)
 174        if stack:
 175            rfc_fullstack = self.stack_substacks(
 176                [bandlimited[x, ...] for x, _ in enumerate(self.angles)]
 177            )
 178            rfc_fullstack_scaled = self._scale_seismic(rfc_fullstack)
 179            self.write_cube_to_disk(
 180                rfc_fullstack_scaled,
 181                f"{fname}fullstack",
 182            )
 183
 184        cumsum = self.apply_cumsum(bandlimited)
 185        if filename_suffix == "":
 186            fname = "seismicCubes_cumsum_"
 187        else:
 188            fname = f"seismicCubes_cumsum_{filename_suffix}_"
 189        normalised_cumsum = self.write_final_cubes_to_disk(cumsum, fname)
 190        if stack:
 191            rai_fullstack = self.stack_substacks(
 192                [cumsum[x, ...] for x, _ in enumerate(self.angles)]
 193            )
 194            rai_fullstack_scaled = self._scale_seismic(rai_fullstack)
 195            self.write_cube_to_disk(
 196                rai_fullstack_scaled,
 197                f"{fname}fullstack",
 198            )
 199        return normalised_cumsum
 200
 201    def apply_augmentations(self, data, name="cumsum"):
 202        seabed = self.faults.faulted_depth_maps[..., 0] / self.cfg.digi
 203        augmented_data, augmented_labels = self.augment_data_and_labels(data, seabed)
 204        for i, ang in enumerate(self.cfg.incident_angles):
 205            data = augmented_data[i, ...]
 206            fname = f"seismicCubes_{name}_{ang}_degrees_normalized_augmented"
 207            self.write_cube_to_disk(data, fname)
 208            self.write_cube_to_disk(augmented_labels, "hc_closures_augmented")
 209
 210    def apply_rmo(self, data, name="cumsum_RMO"):
 211        """Apply RMO to the cumsum with noise. Note rmo application expects angle in last dimension"""
 212        rmo_indices = self.compute_randomRMO_1D(
 213            data.shape[-1], self.cfg.incident_angles, velocity_fraction=0.015
 214        )
 215        rmo_applied_data = np.zeros_like(np.moveaxis(data, 0, -1))
 216        for iline in range(self.cfg.cube_shape[0]):
 217            for xline in range(self.cfg.cube_shape[1]):
 218                rmo_applied_data[iline, xline, ...] = self.apply_rmo_augmentation(
 219                    np.moveaxis(data, 0, -1)[iline, xline, ...],
 220                    rmo_indices,
 221                    remove_mean_rmo=True,
 222                )
 223        rmo_applied_data = np.moveaxis(
 224            rmo_applied_data, -1, 0
 225        )  # Put the angles back into 1st dimension
 226        normalised_rmo_applied_data = self.write_final_cubes_to_disk(
 227            rmo_applied_data, f"seismicCubes_{name}_"
 228        )
 229        return normalised_rmo_applied_data
 230
 231    def build_seismic_volumes(self):
 232        """Build Seismic volumes.
 233
 234        * Use Zoeppritz to create RFC volumes for each required angle, add 0 & 45 degree stacks for QC
 235        * Add exponential weighted noise to the angle stacks
 236        * Apply bandpass filter
 237        * Apply lateral filter
 238        * Calculate cumsum
 239        * Create full stacks by stacking the user-input angles (i.e. not including 0, 45 degree stacks)
 240        * Write seismic volumes to disk
 241        """
 242        if self.cfg.verbose:
 243            print(
 244                f"Generating 3D Rock Properties Rho, Vp and Vs using depth trends for project {self.cfg.project}"
 245            )
 246        # Calculate Raw RFC from rho, vp, vs using Zoeppritz & write to HdF
 247        self.create_rfc_volumes()
 248        self.add_weighted_noise(self.faults.faulted_depth_maps)
 249        if hasattr(self.cfg, "wavelets"):
 250            self.bandlimit_volumes_wavelets(n_wavelets=1)
 251        normalised_cumsum = self.postprocess_rfc_cubes(
 252            self.rfc_noise_added[:], "", stack=True
 253        )
 254        self.apply_augmentations(normalised_cumsum, name="cumsum")
 255        if self.cfg.model_qc_volumes:
 256            _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free", bb=False)
 257            if self.cfg.broadband_qc_volume:
 258                _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free_bb", bb=True)
 259            normalised_cumsum_rmo = self.apply_rmo(normalised_cumsum)
 260            self.apply_augmentations(normalised_cumsum_rmo, name="cumsum_RMO")
 261
 262    def _scale_seismic(self, data):
 263        """Apply scaling factor to final seismic data volumes"""
 264        if data.ndim == 4:
 265            avg_stdev = np.mean([data[x, ...].std() for x in range(data.shape[0])])
 266        elif data.ndim == 3:
 267            avg_stdev = data.std()
 268        else:
 269            print(
 270                f"Input Data has {data.ndim} dimensions, should have 3 or 4 dimensions"
 271            )
 272        scaled_data = data * 100 / avg_stdev
 273        return scaled_data
 274
 275    def write_final_cubes_to_disk(self, dat, name):
 276        scaled_data = self._scale_seismic(dat)
 277
 278        # Apply scaling factors to N, M, F cubes from csv file before determining clip limits
 279        factor_near = self.cfg.rpm_scaling_factors["nearfactor"]
 280        factor_mid = self.cfg.rpm_scaling_factors["midfactor"]
 281        factor_far = self.cfg.rpm_scaling_factors["farfactor"]
 282        cube_incr = 0
 283        if self.cfg.model_qc_volumes and dat.shape[0] > 3:
 284            # 0 degrees has been added at start, and 45 at end
 285            cube_incr = 1
 286        scaled_data[0 + cube_incr, ...] *= factor_near
 287        scaled_data[1 + cube_incr, ...] *= factor_mid
 288        scaled_data[2 + cube_incr, ...] *= factor_far
 289
 290        for i, ang in enumerate(self.cfg.incident_angles):
 291            data = scaled_data[i, ...]
 292            fname = f"{name}_{ang}_degrees"
 293            self.write_cube_to_disk(data, fname)
 294        normed_data = normalize_seismic(scaled_data)
 295        for i, ang in enumerate(self.cfg.incident_angles):
 296            data = normed_data[i, ...]
 297            fname = f"{name}_{ang}_degrees_normalized"
 298            self.write_cube_to_disk(data, fname)
 299        return normed_data
 300
 301    @staticmethod
 302    def random_z_rho_vp_vs(dmin=-7, dmax=7):
 303        delta_z_rho = int(np.random.uniform(dmin, dmax))
 304        delta_z_vp = int(np.random.uniform(dmin, dmax))
 305        delta_z_vs = int(np.random.uniform(dmin, dmax))
 306        return delta_z_rho, delta_z_vp, delta_z_vs
 307
 308    def create_rfc_volumes(self):
 309        """
 310        Build a 3D array of PP-Reflectivity using Zoeppritz for each incident angle given.
 311
 312        :return: 4D array of PP-Reflectivity. 4th dimension holds reflectivities of each incident angle provided
 313        """
 314        theta = np.asanyarray(self.angles).reshape(
 315            (-1, 1)
 316        )  # Convert angles into a column vector
 317
 318        # Make empty array to store RPP via zoeppritz
 319        zoep = np.zeros(
 320            (self.rho.shape[0], self.rho.shape[1], self.rho.shape[2] - 1, theta.size),
 321            dtype="complex64",
 322        )
 323
 324        _rho = self.rho[:]
 325        _vp = self.vp[:]
 326        _vs = self.vs[:]
 327        if self.cfg.verbose:
 328            print("Checking for null values inside create_rfc_volumes:\n")
 329            print(f"Rho: {np.min(_rho)}, {np.max(_rho)}")
 330            print(f"Vp: {np.min(_vp)}, {np.max(_vp)}")
 331            print(f"Vs: {np.min(_vs)}, {np.max(_vs)}")
 332        # Doing this for each trace & all (5) angles is actually quicker than doing entire cubes for each angle
 333        for i in trange(
 334            self.rho.shape[0],
 335            desc=f"Calculating Zoeppritz for {len(self.angles)} angles",
 336        ):
 337            for j in range(self.rho.shape[1]):
 338                rho1, rho2 = _rho[i, j, :-1], _rho[i, j, 1:]
 339                vp1, vp2 = _vp[i, j, :-1], _vp[i, j, 1:]
 340                vs1, vs2 = _vs[i, j, :-1], _vs[i, j, 1:]
 341                rfc = RFC(vp1, vs1, rho1, vp2, vs2, rho2, theta)
 342                zoep[i, j, :, :] = rfc.zoeppritz_reflectivity().T
 343        # Set any voxels with imaginary parts to 0, since all transmitted energy travels along the reflector
 344        # zoep[np.where(np.imag(zoep) != 0)] = 0
 345        # discard complex values and set dtype to float64
 346        zoep = np.real(zoep).astype("float64")
 347        del _rho, _vp, _vs
 348
 349        # Move the angle from last dimension to first
 350        self.rfc_raw[:] = np.moveaxis(zoep, -1, 0)
 351
 352        if self.cfg.hdf_store:
 353            for n, d in zip(
 354                [
 355                    "qc_volume_rfc_raw_{}_degrees".format(
 356                        str(self.angles[x]).replace(".", "_")
 357                    )
 358                    for x in range(self.rfc_raw.shape[0])
 359                ],
 360                [self.rfc_raw[x, ...] for x in range(self.rfc_raw.shape[0])],
 361            ):
 362                write_data_to_hdf(n, d, self.cfg.hdf_master)
 363        if self.cfg.model_qc_volumes:
 364            # Write raw RFC values to disk
 365            _ = [
 366                self.write_cube_to_disk(
 367                    self.rfc_raw[x, ...],
 368                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
 369                )
 370                for x in range(self.rfc_raw.shape[0])
 371            ]
 372            max_amp = np.max(np.abs(self.rfc_raw[:]))
 373            _ = [
 374                self.write_cube_to_disk(
 375                    self.rfc_raw[x, ...],
 376                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
 377                )
 378                for x in range(self.rfc_raw.shape[0])
 379            ]
 380
 381    def noise_3d(self, cube_shape, verbose=False):
 382        if verbose:
 383            print("   ... inside noise3D")
 384
 385        noise_seed = np.random.randint(1, high=2 ** 32 - 1)
 386        sign_seed = np.random.randint(1, high=2 ** 32 - 1)
 387
 388        np.random.seed(noise_seed)
 389        noise3d = np.random.exponential(
 390            1.0 / 100.0, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
 391        )
 392        np.random.seed(sign_seed)
 393        sign = np.random.binomial(
 394            1, 0.5, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
 395        )
 396
 397        sign[sign == 0] = -1
 398        noise3d *= sign
 399        noise3d = noise3d.reshape((cube_shape[0], cube_shape[1], cube_shape[2]))
 400        return noise3d
 401
 402    def add_weighted_noise(self, depth_maps):
 403        """
 404        Apply 3D weighted random noise to ndarray.
 405
 406        :return: Sum of rfc_volumes and 3D noise model, weighted by angle
 407        """
 408
 409        from math import sin, cos
 410        from datagenerator.util import mute_above_seafloor
 411        from datagenerator.util import infill_surface
 412
 413        # Calculate standard deviation ratio to use based on user-input sn_db parameter
 414        if self.cfg.verbose:
 415            print(f"\n...adding random noise to {self.rfc_raw.shape[0]} cubes...")
 416            print(f"S/N ratio = {self.cfg.sn_db:.4f}")
 417        std_ratio = np.sqrt(10 ** (self.cfg.sn_db / 10.0))
 418
 419        # Compute standard deviation in bottom half of cube, and use this to scale the noise
 420        # if wb has holes (i.e. Nan or 0 values), fill holes using replacement with interpolated (NN) values
 421        wb_map = depth_maps[..., 0]
 422        # Add test for all 0's in wb_map in case z_shift has been applied
 423        if (
 424            np.isnan(np.min(wb_map)) or 0 in wb_map and not np.all(wb_map == 0)
 425        ):  # If wb contains nan's or at least one 0, this will return True
 426            wb_map = infill_surface(wb_map)
 427        wb_plus_15samples = wb_map / (self.cfg.digi + 15) * self.cfg.digi
 428
 429        # Use the Middle angle cube for normalisation
 430        if self.cfg.model_qc_volumes:
 431            # 0 and 45 degree cubes have been added
 432            norm_cube = self.rfc_raw[2, ...]
 433        else:
 434            norm_cube = self.rfc_raw[1, ...]
 435        data_below_seafloor = norm_cube[
 436            mute_above_seafloor(wb_plus_15samples, np.ones(norm_cube.shape, "float"))
 437            != 0.0
 438        ]
 439        data_std = data_below_seafloor.std()
 440
 441        # Create array to store the 3D noise model for each angle
 442        noise3d_cubes = np.zeros_like(self.rfc_raw[:])
 443
 444        # Add weighted stack of noise using Hilterman equation to each angle substack
 445        noise_0deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
 446        noise_45deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
 447        # Store the noise models
 448        self.noise_0deg = self.cfg.hdf_init("noise_0deg", shape=noise_0deg.shape)
 449        self.noise_45deg = self.cfg.hdf_init("noise_45deg", shape=noise_45deg.shape)
 450        self.noise_0deg[:] = noise_0deg
 451        self.noise_45deg[:] = noise_45deg
 452
 453        for x, ang in enumerate(self.angles):
 454            weighted_noise = noise_0deg * (cos(ang) ** 2) + noise_45deg * (
 455                sin(ang) ** 2
 456            )
 457            noise_std = weighted_noise.std()
 458            noise3d_cubes[x, ...] = weighted_noise * (data_std / noise_std) / std_ratio
 459            if self.cfg.verbose:
 460                print(
 461                    f"\t...Normalised noise3d for angle {ang}:\tMin: {noise3d_cubes[x, ...].min():.4f},"
 462                    f" mean: {noise3d_cubes[x, ...].mean():.4f}, max: {noise3d_cubes[x, ...].max():.4f},"
 463                    f" std: {noise3d_cubes[x, ...].std():.4f}"
 464                )
 465                print(
 466                    f"\tS/N ratio = {self.cfg.sn_db:3.1f} dB.\n\tstd_ratio = {std_ratio:.4f}"
 467                    f"\n\tdata_std = {data_std:.4f}\n\tnoise_std = {noise_std:.4f}"
 468                )
 469
 470        self.rfc_noise_added[:] = noise3d_cubes + self.rfc_raw[:]
 471
 472    def apply_bandlimits(self, data, frequencies=None):
 473        """
 474        Apply Butterworth Bandpass Filter to data
 475        :param data: 4D array of RFC values
 476        :param frequencies: Explicit frequency bounds (lower, upper)
 477        :return: 4D array of band-limited RFC
 478        """
 479        if self.cfg.verbose:
 480            print(f"Data Min: {np.min(data):.2f}, Data Max: {np.max(data):.2f}")
 481        dt = self.cfg.digi / 1000.0
 482        if (
 483            data.shape[-1] / self.cfg.infill_factor - self.cfg.pad_samples
 484            == self.cfg.cube_shape[-1]
 485        ):
 486            # Input data is infilled
 487            dt /= self.cfg.infill_factor
 488        if frequencies:  # if frequencies explicitly defined
 489            low = frequencies[0]
 490            high = frequencies[1]
 491        else:
 492            low = self.cfg.lowfreq
 493            high = self.cfg.highfreq
 494        b, a = derive_butterworth_bandpass(
 495            low, high, (dt * 1000.0), order=self.cfg.order
 496        )
 497        if self.cfg.verbose:
 498            print(f"\t... Low Frequency; {low:.2f} Hz, High Frequency: {high:.2f} Hz")
 499            print(
 500                f"\t... start_width: {1. / (dt * low):.4f}, end_width: {1. / (dt * high):.4f}"
 501            )
 502
 503        # Loop over 3D angle stack arrays
 504        if self.cfg.verbose:
 505            print(" ... shape of data being bandlimited = ", data.shape, "\n")
 506        num = data.shape[0]
 507        if self.cfg.verbose:
 508            print(f"Applying Bandpass to {num} cubes")
 509        if self.cfg.multiprocess_bp:
 510            # Much faster to use multiprocessing and apply band-limitation to entire cube at once
 511            with Pool(processes=min(num, os.cpu_count() - 2)) as p:
 512                iterator = zip(
 513                    [data[x, ...] for x in range(num)],
 514                    itertools.repeat(b),
 515                    itertools.repeat(a),
 516                )
 517                out_cubes_mp = p.starmap(self._run_bandpass_on_cubes, iterator)
 518            out_cubes = np.asarray(out_cubes_mp)
 519        else:
 520            # multiprocessing can fail using Python version 3.6 and very large arrays
 521            out_cubes = np.zeros_like(data)
 522            for idx in range(num):
 523                out_cubes[idx, ...] = self._run_bandpass_on_cubes(data[idx, ...], b, a)
 524
 525        return out_cubes
 526
 527    @staticmethod
 528    def _run_bandpass_on_cubes(data, b, a):
 529        out_cube = apply_butterworth_bandpass(data, b, a)
 530        return out_cube
 531
 532    def apply_lateral_filter(self, data):
 533        from scipy.ndimage import uniform_filter
 534
 535        n_filt = self.cfg.lateral_filter_size
 536        return uniform_filter(data, size=(0, n_filt, n_filt, 0))
 537
 538    def apply_cumsum(self, data):
 539        """Calculate cumulative sum on the Z-axis of input data
 540        Sipmap's cumsum also applies an Ormsby filter to the cumulatively summed data
 541        To replicate this, apply a butterworth filter to the data using 2, 100Hz frequency limits
 542        todo: make an ormsby function"""
 543        cumsum = data.cumsum(axis=-1)
 544        return self.apply_bandlimits(cumsum, (2.0, 100.0))
 545
 546    @staticmethod
 547    def stack_substacks(cube_list):
 548        """Stack cubes by taking mean"""
 549        return np.mean(cube_list, axis=0)
 550
 551    def augment_data_and_labels(self, normalised_seismic, seabed):
 552        from datagenerator.Augmentation import tz_stretch, uniform_stretch
 553
 554        hc_labels = self.cfg.h5file.root.ModelData.hc_labels[:]
 555        data, labels = tz_stretch(
 556            normalised_seismic,
 557            hc_labels[..., : self.cfg.cube_shape[2] + self.cfg.pad_samples - 1],
 558            seabed,
 559        )
 560        seismic, hc = uniform_stretch(data, labels)
 561        return seismic, hc
 562
 563    def compute_randomRMO_1D(
 564        self, nsamples, inc_angle_list, velocity_fraction=0.015, return_plot=False
 565    ):
 566        """
 567        compute 1D indices for to apply residual moveout via interpolation
 568        - nsamples is the array length in depth
 569        - inc_angle_list is a list of angle in degrees for angle stacks to which
 570        results will be applied
 571        - velocity_fraction is the maximum percent error in absolute value.
 572        for example, .03 would generate RMO based on velocity from .97 to 1.03 times correct velocity
 573        """
 574        from scipy.interpolate import UnivariateSpline
 575
 576        input_indices = np.arange(nsamples)
 577        if self.cfg.qc_plots:
 578            from datagenerator.util import import_matplotlib
 579
 580            plt = import_matplotlib()
 581            plt.close(1)
 582            plt.figure(1, figsize=(4.75, 6.0), dpi=150)
 583            plt.clf()
 584            plt.grid()
 585            plt.xlim((-10, 10))
 586            plt.ylim((nsamples, 0))
 587            plt.ylabel("depth (samples)")
 588            plt.xlabel("RMO (samples)")
 589            plt.savefig(
 590                os.path.join(self.cfg.work_subfolder, "rmo_1d.png"), format="png"
 591            )
 592        # generate 1 to 3 randomly located velocity fractions at random depth indices
 593        for _ in range(250):
 594            num_tie_points = np.random.randint(low=1, high=4)
 595            tie_fractions = np.random.uniform(
 596                low=-velocity_fraction, high=velocity_fraction, size=num_tie_points
 597            )
 598            if num_tie_points > 1:
 599                tie_indices = np.random.uniform(
 600                    low=0.25 * nsamples, high=0.75 * nsamples, size=num_tie_points
 601                ).astype("int")
 602
 603            else:
 604                tie_indices = int(
 605                    np.random.uniform(low=0.25 * nsamples, high=0.75 * nsamples)
 606                )
 607            tie_fractions = np.hstack(([0.0, 0.0], tie_fractions, [0.0, 0.0]))
 608            tie_indices = np.hstack(([0, 1], tie_indices, [nsamples - 2, nsamples - 1]))
 609            tie_indices = np.sort(tie_indices)
 610            if np.diff(tie_indices).min() <= 0.0:
 611                continue
 612            s = UnivariateSpline(tie_indices, tie_fractions, s=0.0)
 613            tie_fraction_array = s(input_indices)
 614            output_indices = np.zeros((nsamples, len(inc_angle_list)))
 615            for i, iangle in enumerate(inc_angle_list):
 616                output_indices[:, i] = input_indices * (
 617                    1.0
 618                    + tie_fraction_array * np.tan(float(iangle) * np.pi / 180.0) ** 2
 619                )
 620            if np.abs(output_indices[:, i] - input_indices).max() < 10.0:
 621                break
 622        for i, iangle in enumerate(inc_angle_list):
 623            if self.cfg.qc_plots:
 624                plt.plot(
 625                    output_indices[:, i] - input_indices,
 626                    input_indices,
 627                    label=str(inc_angle_list[i]),
 628                )
 629
 630                if i == len(inc_angle_list) - 1:
 631                    output_tie_indices = tie_indices * (
 632                        1.0
 633                        + tie_fraction_array[tie_indices]
 634                        * np.tan(float(iangle) * np.pi / 180.0) ** 2
 635                    )
 636                    plt.plot(output_tie_indices - tie_indices, tie_indices, "ko")
 637                    rmo_min = (output_indices[:, -1] - input_indices).min()
 638                    rmo_max = (output_indices[:, -1] - input_indices).max()
 639                    rmo_range = np.array((rmo_min, rmo_max)).round(1)
 640                    plt.title(
 641                        "simultated RMO arrival time offsets\n"
 642                        + "rmo range = "
 643                        + str(rmo_range),
 644                        fontsize=11,
 645                    )
 646                    plt.legend()
 647                    plt.tight_layout()
 648                plt.savefig(
 649                    os.path.join(self.cfg.work_subfolder, "rmo_arrival_time.png"),
 650                    format="png",
 651                )
 652
 653        if return_plot:
 654            return output_indices, plt
 655        else:
 656            return output_indices
 657
 658    def apply_rmo_augmentation(self, angle_gather, rmo_indices, remove_mean_rmo=False):
 659        """
 660        apply residual moveout via interpolation
 661        applied to 'angle_gather' with results from compute_randomRMO_1D in rmo_indices
 662        angle_gather and rmo_indices need to be 2D arrays with the same shape (n_samples x n_angles)
 663        - angle_gather is the array to which RMO is applied
 664        - rmo_indices is the array used to modify the two-way-times of angle_gather
 665        - remove_mean_rmo can be used to (when True) to apply relative RMO that keeps
 666        event energy at roughly the same TWT. for example to avoid applying RMO
 667        to both data and labels
 668        """
 669        if remove_mean_rmo:
 670            # apply de-trending to ensure that rmo does not change TWT
 671            # after rmo compared to before rmo 'augmentation'
 672            residual_times = rmo_indices.mean(axis=-1) - np.arange(
 673                angle_gather.shape[0]
 674            )
 675            residual_rmo_indices = rmo_indices * 1.0
 676            residual_rmo_indices[:, 0] -= residual_times
 677            residual_rmo_indices[:, 1] -= residual_times
 678            residual_rmo_indices[:, 2] -= residual_times
 679            _rmo_indices = residual_rmo_indices
 680        else:
 681            _rmo_indices = rmo_indices
 682
 683        rmo_angle_gather = np.zeros_like(angle_gather)
 684        for iangle in range(len(rmo_indices[-1])):
 685            rmo_angle_gather[:, iangle] = np.interp(
 686                range(angle_gather.shape[0]),
 687                _rmo_indices[:, iangle],
 688                angle_gather[:, iangle],
 689            )
 690
 691        return rmo_angle_gather
 692
 693    def build_property_models_randomised_depth(self, rpm, mixing_method="inv_vel"):
 694        """
 695        Build property models with randomised depth
 696        -------------------------------------------
 697        Builds property models with randomised depth.
 698
 699        v2 but with Backus moduli mixing instead of inverse velocity mixing
 700        mixing_method one of ["InverseVelocity", "BackusModuli"]
 701
 702        Parameters
 703        ----------
 704        rpm : str
 705            Rock properties model.
 706        mixing_method : str
 707            Mixing method for randomised depth.
 708        
 709        Returns
 710        -------
 711        None
 712        """
 713        layer_half_range = self.cfg.rpm_scaling_factors["layershiftsamples"]
 714        property_half_range = self.cfg.rpm_scaling_factors["RPshiftsamples"]
 715
 716        depth = self.cfg.h5file.root.ModelData.faulted_depth[:]
 717        lith = self.faults.faulted_lithology[:]
 718        net_to_gross = self.faults.faulted_net_to_gross[:]
 719
 720        oil_closures = self.cfg.h5file.root.ModelData.oil_closures[:]
 721        gas_closures = self.cfg.h5file.root.ModelData.gas_closures[:]
 722
 723        integer_faulted_age = (
 724            self.cfg.h5file.root.ModelData.faulted_age_volume[:] + 0.00001
 725        ).astype(int)
 726
 727        # Use empty class object to store all Rho, Vp, Vs volumes (randomised, fluid factor and non randomised)
 728        mixed_properties = ElasticProperties3D()
 729        mixed_properties.rho = np.zeros_like(lith)
 730        mixed_properties.vp = np.zeros_like(lith)
 731        mixed_properties.vs = np.zeros_like(lith)
 732        mixed_properties.sum_net = np.zeros_like(lith)
 733        cube_shape = lith.shape
 734
 735        if self.cfg.verbose:
 736            mixed_properties.rho_not_random = np.zeros_like(lith)
 737            mixed_properties.vp_not_random = np.zeros_like(lith)
 738            mixed_properties.vs_not_random = np.zeros_like(lith)
 739
 740        # water layer
 741        mixed_properties = self.water_properties(lith, mixed_properties)
 742
 743        mixed_properties.rho_ff = np.copy(mixed_properties.rho)
 744        mixed_properties.vp_ff = np.copy(mixed_properties.vp)
 745        mixed_properties.vs_ff = np.copy(mixed_properties.vs)
 746
 747        delta_z_lyrs = []
 748        delta_z_shales = []
 749        delta_z_brine_sands = []
 750        delta_z_oil_sands = []
 751        delta_z_gas_sands = []
 752
 753        for z in range(1, integer_faulted_age.max()):
 754            __i, __j, __k = np.where(integer_faulted_age == z)
 755
 756            if len(__k) > 0:
 757                if self.cfg.verbose:
 758                    print(
 759                        f"\n\n*********************\n ... layer number = {z}"
 760                        f"\n ... n2g (min,mean,max) = "
 761                        f"({np.min(net_to_gross[__i, __j, __k]).round(3)},"
 762                        f"{np.mean(net_to_gross[__i, __j, __k]).round(3)},"
 763                        f"{np.max(net_to_gross[__i, __j, __k]).round(3)},)"
 764                    )
 765
 766                delta_z_layer = self.get_delta_z_layer(z, layer_half_range, __k)
 767                delta_z_lyrs.append(delta_z_layer)
 768
 769            # shale required for all voxels
 770            i, j, k = np.where((net_to_gross >= 0.0) & (integer_faulted_age == z))
 771
 772            shale_voxel_count = len(k)
 773            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 774                z, property_half_range
 775            )
 776            delta_z_shales.append((delta_z_rho, delta_z_vp, delta_z_vs))
 777
 778            if len(k) > 0:
 779                _k_rho = list(
 780                    np.array(k + delta_z_layer + delta_z_rho).clip(
 781                        0, cube_shape[-1] - 10
 782                    )
 783                )
 784                _k_vp = list(
 785                    np.array(k + delta_z_layer + delta_z_vp).clip(
 786                        0, cube_shape[-1] - 10
 787                    )
 788                )
 789                _k_vs = list(
 790                    np.array(k + delta_z_layer + delta_z_vs).clip(
 791                        0, cube_shape[-1] - 10
 792                    )
 793                )
 794
 795                if self.cfg.verbose:
 796                    print(
 797                        f" ... shale: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 798                    )
 799                    print(f" ... shale: i = {np.mean(i)}")
 800                    print(f" ... shale: k = {np.mean(k)}")
 801                    print(f" ... shale: _k = {np.mean(_k_rho)}")
 802
 803                mixed_properties = self.calculate_shales(
 804                    xyz=(i, j, k),
 805                    shifts=(_k_rho, _k_vp, _k_vs),
 806                    props=mixed_properties,
 807                    rpm=rpm,
 808                    depth=depth,
 809                    z=z,
 810                )
 811
 812            # brine sand or mixture of brine sand and shale in same voxel
 813            i, j, k = np.where(
 814                (net_to_gross > 0.0)
 815                & (oil_closures == 0.0)
 816                & (gas_closures == 0.0)
 817                & (integer_faulted_age == z)
 818            )
 819            brine_voxel_count = len(k)
 820            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 821                z, property_half_range
 822            )
 823            delta_z_brine_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 824
 825            if len(k) > 0:
 826                _k_rho = list(
 827                    np.array(k + delta_z_layer + delta_z_rho).clip(
 828                        0, cube_shape[-1] - 10
 829                    )
 830                )
 831                _k_vp = list(
 832                    np.array(k + delta_z_layer + delta_z_vp).clip(
 833                        0, cube_shape[-1] - 10
 834                    )
 835                )
 836                _k_vs = list(
 837                    np.array(k + delta_z_layer + delta_z_vs).clip(
 838                        0, cube_shape[-1] - 10
 839                    )
 840                )
 841
 842                if self.cfg.verbose:
 843                    print(
 844                        f"\n ... brine: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 845                    )
 846                    print(f" ... brine: i = {np.mean(i)}")
 847                    print(f" ... brine: k = {np.mean(k)}")
 848                    print(f" ... brine: _k = {np.mean(_k_rho)}")
 849
 850                mixed_properties = self.calculate_sands(
 851                    xyz=(i, j, k),
 852                    shifts=(_k_rho, _k_vp, _k_vs),
 853                    props=mixed_properties,
 854                    rpm=rpm,
 855                    depth=depth,
 856                    ng=net_to_gross,
 857                    z=z,
 858                    fluid="brine",
 859                    mix=mixing_method,
 860                )
 861
 862            # oil sands
 863            i, j, k = np.where(
 864                (net_to_gross > 0.0)
 865                & (oil_closures == 1.0)
 866                & (gas_closures == 0.0)
 867                & (integer_faulted_age == z)
 868            )
 869            oil_voxel_count = len(k)
 870            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 871                z, property_half_range
 872            )
 873            delta_z_oil_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 874
 875            if len(k) > 0:
 876                _k_rho = list(
 877                    np.array(k + delta_z_layer + delta_z_rho).clip(
 878                        0, cube_shape[-1] - 10
 879                    )
 880                )
 881                _k_vp = list(
 882                    np.array(k + delta_z_layer + delta_z_vp).clip(
 883                        0, cube_shape[-1] - 10
 884                    )
 885                )
 886                _k_vs = list(
 887                    np.array(k + delta_z_layer + delta_z_vs).clip(
 888                        0, cube_shape[-1] - 10
 889                    )
 890                )
 891
 892                if self.cfg.verbose:
 893                    print(
 894                        "\n\n ... Perform fluid substitution for oil sands. ",
 895                        "\n ... Number oil voxels in closure = ",
 896                        mixed_properties.rho[i, j, k].shape,
 897                    )
 898                    print(
 899                        " ... closures_oil min/mean/max =  ",
 900                        oil_closures.min(),
 901                        oil_closures.mean(),
 902                        oil_closures.max(),
 903                    )
 904                    print(
 905                        "\n\n ... np.all(oil_sand_rho[:,:,:] == brine_sand_rho[:,:,:]) ",
 906                        np.all(
 907                            rpm.calc_oil_sand_properties(
 908                                depth[:], depth[:], depth[:]
 909                            ).rho
 910                            == rpm.calc_brine_sand_properties(
 911                                depth[:], depth[:], depth[:]
 912                            ).rho
 913                        ),
 914                    )
 915                    print(
 916                        " ... oil: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 917                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 918                    )
 919
 920                if self.cfg.verbose:
 921                    print(" ... oil: i = " + str(np.mean(i)))
 922                    print(" ... oil: k = " + str(np.mean(k)))
 923                    print(" ... oil: _k = " + str(np.mean(_k_rho)))
 924
 925                mixed_properties = self.calculate_sands(
 926                    xyz=(i, j, k),
 927                    shifts=(_k_rho, _k_vp, _k_vs),
 928                    props=mixed_properties,
 929                    rpm=rpm,
 930                    depth=depth,
 931                    ng=net_to_gross,
 932                    z=z,
 933                    fluid="oil",
 934                    mix=mixing_method,
 935                )
 936
 937            # gas sands
 938            i, j, k = np.where(
 939                (net_to_gross > 0.0)
 940                & (oil_closures == 0.0)
 941                & (gas_closures == 1.0)
 942                & (integer_faulted_age == z)
 943            )
 944            gas_voxel_count = len(k)
 945            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 946                z, property_half_range
 947            )
 948            delta_z_gas_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 949
 950            if len(k) > 0:
 951                _k_rho = list(
 952                    np.array(k + delta_z_layer + delta_z_rho).clip(
 953                        0, cube_shape[-1] - 10
 954                    )
 955                )
 956                _k_vp = list(
 957                    np.array(k + delta_z_layer + delta_z_vp).clip(
 958                        0, cube_shape[-1] - 10
 959                    )
 960                )
 961                _k_vs = list(
 962                    np.array(k + delta_z_layer + delta_z_vs).clip(
 963                        0, cube_shape[-1] - 10
 964                    )
 965                )
 966
 967                if self.cfg.verbose:
 968                    print(
 969                        "\n ... Perform fluid substitution for gas sands. ",
 970                        "\n ... Number gas voxels in closure = ",
 971                        mixed_properties.rho[i, j, k].shape,
 972                    )
 973                    print(
 974                        " ... closures_gas min/mean/max =  ",
 975                        gas_closures.min(),
 976                        gas_closures.mean(),
 977                        gas_closures.max(),
 978                    )
 979                    print(
 980                        " ... gas: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 981                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 982                    )
 983
 984                if self.cfg.verbose:
 985                    print(" ... gas: i = " + str(np.mean(i)))
 986                    print(" ... gas: k = " + str(np.mean(k)))
 987                    print(" ... gas: _k = " + str(np.mean(_k_rho)))
 988
 989                mixed_properties = self.calculate_sands(
 990                    xyz=(i, j, k),
 991                    shifts=(_k_rho, _k_vp, _k_vs),
 992                    props=mixed_properties,
 993                    rpm=rpm,
 994                    depth=depth,
 995                    ng=net_to_gross,
 996                    z=z,
 997                    fluid="gas",
 998                    mix=mixing_method,
 999                )
1000
1001            # layer check
1002            __i, __j, __k = np.where(integer_faulted_age == z)
1003
1004            if len(__k) > 0:
1005                if self.cfg.verbose:
1006                    print(
1007                        "\n ... layer number = "
1008                        + str(z)
1009                        + "\n ... sum_net (min,mean,max) = "
1010                        + str(
1011                            (
1012                                mixed_properties.sum_net[__i, __j, __k].min(),
1013                                mixed_properties.sum_net[__i, __j, __k].mean(),
1014                                mixed_properties.sum_net[__i, __j, __k].max(),
1015                            )
1016                        )
1017                    )
1018                    print(
1019                        "\n ... layer number = "
1020                        + str(z)
1021                        + "\n ... (shale, brine, oil, gas) voxel_counts = "
1022                        + str(
1023                            (
1024                                shale_voxel_count,
1025                                brine_voxel_count,
1026                                oil_voxel_count,
1027                                gas_voxel_count,
1028                            )
1029                        )
1030                        + "\n ... shale_count = brine+oil+gas? "
1031                        + str(
1032                            shale_voxel_count
1033                            == brine_voxel_count + oil_voxel_count + gas_voxel_count
1034                        )
1035                        + "\n*********************"
1036                    )
1037
1038        # overwrite rho, vp, vs for salt if required
1039        if self.cfg.include_salt:
1040            # Salt. Set lith = 2.0
1041            i, j, k = np.where(lith == 2.0)
1042            mixed_properties.rho[i, j, k] = 2.17  # divide by 2 since lith = 2.0
1043            mixed_properties.vp[i, j, k] = 4500.0
1044            mixed_properties.vs[i, j, k] = 2250.0
1045            # Include fluidfactor rho, vp, vs inside salt body
1046            mixed_properties.rho_ff[i, j, k] = mixed_properties.rho[i, j, k]
1047            mixed_properties.vp_ff[i, j, k] = mixed_properties.vp[i, j, k]
1048            mixed_properties.vs_ff[i, j, k] = mixed_properties.vs[i, j, k]
1049
1050        if self.cfg.verbose:
1051            print("Checking for null values inside build_randomised_properties:\n")
1052            print(
1053                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1054            )
1055            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1056            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1057
1058        # Fix empty voxels at base of property volumes
1059        mixed_properties = self.fix_zero_values_at_base(mixed_properties)
1060
1061        if self.cfg.verbose:
1062            print(
1063                "Checking for null values inside build_randomised_properties AFTER fix:\n"
1064            )
1065            print(
1066                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1067            )
1068            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1069            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1070
1071        mixed_properties = self.apply_scaling_factors(
1072            mixed_properties, net_to_gross, lith
1073        )
1074        self.rho[:] = mixed_properties.rho
1075        self.vp[:] = mixed_properties.vp
1076        self.vs[:] = mixed_properties.vs
1077
1078        self.rho_ff[:] = mixed_properties.rho_ff
1079        self.vp_ff[:] = mixed_properties.vp_ff
1080        self.vs_ff[:] = mixed_properties.vs_ff
1081
1082        if self.cfg.qc_plots:
1083            print("\nCreating RPM qc plots")
1084            from rockphysics.RockPropertyModels import rpm_qc_plots
1085
1086            rpm_qc_plots(self.cfg, rpm)
1087
1088        if self.cfg.model_qc_volumes:
1089            self.write_property_volumes_to_disk()
1090
1091    def water_properties(self, lith, properties):
1092        i, j, k = np.where(lith < 0.0)
1093        properties.rho[i, j, k] = 1.028
1094        properties.vp[i, j, k] = 1500.0
1095        properties.vs[i, j, k] = 1000.0
1096        return properties
1097
1098    def get_delta_z_layer(self, z, half_range, z_cells):
1099        if z > self.first_random_lyr:
1100            delta_z_layer = int(np.random.uniform(-half_range, half_range))
1101        else:
1102            delta_z_layer = 0
1103        if self.cfg.verbose:
1104            print(f" .... Layer {z}: voxel_count = {len(z_cells)}")
1105            print(f" .... Layer {z}: delta_z_layer = {delta_z_layer}")
1106            print(
1107                f" .... Layer {z}: z-range (m): {np.min(z_cells) * self.cfg.digi}, "
1108                f"{np.max(z_cells) * self.cfg.digi}"
1109            )
1110        return delta_z_layer
1111
1112    def get_delta_z_properties(self, z, half_range):
1113        if z > self.first_random_lyr:
1114            delta_z_rho, delta_z_vp, delta_z_vs = self.random_z_rho_vp_vs(
1115                dmin=-half_range, dmax=half_range
1116            )
1117        else:
1118            delta_z_rho, delta_z_vp, delta_z_vs = (0, 0, 0)
1119        return delta_z_rho, delta_z_vp, delta_z_vs
1120
1121    def calculate_shales(self, xyz, shifts, props, rpm, depth, z):
1122        # Shales required for all voxels, other than water
1123        # Calculate the properties, select how to mix with other facies later
1124        i, j, k = xyz
1125        k_rho, k_vp, k_vs = shifts
1126
1127        z_rho = depth[i, j, k_rho]
1128        z_vp = depth[i, j, k_vp]
1129        z_vs = depth[i, j, k_vs]
1130        shales = rpm.calc_shale_properties(z_rho, z_vp, z_vs)
1131
1132        # Do not use net to gross here.
1133        # Since every voxel will have some shale in, apply the net to gross
1134        # when combining shales and sands
1135        # _ng = (1.0 - ng[i, j, k])
1136
1137        props.rho[i, j, k] = shales.rho  # * _ng
1138        props.rho_ff[i, j, k] = shales.rho  # * _ng
1139        props.vp[i, j, k] = shales.vp  # * _ng
1140        props.vp_ff[i, j, k] = shales.vp  # * _ng
1141        props.vs[i, j, k] = shales.vs  # * _ng
1142        props.vs_ff[i, j, k] = shales.vs  # * _ng
1143        # props.sum_net[i, j, k] = _ng
1144
1145        if self.cfg.verbose and z > self.first_random_lyr:
1146            # Calculate non-randomised properties and differences
1147            _z0 = depth[i, j, k]
1148            _shales0 = rpm.calc_shale_properties(_z0, _z0, _z0)
1149            props.rho_not_random[i, j, k] = _shales0.rho  # * _ng
1150            props.vp_not_random[i, j, k] = _shales0.vp  # * _ng
1151            props.vs_not_random[i, j, k] = _shales0.vs  # * _ng
1152
1153            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1154            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1155            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1156            pct_change_rho = delta_rho.mean().round(3)
1157            pct_change_vp = delta_vp.mean().round(3)
1158            pct_change_vs = delta_vs.mean().round(3)
1159            print(
1160                f" ... shale: randomization percent change (rho,vp,vs) = "
1161                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1162            )
1163            print(
1164                f" ... shale: min/max pre-randomization (rho, vp, vs)  = "
1165                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1166                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1167                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1168                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1169                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1170                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1171            )
1172            print(
1173                f" ... shale: min/max post-randomization (rho, vp, vs) = "
1174                f"{np.min(props.rho[i, j, k]):.3f} - "
1175                f"{np.max(props.rho[i, j, k]):.3f}, "
1176                f"{np.min(props.vp[i, j, k]):.2f} - "
1177                f"{np.max(props.vp[i, j, k]):.2f}, "
1178                f"{np.min(props.vs[i, j, k]):.2f} - "
1179                f"{np.max(props.vs[i, j, k]):.2f}"
1180            )
1181
1182        return props
1183
1184    def calculate_sands(
1185        self, xyz, shifts, props, rpm, depth, ng, z, fluid, mix="inv_vel"
1186    ):
1187        # brine sand or mixture of brine sand and shale in same voxel
1188        # - perform velocity sums in slowness or via backus moduli mixing
1189        i, j, k = xyz
1190        k_rho, k_vp, k_vs = shifts
1191
1192        z_rho = depth[i, j, k_rho]
1193        z_vp = depth[i, j, k_vp]
1194        z_vs = depth[i, j, k_vs]
1195        if fluid == "brine":
1196            sands = rpm.calc_brine_sand_properties(z_rho, z_vp, z_vs)
1197        elif fluid == "oil":
1198            sands = rpm.calc_oil_sand_properties(z_rho, z_vp, z_vs)
1199        elif fluid == "gas":
1200            sands = rpm.calc_gas_sand_properties(z_rho, z_vp, z_vs)
1201
1202        shales = RockProperties(
1203            rho=props.rho[i, j, k], vp=props.vp[i, j, k], vs=props.vs[i, j, k]
1204        )
1205        rpmix = EndMemberMixing(shales, sands, ng[i, j, k])
1206
1207        if mix == "inv_vel":
1208            rpmix.inverse_velocity_mixing()
1209        else:
1210            rpmix.backus_moduli_mixing()
1211
1212        props.sum_net[i, j, k] += ng[i, j, k]
1213        props.rho[i, j, k] = rpmix.rho
1214        props.vp[i, j, k] = rpmix.vp
1215        props.vs[i, j, k] = rpmix.vs
1216
1217        if self.cfg.verbose and z > self.first_random_lyr:
1218            # Calculate non-randomised properties and differences
1219            _z0 = depth[i, j, k]
1220            if fluid == "brine":
1221                _sands0 = rpm.calc_brine_sand_properties(_z0, _z0, _z0)
1222            elif fluid == "oil":
1223                _sands0 = rpm.calc_oil_sand_properties(_z0, _z0, _z0)
1224            elif fluid == "gas":
1225                _sands0 = rpm.calc_gas_sand_properties(_z0, _z0, _z0)
1226
1227            rpmix_0 = EndMemberMixing(shales, _sands0, ng[i, j, k])
1228            if mix == "inv_vel":
1229                # Apply Inverse Velocity mixing
1230                rpmix_0.inverse_velocity_mixing()
1231            else:
1232                # Apply Backus Moduli mixing
1233                rpmix_0.backus_moduli_mixing()
1234            props.rho_not_random[i, j, k] = rpmix_0.rho
1235            props.vp_not_random[i, j, k] = rpmix_0.vp
1236            props.vs_not_random[i, j, k] = rpmix_0.vs
1237
1238            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1239            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1240            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1241            pct_change_rho = delta_rho.mean().round(3)
1242            pct_change_vp = delta_vp.mean().round(3)
1243            pct_change_vs = delta_vs.mean().round(3)
1244            print(
1245                f" ... {fluid}: randomization percent change (rho,vp,vs) = "
1246                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1247            )
1248            print(
1249                f" ... {fluid}: min/max pre-randomization (rho, vp, vs)  = "
1250                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1251                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1252                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1253                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1254                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1255                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1256            )
1257            print(
1258                f" ... {fluid}: min/max post-randomization (rho, vp, vs) = "
1259                f"{np.min(props.rho[i, j, k]):.3f} - "
1260                f"{np.max(props.rho[i, j, k]):.3f}, "
1261                f"{np.min(props.vp[i, j, k]):.2f} - "
1262                f"{np.max(props.vp[i, j, k]):.2f}, "
1263                f"{np.min(props.vs[i, j, k]):.2f} - "
1264                f"{np.max(props.vs[i, j, k]):.2f}"
1265            )
1266
1267        return props
1268
1269    @staticmethod
1270    def fix_zero_values_at_base(props):
1271        """Check for zero values at base of property volumes and replace with
1272        shallower values if present
1273
1274        Args:
1275            a ([type]): [description]
1276            b ([type]): [description]
1277            c ([type]): [description]
1278        """
1279        for vol in [
1280            props.rho,
1281            props.vp,
1282            props.vs,
1283            props.rho_ff,
1284            props.vp_ff,
1285            props.vs_ff,
1286        ]:
1287            for (x, y, z) in np.argwhere(vol == 0.0):
1288                vol[x, y, z] = vol[x, y, z - 1]
1289        return props
1290
1291    def apply_scaling_factors(self, props, ng, lith):
1292        """Apply final random scaling factors to sands and shales"""
1293        rho_factor_shale = self.cfg.rpm_scaling_factors["shalerho_factor"]
1294        vp_factor_shale = self.cfg.rpm_scaling_factors["shalevp_factor"]
1295        vs_factor_shale = self.cfg.rpm_scaling_factors["shalevs_factor"]
1296        rho_factor_sand = self.cfg.rpm_scaling_factors["sandrho_factor"]
1297        vp_factor_sand = self.cfg.rpm_scaling_factors["sandvp_factor"]
1298        vs_factor_sand = self.cfg.rpm_scaling_factors["sandvs_factor"]
1299
1300        if self.cfg.verbose:
1301            print(
1302                f"\n... Additional random scaling factors: "
1303                f"\n ... Shale Rho {rho_factor_shale:.3f}, Vp {vp_factor_shale:.3f}, Vs {vs_factor_shale:.3f}"
1304                f"\n ... Sand Rho {rho_factor_sand:.3f}, Vp {vp_factor_sand:.3f}, Vs {vs_factor_sand:.3f}"
1305            )
1306
1307        # Apply final scaling factors to shale/sand properties
1308        props.rho[(ng <= 1.0e-2) & (lith < 2.0)] = (
1309            props.rho[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1310        )
1311        props.vp[(ng <= 1.0e-2) & (lith < 2.0)] = (
1312            props.vp[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1313        )
1314        props.vs[(ng <= 1.0e-2) & (lith < 2.0)] = (
1315            props.vs[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1316        )
1317        props.rho[(ng > 1.0e-2) & (lith < 2.0)] = (
1318            props.rho[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1319        )
1320        props.vp[(ng > 1.0e-2) & (lith < 2.0)] = (
1321            props.vp[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1322        )
1323        props.vs[(ng > 1.0e-2) & (lith < 2.0)] = (
1324            props.vs[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1325        )
1326        # Apply same factors to the fluid-factor property cubes
1327        props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1328            props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1329        )
1330        props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1331            props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1332        )
1333        props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1334            props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1335        )
1336        props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1337            props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1338        )
1339        props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1340            props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1341        )
1342        props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1343            props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1344        )
1345        return props
1346
1347    def write_property_volumes_to_disk(self):
1348        """Write Rho, Vp, Vs volumes to disk."""
1349        self.write_cube_to_disk(
1350            self.rho[:],
1351            "qc_volume_rho",
1352        )
1353        self.write_cube_to_disk(
1354            self.vp[:],
1355            "qc_volume_vp",
1356        )
1357        self.write_cube_to_disk(
1358            self.vs[:],
1359            "qc_volume_vs",
1360        )
1361
1362
1363class ElasticProperties3D:
1364    """Empty class to hold 3D property cubes Rho, Vp, Vs, Rho_ff, Vp_ff, Vs_ff and sum_net"""
1365
1366    def __init__(self):
1367        self.rho = None
1368        self.vp = None
1369        self.vs = None
1370        self.rho_ff = None
1371        self.vp_ff = None
1372        self.vs_ff = None
1373        self.sum_net = None
1374        self.rho_not_random = None
1375        self.vp_not_random = None
1376        self.vs_not_random = None
1377
1378
1379class RFC:
1380    """
1381    Reflection Coefficient object
1382    Contains methods for calculating the reflection coefficient at an interface
1383    from input Vp, Vs, Rho and incident angle
1384    Angle should be given in degrees and is converted to radians internally
1385    """
1386
1387    def __init__(
1388        self, vp_upper, vs_upper, rho_upper, vp_lower, vs_lower, rho_lower, theta
1389    ):
1390        self.theta = theta
1391        self.vp_upper = vp_upper
1392        self.vs_upper = vs_upper
1393        self.rho_upper = rho_upper
1394        self.vp_lower = vp_lower
1395        self.vs_lower = vs_lower
1396        self.rho_lower = rho_lower
1397
1398    def normal_incidence_rfc(self):
1399        z0 = self.rho_upper * self.vp_upper
1400        z1 = self.rho_lower * self.vp_lower
1401        return (z1 - z0) / (z1 + z0)
1402
1403    def shuey(self):
1404        """Use 3-term Shuey to calculate RFC."""
1405        radians = self._degrees_to_radians()  # Angle to radians
1406        sin_squared = np.sin(radians) ** 2
1407        tan_squared = np.tan(radians) ** 2
1408
1409        # Delta Vp, Vs, Rho
1410        d_vp = self.vp_lower - self.vp_upper
1411        d_vs = self.vs_lower - self.vs_upper
1412        d_rho = self.rho_lower - self.rho_upper
1413        avg_vp = np.mean([self.vp_lower, self.vp_upper])
1414        avg_vs = np.mean([self.vs_lower, self.vs_upper])
1415        avg_rho = np.mean([self.rho_lower, self.rho_upper])
1416
1417        # 3 Term Shuey
1418        r0 = 0.5 * (d_vp / avg_vp + d_rho / avg_rho)
1419        g = 0.5 * (d_vp / avg_vp) - 2.0 * avg_vs ** 2 / avg_vp ** 2 * (
1420            d_rho / avg_rho + 2.0 * d_vs / avg_vs
1421        )
1422        f = 0.5 * (d_vp / avg_vp)
1423
1424        return r0 + g * sin_squared + f * (tan_squared - sin_squared)
1425
1426    def zoeppritz_reflectivity(self):
1427        """Calculate PP reflectivity using Zoeppritz equation"""
1428        theta = self._degrees_to_radians(
1429            dtype="complex"
1430        )  # Convert angle to radians, as complex value
1431        p = np.sin(theta) / self.vp_upper  # ray param
1432        theta2 = np.arcsin(p * self.vp_lower)  # Transmission angle of P-wave
1433        phi1 = np.arcsin(p * self.vs_upper)  # Reflection angle of converted S-wave
1434        phi2 = np.arcsin(p * self.vs_lower)  # Transmission angle of converted S-wave
1435
1436        a = self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0) - self.rho_upper * (
1437            1 - 2 * np.sin(phi1) ** 2.0
1438        )
1439        b = (
1440            self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0)
1441            + 2 * self.rho_upper * np.sin(phi1) ** 2.0
1442        )
1443        c = (
1444            self.rho_upper * (1 - 2 * np.sin(phi1) ** 2.0)
1445            + 2 * self.rho_lower * np.sin(phi2) ** 2.0
1446        )
1447        d = 2 * (
1448            self.rho_lower * self.vs_lower ** 2 - self.rho_upper * self.vs_upper ** 2
1449        )
1450
1451        e = (b * np.cos(theta) / self.vp_upper) + (c * np.cos(theta2) / self.vp_lower)
1452        f = (b * np.cos(phi1) / self.vs_upper) + (c * np.cos(phi2) / self.vs_lower)
1453        g = a - d * np.cos(theta) / self.vp_upper * np.cos(phi2) / self.vs_lower
1454        h = a - d * np.cos(theta2) / self.vp_lower * np.cos(phi1) / self.vs_upper
1455
1456        d = e * f + g * h * p ** 2
1457
1458        zoep_pp = (
1459            f
1460            * (
1461                b * (np.cos(theta) / self.vp_upper)
1462                - c * (np.cos(theta2) / self.vp_lower)
1463            )
1464            - h
1465            * p ** 2
1466            * (a + d * (np.cos(theta) / self.vp_upper) * (np.cos(phi2) / self.vs_lower))
1467        ) * (1 / d)
1468
1469        return np.squeeze(zoep_pp)
1470
1471    def _degrees_to_radians(self, dtype="float"):
1472        """Convert angle in degrees to ange in radians. If dtype != 'float', return a complex dtype array"""
1473        if dtype == "float":
1474            return np.radians(self.theta)
1475        else:
1476            return np.radians(self.theta).astype(np.complex)
1477
1478
1479def derive_butterworth_bandpass(lowcut, highcut, digitisation, order=4):
1480    from scipy.signal import butter
1481
1482    fs = 1.0 / (digitisation / 1000.0)
1483    nyq = 0.5 * fs
1484    low = lowcut / nyq
1485    high = highcut / nyq
1486    b, a = butter(order, [low, high], btype="bandpass", output="ba")
1487    return b, a
1488
1489
1490def apply_butterworth_bandpass(data, b, a):
1491    """Apply Butterworth bandpass to data"""
1492    from scipy.signal import tf2zpk, filtfilt
1493
1494    # Use irlen to remove artefacts generated at base of cubes during bandlimitation
1495    _, p, _ = tf2zpk(b, a)
1496    eps = 1e-9
1497    r = np.max(np.abs(p))
1498    approx_impulse_len = int(np.ceil(np.log(eps) / np.log(r)))
1499    y = filtfilt(b, a, data, method="gust", irlen=approx_impulse_len)
1500    # y = filtfilt(b, a, data)
1501    return y
1502
1503
1504def trim_triangular(low, mid, high):
1505    ###
1506    ### use trimmed triangular distributions
1507    ###
1508    from numpy.random import triangular
1509
1510    for _ in range(50):
1511        num = triangular(2 * low - mid, mid, 2 * high - mid)
1512        if low <= num <= high:
1513            break
1514    return num
1515
1516
1517def apply_wavelet(cube, wavelet):
1518    filtered_cube = np.zeros_like(cube)
1519    for i in range(cube.shape[0]):
1520        for j in range(cube.shape[1]):
1521            filtered_cube[i, j, :] = np.convolve(cube[i, j, :], wavelet, mode="same")
1522    return filtered_cube
class SeismicVolume(datagenerator.Geomodels.Geomodel):
  15class SeismicVolume(Geomodel):
  16    """
  17    SeismicVolume Class
  18    -------------------
  19
  20    This class initializes the seismic volume that contains everything.
  21    """
  22    def __init__(self, parameters, faults, closures) -> None:
  23        """
  24        __init__
  25        --------
  26
  27        The initialization function.
  28
  29        Parameters
  30        ----------
  31        parameters : datagenerator.Parameters
  32            The parameters object of the project.
  33        faults : np.ndarray
  34            The faults array.
  35        closures : np.ndarray
  36            The closures array.
  37        
  38        Returns
  39        -------
  40        None
  41        """
  42        self.cfg = parameters
  43        self.faults = faults
  44        self.traps = closures
  45
  46        if self.cfg.model_qc_volumes:
  47            # Add 0 and 45 degree angles to the list of user-input angles
  48            self.angles = tuple(sorted(set((0,) + self.cfg.incident_angles + (45,))))
  49        else:
  50            self.angles = self.cfg.incident_angles
  51        if self.cfg.verbose:
  52            print(f"Angles: {self.angles}")
  53
  54        self.first_random_lyr = 20  # do not randomise shallow layers
  55
  56        self.rho = self.cfg.hdf_init(
  57            "rho", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  58        )
  59        self.vp = self.cfg.hdf_init(
  60            "vp", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  61        )
  62        self.vs = self.cfg.hdf_init(
  63            "vs", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  64        )
  65        self.rho_ff = self.cfg.hdf_init(
  66            "rho_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  67        )
  68        self.vp_ff = self.cfg.hdf_init(
  69            "vp_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  70        )
  71        self.vs_ff = self.cfg.hdf_init(
  72            "vs_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
  73        )
  74
  75        seis_shape = (
  76            len(self.angles),
  77            *self.cfg.h5file.root.ModelData.faulted_depth.shape,
  78        )
  79        rfc_shape = (seis_shape[0], seis_shape[1], seis_shape[2], seis_shape[3] - 1)
  80        self.rfc_raw = self.cfg.hdf_init("rfc_raw", shape=rfc_shape)
  81        self.rfc_noise_added = self.cfg.hdf_init("rfc_noise_added", shape=rfc_shape)
  82
  83    def build_elastic_properties(self, mixing_method="inv_vel"):
  84        """
  85        Build Elastic Properties
  86        ------------------------
  87
  88        Creates Rho, Vp, Vs volumes using depth trends.
  89
  90        Parameters
  91        ----------
  92        mixing_method : str, optional
  93            Method for mixing Velocities. Defaults to "inv_vel".
  94
  95        """
  96        rpm = select_rpm(self.cfg)
  97        self.build_property_models_randomised_depth(
  98            rpm,
  99            mixing_method=mixing_method
 100        )
 101
 102    def bandlimit_volumes_wavelets(self, n_wavelets=10):
 103        # Pre-prepared n, m, f filter_specs from npy file
 104        fs_nr = np.load(self.cfg.wavelets[0])
 105        fs_md = np.load(self.cfg.wavelets[1])
 106        fs_fr = np.load(self.cfg.wavelets[2])
 107
 108        for wavelet_count in range(n_wavelets):
 109            low_freq_pctile = np.random.uniform(0, 100)
 110            mid_freq_pctile = np.random.uniform(0, 100)
 111            high_freq_pctile = np.random.uniform(0, 100)
 112            low_med_high_percentiles = (
 113                low_freq_pctile,
 114                mid_freq_pctile,
 115                high_freq_pctile,
 116            )
 117            print(f"low_med_high_percentiles: {low_med_high_percentiles}")
 118            f_nr, a_nr, w_nr = generate_wavelet(fs_nr, low_med_high_percentiles)
 119            f_md, a_md, w_md = generate_wavelet(fs_md, low_med_high_percentiles)
 120            f_fr, a_fr, w_fr = generate_wavelet(fs_fr, low_med_high_percentiles)
 121            _f = (f_nr, f_md, f_fr)
 122            _a = (a_nr, a_md, a_fr)
 123            _w = (w_nr, w_md, w_fr)
 124            labels = ["Near", "Mid", "Far"]
 125            pngname = os.path.join(
 126                self.cfg.work_subfolder, f"random_wavelets_{wavelet_count}.png"
 127            )
 128            plot_wavelets(_f, _a, _w, labels, pngname)
 129
 130            rfc_bandlimited_wavelet = np.zeros_like(self.rfc_noise_added)
 131            if self.cfg.model_qc_volumes:
 132                wavelets = [
 133                    w_nr,
 134                    w_nr,
 135                    w_md,
 136                    w_fr,
 137                    w_fr,
 138                ]  # use near for 0 and far for 45 deg cubes
 139            else:
 140                wavelets = [w_nr, w_md, w_fr]
 141
 142            for idx in range(rfc_bandlimited_wavelet.shape[0]):
 143                rfc_bandlimited_wavelet[idx, ...] = apply_wavelet(
 144                    self.rfc_noise_added[idx, ...], wavelets[idx]
 145                )
 146            if self.cfg.lateral_filter_size > 1:
 147                # Apply lateral smoothing filter
 148                rfc_bandlimited_wavelet = self.apply_lateral_filter(
 149                    rfc_bandlimited_wavelet
 150                )
 151            cumsum_wavelet_noise_added = self.apply_cumsum(rfc_bandlimited_wavelet)
 152            _ = self.write_final_cubes_to_disk(
 153                rfc_bandlimited_wavelet, f"seismicCubes_RFC_Wavelet_{wavelet_count}_"
 154            )
 155            _ = self.write_final_cubes_to_disk(
 156                cumsum_wavelet_noise_added,
 157                f"seismicCubes_cumsum_Wavelet_{wavelet_count}_",
 158            )
 159
 160    def postprocess_rfc_cubes(
 161        self, rfc_data, filename_suffix="", bb=False, stack=False
 162    ):
 163        """Postprocess the RFC cubes."""
 164        if bb:
 165            bandlimited = self.apply_bandlimits(rfc_data, (4.0, 90.0))
 166        else:
 167            bandlimited = self.apply_bandlimits(rfc_data)
 168        if self.cfg.lateral_filter_size > 1:
 169            bandlimited = self.apply_lateral_filter(bandlimited)
 170        if filename_suffix == "":
 171            fname = "seismicCubes_RFC_"
 172        else:
 173            fname = f"seismicCubes_RFC_{filename_suffix}_"
 174        _ = self.write_final_cubes_to_disk(bandlimited, fname)
 175        if stack:
 176            rfc_fullstack = self.stack_substacks(
 177                [bandlimited[x, ...] for x, _ in enumerate(self.angles)]
 178            )
 179            rfc_fullstack_scaled = self._scale_seismic(rfc_fullstack)
 180            self.write_cube_to_disk(
 181                rfc_fullstack_scaled,
 182                f"{fname}fullstack",
 183            )
 184
 185        cumsum = self.apply_cumsum(bandlimited)
 186        if filename_suffix == "":
 187            fname = "seismicCubes_cumsum_"
 188        else:
 189            fname = f"seismicCubes_cumsum_{filename_suffix}_"
 190        normalised_cumsum = self.write_final_cubes_to_disk(cumsum, fname)
 191        if stack:
 192            rai_fullstack = self.stack_substacks(
 193                [cumsum[x, ...] for x, _ in enumerate(self.angles)]
 194            )
 195            rai_fullstack_scaled = self._scale_seismic(rai_fullstack)
 196            self.write_cube_to_disk(
 197                rai_fullstack_scaled,
 198                f"{fname}fullstack",
 199            )
 200        return normalised_cumsum
 201
 202    def apply_augmentations(self, data, name="cumsum"):
 203        seabed = self.faults.faulted_depth_maps[..., 0] / self.cfg.digi
 204        augmented_data, augmented_labels = self.augment_data_and_labels(data, seabed)
 205        for i, ang in enumerate(self.cfg.incident_angles):
 206            data = augmented_data[i, ...]
 207            fname = f"seismicCubes_{name}_{ang}_degrees_normalized_augmented"
 208            self.write_cube_to_disk(data, fname)
 209            self.write_cube_to_disk(augmented_labels, "hc_closures_augmented")
 210
 211    def apply_rmo(self, data, name="cumsum_RMO"):
 212        """Apply RMO to the cumsum with noise. Note rmo application expects angle in last dimension"""
 213        rmo_indices = self.compute_randomRMO_1D(
 214            data.shape[-1], self.cfg.incident_angles, velocity_fraction=0.015
 215        )
 216        rmo_applied_data = np.zeros_like(np.moveaxis(data, 0, -1))
 217        for iline in range(self.cfg.cube_shape[0]):
 218            for xline in range(self.cfg.cube_shape[1]):
 219                rmo_applied_data[iline, xline, ...] = self.apply_rmo_augmentation(
 220                    np.moveaxis(data, 0, -1)[iline, xline, ...],
 221                    rmo_indices,
 222                    remove_mean_rmo=True,
 223                )
 224        rmo_applied_data = np.moveaxis(
 225            rmo_applied_data, -1, 0
 226        )  # Put the angles back into 1st dimension
 227        normalised_rmo_applied_data = self.write_final_cubes_to_disk(
 228            rmo_applied_data, f"seismicCubes_{name}_"
 229        )
 230        return normalised_rmo_applied_data
 231
 232    def build_seismic_volumes(self):
 233        """Build Seismic volumes.
 234
 235        * Use Zoeppritz to create RFC volumes for each required angle, add 0 & 45 degree stacks for QC
 236        * Add exponential weighted noise to the angle stacks
 237        * Apply bandpass filter
 238        * Apply lateral filter
 239        * Calculate cumsum
 240        * Create full stacks by stacking the user-input angles (i.e. not including 0, 45 degree stacks)
 241        * Write seismic volumes to disk
 242        """
 243        if self.cfg.verbose:
 244            print(
 245                f"Generating 3D Rock Properties Rho, Vp and Vs using depth trends for project {self.cfg.project}"
 246            )
 247        # Calculate Raw RFC from rho, vp, vs using Zoeppritz & write to HdF
 248        self.create_rfc_volumes()
 249        self.add_weighted_noise(self.faults.faulted_depth_maps)
 250        if hasattr(self.cfg, "wavelets"):
 251            self.bandlimit_volumes_wavelets(n_wavelets=1)
 252        normalised_cumsum = self.postprocess_rfc_cubes(
 253            self.rfc_noise_added[:], "", stack=True
 254        )
 255        self.apply_augmentations(normalised_cumsum, name="cumsum")
 256        if self.cfg.model_qc_volumes:
 257            _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free", bb=False)
 258            if self.cfg.broadband_qc_volume:
 259                _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free_bb", bb=True)
 260            normalised_cumsum_rmo = self.apply_rmo(normalised_cumsum)
 261            self.apply_augmentations(normalised_cumsum_rmo, name="cumsum_RMO")
 262
 263    def _scale_seismic(self, data):
 264        """Apply scaling factor to final seismic data volumes"""
 265        if data.ndim == 4:
 266            avg_stdev = np.mean([data[x, ...].std() for x in range(data.shape[0])])
 267        elif data.ndim == 3:
 268            avg_stdev = data.std()
 269        else:
 270            print(
 271                f"Input Data has {data.ndim} dimensions, should have 3 or 4 dimensions"
 272            )
 273        scaled_data = data * 100 / avg_stdev
 274        return scaled_data
 275
 276    def write_final_cubes_to_disk(self, dat, name):
 277        scaled_data = self._scale_seismic(dat)
 278
 279        # Apply scaling factors to N, M, F cubes from csv file before determining clip limits
 280        factor_near = self.cfg.rpm_scaling_factors["nearfactor"]
 281        factor_mid = self.cfg.rpm_scaling_factors["midfactor"]
 282        factor_far = self.cfg.rpm_scaling_factors["farfactor"]
 283        cube_incr = 0
 284        if self.cfg.model_qc_volumes and dat.shape[0] > 3:
 285            # 0 degrees has been added at start, and 45 at end
 286            cube_incr = 1
 287        scaled_data[0 + cube_incr, ...] *= factor_near
 288        scaled_data[1 + cube_incr, ...] *= factor_mid
 289        scaled_data[2 + cube_incr, ...] *= factor_far
 290
 291        for i, ang in enumerate(self.cfg.incident_angles):
 292            data = scaled_data[i, ...]
 293            fname = f"{name}_{ang}_degrees"
 294            self.write_cube_to_disk(data, fname)
 295        normed_data = normalize_seismic(scaled_data)
 296        for i, ang in enumerate(self.cfg.incident_angles):
 297            data = normed_data[i, ...]
 298            fname = f"{name}_{ang}_degrees_normalized"
 299            self.write_cube_to_disk(data, fname)
 300        return normed_data
 301
 302    @staticmethod
 303    def random_z_rho_vp_vs(dmin=-7, dmax=7):
 304        delta_z_rho = int(np.random.uniform(dmin, dmax))
 305        delta_z_vp = int(np.random.uniform(dmin, dmax))
 306        delta_z_vs = int(np.random.uniform(dmin, dmax))
 307        return delta_z_rho, delta_z_vp, delta_z_vs
 308
 309    def create_rfc_volumes(self):
 310        """
 311        Build a 3D array of PP-Reflectivity using Zoeppritz for each incident angle given.
 312
 313        :return: 4D array of PP-Reflectivity. 4th dimension holds reflectivities of each incident angle provided
 314        """
 315        theta = np.asanyarray(self.angles).reshape(
 316            (-1, 1)
 317        )  # Convert angles into a column vector
 318
 319        # Make empty array to store RPP via zoeppritz
 320        zoep = np.zeros(
 321            (self.rho.shape[0], self.rho.shape[1], self.rho.shape[2] - 1, theta.size),
 322            dtype="complex64",
 323        )
 324
 325        _rho = self.rho[:]
 326        _vp = self.vp[:]
 327        _vs = self.vs[:]
 328        if self.cfg.verbose:
 329            print("Checking for null values inside create_rfc_volumes:\n")
 330            print(f"Rho: {np.min(_rho)}, {np.max(_rho)}")
 331            print(f"Vp: {np.min(_vp)}, {np.max(_vp)}")
 332            print(f"Vs: {np.min(_vs)}, {np.max(_vs)}")
 333        # Doing this for each trace & all (5) angles is actually quicker than doing entire cubes for each angle
 334        for i in trange(
 335            self.rho.shape[0],
 336            desc=f"Calculating Zoeppritz for {len(self.angles)} angles",
 337        ):
 338            for j in range(self.rho.shape[1]):
 339                rho1, rho2 = _rho[i, j, :-1], _rho[i, j, 1:]
 340                vp1, vp2 = _vp[i, j, :-1], _vp[i, j, 1:]
 341                vs1, vs2 = _vs[i, j, :-1], _vs[i, j, 1:]
 342                rfc = RFC(vp1, vs1, rho1, vp2, vs2, rho2, theta)
 343                zoep[i, j, :, :] = rfc.zoeppritz_reflectivity().T
 344        # Set any voxels with imaginary parts to 0, since all transmitted energy travels along the reflector
 345        # zoep[np.where(np.imag(zoep) != 0)] = 0
 346        # discard complex values and set dtype to float64
 347        zoep = np.real(zoep).astype("float64")
 348        del _rho, _vp, _vs
 349
 350        # Move the angle from last dimension to first
 351        self.rfc_raw[:] = np.moveaxis(zoep, -1, 0)
 352
 353        if self.cfg.hdf_store:
 354            for n, d in zip(
 355                [
 356                    "qc_volume_rfc_raw_{}_degrees".format(
 357                        str(self.angles[x]).replace(".", "_")
 358                    )
 359                    for x in range(self.rfc_raw.shape[0])
 360                ],
 361                [self.rfc_raw[x, ...] for x in range(self.rfc_raw.shape[0])],
 362            ):
 363                write_data_to_hdf(n, d, self.cfg.hdf_master)
 364        if self.cfg.model_qc_volumes:
 365            # Write raw RFC values to disk
 366            _ = [
 367                self.write_cube_to_disk(
 368                    self.rfc_raw[x, ...],
 369                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
 370                )
 371                for x in range(self.rfc_raw.shape[0])
 372            ]
 373            max_amp = np.max(np.abs(self.rfc_raw[:]))
 374            _ = [
 375                self.write_cube_to_disk(
 376                    self.rfc_raw[x, ...],
 377                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
 378                )
 379                for x in range(self.rfc_raw.shape[0])
 380            ]
 381
 382    def noise_3d(self, cube_shape, verbose=False):
 383        if verbose:
 384            print("   ... inside noise3D")
 385
 386        noise_seed = np.random.randint(1, high=2 ** 32 - 1)
 387        sign_seed = np.random.randint(1, high=2 ** 32 - 1)
 388
 389        np.random.seed(noise_seed)
 390        noise3d = np.random.exponential(
 391            1.0 / 100.0, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
 392        )
 393        np.random.seed(sign_seed)
 394        sign = np.random.binomial(
 395            1, 0.5, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
 396        )
 397
 398        sign[sign == 0] = -1
 399        noise3d *= sign
 400        noise3d = noise3d.reshape((cube_shape[0], cube_shape[1], cube_shape[2]))
 401        return noise3d
 402
 403    def add_weighted_noise(self, depth_maps):
 404        """
 405        Apply 3D weighted random noise to ndarray.
 406
 407        :return: Sum of rfc_volumes and 3D noise model, weighted by angle
 408        """
 409
 410        from math import sin, cos
 411        from datagenerator.util import mute_above_seafloor
 412        from datagenerator.util import infill_surface
 413
 414        # Calculate standard deviation ratio to use based on user-input sn_db parameter
 415        if self.cfg.verbose:
 416            print(f"\n...adding random noise to {self.rfc_raw.shape[0]} cubes...")
 417            print(f"S/N ratio = {self.cfg.sn_db:.4f}")
 418        std_ratio = np.sqrt(10 ** (self.cfg.sn_db / 10.0))
 419
 420        # Compute standard deviation in bottom half of cube, and use this to scale the noise
 421        # if wb has holes (i.e. Nan or 0 values), fill holes using replacement with interpolated (NN) values
 422        wb_map = depth_maps[..., 0]
 423        # Add test for all 0's in wb_map in case z_shift has been applied
 424        if (
 425            np.isnan(np.min(wb_map)) or 0 in wb_map and not np.all(wb_map == 0)
 426        ):  # If wb contains nan's or at least one 0, this will return True
 427            wb_map = infill_surface(wb_map)
 428        wb_plus_15samples = wb_map / (self.cfg.digi + 15) * self.cfg.digi
 429
 430        # Use the Middle angle cube for normalisation
 431        if self.cfg.model_qc_volumes:
 432            # 0 and 45 degree cubes have been added
 433            norm_cube = self.rfc_raw[2, ...]
 434        else:
 435            norm_cube = self.rfc_raw[1, ...]
 436        data_below_seafloor = norm_cube[
 437            mute_above_seafloor(wb_plus_15samples, np.ones(norm_cube.shape, "float"))
 438            != 0.0
 439        ]
 440        data_std = data_below_seafloor.std()
 441
 442        # Create array to store the 3D noise model for each angle
 443        noise3d_cubes = np.zeros_like(self.rfc_raw[:])
 444
 445        # Add weighted stack of noise using Hilterman equation to each angle substack
 446        noise_0deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
 447        noise_45deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
 448        # Store the noise models
 449        self.noise_0deg = self.cfg.hdf_init("noise_0deg", shape=noise_0deg.shape)
 450        self.noise_45deg = self.cfg.hdf_init("noise_45deg", shape=noise_45deg.shape)
 451        self.noise_0deg[:] = noise_0deg
 452        self.noise_45deg[:] = noise_45deg
 453
 454        for x, ang in enumerate(self.angles):
 455            weighted_noise = noise_0deg * (cos(ang) ** 2) + noise_45deg * (
 456                sin(ang) ** 2
 457            )
 458            noise_std = weighted_noise.std()
 459            noise3d_cubes[x, ...] = weighted_noise * (data_std / noise_std) / std_ratio
 460            if self.cfg.verbose:
 461                print(
 462                    f"\t...Normalised noise3d for angle {ang}:\tMin: {noise3d_cubes[x, ...].min():.4f},"
 463                    f" mean: {noise3d_cubes[x, ...].mean():.4f}, max: {noise3d_cubes[x, ...].max():.4f},"
 464                    f" std: {noise3d_cubes[x, ...].std():.4f}"
 465                )
 466                print(
 467                    f"\tS/N ratio = {self.cfg.sn_db:3.1f} dB.\n\tstd_ratio = {std_ratio:.4f}"
 468                    f"\n\tdata_std = {data_std:.4f}\n\tnoise_std = {noise_std:.4f}"
 469                )
 470
 471        self.rfc_noise_added[:] = noise3d_cubes + self.rfc_raw[:]
 472
 473    def apply_bandlimits(self, data, frequencies=None):
 474        """
 475        Apply Butterworth Bandpass Filter to data
 476        :param data: 4D array of RFC values
 477        :param frequencies: Explicit frequency bounds (lower, upper)
 478        :return: 4D array of band-limited RFC
 479        """
 480        if self.cfg.verbose:
 481            print(f"Data Min: {np.min(data):.2f}, Data Max: {np.max(data):.2f}")
 482        dt = self.cfg.digi / 1000.0
 483        if (
 484            data.shape[-1] / self.cfg.infill_factor - self.cfg.pad_samples
 485            == self.cfg.cube_shape[-1]
 486        ):
 487            # Input data is infilled
 488            dt /= self.cfg.infill_factor
 489        if frequencies:  # if frequencies explicitly defined
 490            low = frequencies[0]
 491            high = frequencies[1]
 492        else:
 493            low = self.cfg.lowfreq
 494            high = self.cfg.highfreq
 495        b, a = derive_butterworth_bandpass(
 496            low, high, (dt * 1000.0), order=self.cfg.order
 497        )
 498        if self.cfg.verbose:
 499            print(f"\t... Low Frequency; {low:.2f} Hz, High Frequency: {high:.2f} Hz")
 500            print(
 501                f"\t... start_width: {1. / (dt * low):.4f}, end_width: {1. / (dt * high):.4f}"
 502            )
 503
 504        # Loop over 3D angle stack arrays
 505        if self.cfg.verbose:
 506            print(" ... shape of data being bandlimited = ", data.shape, "\n")
 507        num = data.shape[0]
 508        if self.cfg.verbose:
 509            print(f"Applying Bandpass to {num} cubes")
 510        if self.cfg.multiprocess_bp:
 511            # Much faster to use multiprocessing and apply band-limitation to entire cube at once
 512            with Pool(processes=min(num, os.cpu_count() - 2)) as p:
 513                iterator = zip(
 514                    [data[x, ...] for x in range(num)],
 515                    itertools.repeat(b),
 516                    itertools.repeat(a),
 517                )
 518                out_cubes_mp = p.starmap(self._run_bandpass_on_cubes, iterator)
 519            out_cubes = np.asarray(out_cubes_mp)
 520        else:
 521            # multiprocessing can fail using Python version 3.6 and very large arrays
 522            out_cubes = np.zeros_like(data)
 523            for idx in range(num):
 524                out_cubes[idx, ...] = self._run_bandpass_on_cubes(data[idx, ...], b, a)
 525
 526        return out_cubes
 527
 528    @staticmethod
 529    def _run_bandpass_on_cubes(data, b, a):
 530        out_cube = apply_butterworth_bandpass(data, b, a)
 531        return out_cube
 532
 533    def apply_lateral_filter(self, data):
 534        from scipy.ndimage import uniform_filter
 535
 536        n_filt = self.cfg.lateral_filter_size
 537        return uniform_filter(data, size=(0, n_filt, n_filt, 0))
 538
 539    def apply_cumsum(self, data):
 540        """Calculate cumulative sum on the Z-axis of input data
 541        Sipmap's cumsum also applies an Ormsby filter to the cumulatively summed data
 542        To replicate this, apply a butterworth filter to the data using 2, 100Hz frequency limits
 543        todo: make an ormsby function"""
 544        cumsum = data.cumsum(axis=-1)
 545        return self.apply_bandlimits(cumsum, (2.0, 100.0))
 546
 547    @staticmethod
 548    def stack_substacks(cube_list):
 549        """Stack cubes by taking mean"""
 550        return np.mean(cube_list, axis=0)
 551
 552    def augment_data_and_labels(self, normalised_seismic, seabed):
 553        from datagenerator.Augmentation import tz_stretch, uniform_stretch
 554
 555        hc_labels = self.cfg.h5file.root.ModelData.hc_labels[:]
 556        data, labels = tz_stretch(
 557            normalised_seismic,
 558            hc_labels[..., : self.cfg.cube_shape[2] + self.cfg.pad_samples - 1],
 559            seabed,
 560        )
 561        seismic, hc = uniform_stretch(data, labels)
 562        return seismic, hc
 563
 564    def compute_randomRMO_1D(
 565        self, nsamples, inc_angle_list, velocity_fraction=0.015, return_plot=False
 566    ):
 567        """
 568        compute 1D indices for to apply residual moveout via interpolation
 569        - nsamples is the array length in depth
 570        - inc_angle_list is a list of angle in degrees for angle stacks to which
 571        results will be applied
 572        - velocity_fraction is the maximum percent error in absolute value.
 573        for example, .03 would generate RMO based on velocity from .97 to 1.03 times correct velocity
 574        """
 575        from scipy.interpolate import UnivariateSpline
 576
 577        input_indices = np.arange(nsamples)
 578        if self.cfg.qc_plots:
 579            from datagenerator.util import import_matplotlib
 580
 581            plt = import_matplotlib()
 582            plt.close(1)
 583            plt.figure(1, figsize=(4.75, 6.0), dpi=150)
 584            plt.clf()
 585            plt.grid()
 586            plt.xlim((-10, 10))
 587            plt.ylim((nsamples, 0))
 588            plt.ylabel("depth (samples)")
 589            plt.xlabel("RMO (samples)")
 590            plt.savefig(
 591                os.path.join(self.cfg.work_subfolder, "rmo_1d.png"), format="png"
 592            )
 593        # generate 1 to 3 randomly located velocity fractions at random depth indices
 594        for _ in range(250):
 595            num_tie_points = np.random.randint(low=1, high=4)
 596            tie_fractions = np.random.uniform(
 597                low=-velocity_fraction, high=velocity_fraction, size=num_tie_points
 598            )
 599            if num_tie_points > 1:
 600                tie_indices = np.random.uniform(
 601                    low=0.25 * nsamples, high=0.75 * nsamples, size=num_tie_points
 602                ).astype("int")
 603
 604            else:
 605                tie_indices = int(
 606                    np.random.uniform(low=0.25 * nsamples, high=0.75 * nsamples)
 607                )
 608            tie_fractions = np.hstack(([0.0, 0.0], tie_fractions, [0.0, 0.0]))
 609            tie_indices = np.hstack(([0, 1], tie_indices, [nsamples - 2, nsamples - 1]))
 610            tie_indices = np.sort(tie_indices)
 611            if np.diff(tie_indices).min() <= 0.0:
 612                continue
 613            s = UnivariateSpline(tie_indices, tie_fractions, s=0.0)
 614            tie_fraction_array = s(input_indices)
 615            output_indices = np.zeros((nsamples, len(inc_angle_list)))
 616            for i, iangle in enumerate(inc_angle_list):
 617                output_indices[:, i] = input_indices * (
 618                    1.0
 619                    + tie_fraction_array * np.tan(float(iangle) * np.pi / 180.0) ** 2
 620                )
 621            if np.abs(output_indices[:, i] - input_indices).max() < 10.0:
 622                break
 623        for i, iangle in enumerate(inc_angle_list):
 624            if self.cfg.qc_plots:
 625                plt.plot(
 626                    output_indices[:, i] - input_indices,
 627                    input_indices,
 628                    label=str(inc_angle_list[i]),
 629                )
 630
 631                if i == len(inc_angle_list) - 1:
 632                    output_tie_indices = tie_indices * (
 633                        1.0
 634                        + tie_fraction_array[tie_indices]
 635                        * np.tan(float(iangle) * np.pi / 180.0) ** 2
 636                    )
 637                    plt.plot(output_tie_indices - tie_indices, tie_indices, "ko")
 638                    rmo_min = (output_indices[:, -1] - input_indices).min()
 639                    rmo_max = (output_indices[:, -1] - input_indices).max()
 640                    rmo_range = np.array((rmo_min, rmo_max)).round(1)
 641                    plt.title(
 642                        "simultated RMO arrival time offsets\n"
 643                        + "rmo range = "
 644                        + str(rmo_range),
 645                        fontsize=11,
 646                    )
 647                    plt.legend()
 648                    plt.tight_layout()
 649                plt.savefig(
 650                    os.path.join(self.cfg.work_subfolder, "rmo_arrival_time.png"),
 651                    format="png",
 652                )
 653
 654        if return_plot:
 655            return output_indices, plt
 656        else:
 657            return output_indices
 658
 659    def apply_rmo_augmentation(self, angle_gather, rmo_indices, remove_mean_rmo=False):
 660        """
 661        apply residual moveout via interpolation
 662        applied to 'angle_gather' with results from compute_randomRMO_1D in rmo_indices
 663        angle_gather and rmo_indices need to be 2D arrays with the same shape (n_samples x n_angles)
 664        - angle_gather is the array to which RMO is applied
 665        - rmo_indices is the array used to modify the two-way-times of angle_gather
 666        - remove_mean_rmo can be used to (when True) to apply relative RMO that keeps
 667        event energy at roughly the same TWT. for example to avoid applying RMO
 668        to both data and labels
 669        """
 670        if remove_mean_rmo:
 671            # apply de-trending to ensure that rmo does not change TWT
 672            # after rmo compared to before rmo 'augmentation'
 673            residual_times = rmo_indices.mean(axis=-1) - np.arange(
 674                angle_gather.shape[0]
 675            )
 676            residual_rmo_indices = rmo_indices * 1.0
 677            residual_rmo_indices[:, 0] -= residual_times
 678            residual_rmo_indices[:, 1] -= residual_times
 679            residual_rmo_indices[:, 2] -= residual_times
 680            _rmo_indices = residual_rmo_indices
 681        else:
 682            _rmo_indices = rmo_indices
 683
 684        rmo_angle_gather = np.zeros_like(angle_gather)
 685        for iangle in range(len(rmo_indices[-1])):
 686            rmo_angle_gather[:, iangle] = np.interp(
 687                range(angle_gather.shape[0]),
 688                _rmo_indices[:, iangle],
 689                angle_gather[:, iangle],
 690            )
 691
 692        return rmo_angle_gather
 693
 694    def build_property_models_randomised_depth(self, rpm, mixing_method="inv_vel"):
 695        """
 696        Build property models with randomised depth
 697        -------------------------------------------
 698        Builds property models with randomised depth.
 699
 700        v2 but with Backus moduli mixing instead of inverse velocity mixing
 701        mixing_method one of ["InverseVelocity", "BackusModuli"]
 702
 703        Parameters
 704        ----------
 705        rpm : str
 706            Rock properties model.
 707        mixing_method : str
 708            Mixing method for randomised depth.
 709        
 710        Returns
 711        -------
 712        None
 713        """
 714        layer_half_range = self.cfg.rpm_scaling_factors["layershiftsamples"]
 715        property_half_range = self.cfg.rpm_scaling_factors["RPshiftsamples"]
 716
 717        depth = self.cfg.h5file.root.ModelData.faulted_depth[:]
 718        lith = self.faults.faulted_lithology[:]
 719        net_to_gross = self.faults.faulted_net_to_gross[:]
 720
 721        oil_closures = self.cfg.h5file.root.ModelData.oil_closures[:]
 722        gas_closures = self.cfg.h5file.root.ModelData.gas_closures[:]
 723
 724        integer_faulted_age = (
 725            self.cfg.h5file.root.ModelData.faulted_age_volume[:] + 0.00001
 726        ).astype(int)
 727
 728        # Use empty class object to store all Rho, Vp, Vs volumes (randomised, fluid factor and non randomised)
 729        mixed_properties = ElasticProperties3D()
 730        mixed_properties.rho = np.zeros_like(lith)
 731        mixed_properties.vp = np.zeros_like(lith)
 732        mixed_properties.vs = np.zeros_like(lith)
 733        mixed_properties.sum_net = np.zeros_like(lith)
 734        cube_shape = lith.shape
 735
 736        if self.cfg.verbose:
 737            mixed_properties.rho_not_random = np.zeros_like(lith)
 738            mixed_properties.vp_not_random = np.zeros_like(lith)
 739            mixed_properties.vs_not_random = np.zeros_like(lith)
 740
 741        # water layer
 742        mixed_properties = self.water_properties(lith, mixed_properties)
 743
 744        mixed_properties.rho_ff = np.copy(mixed_properties.rho)
 745        mixed_properties.vp_ff = np.copy(mixed_properties.vp)
 746        mixed_properties.vs_ff = np.copy(mixed_properties.vs)
 747
 748        delta_z_lyrs = []
 749        delta_z_shales = []
 750        delta_z_brine_sands = []
 751        delta_z_oil_sands = []
 752        delta_z_gas_sands = []
 753
 754        for z in range(1, integer_faulted_age.max()):
 755            __i, __j, __k = np.where(integer_faulted_age == z)
 756
 757            if len(__k) > 0:
 758                if self.cfg.verbose:
 759                    print(
 760                        f"\n\n*********************\n ... layer number = {z}"
 761                        f"\n ... n2g (min,mean,max) = "
 762                        f"({np.min(net_to_gross[__i, __j, __k]).round(3)},"
 763                        f"{np.mean(net_to_gross[__i, __j, __k]).round(3)},"
 764                        f"{np.max(net_to_gross[__i, __j, __k]).round(3)},)"
 765                    )
 766
 767                delta_z_layer = self.get_delta_z_layer(z, layer_half_range, __k)
 768                delta_z_lyrs.append(delta_z_layer)
 769
 770            # shale required for all voxels
 771            i, j, k = np.where((net_to_gross >= 0.0) & (integer_faulted_age == z))
 772
 773            shale_voxel_count = len(k)
 774            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 775                z, property_half_range
 776            )
 777            delta_z_shales.append((delta_z_rho, delta_z_vp, delta_z_vs))
 778
 779            if len(k) > 0:
 780                _k_rho = list(
 781                    np.array(k + delta_z_layer + delta_z_rho).clip(
 782                        0, cube_shape[-1] - 10
 783                    )
 784                )
 785                _k_vp = list(
 786                    np.array(k + delta_z_layer + delta_z_vp).clip(
 787                        0, cube_shape[-1] - 10
 788                    )
 789                )
 790                _k_vs = list(
 791                    np.array(k + delta_z_layer + delta_z_vs).clip(
 792                        0, cube_shape[-1] - 10
 793                    )
 794                )
 795
 796                if self.cfg.verbose:
 797                    print(
 798                        f" ... shale: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 799                    )
 800                    print(f" ... shale: i = {np.mean(i)}")
 801                    print(f" ... shale: k = {np.mean(k)}")
 802                    print(f" ... shale: _k = {np.mean(_k_rho)}")
 803
 804                mixed_properties = self.calculate_shales(
 805                    xyz=(i, j, k),
 806                    shifts=(_k_rho, _k_vp, _k_vs),
 807                    props=mixed_properties,
 808                    rpm=rpm,
 809                    depth=depth,
 810                    z=z,
 811                )
 812
 813            # brine sand or mixture of brine sand and shale in same voxel
 814            i, j, k = np.where(
 815                (net_to_gross > 0.0)
 816                & (oil_closures == 0.0)
 817                & (gas_closures == 0.0)
 818                & (integer_faulted_age == z)
 819            )
 820            brine_voxel_count = len(k)
 821            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 822                z, property_half_range
 823            )
 824            delta_z_brine_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 825
 826            if len(k) > 0:
 827                _k_rho = list(
 828                    np.array(k + delta_z_layer + delta_z_rho).clip(
 829                        0, cube_shape[-1] - 10
 830                    )
 831                )
 832                _k_vp = list(
 833                    np.array(k + delta_z_layer + delta_z_vp).clip(
 834                        0, cube_shape[-1] - 10
 835                    )
 836                )
 837                _k_vs = list(
 838                    np.array(k + delta_z_layer + delta_z_vs).clip(
 839                        0, cube_shape[-1] - 10
 840                    )
 841                )
 842
 843                if self.cfg.verbose:
 844                    print(
 845                        f"\n ... brine: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 846                    )
 847                    print(f" ... brine: i = {np.mean(i)}")
 848                    print(f" ... brine: k = {np.mean(k)}")
 849                    print(f" ... brine: _k = {np.mean(_k_rho)}")
 850
 851                mixed_properties = self.calculate_sands(
 852                    xyz=(i, j, k),
 853                    shifts=(_k_rho, _k_vp, _k_vs),
 854                    props=mixed_properties,
 855                    rpm=rpm,
 856                    depth=depth,
 857                    ng=net_to_gross,
 858                    z=z,
 859                    fluid="brine",
 860                    mix=mixing_method,
 861                )
 862
 863            # oil sands
 864            i, j, k = np.where(
 865                (net_to_gross > 0.0)
 866                & (oil_closures == 1.0)
 867                & (gas_closures == 0.0)
 868                & (integer_faulted_age == z)
 869            )
 870            oil_voxel_count = len(k)
 871            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 872                z, property_half_range
 873            )
 874            delta_z_oil_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 875
 876            if len(k) > 0:
 877                _k_rho = list(
 878                    np.array(k + delta_z_layer + delta_z_rho).clip(
 879                        0, cube_shape[-1] - 10
 880                    )
 881                )
 882                _k_vp = list(
 883                    np.array(k + delta_z_layer + delta_z_vp).clip(
 884                        0, cube_shape[-1] - 10
 885                    )
 886                )
 887                _k_vs = list(
 888                    np.array(k + delta_z_layer + delta_z_vs).clip(
 889                        0, cube_shape[-1] - 10
 890                    )
 891                )
 892
 893                if self.cfg.verbose:
 894                    print(
 895                        "\n\n ... Perform fluid substitution for oil sands. ",
 896                        "\n ... Number oil voxels in closure = ",
 897                        mixed_properties.rho[i, j, k].shape,
 898                    )
 899                    print(
 900                        " ... closures_oil min/mean/max =  ",
 901                        oil_closures.min(),
 902                        oil_closures.mean(),
 903                        oil_closures.max(),
 904                    )
 905                    print(
 906                        "\n\n ... np.all(oil_sand_rho[:,:,:] == brine_sand_rho[:,:,:]) ",
 907                        np.all(
 908                            rpm.calc_oil_sand_properties(
 909                                depth[:], depth[:], depth[:]
 910                            ).rho
 911                            == rpm.calc_brine_sand_properties(
 912                                depth[:], depth[:], depth[:]
 913                            ).rho
 914                        ),
 915                    )
 916                    print(
 917                        " ... oil: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 918                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 919                    )
 920
 921                if self.cfg.verbose:
 922                    print(" ... oil: i = " + str(np.mean(i)))
 923                    print(" ... oil: k = " + str(np.mean(k)))
 924                    print(" ... oil: _k = " + str(np.mean(_k_rho)))
 925
 926                mixed_properties = self.calculate_sands(
 927                    xyz=(i, j, k),
 928                    shifts=(_k_rho, _k_vp, _k_vs),
 929                    props=mixed_properties,
 930                    rpm=rpm,
 931                    depth=depth,
 932                    ng=net_to_gross,
 933                    z=z,
 934                    fluid="oil",
 935                    mix=mixing_method,
 936                )
 937
 938            # gas sands
 939            i, j, k = np.where(
 940                (net_to_gross > 0.0)
 941                & (oil_closures == 0.0)
 942                & (gas_closures == 1.0)
 943                & (integer_faulted_age == z)
 944            )
 945            gas_voxel_count = len(k)
 946            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 947                z, property_half_range
 948            )
 949            delta_z_gas_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 950
 951            if len(k) > 0:
 952                _k_rho = list(
 953                    np.array(k + delta_z_layer + delta_z_rho).clip(
 954                        0, cube_shape[-1] - 10
 955                    )
 956                )
 957                _k_vp = list(
 958                    np.array(k + delta_z_layer + delta_z_vp).clip(
 959                        0, cube_shape[-1] - 10
 960                    )
 961                )
 962                _k_vs = list(
 963                    np.array(k + delta_z_layer + delta_z_vs).clip(
 964                        0, cube_shape[-1] - 10
 965                    )
 966                )
 967
 968                if self.cfg.verbose:
 969                    print(
 970                        "\n ... Perform fluid substitution for gas sands. ",
 971                        "\n ... Number gas voxels in closure = ",
 972                        mixed_properties.rho[i, j, k].shape,
 973                    )
 974                    print(
 975                        " ... closures_gas min/mean/max =  ",
 976                        gas_closures.min(),
 977                        gas_closures.mean(),
 978                        gas_closures.max(),
 979                    )
 980                    print(
 981                        " ... gas: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 982                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 983                    )
 984
 985                if self.cfg.verbose:
 986                    print(" ... gas: i = " + str(np.mean(i)))
 987                    print(" ... gas: k = " + str(np.mean(k)))
 988                    print(" ... gas: _k = " + str(np.mean(_k_rho)))
 989
 990                mixed_properties = self.calculate_sands(
 991                    xyz=(i, j, k),
 992                    shifts=(_k_rho, _k_vp, _k_vs),
 993                    props=mixed_properties,
 994                    rpm=rpm,
 995                    depth=depth,
 996                    ng=net_to_gross,
 997                    z=z,
 998                    fluid="gas",
 999                    mix=mixing_method,
1000                )
1001
1002            # layer check
1003            __i, __j, __k = np.where(integer_faulted_age == z)
1004
1005            if len(__k) > 0:
1006                if self.cfg.verbose:
1007                    print(
1008                        "\n ... layer number = "
1009                        + str(z)
1010                        + "\n ... sum_net (min,mean,max) = "
1011                        + str(
1012                            (
1013                                mixed_properties.sum_net[__i, __j, __k].min(),
1014                                mixed_properties.sum_net[__i, __j, __k].mean(),
1015                                mixed_properties.sum_net[__i, __j, __k].max(),
1016                            )
1017                        )
1018                    )
1019                    print(
1020                        "\n ... layer number = "
1021                        + str(z)
1022                        + "\n ... (shale, brine, oil, gas) voxel_counts = "
1023                        + str(
1024                            (
1025                                shale_voxel_count,
1026                                brine_voxel_count,
1027                                oil_voxel_count,
1028                                gas_voxel_count,
1029                            )
1030                        )
1031                        + "\n ... shale_count = brine+oil+gas? "
1032                        + str(
1033                            shale_voxel_count
1034                            == brine_voxel_count + oil_voxel_count + gas_voxel_count
1035                        )
1036                        + "\n*********************"
1037                    )
1038
1039        # overwrite rho, vp, vs for salt if required
1040        if self.cfg.include_salt:
1041            # Salt. Set lith = 2.0
1042            i, j, k = np.where(lith == 2.0)
1043            mixed_properties.rho[i, j, k] = 2.17  # divide by 2 since lith = 2.0
1044            mixed_properties.vp[i, j, k] = 4500.0
1045            mixed_properties.vs[i, j, k] = 2250.0
1046            # Include fluidfactor rho, vp, vs inside salt body
1047            mixed_properties.rho_ff[i, j, k] = mixed_properties.rho[i, j, k]
1048            mixed_properties.vp_ff[i, j, k] = mixed_properties.vp[i, j, k]
1049            mixed_properties.vs_ff[i, j, k] = mixed_properties.vs[i, j, k]
1050
1051        if self.cfg.verbose:
1052            print("Checking for null values inside build_randomised_properties:\n")
1053            print(
1054                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1055            )
1056            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1057            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1058
1059        # Fix empty voxels at base of property volumes
1060        mixed_properties = self.fix_zero_values_at_base(mixed_properties)
1061
1062        if self.cfg.verbose:
1063            print(
1064                "Checking for null values inside build_randomised_properties AFTER fix:\n"
1065            )
1066            print(
1067                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1068            )
1069            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1070            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1071
1072        mixed_properties = self.apply_scaling_factors(
1073            mixed_properties, net_to_gross, lith
1074        )
1075        self.rho[:] = mixed_properties.rho
1076        self.vp[:] = mixed_properties.vp
1077        self.vs[:] = mixed_properties.vs
1078
1079        self.rho_ff[:] = mixed_properties.rho_ff
1080        self.vp_ff[:] = mixed_properties.vp_ff
1081        self.vs_ff[:] = mixed_properties.vs_ff
1082
1083        if self.cfg.qc_plots:
1084            print("\nCreating RPM qc plots")
1085            from rockphysics.RockPropertyModels import rpm_qc_plots
1086
1087            rpm_qc_plots(self.cfg, rpm)
1088
1089        if self.cfg.model_qc_volumes:
1090            self.write_property_volumes_to_disk()
1091
1092    def water_properties(self, lith, properties):
1093        i, j, k = np.where(lith < 0.0)
1094        properties.rho[i, j, k] = 1.028
1095        properties.vp[i, j, k] = 1500.0
1096        properties.vs[i, j, k] = 1000.0
1097        return properties
1098
1099    def get_delta_z_layer(self, z, half_range, z_cells):
1100        if z > self.first_random_lyr:
1101            delta_z_layer = int(np.random.uniform(-half_range, half_range))
1102        else:
1103            delta_z_layer = 0
1104        if self.cfg.verbose:
1105            print(f" .... Layer {z}: voxel_count = {len(z_cells)}")
1106            print(f" .... Layer {z}: delta_z_layer = {delta_z_layer}")
1107            print(
1108                f" .... Layer {z}: z-range (m): {np.min(z_cells) * self.cfg.digi}, "
1109                f"{np.max(z_cells) * self.cfg.digi}"
1110            )
1111        return delta_z_layer
1112
1113    def get_delta_z_properties(self, z, half_range):
1114        if z > self.first_random_lyr:
1115            delta_z_rho, delta_z_vp, delta_z_vs = self.random_z_rho_vp_vs(
1116                dmin=-half_range, dmax=half_range
1117            )
1118        else:
1119            delta_z_rho, delta_z_vp, delta_z_vs = (0, 0, 0)
1120        return delta_z_rho, delta_z_vp, delta_z_vs
1121
1122    def calculate_shales(self, xyz, shifts, props, rpm, depth, z):
1123        # Shales required for all voxels, other than water
1124        # Calculate the properties, select how to mix with other facies later
1125        i, j, k = xyz
1126        k_rho, k_vp, k_vs = shifts
1127
1128        z_rho = depth[i, j, k_rho]
1129        z_vp = depth[i, j, k_vp]
1130        z_vs = depth[i, j, k_vs]
1131        shales = rpm.calc_shale_properties(z_rho, z_vp, z_vs)
1132
1133        # Do not use net to gross here.
1134        # Since every voxel will have some shale in, apply the net to gross
1135        # when combining shales and sands
1136        # _ng = (1.0 - ng[i, j, k])
1137
1138        props.rho[i, j, k] = shales.rho  # * _ng
1139        props.rho_ff[i, j, k] = shales.rho  # * _ng
1140        props.vp[i, j, k] = shales.vp  # * _ng
1141        props.vp_ff[i, j, k] = shales.vp  # * _ng
1142        props.vs[i, j, k] = shales.vs  # * _ng
1143        props.vs_ff[i, j, k] = shales.vs  # * _ng
1144        # props.sum_net[i, j, k] = _ng
1145
1146        if self.cfg.verbose and z > self.first_random_lyr:
1147            # Calculate non-randomised properties and differences
1148            _z0 = depth[i, j, k]
1149            _shales0 = rpm.calc_shale_properties(_z0, _z0, _z0)
1150            props.rho_not_random[i, j, k] = _shales0.rho  # * _ng
1151            props.vp_not_random[i, j, k] = _shales0.vp  # * _ng
1152            props.vs_not_random[i, j, k] = _shales0.vs  # * _ng
1153
1154            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1155            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1156            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1157            pct_change_rho = delta_rho.mean().round(3)
1158            pct_change_vp = delta_vp.mean().round(3)
1159            pct_change_vs = delta_vs.mean().round(3)
1160            print(
1161                f" ... shale: randomization percent change (rho,vp,vs) = "
1162                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1163            )
1164            print(
1165                f" ... shale: min/max pre-randomization (rho, vp, vs)  = "
1166                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1167                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1168                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1169                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1170                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1171                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1172            )
1173            print(
1174                f" ... shale: min/max post-randomization (rho, vp, vs) = "
1175                f"{np.min(props.rho[i, j, k]):.3f} - "
1176                f"{np.max(props.rho[i, j, k]):.3f}, "
1177                f"{np.min(props.vp[i, j, k]):.2f} - "
1178                f"{np.max(props.vp[i, j, k]):.2f}, "
1179                f"{np.min(props.vs[i, j, k]):.2f} - "
1180                f"{np.max(props.vs[i, j, k]):.2f}"
1181            )
1182
1183        return props
1184
1185    def calculate_sands(
1186        self, xyz, shifts, props, rpm, depth, ng, z, fluid, mix="inv_vel"
1187    ):
1188        # brine sand or mixture of brine sand and shale in same voxel
1189        # - perform velocity sums in slowness or via backus moduli mixing
1190        i, j, k = xyz
1191        k_rho, k_vp, k_vs = shifts
1192
1193        z_rho = depth[i, j, k_rho]
1194        z_vp = depth[i, j, k_vp]
1195        z_vs = depth[i, j, k_vs]
1196        if fluid == "brine":
1197            sands = rpm.calc_brine_sand_properties(z_rho, z_vp, z_vs)
1198        elif fluid == "oil":
1199            sands = rpm.calc_oil_sand_properties(z_rho, z_vp, z_vs)
1200        elif fluid == "gas":
1201            sands = rpm.calc_gas_sand_properties(z_rho, z_vp, z_vs)
1202
1203        shales = RockProperties(
1204            rho=props.rho[i, j, k], vp=props.vp[i, j, k], vs=props.vs[i, j, k]
1205        )
1206        rpmix = EndMemberMixing(shales, sands, ng[i, j, k])
1207
1208        if mix == "inv_vel":
1209            rpmix.inverse_velocity_mixing()
1210        else:
1211            rpmix.backus_moduli_mixing()
1212
1213        props.sum_net[i, j, k] += ng[i, j, k]
1214        props.rho[i, j, k] = rpmix.rho
1215        props.vp[i, j, k] = rpmix.vp
1216        props.vs[i, j, k] = rpmix.vs
1217
1218        if self.cfg.verbose and z > self.first_random_lyr:
1219            # Calculate non-randomised properties and differences
1220            _z0 = depth[i, j, k]
1221            if fluid == "brine":
1222                _sands0 = rpm.calc_brine_sand_properties(_z0, _z0, _z0)
1223            elif fluid == "oil":
1224                _sands0 = rpm.calc_oil_sand_properties(_z0, _z0, _z0)
1225            elif fluid == "gas":
1226                _sands0 = rpm.calc_gas_sand_properties(_z0, _z0, _z0)
1227
1228            rpmix_0 = EndMemberMixing(shales, _sands0, ng[i, j, k])
1229            if mix == "inv_vel":
1230                # Apply Inverse Velocity mixing
1231                rpmix_0.inverse_velocity_mixing()
1232            else:
1233                # Apply Backus Moduli mixing
1234                rpmix_0.backus_moduli_mixing()
1235            props.rho_not_random[i, j, k] = rpmix_0.rho
1236            props.vp_not_random[i, j, k] = rpmix_0.vp
1237            props.vs_not_random[i, j, k] = rpmix_0.vs
1238
1239            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1240            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1241            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1242            pct_change_rho = delta_rho.mean().round(3)
1243            pct_change_vp = delta_vp.mean().round(3)
1244            pct_change_vs = delta_vs.mean().round(3)
1245            print(
1246                f" ... {fluid}: randomization percent change (rho,vp,vs) = "
1247                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1248            )
1249            print(
1250                f" ... {fluid}: min/max pre-randomization (rho, vp, vs)  = "
1251                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1252                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1253                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1254                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1255                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1256                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1257            )
1258            print(
1259                f" ... {fluid}: min/max post-randomization (rho, vp, vs) = "
1260                f"{np.min(props.rho[i, j, k]):.3f} - "
1261                f"{np.max(props.rho[i, j, k]):.3f}, "
1262                f"{np.min(props.vp[i, j, k]):.2f} - "
1263                f"{np.max(props.vp[i, j, k]):.2f}, "
1264                f"{np.min(props.vs[i, j, k]):.2f} - "
1265                f"{np.max(props.vs[i, j, k]):.2f}"
1266            )
1267
1268        return props
1269
1270    @staticmethod
1271    def fix_zero_values_at_base(props):
1272        """Check for zero values at base of property volumes and replace with
1273        shallower values if present
1274
1275        Args:
1276            a ([type]): [description]
1277            b ([type]): [description]
1278            c ([type]): [description]
1279        """
1280        for vol in [
1281            props.rho,
1282            props.vp,
1283            props.vs,
1284            props.rho_ff,
1285            props.vp_ff,
1286            props.vs_ff,
1287        ]:
1288            for (x, y, z) in np.argwhere(vol == 0.0):
1289                vol[x, y, z] = vol[x, y, z - 1]
1290        return props
1291
1292    def apply_scaling_factors(self, props, ng, lith):
1293        """Apply final random scaling factors to sands and shales"""
1294        rho_factor_shale = self.cfg.rpm_scaling_factors["shalerho_factor"]
1295        vp_factor_shale = self.cfg.rpm_scaling_factors["shalevp_factor"]
1296        vs_factor_shale = self.cfg.rpm_scaling_factors["shalevs_factor"]
1297        rho_factor_sand = self.cfg.rpm_scaling_factors["sandrho_factor"]
1298        vp_factor_sand = self.cfg.rpm_scaling_factors["sandvp_factor"]
1299        vs_factor_sand = self.cfg.rpm_scaling_factors["sandvs_factor"]
1300
1301        if self.cfg.verbose:
1302            print(
1303                f"\n... Additional random scaling factors: "
1304                f"\n ... Shale Rho {rho_factor_shale:.3f}, Vp {vp_factor_shale:.3f}, Vs {vs_factor_shale:.3f}"
1305                f"\n ... Sand Rho {rho_factor_sand:.3f}, Vp {vp_factor_sand:.3f}, Vs {vs_factor_sand:.3f}"
1306            )
1307
1308        # Apply final scaling factors to shale/sand properties
1309        props.rho[(ng <= 1.0e-2) & (lith < 2.0)] = (
1310            props.rho[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1311        )
1312        props.vp[(ng <= 1.0e-2) & (lith < 2.0)] = (
1313            props.vp[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1314        )
1315        props.vs[(ng <= 1.0e-2) & (lith < 2.0)] = (
1316            props.vs[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1317        )
1318        props.rho[(ng > 1.0e-2) & (lith < 2.0)] = (
1319            props.rho[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1320        )
1321        props.vp[(ng > 1.0e-2) & (lith < 2.0)] = (
1322            props.vp[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1323        )
1324        props.vs[(ng > 1.0e-2) & (lith < 2.0)] = (
1325            props.vs[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1326        )
1327        # Apply same factors to the fluid-factor property cubes
1328        props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1329            props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1330        )
1331        props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1332            props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1333        )
1334        props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1335            props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1336        )
1337        props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1338            props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1339        )
1340        props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1341            props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1342        )
1343        props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1344            props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1345        )
1346        return props
1347
1348    def write_property_volumes_to_disk(self):
1349        """Write Rho, Vp, Vs volumes to disk."""
1350        self.write_cube_to_disk(
1351            self.rho[:],
1352            "qc_volume_rho",
1353        )
1354        self.write_cube_to_disk(
1355            self.vp[:],
1356            "qc_volume_vp",
1357        )
1358        self.write_cube_to_disk(
1359            self.vs[:],
1360            "qc_volume_vs",
1361        )
SeismicVolume Class

This class initializes the seismic volume that contains everything.

SeismicVolume(parameters, faults, closures)
22    def __init__(self, parameters, faults, closures) -> None:
23        """
24        __init__
25        --------
26
27        The initialization function.
28
29        Parameters
30        ----------
31        parameters : datagenerator.Parameters
32            The parameters object of the project.
33        faults : np.ndarray
34            The faults array.
35        closures : np.ndarray
36            The closures array.
37        
38        Returns
39        -------
40        None
41        """
42        self.cfg = parameters
43        self.faults = faults
44        self.traps = closures
45
46        if self.cfg.model_qc_volumes:
47            # Add 0 and 45 degree angles to the list of user-input angles
48            self.angles = tuple(sorted(set((0,) + self.cfg.incident_angles + (45,))))
49        else:
50            self.angles = self.cfg.incident_angles
51        if self.cfg.verbose:
52            print(f"Angles: {self.angles}")
53
54        self.first_random_lyr = 20  # do not randomise shallow layers
55
56        self.rho = self.cfg.hdf_init(
57            "rho", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
58        )
59        self.vp = self.cfg.hdf_init(
60            "vp", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
61        )
62        self.vs = self.cfg.hdf_init(
63            "vs", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
64        )
65        self.rho_ff = self.cfg.hdf_init(
66            "rho_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
67        )
68        self.vp_ff = self.cfg.hdf_init(
69            "vp_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
70        )
71        self.vs_ff = self.cfg.hdf_init(
72            "vs_ff", shape=self.cfg.h5file.root.ModelData.faulted_depth.shape
73        )
74
75        seis_shape = (
76            len(self.angles),
77            *self.cfg.h5file.root.ModelData.faulted_depth.shape,
78        )
79        rfc_shape = (seis_shape[0], seis_shape[1], seis_shape[2], seis_shape[3] - 1)
80        self.rfc_raw = self.cfg.hdf_init("rfc_raw", shape=rfc_shape)
81        self.rfc_noise_added = self.cfg.hdf_init("rfc_noise_added", shape=rfc_shape)

__init__

The initialization function.

Parameters
  • parameters (datagenerator.Parameters): The parameters object of the project.
  • faults (np.ndarray): The faults array.
  • closures (np.ndarray): The closures array.
Returns
  • None
def build_elastic_properties(self, mixing_method='inv_vel'):
 83    def build_elastic_properties(self, mixing_method="inv_vel"):
 84        """
 85        Build Elastic Properties
 86        ------------------------
 87
 88        Creates Rho, Vp, Vs volumes using depth trends.
 89
 90        Parameters
 91        ----------
 92        mixing_method : str, optional
 93            Method for mixing Velocities. Defaults to "inv_vel".
 94
 95        """
 96        rpm = select_rpm(self.cfg)
 97        self.build_property_models_randomised_depth(
 98            rpm,
 99            mixing_method=mixing_method
100        )
Build Elastic Properties

Creates Rho, Vp, Vs volumes using depth trends.

Parameters
  • mixing_method (str, optional): Method for mixing Velocities. Defaults to "inv_vel".
def bandlimit_volumes_wavelets(self, n_wavelets=10):
102    def bandlimit_volumes_wavelets(self, n_wavelets=10):
103        # Pre-prepared n, m, f filter_specs from npy file
104        fs_nr = np.load(self.cfg.wavelets[0])
105        fs_md = np.load(self.cfg.wavelets[1])
106        fs_fr = np.load(self.cfg.wavelets[2])
107
108        for wavelet_count in range(n_wavelets):
109            low_freq_pctile = np.random.uniform(0, 100)
110            mid_freq_pctile = np.random.uniform(0, 100)
111            high_freq_pctile = np.random.uniform(0, 100)
112            low_med_high_percentiles = (
113                low_freq_pctile,
114                mid_freq_pctile,
115                high_freq_pctile,
116            )
117            print(f"low_med_high_percentiles: {low_med_high_percentiles}")
118            f_nr, a_nr, w_nr = generate_wavelet(fs_nr, low_med_high_percentiles)
119            f_md, a_md, w_md = generate_wavelet(fs_md, low_med_high_percentiles)
120            f_fr, a_fr, w_fr = generate_wavelet(fs_fr, low_med_high_percentiles)
121            _f = (f_nr, f_md, f_fr)
122            _a = (a_nr, a_md, a_fr)
123            _w = (w_nr, w_md, w_fr)
124            labels = ["Near", "Mid", "Far"]
125            pngname = os.path.join(
126                self.cfg.work_subfolder, f"random_wavelets_{wavelet_count}.png"
127            )
128            plot_wavelets(_f, _a, _w, labels, pngname)
129
130            rfc_bandlimited_wavelet = np.zeros_like(self.rfc_noise_added)
131            if self.cfg.model_qc_volumes:
132                wavelets = [
133                    w_nr,
134                    w_nr,
135                    w_md,
136                    w_fr,
137                    w_fr,
138                ]  # use near for 0 and far for 45 deg cubes
139            else:
140                wavelets = [w_nr, w_md, w_fr]
141
142            for idx in range(rfc_bandlimited_wavelet.shape[0]):
143                rfc_bandlimited_wavelet[idx, ...] = apply_wavelet(
144                    self.rfc_noise_added[idx, ...], wavelets[idx]
145                )
146            if self.cfg.lateral_filter_size > 1:
147                # Apply lateral smoothing filter
148                rfc_bandlimited_wavelet = self.apply_lateral_filter(
149                    rfc_bandlimited_wavelet
150                )
151            cumsum_wavelet_noise_added = self.apply_cumsum(rfc_bandlimited_wavelet)
152            _ = self.write_final_cubes_to_disk(
153                rfc_bandlimited_wavelet, f"seismicCubes_RFC_Wavelet_{wavelet_count}_"
154            )
155            _ = self.write_final_cubes_to_disk(
156                cumsum_wavelet_noise_added,
157                f"seismicCubes_cumsum_Wavelet_{wavelet_count}_",
158            )
def postprocess_rfc_cubes(self, rfc_data, filename_suffix='', bb=False, stack=False):
160    def postprocess_rfc_cubes(
161        self, rfc_data, filename_suffix="", bb=False, stack=False
162    ):
163        """Postprocess the RFC cubes."""
164        if bb:
165            bandlimited = self.apply_bandlimits(rfc_data, (4.0, 90.0))
166        else:
167            bandlimited = self.apply_bandlimits(rfc_data)
168        if self.cfg.lateral_filter_size > 1:
169            bandlimited = self.apply_lateral_filter(bandlimited)
170        if filename_suffix == "":
171            fname = "seismicCubes_RFC_"
172        else:
173            fname = f"seismicCubes_RFC_{filename_suffix}_"
174        _ = self.write_final_cubes_to_disk(bandlimited, fname)
175        if stack:
176            rfc_fullstack = self.stack_substacks(
177                [bandlimited[x, ...] for x, _ in enumerate(self.angles)]
178            )
179            rfc_fullstack_scaled = self._scale_seismic(rfc_fullstack)
180            self.write_cube_to_disk(
181                rfc_fullstack_scaled,
182                f"{fname}fullstack",
183            )
184
185        cumsum = self.apply_cumsum(bandlimited)
186        if filename_suffix == "":
187            fname = "seismicCubes_cumsum_"
188        else:
189            fname = f"seismicCubes_cumsum_{filename_suffix}_"
190        normalised_cumsum = self.write_final_cubes_to_disk(cumsum, fname)
191        if stack:
192            rai_fullstack = self.stack_substacks(
193                [cumsum[x, ...] for x, _ in enumerate(self.angles)]
194            )
195            rai_fullstack_scaled = self._scale_seismic(rai_fullstack)
196            self.write_cube_to_disk(
197                rai_fullstack_scaled,
198                f"{fname}fullstack",
199            )
200        return normalised_cumsum

Postprocess the RFC cubes.

def apply_augmentations(self, data, name='cumsum'):
202    def apply_augmentations(self, data, name="cumsum"):
203        seabed = self.faults.faulted_depth_maps[..., 0] / self.cfg.digi
204        augmented_data, augmented_labels = self.augment_data_and_labels(data, seabed)
205        for i, ang in enumerate(self.cfg.incident_angles):
206            data = augmented_data[i, ...]
207            fname = f"seismicCubes_{name}_{ang}_degrees_normalized_augmented"
208            self.write_cube_to_disk(data, fname)
209            self.write_cube_to_disk(augmented_labels, "hc_closures_augmented")
def apply_rmo(self, data, name='cumsum_RMO'):
211    def apply_rmo(self, data, name="cumsum_RMO"):
212        """Apply RMO to the cumsum with noise. Note rmo application expects angle in last dimension"""
213        rmo_indices = self.compute_randomRMO_1D(
214            data.shape[-1], self.cfg.incident_angles, velocity_fraction=0.015
215        )
216        rmo_applied_data = np.zeros_like(np.moveaxis(data, 0, -1))
217        for iline in range(self.cfg.cube_shape[0]):
218            for xline in range(self.cfg.cube_shape[1]):
219                rmo_applied_data[iline, xline, ...] = self.apply_rmo_augmentation(
220                    np.moveaxis(data, 0, -1)[iline, xline, ...],
221                    rmo_indices,
222                    remove_mean_rmo=True,
223                )
224        rmo_applied_data = np.moveaxis(
225            rmo_applied_data, -1, 0
226        )  # Put the angles back into 1st dimension
227        normalised_rmo_applied_data = self.write_final_cubes_to_disk(
228            rmo_applied_data, f"seismicCubes_{name}_"
229        )
230        return normalised_rmo_applied_data

Apply RMO to the cumsum with noise. Note rmo application expects angle in last dimension

def build_seismic_volumes(self):
232    def build_seismic_volumes(self):
233        """Build Seismic volumes.
234
235        * Use Zoeppritz to create RFC volumes for each required angle, add 0 & 45 degree stacks for QC
236        * Add exponential weighted noise to the angle stacks
237        * Apply bandpass filter
238        * Apply lateral filter
239        * Calculate cumsum
240        * Create full stacks by stacking the user-input angles (i.e. not including 0, 45 degree stacks)
241        * Write seismic volumes to disk
242        """
243        if self.cfg.verbose:
244            print(
245                f"Generating 3D Rock Properties Rho, Vp and Vs using depth trends for project {self.cfg.project}"
246            )
247        # Calculate Raw RFC from rho, vp, vs using Zoeppritz & write to HdF
248        self.create_rfc_volumes()
249        self.add_weighted_noise(self.faults.faulted_depth_maps)
250        if hasattr(self.cfg, "wavelets"):
251            self.bandlimit_volumes_wavelets(n_wavelets=1)
252        normalised_cumsum = self.postprocess_rfc_cubes(
253            self.rfc_noise_added[:], "", stack=True
254        )
255        self.apply_augmentations(normalised_cumsum, name="cumsum")
256        if self.cfg.model_qc_volumes:
257            _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free", bb=False)
258            if self.cfg.broadband_qc_volume:
259                _ = self.postprocess_rfc_cubes(self.rfc_raw[:], "noise_free_bb", bb=True)
260            normalised_cumsum_rmo = self.apply_rmo(normalised_cumsum)
261            self.apply_augmentations(normalised_cumsum_rmo, name="cumsum_RMO")

Build Seismic volumes.

  • Use Zoeppritz to create RFC volumes for each required angle, add 0 & 45 degree stacks for QC
  • Add exponential weighted noise to the angle stacks
  • Apply bandpass filter
  • Apply lateral filter
  • Calculate cumsum
  • Create full stacks by stacking the user-input angles (i.e. not including 0, 45 degree stacks)
  • Write seismic volumes to disk
def write_final_cubes_to_disk(self, dat, name):
276    def write_final_cubes_to_disk(self, dat, name):
277        scaled_data = self._scale_seismic(dat)
278
279        # Apply scaling factors to N, M, F cubes from csv file before determining clip limits
280        factor_near = self.cfg.rpm_scaling_factors["nearfactor"]
281        factor_mid = self.cfg.rpm_scaling_factors["midfactor"]
282        factor_far = self.cfg.rpm_scaling_factors["farfactor"]
283        cube_incr = 0
284        if self.cfg.model_qc_volumes and dat.shape[0] > 3:
285            # 0 degrees has been added at start, and 45 at end
286            cube_incr = 1
287        scaled_data[0 + cube_incr, ...] *= factor_near
288        scaled_data[1 + cube_incr, ...] *= factor_mid
289        scaled_data[2 + cube_incr, ...] *= factor_far
290
291        for i, ang in enumerate(self.cfg.incident_angles):
292            data = scaled_data[i, ...]
293            fname = f"{name}_{ang}_degrees"
294            self.write_cube_to_disk(data, fname)
295        normed_data = normalize_seismic(scaled_data)
296        for i, ang in enumerate(self.cfg.incident_angles):
297            data = normed_data[i, ...]
298            fname = f"{name}_{ang}_degrees_normalized"
299            self.write_cube_to_disk(data, fname)
300        return normed_data
@staticmethod
def random_z_rho_vp_vs(dmin=-7, dmax=7):
302    @staticmethod
303    def random_z_rho_vp_vs(dmin=-7, dmax=7):
304        delta_z_rho = int(np.random.uniform(dmin, dmax))
305        delta_z_vp = int(np.random.uniform(dmin, dmax))
306        delta_z_vs = int(np.random.uniform(dmin, dmax))
307        return delta_z_rho, delta_z_vp, delta_z_vs
def create_rfc_volumes(self):
309    def create_rfc_volumes(self):
310        """
311        Build a 3D array of PP-Reflectivity using Zoeppritz for each incident angle given.
312
313        :return: 4D array of PP-Reflectivity. 4th dimension holds reflectivities of each incident angle provided
314        """
315        theta = np.asanyarray(self.angles).reshape(
316            (-1, 1)
317        )  # Convert angles into a column vector
318
319        # Make empty array to store RPP via zoeppritz
320        zoep = np.zeros(
321            (self.rho.shape[0], self.rho.shape[1], self.rho.shape[2] - 1, theta.size),
322            dtype="complex64",
323        )
324
325        _rho = self.rho[:]
326        _vp = self.vp[:]
327        _vs = self.vs[:]
328        if self.cfg.verbose:
329            print("Checking for null values inside create_rfc_volumes:\n")
330            print(f"Rho: {np.min(_rho)}, {np.max(_rho)}")
331            print(f"Vp: {np.min(_vp)}, {np.max(_vp)}")
332            print(f"Vs: {np.min(_vs)}, {np.max(_vs)}")
333        # Doing this for each trace & all (5) angles is actually quicker than doing entire cubes for each angle
334        for i in trange(
335            self.rho.shape[0],
336            desc=f"Calculating Zoeppritz for {len(self.angles)} angles",
337        ):
338            for j in range(self.rho.shape[1]):
339                rho1, rho2 = _rho[i, j, :-1], _rho[i, j, 1:]
340                vp1, vp2 = _vp[i, j, :-1], _vp[i, j, 1:]
341                vs1, vs2 = _vs[i, j, :-1], _vs[i, j, 1:]
342                rfc = RFC(vp1, vs1, rho1, vp2, vs2, rho2, theta)
343                zoep[i, j, :, :] = rfc.zoeppritz_reflectivity().T
344        # Set any voxels with imaginary parts to 0, since all transmitted energy travels along the reflector
345        # zoep[np.where(np.imag(zoep) != 0)] = 0
346        # discard complex values and set dtype to float64
347        zoep = np.real(zoep).astype("float64")
348        del _rho, _vp, _vs
349
350        # Move the angle from last dimension to first
351        self.rfc_raw[:] = np.moveaxis(zoep, -1, 0)
352
353        if self.cfg.hdf_store:
354            for n, d in zip(
355                [
356                    "qc_volume_rfc_raw_{}_degrees".format(
357                        str(self.angles[x]).replace(".", "_")
358                    )
359                    for x in range(self.rfc_raw.shape[0])
360                ],
361                [self.rfc_raw[x, ...] for x in range(self.rfc_raw.shape[0])],
362            ):
363                write_data_to_hdf(n, d, self.cfg.hdf_master)
364        if self.cfg.model_qc_volumes:
365            # Write raw RFC values to disk
366            _ = [
367                self.write_cube_to_disk(
368                    self.rfc_raw[x, ...],
369                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
370                )
371                for x in range(self.rfc_raw.shape[0])
372            ]
373            max_amp = np.max(np.abs(self.rfc_raw[:]))
374            _ = [
375                self.write_cube_to_disk(
376                    self.rfc_raw[x, ...],
377                    f"qc_volume_rfc_raw_{self.angles[x]}_degrees",
378                )
379                for x in range(self.rfc_raw.shape[0])
380            ]

Build a 3D array of PP-Reflectivity using Zoeppritz for each incident angle given.

Returns

4D array of PP-Reflectivity. 4th dimension holds reflectivities of each incident angle provided

def noise_3d(self, cube_shape, verbose=False):
382    def noise_3d(self, cube_shape, verbose=False):
383        if verbose:
384            print("   ... inside noise3D")
385
386        noise_seed = np.random.randint(1, high=2 ** 32 - 1)
387        sign_seed = np.random.randint(1, high=2 ** 32 - 1)
388
389        np.random.seed(noise_seed)
390        noise3d = np.random.exponential(
391            1.0 / 100.0, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
392        )
393        np.random.seed(sign_seed)
394        sign = np.random.binomial(
395            1, 0.5, size=cube_shape[0] * cube_shape[1] * cube_shape[2]
396        )
397
398        sign[sign == 0] = -1
399        noise3d *= sign
400        noise3d = noise3d.reshape((cube_shape[0], cube_shape[1], cube_shape[2]))
401        return noise3d
def add_weighted_noise(self, depth_maps):
403    def add_weighted_noise(self, depth_maps):
404        """
405        Apply 3D weighted random noise to ndarray.
406
407        :return: Sum of rfc_volumes and 3D noise model, weighted by angle
408        """
409
410        from math import sin, cos
411        from datagenerator.util import mute_above_seafloor
412        from datagenerator.util import infill_surface
413
414        # Calculate standard deviation ratio to use based on user-input sn_db parameter
415        if self.cfg.verbose:
416            print(f"\n...adding random noise to {self.rfc_raw.shape[0]} cubes...")
417            print(f"S/N ratio = {self.cfg.sn_db:.4f}")
418        std_ratio = np.sqrt(10 ** (self.cfg.sn_db / 10.0))
419
420        # Compute standard deviation in bottom half of cube, and use this to scale the noise
421        # if wb has holes (i.e. Nan or 0 values), fill holes using replacement with interpolated (NN) values
422        wb_map = depth_maps[..., 0]
423        # Add test for all 0's in wb_map in case z_shift has been applied
424        if (
425            np.isnan(np.min(wb_map)) or 0 in wb_map and not np.all(wb_map == 0)
426        ):  # If wb contains nan's or at least one 0, this will return True
427            wb_map = infill_surface(wb_map)
428        wb_plus_15samples = wb_map / (self.cfg.digi + 15) * self.cfg.digi
429
430        # Use the Middle angle cube for normalisation
431        if self.cfg.model_qc_volumes:
432            # 0 and 45 degree cubes have been added
433            norm_cube = self.rfc_raw[2, ...]
434        else:
435            norm_cube = self.rfc_raw[1, ...]
436        data_below_seafloor = norm_cube[
437            mute_above_seafloor(wb_plus_15samples, np.ones(norm_cube.shape, "float"))
438            != 0.0
439        ]
440        data_std = data_below_seafloor.std()
441
442        # Create array to store the 3D noise model for each angle
443        noise3d_cubes = np.zeros_like(self.rfc_raw[:])
444
445        # Add weighted stack of noise using Hilterman equation to each angle substack
446        noise_0deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
447        noise_45deg = self.noise_3d(self.rfc_raw.shape[1:], verbose=False)
448        # Store the noise models
449        self.noise_0deg = self.cfg.hdf_init("noise_0deg", shape=noise_0deg.shape)
450        self.noise_45deg = self.cfg.hdf_init("noise_45deg", shape=noise_45deg.shape)
451        self.noise_0deg[:] = noise_0deg
452        self.noise_45deg[:] = noise_45deg
453
454        for x, ang in enumerate(self.angles):
455            weighted_noise = noise_0deg * (cos(ang) ** 2) + noise_45deg * (
456                sin(ang) ** 2
457            )
458            noise_std = weighted_noise.std()
459            noise3d_cubes[x, ...] = weighted_noise * (data_std / noise_std) / std_ratio
460            if self.cfg.verbose:
461                print(
462                    f"\t...Normalised noise3d for angle {ang}:\tMin: {noise3d_cubes[x, ...].min():.4f},"
463                    f" mean: {noise3d_cubes[x, ...].mean():.4f}, max: {noise3d_cubes[x, ...].max():.4f},"
464                    f" std: {noise3d_cubes[x, ...].std():.4f}"
465                )
466                print(
467                    f"\tS/N ratio = {self.cfg.sn_db:3.1f} dB.\n\tstd_ratio = {std_ratio:.4f}"
468                    f"\n\tdata_std = {data_std:.4f}\n\tnoise_std = {noise_std:.4f}"
469                )
470
471        self.rfc_noise_added[:] = noise3d_cubes + self.rfc_raw[:]

Apply 3D weighted random noise to ndarray.

Returns

Sum of rfc_volumes and 3D noise model, weighted by angle

def apply_bandlimits(self, data, frequencies=None):
473    def apply_bandlimits(self, data, frequencies=None):
474        """
475        Apply Butterworth Bandpass Filter to data
476        :param data: 4D array of RFC values
477        :param frequencies: Explicit frequency bounds (lower, upper)
478        :return: 4D array of band-limited RFC
479        """
480        if self.cfg.verbose:
481            print(f"Data Min: {np.min(data):.2f}, Data Max: {np.max(data):.2f}")
482        dt = self.cfg.digi / 1000.0
483        if (
484            data.shape[-1] / self.cfg.infill_factor - self.cfg.pad_samples
485            == self.cfg.cube_shape[-1]
486        ):
487            # Input data is infilled
488            dt /= self.cfg.infill_factor
489        if frequencies:  # if frequencies explicitly defined
490            low = frequencies[0]
491            high = frequencies[1]
492        else:
493            low = self.cfg.lowfreq
494            high = self.cfg.highfreq
495        b, a = derive_butterworth_bandpass(
496            low, high, (dt * 1000.0), order=self.cfg.order
497        )
498        if self.cfg.verbose:
499            print(f"\t... Low Frequency; {low:.2f} Hz, High Frequency: {high:.2f} Hz")
500            print(
501                f"\t... start_width: {1. / (dt * low):.4f}, end_width: {1. / (dt * high):.4f}"
502            )
503
504        # Loop over 3D angle stack arrays
505        if self.cfg.verbose:
506            print(" ... shape of data being bandlimited = ", data.shape, "\n")
507        num = data.shape[0]
508        if self.cfg.verbose:
509            print(f"Applying Bandpass to {num} cubes")
510        if self.cfg.multiprocess_bp:
511            # Much faster to use multiprocessing and apply band-limitation to entire cube at once
512            with Pool(processes=min(num, os.cpu_count() - 2)) as p:
513                iterator = zip(
514                    [data[x, ...] for x in range(num)],
515                    itertools.repeat(b),
516                    itertools.repeat(a),
517                )
518                out_cubes_mp = p.starmap(self._run_bandpass_on_cubes, iterator)
519            out_cubes = np.asarray(out_cubes_mp)
520        else:
521            # multiprocessing can fail using Python version 3.6 and very large arrays
522            out_cubes = np.zeros_like(data)
523            for idx in range(num):
524                out_cubes[idx, ...] = self._run_bandpass_on_cubes(data[idx, ...], b, a)
525
526        return out_cubes

Apply Butterworth Bandpass Filter to data

Parameters
  • data: 4D array of RFC values
  • frequencies: Explicit frequency bounds (lower, upper)
Returns

4D array of band-limited RFC

def apply_lateral_filter(self, data):
533    def apply_lateral_filter(self, data):
534        from scipy.ndimage import uniform_filter
535
536        n_filt = self.cfg.lateral_filter_size
537        return uniform_filter(data, size=(0, n_filt, n_filt, 0))
def apply_cumsum(self, data):
539    def apply_cumsum(self, data):
540        """Calculate cumulative sum on the Z-axis of input data
541        Sipmap's cumsum also applies an Ormsby filter to the cumulatively summed data
542        To replicate this, apply a butterworth filter to the data using 2, 100Hz frequency limits
543        todo: make an ormsby function"""
544        cumsum = data.cumsum(axis=-1)
545        return self.apply_bandlimits(cumsum, (2.0, 100.0))

Calculate cumulative sum on the Z-axis of input data Sipmap's cumsum also applies an Ormsby filter to the cumulatively summed data To replicate this, apply a butterworth filter to the data using 2, 100Hz frequency limits todo: make an ormsby function

@staticmethod
def stack_substacks(cube_list):
547    @staticmethod
548    def stack_substacks(cube_list):
549        """Stack cubes by taking mean"""
550        return np.mean(cube_list, axis=0)

Stack cubes by taking mean

def augment_data_and_labels(self, normalised_seismic, seabed):
552    def augment_data_and_labels(self, normalised_seismic, seabed):
553        from datagenerator.Augmentation import tz_stretch, uniform_stretch
554
555        hc_labels = self.cfg.h5file.root.ModelData.hc_labels[:]
556        data, labels = tz_stretch(
557            normalised_seismic,
558            hc_labels[..., : self.cfg.cube_shape[2] + self.cfg.pad_samples - 1],
559            seabed,
560        )
561        seismic, hc = uniform_stretch(data, labels)
562        return seismic, hc
def compute_randomRMO_1D( self, nsamples, inc_angle_list, velocity_fraction=0.015, return_plot=False):
564    def compute_randomRMO_1D(
565        self, nsamples, inc_angle_list, velocity_fraction=0.015, return_plot=False
566    ):
567        """
568        compute 1D indices for to apply residual moveout via interpolation
569        - nsamples is the array length in depth
570        - inc_angle_list is a list of angle in degrees for angle stacks to which
571        results will be applied
572        - velocity_fraction is the maximum percent error in absolute value.
573        for example, .03 would generate RMO based on velocity from .97 to 1.03 times correct velocity
574        """
575        from scipy.interpolate import UnivariateSpline
576
577        input_indices = np.arange(nsamples)
578        if self.cfg.qc_plots:
579            from datagenerator.util import import_matplotlib
580
581            plt = import_matplotlib()
582            plt.close(1)
583            plt.figure(1, figsize=(4.75, 6.0), dpi=150)
584            plt.clf()
585            plt.grid()
586            plt.xlim((-10, 10))
587            plt.ylim((nsamples, 0))
588            plt.ylabel("depth (samples)")
589            plt.xlabel("RMO (samples)")
590            plt.savefig(
591                os.path.join(self.cfg.work_subfolder, "rmo_1d.png"), format="png"
592            )
593        # generate 1 to 3 randomly located velocity fractions at random depth indices
594        for _ in range(250):
595            num_tie_points = np.random.randint(low=1, high=4)
596            tie_fractions = np.random.uniform(
597                low=-velocity_fraction, high=velocity_fraction, size=num_tie_points
598            )
599            if num_tie_points > 1:
600                tie_indices = np.random.uniform(
601                    low=0.25 * nsamples, high=0.75 * nsamples, size=num_tie_points
602                ).astype("int")
603
604            else:
605                tie_indices = int(
606                    np.random.uniform(low=0.25 * nsamples, high=0.75 * nsamples)
607                )
608            tie_fractions = np.hstack(([0.0, 0.0], tie_fractions, [0.0, 0.0]))
609            tie_indices = np.hstack(([0, 1], tie_indices, [nsamples - 2, nsamples - 1]))
610            tie_indices = np.sort(tie_indices)
611            if np.diff(tie_indices).min() <= 0.0:
612                continue
613            s = UnivariateSpline(tie_indices, tie_fractions, s=0.0)
614            tie_fraction_array = s(input_indices)
615            output_indices = np.zeros((nsamples, len(inc_angle_list)))
616            for i, iangle in enumerate(inc_angle_list):
617                output_indices[:, i] = input_indices * (
618                    1.0
619                    + tie_fraction_array * np.tan(float(iangle) * np.pi / 180.0) ** 2
620                )
621            if np.abs(output_indices[:, i] - input_indices).max() < 10.0:
622                break
623        for i, iangle in enumerate(inc_angle_list):
624            if self.cfg.qc_plots:
625                plt.plot(
626                    output_indices[:, i] - input_indices,
627                    input_indices,
628                    label=str(inc_angle_list[i]),
629                )
630
631                if i == len(inc_angle_list) - 1:
632                    output_tie_indices = tie_indices * (
633                        1.0
634                        + tie_fraction_array[tie_indices]
635                        * np.tan(float(iangle) * np.pi / 180.0) ** 2
636                    )
637                    plt.plot(output_tie_indices - tie_indices, tie_indices, "ko")
638                    rmo_min = (output_indices[:, -1] - input_indices).min()
639                    rmo_max = (output_indices[:, -1] - input_indices).max()
640                    rmo_range = np.array((rmo_min, rmo_max)).round(1)
641                    plt.title(
642                        "simultated RMO arrival time offsets\n"
643                        + "rmo range = "
644                        + str(rmo_range),
645                        fontsize=11,
646                    )
647                    plt.legend()
648                    plt.tight_layout()
649                plt.savefig(
650                    os.path.join(self.cfg.work_subfolder, "rmo_arrival_time.png"),
651                    format="png",
652                )
653
654        if return_plot:
655            return output_indices, plt
656        else:
657            return output_indices

compute 1D indices for to apply residual moveout via interpolation

  • nsamples is the array length in depth
  • inc_angle_list is a list of angle in degrees for angle stacks to which results will be applied
  • velocity_fraction is the maximum percent error in absolute value. for example, .03 would generate RMO based on velocity from .97 to 1.03 times correct velocity
def apply_rmo_augmentation(self, angle_gather, rmo_indices, remove_mean_rmo=False):
659    def apply_rmo_augmentation(self, angle_gather, rmo_indices, remove_mean_rmo=False):
660        """
661        apply residual moveout via interpolation
662        applied to 'angle_gather' with results from compute_randomRMO_1D in rmo_indices
663        angle_gather and rmo_indices need to be 2D arrays with the same shape (n_samples x n_angles)
664        - angle_gather is the array to which RMO is applied
665        - rmo_indices is the array used to modify the two-way-times of angle_gather
666        - remove_mean_rmo can be used to (when True) to apply relative RMO that keeps
667        event energy at roughly the same TWT. for example to avoid applying RMO
668        to both data and labels
669        """
670        if remove_mean_rmo:
671            # apply de-trending to ensure that rmo does not change TWT
672            # after rmo compared to before rmo 'augmentation'
673            residual_times = rmo_indices.mean(axis=-1) - np.arange(
674                angle_gather.shape[0]
675            )
676            residual_rmo_indices = rmo_indices * 1.0
677            residual_rmo_indices[:, 0] -= residual_times
678            residual_rmo_indices[:, 1] -= residual_times
679            residual_rmo_indices[:, 2] -= residual_times
680            _rmo_indices = residual_rmo_indices
681        else:
682            _rmo_indices = rmo_indices
683
684        rmo_angle_gather = np.zeros_like(angle_gather)
685        for iangle in range(len(rmo_indices[-1])):
686            rmo_angle_gather[:, iangle] = np.interp(
687                range(angle_gather.shape[0]),
688                _rmo_indices[:, iangle],
689                angle_gather[:, iangle],
690            )
691
692        return rmo_angle_gather

apply residual moveout via interpolation applied to 'angle_gather' with results from compute_randomRMO_1D in rmo_indices angle_gather and rmo_indices need to be 2D arrays with the same shape (n_samples x n_angles)

  • angle_gather is the array to which RMO is applied
  • rmo_indices is the array used to modify the two-way-times of angle_gather
  • remove_mean_rmo can be used to (when True) to apply relative RMO that keeps event energy at roughly the same TWT. for example to avoid applying RMO to both data and labels
def build_property_models_randomised_depth(self, rpm, mixing_method='inv_vel'):
 694    def build_property_models_randomised_depth(self, rpm, mixing_method="inv_vel"):
 695        """
 696        Build property models with randomised depth
 697        -------------------------------------------
 698        Builds property models with randomised depth.
 699
 700        v2 but with Backus moduli mixing instead of inverse velocity mixing
 701        mixing_method one of ["InverseVelocity", "BackusModuli"]
 702
 703        Parameters
 704        ----------
 705        rpm : str
 706            Rock properties model.
 707        mixing_method : str
 708            Mixing method for randomised depth.
 709        
 710        Returns
 711        -------
 712        None
 713        """
 714        layer_half_range = self.cfg.rpm_scaling_factors["layershiftsamples"]
 715        property_half_range = self.cfg.rpm_scaling_factors["RPshiftsamples"]
 716
 717        depth = self.cfg.h5file.root.ModelData.faulted_depth[:]
 718        lith = self.faults.faulted_lithology[:]
 719        net_to_gross = self.faults.faulted_net_to_gross[:]
 720
 721        oil_closures = self.cfg.h5file.root.ModelData.oil_closures[:]
 722        gas_closures = self.cfg.h5file.root.ModelData.gas_closures[:]
 723
 724        integer_faulted_age = (
 725            self.cfg.h5file.root.ModelData.faulted_age_volume[:] + 0.00001
 726        ).astype(int)
 727
 728        # Use empty class object to store all Rho, Vp, Vs volumes (randomised, fluid factor and non randomised)
 729        mixed_properties = ElasticProperties3D()
 730        mixed_properties.rho = np.zeros_like(lith)
 731        mixed_properties.vp = np.zeros_like(lith)
 732        mixed_properties.vs = np.zeros_like(lith)
 733        mixed_properties.sum_net = np.zeros_like(lith)
 734        cube_shape = lith.shape
 735
 736        if self.cfg.verbose:
 737            mixed_properties.rho_not_random = np.zeros_like(lith)
 738            mixed_properties.vp_not_random = np.zeros_like(lith)
 739            mixed_properties.vs_not_random = np.zeros_like(lith)
 740
 741        # water layer
 742        mixed_properties = self.water_properties(lith, mixed_properties)
 743
 744        mixed_properties.rho_ff = np.copy(mixed_properties.rho)
 745        mixed_properties.vp_ff = np.copy(mixed_properties.vp)
 746        mixed_properties.vs_ff = np.copy(mixed_properties.vs)
 747
 748        delta_z_lyrs = []
 749        delta_z_shales = []
 750        delta_z_brine_sands = []
 751        delta_z_oil_sands = []
 752        delta_z_gas_sands = []
 753
 754        for z in range(1, integer_faulted_age.max()):
 755            __i, __j, __k = np.where(integer_faulted_age == z)
 756
 757            if len(__k) > 0:
 758                if self.cfg.verbose:
 759                    print(
 760                        f"\n\n*********************\n ... layer number = {z}"
 761                        f"\n ... n2g (min,mean,max) = "
 762                        f"({np.min(net_to_gross[__i, __j, __k]).round(3)},"
 763                        f"{np.mean(net_to_gross[__i, __j, __k]).round(3)},"
 764                        f"{np.max(net_to_gross[__i, __j, __k]).round(3)},)"
 765                    )
 766
 767                delta_z_layer = self.get_delta_z_layer(z, layer_half_range, __k)
 768                delta_z_lyrs.append(delta_z_layer)
 769
 770            # shale required for all voxels
 771            i, j, k = np.where((net_to_gross >= 0.0) & (integer_faulted_age == z))
 772
 773            shale_voxel_count = len(k)
 774            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 775                z, property_half_range
 776            )
 777            delta_z_shales.append((delta_z_rho, delta_z_vp, delta_z_vs))
 778
 779            if len(k) > 0:
 780                _k_rho = list(
 781                    np.array(k + delta_z_layer + delta_z_rho).clip(
 782                        0, cube_shape[-1] - 10
 783                    )
 784                )
 785                _k_vp = list(
 786                    np.array(k + delta_z_layer + delta_z_vp).clip(
 787                        0, cube_shape[-1] - 10
 788                    )
 789                )
 790                _k_vs = list(
 791                    np.array(k + delta_z_layer + delta_z_vs).clip(
 792                        0, cube_shape[-1] - 10
 793                    )
 794                )
 795
 796                if self.cfg.verbose:
 797                    print(
 798                        f" ... shale: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 799                    )
 800                    print(f" ... shale: i = {np.mean(i)}")
 801                    print(f" ... shale: k = {np.mean(k)}")
 802                    print(f" ... shale: _k = {np.mean(_k_rho)}")
 803
 804                mixed_properties = self.calculate_shales(
 805                    xyz=(i, j, k),
 806                    shifts=(_k_rho, _k_vp, _k_vs),
 807                    props=mixed_properties,
 808                    rpm=rpm,
 809                    depth=depth,
 810                    z=z,
 811                )
 812
 813            # brine sand or mixture of brine sand and shale in same voxel
 814            i, j, k = np.where(
 815                (net_to_gross > 0.0)
 816                & (oil_closures == 0.0)
 817                & (gas_closures == 0.0)
 818                & (integer_faulted_age == z)
 819            )
 820            brine_voxel_count = len(k)
 821            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 822                z, property_half_range
 823            )
 824            delta_z_brine_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 825
 826            if len(k) > 0:
 827                _k_rho = list(
 828                    np.array(k + delta_z_layer + delta_z_rho).clip(
 829                        0, cube_shape[-1] - 10
 830                    )
 831                )
 832                _k_vp = list(
 833                    np.array(k + delta_z_layer + delta_z_vp).clip(
 834                        0, cube_shape[-1] - 10
 835                    )
 836                )
 837                _k_vs = list(
 838                    np.array(k + delta_z_layer + delta_z_vs).clip(
 839                        0, cube_shape[-1] - 10
 840                    )
 841                )
 842
 843                if self.cfg.verbose:
 844                    print(
 845                        f"\n ... brine: (delta_z_rho, delta_z_vp, delta_z_vs) = {delta_z_rho, delta_z_vp, delta_z_vs}"
 846                    )
 847                    print(f" ... brine: i = {np.mean(i)}")
 848                    print(f" ... brine: k = {np.mean(k)}")
 849                    print(f" ... brine: _k = {np.mean(_k_rho)}")
 850
 851                mixed_properties = self.calculate_sands(
 852                    xyz=(i, j, k),
 853                    shifts=(_k_rho, _k_vp, _k_vs),
 854                    props=mixed_properties,
 855                    rpm=rpm,
 856                    depth=depth,
 857                    ng=net_to_gross,
 858                    z=z,
 859                    fluid="brine",
 860                    mix=mixing_method,
 861                )
 862
 863            # oil sands
 864            i, j, k = np.where(
 865                (net_to_gross > 0.0)
 866                & (oil_closures == 1.0)
 867                & (gas_closures == 0.0)
 868                & (integer_faulted_age == z)
 869            )
 870            oil_voxel_count = len(k)
 871            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 872                z, property_half_range
 873            )
 874            delta_z_oil_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 875
 876            if len(k) > 0:
 877                _k_rho = list(
 878                    np.array(k + delta_z_layer + delta_z_rho).clip(
 879                        0, cube_shape[-1] - 10
 880                    )
 881                )
 882                _k_vp = list(
 883                    np.array(k + delta_z_layer + delta_z_vp).clip(
 884                        0, cube_shape[-1] - 10
 885                    )
 886                )
 887                _k_vs = list(
 888                    np.array(k + delta_z_layer + delta_z_vs).clip(
 889                        0, cube_shape[-1] - 10
 890                    )
 891                )
 892
 893                if self.cfg.verbose:
 894                    print(
 895                        "\n\n ... Perform fluid substitution for oil sands. ",
 896                        "\n ... Number oil voxels in closure = ",
 897                        mixed_properties.rho[i, j, k].shape,
 898                    )
 899                    print(
 900                        " ... closures_oil min/mean/max =  ",
 901                        oil_closures.min(),
 902                        oil_closures.mean(),
 903                        oil_closures.max(),
 904                    )
 905                    print(
 906                        "\n\n ... np.all(oil_sand_rho[:,:,:] == brine_sand_rho[:,:,:]) ",
 907                        np.all(
 908                            rpm.calc_oil_sand_properties(
 909                                depth[:], depth[:], depth[:]
 910                            ).rho
 911                            == rpm.calc_brine_sand_properties(
 912                                depth[:], depth[:], depth[:]
 913                            ).rho
 914                        ),
 915                    )
 916                    print(
 917                        " ... oil: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 918                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 919                    )
 920
 921                if self.cfg.verbose:
 922                    print(" ... oil: i = " + str(np.mean(i)))
 923                    print(" ... oil: k = " + str(np.mean(k)))
 924                    print(" ... oil: _k = " + str(np.mean(_k_rho)))
 925
 926                mixed_properties = self.calculate_sands(
 927                    xyz=(i, j, k),
 928                    shifts=(_k_rho, _k_vp, _k_vs),
 929                    props=mixed_properties,
 930                    rpm=rpm,
 931                    depth=depth,
 932                    ng=net_to_gross,
 933                    z=z,
 934                    fluid="oil",
 935                    mix=mixing_method,
 936                )
 937
 938            # gas sands
 939            i, j, k = np.where(
 940                (net_to_gross > 0.0)
 941                & (oil_closures == 0.0)
 942                & (gas_closures == 1.0)
 943                & (integer_faulted_age == z)
 944            )
 945            gas_voxel_count = len(k)
 946            delta_z_rho, delta_z_vp, delta_z_vs = self.get_delta_z_properties(
 947                z, property_half_range
 948            )
 949            delta_z_gas_sands.append((delta_z_rho, delta_z_vp, delta_z_vs))
 950
 951            if len(k) > 0:
 952                _k_rho = list(
 953                    np.array(k + delta_z_layer + delta_z_rho).clip(
 954                        0, cube_shape[-1] - 10
 955                    )
 956                )
 957                _k_vp = list(
 958                    np.array(k + delta_z_layer + delta_z_vp).clip(
 959                        0, cube_shape[-1] - 10
 960                    )
 961                )
 962                _k_vs = list(
 963                    np.array(k + delta_z_layer + delta_z_vs).clip(
 964                        0, cube_shape[-1] - 10
 965                    )
 966                )
 967
 968                if self.cfg.verbose:
 969                    print(
 970                        "\n ... Perform fluid substitution for gas sands. ",
 971                        "\n ... Number gas voxels in closure = ",
 972                        mixed_properties.rho[i, j, k].shape,
 973                    )
 974                    print(
 975                        " ... closures_gas min/mean/max =  ",
 976                        gas_closures.min(),
 977                        gas_closures.mean(),
 978                        gas_closures.max(),
 979                    )
 980                    print(
 981                        " ... gas: (delta_z_rho, delta_z_vp, delta_z_vs) = "
 982                        + str((delta_z_rho, delta_z_vp, delta_z_vs))
 983                    )
 984
 985                if self.cfg.verbose:
 986                    print(" ... gas: i = " + str(np.mean(i)))
 987                    print(" ... gas: k = " + str(np.mean(k)))
 988                    print(" ... gas: _k = " + str(np.mean(_k_rho)))
 989
 990                mixed_properties = self.calculate_sands(
 991                    xyz=(i, j, k),
 992                    shifts=(_k_rho, _k_vp, _k_vs),
 993                    props=mixed_properties,
 994                    rpm=rpm,
 995                    depth=depth,
 996                    ng=net_to_gross,
 997                    z=z,
 998                    fluid="gas",
 999                    mix=mixing_method,
1000                )
1001
1002            # layer check
1003            __i, __j, __k = np.where(integer_faulted_age == z)
1004
1005            if len(__k) > 0:
1006                if self.cfg.verbose:
1007                    print(
1008                        "\n ... layer number = "
1009                        + str(z)
1010                        + "\n ... sum_net (min,mean,max) = "
1011                        + str(
1012                            (
1013                                mixed_properties.sum_net[__i, __j, __k].min(),
1014                                mixed_properties.sum_net[__i, __j, __k].mean(),
1015                                mixed_properties.sum_net[__i, __j, __k].max(),
1016                            )
1017                        )
1018                    )
1019                    print(
1020                        "\n ... layer number = "
1021                        + str(z)
1022                        + "\n ... (shale, brine, oil, gas) voxel_counts = "
1023                        + str(
1024                            (
1025                                shale_voxel_count,
1026                                brine_voxel_count,
1027                                oil_voxel_count,
1028                                gas_voxel_count,
1029                            )
1030                        )
1031                        + "\n ... shale_count = brine+oil+gas? "
1032                        + str(
1033                            shale_voxel_count
1034                            == brine_voxel_count + oil_voxel_count + gas_voxel_count
1035                        )
1036                        + "\n*********************"
1037                    )
1038
1039        # overwrite rho, vp, vs for salt if required
1040        if self.cfg.include_salt:
1041            # Salt. Set lith = 2.0
1042            i, j, k = np.where(lith == 2.0)
1043            mixed_properties.rho[i, j, k] = 2.17  # divide by 2 since lith = 2.0
1044            mixed_properties.vp[i, j, k] = 4500.0
1045            mixed_properties.vs[i, j, k] = 2250.0
1046            # Include fluidfactor rho, vp, vs inside salt body
1047            mixed_properties.rho_ff[i, j, k] = mixed_properties.rho[i, j, k]
1048            mixed_properties.vp_ff[i, j, k] = mixed_properties.vp[i, j, k]
1049            mixed_properties.vs_ff[i, j, k] = mixed_properties.vs[i, j, k]
1050
1051        if self.cfg.verbose:
1052            print("Checking for null values inside build_randomised_properties:\n")
1053            print(
1054                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1055            )
1056            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1057            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1058
1059        # Fix empty voxels at base of property volumes
1060        mixed_properties = self.fix_zero_values_at_base(mixed_properties)
1061
1062        if self.cfg.verbose:
1063            print(
1064                "Checking for null values inside build_randomised_properties AFTER fix:\n"
1065            )
1066            print(
1067                f"Rho: {np.min(mixed_properties.rho)}, {np.max(mixed_properties.rho)}"
1068            )
1069            print(f"Vp: {np.min(mixed_properties.vp)}, {np.max(mixed_properties.vp)}")
1070            print(f"Vs: {np.min(mixed_properties.vs)}, {np.max(mixed_properties.vs)}")
1071
1072        mixed_properties = self.apply_scaling_factors(
1073            mixed_properties, net_to_gross, lith
1074        )
1075        self.rho[:] = mixed_properties.rho
1076        self.vp[:] = mixed_properties.vp
1077        self.vs[:] = mixed_properties.vs
1078
1079        self.rho_ff[:] = mixed_properties.rho_ff
1080        self.vp_ff[:] = mixed_properties.vp_ff
1081        self.vs_ff[:] = mixed_properties.vs_ff
1082
1083        if self.cfg.qc_plots:
1084            print("\nCreating RPM qc plots")
1085            from rockphysics.RockPropertyModels import rpm_qc_plots
1086
1087            rpm_qc_plots(self.cfg, rpm)
1088
1089        if self.cfg.model_qc_volumes:
1090            self.write_property_volumes_to_disk()
Build property models with randomised depth

Builds property models with randomised depth.

v2 but with Backus moduli mixing instead of inverse velocity mixing mixing_method one of ["InverseVelocity", "BackusModuli"]

Parameters
  • rpm (str): Rock properties model.
  • mixing_method (str): Mixing method for randomised depth.
Returns
  • None
def water_properties(self, lith, properties):
1092    def water_properties(self, lith, properties):
1093        i, j, k = np.where(lith < 0.0)
1094        properties.rho[i, j, k] = 1.028
1095        properties.vp[i, j, k] = 1500.0
1096        properties.vs[i, j, k] = 1000.0
1097        return properties
def get_delta_z_layer(self, z, half_range, z_cells):
1099    def get_delta_z_layer(self, z, half_range, z_cells):
1100        if z > self.first_random_lyr:
1101            delta_z_layer = int(np.random.uniform(-half_range, half_range))
1102        else:
1103            delta_z_layer = 0
1104        if self.cfg.verbose:
1105            print(f" .... Layer {z}: voxel_count = {len(z_cells)}")
1106            print(f" .... Layer {z}: delta_z_layer = {delta_z_layer}")
1107            print(
1108                f" .... Layer {z}: z-range (m): {np.min(z_cells) * self.cfg.digi}, "
1109                f"{np.max(z_cells) * self.cfg.digi}"
1110            )
1111        return delta_z_layer
def get_delta_z_properties(self, z, half_range):
1113    def get_delta_z_properties(self, z, half_range):
1114        if z > self.first_random_lyr:
1115            delta_z_rho, delta_z_vp, delta_z_vs = self.random_z_rho_vp_vs(
1116                dmin=-half_range, dmax=half_range
1117            )
1118        else:
1119            delta_z_rho, delta_z_vp, delta_z_vs = (0, 0, 0)
1120        return delta_z_rho, delta_z_vp, delta_z_vs
def calculate_shales(self, xyz, shifts, props, rpm, depth, z):
1122    def calculate_shales(self, xyz, shifts, props, rpm, depth, z):
1123        # Shales required for all voxels, other than water
1124        # Calculate the properties, select how to mix with other facies later
1125        i, j, k = xyz
1126        k_rho, k_vp, k_vs = shifts
1127
1128        z_rho = depth[i, j, k_rho]
1129        z_vp = depth[i, j, k_vp]
1130        z_vs = depth[i, j, k_vs]
1131        shales = rpm.calc_shale_properties(z_rho, z_vp, z_vs)
1132
1133        # Do not use net to gross here.
1134        # Since every voxel will have some shale in, apply the net to gross
1135        # when combining shales and sands
1136        # _ng = (1.0 - ng[i, j, k])
1137
1138        props.rho[i, j, k] = shales.rho  # * _ng
1139        props.rho_ff[i, j, k] = shales.rho  # * _ng
1140        props.vp[i, j, k] = shales.vp  # * _ng
1141        props.vp_ff[i, j, k] = shales.vp  # * _ng
1142        props.vs[i, j, k] = shales.vs  # * _ng
1143        props.vs_ff[i, j, k] = shales.vs  # * _ng
1144        # props.sum_net[i, j, k] = _ng
1145
1146        if self.cfg.verbose and z > self.first_random_lyr:
1147            # Calculate non-randomised properties and differences
1148            _z0 = depth[i, j, k]
1149            _shales0 = rpm.calc_shale_properties(_z0, _z0, _z0)
1150            props.rho_not_random[i, j, k] = _shales0.rho  # * _ng
1151            props.vp_not_random[i, j, k] = _shales0.vp  # * _ng
1152            props.vs_not_random[i, j, k] = _shales0.vs  # * _ng
1153
1154            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1155            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1156            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1157            pct_change_rho = delta_rho.mean().round(3)
1158            pct_change_vp = delta_vp.mean().round(3)
1159            pct_change_vs = delta_vs.mean().round(3)
1160            print(
1161                f" ... shale: randomization percent change (rho,vp,vs) = "
1162                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1163            )
1164            print(
1165                f" ... shale: min/max pre-randomization (rho, vp, vs)  = "
1166                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1167                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1168                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1169                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1170                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1171                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1172            )
1173            print(
1174                f" ... shale: min/max post-randomization (rho, vp, vs) = "
1175                f"{np.min(props.rho[i, j, k]):.3f} - "
1176                f"{np.max(props.rho[i, j, k]):.3f}, "
1177                f"{np.min(props.vp[i, j, k]):.2f} - "
1178                f"{np.max(props.vp[i, j, k]):.2f}, "
1179                f"{np.min(props.vs[i, j, k]):.2f} - "
1180                f"{np.max(props.vs[i, j, k]):.2f}"
1181            )
1182
1183        return props
def calculate_sands(self, xyz, shifts, props, rpm, depth, ng, z, fluid, mix='inv_vel'):
1185    def calculate_sands(
1186        self, xyz, shifts, props, rpm, depth, ng, z, fluid, mix="inv_vel"
1187    ):
1188        # brine sand or mixture of brine sand and shale in same voxel
1189        # - perform velocity sums in slowness or via backus moduli mixing
1190        i, j, k = xyz
1191        k_rho, k_vp, k_vs = shifts
1192
1193        z_rho = depth[i, j, k_rho]
1194        z_vp = depth[i, j, k_vp]
1195        z_vs = depth[i, j, k_vs]
1196        if fluid == "brine":
1197            sands = rpm.calc_brine_sand_properties(z_rho, z_vp, z_vs)
1198        elif fluid == "oil":
1199            sands = rpm.calc_oil_sand_properties(z_rho, z_vp, z_vs)
1200        elif fluid == "gas":
1201            sands = rpm.calc_gas_sand_properties(z_rho, z_vp, z_vs)
1202
1203        shales = RockProperties(
1204            rho=props.rho[i, j, k], vp=props.vp[i, j, k], vs=props.vs[i, j, k]
1205        )
1206        rpmix = EndMemberMixing(shales, sands, ng[i, j, k])
1207
1208        if mix == "inv_vel":
1209            rpmix.inverse_velocity_mixing()
1210        else:
1211            rpmix.backus_moduli_mixing()
1212
1213        props.sum_net[i, j, k] += ng[i, j, k]
1214        props.rho[i, j, k] = rpmix.rho
1215        props.vp[i, j, k] = rpmix.vp
1216        props.vs[i, j, k] = rpmix.vs
1217
1218        if self.cfg.verbose and z > self.first_random_lyr:
1219            # Calculate non-randomised properties and differences
1220            _z0 = depth[i, j, k]
1221            if fluid == "brine":
1222                _sands0 = rpm.calc_brine_sand_properties(_z0, _z0, _z0)
1223            elif fluid == "oil":
1224                _sands0 = rpm.calc_oil_sand_properties(_z0, _z0, _z0)
1225            elif fluid == "gas":
1226                _sands0 = rpm.calc_gas_sand_properties(_z0, _z0, _z0)
1227
1228            rpmix_0 = EndMemberMixing(shales, _sands0, ng[i, j, k])
1229            if mix == "inv_vel":
1230                # Apply Inverse Velocity mixing
1231                rpmix_0.inverse_velocity_mixing()
1232            else:
1233                # Apply Backus Moduli mixing
1234                rpmix_0.backus_moduli_mixing()
1235            props.rho_not_random[i, j, k] = rpmix_0.rho
1236            props.vp_not_random[i, j, k] = rpmix_0.vp
1237            props.vs_not_random[i, j, k] = rpmix_0.vs
1238
1239            delta_rho = 1.0 - (props.rho[i, j, k] / props.rho_not_random[i, j, k])
1240            delta_vp = 1.0 - (props.vp[i, j, k] / props.vp_not_random[i, j, k])
1241            delta_vs = 1.0 - (props.vs[i, j, k] / props.vs_not_random[i, j, k])
1242            pct_change_rho = delta_rho.mean().round(3)
1243            pct_change_vp = delta_vp.mean().round(3)
1244            pct_change_vs = delta_vs.mean().round(3)
1245            print(
1246                f" ... {fluid}: randomization percent change (rho,vp,vs) = "
1247                f"{pct_change_rho}, {pct_change_vp }, {pct_change_vs}"
1248            )
1249            print(
1250                f" ... {fluid}: min/max pre-randomization (rho, vp, vs)  = "
1251                f"{np.min(props.rho_not_random[i, j, k]):.3f} - "
1252                f"{np.max(props.rho_not_random[i, j, k]):.3f}, "
1253                f"{np.min(props.vp_not_random[i, j, k]):.2f} - "
1254                f"{np.max(props.vp_not_random[i, j, k]):.2f}, "
1255                f"{np.min(props.vs_not_random[i, j, k]):.2f} - "
1256                f"{np.max(props.vs_not_random[i, j, k]):.2f}"
1257            )
1258            print(
1259                f" ... {fluid}: min/max post-randomization (rho, vp, vs) = "
1260                f"{np.min(props.rho[i, j, k]):.3f} - "
1261                f"{np.max(props.rho[i, j, k]):.3f}, "
1262                f"{np.min(props.vp[i, j, k]):.2f} - "
1263                f"{np.max(props.vp[i, j, k]):.2f}, "
1264                f"{np.min(props.vs[i, j, k]):.2f} - "
1265                f"{np.max(props.vs[i, j, k]):.2f}"
1266            )
1267
1268        return props
@staticmethod
def fix_zero_values_at_base(props):
1270    @staticmethod
1271    def fix_zero_values_at_base(props):
1272        """Check for zero values at base of property volumes and replace with
1273        shallower values if present
1274
1275        Args:
1276            a ([type]): [description]
1277            b ([type]): [description]
1278            c ([type]): [description]
1279        """
1280        for vol in [
1281            props.rho,
1282            props.vp,
1283            props.vs,
1284            props.rho_ff,
1285            props.vp_ff,
1286            props.vs_ff,
1287        ]:
1288            for (x, y, z) in np.argwhere(vol == 0.0):
1289                vol[x, y, z] = vol[x, y, z - 1]
1290        return props

Check for zero values at base of property volumes and replace with shallower values if present

Args: a ([type]): [description] b ([type]): [description] c ([type]): [description]

def apply_scaling_factors(self, props, ng, lith):
1292    def apply_scaling_factors(self, props, ng, lith):
1293        """Apply final random scaling factors to sands and shales"""
1294        rho_factor_shale = self.cfg.rpm_scaling_factors["shalerho_factor"]
1295        vp_factor_shale = self.cfg.rpm_scaling_factors["shalevp_factor"]
1296        vs_factor_shale = self.cfg.rpm_scaling_factors["shalevs_factor"]
1297        rho_factor_sand = self.cfg.rpm_scaling_factors["sandrho_factor"]
1298        vp_factor_sand = self.cfg.rpm_scaling_factors["sandvp_factor"]
1299        vs_factor_sand = self.cfg.rpm_scaling_factors["sandvs_factor"]
1300
1301        if self.cfg.verbose:
1302            print(
1303                f"\n... Additional random scaling factors: "
1304                f"\n ... Shale Rho {rho_factor_shale:.3f}, Vp {vp_factor_shale:.3f}, Vs {vs_factor_shale:.3f}"
1305                f"\n ... Sand Rho {rho_factor_sand:.3f}, Vp {vp_factor_sand:.3f}, Vs {vs_factor_sand:.3f}"
1306            )
1307
1308        # Apply final scaling factors to shale/sand properties
1309        props.rho[(ng <= 1.0e-2) & (lith < 2.0)] = (
1310            props.rho[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1311        )
1312        props.vp[(ng <= 1.0e-2) & (lith < 2.0)] = (
1313            props.vp[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1314        )
1315        props.vs[(ng <= 1.0e-2) & (lith < 2.0)] = (
1316            props.vs[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1317        )
1318        props.rho[(ng > 1.0e-2) & (lith < 2.0)] = (
1319            props.rho[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1320        )
1321        props.vp[(ng > 1.0e-2) & (lith < 2.0)] = (
1322            props.vp[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1323        )
1324        props.vs[(ng > 1.0e-2) & (lith < 2.0)] = (
1325            props.vs[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1326        )
1327        # Apply same factors to the fluid-factor property cubes
1328        props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1329            props.rho_ff[(ng <= 1.0e-2) & (lith < 2.0)] * rho_factor_shale
1330        )
1331        props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1332            props.vp_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vp_factor_shale
1333        )
1334        props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] = (
1335            props.vs_ff[(ng <= 1.0e-2) & (lith < 2.0)] * vs_factor_shale
1336        )
1337        props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1338            props.rho_ff[(ng > 1.0e-2) & (lith < 2.0)] * rho_factor_sand
1339        )
1340        props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1341            props.vp_ff[(ng > 1.0e-2) & (lith < 2.0)] * vp_factor_sand
1342        )
1343        props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] = (
1344            props.vs_ff[(ng > 1.0e-2) & (lith < 2.0)] * vs_factor_sand
1345        )
1346        return props

Apply final random scaling factors to sands and shales

def write_property_volumes_to_disk(self):
1348    def write_property_volumes_to_disk(self):
1349        """Write Rho, Vp, Vs volumes to disk."""
1350        self.write_cube_to_disk(
1351            self.rho[:],
1352            "qc_volume_rho",
1353        )
1354        self.write_cube_to_disk(
1355            self.vp[:],
1356            "qc_volume_vp",
1357        )
1358        self.write_cube_to_disk(
1359            self.vs[:],
1360            "qc_volume_vs",
1361        )

Write Rho, Vp, Vs volumes to disk.

class ElasticProperties3D:
1364class ElasticProperties3D:
1365    """Empty class to hold 3D property cubes Rho, Vp, Vs, Rho_ff, Vp_ff, Vs_ff and sum_net"""
1366
1367    def __init__(self):
1368        self.rho = None
1369        self.vp = None
1370        self.vs = None
1371        self.rho_ff = None
1372        self.vp_ff = None
1373        self.vs_ff = None
1374        self.sum_net = None
1375        self.rho_not_random = None
1376        self.vp_not_random = None
1377        self.vs_not_random = None

Empty class to hold 3D property cubes Rho, Vp, Vs, Rho_ff, Vp_ff, Vs_ff and sum_net

ElasticProperties3D()
1367    def __init__(self):
1368        self.rho = None
1369        self.vp = None
1370        self.vs = None
1371        self.rho_ff = None
1372        self.vp_ff = None
1373        self.vs_ff = None
1374        self.sum_net = None
1375        self.rho_not_random = None
1376        self.vp_not_random = None
1377        self.vs_not_random = None
class RFC:
1380class RFC:
1381    """
1382    Reflection Coefficient object
1383    Contains methods for calculating the reflection coefficient at an interface
1384    from input Vp, Vs, Rho and incident angle
1385    Angle should be given in degrees and is converted to radians internally
1386    """
1387
1388    def __init__(
1389        self, vp_upper, vs_upper, rho_upper, vp_lower, vs_lower, rho_lower, theta
1390    ):
1391        self.theta = theta
1392        self.vp_upper = vp_upper
1393        self.vs_upper = vs_upper
1394        self.rho_upper = rho_upper
1395        self.vp_lower = vp_lower
1396        self.vs_lower = vs_lower
1397        self.rho_lower = rho_lower
1398
1399    def normal_incidence_rfc(self):
1400        z0 = self.rho_upper * self.vp_upper
1401        z1 = self.rho_lower * self.vp_lower
1402        return (z1 - z0) / (z1 + z0)
1403
1404    def shuey(self):
1405        """Use 3-term Shuey to calculate RFC."""
1406        radians = self._degrees_to_radians()  # Angle to radians
1407        sin_squared = np.sin(radians) ** 2
1408        tan_squared = np.tan(radians) ** 2
1409
1410        # Delta Vp, Vs, Rho
1411        d_vp = self.vp_lower - self.vp_upper
1412        d_vs = self.vs_lower - self.vs_upper
1413        d_rho = self.rho_lower - self.rho_upper
1414        avg_vp = np.mean([self.vp_lower, self.vp_upper])
1415        avg_vs = np.mean([self.vs_lower, self.vs_upper])
1416        avg_rho = np.mean([self.rho_lower, self.rho_upper])
1417
1418        # 3 Term Shuey
1419        r0 = 0.5 * (d_vp / avg_vp + d_rho / avg_rho)
1420        g = 0.5 * (d_vp / avg_vp) - 2.0 * avg_vs ** 2 / avg_vp ** 2 * (
1421            d_rho / avg_rho + 2.0 * d_vs / avg_vs
1422        )
1423        f = 0.5 * (d_vp / avg_vp)
1424
1425        return r0 + g * sin_squared + f * (tan_squared - sin_squared)
1426
1427    def zoeppritz_reflectivity(self):
1428        """Calculate PP reflectivity using Zoeppritz equation"""
1429        theta = self._degrees_to_radians(
1430            dtype="complex"
1431        )  # Convert angle to radians, as complex value
1432        p = np.sin(theta) / self.vp_upper  # ray param
1433        theta2 = np.arcsin(p * self.vp_lower)  # Transmission angle of P-wave
1434        phi1 = np.arcsin(p * self.vs_upper)  # Reflection angle of converted S-wave
1435        phi2 = np.arcsin(p * self.vs_lower)  # Transmission angle of converted S-wave
1436
1437        a = self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0) - self.rho_upper * (
1438            1 - 2 * np.sin(phi1) ** 2.0
1439        )
1440        b = (
1441            self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0)
1442            + 2 * self.rho_upper * np.sin(phi1) ** 2.0
1443        )
1444        c = (
1445            self.rho_upper * (1 - 2 * np.sin(phi1) ** 2.0)
1446            + 2 * self.rho_lower * np.sin(phi2) ** 2.0
1447        )
1448        d = 2 * (
1449            self.rho_lower * self.vs_lower ** 2 - self.rho_upper * self.vs_upper ** 2
1450        )
1451
1452        e = (b * np.cos(theta) / self.vp_upper) + (c * np.cos(theta2) / self.vp_lower)
1453        f = (b * np.cos(phi1) / self.vs_upper) + (c * np.cos(phi2) / self.vs_lower)
1454        g = a - d * np.cos(theta) / self.vp_upper * np.cos(phi2) / self.vs_lower
1455        h = a - d * np.cos(theta2) / self.vp_lower * np.cos(phi1) / self.vs_upper
1456
1457        d = e * f + g * h * p ** 2
1458
1459        zoep_pp = (
1460            f
1461            * (
1462                b * (np.cos(theta) / self.vp_upper)
1463                - c * (np.cos(theta2) / self.vp_lower)
1464            )
1465            - h
1466            * p ** 2
1467            * (a + d * (np.cos(theta) / self.vp_upper) * (np.cos(phi2) / self.vs_lower))
1468        ) * (1 / d)
1469
1470        return np.squeeze(zoep_pp)
1471
1472    def _degrees_to_radians(self, dtype="float"):
1473        """Convert angle in degrees to ange in radians. If dtype != 'float', return a complex dtype array"""
1474        if dtype == "float":
1475            return np.radians(self.theta)
1476        else:
1477            return np.radians(self.theta).astype(np.complex)

Reflection Coefficient object Contains methods for calculating the reflection coefficient at an interface from input Vp, Vs, Rho and incident angle Angle should be given in degrees and is converted to radians internally

RFC(vp_upper, vs_upper, rho_upper, vp_lower, vs_lower, rho_lower, theta)
1388    def __init__(
1389        self, vp_upper, vs_upper, rho_upper, vp_lower, vs_lower, rho_lower, theta
1390    ):
1391        self.theta = theta
1392        self.vp_upper = vp_upper
1393        self.vs_upper = vs_upper
1394        self.rho_upper = rho_upper
1395        self.vp_lower = vp_lower
1396        self.vs_lower = vs_lower
1397        self.rho_lower = rho_lower
def normal_incidence_rfc(self):
1399    def normal_incidence_rfc(self):
1400        z0 = self.rho_upper * self.vp_upper
1401        z1 = self.rho_lower * self.vp_lower
1402        return (z1 - z0) / (z1 + z0)
def shuey(self):
1404    def shuey(self):
1405        """Use 3-term Shuey to calculate RFC."""
1406        radians = self._degrees_to_radians()  # Angle to radians
1407        sin_squared = np.sin(radians) ** 2
1408        tan_squared = np.tan(radians) ** 2
1409
1410        # Delta Vp, Vs, Rho
1411        d_vp = self.vp_lower - self.vp_upper
1412        d_vs = self.vs_lower - self.vs_upper
1413        d_rho = self.rho_lower - self.rho_upper
1414        avg_vp = np.mean([self.vp_lower, self.vp_upper])
1415        avg_vs = np.mean([self.vs_lower, self.vs_upper])
1416        avg_rho = np.mean([self.rho_lower, self.rho_upper])
1417
1418        # 3 Term Shuey
1419        r0 = 0.5 * (d_vp / avg_vp + d_rho / avg_rho)
1420        g = 0.5 * (d_vp / avg_vp) - 2.0 * avg_vs ** 2 / avg_vp ** 2 * (
1421            d_rho / avg_rho + 2.0 * d_vs / avg_vs
1422        )
1423        f = 0.5 * (d_vp / avg_vp)
1424
1425        return r0 + g * sin_squared + f * (tan_squared - sin_squared)

Use 3-term Shuey to calculate RFC.

def zoeppritz_reflectivity(self):
1427    def zoeppritz_reflectivity(self):
1428        """Calculate PP reflectivity using Zoeppritz equation"""
1429        theta = self._degrees_to_radians(
1430            dtype="complex"
1431        )  # Convert angle to radians, as complex value
1432        p = np.sin(theta) / self.vp_upper  # ray param
1433        theta2 = np.arcsin(p * self.vp_lower)  # Transmission angle of P-wave
1434        phi1 = np.arcsin(p * self.vs_upper)  # Reflection angle of converted S-wave
1435        phi2 = np.arcsin(p * self.vs_lower)  # Transmission angle of converted S-wave
1436
1437        a = self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0) - self.rho_upper * (
1438            1 - 2 * np.sin(phi1) ** 2.0
1439        )
1440        b = (
1441            self.rho_lower * (1 - 2 * np.sin(phi2) ** 2.0)
1442            + 2 * self.rho_upper * np.sin(phi1) ** 2.0
1443        )
1444        c = (
1445            self.rho_upper * (1 - 2 * np.sin(phi1) ** 2.0)
1446            + 2 * self.rho_lower * np.sin(phi2) ** 2.0
1447        )
1448        d = 2 * (
1449            self.rho_lower * self.vs_lower ** 2 - self.rho_upper * self.vs_upper ** 2
1450        )
1451
1452        e = (b * np.cos(theta) / self.vp_upper) + (c * np.cos(theta2) / self.vp_lower)
1453        f = (b * np.cos(phi1) / self.vs_upper) + (c * np.cos(phi2) / self.vs_lower)
1454        g = a - d * np.cos(theta) / self.vp_upper * np.cos(phi2) / self.vs_lower
1455        h = a - d * np.cos(theta2) / self.vp_lower * np.cos(phi1) / self.vs_upper
1456
1457        d = e * f + g * h * p ** 2
1458
1459        zoep_pp = (
1460            f
1461            * (
1462                b * (np.cos(theta) / self.vp_upper)
1463                - c * (np.cos(theta2) / self.vp_lower)
1464            )
1465            - h
1466            * p ** 2
1467            * (a + d * (np.cos(theta) / self.vp_upper) * (np.cos(phi2) / self.vs_lower))
1468        ) * (1 / d)
1469
1470        return np.squeeze(zoep_pp)

Calculate PP reflectivity using Zoeppritz equation

def derive_butterworth_bandpass(lowcut, highcut, digitisation, order=4):
1480def derive_butterworth_bandpass(lowcut, highcut, digitisation, order=4):
1481    from scipy.signal import butter
1482
1483    fs = 1.0 / (digitisation / 1000.0)
1484    nyq = 0.5 * fs
1485    low = lowcut / nyq
1486    high = highcut / nyq
1487    b, a = butter(order, [low, high], btype="bandpass", output="ba")
1488    return b, a
def apply_butterworth_bandpass(data, b, a):
1491def apply_butterworth_bandpass(data, b, a):
1492    """Apply Butterworth bandpass to data"""
1493    from scipy.signal import tf2zpk, filtfilt
1494
1495    # Use irlen to remove artefacts generated at base of cubes during bandlimitation
1496    _, p, _ = tf2zpk(b, a)
1497    eps = 1e-9
1498    r = np.max(np.abs(p))
1499    approx_impulse_len = int(np.ceil(np.log(eps) / np.log(r)))
1500    y = filtfilt(b, a, data, method="gust", irlen=approx_impulse_len)
1501    # y = filtfilt(b, a, data)
1502    return y

Apply Butterworth bandpass to data

def trim_triangular(low, mid, high):
1505def trim_triangular(low, mid, high):
1506    ###
1507    ### use trimmed triangular distributions
1508    ###
1509    from numpy.random import triangular
1510
1511    for _ in range(50):
1512        num = triangular(2 * low - mid, mid, 2 * high - mid)
1513        if low <= num <= high:
1514            break
1515    return num
def apply_wavelet(cube, wavelet):
1518def apply_wavelet(cube, wavelet):
1519    filtered_cube = np.zeros_like(cube)
1520    for i in range(cube.shape[0]):
1521        for j in range(cube.shape[1]):
1522            filtered_cube[i, j, :] = np.convolve(cube[i, j, :], wavelet, mode="same")
1523    return filtered_cube