datagenerator.Closures

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

The class of the Geomodel object.

This class contains all the items that make up the Geologic model.

Parameters
  • parameters (datagenerator.Parameters): Parameter object storing all model parameters.
  • depth_maps (np.ndarray): A numpy array containing the depth maps.
  • onlap_horizon_list (list): A list of the onlap horizons.
  • facies (np.ndarray): The generated facies.
Returns
  • None
Closures(parameters, faults, facies, onlap_horizon_list)
 13    def __init__(self, parameters, faults, facies, onlap_horizon_list):
 14        self.closure_dict = dict()
 15        self.cfg = parameters
 16        self.faults = faults
 17        self.facies = facies
 18        self.onlap_list = onlap_horizon_list
 19        self.top_lith_facies = None
 20        self.closure_vol_shape = self.faults.faulted_age_volume.shape
 21        self.closure_segments = self.cfg.hdf_init(
 22            "closure_segments", shape=self.closure_vol_shape
 23        )
 24        self.oil_closures = self.cfg.hdf_init(
 25            "oil_closures", shape=self.closure_vol_shape, dtype="uint8"
 26        )
 27        self.gas_closures = self.cfg.hdf_init(
 28            "gas_closures", shape=self.closure_vol_shape, dtype="uint8"
 29        )
 30        self.brine_closures = self.cfg.hdf_init(
 31            "brine_closures", shape=self.closure_vol_shape, dtype="uint8"
 32        )
 33        self.simple_closures = self.cfg.hdf_init(
 34            "simple_closures", shape=self.closure_vol_shape, dtype="uint8"
 35        )
 36        self.strat_closures = self.cfg.hdf_init(
 37            "strat_closures", shape=self.closure_vol_shape, dtype="uint8"
 38        )
 39        self.fault_closures = self.cfg.hdf_init(
 40            "fault_closures", shape=self.closure_vol_shape, dtype="uint8"
 41        )
 42        self.hc_labels = self.cfg.hdf_init(
 43            "hc_labels", shape=self.closure_vol_shape, dtype="uint8"
 44        )
 45
 46        self.all_closure_segments = self.cfg.hdf_init(
 47            "all_closure_segments", shape=self.closure_vol_shape
 48        )
 49
 50        # Class attributes added from Intersect3D
 51        self.wide_faults = self.cfg.hdf_init(
 52            "wide_faults", shape=self.closure_vol_shape
 53        )
 54        self.fat_faults = self.cfg.hdf_init("fat_faults", shape=self.closure_vol_shape)
 55        self.onlaps_upward = self.cfg.hdf_init(
 56            "onlaps_upward", shape=self.closure_vol_shape
 57        )
 58        self.onlaps_downward = self.cfg.hdf_init(
 59            "onlaps_downward", shape=self.closure_vol_shape
 60        )
 61
 62        # Faulted closures
 63        self.faulted_closures_oil = self.cfg.hdf_init(
 64            "faulted_closures_oil", shape=self.closure_vol_shape, dtype="uint8"
 65        )
 66        self.faulted_closures_gas = self.cfg.hdf_init(
 67            "faulted_closures_gas", shape=self.closure_vol_shape, dtype="uint8"
 68        )
 69        self.faulted_closures_brine = self.cfg.hdf_init(
 70            "faulted_closures_brine", shape=self.closure_vol_shape, dtype="uint8"
 71        )
 72        self.fault_closures_oil_segment_list = list()
 73        self.fault_closures_gas_segment_list = list()
 74        self.fault_closures_brine_segment_list = list()
 75        self.n_fault_closures_oil = 0
 76        self.n_fault_closures_gas = 0
 77        self.n_fault_closures_brine = 0
 78
 79        self.faulted_all_closures = self.cfg.hdf_init(
 80            "faulted_all_closures", shape=self.closure_vol_shape, dtype="uint8"
 81        )
 82        self.fault_all_closures_segment_list = list()
 83        self.n_fault_all_closures = 0
 84
 85        # Onlap closures
 86        self.onlap_closures_oil = self.cfg.hdf_init(
 87            "onlap_closures_oil", shape=self.closure_vol_shape, dtype="uint8"
 88        )
 89        self.onlap_closures_gas = self.cfg.hdf_init(
 90            "onlap_closures_gas", shape=self.closure_vol_shape, dtype="uint8"
 91        )
 92        self.onlap_closures_brine = self.cfg.hdf_init(
 93            "onlap_closures_brine", shape=self.closure_vol_shape, dtype="uint8"
 94        )
 95        self.onlap_closures_oil_segment_list = list()
 96        self.onlap_closures_gas_segment_list = list()
 97        self.onlap_closures_brine_segment_list = list()
 98        self.n_onlap_closures_oil = 0
 99        self.n_onlap_closures_gas = 0
100        self.n_onlap_closures_brine = 0
101
102        self.onlap_all_closures = self.cfg.hdf_init(
103            "onlap_all_closures", shape=self.closure_vol_shape, dtype="uint8"
104        )
105        self.onlap_all_closures_segment_list = list()
106        self.n_onlap_all_closures_oil = 0
107
108        # Simple closures
109        self.simple_closures_oil = self.cfg.hdf_init(
110            "simple_closures_oil", shape=self.closure_vol_shape, dtype="uint8"
111        )
112        self.simple_closures_gas = self.cfg.hdf_init(
113            "simple_closures_gas", shape=self.closure_vol_shape, dtype="uint8"
114        )
115        self.simple_closures_brine = self.cfg.hdf_init(
116            "simple_closures_brine", shape=self.closure_vol_shape, dtype="uint8"
117        )
118        self.simple_closures_oil_segment_list = list()
119        self.simple_closures_gas_segment_list = list()
120        self.simple_closures_brine_segment_list = list()
121        self.n_4way_closures_oil = 0
122        self.n_4way_closures_gas = 0
123        self.n_4way_closures_brine = 0
124
125        self.simple_all_closures = self.cfg.hdf_init(
126            "simple_all_closures", shape=self.closure_vol_shape, dtype="uint8"
127        )
128        self.simple_all_closures_segment_list = list()
129        self.n_4way_all_closures = 0
130
131        # False closures
132        self.false_closures_oil = self.cfg.hdf_init(
133            "false_closures_oil", shape=self.closure_vol_shape, dtype="uint8"
134        )
135        self.false_closures_gas = self.cfg.hdf_init(
136            "false_closures_gas", shape=self.closure_vol_shape, dtype="uint8"
137        )
138        self.false_closures_brine = self.cfg.hdf_init(
139            "false_closures_brine", shape=self.closure_vol_shape, dtype="uint8"
140        )
141        self.n_false_closures_oil = 0
142        self.n_false_closures_gas = 0
143        self.n_false_closures_brine = 0
144
145        self.false_all_closures = self.cfg.hdf_init(
146            "false_all_closures", shape=self.closure_vol_shape, dtype="uint8"
147        )
148        self.n_false_all_closures = 0
149
150        if self.cfg.include_salt:
151            self.salt_closures = self.cfg.hdf_init(
152                "salt_closures", shape=self.closure_vol_shape, dtype="uint8"
153            )
154            self.wide_salt = self.cfg.hdf_init(
155                "wide_salt", shape=self.closure_vol_shape
156            )
157            self.salt_closures_oil = self.cfg.hdf_init(
158                "salt_bounded_closures_oil", shape=self.closure_vol_shape, dtype="uint8"
159            )
160            self.salt_closures_gas = self.cfg.hdf_init(
161                "salt_bounded_closures_gas", shape=self.closure_vol_shape, dtype="uint8"
162            )
163            self.salt_closures_brine = self.cfg.hdf_init(
164                "salt_bounded_closures_brine",
165                shape=self.closure_vol_shape,
166                dtype="uint8",
167            )
168            self.salt_closures_oil_segment_list = list()
169            self.salt_closures_gas_segment_list = list()
170            self.salt_closures_brine_segment_list = list()
171            self.n_salt_closures_oil = 0
172            self.n_salt_closures_gas = 0
173            self.n_salt_closures_brine = 0
174
175            self.salt_all_closures = self.cfg.hdf_init(
176                "salt_bounded_all_closures", shape=self.closure_vol_shape, dtype="uint8"
177            )
178            self.salt_all_closures_segment_list = list()
179            self.n_salt_all_closures = 0

__init__

Initializer for the Geomodel class.

Parameters
  • parameters (datagenerator.Parameters): Parameter object storing all model parameters.
  • depth_maps (np.ndarray): A numpy array containing the depth maps.
  • onlap_horizon_list (list): A list of the onlap horizons.
  • facies (np.ndarray): The generated facies.
