├── .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 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------