datagenerator.Horizons
1import os 2import math 3import numpy as np 4from scipy import stats 5from scipy.interpolate import griddata 6from datagenerator.Parameters import Parameters 7from itertools import groupby 8 9class Horizons: 10 def __init__(self, parameters): 11 self.cfg = parameters 12 self.max_layers = 0 13 14 def insert_feature_into_horizon_stack(self, feature, layer_number, maps): 15 """ 16 Insert an object into horizons which is to be inserted into a single layer 17 18 Made for fans, but should work for any object. 19 Shallower layer thicknesses are adjusted to insert the feature (clipped downwards onto the feature). 20 """ 21 layer_count = 1 22 if feature.ndim == 3: 23 layer_count = feature.shape[-1] 24 new_maps = np.zeros( 25 ( 26 self.cfg.cube_shape[0], 27 self.cfg.cube_shape[1], 28 maps.shape[2] + layer_count, 29 ) 30 ) 31 # Shift horizons below layer downwards by 1 to insert object in specified layer 32 new_maps[..., layer_number + layer_count :] = maps[..., layer_number:] 33 # Insert object into layer_number 34 new_maps[..., layer_number] = maps[..., layer_number] - feature 35 # copy horizons above layer_number into the new set of maps 36 new_maps[..., :layer_number] = maps[..., :layer_number] 37 38 # Don't allow negative thickness after inserting the fan 39 for i in range(new_maps.shape[-1] - 1, 1, -1): 40 layer_thickness = new_maps[..., i] - new_maps[..., i - 1] 41 if np.min(layer_thickness) < 0: 42 np.clip(layer_thickness, 0, a_max=None, out=layer_thickness) 43 new_maps[..., i - 1] = new_maps[..., i] - layer_thickness 44 # Increase the max number of layers in model by 1 45 self.max_layers += layer_count 46 return new_maps 47 48 def insert_seafloor(self, maps): 49 if self.cfg.verbose: 50 print("\n ... inserting 'water bottom' reflector in work cube ...\n") 51 wb_time_map = maps[:, :, 1] - 1.5 52 wb_stats = [ 53 f"{x * self.cfg.digi / self.cfg.infill_factor:.2f}" 54 for x in [wb_time_map.min(), wb_time_map.mean(), wb_time_map.max()] 55 ] 56 self.cfg.write_to_logfile("Seabed Min: {}, Mean: {}, Max: {}".format(*wb_stats)) 57 58 self.cfg.write_to_logfile( 59 msg=None, 60 mainkey="model_parameters", 61 subkey="seabed_min", 62 val=wb_time_map.min(), 63 ) 64 self.cfg.write_to_logfile( 65 msg=None, 66 mainkey="model_parameters", 67 subkey="seabed_mean", 68 val=wb_time_map.mean(), 69 ) 70 self.cfg.write_to_logfile( 71 msg=None, 72 mainkey="model_parameters", 73 subkey="seabed_max", 74 val=wb_time_map.max(), 75 ) 76 77 maps[:, :, 0] = wb_time_map.copy() 78 return maps 79 80 def create_random_net_over_gross_map( 81 self, avg=(0.45, 0.9), stdev=(0.01, 0.05), octave=9 82 ): 83 random_net_over_gross_map = self._perlin(base=None, octave=octave) 84 85 avg_net_over_gross = np.random.uniform(*avg) 86 avg_net_over_gross_stdev = np.random.uniform(*stdev) 87 88 # set stdev and mean of map to desired values 89 random_net_over_gross_map -= random_net_over_gross_map.mean() 90 random_net_over_gross_map *= ( 91 avg_net_over_gross_stdev / random_net_over_gross_map.std() 92 ) 93 random_net_over_gross_map += avg_net_over_gross 94 # clip to stay within a reasonable N/G range 95 random_net_over_gross_map = random_net_over_gross_map.clip(*avg) 96 return random_net_over_gross_map 97 98 def _perlin(self, base=None, octave=1, lac=1.9, do_rotate=True): 99 import noise 100 101 xsize = self.cfg.cube_shape[0] 102 ysize = self.cfg.cube_shape[1] 103 if base is None: 104 base = np.random.randint(255) 105 # randomly rotate image 106 if do_rotate: 107 number_90_deg_rotations = 0 108 fliplr = False 109 flipud = False 110 if xsize == ysize and np.random.binomial(1, 0.5) == 1: 111 number_90_deg_rotations = int(np.random.uniform(1, 4)) 112 # temp = np.rot90(temp, number_90_deg_rotations) 113 # randomly flip left and right, top and bottom 114 if np.random.binomial(1, 0.5) == 1: 115 fliplr = True 116 if np.random.binomial(1, 0.5) == 1: 117 flipud = True 118 119 temp = np.array( 120 [ 121 [ 122 noise.pnoise2( 123 float(i) / xsize, 124 float(j) / ysize, 125 lacunarity=lac, 126 octaves=octave, 127 base=base, 128 ) 129 for j in range(ysize) 130 ] 131 for i in range(xsize) 132 ] 133 ) 134 temp = np.rot90(temp, number_90_deg_rotations) 135 if fliplr: 136 temp = np.fliplr(temp) 137 if flipud: 138 temp = np.flipud(temp) 139 return temp 140 141 def _fit_plane_strike_dip(self, azimuth, dip, grid_shape, verbose=False): 142 # Fits a plane given dip and max dip direction (azimuth) 143 # - create a point at the center of the grid, elevation is zero 144 xyz1 = np.array([grid_shape[0] / 2.0, grid_shape[1] / 2.0, 0.0]) 145 # y = np.array([0., 1., 1.]) 146 # - create a point in the strike direction at same elevation of zero 147 strike_angle = azimuth + 90.0 148 if strike_angle > 360.0: 149 strike_angle -= 360.0 150 if strike_angle > 180.0: 151 strike_angle -= 180.0 152 strike_angle *= np.pi / 180.0 153 distance = np.min(grid_shape) / 4.0 154 x = distance * math.cos(strike_angle) + grid_shape[0] / 2.0 155 y = distance * math.sin(strike_angle) + grid_shape[1] / 2.0 156 xyz2 = np.array([x, y, 0.0]) 157 # - create a point in the max dip direction 158 dip_angle = dip * 1.0 159 if dip_angle > 360.0: 160 dip_angle -= 360.0 161 if dip_angle > 180.0: 162 dip_angle -= 180.0 163 dip_angle *= np.pi / 180.0 164 strike_angle = azimuth 165 if strike_angle > 360.0: 166 strike_angle -= 360.0 167 if strike_angle > 180.0: 168 strike_angle -= 180.0 169 dip_elev = -distance * math.sin(dip_angle) * math.sqrt(2.0) 170 else: 171 dip_elev = distance * math.sin(dip_angle) * math.sqrt(2.0) 172 strike_angle *= np.pi / 180.0 173 x = distance * math.cos(strike_angle) + grid_shape[0] / 2.0 174 y = distance * math.sin(strike_angle) + grid_shape[1] / 2.0 175 xyz3 = np.array([x, y, dip_elev]) 176 # - combine 3 points into single array 177 xyz = np.vstack((xyz1, xyz2, xyz3)) 178 # - fit three points to a plane and compute elevation for all grids on surface 179 a, b, c = self.fit_plane_lsq(xyz) 180 z = self.eval_plane(range(grid_shape[0]), range(grid_shape[1]), a, b, c) 181 # - make plot if not quiet 182 if verbose: 183 import os 184 from datagenerator.util import import_matplotlib 185 186 plt = import_matplotlib() 187 print(f"strike angle = {strike_angle}") 188 print(f"dip angle = {dip_angle}") 189 print(f"points are: {xyz}") 190 plt.figure(1) 191 plt.clf() 192 plt.grid() 193 plt.imshow(z, origin="origin") 194 plt.plot(xyz[:, 0], xyz[:, 1], "yo") 195 plt.colorbar() 196 plt.savefig(os.path.join(self.cfg.work_subfolder, "dipping_plane.png")) 197 plt.close() 198 return z 199 200 @staticmethod 201 def fit_plane_lsq(xyz): 202 # Fits a plane to a point cloud, 203 # Where Z = aX + bY + c ----Eqn #1 204 # Rearranging Eqn1: aX + bY -Z +c =0 205 # Gives normal (a,b,-1) 206 # Normal = (a,b,-1) 207 # [rows, cols] = xyz.shape 208 rows = xyz.shape[0] 209 g = np.ones((rows, 3)) 210 g[:, 0] = xyz[:, 0] # X 211 g[:, 1] = xyz[:, 1] # Y 212 z = xyz[:, 2] 213 (a, b, c), _, _, _ = np.linalg.lstsq(g, z, rcond=-1) 214 normal = np.array([a, b, c]) 215 return normal 216 217 @staticmethod 218 def eval_plane(x, y, a, b, c): 219 # evaluates and returns Z for each input X,Y 220 z = np.zeros((len(x), len(y)), "float") 221 for i in range(len(x)): 222 for j in range(len(y)): 223 z[i, j] = a * x[i] + b * y[j] + c 224 return z 225 226 @staticmethod 227 def halton(dim, nbpts): 228 import math 229 230 h = np.empty(nbpts * dim) 231 h.fill(np.nan) 232 p = np.empty(nbpts) 233 p.fill(np.nan) 234 p1 = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] 235 lognbpts = math.log(nbpts + 1) 236 for i in range(dim): 237 b = p1[i] 238 n = int(math.ceil(lognbpts / math.log(b))) 239 for t in range(n): 240 p[t] = pow(b, -(t + 1)) 241 242 for j in range(nbpts): 243 d = j + 1 244 sum_ = math.fmod(d, b) * p[0] 245 for t in range(1, n): 246 d = math.floor(d / b) 247 sum_ += math.fmod(d, b) * p[t] 248 249 h[j * dim + i] = sum_ 250 251 return h.reshape(nbpts, dim) 252 253 @staticmethod 254 def rotate_point(x, y, angle_in_degrees): 255 """Calculate new coordinates for point after coordinate rotation about (0,0) by angleDegrees""" 256 angle = angle_in_degrees * np.pi / 180.0 257 x1 = math.cos(angle) * x + math.sin(angle) * y 258 y1 = -math.sin(angle) * x + math.cos(angle) * y 259 return x1, y1 260 261 def convert_map_from_samples_to_units(self, maps): 262 """Use digi to convert a copy of the provided maps from samples to units""" 263 converted_maps = maps.copy() 264 converted_maps *= float(self.cfg.digi) 265 return converted_maps 266 267 def write_maps_to_disk(self, horizons, name): 268 """Write horizons to disk.""" 269 fname = os.path.join(self.cfg.work_subfolder, name) 270 np.save(fname, horizons) 271 272 def write_onlap_episodes( 273 self, onlap_horizon_list, depth_maps_gaps, depth_maps_infilled, n=35 274 ): 275 """Write gapped horizons with onlaps + n shallower horizons to a separate files.""" 276 tilting_zmaps = list() 277 interval_thickness_size = depth_maps_gaps.shape[0] * depth_maps_gaps.shape[1] 278 for ihor in onlap_horizon_list: 279 for jhor in range(ihor - n, ihor + 1): 280 if jhor > 0: 281 # compute thickness compared to next horizon using infilled depth maps 282 interval_thickness = ( 283 depth_maps_infilled[:, :, jhor + 1] 284 - depth_maps_infilled[:, :, jhor] 285 ) 286 # compute percentage of interval filled with zeros 287 interval_thickness_zeros_size = interval_thickness[ 288 interval_thickness < 1.0e-5 289 ].shape[0] 290 pct_zeros = ( 291 float(interval_thickness_zeros_size) / interval_thickness_size 292 ) 293 if pct_zeros > 0.02: 294 tilting_zmaps.append(jhor) 295 # Make the tilting maps unique and sorted 296 tilting_zmaps = list(set(tilting_zmaps)) 297 tilting_zmaps.sort() 298 onlaps = np.zeros( 299 (depth_maps_gaps.shape[0], depth_maps_gaps.shape[1], len(tilting_zmaps)) 300 ) 301 for count, h in enumerate(tilting_zmaps): 302 onlaps[..., count] = depth_maps_gaps[..., h] 303 304 # Write to disk 305 self.write_maps_to_disk(onlaps * self.cfg.digi, "depth_maps_onlaps") 306 307 if self.cfg.hdf_store: 308 # Write onlap maps to hdf 309 from datagenerator.util import write_data_to_hdf 310 311 write_data_to_hdf( 312 "depth_maps_onlaps", onlaps * self.cfg.digi, self.cfg.hdf_master 313 ) 314 315 def write_fan_horizons(self, fan_horizon_list, depth_maps): 316 """Write fan layers to a separate file.""" 317 z = depth_maps.copy() 318 fan_horizons = np.zeros((z.shape[0], z.shape[1], len(fan_horizon_list))) 319 for count, horizon in enumerate(fan_horizon_list): 320 thickness_map = z[..., horizon + 1] - z[..., horizon] 321 z[..., horizon][thickness_map <= 0] = np.nan 322 if self.cfg.verbose: 323 print( 324 f"Fan horizon number: {horizon} Max thickness: {np.nanmax(thickness_map)}" 325 ) 326 fan_horizons[..., count] = z[..., horizon] 327 self.write_maps_to_disk(fan_horizons, "depth_maps_fans") 328 329 330class RandomHorizonStack(Horizons): 331 def __init__(self, parameters): 332 self.cfg = parameters 333 self.depth_maps = None 334 self.depth_maps_gaps = None 335 self.max_layers = 0 336 337 # Look up tables 338 self.thicknesses = None 339 self.onlaps = None 340 self.channels = None 341 self.dips = None 342 self.azimuths = None 343 self.facies = None 344 345 def _generate_lookup_tables(self): 346 # Thicknesses 347 self.thicknesses = stats.gamma.rvs(4.0, 2, size=self.cfg.num_lyr_lut) 348 # Onlaps 349 onlap_layer_list = np.sort( 350 np.random.uniform( 351 low=5, high=200, size=int(np.random.triangular(1, 4, 7) + 0.5) 352 ).astype("int") 353 ) 354 # Dips 355 self.dips = ( 356 (1.0 - random.power(100, self.cfg.num_lyr_lut)) 357 * 7.0 358 * self.cfg.dip_factor_max 359 ) 360 # Azimuths 361 self.azimuths = np.random.uniform( 362 low=0.0, high=360.0, size=(self.cfg.num_lyr_lut,) 363 ) 364 # Channels 365 self.channels = np.random.binomial(1, 3.0 / 100.0, self.cfg.num_lyr_lut) 366 367 if self.cfg.verbose: 368 print("self.cfg.num_lyr_lut = ", self.cfg.num_lyr_lut) 369 print("onlap_layer_list = ", onlap_layer_list) 370 onlap_array_dim = int(500 / 1250 * self.cfg.cube_shape[2]) 371 self.onlaps = np.zeros(onlap_array_dim, "int") 372 self.onlaps[onlap_layer_list] = 1 373 374 if not self.cfg.include_channels: 375 self.channels *= 0 376 else: 377 # Make sure channel episodes don't overlap too much 378 for i in range(len(self.channels)): 379 if self.channels[i] == 1: 380 self.channels[i + 1 : i + 6] *= 0 381 382 if self.cfg.verbose: 383 print( 384 f"Number of onlapping flags: {self.onlaps[self.onlaps == 1].shape[0]}" 385 ) 386 print( 387 f" ... horizon number for first onlap episode = {np.argmax(self.onlaps)}" 388 ) 389 print( 390 f" ... number of channelFlags: {self.channels[:250][self.channels[:250] == 1].shape[0]}" 391 ) 392 print( 393 f" ... horizon number for first channel episode: {np.argmax(self.channels)}" 394 ) 395 396 def _random_layer_thickness(self): 397 rand_oct = int(np.random.triangular(left=1.3, mode=2.65, right=5.25)) 398 399 low_thickness_factor = np.random.triangular(left=0.05, mode=0.2, right=0.95) 400 401 high_thickness_factor = np.random.triangular(left=1.05, mode=1.8, right=2.2) 402 403 thickness_factor_map = self._perlin(octave=rand_oct) 404 thickness_factor_map -= thickness_factor_map.mean() 405 thickness_factor_map *= (high_thickness_factor - low_thickness_factor) / ( 406 thickness_factor_map.max() - thickness_factor_map.min() 407 ) 408 thickness_factor_map += 1.0 409 if thickness_factor_map.min() < 0.0: 410 thickness_factor_map -= thickness_factor_map.min() - 0.05 411 412 if self.cfg.verbose: 413 print( 414 f" {thickness_factor_map.mean()}, " 415 f"{thickness_factor_map.min()}, " 416 f" {thickness_factor_map.max()}, " 417 f" {thickness_factor_map.std()}" 418 ) 419 420 return thickness_factor_map 421 422 def _generate_random_depth_structure_map( 423 self, 424 dip_range=(0.25, 1.0), 425 num_points_range=(2, 25), 426 elevation_std=100.0, 427 zero_at_corners=True, 428 initial=False, 429 ): 430 ############################################################################ 431 # generate a 2D array representing a depth (or TWT) structure map. 432 # - grid_size controls output size in (x,y), units are samples 433 # - dip_range controls slope of dipping plane upon which random residual points are placed 434 # - num_points controls number of randomly positioned points are used for residual grid 435 # - elevation_std controls std dev for residual elevation of random points in residual grid 436 ############################################################################ 437 grid_size = self.cfg.cube_shape[:2] 438 # define output grid (padded by 15% in each direction) 439 xi = np.linspace(-0.15, 1.15, int(grid_size[0] * 1.3)) 440 yi = np.linspace(-0.15, 1.15, int(grid_size[1] * 1.3)) 441 442 # start with a gently dipping plane similar to that found on passive shelf margins (< 1 degree dip) 443 azimuth = random.uniform(0.0, 360.0) 444 dip = random.uniform(dip_range[0], dip_range[1]) 445 446 # build a residual surface to add to the dipping plane 447 number_halton_points = int(random.uniform(100, 500) + 0.5) 448 number_random_points = int( 449 random.uniform(num_points_range[0], num_points_range[1]) + 0.5 450 ) 451 z = np.random.rand(number_random_points) 452 z -= z.mean() 453 if initial: 454 elevation_std = self.cfg.initial_layer_stdev 455 z *= elevation_std / z.std() 456 457 dipping_plane = self._fit_plane_strike_dip( 458 azimuth, dip, grid_size, verbose=False 459 ) 460 xx = self.halton(2, number_halton_points)[-number_random_points:] 461 462 # make up some randomly distributed data 463 # - adjust for padding 464 xx *= xi[-1] - xi[0] 465 xx += xi[0] 466 x = xx[:, 0] 467 y = xx[:, 1] 468 469 if zero_at_corners: 470 x = np.hstack((x, [-0.15, -0.15, 1.15, 1.15])) 471 y = np.hstack((y, [-0.15, 1.15, -0.15, 1.15])) 472 z = np.hstack((z, [-0.15, -0.15, -0.15, -0.15])) 473 474 # grid the data. 475 zi = griddata( 476 np.column_stack((x, y)), 477 z, 478 (xi[:, np.newaxis], yi[np.newaxis, :]), 479 method="cubic", 480 ) 481 zi = zi.reshape(xi.shape[0], yi.shape[0]) 482 xi_min_index = np.argmin((xi - 0.0) ** 2) 483 xi_max_index = xi_min_index + grid_size[0] 484 yi_min_index = np.argmin((yi - 0.0) ** 2) 485 yi_max_index = yi_min_index + grid_size[1] 486 zi = zi[xi_min_index:xi_max_index, yi_min_index:yi_max_index] 487 # add to the gently sloping plane 488 zi += dipping_plane 489 490 if initial: 491 zi += dipping_plane - dipping_plane.min() 492 zi += self.cfg.cube_shape[-1] * self.cfg.infill_factor - zi.min() 493 zi_argmin_i, zi_argmin_j = np.unravel_index(zi.argmin(), zi.shape) 494 if self.cfg.verbose: 495 print( 496 f"\tIndices for shallowest point in cube: {zi_argmin_i}, {zi_argmin_j}" 497 ) 498 self.cfg.write_to_logfile( 499 f"number_random_points: {number_random_points:.0f}" 500 ) 501 self.cfg.write_to_logfile(f"dip angle: {dip:.2f}") 502 self.cfg.write_to_logfile( 503 f"dipping_plane min: {dipping_plane.min():.2f}, mean: {dipping_plane.mean():.2f}," 504 f" max: {dipping_plane.max():.2f}" 505 ) 506 self.cfg.write_to_logfile( 507 f"zi min: {zi.min():.2f}, mean: {zi.mean():.2f}, max: {zi.max():.2f}" 508 ) 509 510 self.cfg.write_to_logfile( 511 msg=None, 512 mainkey="model_parameters", 513 subkey="number_random_points", 514 val=number_random_points, 515 ) 516 self.cfg.write_to_logfile( 517 msg=None, 518 mainkey="model_parameters", 519 subkey="dip_angle", 520 val=number_random_points, 521 ) 522 self.cfg.write_to_logfile( 523 msg=None, 524 mainkey="model_parameters", 525 subkey="dipping_plane_min", 526 val=dipping_plane.min(), 527 ) 528 self.cfg.write_to_logfile( 529 msg=None, 530 mainkey="model_parameters", 531 subkey="dipping_plane_mean", 532 val=dipping_plane.mean(), 533 ) 534 self.cfg.write_to_logfile( 535 msg=None, 536 mainkey="model_parameters", 537 subkey="dipping_plane_max", 538 val=dipping_plane.max(), 539 ) 540 self.cfg.write_to_logfile( 541 msg=None, mainkey="model_parameters", subkey="zi_min", val=zi.min() 542 ) 543 self.cfg.write_to_logfile( 544 msg=None, mainkey="model_parameters", subkey="zi_mean", val=zi.mean() 545 ) 546 self.cfg.write_to_logfile( 547 msg=None, mainkey="model_parameters", subkey="zi_max", val=zi.max() 548 ) 549 550 return xx, zi 551 552 def _create_thickness_map(self, random_thickness_factor_map, layer_number): 553 """ 554 Create a random_thickness_map for a given layer. 555 556 Creates a dipping_plane with differential dip from previous layer and computes a new thickness_map which is 557 always positive (i.e. no erosion allowed) 558 559 Parameters 560 ---------- 561 random_thickness_factor_map : ndarray - a random thickness factor map for this layer 562 layer_number : int - the layer number being built in the stack of horizons 563 564 Returns 565 ------- 566 thickness_map : ndarray - the thickness_map for the given layer 567 """ 568 dipping_plane = self._fit_plane_strike_dip( 569 azimuth=self.azimuths[layer_number], 570 dip=self.dips[layer_number], 571 grid_shape=self.cfg.cube_shape[:2], 572 verbose=False, 573 ) 574 dipping_plane -= dipping_plane.min() 575 if self.cfg.verbose: 576 print( 577 f"azi, dip, dipping_plane min/mean/max = {self.azimuths[layer_number]}, {self.dips[layer_number]}, " 578 f"{dipping_plane.min():.2f}, {dipping_plane.mean():.2f}, {dipping_plane.max():.2f}" 579 ) 580 581 # compute new thickness map. For now, don't allow erosion (i.e. thickness_map is always positive) 582 thickness_map = ( 583 ( 584 np.ones(self.cfg.cube_shape[:2], "float") 585 * self.thicknesses[layer_number] 586 - dipping_plane 587 ) 588 * self.cfg.infill_factor 589 * random_thickness_factor_map 590 ) 591 thickness_map = np.clip( 592 thickness_map, 0.0, self.cfg.thickness_max * self.cfg.infill_factor * 1.5 593 ) 594 return thickness_map 595 596 def _create_initial_thickness_factor_map(self): 597 """ 598 Create the thickness_factor map for the layer at the base of the model 599 600 Returns 601 ------- 602 random_thickness_factor_map : ndarray - a random thickness factor map for the initial layer at base 603 """ 604 _, random_thickness_factor_map = self._generate_random_depth_structure_map( 605 dip_range=[0.0, 0.0], num_points_range=(25, 100), elevation_std=0.45 606 ) 607 random_thickness_factor_map -= random_thickness_factor_map.mean() - 1 608 # 609 return random_thickness_factor_map 610 611 def create_depth_maps(self): 612 """Building layers in reverse order - starting at bottom and depositing new layers on top. 613 614 Each layer has random residual dip and pseudo-random residual thickness 615 """ 616 self._generate_lookup_tables() 617 # Create initial depth map at base of model using initial=True 618 _, previous_depth_map = self._generate_random_depth_structure_map( 619 dip_range=[0.0, 75], 620 num_points_range=(3, 5), 621 initial=True, 622 elevation_std=self.cfg.initial_layer_stdev, 623 ) 624 625 # Build layers while minimum depth is less than the seabed_min_depth as set in config (given in metres). 626 # Convert min_depth into samples, don't forget about the infill factor 627 shallowest_depth_to_build = ( 628 self.cfg.seabed_min_depth / float(self.cfg.digi) 629 ) * self.cfg.infill_factor 630 631 # Build layers in a loop from deep to shallow until the minimum depth is reached and then break out. 632 for i in range(20000): 633 # Do the special case for the random_thickness_factor_map for the initial layer (at base) 634 if i == 0: 635 if self.cfg.verbose: 636 print("Building random depth map at base of model") 637 random_thickness_factor_map = ( 638 self._create_initial_thickness_factor_map() 639 ) 640 else: # Otherwise create standard random thickness factor map 641 if self.cfg.verbose: 642 print(f"Building Layer {i}") 643 random_thickness_factor_map = self._random_layer_thickness() 644 # Create the layer's thickness map using the random_thickness_factor_map 645 thickness_map = self._create_thickness_map(random_thickness_factor_map, i) 646 current_depth_map = previous_depth_map - thickness_map 647 648 if self.cfg.verbose: 649 print( 650 f"current_depth_map min/mean/max = {current_depth_map.min():.2f}, {current_depth_map.mean():.2f}," 651 f" {current_depth_map.max():.2f}" 652 ) 653 print( 654 f"thickness_map min/mean/max = {thickness_map.min():.2f}, {thickness_map.mean():.2f}," 655 f" {thickness_map.max():.2f}" 656 ) 657 658 # break out of loop when minimum depth is reached 659 if current_depth_map.min() <= shallowest_depth_to_build: 660 break 661 662 # replace previous depth map for next iteration 663 previous_depth_map = current_depth_map.copy() 664 # save depth map in (top of) 3d array. Resulting order is shallow at top 665 try: 666 depth_maps = np.dstack((current_depth_map, depth_maps)) 667 except UnboundLocalError: 668 # There is no stack of horizons yet - write the base. 669 depth_maps = current_depth_map.copy() 670 if self.cfg.verbose: 671 print(f"Layer {i}, depth_maps.shape = {depth_maps.shape}") 672 self.max_layers = i + 1 673 674 if self.cfg.verbose: 675 print("\n ... finished creating horizon layers ...") 676 # Store maps in hdf file 677 self.depth_maps = self.cfg.hdf_init("depth_maps", shape=depth_maps.shape) 678 self.depth_maps[:] = depth_maps 679 680 self.cfg.write_to_logfile( 681 f"number_layers: {self.max_layers}", 682 mainkey="model_parameters", 683 subkey="number_layers", 684 val=self.max_layers, 685 ) 686 687 688class Onlaps(Horizons): 689 def __init__(self, parameters, depth_maps, thicknesses, max_layers): 690 self.cfg = parameters 691 self.depth_maps = depth_maps 692 self.thicknesses = thicknesses 693 self.max_layers = max_layers 694 self.onlap_horizon_list = list() 695 self._generate_onlap_lookup_table() 696 697 def _generate_onlap_lookup_table(self): 698 # Onlaps 699 onlap_layer_list = np.sort( 700 np.random.uniform( 701 low=5, 702 high=self.max_layers, 703 size=int(np.random.triangular(1, 4, 7) + 0.5), 704 ).astype("int") 705 ) 706 if self.cfg.verbose: 707 print("self.cfg.num_lyr_lut = ", self.cfg.num_lyr_lut) 708 print("onlap_layer_list = ", onlap_layer_list) 709 onlap_array_dim = int(500 / 1250 * self.cfg.cube_shape[2]) 710 self.onlaps = np.zeros(onlap_array_dim, "int") 711 self.onlaps[onlap_layer_list] = 1 712 713 if self.cfg.verbose: 714 print( 715 f"Number of onlapping flags: {self.onlaps[self.onlaps == 1].shape[0]}" 716 ) 717 718 def insert_tilting_episodes(self): 719 ############################################################################ 720 # insert tilting (onlap) episodes 721 # - computations loop from deep horizons toward shallow (top of horizon cube) 722 ############################################################################ 723 if self.cfg.verbose: 724 print("\n ... simulating tilting (onlap) episodes ...") 725 726 azi_list = [] 727 dip_list = [] 728 729 count = 0 730 onlaps_horizon_list = [] 731 layer_has_onlaps = np.zeros((self.depth_maps.shape[2]), "bool") 732 for i in range(self.depth_maps.shape[2] - 2, 0, -1): 733 734 if self.onlaps[i] == 1: 735 # A tilting (onlap) episode occurs for this horizon. 736 # Add a random dipping layer to this and all shallower layers, and adjust depth for layer thickness 737 count += 1 738 if self.cfg.verbose: 739 print( 740 " ... simulate a tilting (onlap) episode ... at output horizon number {}".format( 741 i 742 ) 743 ) 744 onlaps_horizon_list.append(i) 745 746 if not self.cfg.regen: 747 azi = np.random.uniform(low=0.0, high=360.0) 748 azi_list.append(azi) 749 dip = np.random.uniform(low=5.0, high=20.0) 750 dip_list.append(dip) 751 752 dipping_plane2 = ( 753 self._fit_plane_strike_dip( 754 azi_list[count - 1], 755 dip_list[count - 1], 756 self.depth_maps.shape, 757 verbose=False, 758 ) 759 * self.cfg.infill_factor 760 ) 761 dipping_plane2_offset = ( 762 self.thicknesses[i] * self.cfg.infill_factor / 2.0 763 - dipping_plane2.max() 764 ) 765 766 # adjust all shallower layers 767 previous_depth_map = self.depth_maps[:, :, i + 1].copy() 768 for i_horizon in range(i, 0, -1): 769 current_depth_map = self.depth_maps[:, :, i_horizon].copy() 770 thickness_map = previous_depth_map - current_depth_map 771 prior_thickness_map = thickness_map.copy() 772 thickness_map += dipping_plane2 773 thickness_map += dipping_plane2_offset 774 thickness_map = np.clip(thickness_map, 0.0, thickness_map.max()) 775 current_depth_map = previous_depth_map - thickness_map 776 if np.mean(thickness_map - prior_thickness_map) > 0.5: 777 layer_has_onlaps[i_horizon] = 1 778 self.depth_maps[:, :, i_horizon] = current_depth_map.copy() 779 780 # print("\n\n ... depth_maps.shape = {}".format(depth_maps.shape)) 781 if self.cfg.verbose: 782 print( 783 f" ... Finished inserting tilting (onlap) episodes. {count} episodes were added..." 784 ) 785 self.cfg.write_to_logfile( 786 f"number_onlap_episodes: {count}\nonlaps_horizon_list: {str(onlaps_horizon_list)}" 787 ) 788 789 self.cfg.write_to_logfile( 790 msg=None, 791 mainkey="model_parameters", 792 subkey="number_onlap_episodes", 793 val=count, 794 ) 795 self.cfg.write_to_logfile( 796 msg=None, 797 mainkey="model_parameters", 798 subkey="onlaps_horizon_list", 799 val=str(onlaps_horizon_list), 800 ) 801 self.onlap_horizon_list = onlaps_horizon_list 802 return self.onlap_horizon_list 803 804 805class BasinFloorFans(Horizons): 806 def __init__(self, parameters, max_layers): 807 self.cfg = parameters 808 self.max_layers = max_layers 809 self.fan_layers = None 810 # self._generate_fan_lookup_table() 811 812 def _generate_fan_lookup_table(self): 813 # TODO test where & how often fans should be added. Also check if they overlap with onlaps and if so, what to do 814 # Onlaps 815 layers_with_fans = np.sort( 816 np.random.uniform( 817 low=5, high=self.max_layers - 1, size=int(np.random.choice([1, 2, 3])) 818 ).astype("int") 819 ) 820 self.fan_layers = layers_with_fans 821 self.fan_thicknesses = [] 822 self.fans = np.zeros(self.max_layers, "int") 823 self.fans[list(self.fan_layers)] = 1 824 825 def _generate_basin_floor_fan(self, layer_number): 826 # Select parameters for fan 827 _scale = np.random.uniform(50.0, 200.0) # length in pixels 828 _aspect = np.random.uniform(1.5, 4.0) # length width 829 _azimuth = np.random.uniform(0.0, 360.0) 830 _factor = np.random.uniform(1.5, 3.5) 831 _asymmetry_factor = np.random.uniform(-1.0, 1.0) 832 _smoothing_size = np.random.uniform(1.33, 3.0) 833 834 # Choose whether to create a pair of fans 835 pair_of_fans = np.random.choice([True, False]) 836 837 fan_parameters = ( 838 f"{_scale:4.2f}, {_aspect:4.2f}, {_azimuth:4.2f}, {_factor:4.2f}, {_asymmetry_factor:4.2f}," 839 f" {_smoothing_size:4.2f}" 840 ) 841 if self.cfg.verbose: 842 print(fan_parameters) 843 zi = self._generate_fan_thickness_map( 844 scale=_scale, 845 aspect=_aspect, 846 rotate_angle=_azimuth, 847 dip=3.5, 848 entry_point_factor=_factor, 849 asymmetry_factor=_asymmetry_factor, 850 smoothing_size=_smoothing_size, 851 ) 852 853 if pair_of_fans: 854 if self.cfg.verbose: 855 print("Creating a pair of fans\n") 856 _scale2 = _scale * np.random.uniform(0.667, 1.5) # length in pixels 857 _aspect2 = _aspect * np.random.uniform(0.75, 1.33) # length / width 858 _azimuth2 = _azimuth + np.random.uniform(-30.0, 30.0) 859 delta_dist = (_scale + _scale2) / ( 860 (_aspect + _aspect2) 861 * np.random.triangular(0.5, 0.85, 1.5) 862 * np.random.choice([-1.0, 1.0]) 863 ) # length in pixels 864 delta_x, delta_y = self.rotate_point(delta_dist, 0.0, 360.0 - _azimuth2) 865 _factor2 = _factor + np.random.uniform(0.8, 1.25) 866 _asymmetry_factor2 = _asymmetry_factor + np.random.uniform(-0.25, 0.25) 867 _smoothing_size2 = _smoothing_size + np.random.uniform(-0.25, 0.25) 868 869 fan_parameters_2 = ( 870 f"{_scale2:4.2f}, {_aspect2:4.2f}, {_azimuth2:4.2f}, {_factor2:4.2f}," 871 f"{_asymmetry_factor2:4.2f}, {_smoothing_size2:4.2f}" 872 ) 873 874 if self.cfg.verbose: 875 print( 876 f"Fan 1 parameters: {fan_parameters}\nFan 2 parameters: {fan_parameters_2}" 877 ) 878 fan_parameters += f"\n{fan_parameters_2}" 879 zi2 = self._generate_fan_thickness_map( 880 scale=_scale2, 881 aspect=_aspect2, 882 rotate_angle=_azimuth2, 883 dip=3.5, 884 entry_point_factor=_factor2, 885 asymmetry_factor=_asymmetry_factor2, 886 smoothing_size=_smoothing_size2, 887 ) 888 889 zi2_padded = np.zeros((zi.shape[0] * 2, zi.shape[1] * 2), "float") 890 zi2_padded[ 891 zi.shape[0] // 2 : zi.shape[0] + zi.shape[0] // 2, 892 zi.shape[1] // 2 : zi.shape[1] + zi.shape[1] // 2, 893 ] += zi2 894 zi2_padded = np.roll(zi2_padded, int(delta_x + 0.5), axis=0) 895 zi2_padded = np.roll(zi2_padded, int(delta_y + 0.5), axis=1) 896 zi_delta = ( 897 zi2_padded[ 898 zi.shape[0] // 2 : zi.shape[0] + zi.shape[0] // 2, 899 zi.shape[1] // 2 : zi.shape[1] + zi.shape[1] // 2, 900 ] 901 - zi 902 ) 903 zi_delta[zi_delta < 0.0] = 0.0 904 zi3 = zi + zi_delta 905 zi = zi3 906 907 # Plot the fan 908 if self.cfg.qc_plots: 909 from datagenerator.util import import_matplotlib 910 911 plt = import_matplotlib() 912 plt.figure(1) 913 plt.clf() 914 plt.title(fan_parameters) 915 plt.imshow(zi.T) 916 plt.colorbar() 917 plt.savefig( 918 os.path.join( 919 self.cfg.work_subfolder, f"random_fan_layer{layer_number}.png" 920 ) 921 ) 922 923 self.fan_thicknesses.append(zi) 924 return zi 925 926 def _generate_fan_thickness_map( 927 self, 928 scale=2.5, 929 aspect=1.5, 930 rotate_angle=0.0, 931 zero_at_corners=True, 932 dip=3.5, 933 entry_point_factor=1.0, 934 asymmetry_factor=0.5, 935 smoothing_size=2.5, 936 ): 937 ############################################################################ 938 # generate a 2D array representing a depth (or TWT) structure map. 939 # - grid_size controls output size in (x,y), units are samples 940 # - dip_range controls slope of dipping plane upon which random residual points are placed 941 # - num_points controls number of randomly positioned points are used for residual grid 942 # - asymetry_factor: range [-1, 1.]. 0. does nothing, sign determines left/right prominence 943 ############################################################################ 944 945 grid_size = self.cfg.cube_shape[:2] 946 # define output grid (padded by 15% in each direction) 947 xi = np.linspace(-0.15, 1.15, int(float(grid_size[0]) * 1.3)) 948 yi = np.linspace(-0.15, 1.15, int(float(grid_size[1]) * 1.3)) 949 950 # start with a gently dipping plane similar to that found on passive shelf margins (< 1 degree dip) 951 dipping_plane = self._fit_plane_strike_dip( 952 360.0 - rotate_angle, dip, grid_size, verbose=False 953 ).T 954 955 x = np.array( 956 [ 957 0.5 * (1.0 - np.cos(theta)) * np.sin(theta) * np.cos(phi) 958 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 959 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 960 ] 961 ) 962 y = np.array( 963 [ 964 np.cos(theta) 965 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 966 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 967 ] 968 ) 969 z = np.array( 970 [ 971 0.5 * (1.0 - np.cos(theta)) * np.sin(theta) * np.sin(phi) 972 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 973 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 974 ] 975 ) 976 x -= x.min() 977 y -= y.min() 978 if self.cfg.verbose: 979 print("number of xyz points = ", x.size) 980 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 981 x *= (scale / aspect) / (x.max() - x.min()) 982 y *= scale / (y.max() - y.min()) 983 984 # scale back to range of [0,1] 985 x /= grid_size[0] 986 y /= grid_size[1] 987 988 if self.cfg.verbose: 989 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 990 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 991 992 # add asymmetry 993 # - small rotation, stretch, undo rotation 994 if asymmetry_factor != 0.0: 995 x, y = self.rotate_point(x, y, asymmetry_factor * 45.0) 996 x *= 2.0 + np.abs(asymmetry_factor) 997 y *= 2.0 - np.abs(asymmetry_factor) 998 x, y = self.rotate_point(x, y, -asymmetry_factor * 45.0) 999 x /= 2.0 1000 y /= 2.0 1001 if self.cfg.verbose: 1002 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 1003 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 1004 1005 # rotate 1006 x, y = self.rotate_point(x, y, 360.0 - rotate_angle) 1007 1008 # center in the grid 1009 x += 0.5 - x.mean() 1010 y += 0.5 - y.mean() 1011 1012 if self.cfg.verbose: 1013 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 1014 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 1015 print("z min/mean/max = ", z.min(), z.mean(), z.max(), z.max() - z.min()) 1016 1017 # decimate the grid randomly 1018 decimation_factor = 250 1019 1020 fan_seed_x = np.random.randint(1, high=2 ** 32 - 1) 1021 fan_seed_y = np.random.randint(1, high=2 ** 32 - 1) 1022 fan_seed_z = np.random.randint(1, high=2 ** 32 - 1) 1023 point_indices = np.arange(x.size).astype("int") 1024 point_indices = point_indices[: x.size // decimation_factor] 1025 np.random.shuffle(point_indices) 1026 np.random.seed(fan_seed_x) 1027 _x = x + np.random.uniform(low=-0.03, high=0.03, size=x.size) 1028 np.random.seed(fan_seed_y) 1029 _y = y + np.random.uniform(low=-0.03, high=0.03, size=x.size) 1030 np.random.seed(fan_seed_z) 1031 _z = z + np.random.uniform(low=-0.1, high=0.1, size=x.size) 1032 1033 _x = _x[point_indices] 1034 _y = _y[point_indices] 1035 _z = _z[point_indices] 1036 1037 if self.cfg.qc_plots: 1038 from datagenerator.util import import_matplotlib 1039 1040 plt = import_matplotlib() 1041 plt.clf() 1042 plt.grid() 1043 plt.scatter(x * 300, y * 300, c=z) 1044 plt.xlim([0, 300]) 1045 plt.ylim([0, 300]) 1046 plt.colorbar() 1047 1048 if zero_at_corners: 1049 x = np.hstack((x, [-0.15, -0.15, 1.15, 1.15])) 1050 y = np.hstack((y, [-0.15, 1.15, -0.15, 1.15])) 1051 z = np.hstack((z, [-0.15, -0.15, -0.15, -0.15])) 1052 _x = np.hstack((_x, [-0.15, -0.15, 1.15, 1.15])) 1053 _y = np.hstack((_y, [-0.15, 1.15, -0.15, 1.15])) 1054 _z = np.hstack((_z, [-0.15, -0.15, -0.15, -0.15])) 1055 1056 # grid the data. 1057 zi = griddata( 1058 (x, y), 1059 z, 1060 (xi.reshape(xi.shape[0], 1), yi.reshape(1, yi.shape[0])), 1061 method="linear", 1062 ) 1063 _zi = griddata( 1064 (_x, _y), 1065 _z, 1066 (xi.reshape(xi.shape[0], 1), yi.reshape(1, yi.shape[0])), 1067 method="cubic", 1068 ) 1069 1070 zi[np.isnan(zi)] = 0.0 1071 zi[zi <= 0.0] = 0.0 1072 zi_mask = zi.copy() 1073 zi_mask[zi_mask <= 0.015] = 0.0 1074 if self.cfg.verbose: 1075 print("zi_mask[zi_mask >0.].min() = ", zi_mask[zi_mask > 0.0].min()) 1076 print("zi_mask[zi_mask >0.].mean() = ", zi_mask[zi_mask > 0.0].mean()) 1077 print( 1078 "np.percentile(zi_mask[zi_mask >0.],.5) = ", 1079 np.percentile(zi_mask[zi_mask > 0.0], 0.5), 1080 ) 1081 1082 _zi[np.isnan(_zi)] = 0.0 1083 zi = _zi + 0.0 1084 zi[zi_mask <= 0.0] = 0.0 1085 zi[zi <= 0.0] = 0.0 1086 from scipy.ndimage import gaussian_filter, grey_closing 1087 1088 zi = gaussian_filter(zi, smoothing_size) 1089 # For cube_shape 300, 300, use closing size of (10, 10) 1090 closing_size = ( 1091 int(self.cfg.cube_shape[0] / 30), 1092 int(self.cfg.cube_shape[1] / 30), 1093 ) 1094 zi = grey_closing(zi, size=closing_size) 1095 1096 zi = zi.reshape(xi.shape[0], yi.shape[0]) 1097 xi_min_index = np.argmin((xi - 0.0) ** 2) 1098 xi_max_index = xi_min_index + grid_size[0] 1099 yi_min_index = np.argmin((yi - 0.0) ** 2) 1100 yi_max_index = yi_min_index + grid_size[1] 1101 zi = zi[xi_min_index:xi_max_index, yi_min_index:yi_max_index] 1102 # add to the gently sloping plane 1103 dipping_plane[zi == 0.0] = 0.0 1104 dipping_plane -= dipping_plane[dipping_plane != 0.0].min() 1105 dipping_plane[zi <= 0.05] = 0.0 1106 if dipping_plane.max() > 0: 1107 dipping_plane /= dipping_plane[dipping_plane != 0.0].max() 1108 dipping_plane *= zi.max() 1109 if self.cfg.verbose: 1110 print( 1111 "zi[zi > 0.] min/mean/max = ", 1112 zi[zi > 0.0].min(), 1113 zi[zi > 0.0].mean(), 1114 zi[zi > 0.0].max(), 1115 ) 1116 if dipping_plane.max() > 0: 1117 if self.cfg.verbose: 1118 print( 1119 "dipping_plane[dipping_plane > 0.] min/mean/max = ", 1120 dipping_plane[dipping_plane > 0.0].min(), 1121 dipping_plane[dipping_plane > 0.0].mean(), 1122 dipping_plane[dipping_plane > 0.0].max(), 1123 ) 1124 else: 1125 if self.cfg.verbose: 1126 print( 1127 "dipping_plane min/mean/max = ", 1128 dipping_plane.min(), 1129 dipping_plane.mean(), 1130 dipping_plane.max(), 1131 ) 1132 zi += entry_point_factor * dipping_plane 1133 1134 # todo QC size of this smoothing (to smooth edges of fan) 1135 zi = gaussian_filter(zi, smoothing_size * 2.0) 1136 1137 return zi 1138 1139 def insert_fans_into_horizons(self, depth_maps): 1140 new_depth_maps = depth_maps[:].copy() 1141 # Create lookup table for fans 1142 self._generate_fan_lookup_table() 1143 # generate fans using fan_layers 1144 for layer in self.fan_layers: 1145 thickness_map = ( 1146 self._generate_basin_floor_fan(layer) * self.cfg.infill_factor 1147 ) 1148 new_depth_maps = self.insert_feature_into_horizon_stack( 1149 thickness_map, layer, new_depth_maps 1150 ) 1151 # Write fan layers to logfile 1152 self.cfg.write_to_logfile( 1153 f"number_fan_episodes: {len(self.fan_layers)}\n" 1154 f"fans_horizon_list: {str(self.fan_layers)}" 1155 ) 1156 self.cfg.write_to_logfile( 1157 msg=None, 1158 mainkey="model_parameters", 1159 subkey="number_fan_episodes", 1160 val=len(self.fan_layers), 1161 ) 1162 self.cfg.write_to_logfile( 1163 msg=None, 1164 mainkey="model_parameters", 1165 subkey="fan_horizon_list", 1166 val=str(self.fan_layers), 1167 ) 1168 1169 return new_depth_maps 1170 1171 def fan_qc_plot(self, maps, lyrnum, thickness): 1172 """ 1173 Plot a cross-section of the inserted basin floor fan 1174 1175 Display an inline and crossline with the basin floor fan in red in cross-section 1176 Inlay a map of the fan in each subplot 1177 """ 1178 from datagenerator.util import import_matplotlib 1179 1180 plt = import_matplotlib() 1181 plt.close() 1182 fig, axs = plt.subplots(nrows=2, ncols=1, figsize=(20, 15), sharey=True) 1183 axs[0].set_title(f"Layer {lyrnum}") 1184 axs[1].set_title(f"Layer {lyrnum}") 1185 # 1186 max_layer = np.clip( 1187 lyrnum + 10, 0, maps.shape[2] 1188 ) # only plot layers below fan if they exist 1189 for j in range(lyrnum - 5, max_layer, 1): 1190 if j == lyrnum: # highlight fan layers in thicker red line 1191 axs[0].plot( 1192 range(self.cfg.cube_shape[0]), 1193 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1194 "r-", 1195 lw=0.5, 1196 ) 1197 axs[1].plot( 1198 range(self.cfg.cube_shape[0]), 1199 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1200 "r-", 1201 lw=0.5, 1202 ) 1203 else: 1204 axs[0].plot( 1205 range(self.cfg.cube_shape[0]), 1206 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1207 "k-", 1208 lw=0.2, 1209 ) 1210 axs[1].plot( 1211 range(self.cfg.cube_shape[0]), 1212 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1213 "k-", 1214 lw=0.2, 1215 ) 1216 for inl, ax in enumerate(axs): 1217 if inl == 0: 1218 plt.axes([0.8, 0.75, 0.11, 0.11]) 1219 plt.axvline( 1220 x=int(self.cfg.cube_shape[0] / 2), color="grey", linestyle="--" 1221 ) 1222 else: 1223 plt.axes([0.8, 0.33, 0.11, 0.11]) 1224 plt.axhline( 1225 y=int(self.cfg.cube_shape[1] / 2), color="grey", linestyle="--" 1226 ) 1227 plt.imshow(thickness.T) 1228 plt.xticks([]) 1229 plt.yticks([]) 1230 ax.set_ylim( 1231 top=np.min(maps[..., lyrnum - 5]), 1232 bottom=np.max(maps[..., max_layer - 1]), 1233 ) # Reverse Y axis 1234 fig.savefig( 1235 os.path.join(self.cfg.work_subfolder, f"BasinFloorFan_layer_{lyrnum}.png") 1236 ) 1237 plt.close() 1238 1239 1240class Channel(Horizons): 1241 def __init__(self): 1242 self.W = 200.0 # Channel wodth 1243 self.D = 12.0 # Channel depth 1244 self.pad = 100 # padding (number of nodepoints along centerline) 1245 self.deltas = 50.0 # sampling distance along centerline 1246 self.nit = 2000 # number of iterations 1247 self.Cf = 0.022 # dimensionless Chezy friction factor 1248 self.crdist = 1.5 * self.W # threshold distance at which cutoffs occur 1249 self.kl = 60.0 / (365 * 24 * 60 * 60.0) # migration rate constant (m/s) 1250 self.kv = 1.0e-11 # vertical slope-dependent erosion rate constant (m/s) 1251 self.dt = 2 * 0.05 * 365 * 24 * 60 * 60.0 # time step (s) 1252 self.dens = 1000 # density of water (kg/m3) 1253 self.saved_ts = 20 # which time steps will be saved 1254 self.n_bends = 30 # approximate number of bends to model 1255 self.Sl = 0.0 # initial slope (matters more for submarine channels than rivers) 1256 self.t1 = 500 # time step when incision starts 1257 self.t2 = 700 # time step when lateral migration starts 1258 self.t3 = 1400 # time step when aggradation starts 1259 self.aggr_factor = ( 1260 4e-9 # aggradation factor (m/s, about 0.18 m/year, it kicks in after t3) 1261 ) 1262 1263 self.h_mud = 0.3 # thickness of overbank deposits for each time step 1264 self.dx = 20.0 # gridcell size in metres 1265 1266 # Channel objects 1267 self.ch = None 1268 self.chb = None 1269 self.chb_3d = None 1270 1271 def generate_channel_parameters(self): 1272 # select random parameters 1273 pass 1274 1275 def create_channel_3d(self): 1276 # Initialise channel 1277 self.ch = mp.generate_initial_channel( 1278 self.W, self.D, self.Sl, self.deltas, self.pad, self.n_bends 1279 ) 1280 # Create channel belt object 1281 self.chb = mp.ChannelBelt( 1282 channels=[self.ch], cutoffs=[], cl_times=[0.0], cutoff_times=[] 1283 ) 1284 # Migrate channel 1285 self.chb.migrate( 1286 self.nit, 1287 self.saved_ts, 1288 self.deltas, 1289 self.pad, 1290 self.crdist, 1291 self.Cf, 1292 self.kl, 1293 self.kv, 1294 self.dt, 1295 self.dens, 1296 self.t1, 1297 self.t2, 1298 self.t3, 1299 self.aggr_factor, 1300 ) 1301 end_time = self.chb.cl_times[-1] 1302 _xmin = 15000 1303 _xmax = 21000 1304 self.chb_3d, _, _, _, _ = self.chb.build_3d_model( 1305 "fluvial", 1306 h_mud=self.h_mud, 1307 levee_width=800.0, 1308 h=12.0, 1309 w=self.W, 1310 bth=0.0, 1311 dcr=10.0, 1312 dx=self.dx, 1313 delta_s=self.deltas, 1314 starttime=self.chb.cl_times[0], 1315 endtime=end_time, 1316 xmin=_xmin, 1317 xmax=_xmax, 1318 ymin=-3000, 1319 ymax=3000, 1320 ) 1321 1322 def insert_channel_into_horizons(self, layer, depth_maps): 1323 new_depth_maps = self.insert_feature_into_horizon_stack( 1324 self.chb.strat, layer, depth_maps 1325 ) 1326 return new_depth_maps 1327 1328 def insert_channel_facies(self): 1329 pass 1330 1331 1332class Facies: 1333 def __init__(self, parameters, max_layers, onlap_horizon_list, fan_horizon_list): 1334 self.cfg = parameters 1335 self.max_layers = max_layers 1336 self.onlap_horizon_list = onlap_horizon_list 1337 self.fan_horizon_list = fan_horizon_list 1338 self.facies = None 1339 1340 def sand_shale_facies_binomial_dist(self): 1341 """Randomly select sand or shale facies using binomial distribution using the sand_layer_pct from config file""" 1342 sand_layer = np.random.binomial( 1343 1, self.cfg.sand_layer_pct, size=self.max_layers 1344 ) 1345 # Insert a water-layer code of -1 at the top 1346 self.facies = np.hstack((np.array((-1.0)), sand_layer)) 1347 1348 def sand_shale_facies_markov(self): 1349 """Generate a 1D array of facies usinga 2-state Markov process 1350 1351 Note: the Binomial distribution can be generated by setting the sand_layer_thickness to 1/sand_layer_pct 1352 """ 1353 mk = MarkovChainFacies( 1354 self.cfg.sand_layer_pct, self.cfg.sand_layer_thickness, (0, 1) 1355 ) 1356 # Randomly select initial state 1357 facies = mk.generate_states(np.random.choice(2, 1)[0], num=self.max_layers) 1358 self.facies = np.hstack((np.array((-1.0)), facies)) 1359 1360 def set_layers_below_onlaps_to_shale(self): 1361 """ 1362 Set layers immediately below tilting episode (onlap surface) to shale. 1363 1364 Set facies array to 0 (shale) above any layer which is marked as being an onlap surface 1365 """ 1366 onlap_horizon_list_plus_one = np.array(self.onlap_horizon_list) 1367 onlap_horizon_list_plus_one += 1 1368 self.facies[onlap_horizon_list_plus_one] = 0.0 1369 1370 def set_fan_facies(self, depth_maps): 1371 """Set fan facies as sand layers surrounded by shales 1372 1373 Set the fan layer to be a sand. 1374 Set the layers above and below the fan to be shale 1375 """ 1376 # Find those layers which are directly above (and touching) the fan layer 1377 layers_with_zero_thickness = set(np.where(np.diff(depth_maps) == 0)[2]) 1378 layers_above_fan = [] 1379 for f in self.fan_horizon_list: 1380 for i in range(f - 1, 1, -1): 1381 if i in layers_with_zero_thickness: 1382 layers_above_fan.append(i) 1383 else: 1384 break 1385 sand = list(self.fan_horizon_list) 1386 shale = layers_above_fan + list(self.fan_horizon_list + 1) 1387 self.facies[sand] = 1.0 1388 self.facies[shale] = 0.0 1389 1390 1391class MarkovChainFacies: 1392 def __init__(self, sand_fraction, sand_thickness, states=(0, 1)): 1393 """Initialize the MarkovChain instance. 1394 1395 Parameters 1396 ---------- 1397 sand_fraction : float 1398 Fraction of sand layers in model 1399 sand_thickness : int 1400 Thickness of sand layers, given in units of layers 1401 states : iterable 1402 An iterable representing the states of the Markov Chain. 1403 """ 1404 self.sand_fraction = sand_fraction 1405 self.sand_thickness = sand_thickness 1406 self.states = states 1407 self.index_dict = { 1408 self.states[index]: index for index in range(len(self.states)) 1409 } 1410 self.state_dict = { 1411 index: self.states[index] for index in range(len(self.states)) 1412 } 1413 self._transition_matrix() 1414 1415 def _transition_matrix(self): 1416 """TODO Stephs notes go here""" 1417 beta = 1 / self.sand_thickness 1418 alpha = self.sand_fraction / (self.sand_thickness * (1 - self.sand_fraction)) 1419 self.transition = np.array([[1 - alpha, alpha], [beta, 1 - beta]]) 1420 1421 def next_state(self, current_state): 1422 """Returns the state of the random variable at the next instance. 1423 1424 Parameters 1425 ---------- 1426 current_state :str 1427 The current state of the system 1428 """ 1429 return np.random.choice( 1430 self.states, p=self.transition[self.index_dict[current_state], :] 1431 ) 1432 1433 def generate_states(self, current_state, num=100): 1434 """Generate states of the system with length num 1435 1436 Parameters 1437 ---------- 1438 current_state : str 1439 The state of the current random variable 1440 num : int, optional 1441 [description], by default 100 1442 """ 1443 future_states = [] 1444 for _ in range(num): 1445 next_state = self.next_state(current_state) 1446 future_states.append(next_state) 1447 current_state = next_state 1448 return np.array(future_states) 1449 1450 1451def build_unfaulted_depth_maps(parameters: Parameters): 1452 """ 1453 Build Unfaulted Depth Maps 1454 -------------------------- 1455 Generates unfaulted depth maps. 1456 1457 1. Build stack of horizons. 1458 2. Generate a random stack of horizons. 1459 3. Optionally insert basin floor fans 1460 4. Insert onlap episodes 1461 1462 Parameters 1463 ---------- 1464 parameters : str 1465 The key desired to be accessed 1466 1467 Returns 1468 ------- 1469 depth_maps : np.array 1470 The generated depth maps 1471 onlap_horizon_list : list 1472 Onlapping Horizon list 1473 fan_list : np.array | None 1474 List of generated fans 1475 fan_thicknesses : np.array | None 1476 Generated fan thicknesses 1477 """ 1478 horizons = RandomHorizonStack(parameters) 1479 horizons.create_depth_maps() 1480 # Insert onlap episodes 1481 onlaps = Onlaps( 1482 parameters, horizons.depth_maps, horizons.thicknesses, horizons.max_layers 1483 ) 1484 onlap_horizon_list = onlaps.insert_tilting_episodes() 1485 # Insert seafloor 1486 depth_maps = horizons.insert_seafloor(horizons.depth_maps) 1487 1488 fan_list = None 1489 fan_thicknesses = None 1490 if parameters.basin_floor_fans: 1491 bff = BasinFloorFans(parameters, horizons.max_layers) 1492 # Insert Fans 1493 depth_maps = bff.insert_fans_into_horizons(horizons.depth_maps) 1494 for layer, thickness in zip(bff.fan_layers, bff.fan_thicknesses): 1495 bff.fan_qc_plot(depth_maps, layer, thickness) 1496 fan_list = bff.fan_layers 1497 fan_thicknesses = bff.fan_thicknesses 1498 return depth_maps, onlap_horizon_list, fan_list, fan_thicknesses 1499 1500 1501def create_facies_array( 1502 parameters: Parameters, 1503 depth_maps: np.ndarray, 1504 onlap_horizons: list, 1505 fan_horizons: np.ndarray = None 1506) -> np.ndarray: 1507 """ 1508 Create Facies Array 1509 -------------------------- 1510 Generates facies for the model and return an array with the facies. 1511 1512 Parameters 1513 ---------- 1514 parameters : datagenerator.Parameters 1515 Parameter object storing all model parameters. 1516 depth_maps : np.ndarray 1517 A numpy array containing the depth maps. 1518 onlap_horizons : list 1519 A list of the onlap horizons. 1520 fan_horizons : np.ndarray 1521 The fan horizons. 1522 1523 Returns 1524 ------- 1525 facies : np.ndarray 1526 An array that contains the facies for the model. 1527 """ 1528 facies = Facies(parameters, depth_maps.shape[-1], onlap_horizons, fan_horizons) 1529 # facies.sand_shale_facies_binomial_dist() 1530 facies.sand_shale_facies_markov() 1531 if len(onlap_horizons) > 0: 1532 facies.set_layers_below_onlaps_to_shale() 1533 if np.any(fan_horizons): 1534 facies.set_fan_facies(depth_maps) 1535 # Write sand layer % to logfile 1536 sand_pct = facies.facies[facies.facies == 1].size / facies.facies.size 1537 parameters.write_to_logfile( 1538 f"Percent of layers containing sand a priori: {parameters.sand_layer_pct:0.2%}", 1539 mainkey="model_parameters", 1540 subkey="sand_layer_thickness_a_priori", 1541 val=parameters.sand_layer_pct, 1542 ) 1543 parameters.write_to_logfile( 1544 f"Sand unit thickness (in layers) a priori: {parameters.sand_layer_thickness}", 1545 mainkey="model_parameters", 1546 subkey="sand_unit_thickness_a_priori", 1547 val=parameters.sand_layer_thickness, 1548 ) 1549 parameters.write_to_logfile( 1550 f"Percent of layers containing sand a posteriori: {sand_pct:.2%}", 1551 mainkey="model_parameters", 1552 subkey="sand_layer_percent_a_posteriori", 1553 val=sand_pct, 1554 ) 1555 # Count consecutive occurrences (i.e. facies thickness in layers) 1556 facies_groups = [(k, sum(1 for i in g)) for k, g in groupby(facies.facies)] 1557 if parameters.verbose: 1558 print(f"Facies units: {facies_groups}") 1559 avg_sand_thickness = np.nan_to_num( 1560 np.mean([b for a, b in facies_groups if a == 1.0]) 1561 ) # convert nan to 0 1562 parameters.write_to_logfile( 1563 f"Average Sand unit thickness (in layers) a posteriori: {avg_sand_thickness:.1f}", 1564 mainkey="model_parameters", 1565 subkey="sand_unit_thickness_a_posteriori", 1566 val=avg_sand_thickness, 1567 ) 1568 1569 return facies.facies 1570 1571 1572if __name__ == "__main__": 1573 par = Parameters(user_config="../config/config_bps.json", test_mode=100) 1574 par.setup_model() 1575 # p.cube_shape = (100, 100, 1250) 1576 zmaps, onlaps, fan_list, fan_thickness = build_unfaulted_depth_maps(par) 1577 facies = create_facies_array(par, zmaps, onlaps, fan_list) 1578 print(facies)
10class Horizons: 11 def __init__(self, parameters): 12 self.cfg = parameters 13 self.max_layers = 0 14 15 def insert_feature_into_horizon_stack(self, feature, layer_number, maps): 16 """ 17 Insert an object into horizons which is to be inserted into a single layer 18 19 Made for fans, but should work for any object. 20 Shallower layer thicknesses are adjusted to insert the feature (clipped downwards onto the feature). 21 """ 22 layer_count = 1 23 if feature.ndim == 3: 24 layer_count = feature.shape[-1] 25 new_maps = np.zeros( 26 ( 27 self.cfg.cube_shape[0], 28 self.cfg.cube_shape[1], 29 maps.shape[2] + layer_count, 30 ) 31 ) 32 # Shift horizons below layer downwards by 1 to insert object in specified layer 33 new_maps[..., layer_number + layer_count :] = maps[..., layer_number:] 34 # Insert object into layer_number 35 new_maps[..., layer_number] = maps[..., layer_number] - feature 36 # copy horizons above layer_number into the new set of maps 37 new_maps[..., :layer_number] = maps[..., :layer_number] 38 39 # Don't allow negative thickness after inserting the fan 40 for i in range(new_maps.shape[-1] - 1, 1, -1): 41 layer_thickness = new_maps[..., i] - new_maps[..., i - 1] 42 if np.min(layer_thickness) < 0: 43 np.clip(layer_thickness, 0, a_max=None, out=layer_thickness) 44 new_maps[..., i - 1] = new_maps[..., i] - layer_thickness 45 # Increase the max number of layers in model by 1 46 self.max_layers += layer_count 47 return new_maps 48 49 def insert_seafloor(self, maps): 50 if self.cfg.verbose: 51 print("\n ... inserting 'water bottom' reflector in work cube ...\n") 52 wb_time_map = maps[:, :, 1] - 1.5 53 wb_stats = [ 54 f"{x * self.cfg.digi / self.cfg.infill_factor:.2f}" 55 for x in [wb_time_map.min(), wb_time_map.mean(), wb_time_map.max()] 56 ] 57 self.cfg.write_to_logfile("Seabed Min: {}, Mean: {}, Max: {}".format(*wb_stats)) 58 59 self.cfg.write_to_logfile( 60 msg=None, 61 mainkey="model_parameters", 62 subkey="seabed_min", 63 val=wb_time_map.min(), 64 ) 65 self.cfg.write_to_logfile( 66 msg=None, 67 mainkey="model_parameters", 68 subkey="seabed_mean", 69 val=wb_time_map.mean(), 70 ) 71 self.cfg.write_to_logfile( 72 msg=None, 73 mainkey="model_parameters", 74 subkey="seabed_max", 75 val=wb_time_map.max(), 76 ) 77 78 maps[:, :, 0] = wb_time_map.copy() 79 return maps 80 81 def create_random_net_over_gross_map( 82 self, avg=(0.45, 0.9), stdev=(0.01, 0.05), octave=9 83 ): 84 random_net_over_gross_map = self._perlin(base=None, octave=octave) 85 86 avg_net_over_gross = np.random.uniform(*avg) 87 avg_net_over_gross_stdev = np.random.uniform(*stdev) 88 89 # set stdev and mean of map to desired values 90 random_net_over_gross_map -= random_net_over_gross_map.mean() 91 random_net_over_gross_map *= ( 92 avg_net_over_gross_stdev / random_net_over_gross_map.std() 93 ) 94 random_net_over_gross_map += avg_net_over_gross 95 # clip to stay within a reasonable N/G range 96 random_net_over_gross_map = random_net_over_gross_map.clip(*avg) 97 return random_net_over_gross_map 98 99 def _perlin(self, base=None, octave=1, lac=1.9, do_rotate=True): 100 import noise 101 102 xsize = self.cfg.cube_shape[0] 103 ysize = self.cfg.cube_shape[1] 104 if base is None: 105 base = np.random.randint(255) 106 # randomly rotate image 107 if do_rotate: 108 number_90_deg_rotations = 0 109 fliplr = False 110 flipud = False 111 if xsize == ysize and np.random.binomial(1, 0.5) == 1: 112 number_90_deg_rotations = int(np.random.uniform(1, 4)) 113 # temp = np.rot90(temp, number_90_deg_rotations) 114 # randomly flip left and right, top and bottom 115 if np.random.binomial(1, 0.5) == 1: 116 fliplr = True 117 if np.random.binomial(1, 0.5) == 1: 118 flipud = True 119 120 temp = np.array( 121 [ 122 [ 123 noise.pnoise2( 124 float(i) / xsize, 125 float(j) / ysize, 126 lacunarity=lac, 127 octaves=octave, 128 base=base, 129 ) 130 for j in range(ysize) 131 ] 132 for i in range(xsize) 133 ] 134 ) 135 temp = np.rot90(temp, number_90_deg_rotations) 136 if fliplr: 137 temp = np.fliplr(temp) 138 if flipud: 139 temp = np.flipud(temp) 140 return temp 141 142 def _fit_plane_strike_dip(self, azimuth, dip, grid_shape, verbose=False): 143 # Fits a plane given dip and max dip direction (azimuth) 144 # - create a point at the center of the grid, elevation is zero 145 xyz1 = np.array([grid_shape[0] / 2.0, grid_shape[1] / 2.0, 0.0]) 146 # y = np.array([0., 1., 1.]) 147 # - create a point in the strike direction at same elevation of zero 148 strike_angle = azimuth + 90.0 149 if strike_angle > 360.0: 150 strike_angle -= 360.0 151 if strike_angle > 180.0: 152 strike_angle -= 180.0 153 strike_angle *= np.pi / 180.0 154 distance = np.min(grid_shape) / 4.0 155 x = distance * math.cos(strike_angle) + grid_shape[0] / 2.0 156 y = distance * math.sin(strike_angle) + grid_shape[1] / 2.0 157 xyz2 = np.array([x, y, 0.0]) 158 # - create a point in the max dip direction 159 dip_angle = dip * 1.0 160 if dip_angle > 360.0: 161 dip_angle -= 360.0 162 if dip_angle > 180.0: 163 dip_angle -= 180.0 164 dip_angle *= np.pi / 180.0 165 strike_angle = azimuth 166 if strike_angle > 360.0: 167 strike_angle -= 360.0 168 if strike_angle > 180.0: 169 strike_angle -= 180.0 170 dip_elev = -distance * math.sin(dip_angle) * math.sqrt(2.0) 171 else: 172 dip_elev = distance * math.sin(dip_angle) * math.sqrt(2.0) 173 strike_angle *= np.pi / 180.0 174 x = distance * math.cos(strike_angle) + grid_shape[0] / 2.0 175 y = distance * math.sin(strike_angle) + grid_shape[1] / 2.0 176 xyz3 = np.array([x, y, dip_elev]) 177 # - combine 3 points into single array 178 xyz = np.vstack((xyz1, xyz2, xyz3)) 179 # - fit three points to a plane and compute elevation for all grids on surface 180 a, b, c = self.fit_plane_lsq(xyz) 181 z = self.eval_plane(range(grid_shape[0]), range(grid_shape[1]), a, b, c) 182 # - make plot if not quiet 183 if verbose: 184 import os 185 from datagenerator.util import import_matplotlib 186 187 plt = import_matplotlib() 188 print(f"strike angle = {strike_angle}") 189 print(f"dip angle = {dip_angle}") 190 print(f"points are: {xyz}") 191 plt.figure(1) 192 plt.clf() 193 plt.grid() 194 plt.imshow(z, origin="origin") 195 plt.plot(xyz[:, 0], xyz[:, 1], "yo") 196 plt.colorbar() 197 plt.savefig(os.path.join(self.cfg.work_subfolder, "dipping_plane.png")) 198 plt.close() 199 return z 200 201 @staticmethod 202 def fit_plane_lsq(xyz): 203 # Fits a plane to a point cloud, 204 # Where Z = aX + bY + c ----Eqn #1 205 # Rearranging Eqn1: aX + bY -Z +c =0 206 # Gives normal (a,b,-1) 207 # Normal = (a,b,-1) 208 # [rows, cols] = xyz.shape 209 rows = xyz.shape[0] 210 g = np.ones((rows, 3)) 211 g[:, 0] = xyz[:, 0] # X 212 g[:, 1] = xyz[:, 1] # Y 213 z = xyz[:, 2] 214 (a, b, c), _, _, _ = np.linalg.lstsq(g, z, rcond=-1) 215 normal = np.array([a, b, c]) 216 return normal 217 218 @staticmethod 219 def eval_plane(x, y, a, b, c): 220 # evaluates and returns Z for each input X,Y 221 z = np.zeros((len(x), len(y)), "float") 222 for i in range(len(x)): 223 for j in range(len(y)): 224 z[i, j] = a * x[i] + b * y[j] + c 225 return z 226 227 @staticmethod 228 def halton(dim, nbpts): 229 import math 230 231 h = np.empty(nbpts * dim) 232 h.fill(np.nan) 233 p = np.empty(nbpts) 234 p.fill(np.nan) 235 p1 = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] 236 lognbpts = math.log(nbpts + 1) 237 for i in range(dim): 238 b = p1[i] 239 n = int(math.ceil(lognbpts / math.log(b))) 240 for t in range(n): 241 p[t] = pow(b, -(t + 1)) 242 243 for j in range(nbpts): 244 d = j + 1 245 sum_ = math.fmod(d, b) * p[0] 246 for t in range(1, n): 247 d = math.floor(d / b) 248 sum_ += math.fmod(d, b) * p[t] 249 250 h[j * dim + i] = sum_ 251 252 return h.reshape(nbpts, dim) 253 254 @staticmethod 255 def rotate_point(x, y, angle_in_degrees): 256 """Calculate new coordinates for point after coordinate rotation about (0,0) by angleDegrees""" 257 angle = angle_in_degrees * np.pi / 180.0 258 x1 = math.cos(angle) * x + math.sin(angle) * y 259 y1 = -math.sin(angle) * x + math.cos(angle) * y 260 return x1, y1 261 262 def convert_map_from_samples_to_units(self, maps): 263 """Use digi to convert a copy of the provided maps from samples to units""" 264 converted_maps = maps.copy() 265 converted_maps *= float(self.cfg.digi) 266 return converted_maps 267 268 def write_maps_to_disk(self, horizons, name): 269 """Write horizons to disk.""" 270 fname = os.path.join(self.cfg.work_subfolder, name) 271 np.save(fname, horizons) 272 273 def write_onlap_episodes( 274 self, onlap_horizon_list, depth_maps_gaps, depth_maps_infilled, n=35 275 ): 276 """Write gapped horizons with onlaps + n shallower horizons to a separate files.""" 277 tilting_zmaps = list() 278 interval_thickness_size = depth_maps_gaps.shape[0] * depth_maps_gaps.shape[1] 279 for ihor in onlap_horizon_list: 280 for jhor in range(ihor - n, ihor + 1): 281 if jhor > 0: 282 # compute thickness compared to next horizon using infilled depth maps 283 interval_thickness = ( 284 depth_maps_infilled[:, :, jhor + 1] 285 - depth_maps_infilled[:, :, jhor] 286 ) 287 # compute percentage of interval filled with zeros 288 interval_thickness_zeros_size = interval_thickness[ 289 interval_thickness < 1.0e-5 290 ].shape[0] 291 pct_zeros = ( 292 float(interval_thickness_zeros_size) / interval_thickness_size 293 ) 294 if pct_zeros > 0.02: 295 tilting_zmaps.append(jhor) 296 # Make the tilting maps unique and sorted 297 tilting_zmaps = list(set(tilting_zmaps)) 298 tilting_zmaps.sort() 299 onlaps = np.zeros( 300 (depth_maps_gaps.shape[0], depth_maps_gaps.shape[1], len(tilting_zmaps)) 301 ) 302 for count, h in enumerate(tilting_zmaps): 303 onlaps[..., count] = depth_maps_gaps[..., h] 304 305 # Write to disk 306 self.write_maps_to_disk(onlaps * self.cfg.digi, "depth_maps_onlaps") 307 308 if self.cfg.hdf_store: 309 # Write onlap maps to hdf 310 from datagenerator.util import write_data_to_hdf 311 312 write_data_to_hdf( 313 "depth_maps_onlaps", onlaps * self.cfg.digi, self.cfg.hdf_master 314 ) 315 316 def write_fan_horizons(self, fan_horizon_list, depth_maps): 317 """Write fan layers to a separate file.""" 318 z = depth_maps.copy() 319 fan_horizons = np.zeros((z.shape[0], z.shape[1], len(fan_horizon_list))) 320 for count, horizon in enumerate(fan_horizon_list): 321 thickness_map = z[..., horizon + 1] - z[..., horizon] 322 z[..., horizon][thickness_map <= 0] = np.nan 323 if self.cfg.verbose: 324 print( 325 f"Fan horizon number: {horizon} Max thickness: {np.nanmax(thickness_map)}" 326 ) 327 fan_horizons[..., count] = z[..., horizon] 328 self.write_maps_to_disk(fan_horizons, "depth_maps_fans")
15 def insert_feature_into_horizon_stack(self, feature, layer_number, maps): 16 """ 17 Insert an object into horizons which is to be inserted into a single layer 18 19 Made for fans, but should work for any object. 20 Shallower layer thicknesses are adjusted to insert the feature (clipped downwards onto the feature). 21 """ 22 layer_count = 1 23 if feature.ndim == 3: 24 layer_count = feature.shape[-1] 25 new_maps = np.zeros( 26 ( 27 self.cfg.cube_shape[0], 28 self.cfg.cube_shape[1], 29 maps.shape[2] + layer_count, 30 ) 31 ) 32 # Shift horizons below layer downwards by 1 to insert object in specified layer 33 new_maps[..., layer_number + layer_count :] = maps[..., layer_number:] 34 # Insert object into layer_number 35 new_maps[..., layer_number] = maps[..., layer_number] - feature 36 # copy horizons above layer_number into the new set of maps 37 new_maps[..., :layer_number] = maps[..., :layer_number] 38 39 # Don't allow negative thickness after inserting the fan 40 for i in range(new_maps.shape[-1] - 1, 1, -1): 41 layer_thickness = new_maps[..., i] - new_maps[..., i - 1] 42 if np.min(layer_thickness) < 0: 43 np.clip(layer_thickness, 0, a_max=None, out=layer_thickness) 44 new_maps[..., i - 1] = new_maps[..., i] - layer_thickness 45 # Increase the max number of layers in model by 1 46 self.max_layers += layer_count 47 return new_maps
Insert an object into horizons which is to be inserted into a single layer
Made for fans, but should work for any object. Shallower layer thicknesses are adjusted to insert the feature (clipped downwards onto the feature).
49 def insert_seafloor(self, maps): 50 if self.cfg.verbose: 51 print("\n ... inserting 'water bottom' reflector in work cube ...\n") 52 wb_time_map = maps[:, :, 1] - 1.5 53 wb_stats = [ 54 f"{x * self.cfg.digi / self.cfg.infill_factor:.2f}" 55 for x in [wb_time_map.min(), wb_time_map.mean(), wb_time_map.max()] 56 ] 57 self.cfg.write_to_logfile("Seabed Min: {}, Mean: {}, Max: {}".format(*wb_stats)) 58 59 self.cfg.write_to_logfile( 60 msg=None, 61 mainkey="model_parameters", 62 subkey="seabed_min", 63 val=wb_time_map.min(), 64 ) 65 self.cfg.write_to_logfile( 66 msg=None, 67 mainkey="model_parameters", 68 subkey="seabed_mean", 69 val=wb_time_map.mean(), 70 ) 71 self.cfg.write_to_logfile( 72 msg=None, 73 mainkey="model_parameters", 74 subkey="seabed_max", 75 val=wb_time_map.max(), 76 ) 77 78 maps[:, :, 0] = wb_time_map.copy() 79 return maps
81 def create_random_net_over_gross_map( 82 self, avg=(0.45, 0.9), stdev=(0.01, 0.05), octave=9 83 ): 84 random_net_over_gross_map = self._perlin(base=None, octave=octave) 85 86 avg_net_over_gross = np.random.uniform(*avg) 87 avg_net_over_gross_stdev = np.random.uniform(*stdev) 88 89 # set stdev and mean of map to desired values 90 random_net_over_gross_map -= random_net_over_gross_map.mean() 91 random_net_over_gross_map *= ( 92 avg_net_over_gross_stdev / random_net_over_gross_map.std() 93 ) 94 random_net_over_gross_map += avg_net_over_gross 95 # clip to stay within a reasonable N/G range 96 random_net_over_gross_map = random_net_over_gross_map.clip(*avg) 97 return random_net_over_gross_map
201 @staticmethod 202 def fit_plane_lsq(xyz): 203 # Fits a plane to a point cloud, 204 # Where Z = aX + bY + c ----Eqn #1 205 # Rearranging Eqn1: aX + bY -Z +c =0 206 # Gives normal (a,b,-1) 207 # Normal = (a,b,-1) 208 # [rows, cols] = xyz.shape 209 rows = xyz.shape[0] 210 g = np.ones((rows, 3)) 211 g[:, 0] = xyz[:, 0] # X 212 g[:, 1] = xyz[:, 1] # Y 213 z = xyz[:, 2] 214 (a, b, c), _, _, _ = np.linalg.lstsq(g, z, rcond=-1) 215 normal = np.array([a, b, c]) 216 return normal
227 @staticmethod 228 def halton(dim, nbpts): 229 import math 230 231 h = np.empty(nbpts * dim) 232 h.fill(np.nan) 233 p = np.empty(nbpts) 234 p.fill(np.nan) 235 p1 = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31] 236 lognbpts = math.log(nbpts + 1) 237 for i in range(dim): 238 b = p1[i] 239 n = int(math.ceil(lognbpts / math.log(b))) 240 for t in range(n): 241 p[t] = pow(b, -(t + 1)) 242 243 for j in range(nbpts): 244 d = j + 1 245 sum_ = math.fmod(d, b) * p[0] 246 for t in range(1, n): 247 d = math.floor(d / b) 248 sum_ += math.fmod(d, b) * p[t] 249 250 h[j * dim + i] = sum_ 251 252 return h.reshape(nbpts, dim)
254 @staticmethod 255 def rotate_point(x, y, angle_in_degrees): 256 """Calculate new coordinates for point after coordinate rotation about (0,0) by angleDegrees""" 257 angle = angle_in_degrees * np.pi / 180.0 258 x1 = math.cos(angle) * x + math.sin(angle) * y 259 y1 = -math.sin(angle) * x + math.cos(angle) * y 260 return x1, y1
Calculate new coordinates for point after coordinate rotation about (0,0) by angleDegrees
262 def convert_map_from_samples_to_units(self, maps): 263 """Use digi to convert a copy of the provided maps from samples to units""" 264 converted_maps = maps.copy() 265 converted_maps *= float(self.cfg.digi) 266 return converted_maps
Use digi to convert a copy of the provided maps from samples to units
268 def write_maps_to_disk(self, horizons, name): 269 """Write horizons to disk.""" 270 fname = os.path.join(self.cfg.work_subfolder, name) 271 np.save(fname, horizons)
Write horizons to disk.
273 def write_onlap_episodes( 274 self, onlap_horizon_list, depth_maps_gaps, depth_maps_infilled, n=35 275 ): 276 """Write gapped horizons with onlaps + n shallower horizons to a separate files.""" 277 tilting_zmaps = list() 278 interval_thickness_size = depth_maps_gaps.shape[0] * depth_maps_gaps.shape[1] 279 for ihor in onlap_horizon_list: 280 for jhor in range(ihor - n, ihor + 1): 281 if jhor > 0: 282 # compute thickness compared to next horizon using infilled depth maps 283 interval_thickness = ( 284 depth_maps_infilled[:, :, jhor + 1] 285 - depth_maps_infilled[:, :, jhor] 286 ) 287 # compute percentage of interval filled with zeros 288 interval_thickness_zeros_size = interval_thickness[ 289 interval_thickness < 1.0e-5 290 ].shape[0] 291 pct_zeros = ( 292 float(interval_thickness_zeros_size) / interval_thickness_size 293 ) 294 if pct_zeros > 0.02: 295 tilting_zmaps.append(jhor) 296 # Make the tilting maps unique and sorted 297 tilting_zmaps = list(set(tilting_zmaps)) 298 tilting_zmaps.sort() 299 onlaps = np.zeros( 300 (depth_maps_gaps.shape[0], depth_maps_gaps.shape[1], len(tilting_zmaps)) 301 ) 302 for count, h in enumerate(tilting_zmaps): 303 onlaps[..., count] = depth_maps_gaps[..., h] 304 305 # Write to disk 306 self.write_maps_to_disk(onlaps * self.cfg.digi, "depth_maps_onlaps") 307 308 if self.cfg.hdf_store: 309 # Write onlap maps to hdf 310 from datagenerator.util import write_data_to_hdf 311 312 write_data_to_hdf( 313 "depth_maps_onlaps", onlaps * self.cfg.digi, self.cfg.hdf_master 314 )
Write gapped horizons with onlaps + n shallower horizons to a separate files.
316 def write_fan_horizons(self, fan_horizon_list, depth_maps): 317 """Write fan layers to a separate file.""" 318 z = depth_maps.copy() 319 fan_horizons = np.zeros((z.shape[0], z.shape[1], len(fan_horizon_list))) 320 for count, horizon in enumerate(fan_horizon_list): 321 thickness_map = z[..., horizon + 1] - z[..., horizon] 322 z[..., horizon][thickness_map <= 0] = np.nan 323 if self.cfg.verbose: 324 print( 325 f"Fan horizon number: {horizon} Max thickness: {np.nanmax(thickness_map)}" 326 ) 327 fan_horizons[..., count] = z[..., horizon] 328 self.write_maps_to_disk(fan_horizons, "depth_maps_fans")
Write fan layers to a separate file.
331class RandomHorizonStack(Horizons): 332 def __init__(self, parameters): 333 self.cfg = parameters 334 self.depth_maps = None 335 self.depth_maps_gaps = None 336 self.max_layers = 0 337 338 # Look up tables 339 self.thicknesses = None 340 self.onlaps = None 341 self.channels = None 342 self.dips = None 343 self.azimuths = None 344 self.facies = None 345 346 def _generate_lookup_tables(self): 347 # Thicknesses 348 self.thicknesses = stats.gamma.rvs(4.0, 2, size=self.cfg.num_lyr_lut) 349 # Onlaps 350 onlap_layer_list = np.sort( 351 np.random.uniform( 352 low=5, high=200, size=int(np.random.triangular(1, 4, 7) + 0.5) 353 ).astype("int") 354 ) 355 # Dips 356 self.dips = ( 357 (1.0 - random.power(100, self.cfg.num_lyr_lut)) 358 * 7.0 359 * self.cfg.dip_factor_max 360 ) 361 # Azimuths 362 self.azimuths = np.random.uniform( 363 low=0.0, high=360.0, size=(self.cfg.num_lyr_lut,) 364 ) 365 # Channels 366 self.channels = np.random.binomial(1, 3.0 / 100.0, self.cfg.num_lyr_lut) 367 368 if self.cfg.verbose: 369 print("self.cfg.num_lyr_lut = ", self.cfg.num_lyr_lut) 370 print("onlap_layer_list = ", onlap_layer_list) 371 onlap_array_dim = int(500 / 1250 * self.cfg.cube_shape[2]) 372 self.onlaps = np.zeros(onlap_array_dim, "int") 373 self.onlaps[onlap_layer_list] = 1 374 375 if not self.cfg.include_channels: 376 self.channels *= 0 377 else: 378 # Make sure channel episodes don't overlap too much 379 for i in range(len(self.channels)): 380 if self.channels[i] == 1: 381 self.channels[i + 1 : i + 6] *= 0 382 383 if self.cfg.verbose: 384 print( 385 f"Number of onlapping flags: {self.onlaps[self.onlaps == 1].shape[0]}" 386 ) 387 print( 388 f" ... horizon number for first onlap episode = {np.argmax(self.onlaps)}" 389 ) 390 print( 391 f" ... number of channelFlags: {self.channels[:250][self.channels[:250] == 1].shape[0]}" 392 ) 393 print( 394 f" ... horizon number for first channel episode: {np.argmax(self.channels)}" 395 ) 396 397 def _random_layer_thickness(self): 398 rand_oct = int(np.random.triangular(left=1.3, mode=2.65, right=5.25)) 399 400 low_thickness_factor = np.random.triangular(left=0.05, mode=0.2, right=0.95) 401 402 high_thickness_factor = np.random.triangular(left=1.05, mode=1.8, right=2.2) 403 404 thickness_factor_map = self._perlin(octave=rand_oct) 405 thickness_factor_map -= thickness_factor_map.mean() 406 thickness_factor_map *= (high_thickness_factor - low_thickness_factor) / ( 407 thickness_factor_map.max() - thickness_factor_map.min() 408 ) 409 thickness_factor_map += 1.0 410 if thickness_factor_map.min() < 0.0: 411 thickness_factor_map -= thickness_factor_map.min() - 0.05 412 413 if self.cfg.verbose: 414 print( 415 f" {thickness_factor_map.mean()}, " 416 f"{thickness_factor_map.min()}, " 417 f" {thickness_factor_map.max()}, " 418 f" {thickness_factor_map.std()}" 419 ) 420 421 return thickness_factor_map 422 423 def _generate_random_depth_structure_map( 424 self, 425 dip_range=(0.25, 1.0), 426 num_points_range=(2, 25), 427 elevation_std=100.0, 428 zero_at_corners=True, 429 initial=False, 430 ): 431 ############################################################################ 432 # generate a 2D array representing a depth (or TWT) structure map. 433 # - grid_size controls output size in (x,y), units are samples 434 # - dip_range controls slope of dipping plane upon which random residual points are placed 435 # - num_points controls number of randomly positioned points are used for residual grid 436 # - elevation_std controls std dev for residual elevation of random points in residual grid 437 ############################################################################ 438 grid_size = self.cfg.cube_shape[:2] 439 # define output grid (padded by 15% in each direction) 440 xi = np.linspace(-0.15, 1.15, int(grid_size[0] * 1.3)) 441 yi = np.linspace(-0.15, 1.15, int(grid_size[1] * 1.3)) 442 443 # start with a gently dipping plane similar to that found on passive shelf margins (< 1 degree dip) 444 azimuth = random.uniform(0.0, 360.0) 445 dip = random.uniform(dip_range[0], dip_range[1]) 446 447 # build a residual surface to add to the dipping plane 448 number_halton_points = int(random.uniform(100, 500) + 0.5) 449 number_random_points = int( 450 random.uniform(num_points_range[0], num_points_range[1]) + 0.5 451 ) 452 z = np.random.rand(number_random_points) 453 z -= z.mean() 454 if initial: 455 elevation_std = self.cfg.initial_layer_stdev 456 z *= elevation_std / z.std() 457 458 dipping_plane = self._fit_plane_strike_dip( 459 azimuth, dip, grid_size, verbose=False 460 ) 461 xx = self.halton(2, number_halton_points)[-number_random_points:] 462 463 # make up some randomly distributed data 464 # - adjust for padding 465 xx *= xi[-1] - xi[0] 466 xx += xi[0] 467 x = xx[:, 0] 468 y = xx[:, 1] 469 470 if zero_at_corners: 471 x = np.hstack((x, [-0.15, -0.15, 1.15, 1.15])) 472 y = np.hstack((y, [-0.15, 1.15, -0.15, 1.15])) 473 z = np.hstack((z, [-0.15, -0.15, -0.15, -0.15])) 474 475 # grid the data. 476 zi = griddata( 477 np.column_stack((x, y)), 478 z, 479 (xi[:, np.newaxis], yi[np.newaxis, :]), 480 method="cubic", 481 ) 482 zi = zi.reshape(xi.shape[0], yi.shape[0]) 483 xi_min_index = np.argmin((xi - 0.0) ** 2) 484 xi_max_index = xi_min_index + grid_size[0] 485 yi_min_index = np.argmin((yi - 0.0) ** 2) 486 yi_max_index = yi_min_index + grid_size[1] 487 zi = zi[xi_min_index:xi_max_index, yi_min_index:yi_max_index] 488 # add to the gently sloping plane 489 zi += dipping_plane 490 491 if initial: 492 zi += dipping_plane - dipping_plane.min() 493 zi += self.cfg.cube_shape[-1] * self.cfg.infill_factor - zi.min() 494 zi_argmin_i, zi_argmin_j = np.unravel_index(zi.argmin(), zi.shape) 495 if self.cfg.verbose: 496 print( 497 f"\tIndices for shallowest point in cube: {zi_argmin_i}, {zi_argmin_j}" 498 ) 499 self.cfg.write_to_logfile( 500 f"number_random_points: {number_random_points:.0f}" 501 ) 502 self.cfg.write_to_logfile(f"dip angle: {dip:.2f}") 503 self.cfg.write_to_logfile( 504 f"dipping_plane min: {dipping_plane.min():.2f}, mean: {dipping_plane.mean():.2f}," 505 f" max: {dipping_plane.max():.2f}" 506 ) 507 self.cfg.write_to_logfile( 508 f"zi min: {zi.min():.2f}, mean: {zi.mean():.2f}, max: {zi.max():.2f}" 509 ) 510 511 self.cfg.write_to_logfile( 512 msg=None, 513 mainkey="model_parameters", 514 subkey="number_random_points", 515 val=number_random_points, 516 ) 517 self.cfg.write_to_logfile( 518 msg=None, 519 mainkey="model_parameters", 520 subkey="dip_angle", 521 val=number_random_points, 522 ) 523 self.cfg.write_to_logfile( 524 msg=None, 525 mainkey="model_parameters", 526 subkey="dipping_plane_min", 527 val=dipping_plane.min(), 528 ) 529 self.cfg.write_to_logfile( 530 msg=None, 531 mainkey="model_parameters", 532 subkey="dipping_plane_mean", 533 val=dipping_plane.mean(), 534 ) 535 self.cfg.write_to_logfile( 536 msg=None, 537 mainkey="model_parameters", 538 subkey="dipping_plane_max", 539 val=dipping_plane.max(), 540 ) 541 self.cfg.write_to_logfile( 542 msg=None, mainkey="model_parameters", subkey="zi_min", val=zi.min() 543 ) 544 self.cfg.write_to_logfile( 545 msg=None, mainkey="model_parameters", subkey="zi_mean", val=zi.mean() 546 ) 547 self.cfg.write_to_logfile( 548 msg=None, mainkey="model_parameters", subkey="zi_max", val=zi.max() 549 ) 550 551 return xx, zi 552 553 def _create_thickness_map(self, random_thickness_factor_map, layer_number): 554 """ 555 Create a random_thickness_map for a given layer. 556 557 Creates a dipping_plane with differential dip from previous layer and computes a new thickness_map which is 558 always positive (i.e. no erosion allowed) 559 560 Parameters 561 ---------- 562 random_thickness_factor_map : ndarray - a random thickness factor map for this layer 563 layer_number : int - the layer number being built in the stack of horizons 564 565 Returns 566 ------- 567 thickness_map : ndarray - the thickness_map for the given layer 568 """ 569 dipping_plane = self._fit_plane_strike_dip( 570 azimuth=self.azimuths[layer_number], 571 dip=self.dips[layer_number], 572 grid_shape=self.cfg.cube_shape[:2], 573 verbose=False, 574 ) 575 dipping_plane -= dipping_plane.min() 576 if self.cfg.verbose: 577 print( 578 f"azi, dip, dipping_plane min/mean/max = {self.azimuths[layer_number]}, {self.dips[layer_number]}, " 579 f"{dipping_plane.min():.2f}, {dipping_plane.mean():.2f}, {dipping_plane.max():.2f}" 580 ) 581 582 # compute new thickness map. For now, don't allow erosion (i.e. thickness_map is always positive) 583 thickness_map = ( 584 ( 585 np.ones(self.cfg.cube_shape[:2], "float") 586 * self.thicknesses[layer_number] 587 - dipping_plane 588 ) 589 * self.cfg.infill_factor 590 * random_thickness_factor_map 591 ) 592 thickness_map = np.clip( 593 thickness_map, 0.0, self.cfg.thickness_max * self.cfg.infill_factor * 1.5 594 ) 595 return thickness_map 596 597 def _create_initial_thickness_factor_map(self): 598 """ 599 Create the thickness_factor map for the layer at the base of the model 600 601 Returns 602 ------- 603 random_thickness_factor_map : ndarray - a random thickness factor map for the initial layer at base 604 """ 605 _, random_thickness_factor_map = self._generate_random_depth_structure_map( 606 dip_range=[0.0, 0.0], num_points_range=(25, 100), elevation_std=0.45 607 ) 608 random_thickness_factor_map -= random_thickness_factor_map.mean() - 1 609 # 610 return random_thickness_factor_map 611 612 def create_depth_maps(self): 613 """Building layers in reverse order - starting at bottom and depositing new layers on top. 614 615 Each layer has random residual dip and pseudo-random residual thickness 616 """ 617 self._generate_lookup_tables() 618 # Create initial depth map at base of model using initial=True 619 _, previous_depth_map = self._generate_random_depth_structure_map( 620 dip_range=[0.0, 75], 621 num_points_range=(3, 5), 622 initial=True, 623 elevation_std=self.cfg.initial_layer_stdev, 624 ) 625 626 # Build layers while minimum depth is less than the seabed_min_depth as set in config (given in metres). 627 # Convert min_depth into samples, don't forget about the infill factor 628 shallowest_depth_to_build = ( 629 self.cfg.seabed_min_depth / float(self.cfg.digi) 630 ) * self.cfg.infill_factor 631 632 # Build layers in a loop from deep to shallow until the minimum depth is reached and then break out. 633 for i in range(20000): 634 # Do the special case for the random_thickness_factor_map for the initial layer (at base) 635 if i == 0: 636 if self.cfg.verbose: 637 print("Building random depth map at base of model") 638 random_thickness_factor_map = ( 639 self._create_initial_thickness_factor_map() 640 ) 641 else: # Otherwise create standard random thickness factor map 642 if self.cfg.verbose: 643 print(f"Building Layer {i}") 644 random_thickness_factor_map = self._random_layer_thickness() 645 # Create the layer's thickness map using the random_thickness_factor_map 646 thickness_map = self._create_thickness_map(random_thickness_factor_map, i) 647 current_depth_map = previous_depth_map - thickness_map 648 649 if self.cfg.verbose: 650 print( 651 f"current_depth_map min/mean/max = {current_depth_map.min():.2f}, {current_depth_map.mean():.2f}," 652 f" {current_depth_map.max():.2f}" 653 ) 654 print( 655 f"thickness_map min/mean/max = {thickness_map.min():.2f}, {thickness_map.mean():.2f}," 656 f" {thickness_map.max():.2f}" 657 ) 658 659 # break out of loop when minimum depth is reached 660 if current_depth_map.min() <= shallowest_depth_to_build: 661 break 662 663 # replace previous depth map for next iteration 664 previous_depth_map = current_depth_map.copy() 665 # save depth map in (top of) 3d array. Resulting order is shallow at top 666 try: 667 depth_maps = np.dstack((current_depth_map, depth_maps)) 668 except UnboundLocalError: 669 # There is no stack of horizons yet - write the base. 670 depth_maps = current_depth_map.copy() 671 if self.cfg.verbose: 672 print(f"Layer {i}, depth_maps.shape = {depth_maps.shape}") 673 self.max_layers = i + 1 674 675 if self.cfg.verbose: 676 print("\n ... finished creating horizon layers ...") 677 # Store maps in hdf file 678 self.depth_maps = self.cfg.hdf_init("depth_maps", shape=depth_maps.shape) 679 self.depth_maps[:] = depth_maps 680 681 self.cfg.write_to_logfile( 682 f"number_layers: {self.max_layers}", 683 mainkey="model_parameters", 684 subkey="number_layers", 685 val=self.max_layers, 686 )
332 def __init__(self, parameters): 333 self.cfg = parameters 334 self.depth_maps = None 335 self.depth_maps_gaps = None 336 self.max_layers = 0 337 338 # Look up tables 339 self.thicknesses = None 340 self.onlaps = None 341 self.channels = None 342 self.dips = None 343 self.azimuths = None 344 self.facies = None
612 def create_depth_maps(self): 613 """Building layers in reverse order - starting at bottom and depositing new layers on top. 614 615 Each layer has random residual dip and pseudo-random residual thickness 616 """ 617 self._generate_lookup_tables() 618 # Create initial depth map at base of model using initial=True 619 _, previous_depth_map = self._generate_random_depth_structure_map( 620 dip_range=[0.0, 75], 621 num_points_range=(3, 5), 622 initial=True, 623 elevation_std=self.cfg.initial_layer_stdev, 624 ) 625 626 # Build layers while minimum depth is less than the seabed_min_depth as set in config (given in metres). 627 # Convert min_depth into samples, don't forget about the infill factor 628 shallowest_depth_to_build = ( 629 self.cfg.seabed_min_depth / float(self.cfg.digi) 630 ) * self.cfg.infill_factor 631 632 # Build layers in a loop from deep to shallow until the minimum depth is reached and then break out. 633 for i in range(20000): 634 # Do the special case for the random_thickness_factor_map for the initial layer (at base) 635 if i == 0: 636 if self.cfg.verbose: 637 print("Building random depth map at base of model") 638 random_thickness_factor_map = ( 639 self._create_initial_thickness_factor_map() 640 ) 641 else: # Otherwise create standard random thickness factor map 642 if self.cfg.verbose: 643 print(f"Building Layer {i}") 644 random_thickness_factor_map = self._random_layer_thickness() 645 # Create the layer's thickness map using the random_thickness_factor_map 646 thickness_map = self._create_thickness_map(random_thickness_factor_map, i) 647 current_depth_map = previous_depth_map - thickness_map 648 649 if self.cfg.verbose: 650 print( 651 f"current_depth_map min/mean/max = {current_depth_map.min():.2f}, {current_depth_map.mean():.2f}," 652 f" {current_depth_map.max():.2f}" 653 ) 654 print( 655 f"thickness_map min/mean/max = {thickness_map.min():.2f}, {thickness_map.mean():.2f}," 656 f" {thickness_map.max():.2f}" 657 ) 658 659 # break out of loop when minimum depth is reached 660 if current_depth_map.min() <= shallowest_depth_to_build: 661 break 662 663 # replace previous depth map for next iteration 664 previous_depth_map = current_depth_map.copy() 665 # save depth map in (top of) 3d array. Resulting order is shallow at top 666 try: 667 depth_maps = np.dstack((current_depth_map, depth_maps)) 668 except UnboundLocalError: 669 # There is no stack of horizons yet - write the base. 670 depth_maps = current_depth_map.copy() 671 if self.cfg.verbose: 672 print(f"Layer {i}, depth_maps.shape = {depth_maps.shape}") 673 self.max_layers = i + 1 674 675 if self.cfg.verbose: 676 print("\n ... finished creating horizon layers ...") 677 # Store maps in hdf file 678 self.depth_maps = self.cfg.hdf_init("depth_maps", shape=depth_maps.shape) 679 self.depth_maps[:] = depth_maps 680 681 self.cfg.write_to_logfile( 682 f"number_layers: {self.max_layers}", 683 mainkey="model_parameters", 684 subkey="number_layers", 685 val=self.max_layers, 686 )
Building layers in reverse order - starting at bottom and depositing new layers on top.
Each layer has random residual dip and pseudo-random residual thickness
689class Onlaps(Horizons): 690 def __init__(self, parameters, depth_maps, thicknesses, max_layers): 691 self.cfg = parameters 692 self.depth_maps = depth_maps 693 self.thicknesses = thicknesses 694 self.max_layers = max_layers 695 self.onlap_horizon_list = list() 696 self._generate_onlap_lookup_table() 697 698 def _generate_onlap_lookup_table(self): 699 # Onlaps 700 onlap_layer_list = np.sort( 701 np.random.uniform( 702 low=5, 703 high=self.max_layers, 704 size=int(np.random.triangular(1, 4, 7) + 0.5), 705 ).astype("int") 706 ) 707 if self.cfg.verbose: 708 print("self.cfg.num_lyr_lut = ", self.cfg.num_lyr_lut) 709 print("onlap_layer_list = ", onlap_layer_list) 710 onlap_array_dim = int(500 / 1250 * self.cfg.cube_shape[2]) 711 self.onlaps = np.zeros(onlap_array_dim, "int") 712 self.onlaps[onlap_layer_list] = 1 713 714 if self.cfg.verbose: 715 print( 716 f"Number of onlapping flags: {self.onlaps[self.onlaps == 1].shape[0]}" 717 ) 718 719 def insert_tilting_episodes(self): 720 ############################################################################ 721 # insert tilting (onlap) episodes 722 # - computations loop from deep horizons toward shallow (top of horizon cube) 723 ############################################################################ 724 if self.cfg.verbose: 725 print("\n ... simulating tilting (onlap) episodes ...") 726 727 azi_list = [] 728 dip_list = [] 729 730 count = 0 731 onlaps_horizon_list = [] 732 layer_has_onlaps = np.zeros((self.depth_maps.shape[2]), "bool") 733 for i in range(self.depth_maps.shape[2] - 2, 0, -1): 734 735 if self.onlaps[i] == 1: 736 # A tilting (onlap) episode occurs for this horizon. 737 # Add a random dipping layer to this and all shallower layers, and adjust depth for layer thickness 738 count += 1 739 if self.cfg.verbose: 740 print( 741 " ... simulate a tilting (onlap) episode ... at output horizon number {}".format( 742 i 743 ) 744 ) 745 onlaps_horizon_list.append(i) 746 747 if not self.cfg.regen: 748 azi = np.random.uniform(low=0.0, high=360.0) 749 azi_list.append(azi) 750 dip = np.random.uniform(low=5.0, high=20.0) 751 dip_list.append(dip) 752 753 dipping_plane2 = ( 754 self._fit_plane_strike_dip( 755 azi_list[count - 1], 756 dip_list[count - 1], 757 self.depth_maps.shape, 758 verbose=False, 759 ) 760 * self.cfg.infill_factor 761 ) 762 dipping_plane2_offset = ( 763 self.thicknesses[i] * self.cfg.infill_factor / 2.0 764 - dipping_plane2.max() 765 ) 766 767 # adjust all shallower layers 768 previous_depth_map = self.depth_maps[:, :, i + 1].copy() 769 for i_horizon in range(i, 0, -1): 770 current_depth_map = self.depth_maps[:, :, i_horizon].copy() 771 thickness_map = previous_depth_map - current_depth_map 772 prior_thickness_map = thickness_map.copy() 773 thickness_map += dipping_plane2 774 thickness_map += dipping_plane2_offset 775 thickness_map = np.clip(thickness_map, 0.0, thickness_map.max()) 776 current_depth_map = previous_depth_map - thickness_map 777 if np.mean(thickness_map - prior_thickness_map) > 0.5: 778 layer_has_onlaps[i_horizon] = 1 779 self.depth_maps[:, :, i_horizon] = current_depth_map.copy() 780 781 # print("\n\n ... depth_maps.shape = {}".format(depth_maps.shape)) 782 if self.cfg.verbose: 783 print( 784 f" ... Finished inserting tilting (onlap) episodes. {count} episodes were added..." 785 ) 786 self.cfg.write_to_logfile( 787 f"number_onlap_episodes: {count}\nonlaps_horizon_list: {str(onlaps_horizon_list)}" 788 ) 789 790 self.cfg.write_to_logfile( 791 msg=None, 792 mainkey="model_parameters", 793 subkey="number_onlap_episodes", 794 val=count, 795 ) 796 self.cfg.write_to_logfile( 797 msg=None, 798 mainkey="model_parameters", 799 subkey="onlaps_horizon_list", 800 val=str(onlaps_horizon_list), 801 ) 802 self.onlap_horizon_list = onlaps_horizon_list 803 return self.onlap_horizon_list
719 def insert_tilting_episodes(self): 720 ############################################################################ 721 # insert tilting (onlap) episodes 722 # - computations loop from deep horizons toward shallow (top of horizon cube) 723 ############################################################################ 724 if self.cfg.verbose: 725 print("\n ... simulating tilting (onlap) episodes ...") 726 727 azi_list = [] 728 dip_list = [] 729 730 count = 0 731 onlaps_horizon_list = [] 732 layer_has_onlaps = np.zeros((self.depth_maps.shape[2]), "bool") 733 for i in range(self.depth_maps.shape[2] - 2, 0, -1): 734 735 if self.onlaps[i] == 1: 736 # A tilting (onlap) episode occurs for this horizon. 737 # Add a random dipping layer to this and all shallower layers, and adjust depth for layer thickness 738 count += 1 739 if self.cfg.verbose: 740 print( 741 " ... simulate a tilting (onlap) episode ... at output horizon number {}".format( 742 i 743 ) 744 ) 745 onlaps_horizon_list.append(i) 746 747 if not self.cfg.regen: 748 azi = np.random.uniform(low=0.0, high=360.0) 749 azi_list.append(azi) 750 dip = np.random.uniform(low=5.0, high=20.0) 751 dip_list.append(dip) 752 753 dipping_plane2 = ( 754 self._fit_plane_strike_dip( 755 azi_list[count - 1], 756 dip_list[count - 1], 757 self.depth_maps.shape, 758 verbose=False, 759 ) 760 * self.cfg.infill_factor 761 ) 762 dipping_plane2_offset = ( 763 self.thicknesses[i] * self.cfg.infill_factor / 2.0 764 - dipping_plane2.max() 765 ) 766 767 # adjust all shallower layers 768 previous_depth_map = self.depth_maps[:, :, i + 1].copy() 769 for i_horizon in range(i, 0, -1): 770 current_depth_map = self.depth_maps[:, :, i_horizon].copy() 771 thickness_map = previous_depth_map - current_depth_map 772 prior_thickness_map = thickness_map.copy() 773 thickness_map += dipping_plane2 774 thickness_map += dipping_plane2_offset 775 thickness_map = np.clip(thickness_map, 0.0, thickness_map.max()) 776 current_depth_map = previous_depth_map - thickness_map 777 if np.mean(thickness_map - prior_thickness_map) > 0.5: 778 layer_has_onlaps[i_horizon] = 1 779 self.depth_maps[:, :, i_horizon] = current_depth_map.copy() 780 781 # print("\n\n ... depth_maps.shape = {}".format(depth_maps.shape)) 782 if self.cfg.verbose: 783 print( 784 f" ... Finished inserting tilting (onlap) episodes. {count} episodes were added..." 785 ) 786 self.cfg.write_to_logfile( 787 f"number_onlap_episodes: {count}\nonlaps_horizon_list: {str(onlaps_horizon_list)}" 788 ) 789 790 self.cfg.write_to_logfile( 791 msg=None, 792 mainkey="model_parameters", 793 subkey="number_onlap_episodes", 794 val=count, 795 ) 796 self.cfg.write_to_logfile( 797 msg=None, 798 mainkey="model_parameters", 799 subkey="onlaps_horizon_list", 800 val=str(onlaps_horizon_list), 801 ) 802 self.onlap_horizon_list = onlaps_horizon_list 803 return self.onlap_horizon_list
806class BasinFloorFans(Horizons): 807 def __init__(self, parameters, max_layers): 808 self.cfg = parameters 809 self.max_layers = max_layers 810 self.fan_layers = None 811 # self._generate_fan_lookup_table() 812 813 def _generate_fan_lookup_table(self): 814 # TODO test where & how often fans should be added. Also check if they overlap with onlaps and if so, what to do 815 # Onlaps 816 layers_with_fans = np.sort( 817 np.random.uniform( 818 low=5, high=self.max_layers - 1, size=int(np.random.choice([1, 2, 3])) 819 ).astype("int") 820 ) 821 self.fan_layers = layers_with_fans 822 self.fan_thicknesses = [] 823 self.fans = np.zeros(self.max_layers, "int") 824 self.fans[list(self.fan_layers)] = 1 825 826 def _generate_basin_floor_fan(self, layer_number): 827 # Select parameters for fan 828 _scale = np.random.uniform(50.0, 200.0) # length in pixels 829 _aspect = np.random.uniform(1.5, 4.0) # length width 830 _azimuth = np.random.uniform(0.0, 360.0) 831 _factor = np.random.uniform(1.5, 3.5) 832 _asymmetry_factor = np.random.uniform(-1.0, 1.0) 833 _smoothing_size = np.random.uniform(1.33, 3.0) 834 835 # Choose whether to create a pair of fans 836 pair_of_fans = np.random.choice([True, False]) 837 838 fan_parameters = ( 839 f"{_scale:4.2f}, {_aspect:4.2f}, {_azimuth:4.2f}, {_factor:4.2f}, {_asymmetry_factor:4.2f}," 840 f" {_smoothing_size:4.2f}" 841 ) 842 if self.cfg.verbose: 843 print(fan_parameters) 844 zi = self._generate_fan_thickness_map( 845 scale=_scale, 846 aspect=_aspect, 847 rotate_angle=_azimuth, 848 dip=3.5, 849 entry_point_factor=_factor, 850 asymmetry_factor=_asymmetry_factor, 851 smoothing_size=_smoothing_size, 852 ) 853 854 if pair_of_fans: 855 if self.cfg.verbose: 856 print("Creating a pair of fans\n") 857 _scale2 = _scale * np.random.uniform(0.667, 1.5) # length in pixels 858 _aspect2 = _aspect * np.random.uniform(0.75, 1.33) # length / width 859 _azimuth2 = _azimuth + np.random.uniform(-30.0, 30.0) 860 delta_dist = (_scale + _scale2) / ( 861 (_aspect + _aspect2) 862 * np.random.triangular(0.5, 0.85, 1.5) 863 * np.random.choice([-1.0, 1.0]) 864 ) # length in pixels 865 delta_x, delta_y = self.rotate_point(delta_dist, 0.0, 360.0 - _azimuth2) 866 _factor2 = _factor + np.random.uniform(0.8, 1.25) 867 _asymmetry_factor2 = _asymmetry_factor + np.random.uniform(-0.25, 0.25) 868 _smoothing_size2 = _smoothing_size + np.random.uniform(-0.25, 0.25) 869 870 fan_parameters_2 = ( 871 f"{_scale2:4.2f}, {_aspect2:4.2f}, {_azimuth2:4.2f}, {_factor2:4.2f}," 872 f"{_asymmetry_factor2:4.2f}, {_smoothing_size2:4.2f}" 873 ) 874 875 if self.cfg.verbose: 876 print( 877 f"Fan 1 parameters: {fan_parameters}\nFan 2 parameters: {fan_parameters_2}" 878 ) 879 fan_parameters += f"\n{fan_parameters_2}" 880 zi2 = self._generate_fan_thickness_map( 881 scale=_scale2, 882 aspect=_aspect2, 883 rotate_angle=_azimuth2, 884 dip=3.5, 885 entry_point_factor=_factor2, 886 asymmetry_factor=_asymmetry_factor2, 887 smoothing_size=_smoothing_size2, 888 ) 889 890 zi2_padded = np.zeros((zi.shape[0] * 2, zi.shape[1] * 2), "float") 891 zi2_padded[ 892 zi.shape[0] // 2 : zi.shape[0] + zi.shape[0] // 2, 893 zi.shape[1] // 2 : zi.shape[1] + zi.shape[1] // 2, 894 ] += zi2 895 zi2_padded = np.roll(zi2_padded, int(delta_x + 0.5), axis=0) 896 zi2_padded = np.roll(zi2_padded, int(delta_y + 0.5), axis=1) 897 zi_delta = ( 898 zi2_padded[ 899 zi.shape[0] // 2 : zi.shape[0] + zi.shape[0] // 2, 900 zi.shape[1] // 2 : zi.shape[1] + zi.shape[1] // 2, 901 ] 902 - zi 903 ) 904 zi_delta[zi_delta < 0.0] = 0.0 905 zi3 = zi + zi_delta 906 zi = zi3 907 908 # Plot the fan 909 if self.cfg.qc_plots: 910 from datagenerator.util import import_matplotlib 911 912 plt = import_matplotlib() 913 plt.figure(1) 914 plt.clf() 915 plt.title(fan_parameters) 916 plt.imshow(zi.T) 917 plt.colorbar() 918 plt.savefig( 919 os.path.join( 920 self.cfg.work_subfolder, f"random_fan_layer{layer_number}.png" 921 ) 922 ) 923 924 self.fan_thicknesses.append(zi) 925 return zi 926 927 def _generate_fan_thickness_map( 928 self, 929 scale=2.5, 930 aspect=1.5, 931 rotate_angle=0.0, 932 zero_at_corners=True, 933 dip=3.5, 934 entry_point_factor=1.0, 935 asymmetry_factor=0.5, 936 smoothing_size=2.5, 937 ): 938 ############################################################################ 939 # generate a 2D array representing a depth (or TWT) structure map. 940 # - grid_size controls output size in (x,y), units are samples 941 # - dip_range controls slope of dipping plane upon which random residual points are placed 942 # - num_points controls number of randomly positioned points are used for residual grid 943 # - asymetry_factor: range [-1, 1.]. 0. does nothing, sign determines left/right prominence 944 ############################################################################ 945 946 grid_size = self.cfg.cube_shape[:2] 947 # define output grid (padded by 15% in each direction) 948 xi = np.linspace(-0.15, 1.15, int(float(grid_size[0]) * 1.3)) 949 yi = np.linspace(-0.15, 1.15, int(float(grid_size[1]) * 1.3)) 950 951 # start with a gently dipping plane similar to that found on passive shelf margins (< 1 degree dip) 952 dipping_plane = self._fit_plane_strike_dip( 953 360.0 - rotate_angle, dip, grid_size, verbose=False 954 ).T 955 956 x = np.array( 957 [ 958 0.5 * (1.0 - np.cos(theta)) * np.sin(theta) * np.cos(phi) 959 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 960 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 961 ] 962 ) 963 y = np.array( 964 [ 965 np.cos(theta) 966 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 967 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 968 ] 969 ) 970 z = np.array( 971 [ 972 0.5 * (1.0 - np.cos(theta)) * np.sin(theta) * np.sin(phi) 973 for phi in np.arange(0.0, np.pi, np.pi / 180.0) 974 for theta in np.arange(0.0, np.pi, np.pi / 180.0) 975 ] 976 ) 977 x -= x.min() 978 y -= y.min() 979 if self.cfg.verbose: 980 print("number of xyz points = ", x.size) 981 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 982 x *= (scale / aspect) / (x.max() - x.min()) 983 y *= scale / (y.max() - y.min()) 984 985 # scale back to range of [0,1] 986 x /= grid_size[0] 987 y /= grid_size[1] 988 989 if self.cfg.verbose: 990 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 991 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 992 993 # add asymmetry 994 # - small rotation, stretch, undo rotation 995 if asymmetry_factor != 0.0: 996 x, y = self.rotate_point(x, y, asymmetry_factor * 45.0) 997 x *= 2.0 + np.abs(asymmetry_factor) 998 y *= 2.0 - np.abs(asymmetry_factor) 999 x, y = self.rotate_point(x, y, -asymmetry_factor * 45.0) 1000 x /= 2.0 1001 y /= 2.0 1002 if self.cfg.verbose: 1003 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 1004 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 1005 1006 # rotate 1007 x, y = self.rotate_point(x, y, 360.0 - rotate_angle) 1008 1009 # center in the grid 1010 x += 0.5 - x.mean() 1011 y += 0.5 - y.mean() 1012 1013 if self.cfg.verbose: 1014 print("x min/mean/max = ", x.min(), x.mean(), x.max(), x.max() - x.min()) 1015 print("y min/mean/max = ", y.min(), y.mean(), y.max(), y.max() - y.min()) 1016 print("z min/mean/max = ", z.min(), z.mean(), z.max(), z.max() - z.min()) 1017 1018 # decimate the grid randomly 1019 decimation_factor = 250 1020 1021 fan_seed_x = np.random.randint(1, high=2 ** 32 - 1) 1022 fan_seed_y = np.random.randint(1, high=2 ** 32 - 1) 1023 fan_seed_z = np.random.randint(1, high=2 ** 32 - 1) 1024 point_indices = np.arange(x.size).astype("int") 1025 point_indices = point_indices[: x.size // decimation_factor] 1026 np.random.shuffle(point_indices) 1027 np.random.seed(fan_seed_x) 1028 _x = x + np.random.uniform(low=-0.03, high=0.03, size=x.size) 1029 np.random.seed(fan_seed_y) 1030 _y = y + np.random.uniform(low=-0.03, high=0.03, size=x.size) 1031 np.random.seed(fan_seed_z) 1032 _z = z + np.random.uniform(low=-0.1, high=0.1, size=x.size) 1033 1034 _x = _x[point_indices] 1035 _y = _y[point_indices] 1036 _z = _z[point_indices] 1037 1038 if self.cfg.qc_plots: 1039 from datagenerator.util import import_matplotlib 1040 1041 plt = import_matplotlib() 1042 plt.clf() 1043 plt.grid() 1044 plt.scatter(x * 300, y * 300, c=z) 1045 plt.xlim([0, 300]) 1046 plt.ylim([0, 300]) 1047 plt.colorbar() 1048 1049 if zero_at_corners: 1050 x = np.hstack((x, [-0.15, -0.15, 1.15, 1.15])) 1051 y = np.hstack((y, [-0.15, 1.15, -0.15, 1.15])) 1052 z = np.hstack((z, [-0.15, -0.15, -0.15, -0.15])) 1053 _x = np.hstack((_x, [-0.15, -0.15, 1.15, 1.15])) 1054 _y = np.hstack((_y, [-0.15, 1.15, -0.15, 1.15])) 1055 _z = np.hstack((_z, [-0.15, -0.15, -0.15, -0.15])) 1056 1057 # grid the data. 1058 zi = griddata( 1059 (x, y), 1060 z, 1061 (xi.reshape(xi.shape[0], 1), yi.reshape(1, yi.shape[0])), 1062 method="linear", 1063 ) 1064 _zi = griddata( 1065 (_x, _y), 1066 _z, 1067 (xi.reshape(xi.shape[0], 1), yi.reshape(1, yi.shape[0])), 1068 method="cubic", 1069 ) 1070 1071 zi[np.isnan(zi)] = 0.0 1072 zi[zi <= 0.0] = 0.0 1073 zi_mask = zi.copy() 1074 zi_mask[zi_mask <= 0.015] = 0.0 1075 if self.cfg.verbose: 1076 print("zi_mask[zi_mask >0.].min() = ", zi_mask[zi_mask > 0.0].min()) 1077 print("zi_mask[zi_mask >0.].mean() = ", zi_mask[zi_mask > 0.0].mean()) 1078 print( 1079 "np.percentile(zi_mask[zi_mask >0.],.5) = ", 1080 np.percentile(zi_mask[zi_mask > 0.0], 0.5), 1081 ) 1082 1083 _zi[np.isnan(_zi)] = 0.0 1084 zi = _zi + 0.0 1085 zi[zi_mask <= 0.0] = 0.0 1086 zi[zi <= 0.0] = 0.0 1087 from scipy.ndimage import gaussian_filter, grey_closing 1088 1089 zi = gaussian_filter(zi, smoothing_size) 1090 # For cube_shape 300, 300, use closing size of (10, 10) 1091 closing_size = ( 1092 int(self.cfg.cube_shape[0] / 30), 1093 int(self.cfg.cube_shape[1] / 30), 1094 ) 1095 zi = grey_closing(zi, size=closing_size) 1096 1097 zi = zi.reshape(xi.shape[0], yi.shape[0]) 1098 xi_min_index = np.argmin((xi - 0.0) ** 2) 1099 xi_max_index = xi_min_index + grid_size[0] 1100 yi_min_index = np.argmin((yi - 0.0) ** 2) 1101 yi_max_index = yi_min_index + grid_size[1] 1102 zi = zi[xi_min_index:xi_max_index, yi_min_index:yi_max_index] 1103 # add to the gently sloping plane 1104 dipping_plane[zi == 0.0] = 0.0 1105 dipping_plane -= dipping_plane[dipping_plane != 0.0].min() 1106 dipping_plane[zi <= 0.05] = 0.0 1107 if dipping_plane.max() > 0: 1108 dipping_plane /= dipping_plane[dipping_plane != 0.0].max() 1109 dipping_plane *= zi.max() 1110 if self.cfg.verbose: 1111 print( 1112 "zi[zi > 0.] min/mean/max = ", 1113 zi[zi > 0.0].min(), 1114 zi[zi > 0.0].mean(), 1115 zi[zi > 0.0].max(), 1116 ) 1117 if dipping_plane.max() > 0: 1118 if self.cfg.verbose: 1119 print( 1120 "dipping_plane[dipping_plane > 0.] min/mean/max = ", 1121 dipping_plane[dipping_plane > 0.0].min(), 1122 dipping_plane[dipping_plane > 0.0].mean(), 1123 dipping_plane[dipping_plane > 0.0].max(), 1124 ) 1125 else: 1126 if self.cfg.verbose: 1127 print( 1128 "dipping_plane min/mean/max = ", 1129 dipping_plane.min(), 1130 dipping_plane.mean(), 1131 dipping_plane.max(), 1132 ) 1133 zi += entry_point_factor * dipping_plane 1134 1135 # todo QC size of this smoothing (to smooth edges of fan) 1136 zi = gaussian_filter(zi, smoothing_size * 2.0) 1137 1138 return zi 1139 1140 def insert_fans_into_horizons(self, depth_maps): 1141 new_depth_maps = depth_maps[:].copy() 1142 # Create lookup table for fans 1143 self._generate_fan_lookup_table() 1144 # generate fans using fan_layers 1145 for layer in self.fan_layers: 1146 thickness_map = ( 1147 self._generate_basin_floor_fan(layer) * self.cfg.infill_factor 1148 ) 1149 new_depth_maps = self.insert_feature_into_horizon_stack( 1150 thickness_map, layer, new_depth_maps 1151 ) 1152 # Write fan layers to logfile 1153 self.cfg.write_to_logfile( 1154 f"number_fan_episodes: {len(self.fan_layers)}\n" 1155 f"fans_horizon_list: {str(self.fan_layers)}" 1156 ) 1157 self.cfg.write_to_logfile( 1158 msg=None, 1159 mainkey="model_parameters", 1160 subkey="number_fan_episodes", 1161 val=len(self.fan_layers), 1162 ) 1163 self.cfg.write_to_logfile( 1164 msg=None, 1165 mainkey="model_parameters", 1166 subkey="fan_horizon_list", 1167 val=str(self.fan_layers), 1168 ) 1169 1170 return new_depth_maps 1171 1172 def fan_qc_plot(self, maps, lyrnum, thickness): 1173 """ 1174 Plot a cross-section of the inserted basin floor fan 1175 1176 Display an inline and crossline with the basin floor fan in red in cross-section 1177 Inlay a map of the fan in each subplot 1178 """ 1179 from datagenerator.util import import_matplotlib 1180 1181 plt = import_matplotlib() 1182 plt.close() 1183 fig, axs = plt.subplots(nrows=2, ncols=1, figsize=(20, 15), sharey=True) 1184 axs[0].set_title(f"Layer {lyrnum}") 1185 axs[1].set_title(f"Layer {lyrnum}") 1186 # 1187 max_layer = np.clip( 1188 lyrnum + 10, 0, maps.shape[2] 1189 ) # only plot layers below fan if they exist 1190 for j in range(lyrnum - 5, max_layer, 1): 1191 if j == lyrnum: # highlight fan layers in thicker red line 1192 axs[0].plot( 1193 range(self.cfg.cube_shape[0]), 1194 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1195 "r-", 1196 lw=0.5, 1197 ) 1198 axs[1].plot( 1199 range(self.cfg.cube_shape[0]), 1200 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1201 "r-", 1202 lw=0.5, 1203 ) 1204 else: 1205 axs[0].plot( 1206 range(self.cfg.cube_shape[0]), 1207 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1208 "k-", 1209 lw=0.2, 1210 ) 1211 axs[1].plot( 1212 range(self.cfg.cube_shape[0]), 1213 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1214 "k-", 1215 lw=0.2, 1216 ) 1217 for inl, ax in enumerate(axs): 1218 if inl == 0: 1219 plt.axes([0.8, 0.75, 0.11, 0.11]) 1220 plt.axvline( 1221 x=int(self.cfg.cube_shape[0] / 2), color="grey", linestyle="--" 1222 ) 1223 else: 1224 plt.axes([0.8, 0.33, 0.11, 0.11]) 1225 plt.axhline( 1226 y=int(self.cfg.cube_shape[1] / 2), color="grey", linestyle="--" 1227 ) 1228 plt.imshow(thickness.T) 1229 plt.xticks([]) 1230 plt.yticks([]) 1231 ax.set_ylim( 1232 top=np.min(maps[..., lyrnum - 5]), 1233 bottom=np.max(maps[..., max_layer - 1]), 1234 ) # Reverse Y axis 1235 fig.savefig( 1236 os.path.join(self.cfg.work_subfolder, f"BasinFloorFan_layer_{lyrnum}.png") 1237 ) 1238 plt.close()
1140 def insert_fans_into_horizons(self, depth_maps): 1141 new_depth_maps = depth_maps[:].copy() 1142 # Create lookup table for fans 1143 self._generate_fan_lookup_table() 1144 # generate fans using fan_layers 1145 for layer in self.fan_layers: 1146 thickness_map = ( 1147 self._generate_basin_floor_fan(layer) * self.cfg.infill_factor 1148 ) 1149 new_depth_maps = self.insert_feature_into_horizon_stack( 1150 thickness_map, layer, new_depth_maps 1151 ) 1152 # Write fan layers to logfile 1153 self.cfg.write_to_logfile( 1154 f"number_fan_episodes: {len(self.fan_layers)}\n" 1155 f"fans_horizon_list: {str(self.fan_layers)}" 1156 ) 1157 self.cfg.write_to_logfile( 1158 msg=None, 1159 mainkey="model_parameters", 1160 subkey="number_fan_episodes", 1161 val=len(self.fan_layers), 1162 ) 1163 self.cfg.write_to_logfile( 1164 msg=None, 1165 mainkey="model_parameters", 1166 subkey="fan_horizon_list", 1167 val=str(self.fan_layers), 1168 ) 1169 1170 return new_depth_maps
1172 def fan_qc_plot(self, maps, lyrnum, thickness): 1173 """ 1174 Plot a cross-section of the inserted basin floor fan 1175 1176 Display an inline and crossline with the basin floor fan in red in cross-section 1177 Inlay a map of the fan in each subplot 1178 """ 1179 from datagenerator.util import import_matplotlib 1180 1181 plt = import_matplotlib() 1182 plt.close() 1183 fig, axs = plt.subplots(nrows=2, ncols=1, figsize=(20, 15), sharey=True) 1184 axs[0].set_title(f"Layer {lyrnum}") 1185 axs[1].set_title(f"Layer {lyrnum}") 1186 # 1187 max_layer = np.clip( 1188 lyrnum + 10, 0, maps.shape[2] 1189 ) # only plot layers below fan if they exist 1190 for j in range(lyrnum - 5, max_layer, 1): 1191 if j == lyrnum: # highlight fan layers in thicker red line 1192 axs[0].plot( 1193 range(self.cfg.cube_shape[0]), 1194 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1195 "r-", 1196 lw=0.5, 1197 ) 1198 axs[1].plot( 1199 range(self.cfg.cube_shape[0]), 1200 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1201 "r-", 1202 lw=0.5, 1203 ) 1204 else: 1205 axs[0].plot( 1206 range(self.cfg.cube_shape[0]), 1207 maps[int(self.cfg.cube_shape[0] / 2), :, j], 1208 "k-", 1209 lw=0.2, 1210 ) 1211 axs[1].plot( 1212 range(self.cfg.cube_shape[0]), 1213 maps[:, int(self.cfg.cube_shape[1] / 2), j], 1214 "k-", 1215 lw=0.2, 1216 ) 1217 for inl, ax in enumerate(axs): 1218 if inl == 0: 1219 plt.axes([0.8, 0.75, 0.11, 0.11]) 1220 plt.axvline( 1221 x=int(self.cfg.cube_shape[0] / 2), color="grey", linestyle="--" 1222 ) 1223 else: 1224 plt.axes([0.8, 0.33, 0.11, 0.11]) 1225 plt.axhline( 1226 y=int(self.cfg.cube_shape[1] / 2), color="grey", linestyle="--" 1227 ) 1228 plt.imshow(thickness.T) 1229 plt.xticks([]) 1230 plt.yticks([]) 1231 ax.set_ylim( 1232 top=np.min(maps[..., lyrnum - 5]), 1233 bottom=np.max(maps[..., max_layer - 1]), 1234 ) # Reverse Y axis 1235 fig.savefig( 1236 os.path.join(self.cfg.work_subfolder, f"BasinFloorFan_layer_{lyrnum}.png") 1237 ) 1238 plt.close()
Plot a cross-section of the inserted basin floor fan
Display an inline and crossline with the basin floor fan in red in cross-section Inlay a map of the fan in each subplot
1241class Channel(Horizons): 1242 def __init__(self): 1243 self.W = 200.0 # Channel wodth 1244 self.D = 12.0 # Channel depth 1245 self.pad = 100 # padding (number of nodepoints along centerline) 1246 self.deltas = 50.0 # sampling distance along centerline 1247 self.nit = 2000 # number of iterations 1248 self.Cf = 0.022 # dimensionless Chezy friction factor 1249 self.crdist = 1.5 * self.W # threshold distance at which cutoffs occur 1250 self.kl = 60.0 / (365 * 24 * 60 * 60.0) # migration rate constant (m/s) 1251 self.kv = 1.0e-11 # vertical slope-dependent erosion rate constant (m/s) 1252 self.dt = 2 * 0.05 * 365 * 24 * 60 * 60.0 # time step (s) 1253 self.dens = 1000 # density of water (kg/m3) 1254 self.saved_ts = 20 # which time steps will be saved 1255 self.n_bends = 30 # approximate number of bends to model 1256 self.Sl = 0.0 # initial slope (matters more for submarine channels than rivers) 1257 self.t1 = 500 # time step when incision starts 1258 self.t2 = 700 # time step when lateral migration starts 1259 self.t3 = 1400 # time step when aggradation starts 1260 self.aggr_factor = ( 1261 4e-9 # aggradation factor (m/s, about 0.18 m/year, it kicks in after t3) 1262 ) 1263 1264 self.h_mud = 0.3 # thickness of overbank deposits for each time step 1265 self.dx = 20.0 # gridcell size in metres 1266 1267 # Channel objects 1268 self.ch = None 1269 self.chb = None 1270 self.chb_3d = None 1271 1272 def generate_channel_parameters(self): 1273 # select random parameters 1274 pass 1275 1276 def create_channel_3d(self): 1277 # Initialise channel 1278 self.ch = mp.generate_initial_channel( 1279 self.W, self.D, self.Sl, self.deltas, self.pad, self.n_bends 1280 ) 1281 # Create channel belt object 1282 self.chb = mp.ChannelBelt( 1283 channels=[self.ch], cutoffs=[], cl_times=[0.0], cutoff_times=[] 1284 ) 1285 # Migrate channel 1286 self.chb.migrate( 1287 self.nit, 1288 self.saved_ts, 1289 self.deltas, 1290 self.pad, 1291 self.crdist, 1292 self.Cf, 1293 self.kl, 1294 self.kv, 1295 self.dt, 1296 self.dens, 1297 self.t1, 1298 self.t2, 1299 self.t3, 1300 self.aggr_factor, 1301 ) 1302 end_time = self.chb.cl_times[-1] 1303 _xmin = 15000 1304 _xmax = 21000 1305 self.chb_3d, _, _, _, _ = self.chb.build_3d_model( 1306 "fluvial", 1307 h_mud=self.h_mud, 1308 levee_width=800.0, 1309 h=12.0, 1310 w=self.W, 1311 bth=0.0, 1312 dcr=10.0, 1313 dx=self.dx, 1314 delta_s=self.deltas, 1315 starttime=self.chb.cl_times[0], 1316 endtime=end_time, 1317 xmin=_xmin, 1318 xmax=_xmax, 1319 ymin=-3000, 1320 ymax=3000, 1321 ) 1322 1323 def insert_channel_into_horizons(self, layer, depth_maps): 1324 new_depth_maps = self.insert_feature_into_horizon_stack( 1325 self.chb.strat, layer, depth_maps 1326 ) 1327 return new_depth_maps 1328 1329 def insert_channel_facies(self): 1330 pass
1242 def __init__(self): 1243 self.W = 200.0 # Channel wodth 1244 self.D = 12.0 # Channel depth 1245 self.pad = 100 # padding (number of nodepoints along centerline) 1246 self.deltas = 50.0 # sampling distance along centerline 1247 self.nit = 2000 # number of iterations 1248 self.Cf = 0.022 # dimensionless Chezy friction factor 1249 self.crdist = 1.5 * self.W # threshold distance at which cutoffs occur 1250 self.kl = 60.0 / (365 * 24 * 60 * 60.0) # migration rate constant (m/s) 1251 self.kv = 1.0e-11 # vertical slope-dependent erosion rate constant (m/s) 1252 self.dt = 2 * 0.05 * 365 * 24 * 60 * 60.0 # time step (s) 1253 self.dens = 1000 # density of water (kg/m3) 1254 self.saved_ts = 20 # which time steps will be saved 1255 self.n_bends = 30 # approximate number of bends to model 1256 self.Sl = 0.0 # initial slope (matters more for submarine channels than rivers) 1257 self.t1 = 500 # time step when incision starts 1258 self.t2 = 700 # time step when lateral migration starts 1259 self.t3 = 1400 # time step when aggradation starts 1260 self.aggr_factor = ( 1261 4e-9 # aggradation factor (m/s, about 0.18 m/year, it kicks in after t3) 1262 ) 1263 1264 self.h_mud = 0.3 # thickness of overbank deposits for each time step 1265 self.dx = 20.0 # gridcell size in metres 1266 1267 # Channel objects 1268 self.ch = None 1269 self.chb = None 1270 self.chb_3d = None
1276 def create_channel_3d(self): 1277 # Initialise channel 1278 self.ch = mp.generate_initial_channel( 1279 self.W, self.D, self.Sl, self.deltas, self.pad, self.n_bends 1280 ) 1281 # Create channel belt object 1282 self.chb = mp.ChannelBelt( 1283 channels=[self.ch], cutoffs=[], cl_times=[0.0], cutoff_times=[] 1284 ) 1285 # Migrate channel 1286 self.chb.migrate( 1287 self.nit, 1288 self.saved_ts, 1289 self.deltas, 1290 self.pad, 1291 self.crdist, 1292 self.Cf, 1293 self.kl, 1294 self.kv, 1295 self.dt, 1296 self.dens, 1297 self.t1, 1298 self.t2, 1299 self.t3, 1300 self.aggr_factor, 1301 ) 1302 end_time = self.chb.cl_times[-1] 1303 _xmin = 15000 1304 _xmax = 21000 1305 self.chb_3d, _, _, _, _ = self.chb.build_3d_model( 1306 "fluvial", 1307 h_mud=self.h_mud, 1308 levee_width=800.0, 1309 h=12.0, 1310 w=self.W, 1311 bth=0.0, 1312 dcr=10.0, 1313 dx=self.dx, 1314 delta_s=self.deltas, 1315 starttime=self.chb.cl_times[0], 1316 endtime=end_time, 1317 xmin=_xmin, 1318 xmax=_xmax, 1319 ymin=-3000, 1320 ymax=3000, 1321 )
1333class Facies: 1334 def __init__(self, parameters, max_layers, onlap_horizon_list, fan_horizon_list): 1335 self.cfg = parameters 1336 self.max_layers = max_layers 1337 self.onlap_horizon_list = onlap_horizon_list 1338 self.fan_horizon_list = fan_horizon_list 1339 self.facies = None 1340 1341 def sand_shale_facies_binomial_dist(self): 1342 """Randomly select sand or shale facies using binomial distribution using the sand_layer_pct from config file""" 1343 sand_layer = np.random.binomial( 1344 1, self.cfg.sand_layer_pct, size=self.max_layers 1345 ) 1346 # Insert a water-layer code of -1 at the top 1347 self.facies = np.hstack((np.array((-1.0)), sand_layer)) 1348 1349 def sand_shale_facies_markov(self): 1350 """Generate a 1D array of facies usinga 2-state Markov process 1351 1352 Note: the Binomial distribution can be generated by setting the sand_layer_thickness to 1/sand_layer_pct 1353 """ 1354 mk = MarkovChainFacies( 1355 self.cfg.sand_layer_pct, self.cfg.sand_layer_thickness, (0, 1) 1356 ) 1357 # Randomly select initial state 1358 facies = mk.generate_states(np.random.choice(2, 1)[0], num=self.max_layers) 1359 self.facies = np.hstack((np.array((-1.0)), facies)) 1360 1361 def set_layers_below_onlaps_to_shale(self): 1362 """ 1363 Set layers immediately below tilting episode (onlap surface) to shale. 1364 1365 Set facies array to 0 (shale) above any layer which is marked as being an onlap surface 1366 """ 1367 onlap_horizon_list_plus_one = np.array(self.onlap_horizon_list) 1368 onlap_horizon_list_plus_one += 1 1369 self.facies[onlap_horizon_list_plus_one] = 0.0 1370 1371 def set_fan_facies(self, depth_maps): 1372 """Set fan facies as sand layers surrounded by shales 1373 1374 Set the fan layer to be a sand. 1375 Set the layers above and below the fan to be shale 1376 """ 1377 # Find those layers which are directly above (and touching) the fan layer 1378 layers_with_zero_thickness = set(np.where(np.diff(depth_maps) == 0)[2]) 1379 layers_above_fan = [] 1380 for f in self.fan_horizon_list: 1381 for i in range(f - 1, 1, -1): 1382 if i in layers_with_zero_thickness: 1383 layers_above_fan.append(i) 1384 else: 1385 break 1386 sand = list(self.fan_horizon_list) 1387 shale = layers_above_fan + list(self.fan_horizon_list + 1) 1388 self.facies[sand] = 1.0 1389 self.facies[shale] = 0.0
1341 def sand_shale_facies_binomial_dist(self): 1342 """Randomly select sand or shale facies using binomial distribution using the sand_layer_pct from config file""" 1343 sand_layer = np.random.binomial( 1344 1, self.cfg.sand_layer_pct, size=self.max_layers 1345 ) 1346 # Insert a water-layer code of -1 at the top 1347 self.facies = np.hstack((np.array((-1.0)), sand_layer))
Randomly select sand or shale facies using binomial distribution using the sand_layer_pct from config file
1349 def sand_shale_facies_markov(self): 1350 """Generate a 1D array of facies usinga 2-state Markov process 1351 1352 Note: the Binomial distribution can be generated by setting the sand_layer_thickness to 1/sand_layer_pct 1353 """ 1354 mk = MarkovChainFacies( 1355 self.cfg.sand_layer_pct, self.cfg.sand_layer_thickness, (0, 1) 1356 ) 1357 # Randomly select initial state 1358 facies = mk.generate_states(np.random.choice(2, 1)[0], num=self.max_layers) 1359 self.facies = np.hstack((np.array((-1.0)), facies))
Generate a 1D array of facies usinga 2-state Markov process
Note: the Binomial distribution can be generated by setting the sand_layer_thickness to 1/sand_layer_pct
1361 def set_layers_below_onlaps_to_shale(self): 1362 """ 1363 Set layers immediately below tilting episode (onlap surface) to shale. 1364 1365 Set facies array to 0 (shale) above any layer which is marked as being an onlap surface 1366 """ 1367 onlap_horizon_list_plus_one = np.array(self.onlap_horizon_list) 1368 onlap_horizon_list_plus_one += 1 1369 self.facies[onlap_horizon_list_plus_one] = 0.0
Set layers immediately below tilting episode (onlap surface) to shale.
Set facies array to 0 (shale) above any layer which is marked as being an onlap surface
1371 def set_fan_facies(self, depth_maps): 1372 """Set fan facies as sand layers surrounded by shales 1373 1374 Set the fan layer to be a sand. 1375 Set the layers above and below the fan to be shale 1376 """ 1377 # Find those layers which are directly above (and touching) the fan layer 1378 layers_with_zero_thickness = set(np.where(np.diff(depth_maps) == 0)[2]) 1379 layers_above_fan = [] 1380 for f in self.fan_horizon_list: 1381 for i in range(f - 1, 1, -1): 1382 if i in layers_with_zero_thickness: 1383 layers_above_fan.append(i) 1384 else: 1385 break 1386 sand = list(self.fan_horizon_list) 1387 shale = layers_above_fan + list(self.fan_horizon_list + 1) 1388 self.facies[sand] = 1.0 1389 self.facies[shale] = 0.0
Set fan facies as sand layers surrounded by shales
Set the fan layer to be a sand. Set the layers above and below the fan to be shale
1392class MarkovChainFacies: 1393 def __init__(self, sand_fraction, sand_thickness, states=(0, 1)): 1394 """Initialize the MarkovChain instance. 1395 1396 Parameters 1397 ---------- 1398 sand_fraction : float 1399 Fraction of sand layers in model 1400 sand_thickness : int 1401 Thickness of sand layers, given in units of layers 1402 states : iterable 1403 An iterable representing the states of the Markov Chain. 1404 """ 1405 self.sand_fraction = sand_fraction 1406 self.sand_thickness = sand_thickness 1407 self.states = states 1408 self.index_dict = { 1409 self.states[index]: index for index in range(len(self.states)) 1410 } 1411 self.state_dict = { 1412 index: self.states[index] for index in range(len(self.states)) 1413 } 1414 self._transition_matrix() 1415 1416 def _transition_matrix(self): 1417 """TODO Stephs notes go here""" 1418 beta = 1 / self.sand_thickness 1419 alpha = self.sand_fraction / (self.sand_thickness * (1 - self.sand_fraction)) 1420 self.transition = np.array([[1 - alpha, alpha], [beta, 1 - beta]]) 1421 1422 def next_state(self, current_state): 1423 """Returns the state of the random variable at the next instance. 1424 1425 Parameters 1426 ---------- 1427 current_state :str 1428 The current state of the system 1429 """ 1430 return np.random.choice( 1431 self.states, p=self.transition[self.index_dict[current_state], :] 1432 ) 1433 1434 def generate_states(self, current_state, num=100): 1435 """Generate states of the system with length num 1436 1437 Parameters 1438 ---------- 1439 current_state : str 1440 The state of the current random variable 1441 num : int, optional 1442 [description], by default 100 1443 """ 1444 future_states = [] 1445 for _ in range(num): 1446 next_state = self.next_state(current_state) 1447 future_states.append(next_state) 1448 current_state = next_state 1449 return np.array(future_states)
1393 def __init__(self, sand_fraction, sand_thickness, states=(0, 1)): 1394 """Initialize the MarkovChain instance. 1395 1396 Parameters 1397 ---------- 1398 sand_fraction : float 1399 Fraction of sand layers in model 1400 sand_thickness : int 1401 Thickness of sand layers, given in units of layers 1402 states : iterable 1403 An iterable representing the states of the Markov Chain. 1404 """ 1405 self.sand_fraction = sand_fraction 1406 self.sand_thickness = sand_thickness 1407 self.states = states 1408 self.index_dict = { 1409 self.states[index]: index for index in range(len(self.states)) 1410 } 1411 self.state_dict = { 1412 index: self.states[index] for index in range(len(self.states)) 1413 } 1414 self._transition_matrix()
Initialize the MarkovChain instance.
Parameters
- sand_fraction (float): Fraction of sand layers in model
- sand_thickness (int): Thickness of sand layers, given in units of layers
- states (iterable): An iterable representing the states of the Markov Chain.
1422 def next_state(self, current_state): 1423 """Returns the state of the random variable at the next instance. 1424 1425 Parameters 1426 ---------- 1427 current_state :str 1428 The current state of the system 1429 """ 1430 return np.random.choice( 1431 self.states, p=self.transition[self.index_dict[current_state], :] 1432 )
Returns the state of the random variable at the next instance.
Parameters
- current_state (str): The current state of the system
1434 def generate_states(self, current_state, num=100): 1435 """Generate states of the system with length num 1436 1437 Parameters 1438 ---------- 1439 current_state : str 1440 The state of the current random variable 1441 num : int, optional 1442 [description], by default 100 1443 """ 1444 future_states = [] 1445 for _ in range(num): 1446 next_state = self.next_state(current_state) 1447 future_states.append(next_state) 1448 current_state = next_state 1449 return np.array(future_states)
Generate states of the system with length num
Parameters
- current_state (str): The state of the current random variable
- num (int, optional): [description], by default 100
1452def build_unfaulted_depth_maps(parameters: Parameters): 1453 """ 1454 Build Unfaulted Depth Maps 1455 -------------------------- 1456 Generates unfaulted depth maps. 1457 1458 1. Build stack of horizons. 1459 2. Generate a random stack of horizons. 1460 3. Optionally insert basin floor fans 1461 4. Insert onlap episodes 1462 1463 Parameters 1464 ---------- 1465 parameters : str 1466 The key desired to be accessed 1467 1468 Returns 1469 ------- 1470 depth_maps : np.array 1471 The generated depth maps 1472 onlap_horizon_list : list 1473 Onlapping Horizon list 1474 fan_list : np.array | None 1475 List of generated fans 1476 fan_thicknesses : np.array | None 1477 Generated fan thicknesses 1478 """ 1479 horizons = RandomHorizonStack(parameters) 1480 horizons.create_depth_maps() 1481 # Insert onlap episodes 1482 onlaps = Onlaps( 1483 parameters, horizons.depth_maps, horizons.thicknesses, horizons.max_layers 1484 ) 1485 onlap_horizon_list = onlaps.insert_tilting_episodes() 1486 # Insert seafloor 1487 depth_maps = horizons.insert_seafloor(horizons.depth_maps) 1488 1489 fan_list = None 1490 fan_thicknesses = None 1491 if parameters.basin_floor_fans: 1492 bff = BasinFloorFans(parameters, horizons.max_layers) 1493 # Insert Fans 1494 depth_maps = bff.insert_fans_into_horizons(horizons.depth_maps) 1495 for layer, thickness in zip(bff.fan_layers, bff.fan_thicknesses): 1496 bff.fan_qc_plot(depth_maps, layer, thickness) 1497 fan_list = bff.fan_layers 1498 fan_thicknesses = bff.fan_thicknesses 1499 return depth_maps, onlap_horizon_list, fan_list, fan_thicknesses
Build Unfaulted Depth Maps
Generates unfaulted depth maps.
- Build stack of horizons.
- Generate a random stack of horizons.
- Optionally insert basin floor fans
- Insert onlap episodes
Parameters
- parameters (str): The key desired to be accessed
Returns
- depth_maps (np.array): The generated depth maps
- onlap_horizon_list (list): Onlapping Horizon list
- fan_list (np.array | None): List of generated fans
- fan_thicknesses (np.array | None): Generated fan thicknesses
1502def create_facies_array( 1503 parameters: Parameters, 1504 depth_maps: np.ndarray, 1505 onlap_horizons: list, 1506 fan_horizons: np.ndarray = None 1507) -> np.ndarray: 1508 """ 1509 Create Facies Array 1510 -------------------------- 1511 Generates facies for the model and return an array with the facies. 1512 1513 Parameters 1514 ---------- 1515 parameters : datagenerator.Parameters 1516 Parameter object storing all model parameters. 1517 depth_maps : np.ndarray 1518 A numpy array containing the depth maps. 1519 onlap_horizons : list 1520 A list of the onlap horizons. 1521 fan_horizons : np.ndarray 1522 The fan horizons. 1523 1524 Returns 1525 ------- 1526 facies : np.ndarray 1527 An array that contains the facies for the model. 1528 """ 1529 facies = Facies(parameters, depth_maps.shape[-1], onlap_horizons, fan_horizons) 1530 # facies.sand_shale_facies_binomial_dist() 1531 facies.sand_shale_facies_markov() 1532 if len(onlap_horizons) > 0: 1533 facies.set_layers_below_onlaps_to_shale() 1534 if np.any(fan_horizons): 1535 facies.set_fan_facies(depth_maps) 1536 # Write sand layer % to logfile 1537 sand_pct = facies.facies[facies.facies == 1].size / facies.facies.size 1538 parameters.write_to_logfile( 1539 f"Percent of layers containing sand a priori: {parameters.sand_layer_pct:0.2%}", 1540 mainkey="model_parameters", 1541 subkey="sand_layer_thickness_a_priori", 1542 val=parameters.sand_layer_pct, 1543 ) 1544 parameters.write_to_logfile( 1545 f"Sand unit thickness (in layers) a priori: {parameters.sand_layer_thickness}", 1546 mainkey="model_parameters", 1547 subkey="sand_unit_thickness_a_priori", 1548 val=parameters.sand_layer_thickness, 1549 ) 1550 parameters.write_to_logfile( 1551 f"Percent of layers containing sand a posteriori: {sand_pct:.2%}", 1552 mainkey="model_parameters", 1553 subkey="sand_layer_percent_a_posteriori", 1554 val=sand_pct, 1555 ) 1556 # Count consecutive occurrences (i.e. facies thickness in layers) 1557 facies_groups = [(k, sum(1 for i in g)) for k, g in groupby(facies.facies)] 1558 if parameters.verbose: 1559 print(f"Facies units: {facies_groups}") 1560 avg_sand_thickness = np.nan_to_num( 1561 np.mean([b for a, b in facies_groups if a == 1.0]) 1562 ) # convert nan to 0 1563 parameters.write_to_logfile( 1564 f"Average Sand unit thickness (in layers) a posteriori: {avg_sand_thickness:.1f}", 1565 mainkey="model_parameters", 1566 subkey="sand_unit_thickness_a_posteriori", 1567 val=avg_sand_thickness, 1568 ) 1569 1570 return facies.facies
Create Facies Array
Generates facies for the model and return an array with the facies.
Parameters
- parameters (datagenerator.Parameters): Parameter object storing all model parameters.
- depth_maps (np.ndarray): A numpy array containing the depth maps.
- onlap_horizons (list): A list of the onlap horizons.
- fan_horizons (np.ndarray): The fan horizons.
Returns
- facies (np.ndarray): An array that contains the facies for the model.