def create_closure_labels_from_depth_maps(self, depth_maps, depth_maps_infilled, max_col_height):
181    def create_closure_labels_from_depth_maps(
182        self, depth_maps, depth_maps_infilled, max_col_height
183    ):
184        if self.cfg.verbose:
185            print("\n\t... inside insertClosureLabels3D ")
186            print(
187                f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f},"
188                f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}"
189            )
190
191        # create 3D cube to hold segmentation results
192        closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32")
193
194        # create grids with grid indices
195        ii, jj = self.build_meshgrid()
196
197        # loop through horizons in 'depth_maps'
198        voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8)
199        layers_with_closure = 0
200
201        avg_sand_thickness = list()
202        avg_shale_thickness = list()
203        avg_unit_thickness = list()
204        for ihorizon in range(depth_maps.shape[2] - 1):
205            avg_unit_thickness.append(
206                np.mean(
207                    depth_maps_infilled[..., ihorizon + 1]
208                    - depth_maps_infilled[..., ihorizon]
209                )
210            )
211
212            if self.top_lith_facies[ihorizon] > 0:
213                # If facies is not shale, calculate a closure map for the layer
214                if self.cfg.verbose:
215                    print(
216                        f"\n...closure voxels computation for layer {ihorizon} in horizon list."
217                    )
218                avg_sand_thickness.append(
219                    np.mean(
220                        depth_maps_infilled[..., ihorizon + 1]
221                        - depth_maps_infilled[..., ihorizon]
222                    )
223                )
224                # compute a closure map
225                # - identical to top structure map when not in closure, 'max flooding' depth when in closure
226                # - use thicknesses converted to samples instead of ft or ms
227                # - assumes that fault intersections are inserted in input map with value of 0.
228                # - assumes that input map values represent depth (i.e., bigger values are deeper)
229                top_structure_depth_map = depth_maps[:, :, ihorizon].copy()
230                top_structure_depth_map[
231                    np.isnan(top_structure_depth_map)
232                ] = 0.0  # replace nans with 0.
233                top_structure_depth_map /= float(self.cfg.digi)
234                if self.cfg.partial_voxels:
235                    top_structure_depth_map -= (
236                        1.0  # account for voxels partially in layer
237                    )
238                base_structure_depth_map = depth_maps_infilled[
239                    :, :, ihorizon + 1
240                ].copy()
241                base_structure_depth_map[
242                    np.isnan(top_structure_depth_map)
243                ] = 0.0  # replace nans with 0.
244                base_structure_depth_map /= float(self.cfg.digi)
245                print(
246                    " ...inside create_closure_labels_from_depth_maps... ihorizon, self.top_lith_facies[ihorizon] = ",
247                    ihorizon,
248                    self.top_lith_facies[ihorizon],
249                )
250                # if there is non-zero thickness between top/base closure
251                if top_structure_depth_map.min() != top_structure_depth_map.max():
252                    max_column = max_col_height[ihorizon] / self.cfg.digi
253                    if self.cfg.verbose:
254                        print(
255                            f"   ...avg depth for layer {ihorizon}.",
256                            top_structure_depth_map.mean(),
257                        )
258                    if self.cfg.verbose:
259                        print(
260                            f"   ...maximum column height for layer {ihorizon}.",
261                            max_column,
262                        )
263
264                    if ihorizon == 27000 or ihorizon == 1000:
265                        closure_depth_map = _flood_fill(
266                            top_structure_depth_map,
267                            max_column_height=max_column,
268                            verbose=True,
269                            debug=True,
270                        )
271                    else:
272                        closure_depth_map = _flood_fill(
273                            top_structure_depth_map, max_column_height=max_column
274                        )
275                    closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[
276                        closure_depth_map == 0
277                    ]
278                    closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[
279                        closure_depth_map == 1
280                    ]
281                    closure_depth_map[
282                        closure_depth_map == 1e5
283                    ] = top_structure_depth_map[closure_depth_map == 1e5]
284                    # Select the maximum value between the top sand map and the flood-filled closure map
285                    closure_depth_map = np.max(
286                        np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1
287                    )
288                    closure_depth_map = np.min(
289                        np.dstack((closure_depth_map, base_structure_depth_map)),
290                        axis=-1,
291                    )
292                    if self.cfg.verbose:
293                        print(
294                            f"\n    ... layer {ihorizon},"
295                            f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f},"
296                            f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max"
297                            f" {closure_depth_map.min():.2f} {closure_depth_map.max()}"
298                        )
299                    closure_thickness = closure_depth_map - top_structure_depth_map
300                    closure_thickness_no_nan = closure_thickness[
301                        ~np.isnan(closure_thickness)
302                    ]
303                    max_closure = int(np.around(closure_thickness_no_nan.max(), 0))
304                    if self.cfg.verbose:
305                        print(f"    ... layer {ihorizon}, max_closure {max_closure}")
306
307                    # locate 3D zone in closure after checking that closures exist for this horizon
308                    # if False in (top_structure_depth_map == closure_depth_map):
309                    if max_closure > 0:
310                        # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map
311                        # put label in cube between top_structure_depth_map and closure_depth_map
312                        top_structure_depth_map_integer = top_structure_depth_map
313                        closure_depth_map_integer = closure_depth_map
314
315                        if self.cfg.verbose:
316                            closure_map_min = closure_depth_map_integer[
317                                closure_depth_map_integer > 0.1
318                            ].min()
319                            closure_map_max = closure_depth_map_integer[
320                                closure_depth_map_integer > 0.1
321                            ].max()
322                            print(
323                                f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, "
324                                f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()},"
325                                f" closure map min, max: {closure_map_min}, {closure_map_max}"
326                            )
327
328                        slices_with_substitution = 0
329                        print("    ... max_closure: {}".format(max_closure))
330                        for k in range(
331                            max_closure + 1
332                        ):  # add one more sample than seemingly needed for round-off
333                            # Subtract 2 from the closure cube shape since adding one later
334                            horizon_slice = (k + top_structure_depth_map).clip(
335                                0, closure_segments.shape[2] - 2
336                            )
337                            sublayer_kk = horizon_slice[
338                                horizon_slice < closure_depth_map.astype("int")
339                            ]
340                            sublayer_ii = ii[
341                                horizon_slice < closure_depth_map.astype("int")
342                            ]
343                            sublayer_jj = jj[
344                                horizon_slice < closure_depth_map.astype("int")
345                            ]
346
347                            if sublayer_ii.size > 0:
348                                slices_with_substitution += 1
349
350                                i_indices = sublayer_ii
351                                j_indices = sublayer_jj
352                                k_indices = sublayer_kk + 1
353
354                                try:
355                                    closure_segments[
356                                        i_indices, j_indices, k_indices.astype("int")
357                                    ] += 1.0
358                                    voxel_change_count[
359                                        i_indices, j_indices, k_indices.astype("int")
360                                    ] += 1
361                                except IndexError:
362                                    print("\nIndex is out of bounds.")
363                                    print(f"\tclosure_segments: {closure_segments}")
364                                    print(f"\tvoxel_change_count: {voxel_change_count}")
365                                    print(f"\ti_indices: {i_indices}")
366                                    print(f"\tj_indices: {j_indices}")
367                                    print(f"\tk_indices: {k_indices.astype('int')}")
368                                    pass
369
370                        if slices_with_substitution > 0:
371                            layers_with_closure += 1
372
373                        if self.cfg.verbose:
374                            print(
375                                "    ... finished putting closures in closures_segments for layer ...",
376                                ihorizon,
377                            )
378
379                    else:
380                        continue
381            else:
382                # Calculate shale unit thicknesses
383                avg_shale_thickness.append(
384                    np.mean(
385                        depth_maps_infilled[..., ihorizon + 1]
386                        - depth_maps_infilled[..., ihorizon]
387                    )
388                )
389
390        if len(avg_sand_thickness) == 0:
391            avg_sand_thickness = 0
392        self.cfg.write_to_logfile(
393            f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, "
394            f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, "
395            f"max: {np.max(avg_sand_thickness):.2f}"
396        )
397        self.cfg.write_to_logfile(
398            f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, "
399            f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, "
400            f"max: {np.max(avg_shale_thickness):.2f}"
401        )
402        self.cfg.write_to_logfile(
403            f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, "
404            f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, "
405            f"max: {np.max(avg_unit_thickness):.2f}"
406        )
407        self.cfg.write_to_logfile(
408            msg=None,
409            mainkey="model_parameters",
410            subkey="sand_unit_thickness_combined_mean",
411            val=np.mean(avg_sand_thickness),
412        )
413        self.cfg.write_to_logfile(
414            msg=None,
415            mainkey="model_parameters",
416            subkey="sand_unit_thickness_combined_std",
417            val=np.std(avg_sand_thickness),
418        )
419        self.cfg.write_to_logfile(
420            msg=None,
421            mainkey="model_parameters",
422            subkey="sand_unit_thickness_combined_min",
423            val=np.min(avg_sand_thickness),
424        )
425        self.cfg.write_to_logfile(
426            msg=None,
427            mainkey="model_parameters",
428            subkey="sand_unit_thickness_combined_max",
429            val=np.max(avg_sand_thickness),
430        )
431        #
432        self.cfg.write_to_logfile(
433            msg=None,
434            mainkey="model_parameters",
435            subkey="shale_unit_thickness_combined_mean",
436            val=np.mean(avg_shale_thickness),
437        )
438        self.cfg.write_to_logfile(
439            msg=None,
440            mainkey="model_parameters",
441            subkey="shale_unit_thickness_combined_std",
442            val=np.std(avg_shale_thickness),
443        )
444        self.cfg.write_to_logfile(
445            msg=None,
446            mainkey="model_parameters",
447            subkey="shale_unit_thickness_combined_min",
448            val=np.min(avg_shale_thickness),
449        )
450        self.cfg.write_to_logfile(
451            msg=None,
452            mainkey="model_parameters",
453            subkey="shale_unit_thickness_combined_max",
454            val=np.max(avg_shale_thickness),
455        )
456
457        self.cfg.write_to_logfile(
458            msg=None,
459            mainkey="model_parameters",
460            subkey="overall_unit_thickness_combined_mean",
461            val=np.mean(avg_unit_thickness),
462        )
463        self.cfg.write_to_logfile(
464            msg=None,
465            mainkey="model_parameters",
466            subkey="overall_unit_thickness_combined_std",
467            val=np.std(avg_unit_thickness),
468        )
469        self.cfg.write_to_logfile(
470            msg=None,
471            mainkey="model_parameters",
472            subkey="overall_unit_thickness_combined_min",
473            val=np.min(avg_unit_thickness),
474        )
475        self.cfg.write_to_logfile(
476            msg=None,
477            mainkey="model_parameters",
478            subkey="overall_unit_thickness_combined_max",
479            val=np.max(avg_unit_thickness),
480        )
481
482        non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0]
483        pct_non_zero = float(non_zero_pixels) / (
484            closure_segments.shape[0]
485            * closure_segments.shape[1]
486            * closure_segments.shape[2]
487        )
488        if self.cfg.verbose:
489            print(
490                "    ...closure_segments min {}, mean {}, max {}, % non-zero {}".format(
491                    closure_segments.min(),
492                    closure_segments.mean(),
493                    closure_segments.max(),
494                    pct_non_zero,
495                )
496            )
497
498        print(f"\t... layers_with_closure {layers_with_closure}")
499        print("\t... finished putting closures in closure_segments ...\n")
500
501        if self.cfg.verbose:
502            print(
503                f"\n   ...closure segments created. min: {closure_segments.min()}, "
504                f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}"
505                f" voxel count: {closure_segments[closure_segments != 0].shape}"
506            )
507
508        return closure_segments
def create_closure_labels_from_all_depth_maps(self, depth_maps, depth_maps_infilled, max_col_height):
510    def create_closure_labels_from_all_depth_maps(
511        self, depth_maps, depth_maps_infilled, max_col_height
512    ):
513        if self.cfg.verbose:
514            print("\n\t... inside insertClosureLabels3D ")
515            print(
516                f"\t... depth_maps min {depth_maps.min():.2f}, mean {depth_maps.mean():.2f},"
517                f" max {depth_maps.max():.2f}, cube_shape {self.cfg.cube_shape}"
518            )
519
520        # create 3D cube to hold segmentation results
521        closure_segments = np.zeros(self.faults.faulted_lithology.shape, "float32")
522
523        # create grids with grid indices
524        ii, jj = self.build_meshgrid()
525
526        # loop through horizons in 'depth_maps'
527        voxel_change_count = np.zeros(self.cfg.cube_shape, dtype=np.uint8)
528        layers_with_closure = 0
529
530        avg_sand_thickness = list()
531        avg_shale_thickness = list()
532        avg_unit_thickness = list()
533        for ihorizon in range(depth_maps.shape[2] - 1):
534            avg_unit_thickness.append(
535                np.mean(
536                    depth_maps_infilled[..., ihorizon + 1]
537                    - depth_maps_infilled[..., ihorizon]
538                )
539            )
540            # calculate a closure map for the layer
541            if self.cfg.verbose:
542                print(
543                    f"\n...closure voxels computation for layer {ihorizon} in horizon list."
544                )
545
546            # compute a closure map
547            # - identical to top structure map when not in closure, 'max flooding' depth when in closure
548            # - use thicknesses converted to samples instead of ft or ms
549            # - assumes that fault intersections are inserted in input map with value of 0.
550            # - assumes that input map values represent depth (i.e., bigger values are deeper)
551            top_structure_depth_map = depth_maps[:, :, ihorizon].copy()
552            top_structure_depth_map[
553                np.isnan(top_structure_depth_map)
554            ] = 0.0  # replace nans with 0.
555            top_structure_depth_map /= float(self.cfg.digi)
556            if self.cfg.partial_voxels:
557                top_structure_depth_map -= 1.0  # account for voxels partially in layer
558            base_structure_depth_map = depth_maps_infilled[:, :, ihorizon + 1].copy()
559            base_structure_depth_map[
560                np.isnan(top_structure_depth_map)
561            ] = 0.0  # replace nans with 0.
562            base_structure_depth_map /= float(self.cfg.digi)
563            print(
564                " ...inside create_closure_labels_from_depth_maps... ihorizon = ",
565                ihorizon,
566            )
567            # if there is non-zero thickness between top/base closure
568            if top_structure_depth_map.min() != top_structure_depth_map.max():
569                max_column = max_col_height[ihorizon] / self.cfg.digi
570                if self.cfg.verbose:
571                    print(
572                        f"   ...avg depth for layer {ihorizon}.",
573                        top_structure_depth_map.mean(),
574                    )
575                if self.cfg.verbose:
576                    print(
577                        f"   ...maximum column height for layer {ihorizon}.", max_column
578                    )
579
580                if ihorizon == 27000 or ihorizon == 1000:
581                    closure_depth_map = _flood_fill(
582                        top_structure_depth_map,
583                        max_column_height=max_column,
584                        verbose=True,
585                        debug=True,
586                    )
587                else:
588                    closure_depth_map = _flood_fill(
589                        top_structure_depth_map, max_column_height=max_column
590                    )
591                closure_depth_map[closure_depth_map == 0] = top_structure_depth_map[
592                    closure_depth_map == 0
593                ]
594                closure_depth_map[closure_depth_map == 1] = top_structure_depth_map[
595                    closure_depth_map == 1
596                ]
597                closure_depth_map[closure_depth_map == 1e5] = top_structure_depth_map[
598                    closure_depth_map == 1e5
599                ]
600                # Select the maximum value between the top sand map and the flood-filled closure map
601                closure_depth_map = np.max(
602                    np.dstack((closure_depth_map, top_structure_depth_map)), axis=-1
603                )
604                closure_depth_map = np.min(
605                    np.dstack((closure_depth_map, base_structure_depth_map)), axis=-1
606                )
607                if self.cfg.verbose:
608                    print(
609                        f"\n    ... layer {ihorizon},"
610                        f"\n\ttop structure map min, max {top_structure_depth_map.min():.2f},"
611                        f" {top_structure_depth_map.max():.2f}\n\tclosure_depth_map min, max"
612                        f" {closure_depth_map.min():.2f} {closure_depth_map.max()}"
613                    )
614                closure_thickness = closure_depth_map - top_structure_depth_map
615                closure_thickness_no_nan = closure_thickness[
616                    ~np.isnan(closure_thickness)
617                ]
618                max_closure = int(np.around(closure_thickness_no_nan.max(), 0))
619                if self.cfg.verbose:
620                    print(f"    ... layer {ihorizon}, max_closure {max_closure}")
621
622                # locate 3D zone in closure after checking that closures exist for this horizon
623                # if False in (top_structure_depth_map == closure_depth_map):
624                if max_closure > 0:
625                    # locate voxels anywhere in layer where top_structure_depth_map < closure_depth_map
626                    # put label in cube between top_structure_depth_map and closure_depth_map
627                    top_structure_depth_map_integer = top_structure_depth_map
628                    closure_depth_map_integer = closure_depth_map
629
630                    if self.cfg.verbose:
631                        closure_map_min = closure_depth_map_integer[
632                            closure_depth_map_integer > 0.1
633                        ].min()
634                        closure_map_max = closure_depth_map_integer[
635                            closure_depth_map_integer > 0.1
636                        ].max()
637                        print(
638                            f"\t... (2) layer: {ihorizon}, max_closure; {max_closure}, top structure map min, "
639                            f"max: {top_structure_depth_map.min()}, {top_structure_depth_map_integer.max()},"
640                            f" closure map min, max: {closure_map_min}, {closure_map_max}"
641                        )
642
643                    slices_with_substitution = 0
644                    print("    ... max_closure: {}".format(max_closure))
645                    for k in range(
646                        max_closure + 1
647                    ):  # add one more sample than seemingly needed for round-off
648                        # Subtract 2 from the closure cube shape since adding one later
649                        horizon_slice = (k + top_structure_depth_map).clip(
650                            0, closure_segments.shape[2] - 2
651                        )
652                        sublayer_kk = horizon_slice[
653                            horizon_slice < closure_depth_map.astype("int")
654                        ]
655                        sublayer_ii = ii[
656                            horizon_slice < closure_depth_map.astype("int")
657                        ]
658                        sublayer_jj = jj[
659                            horizon_slice < closure_depth_map.astype("int")
660                        ]
661
662                        if sublayer_ii.size > 0:
663                            slices_with_substitution += 1
664
665                            i_indices = sublayer_ii
666                            j_indices = sublayer_jj
667                            k_indices = sublayer_kk + 1
668
669                            try:
670                                closure_segments[
671                                    i_indices, j_indices, k_indices.astype("int")
672                                ] += 1.0
673                                voxel_change_count[
674                                    i_indices, j_indices, k_indices.astype("int")
675                                ] += 1
676                            except IndexError:
677                                print("\nIndex is out of bounds.")
678                                print(f"\tclosure_segments: {closure_segments}")
679                                print(f"\tvoxel_change_count: {voxel_change_count}")
680                                print(f"\ti_indices: {i_indices}")
681                                print(f"\tj_indices: {j_indices}")
682                                print(f"\tk_indices: {k_indices.astype('int')}")
683                                pass
684
685                    if slices_with_substitution > 0:
686                        layers_with_closure += 1
687
688                    if self.cfg.verbose:
689                        print(
690                            "    ... finished putting closures in closures_segments for layer ...",
691                            ihorizon,
692                        )
693
694                else:
695                    continue
696
697            if self.facies[ihorizon] == 1:
698                avg_sand_thickness.append(
699                    np.mean(
700                        depth_maps_infilled[..., ihorizon + 1]
701                        - depth_maps_infilled[..., ihorizon]
702                    )
703                )
704            elif self.facies[ihorizon] == 0:
705                # Calculate shale unit thicknesses
706                avg_shale_thickness.append(
707                    np.mean(
708                        depth_maps_infilled[..., ihorizon + 1]
709                        - depth_maps_infilled[..., ihorizon]
710                    )
711                )
712
713        # TODO  handle case where avg_sand_thickness is zero-size array
714        try:
715            self.cfg.write_to_logfile(
716                f"Sand Unit Thickness (m): mean: {np.mean(avg_sand_thickness):.2f}, "
717                f"std: {np.std(avg_sand_thickness):.2f}, min: {np.nanmin(avg_sand_thickness):.2f}, "
718                f"max: {np.max(avg_sand_thickness):.2f}"
719            )
720        except:
721            print("No sands in model")
722        self.cfg.write_to_logfile(
723            f"Shale Unit Thickness (m): mean: {np.mean(avg_shale_thickness):.2f}, "
724            f"std: {np.std(avg_shale_thickness):.2f}, min: {np.min(avg_shale_thickness):.2f}, "
725            f"max: {np.max(avg_shale_thickness):.2f}"
726        )
727        self.cfg.write_to_logfile(
728            f"Overall Unit Thickness (m): mean: {np.mean(avg_unit_thickness):.2f}, "
729            f"std: {np.std(avg_unit_thickness):.2f}, min: {np.min(avg_unit_thickness):.2f}, "
730            f"max: {np.max(avg_unit_thickness):.2f}"
731        )
732
733        self.cfg.write_to_logfile(
734            msg=None,
735            mainkey="model_parameters",
736            subkey="sand_unit_thickness_mean",
737            val=np.mean(avg_sand_thickness),
738        )
739        self.cfg.write_to_logfile(
740            msg=None,
741            mainkey="model_parameters",
742            subkey="sand_unit_thickness_std",
743            val=np.std(avg_sand_thickness),
744        )
745        self.cfg.write_to_logfile(
746            msg=None,
747            mainkey="model_parameters",
748            subkey="sand_unit_thickness_min",
749            val=np.min(avg_sand_thickness),
750        )
751        self.cfg.write_to_logfile(
752            msg=None,
753            mainkey="model_parameters",
754            subkey="sand_unit_thickness_max",
755            val=np.max(avg_sand_thickness),
756        )
757        #
758        self.cfg.write_to_logfile(
759            msg=None,
760            mainkey="model_parameters",
761            subkey="shale_unit_thickness_mean",
762            val=np.mean(avg_shale_thickness),
763        )
764        self.cfg.write_to_logfile(
765            msg=None,
766            mainkey="model_parameters",
767            subkey="shale_unit_thickness_std",
768            val=np.std(avg_shale_thickness),
769        )
770        self.cfg.write_to_logfile(
771            msg=None,
772            mainkey="model_parameters",
773            subkey="shale_unit_thickness_min",
774            val=np.min(avg_shale_thickness),
775        )
776        self.cfg.write_to_logfile(
777            msg=None,
778            mainkey="model_parameters",
779            subkey="shale_unit_thickness_max",
780            val=np.max(avg_shale_thickness),
781        )
782
783        self.cfg.write_to_logfile(
784            msg=None,
785            mainkey="model_parameters",
786            subkey="overall_unit_thickness_mean",
787            val=np.mean(avg_unit_thickness),
788        )
789        self.cfg.write_to_logfile(
790            msg=None,
791            mainkey="model_parameters",
792            subkey="overall_unit_thickness_std",
793            val=np.std(avg_unit_thickness),
794        )
795        self.cfg.write_to_logfile(
796            msg=None,
797            mainkey="model_parameters",
798            subkey="overall_unit_thickness_min",
799            val=np.min(avg_unit_thickness),
800        )
801        self.cfg.write_to_logfile(
802            msg=None,
803            mainkey="model_parameters",
804            subkey="overall_unit_thickness_max",
805            val=np.max(avg_unit_thickness),
806        )
807
808        non_zero_pixels = closure_segments[closure_segments != 0.0].shape[0]
809        pct_non_zero = float(non_zero_pixels) / (
810            closure_segments.shape[0]
811            * closure_segments.shape[1]
812            * closure_segments.shape[2]
813        )
814        if self.cfg.verbose:
815            print(
816                "    ...closure_segments min {}, mean {}, max {}, % non-zero {}".format(
817                    closure_segments.min(),
818                    closure_segments.mean(),
819                    closure_segments.max(),
820                    pct_non_zero,
821                )
822            )
823
824        print(f"\t... layers_with_closure {layers_with_closure}")
825        print("\t... finished putting closures in closure_segments ...\n")
826
827        if self.cfg.verbose:
828            print(
829                f"\n   ...closure segments created. min: {closure_segments.min()}, "
830                f"mean: {closure_segments.mean():.2f}, max: {closure_segments.max()}"
831                f" voxel count: {closure_segments[closure_segments != 0].shape}"
832            )
833
834        return closure_segments
def find_top_lith_horizons(self):
836    def find_top_lith_horizons(self):
837        """
838        Find horizons which are the top of layers where the lithology changes
839
840        Combine layers of the same lithology and retain the top of these new layers for closure calculations.
841        """
842        top_lith_indices = list(np.array(self.onlap_list) - 1)
843        for i, _ in enumerate(self.facies[:-1]):
844            if i == 0:
845                continue
846            print(
847                f"i: {i}, sand_layer_label[i-1]: {self.facies[i - 1]},"
848                f" sand_layer_label[i]: {self.facies[i]}"
849            )
850            if self.facies[i] != self.facies[i - 1]:
851                top_lith_indices.append(i)
852                if self.cfg.verbose:
853                    print(
854                        "  ... layer lith different than layer above it. i = {}".format(
855                            i
856                        )
857                    )
858        top_lith_indices.sort()
859        if self.cfg.verbose:
860            print(
861                "\n   ...layers selected for closure computations...\n",
862                top_lith_indices,
863            )
864        self.top_lith_indices = np.array(top_lith_indices)
865        self.top_lith_facies = self.facies[top_lith_indices]
866
867        # return top_lith_indices

Find horizons which are the top of layers where the lithology changes

Combine layers of the same lithology and retain the top of these new layers for closure calculations.

def create_closures(self):
 869    def create_closures(self):
 870        if self.cfg.verbose:
 871            print("\n\n ... create 3D labels for closure")
 872
 873        # Convert nan to 0's
 874        old_depth_maps = np.nan_to_num(self.faults.faulted_depth_maps[:], copy=True)
 875        old_depth_maps_gaps = np.nan_to_num(
 876            self.faults.faulted_depth_maps_gaps[:], copy=True
 877        )
 878
 879        # Convert from samples to units
 880        old_depth_maps_gaps = self.convert_map_from_samples_to_units(
 881            old_depth_maps_gaps
 882        )
 883        old_depth_maps = self.convert_map_from_samples_to_units(old_depth_maps)
 884
 885        # keep only horizons corresponding to top of layers where lithology changes
 886        self.find_top_lith_horizons()
 887        all_lith_indices = np.arange(old_depth_maps.shape[-1])
 888        import sys
 889
 890        print("All lith indices (last, then all):", self.facies[-1], all_lith_indices)
 891        sys.stdout.flush()
 892
 893        depth_maps_gaps_top_lith = old_depth_maps_gaps[
 894            :, :, self.top_lith_indices
 895        ].copy()
 896        depth_maps_gaps_all_lith = old_depth_maps_gaps[:, :, all_lith_indices].copy()
 897        depth_maps_top_lith = old_depth_maps[:, :, self.top_lith_indices].copy()
 898        depth_maps_all_lith = old_depth_maps[:, :, all_lith_indices].copy()
 899        max_column_heights = variable_max_column_height(
 900            self.top_lith_indices,
 901            self.faults.faulted_depth_maps_gaps.shape[-1],
 902            self.cfg.max_column_height[0],
 903            self.cfg.max_column_height[1],
 904        )
 905        all_max_column_heights = variable_max_column_height(
 906            all_lith_indices,
 907            self.faults.faulted_depth_maps_gaps.shape[-1],
 908            self.cfg.max_column_height[0],
 909            self.cfg.max_column_height[1],
 910        )
 911
 912        if self.cfg.verbose:
 913            print("\n   ...facies for closure computations...\n", self.top_lith_facies)
 914            print(
 915                "\n   ...max column heights for closure computations...\n",
 916                max_column_heights,
 917            )
 918
 919        self.closure_segments[:] = self.create_closure_labels_from_depth_maps(
 920            depth_maps_gaps_top_lith, depth_maps_top_lith, max_column_heights
 921        )
 922
 923        self.all_closure_segments[:] = self.create_closure_labels_from_all_depth_maps(
 924            depth_maps_gaps_all_lith, depth_maps_all_lith, all_max_column_heights
 925        )
 926
 927        if self.cfg.verbose:
 928            print(
 929                "     ...+++... number of nan's in depth_maps_gaps before insertClosureLabels3D ...+++... {}".format(
 930                    old_depth_maps_gaps[np.isnan(old_depth_maps_gaps)].shape
 931                )
 932            )
 933            print(
 934                "     ...+++... number of nan's in depth_maps_gaps after insertClosureLabels3D ...+++... {}".format(
 935                    self.faults.faulted_depth_maps_gaps[
 936                        np.isnan(self.faults.faulted_depth_maps_gaps)
 937                    ].shape
 938                )
 939            )
 940            print(
 941                "     ...+++... number of nan's in depth_maps after insertClosureLabels3D ...+++... {}".format(
 942                    self.faults.faulted_depth_maps[
 943                        np.isnan(self.faults.faulted_depth_maps)
 944                    ].shape
 945                )
 946            )
 947            _closure_segments = self.closure_segments[:]
 948            print(
 949                "     ...+++... number of closure voxels in self.closure_segments ...+++... {}".format(
 950                    _closure_segments[_closure_segments > 0.0].shape
 951                )
 952            )
 953            del _closure_segments
 954
 955        labels_clean, self.closure_segments[:] = self.segment_closures(
 956            self.closure_segments[:], remove_shale=True
 957        )
 958        label_values, labels_clean = self.parse_label_values_and_counts(labels_clean)
 959
 960        labels_clean_all, self.all_closure_segments[:] = self.segment_closures(
 961            self.all_closure_segments[:], remove_shale=False
 962        )
 963        label_values_all, labels_clean_all = self.parse_label_values_and_counts(
 964            labels_clean_all
 965        )
 966        self.write_cube_to_disk(self.all_closure_segments[:], "all_closure_segments")
 967
 968        # Assign fluid types
 969        (
 970            self.oil_closures[:],
 971            self.gas_closures[:],
 972            self.brine_closures[:],
 973        ) = self.assign_fluid_types(label_values, labels_clean)
 974        all_closures_final = (labels_clean_all != 0).astype("uint8")
 975
 976        # Identify closures by type (simple, faulted, onlap or salt bounded)
 977        self.find_faulted_closures(label_values, labels_clean)
 978        self.find_onlap_closures(label_values, labels_clean)
 979        self.find_simple_closures(label_values, labels_clean)
 980        self.find_false_closures(label_values, labels_clean)
 981
 982        self.find_faulted_all_closures(label_values_all, labels_clean_all)
 983        self.find_onlap_all_closures(label_values_all, labels_clean_all)
 984        self.find_simple_all_closures(label_values_all, labels_clean_all)
 985        self.find_false_all_closures(label_values_all, labels_clean_all)
 986
 987        if self.cfg.include_salt:
 988            self.find_salt_bounded_closures(label_values, labels_clean)
 989            self.find_salt_bounded_all_closures(label_values_all, labels_clean_all)
 990
 991        # Remove false closures from oil & gas closure cubes
 992        if self.n_false_closures_oil > 0:
 993            print(f"Removing {self.n_false_closures_oil} false oil closures")
 994            self.oil_closures[self.false_closures_oil == 1] = 0.0
 995        if self.n_false_closures_gas > 0:
 996            print(f"Removing {self.n_false_closures_gas} false gas closures")
 997            self.gas_closures[self.false_closures_gas == 1] = 0.0
 998
 999        # Remove false closures from allclosure cube
