datagenerator.util

   1import os
   2import numpy as np
   3
   4
   5def find_line_with_most_voxels(volume, voxel_thresholds, cfg):
   6    """
   7    Find the line with the most voxels
   8    ----------------------------------
   9
  10    Finds the line wtih the most voxels.
  11
  12    Loop over a volume and locate the inline which has the highest number of
  13    voxels which meet the threshold criteria
  14
  15    If multiple thresholds are used, first is used as a lower bound,
  16    and second is used as an upper bound
  17
  18    Parameters
  19    ----------
  20    volume : np.ndarray
  21        3D volume (ndarray)
  22    voxel_thresholds: list
  23        Used for thresholding
  24    cfg : dict
  25        model parameters
  26
  27    Returns
  28    -------
  29    inline_index : int
  30        Inline containing highest number of voxels meeting criteria.
  31    """
  32    max_voxels = 0
  33    inline_index = int(cfg.cube_shape[0] / 2)
  34    for ii in range(volume.shape[0]):
  35        voxels = volume[ii, ...].copy()
  36        try:  # if two values provided...
  37            voxels = np.where(
  38                ((voxels >= voxel_thresholds[0] & voxels < voxel_thresholds[1])),
  39                1.0,
  40                0.0,
  41            )
  42        except TypeError:  # Just use 1 threshold
  43            voxels = np.where(voxels > voxel_thresholds, 1.0, 0.0)
  44        if voxels[voxels == 1.0].size > max_voxels:
  45            max_voxels = voxels[voxels == 1.0].size
  46            inline_index = int(ii)
  47
  48    return inline_index
  49
  50
  51def plot_voxels_not_in_regular_layers(
  52    volume: np.ndarray,
  53    threshold: float,
  54    title: str,
  55    png_name: str,
  56    cfg
  57) -> None:
  58    """
  59        Plot voxels not in regular layers
  60
  61        Analyze voxel values not in regular layers by plotting a
  62        histogram of voxels above a given threshold.
  63
  64        Parameters
  65        ----------
  66        volume : np.ndarray
  67            3D volume (ndarray)
  68        threshold : float
  69            Threshold for voxel values.
  70        title : str
  71            Title for plot.
  72        png_name : str
  73            Name of png file to save.
  74        cfg : dict
  75            Model configurations.
  76
  77        Returns
  78        -------
  79        None
  80    """
  81    plt = import_matplotlib()
  82    voxels = volume[volume > threshold]
  83    plt.figure(1, figsize=(20, 15))
  84    plt.clf()
  85    plt.title(title)
  86    plt.hist(voxels, 81)
  87    plt.savefig(os.path.join(cfg.work_subfolder, png_name), format="png")
  88    plt.close(1)
  89
  90
  91def plot_xsection(volume, maps, line_num, title, png_name, cfg, cmap="prism") -> None:
  92    """
  93    Plot cross section.
  94
  95    Parameters
  96    ----------
  97    volume : np.ndarray
  98        The 3D volume as a numpy array
  99    maps : _type_
 100        _description_
 101    line_num : _type_
 102        _description_
 103    title : _type_
 104        _description_
 105    png_name : _type_
 106        _description_
 107    cfg : _type_
 108        _description_
 109    cmap : str, optional
 110        _description_, by default "prism"
 111    
 112    Returns
 113    -------
 114    None
 115    """
 116    plt = import_matplotlib()
 117    plt.clf()
 118    plt.title(f"{title}\nInline: {line_num}")
 119    plt.figure(1, figsize=(20, 15))
 120    # If depth maps is infilled and volume is not, update the Z axis of the volume
 121    # Pick the 75%'th horizon to check
 122    if (
 123        np.max(maps[:, :, -int(maps.shape[-1] / 4)]) > volume.shape[-1]
 124    ):  # don't use last horizon
 125        plt.imshow(
 126            np.fliplr(
 127                np.rot90(
 128                    volume[line_num, :, : (volume.shape[-1] * cfg.infill_factor) - 1], 3
 129                )
 130            ),
 131            aspect="auto",
 132            cmap=cmap,
 133        )
 134    else:
 135        plt.imshow(
 136            np.fliplr(np.rot90(volume[line_num, ...], 3)), aspect="auto", cmap=cmap
 137        )
 138    plt.colorbar()
 139    plt.ylim((volume.shape[-1], 0))
 140    for i in range(0, maps.shape[-1], 1):
 141        plt.plot(range(cfg.cube_shape[0]), maps[line_num, :, i], "k-", lw=0.3)
 142    plt.savefig(os.path.join(cfg.work_subfolder, png_name), format="png")
 143    plt.close(1)
 144
 145
 146def plot_3D_faults_plot(cfg, faults, plot_faults=True, plot_throws=True) -> None:
 147    """
 148    Plot 3D faults plot
 149    -------------------
 150
 151    Plots the faults in a 3D plot.
 152
 153    Parameters
 154    ----------
 155    cfg : _type_
 156        The configuration.
 157    faults : np.ndarray
 158        The faults a numpy object.
 159    plot_faults : bool, optional
 160        Whether to plot the faults or not, by default True
 161    plot_throws : bool, optional
 162        Whether to plot the fault throws or not, by default True
 163    
 164    Returns
 165    -------
 166    None
 167    """
 168    from plotly.offline import plot
 169    import plotly.graph_objects as go
 170
 171    fi1 = faults.fault_planes
 172    decimation_factor = 2
 173    x1, y1, z1 = np.where(
 174        fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
 175    )
 176    x1 *= decimation_factor
 177    y1 *= decimation_factor
 178    z1 *= -decimation_factor
 179    trace1 = go.Scatter3d(
 180        x=x1,
 181        y=y1,
 182        z=z1,
 183        name="fault_segments",
 184        mode="markers",
 185        marker=dict(size=2.0, color="blue", opacity=0.025),
 186    )
 187
 188    if plot_faults:
 189        fi2 = faults.fault_intersections
 190        x2, y2, z2 = np.where(
 191            fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.25
 192        )
 193        x2 *= decimation_factor
 194        y2 *= decimation_factor
 195        z2 *= -decimation_factor
 196        trace2 = go.Scatter3d(
 197            x=x2,
 198            y=y2,
 199            z=z2,
 200            name="fault_intersection_segments",
 201            mode="markers",
 202            marker=dict(
 203                size=1.5,
 204                color="red",  # set color to an array/list of desired values
 205                colorscale="Viridis",  # choose a colorscale
 206                opacity=0.025,
 207            ),
 208        )
 209
 210        data = [trace1, trace2]
 211        layout = go.Layout(
 212            title=go.layout.Title(
 213                text="Fault Segments & Intersections"
 214                + "<br>"
 215                + '<span style="font-size: 12px;">'
 216                + cfg.work_subfolder
 217                + "</span>",
 218                xref="paper",
 219                x=0,
 220            )
 221        )
 222        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
 223        fig3 = go.Figure(data=data, layout=layout)
 224        fig3.update_layout(
 225            scene_camera=camera,
 226            autosize=False,
 227            width=960,
 228            height=720,
 229            scene=dict(
 230                aspectmode="manual",
 231                aspectratio=dict(x=1, y=1, z=1),
 232                xaxis=dict(
 233                    nticks=4,
 234                    range=[0, 300],
 235                ),
 236                yaxis=dict(
 237                    nticks=4,
 238                    range=[0, 300],
 239                ),
 240                zaxis=dict(
 241                    nticks=4,
 242                    range=[-1260, 0],
 243                ),
 244            ),
 245            margin=dict(r=20, l=10, b=10, t=25),
 246        )
 247        output_filename = os.path.join(
 248            cfg.work_subfolder,
 249            f"qcplot_fault_segments_and_intersections_3D__{cfg.date_stamp}",
 250        )
 251        plot(fig3, filename=f"{output_filename}.html", auto_open=False)
 252        try:
 253            fig3.write_image(f"{output_filename}.png")
 254        except:
 255            print("png file saving failed")
 256        print(
 257            "\n   ... util/plot_3D_faults_plot finished creating 3D plot at:\n       "
 258            + output_filename
 259        )
 260
 261    if plot_throws:
 262        fi3 = faults.fault_plane_throw
 263        z3 = fi3[x1, y1, -z1]
 264        trace3 = go.Scatter3d(
 265            x=x1,
 266            y=y1,
 267            z=z1,
 268            name="fault_intersection_segments",
 269            mode="markers",
 270            marker=dict(
 271                size=4.0,
 272                color=z3,  # set color to an array/list of desired values
 273                colorscale="Viridis",  # choose a colorscale
 274                opacity=0.05,
 275            ),
 276        )
 277        data = [trace3]
 278        layout = go.Layout(
 279            title=go.layout.Title(
 280                text="Fault Throw along fault plane"
 281                + "<br>"
 282                + '<span style="font-size: 12px;">'
 283                + cfg.work_subfolder
 284                + "</span>",
 285                xref="paper",
 286                x=0,
 287            )
 288        )
 289        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
 290        fig4 = go.Figure(data=data, layout=layout)
 291        fig4.update_layout(
 292            scene_camera=camera,
 293            autosize=False,
 294            width=960,
 295            height=720,
 296            scene=dict(
 297                aspectmode="manual",
 298                aspectratio=dict(x=1, y=1, z=1),
 299                xaxis=dict(
 300                    nticks=4,
 301                    range=[0, 300],
 302                ),
 303                yaxis=dict(
 304                    nticks=4,
 305                    range=[0, 300],
 306                ),
 307                zaxis=dict(
 308                    nticks=4,
 309                    range=[-1260, 0],
 310                ),
 311            ),
 312            margin=dict(r=20, l=10, b=10, t=25),
 313        )
 314        output_filename = os.path.join(
 315            cfg.work_subfolder, f"qcplot_fault_segments_throw_3D__{cfg.date_stamp}"
 316        )
 317        plot(fig4, filename=f"{output_filename}.html", auto_open=False)
 318        try:
 319            fig4.write_image(f"{output_filename}.png")
 320        except:
 321            print("png file saving failed")
 322        print(
 323            "\n   ... util/plot_3D_faults_plot finished creating 3D plot at:\n       "
 324            + output_filename
 325        )
 326
 327
 328def plot_3D_closure_plot(cfg, closures, plot_closures=True, plot_strat_closures=True) -> None:
 329    """
 330    Plot 3D closures
 331    ----------------
 332
 333    Plot the closures in 3D.
 334
 335    Parameters
 336    ----------
 337    cfg : dict
 338        The configuration.
 339    closures : np.ndarray
 340        Closures array.
 341    plot_closures : bool, optional
 342        Whether to plot the closures or not, by default True
 343    plot_strat_closures : bool, optional
 344        Whether to plot statigraphic closures or not, by default True
 345    
 346    Returns
 347    -------
 348    None
 349    """
 350    from plotly.offline import plot
 351    import plotly.graph_objects as go
 352
 353    fi1 = closures.gas_closures
 354    decimation_factor = 2
 355    x1, y1, z1 = np.where(
 356        fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
 357    )
 358    x1 *= decimation_factor
 359    y1 *= decimation_factor
 360    z1 *= -decimation_factor
 361
 362    fi2 = closures.oil_closures
 363    x2, y2, z2 = np.where(
 364        fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.25
 365    )
 366    x2 *= decimation_factor
 367    y2 *= decimation_factor
 368    z2 *= -decimation_factor
 369
 370    fi3 = closures.brine_closures
 371    x3, y3, z3 = np.where(
 372        fi3[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
 373    )
 374    x3 *= decimation_factor
 375    y3 *= decimation_factor
 376    z3 *= -decimation_factor
 377
 378    size = 2.0
 379    opacity = 0.15
 380
 381    if plot_closures:
 382        trace1 = go.Scatter3d(
 383            x=x1,
 384            y=y1,
 385            z=z1,
 386            name="gas_segments",
 387            mode="markers",
 388            marker=dict(size=size, color="red", opacity=opacity),
 389        )
 390
 391        trace2 = go.Scatter3d(
 392            x=x2,
 393            y=y2,
 394            z=z2,
 395            name="oil_segments",
 396            mode="markers",
 397            marker=dict(
 398                size=size,
 399                color="green",  # set color to an array/list of desired values
 400                colorscale="Viridis",  # choose a colorscale
 401                opacity=opacity,
 402            ),
 403        )
 404
 405        trace3 = go.Scatter3d(
 406            x=x3,
 407            y=y3,
 408            z=z3,
 409            name="brine_segments",
 410            mode="markers",
 411            marker=dict(
 412                size=size,
 413                color="blue",  # set color to an array/list of desired values
 414                colorscale="Viridis",  # choose a colorscale
 415                opacity=opacity / 5.0,
 416            ),
 417        )
 418
 419        data = [trace1, trace2, trace3]
 420        layout = go.Layout(
 421            title=go.layout.Title(
 422                text="Closure Segments (Gas, Oil, Brine)"
 423                + "<br>"
 424                + '<span style="font-size: 12px;">'
 425                + cfg.work_subfolder
 426                + "</span>",
 427                xref="paper",
 428                x=0,
 429            )
 430        )
 431        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
 432        fig4 = go.Figure(data=data, layout=layout)
 433        fig4.update_layout(
 434            scene_camera=camera,
 435            autosize=False,
 436            width=960,
 437            height=720,
 438            scene=dict(
 439                aspectmode="manual",
 440                aspectratio=dict(x=1, y=1, z=1),
 441                xaxis=dict(
 442                    nticks=4,
 443                    range=[0, 300],
 444                ),
 445                yaxis=dict(
 446                    nticks=4,
 447                    range=[0, 300],
 448                ),
 449                zaxis=dict(
 450                    nticks=4,
 451                    range=[-1260, 0],
 452                ),
 453            ),
 454            # width=650,
 455            margin=dict(r=20, l=10, b=10, t=25),
 456        )
 457        output_filename = os.path.join(
 458            cfg.work_subfolder, f"qcplot_closure_segments_3D_{cfg.date_stamp}"
 459        )
 460        plot(fig4, filename=f"{output_filename}.html", auto_open=False)
 461        try:
 462            fig4.write_image(f"{output_filename}.png")
 463        except ValueError:
 464            print("png file saving failed")
 465        print(
 466            "\n   ... util/plot_3D_closure_plot finished creating 3D plot at:\n       "
 467            + output_filename
 468        )
 469
 470    if plot_strat_closures:
 471        ###-------------------------------------------------------------------------
 472        ### Make 3D plot of strat traps (onlap/pinch-out traps)
 473        ###-------------------------------------------------------------------------
 474        fi4 = closures.strat_closures
 475        decimation_factor = 2
 476        x4, y4, z4 = np.where(
 477            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 478            & (fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 479        )
 480        x4 *= decimation_factor
 481        y4 *= decimation_factor
 482        z4 *= -decimation_factor
 483
 484        x5, y5, z5 = np.where(
 485            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 486            & (fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 487        )
 488        x5 *= decimation_factor
 489        y5 *= decimation_factor
 490        z5 *= -decimation_factor
 491
 492        x6, y6, z6 = np.where(
 493            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 494            & (fi3[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
 495        )
 496        x6 *= decimation_factor
 497        y6 *= decimation_factor
 498        z6 *= -decimation_factor
 499
 500        size = 2.0
 501        opacity = 0.15
 502
 503        trace1 = go.Scatter3d(
 504            x=x4,
 505            y=y4,
 506            z=z4,
 507            name="gas_strat",
 508            mode="markers",
 509            marker=dict(size=size, color="red", opacity=opacity),
 510        )
 511
 512        trace2 = go.Scatter3d(
 513            x=x5,
 514            y=y5,
 515            z=z5,
 516            name="oil_strat",
 517            mode="markers",
 518            marker=dict(
 519                size=size,
 520                color="green",  # set color to an array/list of desired values
 521                colorscale="Viridis",  # choose a colorscale
 522                opacity=opacity,
 523            ),
 524        )
 525
 526        trace3 = go.Scatter3d(
 527            x=x6,
 528            y=y6,
 529            z=z6,
 530            name="brine_strat",
 531            mode="markers",
 532            marker=dict(
 533                size=size,
 534                color="blue",  # set color to an array/list of desired values
 535                colorscale="Viridis",  # choose a colorscale
 536                opacity=opacity / 5.0,
 537            ),
 538        )
 539
 540        data = [trace1, trace2, trace3]
 541        layout = go.Layout(
 542            title=go.layout.Title(
 543                text="Stratigraphic Closure Segments (Gas, Oil, Brine)"
 544                + "<br>"
 545                + '<span style="font-size: 12px;">'
 546                + cfg.work_subfolder
 547                + "</span>",
 548                xref="paper",
 549                x=0,
 550            )
 551        )
 552        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
 553        fig5 = go.Figure(data=data, layout=layout)
 554        fig5.update_layout(
 555            scene_camera=camera,
 556            autosize=False,
 557            width=960,
 558            height=720,
 559            scene=dict(
 560                aspectmode="manual",
 561                aspectratio=dict(x=1, y=1, z=1),
 562                xaxis=dict(
 563                    nticks=4,
 564                    range=[0, 300],
 565                ),
 566                yaxis=dict(
 567                    nticks=4,
 568                    range=[0, 300],
 569                ),
 570                zaxis=dict(
 571                    nticks=4,
 572                    range=[-1260, 0],
 573                ),
 574            ),
 575            # width=650,
 576            margin=dict(r=20, l=10, b=10, t=25),
 577        )
 578        output_filename = os.path.join(
 579            cfg.work_subfolder, f"qcplot_strat_closure_segments_3D_{cfg.date_stamp}"
 580        )
 581        plot(fig5, filename=f"{output_filename}.html", auto_open=False)
 582        try:
 583            fig5.write_image(f"{output_filename}.png")
 584        except:
 585            print("png file saving failed")
 586        print(
 587            "\n   ... util/plot_3D_strat_closure_plot finished creating 3D plot at:\n       "
 588            + output_filename
 589        )
 590
 591
 592def plot_facies_qc(cfg, lith, seismic, facies, maps) -> None:
 593    """
 594    Plot Facies for QC
 595    ------------------
 596
 597    Plots the facies to aid in QC of the model.
 598
 599    Parameters
 600    ----------
 601    cfg : dict
 602        The configuration used in the model.
 603    lith : np.ndarray
 604        The lithology model.
 605    seismic : np.ndarray
 606        The seismic model.
 607    facies : np.ndarray
 608        The facies model.
 609    maps : dict
 610        The maps used in the model.
 611    
 612    Returns
 613    -------
 614    None
 615    """
 616    plt = import_matplotlib()
 617    from itertools import groupby
 618    import matplotlib as mpl
 619
 620    # Plot Centre Inline of facies and seismic
 621    iline = lith.shape[0] // 2
 622    avg_sand_unit_thickness = np.mean(
 623        [b for a, b in [(k, sum(1 for i in g)) for k, g in groupby(facies)] if a == 1.0]
 624    )
 625    textstr = "\n".join(
 626        (
 627            f"Sand % Input: {cfg.sand_layer_pct:.1%}",
 628            f"Sand % Actual: {facies[facies == 1.].size / facies.size:.1%}",
 629            f"Sand thickness (layers) Input: {cfg.sand_layer_thickness}",
 630            f"Sand thickness (layers) Actual: {avg_sand_unit_thickness:.1f}",
 631            f"Sand Voxel % in model: {lith[lith[:] == 1].size / lith[lith[:] >= 0].size:.1%}",
 632        )
 633    )
 634    fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(20, 15), sharey=True)
 635    fig.suptitle("Facies QC Plot")
 636    axs[0].set_ylim(cfg.cube_shape[-1])
 637    if cfg.include_salt and np.max(lith[iline, ...]) > 1:
 638        axs[0].imshow(
 639            np.fliplr(np.rot90(lith[iline, ...], 3)),
 640            aspect="auto",
 641            cmap=mpl.colors.ListedColormap(["blue", "saddlebrown", "gold", "grey"]),
 642        )
 643    else:
 644        axs[0].imshow(
 645            np.fliplr(np.rot90(lith[iline, ...], 3)),
 646            aspect="auto",
 647            cmap=mpl.colors.ListedColormap(["blue", "saddlebrown", "gold"]),
 648        )
 649    _img = axs[1].imshow(
 650        np.fliplr(np.rot90(seismic[iline, ...], 3)),
 651        aspect="auto",
 652        cmap="Greys",
 653        vmin=-300,
 654        vmax=300,
 655    )
 656    # fig.colorbar(_img, ax=axs[1])
 657    props = dict(boxstyle="round", alpha=0.5)
 658    # Add textbox with textstr to facies subplot
 659    axs[0].text(
 660        0.05,
 661        0.95,
 662        textstr,
 663        transform=axs[0].transAxes,
 664        fontsize=14,
 665        verticalalignment="top",
 666        bbox=props,
 667    )
 668    for i in range(maps.shape[-1]):
 669        axs[0].plot(range(cfg.cube_shape[0]), maps[iline, :, i], "k-", lw=0.3)
 670
 671    fig.savefig(os.path.join(cfg.work_subfolder, "QC_plot__Facies_FullStackCumulativeSum.png"))
 672    plt.close()
 673
 674
 675def infill_surface(surface: np.ndarray) -> np.ndarray:
 676    """
 677    Infill Surface
 678    --------------
 679
 680    Infill surfaces.
 681
 682    Fill holes in input 2D surface.
 683    Holes have value of either nan or zero.
 684    Fill using replacement with interpolated (nearest-neighbor) value.
 685
 686    Parameters
 687    ----------
 688    surface : np.ndarray
 689        2D numpy array
 690
 691    Returns
 692    -------
 693    surface_infilled : np.ndarray
 694        (2D) surface_infilled
 695    """
 696    from scipy.interpolate import NearestNDInterpolator
 697
 698    # get x,y,z indices for non-nan points on surface
 699    xx, yy = np.meshgrid(
 700        range(surface.shape[0]), range(surface.shape[1]), sparse=False, indexing="ij"
 701    )
 702    x_no_nan = xx[~np.isnan(surface)]
 703    y_no_nan = yy[~np.isnan(surface)]
 704    z_surface = surface[~np.isnan(surface)]
 705
 706    if z_surface.flatten().shape[0] > 2:
 707        # set up nearest-neighbor interpolator function
 708        xy = zip(x_no_nan, y_no_nan)
 709        nn = NearestNDInterpolator(xy, z_surface)
 710        # interpolate at every x,y on regular grid
 711        surface_nn = nn(xx, yy)
 712        # replace input surface with interpolated (nearest-neighbor)
 713        # - if input value is either nan or zero
 714        surface_infilled = surface.copy()
 715        surface_infilled[np.isnan(surface)] = surface_nn[np.isnan(surface)]
 716        surface_infilled[surface == 0] = surface_nn[surface == 0]
 717    else:
 718        surface_infilled = z_surface.copy()
 719
 720    if surface_infilled[np.isnan(surface_infilled)].shape[0] > 0:
 721        count = surface_infilled[np.isnan(surface_infilled)].shape[0]
 722        print(
 723            f"\n\n\n   ...inside infill_surface: there are some NaN values in the surface. count = {count}"
 724        )
 725
 726    return surface_infilled
 727
 728
 729def mute_above_seafloor(surface, xyz):
 730    """
 731    Mute data above seafloor
 732    ------------------------
 733
 734    Mute a cube above a surface that contains
 735    indices for the 3rd axis of the cube.
 736
 737    Parameters
 738    ----------
 739    surface : np.ndarray
 740        Mute above this (2D) surface
 741    xyz : np.ndarray
 742        Cube to which muting is applied.
 743
 744    Returns
 745    -------
 746    xyz_muted : np.ndarray
 747        Muted (3D array)
 748    """
 749
 750    krange = np.arange(xyz.shape[2])
 751
 752    # broadcast indices of vertical axis to 3D
 753    krange = krange.reshape((1, 1, len(krange)))
 754
 755    if np.isnan(np.min(surface)) or 0 in surface and not np.all(surface == 0):
 756        # If surface contains nan's or at least one 0, this will return True, therefore...
 757        # fill empty picks with nearest-neighbor infilling
 758        # If all 0's, skip it
 759        surface_infilled = infill_surface(surface)
 760    else:
 761        surface_infilled = surface
 762
 763    # broadcast surface to 3D
 764    surface_3d = surface_infilled.reshape((surface.shape[0], surface.shape[1], 1))
 765
 766    # apply muting to copy of xyz
 767    xyz_muted = xyz.copy()
 768    xyz_muted[krange < surface_3d] *= 0.0
 769
 770    return xyz_muted
 771
 772
 773def is_it_in_hull(hull, p) -> np.ndarray[bool]:
 774    """
 775    Is it in hull?
 776    --------------
 777    Checks if point is in hull
 778
 779    Test if points in `p` are in `hull`
 780
 781    `p` should be a `NxK` coordinates of `N` points in `K` dimensions
 782    `hull` is either a scipy.spatial.Delaunay object or the `MxK` array of the
 783    coordinates of `M` points in `K`dimensions for which Delaunay triangulation
 784    will be computed
 785
 786    Parameters
 787    ----------
 788    hull : np.ndarray
 789        The hull object.
 790    p : np.ndarray
 791        Point(s) to check.
 792
 793    Returns
 794    -------
 795    np.ndarray[bool]
 796        An array containing the booleans where the object is hull.
 797
 798
 799    """
 800    from scipy.spatial import Delaunay
 801
 802    if not isinstance(hull, Delaunay):
 803        hull = Delaunay(hull)
 804
 805    return hull.find_simplex(p) >= 0
 806
 807
 808# Utility Functions
 809def even_odd(number):
 810    if number % 2 == 0:
 811        return "Even"
 812    else:
 813        return "Odd"
 814
 815
 816def next_odd(number):
 817    if even_odd(number) == "Even":
 818        odd_number = number + 1
 819    else:
 820        odd_number = number
 821    return odd_number
 822
 823
 824def find_index_next_bigger(mylist, mynumber):
 825    # return the index of the next number in mylist that is bigger (or equal to) mynumber
 826    # example:   mynumber=46;  mylist=[1,5,13,25,36,48,55,67,73,85,90]
 827    # example:  find_index_next_bigger(mylist,mynumber) returns 5
 828    return next((mylist.index(n) for n in mylist if n >= mynumber), len(mylist))
 829
 830
 831def linearinfill(x, y, newx):
 832    # ***************************************************************************************
 833    #
 834    #   Function to return data spaced at locations specified by input variable 'newx' after
 835    #   fitting linear function. Input data specified by 'x' and 'y' so data
 836    #   can be irregularly sampled.
 837    #
 838    # ***************************************************************************************
 839    s = np.interp(newx, x, y)
 840    return s
 841
 842
 843def check_resident_memory():
 844    import os
 845    import sys
 846
 847    if "linux" in sys.platform:
 848        _proc_status = "/proc/%d/status" % os.getpid()
 849        _scale = {
 850            "kB": 1024.0,
 851            "mB": 1024.0 * 1024.0,
 852            "KB": 1024.0,
 853            "MB": 1024.0 * 1024.0,
 854        }
 855
 856        t = open(_proc_status)
 857        v = t.read()
 858        t.close()
 859
 860        i = v.index("VmRSS:")
 861        v = v[i:].split(None, 3)
 862
 863        return (float(v[1]) * _scale[v[2]]) / 1024 ** 3
 864    else:
 865        return 0.0
 866
 867
 868def import_matplotlib():
 869    """
 870    Function to perform matplotlib imports with non-interactive backend when running in background
 871    DPG 17/2/19
 872    """
 873    import sys
 874
 875    if bool(getattr(sys, "ps1", sys.flags.interactive)):
 876        # this is an interactive session
 877        from matplotlib import pyplot as plt
 878
 879        plt.ion()
 880    else:
 881        # import matplotlib with non-interactive backend ('Agg')
 882        import matplotlib
 883
 884        matplotlib.use("Agg")
 885        from matplotlib import pyplot as plt
 886
 887        plt.ioff()
 888    return plt
 889
 890
 891def write_data_to_hdf(n, d, h5file):
 892    import h5py
 893
 894    with h5py.File(h5file, "a") as hf:
 895        hf.create_dataset(name=n, data=d, compression="lzf")
 896
 897
 898# Useful functions, not directly used for data generation
 899def qc_folders(directory):
 900    """Count how many model_parameter.txt files contain elapse time
 901
 902    Use this to remove models that failed to complete for whatever reason
 903    """
 904    import glob
 905    import os
 906
 907    valid_models = list()
 908    model_closures = dict()
 909    # Grep for elapse in model_parameters.txt files in geocrawler subdirectories
 910    par = glob.glob(os.path.join(directory, "geoc*/model_parameters*.txt"))
 911    # Elapse time
 912    for p in par:
 913        with open(p, "r") as f:
 914            for line in f.readlines():
 915                if "elapse" in line:
 916                    valid_models.append(os.path.dirname(p))
 917                if "Number of Closures" in line:
 918                    num_closures = line.split()[-1]
 919                    model_closures[os.path.dirname(p)] = int(num_closures)
 920
 921    # Invalid folders
 922    invalid_models = list(
 923        set(glob.glob("{}/geoc*".format(directory))) - set(valid_models)
 924    )
 925    # Remove tar.gz files from invalid_models
 926    invalid_final = [x for x in invalid_models if not x.endswith("tar.gz")]
 927
 928    # Also sort closures by number of closures
 929    sorted_closures = [
 930        (k, model_closures[k])
 931        for k in sorted(model_closures, key=model_closures.get, reverse=True)
 932    ]
 933
 934    return invalid_final, valid_models, sorted_closures
 935
 936
 937def hanflat(inarray, pctflat):
 938
 939    # ***************************************************************************************
 940    #
 941    #   Function applies a Hanning taper to ends of "inarray".
 942    #   Center "pctflat" of samples remain unchanged.
 943    #
 944    #   Parameters:
 945    #   array :       array of values to have ends tapered
 946    #   pctflat :     this percent of  samples to remain unchanged (e.g. 0.80)
 947    #
 948    #   Returns weights (not weights applied to input array)
 949    #
 950    # ***************************************************************************************
 951
 952    numsamples = len(inarray)
 953    lowflatindex = int(round(numsamples * (1.0 - pctflat) / 2.0))
 954    hiflatindex = numsamples - lowflatindex
 955
 956    # get hanning for numsamples*(1.0-pctflat)
 957    hanwgt = np.hanning(len(inarray) - (hiflatindex - lowflatindex))
 958
 959    # piece together hanning weights at ends and weights=1.0 in flat portion
 960    outarray = np.ones(len(inarray), dtype=float)
 961    outarray[:lowflatindex] = hanwgt[:lowflatindex]
 962    outarray[hiflatindex:] = hanwgt[numsamples - hiflatindex :]
 963
 964    return outarray
 965
 966
 967def hanflat2d(inarray2d, pctflat):
 968    hanwgt1 = hanflat(np.ones((inarray2d.shape[0])), pctflat)
 969    hanwgt2 = hanflat(np.ones((inarray2d.shape[1])), pctflat)
 970    return np.sqrt(np.outer(hanwgt1, hanwgt2))
 971
 972
 973def hanflat3d(inarray3d, pctflat):
 974    hanwgt1 = hanflat(np.ones((inarray3d.shape[0])), pctflat)
 975    hanwgt2 = hanflat(np.ones((inarray3d.shape[1])), pctflat)
 976    hanwgt3 = hanflat(np.ones((inarray3d.shape[2])), pctflat)
 977    _a = np.outer(hanwgt1, hanwgt2)
 978    _b = np.multiply.outer(_a.ravel(), hanwgt3.ravel())
 979    return _b.reshape(inarray3d.shape) ** (1.0 / 3.0)
 980
 981
 982def create_3D_qc_plot(
 983    cfg,
 984    seismic,
 985    brine,
 986    oil,
 987    gas,
 988    fault_segments,
 989    title_text,
 990    camera_steps=20
 991) -> None:
 992    """
 993    Creates a 3D QC plot
 994    --------------------
 995
 996    Generates a 3D QC plot for aiding in QC.
 997
 998    Parameters
 999    ----------
1000    cfg : _type_
1001        _description_
1002    seismic : _type_
1003        _description_
1004    brine : _type_
1005        _description_
1006    oil : _type_
1007        _description_
1008    gas : _type_
1009        _description_
1010    fault_segments : _type_
1011        _description_
1012    title_text : _type_
1013        _description_
1014    camera_steps : int, optional
1015        _description_, by default 20
1016
1017    Returns
1018    -------
1019    None
1020    """
1021
1022    from scipy.ndimage import gaussian_filter1d, gaussian_filter
1023    from vtkplotter import addons, show, Text2D, Volume, Plotter
1024    from vtkplotter.pyplot import cornerHistogram
1025    from vtkplotter.vtkio import screenshot
1026
1027    # custom lighting for meshes
1028    ambient = 0.4
1029    diffuse = 0.1
1030    specular = 0.5
1031    specular_power = 30
1032
1033    # Brine
1034    # rescale the vertical axis to have similar height to x & y axes width
1035    v_exagg = 1.0 / (cfg.cube_shape[-1] // cfg.cube_shape[0])
1036    vol_br = Volume(brine[:, :, ::-1], spacing=(1, 1, v_exagg))
1037    vol_br = Volume(vol_br.GetMapper().GetInput())
1038    print(
1039        f" ... brine_closure amp range = {brine.min()}, {brine.mean()}, {brine.max()}"
1040    )
1041    brine_surface = (
1042        vol_br.isosurface(threshold=0.99 * brine.max())
1043        .c("blue")
1044        .lighting(
1045            ambient=ambient,
1046            diffuse=diffuse,
1047            specular=specular + 0.2,
1048            specularPower=specular_power,
1049        )
1050    )
1051    # Oil
1052    vol_oil = Volume(oil[:, :, ::-1], spacing=(1, 1, v_exagg))
1053    vol_oil = Volume(vol_oil.GetMapper().GetInput())
1054    print(f" ... oil_closure amp range = {oil.min()}, {oil.mean()}, {oil.max()}")
1055    oil_surface = (
1056        vol_oil.isosurface(threshold=0.99 * brine.max())
1057        .c("green")
1058        .lighting(
1059            ambient=ambient,
1060            diffuse=diffuse,
1061            specular=specular,
1062            specularPower=specular_power,
1063        )
1064    )
1065    # Gas
1066    vol_gas = Volume(gas[:, :, ::-1], spacing=(1, 1, v_exagg))
1067    vol_gas = Volume(vol_gas.GetMapper().GetInput())
1068    print(f" ... gas amp range = {gas.min()}, {gas.mean()}, {gas.max()}")
1069    gas_surface = (
1070        vol_gas.isosurface(threshold=0.99 * brine.max())
1071        .c("red")
1072        .lighting(
1073            ambient=ambient,
1074            diffuse=diffuse,
1075            specular=specular,
1076            specularPower=specular_power,
1077        )
1078    )
1079    # Faults
1080    try:
1081        sigma = 0.35
1082        threshold = 0.0759
1083        fault_segments = gaussian_filter(fault_segments, sigma=(sigma, sigma, 1))
1084        fault_segments = gaussian_filter1d(fault_segments, sigma=sigma, axis=2)
1085        vol_fault = Volume(fault_segments[:, :, ::-1], spacing=(1, 1, v_exagg))
1086        vol_fault = Volume(vol_fault.GetMapper().GetInput())
1087        fault_surface = (
1088            vol_fault.isosurface(threshold=threshold)
1089            .c((0.5, 0.5, 0.5))
1090            .alpha(0.05)
1091            .lighting(
1092                ambient=ambient,
1093                diffuse=diffuse,
1094                specular=specular * 1.5,
1095                specularPower=specular_power * 2,
1096            )
1097        )
1098        include_faults = True
1099    except:
1100        include_faults = False
1101
1102    # Seismic
1103    # re-order z so that big indices are deep
1104    vol = Volume(seismic[:, :, ::-1], spacing=(1, 1, v_exagg))
1105    vol.addScalarBar3D()
1106    vol = Volume(vol.GetMapper().GetInput())
1107
1108    # Create the 3D scene and save images
1109    la, ld = 0.9, 0.9  # ambient, diffuse
1110    cmaps = ["bone_r", "gist_ncar_r", "jet", "Spectral_r", "hot_r", "gist_earth_r"]
1111
1112    box = vol.box().wireframe().alpha(0)  # make an invisible box
1113
1114    vp = Plotter(
1115        axes=1,
1116        bg="white",
1117        bg2="lightblue",
1118        size=(900, 600),
1119        title=title_text,
1120        interactive=False,
1121        offscreen=True,
1122    )
1123    vp.show(box, newPlotter=True, interactive=False)
1124    # vp.showInset(vol, c=cmaps[0], pos=(1, 1), size=0.3, draggable=False)
1125
1126    # inits
1127    visibles = [None, None, None]
1128    cmap = cmaps[0]
1129    dims = vol.dimensions()
1130    i_init = 0
1131    msh = vol.xSlice(i_init).pointColors(cmap=cmap).lighting("", la, ld, 0)
1132    msh.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1133    vp.renderer.AddActor(msh)
1134    visibles[0] = msh
1135    j_init = 0
1136    msh2 = vol.ySlice(j_init).pointColors(cmap=cmap).lighting("", la, ld, 0)
1137    msh2.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1138    vp.renderer.AddActor(msh)
1139    visibles[1] = msh2
1140    k_init = 0
1141    msh3 = vol.zSlice(k_init).pointColors(cmap=cmap).lighting("", la, ld, 0, 0)
1142    msh3.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1143    vp.renderer.AddActor(msh3)
1144    visibles[2] = msh3
1145
1146    # the colormap button
1147    def buttonfunc():
1148        global cmap
1149        bu.switch()
1150        cmap = bu.status()
1151        for mesh in visibles:
1152            if mesh:
1153                mesh.pointColors(cmap=cmap)
1154        vp.renderer.RemoveActor(mesh.scalarbar)
1155        mesh.scalarbar = addons.addScalarBar(
1156            mesh, pos=(0.04, 0.0), horizontal=True, titleFontSize=0
1157        )
1158        vp.renderer.AddActor(mesh.scalarbar)
1159
1160    bu = vp.addButton(
1161        buttonfunc,
1162        pos=(0.27, 0.005),
1163        states=cmaps,
1164        c=["db"] * len(cmaps),
1165        bc=["lb"] * len(cmaps),
1166        size=14,
1167        bold=True,
1168    )
1169
1170    hist = cornerHistogram(
1171        seismic,
1172        s=0.2,
1173        bins=75,
1174        logscale=0,
1175        pos=(0.02, 0.02),
1176        c=((0.0, 0.0, 0.75)),
1177        bg=((0.1, 0.1, 0.1)),
1178        alpha=0.7,
1179    )
1180
1181    # Show the meshes and start interacting
1182    viewpoint1 = [-200.0, -200.0, 500.0]
1183    focalpoint1 = [300, 300, 150]
1184    vp.addLight(pos=viewpoint1, focalPoint=focalpoint1)
1185
1186    viewpoint2 = [-500.0, -500.0, 500.0]
1187    focalpoint2 = [150, 150, 150]
1188    vp.addLight(pos=viewpoint2, focalPoint=focalpoint2)
1189
1190    viewpoint3 = [-400.0, -400.0, 600.0]
1191    focalpoint3 = [150, 150, 150]
1192    vp.addLight(pos=viewpoint3, focalPoint=focalpoint3)
1193
1194    # Can now add any other object to the Plotter scene:
1195    text0 = Text2D(title_text, s=1.0, font="arial")
1196
1197    # set camera position
1198    start_pos = [825.0, 350.0, 600.0]
1199    start_vu = [-0.4, -0.1, 0.92]
1200    vp.camera.SetPosition(start_pos)
1201    vp.camera.SetFocalPoint([150.0, 150.0, 150.0])
1202    vp.camera.SetViewUp(start_vu)
1203    vp.camera.SetDistance(1065.168)
1204    vp.camera.SetClippingRange([517.354, 1757.322])
1205
1206    if include_faults:
1207        vp.show(
1208            msh,
1209            msh2,
1210            msh3,
1211            hist,
1212            brine_surface,
1213            oil_surface,
1214            gas_surface,
1215            fault_surface,
1216            text0,
1217            N=1,
1218            zoom=1.5,
1219        )
1220    else:
1221        vp.show(
1222            msh,
1223            msh2,
1224            msh3,
1225            hist,
1226            brine_surface,
1227            oil_surface,
1228            gas_surface,
1229            text0,
1230            N=1,
1231            zoom=1.5,
1232        )
1233
1234    # capture the scene from several vantage points
1235    delay = str(600 // camera_steps)
1236
1237    # define end camera position
1238    end_pos = [450.0, 275.0, 1150.0]
1239    end_vu = [-0.9, -0.3, 0.3]
1240
1241    n_images1 = 0
1242    camera_steps_1 = camera_steps // 3
1243    for icam in range(camera_steps):
1244        _scalar = float(icam) / (camera_steps - 1)
1245        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1246        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1247        vp.camera.SetPosition(_pos)
1248        vp.camera.SetViewUp(_vu)
1249        screenshot(
1250            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images1:03d}.png",
1251            scale=None,
1252            returnNumpy=False,
1253        )
1254        if cfg.verbose:
1255            print("\n" + str(n_images1), str(_pos), str(_vu))
1256        n_images1 += 1
1257
1258    start_pos = [450.0, 275.0, 1150.0]
1259    start_vu = [-0.9, -0.3, 0.3]
1260    end_pos = [275.0, 450.0, 1150.0]
1261    end_vu = [-0.3, -0.9, 0.3]
1262
1263    n_images2 = n_images1
1264    camera_steps_2 = camera_steps // 3
1265    for icam in range(camera_steps // 3):
1266        _scalar = float(icam) / (camera_steps - 1)
1267        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1268        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1269        vp.camera.SetPosition(_pos)
1270        vp.camera.SetViewUp(_vu)
1271        screenshot(
1272            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images2:03d}.png",
1273            scale=None,
1274            returnNumpy=False,
1275        )
1276        if cfg.verbose:
1277            print("\n" + str(n_images2), str(_pos), str(_vu))
1278        n_images2 += 1
1279
1280    start_pos = [275.0, 450.0, 1150.0]
1281    start_vu = [-0.3, -0.9, 0.3]
1282    end_vu = [-0.1, -0.4, 0.92]
1283    end_pos = [350.0, 825.0, 600.0]
1284
1285    n_images3 = n_images2
1286    camera_steps_3 = camera_steps // 3
1287    for icam in range(camera_steps // 3):
1288        _scalar = float(icam) / (camera_steps - 1)
1289        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1290        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1291        vp.camera.SetPosition(_pos)
1292        vp.camera.SetViewUp(_vu)
1293        screenshot(
1294            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images3:03d}.png",
1295            scale=None,
1296            returnNumpy=False,
1297        )
1298        if cfg.verbose:
1299            print("\n" + str(n_images3), str(_pos), str(_vu))
1300        n_images3 += 1
1301
1302    n_images4 = n_images3
1303    for icam in range(camera_steps // 6):
1304        fig_num = n_images3 + icam
1305        os.system(
1306            f"cp {cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images3 - 1:03d}.png "
1307            + f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{fig_num:03d}.png"
1308        )
1309        n_images4 += 1
1310
1311    # create animated gif from screenshots, convert requires ImageTools bin folder to be on $PATH
1312    output_imagename = os.path.join(
1313        cfg.work_subfolder, f"qc_image_3d_{cfg.date_stamp}.gif"
1314    )
1315    os.system(
1316        f"convert -delay {delay} {cfg.temp_folder}/screenshot_{cfg.date_stamp}_*.png -loop 0 \
1317          -dispose previous {output_imagename}"
1318    )
1319
1320    vp.close()
1321    vp.closeWindow()
def find_line_with_most_voxels(volume, voxel_thresholds, cfg):
 6def find_line_with_most_voxels(volume, voxel_thresholds, cfg):
 7    """
 8    Find the line with the most voxels
 9    ----------------------------------
10
11    Finds the line wtih the most voxels.
12
13    Loop over a volume and locate the inline which has the highest number of
14    voxels which meet the threshold criteria
15
16    If multiple thresholds are used, first is used as a lower bound,
17    and second is used as an upper bound
18
19    Parameters
20    ----------
21    volume : np.ndarray
22        3D volume (ndarray)
23    voxel_thresholds: list
24        Used for thresholding
25    cfg : dict
26        model parameters
27
28    Returns
29    -------
30    inline_index : int
31        Inline containing highest number of voxels meeting criteria.
32    """
33    max_voxels = 0
34    inline_index = int(cfg.cube_shape[0] / 2)
35    for ii in range(volume.shape[0]):
36        voxels = volume[ii, ...].copy()
37        try:  # if two values provided...
38            voxels = np.where(
39                ((voxels >= voxel_thresholds[0] & voxels < voxel_thresholds[1])),
40                1.0,
41                0.0,
42            )
43        except TypeError:  # Just use 1 threshold
44            voxels = np.where(voxels > voxel_thresholds, 1.0, 0.0)
45        if voxels[voxels == 1.0].size > max_voxels:
46            max_voxels = voxels[voxels == 1.0].size
47            inline_index = int(ii)
48
49    return inline_index
Find the line with the most voxels

Finds the line wtih the most voxels.

Loop over a volume and locate the inline which has the highest number of voxels which meet the threshold criteria

If multiple thresholds are used, first is used as a lower bound, and second is used as an upper bound

Parameters
  • volume (np.ndarray): 3D volume (ndarray)
  • voxel_thresholds (list): Used for thresholding
  • cfg (dict): model parameters
Returns
  • inline_index (int): Inline containing highest number of voxels meeting criteria.
def plot_voxels_not_in_regular_layers( volume: numpy.ndarray, threshold: float, title: str, png_name: str, cfg) -> None:
52def plot_voxels_not_in_regular_layers(
53    volume: np.ndarray,
54    threshold: float,
55    title: str,
56    png_name: str,
57    cfg
58) -> None:
59    """
60        Plot voxels not in regular layers
61
62        Analyze voxel values not in regular layers by plotting a
63        histogram of voxels above a given threshold.
64
65        Parameters
66        ----------
67        volume : np.ndarray
68            3D volume (ndarray)
69        threshold : float
70            Threshold for voxel values.
71        title : str
72            Title for plot.
73        png_name : str
74            Name of png file to save.
75        cfg : dict
76            Model configurations.
77
78        Returns
79        -------
80        None
81    """
82    plt = import_matplotlib()
83    voxels = volume[volume > threshold]
84    plt.figure(1, figsize=(20, 15))
85    plt.clf()
86    plt.title(title)
87    plt.hist(voxels, 81)
88    plt.savefig(os.path.join(cfg.work_subfolder, png_name), format="png")
89    plt.close(1)

Plot voxels not in regular layers

Analyze voxel values not in regular layers by plotting a histogram of voxels above a given threshold.

Parameters
  • volume (np.ndarray): 3D volume (ndarray)
  • threshold (float): Threshold for voxel values.
  • title (str): Title for plot.
  • png_name (str): Name of png file to save.
  • cfg (dict): Model configurations.
Returns
  • None
def plot_xsection(volume, maps, line_num, title, png_name, cfg, cmap='prism') -> None:
 92def plot_xsection(volume, maps, line_num, title, png_name, cfg, cmap="prism") -> None:
 93    """
 94    Plot cross section.
 95
 96    Parameters
 97    ----------
 98    volume : np.ndarray
 99        The 3D volume as a numpy array
100    maps : _type_
101        _description_
102    line_num : _type_
103        _description_
104    title : _type_
105        _description_
106    png_name : _type_
107        _description_
108    cfg : _type_
109        _description_
110    cmap : str, optional
111        _description_, by default "prism"
112    
113    Returns
114    -------
115    None
116    """
117    plt = import_matplotlib()
118    plt.clf()
119    plt.title(f"{title}\nInline: {line_num}")
120    plt.figure(1, figsize=(20, 15))
121    # If depth maps is infilled and volume is not, update the Z axis of the volume
122    # Pick the 75%'th horizon to check
123    if (
124        np.max(maps[:, :, -int(maps.shape[-1] / 4)]) > volume.shape[-1]
125    ):  # don't use last horizon
126        plt.imshow(
127            np.fliplr(
128                np.rot90(
129                    volume[line_num, :, : (volume.shape[-1] * cfg.infill_factor) - 1], 3
130                )
131            ),
132            aspect="auto",
133            cmap=cmap,
134        )
135    else:
136        plt.imshow(
137            np.fliplr(np.rot90(volume[line_num, ...], 3)), aspect="auto", cmap=cmap
138        )
139    plt.colorbar()
140    plt.ylim((volume.shape[-1], 0))
141    for i in range(0, maps.shape[-1], 1):
142        plt.plot(range(cfg.cube_shape[0]), maps[line_num, :, i], "k-", lw=0.3)
143    plt.savefig(os.path.join(cfg.work_subfolder, png_name), format="png")
144    plt.close(1)

Plot cross section.

Parameters
  • volume (np.ndarray): The 3D volume as a numpy array
  • maps (_type_): _description_
  • line_num (_type_): _description_
  • title (_type_): _description_
  • png_name (_type_): _description_
  • cfg (_type_): _description_
  • cmap (str, optional): _description_, by default "prism"
Returns
  • None
def plot_3D_faults_plot(cfg, faults, plot_faults=True, plot_throws=True) -> None:
147def plot_3D_faults_plot(cfg, faults, plot_faults=True, plot_throws=True) -> None:
148    """
149    Plot 3D faults plot
150    -------------------
151
152    Plots the faults in a 3D plot.
153
154    Parameters
155    ----------
156    cfg : _type_
157        The configuration.
158    faults : np.ndarray
159        The faults a numpy object.
160    plot_faults : bool, optional
161        Whether to plot the faults or not, by default True
162    plot_throws : bool, optional
163        Whether to plot the fault throws or not, by default True
164    
165    Returns
166    -------
167    None
168    """
169    from plotly.offline import plot
170    import plotly.graph_objects as go
171
172    fi1 = faults.fault_planes
173    decimation_factor = 2
174    x1, y1, z1 = np.where(
175        fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
176    )
177    x1 *= decimation_factor
178    y1 *= decimation_factor
179    z1 *= -decimation_factor
180    trace1 = go.Scatter3d(
181        x=x1,
182        y=y1,
183        z=z1,
184        name="fault_segments",
185        mode="markers",
186        marker=dict(size=2.0, color="blue", opacity=0.025),
187    )
188
189    if plot_faults:
190        fi2 = faults.fault_intersections
191        x2, y2, z2 = np.where(
192            fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.25
193        )
194        x2 *= decimation_factor
195        y2 *= decimation_factor
196        z2 *= -decimation_factor
197        trace2 = go.Scatter3d(
198            x=x2,
199            y=y2,
200            z=z2,
201            name="fault_intersection_segments",
202            mode="markers",
203            marker=dict(
204                size=1.5,
205                color="red",  # set color to an array/list of desired values
206                colorscale="Viridis",  # choose a colorscale
207                opacity=0.025,
208            ),
209        )
210
211        data = [trace1, trace2]
212        layout = go.Layout(
213            title=go.layout.Title(
214                text="Fault Segments & Intersections"
215                + "<br>"
216                + '<span style="font-size: 12px;">'
217                + cfg.work_subfolder
218                + "</span>",
219                xref="paper",
220                x=0,
221            )
222        )
223        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
224        fig3 = go.Figure(data=data, layout=layout)
225        fig3.update_layout(
226            scene_camera=camera,
227            autosize=False,
228            width=960,
229            height=720,
230            scene=dict(
231                aspectmode="manual",
232                aspectratio=dict(x=1, y=1, z=1),
233                xaxis=dict(
234                    nticks=4,
235                    range=[0, 300],
236                ),
237                yaxis=dict(
238                    nticks=4,
239                    range=[0, 300],
240                ),
241                zaxis=dict(
242                    nticks=4,
243                    range=[-1260, 0],
244                ),
245            ),
246            margin=dict(r=20, l=10, b=10, t=25),
247        )
248        output_filename = os.path.join(
249            cfg.work_subfolder,
250            f"qcplot_fault_segments_and_intersections_3D__{cfg.date_stamp}",
251        )
252        plot(fig3, filename=f"{output_filename}.html", auto_open=False)
253        try:
254            fig3.write_image(f"{output_filename}.png")
255        except:
256            print("png file saving failed")
257        print(
258            "\n   ... util/plot_3D_faults_plot finished creating 3D plot at:\n       "
259            + output_filename
260        )
261
262    if plot_throws:
263        fi3 = faults.fault_plane_throw
264        z3 = fi3[x1, y1, -z1]
265        trace3 = go.Scatter3d(
266            x=x1,
267            y=y1,
268            z=z1,
269            name="fault_intersection_segments",
270            mode="markers",
271            marker=dict(
272                size=4.0,
273                color=z3,  # set color to an array/list of desired values
274                colorscale="Viridis",  # choose a colorscale
275                opacity=0.05,
276            ),
277        )
278        data = [trace3]
279        layout = go.Layout(
280            title=go.layout.Title(
281                text="Fault Throw along fault plane"
282                + "<br>"
283                + '<span style="font-size: 12px;">'
284                + cfg.work_subfolder
285                + "</span>",
286                xref="paper",
287                x=0,
288            )
289        )
290        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
291        fig4 = go.Figure(data=data, layout=layout)
292        fig4.update_layout(
293            scene_camera=camera,
294            autosize=False,
295            width=960,
296            height=720,
297            scene=dict(
298                aspectmode="manual",
299                aspectratio=dict(x=1, y=1, z=1),
300                xaxis=dict(
301                    nticks=4,
302                    range=[0, 300],
303                ),
304                yaxis=dict(
305                    nticks=4,
306                    range=[0, 300],
307                ),
308                zaxis=dict(
309                    nticks=4,
310                    range=[-1260, 0],
311                ),
312            ),
313            margin=dict(r=20, l=10, b=10, t=25),
314        )
315        output_filename = os.path.join(
316            cfg.work_subfolder, f"qcplot_fault_segments_throw_3D__{cfg.date_stamp}"
317        )
318        plot(fig4, filename=f"{output_filename}.html", auto_open=False)
319        try:
320            fig4.write_image(f"{output_filename}.png")
321        except:
322            print("png file saving failed")
323        print(
324            "\n   ... util/plot_3D_faults_plot finished creating 3D plot at:\n       "
325            + output_filename
326        )

Plot 3D faults plot

Plots the faults in a 3D plot.

Parameters
  • cfg (_type_): The configuration.
  • faults (np.ndarray): The faults a numpy object.
  • plot_faults (bool, optional): Whether to plot the faults or not, by default True
  • plot_throws (bool, optional): Whether to plot the fault throws or not, by default True
Returns
  • None
def plot_3D_closure_plot(cfg, closures, plot_closures=True, plot_strat_closures=True) -> None:
329def plot_3D_closure_plot(cfg, closures, plot_closures=True, plot_strat_closures=True) -> None:
330    """
331    Plot 3D closures
332    ----------------
333
334    Plot the closures in 3D.
335
336    Parameters
337    ----------
338    cfg : dict
339        The configuration.
340    closures : np.ndarray
341        Closures array.
342    plot_closures : bool, optional
343        Whether to plot the closures or not, by default True
344    plot_strat_closures : bool, optional
345        Whether to plot statigraphic closures or not, by default True
346    
347    Returns
348    -------
349    None
350    """
351    from plotly.offline import plot
352    import plotly.graph_objects as go
353
354    fi1 = closures.gas_closures
355    decimation_factor = 2
356    x1, y1, z1 = np.where(
357        fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
358    )
359    x1 *= decimation_factor
360    y1 *= decimation_factor
361    z1 *= -decimation_factor
362
363    fi2 = closures.oil_closures
364    x2, y2, z2 = np.where(
365        fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.25
366    )
367    x2 *= decimation_factor
368    y2 *= decimation_factor
369    z2 *= -decimation_factor
370
371    fi3 = closures.brine_closures
372    x3, y3, z3 = np.where(
373        fi3[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5
374    )
375    x3 *= decimation_factor
376    y3 *= decimation_factor
377    z3 *= -decimation_factor
378
379    size = 2.0
380    opacity = 0.15
381
382    if plot_closures:
383        trace1 = go.Scatter3d(
384            x=x1,
385            y=y1,
386            z=z1,
387            name="gas_segments",
388            mode="markers",
389            marker=dict(size=size, color="red", opacity=opacity),
390        )
391
392        trace2 = go.Scatter3d(
393            x=x2,
394            y=y2,
395            z=z2,
396            name="oil_segments",
397            mode="markers",
398            marker=dict(
399                size=size,
400                color="green",  # set color to an array/list of desired values
401                colorscale="Viridis",  # choose a colorscale
402                opacity=opacity,
403            ),
404        )
405
406        trace3 = go.Scatter3d(
407            x=x3,
408            y=y3,
409            z=z3,
410            name="brine_segments",
411            mode="markers",
412            marker=dict(
413                size=size,
414                color="blue",  # set color to an array/list of desired values
415                colorscale="Viridis",  # choose a colorscale
416                opacity=opacity / 5.0,
417            ),
418        )
419
420        data = [trace1, trace2, trace3]
421        layout = go.Layout(
422            title=go.layout.Title(
423                text="Closure Segments (Gas, Oil, Brine)"
424                + "<br>"
425                + '<span style="font-size: 12px;">'
426                + cfg.work_subfolder
427                + "</span>",
428                xref="paper",
429                x=0,
430            )
431        )
432        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
433        fig4 = go.Figure(data=data, layout=layout)
434        fig4.update_layout(
435            scene_camera=camera,
436            autosize=False,
437            width=960,
438            height=720,
439            scene=dict(
440                aspectmode="manual",
441                aspectratio=dict(x=1, y=1, z=1),
442                xaxis=dict(
443                    nticks=4,
444                    range=[0, 300],
445                ),
446                yaxis=dict(
447                    nticks=4,
448                    range=[0, 300],
449                ),
450                zaxis=dict(
451                    nticks=4,
452                    range=[-1260, 0],
453                ),
454            ),
455            # width=650,
456            margin=dict(r=20, l=10, b=10, t=25),
457        )
458        output_filename = os.path.join(
459            cfg.work_subfolder, f"qcplot_closure_segments_3D_{cfg.date_stamp}"
460        )
461        plot(fig4, filename=f"{output_filename}.html", auto_open=False)
462        try:
463            fig4.write_image(f"{output_filename}.png")
464        except ValueError:
465            print("png file saving failed")
466        print(
467            "\n   ... util/plot_3D_closure_plot finished creating 3D plot at:\n       "
468            + output_filename
469        )
470
471    if plot_strat_closures:
472        ###-------------------------------------------------------------------------
473        ### Make 3D plot of strat traps (onlap/pinch-out traps)
474        ###-------------------------------------------------------------------------
475        fi4 = closures.strat_closures
476        decimation_factor = 2
477        x4, y4, z4 = np.where(
478            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
479            & (fi1[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
480        )
481        x4 *= decimation_factor
482        y4 *= decimation_factor
483        z4 *= -decimation_factor
484
485        x5, y5, z5 = np.where(
486            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
487            & (fi2[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
488        )
489        x5 *= decimation_factor
490        y5 *= decimation_factor
491        z5 *= -decimation_factor
492
493        x6, y6, z6 = np.where(
494            (fi4[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
495            & (fi3[::decimation_factor, ::decimation_factor, ::decimation_factor] > 0.5)
496        )
497        x6 *= decimation_factor
498        y6 *= decimation_factor
499        z6 *= -decimation_factor
500
501        size = 2.0
502        opacity = 0.15
503
504        trace1 = go.Scatter3d(
505            x=x4,
506            y=y4,
507            z=z4,
508            name="gas_strat",
509            mode="markers",
510            marker=dict(size=size, color="red", opacity=opacity),
511        )
512
513        trace2 = go.Scatter3d(
514            x=x5,
515            y=y5,
516            z=z5,
517            name="oil_strat",
518            mode="markers",
519            marker=dict(
520                size=size,
521                color="green",  # set color to an array/list of desired values
522                colorscale="Viridis",  # choose a colorscale
523                opacity=opacity,
524            ),
525        )
526
527        trace3 = go.Scatter3d(
528            x=x6,
529            y=y6,
530            z=z6,
531            name="brine_strat",
532            mode="markers",
533            marker=dict(
534                size=size,
535                color="blue",  # set color to an array/list of desired values
536                colorscale="Viridis",  # choose a colorscale
537                opacity=opacity / 5.0,
538            ),
539        )
540
541        data = [trace1, trace2, trace3]
542        layout = go.Layout(
543            title=go.layout.Title(
544                text="Stratigraphic Closure Segments (Gas, Oil, Brine)"
545                + "<br>"
546                + '<span style="font-size: 12px;">'
547                + cfg.work_subfolder
548                + "</span>",
549                xref="paper",
550                x=0,
551            )
552        )
553        camera = dict(eye=dict(x=1.25 / 1.2, y=1.25 / 1.2, z=0.75 / 1.2))
554        fig5 = go.Figure(data=data, layout=layout)
555        fig5.update_layout(
556            scene_camera=camera,
557            autosize=False,
558            width=960,
559            height=720,
560            scene=dict(
561                aspectmode="manual",
562                aspectratio=dict(x=1, y=1, z=1),
563                xaxis=dict(
564                    nticks=4,
565                    range=[0, 300],
566                ),
567                yaxis=dict(
568                    nticks=4,
569                    range=[0, 300],
570                ),
571                zaxis=dict(
572                    nticks=4,
573                    range=[-1260, 0],
574                ),
575            ),
576            # width=650,
577            margin=dict(r=20, l=10, b=10, t=25),
578        )
579        output_filename = os.path.join(
580            cfg.work_subfolder, f"qcplot_strat_closure_segments_3D_{cfg.date_stamp}"
581        )
582        plot(fig5, filename=f"{output_filename}.html", auto_open=False)
583        try:
584            fig5.write_image(f"{output_filename}.png")
585        except:
586            print("png file saving failed")
587        print(
588            "\n   ... util/plot_3D_strat_closure_plot finished creating 3D plot at:\n       "
589            + output_filename
590        )

Plot 3D closures

Plot the closures in 3D.

Parameters
  • cfg (dict): The configuration.
  • closures (np.ndarray): Closures array.
  • plot_closures (bool, optional): Whether to plot the closures or not, by default True
  • plot_strat_closures (bool, optional): Whether to plot statigraphic closures or not, by default True
Returns
  • None
def plot_facies_qc(cfg, lith, seismic, facies, maps) -> None:
593def plot_facies_qc(cfg, lith, seismic, facies, maps) -> None:
594    """
595    Plot Facies for QC
596    ------------------
597
598    Plots the facies to aid in QC of the model.
599
600    Parameters
601    ----------
602    cfg : dict
603        The configuration used in the model.
604    lith : np.ndarray
605        The lithology model.
606    seismic : np.ndarray
607        The seismic model.
608    facies : np.ndarray
609        The facies model.
610    maps : dict
611        The maps used in the model.
612    
613    Returns
614    -------
615    None
616    """
617    plt = import_matplotlib()
618    from itertools import groupby
619    import matplotlib as mpl
620
621    # Plot Centre Inline of facies and seismic
622    iline = lith.shape[0] // 2
623    avg_sand_unit_thickness = np.mean(
624        [b for a, b in [(k, sum(1 for i in g)) for k, g in groupby(facies)] if a == 1.0]
625    )
626    textstr = "\n".join(
627        (
628            f"Sand % Input: {cfg.sand_layer_pct:.1%}",
629            f"Sand % Actual: {facies[facies == 1.].size / facies.size:.1%}",
630            f"Sand thickness (layers) Input: {cfg.sand_layer_thickness}",
631            f"Sand thickness (layers) Actual: {avg_sand_unit_thickness:.1f}",
632            f"Sand Voxel % in model: {lith[lith[:] == 1].size / lith[lith[:] >= 0].size:.1%}",
633        )
634    )
635    fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(20, 15), sharey=True)
636    fig.suptitle("Facies QC Plot")
637    axs[0].set_ylim(cfg.cube_shape[-1])
638    if cfg.include_salt and np.max(lith[iline, ...]) > 1:
639        axs[0].imshow(
640            np.fliplr(np.rot90(lith[iline, ...], 3)),
641            aspect="auto",
642            cmap=mpl.colors.ListedColormap(["blue", "saddlebrown", "gold", "grey"]),
643        )
644    else:
645        axs[0].imshow(
646            np.fliplr(np.rot90(lith[iline, ...], 3)),
647            aspect="auto",
648            cmap=mpl.colors.ListedColormap(["blue", "saddlebrown", "gold"]),
649        )
650    _img = axs[1].imshow(
651        np.fliplr(np.rot90(seismic[iline, ...], 3)),
652        aspect="auto",
653        cmap="Greys",
654        vmin=-300,
655        vmax=300,
656    )
657    # fig.colorbar(_img, ax=axs[1])
658    props = dict(boxstyle="round", alpha=0.5)
659    # Add textbox with textstr to facies subplot
660    axs[0].text(
661        0.05,
662        0.95,
663        textstr,
664        transform=axs[0].transAxes,
665        fontsize=14,
666        verticalalignment="top",
667        bbox=props,
668    )
669    for i in range(maps.shape[-1]):
670        axs[0].plot(range(cfg.cube_shape[0]), maps[iline, :, i], "k-", lw=0.3)
671
672    fig.savefig(os.path.join(cfg.work_subfolder, "QC_plot__Facies_FullStackCumulativeSum.png"))
673    plt.close()
Plot Facies for QC

Plots the facies to aid in QC of the model.

Parameters
  • cfg (dict): The configuration used in the model.
  • lith (np.ndarray): The lithology model.
  • seismic (np.ndarray): The seismic model.
  • facies (np.ndarray): The facies model.
  • maps (dict): The maps used in the model.
Returns
  • None
def infill_surface(surface: numpy.ndarray) -> numpy.ndarray:
676def infill_surface(surface: np.ndarray) -> np.ndarray:
677    """
678    Infill Surface
679    --------------
680
681    Infill surfaces.
682
683    Fill holes in input 2D surface.
684    Holes have value of either nan or zero.
685    Fill using replacement with interpolated (nearest-neighbor) value.
686
687    Parameters
688    ----------
689    surface : np.ndarray
690        2D numpy array
691
692    Returns
693    -------
694    surface_infilled : np.ndarray
695        (2D) surface_infilled
696    """
697    from scipy.interpolate import NearestNDInterpolator
698
699    # get x,y,z indices for non-nan points on surface
700    xx, yy = np.meshgrid(
701        range(surface.shape[0]), range(surface.shape[1]), sparse=False, indexing="ij"
702    )
703    x_no_nan = xx[~np.isnan(surface)]
704    y_no_nan = yy[~np.isnan(surface)]
705    z_surface = surface[~np.isnan(surface)]
706
707    if z_surface.flatten().shape[0] > 2:
708        # set up nearest-neighbor interpolator function
709        xy = zip(x_no_nan, y_no_nan)
710        nn = NearestNDInterpolator(xy, z_surface)
711        # interpolate at every x,y on regular grid
712        surface_nn = nn(xx, yy)
713        # replace input surface with interpolated (nearest-neighbor)
714        # - if input value is either nan or zero
715        surface_infilled = surface.copy()
716        surface_infilled[np.isnan(surface)] = surface_nn[np.isnan(surface)]
717        surface_infilled[surface == 0] = surface_nn[surface == 0]
718    else:
719        surface_infilled = z_surface.copy()
720
721    if surface_infilled[np.isnan(surface_infilled)].shape[0] > 0:
722        count = surface_infilled[np.isnan(surface_infilled)].shape[0]
723        print(
724            f"\n\n\n   ...inside infill_surface: there are some NaN values in the surface. count = {count}"
725        )
726
727    return surface_infilled
Infill Surface

Infill surfaces.

Fill holes in input 2D surface. Holes have value of either nan or zero. Fill using replacement with interpolated (nearest-neighbor) value.

Parameters
  • surface (np.ndarray): 2D numpy array
Returns
  • surface_infilled (np.ndarray): (2D) surface_infilled
def mute_above_seafloor(surface, xyz):
730def mute_above_seafloor(surface, xyz):
731    """
732    Mute data above seafloor
733    ------------------------
734
735    Mute a cube above a surface that contains
736    indices for the 3rd axis of the cube.
737
738    Parameters
739    ----------
740    surface : np.ndarray
741        Mute above this (2D) surface
742    xyz : np.ndarray
743        Cube to which muting is applied.
744
745    Returns
746    -------
747    xyz_muted : np.ndarray
748        Muted (3D array)
749    """
750
751    krange = np.arange(xyz.shape[2])
752
753    # broadcast indices of vertical axis to 3D
754    krange = krange.reshape((1, 1, len(krange)))
755
756    if np.isnan(np.min(surface)) or 0 in surface and not np.all(surface == 0):
757        # If surface contains nan's or at least one 0, this will return True, therefore...
758        # fill empty picks with nearest-neighbor infilling
759        # If all 0's, skip it
760        surface_infilled = infill_surface(surface)
761    else:
762        surface_infilled = surface
763
764    # broadcast surface to 3D
765    surface_3d = surface_infilled.reshape((surface.shape[0], surface.shape[1], 1))
766
767    # apply muting to copy of xyz
768    xyz_muted = xyz.copy()
769    xyz_muted[krange < surface_3d] *= 0.0
770
771    return xyz_muted
Mute data above seafloor

Mute a cube above a surface that contains indices for the 3rd axis of the cube.

Parameters
  • surface (np.ndarray): Mute above this (2D) surface
  • xyz (np.ndarray): Cube to which muting is applied.
Returns
  • xyz_muted (np.ndarray): Muted (3D array)
def is_it_in_hull(hull, p) -> numpy.ndarray[bool]:
774def is_it_in_hull(hull, p) -> np.ndarray[bool]:
775    """
776    Is it in hull?
777    --------------
778    Checks if point is in hull
779
780    Test if points in `p` are in `hull`
781
782    `p` should be a `NxK` coordinates of `N` points in `K` dimensions
783    `hull` is either a scipy.spatial.Delaunay object or the `MxK` array of the
784    coordinates of `M` points in `K`dimensions for which Delaunay triangulation
785    will be computed
786
787    Parameters
788    ----------
789    hull : np.ndarray
790        The hull object.
791    p : np.ndarray
792        Point(s) to check.
793
794    Returns
795    -------
796    np.ndarray[bool]
797        An array containing the booleans where the object is hull.
798
799
800    """
801    from scipy.spatial import Delaunay
802
803    if not isinstance(hull, Delaunay):
804        hull = Delaunay(hull)
805
806    return hull.find_simplex(p) >= 0

Is it in hull?

Checks if point is in hull

Test if points in p are in hull

p should be a NxK coordinates of N points in K dimensions hull is either a scipy.spatial.Delaunay object or the MxK array of the coordinates of M points in Kdimensions for which Delaunay triangulation will be computed

Parameters
  • hull (np.ndarray): The hull object.
  • p (np.ndarray): Point(s) to check.
Returns
  • np.ndarray[bool]: An array containing the booleans where the object is hull.
def even_odd(number):
810def even_odd(number):
811    if number % 2 == 0:
812        return "Even"
813    else:
814        return "Odd"
def next_odd(number):
817def next_odd(number):
818    if even_odd(number) == "Even":
819        odd_number = number + 1
820    else:
821        odd_number = number
822    return odd_number
def find_index_next_bigger(mylist, mynumber):
825def find_index_next_bigger(mylist, mynumber):
826    # return the index of the next number in mylist that is bigger (or equal to) mynumber
827    # example:   mynumber=46;  mylist=[1,5,13,25,36,48,55,67,73,85,90]
828    # example:  find_index_next_bigger(mylist,mynumber) returns 5
829    return next((mylist.index(n) for n in mylist if n >= mynumber), len(mylist))
def linearinfill(x, y, newx):
832def linearinfill(x, y, newx):
833    # ***************************************************************************************
834    #
835    #   Function to return data spaced at locations specified by input variable 'newx' after
836    #   fitting linear function. Input data specified by 'x' and 'y' so data
837    #   can be irregularly sampled.
838    #
839    # ***************************************************************************************
840    s = np.interp(newx, x, y)
841    return s
def check_resident_memory():
844def check_resident_memory():
845    import os
846    import sys
847
848    if "linux" in sys.platform:
849        _proc_status = "/proc/%d/status" % os.getpid()
850        _scale = {
851            "kB": 1024.0,
852            "mB": 1024.0 * 1024.0,
853            "KB": 1024.0,
854            "MB": 1024.0 * 1024.0,
855        }
856
857        t = open(_proc_status)
858        v = t.read()
859        t.close()
860
861        i = v.index("VmRSS:")
862        v = v[i:].split(None, 3)
863
864        return (float(v[1]) * _scale[v[2]]) / 1024 ** 3
865    else:
866        return 0.0
def import_matplotlib():
869def import_matplotlib():
870    """
871    Function to perform matplotlib imports with non-interactive backend when running in background
872    DPG 17/2/19
873    """
874    import sys
875
876    if bool(getattr(sys, "ps1", sys.flags.interactive)):
877        # this is an interactive session
878        from matplotlib import pyplot as plt
879
880        plt.ion()
881    else:
882        # import matplotlib with non-interactive backend ('Agg')
883        import matplotlib
884
885        matplotlib.use("Agg")
886        from matplotlib import pyplot as plt
887
888        plt.ioff()
889    return plt

Function to perform matplotlib imports with non-interactive backend when running in background DPG 17/2/19

def write_data_to_hdf(n, d, h5file):
892def write_data_to_hdf(n, d, h5file):
893    import h5py
894
895    with h5py.File(h5file, "a") as hf:
896        hf.create_dataset(name=n, data=d, compression="lzf")
def qc_folders(directory):
900def qc_folders(directory):
901    """Count how many model_parameter.txt files contain elapse time
902
903    Use this to remove models that failed to complete for whatever reason
904    """
905    import glob
906    import os
907
908    valid_models = list()
909    model_closures = dict()
910    # Grep for elapse in model_parameters.txt files in geocrawler subdirectories
911    par = glob.glob(os.path.join(directory, "geoc*/model_parameters*.txt"))
912    # Elapse time
913    for p in par:
914        with open(p, "r") as f:
915            for line in f.readlines():
916                if "elapse" in line:
917                    valid_models.append(os.path.dirname(p))
918                if "Number of Closures" in line:
919                    num_closures = line.split()[-1]
920                    model_closures[os.path.dirname(p)] = int(num_closures)
921
922    # Invalid folders
923    invalid_models = list(
924        set(glob.glob("{}/geoc*".format(directory))) - set(valid_models)
925    )
926    # Remove tar.gz files from invalid_models
927    invalid_final = [x for x in invalid_models if not x.endswith("tar.gz")]
928
929    # Also sort closures by number of closures
930    sorted_closures = [
931        (k, model_closures[k])
932        for k in sorted(model_closures, key=model_closures.get, reverse=True)
933    ]
934
935    return invalid_final, valid_models, sorted_closures

Count how many model_parameter.txt files contain elapse time

Use this to remove models that failed to complete for whatever reason

def hanflat(inarray, pctflat):
938def hanflat(inarray, pctflat):
939
940    # ***************************************************************************************
941    #
942    #   Function applies a Hanning taper to ends of "inarray".
943    #   Center "pctflat" of samples remain unchanged.
944    #
945    #   Parameters:
946    #   array :       array of values to have ends tapered
947    #   pctflat :     this percent of  samples to remain unchanged (e.g. 0.80)
948    #
949    #   Returns weights (not weights applied to input array)
950    #
951    # ***************************************************************************************
952
953    numsamples = len(inarray)
954    lowflatindex = int(round(numsamples * (1.0 - pctflat) / 2.0))
955    hiflatindex = numsamples - lowflatindex
956
957    # get hanning for numsamples*(1.0-pctflat)
958    hanwgt = np.hanning(len(inarray) - (hiflatindex - lowflatindex))
959
960    # piece together hanning weights at ends and weights=1.0 in flat portion
961    outarray = np.ones(len(inarray), dtype=float)
962    outarray[:lowflatindex] = hanwgt[:lowflatindex]
963    outarray[hiflatindex:] = hanwgt[numsamples - hiflatindex :]
964
965    return outarray
def hanflat2d(inarray2d, pctflat):
968def hanflat2d(inarray2d, pctflat):
969    hanwgt1 = hanflat(np.ones((inarray2d.shape[0])), pctflat)
970    hanwgt2 = hanflat(np.ones((inarray2d.shape[1])), pctflat)
971    return np.sqrt(np.outer(hanwgt1, hanwgt2))
def hanflat3d(inarray3d, pctflat):
974def hanflat3d(inarray3d, pctflat):
975    hanwgt1 = hanflat(np.ones((inarray3d.shape[0])), pctflat)
976    hanwgt2 = hanflat(np.ones((inarray3d.shape[1])), pctflat)
977    hanwgt3 = hanflat(np.ones((inarray3d.shape[2])), pctflat)
978    _a = np.outer(hanwgt1, hanwgt2)
979    _b = np.multiply.outer(_a.ravel(), hanwgt3.ravel())
980    return _b.reshape(inarray3d.shape) ** (1.0 / 3.0)
def create_3D_qc_plot( cfg, seismic, brine, oil, gas, fault_segments, title_text, camera_steps=20) -> None:
 983def create_3D_qc_plot(
 984    cfg,
 985    seismic,
 986    brine,
 987    oil,
 988    gas,
 989    fault_segments,
 990    title_text,
 991    camera_steps=20
 992) -> None:
 993    """
 994    Creates a 3D QC plot
 995    --------------------
 996
 997    Generates a 3D QC plot for aiding in QC.
 998
 999    Parameters
1000    ----------
1001    cfg : _type_
1002        _description_
1003    seismic : _type_
1004        _description_
1005    brine : _type_
1006        _description_
1007    oil : _type_
1008        _description_
1009    gas : _type_
1010        _description_
1011    fault_segments : _type_
1012        _description_
1013    title_text : _type_
1014        _description_
1015    camera_steps : int, optional
1016        _description_, by default 20
1017
1018    Returns
1019    -------
1020    None
1021    """
1022
1023    from scipy.ndimage import gaussian_filter1d, gaussian_filter
1024    from vtkplotter import addons, show, Text2D, Volume, Plotter
1025    from vtkplotter.pyplot import cornerHistogram
1026    from vtkplotter.vtkio import screenshot
1027
1028    # custom lighting for meshes
1029    ambient = 0.4
1030    diffuse = 0.1
1031    specular = 0.5
1032    specular_power = 30
1033
1034    # Brine
1035    # rescale the vertical axis to have similar height to x & y axes width
1036    v_exagg = 1.0 / (cfg.cube_shape[-1] // cfg.cube_shape[0])
1037    vol_br = Volume(brine[:, :, ::-1], spacing=(1, 1, v_exagg))
1038    vol_br = Volume(vol_br.GetMapper().GetInput())
1039    print(
1040        f" ... brine_closure amp range = {brine.min()}, {brine.mean()}, {brine.max()}"
1041    )
1042    brine_surface = (
1043        vol_br.isosurface(threshold=0.99 * brine.max())
1044        .c("blue")
1045        .lighting(
1046            ambient=ambient,
1047            diffuse=diffuse,
1048            specular=specular + 0.2,
1049            specularPower=specular_power,
1050        )
1051    )
1052    # Oil
1053    vol_oil = Volume(oil[:, :, ::-1], spacing=(1, 1, v_exagg))
1054    vol_oil = Volume(vol_oil.GetMapper().GetInput())
1055    print(f" ... oil_closure amp range = {oil.min()}, {oil.mean()}, {oil.max()}")
1056    oil_surface = (
1057        vol_oil.isosurface(threshold=0.99 * brine.max())
1058        .c("green")
1059        .lighting(
1060            ambient=ambient,
1061            diffuse=diffuse,
1062            specular=specular,
1063            specularPower=specular_power,
1064        )
1065    )
1066    # Gas
1067    vol_gas = Volume(gas[:, :, ::-1], spacing=(1, 1, v_exagg))
1068    vol_gas = Volume(vol_gas.GetMapper().GetInput())
1069    print(f" ... gas amp range = {gas.min()}, {gas.mean()}, {gas.max()}")
1070    gas_surface = (
1071        vol_gas.isosurface(threshold=0.99 * brine.max())
1072        .c("red")
1073        .lighting(
1074            ambient=ambient,
1075            diffuse=diffuse,
1076            specular=specular,
1077            specularPower=specular_power,
1078        )
1079    )
1080    # Faults
1081    try:
1082        sigma = 0.35
1083        threshold = 0.0759
1084        fault_segments = gaussian_filter(fault_segments, sigma=(sigma, sigma, 1))
1085        fault_segments = gaussian_filter1d(fault_segments, sigma=sigma, axis=2)
1086        vol_fault = Volume(fault_segments[:, :, ::-1], spacing=(1, 1, v_exagg))
1087        vol_fault = Volume(vol_fault.GetMapper().GetInput())
1088        fault_surface = (
1089            vol_fault.isosurface(threshold=threshold)
1090            .c((0.5, 0.5, 0.5))
1091            .alpha(0.05)
1092            .lighting(
1093                ambient=ambient,
1094                diffuse=diffuse,
1095                specular=specular * 1.5,
1096                specularPower=specular_power * 2,
1097            )
1098        )
1099        include_faults = True
1100    except:
1101        include_faults = False
1102
1103    # Seismic
1104    # re-order z so that big indices are deep
1105    vol = Volume(seismic[:, :, ::-1], spacing=(1, 1, v_exagg))
1106    vol.addScalarBar3D()
1107    vol = Volume(vol.GetMapper().GetInput())
1108
1109    # Create the 3D scene and save images
1110    la, ld = 0.9, 0.9  # ambient, diffuse
1111    cmaps = ["bone_r", "gist_ncar_r", "jet", "Spectral_r", "hot_r", "gist_earth_r"]
1112
1113    box = vol.box().wireframe().alpha(0)  # make an invisible box
1114
1115    vp = Plotter(
1116        axes=1,
1117        bg="white",
1118        bg2="lightblue",
1119        size=(900, 600),
1120        title=title_text,
1121        interactive=False,
1122        offscreen=True,
1123    )
1124    vp.show(box, newPlotter=True, interactive=False)
1125    # vp.showInset(vol, c=cmaps[0], pos=(1, 1), size=0.3, draggable=False)
1126
1127    # inits
1128    visibles = [None, None, None]
1129    cmap = cmaps[0]
1130    dims = vol.dimensions()
1131    i_init = 0
1132    msh = vol.xSlice(i_init).pointColors(cmap=cmap).lighting("", la, ld, 0)
1133    msh.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1134    vp.renderer.AddActor(msh)
1135    visibles[0] = msh
1136    j_init = 0
1137    msh2 = vol.ySlice(j_init).pointColors(cmap=cmap).lighting("", la, ld, 0)
1138    msh2.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1139    vp.renderer.AddActor(msh)
1140    visibles[1] = msh2
1141    k_init = 0
1142    msh3 = vol.zSlice(k_init).pointColors(cmap=cmap).lighting("", la, ld, 0, 0)
1143    msh3.addScalarBar(pos=(0.04, 0.0), horizontal=True, titleFontSize=0)
1144    vp.renderer.AddActor(msh3)
1145    visibles[2] = msh3
1146
1147    # the colormap button
1148    def buttonfunc():
1149        global cmap
1150        bu.switch()
1151        cmap = bu.status()
1152        for mesh in visibles:
1153            if mesh:
1154                mesh.pointColors(cmap=cmap)
1155        vp.renderer.RemoveActor(mesh.scalarbar)
1156        mesh.scalarbar = addons.addScalarBar(
1157            mesh, pos=(0.04, 0.0), horizontal=True, titleFontSize=0
1158        )
1159        vp.renderer.AddActor(mesh.scalarbar)
1160
1161    bu = vp.addButton(
1162        buttonfunc,
1163        pos=(0.27, 0.005),
1164        states=cmaps,
1165        c=["db"] * len(cmaps),
1166        bc=["lb"] * len(cmaps),
1167        size=14,
1168        bold=True,
1169    )
1170
1171    hist = cornerHistogram(
1172        seismic,
1173        s=0.2,
1174        bins=75,
1175        logscale=0,
1176        pos=(0.02, 0.02),
1177        c=((0.0, 0.0, 0.75)),
1178        bg=((0.1, 0.1, 0.1)),
1179        alpha=0.7,
1180    )
1181
1182    # Show the meshes and start interacting
1183    viewpoint1 = [-200.0, -200.0, 500.0]
1184    focalpoint1 = [300, 300, 150]
1185    vp.addLight(pos=viewpoint1, focalPoint=focalpoint1)
1186
1187    viewpoint2 = [-500.0, -500.0, 500.0]
1188    focalpoint2 = [150, 150, 150]
1189    vp.addLight(pos=viewpoint2, focalPoint=focalpoint2)
1190
1191    viewpoint3 = [-400.0, -400.0, 600.0]
1192    focalpoint3 = [150, 150, 150]
1193    vp.addLight(pos=viewpoint3, focalPoint=focalpoint3)
1194
1195    # Can now add any other object to the Plotter scene:
1196    text0 = Text2D(title_text, s=1.0, font="arial")
1197
1198    # set camera position
1199    start_pos = [825.0, 350.0, 600.0]
1200    start_vu = [-0.4, -0.1, 0.92]
1201    vp.camera.SetPosition(start_pos)
1202    vp.camera.SetFocalPoint([150.0, 150.0, 150.0])
1203    vp.camera.SetViewUp(start_vu)
1204    vp.camera.SetDistance(1065.168)
1205    vp.camera.SetClippingRange([517.354, 1757.322])
1206
1207    if include_faults:
1208        vp.show(
1209            msh,
1210            msh2,
1211            msh3,
1212            hist,
1213            brine_surface,
1214            oil_surface,
1215            gas_surface,
1216            fault_surface,
1217            text0,
1218            N=1,
1219            zoom=1.5,
1220        )
1221    else:
1222        vp.show(
1223            msh,
1224            msh2,
1225            msh3,
1226            hist,
1227            brine_surface,
1228            oil_surface,
1229            gas_surface,
1230            text0,
1231            N=1,
1232            zoom=1.5,
1233        )
1234
1235    # capture the scene from several vantage points
1236    delay = str(600 // camera_steps)
1237
1238    # define end camera position
1239    end_pos = [450.0, 275.0, 1150.0]
1240    end_vu = [-0.9, -0.3, 0.3]
1241
1242    n_images1 = 0
1243    camera_steps_1 = camera_steps // 3
1244    for icam in range(camera_steps):
1245        _scalar = float(icam) / (camera_steps - 1)
1246        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1247        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1248        vp.camera.SetPosition(_pos)
1249        vp.camera.SetViewUp(_vu)
1250        screenshot(
1251            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images1:03d}.png",
1252            scale=None,
1253            returnNumpy=False,
1254        )
1255        if cfg.verbose:
1256            print("\n" + str(n_images1), str(_pos), str(_vu))
1257        n_images1 += 1
1258
1259    start_pos = [450.0, 275.0, 1150.0]
1260    start_vu = [-0.9, -0.3, 0.3]
1261    end_pos = [275.0, 450.0, 1150.0]
1262    end_vu = [-0.3, -0.9, 0.3]
1263
1264    n_images2 = n_images1
1265    camera_steps_2 = camera_steps // 3
1266    for icam in range(camera_steps // 3):
1267        _scalar = float(icam) / (camera_steps - 1)
1268        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1269        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1270        vp.camera.SetPosition(_pos)
1271        vp.camera.SetViewUp(_vu)
1272        screenshot(
1273            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images2:03d}.png",
1274            scale=None,
1275            returnNumpy=False,
1276        )
1277        if cfg.verbose:
1278            print("\n" + str(n_images2), str(_pos), str(_vu))
1279        n_images2 += 1
1280
1281    start_pos = [275.0, 450.0, 1150.0]
1282    start_vu = [-0.3, -0.9, 0.3]
1283    end_vu = [-0.1, -0.4, 0.92]
1284    end_pos = [350.0, 825.0, 600.0]
1285
1286    n_images3 = n_images2
1287    camera_steps_3 = camera_steps // 3
1288    for icam in range(camera_steps // 3):
1289        _scalar = float(icam) / (camera_steps - 1)
1290        _pos = np.array(start_pos) + (np.array(end_pos) - np.array(start_pos)) * _scalar
1291        _vu = np.array(start_vu) + (np.array(end_vu) - np.array(start_vu)) * _scalar
1292        vp.camera.SetPosition(_pos)
1293        vp.camera.SetViewUp(_vu)
1294        screenshot(
1295            filename=f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images3:03d}.png",
1296            scale=None,
1297            returnNumpy=False,
1298        )
1299        if cfg.verbose:
1300            print("\n" + str(n_images3), str(_pos), str(_vu))
1301        n_images3 += 1
1302
1303    n_images4 = n_images3
1304    for icam in range(camera_steps // 6):
1305        fig_num = n_images3 + icam
1306        os.system(
1307            f"cp {cfg.temp_folder}/screenshot_{cfg.date_stamp}_{n_images3 - 1:03d}.png "
1308            + f"{cfg.temp_folder}/screenshot_{cfg.date_stamp}_{fig_num:03d}.png"
1309        )
1310        n_images4 += 1
1311
1312    # create animated gif from screenshots, convert requires ImageTools bin folder to be on $PATH
1313    output_imagename = os.path.join(
1314        cfg.work_subfolder, f"qc_image_3d_{cfg.date_stamp}.gif"
1315    )
1316    os.system(
1317        f"convert -delay {delay} {cfg.temp_folder}/screenshot_{cfg.date_stamp}_*.png -loop 0 \
1318          -dispose previous {output_imagename}"
1319    )
1320
1321    vp.close()
1322    vp.closeWindow()

Creates a 3D QC plot

Generates a 3D QC plot for aiding in QC.

Parameters
  • cfg (_type_): _description_
  • seismic (_type_): _description_
  • brine (_type_): _description_
  • oil (_type_): _description_
  • gas (_type_): _description_
  • fault_segments (_type_): _description_
  • title_text (_type_): _description_
  • camera_steps (int, optional): _description_, by default 20
Returns
  • None