├── .idea ├── .gitignore ├── GeoModeling.iml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml └── modules.xml ├── README.md ├── channelSim.py ├── faultsSim.py ├── gmenv_linux.yml ├── gmenv_win10.yml └── modules.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/GeoModeling.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoModeling 2 | Create realistic geological models with folds, faults and meandering river sediments. 3 | -------------------------------------------------------------------------------- /channelSim.py: -------------------------------------------------------------------------------- 1 | from modules import * 2 | 3 | # Meander channel simulation. 4 | model = GeoModel(extent=[0, 1e4, 0, 1e4, 0, 1e3], resolution=[20.0, 20.0, 2.0]) 5 | model.add_rho_v(h_layer_range=[10, 20], vp_range=[3.5, 5.5], vs_range=[2.5, 3.1], rho_range=[2.2, 2.6]) 6 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[750, 750], 7 | W_range=[500, 500], D_range=[60, 60], n_bends_range=[20, 20], nit_range=[1500, 1500], 8 | kl_range=[50, 50], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 9 | Cf_range=[0.1, 0.1], strike_range=[0, 0], h_lag_range=[25, 25], mode='complex', 10 | save_it=20, h_levee_range=[0.85, 0.85], w_levee_range=[2000, 2000], show_migration=False, 11 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 12 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[650, 650], 13 | W_range=[300, 300], D_range=[50, 50], n_bends_range=[20, 20], nit_range=[1500, 1500], 14 | kl_range=[50, 50], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 15 | Cf_range=[0.1, 0.1], strike_range=[60, 60], h_lag_range=[20, 20], mode='complex', 16 | save_it=20, h_levee_range=[0.8, 0.8], w_levee_range=[1200, 1200], show_migration=False, 17 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 18 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[550, 550], 19 | W_range=[400, 400], D_range=[45, 45], n_bends_range=[20, 20], nit_range=[1500, 1500], 20 | kl_range=[50, 50], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 21 | Cf_range=[0.08, 0.08], strike_range=[120, 120], h_lag_range=[18, 18], mode='complex', 22 | save_it=20, h_levee_range=[0.8, 0.8], w_levee_range=[1600, 1600], show_migration=False, 23 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 24 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[450, 450], 25 | W_range=[280, 280], D_range=[50, 50], n_bends_range=[20, 20], nit_range=[1800, 1800], 26 | kl_range=[50, 50], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 27 | Cf_range=[0.06, 0.06], strike_range=[180, 180], h_lag_range=[20, 20], mode='complex', 28 | save_it=20, h_levee_range=[0.75, 0.75], w_levee_range=[1120, 1120], show_migration=False, 29 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 30 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[350, 350], 31 | W_range=[450, 450], D_range=[55, 55], n_bends_range=[20, 20], nit_range=[1600, 1600], 32 | kl_range=[45, 45], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 33 | Cf_range=[0.12, 0.12], strike_range=[240, 240], h_lag_range=[25, 25], mode='complex', 34 | save_it=20, h_levee_range=[0.85, 0.85], w_levee_range=[1800, 1800], show_migration=False, 35 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 36 | model.add_meander(N=1, X_pos_range=None, Y_pos_range=None, Z_pos_range=[250, 250], 37 | W_range=[350, 350], D_range=[60, 60], n_bends_range=[20, 20], nit_range=[2000, 2000], 38 | kl_range=[60, 60], pad_down=0, delta_s=None, dt=0.1, perturb_range=None, 39 | Cf_range=[0.1, 0.1], strike_range=None, h_lag_range=[25, 25], mode='complex', 40 | save_it=20, h_levee_range=[0.8, 0.8], w_levee_range=[1400, 1400], show_migration=False, 41 | figure_title='Meandering River Migration (execute cutoff)') # Add meandering channel. 42 | model.add_fold(N=15, sigma_range=[800, 1200], A_range=[40, 60], sync=True) # Add folds. 43 | model.add_fault(N=10, curved_fault_surface=False, reference_point_range=[500, 9500, 500, 9500, 300, 600], 44 | lx_range=[1.0, 1.0], ly_range=[0.06, 0.08], phi_range=None, theta_range=[50, 70], 45 | d_max_range=[0.03, 0.03], gamma_range=[0.04, 0.04], beta_range=[0.5, 0.5]) 46 | model.compute_impedance() 47 | model.compute_rc() 48 | model.rectangular_grid(resolution=[20, 20, 2], param=['lith_facies', 'vp', 'vs', 'density', 'rc']) 49 | model.crop(param=['lith_facies', 'vp', 'vs', 'density', 'rc']) 50 | model.make_synseis(plot_wavelet=False) 51 | model.add_noise(noise_type='uniform', ratio=0.3) 52 | np.save('lithology_model_real.npy', model.lith_facies) 53 | np.save('vp_real.npy', model.vp) 54 | np.save('vs_real.npy', model.vs) 55 | np.save('density_real.npy', model.density) 56 | np.save('rc_real.npy', model.rc) 57 | np.save('seismic_real.npy', model.seis) 58 | # Plot 3D model. 59 | lith_cmap = ['gray', 'red', 'yellow', 'saddlebrown', 'blue'] 60 | p0 = BackgroundPlotter(shape=(1, 2)) 61 | p0.subplot(0, 0) 62 | model.show(plotter=p0, param='lith_facies', zscale='auto', cmap=lith_cmap, slices=False) 63 | p0.subplot(0, 1) 64 | model.show(plotter=p0, param='seis', zscale='auto', cmap='seismic', slices=False) 65 | p0.link_views() 66 | p1 = BackgroundPlotter(shape=(1, 2)) 67 | p1.subplot(0, 0) 68 | model.show(plotter=p1, param='lith_facies', zscale='auto', cmap=lith_cmap, slices=True) 69 | p1.subplot(0, 1) 70 | model.show(plotter=p1, param='seis', zscale='auto', cmap='seismic', slices=True) 71 | p1.link_views() 72 | p2 = BackgroundPlotter() 73 | model.show(plotter=p2, param='lith_facies', zscale='auto', cmap=['red', 'yellow', 'saddlebrown', 'blue'], 74 | point_cloud=True, hide_value=[0]) 75 | p0.app.exec_() # Show all figures. 76 | p1.app.exec_() 77 | p2.app.exec_() 78 | -------------------------------------------------------------------------------- /faultsSim.py: -------------------------------------------------------------------------------- 1 | from modules import * 2 | 3 | random.seed(0) 4 | np.random.seed(0) 5 | 6 | test = 'test2' 7 | 8 | if test == 'test1': 9 | # Folds and faults simulation. 10 | model = GeoModel(extent=[0, 1e4, 0, 1e4, 0, 2e3], resolution=[100, 100, 20]) 11 | model.add_rho_v(h_layer_range=[20, 100], vp_range=[3.0, 5.5], rho_range=[2.3, 2.7]) 12 | model.compute_impedance() 13 | model.compute_rc() 14 | model.add_fold() 15 | model.add_fault(N=3, mark_faults=True, curved_fault_surface=False, computation_mode='parallel', 16 | reference_point_range=[0, 1e4, 0, 1e4, 500, 1500], 17 | phi_range=[90, 90], theta_range=[70, 80], lx_range=[1.0, 1.0], ly_range=[0.2, 0.3], 18 | gamma_range=[0.1, 0.1], beta_range=[0.5, 0.5], d_max_range=[0.1, 0.1]) 19 | model.make_synseis(plot_wavelet=False) 20 | model.add_noise(noise_type='uniform', ratio=0.05) 21 | print(np.amin(model.seis), np.amax(model.seis)) 22 | # Plot 3D models. 23 | pv.set_plot_theme('ParaView') # Set plot theme as ParaView. 24 | p0 = BackgroundPlotter(shape=(2, 2)) 25 | p0.subplot(0, 0) # P-wave velocity model. 26 | model.show(plotter=p0, param='vp', cmap='rainbow', zscale='auto') 27 | p0.subplot(0, 1) # Density model. 28 | model.show(plotter=p0, param='density', cmap='rainbow', zscale='auto') 29 | p0.subplot(1, 0) # Reflection coefficient model. 30 | model.show(plotter=p0, param='rc', cmap='gray_r', zscale='auto') 31 | p0.subplot(1, 1) # Fault probability model. 32 | model.show(plotter=p0, param='seis', cmap='seismic', zscale='auto') 33 | p0.link_views() 34 | # Resample on grid. 35 | model.rectangular_grid(param=['rc', 'vp', 'density'], resolution=[20, 20, 4]) 36 | model.crop(param=['rc', 'vp', 'density']) 37 | model.make_synseis(plot_wavelet=False) 38 | np.save('rc.npy', model.rc) 39 | np.save('vp.npy', model.vp) 40 | np.save('density.npy', model.density) 41 | np.save('seis.npy', model.seis) 42 | # Plot 3D models. 43 | p1 = BackgroundPlotter(shape=(2, 2)) 44 | p1.subplot(0, 0) 45 | model.show(plotter=p1, param='vp', cmap='rainbow', zscale='auto') 46 | p1.subplot(0, 1) 47 | model.show(plotter=p1, param='density', cmap='rainbow', zscale='auto') 48 | p1.subplot(1, 0) 49 | model.show(plotter=p1, param='rc', cmap='gray_r', zscale='auto') 50 | p1.subplot(1, 1) 51 | model.show(plotter=p1, param='seis', cmap='seismic', zscale='auto') 52 | p1.link_views() 53 | p0.app.exec_() 54 | p1.app.exec_() # Show all figures. 55 | -------------------------------------------------------------------------------- /gmenv_linux.yml: -------------------------------------------------------------------------------- 1 | name: gm 2 | channels: 3 | - https://mirrors.ustc.edu.cn/anaconda/pkgs/main 4 | - https://mirrors.ustc.edu.cn/anaconda/cloud/conda-forge 5 | - defaults 6 | dependencies: 7 | - _libgcc_mutex=0.1=conda_forge 8 | - _openmp_mutex=4.5=1_gnu 9 | - ca-certificates=2021.10.26=h06a4308_2 10 | - certifi=2021.10.8=py38h06a4308_0 11 | - ld_impl_linux-64=2.36.1=hea4e1c9_1 12 | - libffi=3.3=h58526e2_2 13 | - libgcc-ng=9.3.0=h2828fa1_19 14 | - libgomp=9.3.0=h2828fa1_19 15 | - libstdcxx-ng=9.3.0=h6de172a_19 16 | - ncurses=6.2=h58526e2_4 17 | - openssl=1.1.1l=h7f8727e_0 18 | - pip=21.1.3=pyhd8ed1ab_0 19 | - pysocks=1.7.1=py38h06a4308_0 20 | - python=3.8.8=hdb3f193_5 21 | - python_abi=3.8=2_cp38 22 | - readline=8.1=h46c0cb4_0 23 | - setuptools=49.6.0=py38h578d9bd_3 24 | - sqlite=3.36.0=h9cd32fc_0 25 | - tk=8.6.10=h21135ba_1 26 | - tqdm=4.62.3=pyhd3eb1b0_1 27 | - wheel=0.36.2=pyhd3deb0d_0 28 | - xz=5.2.5=h516909a_1 29 | - zlib=1.2.11=h516909a_1010 30 | - pip: 31 | - appdirs==1.4.4 32 | - attrs==21.2.0 33 | - autobahn==21.3.1 34 | - automat==20.2.0 35 | - cffi==1.14.6 36 | - constantly==15.1.0 37 | - cryptography==3.4.7 38 | - cycler==0.10.0 39 | - freetype-py==2.2.0 40 | - hsluv==5.0.2 41 | - hyperlink==21.0.0 42 | - idna==3.2 43 | - imageio==2.9.0 44 | - incremental==21.3.0 45 | - joblib==1.1.0 46 | - kiwisolver==1.3.1 47 | - llvmlite==0.36.0 48 | - matplotlib==3.3.4 49 | - meshio==4.4.6 50 | - numba==0.53.1 51 | - numpy==1.19.2 52 | - pandas==1.3.0 53 | - pillow==8.3.1 54 | - prettytable==2.1.0 55 | - pycparser==2.20 56 | - pyopengl==3.1.5 57 | - pyparsing==2.4.7 58 | - pyqt5==5.15.4 59 | - pyqt5-qt5==5.15.2 60 | - pyqt5-sip==12.9.0 61 | - python-dateutil==2.8.1 62 | - pytz==2021.1 63 | - pyvista==0.31.3 64 | - pyvistaqt==0.5.0 65 | - qtpy==1.9.0 66 | - scipy==1.6.2 67 | - scooby==0.5.7 68 | - six==1.16.0 69 | - transforms3d==0.3.1 70 | - twisted==21.2.0 71 | - txaio==21.2.1 72 | - vispy==0.9.0 73 | - vtk==9.0.3 74 | - wcwidth==0.2.5 75 | - wslink==0.2.0 76 | - zope-interface==5.4.0 77 | prefix: /data/2019/wgy/anaconda3/envs/gm 78 | -------------------------------------------------------------------------------- /gmenv_win10.yml: -------------------------------------------------------------------------------- 1 | name: gm 2 | channels: 3 | - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main 4 | - defaults 5 | dependencies: 6 | - ca-certificates=2021.7.5=haa95532_1 7 | - certifi=2021.5.30=py38haa95532_0 8 | - openssl=1.1.1k=h2bbff1b_0 9 | - pip=21.1.3=py38haa95532_0 10 | - python=3.8.8=hdbf39b2_5 11 | - setuptools=52.0.0=py38haa95532_0 12 | - sqlite=3.36.0=h2bbff1b_0 13 | - vc=14.2=h21ff451_1 14 | - vs2015_runtime=14.27.29016=h5e58377_2 15 | - wheel=0.36.2=pyhd3eb1b0_0 16 | - wincertstore=0.2=py38_0 17 | - pip: 18 | - appdirs==1.4.4 19 | - attrs==21.2.0 20 | - autobahn==21.3.1 21 | - automat==20.2.0 22 | - cffi==1.14.6 23 | - constantly==15.1.0 24 | - cryptography==3.4.7 25 | - cycler==0.10.0 26 | - hyperlink==21.0.0 27 | - idna==3.2 28 | - imageio==2.9.0 29 | - incremental==21.3.0 30 | - joblib==1.1.0 31 | - kiwisolver==1.3.1 32 | - llvmlite==0.36.0 33 | - matplotlib==3.3.4 34 | - meshio==4.4.6 35 | - numba==0.53.1 36 | - numpy==1.19.2 37 | - pandas==1.3.1 38 | - pillow==8.3.1 39 | - prettytable==2.1.0 40 | - pycparser==2.20 41 | - pyparsing==2.4.7 42 | - pyqt5==5.15.4 43 | - pyqt5-qt5==5.15.2 44 | - pyqt5-sip==12.9.0 45 | - python-dateutil==2.8.1 46 | - pytz==2021.1 47 | - pyvista==0.31.3 48 | - pyvistaqt==0.5.0 49 | - qtpy==1.9.0 50 | - scipy==1.6.2 51 | - scooby==0.5.7 52 | - six==1.16.0 53 | - transforms3d==0.3.1 54 | - twisted==21.2.0 55 | - twisted-iocpsupport==1.0.1 56 | - txaio==21.2.1 57 | - vtk==9.0.3 58 | - wcwidth==0.2.5 59 | - wslink==0.2.0 60 | - zope-interface==5.4.0 61 | prefix: C:\Users\WGY\anaconda3\envs\gm 62 | -------------------------------------------------------------------------------- /modules.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import random 4 | import math 5 | import matplotlib.pyplot as plt 6 | import scipy.interpolate 7 | from scipy import ndimage 8 | from prettytable import PrettyTable 9 | from PIL import Image, ImageDraw 10 | from pyvistaqt import BackgroundPlotter 11 | from scipy.spatial import distance 12 | import numba 13 | import sys 14 | import pyvista as pv 15 | import time 16 | import multiprocessing 17 | from joblib import Parallel, delayed 18 | 19 | 20 | class BiharmonicSpline2D: 21 | """ 22 | 2D Bi-harmonic Spline Interpolation. 23 | """ 24 | 25 | def __init__(self, x, y): 26 | """ 27 | Use coordinates of the known points to calculate the weight "w" in the interpolation function. 28 | :param x: (numpy.ndarray) - x-coordinates of the known points. 29 | :param y: (numpy.ndarray) - y-coordinates of the known points. 30 | """ 31 | self.x = x 32 | self.y = y 33 | # Check if the coordinates' shapes are identical. 34 | if not self.x.shape == self.y.shape: 35 | raise ValueError("The coordinates' shapes of known points are not identical.") 36 | # Flatten the coordinates if they are not. 37 | if self.x.ndim != 1 and self.y.ndim != 1: 38 | self.x = self.x.ravel(order='C') 39 | self.y = self.y.ravel(order='C') 40 | # Calculate the 1D Green function matrix. 41 | green = np.zeros(shape=[len(self.x), len(self.x)], dtype='float32') 42 | for i in range(len(x)): 43 | green[i, :] = np.abs(self.x[i] - self.x) ** 3 44 | # Calculate weights. 45 | if np.linalg.matrix_rank(green) == green.shape[0]: # See if the Green matrix is reversible. 46 | self.w = np.linalg.inv(green) @ self.y 47 | else: 48 | self.w = np.linalg.pinv(green) @ self.y # Pseudo-inverse. 49 | 50 | def __call__(self, x_new): 51 | """ 52 | Interpolate new points. 53 | :param x_new: (numpy.ndarray) - x-coordinates of the new points. 54 | :return: (numpy.ndarray) - y-coordinates of the new points. 55 | """ 56 | original_shape = x_new.shape 57 | # Flatten the coordinates if they are not. 58 | if x_new.ndim != 1: 59 | x_new = x_new.ravel(order='C') 60 | # Calculate the 1D Green function matrix. 61 | green = np.zeros(shape=[len(x_new), len(self.x)], dtype='float32') 62 | for i in range(len(x_new)): 63 | green[i, :] = np.abs(x_new[i] - self.x) ** 3 64 | # Calculate y-coordinates of new points. 65 | y_new = green @ self.w 66 | y_new = y_new.reshape(original_shape, order='C') 67 | return y_new 68 | 69 | 70 | class BiharmonicSpline3D: 71 | """ 72 | 3D Bi-harmonic Spline Interpolation. 73 | """ 74 | 75 | def __init__(self, x, y, z): 76 | """ 77 | Use coordinates of the known points to calculate the weight "w" in the interpolation function. 78 | :param x: (numpy.ndarray) - x-coordinates of the known points. 79 | :param y: (numpy.ndarray) - y-coordinates of the known points. 80 | :param z: (numpy.ndarray) - z-coordinates of the known points. 81 | """ 82 | self.x = x 83 | self.y = y 84 | self.z = z 85 | # Check if the coordinates' shapes are identical. 86 | if not self.x.shape == self.y.shape == self.z.shape: 87 | raise ValueError("The coordinates' shapes of known points are not identical.") 88 | # Flatten the coordinates if they are not. 89 | if self.x.ndim != 1 and self.y.ndim != 1 and self.z.ndim != 1: 90 | self.x = self.x.ravel(order='C') 91 | self.y = self.y.ravel(order='C') 92 | self.z = self.z.ravel(order='C') 93 | # Calculate the 2D Green function matrix. 94 | delta_x = np.zeros(shape=[len(self.x), len(self.x)], dtype='float32') 95 | delta_y = np.zeros(shape=[len(self.y), len(self.y)], dtype='float32') 96 | for i in range(len(x)): 97 | delta_x[i, :] = self.x[i] - self.x # Calculate the x-coordinate difference between two points. 98 | delta_y[i, :] = self.y[i] - self.y # Calculate the y-coordinate difference between two points. 99 | mod = np.sqrt(delta_x ** 2 + delta_y ** 2) # The vector's modulus between two points. 100 | mod = mod.ravel(order='C') # Flatten the 2D mod array to 1D. 101 | green = np.zeros(shape=mod.shape, dtype='float32') # Initialize the Green function matrix. 102 | # Calculate the Green function matrix at non-zero points. 103 | green[mod != 0] = mod[mod != 0] ** 2 * (np.log(mod[mod != 0]) - 1) 104 | green = green.reshape(delta_x.shape) # Reshape the matrix to 2-D array shape. 105 | # Calculate weights. 106 | if np.linalg.matrix_rank(green) == green.shape[0]: # See if the Green matrix is reversible. 107 | self.w = np.linalg.inv(green) @ self.z 108 | else: 109 | self.w = np.linalg.pinv(green) @ self.z # Pseudo-inverse. 110 | 111 | def __call__(self, x_new, y_new): 112 | """ 113 | Interpolate new points. 114 | :param x_new: (numpy.ndarray) - x-coordinates of the new points. 115 | :param y_new: (numpy.ndarray) - y-coordinates of the new points. 116 | :return: (numpy.ndarray) - z-coordinates of the new points. 117 | """ 118 | original_shape = x_new.shape 119 | # Check if the coordinates' shapes are identical. 120 | if not x_new.shape == y_new.shape: 121 | raise ValueError("The coordinates' shapes of known points are not identical.") 122 | # Flatten the coordinates if they are not. 123 | if x_new.ndim != 1 and y_new.ndim != 1: 124 | x_new = x_new.ravel(order='C') 125 | y_new = y_new.ravel(order='C') 126 | delta_x = np.zeros(shape=[len(x_new), len(self.x)], dtype='float32') 127 | delta_y = np.zeros(shape=[len(y_new), len(self.y)], dtype='float32') 128 | for i in range(len(x_new)): 129 | delta_x[i, :] = x_new[i] - self.x 130 | delta_y[i, :] = y_new[i] - self.y 131 | mod = np.sqrt(delta_x ** 2 + delta_y ** 2) # The vector's modulus between two points. 132 | mod = mod.ravel(order='C') # Flatten the 2D mod array to 1D. 133 | green = np.zeros(shape=mod.shape, dtype='float32') 134 | green[mod != 0] = mod[mod != 0] ** 2 * (np.log(mod[mod != 0]) - 1) 135 | green = green.reshape(delta_x.shape) 136 | # Calculate z-coordinates of new points. 137 | z_new = green @ self.w 138 | z_new = z_new.reshape(original_shape, order='C') 139 | return z_new 140 | 141 | 142 | class GeoModel: 143 | def __init__(self, extent, resolution): 144 | """ 145 | Initialize the 3-D geological model. The current function can only create Regular Grid. 146 | :param extent: (List of floats) - [X_min, X_max, Y_min, Y_max, Z_min, Z_max]. Extent of the model. 147 | Unit: meter. 148 | :param resolution: (List of floats) - [X_res, Y_res, Z_res]. Model's resolution in each dimension. 149 | Unit: meter. 150 | """ 151 | self.X_min, self.X_max = extent[0], extent[1] 152 | self.Y_min, self.Y_max = extent[2], extent[3] 153 | self.Z_min, self.Z_max = extent[4], extent[5] 154 | self.X_resolution, self.Y_resolution, self.Z_resolution = resolution[0], resolution[1], resolution[2] 155 | X = np.arange(start=self.X_min, stop=self.X_max + self.X_resolution, step=self.X_resolution, dtype='float32') 156 | Y = np.arange(start=self.Y_min, stop=self.Y_max + self.Y_resolution, step=self.Y_resolution, dtype='float32') 157 | Z = np.arange(start=self.Z_min, stop=self.Z_max + self.Z_resolution, step=self.Z_resolution, dtype='float32') 158 | self.X_points, self.Y_points, self.Z_points = len(X), len(Y), len(Z) 159 | self.X, self.Y, self.Z = np.meshgrid(X, Y, Z) 160 | self.rc = np.zeros(shape=self.Z.shape, dtype='float32') # Reflection coefficient. 161 | self.vp = np.ones(shape=self.Z.shape, dtype='float32') # P-wave velocity. 162 | self.vs = np.ones(shape=self.Z.shape, dtype='float32') # S-wave velocity. 163 | self.density = np.ones(shape=self.Z.shape, dtype='float32') # Density. 164 | self.Ip = np.zeros(shape=self.Z.shape, dtype='float32') # P-wave impedance. 165 | self.Is = np.zeros(shape=self.Z.shape, dtype='float32') # S-wave impedance. 166 | self.seis = np.array([]) # Synthetic seismic data. 167 | self.fm = np.zeros(shape=self.Z.shape, dtype='float32') # The fault marker. 168 | self.channel_facies = np.zeros(shape=self.Z.shape, dtype='float32') # The channel sub-facies marker. 169 | self.lith_facies = np.zeros(shape=self.Z.shape, dtype='float32') # The lithology facies marker. 170 | self.channel = [] # List of channels. 171 | self.oxbow = [] # List of oxbows. 172 | self.autocrop = {} # Dictionary of z coordinates used for auto-cropping the model. 173 | # Create a table of lithology parameters. 174 | index = ['basalt', 'diabase', 'fine silty mudstone', 'oil shale', 'mudstone', 'silty mudstone', 'lime dolomite', 175 | 'pelitic dolomite', 'siltstone', 'pebbled sandstone', 'conglomerate', 'fine sandstone', 176 | 'pelitic siltstone'] # Lithology names. 177 | column = ['vp_miu', 'vp_sigma', 'vs_miu', 'vs_sigma', 'rho_miu', 'rho_sigma'] # Parameter names. 178 | data = np.array([[3.38, 0.20, 1.96, 0.15, 2.15, 0.18], 179 | [5.14, 0.21, 3.01, 0.13, 2.67, 0.20], 180 | [3.72, 0.19, 2.33, 0.18, 2.47, 0.21], 181 | [3.85, 0.18, 2.47, 0.16, 2.42, 0.19], 182 | [4.78, 0.23, 3.01, 0.20, 2.70, 0.17], 183 | [5.43, 0.15, 3.47, 0.18, 2.60, 0.20], 184 | [5.70, 0.18, 3.09, 0.16, 2.65, 0.18], 185 | [5.61, 0.16, 3.13, 0.15, 2.57, 0.22], 186 | [4.24, 0.20, 2.69, 0.18, 2.28, 0.17], 187 | [4.35, 0.17, 2.73, 0.20, 2.22, 0.18], 188 | [5.54, 0.18, 3.01, 0.21, 2.56, 0.21], 189 | [5.26, 0.20, 3.26, 0.18, 2.59, 0.21], 190 | [4.58, 0.18, 2.71, 0.16, 2.34, 0.19]], dtype='float32') # Parameter values. 191 | self.lith_param = pd.DataFrame(data=data, index=index, columns=column) # Lithology parameter data-frame. 192 | # Print model's information. 193 | print('Model extent:') 194 | print('X range: %.2fm-%.2fm' % (self.X_min, self.X_max)) 195 | print('Y range: %.2fm-%.2fm' % (self.Y_min, self.Y_max)) 196 | print('Z range: %.2fm-%.2fm' % (self.Z_min, self.Z_max)) 197 | print('Model resolution (XYZ): [%.2fm x %.2fm x %.2fm]' % 198 | (self.X_resolution, self.Y_resolution, self.Z_resolution)) 199 | print('Model points (XYZ): [%d x %d x %d]' % (self.X_points, self.Y_points, self.Z_points)) 200 | 201 | def add_rc(self, rc_range=None): 202 | """ 203 | Add random reflection coefficient (rc) to the model. 204 | :param rc_range: (List of floats) - [min, max]. Default is [-1, 1]. 205 | The range of reflection coefficient. 206 | """ 207 | if rc_range is None: 208 | rc_min, rc_max = -1, 1 209 | else: 210 | rc_min, rc_max = rc_range[0], rc_range[1] 211 | rdm = (rc_max - rc_min) * np.random.random_sample((self.Z_points,)) + rc_min 212 | rc = np.ones(shape=self.Z.shape, dtype='float32') 213 | for i in range(self.Z_points): 214 | rc[:, :, i] = rc[:, :, i] * rdm[i] 215 | self.rc = rc 216 | # Set the first and the last rc to 0. 217 | self.rc[:, :, 0] *= 0 218 | self.rc[:, :, -1] *= 0 219 | 220 | def add_rho_v(self, h_layer_range=None, vp_range=None, vs_range=None, rho_range=None): 221 | """ 222 | Add density, p-wave velocity and s-wave velocity. 223 | :param h_layer_range: (List of floats) - Default is [10, 20% * Z_range]. The range of layer thickness. 224 | :param vp_range: (List of floats) - Default is [2500, 5500]. The range of p-wave velocity (m/s). 225 | :param vs_range: (List of floats) - Default is [1440, 3175]. The range of s-wave velocity (m/s). 226 | :param rho_range: (List of floats) - Default is [2.2, 2.7]. The range of density (g/cm3). 227 | """ 228 | # Initialization. 229 | depth_top = self.Z_min # The initial layer's top depth. 230 | ind_bottom = 0 # The initial array index of layer's bottom depth. 231 | if vp_range is None: 232 | vp_range = [2.5, 5.5] 233 | if vs_range is None: 234 | vs_range = [1.44, 3.175] 235 | if rho_range is None: 236 | rho_range = [2.2, 2.7] 237 | # Assign velocity and density from the top to the bottom. 238 | while ind_bottom < self.Z_points - 1: 239 | # Set layer thickness randomly. 240 | if h_layer_range is None: 241 | h_layer = random.uniform(10, 0.2 * (self.Z_max - self.Z_min)) 242 | else: 243 | h_layer = random.uniform(h_layer_range[0], h_layer_range[1]) 244 | # Compute the layer's bottom depth. 245 | depth_bottom = depth_top + h_layer 246 | # Layer's bottom depth can not be greater than Z_max. 247 | if depth_bottom > self.Z_max: 248 | depth_bottom = self.Z_max 249 | # Compute array index. 250 | ind_top = int((depth_top - self.Z_min) // self.Z_resolution) # Layer's top depth index. 251 | ind_bottom = int((depth_bottom - self.Z_min) // self.Z_resolution) # Layer's bottom depth index. 252 | # Assign velocity and density. 253 | vp_layer = random.uniform(vp_range[0], vp_range[1]) 254 | vs_layer = random.uniform(vs_range[0], vs_range[1]) 255 | rho_layer = random.uniform(rho_range[0], rho_range[1]) 256 | if vp_layer / vs_layer < 1.5: 257 | vs_layer = vp_layer / 1.5 258 | if vp_layer / vs_layer > 2.3: 259 | vs_layer = vp_layer / 2.3 260 | self.vp[:, :, ind_top:ind_bottom + 1] *= vp_layer # P-wave velocity. 261 | self.vs[:, :, ind_top:ind_bottom + 1] *= vs_layer # S-wave velocity. 262 | self.density[:, :, ind_top:ind_bottom + 1] *= rho_layer # Density. 263 | # Update layer top depth. 264 | depth_top = (ind_bottom + 1) * self.Z_resolution 265 | 266 | def compute_impedance(self): 267 | """ 268 | Compute P-wave impedance and S-wave impedance. 269 | """ 270 | self.Ip = self.vp * self.density 271 | self.Is = self.vs * self.density 272 | 273 | def compute_rc(self): 274 | """ 275 | Compute reflection coefficient from P-wave impedance. 276 | RC = (Ii+1 - Ii) / (Ii+1 + Ii) 277 | """ 278 | for i in range(self.Ip.shape[2] - 1): 279 | sys.stdout.write('\rComputing reflection coefficient: %.2f%%' % ((i + 1) / (self.Ip.shape[2] - 1) * 100)) 280 | self.rc[:, :, i] = (self.Ip[:, :, i + 1] - self.Ip[:, :, i]) / (self.Ip[:, :, i + 1] + self.Ip[:, :, i]) 281 | sys.stdout.write('\n') 282 | 283 | def make_synseis(self, A=100, fm=30, dt=0.002, wavelet_len=0.1, plot_wavelet=True): 284 | """ 285 | Make synthetic seismic data. 286 | :param A: (float) - Default is 100. The maximum amplitude of ricker wavelet. 287 | :param fm: (Float) - Default is 30Hz. Dominant frequency of the wavelet. 288 | :param dt: (Float) - Default is 0.002s. Sampling time interval of the wavelet. 289 | :param wavelet_len: (Float) - Default is 0.1s. Time duration of the wavelet. 290 | :param plot_wavelet: (Bool) - Default is True. Whether to visualize the wavelet. 291 | """ 292 | self.seis = np.zeros(shape=self.rc.shape, dtype='float32') 293 | t = np.arange(-wavelet_len / 2, wavelet_len / 2, dt, dtype='float32') 294 | ricker = A * (1 - 2 * math.pi ** 2 * fm ** 2 * t ** 2) * np.exp(-math.pi ** 2 * fm ** 2 * t ** 2) 295 | for i in range(self.rc.shape[0]): 296 | for j in range(self.rc.shape[1]): 297 | sys.stdout.write('\rGenerating synthetic seismic data: %.2f%%' % 298 | ((i*self.rc.shape[1]+j+1) / (self.rc.shape[0]*self.rc.shape[1]) * 100)) 299 | self.seis[i, j, :] = np.convolve(ricker, self.rc[i, j, :], mode='same') 300 | sys.stdout.write('\n') 301 | if plot_wavelet: 302 | plt.figure() 303 | plt.style.use('bmh') 304 | plt.plot(t, ricker, lw=2) 305 | plt.xlabel('t(s)') 306 | plt.ylabel('y') 307 | plt.show() 308 | 309 | def add_noise(self, noise_type='uniform', ratio=0.05): 310 | """ 311 | Add random noise to synthetic seismic data. 312 | :param noise_type: (String) - The noise type. Default is uniform random noise. 313 | Options are: 314 | 1. 'uniform': generate random noise with uniform distribution. 315 | 2. 'gaussian': generate random noise with normal distribution. 316 | :param ratio: (Float) - Default is 5%. The noise amplitude to seismic absolute maximum amplitude ratio. 317 | """ 318 | print(f'Adding {noise_type} noise...') 319 | seis_min, seis_max = np.amin(np.abs(self.seis)), np.amax(np.abs(self.seis)) 320 | seis_mean = np.average(np.abs(self.seis)) # Get the average amplitude of synthetic seismic data. 321 | print('Seismic data [min, max, mean]: [%.2f, %.2f, %.2f]' % (seis_min, seis_max, seis_mean)) 322 | if noise_type == 'uniform': # Add uniform random noise. 323 | # Generate noise. 324 | noise_min, noise_max = [-ratio * seis_mean, ratio * seis_mean] # The range of uniform random noise. 325 | print('Noise range: [%.2f, %.2f]' % (noise_min, noise_max)) 326 | noise = (noise_max - noise_min) * np.random.random_sample(self.seis.shape) + noise_min 327 | if noise_type == 'gaussian': # Add random noise with normal distribution. 328 | # Generate noise. 329 | noise_std = ratio * seis_mean # The standard deviation of random noise. 330 | print('Noise info [mean, std]: [0.00, %.2fs]' % noise_std) 331 | noise = np.random.normal(loc=0, scale=noise_std, size=self.seis.shape) 332 | # Add noise to seismic data. 333 | noise = noise.astype('float32') # Change data type. 334 | self.seis = self.seis + noise 335 | 336 | def add_fold(self, N=10, miu_X_range=None, miu_Y_range=None, sigma_range=None, A_range=None, sync=False): 337 | """ 338 | Simulate folds with a combination of Gaussian functions. 339 | :param N: (Integer) - Default is 10. Control the number of Gaussian functions. 340 | :param miu_X_range: (List of floats) - [min, max]. Default is [X_min, X_max). 341 | Center coordinate's (X-coordinate) range of the Gaussian function. 342 | :param miu_Y_range: (List of floats) - [min, max]. Default is [Y_min, Y_max). 343 | Center coordinate's (Y-coordinate) range of the Gaussian function. 344 | :param sigma_range: (List of floats) - [min, max]. 345 | Default is [10% * min(X_range, Y_range), 20% * max(X_range, Y_range)). 346 | The half-width's range of the Gaussian function. 347 | :param A_range: (List of floats) - [min, max]. 348 | Default is [5% * Z_range, 10% * Z_range). 349 | The amplitude's range of the Gaussian function. 350 | :param sync: (Bool) - Default is False: deeper layers have more uplift. 351 | If True: layers with same XY have same uplift. 352 | """ 353 | t_start = time.perf_counter() 354 | sys.stdout.write('Simulating folding structure...') 355 | Gaussian_sum = 0 # Initialize the summation of Gaussian function. 356 | fold_parameter = PrettyTable() # For visualizing parameters 357 | fold_parameter.field_names = ['Fold Number', 'miu_X', 'miu_Y', 'sigma', 'Amplitude'] 358 | fold_parameter.float_format = '.2' 359 | for i in range(N): 360 | if miu_X_range is None: 361 | miu_X = random.uniform(self.X_min, self.X_max) 362 | else: 363 | miu_X = random.uniform(miu_X_range[0], miu_X_range[1]) 364 | if miu_Y_range is None: 365 | miu_Y = random.uniform(self.Y_min, self.Y_max) 366 | else: 367 | miu_Y = random.uniform(miu_Y_range[0], miu_Y_range[1]) 368 | if sigma_range is None: 369 | sigma = random.uniform(0.1 * min(self.X_max - self.X_min, self.Y_max - self.Y_min), 370 | 0.2 * min(self.X_max - self.X_min, self.Y_max - self.Y_min)) 371 | else: 372 | sigma = random.uniform(sigma_range[0], sigma_range[1]) 373 | if A_range is None: 374 | A = random.uniform(0.05 * (self.Z_max - self.Z_min), 0.1 * (self.Z_max - self.Z_min)) 375 | else: 376 | A = random.uniform(A_range[0], A_range[1]) 377 | # The Gaussian function. 378 | f_Gaussian = A * np.exp(-1 * ((self.X - miu_X) ** 2 + (self.Y - miu_Y) ** 2) / (2 * sigma ** 2)) 379 | Gaussian_sum += f_Gaussian # Combine the Gaussian functions. 380 | fold_parameter.add_row([i + 1, miu_X, miu_Y, sigma, A]) # Visualizing parameters. 381 | # Shift the Z-coordinates vertically. 382 | if sync is False: 383 | self.Z = self.Z - self.Z / self.Z.max() * Gaussian_sum 384 | else: 385 | self.Z = self.Z - Gaussian_sum 386 | # Limit the model in defined extent range. 387 | self.Z[self.Z > self.Z_max] = self.Z_max 388 | self.Z[self.Z < self.Z_min] = self.Z_min 389 | t_end = time.perf_counter() 390 | sys.stdout.write('Done.\n') 391 | print('Simulation time: %.2fs' % (t_end - t_start)) 392 | print(fold_parameter) 393 | 394 | def add_fault(self, N=3, reference_point_range=None, phi_range=None, theta_range=None, 395 | d_max_range=None, lx_range=None, ly_range=None, gamma_range=None, beta_range=None, 396 | curved_fault_surface=True, n_perturb=20, perturb_range=None, 397 | mark_faults=False, 398 | computation_mode='parallel'): 399 | """ 400 | Simulate faults. 401 | :param N: (Integer) - Default is 3. Number of faults in a model. 402 | :param reference_point_range: (List of floats) - [X0_min, X0_max, Y0_min, Y0_max, Z0_min, Z0_max]. 403 | Default is X0 = [X_min, X_max), 404 | Y0 = [Y_min, Y_max), 405 | Z0 = [Z_min, Z_max). 406 | The 3-D coordinates' range of a fault surface's center point. 407 | :param phi_range: (List of floats) - [min, max]. Default is [0, 360). The range of strike angle. 408 | :param theta_range: (List of floats) - [min, max]. Default is [0, 90). The range of dip angle. 409 | :param d_max_range: (List of floats) - [min, max]. Default is [10% * y_range, 30% * y_range). 410 | The range of maximum displacement on the fault surface. 411 | The customized range should be fractions of y_range (e.g. [0.1, 1.1]), where y_range is the 412 | model's length in y-direction (dip direction) of fault surface coordinates. 413 | :param lx_range: (List of floats) - [min, max]. Default is [50% * x_range, 100% * x_range). 414 | The range of strike direction axis' length of the elliptic displacement field on fault surface. 415 | The customized range should be fractions of x_range (e.g. [0.1, 1.1]), where x_range is the 416 | model's length in x-direction (strike direction) of fault surface coordinates. 417 | :param ly_range: (List of floats) - [min, max]. Default is [50% * y_range, 100% * y_range). 418 | The range of dip direction axis' length of the elliptic displacement field on fault surface. 419 | The customized range should be fractions of y_range (e.g. [0.1, 1.1]), where y_range is the 420 | model's length in y-direction (dip direction) of fault surface coordinates. 421 | :param gamma_range: (List of floats) - [min, max]. Default is [10% * z_range, 50% * z_range). 422 | The range of reverse drag radius. 423 | The customized range should be fractions of z_range (e.g. [0.1, 1.1]), where z_range is the 424 | model's length in z-direction (normal direction) of the fault surface coordinates. 425 | :param beta_range: (List of floats) - [min, max]. Default is [0.5, 1). 426 | The range of hanging-wall's displacement / d_max. 427 | :param curved_fault_surface: (Bool) - Default is True. 428 | Whether to create curved fault surface. 429 | :param n_perturb: (Integer) - Default is 20. Number of perturbation points near the fault surface. 430 | :param perturb_range: (List of floats) - Default is [-5% * z_range, 5% * z_range). 431 | The range of perturbation points' z coordinates. 432 | The customized range should be fractions of z_range (e.g. [-0.05, 0.05]), where z_range is 433 | the model's length in z-direction (normal direction) of the fault surface coordinates. 434 | :param mark_faults: (Bool) - Default is False. If True, mark faults with label "1" and others with label "0". 435 | :param computation_mode: (String) - Default is "parallel", which is to break down the model's coordinate arrays 436 | into slices and simulate curved faults in parallel. 437 | 'non-parallel' takes the whole coordinate arrays as input to simulate curved faults. 438 | Notice that when the model size is small (e.g. 32 x 32 x 32), taking the whole 439 | coordinate arrays as input will be faster. 440 | In addition, when the memory space is not enough, use the 'parallel' mode may solve the 441 | problem. 442 | """ 443 | t_start = time.perf_counter() 444 | if curved_fault_surface: 445 | if computation_mode != 'parallel' and computation_mode != 'non-parallel': 446 | raise ValueError("'computation_mode' must be 'parallel' or 'non-parallel'.") 447 | else: 448 | sys.stdout.write(f'Simulating curved fault in {computation_mode} mode...') 449 | else: 450 | sys.stdout.write('Simulating planar fault...') 451 | fault_parameter = PrettyTable() # For visualizing parameters. 452 | fault_parameter.field_names = ['Fault Number', 'X0', 'Y0', 'Z0', 'phi', 'theta', 'dmax', 'lx', 'ly', 453 | 'gamma', 'beta'] 454 | fault_parameter.float_format = '.2' 455 | for n in range(N): 456 | if reference_point_range is None: 457 | X0, Y0, Z0 = random.uniform(self.X.min(), self.X.max()), \ 458 | random.uniform(self.Y.min(), self.Y.max()), \ 459 | random.uniform(self.Z.min(), self.Z.max()) 460 | else: 461 | X0, Y0, Z0 = random.uniform(reference_point_range[0], reference_point_range[1]), \ 462 | random.uniform(reference_point_range[2], reference_point_range[3]), \ 463 | random.uniform(reference_point_range[4], reference_point_range[5]) 464 | if phi_range is None: 465 | phi = random.uniform(0, 360) 466 | else: 467 | phi = random.uniform(phi_range[0], phi_range[1]) 468 | if theta_range is None: 469 | theta = random.uniform(0, 90) 470 | else: 471 | theta = random.uniform(theta_range[0], theta_range[1]) 472 | phi, theta = [math.radians(phi), math.radians(theta)] # Convert from angle to radian. 473 | R = [[math.sin(phi), - math.cos(phi), 0], # Rotation matrix. 474 | [math.cos(phi) * math.cos(theta), math.sin(phi) * math.cos(theta), math.sin(theta)], 475 | [math.cos(phi) * math.sin(theta), math.sin(phi) * math.sin(theta), -math.cos(theta)]] 476 | R = np.array(R, dtype='float32') 477 | # The points' global coordinates relative to the reference point. 478 | cor_g = np.array([(self.X - X0).ravel(order='C'), 479 | (self.Y - Y0).ravel(order='C'), 480 | (self.Z - Z0).ravel(order='C')], dtype='float32') 481 | # Coordinates rotation. 482 | # "x" is the strike direction, "y" is the dip direction and "z" is the normal direction. 483 | [x, y, z] = R @ cor_g 484 | x = x.reshape(self.X.shape, order='C') 485 | y = y.reshape(self.Y.shape, order='C') 486 | z = z.reshape(self.Z.shape, order='C') 487 | if lx_range is None: 488 | lx = random.uniform(0.5 * (x.max() - x.min()), 1.0 * (x.max() - x.min())) 489 | else: 490 | lx = random.uniform(lx_range[0] * (x.max() - x.min()), lx_range[1] * (x.max() - x.min())) 491 | if ly_range is None: 492 | ly = random.uniform(0.1 * (y.max() - y.min()), 0.5 * (y.max() - y.min())) 493 | else: 494 | ly = random.uniform(ly_range[0] * (y.max() - y.min()), ly_range[1] * (y.max() - y.min())) 495 | r = np.sqrt((x / lx) ** 2 + (y / ly) ** 2) # The elliptic surface along the fault plane. 496 | r[r > 1] = 1 # To make the displacement = 0 outside the elliptic surface's boundary. 497 | # The elliptic displacement field along the fault plane. 498 | if d_max_range is None: 499 | d_max = random.uniform(0.1 * (y.max() - y.min()), 0.3 * (y.max() - y.min())) 500 | else: 501 | d_max = random.uniform(d_max_range[0] * (y.max() - y.min()), d_max_range[1] * (y.max() - y.min())) 502 | d = 2 * d_max * (1 - r) * np.sqrt((1 + r) ** 2 / 4 - r ** 2) 503 | f = 0 # Define fault surface (0 for plane surface). 504 | # Create curved fault surface. 505 | if curved_fault_surface: 506 | # Randomly choose the 3-D coordinates of perturbation points. 507 | if perturb_range is None: 508 | perturb_range = [-0.05 * (z.max() - z.min()), 0.05 * (z.max() - z.min())] 509 | else: 510 | perturb_range = [perturb_range[0] * (z.max() - z.min()), perturb_range[1] * (z.max() - z.min())] 511 | x_perturb = (x.max() - x.min()) * np.random.random_sample((n_perturb,)) + x.min() 512 | y_perturb = (y.max() - y.min()) * np.random.random_sample((n_perturb,)) + y.min() 513 | z_perturb = \ 514 | (perturb_range[1] - perturb_range[0]) * np.random.random_sample((n_perturb,)) + perturb_range[0] 515 | # Use the perturbation points to calculate the parameters of Bi-harmonic Spline interpolator. 516 | interpolator = BiharmonicSpline3D(x_perturb, y_perturb, z_perturb) 517 | # Interpolate a curved fault surfaces. 518 | if computation_mode == 'parallel': 519 | n_cores = multiprocessing.cpu_count() # Get the number of cpu cores. 520 | f = Parallel(n_jobs=n_cores)(delayed(compute_f_parallel)(i, x, y, interpolator) 521 | for i in range(x.shape[0])) # Compute in parallel. 522 | f = np.array(f, dtype='float32') 523 | else: 524 | f = interpolator(x, y) 525 | # Mark faults. 526 | if mark_faults: 527 | z_resolution = (z.max() - z.min()) / (self.Z_points - 1) 528 | ind = (np.abs(z - f) < z_resolution) & (d > 0) 529 | self.fm[ind] = 1 530 | # Nonlinear scalar function that decreases along z-axis from fault surface. 531 | if gamma_range is None: 532 | gamma = random.uniform(0.1 * (z.max() - z.min()), 0.5 * (z.max() - z.min())) 533 | else: 534 | gamma = random.uniform(gamma_range[0] * (z.max() - z.min()), gamma_range[1] * (z.max() - z.min())) 535 | alpha = (1 - np.abs(z - f) / gamma) ** 2 536 | # Initialize the displacement array. 537 | Dx = 0 # Strike displacement. 538 | Dy = np.zeros(shape=y.shape, dtype='float32') # Dip displacement 539 | Dz = 0 # Normal displacement. 540 | # Calculate volumetric displacement of the hanging-wall. 541 | if beta_range is None: 542 | beta = random.uniform(0.5, 1) 543 | else: 544 | beta = random.uniform(beta_range[0], beta_range[1]) 545 | Dy[(z > f) & (z <= f + gamma)] = beta * d[(z > f) & (z <= f + gamma)] * alpha[(z > f) & (z <= f + gamma)] 546 | # Calculate volumetric displacement of the foot-wall. 547 | Dy[(z >= f - gamma) & (z <= f)] = \ 548 | (beta - 1) * d[(z >= f - gamma) & (z <= f)] * alpha[(z >= f - gamma) & (z <= f)] 549 | # Add fault displacement. 550 | x = x + Dx 551 | y = y + Dy 552 | if curved_fault_surface: 553 | if computation_mode == 'parallel': 554 | Dz = Parallel(n_jobs=n_cores)(delayed(compute_Dz_parallel)(i, x, y, f, interpolator) 555 | for i in range(x.shape[0])) # Compute in parallel. 556 | Dz = np.array(Dz, dtype='float32') 557 | else: 558 | Dz = interpolator(x, y) - f 559 | z = z + Dz 560 | # Transform back to global coordinate. 561 | cor_f = np.array([x.ravel(order='C'), # "cor_f" is the points' fault-plane coordinates. 562 | y.ravel(order='C'), 563 | z.ravel(order='C')], dtype='float32') 564 | [X_faulted, Y_faulted, Z_faulted] = np.linalg.inv(R) @ cor_f + np.array([[X0], [Y0], [Z0]], dtype='float32') 565 | self.X = X_faulted.reshape(self.X.shape, order='C') 566 | self.Y = Y_faulted.reshape(self.Y.shape, order='C') 567 | self.Z = Z_faulted.reshape(self.Z.shape, order='C') 568 | fault_parameter.add_row([n + 1, X0, Y0, Z0, phi * 180 / math.pi, theta * 180 / math.pi, 569 | d_max, lx, ly, gamma, beta]) 570 | # Limit model in defined extent range. 571 | self.X[self.X > self.X_max] = self.X_max 572 | self.X[self.X < self.X_min] = self.X_min 573 | self.Y[self.Y > self.Y_max] = self.Y_max 574 | self.Y[self.Y < self.Y_min] = self.Y_min 575 | self.Z[self.Z > self.Z_max] = self.Z_max 576 | self.Z[self.Z < self.Z_min] = self.Z_min 577 | t_end = time.perf_counter() 578 | sys.stdout.write('Done.\n') 579 | print('Simulation time: %.2fs' % (t_end - t_start)) 580 | print(fault_parameter) 581 | 582 | def add_meander(self, N, X_pos_range=None, Y_pos_range=None, Z_pos_range=None, strike_range=None, 583 | delta_s=None, n_bends_range=None, perturb_range=None, nit_range=None, dt=None, save_it=10, 584 | W_range=None, D_range=None, kl_range=None, Cf_range=None, pad_up=None, pad_down=None, 585 | h_lag_range=None, h_levee_range=None, w_levee_range=None, h_oxbow_mud_range=None, 586 | critical_distance=None, show_migration=False, figure_title=None, mode='simple'): 587 | """ 588 | Simulate meandering river migration and deposition. 589 | :param N: (Integer) - Number of meandering rivers. 590 | :param X_pos_range: (List of floats) - [min, max]. 591 | Default is [0, s_init(straight center-line's length) - X_range). 592 | Range of river center-line's x-coordinate which the 3D model starts at. 593 | :param Y_pos_range: (List of floats) - [min, max]. Default is [Y_min + 10%*Y_range, Y_max - 10%*Y_range). 594 | Range of the initial straight center-line's y-coordinate. 595 | :param Z_pos_range: (List of floats) - [min, max]. Default is [Z_min + 10%*Z_range, Z_max - 10%*Z_range). 596 | Range of the initial straight center-line's z-coordinate. 597 | :param strike_range: (List of floats) - [min, max]. Default is [0, 360). 598 | Range of the river's strike angle (Relative to x-direction). 599 | :param delta_s: (Floats) - Default is self-adaptive according to the length of initial straight center-line 600 | (s_init // 600). 601 | Sampling interval along the river's center-line. 602 | :param n_bends_range: (List of integers) - [min, max]. Default is [10, 20). 603 | Range of the number of bends in the initial center-line. 604 | :param perturb_range: (List of floats) - [min, max]. Default is [200, 500). Range of perturbation amplitude for 605 | center-line initialization. 606 | :param nit_range: (List of integers) - [min, max]. Default is [500, 2000). Range of the number of iteration. 607 | :param dt: (Float) - Default is 0.1. Time interval of the migration (year). 608 | :param save_it: (Integer) - Default is 10. Save center-line for every "save_it" iteration. 609 | :param W_range: (List of floats) - [min, max]. Default is [50, 1500). 610 | Range of the river's width (assuming uniform width). 611 | :param D_range: (List of floats) - [min, max]. Default is [20, 200). Range of the river's depth. 612 | :param kl_range: (List of floats) - [min, max]. Default is [10, 50). 613 | Range of the migration rate constant (m/year). 614 | :param Cf_range: (List of floats) - [min, max]. Default is [0.05, 0.1). Range of the friction factor. 615 | :param pad_up: (Integer) - Default is 5. Number of padding points at upstream to fix the center-line. 616 | :param pad_down: (Integer) - Default is 0. Number of padding points at downstream to fix the center-line. 617 | :param h_lag_range: (List of floats) - [min, max]. Default is [20% * Depth, 50% * Depth). 618 | The maximum thickness of riverbed's lag deposits. 619 | :param h_levee_range: (List of floats) - [min, max]. Default is [0.1, 1). 620 | Range of the maximum levee thickness per event. 621 | :param w_levee_range: (List of floats) - [min, max]. Default is [2 * channel width, 6 * channel width). 622 | Range of the levee width. 623 | :param h_oxbow_mud_range: (List of floats) - [min, max]. Default is [50% * (D - h_lag), 80% * (D - h_lag)]. 624 | The range of oxbow lake mudstone thickness. It is a fraction of (D - h_lag). 625 | For example, enter [0.3, 0.6] will randomly select the mudstone thickness from 626 | 30% * (D - h_lag) to 60% * (D - h_lag). 627 | :param critical_distance: (Float) - Default is river width.The critical distance. Cutoff occurs when distance 628 | of two points on center-line is shorter than (or equal to) the critical distance. 629 | :param show_migration: (Bool) - Default is False. If True, show river migration progress on X-Y plane. 630 | :param figure_title: (String) - Default is "Meandering River Migration". The title of river migration figure. 631 | :param mode: (String) - Default is "simple". Simulation mode ["simple", "complex"]. 632 | For "simple" mode, just simulate the riverbed lag deposit of the most recent meandering river after 633 | the last migration. 634 | For "complex" mode, simulate the following deposition sub-facies: 635 | 1. riverbed sub-facies including lag deposit and point-bar deposit. 636 | 2. natural levee sub-facies. 637 | 3. oxbow-lake sub-facies. 638 | """ 639 | if mode != 'simple' and mode != 'complex': 640 | raise ValueError(f"No such mode as '{mode}', choose 'simple' mode or 'complex' mode.") 641 | t_start = time.perf_counter() 642 | print(f'Simulating meandering channel in {mode} mode...') 643 | # Make table to display river parameters. 644 | meander_parameter = PrettyTable() 645 | if mode == 'simple': 646 | meander_parameter.field_names = ['Meander number', 'X', 'Y', 'Z', 'strike', 'width', 'depth', 's_init', 647 | 'n_bends', 'iteration', 'dt (year)', 'migration rate constant (m/year)', 648 | 'friction factor', 'h_lag (m)'] 649 | if mode == 'complex': 650 | meander_parameter.field_names = ['Meander number', 'X', 'Y', 'Z', 'strike', 'width', 'depth', 's_init', 651 | 'n_bends', 'iteration', 'dt (year)', 'migration rate constant (m/year)', 652 | 'friction factor', 'h_lag (m)', 'h_levee (m)', 'w_levee (m)'] 653 | meander_parameter.float_format = '.2' 654 | # Simulation begins. 655 | for n in range(N): # Number of rivers. 656 | # Print progress. 657 | print('Channel[%d/%d]:' % (n + 1, N)) 658 | # Initialize markers. 659 | marker = np.zeros(self.Z.shape, dtype='float32') 660 | # Assign parameters. 661 | if strike_range is None: # River strike. 662 | strike = random.uniform(0, 360) 663 | else: 664 | strike = random.uniform(strike_range[0], strike_range[1]) 665 | if n_bends_range is None: # Initial number of bends. 666 | n_bends = random.randint(10, 20) 667 | else: 668 | n_bends = random.randint(n_bends_range[0], n_bends_range[1]) 669 | if perturb_range is None: # The perturbation amplitude for river center-line initialization. 670 | perturb_range = [200, 500] 671 | if nit_range is None: # Number of iteration. 672 | nit = random.randint(500, 2000) 673 | else: 674 | nit = random.randint(nit_range[0], nit_range[1]) 675 | if dt is None: # Migration time interval. 676 | dt = 0.1 677 | if W_range is None: # River width. 678 | W = random.uniform(50, 1500) 679 | else: 680 | W = random.uniform(W_range[0], W_range[1]) 681 | if D_range is None: # River maximum depth. 682 | D = random.uniform(20, 200) 683 | else: 684 | D = random.uniform(D_range[0], D_range[1]) 685 | if D > W: # Avoid that river depth is greater than river width. 686 | D = W 687 | if h_lag_range is None: # Riverbed lag deposit thickness. 688 | h_lag = random.uniform(0.2 * D, 0.5 * D) 689 | else: 690 | h_lag = random.uniform(h_lag_range[0], h_lag_range[1]) 691 | if h_lag > D: 692 | h_lag = D # Lag deposit thickness can not be greater than channel's maximum depth. 693 | if h_levee_range is None: # Natural levee thickness per event. 694 | h_levee = random.uniform(0.1, 1) 695 | else: 696 | h_levee = random.uniform(h_levee_range[0], h_levee_range[1]) 697 | if w_levee_range is None: # Natural levee width. 698 | w_levee = random.uniform(2 * W, 6 * W) 699 | else: 700 | w_levee = random.uniform(w_levee_range[0], w_levee_range[1]) 701 | if h_oxbow_mud_range is None: 702 | h_oxbow_mud = random.uniform((D - h_lag) * 0.5, (D - h_lag) * 0.8) 703 | else: 704 | h_oxbow_mud = random.uniform(h_oxbow_mud_range[0] * (D - h_lag), h_oxbow_mud_range[1] * (D - h_lag)) 705 | if critical_distance is None: # Cutoff critical distance. 706 | critical_distance = W 707 | if kl_range is None: # Migration rate constant. 708 | kl = random.uniform(10, 50) 709 | else: 710 | kl = random.uniform(kl_range[0], kl_range[1]) 711 | if Cf_range is None: # Friction factor. 712 | Cf = random.uniform(0.05, 0.1) 713 | else: 714 | Cf = random.uniform(Cf_range[0], Cf_range[1]) 715 | if pad_up is None: # Number of upstream padding points. 716 | pad_up = 5 717 | if pad_down is None: # Number of downstream padding points. 718 | pad_down = 0 719 | s_init = 5.0 * n_bends * W # The length of the initial straight center-line. 720 | if delta_s is None: # River center-line sampling interval. 721 | delta_s = s_init // 600 # The default self-adaptive sampling interval (about 600 segments). 722 | # Make sure that the center-line still crosses the model's X-Y plane after any kind of rotation. 723 | patch = math.sqrt((self.X_max - self.X_min) ** 2 + 4 * (self.Y_max - self.Y_min) ** 2) - (self.X_max - 724 | self.X_min) 725 | if s_init < self.X_max - self.X_min + patch: 726 | s_init = self.X_max - self.X_min + patch 727 | if X_pos_range is None: 728 | X_pos = random.uniform(patch / 2, s_init + self.X_min - self.X_max - patch / 2) 729 | else: 730 | X_pos = random.uniform(X_pos_range[0], X_pos_range[1]) 731 | if X_pos < patch / 2: 732 | X_pos = patch / 2 733 | if X_pos > s_init + self.X_min - self.X_max - patch / 2: 734 | X_pos = s_init + self.X_min - self.X_max - patch / 2 735 | if Y_pos_range is None: 736 | Y_pos = random.uniform(self.Y_min + 0.1 * (self.Y_max - self.Y_min), 737 | self.Y_max - 0.1 * (self.Y_max - self.Y_min)) 738 | else: 739 | Y_pos = random.uniform(Y_pos_range[0], Y_pos_range[1]) 740 | if Z_pos_range is None: 741 | Z_pos = random.uniform(self.Z_min + 0.1 * (self.Z_max - self.Z_min), 742 | self.Z_max - 0.1 * (self.Z_max - self.Z_min)) 743 | else: 744 | Z_pos = random.uniform(Z_pos_range[0], Z_pos_range[1]) 745 | # Add parameters to table. 746 | if mode == 'simple': 747 | meander_parameter.add_row([n + 1, X_pos, Y_pos, Z_pos, strike, W, D, s_init, n_bends, nit, dt, kl, Cf, 748 | h_lag]) 749 | if mode == 'complex': 750 | meander_parameter.add_row([n + 1, X_pos, Y_pos, Z_pos, strike, W, D, s_init, n_bends, nit, dt, kl, Cf, 751 | h_lag, h_levee, w_levee]) 752 | # Print river parameter table. 753 | print(meander_parameter) 754 | # Initialize river list. 755 | centerline = [] 756 | # Initialize oxbow-lake list. 757 | oxbow_per_channel = [] 758 | X_ctl, Y_ctl, Z_ctl = initialize_centerline(s_init, Y_pos, Z_pos, delta_s, n_bends, perturb_range) 759 | # Re-sample center-line so that delta_s is roughly constant. 760 | X_ctl, Y_ctl, Z_ctl = resample_centerline(X_ctl, Y_ctl, Z_ctl, delta_s) 761 | # Save current river parameters. 762 | centerline.append(Channel(X_ctl, Y_ctl, Z_ctl, W, D)) 763 | # Start river migration. 764 | for it in range(nit): 765 | # Compute derivative of x (dx), y (dy), center-line's length (s) and distances between two 766 | # consecutive points along center-line. 767 | dx, dy, s, ds = compute_curvelength(X_ctl, Y_ctl) 768 | # Compute curvatures at each points on center-line. 769 | c = compute_curvature(X_ctl, Y_ctl) 770 | # Compute sinuosity. 771 | sinuosity = s / (X_ctl[-1] - X_ctl[0]) 772 | # Compute migration rate of each point. 773 | R1 = compute_migration_rate(curv=c, ds=ds, W=W, kl=kl, Cf=Cf, D=D, pad_up=pad_up, pad_down=pad_down) 774 | # Adjust migration rate. 775 | R1 = sinuosity ** (-2 / 3.0) * R1 776 | # Compute coordinates after migration. 777 | ns = len(R1) 778 | dx_ds = dx[pad_up:ns - pad_down] / ds[pad_up:ns - pad_down] 779 | dy_ds = dy[pad_up:ns - pad_down] / ds[pad_up:ns - pad_down] 780 | X_ctl[pad_up:ns - pad_down] = X_ctl[pad_up:ns - pad_down] + R1[pad_up:ns - pad_down] * dy_ds * dt 781 | Y_ctl[pad_up:ns - pad_down] = Y_ctl[pad_up:ns - pad_down] - R1[pad_up:ns - pad_down] * dx_ds * dt 782 | # Re-sample center-line so that delta_s is roughly constant. 783 | X_ctl, Y_ctl, Z_ctl = resample_centerline(X_ctl, Y_ctl, Z_ctl, delta_s) 784 | # Find and execute cutoff. 785 | X_ox, Y_ox, Z_ox, X_ctl, Y_ctl, Z_ctl = execute_cutoff(X_ctl, Y_ctl, Z_ctl, delta_s, critical_distance) 786 | # Re-sample center-line so that delta_s is roughly constant. 787 | X_ctl, Y_ctl, Z_ctl = resample_centerline(X_ctl, Y_ctl, Z_ctl, delta_s) 788 | # Save oxbow-lake parameters. 789 | oxbow_per_channel.append(Oxbow(X_ox, Y_ox, Z_ox, W, D)) 790 | # Save river parameters. 791 | if it > 0 and it % save_it == 0: 792 | centerline.append(Channel(X_ctl, Y_ctl, Z_ctl, W, D)) 793 | # Print progress. 794 | sys.stdout.write('\rMigration progress:%.2f%%' % ((it + 1) / nit * 100)) 795 | sys.stdout.write('\n') 796 | self.channel.append(centerline) # Save different rivers. 797 | self.oxbow.append(oxbow_per_channel) # Save oxbows of different rivers. 798 | # Show river migration and oxbows on X-Y plane. 799 | if show_migration: 800 | plot_channel2D(centerline, oxbow_per_channel, title=figure_title, interval=2) 801 | n_centerline = len(centerline) # Number of saved rivers during migration. 802 | if mode == 'simple': # In this mode, only simulate lag deposit of the most recent river. 803 | # Check requirement. 804 | if self.X_resolution != self.Y_resolution: 805 | raise ValueError("For river deposits simulation, model's X & Y resolution must be identical.") 806 | # Get coordinates of the most recent center-line. 807 | X_ctl, Y_ctl, Z_ctl = centerline[-1].x, centerline[-1].y, centerline[-1].z 808 | # Select the center-line segment in target area. 809 | ind = (X_ctl >= X_pos - patch / 2) & (X_ctl <= X_pos + self.X_max - self.X_min + patch / 2) 810 | X_ctl, Y_ctl, Z_ctl = X_ctl[ind], Y_ctl[ind], Z_ctl[ind] 811 | # Re-sample center-line according to model's resolution. 812 | if delta_s > self.X_resolution: # Center-line's resolution can not be larger than model's 813 | X_ctl, Y_ctl, Z_ctl = resample_centerline(X_ctl, Y_ctl, Z_ctl, self.X_resolution) 814 | # Rotate the channel by its strike. 815 | R = np.array([[math.cos(math.radians(strike)), -math.sin(math.radians(strike))], # Rotation matrix. 816 | [math.sin(math.radians(strike)), math.cos(math.radians(strike))]], dtype='float32') 817 | center_X, center_Y = (self.X_max - self.X_min) / 2 + X_pos, Y_pos # Rotation center. 818 | [X_ctl, Y_ctl] = R @ [X_ctl - center_X, Y_ctl - center_Y] # Rotate. 819 | X_ctl += center_X 820 | Y_ctl += center_Y 821 | # Rasterize center-line and compute distance to center-line on X-Y plane. 822 | dist = compute_centerline_distance(X_ctl, Y_ctl, X_pos, self.X_min, self.Y_min, self.X_resolution, 823 | self.X_points, self.Y_points) 824 | # Initialize topography. 825 | topo = np.ones(shape=[self.X_points, self.Y_points], dtype='float32') * Z_pos 826 | # River erosion. 827 | ze = erosion_surface(cl_dist=dist, z=Z_ctl, W=W, D=D) 828 | ze[ze < self.Z_min] = self.Z_min # Limit in model's z-range. 829 | ze[ze > self.Z_max] = self.Z_max # Limit in model's z-range. 830 | topo = np.maximum(topo, ze) 831 | # Riverbed lag deposit (gravel/conglomerate). 832 | zl = lag_surface(cl_dist=dist, z=Z_ctl, h_lag=h_lag, D=D) 833 | zl[zl < self.Z_min] = self.Z_min # Limit in model's z-range. 834 | zl[zl > self.Z_max] = self.Z_max # Limit in model's z-range. 835 | index = np.argwhere(zl < topo) 836 | indx, indy = index[:, 0], index[:, 1] 837 | indz1 = ((zl[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 838 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 839 | for i in range(len(indx)): 840 | sys.stdout.write('\rDeposition progress: %.2f%%' % ((i+1) / len(indx) * 100)) 841 | # Riverbed lag deposit: face code "1". 842 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 843 | # Gravel: lithology code "1". 844 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 845 | # Change marker. 846 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 847 | sys.stdout.write('\n') 848 | if mode == 'complex': # In this mode, simulate various kinds of river deposit. 849 | # Check requirement. 850 | if self.X_resolution != self.Y_resolution: 851 | raise ValueError("For river deposits simulation, model's X & Y resolution must be identical.") 852 | # Initialize topography. 853 | topo = np.ones(shape=[self.X_points, self.Y_points], dtype='float32') * Z_pos 854 | # Initialize global oxbow-lake center-line distance with a large number. 855 | oxbow_dist = np.ones(shape=[self.X_points, self.Y_points], dtype='float32') * 1e10 856 | # Create river deposits center-line by center-line. 857 | for i_ctl in range(n_centerline): 858 | # Get the coordinates of the center-line. 859 | X_ctl, Y_ctl, Z_ctl = centerline[i_ctl].x, centerline[i_ctl].y, centerline[i_ctl].z 860 | # Select the center-line segment in target area. 861 | ind = (X_ctl >= X_pos - patch / 2) & (X_ctl <= X_pos + self.X_max - self.X_min + patch / 2) 862 | X_ctl, Y_ctl, Z_ctl = X_ctl[ind], Y_ctl[ind], Z_ctl[ind] 863 | # Re-sample center-line according to model's resolution. 864 | if delta_s > self.X_resolution: # Center-line's resolution must be smaller than model's. 865 | X_ctl, Y_ctl, Z_ctl = resample_centerline(X_ctl, Y_ctl, Z_ctl, self.X_resolution) 866 | # Rotate the river center-line by its strike. 867 | R = np.array([[math.cos(math.radians(strike)), -math.sin(math.radians(strike))], # Rotation matrix. 868 | [math.sin(math.radians(strike)), math.cos(math.radians(strike))]], dtype='float32') 869 | center_X, center_Y = (self.X_max - self.X_min) / 2 + X_pos, Y_pos # Rotation center. 870 | [X_ctl, Y_ctl] = R @ [X_ctl - center_X, Y_ctl - center_Y] # Rotate channel center-line. 871 | X_ctl += center_X 872 | Y_ctl += center_Y 873 | # Rasterize channel center-line and compute distance to center-line on X-Y plane. 874 | dist = compute_centerline_distance(X_ctl, Y_ctl, X_pos, self.X_min, self.Y_min, self.X_resolution, 875 | self.X_points, self.Y_points) 876 | # Make a list of oxbow-lake center-line coordinates between i and i-1 migration. 877 | X_ox, Y_ox, Z_ox = [], [], [] 878 | # Check if cutoff happens before. 879 | if i_ctl > 0: 880 | for i_cn in range((i_ctl - 1) * save_it, i_ctl * save_it, 1): 881 | X_oxbow, Y_oxbow, Z_oxbow = oxbow_per_channel[i_cn].x, oxbow_per_channel[i_cn].y, \ 882 | oxbow_per_channel[i_cn].z 883 | if len(X_oxbow) > 0: 884 | n_oxbow = len(X_oxbow) 885 | for i_oxbow in range(n_oxbow): 886 | # Ensure that oxbow center-line's resolution is not larger than model's resolution. 887 | if delta_s > self.X_resolution: 888 | X_oxbow[i_oxbow], Y_oxbow[i_oxbow], Z_oxbow[i_oxbow] = \ 889 | resample_centerline(X_oxbow[i_oxbow], Y_oxbow[i_oxbow], 890 | Z_oxbow[i_oxbow], self.X_resolution) 891 | # Select oxbow center-line in target area. 892 | ind = (X_oxbow[i_oxbow] >= X_pos - patch / 2) & \ 893 | (X_oxbow[i_oxbow] <= X_pos + self.X_max - self.X_min + patch / 2) 894 | X_oxbow[i_oxbow], Y_oxbow[i_oxbow], Z_oxbow[i_oxbow] = \ 895 | X_oxbow[i_oxbow][ind], Y_oxbow[i_oxbow][ind], Z_oxbow[i_oxbow][ind] 896 | # Rotate oxbow-lake center-line by river strike. 897 | if len(X_oxbow[i_oxbow]) > 0: 898 | R = np.array([[math.cos(math.radians(strike)), -math.sin(math.radians(strike))], 899 | [math.sin(math.radians(strike)), math.cos(math.radians(strike))]], 900 | dtype='float32') # Rotation matrix. 901 | # Rotation center. 902 | center_X, center_Y = (self.X_max - self.X_min) / 2 + X_pos, Y_pos 903 | # Rotate. 904 | [X_oxbow[i_oxbow], Y_oxbow[i_oxbow]] = \ 905 | R @ [X_oxbow[i_oxbow] - center_X, Y_oxbow[i_oxbow] - center_Y] 906 | X_oxbow[i_oxbow] += center_X 907 | Y_oxbow[i_oxbow] += center_Y 908 | # Assemble the oxbows' coordinates between i and i-1 migration. 909 | X_ox.append(X_oxbow[i_oxbow]) 910 | Y_ox.append(Y_oxbow[i_oxbow]) 911 | Z_ox.append(Z_oxbow[i_oxbow]) 912 | # If cutoffs occur before, compute distance from their center-line. 913 | if len(X_ox) > 0: 914 | for i_ox in range(len(X_ox)): 915 | # Compute distance from oxbow-lake center-line. 916 | ox_dist = compute_centerline_distance(X_ox[i_ox], Y_ox[i_ox], X_pos, self.X_min, 917 | self.Y_min, self.X_resolution, self.X_points, 918 | self.Y_points) 919 | # Update global oxbow-lake center-line distance. 920 | oxbow_dist = np.minimum(oxbow_dist, ox_dist) 921 | # Oxbow-lake erosion. 922 | ze = erosion_surface(cl_dist=ox_dist, z=Z_ox[i_ox], W=W, D=D) 923 | ze[ze < self.Z_min] = self.Z_min # Limit in model's z-range. 924 | ze[ze > self.Z_max] = self.Z_max # Limit in model's z-range. 925 | index = np.argwhere(ze > topo) 926 | indx, indy = index[:, 0], index[:, 1] 927 | indz1 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 928 | indz2 = ((ze[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 929 | for i in range(len(indx)): 930 | # Other deposit: face code "0". 931 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 932 | # Other lithology: lithology code "0". 933 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 934 | # Change marker. 935 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 936 | topo = np.maximum(topo, ze) 937 | # Oxbow-lake lag deposit (gravel/conglomerate). 938 | zl = lag_surface(cl_dist=ox_dist, z=Z_ox[i_ox], h_lag=h_lag, D=D) 939 | zl[ox_dist >= W] = 1e10 # Deposit inside oxbow. 940 | zl[zl < self.Z_min] = self.Z_min # Limit in model's z-range. 941 | zl[zl > self.Z_max] = self.Z_max # Limit in model's z-range. 942 | index = np.argwhere(zl < topo) 943 | indx, indy = index[:, 0], index[:, 1] 944 | indz1 = ((zl[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 945 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 946 | for i in range(len(indx)): 947 | # Oxbow-lake deposit: face code "4". 948 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 4 949 | # Gravel: lithology code "1". 950 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 951 | # Change marker. 952 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 953 | topo = np.minimum(topo, zl) 954 | # Oxbow-lake lag deposit (mudstone). 955 | zl = lag_surface(cl_dist=ox_dist, z=Z_ox[i_ox], h_lag=h_lag + h_oxbow_mud, D=D) 956 | zl[ox_dist >= W] = 1e10 # Deposit inside oxbow. 957 | zl[zl < self.Z_min] = self.Z_min # Limit in model's z-range. 958 | zl[zl > self.Z_max] = self.Z_max # Limit in model's z-range. 959 | index = np.argwhere(zl < topo) 960 | indx, indy = index[:, 0], index[:, 1] 961 | indz1 = ((zl[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 962 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 963 | for i in range(len(indx)): 964 | # Oxbow-lake deposit: face code "4". 965 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 4 966 | # Mudstone: lithology code "4" 967 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 4 968 | # Change marker. 969 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 4 970 | topo = np.minimum(topo, zl) 971 | # River erosion. 972 | ze = erosion_surface(cl_dist=dist, z=Z_ctl, W=W, D=D) 973 | ze[ze < self.Z_min] = self.Z_min # Limit in model's z-range. 974 | ze[ze > self.Z_max] = self.Z_max # Limit in model's z-range. 975 | index = np.argwhere(ze > topo) 976 | indx, indy = index[:, 0], index[:, 1] 977 | indz1 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 978 | indz2 = ((ze[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 979 | for i in range(len(indx)): 980 | # Other deposit: face code "0". 981 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 982 | # Other lithology: lithology code "0". 983 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 984 | # Change marker. 985 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 0 986 | topo = np.maximum(topo, ze) 987 | # Riverbed lag deposit (gravel/conglomerate). 988 | zl = lag_surface(cl_dist=dist, z=Z_ctl, h_lag=h_lag, D=D) 989 | zl[dist >= W] = 1e10 # Deposit inside channel. 990 | zl[zl < self.Z_min] = self.Z_min # Limit in model's z-range. 991 | zl[zl > self.Z_max] = self.Z_max # Limit in model's z-range. 992 | index = np.argwhere(zl < topo) 993 | indx, indy = index[:, 0], index[:, 1] 994 | indz1 = ((zl[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 995 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 996 | for i in range(len(indx)): 997 | # Riverbed lag deposit: face code "1". 998 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 999 | # Gravel: lithology code "1". 1000 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 1001 | # Change marker. 1002 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 1 1003 | topo = np.minimum(topo, zl) 1004 | # Riverbed point-bar deposit. 1005 | if i_ctl != n_centerline - 1: # The most recent river has no point-bar deposit. 1006 | zpb = pointbar_surface(cl_dist=dist, z=Z_ctl, W=W, D=D) 1007 | zpb[oxbow_dist <= W] = 1e10 # Clear point-bar deposit inside oxbow lake. 1008 | zpb[zpb < self.Z_min] = self.Z_min # Limit in model's z-range. 1009 | zpb[zpb > self.Z_max] = self.Z_max # Limit in model's z-range. 1010 | index = np.argwhere(zpb < topo) 1011 | indx, indy = index[:, 0], index[:, 1] 1012 | indz1 = ((zpb[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 1013 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 1014 | for i in range(len(indx)): 1015 | # Riverbed point-bar deposit: face code "2". 1016 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 2 1017 | # Sandstone: lithology code "2". 1018 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 2 1019 | # Change marker. 1020 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 2 1021 | topo = np.minimum(topo, zpb) 1022 | # Natural levee deposit. 1023 | if i_ctl != n_centerline - 1: # The most recent river has no levee deposit. 1024 | zlv = levee_surface(cl_dist=dist, h_levee=h_levee, w_levee=w_levee, W=W, tp=topo) 1025 | zlv[zlv < self.Z_min] = self.Z_min # Limit in model's z-range. 1026 | zlv[zlv > self.Z_max] = self.Z_max # Limit in model's z-range. 1027 | index = np.argwhere(zlv < topo) 1028 | indx, indy = index[:, 0], index[:, 1] 1029 | indz1 = ((zlv[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 1030 | indz2 = ((topo[indx, indy] - self.Z_min) / self.Z_resolution).astype('int32') 1031 | for i in range(len(indx)): 1032 | # Natural levee deposit: face code "3". 1033 | self.channel_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 3 1034 | # Siltstone: lithology code "3". 1035 | self.lith_facies[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 3 1036 | # Change marker. 1037 | marker[indy[i], indx[i], indz1[i]:indz2[i] + 1] = 3 1038 | topo = np.minimum(topo, zlv) 1039 | # Print progress. 1040 | sys.stdout.write('\rDeposition progress:%.2f%% centerline[%d/%d]' % 1041 | ((i_ctl + 1) / n_centerline * 100, i_ctl + 1, n_centerline)) 1042 | sys.stdout.write('\n') 1043 | # Update lithology parameters. 1044 | gravel = self.lith_param.loc['conglomerate', :] # Gravel parameters. 1045 | sand = self.lith_param.loc['pebbled sandstone', :] # Sandstone parameters. 1046 | silt = self.lith_param.loc['siltstone', :] # Siltstone parameters. 1047 | mud = self.lith_param.loc['mudstone', :] # Mudstone parameters. 1048 | self.vp[(self.lith_facies == 1) & (marker == 1)] = random.uniform(gravel.vp_miu - gravel.vp_sigma, 1049 | gravel.vp_miu + gravel.vp_sigma) 1050 | self.vp[(self.lith_facies == 2) & (marker == 2)] = random.uniform(sand.vp_miu - sand.vp_sigma, 1051 | sand.vp_miu + sand.vp_sigma) 1052 | self.vp[(self.lith_facies == 3) & (marker == 3)] = random.uniform(silt.vp_miu - silt.vp_sigma, 1053 | silt.vp_miu + silt.vp_sigma) 1054 | self.vp[(self.lith_facies == 4) & (marker == 4)] = random.uniform(mud.vp_miu - mud.vp_sigma, 1055 | mud.vp_miu + mud.vp_sigma) 1056 | self.vs[(self.lith_facies == 1) & (marker == 1)] = random.uniform(gravel.vs_miu - gravel.vs_sigma, 1057 | gravel.vs_miu + gravel.vs_sigma) 1058 | self.vs[(self.lith_facies == 2) & (marker == 2)] = random.uniform(sand.vs_miu - sand.vs_sigma, 1059 | sand.vs_miu + sand.vs_sigma) 1060 | self.vs[(self.lith_facies == 3) & (marker == 3)] = random.uniform(silt.vs_miu - silt.vs_sigma, 1061 | silt.vs_miu + silt.vs_sigma) 1062 | self.vs[(self.lith_facies == 4) & (marker == 4)] = random.uniform(mud.vs_miu - mud.vs_sigma, 1063 | mud.vs_miu + mud.vs_sigma) 1064 | self.density[(self.lith_facies == 1) & (marker == 1)] = random.uniform(gravel.rho_miu - gravel.rho_sigma, 1065 | gravel.rho_miu + gravel.rho_sigma) 1066 | self.density[(self.lith_facies == 2) & (marker == 2)] = random.uniform(sand.rho_miu - sand.rho_sigma, 1067 | sand.rho_miu + sand.rho_sigma) 1068 | self.density[(self.lith_facies == 3) & (marker == 3)] = random.uniform(silt.rho_miu - silt.rho_sigma, 1069 | silt.rho_miu + silt.rho_sigma) 1070 | self.density[(self.lith_facies == 4) & (marker == 4)] = random.uniform(mud.rho_miu - mud.rho_sigma, 1071 | mud.rho_miu + mud.rho_sigma) 1072 | # Print simulation time. 1073 | t_end = time.perf_counter() 1074 | print('Simulation time: %.2fs' % (t_end - t_start)) 1075 | 1076 | def rectangular_grid(self, resolution=None, param=None, method='nearest', fill_value=np.nan): 1077 | """ 1078 | Re-sample the model on rectangular (quad) grid using interpolation. 1079 | :param resolution: (List of floats) - Default is [4.0, 4.0, 4.0]. The 3D grid spacing. 1080 | :param param: (String or list of strings) - Default is 'all'. Model parameters to be re-sampled. 1081 | If 'all', will re-sample all parameters, 1082 | or you can choose one or more parameters like ['vp'] or ['rc', 'vp', 'vs']. 1083 | :param method: (String) - Default is 'nearest'. Method of interpolation. 1084 | Options are 'nearest', 'linear' and 'cubic'. 1085 | :param fill_value: (Float) - Default is NaN.Value used to fill in for requested points outside of the 1086 | convex hull of the input points. This parameter has no effect for the 'nearest' method. 1087 | """ 1088 | # Before interpolation, store the boundary coordinates for auto-cropping the model. 1089 | self.autocrop = {'xmin': np.round(np.amax(self.X[:, 0, :]), 2), 'xmax': np.round(np.amin(self.X[:, -1, :]), 2), 1090 | 'ymin': np.round(np.amax(self.Y[0, :, :]), 2), 'ymax': np.round(np.amin(self.Y[-1, :, :]), 2), 1091 | 'zmin': np.round(np.amax(self.Z[:, :, 0]), 2), 'zmax': np.round(np.amin(self.Z[:, :, -1]), 2)} 1092 | # Set resolution of 3D rectangular grid. 1093 | if resolution is None: 1094 | dx, dy, dz = self.X_resolution, self.Y_resolution, self.Z_resolution 1095 | else: 1096 | dx, dy, dz = resolution[0], resolution[1], resolution[2] 1097 | self.X_resolution, self.Y_resolution, self.Z_resolution = dx, dy, dz 1098 | print('Interpolate on %.2fm x %.2fm x %.2fm grid...' % (dx, dy, dz)) 1099 | # Make 3D rectangular grid. 1100 | x = np.arange(start=self.X_min, stop=self.X_max + dx, step=dx) 1101 | y = np.arange(start=self.Y_min, stop=self.Y_max + dy, step=dy) 1102 | z = np.arange(start=self.Z_min, stop=self.Z_max + dz, step=dz) 1103 | x_mesh, y_mesh, z_mesh = np.meshgrid(x, y, z) 1104 | # Interpolate. 1105 | t_start = time.perf_counter() 1106 | ctp = np.c_[self.X.ravel(order='F'), 1107 | self.Y.ravel(order='F'), 1108 | self.Z.ravel(order='F')] # Coordinates of control points. 1109 | self.X, self.Y, self.Z = x_mesh.copy(), y_mesh.copy(), z_mesh.copy() 1110 | self.Y_points, self.X_points, self.Z_points = self.X.shape 1111 | if param is None: 1112 | param = 'all' 1113 | if param == 'all' or 'rc' in param: 1114 | sys.stdout.write('Interpolating reflection coefficient...') 1115 | self.rc = scipy.interpolate.griddata(points=ctp, values=self.rc.ravel(order='F'), 1116 | xi=(x_mesh, y_mesh, z_mesh), 1117 | method=method, fill_value=fill_value) # Reflection coefficient. 1118 | sys.stdout.write('Done.\n') 1119 | if param == 'all' or 'vp' in param: 1120 | sys.stdout.write('Interpolating p-wave velocity...') 1121 | self.vp = scipy.interpolate.griddata(points=ctp, values=self.vp.ravel(order='F'), 1122 | xi=(x_mesh, y_mesh, z_mesh), 1123 | method=method, fill_value=fill_value) # P-wave velocity. 1124 | sys.stdout.write('Done.\n') 1125 | if param == 'all' or 'vs' in param: 1126 | sys.stdout.write('Interpolating s-wave velocity...') 1127 | self.vs = scipy.interpolate.griddata(points=ctp, values=self.vs.ravel(order='F'), 1128 | xi=(x_mesh, y_mesh, z_mesh), 1129 | method=method, fill_value=fill_value) # S-wave velocity. 1130 | sys.stdout.write('Done.\n') 1131 | if param == 'all' or 'density' in param: 1132 | sys.stdout.write('Interpolating density...') 1133 | self.density = scipy.interpolate.griddata(points=ctp, values=self.density.ravel(order='F'), 1134 | xi=(x_mesh, y_mesh, z_mesh), 1135 | method=method, fill_value=fill_value) # density. 1136 | sys.stdout.write('Done.\n') 1137 | if param == 'all' or 'Ip' in param: 1138 | sys.stdout.write('Interpolating p-wave impedance...') 1139 | self.Ip = scipy.interpolate.griddata(points=ctp, values=self.Ip.ravel(order='F'), 1140 | xi=(x_mesh, y_mesh, z_mesh), 1141 | method=method, fill_value=fill_value) # P-wave impedance. 1142 | sys.stdout.write('Done.\n') 1143 | if param == 'all' or 'Is' in param: 1144 | sys.stdout.write('Interpolating s-wave impedance...') 1145 | self.Is = scipy.interpolate.griddata(points=ctp, values=self.Is.ravel(order='F'), 1146 | xi=(x_mesh, y_mesh, z_mesh), 1147 | method=method, fill_value=fill_value) # S-wave impedance. 1148 | sys.stdout.write('Done.\n') 1149 | if param == 'all' or 'lith_facies' in param: 1150 | sys.stdout.write('Interpolating lithology facies...') 1151 | self.lith_facies = scipy.interpolate.griddata(points=ctp, values=self.lith_facies.ravel(order='F'), 1152 | xi=(x_mesh, y_mesh, z_mesh), 1153 | method=method, fill_value=fill_value) # Lithology facies. 1154 | sys.stdout.write('Done.\n') 1155 | if param == 'all' or 'channel_facies' in param: 1156 | sys.stdout.write('Interpolating channel facies...') 1157 | self.channel_facies = scipy.interpolate.griddata(points=ctp, values=self.channel_facies.ravel(order='F'), 1158 | xi=(x_mesh, y_mesh, z_mesh), 1159 | method=method, fill_value=fill_value) # Channel facies. 1160 | sys.stdout.write('Done.\n') 1161 | if param == 'all' or 'fm' in param: 1162 | sys.stdout.write('Interpolating fault marker...') 1163 | self.fm = scipy.interpolate.griddata(points=ctp, values=self.fm.ravel(order='F'), 1164 | xi=(x_mesh, y_mesh, z_mesh), 1165 | method=method, fill_value=fill_value) # Fault marker. 1166 | sys.stdout.write('Done.\n') 1167 | t_end = time.perf_counter() 1168 | print('Interpolation finished. Time: %.2fs' % (t_end - t_start)) 1169 | print('Model extent:') 1170 | print('X range: %.2fm-%.2fm' % (self.X_min, self.X_max)) 1171 | print('Y range: %.2fm-%.2fm' % (self.Y_min, self.Y_max)) 1172 | print('Z range: %.2fm-%.2fm' % (self.Z_min, self.Z_max)) 1173 | print('Model resolution (XYZ): [%.2fm x %.2fm x %.2fm]' % 1174 | (self.X_resolution, self.Y_resolution, self.Z_resolution)) 1175 | print('Model points (XYZ): [%d x %d x %d]' % (self.X_points, self.Y_points, self.Z_points)) 1176 | 1177 | def crop(self, x_bound=None, y_bound=None, z_bound=None, param=None): 1178 | """ 1179 | Crop rectangular (quad) grid model. 1180 | :param x_bound: (List of floats) - X coordinates used to crop the model, which is a list of [xmin, xmax]. 1181 | For example, x_bound = [200, 1000] means to crop the model from x = 200 to x = 1000. 1182 | If x_bound is None, will automatically crop the model from the maximum x of the east boundary to 1183 | the minimum x of the west boundary. 1184 | :param y_bound: (List of floats) - Y coordinates used to crop the model, which is a list of [ymin, ymax]. 1185 | For example, y_bound = [200, 1000] means to crop the model from y = 200 to y = 1000. 1186 | If y_bound is None, will automatically crop the model from the maximum y of the south boundary 1187 | to the minimum y of the north boundary. 1188 | :param z_bound: (List of floats) - Z coordinates used to crop the model, which is a list of [zmin, zmax]. 1189 | For example, z_bound = [200, 1000] means to crop the model from z = 200 to z = 1000. 1190 | If z_bound is None, will automatically crop the model from the maximum z of the bottom boundary 1191 | to the minimum z of the top boundary. 1192 | :param param: (String or list of strings) - Default is 'all'. The parameter cube to crop. 1193 | If 'all', will crop all parameter cubes. 1194 | Or you can choose one or more parameter cubes like ['vp'] or ['vp', 'Ip', 'rc'] 1195 | """ 1196 | print('Cropping model...') 1197 | if x_bound is None: # When x_bound is None, auto-crop the model in x direction. 1198 | condx = (self.X >= self.autocrop['xmin']) & (self.X <= self.autocrop['xmax']) 1199 | print('Crop X from %.2fm-%.2fm (auto-crop)' % (self.autocrop['xmin'], self.autocrop['xmax'])) 1200 | else: # When x_bound is defined, crop the model by defined x coordinates 1201 | condx = (self.X >= min(x_bound)) & (self.X <= max(x_bound)) 1202 | print('Crop X from %.2fm-%.2fm' % (min(x_bound), max(x_bound))) 1203 | if y_bound is None: # When y_bound is None, auto-crop the model in y direction. 1204 | condy = (self.Y >= self.autocrop['ymin']) & (self.Y <= self.autocrop['ymax']) 1205 | print('Crop Y from %.2fm-%.2fm (auto-crop)' % (self.autocrop['ymin'], self.autocrop['ymax'])) 1206 | else: # When y_bound is defined, crop the model by defined y direction. 1207 | condy = (self.Y >= min(y_bound)) & (self.Y <= max(y_bound)) 1208 | print('Crop Y from %.2fm-%.2fm' % (min(y_bound), max(y_bound))) 1209 | if z_bound is None: # When z_bound is None, auto-crop the model in z direction. 1210 | condz = (self.Z >= self.autocrop['zmin']) & (self.Z <= self.autocrop['zmax']) 1211 | print('Crop Z from %.2fm-%.2fm (auto-crop)' % (self.autocrop['zmin'], self.autocrop['zmax'])) 1212 | else: # When z_bound is defined, crop the model by defined z coordinates. 1213 | condz = (self.Z >= min(z_bound)) & (self.Z <= max(z_bound)) 1214 | print('Crop Z from %.2fm-%.2fm (auto-crop)' % (min(z_bound), max(z_bound))) 1215 | if param is None: 1216 | param = 'all' 1217 | # XYZ indexes that meet the conditions. 1218 | indx = np.argwhere(condx)[:, 1] 1219 | indy = np.argwhere(condy)[:, 0] 1220 | indz = np.argwhere(condz)[:, -1] 1221 | # Cropping the model. 1222 | sys.stdout.write('Cropping X cube...') 1223 | self.X = self.X[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1224 | sys.stdout.write('Done.\n') 1225 | sys.stdout.write('Cropping Y cube...') 1226 | self.Y = self.Y[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1227 | sys.stdout.write('Done.\n') 1228 | sys.stdout.write('Cropping Z cube...') 1229 | self.Z = self.Z[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1230 | sys.stdout.write('Done.\n') 1231 | if param == 'all' or 'rc' in param: 1232 | sys.stdout.write('Cropping rc cube...') 1233 | self.rc = self.rc[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1234 | sys.stdout.write('Done.\n') 1235 | if param == 'all' or 'vp' in param: 1236 | sys.stdout.write('Cropping vp cube...') 1237 | self.vp = self.vp[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1238 | sys.stdout.write('Done.\n') 1239 | if param == 'all' or 'vs' in param: 1240 | sys.stdout.write('Cropping vs cube...') 1241 | self.vs = self.vs[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1242 | sys.stdout.write('Done.\n') 1243 | if param == 'all' or 'density' in param: 1244 | sys.stdout.write('Cropping density cube...') 1245 | self.density = self.density[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1246 | sys.stdout.write('Done.\n') 1247 | if param == 'all' or 'Ip' in param: 1248 | sys.stdout.write('Cropping Ip cube...') 1249 | self.Ip = self.Ip[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1250 | sys.stdout.write('Done.\n') 1251 | if param == 'all' or 'Is' in param: 1252 | sys.stdout.write('Cropping Is cube...') 1253 | self.Is = self.Is[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1254 | sys.stdout.write('Done.\n') 1255 | if param == 'all' or 'seis' in param: 1256 | sys.stdout.write('Cropping seis cube...') 1257 | self.seis = self.seis[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1258 | sys.stdout.write('Done.\n') 1259 | if param == 'all' or 'lith_facies' in param: 1260 | sys.stdout.write('Cropping lith_facies cube...') 1261 | self.lith_facies = self.lith_facies[min(indy):max(indy) + 1, min(indx):max(indx) + 1, 1262 | min(indz):max(indz) + 1] 1263 | sys.stdout.write('Done.\n') 1264 | if param == 'all' or 'channel_facies' in param: 1265 | sys.stdout.write('Cropping channel_facies cube...') 1266 | self.channel_facies = self.channel_facies[min(indy):max(indy) + 1, min(indx):max(indx) + 1, 1267 | min(indz):max(indz) + 1] 1268 | sys.stdout.write('Done.\n') 1269 | if param == 'all' or 'fm' in param: 1270 | sys.stdout.write('Cropping rc cube...') 1271 | self.fm = self.fm[min(indy):max(indy) + 1, min(indx):max(indx) + 1, min(indz):max(indz) + 1] 1272 | sys.stdout.write('Done.\n') 1273 | self.X_min, self.X_max = np.amin(self.X), np.amax(self.X) 1274 | self.Y_min, self.Y_max = np.amin(self.Y), np.amax(self.Y) 1275 | self.Z_min, self.Z_max = np.amin(self.Z), np.amax(self.Z) 1276 | self.Y_points, self.X_points, self.Z_points = self.X.shape 1277 | # Print model info. 1278 | print('Model extent:') 1279 | print('X range: %.2fm-%.2fm' % (self.X_min, self.X_max)) 1280 | print('Y range: %.2fm-%.2fm' % (self.Y_min, self.Y_max)) 1281 | print('Z range: %.2fm-%.2fm' % (self.Z_min, self.Z_max)) 1282 | print('Model resolution (XYZ): [%.2fm x %.2fm x %.2fm]' % 1283 | (self.X_resolution, self.Y_resolution, self.Z_resolution)) 1284 | print('Model points (XYZ): [%d x %d x %d]' % (self.X_points, self.Y_points, self.Z_points)) 1285 | 1286 | def show(self, plotter=None, param=None, zscale=None, cmap=None, slices=False, point_cloud=False, 1287 | hide_value=None): 1288 | """ 1289 | Visualize the model. 1290 | :param plotter: (pyvista.Plotter) - Default is None, which is to create a new plotter. Can also accept a plotter 1291 | from outside. 1292 | :param param: (String) - Choose a parameter to visualize. 1293 | Options are 'rc', 'vp', 'vs', 'density', 'channel_facies', 'lith_facies' and 'fm'. 1294 | :param zscale: (Float or string) - Scaling in the z direction. Default is not to change the existing scaling. 1295 | If 'auto', will scale the min(x range, y range) / z range to the Golden Ratio. 1296 | :param cmap: (String) - Colormap used to visualize the model. 1297 | :param slices: (Bool) - Whether to display the model as orthogonal slices. Default is False. 1298 | :param point_cloud: (Bool) - Whether to display the model as point cloud. Default is False. 1299 | :param hide_value: (List) - List of values that will be hidden when visualizing the model as point cloud. 1300 | For example, hide_value=[0, 2, 3] means not to display points with value=0, 2 or 3. 1301 | Only be effective when point_cloud=True. 1302 | :return: plotter: (pyvista.Plotter) - A pyvista plotter with the visualized model. 1303 | """ 1304 | if plotter is None: 1305 | pv.set_plot_theme('document') 1306 | plotter = BackgroundPlotter() 1307 | if zscale is not None: 1308 | if zscale == 'auto': 1309 | zscale = min(self.X_max - self.X_min, self.Y_max - self.Y_min) / (self.Z_max - self.Z_min) * 0.618 1310 | plotter.set_scale(zscale=zscale) 1311 | if cmap is None: 1312 | cmap = 'viridis' 1313 | points = np.c_[self.X.ravel(order='F'), self.Y.ravel(order='F'), self.Z.ravel(order='F')] 1314 | grid = pv.StructuredGrid(self.X, self.Y, self.Z) 1315 | if param == 'rc': 1316 | values = self.rc.ravel(order='F') 1317 | scalar_bar_title = 'RC' 1318 | title = 'Reflection Coefficient Model' 1319 | if param == 'vp': 1320 | values = self.vp.ravel(order='F') 1321 | scalar_bar_title = 'Vp(km/s)' 1322 | title = 'P-wave Velocity Model' 1323 | if param == 'vs': 1324 | values = self.vs.ravel(order='F') 1325 | scalar_bar_title = 'Vs(km/s)' 1326 | title = 'S-wave Velocity Model' 1327 | if param == 'density': 1328 | values = self.density.ravel(order='F') 1329 | scalar_bar_title = 'Density(g/cm^3)' 1330 | title = 'Density Model' 1331 | if param == 'Ip': 1332 | values = self.Ip.ravel(order='F') 1333 | scalar_bar_title = 'Ip' 1334 | title = 'P-wave Impedance' 1335 | if param == 'Is': 1336 | values = self.Is.ravel(order='F') 1337 | scalar_bar_title = 'Is' 1338 | title = 'S-wave Impedance' 1339 | if param == 'seis': 1340 | values = self.seis.ravel(order='F') 1341 | scalar_bar_title = 'amp' 1342 | title = 'Seismic' 1343 | if param == 'channel_facies': 1344 | values = self.channel_facies.ravel(order='F') 1345 | scalar_bar_title = 'Channel Facies Code' 1346 | title = 'Channel Facies Model' 1347 | if param == 'lith_facies': 1348 | values = self.lith_facies.ravel(order='F') 1349 | scalar_bar_title = 'Lithology Facies Code' 1350 | title = 'Lithology Facies Model' 1351 | if param == 'fm': 1352 | values = self.fm.ravel(order='F') 1353 | scalar_bar_title = 'Fault Probability' 1354 | title = 'Fault Probability Model' 1355 | sargs = dict(height=0.5, vertical=True, position_x=0.85, position_y=0.05, 1356 | title=scalar_bar_title, title_font_size=20) 1357 | if point_cloud: 1358 | if hide_value is not None: 1359 | for v in hide_value: 1360 | points = points[values != v, :] 1361 | values = values[values != v] 1362 | pc = pv.PolyData(points) 1363 | pc[param] = values 1364 | plotter.add_mesh(pc, render_points_as_spheres=True, scalars=param, show_scalar_bar=True, cmap=cmap, 1365 | scalar_bar_args=sargs) 1366 | else: 1367 | grid[param] = values 1368 | if slices: 1369 | plotter.add_mesh_slice_orthogonal(grid, scalars=param, show_scalar_bar=True, cmap=cmap, 1370 | scalar_bar_args=sargs) 1371 | else: 1372 | plotter.add_mesh(grid, scalars=param, show_scalar_bar=True, cmap=cmap, scalar_bar_args=sargs) 1373 | plotter.add_mesh(grid.outline(), color='k') 1374 | plotter.add_text(title, font_size=15) 1375 | plotter.show_bounds() 1376 | plotter.add_axes() 1377 | return plotter 1378 | 1379 | 1380 | def compute_f_parallel(i, x=None, y=None, interpolator=None): 1381 | """ 1382 | Compute the curved fault surface's z coordinates using the bi-harmonic spline interpolation in parallel . 1383 | :param i: (Integer) - The slice index number (axis=0) of the model's x and y coordinate arrays in fault plane 1384 | coordinate system. 1385 | :param x: (numpy.3darray) - The model's x coordinate array in fault plane coordinate system. 1386 | :param y: (numpy.3darray) - The model's y coordinate array in fault plane coordinate system. 1387 | :param interpolator: (class BiharmonicSpline3D) - The bi-harmonic spline interpolator initialized by 1388 | random perturbation points near the planar fault plane. 1389 | :return: (List of numpy.2darrays) - A slice (axis=0) of curved fault surface's z coordinates in fault plane 1390 | coordinate system. 1391 | """ 1392 | out = interpolator(x[i, :, :], y[i, :, :]) 1393 | return out 1394 | 1395 | 1396 | def compute_Dz_parallel(i, x=None, y=None, f=None, interpolator=None): 1397 | """ 1398 | Compute the model's displacement in the fault surface's normal direction. 1399 | :param i: (Integer) - The slice index number (axis=0) of the model's x and y coordinate arrays in fault plane 1400 | coordinate system. 1401 | :param x: (numpy.3darray) - The model's x coordinate array in fault plane coordinate system. 1402 | :param y: (numpy.3darray) - The model's y coordinate array in fault plane coordinate system. 1403 | :param f: (numpy.3darray) - The fault surface's z-coordinate array in fault plane coordinate system. 1404 | :param interpolator: (class BiharmonicSpline3D) - The bi-harmonic spline interpolator initialized by 1405 | random perturbation points near the planar fault plane. 1406 | :return: (List of numpy.2darrays) - A slice (axis=0) of the model's displacement in the fault plane's 1407 | normal direction. 1408 | """ 1409 | out = interpolator(x[i, :, :], y[i, :, :]) - f[i, :, :] 1410 | return out 1411 | 1412 | 1413 | class Channel: 1414 | """ 1415 | Store the river center-line coordinates, river width and maximum depth. 1416 | """ 1417 | 1418 | def __init__(self, x, y, z, W, D): 1419 | """ 1420 | :param x: (numpy.1darray) - x-coordinates of center-line. 1421 | :param y: (numpy.1darray) - y-coordinates of center-line. 1422 | :param z: (numpy.1darray) - z-coordinates of center-line. 1423 | :param W: (Float) - River width. 1424 | :param D: (Float) - River maximum depth. 1425 | """ 1426 | self.x = x 1427 | self.y = y 1428 | self.z = z 1429 | self.W = W 1430 | self.D = D 1431 | 1432 | 1433 | class Oxbow: 1434 | """ 1435 | Store the oxbow-lake center-line coordinates, oxbow-lake width and maximum depth. 1436 | """ 1437 | 1438 | def __init__(self, xc, yc, zc, W, D): 1439 | """ 1440 | :param xc: (numpy.1darray) - x-coordinates of oxbow center-line. 1441 | :param yc: (numpy.1darray) - y-coordinates of oxbow center-line. 1442 | :param zc: (numpy.1darray) - z-coordinates of oxbow center-line. 1443 | :param W: (Float) - Oxbow-lake width. 1444 | :param D: (Float) - Oxbow-lake maximum depth. 1445 | """ 1446 | self.x = xc 1447 | self.y = yc 1448 | self.z = zc 1449 | self.W = W 1450 | self.D = D 1451 | 1452 | 1453 | def initialize_centerline(s_init, ypos, zpos, delta_s, n_bends, perturb): 1454 | """ 1455 | Initialize river center-line. Assuming x is the longitudinal flow direction. First create a straight river 1456 | center-line, then add perturbation to make it bended. 1457 | :param s_init: (Float) - Length of the straight center-line. 1458 | :param ypos: (Float) - y position of the center-line. 1459 | :param zpos: (Float) - z position of the center-line. 1460 | :param delta_s: (Float) - Distance between two consecutive points along center-line. 1461 | :param n_bends: (Integer) - Number of bends in the center-line. 1462 | :param perturb: (List) - y-coordinates' range of perturbation points. 1463 | :return: x: (numpy.1darray) - x-coordinates of the initial center-line. 1464 | y: (numpy.1darray) - y-coordinates of the initial center-line. 1465 | z: (numpy.1darray) - z-coordinates of the initial center-line. 1466 | """ 1467 | x = np.arange(0, s_init + delta_s, delta_s, dtype='float32') 1468 | # Generate perturbation points. 1469 | xp = np.linspace(0, s_init, n_bends + 2, dtype='float32') 1470 | yp = np.ones(len(xp), dtype='float32') * ypos 1471 | for i in range(1, len(yp) - 1): 1472 | ptb = random.uniform(perturb[0], perturb[1]) 1473 | yp[i] += (-1) ** i * ptb 1474 | # Interpolate bended center-line. 1475 | interpolator = BiharmonicSpline2D(xp, yp) 1476 | y = interpolator(x) 1477 | z = np.ones(len(x), dtype='float32') * zpos 1478 | return x, y, z 1479 | 1480 | 1481 | def resample_centerline(x, y, z, delta_s): 1482 | """ 1483 | Re-sample center-line so that delta_s is roughly constant. Modified from Zoltan Sylvester's meanderpy. 1484 | [https://github.com/zsylvester/meanderpy] 1485 | :param x: (numpy.1darray) - x-coordinates of center-line. 1486 | :param y: (numpy.1darray) - y-coordinates of center-line. 1487 | :param z: (numpy.1darray) - z-coordinates of center-line. 1488 | :param delta_s: (Float) - Distance between two consecutive points along center-line. 1489 | :return: 1490 | """ 1491 | dx, dy, s, ds = compute_curvelength(x, y) 1492 | # Cubic spline interpolation. s=0 means no smoothing. 1493 | tck = scipy.interpolate.splprep([x, y, z], s=0) 1494 | unew = np.linspace(0, 1, 1 + int(round(s / delta_s))) 1495 | out = scipy.interpolate.splev(unew, tck[0]) 1496 | x_res, y_res, z_res = out[0], out[1], out[2] 1497 | x_res, y_res, z_res = x_res.astype('float32'), y_res.astype('float32'), z_res.astype('float32') 1498 | return x_res, y_res, z_res 1499 | 1500 | 1501 | def compute_curvelength(x, y): 1502 | """ 1503 | Compute the length of center-line. Modified from Zoltan Sylvester's meanderpy. 1504 | [https://github.com/zsylvester/meanderpy] 1505 | :param x: (numpy.1darray) - x-coordinates of center-line. 1506 | :param y: (numpy.1darray) - y-coordinates of center-line. 1507 | :return: dx: (numpy.1darray) - First derivative of each point's x-coordinates on center-line. 1508 | dy: (numpy.1darray) - First derivative of each point's y-coordinates on center-line. 1509 | s: (Float) - The length of center-line. 1510 | ds: (numpy.1darray) - The length of curve between two consecutive points along the center-line. 1511 | """ 1512 | dx = np.gradient(x) 1513 | dy = np.gradient(y) 1514 | ds = np.sqrt(dx ** 2 + dy ** 2) 1515 | s = np.sum(ds[1:]) 1516 | return dx, dy, s, ds 1517 | 1518 | 1519 | def compute_curvature(x, y): 1520 | """ 1521 | Compute the curvatures at each points of center-line. Modified from Zoltan Sylvester's meanderpy. 1522 | [https://github.com/zsylvester/meanderpy] 1523 | :param x: (numpy.1darray) - x-coordinates of center-line. 1524 | :param y: (numpy.1darray) - y-coordinates of center-line. 1525 | :return: curvature: (numpy.1darray) - Curvatures at each points of center-line. 1526 | """ 1527 | # First derivatives. 1528 | dx = np.gradient(x) 1529 | dy = np.gradient(y) 1530 | # Second derivatives. 1531 | ddx = np.gradient(dx) 1532 | ddy = np.gradient(dy) 1533 | curvature = (dx * ddy - dy * ddx) / ((dx ** 2 + dy ** 2) ** 1.5) 1534 | return curvature 1535 | 1536 | 1537 | @numba.jit(nopython=True) # Use numba to speed up the computation. 1538 | def compute_tangential_angle(x, y): 1539 | """ 1540 | Compute tangential angle at each point of center-line. 1541 | :param x: (numpy.1darray) - x-coordinates of center-line. 1542 | :param y: (numpy.1darray) - y-coordinates of center-line. 1543 | :return: beta: (numpy.1darray) - Tangential angle (radian) of each point. 1544 | """ 1545 | beta = np.zeros(len(x), dtype='float32') # Initialization. 1546 | for i in range(len(x)): 1547 | # The first point. 1548 | if i == 0: 1549 | if x[i + 1] == x[i]: # Avoid division by zero. 1550 | beta[i] = math.atan((y[i + 1] - y[i]) / 1e-6) 1551 | else: 1552 | beta[i] = math.atan((y[i + 1] - y[i]) / (x[i + 1] - x[i])) 1553 | # The arc-tangent function can only return [-90? 90i, which means the angle in first quadrant is the same 1554 | # as the angle in third quadrant, and the angle in second quadrant is the same as the angle in fourth 1555 | # quadrant. The angles are in [-180? 180i through the process below. 1556 | if y[i + 1] > y[i] and x[i + 1] < x[i]: 1557 | beta[i] += math.pi 1558 | if y[i + 1] < y[i] and x[i + 1] < x[i]: 1559 | beta[i] -= math.pi 1560 | # The end point. 1561 | elif i == len(x) - 1: 1562 | if x[i] == x[i - 1]: # Avoid division by zero. 1563 | beta[i] = math.atan((y[i] - y[i - 1]) / 1e-6) 1564 | else: 1565 | beta[i] = math.atan((y[i] - y[i - 1]) / (x[i] - x[i - 1])) 1566 | # Angle transform. 1567 | if y[i] > y[i - 1] and x[i] < x[i - 1]: 1568 | beta[i] += math.pi 1569 | if y[i] < y[i - 1] and x[i] < x[i - 1]: 1570 | beta[i] -= math.pi 1571 | # The interval points. Use three points (backward and forward) to compute the tangential angle. 1572 | else: 1573 | if x[i + 1] == x[i]: # Avoid division by zero. 1574 | beta_forward = math.atan((y[i + 1] - y[i]) / 1e-6) 1575 | else: 1576 | beta_forward = math.atan((y[i + 1] - y[i]) / (x[i + 1] - x[i])) 1577 | if x[i] == x[i - 1]: # Avoid division by zero. 1578 | beta_backward = math.atan((y[i] - y[i - 1]) / 1e-6) 1579 | else: 1580 | beta_backward = math.atan((y[i] - y[i - 1]) / (x[i] - x[i - 1])) 1581 | # Angle transform. 1582 | if y[i + 1] > y[i] and x[i + 1] < x[i]: 1583 | beta_forward += math.pi 1584 | if y[i + 1] < y[i] and x[i + 1] < x[i]: 1585 | beta_forward -= math.pi 1586 | if y[i] > y[i - 1] and x[i] < x[i - 1]: 1587 | beta_backward += math.pi 1588 | if y[i] < y[i - 1] and x[i] < x[i - 1]: 1589 | beta_backward -= math.pi 1590 | beta[i] = 0.5 * (beta_forward + beta_backward) 1591 | # This is the situation that the flow direction is opposite to the x-direction AND the middle point is 1592 | # higher or lower than both forward point and backward point. 1593 | if x[i + 1] < x[i - 1] and \ 1594 | ((y[i] >= y[i + 1] and y[i] >= y[i - 1]) or (y[i] <= y[i - 1] and y[i] <= y[i + 1])): 1595 | if beta[i] >= 0.0: 1596 | beta[i] -= math.pi 1597 | else: 1598 | beta[i] += math.pi 1599 | return beta 1600 | 1601 | 1602 | @numba.jit(nopython=True) # Use numba to speed up the computation. 1603 | def compute_migration_rate(curv, ds, W, kl, Cf, D, pad_up, pad_down): 1604 | """ 1605 | Compute migration rate of Howard-Knutson (1984) model. Modified from Zoltan Sylvester's meanderpy. 1606 | [https://github.com/zsylvester/meanderpy] 1607 | :param curv: (numpy.1darray) - Curvature of each point on center-line. 1608 | :param ds: (numpy.1darray) - Distances between two consecutive points on center-line. 1609 | :param W: (Float) - River's width. 1610 | :param kl: (Float) - Migration constant (m/year). 1611 | :param Cf: (Float) - Friction factor. 1612 | :param D: (Float) - River's depth. 1613 | :param pad_up: (Integer) - Number of points that will not migrate at upstream. 1614 | :param pad_down: (Integer) - Number of points that will not migrate at downstream. 1615 | :return: R1: (numpy.1darray) - The migration rate. 1616 | """ 1617 | omega = -1.0 1618 | gamma = 2.5 1619 | k = 1.0 1620 | R0 = kl * W * curv # Nominal migration rate. 1621 | R1 = np.zeros(len(R0), dtype='float32') # Initialize adjusted migration rate. 1622 | alpha = 2 * k * Cf / D 1623 | if pad_up < 5: 1624 | pad_up = 5 1625 | for i in range(pad_up, len(R0) - pad_down): 1626 | si = np.concatenate( 1627 | (np.array([0]), np.cumsum(ds[i - 1::-1]))) # Cumulate distances backward from current point. 1628 | G = np.exp(-alpha * si) 1629 | # Adjusted migration rate in Howard-Knutson model. 1630 | R1[i] = omega * R0[i] + gamma * np.sum(R0[i::-1] * G) / np.sum(G) 1631 | return R1 1632 | 1633 | 1634 | def channel_bank(x, y, W): 1635 | """ 1636 | Compute river banks' coordinates. 1637 | :param x: (numpy.1darray) - x-coordinates of the center-line. 1638 | :param y: (numpy.1darray) - y-coordinates of the center-line. 1639 | :param W: (Float) - The channel's width. 1640 | :return: xb: (numpy.2darray) - The x-coordinates of river banks. Shape: [len(x), 2]. 1641 | Each row is the x-coordinates of two banks of a point in center-line. 1642 | yb: (numpy.2darray) - The x-coordinates of river banks. Shape: [len(x), 2]. 1643 | Each row is the y-coordinates of two banks of a point in center-line. 1644 | """ 1645 | ns = len(x) 1646 | angle = compute_tangential_angle(x, y) 1647 | # Get the parabolas' endpoints' y-coordinates of each points on center-line. 1648 | # Note that this is not the bank's y-coordinates until they are rotated. 1649 | xb = np.c_[x, x] 1650 | yb = np.c_[y - W / 2, y + W / 2] 1651 | # Compute the parabola. 1652 | for i in range(ns): 1653 | R = np.array([[math.cos(angle[i]), -math.sin(angle[i])], # Rotation matrix 1654 | [math.sin(angle[i]), math.cos(angle[i])]]) 1655 | [xb[i, :], yb[i, :]] = R @ [xb[i, :] - x[i], yb[i, :] - y[i]] # Rotate to normal direction. 1656 | xb[i, :] += x[i] # x-coordinates of the erosion surface. 1657 | yb[i, :] += y[i] # y-coordinates of the erosion surface. 1658 | return xb, yb 1659 | 1660 | 1661 | def compute_centerline_distance(x, y, xpos, xmin, ymin, dx, nx, ny): 1662 | """ 1663 | Rasterize center-line and compute distance to center-line on X-Y plane. Modified from Zoltan Sylvester's meanderpy. 1664 | [https://github.com/zsylvester/meanderpy] 1665 | :param x: (numpy.1darray) - x-coordinates of center-line. 1666 | :param y: (numpy.1darray) - y-coordinates of center-line. 1667 | :param xpos: (Float) - Center-line's x-coordinate which the 3D model starts at. 1668 | :param xmin: (Float) - Minimum x-coordinates of the model. 1669 | :param ymin: (Float) - Minimum y-coordinates of the model. 1670 | :param dx: (Float) - X & Y resolution of the model. 1671 | :param nx: (Integer) - Number of points on model's x-direction. 1672 | :param ny: (Integer) - Number of points on model's y-direction. 1673 | :return: dist: (numpy.2darray) - Distance to center-line on X-Y plane. 1674 | """ 1675 | ctl_pixels = [] 1676 | offset = xpos - xmin 1677 | for i in range(len(x)): 1678 | px = int((x[i] - offset - xmin) / dx) 1679 | py = int((y[i] - ymin) / dx) 1680 | if 0 <= px < nx and 0 <= py < ny: 1681 | ctl_pixels.append((py, px)) 1682 | # Rasterize center-line. 1683 | img = Image.new(mode='RGB', size=(ny, nx), color='white') # Background is white. 1684 | draw = ImageDraw.Draw(img) 1685 | draw.line(ctl_pixels, fill='rgb(0, 0, 0)') # Center-line is black. 1686 | # Transfer image to array. 1687 | pix = np.array(img) 1688 | ctl = pix[:, :, 0] 1689 | ctl[ctl == 255] = 1 # Background is 1, center-line is 0. 1690 | # Compute distance to center-line. 1691 | dist_map = ndimage.distance_transform_edt(ctl) 1692 | dist = dist_map * dx # The real distance. 1693 | dist.astype('float32') 1694 | return dist 1695 | 1696 | 1697 | def erosion_surface(cl_dist, z, W, D): 1698 | """ 1699 | Create erosion surface. 1700 | :param cl_dist: (numpy.2darray) - Distance from center-line on X-Y plane. 1701 | :param z: (numpy.1darray) - z-coordinates of center-line. 1702 | :param W: (Float) - River's width. 1703 | :param D: (Float) - River's maximum depth. 1704 | :return: ze: (numpy.2darray) - z-coordinates of erosion surface. 1705 | """ 1706 | if len(z[z - z[0] != 0]): 1707 | raise ValueError('Can not process center-line with changing z-coordinates.') 1708 | ze = z[0] + 4 * D / W ** 2 * (W ** 2 / 4 - cl_dist ** 2) 1709 | ze = ze.astype('float32') 1710 | return ze 1711 | 1712 | 1713 | def lag_surface(cl_dist, z, h_lag, D): 1714 | """ 1715 | Create Riverbed lag deposit surface. 1716 | :param cl_dist: (numpy.2darray) - Distance from center-line on X-Y plane. 1717 | :param z: (numpy.1darray) - z-coordinates of center-line. 1718 | :param h_lag: (Float) - The maximum thickness of lag deposit. 1719 | :param D: (Float) - River's maximum depth. 1720 | :return: zl: (numpy.2darray) - z-coordinates of lag deposit surface. 1721 | """ 1722 | if len(z[z - z[0] != 0]): 1723 | raise ValueError('Can not process center-line with changing z-coordinates.') 1724 | zl = (z[0] + D - h_lag) * np.ones(shape=cl_dist.shape) 1725 | zl = zl.astype('float32') 1726 | return zl 1727 | 1728 | 1729 | def pointbar_surface(cl_dist, z, W, D): 1730 | """ 1731 | Create Riverbed point-bar surface. 1732 | :param cl_dist: (numpy.2darray) - Distance from center-line on X-Y plane. 1733 | :param z: (numpy.1darray) - z-coordinates of center-line. 1734 | :param W: (Float) - River's width. 1735 | :param D: (Float) - River's depth. 1736 | :return: zpb: (numpy.2darray) - z-coordinates of point-bar surface. 1737 | """ 1738 | if len(z[z - z[0] != 0]): 1739 | raise ValueError('Can not process center-line with changing z-coordinates.') 1740 | zpb = z[0] + D * np.exp(-(cl_dist ** 2) / (2 * (W / 4) ** 2)) 1741 | zpb = zpb.astype('float32') 1742 | return zpb 1743 | 1744 | 1745 | def levee_surface(cl_dist, h_levee, w_levee, W, tp): 1746 | """ 1747 | Create natural levee surface. 1748 | :param cl_dist: (numpy.2darray) - Distance from center-line on X-Y plane. 1749 | :param h_levee: (Float) - The Maximum thickness of levee. 1750 | :param w_levee: (Float) - The width of levee. 1751 | :param W: (Float) - River's width. 1752 | :param tp: (numpy.2darray) - Topography. 1753 | :return: zlv: (numpy.2darray) - z-coordinates of levee surface. 1754 | """ 1755 | th1 = -2 * h_levee / w_levee * (cl_dist - W / 2 - w_levee / 2) 1756 | th2 = np.ones(shape=cl_dist.shape) * h_levee 1757 | th1, th2 = th1.astype('float32'), th2.astype('float32') 1758 | th_levee = np.minimum(th1, th2) 1759 | th_levee[th_levee < 0] = 0 1760 | zlv = tp - th_levee 1761 | return zlv 1762 | 1763 | 1764 | def kth_diag_indices(a, k): 1765 | """ 1766 | Function for finding diagonal indices with k offset. 1767 | [From https://stackoverflow.com/questions/10925671/numpy-k-th-diagonal-indices] 1768 | :param a: (numpy.2darray) - The input array. 1769 | :param k: (Integer) - The offset. For example, k=1 represents the diagonal elements 1 step below the main diagonal, 1770 | k=-1 represents the diagonal elements 1 step above the main diagonal. 1771 | :return: rows: (numpy.1darray) - The row indices. 1772 | col: (numpy.1darray) - The column indices. 1773 | """ 1774 | rows, cols = np.diag_indices_from(a) 1775 | if k < 0: 1776 | return rows[:k], cols[-k:] 1777 | elif k > 0: 1778 | return rows[k:], cols[:-k] 1779 | else: 1780 | return rows, cols 1781 | 1782 | 1783 | def find_neck(x, y, delta_s, critical_dist, n_buffer=20): 1784 | """ 1785 | Find the location of neck cutoff. Modified from Zoltan Sylvester's meanderpy. 1786 | [https://github.com/zsylvester/meanderpy] 1787 | :param x: (numpy.1darray) - x-coordinates of center-line. 1788 | :param y: (numpy.1darray) - y-coordinates of center-line. 1789 | :param critical_dist: (Float) - The critical distance. Cutoff occurs when distance of two points on center-line is 1790 | shorter than (or equal to) the critical distance. 1791 | :param delta_s: (Float) - Distance between two consecutive points on center-line. 1792 | :param n_buffer: (Integer) - Number of buffer points, preventing that cutoff occurs where there's no bend. 1793 | :return: ind1: (numpy.1darray) - Indexes of center-line coordinates array where the cutoffs start. 1794 | ind2: (numpy.1darray) - Indexes of center-line coordinates array where the cutoffs end. 1795 | """ 1796 | # Number of neighbors that will be ignored for neck search. 1797 | n_ignore = int((critical_dist + n_buffer * delta_s) / delta_s) 1798 | # Compute Euclidean distance between each pair of points on center-line. 1799 | dist = distance.cdist(np.array([x, y], dtype='float32').T, np.array([x, y], dtype='float32').T, metric='euclidean') 1800 | # Set distances greater than critical distance to NAN. 1801 | dist[dist > critical_dist] = np.NAN 1802 | # Set ignored neighbors' distance to NAN. 1803 | for i in range(-n_ignore, n_ignore + 1): 1804 | rows, cols = kth_diag_indices(dist, i) 1805 | dist[rows, cols] = np.NAN 1806 | # Find where the distance is lower than critical distance. 1807 | r, c = np.where(~np.isnan(dist)) 1808 | # Take only the points measured downstream. 1809 | ind1 = r[np.where(r < c)[0]] 1810 | ind2 = c[np.where(r < c)[0]] 1811 | return ind1, ind2 1812 | 1813 | 1814 | def execute_cutoff(x, y, z, delta_s, critical_dist): 1815 | """ 1816 | Execute cutoff on center-line. Modified from Zoltan Sylvester's meanderpy. 1817 | [https://github.com/zsylvester/meanderpy] 1818 | :param x: (numpy.1darray) - x-coordinates of center-line. 1819 | :param y: (numpy.1darray) - y-coordinates of center-line. 1820 | :param z: (numpy.1darray) - z-coordinates of center-line. 1821 | :param delta_s: (Float) - Distance between two consecutive points on center-line. 1822 | :param critical_dist: (Float) - The critical distance. Cutoff occurs when distance of two points on center-line is 1823 | shorter than (or equal to) the critical distance. 1824 | :return: xc: (List) - x-coordinates of cutoffs. 1825 | yc: (List) - y-coordinates of cutoffs. 1826 | zc: (List) - z-coordinates of cutoffs. 1827 | x: (numpy.1darray) - x-coordinates of center-line after cutoff. 1828 | y: (numpy.1darray) - y-coordinates of center-line after cutoff. 1829 | z: (numpy.1darray) - z-coordinates of center-line after cutoff. 1830 | """ 1831 | xc = [] 1832 | yc = [] 1833 | zc = [] 1834 | ind1, ind2 = find_neck(x, y, delta_s, critical_dist) 1835 | while len(ind1) > 0: 1836 | xc.append(x[ind1[0]:ind2[0] + 1]) # x-coordinates of cutoffs. 1837 | yc.append(y[ind1[0]:ind2[0] + 1]) # y-coordinates of cutoffs. 1838 | zc.append(z[ind1[0]:ind2[0] + 1]) # z-coordinates of cutoffs. 1839 | x = np.concatenate((x[:ind1[0] + 1], x[ind2[0]:])) # x-coordinates of center-line after cutoff. 1840 | y = np.concatenate((y[:ind1[0] + 1], y[ind2[0]:])) # y-coordinates of center-line after cutoff. 1841 | z = np.concatenate((z[:ind1[0] + 1], z[ind2[0]:])) # z-coordinates of center-line after cutoff. 1842 | ind1, ind2 = find_neck(x, y, delta_s, critical_dist) 1843 | return xc, yc, zc, x, y, z 1844 | 1845 | 1846 | def plot_channel2D(channel_obj, oxbow_obj, title=None, interval=10): 1847 | """ 1848 | Plot channel's migration on X-Y plane. 1849 | :param channel_obj: (List) - The channel objects. 1850 | :param oxbow_obj: (List) - The oxbow-lake objects. 1851 | :param title: (String) - The title of the figure. 1852 | :param interval: (Integer) - Plot channel for every "interval" channels. 1853 | """ 1854 | # Set figure parameters. 1855 | plt.figure(figsize=(16, 9)) 1856 | plt.xlabel('X(m)', fontsize=15) 1857 | plt.ylabel('Y(m)', fontsize=15) 1858 | plt.axis('equal') 1859 | if title is None: 1860 | title = 'Meandering River Migration' 1861 | plt.title(title, fontsize=20) 1862 | plt.tick_params(labelsize=15) 1863 | for i in range(0, len(channel_obj), interval): 1864 | x, y = channel_obj[i].x, channel_obj[i].y # Get center-line coordinates. 1865 | W = channel_obj[i].W # Get river's width. 1866 | xb, yb = channel_bank(x, y, W) # Compute bank coordinates. 1867 | # Make the banks a closed curve. 1868 | xb = np.hstack((xb[:, 0], xb[:, 1][::-1])) 1869 | yb = np.hstack((yb[:, 0], yb[:, 1][::-1])) 1870 | if i == 0: 1871 | plt.fill(xb, yb, facecolor='grey', edgecolor='black', alpha=1.0) 1872 | elif i + interval >= len(channel_obj): 1873 | plt.fill(xb, yb, facecolor='blue', edgecolor='black', alpha=1.0) 1874 | # plt.plot(x, y, 'ko--', linewidth=2, markersize=10) 1875 | else: 1876 | plt.fill(xb, yb, facecolor='yellow', edgecolor='black', alpha=0.5) 1877 | # If there are oxbow-lakes, plot oxbow-lakes. 1878 | for i in range(len(oxbow_obj)): 1879 | # Get oxbow-lake center-line coordinates. 1880 | # Note that different oxbow-lake center-line coordinates are stored in different arrays. 1881 | xo, yo = oxbow_obj[i].x, oxbow_obj[i].y # xc: List[array0, array1, ...] 1882 | W = oxbow_obj[i].W # Get oxbow-lake width. 1883 | if len(xo) > 0: 1884 | n_oxbow = len(xo) # Number of oxbow-lakes. 1885 | for j in range(n_oxbow): 1886 | xbo, ybo = channel_bank(xo[j], yo[j], W) # Compute bank coordinates of oxbow-lakes. 1887 | # Make the banks a closed curve. 1888 | xbo = np.hstack((xbo[:, 0], xbo[:, 1][::-1])) 1889 | ybo = np.hstack((ybo[:, 0], ybo[:, 1][::-1])) 1890 | plt.fill(xbo, ybo, facecolor='deepskyblue', edgecolor='black', alpha=1.0) 1891 | plt.show() 1892 | --------------------------------------------------------------------------------