1000        if self.n_false_all_closures > 0:
1001            print(f"Removing {self.n_false_all_closures} false all closures")
1002            self.all_closure_segments[self.false_all_closures == 1] = 0.0
1003
1004        # Create a closure cube with voxel count as labels, and include closure type in decimal
1005        # e.g. simple closure of size 5000 = 5000.1
1006        #      faulted closure of size 5000 = 5000.2
1007        #      onlap closure of size 5000 = 5000.3
1008        #      salt-bounded closure of size 5000 = 5000.4
1009        hc_closure_codes = np.zeros_like(self.gas_closures, dtype="float32")
1010
1011        # AZ: COULD RUN THESE CLOSURE SIZE FILTERS ON ALL_CLOSURES, IF DESIRED
1012
1013        if "simple" in self.cfg.closure_types:
1014            print("Filtering 4 Way Closures")
1015            (
1016                self.simple_closures_oil[:],
1017                self.n_4way_closures_oil,
1018            ) = self.closure_size_filter(
1019                self.simple_closures_oil[:],
1020                self.cfg.closure_min_voxels_simple,
1021                self.n_4way_closures_oil,
1022            )
1023            (
1024                self.simple_closures_gas[:],
1025                self.n_4way_closures_gas,
1026            ) = self.closure_size_filter(
1027                self.simple_closures_gas[:],
1028                self.cfg.closure_min_voxels_simple,
1029                self.n_4way_closures_gas,
1030            )
1031
1032            # Add simple closures to closure code cube
1033            hc_closures = (
1034                self.simple_closures_oil[:] + self.simple_closures_gas[:]
1035            ).astype("float32")
1036            labels, num = measure.label(
1037                hc_closures, connectivity=2, background=0, return_num=True
1038            )
1039            hc_closure_codes = self.parse_closure_codes(
1040                hc_closure_codes, labels, num, code=0.1
1041            )
1042        else:  # if closure type not in config, set HC closures to 0
1043            self.simple_closures_oil[:] *= 0
1044            self.simple_closures_gas[:] *= 0
1045            self.simple_all_closures[:] *= 0
1046
1047        self.oil_closures[self.simple_closures_oil[:] > 0.0] = 1.0
1048        self.oil_closures[self.simple_closures_oil[:] < 0.0] = 0.0
1049        self.gas_closures[self.simple_closures_gas[:] > 0.0] = 1.0
1050        self.gas_closures[self.simple_closures_gas[:] < 0.0] = 0.0
1051
1052        all_closures_final[self.simple_all_closures[:] > 0.0] = 1.0
1053        all_closures_final[self.simple_all_closures[:] < 0.0] = 0.0
1054
1055        if "faulted" in self.cfg.closure_types:
1056            print("Filtering 4 Way Closures")
1057            # Grow the faulted closures to the fault planes
1058            self.faulted_closures_oil[:] = self.grow_to_fault2(
1059                self.faulted_closures_oil[:]
1060            )
1061            self.faulted_closures_gas[:] = self.grow_to_fault2(
1062                self.faulted_closures_gas[:]
1063            )
1064
1065            (
1066                self.faulted_closures_oil[:],
1067                self.n_fault_closures_oil,
1068            ) = self.closure_size_filter(
1069                self.faulted_closures_oil[:],
1070                self.cfg.closure_min_voxels_faulted,
1071                self.n_fault_closures_oil,
1072            )
1073            (
1074                self.faulted_closures_gas[:],
1075                self.n_fault_closures_gas,
1076            ) = self.closure_size_filter(
1077                self.faulted_closures_gas[:],
1078                self.cfg.closure_min_voxels_faulted,
1079                self.n_fault_closures_gas,
1080            )
1081
1082            self.faulted_all_closures[:] = self.grow_to_fault2(
1083                self.faulted_all_closures[:],
1084                grow_only_sand_closures=False,
1085                remove_small_closures=False,
1086            )
1087
1088            # Add faulted closures to closure code cube
1089            hc_closures = self.faulted_closures_oil[:] + self.faulted_closures_gas[:]
1090            labels, num = measure.label(
1091                hc_closures, connectivity=2, background=0, return_num=True
1092            )
1093            hc_closure_codes = self.parse_closure_codes(
1094                hc_closure_codes, labels, num, code=0.2
1095            )
1096        else:  # if closure type not in config, set HC closures to 0
1097            self.faulted_closures_oil[:] *= 0
1098            self.faulted_closures_gas[:] *= 0
1099            self.faulted_all_closures[:] *= 0
1100
1101        self.oil_closures[self.faulted_closures_oil[:] > 0.0] = 1.0
1102        self.oil_closures[self.faulted_closures_oil[:] < 0.0] = 0.0
1103        self.gas_closures[self.faulted_closures_gas[:] > 0.0] = 1.0
1104        self.gas_closures[self.faulted_closures_gas[:] < 0.0] = 0.0
1105
1106        all_closures_final[self.faulted_all_closures[:] > 0.0] = 1.0
1107        all_closures_final[self.faulted_all_closures[:] < 0.0] = 0.0
1108
1109        if "onlap" in self.cfg.closure_types:
1110            print("Filtering Onlap Closures")
1111            (
1112                self.onlap_closures_oil[:],
1113                self.n_onlap_closures_oil,
1114            ) = self.closure_size_filter(
1115                self.onlap_closures_oil[:],
1116                self.cfg.closure_min_voxels_onlap,
1117                self.n_onlap_closures_oil,
1118            )
1119            (
1120                self.onlap_closures_gas[:],
1121                self.n_onlap_closures_gas,
1122            ) = self.closure_size_filter(
1123                self.onlap_closures_gas[:],
1124                self.cfg.closure_min_voxels_onlap,
1125                self.n_onlap_closures_gas,
1126            )
1127
1128            # Add faulted closures to closure code cube
1129            hc_closures = self.onlap_closures_oil[:] + self.onlap_closures_gas[:]
1130            labels, num = measure.label(
1131                hc_closures, connectivity=2, background=0, return_num=True
1132            )
1133            hc_closure_codes = self.parse_closure_codes(
1134                hc_closure_codes, labels, num, code=0.3
1135            )
1136            # labels = labels.astype('float32')
1137            # if num > 0:
1138            #     for x in range(1, num + 1):
1139            #         y = 0.3 + labels[labels == x].size
1140            #         labels[labels == x] = y
1141            #     hc_closure_codes += labels
1142        else:  # if closure type not in config, set HC closures to 0
1143            self.onlap_closures_oil[:] *= 0
1144            self.onlap_closures_gas[:] *= 0
1145            self.onlap_all_closures[:] *= 0
1146
1147        self.oil_closures[self.onlap_closures_oil[:] > 0.0] = 1.0
1148        self.oil_closures[self.onlap_closures_oil[:] < 0.0] = 0.0
1149        self.gas_closures[self.onlap_closures_gas[:] > 0.0] = 1.0
1150        self.gas_closures[self.onlap_closures_gas[:] < 0.0] = 0.0
1151        all_closures_final[self.onlap_all_closures[:] > 0.0] = 1.0
1152        all_closures_final[self.onlap_all_closures[:] < 0.0] = 0.0
1153
1154        if self.cfg.include_salt:
1155            # Grow the salt-bounded closures to the salt body
1156            salt_closures_oil_grown = np.zeros_like(self.salt_closures_oil[:])
1157            salt_closures_gas_grown = np.zeros_like(self.salt_closures_gas[:])
1158
1159            if np.max(self.salt_closures_oil[:]) > 0.0:
1160                self.write_cube_to_disk(
1161                    self.salt_closures_oil[:], "salt_closures_oil_initial"
1162                )
1163                print(
1164                    f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}"
1165                )
1166                salt_closures_oil_grown = self.grow_to_salt(self.salt_closures_oil[:])
1167                self.salt_closures_oil[:] = salt_closures_oil_grown
1168                print(
1169                    f"Salt-bounded Oil Closure voxel count: {self.salt_closures_oil[:][self.salt_closures_oil[:] > 0].size}"
1170                )
1171            if np.max(self.salt_closures_gas[:]) > 0.0:
1172                self.write_cube_to_disk(
1173                    self.salt_closures_gas[:], "salt_closures_gas_initial"
1174                )
1175                print(
1176                    f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 0].size}"
1177                )
1178                salt_closures_gas_grown = self.grow_to_salt(self.salt_closures_gas[:])
1179                self.salt_closures_gas[:] = salt_closures_gas_grown
1180                print(
1181                    f"Salt-bounded Gas Closure voxel count: {self.salt_closures_gas[:][self.salt_closures_gas[:] > 1].size}"
1182                )
1183            if np.max(self.salt_all_closures[:]) > 0.0:
1184                self.write_cube_to_disk(
1185                    self.salt_all_closures[:], "salt_all_closures_initial"
1186                )  # maybe remove later
1187                print(
1188                    f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 0].size}"
1189                )
1190                salt_all_closures_grown = self.grow_to_salt(self.salt_all_closures[:])
1191                self.salt_all_closures[:] = salt_all_closures_grown
1192                print(
1193                    f"Salt-bounded All Closure voxel count: {self.salt_all_closures[:][self.salt_all_closures[:] > 1].size}"
1194                )
1195            else:
1196                salt_all_closures_grown = np.zeros_like(self.salt_all_closures)
1197
1198            if np.max(self.salt_closures_oil[:]) > 0.0:
1199                self.write_cube_to_disk(
1200                    self.salt_closures_oil[:], "salt_closures_oil_grown"
1201                )
1202            if np.max(self.salt_closures_gas[:]) > 0.0:
1203                self.write_cube_to_disk(
1204                    self.salt_closures_gas[:], "salt_closures_gas_grown"
1205                )
1206            if np.max(self.salt_all_closures[:]) > 0.0:
1207                self.write_cube_to_disk(
1208                    self.salt_all_closures[:], "salt_all_closures_grown"
1209                )  # maybe remove later
1210
1211            (
1212                self.salt_closures_oil[:],
1213                self.n_salt_closures_oil,
1214            ) = self.closure_size_filter(
1215                self.salt_closures_oil[:],
1216                self.cfg.closure_min_voxels,
1217                self.n_salt_closures_oil,
1218            )
1219            (
1220                self.salt_closures_gas[:],
1221                self.n_salt_closures_gas,
1222            ) = self.closure_size_filter(
1223                self.salt_closures_gas[:],
1224                self.cfg.closure_min_voxels,
1225                self.n_salt_closures_gas,
1226            )
1227
1228            # Append salt-bounded closures to main closure cubes for oil and gas
1229            if np.max(salt_closures_oil_grown) > 0.0:
1230                self.oil_closures[salt_closures_oil_grown > 0.0] = 1.0
1231                self.oil_closures[salt_closures_oil_grown < 0.0] = 0.0
1232            if np.max(salt_closures_gas_grown) > 0.0:
1233                self.gas_closures[salt_closures_gas_grown > 0.0] = 1.0
1234                self.gas_closures[salt_closures_gas_grown < 0.0] = 0.0
1235            if np.max(salt_all_closures_grown) > 0.0:
1236                all_closures_final[salt_all_closures_grown > 0.0] = 1.0
1237                all_closures_final[salt_all_closures_grown < 0.0] = 0.0
1238
1239            # Add faulted closures to closure code cube
1240            hc_closures = self.salt_closures_oil[:] + self.salt_closures_gas[:]
1241            labels, num = measure.label(
1242                hc_closures, connectivity=2, background=0, return_num=True
1243            )
1244            hc_closure_codes = self.parse_closure_codes(
1245                hc_closure_codes, labels, num, code=0.4
1246            )
1247
1248        # Write hc_closure_codes to disk
1249        self.write_cube_to_disk(hc_closure_codes, "closure_segments_hc_voxelcount")
1250
1251        # Create closure volumes by type
1252        if self.simple_closures[:] is None:
1253            self.simple_closures[:] = self.simple_closures_oil[:].astype("uint8")
1254        else:
1255            self.simple_closures[:] += self.simple_closures_oil[:].astype("uint8")
1256        self.simple_closures[:] += self.simple_closures_gas[:].astype("uint8")
1257        self.simple_closures[:] += self.simple_closures_brine[:].astype("uint8")
1258        # Onlap closures
1259        if self.strat_closures is None:
1260            self.strat_closures[:] = self.onlap_closures_oil[:].astype("uint8")
1261        else:
1262            self.strat_closures[:] += self.onlap_closures_oil[:].astype("uint8")
1263        self.strat_closures[:] += self.onlap_closures_gas[:].astype("uint8")
1264        self.strat_closures[:] += self.onlap_closures_brine[:].astype("uint8")
1265        # Fault closures
1266        if self.fault_closures is None:
1267            self.fault_closures[:] = self.faulted_closures_oil[:].astype("uint8")
1268        else:
1269            self.fault_closures[:] += self.faulted_closures_oil[:].astype("uint8")
1270        self.fault_closures[:] += self.faulted_closures_gas[:].astype("uint8")
1271        self.fault_closures[:] += self.faulted_closures_brine[:].astype("uint8")
1272
1273        # Salt-bounded closures
1274        if self.cfg.include_salt:
1275            if self.salt_closures is None:
1276                self.salt_closures[:] = self.salt_closures_oil[:].astype("uint8")
1277            else:
1278                self.salt_closures[:] += self.salt_closures_oil[:].astype("uint8")
1279            self.salt_closures[:] += self.salt_closures_gas[:].astype("uint8")
1280
1281        # Convert closure cubes from int16 to uint8 for writing to disk
1282        self.closure_segments[:] = self.closure_segments[:].astype("uint8")
1283
1284        # add any oil/gas/brine closures into all_closures_final in case missed
1285        all_closures_final[:][self.oil_closures[:] > 0] = 1
1286        all_closures_final[:][self.gas_closures[:] > 0] = 1
1287        all_closures_final[:][self.gas_closures[:] > 0] = 1
1288        # Write all_closures_final to disk
1289        self.write_cube_to_disk(all_closures_final.astype("uint8"), "trap_label")
1290
1291        # add any oil/gas/brine closures into reservoir in case missed
1292        self.faults.reservoir[:][self.oil_closures[:] > 0] = 1
1293        self.faults.reservoir[:][self.gas_closures[:] > 0] = 1
1294        self.faults.reservoir[:][self.brine_closures[:] > 0] = 1
1295        # write reservoir_label to disk
1296        self.write_cube_to_disk(
1297            self.faults.reservoir[:].astype("uint8"), "reservoir_label"
1298        )
1299
1300        if self.cfg.qc_plots:
1301            from datagenerator.util import plot_xsection
1302            from datagenerator.util import find_line_with_most_voxels
1303
1304            # visualize closures QC
1305            inline_index_cl = find_line_with_most_voxels(
1306                self.closure_segments, 0.5, self.cfg
1307            )
1308            plot_xsection(
1309                volume=labels_clean,
1310                maps=self.faults.faulted_depth_maps_gaps,
1311                line_num=inline_index_cl,
1312                title="Example Trav through 3D model\nclosures after faulting",
1313                png_name="QC_plot__AfterFaulting_closure_segments.png",
1314                cmap="gist_ncar_r",
1315                cfg=self.cfg,
1316            )
def closure_size_filter(self, closure_type, threshold, count):
1318    def closure_size_filter(self, closure_type, threshold, count):
1319        labels, num = measure.label(
1320            closure_type, connectivity=2, background=0, return_num=True
1321        )
1322        if (
1323            num > 0
1324        ):  # TODO add whether smallest closure is below threshold constraint too
1325            s = [labels[labels == x].size for x in range(1, 1 + np.max(labels))]
1326            labels = morphology.remove_small_objects(labels, threshold, connectivity=2)
1327            t = [labels[labels == x].size for x in range(1, 1 + np.max(labels))]
1328            print(
1329                f"Closure sizes before filter: {s}\nThreshold: {threshold}\n"
1330                f"Closure sizes after filter: {t}"
1331            )
1332            count = len(t)
1333        return labels, count
def closure_type_info_for_log(self):
1335    def closure_type_info_for_log(self):
1336        fluid_types = ["oil", "gas", "brine"]
1337        if "faulted" in self.cfg.closure_types:
1338            # Faulted closures
1339            for name, fluid, num in zip(
1340                fluid_types,
1341                [
1342                    self.faulted_closures_oil[:],
1343                    self.faulted_closures_gas[:],
1344                    self.faulted_closures_brine[:],
1345                ],
1346                [
1347                    self.n_fault_closures_oil,
1348                    self.n_fault_closures_gas,
1349                    self.n_fault_closures_brine,
1350                ],
1351            ):
1352                n_voxels = fluid[fluid[:] > 0.0].size
1353                msg = f"n_fault_closures_{name}: {num:03d}\n"
1354                msg += f"n_voxels_fault_closures_{name}: {n_voxels:08d}\n"
1355                print(msg)
1356                self.cfg.write_to_logfile(msg)
1357                self.cfg.write_to_logfile(
1358                    msg=None,
1359                    mainkey="model_parameters",
1360                    subkey=f"n_fault_closures_{name}",
1361                    val=num,
1362                )
1363                self.cfg.write_to_logfile(
1364                    msg=None,
1365                    mainkey="model_parameters",
1366                    subkey=f"n_voxels_fault_closures_{name}",
1367                    val=n_voxels,
1368                )
1369                closure_statistics = self.calculate_closure_statistics(
1370                    fluid, f"Faulted {name.capitalize()}"
1371                )
1372                if closure_statistics:
1373                    print(closure_statistics)
1374                    self.cfg.write_to_logfile(closure_statistics)
1375
1376        if "onlap" in self.cfg.closure_types:
1377            # Onlap Closures
1378            for name, fluid, num in zip(
1379                fluid_types,
1380                [
1381                    self.onlap_closures_oil[:],
1382                    self.onlap_closures_gas[:],
1383                    self.onlap_closures_brine[:],
1384                ],
1385                [
1386                    self.n_onlap_closures_oil,
1387                    self.n_onlap_closures_gas,
1388                    self.n_onlap_closures_brine,
1389                ],
1390            ):
1391                n_voxels = fluid[fluid[:] > 0.0].size
1392                msg = f"n_onlap_closures_{name}: {num:03d}\n"
1393                msg += f"n_voxels_onlap_closures_{name}: {n_voxels:08d}\n"
1394                print(msg)
1395                self.cfg.write_to_logfile(msg)
1396                self.cfg.write_to_logfile(
1397                    msg=None,
1398                    mainkey="model_parameters",
1399                    subkey=f"n_onlap_closures_{name}",
1400                    val=num,
1401                )
1402                self.cfg.write_to_logfile(
1403                    msg=None,
1404                    mainkey="model_parameters",
1405                    subkey=f"n_voxels_onlap_closures_{name}",
1406                    val=n_voxels,
1407                )
1408                closure_statistics = self.calculate_closure_statistics(
1409                    fluid, f"Onlap {name.capitalize()}"
1410                )
1411                if closure_statistics:
1412                    print(closure_statistics)
1413                    self.cfg.write_to_logfile(closure_statistics)
1414
1415        if "simple" in self.cfg.closure_types:
1416            # Simple Closures
1417            for name, fluid, num in zip(
1418                fluid_types,
1419                [
1420                    self.simple_closures_oil[:],
1421                    self.simple_closures_gas[:],
1422                    self.simple_closures_brine[:],
1423                ],
1424                [
1425                    self.n_4way_closures_oil,
1426                    self.n_4way_closures_gas,
1427                    self.n_4way_closures_brine,
1428                ],
1429            ):
1430                n_voxels = fluid[fluid[:] > 0.0].size
1431                msg = f"n_4way_closures_{name}: {num:03d}\n"
1432                msg += f"n_voxels_4way_closures_{name}: {n_voxels:08d}\n"
1433                print(msg)
1434                self.cfg.write_to_logfile(msg)
1435                self.cfg.write_to_logfile(
1436                    msg=None,
1437                    mainkey="model_parameters",
1438                    subkey=f"n_4way_closures_{name}",
1439                    val=num,
1440                )
1441                self.cfg.write_to_logfile(
1442                    msg=None,
1443                    mainkey="model_parameters",
1444                    subkey=f"n_voxels_4way_closures_{name}",
1445                    val=n_voxels,
1446                )
1447                closure_statistics = self.calculate_closure_statistics(
1448                    fluid, f"4-Way {name.capitalize()}"
1449                )
1450                if closure_statistics:
1451                    print(closure_statistics)
1452                    self.cfg.write_to_logfile(closure_statistics)
1453
1454        if self.cfg.include_salt:
1455            # Salt-Bounded Closures
1456            for name, fluid, num in zip(
1457                fluid_types,
1458                [
1459                    self.salt_closures_oil[:],
1460                    self.salt_closures_gas[:],
1461                    self.salt_closures_brine[:],
1462                ],
1463                [
1464                    self.n_salt_closures_oil,
1465                    self.n_salt_closures_gas,
1466                    self.n_salt_closures_brine,
1467                ],
1468            ):
1469                n_voxels = fluid[fluid[:] > 0.0].size
1470                msg = f"n_salt_closures_{name}: {num:03d}\n"
1471                msg += f"n_voxels_salt_closures_{name}: {n_voxels:08d}\n"
1472                print(msg)
1473                self.cfg.write_to_logfile(msg)
1474                self.cfg.write_to_logfile(
1475                    msg=None,
1476                    mainkey="model_parameters",
1477                    subkey=f"n_salt_closures_{name}",
1478                    val=num,
1479                )
1480                self.cfg.write_to_logfile(
1481                    msg=None,
1482                    mainkey="model_parameters",
1483                    subkey=f"n_voxels_salt_closures_{name}",
1484                    val=n_voxels,
1485                )
1486                closure_statistics = self.calculate_closure_statistics(
1487                    fluid, f"Salt {name.capitalize()}"
1488                )
1489                if closure_statistics:
1490                    print(closure_statistics)
1491                    self.cfg.write_to_logfile(closure_statistics)
def get_voxel_counts(self, closures):
1493    def get_voxel_counts(self, closures):
1494        next_label = 0
1495        label_values = [0]
1496        label_counts = [closures[closures == 0].size]
1497        for i in range(closures.max() + 1):
1498            try:
1499                next_label = closures[closures > next_label].min()
1500            except (TypeError, ValueError):
1501                break
1502            label_values.append(next_label)
1503            label_counts.append(closures[closures == next_label].size)
1504            print(
1505                f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}"
1506            )
1507
1508        print(
1509            f'{72 * "*"}\n\tNum Closures: {len(label_counts) - 1}\n\tVoxel counts\n{label_counts[1:]}\n{72 * "*"}'
1510        )
1511        for vox_count in label_counts:
1512            if vox_count < self.cfg.closure_min_voxels:
1513                print(f"voxel_count: {vox_count}")
def populate_closure_dict(self, labels, fluid, seismic_nmf=None):
1515    def populate_closure_dict(self, labels, fluid, seismic_nmf=None):
1516        clist = []
1517        max_num = np.max(labels)
1518        if seismic_nmf is not None:
1519            # calculate ai_gi
1520            ai, gi = compute_ai_gi(self.cfg, seismic_nmf)
1521        for i in range(1, max_num + 1):
1522            _c = np.where(labels == i)
1523            cl = dict()
1524            cl["model_id"] = os.path.basename(self.cfg.work_subfolder)
1525            cl["fluid"] = fluid
1526            cl["n_voxels"] = len(_c[0])
1527            # np.min() or x.min() returns type numpy.int64 which SQLITE cannot handle. Convert to int
1528            cl["x_min"] = int(np.min(_c[0]))
1529            cl["x_max"] = int(np.max(_c[0]))
1530            cl["y_min"] = int(np.min(_c[1]))
1531            cl["y_max"] = int(np.max(_c[1]))
1532            cl["z_min"] = int(np.min(_c[2]))
1533            cl["z_max"] = int(np.max(_c[2]))
1534            cl["zbml_min"] = np.min(self.faults.faulted_depth[_c])
1535            cl["zbml_max"] = np.max(self.faults.faulted_depth[_c])
1536            cl["zbml_avg"] = np.mean(self.faults.faulted_depth[_c])
1537            cl["zbml_std"] = np.std(self.faults.faulted_depth[_c])
1538            cl["zbml_25pct"] = np.percentile(self.faults.faulted_depth[_c], 25)
1539            cl["zbml_median"] = np.percentile(self.faults.faulted_depth[_c], 50)
1540            cl["zbml_75pct"] = np.percentile(self.faults.faulted_depth[_c], 75)
1541            cl["ng_min"] = np.min(self.faults.faulted_net_to_gross[_c])
1542            cl["ng_max"] = np.max(self.faults.faulted_net_to_gross[_c])
1543            cl["ng_avg"] = np.mean(self.faults.faulted_net_to_gross[_c])
1544            cl["ng_std"] = np.std(self.faults.faulted_net_to_gross[_c])
1545            cl["ng_25pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 25)
1546            cl["ng_median"] = np.median(self.faults.faulted_net_to_gross[_c])
1547            cl["ng_75pct"] = np.percentile(self.faults.faulted_net_to_gross[_c], 75)
1548            # Check for intersections with faults, salt and onlaps for closure type
1549            cl["intersects_fault"] = False
1550            cl["intersects_onlap"] = False
1551            cl["intersects_salt"] = False
1552            if np.max(self.wide_faults[_c] > 0):
1553                cl["intersects_fault"] = True
1554            if np.max(self.onlaps_upward[_c] > 0):
1555                cl["intersects_onlap"] = True
1556            if self.cfg.include_salt and np.max(self.wide_salt[_c] > 0):
1557                cl["intersects_salt"] = True
1558
1559            if seismic_nmf is not None:
1560                # Using only the top of the closure, calculate seismic properties
1561                labels_copy = labels.copy()
1562                labels_copy[labels_copy != i] = 0
1563                top_closure = get_top_of_closure(labels_copy)
1564                near = seismic_nmf[0, ...][np.where(top_closure == 1)]
1565                cl["near_min"] = np.min(near)
1566                cl["near_max"] = np.max(near)
1567                cl["near_avg"] = np.mean(near)
1568                cl["near_std"] = np.std(near)
1569                cl["near_25pct"] = np.percentile(near, 25)
1570                cl["near_median"] = np.percentile(near, 50)
1571                cl["near_75pct"] = np.percentile(near, 75)
1572                mid = seismic_nmf[1, ...][np.where(top_closure == 1)]
1573                cl["mid_min"] = np.min(mid)
1574                cl["mid_max"] = np.max(mid)
1575                cl["mid_avg"] = np.mean(mid)
1576                cl["mid_std"] = np.std(mid)
1577                cl["mid_25pct"] = np.percentile(mid, 25)
1578                cl["mid_median"] = np.percentile(mid, 50)
1579                cl["mid_75pct"] = np.percentile(mid, 75)
1580                far = seismic_nmf[2, ...][np.where(top_closure == 1)]
1581                cl["far_min"] = np.min(far)
1582                cl["far_max"] = np.max(far)
1583                cl["far_avg"] = np.mean(far)
1584                cl["far_std"] = np.std(far)
1585                cl["far_25pct"] = np.percentile(far, 25)
1586                cl["far_median"] = np.percentile(far, 50)
1587                cl["far_75pct"] = np.percentile(far, 75)
1588                intercept = ai[np.where(top_closure == 1)]
1589                cl["intercept_min"] = np.min(intercept)
1590                cl["intercept_max"] = np.max(intercept)
1591                cl["intercept_avg"] = np.mean(intercept)
1592                cl["intercept_std"] = np.std(intercept)
1593                cl["intercept_25pct"] = np.percentile(intercept, 25)
1594                cl["intercept_median"] = np.percentile(intercept, 50)
1595                cl["intercept_75pct"] = np.percentile(intercept, 75)
1596                gradient = gi[np.where(top_closure == 1)]
1597                cl["gradient_min"] = np.min(gradient)
1598                cl["gradient_max"] = np.max(gradient)
1599                cl["gradient_avg"] = np.mean(gradient)
1600                cl["gradient_std"] = np.std(gradient)
1601                cl["gradient_25pct"] = np.percentile(gradient, 25)
1602                cl["gradient_median"] = np.percentile(gradient, 50)
1603                cl["gradient_75pct"] = np.percentile(gradient, 75)
1604
1605            clist.append(cl)
1606
1607        return clist
def write_closure_info_to_log(self, seismic_nmf=None):
1609    def write_closure_info_to_log(self, seismic_nmf=None):
1610        """store info about closure in log file"""
1611        top_sand_layers = [x for x in self.top_lith_indices if self.facies[x] == 1.0]
1612        self.cfg.write_to_logfile(
1613            msg=None,
1614            mainkey="model_parameters",
1615            subkey="top_sand_layers",
1616            val=top_sand_layers,
1617        )
1618        o = measure.label(self.oil_closures[:], connectivity=2, background=0)
1619        g = measure.label(self.gas_closures[:], connectivity=2, background=0)
1620        b = measure.label(self.brine_closures[:], connectivity=2, background=0)
1621        oil_closures = self.populate_closure_dict(o, "oil", seismic_nmf)
1622        gas_closures = self.populate_closure_dict(g, "gas", seismic_nmf)
1623        brine_closures = self.populate_closure_dict(b, "brine", seismic_nmf)
1624        all_closures = oil_closures + gas_closures + brine_closures
1625        for i, c in enumerate(all_closures):
1626            self.cfg.sqldict[f"closure_{i+1}"] = c
1627        num_labels = np.max(o) + np.max(g)
1628        self.cfg.write_to_logfile(
1629            msg=None,
1630            mainkey="model_parameters",
1631            subkey="number_hc_closures",
1632            val=num_labels,
1633        )
1634        # Add total number of closure voxels, with ratio of closure voxels given as a percentage
1635        closure_voxel_count = o[o > 0].size + g[g > 0].size
1636        closure_voxel_pct = closure_voxel_count / o.size
1637        self.cfg.write_to_logfile(
1638            msg=None,
1639            mainkey="model_parameters",
1640            subkey="closure_voxel_count",
1641            val=closure_voxel_count,
1642        )
1643        self.cfg.write_to_logfile(
1644            msg=None,
1645            mainkey="model_parameters",
1646            subkey="closure_voxel_pct",
1647            val=closure_voxel_pct * 100,
1648        )
1649        # Same for Brine
1650        _brine_voxels = b[b == 1].size
1651        _brine_voxels_pct = _brine_voxels / b.size
1652        self.cfg.write_to_logfile(
1653            msg=None,
1654            mainkey="model_parameters",
1655            subkey="closure_voxel_count_brine",
1656            val=_brine_voxels,
1657        )
1658        self.cfg.write_to_logfile(
1659            msg=None,
1660            mainkey="model_parameters",
1661            subkey="closure_voxel_pct_brine",
1662            val=_brine_voxels_pct * 100,
1663        )
1664        # Same for Oil
1665        _oil_voxels = o[o == 1].size
1666        _oil_voxels_pct = _oil_voxels / o.size
1667        self.cfg.write_to_logfile(
1668            msg=None,
1669            mainkey="model_parameters",
1670            subkey="closure_voxel_count_oil",
1671            val=_oil_voxels,
1672        )
1673        self.cfg.write_to_logfile(
1674            msg=None,
1675            mainkey="model_parameters",
1676            subkey="closure_voxel_pct_oil",
1677            val=_oil_voxels_pct * 100,
1678        )
1679        # Same for Gas
1680        _gas_voxels = g[g == 1].size
1681        _gas_voxels_pct = _gas_voxels / g.size
1682        self.cfg.write_to_logfile(
1683            msg=None,
1684            mainkey="model_parameters",
1685            subkey="closure_voxel_count_gas",
1686            val=_gas_voxels,
1687        )
1688        self.cfg.write_to_logfile(
1689            msg=None,
1690            mainkey="model_parameters",
1691            subkey="closure_voxel_pct_gas",
1692            val=_gas_voxels_pct,
1693        )
1694        # Write old logfile as well as the sql dict
1695        msg = f"layers for closure computation: {str(self.top_lith_indices)}\n"
1696        msg += f"Number of HC Closures : {num_labels}\n"
1697        msg += (
1698            f"Closure voxel count: {closure_voxel_count} - "
1699            f"{closure_voxel_pct:5.2%}\n"
1700        )
1701        msg += (
1702            f"Closure voxel count: (brine) {_brine_voxels} - {_brine_voxels_pct:5.2%}\n"
1703        )
1704        msg += f"Closure voxel count: (oil) {_oil_voxels} - {_oil_voxels_pct:5.2%}\n"
1705        msg += f"Closure voxel count: (gas) {_gas_voxels} - {_gas_voxels_pct:5.2%}\n"
1706        print(msg)
1707        for i in range(self.facies.shape[0]):
1708            if self.facies[i] == 1:
1709                msg += f"  layers for closure computation:   {i}, sand\n"
1710            else:
1711                msg += f"  layers for closure computation:   {i}, shale\n"
1712        self.cfg.write_to_logfile(msg)

store info about closure in log file

def parse_label_values_and_counts(self, labels_clean):
1714    def parse_label_values_and_counts(self, labels_clean):
1715        """parse label values and counts"""
1716        if self.cfg.verbose:
1717            print(" Inside parse_label_values_and_counts")
1718        next_label = 0
1719        label_values = [0]
1720        label_counts = [labels_clean[labels_clean == 0].size]
1721        for i in range(1, labels_clean.max() + 1):
1722            try:
1723                next_label = labels_clean[labels_clean > next_label].min()
1724            except (TypeError, ValueError):
1725                break
1726            label_values.append(next_label)
1727            label_counts.append(labels_clean[labels_clean == next_label].size)
1728            print(
1729                f"Label: {i}, label_values: {label_values[-1]}, label_counts: {label_counts[-1]}"
1730            )
1731        # force labels to use consecutive integer values
1732        for i, ilabel in enumerate(label_values):
1733            labels_clean[labels_clean == ilabel] = i
1734            label_values[i] = i
1735        # labels_clean = self.remove_small_objects(labels_clean)  # already applied to labels_clean
1736        # Remove label_value 0
1737        label_values.remove(0)
1738        return label_values, labels_clean

parse label values and counts

def assign_fluid_types(self, label_values, labels_clean):
1740    def assign_fluid_types(self, label_values, labels_clean):
1741        """randomly assign oil or gas to closure"""
1742        print(
1743            " labels_clean.min(), labels_clean.max() = ",
1744            labels_clean.min(),
1745            labels_clean.max(),
1746        )
1747        _brine_closures = (labels_clean * 0.0).astype("uint8")
1748        _oil_closures = (labels_clean * 0.0).astype("uint8")
1749        _gas_closures = (labels_clean * 0.0).astype("uint8")
1750
1751        fluid_type_code = np.random.randint(3, size=labels_clean.max() + 1)
1752
1753        _closure_segments = self.closure_segments[:]
1754        for i in range(1, labels_clean.max() + 1):
1755            voxel_count = labels_clean[labels_clean == i].size
1756            if voxel_count > 0:
1757                print(f"Voxel Count: {voxel_count}\tFluid type: {fluid_type_code[i]}")
1758            # not in closure = 0
1759            # closure with brine filled reservoir fluid_type_code = 1
1760            # closure with oil filled reservoir fluid_type_code = 2
1761            # closure with gas filled reservoir fluid_type_code = 3
1762            if i in label_values:
1763                if fluid_type_code[i] == 0:
1764                    # brine: change labels_clean contents to fluid_type_code = 1 (same as background)
1765                    _brine_closures[
1766                        np.logical_and(labels_clean == i, _closure_segments > 0)
1767                    ] = 1
1768                elif fluid_type_code[i] == 1:
1769                    # oil: change labels_clean contents to fluid_type_code = 2
1770                    _oil_closures[labels_clean == i] = 1
1771                elif fluid_type_code[i] == 2:
1772                    # gas: change labels_clean contents to fluid_type_code = 3
1773                    _gas_closures[labels_clean == i] = 1
1774        return _oil_closures, _gas_closures, _brine_closures

randomly assign oil or gas to closure

def remove_small_objects(self, labels, min_filter=True):
1776    def remove_small_objects(self, labels, min_filter=True):
1777        try:
1778            # Use the global minimum voxel size initially, before closure types are identified
1779            labels_clean = morphology.remove_small_objects(
1780                labels, self.cfg.closure_min_voxels
1781            )
1782            if self.cfg.verbose:
1783                print("labels_clean succeeded.")
1784                print(
1785                    " labels.min:{}, labels.max: {}".format(labels.min(), labels.max())
1786                )
1787                print(
1788                    " labels_clean min:{}, labels_clean max: {}".format(
1789                        labels_clean.min(), labels_clean.max()
1790                    )
1791                )
1792        except Exception as e:
1793            print(
1794                f"Closures/create_closures: labels_clean (remove_small_objects) did not succeed: {e}"
1795            )
1796            if min_filter:
1797                labels_clean = minimum_filter(labels, size=(3, 3, 3))
1798                if self.cfg.verbose:
1799                    print(
1800                        " labels.min:{}, labels.max: {}".format(
1801                            labels.min(), labels.max()
1802                        )
1803                    )
1804                    print(
1805                        " labels_clean min:{}, labels_clean max: {}".format(
1806                            labels_clean.min(), labels_clean.max()
1807                        )
1808                    )
1809        return labels_clean
def segment_closures(self, _closure_segments, remove_shale=True):
1811    def segment_closures(self, _closure_segments, remove_shale=True):
1812        """Segment the closures so that they can be randomly filled with hydrocarbons"""
1813
1814        _closure_segments = np.clip(_closure_segments, 0.0, 1.0)
1815        # remove tiny clusters
1816        _closure_segments = minimum_filter(
1817            _closure_segments.astype("int16"), size=(3, 3, 1)
1818        )
1819        _closure_segments = maximum_filter(_closure_segments, size=(3, 3, 1))
1820
1821        if remove_shale:
1822            # restrict closures to sand (non-shale) voxels
1823            if self.faults.faulted_lithology.shape[2] == _closure_segments.shape[2]:
1824                sand_shale = self.faults.faulted_lithology[:].copy()
1825            else:
1826                sand_shale = self.faults.faulted_lithology[
1827                    :, :, :: self.cfg.infill_factor
1828                ].copy()
1829            _closure_segments[sand_shale <= 0.0] = 0
1830            del sand_shale
1831        labels = measure.label(_closure_segments, connectivity=2, background=0)
1832
1833        labels_clean = self.remove_small_objects(labels)
1834        return labels_clean, _closure_segments

Segment the closures so that they can be randomly filled with hydrocarbons

def write_closure_volumes_to_disk(self):
1836    def write_closure_volumes_to_disk(self):
1837        # Create files for closure volumes
1838        self.write_cube_to_disk(self.brine_closures[:], "closure_segments_brine")
1839        self.write_cube_to_disk(self.oil_closures[:], "closure_segments_oil")
1840        self.write_cube_to_disk(self.gas_closures[:], "closure_segments_gas")
1841        # Create combined HC cube by adding oil and gas closures
1842        self.hc_labels[:] = (self.oil_closures[:] + self.gas_closures[:]).astype(
1843            "uint8"
1844        )
1845        self.write_cube_to_disk(self.hc_labels[:], "closure_segments_hc")
1846
1847        if self.cfg.model_qc_volumes:
1848            self.write_cube_to_disk(self.closure_segments, "closure_segments_raw_all")
1849            self.write_cube_to_disk(self.simple_closures, "closure_segments_simple")
1850            self.write_cube_to_disk(self.strat_closures, "closure_segments_strat")
1851            self.write_cube_to_disk(self.fault_closures, "closure_segments_fault")
1852
1853        # Triple check that no small closures exist in the final closure files
1854        for i, c in enumerate(
1855            [
1856                self.oil_closures,
1857                self.gas_closures,
1858                self.simple_closures,
1859                self.strat_closures,
1860                self.fault_closures,
1861            ]
1862        ):
1863            _t = measure.label(c, connectivity=2, background=0)
1864            counts = [_t[_t == x].size for x in range(np.max(_t))]
1865            print(f"Final closure volume voxels sizes: {counts}")
1866            for n, x in enumerate(counts):
1867                if x < self.cfg.closure_min_voxels:
1868                    print(f"Voxel count: {x}\t Count:{i}, index: {n}")
1869
1870        # Return the hydrocarbon closure labels so that augmentation can be applied to the data & labels
1871        # return self.oil_closures + self.gas_closures
def calculate_closure_statistics(self, in_array, closure_type):
1873    def calculate_closure_statistics(self, in_array, closure_type):
1874        """
1875        Calculate the size and location of isolated features in an array
1876
1877        :param in_array: ndarray. Input array to be labelled, where non-zero values are counted as features
1878        :param closure_type: string. Closure type label
1879        :param digi: int or float. To convert depth values from samples to units
1880        :return: string. Concatenated string of closure statistics to be written to log
1881        """
1882        labelled_array, max_labels = measure.label(
1883            in_array, connectivity=2, return_num=True
1884        )
1885        msg = ""
1886        for i in range(1, max_labels + 1):  # start at 1 to avoid counting 0's
1887            trap = np.where(labelled_array == i)
1888            ranges = [([np.min(trap[x]), np.max(trap[x])]) for x, _ in enumerate(trap)]
1889            sizes = [x[1] - x[0] for x in ranges]
1890            n_voxels = labelled_array[labelled_array == i].size
1891            if sum(sizes) > 0:
1892                msg += (
1893                    f"{closure_type}\t"
1894                    f"Num X,Y,Z Samples: {str(sizes).ljust(15)}\t"
1895                    f"Num Voxels: {str(n_voxels).ljust(5)}\t"
1896                    f"Track: {2000 + ranges[0][0]}-{2000 + ranges[0][1]}\t"
1897                    f"Bin: {1000 + ranges[1][0]}-{1000 + ranges[1][1]}\t"
1898                    f"Depth: {ranges[2][0] * self.cfg.digi}-{ranges[2][1] * self.cfg.digi + self.cfg.digi / 2}\n"
1899                )
1900        return msg

Calculate the size and location of isolated features in an array

Parameters
  • in_array: ndarray. Input array to be labelled, where non-zero values are counted as features
  • closure_type: string. Closure type label
  • digi: int or float. To convert depth values from samples to units
Returns

string. Concatenated string of closure statistics to be written to log

def find_faulted_closures(self, closure_segment_list, closure_segments):
1902    def find_faulted_closures(self, closure_segment_list, closure_segments):
1903        self._dilate_faults()
1904        for iclosure in closure_segment_list:
1905            i, j, k = np.where(closure_segments == iclosure)
1906            faults_within_closure = self.wide_faults[i, j, k]
1907            if faults_within_closure.max() > 0:
1908                if self.oil_closures[i, j, k].max() > 0:
1909                    # Faulted oil closure
1910                    self.faulted_closures_oil[i, j, k] = 1.0
1911                    self.n_fault_closures_oil += 1
1912                    self.fault_closures_oil_segment_list.append(iclosure)
1913                elif self.gas_closures[i, j, k].max() > 0:
1914                    # Faulted gas closure
1915                    self.faulted_closures_gas[i, j, k] = 1.0
1916                    self.n_fault_closures_gas += 1
1917                    self.fault_closures_gas_segment_list.append(iclosure)
1918                elif self.brine_closures[i, j, k].max() > 0:
1919                    # Faulted brine closure
1920                    self.faulted_closures_brine[i, j, k] = 1.0
1921                    self.n_fault_closures_brine += 1
1922                    self.fault_closures_brine_segment_list.append(iclosure)
1923                else:
1924                    print(
1925                        "Closure is faulted but does not have oil, gas or brine assigned"
1926                    )
def find_onlap_closures(self, closure_segment_list, closure_segments):
1928    def find_onlap_closures(self, closure_segment_list, closure_segments):
1929        for iclosure in closure_segment_list:
1930            i, j, k = np.where(closure_segments == iclosure)
1931            onlaps_within_closure = self.onlaps_upward[i, j, k]
1932            if onlaps_within_closure.max() > 0:
1933                if self.oil_closures[i, j, k].max() > 0:
1934                    self.onlap_closures_oil[i, j, k] = 1.0
1935                    self.n_onlap_closures_oil += 1
1936                    self.onlap_closures_oil_segment_list.append(iclosure)
1937                elif self.gas_closures[i, j, k].max() > 0:
1938                    self.onlap_closures_gas[i, j, k] = 1.0
1939                    self.n_onlap_closures_gas += 1
1940                    self.onlap_closures_gas_segment_list.append(iclosure)
1941                elif self.brine_closures[i, j, k].max() > 0:
1942                    self.onlap_closures_brine[i, j, k] = 1.0
1943                    self.n_onlap_closures_brine += 1
1944                    self.onlap_closures_brine_segment_list.append(iclosure)
1945                else:
1946                    print(
1947                        "Closure is onlap but does not have oil, gas or brine assigned"
1948                    )
def find_simple_closures(self, closure_segment_list, closure_segments):
1950    def find_simple_closures(self, closure_segment_list, closure_segments):
1951        for iclosure in closure_segment_list:
1952            i, j, k = np.where(closure_segments == iclosure)
1953            faults_within_closure = self.wide_faults[i, j, k]
1954            onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:])
1955            onlaps_within_closure = onlaps[i, j, k]
1956            oil_within_closure = self.oil_closures[i, j, k]
1957            gas_within_closure = self.gas_closures[i, j, k]
1958            brine_within_closure = self.brine_closures[i, j, k]
1959            if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0:
1960                if oil_within_closure.max() > 0:
1961                    self.simple_closures_oil[i, j, k] = 1.0
1962                    self.n_4way_closures_oil += 1
1963                elif gas_within_closure.max() > 0:
1964                    self.simple_closures_gas[i, j, k] = 1.0
1965                    self.n_4way_closures_gas += 1
1966                elif brine_within_closure.max() > 0:
1967                    self.simple_closures_brine[i, j, k] = 1.0
1968                    self.n_4way_closures_brine += 1
1969                else:
1970                    print(
1971                        "Closure is not faulted or onlap but does not have oil, gas or brine assigned"
1972                    )
def find_false_closures(self, closure_segment_list, closure_segments):
1974    def find_false_closures(self, closure_segment_list, closure_segments):
1975        for iclosure in closure_segment_list:
1976            i, j, k = np.where(closure_segments == iclosure)
1977            faults_within_closure = self.fat_faults[i, j, k]
1978            onlaps_within_closure = self.onlaps_downward[i, j, k]
1979            for fluid, false, num in zip(
1980                [self.oil_closures, self.gas_closures, self.brine_closures],
1981                [
1982                    self.false_closures_oil,
1983                    self.false_closures_gas,
1984                    self.false_closures_brine,
1985                ],
1986                [
1987                    self.n_false_closures_oil,
1988                    self.n_false_closures_gas,
1989                    self.n_false_closures_brine,
1990                ],
1991            ):
1992                fluid_within_closure = fluid[i, j, k]
1993                if fluid_within_closure.max() > 0:
1994                    if onlaps_within_closure.max() > 0:
1995                        _faulted_closure_threshold = float(
1996                            faults_within_closure[faults_within_closure > 0].size
1997                            / fluid_within_closure[fluid_within_closure > 0].size
1998                        )
1999                        _onlap_closure_threshold = float(
2000                            onlaps_within_closure[onlaps_within_closure > 0].size
2001                            / fluid_within_closure[fluid_within_closure > 0].size
2002                        )
2003                        if (
2004                            _faulted_closure_threshold > 0.65
2005                            and _onlap_closure_threshold > 0.65
2006                        ):
2007                            false[i, j, k] = 1
2008                            num += 1
def find_salt_bounded_closures(self, closure_segment_list, closure_segments):
2010    def find_salt_bounded_closures(self, closure_segment_list, closure_segments):
2011        self._dilate_salt()
2012        for iclosure in closure_segment_list:
2013            i, j, k = np.where(closure_segments == iclosure)
2014            salt_within_closure = self.wide_salt[i, j, k]
2015            if salt_within_closure.max() > 0:
2016                if self.oil_closures[i, j, k].max() > 0:
2017                    # salt bounded oil closure
2018                    self.salt_closures_oil[i, j, k] = 1.0
2019                    self.n_salt_closures_oil += 1
2020                    self.salt_closures_oil_segment_list.append(iclosure)
2021                elif self.gas_closures[i, j, k].max() > 0:
2022                    # salt bounded gas closure
2023                    self.salt_closures_gas[i, j, k] = 1.0
2024                    self.n_salt_closures_gas += 1
2025                    self.salt_closures_gas_segment_list.append(iclosure)
2026                elif self.brine_closures[i, j, k].max() > 0:
2027                    # salt bounded brine closure
2028                    self.salt_closures_brine[i, j, k] = 1.0
2029                    self.n_salt_closures_brine += 1
2030                    self.salt_closures_brine_segment_list.append(iclosure)
2031                else:
2032                    print(
2033                        "Closure is salt bounded but does not have oil, gas or brine assigned"
2034                    )
def find_faulted_all_closures(self, closure_segment_list, closure_segments):
2036    def find_faulted_all_closures(self, closure_segment_list, closure_segments):
2037        for iclosure in closure_segment_list:
2038            i, j, k = np.where(closure_segments == iclosure)
2039            faults_within_closure = self.wide_faults[i, j, k]
2040            if faults_within_closure.max() > 0:
2041                self.faulted_all_closures[i, j, k] = 1.0
2042                self.n_fault_all_closures += 1
2043                self.fault_all_closures_segment_list.append(iclosure)
def find_onlap_all_closures(self, closure_segment_list, closure_segments):
2045    def find_onlap_all_closures(self, closure_segment_list, closure_segments):
2046        for iclosure in closure_segment_list:
2047            i, j, k = np.where(closure_segments == iclosure)
2048            onlaps_within_closure = self.onlaps_upward[i, j, k]
2049            if onlaps_within_closure.max() > 0:
2050                self.onlap_all_closures[i, j, k] = 1.0
2051                self.n_onlap_all_closures += 1
2052                self.onlap_all_closures_segment_list.append(iclosure)
def find_simple_all_closures(self, closure_segment_list, closure_segments):
2054    def find_simple_all_closures(self, closure_segment_list, closure_segments):
2055        for iclosure in closure_segment_list:
2056            i, j, k = np.where(closure_segments == iclosure)
2057            faults_within_closure = self.wide_faults[i, j, k]
2058            onlaps = self._threshold_volumes(self.faults.faulted_onlap_segments[:])
2059            onlaps_within_closure = onlaps[i, j, k]
2060            if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0:
2061                self.simple_all_closures[i, j, k] = 1.0
2062                self.n_4way_all_closures += 1
def find_false_all_closures(self, closure_segment_list, closure_segments):
2064    def find_false_all_closures(self, closure_segment_list, closure_segments):
2065        for iclosure in closure_segment_list:
2066            i, j, k = np.where(closure_segments == iclosure)
2067            faults_within_closure = self.fat_faults[i, j, k]
2068            onlaps_within_closure = self.onlaps_downward[i, j, k]
2069            if onlaps_within_closure.max() > 0:
2070                _faulted_closure_threshold = float(
2071                    faults_within_closure[faults_within_closure > 0].size / i.size
2072                )
2073                _onlap_closure_threshold = float(
2074                    onlaps_within_closure[onlaps_within_closure > 0].size / i.size
2075                )
2076                if (
2077                    _faulted_closure_threshold > 0.65
2078                    and _onlap_closure_threshold > 0.65
2079                ):
2080                    self.false_all_closures[i, j, k] = 1
2081                    self.n_false_all_closures += 1
def find_salt_bounded_all_closures(self, closure_segment_list, closure_segments):
2083    def find_salt_bounded_all_closures(self, closure_segment_list, closure_segments):
2084        self._dilate_salt()
2085        for iclosure in closure_segment_list:
2086            i, j, k = np.where(closure_segments == iclosure)
2087            salt_within_closure = self.wide_salt[i, j, k]
2088            if salt_within_closure.max() > 0:
2089                self.salt_all_closures[i, j, k] = 1.0
2090                self.n_salt_all_closures += 1
2091                self.salt_all_closures_segment_list.append(iclosure)
def grow_to_fault2( self, closures, grow_only_sand_closures=True, remove_small_closures=True):
2134    def grow_to_fault2(
2135        self, closures, grow_only_sand_closures=True, remove_small_closures=True
2136    ):
2137        # - grow closures laterally and up within layer and within fault block
2138        print(
2139            "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..."
2140        )
2141        self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2")
2142
2143        # dilated_fault_closures = closures.copy()
2144        # n_faulted_closures = dilated_fault_closures.max()
2145        labels_clean = self.closure_segments[:].copy()
2146        labels_clean[closures == 0] = 0
2147        labels_clean_list = list(set(labels_clean.flatten()))
2148        labels_clean_list.remove(0)
2149        initial_closures = labels_clean.copy()
2150        print("\n    ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list))
2151        print("    ... grow_to_fault2: faulted_closures = ", labels_clean_list)
2152
2153        # TODO remove this once small closures are found and fixed
2154        voxel_sizes = [
2155            self.closure_segments[self.closure_segments[:] == i].size
2156            for i in labels_clean_list
2157        ]
2158        for _v in voxel_sizes:
2159            print(f"Voxel_Sizes: {_v}")
2160            if _v < self.cfg.closure_min_voxels:
2161                print(_v)
2162
2163        depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float)
2164        _depths = np.arange(self.faults.faulted_age_volume.shape[2])
2165        depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2])
2166        _ng = self.faults.faulted_net_to_gross[:].copy()
2167        # Cannot solely use NG anymore since shales may have variable net to gross
2168        _lith = self.faults.faulted_lithology[:].copy()
2169        _age = self.faults.faulted_age_volume[:].copy()
2170        fault_throw = self.faults.max_fault_throw[:]
2171
2172        for il, i in enumerate(labels_clean_list):
2173            fault_blocks_list = list(set(fault_throw[labels_clean == i].flatten()))
2174            print("    ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list)
2175            for jl, j in enumerate(fault_blocks_list):
2176                print(
2177                    "\n\n    ... label, throw = ",
2178                    i,
2179                    j,
2180                    list(set(fault_throw[labels_clean == i].flatten())),
2181                    labels_clean[labels_clean == i].size,
2182                    fault_throw[fault_throw == j].size,
2183                    fault_throw[
2184                        np.where((labels_clean == i) & (fault_throw[:] == j))
2185                    ].size,
2186                )
2187                single_closure = labels_clean * 0.0
2188                size = single_closure[
2189                    np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25))
2190                ].size
2191                if size >= self.cfg.closure_min_voxels:
2192                    print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}")
2193                    single_closure[
2194                        np.where((labels_clean == i) & (np.abs(fault_throw - j) < 0.25))
2195                    ] = 1
2196                if single_closure[single_closure > 0].size == 0:
2197                    # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0
2198                    labels_clean[np.where(labels_clean == i)] = 0
2199                    continue
2200                avg_ng = _ng[single_closure == 1].mean()
2201                _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int")
2202                _ng_voxels = _ng[single_closure == 1]
2203                _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0]
2204                min_geo_age = _geo_age_voxels.min() - 0.5
2205                avg_geo_age = int(_geo_age_voxels.mean())
2206                max_geo_age = _geo_age_voxels.max() + 0.5
2207                _depth_geobody_voxels = depth_cube[single_closure == 1]
2208                min_depth = _depth_geobody_voxels.min()
2209                max_depth = _depth_geobody_voxels.max()
2210                avg_throw = np.median(fault_throw[single_closure == 1])
2211
2212                closure_boundary_cube = closures * 0.0
2213                if grow_only_sand_closures:
2214                    lith_flag = _lith > 0.0
2215                else:
2216                    lith_flag = _lith >= 0.0
2217                closure_boundary_cube[
2218                    np.where(
2219                        lith_flag
2220                        & (_age > min_geo_age)
2221                        & (_age < max_geo_age)
2222                        & (fault_throw == avg_throw)
2223                        & (depth_cube <= max_depth)
2224                    )
2225                ] = 1.0
2226                print(
2227                    "\n    ... grow_to_fault2: closure_boundary_cube voxels = ",
2228                    closure_boundary_cube[closure_boundary_cube == 1].size,
2229                )
2230
2231                n_voxel = single_closure[single_closure == 1].size
2232
2233                original_voxels = n_voxel + 0
2234                print(
2235                    "\n    ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ",
2236                    i,
2237                    j,
2238                    n_voxel,
2239                    (min_geo_age, avg_geo_age, max_geo_age),
2240                    (min_depth, max_depth),
2241                    avg_ng,
2242                    il,
2243                    " / ",
2244                    len(labels_clean_list),
2245                )
2246
2247                grown_closure = single_closure.copy()
2248                grown_closure[depth_cube >= max_depth] = 0
2249                delta_voxel = 0
2250                previous_delta_voxel = 1e9
2251                converged = False
2252                for ii in range(15):
2253                    grown_closure = self.grow_lateral(grown_closure, 1, dist=2)
2254                    grown_closure = self.grow_upward(grown_closure, 1, dist=1)
2255                    # stay within layer, within age, within fault block, above HCWC
2256                    grown_closure[closure_boundary_cube == 0.0] = 0.0
2257                    single_closure = single_closure + grown_closure
2258                    single_closure[single_closure > 0] = i
2259                    new_n_voxel = single_closure[single_closure > 0].size
2260                    previous_delta_voxel = delta_voxel + 0
2261                    delta_voxel = new_n_voxel - n_voxel
2262                    print(
2263                        "    ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel,"
2264                        " delta_voxel>previous_delta_voxel = ",
2265                        i,
2266                        ii,
2267                        new_n_voxel,
2268                        delta_voxel,
2269                        previous_delta_voxel,
2270                        delta_voxel > previous_delta_voxel,
2271                    )
2272                    if n_voxel == new_n_voxel:
2273                        # finish bottom voxel layer near "HCWC"
2274                        grown_closure = self.grow_downward(
2275                            grown_closure, 1, dist=1, verbose=False
2276                        )
2277                        # stay within layer, within age, within fault block, above HCWC
2278                        grown_closure[closure_boundary_cube == 0.0] = 0.0
2279                        single_closure = single_closure + grown_closure
2280                        single_closure[single_closure > 0] = i
2281                        converged = True
2282                        break
2283                    else:
2284                        n_voxel = new_n_voxel
2285                    previous_delta_voxel = delta_voxel + 0
2286                if converged is True:
2287                    labels_clean[single_closure > 0] = i
2288                    msg_postscript = " converged"
2289                else:
2290                    labels_clean[labels_clean == i] = -i
2291                    msg_postscript = " NOT converged"
2292                msg = (
2293                    f"closure_id: {int(i):04d}, fault_id: {int(j + .5):04d}, "
2294                    + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, "
2295                    + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}"
2296                )
2297                print(msg + msg_postscript)
2298                self.cfg.write_to_logfile(msg + msg_postscript)
2299
2300        # Set small closures to 0 after growth
2301        # _grown_labels = measure.label(labels_clean, connectivity=2, background=0)
2302        # for x in np.unique(_grown_labels):
2303        #     size = _grown_labels[_grown_labels == x].size
2304        #     print(f'Size before editing: {size}')
2305        #     if size < self.cfg.closure_min_voxels:
2306        #         labels_clean[_grown_labels == x] = 0.
2307        # for x in np.unique(labels_clean):
2308        #     size = labels_clean[labels_clean == x].size
2309        #     print(f'Size after editing: {size}')
2310
2311        if remove_small_closures:
2312            _initial_labels = measure.label(
2313                initial_closures, connectivity=2, background=0
2314            )
2315            _grown_labels = measure.label(labels_clean, connectivity=2, background=0)
2316            for x in np.unique(_grown_labels):
2317                size_initial = _initial_labels[_initial_labels == x].size
2318                size_grown = _grown_labels[_grown_labels == x].size
2319                print(f"Size before editing: {size_initial}")
2320                print(f"Size after editing: {size_grown}")
2321                if size_grown < self.cfg.closure_min_voxels:
2322                    print(
2323                        f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed"
2324                    )
2325                    labels_clean[_grown_labels == x] = 0.0
2326        return labels_clean
def grow_to_salt(self, closures):
2328    def grow_to_salt(self, closures):
2329        # - grow closures laterally and up within layer up to salt body
2330        print("\n\n ... grow_to_salt: grow closures laterally and up within layer ...")
2331        self.cfg.write_to_logfile("growing closures to salt body: grow_to_salt")
2332
2333        labels_clean = measure.label(
2334            self.closure_segments[:], connectivity=2, background=0
2335        )
2336        labels_clean[closures == 0] = 0
2337        # labels_clean = self.closure_segments[:].copy()
2338        # labels_clean[closures == 0] = 0
2339        labels_clean_list = list(set(labels_clean.flatten()))
2340        labels_clean_list.remove(0)
2341        initial_closures = labels_clean.copy()
2342        print("\n    ... grow_to_salt: n_salt_closures = ", len(labels_clean_list))
2343        print("    ... grow_to_salt: salt_closures = ", labels_clean_list)
2344
2345        depth_cube = np.zeros(self.faults.faulted_age_volume.shape, float)
2346        _depths = np.arange(self.faults.faulted_age_volume.shape[2])
2347        depth_cube += _depths.reshape(1, 1, self.faults.faulted_age_volume.shape[2])
2348        _ng = self.faults.faulted_net_to_gross[:].copy()
2349        _age = self.faults.faulted_age_volume[:].copy()
2350        salt = self.faults.salt_model.salt_segments[:]
2351
2352        for il, i in enumerate(labels_clean_list):
2353            salt_list = list(set(salt[labels_clean == i].flatten()))
2354            print("    ... grow_to_fault2: salt_list = ", salt_list)
2355            single_closure = labels_clean * 0.0
2356            size = single_closure[np.where(labels_clean == i)].size
2357            if size >= self.cfg.closure_min_voxels:
2358                print(f"Label: {i}, Voxel_Count: {size}")
2359                single_closure[np.where(labels_clean == i)] = 1
2360            if single_closure[single_closure > 0].size == 0:
2361                labels_clean[np.where(labels_clean == i)] = 0
2362                continue
2363            avg_ng = _ng[single_closure == 1].mean()
2364            _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int")
2365            _ng_voxels = _ng[single_closure == 1]
2366            _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0]
2367            min_geo_age = _geo_age_voxels.min() - 0.5
2368            avg_geo_age = int(_geo_age_voxels.mean())
2369            max_geo_age = _geo_age_voxels.max() + 0.5
2370            _depth_geobody_voxels = depth_cube[single_closure == 1]
2371            min_depth = _depth_geobody_voxels.min()
2372            max_depth = _depth_geobody_voxels.max()
2373            # Define AOI where salt has been dilated
2374            # close_to_salt = np.zeros_like(salt)
2375            # close_to_salt[self.wide_salt[:] == 1] = 1.0
2376            # close_to_salt[salt == 1] = 0.0
2377
2378            closure_boundary_cube = closures * 0.0
2379            closure_boundary_cube[
2380                np.where(
2381                    (_ng > 0.3)
2382                    & (_age > min_geo_age)  # account for partial voxels
2383                    & (_age < max_geo_age)
2384                    & (salt == 0.0)
2385                    & (depth_cube <= max_depth)
2386                )
2387            ] = 1.0
2388            print(
2389                "\n    ... grow_to_fault2: closure_boundary_cube voxels = ",
2390                closure_boundary_cube[closure_boundary_cube == 1].size,
2391            )
2392
2393            n_voxel = single_closure[single_closure == 1].size
2394
2395            original_voxels = n_voxel + 0
2396            print(
2397                "\n    ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ",
2398                i,
2399                n_voxel,
2400                (min_geo_age, avg_geo_age, max_geo_age),
2401                (min_depth, max_depth),
2402                avg_ng,
2403                il,
2404                " / ",
2405                len(labels_clean_list),
2406            )
2407
2408            grown_closure = single_closure.copy()
2409            grown_closure[depth_cube >= max_depth] = 0
2410            delta_voxel = 0
2411            previous_delta_voxel = 1e9
2412            converged = False
2413            for ii in range(99):
2414                grown_closure = self.grow_lateral(grown_closure, 1, dist=2)
2415                grown_closure = self.grow_upward(grown_closure, 1, dist=1)
2416                # stay within layer, within age, close to salt and above HCWC
2417                grown_closure[closure_boundary_cube == 0.0] = 0.0
2418                single_closure = single_closure + grown_closure
2419                single_closure[single_closure > 0] = i
2420                new_n_voxel = single_closure[single_closure > 0].size
2421                previous_delta_voxel = delta_voxel + 0
2422                delta_voxel = new_n_voxel - n_voxel
2423                print(
2424                    "    ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel,"
2425                    " delta_voxel>previous_delta_voxel = ",
2426                    i,
2427                    ii,
2428                    new_n_voxel,
2429                    delta_voxel,
2430                    previous_delta_voxel,
2431                    delta_voxel > previous_delta_voxel,
2432                )
2433
2434                # If grown voxel is touching the egde of survey, stop and remove closure
2435                _a, _b, _ = np.where(single_closure > 0)
2436                max_boundary_i = self.cfg.cube_shape[0] - 1
2437                max_boundary_j = self.cfg.cube_shape[1] - 1
2438                if (
2439                    np.min(_a) == 0
2440                    or np.max(_a) == max_boundary_i
2441                    or np.min(_b) == 0
2442                    or np.max(_b) == max_boundary_j
2443                ):
2444                    print("Boundary reached, removing closure")
2445                    converged = False
2446                    break
2447
2448                if n_voxel == new_n_voxel:
2449                    # finish bottom voxel layer near HCWC
2450                    grown_closure = self.grow_downward(
2451                        grown_closure, 1, dist=1, verbose=False
2452                    )
2453                    # stay within layer, within age, within fault block, above HCWC
2454                    grown_closure[closure_boundary_cube == 0.0] = 0.0
2455                    single_closure = single_closure + grown_closure
2456                    single_closure[single_closure > 0] = i
2457                    converged = True
2458                    break
2459                else:
2460                    n_voxel = new_n_voxel
2461                previous_delta_voxel = delta_voxel + 0
2462            if converged is True:
2463                labels_clean[single_closure > 0] = i
2464                msg_postscript = " converged"
2465            else:
2466                labels_clean[labels_clean == i] = -i
2467                msg_postscript = " NOT converged"
2468            msg = (
2469                f"closure_id: {int(i):04d}, "
2470                + f"original_voxels: {original_voxels:11.0f}, new_n_voxel: {new_n_voxel:11.0f}, "
2471                + f"percent_growth: {float(new_n_voxel / original_voxels):6.2f}"
2472            )
2473            print(msg + msg_postscript)
2474            self.cfg.write_to_logfile(msg + msg_postscript)
2475
2476        # Set small closures to 0 after growth
2477        _initial_labels = measure.label(initial_closures, connectivity=2, background=0)
2478        _grown_labels = measure.label(labels_clean, connectivity=2, background=0)
2479        for x in np.unique(_grown_labels)[
2480            1:
2481        ]:  # ignore the first label of 0 (closures only)
2482            size_initial = _initial_labels[_initial_labels == x].size
2483            size_grown = _grown_labels[_grown_labels == x].size
2484            print(f"Size before editing: {size_initial}")
2485            print(f"Size after editing: {size_grown}")
2486            if size_grown < self.cfg.closure_min_voxels:
2487                print(
2488                    f"Closure below threshold of {self.cfg.closure_min_voxels} and will be removed"
2489                )
2490                labels_clean[_grown_labels == x] = 0.0
2491
2492        return labels_clean
@staticmethod
def grow_lateral(geobody, iterations, dist=1, verbose=False):
2494    @staticmethod
2495    def grow_lateral(geobody, iterations, dist=1, verbose=False):
2496        from scipy.ndimage.morphology import grey_dilation
2497
2498        dist_size = 2 * dist + 1
2499        mask = np.zeros((dist_size, dist_size, 1))
2500        mask[:, :, :] = 1
2501        _geobody = geobody.copy()
2502        if verbose:
2503            print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape)
2504        for k in range(iterations):
2505            try:
2506                _geobody = grey_dilation(_geobody, footprint=mask)
2507                if verbose:
2508                    print(
2509                        " ...grow_lateral: k, _geobody.shape = ",
2510                        k,
2511                        _geobody[_geobody > 0].shape,
2512                    )
2513            except:
2514                break
2515        return _geobody
@staticmethod
def grow_upward(geobody, iterations, dist=1, verbose=False):
2517    @staticmethod
2518    def grow_upward(geobody, iterations, dist=1, verbose=False):
2519        from scipy.ndimage.morphology import grey_dilation
2520
2521        dist_size = 2 * dist + 1
2522        mask = np.zeros((1, 1, dist_size))
2523        mask[0, 0, : dist + 1] = 1
2524        _geobody = geobody.copy()
2525        if verbose:
2526            print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape)
2527        for k in range(iterations):
2528            try:
2529                _geobody = grey_dilation(_geobody, footprint=mask)
2530                if verbose:
2531                    print(
2532                        " ...grow_upward: k, _geobody.shape = ",
2533                        k,
2534                        _geobody[_geobody > 0].shape,
2535                    )
2536            except:
2537                break
2538        return _geobody
@staticmethod
def grow_downward(geobody, iterations, dist=1, verbose=False):
2540    @staticmethod
2541    def grow_downward(geobody, iterations, dist=1, verbose=False):
2542        from scipy.ndimage.morphology import grey_dilation
2543
2544        dist_size = 2 * dist + 1
2545        mask = np.zeros((1, 1, dist_size))
2546        mask[0, 0, dist:] = 1
2547        _geobody = geobody.copy()
2548        if verbose:
2549            print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape)
2550        for k in range(iterations):
2551            try:
2552                _geobody = grey_dilation(_geobody, footprint=mask)
2553                if verbose:
2554                    print(
2555                        " ...grow_downward: k, _geobody.shape = ",
2556                        k,
2557                        _geobody[_geobody > 0].shape,
2558                    )
2559            except:
2560                break
2561        return _geobody
def parse_closure_codes(self, hc_closure_codes, labels, num, code=0.1):
2569    def parse_closure_codes(self, hc_closure_codes, labels, num, code=0.1):
2570        labels = labels.astype("float32")
2571        if num > 0:
2572            for x in range(1, num + 1):
2573                y = code + labels[labels == x].size
2574                labels[labels == x] = y
2575            hc_closure_codes += labels
2576        return hc_closure_codes
class Intersect3D(Closures):
2579class Intersect3D(Closures):
2580    def __init__(
2581        self,
2582        faults,
2583        onlaps,
2584        oil_closures,
2585        gas_closures,
2586        brine_closures,
2587        closure_segment_list,
2588        closure_segments,
2589        parameters,
2590    ):
2591        self.closure_segment_list = closure_segment_list
2592        self.closure_segments = closure_segments
2593        self.cfg = parameters
2594
2595        self.fault_throw = faults.max_fault_throw
2596        self.geologic_age = faults.faulted_age_volume
2597        self.geomodel_ng = faults.faulted_net_to_gross
2598        self.faults = self._threshold_volumes(faults.fault_planes.copy())
2599        self.onlaps = self._threshold_volumes(onlaps.copy())
2600        self.oil_closures = self._threshold_volumes(oil_closures.copy())
2601        self.gas_closures = self._threshold_volumes(gas_closures.copy())
2602        self.brine_closures = self._threshold_volumes(brine_closures.copy())
2603
2604        self.wide_faults = None
2605        self.fat_faults = None
2606        self.onlaps_upward = None
2607        self.onlaps_downward = None
2608        self._dilate_faults_and_onlaps()
2609
2610        # Outputs
2611        self.faulted_closures_oil = np.zeros_like(self.oil_closures)
2612        self.faulted_closures_gas = np.zeros_like(self.gas_closures)
2613        self.faulted_closures_brine = np.zeros_like(self.brine_closures)
2614        self.fault_closures_oil_segment_list = list()
2615        self.fault_closures_gas_segment_list = list()
2616        self.fault_closures_brine_segment_list = list()
2617        self.n_fault_closures_oil = 0
2618        self.n_fault_closures_gas = 0
2619        self.n_fault_closures_brine = 0
2620
2621        self.onlap_closures_oil = np.zeros_like(self.oil_closures)
2622        self.onlap_closures_gas = np.zeros_like(self.gas_closures)
2623        self.onlap_closures_brine = np.zeros_like(self.brine_closures)
2624        self.onlap_closures_oil_segment_list = list()
2625        self.onlap_closures_gas_segment_list = list()
2626        self.onlap_closures_brine_segment_list = list()
2627        self.n_onlap_closures_oil = 0
2628        self.n_onlap_closures_gas = 0
2629        self.n_onlap_closures_brine = 0
2630
2631        self.simple_closures_oil = np.zeros_like(self.oil_closures)
2632        self.simple_closures_gas = np.zeros_like(self.gas_closures)
2633        self.simple_closures_brine = np.zeros_like(self.brine_closures)
2634        self.n_4way_closures_oil = 0
2635        self.n_4way_closures_gas = 0
2636        self.n_4way_closures_brine = 0
2637
2638        self.false_closures_oil = np.zeros_like(self.oil_closures)
2639        self.false_closures_gas = np.zeros_like(self.gas_closures)
2640        self.false_closures_brine = np.zeros_like(self.brine_closures)
2641        self.n_false_closures_oil = 0
2642        self.n_false_closures_gas = 0
2643        self.n_false_closures_brine = 0
2644
2645    def find_faulted_closures(self):
2646        for iclosure in self.closure_segment_list:
2647            i, j, k = np.where(self.closure_segments == iclosure)
2648            faults_within_closure = self.wide_faults[i, j, k]
2649            if faults_within_closure.max() > 0:
2650                if self.oil_closures[i, j, k].max() > 0:
2651                    # Faulted oil closure
2652                    self.faulted_closures_oil[i, j, k] = 1.0
2653                    self.n_fault_closures_oil += 1
2654                    self.fault_closures_oil_segment_list.append(iclosure)
2655                elif self.gas_closures[i, j, k].max() > 0:
2656                    # Faulted gas closure
2657                    self.faulted_closures_gas[i, j, k] = 1.0
2658                    self.n_fault_closures_gas += 1
2659                    self.fault_closures_gas_segment_list.append(iclosure)
2660                elif self.brine_closures[i, j, k].max() > 0:
2661                    # Faulted brine closure
2662                    self.faulted_closures_brine[i, j, k] = 1.0
2663                    self.n_fault_closures_brine += 1
2664                    self.fault_closures_brine_segment_list.append(iclosure)
2665                else:
2666                    print(
2667                        "Closure is faulted but does not have oil, gas or brine assigned"
2668                    )
2669
2670    def find_onlap_closures(self):
2671        for iclosure in self.closure_segment_list:
2672            i, j, k = np.where(self.closure_segments == iclosure)
2673            onlaps_within_closure = self.onlaps_upward[i, j, k]
2674            if onlaps_within_closure.max() > 0:
2675                if self.oil_closures[i, j, k].max() > 0:
2676                    self.onlap_closures_oil[i, j, k] = 1.0
2677                    self.n_onlap_closures_oil += 1
2678                    self.onlap_closures_oil_segment_list.append(iclosure)
2679                elif self.gas_closures[i, j, k].max() > 0:
2680                    self.onlap_closures_gas[i, j, k] = 1.0
2681                    self.n_onlap_closures_gas += 1
2682                    self.onlap_closures_gas_segment_list.append(iclosure)
2683                elif self.brine_closures[i, j, k].max() > 0:
2684                    self.onlap_closures_brine[i, j, k] = 1.0
2685                    self.n_onlap_closures_brine += 1
2686                    self.onlap_closures_brine_segment_list.append(iclosure)
2687                else:
2688                    print(
2689                        "Closure is onlap but does not have oil, gas or brine assigned"
2690                    )
2691
2692    def find_simple_closures(self):
2693        for iclosure in self.closure_segment_list:
2694            i, j, k = np.where(self.closure_segments == iclosure)
2695            faults_within_closure = self.wide_faults[i, j, k]
2696            onlaps_within_closure = self.onlaps[i, j, k]
2697            oil_within_closure = self.oil_closures[i, j, k]
2698            gas_within_closure = self.gas_closures[i, j, k]
2699            brine_within_closure = self.brine_closures[i, j, k]
2700            if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0:
2701                if oil_within_closure.max() > 0:
2702                    self.simple_closures_oil[i, j, k] = 1.0
2703                    self.n_4way_closures_oil += 1
2704                elif gas_within_closure.max() > 0:
2705                    self.simple_closures_gas[i, j, k] = 1.0
2706                    self.n_4way_closures_gas += 1
2707                elif brine_within_closure.max() > 0:
2708                    self.simple_closures_brine[i, j, k] = 1.0
2709                    self.n_4way_closures_brine += 1
2710                else:
2711                    print(
2712                        "Closure is not faulted or onlap but does not have oil, gas or brine assigned"
2713                    )
2714
2715    def find_false_closures(self):
2716        for iclosure in self.closure_segment_list:
2717            i, j, k = np.where(self.closure_segments == iclosure)
2718            faults_within_closure = self.fat_faults[i, j, k]
2719            onlaps_within_closure = self.onlaps_downward[i, j, k]
2720            for fluid, false, num in zip(
2721                [self.oil_closures, self.gas_closures, self.brine_closures],
2722                [
2723                    self.false_closures_oil,
2724                    self.false_closures_gas,
2725                    self.false_closures_brine,
2726                ],
2727                [
2728                    self.n_false_closures_oil,
2729                    self.n_false_closures_gas,
2730                    self.n_false_closures_brine,
2731                ],
2732            ):
2733                fluid_within_closure = fluid[i, j, k]
2734                if fluid_within_closure.max() > 0:
2735                    if onlaps_within_closure.max() > 0:
2736                        _faulted_closure_threshold = float(
2737                            faults_within_closure[faults_within_closure > 0].size
2738                            / fluid_within_closure[fluid_within_closure > 0].size
2739                        )
2740                        _onlap_closure_threshold = float(
2741                            onlaps_within_closure[onlaps_within_closure > 0].size
2742                            / fluid_within_closure[fluid_within_closure > 0].size
2743                        )
2744                        if (
2745                            _faulted_closure_threshold > 0.65
2746                            and _onlap_closure_threshold > 0.65
2747                        ):
2748                            false[i, j, k] = 1
2749                            num += 1
2750
2751    def grow_to_fault2(self, closures):
2752        # - grow closures laterally and up within layer and within fault block
2753        print(
2754            "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..."
2755        )
2756        self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2")
2757
2758        dilated_fault_closures = closures.copy()
2759        n_faulted_closures = dilated_fault_closures.max()
2760        labels_clean = self.closure_segments.copy()
2761        labels_clean[closures == 0] = 0
2762        labels_clean_list = list(set(labels_clean.flatten()))
2763        labels_clean_list.remove(0)
2764        print("\n    ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list))
2765        print("    ... grow_to_fault2: faulted_closures = ", labels_clean_list)
2766
2767        # fixme remove this once small closures are found and fixed
2768        voxel_sizes = [
2769            self.closure_segments[self.closure_segments == i].size
2770            for i in labels_clean_list
2771        ]
2772        for _v in voxel_sizes:
2773            print(f"Voxel_Sizes: {_v}")
2774            if _v < self.cfg.closure_min_voxels:
2775                print(_v)
2776
2777        depth_cube = np.zeros(self.geologic_age.shape, float)
2778        _depths = np.arange(self.geologic_age.shape[2])
2779        depth_cube += _depths.reshape(1, 1, self.geologic_age.shape[2])
2780        _ng = self.geomodel_ng.copy()
2781        _age = self.geologic_age.copy()
2782
2783        for il, i in enumerate(labels_clean_list):
2784            fault_blocks_list = list(set(self.fault_throw[labels_clean == i].flatten()))
2785            print("    ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list)
2786            for jl, j in enumerate(fault_blocks_list):
2787                print(
2788                    "\n\n    ... label, throw = ",
2789                    i,
2790                    j,
2791                    list(set(self.fault_throw[labels_clean == i].flatten())),
2792                    labels_clean[labels_clean == i].size,
2793                    self.fault_throw[self.fault_throw == j].size,
2794                    self.fault_throw[
2795                        np.where((labels_clean == i) & (self.fault_throw == j))
2796                    ].size,
2797                )
2798                single_closure = labels_clean * 0.0
2799                size = single_closure[
2800                    np.where(
2801                        (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25)
2802                    )
2803                ].size
2804                if size >= self.cfg.closure_min_voxels:
2805                    print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}")
2806                    single_closure[
2807                        np.where(
2808                            (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25)
2809                        )
2810                    ] = 1
2811                if single_closure[single_closure > 0].size == 0:
2812                    # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0
2813                    labels_clean[np.where(labels_clean == i)] = 0
2814                    continue
2815                avg_ng = _ng[single_closure == 1].mean()
2816                _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int")
2817                _ng_voxels = _ng[single_closure == 1]
2818                _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0]
2819                min_geo_age = _geo_age_voxels.min() - 0.5
2820                avg_geo_age = int(_geo_age_voxels.mean())
2821                max_geo_age = _geo_age_voxels.max() + 0.5
2822                _depth_geobody_voxels = depth_cube[single_closure == 1]
2823                min_depth = _depth_geobody_voxels.min()
2824                max_depth = _depth_geobody_voxels.max()
2825                avg_throw = np.median(self.fault_throw[single_closure == 1])
2826
2827                closure_boundary_cube = closures * 0.0
2828                closure_boundary_cube[
2829                    np.where(
2830                        (_ng > 0.0)
2831                        & (_age > min_geo_age)
2832                        & (_age < max_geo_age)
2833                        & (self.fault_throw == avg_throw)
2834                        & (depth_cube <= max_depth)
2835                    )
2836                ] = 1.0
2837                print(
2838                    "\n    ... grow_to_fault2: closure_boundary_cube voxels = ",
2839                    closure_boundary_cube[closure_boundary_cube == 1].size,
2840                )
2841
2842                n_voxel = single_closure[single_closure == 1].size
2843
2844                original_voxels = n_voxel + 0
2845                print(
2846                    "\n    ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ",
2847                    i,
2848                    j,
2849                    n_voxel,
2850                    (min_geo_age, avg_geo_age, max_geo_age),
2851                    (min_depth, max_depth),
2852                    avg_ng,
2853                    il,
2854                    " / ",
2855                    len(labels_clean_list),
2856                )
2857
2858                grown_closure = single_closure.copy()
2859                grown_closure[depth_cube >= max_depth] = 0
2860                delta_voxel = 0
2861                previous_delta_voxel = 1e9
2862                converged = False
2863                for ii in range(15):
2864                    grown_closure = self.grow_lateral(grown_closure, 1, dist=2)
2865                    grown_closure = self.grow_upward(grown_closure, 1, dist=1)
2866                    # stay within layer, within age, within fault block, above HCWC
2867                    grown_closure[closure_boundary_cube == 0.0] = 0.0
2868                    single_closure = single_closure + grown_closure
2869                    single_closure[single_closure > 0] = i
2870                    new_n_voxel = single_closure[single_closure > 0].size
2871                    previous_delta_voxel = delta_voxel + 0
2872                    delta_voxel = new_n_voxel - n_voxel
2873                    print(
2874                        "    ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel,"
2875                        " delta_voxel>previous_delta_voxel = ",
2876                        i,
2877                        ii,
2878                        new_n_voxel,
2879                        delta_voxel,
2880                        previous_delta_voxel,
2881                        delta_voxel > previous_delta_voxel,
2882                    )
2883                    if n_voxel == new_n_voxel:
2884                        # finish bottom voxel layer near "HCWC"
2885                        grown_closure = self.grow_downward(
2886                            grown_closure, 1, dist=1, verbose=False
2887                        )
2888                        # stay within layer, within age, within fault block, above HCWC
2889                        grown_closure[closure_boundary_cube == 0.0] = 0.0
2890                        single_closure = single_closure + grown_closure
2891                        single_closure[single_closure > 0] = i
2892                        converged = True
2893                        break
2894                    else:
2895                        n_voxel = new_n_voxel
2896                    previous_delta_voxel = delta_voxel + 0
2897                if converged is True:
2898                    labels_clean[single_closure > 0] = i
2899                    msg_postscript = " converged"
2900                else:
2901                    labels_clean[labels_clean == i] = -i
2902                    msg_postscript = " NOT converged"
2903                msg = (
2904                    "closure_id: "
2905                    + format(i, "4d")
2906                    + ", fault_id: "
2907                    + format(int(j + 0.5), "4d")
2908                    + ", original_voxels: "
2909                    + format(original_voxels, "11,.0f")
2910                    + ", new_n_voxel: "
2911                    + format(new_n_voxel, "11,.0f")
2912                    + ", percent_growth: "
2913                    + format(float(new_n_voxel) / original_voxels, "6.2f")
2914                )
2915                print(msg + msg_postscript)
2916                self.cfg.write_to_logfile(msg + msg_postscript)
2917
2918        # Set small closures to 0 after growth
2919        _grown_labels = measure.label(labels_clean, connectivity=2, background=0)
2920        for x in np.unique(_grown_labels):
2921            size = _grown_labels[_grown_labels == x].size
2922            print(f"Size before editing: {size}")
2923            if size < self.cfg.closure_min_voxels:
2924                labels_clean[_grown_labels == x] = 0.0
2925        for x in np.unique(labels_clean):
2926            size = labels_clean[labels_clean == x].size
2927            print(f"Size after editing: {size}")
2928
2929        return labels_clean
2930
2931    def _dilate_faults_and_onlaps(self):
2932        self.wide_faults = self.grow_lateral(self.faults, 9, dist=1, verbose=False)
2933        self.fat_faults = self.grow_lateral(self.faults, 21, dist=1, verbose=False)
2934        mask = np.zeros((1, 1, 3))
2935        mask[0, 0, :2] = 1
2936        self.onlaps_upward = morphology.binary_dilation(self.onlaps, mask)
2937        mask = np.zeros((1, 1, 3))
2938        mask[0, 0, 1:] = 1
2939        self.onlaps_downward = self.onlaps.copy()
2940        for k in range(30):
2941            try:
2942                self.onlaps_downward = morphology.binary_dilation(
2943                    self.onlaps_downward, mask
2944                )
2945            except:
2946                break
2947
2948    @staticmethod
2949    def _threshold_volumes(volume, threshold=0.5):
2950        volume[volume >= threshold] = 1.0
2951        volume[volume < threshold] = 0.0
2952        return volume
2953
2954    @staticmethod
2955    def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False):
2956        from scipy.ndimage import maximum_filter
2957
2958        hdist_size = 2 * hdist + 1
2959        vdist_size = 2 * vdist + 1
2960        mask = np.zeros((hdist_size, hdist_size, vdist_size))
2961        mask[:, :, : vdist + 1] = 1
2962        _geobody = geobody.copy()
2963        if verbose:
2964            print(
2965                " ...grow_up_and_lateral: _geobody.shape = ",
2966                _geobody[_geobody > 0].shape,
2967            )
2968        for k in range(iterations):
2969            try:
2970                _geobody = maximum_filter(_geobody, footprint=mask)
2971                if verbose:
2972                    print(
2973                        " ...grow_up_and_lateral: k, _geobody.shape = ",
2974                        k,
2975                        _geobody[_geobody > 0].shape,
2976                    )
2977            except:
2978                break
2979        return _geobody
2980
2981    @staticmethod
2982    def grow_lateral(geobody, iterations, dist=1, verbose=False):
2983        from scipy.ndimage.morphology import grey_dilation
2984
2985        dist_size = 2 * dist + 1
2986        mask = np.zeros((dist_size, dist_size, 1))
2987        mask[:, :, :] = 1
2988        _geobody = geobody.copy()
2989        if verbose:
2990            print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape)
2991        for k in range(iterations):
2992            try:
2993                _geobody = grey_dilation(_geobody, footprint=mask)
2994                if verbose:
2995                    print(
2996                        " ...grow_lateral: k, _geobody.shape = ",
2997                        k,
2998                        _geobody[_geobody > 0].shape,
2999                    )
3000            except:
3001                break
3002        return _geobody
3003
3004    @staticmethod
3005    def grow_upward(geobody, iterations, dist=1, verbose=False):
3006        from scipy.ndimage.morphology import grey_dilation
3007
3008        dist_size = 2 * dist + 1
3009        mask = np.zeros((1, 1, dist_size))
3010        mask[0, 0, : dist + 1] = 1
3011        _geobody = geobody.copy()
3012        if verbose:
3013            print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape)
3014        for k in range(iterations):
3015            try:
3016                _geobody = grey_dilation(_geobody, footprint=mask)
3017                if verbose:
3018                    print(
3019                        " ...grow_upward: k, _geobody.shape = ",
3020                        k,
3021                        _geobody[_geobody > 0].shape,
3022                    )
3023            except:
3024                break
3025        return _geobody
3026
3027    @staticmethod
3028    def grow_downward(geobody, iterations, dist=1, verbose=False):
3029        from scipy.ndimage.morphology import grey_dilation
3030
3031        dist_size = 2 * dist + 1
3032        mask = np.zeros((1, 1, dist_size))
3033        mask[0, 0, dist:] = 1
3034        _geobody = geobody.copy()
3035        if verbose:
3036            print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape)
3037        for k in range(iterations):
3038            try:
3039                _geobody = grey_dilation(_geobody, footprint=mask)
3040                if verbose:
3041                    print(
3042                        " ...grow_downward: k, _geobody.shape = ",
3043                        k,
3044                        _geobody[_geobody > 0].shape,
3045                    )
3046            except:
3047                break
3048        return _geobody
Geomodel

