datagenerator.meanderpy

   1import numpy as np
   2import matplotlib.pyplot as plt
   3import scipy.interpolate
   4from scipy.spatial import distance
   5from scipy import ndimage
   6from PIL import Image, ImageDraw
   7from skimage import measure
   8from skimage import morphology
   9
  10# from matplotlib.colors import LinearSegmentedColormap
  11# import time, sys
  12import numba
  13import matplotlib.colors as mcolors
  14from matplotlib import cm
  15from tqdm import trange
  16
  17
  18class Channel:
  19    """class for Channel objects"""
  20
  21    def __init__(self, x, y, z, W, D):
  22        """Initialize Channel object
  23
  24        :param x: x-coordinate of centerline
  25        :param y: y-coordinate of centerline
  26        :param z: z-coordinate of centerline
  27        :param W: channel width
  28        :param D: channel depth"""
  29
  30        self.x = x
  31        self.y = y
  32        self.z = z
  33        self.W = W
  34        self.D = D
  35
  36
  37class Cutoff:
  38    """class for Cutoff objects"""
  39
  40    def __init__(self, x, y, z, W, D):
  41        """Initialize Cutoff object
  42
  43        :param x: x-coordinate of centerline
  44        :param y: y-coordinate of centerline
  45        :param z: z-coordinate of centerline
  46        :param W: channel width
  47        :param D: channel depth"""
  48
  49        self.x = x
  50        self.y = y
  51        self.z = z
  52        self.W = W
  53        self.D = D
  54
  55
  56class ChannelBelt3D:
  57    """class for 3D models of channel belts"""
  58
  59    def __init__(self, model_type, topo, strat, facies, facies_code, dx, channels):
  60        """initialize ChannelBelt3D object
  61
  62        :param model_type: type of model to be built; can be either 'fluvial' or 'submarine'
  63        :param topo: set of topographic surfaces (3D numpy array)
  64        :param strat: set of stratigraphic surfaces (3D numpy array)
  65        :param facies: facies volume (3D numpy array)
  66        :param facies_code: dictionary of facies codes, e.g. {0:'oxbow', 1:'point bar', 2:'levee'}
  67        :param dx: gridcell size (m)
  68        :param channels: list of channel objects that form 3D model"""
  69
  70        self.model_type = model_type
  71        self.topo = topo
  72        self.strat = strat
  73        self.facies = facies
  74        self.facies_code = facies_code
  75        self.dx = dx
  76        self.channels = channels
  77
  78    def plot_xsection(self, xsec, colors, ve):
  79        """method for plotting a cross section through a 3D model; also plots map of
  80        basal erosional surface and map of final geomorphic surface
  81
  82        :param xsec: location of cross section along the x-axis (in pixel/ voxel coordinates)
  83        :param colors: list of RGB values that define the colors for different facies
  84        :param ve: vertical exaggeration
  85        :return: handles to the three figures"""
  86
  87        strat = self.strat
  88        dx = self.dx
  89        fig1 = plt.figure(figsize=(20, 5))
  90        ax1 = fig1.add_subplot(111)
  91        r, c, ts = np.shape(strat)
  92        Xv = dx * np.arange(0, r)
  93        for i in range(0, ts - 1, 3):
  94            X1 = np.concatenate((Xv, Xv[::-1]))
  95            Y1 = np.concatenate((strat[:, xsec, i], strat[::-1, xsec, i + 1]))
  96            Y2 = np.concatenate((strat[:, xsec, i + 1], strat[::-1, xsec, i + 2]))
  97            Y3 = np.concatenate((strat[:, xsec, i + 2], strat[::-1, xsec, i + 3]))
  98            if self.model_type == "submarine":
  99                ax1.fill(
 100                    X1, Y1, facecolor=colors[2], linewidth=0.5, edgecolor=[0, 0, 0]
 101                )  # oxbow mud
 102                ax1.fill(
 103                    X1, Y2, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
 104                )  # point bar sand
 105                ax1.fill(X1, Y3, facecolor=colors[1], linewidth=0.5)  # levee mud
 106            if self.model_type == "fluvial":
 107                ax1.fill(
 108                    X1, Y1, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
 109                )  # levee mud
 110                ax1.fill(
 111                    X1, Y2, facecolor=colors[1], linewidth=0.5, edgecolor=[0, 0, 0]
 112                )  # oxbow mud
 113                ax1.fill(X1, Y3, facecolor=colors[2], linewidth=0.5)  # channel sand
 114        ax1.set_xlim(0, dx * (r - 1))
 115        ax1.set_aspect(ve, adjustable="datalim")
 116        fig2 = plt.figure()
 117        ax2 = fig2.add_subplot(111)
 118        ax2.contourf(strat[:, :, ts - 1], 100, cmap="viridis")
 119        ax2.contour(
 120            strat[:, :, ts - 1],
 121            100,
 122            colors="k",
 123            linestyles="solid",
 124            linewidths=0.1,
 125            alpha=0.4,
 126        )
 127        ax2.plot([xsec, xsec], [0, r], "k", linewidth=2)
 128        ax2.axis([0, c, 0, r])
 129        ax2.set_aspect("equal", adjustable="box")
 130        ax2.set_title("final geomorphic surface")
 131        ax2.tick_params(
 132            bottom=False,
 133            top=False,
 134            left=False,
 135            right=False,
 136            labelbottom=False,
 137            labelleft=False,
 138        )
 139        fig3 = plt.figure()
 140        ax3 = fig3.add_subplot(111)
 141        ax3.contourf(strat[:, :, 0], 100, cmap="viridis")
 142        ax3.contour(
 143            strat[:, :, 0],
 144            100,
 145            colors="k",
 146            linestyles="solid",
 147            linewidths=0.1,
 148            alpha=0.4,
 149        )
 150        ax3.plot([xsec, xsec], [0, r], "k", linewidth=2)
 151        ax3.axis([0, c, 0, r])
 152        ax3.set_aspect("equal", adjustable="box")
 153        ax3.set_title("basal erosional surface")
 154        ax3.tick_params(
 155            bottom=False,
 156            top=False,
 157            left=False,
 158            right=False,
 159            labelbottom=False,
 160            labelleft=False,
 161        )
 162        return fig1, fig2, fig3
 163
 164
 165class ChannelBelt:
 166    """class for ChannelBelt objects"""
 167
 168    def __init__(self, channels, cutoffs, cl_times, cutoff_times):
 169        """initialize ChannelBelt object
 170
 171        :param channels: list of Channel objects
 172        :param cutoffs: list of Cutoff objects
 173        :param cl_times: list of ages of Channel objects (in years)
 174        :param cutoff_times: list of ages of Cutoff objects"""
 175
 176        self.channels = channels
 177        self.cutoffs = cutoffs
 178        self.cl_times = cl_times
 179        self.cutoff_times = cutoff_times
 180
 181    def migrate(
 182        self,
 183        nit,
 184        saved_ts,
 185        deltas,
 186        pad,
 187        crdist,
 188        Cf,
 189        kl,
 190        kv,
 191        dt,
 192        dens,
 193        t1,
 194        t2,
 195        t3,
 196        aggr_factor,
 197        *D
 198    ):
 199        """method for computing migration rates along channel centerlines and moving the centerlines accordingly
 200
 201        :param nit: number of iterations
 202        :param saved_ts: which time steps will be saved; e.g., if saved_ts = 10, every tenth time step will be saved
 203        :param deltas: distance between nodes on centerline
 204        :param pad: padding (number of nodepoints along centerline)
 205        :param crdist: threshold distance at which cutoffs occur
 206        :param Cf: dimensionless Chezy friction factor
 207        :param kl: migration rate constant (m/s)
 208        :param kv: vertical slope-dependent erosion rate constant (m/s)
 209        :param dt: time step (s)
 210        :param dens: density of fluid (kg/m3)
 211        :param t1: time step when incision starts
 212        :param t2: time step when lateral migration starts
 213        :param t3: time step when aggradation starts
 214        :param aggr_factor: aggradation factor
 215        :param D: channel depth (m)"""
 216
 217        channel = self.channels[
 218            -1
 219        ]  # first channel is the same as last channel of input
 220        x = channel.x
 221        y = channel.y
 222        z = channel.z
 223        W = channel.W
 224        if len(D) == 0:
 225            D = channel.D
 226        else:
 227            D = D[0]
 228        k = 1.0  # constant in HK equation
 229        xc = []  # initialize cutoff coordinates
 230        # determine age of last channel:
 231        if len(self.cl_times) > 0:
 232            last_cl_time = self.cl_times[-1]
 233        else:
 234            last_cl_time = 0
 235        dx, dy, dz, ds, s = compute_derivatives(x, y, z)
 236        slope = np.gradient(z) / ds
 237        # padding at the beginning can be shorter than padding at the downstream end:
 238        pad1 = int(pad / 10.0)
 239        if pad1 < 5:
 240            pad1 = 5
 241        omega = (
 242            -1.0
 243        )  # constant in migration rate calculation (Howard and Knutson, 1984)
 244        gamma = 2.5  # from Ikeda et al., 1981 and Howard and Knutson, 1984
 245        for itn in trange(nit):  # main loop
 246            # update_progress(itn/nit)
 247            x, y = migrate_one_step(
 248                x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma
 249            )
 250            # x, y = migrate_one_step_w_bias(x,y,z,W,kl,dt,k,Cf,D,pad,pad1,omega,gamma)
 251            x, y, z, xc, yc, zc = cut_off_cutoffs(
 252                x, y, z, s, crdist, deltas
 253            )  # find and execute cutoffs
 254            x, y, z, dx, dy, dz, ds, s = resample_centerline(
 255                x, y, z, deltas
 256            )  # resample centerline
 257            slope = np.gradient(z) / ds
 258            # for itn<=t1, z is unchanged; for itn>t1:
 259            if (itn > t1) & (itn <= t2):  # incision
 260                if np.min(np.abs(slope)) != 0:  # if slope is not zero
 261                    z = z + kv * dens * 9.81 * D * slope * dt
 262                else:
 263                    z = z - kv * dens * 9.81 * D * dt * 0.05  # if slope is zero
 264            if (itn > t2) & (itn <= t3):  # lateral migration
 265                if np.min(np.abs(slope)) != 0:  # if slope is not zero
 266                    # use the median slope to counterbalance incision:
 267                    z = (
 268                        z
 269                        + kv * dens * 9.81 * D * slope * dt
 270                        - kv * dens * 9.81 * D * np.median(slope) * dt
 271                    )
 272                else:
 273                    z = z  # no change in z
 274            if itn > t3:  # aggradation
 275                if np.min(np.abs(slope)) != 0:  # if slope is not zero
 276                    # 'aggr_factor' should be larger than 1 so that this leads to overall aggradation:
 277                    z = (
 278                        z
 279                        + kv * dens * 9.81 * D * slope * dt
 280                        - aggr_factor * kv * dens * 9.81 * D * np.mean(slope) * dt
 281                    )
 282                else:
 283                    z = z + aggr_factor * dt
 284            if len(xc) > 0:  # save cutoff data
 285                self.cutoff_times.append(
 286                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
 287                )
 288                cutoff = Cutoff(xc, yc, zc, W, D)  # create cutoff object
 289                self.cutoffs.append(cutoff)
 290            # saving centerlines:
 291            if np.mod(itn, saved_ts) == 0:
 292                self.cl_times.append(
 293                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
 294                )
 295                channel = Channel(x, y, z, W, D)  # create channel object
 296                self.channels.append(channel)
 297
 298    def plot(self, plot_type, pb_age, ob_age, end_time, n_channels):
 299        """method  for plotting ChannelBelt object
 300
 301        :param plot_type: can be either 'strat' (for stratigraphic plot), 'morph' (for morphologic plot), or 'age' (for age plot)
 302        :param pb_age: age of point bars (in years) at which they get covered by vegetation
 303        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation
 304        :param end_time: age of last channel to be plotted (in years)
 305        :param n_channels: total number of channels (used in 'age' plots; can be larger than number of channels being plotted)
 306        :return: handle to figure"""
 307
 308        cot = np.array(self.cutoff_times)
 309        sclt = np.array(self.cl_times)
 310        if end_time > 0:
 311            cot = cot[cot <= end_time]
 312            sclt = sclt[sclt <= end_time]
 313        times = np.sort(np.hstack((cot, sclt)))
 314        times = np.unique(times)
 315        order = 0  # variable for ordering objects in plot
 316        # set up min and max x and y coordinates of the plot:
 317        xmin = np.min(self.channels[0].x)
 318        xmax = np.max(self.channels[0].x)
 319        ymax = 0
 320        for i in range(len(self.channels)):
 321            ymax = max(ymax, np.max(np.abs(self.channels[i].y)))
 322        ymax = ymax + 2 * self.channels[0].W  # add a bit of space on top and bottom
 323        ymin = -1 * ymax
 324        # size figure so that its size matches the size of the model:
 325        fig = plt.figure(figsize=(20, (ymax - ymin) * 20 / (xmax - xmin)))
 326        if plot_type == "morph":
 327            pb_crit = len(times[times < times[-1] - pb_age]) / float(len(times))
 328            ob_crit = len(times[times < times[-1] - ob_age]) / float(len(times))
 329            green = (106 / 255.0, 159 / 255.0, 67 / 255.0)  # vegetation color
 330            pb_color = (189 / 255.0, 153 / 255.0, 148 / 255.0)  # point bar color
 331            ob_color = (15 / 255.0, 58 / 255.0, 65 / 255.0)  # oxbow color
 332            pb_cmap = make_colormap(
 333                [green, green, pb_crit, green, pb_color, 1.0, pb_color]
 334            )  # colormap for point bars
 335            ob_cmap = make_colormap(
 336                [green, green, ob_crit, green, ob_color, 1.0, ob_color]
 337            )  # colormap for oxbows
 338            plt.fill(
 339                [xmin, xmax, xmax, xmin],
 340                [ymin, ymin, ymax, ymax],
 341                color=(106 / 255.0, 159 / 255.0, 67 / 255.0),
 342            )
 343        if plot_type == "age":
 344            age_cmap = cm.get_cmap("magma", n_channels)
 345        for i in range(0, len(times)):
 346            if times[i] in sclt:
 347                ind = np.where(sclt == times[i])[0][0]
 348                x1 = self.channels[ind].x
 349                y1 = self.channels[ind].y
 350                W = self.channels[ind].W
 351                xm, ym = get_channel_banks(x1, y1, W)
 352                if plot_type == "morph":
 353                    if times[i] > times[-1] - pb_age:
 354                        plt.fill(
 355                            xm,
 356                            ym,
 357                            facecolor=pb_cmap(i / float(len(times) - 1)),
 358                            edgecolor="k",
 359                            linewidth=0.2,
 360                        )
 361                    else:
 362                        plt.fill(xm, ym, facecolor=pb_cmap(i / float(len(times) - 1)))
 363                if plot_type == "strat":
 364                    order += 1
 365                    plt.fill(
 366                        xm,
 367                        ym,
 368                        "xkcd:light tan",
 369                        edgecolor="k",
 370                        linewidth=0.25,
 371                        zorder=order,
 372                    )
 373                if plot_type == "age":
 374                    order += 1
 375                    plt.fill(
 376                        xm,
 377                        ym,
 378                        facecolor=age_cmap(i / float(n_channels - 1)),
 379                        edgecolor="k",
 380                        linewidth=0.1,
 381                        zorder=order,
 382                    )
 383            if times[i] in cot:
 384                ind = np.where(cot == times[i])[0][0]
 385                for j in range(0, len(self.cutoffs[ind].x)):
 386                    x1 = self.cutoffs[ind].x[j]
 387                    y1 = self.cutoffs[ind].y[j]
 388                    xm, ym = get_channel_banks(x1, y1, self.cutoffs[ind].W)
 389                    if plot_type == "morph":
 390                        plt.fill(xm, ym, color=ob_cmap(i / float(len(times) - 1)))
 391                    if plot_type == "strat":
 392                        order = order + 1
 393                        plt.fill(
 394                            xm,
 395                            ym,
 396                            "xkcd:ocean blue",
 397                            edgecolor="k",
 398                            linewidth=0.25,
 399                            zorder=order,
 400                        )
 401                    if plot_type == "age":
 402                        order += 1
 403                        plt.fill(
 404                            xm,
 405                            ym,
 406                            "xkcd:sea blue",
 407                            edgecolor="k",
 408                            linewidth=0.1,
 409                            zorder=order,
 410                        )
 411
 412        x1 = self.channels[len(sclt) - 1].x
 413        y1 = self.channels[len(sclt) - 1].y
 414        xm, ym = get_channel_banks(x1, y1, self.channels[len(sclt) - 1].W)
 415        order = order + 1
 416        if plot_type == "age":
 417            plt.fill(
 418                xm,
 419                ym,
 420                color="xkcd:sea blue",
 421                zorder=order,
 422                edgecolor="k",
 423                linewidth=0.1,
 424            )
 425        else:
 426            plt.fill(
 427                xm, ym, color=(16 / 255.0, 73 / 255.0, 90 / 255.0), zorder=order
 428            )  # ,edgecolor='k')
 429        plt.axis("equal")
 430        plt.xlim(xmin, xmax)
 431        plt.ylim(ymin, ymax)
 432        return fig
 433
 434    def create_movie(
 435        self,
 436        xmin,
 437        xmax,
 438        plot_type,
 439        filename,
 440        dirname,
 441        pb_age,
 442        ob_age,
 443        end_time,
 444        n_channels,
 445    ):
 446        """method for creating movie frames (PNG files) that capture the plan-view evolution of a channel belt through time
 447        movie has to be assembled from the PNG file after this method is applied
 448
 449        :param xmin: value of x coodinate on the left side of frame
 450        :param xmax: value of x coordinate on right side of frame
 451        :param plot_type: plot type; can be either 'strat' (for stratigraphic plot) or 'morph' (for morphologic plot)
 452        :param filename: first few characters of the output filenames
 453        :param dirname: name of directory where output files should be written
 454        :param pb_age: age of point bars (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
 455        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
 456        :param scale: scaling factor (e.g., 2) that determines how many times larger you want the frame to be, compared to the default scaling of the figure
 457        :param end_time: time at which simulation should be stopped
 458        :param n_channels: total number of channels + cutoffs for which simulation is run (usually it is len(chb.cutoffs) + len(chb.channels)). Used when plot_type = 'age'"""
 459
 460        sclt = np.array(self.cl_times)
 461        if type(end_time) != list:
 462            sclt = sclt[sclt <= end_time]
 463        channels = self.channels[: len(sclt)]
 464        ymax = 0
 465        for i in range(len(channels)):
 466            ymax = max(ymax, np.max(np.abs(channels[i].y)))
 467        ymax = ymax + 2 * channels[0].W  # add a bit of space on top and bottom
 468        ymin = -1 * ymax
 469        for i in range(0, len(sclt)):
 470            fig = self.plot(plot_type, pb_age, ob_age, sclt[i], n_channels)
 471            scale = 1
 472            fig_height = scale * fig.get_figheight()
 473            fig_width = (xmax - xmin) * fig_height / (ymax - ymin)
 474            fig.set_figwidth(fig_width)
 475            fig.set_figheight(fig_height)
 476            fig.gca().set_xlim(xmin, xmax)
 477            fig.gca().set_xticks([])
 478            fig.gca().set_yticks([])
 479            plt.plot(
 480                [xmin + 200, xmin + 200 + 5000],
 481                [ymin + 200, ymin + 200],
 482                "k",
 483                linewidth=2,
 484            )
 485            plt.text(xmin + 200 + 2000, ymin + 200 + 100, "5 km", fontsize=14)
 486            fname = dirname + filename + "%03d.png" % (i)
 487            fig.savefig(fname, bbox_inches="tight")
 488            plt.close()
 489
 490    def build_3d_model(
 491        self,
 492        model_type,
 493        h_mud,
 494        levee_width,
 495        h,
 496        w,
 497        bth,
 498        dcr,
 499        dx,
 500        delta_s,
 501        starttime,
 502        endtime,
 503        xmin,
 504        xmax,
 505        ymin,
 506        ymax,
 507    ):
 508        """method for building 3D model from set of centerlines (that are part of a ChannelBelt object)
 509
 510        :param model_type: model type ('fluvial' or 'submarine')
 511        :param h_mud: maximum thickness of overbank mud
 512        :param levee_width: width of overbank mud
 513        :param h: channel depth
 514        :param w: channel width
 515        :param bth: thickness of channel sand (only used in submarine models)
 516        :param dcr: critical channel depth where sand thickness goes to zero (only used in submarine models)
 517        :param dx: cell size in x and y directions
 518        :param delta_s: sampling distance alogn centerlines
 519        :param starttime: age of centerline that will be used as the first centerline in the model
 520        :param endtime: age of centerline that will be used as the last centerline in the model
 521        :param xmin: minimum x coordinate that defines the model domain; if xmin is set to zero,
 522        a plot of the centerlines is generated and the model domain has to be defined by clicking its upper left and lower right corners
 523        :param xmax: maximum x coordinate that defines the model domain
 524        :param ymin: minimum y coordinate that defines the model domain
 525        :param ymax: maximum y coordinate that defines the model domain
 526        :return chb_3d: a ChannelBelt3D object
 527        :return xmin, xmax, ymin, ymax: x and y coordinates that define the model domain (so that they can be reused later)"""
 528
 529        sclt = np.array(self.cl_times)
 530        ind1 = np.where(sclt >= starttime)[0][0]
 531        ind2 = np.where(sclt <= endtime)[0][-1]
 532        sclt = sclt[ind1 : ind2 + 1]
 533        channels = self.channels[ind1 : ind2 + 1]
 534        cot = np.array(self.cutoff_times)
 535        if (
 536            (len(cot) > 0)
 537            & (len(np.where(cot >= starttime)[0]) > 0)
 538            & (len(np.where(cot <= endtime)[0]) > 0)
 539        ):
 540            cfind1 = np.where(cot >= starttime)[0][0]
 541            cfind2 = np.where(cot <= endtime)[0][-1]
 542            cot = cot[cfind1 : cfind2 + 1]
 543            cutoffs = self.cutoffs[cfind1 : cfind2 + 1]
 544        else:
 545            cot = []
 546            cutoffs = []
 547        n_steps = len(sclt)  # number of events
 548        if xmin == 0:  # plot centerlines and define model domain
 549            plt.figure(figsize=(15, 4))
 550            maxX, minY, maxY = 0, 0, 0
 551            for i in range(n_steps):  # plot centerlines
 552                plt.plot(channels[i].x, channels[i].y, "k")
 553                maxX = max(maxX, np.max(channels[i].x))
 554                maxY = max(maxY, np.max(channels[i].y))
 555                minY = min(minY, np.min(channels[i].y))
 556            plt.axis([0, maxX, minY - 10 * w, maxY + 10 * w])
 557            plt.gca().set_aspect("equal", adjustable="box")
 558            plt.tight_layout()
 559            pts = np.zeros((2, 2))
 560            for i in range(0, 2):
 561                pt = np.asarray(plt.ginput(1))
 562                pts[i, :] = pt
 563                plt.scatter(pt[0][0], pt[0][1])
 564            plt.plot(
 565                [pts[0, 0], pts[1, 0], pts[1, 0], pts[0, 0], pts[0, 0]],
 566                [pts[0, 1], pts[0, 1], pts[1, 1], pts[1, 1], pts[0, 1]],
 567                "r",
 568            )
 569            xmin = min(pts[0, 0], pts[1, 0])
 570            xmax = max(pts[0, 0], pts[1, 0])
 571            ymin = min(pts[0, 1], pts[1, 1])
 572            ymax = max(pts[0, 1], pts[1, 1])
 573        iwidth = int((xmax - xmin) / dx)
 574        iheight = int((ymax - ymin) / dx)
 575        topo = np.zeros(
 576            (iheight, iwidth, 4 * n_steps)
 577        )  # array for storing topographic surfaces
 578        dists = np.zeros((iheight, iwidth, n_steps))
 579        zmaps = np.zeros((iheight, iwidth, n_steps))
 580        facies = np.zeros((4 * n_steps, 1))
 581        # create initial topography:
 582        x1 = np.linspace(0, iwidth - 1, iwidth)
 583        y1 = np.linspace(0, iheight - 1, iheight)
 584        xv, yv = np.meshgrid(x1, y1)
 585        z1 = channels[0].z
 586        z1 = z1[(channels[0].x > xmin) & (channels[0].x < xmax)]
 587        topoinit = (
 588            z1[0] - ((z1[0] - z1[-1]) / (xmax - xmin)) * xv * dx
 589        )  # initial (sloped) topography
 590        topo[:, :, 0] = topoinit.copy()
 591        surf = topoinit.copy()
 592        facies[0] = np.NaN
 593        # generate surfaces:
 594        channels3D = []
 595        for i in trange(n_steps):
 596            x = channels[i].x
 597            y = channels[i].y
 598            z = channels[i].z
 599            cutoff_ind = []
 600            # check if there were cutoffs during the last time step and collect indices in an array:
 601            for j in range(len(cot)):
 602                if (cot[j] >= sclt[i - 1]) & (cot[j] < sclt[i]):
 603                    cutoff_ind = np.append(cutoff_ind, j)
 604            # create distance map:
 605            cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x1, y1, z1 = dist_map(
 606                x, y, z, xmin, xmax, ymin, ymax, dx, delta_s
 607            )
 608            if i == 0:
 609                cl_dist_prev = cl_dist
 610            # erosion:
 611            surf = np.minimum(surf, erosion_surface(h, w / dx, cl_dist, z_map))
 612            topo[:, :, 4 * i] = surf  # erosional surface
 613            dists[:, :, i] = cl_dist
 614            zmaps[:, :, i] = z_map
 615            facies[4 * i] = np.NaN
 616
 617            if model_type == "fluvial":
 618                pb = point_bar_surface(cl_dist, z_map, h, w / dx)
 619                th = np.maximum(surf, pb) - surf
 620                th_oxbows = th.copy()
 621                # setting sand thickness to zero at cutoff locations:
 622                if len(cutoff_ind) > 0:
 623                    cutoff_dists = 1e10 * np.ones(
 624                        np.shape(th)
 625                    )  # initialize cutoff_dists with a large number
 626                    for j in range(len(cutoff_ind)):
 627                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
 628                            cutoffs[int(cutoff_ind[j])].x[0],
 629                            cutoffs[int(cutoff_ind[j])].y[0],
 630                            cutoffs[int(cutoff_ind[j])].z[0],
 631                            xmin,
 632                            xmax,
 633                            ymin,
 634                            ymax,
 635                            dx,
 636                        )
 637                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
 638                    th_oxbows[
 639                        cutoff_dists >= 0.9 * w / dx
 640                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
 641                    th[
 642                        cutoff_dists < 0.9 * w / dx
 643                    ] = 0  # set point bar thickness to zero inside of oxbows
 644                else:  # no cutoffs
 645                    th_oxbows = np.zeros(np.shape(th))
 646                th[th < 0] = 0  # eliminate negative th values
 647                surf = (
 648                    surf + th_oxbows
 649                )  # update topographic surface with oxbow deposit thickness
 650                topo[:, :, 4 * i + 1] = surf  # top of oxbow mud
 651                facies[4 * i + 1] = 0
 652                surf = surf + th  # update topographic surface with sand thickness
 653                topo[:, :, 4 * i + 2] = surf  # top of sand
 654                facies[4 * i + 2] = 1
 655                surf = surf + mud_surface(
 656                    h_mud, levee_width / dx, cl_dist, w / dx, z_map, surf
 657                )  # mud/levee deposition
 658                topo[:, :, 4 * i + 3] = surf  # top of levee
 659                facies[4 * i + 3] = 2
 660                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
 661
 662            if model_type == "submarine":
 663                surf = surf + mud_surface(
 664                    h_mud[i], levee_width / dx, cl_dist, w / dx, z_map, surf
 665                )  # mud/levee deposition
 666                topo[:, :, 4 * i + 1] = surf  # top of levee
 667                facies[4 * i + 1] = 2
 668                # sand thickness:
 669                th, relief = sand_surface(surf, bth, dcr, z_map, h)
 670                th[th < 0] = 0  # eliminate negative th values
 671                th[cl_dist > 1.0 * w / dx] = 0  # eliminate sand outside of channel
 672                th_oxbows = th.copy()
 673                # setting sand thickness to zero at cutoff locations:
 674                if len(cutoff_ind) > 0:
 675                    cutoff_dists = 1e10 * np.ones(
 676                        np.shape(th)
 677                    )  # initialize cutoff_dists with a large number
 678                    for j in range(len(cutoff_ind)):
 679                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
 680                            cutoffs[int(cutoff_ind[j])].x[0],
 681                            cutoffs[int(cutoff_ind[j])].y[0],
 682                            cutoffs[int(cutoff_ind[j])].z[0],
 683                            xmin,
 684                            xmax,
 685                            ymin,
 686                            ymax,
 687                            dx,
 688                        )
 689                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
 690                    th_oxbows[
 691                        cutoff_dists >= 0.9 * w / dx
 692                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
 693                    th[
 694                        cutoff_dists < 0.9 * w / dx
 695                    ] = 0  # set point bar thickness to zero inside of oxbows
 696                    # adding back sand near the channel axis (submarine only):
 697                    # th[cl_dist<0.5*w/dx] = bth*(1 - relief[cl_dist<0.5*w/dx]/dcr)
 698                else:  # no cutoffs
 699                    th_oxbows = np.zeros(np.shape(th))
 700                surf = (
 701                    surf + th_oxbows
 702                )  # update topographic surface with oxbow deposit thickness
 703                topo[:, :, 4 * i + 2] = surf  # top of oxbow mud
 704                facies[4 * i + 2] = 0
 705                surf = surf + th  # update topographic surface with sand thickness
 706                topo[:, :, 4 * i + 3] = surf  # top of sand
 707                facies[4 * i + 3] = 1
 708                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
 709
 710            cl_dist_prev = cl_dist.copy()
 711        topo = np.concatenate(
 712            (np.reshape(topoinit, (iheight, iwidth, 1)), topo), axis=2
 713        )  # add initial topography to array
 714        strat = topostrat(topo)  # create stratigraphic surfaces
 715        strat = np.delete(
 716            strat, np.arange(4 * n_steps + 1)[1::4], 2
 717        )  # get rid of unnecessary stratigraphic surfaces (duplicates)
 718        facies = np.delete(
 719            facies, np.arange(4 * n_steps)[::4]
 720        )  # get rid of unnecessary facies layers (NaNs)
 721        if model_type == "fluvial":
 722            facies_code = {0: "oxbow", 1: "point bar", 2: "levee"}
 723        if model_type == "submarine":
 724            facies_code = {0: "oxbow", 1: "channel sand", 2: "levee"}
 725        chb_3d = ChannelBelt3D(
 726            model_type, topo, strat, facies, facies_code, dx, channels3D
 727        )
 728        return chb_3d, xmin, xmax, ymin, ymax  # , dists, zmaps
 729
 730
 731def resample_centerline(x, y, z, deltas):
 732    """resample centerline so that 'deltas' is roughly constant, using parametric
 733    spline representation of curve; note that there is *no* smoothing
 734
 735    :param x: x-coordinates of centerline
 736    :param y: y-coordinates of centerline
 737    :param z: z-coordinates of centerline
 738    :param deltas: distance between points on centerline
 739    :return x: x-coordinates of resampled centerline
 740    :return y: y-coordinates of resampled centerline
 741    :return z: z-coordinates of resampled centerline
 742    :return dx: dx of resampled centerline
 743    :return dy: dy of resampled centerline
 744    :return dz: dz of resampled centerline
 745    :return s: s-coordinates of resampled centerline"""
 746
 747    dx, dy, dz, ds, s = compute_derivatives(x, y, z)  # compute derivatives
 748    tck, u = scipy.interpolate.splprep([x, y, z], s=0)
 749    unew = np.linspace(0, 1, 1 + int(round(s[-1] / deltas)))  # vector for resampling
 750    out = scipy.interpolate.splev(unew, tck)  # resampling
 751    x, y, z = out[0], out[1], out[2]  # assign new coordinate values
 752    dx, dy, dz, ds, s = compute_derivatives(x, y, z)  # recompute derivatives
 753    return x, y, z, dx, dy, dz, ds, s
 754
 755
 756def migrate_one_step(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
 757    """migrate centerline during one time step, using the migration computed as in Howard & Knutson (1984)
 758
 759    :param x: x-coordinates of centerline
 760    :param y: y-coordinates of centerline
 761    :param z: z-coordinates of centerline
 762    :param W: channel width
 763    :param kl: migration rate (or erodibility) constant (m/s)
 764    :param dt: duration of time step (s)
 765    :param k: constant for calculating the exponent alpha (= 1.0)
 766    :param Cf: dimensionless Chezy friction factor
 767    :param D: channel depth
 768    :param omega: constant in Howard & Knutson equation (= -1.0)
 769    :param gamma: constant in Howard & Knutson equation (= 2.5)
 770    :return x: new x-coordinates of centerline after migration
 771    :return y: new y-coordinates of centerline after migration
 772    """
 773
 774    ns = len(x)
 775    curv = compute_curvature(x, y)
 776    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
 777    sinuosity = s[-1] / (x[-1] - x[0])
 778    curv = W * curv  # dimensionless curvature
 779    R0 = (
 780        kl * curv
 781    )  # simple linear relationship between curvature and nominal migration rate
 782    alpha = k * 2 * Cf / D  # exponent for convolution function G
 783    R1 = compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0)
 784    R1 = sinuosity ** (-2 / 3.0) * R1
 785    # calculate new centerline coordinates:
 786    dy_ds = dy[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
 787    dx_ds = dx[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
 788    # adjust x and y coordinates (this *is* the migration):
 789    x[pad1 : ns - pad + 1] = (
 790        x[pad1 : ns - pad + 1] + R1[pad1 : ns - pad + 1] * dy_ds * dt
 791    )
 792    y[pad1 : ns - pad + 1] = (
 793        y[pad1 : ns - pad + 1] - R1[pad1 : ns - pad + 1] * dx_ds * dt
 794    )
 795    return x, y
 796
 797
 798def migrate_one_step_w_bias(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
 799    ns = len(x)
 800    curv = compute_curvature(x, y)
 801    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
 802    sinuosity = s[-1] / (x[-1] - x[0])
 803    curv = W * curv  # dimensionless curvature
 804    R0 = (
 805        kl * curv
 806    )  # simple linear relationship between curvature and nominal migration rate
 807    alpha = k * 2 * Cf / D  # exponent for convolution function G
 808    R1 = compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0)
 809    R1 = sinuosity ** (-2 / 3.0) * R1
 810    pad = -1
 811    # calculate new centerline coordinates:
 812    dy_ds = dy[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
 813    dx_ds = dx[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
 814    tilt_factor = 0.2
 815    T = kl * tilt_factor * np.ones(np.shape(x))
 816    angle = 90.0
 817    # adjust x and y coordinates (this *is* the migration):
 818    x[pad1 : ns - pad + 1] = (
 819        x[pad1 : ns - pad + 1]
 820        + R1[pad1 : ns - pad + 1] * dy_ds * dt
 821        + T[pad1 : ns - pad + 1]
 822        * dy_ds
 823        * dt
 824        * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds)
 825    )
 826    y[pad1 : ns - pad + 1] = (
 827        y[pad1 : ns - pad + 1]
 828        - R1[pad1 : ns - pad + 1] * dx_ds * dt
 829        - T[pad1 : ns - pad + 1]
 830        * dx_ds
 831        * dt
 832        * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds)
 833    )
 834    return x, y
 835
 836
 837def generate_initial_channel(W, D, Sl, deltas, pad, n_bends):
 838    """generate straight Channel object with some noise added that can serve
 839    as input for initializing a ChannelBelt object
 840    W - channel width
 841    D - channel depth
 842    Sl - channel gradient
 843    deltas - distance between nodes on centerline
 844    pad - padding (number of nodepoints along centerline)
 845    n_bends - approximate number of bends to be simulated"""
 846    noisy_len = n_bends * 10 * W / 2.0  # length of noisy part of initial centerline
 847    pad1 = int(
 848        pad / 10.0
 849    )  # padding at upstream end can be shorter than padding on downstream end
 850    if pad1 < 5:
 851        pad1 = 5
 852    x = np.linspace(
 853        0, noisy_len + (pad + pad1) * deltas, int(noisy_len / deltas + pad + pad1) + 1
 854    )  # x coordinate
 855    y = 10.0 * (
 856        2
 857        * np.random.random_sample(
 858            int(noisy_len / deltas) + 1,
 859        )
 860        - 1
 861    )
 862    y = np.hstack(
 863        (
 864            np.zeros(
 865                (pad1),
 866            ),
 867            y,
 868            np.zeros(
 869                (pad),
 870            ),
 871        )
 872    )  # y coordinate
 873    deltaz = Sl * deltas * (len(x) - 1)
 874    z = np.linspace(0, deltaz, len(x))[::-1]  # z coordinate
 875    return Channel(x, y, z, W, D)
 876
 877
 878@numba.jit(nopython=True)  # use Numba to speed up the heaviest computation
 879def compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0):
 880    """compute migration rate as weighted sum of upstream curvatures
 881    pad - padding (number of nodepoints along centerline)
 882    ns - number of points in centerline
 883    ds - distances between points in centerline
 884    omega - constant in HK model
 885    gamma - constant in HK model
 886    R0 - nominal migration rate (dimensionless curvature * migration rate constant)"""
 887    R1 = np.zeros(ns)  # preallocate adjusted channel migration rate
 888    pad1 = int(
 889        pad / 10.0
 890    )  # padding at upstream end can be shorter than padding on downstream end
 891    if pad1 < 5:
 892        pad1 = 5
 893    for i in range(pad1, ns - pad):
 894        si2 = np.hstack(
 895            (np.array([0]), np.cumsum(ds[i - 1 :: -1]))
 896        )  # distance along centerline, backwards from current point
 897        G = np.exp(-alpha * si2)  # convolution vector
 898        R1[i] = omega * R0[i] + gamma * np.sum(R0[i::-1] * G) / np.sum(
 899            G
 900        )  # main equation
 901    return R1
 902
 903
 904def compute_derivatives(x, y, z):
 905    """function for computing first derivatives of a curve (centerline)
 906    x,y are cartesian coodinates of the curve
 907    outputs:
 908    dx - first derivative of x coordinate
 909    dy - first derivative of y coordinate
 910    ds - distances between consecutive points along the curve
 911    s - cumulative distance along the curve"""
 912    dx = np.gradient(x)  # first derivatives
 913    dy = np.gradient(y)
 914    dz = np.gradient(z)
 915    ds = np.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
 916    s = np.hstack((0, np.cumsum(ds[1:])))
 917    return dx, dy, dz, ds, s
 918
 919
 920def compute_curvature(x, y):
 921    """function for computing first derivatives and curvature of a curve (centerline)
 922    x,y are cartesian coodinates of the curve
 923    outputs:
 924    dx - first derivative of x coordinate
 925    dy - first derivative of y coordinate
 926    ds - distances between consecutive points along the curve
 927    s - cumulative distance along the curve
 928    curvature - curvature of the curve (in 1/units of x and y)"""
 929    dx = np.gradient(x)  # first derivatives
 930    dy = np.gradient(y)
 931    ddx = np.gradient(dx)  # second derivatives
 932    ddy = np.gradient(dy)
 933    curvature = (dx * ddy - dy * ddx) / ((dx ** 2 + dy ** 2) ** 1.5)
 934    return curvature
 935
 936
 937def make_colormap(seq):
 938    """Return a LinearSegmentedColormap
 939    seq: a sequence of floats and RGB-tuples. The floats should be increasing
 940    and in the interval (0,1).
 941    [from: https://stackoverflow.com/questions/16834861/create-own-colormap-using-matplotlib-and-plot-color-scale]
 942    """
 943    seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
 944    cdict = {"red": [], "green": [], "blue": []}
 945    for i, item in enumerate(seq):
 946        if isinstance(item, float):
 947            r1, g1, b1 = seq[i - 1]
 948            r2, g2, b2 = seq[i + 1]
 949            cdict["red"].append([item, r1, r2])
 950            cdict["green"].append([item, g1, g2])
 951            cdict["blue"].append([item, b1, b2])
 952    return mcolors.LinearSegmentedColormap("CustomMap", cdict)
 953
 954
 955def kth_diag_indices(a, k):
 956    """function for finding diagonal indices with k offset
 957    [from https://stackoverflow.com/questions/10925671/numpy-k-th-diagonal-indices]"""
 958    rows, cols = np.diag_indices_from(a)
 959    if k < 0:
 960        return rows[:k], cols[-k:]
 961    elif k > 0:
 962        return rows[k:], cols[:-k]
 963    else:
 964        return rows, cols
 965
 966
 967def find_cutoffs(x, y, crdist, deltas):
 968    """function for identifying locations of cutoffs along a centerline
 969    and the indices of the segments that will become part of the oxbows
 970    x,y - coordinates of centerline
 971    crdist - critical cutoff distance
 972    deltas - distance between neighboring points along the centerline"""
 973    diag_blank_width = int((crdist + 20 * deltas) / deltas)
 974    # distance matrix for centerline points:
 975    dist = distance.cdist(np.array([x, y]).T, np.array([x, y]).T)
 976    dist[
 977        dist > crdist
 978    ] = np.NaN  # set all values that are larger than the cutoff threshold to NaN
 979    # set matrix to NaN along the diagonal zone:
 980    for k in range(-diag_blank_width, diag_blank_width + 1):
 981        rows, cols = kth_diag_indices(dist, k)
 982        dist[rows, cols] = np.NaN
 983    i1, i2 = np.where(~np.isnan(dist))
 984    ind1 = i1[np.where(i1 < i2)[0]]  # get rid of unnecessary indices
 985    ind2 = i2[np.where(i1 < i2)[0]]  # get rid of unnecessary indices
 986    return ind1, ind2  # return indices of cutoff points and cutoff coordinates
 987
 988
 989def cut_off_cutoffs(x, y, z, s, crdist, deltas):
 990    """function for executing cutoffs - removing oxbows from centerline and storing cutoff coordinates
 991    x,y - coordinates of centerline
 992    crdist - critical cutoff distance
 993    deltas - distance between neighboring points along the centerline
 994    outputs:
 995    x,y,z - updated coordinates of centerline
 996    xc, yc, zc - lists with coordinates of cutoff segments"""
 997    xc = []
 998    yc = []
 999    zc = []
1000    ind1, ind2 = find_cutoffs(x, y, crdist, deltas)  # initial check for cutoffs
1001    while len(ind1) > 0:
1002        xc.append(x[ind1[0] : ind2[0] + 1])  # x coordinates of cutoff
1003        yc.append(y[ind1[0] : ind2[0] + 1])  # y coordinates of cutoff
1004        zc.append(z[ind1[0] : ind2[0] + 1])  # z coordinates of cutoff
1005        x = np.hstack((x[: ind1[0] + 1], x[ind2[0] :]))  # x coordinates after cutoff
1006        y = np.hstack((y[: ind1[0] + 1], y[ind2[0] :]))  # y coordinates after cutoff
1007        z = np.hstack((z[: ind1[0] + 1], z[ind2[0] :]))  # z coordinates after cutoff
1008        ind1, ind2 = find_cutoffs(x, y, crdist, deltas)
1009    return x, y, z, xc, yc, zc
1010
1011
1012def get_channel_banks(x, y, W):
1013    """function for finding coordinates of channel banks, given a centerline and a channel width
1014    x,y - coordinates of centerline
1015    W - channel width
1016    outputs:
1017    xm, ym - coordinates of channel banks (both left and right banks)"""
1018    x1 = x.copy()
1019    y1 = y.copy()
1020    x2 = x.copy()
1021    y2 = y.copy()
1022    ns = len(x)
1023    dx = np.diff(x)
1024    dy = np.diff(y)
1025    ds = np.sqrt(dx ** 2 + dy ** 2)
1026    x1[:-1] = x[:-1] + 0.5 * W * np.diff(y) / ds
1027    y1[:-1] = y[:-1] - 0.5 * W * np.diff(x) / ds
1028    x2[:-1] = x[:-1] - 0.5 * W * np.diff(y) / ds
1029    y2[:-1] = y[:-1] + 0.5 * W * np.diff(x) / ds
1030    x1[ns - 1] = x[ns - 1] + 0.5 * W * (y[ns - 1] - y[ns - 2]) / ds[ns - 2]
1031    y1[ns - 1] = y[ns - 1] - 0.5 * W * (x[ns - 1] - x[ns - 2]) / ds[ns - 2]
1032    x2[ns - 1] = x[ns - 1] - 0.5 * W * (y[ns - 1] - y[ns - 2]) / ds[ns - 2]
1033    y2[ns - 1] = y[ns - 1] + 0.5 * W * (x[ns - 1] - x[ns - 2]) / ds[ns - 2]
1034    xm = np.hstack((x1, x2[::-1]))
1035    ym = np.hstack((y1, y2[::-1]))
1036    return xm, ym
1037
1038
1039def dist_map(x, y, z, xmin, xmax, ymin, ymax, dx, delta_s):
1040    """function for centerline rasterization and distance map calculation
1041    inputs:
1042    x,y,z - coordinates of centerline
1043    xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest
1044    dx - gridcell size (m)
1045    delta_s - distance between points along centerline (m)
1046    returns:
1047    cl_dist - distance map (distance from centerline)
1048    x_pix, y_pix, z_pix - x,y, and z pixel coordinates of the centerline
1049    s_pix - along-channel distance in pixels
1050    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1051    x, y, z - x,y,z centerline coordinates clipped to the 3D model domain"""
1052    y = y[(x > xmin) & (x < xmax)]
1053    z = z[(x > xmin) & (x < xmax)]
1054    x = x[(x > xmin) & (x < xmax)]
1055    dummy, dy, dz, ds, s = compute_derivatives(x, y, z)
1056    if len(np.where(ds > 2 * delta_s)[0]) > 0:
1057        inds = np.where(ds > 2 * delta_s)[0]
1058        inds = np.hstack((0, inds, len(x)))
1059        lengths = np.diff(inds)
1060        long_segment = np.where(lengths == max(lengths))[0][0]
1061        start_ind = inds[long_segment] + 1
1062        end_ind = inds[long_segment + 1]
1063        if end_ind < len(x):
1064            x = x[start_ind:end_ind]
1065            y = y[start_ind:end_ind]
1066            z = z[start_ind:end_ind]
1067        else:
1068            x = x[start_ind:]
1069            y = y[start_ind:]
1070            z = z[start_ind:]
1071    xdist = xmax - xmin
1072    ydist = ymax - ymin
1073    iwidth = int((xmax - xmin) / dx)
1074    iheight = int((ymax - ymin) / dx)
1075    xratio = iwidth / xdist
1076    # create list with pixel coordinates:
1077    pixels = []
1078    for i in range(0, len(x)):
1079        px = int(iwidth - (xmax - x[i]) * xratio)
1080        py = int(iheight - (ymax - y[i]) * xratio)
1081        pixels.append((px, py))
1082    # create image and numpy array:
1083    img = Image.new("RGB", (iwidth, iheight), "white")
1084    draw = ImageDraw.Draw(img)
1085    draw.line(pixels, fill="rgb(0, 0, 0)")  # draw centerline as black line
1086    pix = np.array(img)
1087    cl = pix[:, :, 0]
1088    cl[cl == 255] = 1  # set background to 1 (centerline is 0)
1089    y_pix, x_pix = np.where(cl == 0)
1090    x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1091    # This next block of code is kind of a hack. Looking for, and eliminating, 'bad' pixels.
1092    img = np.array(img)
1093    img = img[:, :, 0]
1094    img[img == 255] = 1
1095    img1 = morphology.binary_dilation(img, morphology.square(2)).astype(np.uint8)
1096    if len(np.where(img1 == 0)[0]) > 0:
1097        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1098        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1099    img1 = morphology.binary_dilation(
1100        img, np.array([[1, 0, 1], [1, 1, 1]], dtype=np.uint8)
1101    ).astype(np.uint8)
1102    if len(np.where(img1 == 0)[0]) > 0:
1103        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1104        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1105    img1 = morphology.binary_dilation(
1106        img, np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=np.uint8)
1107    ).astype(np.uint8)
1108    if len(np.where(img1 == 0)[0]) > 0:
1109        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1110        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1111    # redo the distance calculation (because x_pix and y_pix do not always contain all the points in cl):
1112    cl[cl == 0] = 1
1113    cl[y_pix, x_pix] = 0
1114    cl_dist, inds = ndimage.distance_transform_edt(cl, return_indices=True)
1115    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
1116    dx_pix = np.diff(x_pix)
1117    dy_pix = np.diff(y_pix)
1118    ds_pix = np.sqrt(dx_pix ** 2 + dy_pix ** 2)
1119    s_pix = np.hstack((0, np.cumsum(ds_pix)))
1120    f = scipy.interpolate.interp1d(s, z)
1121    snew = s_pix * s[-1] / s_pix[-1]
1122    if snew[-1] > s[-1]:
1123        snew[-1] = s[-1]
1124    snew[snew < s[0]] = s[0]
1125    z_pix = f(snew)
1126    # create z_map:
1127    z_map = np.zeros(np.shape(cl_dist))
1128    z_map[y_pix, x_pix] = z_pix
1129    xinds = inds[1, :, :]
1130    yinds = inds[0, :, :]
1131    for i in range(0, len(x_pix)):
1132        z_map[(xinds == x_pix[i]) & (yinds == y_pix[i])] = z_pix[i]
1133    return cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x, y, z
1134
1135
1136def erosion_surface(h, w, cl_dist, z):
1137    """function for creating a parabolic erosional surface
1138    inputs:
1139    h - geomorphic channel depth (m)
1140    w - geomorphic channel width (in pixels, as cl_dist is also given in pixels)
1141    cl_dist - distance map (distance from centerline)
1142    z - reference elevation (m)
1143    returns:
1144    surf - map of the quadratic erosional surface (m)
1145    """
1146    surf = z + (4 * h / w ** 2) * (cl_dist + w * 0.5) * (cl_dist - w * 0.5)
1147    return surf
1148
1149
1150def point_bar_surface(cl_dist, z, h, w):
1151    """function for creating a Gaussian-based point bar surface
1152    used in 3D fluvial model
1153    inputs:
1154    cl_dist - distance map (distance from centerline)
1155    z - reference elevation (m)
1156    h - channel depth (m)
1157    w - channel width, in pixels, as cl_dist is also given in pixels
1158    returns:
1159    pb - map of the Gaussian surface that can be used to from a point bar deposit (m)"""
1160    pb = z - h * np.exp(-(cl_dist ** 2) / (2 * (w * 0.33) ** 2))
1161    return pb
1162
1163
1164def sand_surface(surf, bth, dcr, z_map, h):
1165    """function for creating the top horizontal surface sand-rich deposit in the bottom of the channel
1166    used in 3D submarine channel models
1167    inputs:
1168    surf - current geomorphic surface
1169    bth - thickness of sand deposit in axis of channel (m)
1170    dcr - critical channel depth, above which there is no sand deposition (m)
1171    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1172    h - channel depth (m)
1173    returns:
1174    th - thickness map of sand deposit (m)
1175    relief - map of channel relief (m)"""
1176    relief = np.abs(surf - z_map + h)
1177    relief = np.abs(relief - np.amin(relief))
1178    th = bth * (1 - relief / dcr)  # bed thickness inversely related to relief
1179    th[th < 0] = 0.0  # set negative th values to zero
1180    return th, relief
1181
1182
1183def mud_surface(h_mud, levee_width, cl_dist, w, z_map, topo):
1184    """function for creating a map of overbank deposit thickness
1185    inputs:
1186    h_mud - maximum thickness of overbank deposit (m)
1187    levee_width - half-width of overbank deposit (m)
1188    cl_dist - distance map (distance from centerline)
1189    w - channel width (in pixels, as cl_dist is also given in pixels)
1190    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1191    topo - current geomorphic surface
1192    returns:
1193    surf - map of overbank deposit thickness (m)"""
1194    # create a surface that thins linearly away from the channel centerline:
1195    surf1 = (-2 * h_mud / levee_width) * cl_dist + h_mud
1196    surf2 = (2 * h_mud / levee_width) * cl_dist + h_mud
1197    surf = np.minimum(surf1, surf2)
1198    # surface for 'eroding' the central part of the mud layer:
1199    surf3 = h_mud + (4 * 1.5 * h_mud / w ** 2) * (cl_dist + w * 0.5) * (
1200        cl_dist - w * 0.5
1201    )
1202    surf = np.minimum(surf, surf3)
1203    surf[surf < 0] = 0
1204    # eliminate negative thicknesses
1205    return surf
1206
1207
1208def topostrat(topo):
1209    """function for converting a stack of geomorphic surfaces into stratigraphic surfaces
1210    inputs:
1211    topo - 3D numpy array of geomorphic surfaces
1212    returns:
1213    strat - 3D numpy array of stratigraphic surfaces
1214    """
1215    r, c, ts = np.shape(topo)
1216    strat = np.copy(topo)
1217    for i in range(0, ts):
1218        strat[:, :, i] = np.amin(topo[:, :, i:], axis=2)
1219    return strat
1220
1221
1222def cl_dist_map(x, y, z, xmin, xmax, ymin, ymax, dx):
1223    """function for centerline rasterization and distance map calculation (does not return zmap)
1224    used for cutoffs only
1225    inputs:
1226    x,y,z - coordinates of centerline
1227    xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest
1228    dx - gridcell size (m)
1229    returns:
1230    cl_dist - distance map (distance from centerline)
1231    x_pix, y_pix, - x and y pixel coordinates of the centerline
1232    """
1233    y = y[(x > xmin) & (x < xmax)]
1234    z = z[(x > xmin) & (x < xmax)]
1235    x = x[(x > xmin) & (x < xmax)]
1236    xdist = xmax - xmin
1237    ydist = ymax - ymin
1238    iwidth = int((xmax - xmin) / dx)
1239    iheight = int((ymax - ymin) / dx)
1240    xratio = iwidth / xdist
1241    # create list with pixel coordinates:
1242    pixels = []
1243    for i in range(0, len(x)):
1244        px = int(iwidth - (xmax - x[i]) * xratio)
1245        py = int(iheight - (ymax - y[i]) * xratio)
1246        pixels.append((px, py))
1247    # create image and numpy array:
1248    img = Image.new("RGB", (iwidth, iheight), "white")
1249    draw = ImageDraw.Draw(img)
1250    draw.line(pixels, fill="rgb(0, 0, 0)")  # draw centerline as black line
1251    pix = np.array(img)
1252    cl = pix[:, :, 0]
1253    cl[cl == 255] = 1  # set background to 1 (centerline is 0)
1254    # calculate Euclidean distance map:
1255    cl_dist, inds = ndimage.distance_transform_edt(cl, return_indices=True)
1256    y_pix, x_pix = np.where(cl == 0)
1257    return cl_dist, x_pix, y_pix
1258
1259
1260def eliminate_bad_pixels(img, img1):
1261    x_ind = np.where(img1 == 0)[1][0]
1262    y_ind = np.where(img1 == 0)[0][0]
1263    img[y_ind : y_ind + 2, x_ind : x_ind + 2] = np.ones(
1264        1,
1265    ).astype(np.uint8)
1266    all_labels = measure.label(img, background=1, connectivity=2)
1267    cl = all_labels.copy()
1268    cl[cl == 2] = 0
1269    cl[cl > 0] = 1
1270    y_pix, x_pix = np.where(cl == 1)
1271    return x_pix, y_pix
1272
1273
1274def order_cl_pixels(x_pix, y_pix):
1275    dist = distance.cdist(np.array([x_pix, y_pix]).T, np.array([x_pix, y_pix]).T)
1276    dist[np.diag_indices_from(dist)] = 100.0
1277    ind = np.argmin(x_pix)  # select starting point on left side of image
1278    clinds = [ind]
1279    count = 0
1280    while count < len(x_pix):
1281        t = dist[ind, :].copy()
1282        if len(clinds) > 2:
1283            t[clinds[-2]] = t[clinds[-2]] + 100.0
1284            t[clinds[-3]] = t[clinds[-3]] + 100.0
1285        ind = np.argmin(t)
1286        clinds.append(ind)
1287        count = count + 1
1288    x_pix = x_pix[clinds]
1289    y_pix = y_pix[clinds]
1290    return x_pix, y_pix
class Channel:
19class Channel:
20    """class for Channel objects"""
21
22    def __init__(self, x, y, z, W, D):
23        """Initialize Channel object
24
25        :param x: x-coordinate of centerline
26        :param y: y-coordinate of centerline
27        :param z: z-coordinate of centerline
28        :param W: channel width
29        :param D: channel depth"""
30
31        self.x = x
32        self.y = y
33        self.z = z
34        self.W = W
35        self.D = D

class for Channel objects

Channel(x, y, z, W, D)
22    def __init__(self, x, y, z, W, D):
23        """Initialize Channel object
24
25        :param x: x-coordinate of centerline
26        :param y: y-coordinate of centerline
27        :param z: z-coordinate of centerline
28        :param W: channel width
29        :param D: channel depth"""
30
31        self.x = x
32        self.y = y
33        self.z = z
34        self.W = W
35        self.D = D

Initialize Channel object

Parameters
  • x: x-coordinate of centerline
  • y: y-coordinate of centerline
  • z: z-coordinate of centerline
  • W: channel width
  • D: channel depth
class Cutoff:
38class Cutoff:
39    """class for Cutoff objects"""
40
41    def __init__(self, x, y, z, W, D):
42        """Initialize Cutoff object
43
44        :param x: x-coordinate of centerline
45        :param y: y-coordinate of centerline
46        :param z: z-coordinate of centerline
47        :param W: channel width
48        :param D: channel depth"""
49
50        self.x = x
51        self.y = y
52        self.z = z
53        self.W = W
54        self.D = D

class for Cutoff objects

Cutoff(x, y, z, W, D)
41    def __init__(self, x, y, z, W, D):
42        """Initialize Cutoff object
43
44        :param x: x-coordinate of centerline
45        :param y: y-coordinate of centerline
46        :param z: z-coordinate of centerline
47        :param W: channel width
48        :param D: channel depth"""
49
50        self.x = x
51        self.y = y
52        self.z = z
53        self.W = W
54        self.D = D

Initialize Cutoff object

Parameters
  • x: x-coordinate of centerline
  • y: y-coordinate of centerline
  • z: z-coordinate of centerline
  • W: channel width
  • D: channel depth
class ChannelBelt3D:
 57class ChannelBelt3D:
 58    """class for 3D models of channel belts"""
 59
 60    def __init__(self, model_type, topo, strat, facies, facies_code, dx, channels):
 61        """initialize ChannelBelt3D object
 62
 63        :param model_type: type of model to be built; can be either 'fluvial' or 'submarine'
 64        :param topo: set of topographic surfaces (3D numpy array)
 65        :param strat: set of stratigraphic surfaces (3D numpy array)
 66        :param facies: facies volume (3D numpy array)
 67        :param facies_code: dictionary of facies codes, e.g. {0:'oxbow', 1:'point bar', 2:'levee'}
 68        :param dx: gridcell size (m)
 69        :param channels: list of channel objects that form 3D model"""
 70
 71        self.model_type = model_type
 72        self.topo = topo
 73        self.strat = strat
 74        self.facies = facies
 75        self.facies_code = facies_code
 76        self.dx = dx
 77        self.channels = channels
 78
 79    def plot_xsection(self, xsec, colors, ve):
 80        """method for plotting a cross section through a 3D model; also plots map of
 81        basal erosional surface and map of final geomorphic surface
 82
 83        :param xsec: location of cross section along the x-axis (in pixel/ voxel coordinates)
 84        :param colors: list of RGB values that define the colors for different facies
 85        :param ve: vertical exaggeration
 86        :return: handles to the three figures"""
 87
 88        strat = self.strat
 89        dx = self.dx
 90        fig1 = plt.figure(figsize=(20, 5))
 91        ax1 = fig1.add_subplot(111)
 92        r, c, ts = np.shape(strat)
 93        Xv = dx * np.arange(0, r)
 94        for i in range(0, ts - 1, 3):
 95            X1 = np.concatenate((Xv, Xv[::-1]))
 96            Y1 = np.concatenate((strat[:, xsec, i], strat[::-1, xsec, i + 1]))
 97            Y2 = np.concatenate((strat[:, xsec, i + 1], strat[::-1, xsec, i + 2]))
 98            Y3 = np.concatenate((strat[:, xsec, i + 2], strat[::-1, xsec, i + 3]))
 99            if self.model_type == "submarine":
100                ax1.fill(
101                    X1, Y1, facecolor=colors[2], linewidth=0.5, edgecolor=[0, 0, 0]
102                )  # oxbow mud
103                ax1.fill(
104                    X1, Y2, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
105                )  # point bar sand
106                ax1.fill(X1, Y3, facecolor=colors[1], linewidth=0.5)  # levee mud
107            if self.model_type == "fluvial":
108                ax1.fill(
109                    X1, Y1, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
110                )  # levee mud
111                ax1.fill(
112                    X1, Y2, facecolor=colors[1], linewidth=0.5, edgecolor=[0, 0, 0]
113                )  # oxbow mud
114                ax1.fill(X1, Y3, facecolor=colors[2], linewidth=0.5)  # channel sand
115        ax1.set_xlim(0, dx * (r - 1))
116        ax1.set_aspect(ve, adjustable="datalim")
117        fig2 = plt.figure()
118        ax2 = fig2.add_subplot(111)
119        ax2.contourf(strat[:, :, ts - 1], 100, cmap="viridis")
120        ax2.contour(
121            strat[:, :, ts - 1],
122            100,
123            colors="k",
124            linestyles="solid",
125            linewidths=0.1,
126            alpha=0.4,
127        )
128        ax2.plot([xsec, xsec], [0, r], "k", linewidth=2)
129        ax2.axis([0, c, 0, r])
130        ax2.set_aspect("equal", adjustable="box")
131        ax2.set_title("final geomorphic surface")
132        ax2.tick_params(
133            bottom=False,
134            top=False,
135            left=False,
136            right=False,
137            labelbottom=False,
138            labelleft=False,
139        )
140        fig3 = plt.figure()
141        ax3 = fig3.add_subplot(111)
142        ax3.contourf(strat[:, :, 0], 100, cmap="viridis")
143        ax3.contour(
144            strat[:, :, 0],
145            100,
146            colors="k",
147            linestyles="solid",
148            linewidths=0.1,
149            alpha=0.4,
150        )
151        ax3.plot([xsec, xsec], [0, r], "k", linewidth=2)
152        ax3.axis([0, c, 0, r])
153        ax3.set_aspect("equal", adjustable="box")
154        ax3.set_title("basal erosional surface")
155        ax3.tick_params(
156            bottom=False,
157            top=False,
158            left=False,
159            right=False,
160            labelbottom=False,
161            labelleft=False,
162        )
163        return fig1, fig2, fig3

class for 3D models of channel belts

ChannelBelt3D(model_type, topo, strat, facies, facies_code, dx, channels)
60    def __init__(self, model_type, topo, strat, facies, facies_code, dx, channels):
61        """initialize ChannelBelt3D object
62
63        :param model_type: type of model to be built; can be either 'fluvial' or 'submarine'
64        :param topo: set of topographic surfaces (3D numpy array)
65        :param strat: set of stratigraphic surfaces (3D numpy array)
66        :param facies: facies volume (3D numpy array)
67        :param facies_code: dictionary of facies codes, e.g. {0:'oxbow', 1:'point bar', 2:'levee'}
68        :param dx: gridcell size (m)
69        :param channels: list of channel objects that form 3D model"""
70
71        self.model_type = model_type
72        self.topo = topo
73        self.strat = strat
74        self.facies = facies
75        self.facies_code = facies_code
76        self.dx = dx
77        self.channels = channels

initialize ChannelBelt3D object

Parameters
  • model_type: type of model to be built; can be either 'fluvial' or 'submarine'
  • topo: set of topographic surfaces (3D numpy array)
  • strat: set of stratigraphic surfaces (3D numpy array)
  • facies: facies volume (3D numpy array)
  • facies_code: dictionary of facies codes, e.g. {0:'oxbow', 1:'point bar', 2: 'levee'}
  • dx: gridcell size (m)
  • channels: list of channel objects that form 3D model
def plot_xsection(self, xsec, colors, ve):
 79    def plot_xsection(self, xsec, colors, ve):
 80        """method for plotting a cross section through a 3D model; also plots map of
 81        basal erosional surface and map of final geomorphic surface
 82
 83        :param xsec: location of cross section along the x-axis (in pixel/ voxel coordinates)
 84        :param colors: list of RGB values that define the colors for different facies
 85        :param ve: vertical exaggeration
 86        :return: handles to the three figures"""
 87
 88        strat = self.strat
 89        dx = self.dx
 90        fig1 = plt.figure(figsize=(20, 5))
 91        ax1 = fig1.add_subplot(111)
 92        r, c, ts = np.shape(strat)
 93        Xv = dx * np.arange(0, r)
 94        for i in range(0, ts - 1, 3):
 95            X1 = np.concatenate((Xv, Xv[::-1]))
 96            Y1 = np.concatenate((strat[:, xsec, i], strat[::-1, xsec, i + 1]))
 97            Y2 = np.concatenate((strat[:, xsec, i + 1], strat[::-1, xsec, i + 2]))
 98            Y3 = np.concatenate((strat[:, xsec, i + 2], strat[::-1, xsec, i + 3]))
 99            if self.model_type == "submarine":
100                ax1.fill(
101                    X1, Y1, facecolor=colors[2], linewidth=0.5, edgecolor=[0, 0, 0]
102                )  # oxbow mud
103                ax1.fill(
104                    X1, Y2, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
105                )  # point bar sand
106                ax1.fill(X1, Y3, facecolor=colors[1], linewidth=0.5)  # levee mud
107            if self.model_type == "fluvial":
108                ax1.fill(
109                    X1, Y1, facecolor=colors[0], linewidth=0.5, edgecolor=[0, 0, 0]
110                )  # levee mud
111                ax1.fill(
112                    X1, Y2, facecolor=colors[1], linewidth=0.5, edgecolor=[0, 0, 0]
113                )  # oxbow mud
114                ax1.fill(X1, Y3, facecolor=colors[2], linewidth=0.5)  # channel sand
115        ax1.set_xlim(0, dx * (r - 1))
116        ax1.set_aspect(ve, adjustable="datalim")
117        fig2 = plt.figure()
118        ax2 = fig2.add_subplot(111)
119        ax2.contourf(strat[:, :, ts - 1], 100, cmap="viridis")
120        ax2.contour(
121            strat[:, :, ts - 1],
122            100,
123            colors="k",
124            linestyles="solid",
125            linewidths=0.1,
126            alpha=0.4,
127        )
128        ax2.plot([xsec, xsec], [0, r], "k", linewidth=2)
129        ax2.axis([0, c, 0, r])
130        ax2.set_aspect("equal", adjustable="box")
131        ax2.set_title("final geomorphic surface")
132        ax2.tick_params(
133            bottom=False,
134            top=False,
135            left=False,
136            right=False,
137            labelbottom=False,
138            labelleft=False,
139        )
140        fig3 = plt.figure()
141        ax3 = fig3.add_subplot(111)
142        ax3.contourf(strat[:, :, 0], 100, cmap="viridis")
143        ax3.contour(
144            strat[:, :, 0],
145            100,
146            colors="k",
147            linestyles="solid",
148            linewidths=0.1,
149            alpha=0.4,
150        )
151        ax3.plot([xsec, xsec], [0, r], "k", linewidth=2)
152        ax3.axis([0, c, 0, r])
153        ax3.set_aspect("equal", adjustable="box")
154        ax3.set_title("basal erosional surface")
155        ax3.tick_params(
156            bottom=False,
157            top=False,
158            left=False,
159            right=False,
160            labelbottom=False,
161            labelleft=False,
162        )
163        return fig1, fig2, fig3

method for plotting a cross section through a 3D model; also plots map of basal erosional surface and map of final geomorphic surface

Parameters
  • xsec: location of cross section along the x-axis (in pixel/ voxel coordinates)
  • colors: list of RGB values that define the colors for different facies
  • ve: vertical exaggeration
Returns

handles to the three figures

class ChannelBelt:
166class ChannelBelt:
167    """class for ChannelBelt objects"""
168
169    def __init__(self, channels, cutoffs, cl_times, cutoff_times):
170        """initialize ChannelBelt object
171
172        :param channels: list of Channel objects
173        :param cutoffs: list of Cutoff objects
174        :param cl_times: list of ages of Channel objects (in years)
175        :param cutoff_times: list of ages of Cutoff objects"""
176
177        self.channels = channels
178        self.cutoffs = cutoffs
179        self.cl_times = cl_times
180        self.cutoff_times = cutoff_times
181
182    def migrate(
183        self,
184        nit,
185        saved_ts,
186        deltas,
187        pad,
188        crdist,
189        Cf,
190        kl,
191        kv,
192        dt,
193        dens,
194        t1,
195        t2,
196        t3,
197        aggr_factor,
198        *D
199    ):
200        """method for computing migration rates along channel centerlines and moving the centerlines accordingly
201
202        :param nit: number of iterations
203        :param saved_ts: which time steps will be saved; e.g., if saved_ts = 10, every tenth time step will be saved
204        :param deltas: distance between nodes on centerline
205        :param pad: padding (number of nodepoints along centerline)
206        :param crdist: threshold distance at which cutoffs occur
207        :param Cf: dimensionless Chezy friction factor
208        :param kl: migration rate constant (m/s)
209        :param kv: vertical slope-dependent erosion rate constant (m/s)
210        :param dt: time step (s)
211        :param dens: density of fluid (kg/m3)
212        :param t1: time step when incision starts
213        :param t2: time step when lateral migration starts
214        :param t3: time step when aggradation starts
215        :param aggr_factor: aggradation factor
216        :param D: channel depth (m)"""
217
218        channel = self.channels[
219            -1
220        ]  # first channel is the same as last channel of input
221        x = channel.x
222        y = channel.y
223        z = channel.z
224        W = channel.W
225        if len(D) == 0:
226            D = channel.D
227        else:
228            D = D[0]
229        k = 1.0  # constant in HK equation
230        xc = []  # initialize cutoff coordinates
231        # determine age of last channel:
232        if len(self.cl_times) > 0:
233            last_cl_time = self.cl_times[-1]
234        else:
235            last_cl_time = 0
236        dx, dy, dz, ds, s = compute_derivatives(x, y, z)
237        slope = np.gradient(z) / ds
238        # padding at the beginning can be shorter than padding at the downstream end:
239        pad1 = int(pad / 10.0)
240        if pad1 < 5:
241            pad1 = 5
242        omega = (
243            -1.0
244        )  # constant in migration rate calculation (Howard and Knutson, 1984)
245        gamma = 2.5  # from Ikeda et al., 1981 and Howard and Knutson, 1984
246        for itn in trange(nit):  # main loop
247            # update_progress(itn/nit)
248            x, y = migrate_one_step(
249                x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma
250            )
251            # x, y = migrate_one_step_w_bias(x,y,z,W,kl,dt,k,Cf,D,pad,pad1,omega,gamma)
252            x, y, z, xc, yc, zc = cut_off_cutoffs(
253                x, y, z, s, crdist, deltas
254            )  # find and execute cutoffs
255            x, y, z, dx, dy, dz, ds, s = resample_centerline(
256                x, y, z, deltas
257            )  # resample centerline
258            slope = np.gradient(z) / ds
259            # for itn<=t1, z is unchanged; for itn>t1:
260            if (itn > t1) & (itn <= t2):  # incision
261                if np.min(np.abs(slope)) != 0:  # if slope is not zero
262                    z = z + kv * dens * 9.81 * D * slope * dt
263                else:
264                    z = z - kv * dens * 9.81 * D * dt * 0.05  # if slope is zero
265            if (itn > t2) & (itn <= t3):  # lateral migration
266                if np.min(np.abs(slope)) != 0:  # if slope is not zero
267                    # use the median slope to counterbalance incision:
268                    z = (
269                        z
270                        + kv * dens * 9.81 * D * slope * dt
271                        - kv * dens * 9.81 * D * np.median(slope) * dt
272                    )
273                else:
274                    z = z  # no change in z
275            if itn > t3:  # aggradation
276                if np.min(np.abs(slope)) != 0:  # if slope is not zero
277                    # 'aggr_factor' should be larger than 1 so that this leads to overall aggradation:
278                    z = (
279                        z
280                        + kv * dens * 9.81 * D * slope * dt
281                        - aggr_factor * kv * dens * 9.81 * D * np.mean(slope) * dt
282                    )
283                else:
284                    z = z + aggr_factor * dt
285            if len(xc) > 0:  # save cutoff data
286                self.cutoff_times.append(
287                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
288                )
289                cutoff = Cutoff(xc, yc, zc, W, D)  # create cutoff object
290                self.cutoffs.append(cutoff)
291            # saving centerlines:
292            if np.mod(itn, saved_ts) == 0:
293                self.cl_times.append(
294                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
295                )
296                channel = Channel(x, y, z, W, D)  # create channel object
297                self.channels.append(channel)
298
299    def plot(self, plot_type, pb_age, ob_age, end_time, n_channels):
300        """method  for plotting ChannelBelt object
301
302        :param plot_type: can be either 'strat' (for stratigraphic plot), 'morph' (for morphologic plot), or 'age' (for age plot)
303        :param pb_age: age of point bars (in years) at which they get covered by vegetation
304        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation
305        :param end_time: age of last channel to be plotted (in years)
306        :param n_channels: total number of channels (used in 'age' plots; can be larger than number of channels being plotted)
307        :return: handle to figure"""
308
309        cot = np.array(self.cutoff_times)
310        sclt = np.array(self.cl_times)
311        if end_time > 0:
312            cot = cot[cot <= end_time]
313            sclt = sclt[sclt <= end_time]
314        times = np.sort(np.hstack((cot, sclt)))
315        times = np.unique(times)
316        order = 0  # variable for ordering objects in plot
317        # set up min and max x and y coordinates of the plot:
318        xmin = np.min(self.channels[0].x)
319        xmax = np.max(self.channels[0].x)
320        ymax = 0
321        for i in range(len(self.channels)):
322            ymax = max(ymax, np.max(np.abs(self.channels[i].y)))
323        ymax = ymax + 2 * self.channels[0].W  # add a bit of space on top and bottom
324        ymin = -1 * ymax
325        # size figure so that its size matches the size of the model:
326        fig = plt.figure(figsize=(20, (ymax - ymin) * 20 / (xmax - xmin)))
327        if plot_type == "morph":
328            pb_crit = len(times[times < times[-1] - pb_age]) / float(len(times))
329            ob_crit = len(times[times < times[-1] - ob_age]) / float(len(times))
330            green = (106 / 255.0, 159 / 255.0, 67 / 255.0)  # vegetation color
331            pb_color = (189 / 255.0, 153 / 255.0, 148 / 255.0)  # point bar color
332            ob_color = (15 / 255.0, 58 / 255.0, 65 / 255.0)  # oxbow color
333            pb_cmap = make_colormap(
334                [green, green, pb_crit, green, pb_color, 1.0, pb_color]
335            )  # colormap for point bars
336            ob_cmap = make_colormap(
337                [green, green, ob_crit, green, ob_color, 1.0, ob_color]
338            )  # colormap for oxbows
339            plt.fill(
340                [xmin, xmax, xmax, xmin],
341                [ymin, ymin, ymax, ymax],
342                color=(106 / 255.0, 159 / 255.0, 67 / 255.0),
343            )
344        if plot_type == "age":
345            age_cmap = cm.get_cmap("magma", n_channels)
346        for i in range(0, len(times)):
347            if times[i] in sclt:
348                ind = np.where(sclt == times[i])[0][0]
349                x1 = self.channels[ind].x
350                y1 = self.channels[ind].y
351                W = self.channels[ind].W
352                xm, ym = get_channel_banks(x1, y1, W)
353                if plot_type == "morph":
354                    if times[i] > times[-1] - pb_age:
355                        plt.fill(
356                            xm,
357                            ym,
358                            facecolor=pb_cmap(i / float(len(times) - 1)),
359                            edgecolor="k",
360                            linewidth=0.2,
361                        )
362                    else:
363                        plt.fill(xm, ym, facecolor=pb_cmap(i / float(len(times) - 1)))
364                if plot_type == "strat":
365                    order += 1
366                    plt.fill(
367                        xm,
368                        ym,
369                        "xkcd:light tan",
370                        edgecolor="k",
371                        linewidth=0.25,
372                        zorder=order,
373                    )
374                if plot_type == "age":
375                    order += 1
376                    plt.fill(
377                        xm,
378                        ym,
379                        facecolor=age_cmap(i / float(n_channels - 1)),
380                        edgecolor="k",
381                        linewidth=0.1,
382                        zorder=order,
383                    )
384            if times[i] in cot:
385                ind = np.where(cot == times[i])[0][0]
386                for j in range(0, len(self.cutoffs[ind].x)):
387                    x1 = self.cutoffs[ind].x[j]
388                    y1 = self.cutoffs[ind].y[j]
389                    xm, ym = get_channel_banks(x1, y1, self.cutoffs[ind].W)
390                    if plot_type == "morph":
391                        plt.fill(xm, ym, color=ob_cmap(i / float(len(times) - 1)))
392                    if plot_type == "strat":
393                        order = order + 1
394                        plt.fill(
395                            xm,
396                            ym,
397                            "xkcd:ocean blue",
398                            edgecolor="k",
399                            linewidth=0.25,
400                            zorder=order,
401                        )
402                    if plot_type == "age":
403                        order += 1
404                        plt.fill(
405                            xm,
406                            ym,
407                            "xkcd:sea blue",
408                            edgecolor="k",
409                            linewidth=0.1,
410                            zorder=order,
411                        )
412
413        x1 = self.channels[len(sclt) - 1].x
414        y1 = self.channels[len(sclt) - 1].y
415        xm, ym = get_channel_banks(x1, y1, self.channels[len(sclt) - 1].W)
416        order = order + 1
417        if plot_type == "age":
418            plt.fill(
419                xm,
420                ym,
421                color="xkcd:sea blue",
422                zorder=order,
423                edgecolor="k",
424                linewidth=0.1,
425            )
426        else:
427            plt.fill(
428                xm, ym, color=(16 / 255.0, 73 / 255.0, 90 / 255.0), zorder=order
429            )  # ,edgecolor='k')
430        plt.axis("equal")
431        plt.xlim(xmin, xmax)
432        plt.ylim(ymin, ymax)
433        return fig
434
435    def create_movie(
436        self,
437        xmin,
438        xmax,
439        plot_type,
440        filename,
441        dirname,
442        pb_age,
443        ob_age,
444        end_time,
445        n_channels,
446    ):
447        """method for creating movie frames (PNG files) that capture the plan-view evolution of a channel belt through time
448        movie has to be assembled from the PNG file after this method is applied
449
450        :param xmin: value of x coodinate on the left side of frame
451        :param xmax: value of x coordinate on right side of frame
452        :param plot_type: plot type; can be either 'strat' (for stratigraphic plot) or 'morph' (for morphologic plot)
453        :param filename: first few characters of the output filenames
454        :param dirname: name of directory where output files should be written
455        :param pb_age: age of point bars (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
456        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
457        :param scale: scaling factor (e.g., 2) that determines how many times larger you want the frame to be, compared to the default scaling of the figure
458        :param end_time: time at which simulation should be stopped
459        :param n_channels: total number of channels + cutoffs for which simulation is run (usually it is len(chb.cutoffs) + len(chb.channels)). Used when plot_type = 'age'"""
460
461        sclt = np.array(self.cl_times)
462        if type(end_time) != list:
463            sclt = sclt[sclt <= end_time]
464        channels = self.channels[: len(sclt)]
465        ymax = 0
466        for i in range(len(channels)):
467            ymax = max(ymax, np.max(np.abs(channels[i].y)))
468        ymax = ymax + 2 * channels[0].W  # add a bit of space on top and bottom
469        ymin = -1 * ymax
470        for i in range(0, len(sclt)):
471            fig = self.plot(plot_type, pb_age, ob_age, sclt[i], n_channels)
472            scale = 1
473            fig_height = scale * fig.get_figheight()
474            fig_width = (xmax - xmin) * fig_height / (ymax - ymin)
475            fig.set_figwidth(fig_width)
476            fig.set_figheight(fig_height)
477            fig.gca().set_xlim(xmin, xmax)
478            fig.gca().set_xticks([])
479            fig.gca().set_yticks([])
480            plt.plot(
481                [xmin + 200, xmin + 200 + 5000],
482                [ymin + 200, ymin + 200],
483                "k",
484                linewidth=2,
485            )
486            plt.text(xmin + 200 + 2000, ymin + 200 + 100, "5 km", fontsize=14)
487            fname = dirname + filename + "%03d.png" % (i)
488            fig.savefig(fname, bbox_inches="tight")
489            plt.close()
490
491    def build_3d_model(
492        self,
493        model_type,
494        h_mud,
495        levee_width,
496        h,
497        w,
498        bth,
499        dcr,
500        dx,
501        delta_s,
502        starttime,
503        endtime,
504        xmin,
505        xmax,
506        ymin,
507        ymax,
508    ):
509        """method for building 3D model from set of centerlines (that are part of a ChannelBelt object)
510
511        :param model_type: model type ('fluvial' or 'submarine')
512        :param h_mud: maximum thickness of overbank mud
513        :param levee_width: width of overbank mud
514        :param h: channel depth
515        :param w: channel width
516        :param bth: thickness of channel sand (only used in submarine models)
517        :param dcr: critical channel depth where sand thickness goes to zero (only used in submarine models)
518        :param dx: cell size in x and y directions
519        :param delta_s: sampling distance alogn centerlines
520        :param starttime: age of centerline that will be used as the first centerline in the model
521        :param endtime: age of centerline that will be used as the last centerline in the model
522        :param xmin: minimum x coordinate that defines the model domain; if xmin is set to zero,
523        a plot of the centerlines is generated and the model domain has to be defined by clicking its upper left and lower right corners
524        :param xmax: maximum x coordinate that defines the model domain
525        :param ymin: minimum y coordinate that defines the model domain
526        :param ymax: maximum y coordinate that defines the model domain
527        :return chb_3d: a ChannelBelt3D object
528        :return xmin, xmax, ymin, ymax: x and y coordinates that define the model domain (so that they can be reused later)"""
529
530        sclt = np.array(self.cl_times)
531        ind1 = np.where(sclt >= starttime)[0][0]
532        ind2 = np.where(sclt <= endtime)[0][-1]
533        sclt = sclt[ind1 : ind2 + 1]
534        channels = self.channels[ind1 : ind2 + 1]
535        cot = np.array(self.cutoff_times)
536        if (
537            (len(cot) > 0)
538            & (len(np.where(cot >= starttime)[0]) > 0)
539            & (len(np.where(cot <= endtime)[0]) > 0)
540        ):
541            cfind1 = np.where(cot >= starttime)[0][0]
542            cfind2 = np.where(cot <= endtime)[0][-1]
543            cot = cot[cfind1 : cfind2 + 1]
544            cutoffs = self.cutoffs[cfind1 : cfind2 + 1]
545        else:
546            cot = []
547            cutoffs = []
548        n_steps = len(sclt)  # number of events
549        if xmin == 0:  # plot centerlines and define model domain
550            plt.figure(figsize=(15, 4))
551            maxX, minY, maxY = 0, 0, 0
552            for i in range(n_steps):  # plot centerlines
553                plt.plot(channels[i].x, channels[i].y, "k")
554                maxX = max(maxX, np.max(channels[i].x))
555                maxY = max(maxY, np.max(channels[i].y))
556                minY = min(minY, np.min(channels[i].y))
557            plt.axis([0, maxX, minY - 10 * w, maxY + 10 * w])
558            plt.gca().set_aspect("equal", adjustable="box")
559            plt.tight_layout()
560            pts = np.zeros((2, 2))
561            for i in range(0, 2):
562                pt = np.asarray(plt.ginput(1))
563                pts[i, :] = pt
564                plt.scatter(pt[0][0], pt[0][1])
565            plt.plot(
566                [pts[0, 0], pts[1, 0], pts[1, 0], pts[0, 0], pts[0, 0]],
567                [pts[0, 1], pts[0, 1], pts[1, 1], pts[1, 1], pts[0, 1]],
568                "r",
569            )
570            xmin = min(pts[0, 0], pts[1, 0])
571            xmax = max(pts[0, 0], pts[1, 0])
572            ymin = min(pts[0, 1], pts[1, 1])
573            ymax = max(pts[0, 1], pts[1, 1])
574        iwidth = int((xmax - xmin) / dx)
575        iheight = int((ymax - ymin) / dx)
576        topo = np.zeros(
577            (iheight, iwidth, 4 * n_steps)
578        )  # array for storing topographic surfaces
579        dists = np.zeros((iheight, iwidth, n_steps))
580        zmaps = np.zeros((iheight, iwidth, n_steps))
581        facies = np.zeros((4 * n_steps, 1))
582        # create initial topography:
583        x1 = np.linspace(0, iwidth - 1, iwidth)
584        y1 = np.linspace(0, iheight - 1, iheight)
585        xv, yv = np.meshgrid(x1, y1)
586        z1 = channels[0].z
587        z1 = z1[(channels[0].x > xmin) & (channels[0].x < xmax)]
588        topoinit = (
589            z1[0] - ((z1[0] - z1[-1]) / (xmax - xmin)) * xv * dx
590        )  # initial (sloped) topography
591        topo[:, :, 0] = topoinit.copy()
592        surf = topoinit.copy()
593        facies[0] = np.NaN
594        # generate surfaces:
595        channels3D = []
596        for i in trange(n_steps):
597            x = channels[i].x
598            y = channels[i].y
599            z = channels[i].z
600            cutoff_ind = []
601            # check if there were cutoffs during the last time step and collect indices in an array:
602            for j in range(len(cot)):
603                if (cot[j] >= sclt[i - 1]) & (cot[j] < sclt[i]):
604                    cutoff_ind = np.append(cutoff_ind, j)
605            # create distance map:
606            cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x1, y1, z1 = dist_map(
607                x, y, z, xmin, xmax, ymin, ymax, dx, delta_s
608            )
609            if i == 0:
610                cl_dist_prev = cl_dist
611            # erosion:
612            surf = np.minimum(surf, erosion_surface(h, w / dx, cl_dist, z_map))
613            topo[:, :, 4 * i] = surf  # erosional surface
614            dists[:, :, i] = cl_dist
615            zmaps[:, :, i] = z_map
616            facies[4 * i] = np.NaN
617
618            if model_type == "fluvial":
619                pb = point_bar_surface(cl_dist, z_map, h, w / dx)
620                th = np.maximum(surf, pb) - surf
621                th_oxbows = th.copy()
622                # setting sand thickness to zero at cutoff locations:
623                if len(cutoff_ind) > 0:
624                    cutoff_dists = 1e10 * np.ones(
625                        np.shape(th)
626                    )  # initialize cutoff_dists with a large number
627                    for j in range(len(cutoff_ind)):
628                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
629                            cutoffs[int(cutoff_ind[j])].x[0],
630                            cutoffs[int(cutoff_ind[j])].y[0],
631                            cutoffs[int(cutoff_ind[j])].z[0],
632                            xmin,
633                            xmax,
634                            ymin,
635                            ymax,
636                            dx,
637                        )
638                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
639                    th_oxbows[
640                        cutoff_dists >= 0.9 * w / dx
641                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
642                    th[
643                        cutoff_dists < 0.9 * w / dx
644                    ] = 0  # set point bar thickness to zero inside of oxbows
645                else:  # no cutoffs
646                    th_oxbows = np.zeros(np.shape(th))
647                th[th < 0] = 0  # eliminate negative th values
648                surf = (
649                    surf + th_oxbows
650                )  # update topographic surface with oxbow deposit thickness
651                topo[:, :, 4 * i + 1] = surf  # top of oxbow mud
652                facies[4 * i + 1] = 0
653                surf = surf + th  # update topographic surface with sand thickness
654                topo[:, :, 4 * i + 2] = surf  # top of sand
655                facies[4 * i + 2] = 1
656                surf = surf + mud_surface(
657                    h_mud, levee_width / dx, cl_dist, w / dx, z_map, surf
658                )  # mud/levee deposition
659                topo[:, :, 4 * i + 3] = surf  # top of levee
660                facies[4 * i + 3] = 2
661                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
662
663            if model_type == "submarine":
664                surf = surf + mud_surface(
665                    h_mud[i], levee_width / dx, cl_dist, w / dx, z_map, surf
666                )  # mud/levee deposition
667                topo[:, :, 4 * i + 1] = surf  # top of levee
668                facies[4 * i + 1] = 2
669                # sand thickness:
670                th, relief = sand_surface(surf, bth, dcr, z_map, h)
671                th[th < 0] = 0  # eliminate negative th values
672                th[cl_dist > 1.0 * w / dx] = 0  # eliminate sand outside of channel
673                th_oxbows = th.copy()
674                # setting sand thickness to zero at cutoff locations:
675                if len(cutoff_ind) > 0:
676                    cutoff_dists = 1e10 * np.ones(
677                        np.shape(th)
678                    )  # initialize cutoff_dists with a large number
679                    for j in range(len(cutoff_ind)):
680                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
681                            cutoffs[int(cutoff_ind[j])].x[0],
682                            cutoffs[int(cutoff_ind[j])].y[0],
683                            cutoffs[int(cutoff_ind[j])].z[0],
684                            xmin,
685                            xmax,
686                            ymin,
687                            ymax,
688                            dx,
689                        )
690                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
691                    th_oxbows[
692                        cutoff_dists >= 0.9 * w / dx
693                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
694                    th[
695                        cutoff_dists < 0.9 * w / dx
696                    ] = 0  # set point bar thickness to zero inside of oxbows
697                    # adding back sand near the channel axis (submarine only):
698                    # th[cl_dist<0.5*w/dx] = bth*(1 - relief[cl_dist<0.5*w/dx]/dcr)
699                else:  # no cutoffs
700                    th_oxbows = np.zeros(np.shape(th))
701                surf = (
702                    surf + th_oxbows
703                )  # update topographic surface with oxbow deposit thickness
704                topo[:, :, 4 * i + 2] = surf  # top of oxbow mud
705                facies[4 * i + 2] = 0
706                surf = surf + th  # update topographic surface with sand thickness
707                topo[:, :, 4 * i + 3] = surf  # top of sand
708                facies[4 * i + 3] = 1
709                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
710
711            cl_dist_prev = cl_dist.copy()
712        topo = np.concatenate(
713            (np.reshape(topoinit, (iheight, iwidth, 1)), topo), axis=2
714        )  # add initial topography to array
715        strat = topostrat(topo)  # create stratigraphic surfaces
716        strat = np.delete(
717            strat, np.arange(4 * n_steps + 1)[1::4], 2
718        )  # get rid of unnecessary stratigraphic surfaces (duplicates)
719        facies = np.delete(
720            facies, np.arange(4 * n_steps)[::4]
721        )  # get rid of unnecessary facies layers (NaNs)
722        if model_type == "fluvial":
723            facies_code = {0: "oxbow", 1: "point bar", 2: "levee"}
724        if model_type == "submarine":
725            facies_code = {0: "oxbow", 1: "channel sand", 2: "levee"}
726        chb_3d = ChannelBelt3D(
727            model_type, topo, strat, facies, facies_code, dx, channels3D
728        )
729        return chb_3d, xmin, xmax, ymin, ymax  # , dists, zmaps

class for ChannelBelt objects

ChannelBelt(channels, cutoffs, cl_times, cutoff_times)
169    def __init__(self, channels, cutoffs, cl_times, cutoff_times):
170        """initialize ChannelBelt object
171
172        :param channels: list of Channel objects
173        :param cutoffs: list of Cutoff objects
174        :param cl_times: list of ages of Channel objects (in years)
175        :param cutoff_times: list of ages of Cutoff objects"""
176
177        self.channels = channels
178        self.cutoffs = cutoffs
179        self.cl_times = cl_times
180        self.cutoff_times = cutoff_times

initialize ChannelBelt object

Parameters
  • channels: list of Channel objects
  • cutoffs: list of Cutoff objects
  • cl_times: list of ages of Channel objects (in years)
  • cutoff_times: list of ages of Cutoff objects
def migrate( self, nit, saved_ts, deltas, pad, crdist, Cf, kl, kv, dt, dens, t1, t2, t3, aggr_factor, *D):
182    def migrate(
183        self,
184        nit,
185        saved_ts,
186        deltas,
187        pad,
188        crdist,
189        Cf,
190        kl,
191        kv,
192        dt,
193        dens,
194        t1,
195        t2,
196        t3,
197        aggr_factor,
198        *D
199    ):
200        """method for computing migration rates along channel centerlines and moving the centerlines accordingly
201
202        :param nit: number of iterations
203        :param saved_ts: which time steps will be saved; e.g., if saved_ts = 10, every tenth time step will be saved
204        :param deltas: distance between nodes on centerline
205        :param pad: padding (number of nodepoints along centerline)
206        :param crdist: threshold distance at which cutoffs occur
207        :param Cf: dimensionless Chezy friction factor
208        :param kl: migration rate constant (m/s)
209        :param kv: vertical slope-dependent erosion rate constant (m/s)
210        :param dt: time step (s)
211        :param dens: density of fluid (kg/m3)
212        :param t1: time step when incision starts
213        :param t2: time step when lateral migration starts
214        :param t3: time step when aggradation starts
215        :param aggr_factor: aggradation factor
216        :param D: channel depth (m)"""
217
218        channel = self.channels[
219            -1
220        ]  # first channel is the same as last channel of input
221        x = channel.x
222        y = channel.y
223        z = channel.z
224        W = channel.W
225        if len(D) == 0:
226            D = channel.D
227        else:
228            D = D[0]
229        k = 1.0  # constant in HK equation
230        xc = []  # initialize cutoff coordinates
231        # determine age of last channel:
232        if len(self.cl_times) > 0:
233            last_cl_time = self.cl_times[-1]
234        else:
235            last_cl_time = 0
236        dx, dy, dz, ds, s = compute_derivatives(x, y, z)
237        slope = np.gradient(z) / ds
238        # padding at the beginning can be shorter than padding at the downstream end:
239        pad1 = int(pad / 10.0)
240        if pad1 < 5:
241            pad1 = 5
242        omega = (
243            -1.0
244        )  # constant in migration rate calculation (Howard and Knutson, 1984)
245        gamma = 2.5  # from Ikeda et al., 1981 and Howard and Knutson, 1984
246        for itn in trange(nit):  # main loop
247            # update_progress(itn/nit)
248            x, y = migrate_one_step(
249                x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma
250            )
251            # x, y = migrate_one_step_w_bias(x,y,z,W,kl,dt,k,Cf,D,pad,pad1,omega,gamma)
252            x, y, z, xc, yc, zc = cut_off_cutoffs(
253                x, y, z, s, crdist, deltas
254            )  # find and execute cutoffs
255            x, y, z, dx, dy, dz, ds, s = resample_centerline(
256                x, y, z, deltas
257            )  # resample centerline
258            slope = np.gradient(z) / ds
259            # for itn<=t1, z is unchanged; for itn>t1:
260            if (itn > t1) & (itn <= t2):  # incision
261                if np.min(np.abs(slope)) != 0:  # if slope is not zero
262                    z = z + kv * dens * 9.81 * D * slope * dt
263                else:
264                    z = z - kv * dens * 9.81 * D * dt * 0.05  # if slope is zero
265            if (itn > t2) & (itn <= t3):  # lateral migration
266                if np.min(np.abs(slope)) != 0:  # if slope is not zero
267                    # use the median slope to counterbalance incision:
268                    z = (
269                        z
270                        + kv * dens * 9.81 * D * slope * dt
271                        - kv * dens * 9.81 * D * np.median(slope) * dt
272                    )
273                else:
274                    z = z  # no change in z
275            if itn > t3:  # aggradation
276                if np.min(np.abs(slope)) != 0:  # if slope is not zero
277                    # 'aggr_factor' should be larger than 1 so that this leads to overall aggradation:
278                    z = (
279                        z
280                        + kv * dens * 9.81 * D * slope * dt
281                        - aggr_factor * kv * dens * 9.81 * D * np.mean(slope) * dt
282                    )
283                else:
284                    z = z + aggr_factor * dt
285            if len(xc) > 0:  # save cutoff data
286                self.cutoff_times.append(
287                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
288                )
289                cutoff = Cutoff(xc, yc, zc, W, D)  # create cutoff object
290                self.cutoffs.append(cutoff)
291            # saving centerlines:
292            if np.mod(itn, saved_ts) == 0:
293                self.cl_times.append(
294                    last_cl_time + (itn + 1) * dt / (365 * 24 * 60 * 60.0)
295                )
296                channel = Channel(x, y, z, W, D)  # create channel object
297                self.channels.append(channel)

method for computing migration rates along channel centerlines and moving the centerlines accordingly

Parameters
  • nit: number of iterations
  • saved_ts: which time steps will be saved; e.g., if saved_ts = 10, every tenth time step will be saved
  • deltas: distance between nodes on centerline
  • pad: padding (number of nodepoints along centerline)
  • crdist: threshold distance at which cutoffs occur
  • Cf: dimensionless Chezy friction factor
  • kl: migration rate constant (m/s)
  • kv: vertical slope-dependent erosion rate constant (m/s)
  • dt: time step (s)
  • dens: density of fluid (kg/m3)
  • t1: time step when incision starts
  • t2: time step when lateral migration starts
  • t3: time step when aggradation starts
  • aggr_factor: aggradation factor
  • D: channel depth (m)
def plot(self, plot_type, pb_age, ob_age, end_time, n_channels):
299    def plot(self, plot_type, pb_age, ob_age, end_time, n_channels):
300        """method  for plotting ChannelBelt object
301
302        :param plot_type: can be either 'strat' (for stratigraphic plot), 'morph' (for morphologic plot), or 'age' (for age plot)
303        :param pb_age: age of point bars (in years) at which they get covered by vegetation
304        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation
305        :param end_time: age of last channel to be plotted (in years)
306        :param n_channels: total number of channels (used in 'age' plots; can be larger than number of channels being plotted)
307        :return: handle to figure"""
308
309        cot = np.array(self.cutoff_times)
310        sclt = np.array(self.cl_times)
311        if end_time > 0:
312            cot = cot[cot <= end_time]
313            sclt = sclt[sclt <= end_time]
314        times = np.sort(np.hstack((cot, sclt)))
315        times = np.unique(times)
316        order = 0  # variable for ordering objects in plot
317        # set up min and max x and y coordinates of the plot:
318        xmin = np.min(self.channels[0].x)
319        xmax = np.max(self.channels[0].x)
320        ymax = 0
321        for i in range(len(self.channels)):
322            ymax = max(ymax, np.max(np.abs(self.channels[i].y)))
323        ymax = ymax + 2 * self.channels[0].W  # add a bit of space on top and bottom
324        ymin = -1 * ymax
325        # size figure so that its size matches the size of the model:
326        fig = plt.figure(figsize=(20, (ymax - ymin) * 20 / (xmax - xmin)))
327        if plot_type == "morph":
328            pb_crit = len(times[times < times[-1] - pb_age]) / float(len(times))
329            ob_crit = len(times[times < times[-1] - ob_age]) / float(len(times))
330            green = (106 / 255.0, 159 / 255.0, 67 / 255.0)  # vegetation color
331            pb_color = (189 / 255.0, 153 / 255.0, 148 / 255.0)  # point bar color
332            ob_color = (15 / 255.0, 58 / 255.0, 65 / 255.0)  # oxbow color
333            pb_cmap = make_colormap(
334                [green, green, pb_crit, green, pb_color, 1.0, pb_color]
335            )  # colormap for point bars
336            ob_cmap = make_colormap(
337                [green, green, ob_crit, green, ob_color, 1.0, ob_color]
338            )  # colormap for oxbows
339            plt.fill(
340                [xmin, xmax, xmax, xmin],
341                [ymin, ymin, ymax, ymax],
342                color=(106 / 255.0, 159 / 255.0, 67 / 255.0),
343            )
344        if plot_type == "age":
345            age_cmap = cm.get_cmap("magma", n_channels)
346        for i in range(0, len(times)):
347            if times[i] in sclt:
348                ind = np.where(sclt == times[i])[0][0]
349                x1 = self.channels[ind].x
350                y1 = self.channels[ind].y
351                W = self.channels[ind].W
352                xm, ym = get_channel_banks(x1, y1, W)
353                if plot_type == "morph":
354                    if times[i] > times[-1] - pb_age:
355                        plt.fill(
356                            xm,
357                            ym,
358                            facecolor=pb_cmap(i / float(len(times) - 1)),
359                            edgecolor="k",
360                            linewidth=0.2,
361                        )
362                    else:
363                        plt.fill(xm, ym, facecolor=pb_cmap(i / float(len(times) - 1)))
364                if plot_type == "strat":
365                    order += 1
366                    plt.fill(
367                        xm,
368                        ym,
369                        "xkcd:light tan",
370                        edgecolor="k",
371                        linewidth=0.25,
372                        zorder=order,
373                    )
374                if plot_type == "age":
375                    order += 1
376                    plt.fill(
377                        xm,
378                        ym,
379                        facecolor=age_cmap(i / float(n_channels - 1)),
380                        edgecolor="k",
381                        linewidth=0.1,
382                        zorder=order,
383                    )
384            if times[i] in cot:
385                ind = np.where(cot == times[i])[0][0]
386                for j in range(0, len(self.cutoffs[ind].x)):
387                    x1 = self.cutoffs[ind].x[j]
388                    y1 = self.cutoffs[ind].y[j]
389                    xm, ym = get_channel_banks(x1, y1, self.cutoffs[ind].W)
390                    if plot_type == "morph":
391                        plt.fill(xm, ym, color=ob_cmap(i / float(len(times) - 1)))
392                    if plot_type == "strat":
393                        order = order + 1
394                        plt.fill(
395                            xm,
396                            ym,
397                            "xkcd:ocean blue",
398                            edgecolor="k",
399                            linewidth=0.25,
400                            zorder=order,
401                        )
402                    if plot_type == "age":
403                        order += 1
404                        plt.fill(
405                            xm,
406                            ym,
407                            "xkcd:sea blue",
408                            edgecolor="k",
409                            linewidth=0.1,
410                            zorder=order,
411                        )
412
413        x1 = self.channels[len(sclt) - 1].x
414        y1 = self.channels[len(sclt) - 1].y
415        xm, ym = get_channel_banks(x1, y1, self.channels[len(sclt) - 1].W)
416        order = order + 1
417        if plot_type == "age":
418            plt.fill(
419                xm,
420                ym,
421                color="xkcd:sea blue",
422                zorder=order,
423                edgecolor="k",
424                linewidth=0.1,
425            )
426        else:
427            plt.fill(
428                xm, ym, color=(16 / 255.0, 73 / 255.0, 90 / 255.0), zorder=order
429            )  # ,edgecolor='k')
430        plt.axis("equal")
431        plt.xlim(xmin, xmax)
432        plt.ylim(ymin, ymax)
433        return fig

method for plotting ChannelBelt object

Parameters
  • plot_type: can be either 'strat' (for stratigraphic plot), 'morph' (for morphologic plot), or 'age' (for age plot)
  • pb_age: age of point bars (in years) at which they get covered by vegetation
  • ob_age: age of oxbow lakes (in years) at which they get covered by vegetation
  • end_time: age of last channel to be plotted (in years)
  • n_channels: total number of channels (used in 'age' plots; can be larger than number of channels being plotted)
Returns

handle to figure

def create_movie( self, xmin, xmax, plot_type, filename, dirname, pb_age, ob_age, end_time, n_channels):
435    def create_movie(
436        self,
437        xmin,
438        xmax,
439        plot_type,
440        filename,
441        dirname,
442        pb_age,
443        ob_age,
444        end_time,
445        n_channels,
446    ):
447        """method for creating movie frames (PNG files) that capture the plan-view evolution of a channel belt through time
448        movie has to be assembled from the PNG file after this method is applied
449
450        :param xmin: value of x coodinate on the left side of frame
451        :param xmax: value of x coordinate on right side of frame
452        :param plot_type: plot type; can be either 'strat' (for stratigraphic plot) or 'morph' (for morphologic plot)
453        :param filename: first few characters of the output filenames
454        :param dirname: name of directory where output files should be written
455        :param pb_age: age of point bars (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
456        :param ob_age: age of oxbow lakes (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
457        :param scale: scaling factor (e.g., 2) that determines how many times larger you want the frame to be, compared to the default scaling of the figure
458        :param end_time: time at which simulation should be stopped
459        :param n_channels: total number of channels + cutoffs for which simulation is run (usually it is len(chb.cutoffs) + len(chb.channels)). Used when plot_type = 'age'"""
460
461        sclt = np.array(self.cl_times)
462        if type(end_time) != list:
463            sclt = sclt[sclt <= end_time]
464        channels = self.channels[: len(sclt)]
465        ymax = 0
466        for i in range(len(channels)):
467            ymax = max(ymax, np.max(np.abs(channels[i].y)))
468        ymax = ymax + 2 * channels[0].W  # add a bit of space on top and bottom
469        ymin = -1 * ymax
470        for i in range(0, len(sclt)):
471            fig = self.plot(plot_type, pb_age, ob_age, sclt[i], n_channels)
472            scale = 1
473            fig_height = scale * fig.get_figheight()
474            fig_width = (xmax - xmin) * fig_height / (ymax - ymin)
475            fig.set_figwidth(fig_width)
476            fig.set_figheight(fig_height)
477            fig.gca().set_xlim(xmin, xmax)
478            fig.gca().set_xticks([])
479            fig.gca().set_yticks([])
480            plt.plot(
481                [xmin + 200, xmin + 200 + 5000],
482                [ymin + 200, ymin + 200],
483                "k",
484                linewidth=2,
485            )
486            plt.text(xmin + 200 + 2000, ymin + 200 + 100, "5 km", fontsize=14)
487            fname = dirname + filename + "%03d.png" % (i)
488            fig.savefig(fname, bbox_inches="tight")
489            plt.close()

method for creating movie frames (PNG files) that capture the plan-view evolution of a channel belt through time movie has to be assembled from the PNG file after this method is applied

Parameters
  • xmin: value of x coodinate on the left side of frame
  • xmax: value of x coordinate on right side of frame
  • plot_type: plot type; can be either 'strat' (for stratigraphic plot) or 'morph' (for morphologic plot)
  • filename: first few characters of the output filenames
  • dirname: name of directory where output files should be written
  • pb_age: age of point bars (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
  • ob_age: age of oxbow lakes (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type')
  • scale: scaling factor (e.g., 2) that determines how many times larger you want the frame to be, compared to the default scaling of the figure
  • end_time: time at which simulation should be stopped
  • n_channels: total number of channels + cutoffs for which simulation is run (usually it is len(chb.cutoffs) + len(chb.channels)). Used when plot_type = 'age'
def build_3d_model( self, model_type, h_mud, levee_width, h, w, bth, dcr, dx, delta_s, starttime, endtime, xmin, xmax, ymin, ymax):
491    def build_3d_model(
492        self,
493        model_type,
494        h_mud,
495        levee_width,
496        h,
497        w,
498        bth,
499        dcr,
500        dx,
501        delta_s,
502        starttime,
503        endtime,
504        xmin,
505        xmax,
506        ymin,
507        ymax,
508    ):
509        """method for building 3D model from set of centerlines (that are part of a ChannelBelt object)
510
511        :param model_type: model type ('fluvial' or 'submarine')
512        :param h_mud: maximum thickness of overbank mud
513        :param levee_width: width of overbank mud
514        :param h: channel depth
515        :param w: channel width
516        :param bth: thickness of channel sand (only used in submarine models)
517        :param dcr: critical channel depth where sand thickness goes to zero (only used in submarine models)
518        :param dx: cell size in x and y directions
519        :param delta_s: sampling distance alogn centerlines
520        :param starttime: age of centerline that will be used as the first centerline in the model
521        :param endtime: age of centerline that will be used as the last centerline in the model
522        :param xmin: minimum x coordinate that defines the model domain; if xmin is set to zero,
523        a plot of the centerlines is generated and the model domain has to be defined by clicking its upper left and lower right corners
524        :param xmax: maximum x coordinate that defines the model domain
525        :param ymin: minimum y coordinate that defines the model domain
526        :param ymax: maximum y coordinate that defines the model domain
527        :return chb_3d: a ChannelBelt3D object
528        :return xmin, xmax, ymin, ymax: x and y coordinates that define the model domain (so that they can be reused later)"""
529
530        sclt = np.array(self.cl_times)
531        ind1 = np.where(sclt >= starttime)[0][0]
532        ind2 = np.where(sclt <= endtime)[0][-1]
533        sclt = sclt[ind1 : ind2 + 1]
534        channels = self.channels[ind1 : ind2 + 1]
535        cot = np.array(self.cutoff_times)
536        if (
537            (len(cot) > 0)
538            & (len(np.where(cot >= starttime)[0]) > 0)
539            & (len(np.where(cot <= endtime)[0]) > 0)
540        ):
541            cfind1 = np.where(cot >= starttime)[0][0]
542            cfind2 = np.where(cot <= endtime)[0][-1]
543            cot = cot[cfind1 : cfind2 + 1]
544            cutoffs = self.cutoffs[cfind1 : cfind2 + 1]
545        else:
546            cot = []
547            cutoffs = []
548        n_steps = len(sclt)  # number of events
549        if xmin == 0:  # plot centerlines and define model domain
550            plt.figure(figsize=(15, 4))
551            maxX, minY, maxY = 0, 0, 0
552            for i in range(n_steps):  # plot centerlines
553                plt.plot(channels[i].x, channels[i].y, "k")
554                maxX = max(maxX, np.max(channels[i].x))
555                maxY = max(maxY, np.max(channels[i].y))
556                minY = min(minY, np.min(channels[i].y))
557            plt.axis([0, maxX, minY - 10 * w, maxY + 10 * w])
558            plt.gca().set_aspect("equal", adjustable="box")
559            plt.tight_layout()
560            pts = np.zeros((2, 2))
561            for i in range(0, 2):
562                pt = np.asarray(plt.ginput(1))
563                pts[i, :] = pt
564                plt.scatter(pt[0][0], pt[0][1])
565            plt.plot(
566                [pts[0, 0], pts[1, 0], pts[1, 0], pts[0, 0], pts[0, 0]],
567                [pts[0, 1], pts[0, 1], pts[1, 1], pts[1, 1], pts[0, 1]],
568                "r",
569            )
570            xmin = min(pts[0, 0], pts[1, 0])
571            xmax = max(pts[0, 0], pts[1, 0])
572            ymin = min(pts[0, 1], pts[1, 1])
573            ymax = max(pts[0, 1], pts[1, 1])
574        iwidth = int((xmax - xmin) / dx)
575        iheight = int((ymax - ymin) / dx)
576        topo = np.zeros(
577            (iheight, iwidth, 4 * n_steps)
578        )  # array for storing topographic surfaces
579        dists = np.zeros((iheight, iwidth, n_steps))
580        zmaps = np.zeros((iheight, iwidth, n_steps))
581        facies = np.zeros((4 * n_steps, 1))
582        # create initial topography:
583        x1 = np.linspace(0, iwidth - 1, iwidth)
584        y1 = np.linspace(0, iheight - 1, iheight)
585        xv, yv = np.meshgrid(x1, y1)
586        z1 = channels[0].z
587        z1 = z1[(channels[0].x > xmin) & (channels[0].x < xmax)]
588        topoinit = (
589            z1[0] - ((z1[0] - z1[-1]) / (xmax - xmin)) * xv * dx
590        )  # initial (sloped) topography
591        topo[:, :, 0] = topoinit.copy()
592        surf = topoinit.copy()
593        facies[0] = np.NaN
594        # generate surfaces:
595        channels3D = []
596        for i in trange(n_steps):
597            x = channels[i].x
598            y = channels[i].y
599            z = channels[i].z
600            cutoff_ind = []
601            # check if there were cutoffs during the last time step and collect indices in an array:
602            for j in range(len(cot)):
603                if (cot[j] >= sclt[i - 1]) & (cot[j] < sclt[i]):
604                    cutoff_ind = np.append(cutoff_ind, j)
605            # create distance map:
606            cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x1, y1, z1 = dist_map(
607                x, y, z, xmin, xmax, ymin, ymax, dx, delta_s
608            )
609            if i == 0:
610                cl_dist_prev = cl_dist
611            # erosion:
612            surf = np.minimum(surf, erosion_surface(h, w / dx, cl_dist, z_map))
613            topo[:, :, 4 * i] = surf  # erosional surface
614            dists[:, :, i] = cl_dist
615            zmaps[:, :, i] = z_map
616            facies[4 * i] = np.NaN
617
618            if model_type == "fluvial":
619                pb = point_bar_surface(cl_dist, z_map, h, w / dx)
620                th = np.maximum(surf, pb) - surf
621                th_oxbows = th.copy()
622                # setting sand thickness to zero at cutoff locations:
623                if len(cutoff_ind) > 0:
624                    cutoff_dists = 1e10 * np.ones(
625                        np.shape(th)
626                    )  # initialize cutoff_dists with a large number
627                    for j in range(len(cutoff_ind)):
628                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
629                            cutoffs[int(cutoff_ind[j])].x[0],
630                            cutoffs[int(cutoff_ind[j])].y[0],
631                            cutoffs[int(cutoff_ind[j])].z[0],
632                            xmin,
633                            xmax,
634                            ymin,
635                            ymax,
636                            dx,
637                        )
638                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
639                    th_oxbows[
640                        cutoff_dists >= 0.9 * w / dx
641                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
642                    th[
643                        cutoff_dists < 0.9 * w / dx
644                    ] = 0  # set point bar thickness to zero inside of oxbows
645                else:  # no cutoffs
646                    th_oxbows = np.zeros(np.shape(th))
647                th[th < 0] = 0  # eliminate negative th values
648                surf = (
649                    surf + th_oxbows
650                )  # update topographic surface with oxbow deposit thickness
651                topo[:, :, 4 * i + 1] = surf  # top of oxbow mud
652                facies[4 * i + 1] = 0
653                surf = surf + th  # update topographic surface with sand thickness
654                topo[:, :, 4 * i + 2] = surf  # top of sand
655                facies[4 * i + 2] = 1
656                surf = surf + mud_surface(
657                    h_mud, levee_width / dx, cl_dist, w / dx, z_map, surf
658                )  # mud/levee deposition
659                topo[:, :, 4 * i + 3] = surf  # top of levee
660                facies[4 * i + 3] = 2
661                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
662
663            if model_type == "submarine":
664                surf = surf + mud_surface(
665                    h_mud[i], levee_width / dx, cl_dist, w / dx, z_map, surf
666                )  # mud/levee deposition
667                topo[:, :, 4 * i + 1] = surf  # top of levee
668                facies[4 * i + 1] = 2
669                # sand thickness:
670                th, relief = sand_surface(surf, bth, dcr, z_map, h)
671                th[th < 0] = 0  # eliminate negative th values
672                th[cl_dist > 1.0 * w / dx] = 0  # eliminate sand outside of channel
673                th_oxbows = th.copy()
674                # setting sand thickness to zero at cutoff locations:
675                if len(cutoff_ind) > 0:
676                    cutoff_dists = 1e10 * np.ones(
677                        np.shape(th)
678                    )  # initialize cutoff_dists with a large number
679                    for j in range(len(cutoff_ind)):
680                        cutoff_dist, cfx_pix, cfy_pix = cl_dist_map(
681                            cutoffs[int(cutoff_ind[j])].x[0],
682                            cutoffs[int(cutoff_ind[j])].y[0],
683                            cutoffs[int(cutoff_ind[j])].z[0],
684                            xmin,
685                            xmax,
686                            ymin,
687                            ymax,
688                            dx,
689                        )
690                        cutoff_dists = np.minimum(cutoff_dists, cutoff_dist)
691                    th_oxbows[
692                        cutoff_dists >= 0.9 * w / dx
693                    ] = 0  # set oxbow fill thickness to zero outside of oxbows
694                    th[
695                        cutoff_dists < 0.9 * w / dx
696                    ] = 0  # set point bar thickness to zero inside of oxbows
697                    # adding back sand near the channel axis (submarine only):
698                    # th[cl_dist<0.5*w/dx] = bth*(1 - relief[cl_dist<0.5*w/dx]/dcr)
699                else:  # no cutoffs
700                    th_oxbows = np.zeros(np.shape(th))
701                surf = (
702                    surf + th_oxbows
703                )  # update topographic surface with oxbow deposit thickness
704                topo[:, :, 4 * i + 2] = surf  # top of oxbow mud
705                facies[4 * i + 2] = 0
706                surf = surf + th  # update topographic surface with sand thickness
707                topo[:, :, 4 * i + 3] = surf  # top of sand
708                facies[4 * i + 3] = 1
709                channels3D.append(Channel(x1 - xmin, y1 - ymin, z1, w, h))
710
711            cl_dist_prev = cl_dist.copy()
712        topo = np.concatenate(
713            (np.reshape(topoinit, (iheight, iwidth, 1)), topo), axis=2
714        )  # add initial topography to array
715        strat = topostrat(topo)  # create stratigraphic surfaces
716        strat = np.delete(
717            strat, np.arange(4 * n_steps + 1)[1::4], 2
718        )  # get rid of unnecessary stratigraphic surfaces (duplicates)
719        facies = np.delete(
720            facies, np.arange(4 * n_steps)[::4]
721        )  # get rid of unnecessary facies layers (NaNs)
722        if model_type == "fluvial":
723            facies_code = {0: "oxbow", 1: "point bar", 2: "levee"}
724        if model_type == "submarine":
725            facies_code = {0: "oxbow", 1: "channel sand", 2: "levee"}
726        chb_3d = ChannelBelt3D(
727            model_type, topo, strat, facies, facies_code, dx, channels3D
728        )
729        return chb_3d, xmin, xmax, ymin, ymax  # , dists, zmaps

method for building 3D model from set of centerlines (that are part of a ChannelBelt object)

Parameters
  • model_type: model type ('fluvial' or 'submarine')
  • h_mud: maximum thickness of overbank mud
  • levee_width: width of overbank mud
  • h: channel depth
  • w: channel width
  • bth: thickness of channel sand (only used in submarine models)
  • dcr: critical channel depth where sand thickness goes to zero (only used in submarine models)
  • dx: cell size in x and y directions
  • delta_s: sampling distance alogn centerlines
  • starttime: age of centerline that will be used as the first centerline in the model
  • endtime: age of centerline that will be used as the last centerline in the model
  • xmin: minimum x coordinate that defines the model domain; if xmin is set to zero, a plot of the centerlines is generated and the model domain has to be defined by clicking its upper left and lower right corners
  • xmax: maximum x coordinate that defines the model domain
  • ymin: minimum y coordinate that defines the model domain
  • ymax: maximum y coordinate that defines the model domain
Returns

a ChannelBelt3D object

Returns

x and y coordinates that define the model domain (so that they can be reused later)

def resample_centerline(x, y, z, deltas):
732def resample_centerline(x, y, z, deltas):
733    """resample centerline so that 'deltas' is roughly constant, using parametric
734    spline representation of curve; note that there is *no* smoothing
735
736    :param x: x-coordinates of centerline
737    :param y: y-coordinates of centerline
738    :param z: z-coordinates of centerline
739    :param deltas: distance between points on centerline
740    :return x: x-coordinates of resampled centerline
741    :return y: y-coordinates of resampled centerline
742    :return z: z-coordinates of resampled centerline
743    :return dx: dx of resampled centerline
744    :return dy: dy of resampled centerline
745    :return dz: dz of resampled centerline
746    :return s: s-coordinates of resampled centerline"""
747
748    dx, dy, dz, ds, s = compute_derivatives(x, y, z)  # compute derivatives
749    tck, u = scipy.interpolate.splprep([x, y, z], s=0)
750    unew = np.linspace(0, 1, 1 + int(round(s[-1] / deltas)))  # vector for resampling
751    out = scipy.interpolate.splev(unew, tck)  # resampling
752    x, y, z = out[0], out[1], out[2]  # assign new coordinate values
753    dx, dy, dz, ds, s = compute_derivatives(x, y, z)  # recompute derivatives
754    return x, y, z, dx, dy, dz, ds, s

resample centerline so that 'deltas' is roughly constant, using parametric spline representation of curve; note that there is no smoothing

Parameters
  • x: x-coordinates of centerline
  • y: y-coordinates of centerline
  • z: z-coordinates of centerline
  • deltas: distance between points on centerline
Returns

x-coordinates of resampled centerline

Returns

y-coordinates of resampled centerline

Returns

z-coordinates of resampled centerline

Returns

dx of resampled centerline

Returns

dy of resampled centerline

Returns

dz of resampled centerline

Returns

s-coordinates of resampled centerline

def migrate_one_step(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
757def migrate_one_step(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
758    """migrate centerline during one time step, using the migration computed as in Howard & Knutson (1984)
759
760    :param x: x-coordinates of centerline
761    :param y: y-coordinates of centerline
762    :param z: z-coordinates of centerline
763    :param W: channel width
764    :param kl: migration rate (or erodibility) constant (m/s)
765    :param dt: duration of time step (s)
766    :param k: constant for calculating the exponent alpha (= 1.0)
767    :param Cf: dimensionless Chezy friction factor
768    :param D: channel depth
769    :param omega: constant in Howard & Knutson equation (= -1.0)
770    :param gamma: constant in Howard & Knutson equation (= 2.5)
771    :return x: new x-coordinates of centerline after migration
772    :return y: new y-coordinates of centerline after migration
773    """
774
775    ns = len(x)
776    curv = compute_curvature(x, y)
777    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
778    sinuosity = s[-1] / (x[-1] - x[0])
779    curv = W * curv  # dimensionless curvature
780    R0 = (
781        kl * curv
782    )  # simple linear relationship between curvature and nominal migration rate
783    alpha = k * 2 * Cf / D  # exponent for convolution function G
784    R1 = compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0)
785    R1 = sinuosity ** (-2 / 3.0) * R1
786    # calculate new centerline coordinates:
787    dy_ds = dy[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
788    dx_ds = dx[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
789    # adjust x and y coordinates (this *is* the migration):
790    x[pad1 : ns - pad + 1] = (
791        x[pad1 : ns - pad + 1] + R1[pad1 : ns - pad + 1] * dy_ds * dt
792    )
793    y[pad1 : ns - pad + 1] = (
794        y[pad1 : ns - pad + 1] - R1[pad1 : ns - pad + 1] * dx_ds * dt
795    )
796    return x, y

migrate centerline during one time step, using the migration computed as in Howard & Knutson (1984)

Parameters
  • x: x-coordinates of centerline
  • y: y-coordinates of centerline
  • z: z-coordinates of centerline
  • W: channel width
  • kl: migration rate (or erodibility) constant (m/s)
  • dt: duration of time step (s)
  • k: constant for calculating the exponent alpha (= 1.0)
  • Cf: dimensionless Chezy friction factor
  • D: channel depth
  • omega: constant in Howard & Knutson equation (= -1.0)
  • gamma: constant in Howard & Knutson equation (= 2.5)
Returns

new x-coordinates of centerline after migration

Returns

new y-coordinates of centerline after migration

def migrate_one_step_w_bias(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
799def migrate_one_step_w_bias(x, y, z, W, kl, dt, k, Cf, D, pad, pad1, omega, gamma):
800    ns = len(x)
801    curv = compute_curvature(x, y)
802    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
803    sinuosity = s[-1] / (x[-1] - x[0])
804    curv = W * curv  # dimensionless curvature
805    R0 = (
806        kl * curv
807    )  # simple linear relationship between curvature and nominal migration rate
808    alpha = k * 2 * Cf / D  # exponent for convolution function G
809    R1 = compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0)
810    R1 = sinuosity ** (-2 / 3.0) * R1
811    pad = -1
812    # calculate new centerline coordinates:
813    dy_ds = dy[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
814    dx_ds = dx[pad1 : ns - pad + 1] / ds[pad1 : ns - pad + 1]
815    tilt_factor = 0.2
816    T = kl * tilt_factor * np.ones(np.shape(x))
817    angle = 90.0
818    # adjust x and y coordinates (this *is* the migration):
819    x[pad1 : ns - pad + 1] = (
820        x[pad1 : ns - pad + 1]
821        + R1[pad1 : ns - pad + 1] * dy_ds * dt
822        + T[pad1 : ns - pad + 1]
823        * dy_ds
824        * dt
825        * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds)
826    )
827    y[pad1 : ns - pad + 1] = (
828        y[pad1 : ns - pad + 1]
829        - R1[pad1 : ns - pad + 1] * dx_ds * dt
830        - T[pad1 : ns - pad + 1]
831        * dx_ds
832        * dt
833        * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds)
834    )
835    return x, y
def generate_initial_channel(W, D, Sl, deltas, pad, n_bends):
838def generate_initial_channel(W, D, Sl, deltas, pad, n_bends):
839    """generate straight Channel object with some noise added that can serve
840    as input for initializing a ChannelBelt object
841    W - channel width
842    D - channel depth
843    Sl - channel gradient
844    deltas - distance between nodes on centerline
845    pad - padding (number of nodepoints along centerline)
846    n_bends - approximate number of bends to be simulated"""
847    noisy_len = n_bends * 10 * W / 2.0  # length of noisy part of initial centerline
848    pad1 = int(
849        pad / 10.0
850    )  # padding at upstream end can be shorter than padding on downstream end
851    if pad1 < 5:
852        pad1 = 5
853    x = np.linspace(
854        0, noisy_len + (pad + pad1) * deltas, int(noisy_len / deltas + pad + pad1) + 1
855    )  # x coordinate
856    y = 10.0 * (
857        2
858        * np.random.random_sample(
859            int(noisy_len / deltas) + 1,
860        )
861        - 1
862    )
863    y = np.hstack(
864        (
865            np.zeros(
866                (pad1),
867            ),
868            y,
869            np.zeros(
870                (pad),
871            ),
872        )
873    )  # y coordinate
874    deltaz = Sl * deltas * (len(x) - 1)
875    z = np.linspace(0, deltaz, len(x))[::-1]  # z coordinate
876    return Channel(x, y, z, W, D)

generate straight Channel object with some noise added that can serve as input for initializing a ChannelBelt object W - channel width D - channel depth Sl - channel gradient deltas - distance between nodes on centerline pad - padding (number of nodepoints along centerline) n_bends - approximate number of bends to be simulated

@numba.jit(nopython=True)
def compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0):
879@numba.jit(nopython=True)  # use Numba to speed up the heaviest computation
880def compute_migration_rate(pad, ns, ds, alpha, omega, gamma, R0):
881    """compute migration rate as weighted sum of upstream curvatures
882    pad - padding (number of nodepoints along centerline)
883    ns - number of points in centerline
884    ds - distances between points in centerline
885    omega - constant in HK model
886    gamma - constant in HK model
887    R0 - nominal migration rate (dimensionless curvature * migration rate constant)"""
888    R1 = np.zeros(ns)  # preallocate adjusted channel migration rate
889    pad1 = int(
890        pad / 10.0
891    )  # padding at upstream end can be shorter than padding on downstream end
892    if pad1 < 5:
893        pad1 = 5
894    for i in range(pad1, ns - pad):
895        si2 = np.hstack(
896            (np.array([0]), np.cumsum(ds[i - 1 :: -1]))
897        )  # distance along centerline, backwards from current point
898        G = np.exp(-alpha * si2)  # convolution vector
899        R1[i] = omega * R0[i] + gamma * np.sum(R0[i::-1] * G) / np.sum(
900            G
901        )  # main equation
902    return R1

compute migration rate as weighted sum of upstream curvatures pad - padding (number of nodepoints along centerline) ns - number of points in centerline ds - distances between points in centerline omega - constant in HK model gamma - constant in HK model R0 - nominal migration rate (dimensionless curvature * migration rate constant)

def compute_derivatives(x, y, z):
905def compute_derivatives(x, y, z):
906    """function for computing first derivatives of a curve (centerline)
907    x,y are cartesian coodinates of the curve
908    outputs:
909    dx - first derivative of x coordinate
910    dy - first derivative of y coordinate
911    ds - distances between consecutive points along the curve
912    s - cumulative distance along the curve"""
913    dx = np.gradient(x)  # first derivatives
914    dy = np.gradient(y)
915    dz = np.gradient(z)
916    ds = np.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
917    s = np.hstack((0, np.cumsum(ds[1:])))
918    return dx, dy, dz, ds, s

function for computing first derivatives of a curve (centerline) x,y are cartesian coodinates of the curve outputs: dx - first derivative of x coordinate dy - first derivative of y coordinate ds - distances between consecutive points along the curve s - cumulative distance along the curve

def compute_curvature(x, y):
921def compute_curvature(x, y):
922    """function for computing first derivatives and curvature of a curve (centerline)
923    x,y are cartesian coodinates of the curve
924    outputs:
925    dx - first derivative of x coordinate
926    dy - first derivative of y coordinate
927    ds - distances between consecutive points along the curve
928    s - cumulative distance along the curve
929    curvature - curvature of the curve (in 1/units of x and y)"""
930    dx = np.gradient(x)  # first derivatives
931    dy = np.gradient(y)
932    ddx = np.gradient(dx)  # second derivatives
933    ddy = np.gradient(dy)
934    curvature = (dx * ddy - dy * ddx) / ((dx ** 2 + dy ** 2) ** 1.5)
935    return curvature

function for computing first derivatives and curvature of a curve (centerline) x,y are cartesian coodinates of the curve outputs: dx - first derivative of x coordinate dy - first derivative of y coordinate ds - distances between consecutive points along the curve s - cumulative distance along the curve curvature - curvature of the curve (in 1/units of x and y)

def make_colormap(seq):
938def make_colormap(seq):
939    """Return a LinearSegmentedColormap
940    seq: a sequence of floats and RGB-tuples. The floats should be increasing
941    and in the interval (0,1).
942    [from: https://stackoverflow.com/questions/16834861/create-own-colormap-using-matplotlib-and-plot-color-scale]
943    """
944    seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
945    cdict = {"red": [], "green": [], "blue": []}
946    for i, item in enumerate(seq):
947        if isinstance(item, float):
948            r1, g1, b1 = seq[i - 1]
949            r2, g2, b2 = seq[i + 1]
950            cdict["red"].append([item, r1, r2])
951            cdict["green"].append([item, g1, g2])
952            cdict["blue"].append([item, b1, b2])
953    return mcolors.LinearSegmentedColormap("CustomMap", cdict)

Return a LinearSegmentedColormap seq: a sequence of floats and RGB-tuples. The floats should be increasing and in the interval (0,1). [from: https://stackoverflow.com/questions/16834861/create-own-colormap-using-matplotlib-and-plot-color-scale]

def kth_diag_indices(a, k):
956def kth_diag_indices(a, k):
957    """function for finding diagonal indices with k offset
958    [from https://stackoverflow.com/questions/10925671/numpy-k-th-diagonal-indices]"""
959    rows, cols = np.diag_indices_from(a)
960    if k < 0:
961        return rows[:k], cols[-k:]
962    elif k > 0:
963        return rows[k:], cols[:-k]
964    else:
965        return rows, cols

function for finding diagonal indices with k offset [from https://stackoverflow.com/questions/10925671/numpy-k-th-diagonal-indices]

def find_cutoffs(x, y, crdist, deltas):
968def find_cutoffs(x, y, crdist, deltas):
969    """function for identifying locations of cutoffs along a centerline
970    and the indices of the segments that will become part of the oxbows
971    x,y - coordinates of centerline
972    crdist - critical cutoff distance
973    deltas - distance between neighboring points along the centerline"""
974    diag_blank_width = int((crdist + 20 * deltas) / deltas)
975    # distance matrix for centerline points:
976    dist = distance.cdist(np.array([x, y]).T, np.array([x, y]).T)
977    dist[
978        dist > crdist
979    ] = np.NaN  # set all values that are larger than the cutoff threshold to NaN
980    # set matrix to NaN along the diagonal zone:
981    for k in range(-diag_blank_width, diag_blank_width + 1):
982        rows, cols = kth_diag_indices(dist, k)
983        dist[rows, cols] = np.NaN
984    i1, i2 = np.where(~np.isnan(dist))
985    ind1 = i1[np.where(i1 < i2)[0]]  # get rid of unnecessary indices
986    ind2 = i2[np.where(i1 < i2)[0]]  # get rid of unnecessary indices
987    return ind1, ind2  # return indices of cutoff points and cutoff coordinates

function for identifying locations of cutoffs along a centerline and the indices of the segments that will become part of the oxbows x,y - coordinates of centerline crdist - critical cutoff distance deltas - distance between neighboring points along the centerline

def cut_off_cutoffs(x, y, z, s, crdist, deltas):
 990def cut_off_cutoffs(x, y, z, s, crdist, deltas):
 991    """function for executing cutoffs - removing oxbows from centerline and storing cutoff coordinates
 992    x,y - coordinates of centerline
 993    crdist - critical cutoff distance
 994    deltas - distance between neighboring points along the centerline
 995    outputs:
 996    x,y,z - updated coordinates of centerline
 997    xc, yc, zc - lists with coordinates of cutoff segments"""
 998    xc = []
 999    yc = []
1000    zc = []
1001    ind1, ind2 = find_cutoffs(x, y, crdist, deltas)  # initial check for cutoffs
1002    while len(ind1) > 0:
1003        xc.append(x[ind1[0] : ind2[0] + 1])  # x coordinates of cutoff
1004        yc.append(y[ind1[0] : ind2[0] + 1])  # y coordinates of cutoff
1005        zc.append(z[ind1[0] : ind2[0] + 1])  # z coordinates of cutoff
1006        x = np.hstack((x[: ind1[0] + 1], x[ind2[0] :]))  # x coordinates after cutoff
1007        y = np.hstack((y[: ind1[0] + 1], y[ind2[0] :]))  # y coordinates after cutoff
1008        z = np.hstack((z[: ind1[0] + 1], z[ind2[0] :]))  # z coordinates after cutoff
1009        ind1, ind2 = find_cutoffs(x, y, crdist, deltas)
1010    return x, y, z, xc, yc, zc

function for executing cutoffs - removing oxbows from centerline and storing cutoff coordinates x,y - coordinates of centerline crdist - critical cutoff distance deltas - distance between neighboring points along the centerline outputs: x,y,z - updated coordinates of centerline xc, yc, zc - lists with coordinates of cutoff segments

def get_channel_banks(x, y, W):
1013def get_channel_banks(x, y, W):
1014    """function for finding coordinates of channel banks, given a centerline and a channel width
1015    x,y - coordinates of centerline
1016    W - channel width
1017    outputs:
1018    xm, ym - coordinates of channel banks (both left and right banks)"""
1019    x1 = x.copy()
1020    y1 = y.copy()
1021    x2 = x.copy()
1022    y2 = y.copy()
1023    ns = len(x)
1024    dx = np.diff(x)
1025    dy = np.diff(y)
1026    ds = np.sqrt(dx ** 2 + dy ** 2)
1027    x1[:-1] = x[:-1] + 0.5 * W * np.diff(y) / ds
1028    y1[:-1] = y[:-1] - 0.5 * W * np.diff(x) / ds
1029    x2[:-1] = x[:-1] - 0.5 * W * np.diff(y) / ds
1030    y2[:-1] = y[:-1] + 0.5 * W * np.diff(x) / ds
1031    x1[ns - 1] = x[ns - 1] + 0.5 * W * (y[ns - 1] - y[ns - 2]) / ds[ns - 2]
1032    y1[ns - 1] = y[ns - 1] - 0.5 * W * (x[ns - 1] - x[ns - 2]) / ds[ns - 2]
1033    x2[ns - 1] = x[ns - 1] - 0.5 * W * (y[ns - 1] - y[ns - 2]) / ds[ns - 2]
1034    y2[ns - 1] = y[ns - 1] + 0.5 * W * (x[ns - 1] - x[ns - 2]) / ds[ns - 2]
1035    xm = np.hstack((x1, x2[::-1]))
1036    ym = np.hstack((y1, y2[::-1]))
1037    return xm, ym

function for finding coordinates of channel banks, given a centerline and a channel width x,y - coordinates of centerline W - channel width outputs: xm, ym - coordinates of channel banks (both left and right banks)

def dist_map(x, y, z, xmin, xmax, ymin, ymax, dx, delta_s):
1040def dist_map(x, y, z, xmin, xmax, ymin, ymax, dx, delta_s):
1041    """function for centerline rasterization and distance map calculation
1042    inputs:
1043    x,y,z - coordinates of centerline
1044    xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest
1045    dx - gridcell size (m)
1046    delta_s - distance between points along centerline (m)
1047    returns:
1048    cl_dist - distance map (distance from centerline)
1049    x_pix, y_pix, z_pix - x,y, and z pixel coordinates of the centerline
1050    s_pix - along-channel distance in pixels
1051    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1052    x, y, z - x,y,z centerline coordinates clipped to the 3D model domain"""
1053    y = y[(x > xmin) & (x < xmax)]
1054    z = z[(x > xmin) & (x < xmax)]
1055    x = x[(x > xmin) & (x < xmax)]
1056    dummy, dy, dz, ds, s = compute_derivatives(x, y, z)
1057    if len(np.where(ds > 2 * delta_s)[0]) > 0:
1058        inds = np.where(ds > 2 * delta_s)[0]
1059        inds = np.hstack((0, inds, len(x)))
1060        lengths = np.diff(inds)
1061        long_segment = np.where(lengths == max(lengths))[0][0]
1062        start_ind = inds[long_segment] + 1
1063        end_ind = inds[long_segment + 1]
1064        if end_ind < len(x):
1065            x = x[start_ind:end_ind]
1066            y = y[start_ind:end_ind]
1067            z = z[start_ind:end_ind]
1068        else:
1069            x = x[start_ind:]
1070            y = y[start_ind:]
1071            z = z[start_ind:]
1072    xdist = xmax - xmin
1073    ydist = ymax - ymin
1074    iwidth = int((xmax - xmin) / dx)
1075    iheight = int((ymax - ymin) / dx)
1076    xratio = iwidth / xdist
1077    # create list with pixel coordinates:
1078    pixels = []
1079    for i in range(0, len(x)):
1080        px = int(iwidth - (xmax - x[i]) * xratio)
1081        py = int(iheight - (ymax - y[i]) * xratio)
1082        pixels.append((px, py))
1083    # create image and numpy array:
1084    img = Image.new("RGB", (iwidth, iheight), "white")
1085    draw = ImageDraw.Draw(img)
1086    draw.line(pixels, fill="rgb(0, 0, 0)")  # draw centerline as black line
1087    pix = np.array(img)
1088    cl = pix[:, :, 0]
1089    cl[cl == 255] = 1  # set background to 1 (centerline is 0)
1090    y_pix, x_pix = np.where(cl == 0)
1091    x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1092    # This next block of code is kind of a hack. Looking for, and eliminating, 'bad' pixels.
1093    img = np.array(img)
1094    img = img[:, :, 0]
1095    img[img == 255] = 1
1096    img1 = morphology.binary_dilation(img, morphology.square(2)).astype(np.uint8)
1097    if len(np.where(img1 == 0)[0]) > 0:
1098        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1099        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1100    img1 = morphology.binary_dilation(
1101        img, np.array([[1, 0, 1], [1, 1, 1]], dtype=np.uint8)
1102    ).astype(np.uint8)
1103    if len(np.where(img1 == 0)[0]) > 0:
1104        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1105        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1106    img1 = morphology.binary_dilation(
1107        img, np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=np.uint8)
1108    ).astype(np.uint8)
1109    if len(np.where(img1 == 0)[0]) > 0:
1110        x_pix, y_pix = eliminate_bad_pixels(img, img1)
1111        x_pix, y_pix = order_cl_pixels(x_pix, y_pix)
1112    # redo the distance calculation (because x_pix and y_pix do not always contain all the points in cl):
1113    cl[cl == 0] = 1
1114    cl[y_pix, x_pix] = 0
1115    cl_dist, inds = ndimage.distance_transform_edt(cl, return_indices=True)
1116    dx, dy, dz, ds, s = compute_derivatives(x, y, z)
1117    dx_pix = np.diff(x_pix)
1118    dy_pix = np.diff(y_pix)
1119    ds_pix = np.sqrt(dx_pix ** 2 + dy_pix ** 2)
1120    s_pix = np.hstack((0, np.cumsum(ds_pix)))
1121    f = scipy.interpolate.interp1d(s, z)
1122    snew = s_pix * s[-1] / s_pix[-1]
1123    if snew[-1] > s[-1]:
1124        snew[-1] = s[-1]
1125    snew[snew < s[0]] = s[0]
1126    z_pix = f(snew)
1127    # create z_map:
1128    z_map = np.zeros(np.shape(cl_dist))
1129    z_map[y_pix, x_pix] = z_pix
1130    xinds = inds[1, :, :]
1131    yinds = inds[0, :, :]
1132    for i in range(0, len(x_pix)):
1133        z_map[(xinds == x_pix[i]) & (yinds == y_pix[i])] = z_pix[i]
1134    return cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x, y, z

function for centerline rasterization and distance map calculation inputs: x,y,z - coordinates of centerline xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest dx - gridcell size (m) delta_s - distance between points along centerline (m) returns: cl_dist - distance map (distance from centerline) x_pix, y_pix, z_pix - x,y, and z pixel coordinates of the centerline s_pix - along-channel distance in pixels z_map - map of reference channel thalweg elevation (elevation of closest point along centerline) x, y, z - x,y,z centerline coordinates clipped to the 3D model domain

def erosion_surface(h, w, cl_dist, z):
1137def erosion_surface(h, w, cl_dist, z):
1138    """function for creating a parabolic erosional surface
1139    inputs:
1140    h - geomorphic channel depth (m)
1141    w - geomorphic channel width (in pixels, as cl_dist is also given in pixels)
1142    cl_dist - distance map (distance from centerline)
1143    z - reference elevation (m)
1144    returns:
1145    surf - map of the quadratic erosional surface (m)
1146    """
1147    surf = z + (4 * h / w ** 2) * (cl_dist + w * 0.5) * (cl_dist - w * 0.5)
1148    return surf

function for creating a parabolic erosional surface inputs: h - geomorphic channel depth (m) w - geomorphic channel width (in pixels, as cl_dist is also given in pixels) cl_dist - distance map (distance from centerline) z - reference elevation (m) returns: surf - map of the quadratic erosional surface (m)

def point_bar_surface(cl_dist, z, h, w):
1151def point_bar_surface(cl_dist, z, h, w):
1152    """function for creating a Gaussian-based point bar surface
1153    used in 3D fluvial model
1154    inputs:
1155    cl_dist - distance map (distance from centerline)
1156    z - reference elevation (m)
1157    h - channel depth (m)
1158    w - channel width, in pixels, as cl_dist is also given in pixels
1159    returns:
1160    pb - map of the Gaussian surface that can be used to from a point bar deposit (m)"""
1161    pb = z - h * np.exp(-(cl_dist ** 2) / (2 * (w * 0.33) ** 2))
1162    return pb

function for creating a Gaussian-based point bar surface used in 3D fluvial model inputs: cl_dist - distance map (distance from centerline) z - reference elevation (m) h - channel depth (m) w - channel width, in pixels, as cl_dist is also given in pixels returns: pb - map of the Gaussian surface that can be used to from a point bar deposit (m)

def sand_surface(surf, bth, dcr, z_map, h):
1165def sand_surface(surf, bth, dcr, z_map, h):
1166    """function for creating the top horizontal surface sand-rich deposit in the bottom of the channel
1167    used in 3D submarine channel models
1168    inputs:
1169    surf - current geomorphic surface
1170    bth - thickness of sand deposit in axis of channel (m)
1171    dcr - critical channel depth, above which there is no sand deposition (m)
1172    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1173    h - channel depth (m)
1174    returns:
1175    th - thickness map of sand deposit (m)
1176    relief - map of channel relief (m)"""
1177    relief = np.abs(surf - z_map + h)
1178    relief = np.abs(relief - np.amin(relief))
1179    th = bth * (1 - relief / dcr)  # bed thickness inversely related to relief
1180    th[th < 0] = 0.0  # set negative th values to zero
1181    return th, relief

function for creating the top horizontal surface sand-rich deposit in the bottom of the channel used in 3D submarine channel models inputs: surf - current geomorphic surface bth - thickness of sand deposit in axis of channel (m) dcr - critical channel depth, above which there is no sand deposition (m) z_map - map of reference channel thalweg elevation (elevation of closest point along centerline) h - channel depth (m) returns: th - thickness map of sand deposit (m) relief - map of channel relief (m)

def mud_surface(h_mud, levee_width, cl_dist, w, z_map, topo):
1184def mud_surface(h_mud, levee_width, cl_dist, w, z_map, topo):
1185    """function for creating a map of overbank deposit thickness
1186    inputs:
1187    h_mud - maximum thickness of overbank deposit (m)
1188    levee_width - half-width of overbank deposit (m)
1189    cl_dist - distance map (distance from centerline)
1190    w - channel width (in pixels, as cl_dist is also given in pixels)
1191    z_map - map of reference channel thalweg elevation (elevation of closest point along centerline)
1192    topo - current geomorphic surface
1193    returns:
1194    surf - map of overbank deposit thickness (m)"""
1195    # create a surface that thins linearly away from the channel centerline:
1196    surf1 = (-2 * h_mud / levee_width) * cl_dist + h_mud
1197    surf2 = (2 * h_mud / levee_width) * cl_dist + h_mud
1198    surf = np.minimum(surf1, surf2)
1199    # surface for 'eroding' the central part of the mud layer:
1200    surf3 = h_mud + (4 * 1.5 * h_mud / w ** 2) * (cl_dist + w * 0.5) * (
1201        cl_dist - w * 0.5
1202    )
1203    surf = np.minimum(surf, surf3)
1204    surf[surf < 0] = 0
1205    # eliminate negative thicknesses
1206    return surf

function for creating a map of overbank deposit thickness inputs: h_mud - maximum thickness of overbank deposit (m) levee_width - half-width of overbank deposit (m) cl_dist - distance map (distance from centerline) w - channel width (in pixels, as cl_dist is also given in pixels) z_map - map of reference channel thalweg elevation (elevation of closest point along centerline) topo - current geomorphic surface returns: surf - map of overbank deposit thickness (m)

def topostrat(topo):
1209def topostrat(topo):
1210    """function for converting a stack of geomorphic surfaces into stratigraphic surfaces
1211    inputs:
1212    topo - 3D numpy array of geomorphic surfaces
1213    returns:
1214    strat - 3D numpy array of stratigraphic surfaces
1215    """
1216    r, c, ts = np.shape(topo)
1217    strat = np.copy(topo)
1218    for i in range(0, ts):
1219        strat[:, :, i] = np.amin(topo[:, :, i:], axis=2)
1220    return strat

function for converting a stack of geomorphic surfaces into stratigraphic surfaces inputs: topo - 3D numpy array of geomorphic surfaces returns: strat - 3D numpy array of stratigraphic surfaces

def cl_dist_map(x, y, z, xmin, xmax, ymin, ymax, dx):
1223def cl_dist_map(x, y, z, xmin, xmax, ymin, ymax, dx):
1224    """function for centerline rasterization and distance map calculation (does not return zmap)
1225    used for cutoffs only
1226    inputs:
1227    x,y,z - coordinates of centerline
1228    xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest
1229    dx - gridcell size (m)
1230    returns:
1231    cl_dist - distance map (distance from centerline)
1232    x_pix, y_pix, - x and y pixel coordinates of the centerline
1233    """
1234    y = y[(x > xmin) & (x < xmax)]
1235    z = z[(x > xmin) & (x < xmax)]
1236    x = x[(x > xmin) & (x < xmax)]
1237    xdist = xmax - xmin
1238    ydist = ymax - ymin
1239    iwidth = int((xmax - xmin) / dx)
1240    iheight = int((ymax - ymin) / dx)
1241    xratio = iwidth / xdist
1242    # create list with pixel coordinates:
1243    pixels = []
1244    for i in range(0, len(x)):
1245        px = int(iwidth - (xmax - x[i]) * xratio)
1246        py = int(iheight - (ymax - y[i]) * xratio)
1247        pixels.append((px, py))
1248    # create image and numpy array:
1249    img = Image.new("RGB", (iwidth, iheight), "white")
1250    draw = ImageDraw.Draw(img)
1251    draw.line(pixels, fill="rgb(0, 0, 0)")  # draw centerline as black line
1252    pix = np.array(img)
1253    cl = pix[:, :, 0]
1254    cl[cl == 255] = 1  # set background to 1 (centerline is 0)
1255    # calculate Euclidean distance map:
1256    cl_dist, inds = ndimage.distance_transform_edt(cl, return_indices=True)
1257    y_pix, x_pix = np.where(cl == 0)
1258    return cl_dist, x_pix, y_pix

function for centerline rasterization and distance map calculation (does not return zmap) used for cutoffs only inputs: x,y,z - coordinates of centerline xmin, xmax, ymin, ymax - x and y coordinates that define the area of interest dx - gridcell size (m) returns: cl_dist - distance map (distance from centerline) x_pix, y_pix, - x and y pixel coordinates of the centerline

def eliminate_bad_pixels(img, img1):
1261def eliminate_bad_pixels(img, img1):
1262    x_ind = np.where(img1 == 0)[1][0]
1263    y_ind = np.where(img1 == 0)[0][0]
1264    img[y_ind : y_ind + 2, x_ind : x_ind + 2] = np.ones(
1265        1,
1266    ).astype(np.uint8)
1267    all_labels = measure.label(img, background=1, connectivity=2)
1268    cl = all_labels.copy()
1269    cl[cl == 2] = 0
1270    cl[cl > 0] = 1
1271    y_pix, x_pix = np.where(cl == 1)
1272    return x_pix, y_pix
def order_cl_pixels(x_pix, y_pix):
1275def order_cl_pixels(x_pix, y_pix):
1276    dist = distance.cdist(np.array([x_pix, y_pix]).T, np.array([x_pix, y_pix]).T)
1277    dist[np.diag_indices_from(dist)] = 100.0
1278    ind = np.argmin(x_pix)  # select starting point on left side of image
1279    clinds = [ind]
1280    count = 0
1281    while count < len(x_pix):
1282        t = dist[ind, :].copy()
1283        if len(clinds) > 2:
1284            t[clinds[-2]] = t[clinds[-2]] + 100.0
1285            t[clinds[-3]] = t[clinds[-3]] + 100.0
1286        ind = np.argmin(t)
1287        clinds.append(ind)
1288        count = count + 1
1289    x_pix = x_pix[clinds]
1290    y_pix = y_pix[clinds]
1291    return x_pix, y_pix