The class of the Geomodel object.

This class contains all the items that make up the Geologic model.

Parameters
  • parameters (datagenerator.Parameters): Parameter object storing all model parameters.
  • depth_maps (np.ndarray): A numpy array containing the depth maps.
  • onlap_horizon_list (list): A list of the onlap horizons.
  • facies (np.ndarray): The generated facies.
Returns
  • None
Intersect3D( faults, onlaps, oil_closures, gas_closures, brine_closures, closure_segment_list, closure_segments, parameters)
2580    def __init__(
2581        self,
2582        faults,
2583        onlaps,
2584        oil_closures,
2585        gas_closures,
2586        brine_closures,
2587        closure_segment_list,
2588        closure_segments,
2589        parameters,
2590    ):
2591        self.closure_segment_list = closure_segment_list
2592        self.closure_segments = closure_segments
2593        self.cfg = parameters
2594
2595        self.fault_throw = faults.max_fault_throw
2596        self.geologic_age = faults.faulted_age_volume
2597        self.geomodel_ng = faults.faulted_net_to_gross
2598        self.faults = self._threshold_volumes(faults.fault_planes.copy())
2599        self.onlaps = self._threshold_volumes(onlaps.copy())
2600        self.oil_closures = self._threshold_volumes(oil_closures.copy())
2601        self.gas_closures = self._threshold_volumes(gas_closures.copy())
2602        self.brine_closures = self._threshold_volumes(brine_closures.copy())
2603
2604        self.wide_faults = None
2605        self.fat_faults = None
2606        self.onlaps_upward = None
2607        self.onlaps_downward = None
2608        self._dilate_faults_and_onlaps()
2609
2610        # Outputs
2611        self.faulted_closures_oil = np.zeros_like(self.oil_closures)
2612        self.faulted_closures_gas = np.zeros_like(self.gas_closures)
2613        self.faulted_closures_brine = np.zeros_like(self.brine_closures)
2614        self.fault_closures_oil_segment_list = list()
2615        self.fault_closures_gas_segment_list = list()
2616        self.fault_closures_brine_segment_list = list()
2617        self.n_fault_closures_oil = 0
2618        self.n_fault_closures_gas = 0
2619        self.n_fault_closures_brine = 0
2620
2621        self.onlap_closures_oil = np.zeros_like(self.oil_closures)
2622        self.onlap_closures_gas = np.zeros_like(self.gas_closures)
2623        self.onlap_closures_brine = np.zeros_like(self.brine_closures)
2624        self.onlap_closures_oil_segment_list = list()
2625        self.onlap_closures_gas_segment_list = list()
2626        self.onlap_closures_brine_segment_list = list()
2627        self.n_onlap_closures_oil = 0
2628        self.n_onlap_closures_gas = 0
2629        self.n_onlap_closures_brine = 0
2630
2631        self.simple_closures_oil = np.zeros_like(self.oil_closures)
2632        self.simple_closures_gas = np.zeros_like(self.gas_closures)
2633        self.simple_closures_brine = np.zeros_like(self.brine_closures)
2634        self.n_4way_closures_oil = 0
2635        self.n_4way_closures_gas = 0
2636        self.n_4way_closures_brine = 0
2637
2638        self.false_closures_oil = np.zeros_like(self.oil_closures)
2639        self.false_closures_gas = np.zeros_like(self.gas_closures)
2640        self.false_closures_brine = np.zeros_like(self.brine_closures)
2641        self.n_false_closures_oil = 0
2642        self.n_false_closures_gas = 0
2643        self.n_false_closures_brine = 0

__init__

Initializer for the Geomodel class.

Parameters
  • parameters (datagenerator.Parameters): Parameter object storing all model parameters.
  • depth_maps (np.ndarray): A numpy array containing the depth maps.
  • onlap_horizon_list (list): A list of the onlap horizons.
  • facies (np.ndarray): The generated facies.
def find_faulted_closures(self):
2645    def find_faulted_closures(self):
2646        for iclosure in self.closure_segment_list:
2647            i, j, k = np.where(self.closure_segments == iclosure)
2648            faults_within_closure = self.wide_faults[i, j, k]
2649            if faults_within_closure.max() > 0:
2650                if self.oil_closures[i, j, k].max() > 0:
2651                    # Faulted oil closure
2652                    self.faulted_closures_oil[i, j, k] = 1.0
2653                    self.n_fault_closures_oil += 1
2654                    self.fault_closures_oil_segment_list.append(iclosure)
2655                elif self.gas_closures[i, j, k].max() > 0:
2656                    # Faulted gas closure
2657                    self.faulted_closures_gas[i, j, k] = 1.0
2658                    self.n_fault_closures_gas += 1
2659                    self.fault_closures_gas_segment_list.append(iclosure)
2660                elif self.brine_closures[i, j, k].max() > 0:
2661                    # Faulted brine closure
2662                    self.faulted_closures_brine[i, j, k] = 1.0
2663                    self.n_fault_closures_brine += 1
2664                    self.fault_closures_brine_segment_list.append(iclosure)
2665                else:
2666                    print(
2667                        "Closure is faulted but does not have oil, gas or brine assigned"
2668                    )
def find_onlap_closures(self):
2670    def find_onlap_closures(self):
2671        for iclosure in self.closure_segment_list:
2672            i, j, k = np.where(self.closure_segments == iclosure)
2673            onlaps_within_closure = self.onlaps_upward[i, j, k]
2674            if onlaps_within_closure.max() > 0:
2675                if self.oil_closures[i, j, k].max() > 0:
2676                    self.onlap_closures_oil[i, j, k] = 1.0
2677                    self.n_onlap_closures_oil += 1
2678                    self.onlap_closures_oil_segment_list.append(iclosure)
2679                elif self.gas_closures[i, j, k].max() > 0:
2680                    self.onlap_closures_gas[i, j, k] = 1.0
2681                    self.n_onlap_closures_gas += 1
2682                    self.onlap_closures_gas_segment_list.append(iclosure)
2683                elif self.brine_closures[i, j, k].max() > 0:
2684                    self.onlap_closures_brine[i, j, k] = 1.0
2685                    self.n_onlap_closures_brine += 1
2686                    self.onlap_closures_brine_segment_list.append(iclosure)
2687                else:
2688                    print(
2689                        "Closure is onlap but does not have oil, gas or brine assigned"
2690                    )
def find_simple_closures(self):
2692    def find_simple_closures(self):
2693        for iclosure in self.closure_segment_list:
2694            i, j, k = np.where(self.closure_segments == iclosure)
2695            faults_within_closure = self.wide_faults[i, j, k]
2696            onlaps_within_closure = self.onlaps[i, j, k]
2697            oil_within_closure = self.oil_closures[i, j, k]
2698            gas_within_closure = self.gas_closures[i, j, k]
2699            brine_within_closure = self.brine_closures[i, j, k]
2700            if faults_within_closure.max() == 0 and onlaps_within_closure.max() == 0:
2701                if oil_within_closure.max() > 0:
2702                    self.simple_closures_oil[i, j, k] = 1.0
2703                    self.n_4way_closures_oil += 1
2704                elif gas_within_closure.max() > 0:
2705                    self.simple_closures_gas[i, j, k] = 1.0
2706                    self.n_4way_closures_gas += 1
2707                elif brine_within_closure.max() > 0:
2708                    self.simple_closures_brine[i, j, k] = 1.0
2709                    self.n_4way_closures_brine += 1
2710                else:
2711                    print(
2712                        "Closure is not faulted or onlap but does not have oil, gas or brine assigned"
2713                    )
def find_false_closures(self):
2715    def find_false_closures(self):
2716        for iclosure in self.closure_segment_list:
2717            i, j, k = np.where(self.closure_segments == iclosure)
2718            faults_within_closure = self.fat_faults[i, j, k]
2719            onlaps_within_closure = self.onlaps_downward[i, j, k]
2720            for fluid, false, num in zip(
2721                [self.oil_closures, self.gas_closures, self.brine_closures],
2722                [
2723                    self.false_closures_oil,
2724                    self.false_closures_gas,
2725                    self.false_closures_brine,
2726                ],
2727                [
2728                    self.n_false_closures_oil,
2729                    self.n_false_closures_gas,
2730                    self.n_false_closures_brine,
2731                ],
2732            ):
2733                fluid_within_closure = fluid[i, j, k]
2734                if fluid_within_closure.max() > 0:
2735                    if onlaps_within_closure.max() > 0:
2736                        _faulted_closure_threshold = float(
2737                            faults_within_closure[faults_within_closure > 0].size
2738                            / fluid_within_closure[fluid_within_closure > 0].size
2739                        )
2740                        _onlap_closure_threshold = float(
2741                            onlaps_within_closure[onlaps_within_closure > 0].size
2742                            / fluid_within_closure[fluid_within_closure > 0].size
2743                        )
2744                        if (
2745                            _faulted_closure_threshold > 0.65
2746                            and _onlap_closure_threshold > 0.65
2747                        ):
2748                            false[i, j, k] = 1
2749                            num += 1
def grow_to_fault2(self, closures):
2751    def grow_to_fault2(self, closures):
2752        # - grow closures laterally and up within layer and within fault block
2753        print(
2754            "\n\n ... grow_to_fault2: grow closures laterally and up within layer and within fault block ..."
2755        )
2756        self.cfg.write_to_logfile("growing closures to fault plane: grow_to_fault2")
2757
2758        dilated_fault_closures = closures.copy()
2759        n_faulted_closures = dilated_fault_closures.max()
2760        labels_clean = self.closure_segments.copy()
2761        labels_clean[closures == 0] = 0
2762        labels_clean_list = list(set(labels_clean.flatten()))
2763        labels_clean_list.remove(0)
2764        print("\n    ... grow_to_fault2: n_faulted_closures = ", len(labels_clean_list))
2765        print("    ... grow_to_fault2: faulted_closures = ", labels_clean_list)
2766
2767        # fixme remove this once small closures are found and fixed
2768        voxel_sizes = [
2769            self.closure_segments[self.closure_segments == i].size
2770            for i in labels_clean_list
2771        ]
2772        for _v in voxel_sizes:
2773            print(f"Voxel_Sizes: {_v}")
2774            if _v < self.cfg.closure_min_voxels:
2775                print(_v)
2776
2777        depth_cube = np.zeros(self.geologic_age.shape, float)
2778        _depths = np.arange(self.geologic_age.shape[2])
2779        depth_cube += _depths.reshape(1, 1, self.geologic_age.shape[2])
2780        _ng = self.geomodel_ng.copy()
2781        _age = self.geologic_age.copy()
2782
2783        for il, i in enumerate(labels_clean_list):
2784            fault_blocks_list = list(set(self.fault_throw[labels_clean == i].flatten()))
2785            print("    ... grow_to_fault2: fault_blocks_list = ", fault_blocks_list)
2786            for jl, j in enumerate(fault_blocks_list):
2787                print(
2788                    "\n\n    ... label, throw = ",
2789                    i,
2790                    j,
2791                    list(set(self.fault_throw[labels_clean == i].flatten())),
2792                    labels_clean[labels_clean == i].size,
2793                    self.fault_throw[self.fault_throw == j].size,
2794                    self.fault_throw[
2795                        np.where((labels_clean == i) & (self.fault_throw == j))
2796                    ].size,
2797                )
2798                single_closure = labels_clean * 0.0
2799                size = single_closure[
2800                    np.where(
2801                        (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25)
2802                    )
2803                ].size
2804                if size >= self.cfg.closure_min_voxels:
2805                    print(f"Label: {i}, fault_block: {j}, Voxel_Count: {size}")
2806                    single_closure[
2807                        np.where(
2808                            (labels_clean == i) & (np.abs(self.fault_throw - j) < 0.25)
2809                        )
2810                    ] = 1
2811                if single_closure[single_closure > 0].size == 0:
2812                    # labels_clean[np.where((labels_clean == i) & (np.abs(self.fault_throw - j) < .25))] = 0
2813                    labels_clean[np.where(labels_clean == i)] = 0
2814                    continue
2815                avg_ng = _ng[single_closure == 1].mean()
2816                _geo_age_voxels = (_age[single_closure == 1] + 0.5).astype("int")
2817                _ng_voxels = _ng[single_closure == 1]
2818                _geo_age_voxels = _geo_age_voxels[_ng_voxels >= avg_ng / 2.0]
2819                min_geo_age = _geo_age_voxels.min() - 0.5
2820                avg_geo_age = int(_geo_age_voxels.mean())
2821                max_geo_age = _geo_age_voxels.max() + 0.5
2822                _depth_geobody_voxels = depth_cube[single_closure == 1]
2823                min_depth = _depth_geobody_voxels.min()
2824                max_depth = _depth_geobody_voxels.max()
2825                avg_throw = np.median(self.fault_throw[single_closure == 1])
2826
2827                closure_boundary_cube = closures * 0.0
2828                closure_boundary_cube[
2829                    np.where(
2830                        (_ng > 0.0)
2831                        & (_age > min_geo_age)
2832                        & (_age < max_geo_age)
2833                        & (self.fault_throw == avg_throw)
2834                        & (depth_cube <= max_depth)
2835                    )
2836                ] = 1.0
2837                print(
2838                    "\n    ... grow_to_fault2: closure_boundary_cube voxels = ",
2839                    closure_boundary_cube[closure_boundary_cube == 1].size,
2840                )
2841
2842                n_voxel = single_closure[single_closure == 1].size
2843
2844                original_voxels = n_voxel + 0
2845                print(
2846                    "\n    ... closure label number, avg_throw, geobody shape, geo_age min/mean/max, depth min/max, avg_ng = ",
2847                    i,
2848                    j,
2849                    n_voxel,
2850                    (min_geo_age, avg_geo_age, max_geo_age),
2851                    (min_depth, max_depth),
2852                    avg_ng,
2853                    il,
2854                    " / ",
2855                    len(labels_clean_list),
2856                )
2857
2858                grown_closure = single_closure.copy()
2859                grown_closure[depth_cube >= max_depth] = 0
2860                delta_voxel = 0
2861                previous_delta_voxel = 1e9
2862                converged = False
2863                for ii in range(15):
2864                    grown_closure = self.grow_lateral(grown_closure, 1, dist=2)
2865                    grown_closure = self.grow_upward(grown_closure, 1, dist=1)
2866                    # stay within layer, within age, within fault block, above HCWC
2867                    grown_closure[closure_boundary_cube == 0.0] = 0.0
2868                    single_closure = single_closure + grown_closure
2869                    single_closure[single_closure > 0] = i
2870                    new_n_voxel = single_closure[single_closure > 0].size
2871                    previous_delta_voxel = delta_voxel + 0
2872                    delta_voxel = new_n_voxel - n_voxel
2873                    print(
2874                        "    ... i, ii, closure label number, geobody shape, delta_voxel, previous_delta_voxel,"
2875                        " delta_voxel>previous_delta_voxel = ",
2876                        i,
2877                        ii,
2878                        new_n_voxel,
2879                        delta_voxel,
2880                        previous_delta_voxel,
2881                        delta_voxel > previous_delta_voxel,
2882                    )
2883                    if n_voxel == new_n_voxel:
2884                        # finish bottom voxel layer near "HCWC"
2885                        grown_closure = self.grow_downward(
2886                            grown_closure, 1, dist=1, verbose=False
2887                        )
2888                        # stay within layer, within age, within fault block, above HCWC
2889                        grown_closure[closure_boundary_cube == 0.0] = 0.0
2890                        single_closure = single_closure + grown_closure
2891                        single_closure[single_closure > 0] = i
2892                        converged = True
2893                        break
2894                    else:
2895                        n_voxel = new_n_voxel
2896                    previous_delta_voxel = delta_voxel + 0
2897                if converged is True:
2898                    labels_clean[single_closure > 0] = i
2899                    msg_postscript = " converged"
2900                else:
2901                    labels_clean[labels_clean == i] = -i
2902                    msg_postscript = " NOT converged"
2903                msg = (
2904                    "closure_id: "
2905                    + format(i, "4d")
2906                    + ", fault_id: "
2907                    + format(int(j + 0.5), "4d")
2908                    + ", original_voxels: "
2909                    + format(original_voxels, "11,.0f")
2910                    + ", new_n_voxel: "
2911                    + format(new_n_voxel, "11,.0f")
2912                    + ", percent_growth: "
2913                    + format(float(new_n_voxel) / original_voxels, "6.2f")
2914                )
2915                print(msg + msg_postscript)
2916                self.cfg.write_to_logfile(msg + msg_postscript)
2917
2918        # Set small closures to 0 after growth
2919        _grown_labels = measure.label(labels_clean, connectivity=2, background=0)
2920        for x in np.unique(_grown_labels):
2921            size = _grown_labels[_grown_labels == x].size
2922            print(f"Size before editing: {size}")
2923            if size < self.cfg.closure_min_voxels:
2924                labels_clean[_grown_labels == x] = 0.0
2925        for x in np.unique(labels_clean):
2926            size = labels_clean[labels_clean == x].size
2927            print(f"Size after editing: {size}")
2928
2929        return labels_clean
@staticmethod
def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False):
2954    @staticmethod
2955    def grow_up_and_lateral(geobody, iterations, vdist=1, hdist=1, verbose=False):
2956        from scipy.ndimage import maximum_filter
2957
2958        hdist_size = 2 * hdist + 1
2959        vdist_size = 2 * vdist + 1
2960        mask = np.zeros((hdist_size, hdist_size, vdist_size))
2961        mask[:, :, : vdist + 1] = 1
2962        _geobody = geobody.copy()
2963        if verbose:
2964            print(
2965                " ...grow_up_and_lateral: _geobody.shape = ",
2966                _geobody[_geobody > 0].shape,
2967            )
2968        for k in range(iterations):
2969            try:
2970                _geobody = maximum_filter(_geobody, footprint=mask)
2971                if verbose:
2972                    print(
2973                        " ...grow_up_and_lateral: k, _geobody.shape = ",
2974                        k,
2975                        _geobody[_geobody > 0].shape,
2976                    )
2977            except:
2978                break
2979        return _geobody
@staticmethod
def grow_lateral(geobody, iterations, dist=1, verbose=False):
2981    @staticmethod
2982    def grow_lateral(geobody, iterations, dist=1, verbose=False):
2983        from scipy.ndimage.morphology import grey_dilation
2984
2985        dist_size = 2 * dist + 1
2986        mask = np.zeros((dist_size, dist_size, 1))
2987        mask[:, :, :] = 1
2988        _geobody = geobody.copy()
2989        if verbose:
2990            print(" ...grow_lateral: _geobody.shape = ", _geobody[_geobody > 0].shape)
2991        for k in range(iterations):
2992            try:
2993                _geobody = grey_dilation(_geobody, footprint=mask)
2994                if verbose:
2995                    print(
2996                        " ...grow_lateral: k, _geobody.shape = ",
2997                        k,
2998                        _geobody[_geobody > 0].shape,
2999                    )
3000            except:
3001                break
3002        return _geobody
@staticmethod
def grow_upward(geobody, iterations, dist=1, verbose=False):
3004    @staticmethod
3005    def grow_upward(geobody, iterations, dist=1, verbose=False):
3006        from scipy.ndimage.morphology import grey_dilation
3007
3008        dist_size = 2 * dist + 1
3009        mask = np.zeros((1, 1, dist_size))
3010        mask[0, 0, : dist + 1] = 1
3011        _geobody = geobody.copy()
3012        if verbose:
3013            print(" ...grow_upward: _geobody.shape = ", _geobody[_geobody > 0].shape)
3014        for k in range(iterations):
3015            try:
3016                _geobody = grey_dilation(_geobody, footprint=mask)
3017                if verbose:
3018                    print(
3019                        " ...grow_upward: k, _geobody.shape = ",
3020                        k,
3021                        _geobody[_geobody > 0].shape,
3022                    )
3023            except:
3024                break
3025        return _geobody
@staticmethod
def grow_downward(geobody, iterations, dist=1, verbose=False):
3027    @staticmethod
3028    def grow_downward(geobody, iterations, dist=1, verbose=False):
3029        from scipy.ndimage.morphology import grey_dilation
3030
3031        dist_size = 2 * dist + 1
3032        mask = np.zeros((1, 1, dist_size))
3033        mask[0, 0, dist:] = 1
3034        _geobody = geobody.copy()
3035        if verbose:
3036            print(" ...grow_downward: _geobody.shape = ", _geobody[_geobody > 0].shape)
3037        for k in range(iterations):
3038            try:
3039                _geobody = grey_dilation(_geobody, footprint=mask)
3040                if verbose:
3041                    print(
3042                        " ...grow_downward: k, _geobody.shape = ",
3043                        k,
3044                        _geobody[_geobody > 0].shape,
3045                    )
3046            except:
3047                break
3048        return _geobody
def variable_max_column_height(top_lith_idx, num_horizons, hmin=25, hmax=200):
3051def variable_max_column_height(top_lith_idx, num_horizons, hmin=25, hmax=200):
3052    """
3053    Create a 1-D array of maximum column heights using linear function in layer numbers
3054    Shallow closures will have small vertical closure heights
3055    Deep closures will have larger vertical closure heights
3056
3057    Would be better to use a pressure profile to determine maximum column heights at given depths
3058
3059    :param top_lith_idx: 1-D array of horizon numbers corresponding to top of layers where lithology changes
3060    :param num_horizons: Total number of horizons in model
3061    :param hmin: Minimum column height to use in linear function
3062    :param hmax: Maximum column height to use in linear function
3063    :return: 1-D array of column heights of closures
3064    """
3065    # Use a linear function to determine max column height based on layer number
3066    column_heights = np.linspace(hmin, hmax, num=num_horizons)
3067    max_col_heights = column_heights[top_lith_idx]
3068    return max_col_heights

Create a 1-D array of maximum column heights using linear function in layer numbers Shallow closures will have small vertical closure heights Deep closures will have larger vertical closure heights

Would be better to use a pressure profile to determine maximum column heights at given depths

Parameters
  • top_lith_idx: 1-D array of horizon numbers corresponding to top of layers where lithology changes
  • num_horizons: Total number of horizons in model
  • hmin: Minimum column height to use in linear function
  • hmax: Maximum column height to use in linear function
Returns

1-D array of column heights of closures

def fill_to_spill(test_array, array_flags, empty_value=1e+22, quiet=True):
3072def fill_to_spill(test_array, array_flags, empty_value=1.0e22, quiet=True):
3073    if not quiet:
3074        print("   ... start fillToSpill ......")
3075    temp_array = test_array.copy()
3076    flags = array_flags.copy()
3077    test_array_max = 2.0 * (temp_array[~np.isnan(temp_array)]).max()
3078    temp_array[array_flags == 255] = -empty_value
3079    flood_filled = test_array_max - flood_fill_heap(
3080        test_array_max - temp_array, empty_value=empty_value
3081    )
3082    if not quiet:
3083        print("   ... finish fillToSpill ......")
3084
3085    flood_filled[array_flags != 1] = 0
3086    flood_filled[flood_filled == 1.0e5] = 0
3087    flags[flood_filled == empty_value] = 0
3088
3089    return flood_filled
def flood_fill_heap(test_array, empty_value=1e+22, quiet=True):
3092def flood_fill_heap(test_array, empty_value=1.0e22, quiet=True):
3093    # from internet: http://arcgisandpython.blogspot.co.uk/2012/01/python-flood-fill-algorithm.html
3094
3095    import heapq
3096    from scipy import ndimage
3097
3098    input_array = np.copy(test_array)
3099    num_validPoints = (
3100        test_array.flatten().shape[0]
3101        - input_array[np.isnan(input_array)].shape[0]
3102        - input_array[input_array > empty_value / 2].shape[0]
3103    )
3104    if not quiet:
3105        print(
3106            "     ... flood_fill_heap ... number of valid input horizon picks = ",
3107            num_validPoints,
3108        )
3109
3110    validPoints = input_array[~np.isnan(input_array)]
3111    validPoints = validPoints[validPoints < empty_value / 2]
3112    validPoints = validPoints[validPoints < 1.0e5]
3113    validPoints = validPoints[validPoints > np.percentile(validPoints, 2)]
3114
3115    if len(validPoints) > 2:
3116        amin = validPoints.min()
3117        amax = validPoints.max()
3118    else:
3119        return test_array
3120
3121    if not quiet:
3122        print(
3123            "    ... validPoints stats = ",
3124            validPoints.min(),
3125            np.median(validPoints),
3126            validPoints.mean(),
3127            validPoints.max(),
3128        )
3129        print(
3130            "    ... validPoints %tiles = ",
3131            np.percentile(validPoints, 0),
3132            np.percentile(validPoints, 1),
3133            np.percentile(validPoints, 5),
3134            np.percentile(validPoints, 10),
3135            np.percentile(validPoints, 25),
3136            np.percentile(validPoints, 50),
3137            np.percentile(validPoints, 75),
3138            np.percentile(validPoints, 90),
3139            np.percentile(validPoints, 95),
3140            np.percentile(validPoints, 99),
3141            np.percentile(validPoints, 100),
3142        )
3143        from datagenerator.util import import_matplotlib
3144
3145        plt = import_matplotlib()
3146        plt.figure(5)
3147        plt.clf()
3148        plt.imshow(np.flipud(input_array), vmin=amin, vmax=amax, cmap="jet_r")
3149        plt.colorbar()
3150        plt.show()
3151        plt.savefig("flood_fill.png", format="png")
3152        plt.close()
3153
3154        print("     ... min & max for surface = ", amin, amax)
3155
3156    # set empty values and nan's to huge
3157    input_array[np.isnan(input_array)] = empty_value
3158
3159    # Set h_max to a value larger than the array maximum to ensure that the while loop will terminate
3160    h_max = np.max(input_array * 2.0)
3161
3162    # Build mask of cells with data not on the edge of the image
3163    # Use 3x3 square structuring element
3164    el = ndimage.generate_binary_structure(2, 2).astype(np.int)
3165    inside_mask = ndimage.binary_erosion(~np.isnan(input_array), structure=el)
3166    inside_mask[input_array == empty_value] = False
3167    edge_mask = np.invert(inside_mask)
3168    # Initialize output array as max value test_array except edges
3169    output_array = np.copy(input_array)
3170    output_array[inside_mask] = h_max
3171
3172    if not quiet:
3173        plt.figure(6)
3174        plt.clf()
3175        plt.imshow(np.flipud(input_array), cmap="jet_r")
3176        plt.colorbar()
3177        plt.show()
3178        plt.savefig("flood_fill2.png", format="png")
3179        plt.close()
3180
3181    # Build priority queue and place edge pixels into priority queue
3182    # Last value is flag to indicate if cell is an edge cell
3183    put = heapq.heappush
3184    get = heapq.heappop
3185    fill_heap = [
3186        (output_array[t_row, t_col], int(t_row), int(t_col), 1)
3187        for t_row, t_col in np.transpose(np.where(edge_mask))
3188    ]
3189    heapq.heapify(fill_heap)
3190
3191    # Iterate until priority queue is empty
3192    while 1:
3193        try:
3194            h_crt, t_row, t_col, edge_flag = get(fill_heap)
3195        except IndexError:
3196            break
3197        for n_row, n_col in [
3198            ((t_row - 1), t_col),
3199            ((t_row + 1), t_col),
3200            (t_row, (t_col - 1)),
3201            (t_row, (t_col + 1)),
3202        ]:
3203            # Skip cell if outside array edges
3204            if edge_flag:
3205                try:
3206                    if not inside_mask[n_row, n_col]:
3207                        continue
3208                except IndexError:
3209                    continue
3210            if output_array[n_row, n_col] == h_max:
3211                output_array[n_row, n_col] = max(h_crt, input_array[n_row, n_col])
3212                put(fill_heap, (output_array[n_row, n_col], n_row, n_col, 0))
3213    output_array[output_array == empty_value] = np.nan
3214    return output_array
def get_top_of_closure(inarray, pad_up=0, pad_down=0):
3319def get_top_of_closure(inarray, pad_up=0, pad_down=0):
3320    """Create a mask leaving only the top of a closure."""
3321    mask = inarray != 0
3322    t = np.where(mask.any(axis=-1), mask.argmax(axis=-1), -1)
3323    xy = np.argwhere(t > 0)
3324    z = t[t > 0]
3325    outarray = np.zeros_like(inarray)
3326    for (x, y), z in zip(xy, z):
3327        zmin = z - pad_up
3328        zmax = z + pad_down + 1
3329        outarray[x, y, zmin:zmax] = 1
3330    return outarray

Create a mask leaving only the top of a closure.

def lsq(x, y, axis=-1):
3333def lsq(x, y, axis=-1):
3334    ###
3335    ### compute the slope and intercept for an array with points to be fit
3336    ### in the last dimension. can be in other axis using the 'axis' parmameter.
3337    ###
3338    ### returns:
3339    ### - intercept
3340    ### - slope
3341    ### - pearson r (normalized cross-correlation coefficient)
3342    ###
3343    ### output will have dimensions of input with one less axis
3344    ### - (specified by axis parameter)
3345    ###
3346
3347    """
3348    # compute x and y with mean removed
3349    x_zeromean = x * 1.
3350    x_zeromean -= x.mean(axis=axis).reshape(x.shape[0],x.shape[1],1)
3351    y_zeromean = y * 1.
3352    y_zeromean -= y.mean(axis=axis).reshape(y.shape[0],y.shape[1],1)
3353    """
3354
3355    # compute pearsonr
3356    r = np.sum(x * y, axis=axis) - np.sum(x) * np.sum(y, axis=axis) / y.shape[axis]
3357    r /= np.sqrt(
3358        (np.sum(x ** 2, axis=axis) - np.sum(x, axis=axis) ** 2 / y.shape[axis])
3359        * (np.sum(y ** 2, axis=axis) - np.sum(y, axis=axis) ** 2 / y.shape[axis])
3360    )
3361
3362    # compute slope
3363    slope = r * y.std(axis=axis) / x.std(axis=axis)
3364
3365    # compute intercept
3366    intercept = y.mean(axis=axis) - slope * x.mean(axis=axis)
3367
3368    return intercept, slope, r

compute x and y with mean removed

x_zeromean = x * 1. x_zeromean -= x.mean(axis=axis).reshape(x.shape[0],x.shape[1],1) y_zeromean = y * 1. y_zeromean -= y.mean(axis=axis).reshape(y.shape[0],y.shape[1],1)

def compute_ai_gi(parameters, seismic_data):
3371def compute_ai_gi(parameters, seismic_data):
3372    """[summary]
3373
3374    Args:
3375        cfg (Parameter class object): Model Parameters
3376        seismic_data (np.array): Seismic data with shape n * x * y * z,
3377                                 where n is number of angle stacks
3378    """
3379    inc_angles = np.array(parameters.incident_angles)
3380    inc_angles = np.sin(inc_angles * np.pi / 180.0) ** 2
3381    inc_angles = inc_angles.reshape(len(inc_angles), 1, 1, 1)
3382
3383    intercept, slope, _ = lsq(inc_angles, seismic_data, axis=0)
3384
3385    intercept[np.isnan(intercept)] = 0.0
3386    slope[np.isnan(slope)] = 0.0
3387    intercept[np.isinf(intercept)] = 0.0
3388    slope[np.isinf(slope)] = 0.0
3389    return intercept, slope

[summary]

Args: cfg (Parameter class object): Model Parameters seismic_data (np.array): Seismic data with shape n * x * y * z, where n is number of angle stacks