├── LICENSE.txt ├── README.md ├── define_3D_domain.png ├── fluvial_meanderpy_example_map.png ├── fluvial_meanderpy_example_section.png ├── meanderpy ├── .gitignore ├── __init__.py ├── meanderpy.ipynb └── meanderpy.py ├── meanderpy_logo.svg ├── meanderpy_sketch.png ├── meanderpy_strat_vs_morph.png └── setup.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [Zoltan Sylvester] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 'meanderpy' is a Python module that implements a simple numerical model of meandering, the one described by Howard & Knutson in their 1984 paper ["Sufficient Conditions for River Meandering: A Simulation Approach"](https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/WR020i011p01659). This is a kinematic model that is based on computing migration rate as the weighted sum of upstream curvatures; flow velocity does not enter the equation. Curvature is transformed into a 'nominal migration rate' through multiplication with a migration rate (or erodibility) constant; in the Howard & Knutson (1984) paper this is a nonlinear relationship based on field observations that suggested a complex link between curvature and migration rate. In the 'meanderpy' module we use a simple linear relationship between the nominal migration rate and curvature, as recent work using time-lapse satellite imagery suggests that high curvatures result in high migration rates ([Sylvester et al., 2019](https://doi.org/10.1130/G45608.1)). 6 | 7 | ## Installation 8 | 9 | pip install meanderpy 10 | 11 | ## Requirements 12 | 13 | - numpy 14 | - matplotlib 15 | - scipy 16 | - PIL 17 | - numba 18 | - scikit-image 19 | - tqdm 20 | - jupyter 21 | 22 | ## Usage 23 | 24 | 25 | 26 | The sketch above shows the three 'meanderpy' components: channel, cutoff, channel belt. These are implemented as classes; a 'Channel' and a 'Cutoff' are defined by their width, depth, and x,y,z centerline coordinates, and a 'ChannelBelt' is a collection of channels and cutoffs. In addition, the 'ChannelBelt' object also has a 'cl_times' and a 'cutoff_times' attribute that specify the age of the channels and the cutoffs. This age is relative to the start time of the simulation (= the first channel, age = 0.0). 27 | 28 | To run the below cells, you must first import the library: 29 | 30 | ```python 31 | import meanderpy as mp 32 | import numpy as np 33 | ``` 34 | 35 | A reasonable set of input parameters are as follows: 36 | 37 | ```python 38 | nit = 1500 # number of iterations 39 | W = 200.0 # channel width (m) 40 | D = 6.0 # channel depth (m) 41 | depths = D * np.ones((nit,)) # channel depths for different iterations 42 | pad = 100 # padding (number of nodepoints along centerline) 43 | deltas = 50.0 # sampling distance along centerline 44 | Cfs = 0.011 * np.ones((nit,)) # dimensionless Chezy friction factor 45 | crdist = 2 * W # threshold distance at which cutoffs occur 46 | kl = 60.0/(365*24*60*60.0) # migration rate constant (m/s) 47 | kv = 1.0e-12 # vertical slope-dependent erosion rate constant (m/s) 48 | dt = 2*0.05*365*24*60*60.0 # time step (s) 49 | dens = 1000 # density of water (kg/m3) 50 | saved_ts = 20 # which time steps will be saved 51 | n_bends = 30 # approximate number of bends you want to model 52 | Sl = 0.0 # initial slope (matters more for submarine channels than rivers) 53 | t1 = 500 # time step when incision starts 54 | t2 = 700 # time step when lateral migration starts 55 | t3 = 1200 # time step when aggradation starts 56 | aggr_factor = 2e-9 # aggradation factor (m/s, about 0.18 m/year, it kicks in after t3) 57 | ``` 58 | 59 | The initial Channel object can be created using the 'generate_initial_channel' function. This creates a straight line, with some noise added. However, a Channel can be created (and then used as the first channel in a ChannelBelt) using any set of x,y,z,W,D variables. 60 | 61 | ```python 62 | ch = mp.generate_initial_channel(W, depths[0], Sl, deltas, pad, n_bends) # initialize channel 63 | chb = mp.ChannelBelt(channels=[ch], cutoffs=[], cl_times=[0.0], cutoff_times=[]) # create channel belt object 64 | ``` 65 | 66 | The core functionality of 'meanderpy' is built into the 'migrate' method of the 'ChannelBelt' class. This is the function that computes migration rates and moves the channel centerline to its new position. The last Channel of a ChannelBelt can be further migrated through applying the 'migrate' method to the ChannelBelt instance. 67 | 68 | ```python 69 | chb.migrate(nit,saved_ts,deltas,pad,crdist,depths,Cfs,kl,kv,dt,dens,t1,t2,t3,aggr_factor) # channel migration 70 | ``` 71 | 72 | ChannelBelt objects can be visualized using the 'plot' method. This creates a map of all the channels and cutoffs in the channel belt; there are two styles of plotting: a 'stratigraphic' view and a 'morphologic' view (see below). The morphologic view tries to account for the fact that older point bars and oxbow lakes tend to be gradually covered with vegetation. 73 | 74 | ```python 75 | # migrate an additional 1000 iterations and plot results 76 | chb.migrate(1000,saved_ts,deltas,pad,crdist,depths,Cfs,kl,kv,dt,dens,t1,t2,t3,aggr_factor) 77 | fig = chb.plot('strat', 20, 60, chb.cl_times[-1], len(chb.channels)) # plotting 78 | ``` 79 | 80 | 81 | 82 | A series of movie frames (in PNG format) can be created using the 'create_movie' method: 83 | 84 | ```python 85 | chb.create_movie(xmin,xmax,plot_type,filename,dirname,pb_age,ob_age,scale,end_time) 86 | ``` 87 | The frames have to be assembled into an animation outside of 'meanderpy'. 88 | 89 | ## Build 3D model 90 | 91 | 'meanderpy' includes the functionality to build 3D stratigraphic models. However, this functionality is decoupled from the centerline generation, mainly because it would be computationally expensive to generate surfaces for all centerlines, along their whole lengths. Instead, the 3D model is only created after a Channelbelt object has been generated; a model domain is defined either through specifying the xmin, xmax, ymin, ymax coordinates, or through clicking the upper left and lower right corners of the domain, using the matplotlib 'ginput' command: 92 | 93 | 94 | 95 | Important parameters for a fluvial 3D model are the following: 96 | 97 | ```python 98 | Sl = 0.0 # initial slope (matters more for submarine channels than rivers) 99 | t1 = 500 # time step when incision starts 100 | t2 = 700 # time step when lateral migration starts 101 | t3 = 1400 # time step when aggradation starts 102 | aggr_factor = 4e-9 # aggradation rate (in m/s, it kicks in after t3) 103 | h_mud = 0.4 # thickness of overbank deposit for each time step 104 | dx = 10.0 # gridcell size in meters 105 | ``` 106 | The first five of these parameters have to be specified before creating the centerlines. The initial slope (Sl) in a fluvial model is best set to zero, as typical gradients in meandering rivers are very low and artifacts associated with the along-channel slope variation will be visible in the model surfaces [this is not an issue with steeper submarine channel models]. t1 is the time step when incision starts; before t1, the centerlines are given time to develop some sinuosity. At time t2, incision stops and the channel only migrates laterally until t3; this is the time when aggradation starts. The rate of incision (if Sl is set to zero) is set by the quantity 'kv x dens x 9.81 x D x dt x 0.01' (as if the slope was 0.01, but of course it is not), where kv is the vertical incision rate constant. This approach does not require a new incision rate constant. The rate of aggradation is set by 'aggr_factor x dt' (so 'aggr_factor' must be a small number, as it is measured in m/s). 'h_mud' is the maximum thickness of the overbank deposit in each time step, and 'dx' is the gridcell size in meters. 'h_mud' has to be large enough that it matches the channel aggradation rate; weird artefacts are generated otherwise. 107 | 108 | The Jupyter notebook has two examples for building 3D models, for a fluvial and a submarine channel system. The 'plot_xsection' method can be used to create a cross section at a given x (pixel) coordinate (this is the first argument of the function). The second argument determines the colors that are used for the different facies (in this case: brown, yellow, brown RGB values). The third argument is the vertical exaggeration. 109 | 110 | ```python 111 | fig1,fig2,fig3 = chb_3d.plot_xsection(343, [[0.5,0.25,0],[0.9,0.9,0],[0.5,0.25,0]], 4) 112 | ``` 113 | This function also plots the basal erosional surface and the final topographic surface. An example topographic surface and a zoomed-in cross section are shown below. 114 | 115 | 116 | 117 | 118 | 119 | ## Google Colab notebook 120 | 121 | If you don't want to deal with any local Python environments and installations, you should be able to run meanderpy in [this Google Colab notebook](https://colab.research.google.com/drive/1eZgGD_eXddaAeqxmI9guGIcTjjrLXmKO?usp=sharing). 122 | 123 | ## Related publications 124 | 125 | If you use meanderpy in your work, please consider citing one or more of these publications: 126 | 127 | Sylvester, Z., Durkin, P., and Covault, J.A., 2019, High curvatures drive river meandering: Geology, v. 47, p. 263–266, [doi:10.1130/G45608.1](https://doi.org/10.1130/G45608.1). 128 | 129 | Sylvester, Z., and Covault, J.A., 2016, Development of cutoff-related knickpoints during early evolution of submarine channels: Geology, v. 44, p. 835–838, [doi:10.1130/G38397.1](https://doi.org/10.1130/G38397.1). 130 | 131 | Covault, J.A., Sylvester, Z., Hubbard, S.M., and Jobe, Z.R., 2016, The Stratigraphic Record of Submarine-Channel Evolution: The Sedimentary Record, v. 14, no. 3, p. 4-11, [doi:10.2210/sedred.2016.3](https://www.sepm.org/files/143article.hqx9r9brxux8f2se.pdf). 132 | 133 | Sylvester, Z., Pirmez, C., and Cantelli, A., 2011, A model of submarine channel-levee evolution based on channel trajectories: Implications for stratigraphic architecture: Marine and Petroleum Geology, v. 28, p. 716–727, [doi:10.1016/j.marpetgeo.2010.05.012](https://doi.org/10.1016/j.marpetgeo.2010.05.012). 134 | 135 | ## Acknowledgements 136 | 137 | While the code in 'meanderpy' was written relatively recently, many of the ideas implemented in it come from numerous discussions with Carlos Pirmez, Alessandro Cantelli, Matt Wolinsky, Nick Howes, and Jake Covault. Funding for this work comes from the [Quantitative Clastics Laboratory industrial consortium](http://www.beg.utexas.edu/qcl) at the Bureau of Economic Geology, The University of Texas at Austin. 138 | 139 | ## License 140 | 141 | meanderpy is licensed under the [Apache License 2.0](https://github.com/zsylvester/meanderpy/blob/master/LICENSE.txt). 142 | -------------------------------------------------------------------------------- /define_3D_domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsylvester/meanderpy/10938b0eafab77830304185d380dd1751c758d8b/define_3D_domain.png -------------------------------------------------------------------------------- /fluvial_meanderpy_example_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsylvester/meanderpy/10938b0eafab77830304185d380dd1751c758d8b/fluvial_meanderpy_example_map.png -------------------------------------------------------------------------------- /fluvial_meanderpy_example_section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsylvester/meanderpy/10938b0eafab77830304185d380dd1751c758d8b/fluvial_meanderpy_example_section.png -------------------------------------------------------------------------------- /meanderpy/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /meanderpy/__init__.py: -------------------------------------------------------------------------------- 1 | name = "meanderpy" 2 | from .meanderpy import * -------------------------------------------------------------------------------- /meanderpy/meanderpy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import meanderpy as mp\n", 10 | "import matplotlib.pyplot as plt\n", 11 | "import numpy as np\n", 12 | "%matplotlib qt" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 6, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "data": { 22 | "text/plain": [ 23 | "" 24 | ] 25 | }, 26 | "execution_count": 6, 27 | "metadata": {}, 28 | "output_type": "execute_result" 29 | } 30 | ], 31 | "source": [ 32 | "from importlib import reload\n", 33 | "reload(mp)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "## Input parameters" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 3, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "nit = 2000 # number of iterations\n", 50 | "W = 200.0 # channel width (m)\n", 51 | "D = 6.0 # channel depth (m)\n", 52 | "depths = D * np.ones((nit,)) # channel depths for different iterations \n", 53 | "pad = 100 # padding (number of nodepoints along centerline)\n", 54 | "deltas = 50.0 # sampling distance along centerline \n", 55 | "Cfs = 0.011 * np.ones((nit,)) # dimensionless Chezy friction factor\n", 56 | "crdist = 2 * W # threshold distance at which cutoffs occur\n", 57 | "kl = 60.0/(365*24*60*60.0) # migration rate constant (m/s)\n", 58 | "kv = 1.0e-11 # vertical slope-dependent erosion rate constant (m/s)\n", 59 | "dt = 2*0.05*365*24*60*60.0 # time step (s)\n", 60 | "dens = 1000 # density of water (kg/m3)\n", 61 | "saved_ts = 20 # which time steps will be saved\n", 62 | "n_bends = 30 # approximate number of bends you want to model\n", 63 | "Sl = 0.002 # initial slope (matters more for submarine channels than rivers)\n", 64 | "t1 = 500 # time step when incision starts\n", 65 | "t2 = 700 # time step when lateral migration starts\n", 66 | "t3 = 1200 # time step when aggradation starts\n", 67 | "aggr_factor = 2e-9 # aggradation factor (m/s, about 0.18 m/year, it kicks in after t3)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "## Initialize model" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 7, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "ch = mp.generate_initial_channel(W, depths[0], Sl, deltas, pad, n_bends) # initialize channel\n", 84 | "chb = mp.ChannelBelt(channels=[ch], cutoffs=[], cl_times=[0.0], cutoff_times=[]) # create channel belt object" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": {}, 90 | "source": [ 91 | "## Run simulation" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 8, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stderr", 101 | "output_type": "stream", 102 | "text": [ 103 | "100%|███████████████████████████████████████| 2000/2000 [00:21<00:00, 91.67it/s]\n" 104 | ] 105 | } 106 | ], 107 | "source": [ 108 | "chb.migrate(nit,saved_ts,deltas,pad,crdist,depths,Cfs,kl,kv,dt,dens) #,t1,t2,t3,aggr_factor) # channel migration\n", 109 | "fig = chb.plot('strat', 20, 60, chb.cl_times[-1], len(chb.channels)) # plotting" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 9, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "# check the z-profiles (to see whether there is the right amount of incision/aggradation):\n", 119 | "plt.figure()\n", 120 | "for channel in chb.channels:\n", 121 | " plt.plot(channel.x, channel.z, 'k', linewidth=0.5)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "Create a \"geomorphologic\" display that takes into account that older point bars and cutoffs are covered by vegetation:" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": 10, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "fig = chb.plot('morph', 20, 60, chb.cl_times[-1], len(chb.channels))" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Create a map that is colored by the age of the point bars:" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 14, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "fig = chb.plot('age', 20, 60, chb.cl_times[-1], len(chb.channels))" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "## Create movie" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": 9, 166 | "metadata": {}, 167 | "outputs": [], 168 | "source": [ 169 | "dirname = '/Users/zoltan/Dropbox/Channels/temp/'\n", 170 | "chb.create_movie(xmin=10000, xmax=30000, plot_type='strat', filename='movie', dirname=dirname,\n", 171 | " pb_age = 1, ob_age = 20, end_time = chb.cl_times[-1], n_channels = len(chb.channels))" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "## Build 3D fluvial model\n", 179 | "\n", 180 | "### Non-interactive definition of x- and y-extent\n", 181 | "\n", 182 | "If the parameters 'xmin', 'xmax', ymin', and 'ymax' are non-zero (as in the cell below), they will be used to define the extent of the area of interest used to build the 3D model. At least initially, it is a good idea to keep this segment relatively small (only a few bends long) to avoid building very large models." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 15, 188 | "metadata": {}, 189 | "outputs": [ 190 | { 191 | "name": "stderr", 192 | "output_type": "stream", 193 | "text": [ 194 | "100%|█████████████████████████████████████████| 100/100 [00:39<00:00, 2.50it/s]" 195 | ] 196 | }, 197 | { 198 | "name": "stdout", 199 | "output_type": "stream", 200 | "text": [ 201 | "43.98457647341001\n" 202 | ] 203 | }, 204 | { 205 | "name": "stderr", 206 | "output_type": "stream", 207 | "text": [ 208 | "\n" 209 | ] 210 | } 211 | ], 212 | "source": [ 213 | "h_mud = 2.0 * np.ones((len(chb.channels),)) # thickness of overbank deposit for each time step\n", 214 | "dx = 10.0 # gridcell size in meters\n", 215 | "diff_scale = 1.0 * W/dx\n", 216 | "v_coarse = 8.0 # deposition rate of coarse overbank sediment, in m/year (excluding times of no flooding)\n", 217 | "v_fine = 0.0 # deposition rate of fine overbank sediment, in m/year (excluding times of no flooding)\n", 218 | "\n", 219 | "chb_3d, xmin, xmax, ymin, ymax, dists, zmaps = mp.build_3d_model(chb, 'fluvial', \n", 220 | " h_mud=h_mud, h=12.0, w=W, dx=dx, delta_s=deltas, dt=dt, \n", 221 | " starttime=chb.cl_times[0], endtime=chb.cl_times[-1],\n", 222 | " diff_scale=diff_scale, v_fine=v_fine, v_coarse=v_coarse, \n", 223 | " xmin=15000, xmax=20000, ymin=-3500, ymax=3500)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 17, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "# create plots\n", 233 | "fig1,fig2,fig3 = chb_3d.plot_xsection(300, [[0.9,0.9,0],[0.5,0.25,0]], 4)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "## Build fluvial model with variable depths and well-defined scrolls" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 25, 246 | "metadata": {}, 247 | "outputs": [ 248 | { 249 | "name": "stderr", 250 | "output_type": "stream", 251 | "text": [ 252 | "100%|███████████████████████████████████████| 2000/2000 [00:22<00:00, 90.27it/s]\n" 253 | ] 254 | } 255 | ], 256 | "source": [ 257 | "nit = 2000 # number of iterations\n", 258 | "W = 200.0 # channel width (m)\n", 259 | "D = 6.0\n", 260 | "saved_ts = 20 # which time steps will be saved# channel depth (m)\n", 261 | "# create variable depth sequence:\n", 262 | "depths = D * np.ones((nit,)) + np.repeat(1.5*(np.random.random_sample(int(nit/saved_ts))-0.5), saved_ts)\n", 263 | "pad = 100 # padding (number of nodepoints along centerline)\n", 264 | "deltas = 50.0 # sampling distance along centerline \n", 265 | "Cfs = 0.011 * np.ones((nit,)) # dimensionless Chezy friction factor\n", 266 | "crdist = 2 * W # threshold distance at which cutoffs occur\n", 267 | "kl = 60.0/(365*24*60*60.0) # migration rate constant (m/s)\n", 268 | "kv = 1.0e-12 # vertical slope-dependent erosion rate constant (m/s)\n", 269 | "dt = 2*0.05*365*24*60*60.0 # time step (s)\n", 270 | "dens = 1000 # density of water (kg/m3)\n", 271 | "n_bends = 30 # approximate number of bends you want to model\n", 272 | "Sl = 0.00 # initial slope (matters more for submarine channels than rivers)\n", 273 | "\n", 274 | "ch = mp.generate_initial_channel(W, depths[0], Sl, deltas, pad, n_bends) # initialize channel\n", 275 | "chb = mp.ChannelBelt(channels=[ch], cutoffs=[], cl_times=[0.0], cutoff_times=[]) # create channel belt object\n", 276 | "\n", 277 | "chb.migrate(nit,saved_ts,deltas,pad,crdist,depths,Cfs,kl,kv,dt,dens) # channel migration\n", 278 | "fig = chb.plot('strat', 20, 60, chb.cl_times[-1], len(chb.channels)) # plotting" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": 27, 284 | "metadata": { 285 | "tags": [] 286 | }, 287 | "outputs": [ 288 | { 289 | "name": "stderr", 290 | "output_type": "stream", 291 | "text": [ 292 | "100%|█████████████████████████████████████████| 100/100 [01:07<00:00, 1.48it/s]\n" 293 | ] 294 | } 295 | ], 296 | "source": [ 297 | "# add a bit more incision:\n", 298 | "for i in range(len(chb.channels)):\n", 299 | " chb.channels[i].z = np.ones(np.shape(chb.channels[i].x))*(-0.1 * i)\n", 300 | "# create 'h_mud' sequence that mimicks the varibaility in depth through time:\n", 301 | "depths1 = depths[::saved_ts]\n", 302 | "depths1 = np.hstack((depths1[0], depths1))\n", 303 | "h_mud = depths1 - 5.0 # maximum thickness of overbank deposit for each time step\n", 304 | "dx = 10.0 # gridcell size in meters\n", 305 | "# reduce diffusion length scale:\n", 306 | "diff_scale = 1.0 * W/dx\n", 307 | "# increase deposition rate of coares sediment:\n", 308 | "v_coarse = 20.0 # deposition rate of coarse overbank sediment, in m/year (excluding times of no flooding)\n", 309 | "v_fine = 0.0 # deposition rate of fine overbank sediment, in m/year (excluding times of no flooding)" 310 | ] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "## Interactive definition of x- and y-extent\n", 317 | "After you run the next cell, you need to select the upper left and lower right corners of the area of interest for which you want to build a 3D model. At least initially, it is a good idea to keep this segment relatively small (only a few bends long) to avoid building very large models. The area will only be highlighted (as a red rectangle) after the 3d model building has finished." 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": 23, 323 | "metadata": {}, 324 | "outputs": [ 325 | { 326 | "name": "stderr", 327 | "output_type": "stream", 328 | "text": [ 329 | "100%|█████████████████████████████████████████| 100/100 [01:00<00:00, 1.65it/s]\n" 330 | ] 331 | } 332 | ], 333 | "source": [ 334 | "chb_3d, xmin, xmax, ymin, ymax, dists, zmaps = mp.build_3d_model(chb, 'fluvial', \n", 335 | " h_mud=h_mud, h=12.0, w=W, \n", 336 | " bth=0.0, dcr=10.0, dx=dx, delta_s=deltas, dt=dt, starttime=chb.cl_times[0], endtime=chb.cl_times[-1],\n", 337 | " diff_scale=diff_scale, v_fine=v_fine, v_coarse=v_coarse)\n", 338 | "\n", 339 | "# create plots\n", 340 | "fig1,fig2,fig3 = chb_3d.plot_xsection(200, [[0.9,0.9,0],[0.5,0.25,0]], 4)" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "metadata": {}, 346 | "source": [ 347 | "## Build 3D submarine channel model" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 42, 353 | "metadata": {}, 354 | "outputs": [], 355 | "source": [ 356 | "reload(mp)\n", 357 | "nit = 2000 # number of iterations\n", 358 | "W = 200.0 # channel width (m)\n", 359 | "D = 6.0 # channel depth (m)\n", 360 | "depths = D * np.ones((nit,)) # channel depths for different iterations \n", 361 | "pad = 50 # padding (number of nodepoints along centerline)\n", 362 | "deltas = W/4 # sampling distance along centerline \n", 363 | "Cfs = 0.011 * np.ones((nit,)) # dimensionless Chezy friction factor\n", 364 | "crdist = 1.5 * W # threshold distance at which cutoffs occur\n", 365 | "kl = 60.0/(365*24*60*60.0) # migration rate constant (m/s)\n", 366 | "kv = 1.0e-12 # vertical slope-dependent erosion rate constant (m/s)\n", 367 | "dt = 2*0.05*365*24*60*60.0 # time step (s)\n", 368 | "dens = 1000 # density of water (kg/m3)\n", 369 | "saved_ts = 20 # which time steps will be saved\n", 370 | "n_bends = 30 # approximate number of bends you want to model\n", 371 | "Sl = 0.01 # initial slope (matters more for submarine channels than rivers)" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 43, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "ch = mp.generate_initial_channel(W, depths[0], Sl, deltas, pad, n_bends) # initialize channel\n", 381 | "chb = mp.ChannelBelt(channels=[ch], cutoffs=[], cl_times=[0.0], cutoff_times=[]) # create channel belt object" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": 44, 387 | "metadata": {}, 388 | "outputs": [ 389 | { 390 | "name": "stderr", 391 | "output_type": "stream", 392 | "text": [ 393 | "100%|███████████████████████████████████████| 2000/2000 [00:23<00:00, 84.27it/s]\n" 394 | ] 395 | } 396 | ], 397 | "source": [ 398 | "# chb.migrate(nit,saved_ts,deltas,pad,crdist,depths,Cfs,kl,kv,dt,dens,t1,t2,t3,aggr_factor) # channel migration\n", 399 | "chb.migrate(nit, saved_ts, deltas, pad, crdist, depths, Cfs, kl, kv, dt, dens)\n", 400 | "fig = chb.plot('strat',20,60,chb.cl_times[-1],len(chb.channels)) # plotting" 401 | ] 402 | }, 403 | { 404 | "cell_type": "markdown", 405 | "metadata": {}, 406 | "source": [ 407 | "After you run the next cell, you need to select the upper left and lower right corners of the area of interest for which you want to build a 3D model. At least initially, it is a good idea to keep this segment relatively small (only a few bends long) to avoid building very large models. The area will only be highlighted (as a red rectangle) after the 3d model building has finished." 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 45, 413 | "metadata": {}, 414 | "outputs": [ 415 | { 416 | "name": "stderr", 417 | "output_type": "stream", 418 | "text": [ 419 | "100%|█████████████████████████████████████████| 100/100 [03:21<00:00, 2.01s/it]\n" 420 | ] 421 | } 422 | ], 423 | "source": [ 424 | "h_mud = 4.0 * np.ones((len(chb.cl_times),)) # thickness of overbank deposit for each time step\n", 425 | "dx = 10.0 # gridcell size in meters\n", 426 | "diff_scale = 3 * W/dx\n", 427 | "v_coarse = 4.0 # deposition rate of coarse overbank sediment, in m/year (excluding times of no flow in channel)\n", 428 | "v_fine = 0.0 # deposition rate of fine overbank sediment, in m/year (excluding times of no flow in channel)\n", 429 | "\n", 430 | "chb_3d, xmin, xmax, ymin, ymax, dists, zmaps = mp.build_3d_model(chb, \n", 431 | " 'submarine', h_mud=h_mud, h=15.0, w=W, \n", 432 | " bth=4.0, dcr=6.0, dx=dx, delta_s=deltas, dt=dt, starttime=chb.cl_times[0], endtime=chb.cl_times[-1],\n", 433 | " diff_scale=diff_scale, v_fine=v_fine, v_coarse=v_coarse)" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": 47, 439 | "metadata": {}, 440 | "outputs": [], 441 | "source": [ 442 | "fig1,fig2,fig3 = chb_3d.plot_xsection(1000, [[0.9,0.9,0], [0.5,0.25,0]], 10)" 443 | ] 444 | } 445 | ], 446 | "metadata": { 447 | "kernelspec": { 448 | "display_name": "Python 3 (ipykernel)", 449 | "language": "python", 450 | "name": "python3" 451 | }, 452 | "language_info": { 453 | "codemirror_mode": { 454 | "name": "ipython", 455 | "version": 3 456 | }, 457 | "file_extension": ".py", 458 | "mimetype": "text/x-python", 459 | "name": "python", 460 | "nbconvert_exporter": "python", 461 | "pygments_lexer": "ipython3", 462 | "version": "3.11.4" 463 | } 464 | }, 465 | "nbformat": 4, 466 | "nbformat_minor": 4 467 | } 468 | -------------------------------------------------------------------------------- /meanderpy/meanderpy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import scipy.interpolate 4 | from scipy.spatial import distance 5 | from scipy import ndimage 6 | from PIL import Image, ImageDraw 7 | from skimage import measure 8 | from skimage import morphology 9 | # from skimage import filters 10 | from scipy.ndimage import gaussian_filter 11 | from matplotlib.colors import LinearSegmentedColormap 12 | import time, sys 13 | import numba 14 | import matplotlib.colors as mcolors 15 | from matplotlib import cm 16 | from tqdm import trange 17 | import h5py 18 | from scipy.signal import savgol_filter 19 | 20 | 21 | class Channel: 22 | """class for Channel objects""" 23 | 24 | def __init__(self,x,y,z,W,D): 25 | """ 26 | Initialize Channel object. 27 | 28 | Parameters 29 | ---------- 30 | x : array_like 31 | x-coordinate of centerline. 32 | y : array_like 33 | y-coordinate of centerline. 34 | z : array_like 35 | z-coordinate of centerline. 36 | W : float 37 | Channel width. 38 | D : float 39 | Channel depth. 40 | """ 41 | 42 | self.x = x 43 | self.y = y 44 | self.z = z 45 | self.W = W 46 | self.D = D 47 | 48 | class Cutoff: 49 | """class for Cutoff objects""" 50 | 51 | def __init__(self,x,y,z,W,D): 52 | """ 53 | Initialize Cutoff object. 54 | 55 | Parameters 56 | ---------- 57 | x : array_like 58 | x-coordinate of centerline. 59 | y : array_like 60 | y-coordinate of centerline. 61 | z : array_like 62 | z-coordinate of centerline. 63 | W : float 64 | Channel width. 65 | D : float 66 | Channel depth. 67 | """ 68 | 69 | self.x = x 70 | self.y = y 71 | self.z = z 72 | self.W = W 73 | self.D = D 74 | 75 | class ChannelBelt3D: 76 | """class for 3D models of channel belts""" 77 | 78 | def __init__(self, model_type, topo, strat, facies, facies_code, dx, channels): 79 | """ 80 | Initialize ChannelBelt3D object. 81 | 82 | Parameters 83 | ---------- 84 | model_type : str 85 | Type of model to be built; can be either 'fluvial' or 'submarine'. 86 | topo : numpy.ndarray 87 | Set of topographic surfaces (3D numpy array). 88 | strat : numpy.ndarray 89 | Set of stratigraphic surfaces (3D numpy array). 90 | facies : numpy.ndarray 91 | Facies volume (3D numpy array). 92 | facies_code : dict 93 | Dictionary of facies codes, e.g. {0:'oxbow', 1:'point bar', 2:'levee'}. 94 | dx : float 95 | Gridcell size (m). 96 | channels : list 97 | List of channel objects that form 3D model. 98 | """ 99 | 100 | self.model_type = model_type 101 | self.topo = topo 102 | self.strat = strat 103 | self.facies = facies 104 | self.facies_code = facies_code 105 | self.dx = dx 106 | self.channels = channels 107 | 108 | def plot_xsection(self, xsec, colors, ve): 109 | """ 110 | Method for plotting a cross section through a 3D model; also plots map of 111 | basal erosional surface and map of final geomorphic surface. 112 | 113 | Parameters 114 | ---------- 115 | xsec : int 116 | Location of cross section along the x-axis (in pixel/voxel coordinates). 117 | colors : list of tuple 118 | List of RGB values that define the colors for different facies. 119 | ve : float 120 | Vertical exaggeration. 121 | 122 | Returns 123 | ------- 124 | fig1 : matplotlib.figure.Figure 125 | Figure handle for the cross section plot. 126 | fig2 : matplotlib.figure.Figure 127 | Figure handle for the final geomorphic surface plot. 128 | fig3 : matplotlib.figure.Figure 129 | Figure handle for the basal erosional surface plot. 130 | """ 131 | 132 | strat = self.strat 133 | dx = self.dx 134 | fig1 = plt.figure(figsize=(20,5)) 135 | ax1 = fig1.add_subplot(111) 136 | r,c,ts = np.shape(strat) 137 | Xv = dx * np.arange(0,r) 138 | for i in range(0,ts-1,2): 139 | X1 = np.concatenate((Xv, Xv[::-1])) 140 | Y1 = np.concatenate((strat[:,xsec,i], strat[::-1,xsec,i+1])) 141 | Y2 = np.concatenate((strat[:,xsec,i+1], strat[::-1,xsec,i+2])) 142 | # Y3 = np.concatenate((strat[:,xsec,i+2], strat[::-1,xsec,i+3])) 143 | if self.model_type == 'submarine': 144 | ax1.fill(X1, Y1, facecolor=colors[0], linewidth=0.5, edgecolor=[0,0,0]) # channel sand 145 | ax1.fill(X1, Y2, facecolor=colors[1], linewidth=0.5, edgecolor=[0,0,0]) # levee mud 146 | if self.model_type == 'fluvial': 147 | ax1.fill(X1, Y1, facecolor=colors[0], linewidth=0.5, edgecolor=[0,0,0]) # channel sand 148 | ax1.fill(X1, Y2, facecolor=colors[1], linewidth=0.5, edgecolor=[0,0,0]) # levee mud 149 | # ax1.fill(X1, Y3, facecolor=colors[2], linewidth=0.5) # channel sand 150 | ax1.set_xlim(0,dx*(r-1)) 151 | ax1.set_aspect(ve, adjustable='datalim') 152 | fig2 = plt.figure() 153 | ax2 = fig2.add_subplot(111) 154 | ax2.contourf(strat[:,:,ts-1],100,cmap='viridis') 155 | ax2.contour(strat[:,:,ts-1],100,colors='k',linestyles='solid',linewidths=0.1,alpha=0.4) 156 | ax2.plot([xsec, xsec],[0,r],'k',linewidth=2) 157 | ax2.axis([0,c,0,r]) 158 | ax2.set_aspect('equal', adjustable='box') 159 | ax2.set_title('final geomorphic surface') 160 | ax2.tick_params(bottom=False,top=False,left=False,right=False,labelbottom=False,labelleft=False) 161 | fig3 = plt.figure() 162 | ax3 = fig3.add_subplot(111) 163 | ax3.contourf(strat[:,:,0],100,cmap='viridis') 164 | ax3.contour(strat[:,:,0],100,colors='k',linestyles='solid',linewidths=0.1,alpha=0.4) 165 | ax3.plot([xsec, xsec],[0,r],'k',linewidth=2) 166 | ax3.axis([0,c,0,r]) 167 | ax3.set_aspect('equal', adjustable='box') 168 | ax3.set_title('basal erosional surface') 169 | ax3.tick_params(bottom=False,top=False,left=False,right=False,labelbottom=False,labelleft=False) 170 | return fig1, fig2, fig3 171 | 172 | class ChannelBelt: 173 | """class for ChannelBelt objects""" 174 | 175 | def __init__(self, channels, cutoffs, cl_times, cutoff_times): 176 | """ 177 | Initialize ChannelBelt object. 178 | 179 | Parameters 180 | ---------- 181 | channels : list of Channel 182 | List of Channel objects. 183 | cutoffs : list of Cutoff 184 | List of Cutoff objects. 185 | cl_times : list of float 186 | List of ages of Channel objects (in years). 187 | cutoff_times : list of float 188 | List of ages of Cutoff objects. 189 | """ 190 | 191 | self.channels = channels 192 | self.cutoffs = cutoffs 193 | self.cl_times = cl_times 194 | self.cutoff_times = cutoff_times 195 | 196 | def migrate(self, nit, saved_ts, deltas, pad, crdist, depths, Cfs, kl, kv, dt, dens, autoaggradation=True, Scr=0.001, t1=None, t2=None, t3=None, aggr_factor=None): 197 | """ 198 | Compute migration rates along channel centerlines and move the centerlines accordingly. 199 | 200 | Parameters 201 | ---------- 202 | nit : int 203 | Number of iterations. 204 | saved_ts : int 205 | Which time steps will be saved; e.g., if saved_ts = 10, every tenth time step will be saved. 206 | deltas : float 207 | Distance between nodes on centerline. 208 | pad : int 209 | Padding (number of nodepoints along centerline). 210 | crdist : float 211 | Threshold distance at which cutoffs occur. 212 | depths : array_like 213 | Array of channel depths (can vary across iterations). 214 | Cfs : array_like 215 | Array of dimensionless Chezy friction factors (can vary across iterations). 216 | kl : float 217 | Migration rate constant (m/s). 218 | kv : float 219 | Vertical slope-dependent erosion rate constant (m/s). 220 | dt : float 221 | Time step (s). 222 | dens : float 223 | Density of fluid (kg/m^3). 224 | autoaggradation : bool, optional 225 | If True, autoaggradation is applied. Default is True. 226 | Scr : float, optional 227 | Critical slope for autoaggradation. Default is 0.001. 228 | t1 : int, optional 229 | Time step when incision starts. Default is None. 230 | t2 : int, optional 231 | Time step when lateral migration starts. Default is None. 232 | t3 : int, optional 233 | Time step when aggradation starts. Default is None. 234 | aggr_factor : float, optional 235 | Aggradation factor. Default is None. 236 | """ 237 | 238 | channel = self.channels[-1] # first channel is the same as last channel of input 239 | x = channel.x; y = channel.y; z = channel.z 240 | W = channel.W 241 | D = channel.D 242 | k = 1.0 # constant in HK equation 243 | xc = [] # initialize cutoff coordinates 244 | # determine age of last channel: 245 | if len(self.cl_times)>0: 246 | last_cl_time = self.cl_times[-1] 247 | else: 248 | last_cl_time = 0 249 | dx, dy, dz, ds, s = compute_derivatives(x,y,z) 250 | slope = np.gradient(z)/ds 251 | # padding at the beginning can be shorter than padding at the downstream end: 252 | pad1 = int(pad/10.0) 253 | if pad1<5: 254 | pad1 = 5 255 | for itn in trange(nit): # main loop 256 | D = depths[itn] 257 | Cf = Cfs[itn] 258 | x, y = migrate_one_step(x,y,z,W,kl,dt,k,Cf,D,pad,pad1) 259 | # x, y = migrate_one_step_w_bias(x,y,z,W,kl,dt,k,Cf,D,pad,pad1) 260 | x,y,z,xc,yc,zc = cut_off_cutoffs(x,y,z,s,crdist,deltas) # find and execute cutoffs 261 | x,y,z,dx,dy,dz,ds,s = resample_centerline(x,y,z,deltas) # resample centerline 262 | z = savgol_filter(z, 21, 2) # filter z-values - needed for autoaggradation 263 | slope = np.gradient(z)/ds # positive number 264 | if autoaggradation: 265 | if np.max(slope) > 0.001: 266 | R = 1.65; C = 0.1 # parameters for autoaggradation 267 | z = z + kv*dens*9.81*R*C*D*(Scr-slope)*dt # autoaggradation 268 | else: 269 | # for itn<=t1, z is unchanged; for itn>t1: 270 | if (itn>t1) & (itn<=t2): # incision 271 | if np.min(np.abs(slope))!=0: # if slope is not zero 272 | z = z + kv*dens*9.81*D*slope*dt 273 | else: 274 | z = z - kv*dens*9.81*D*dt*0.05 # if slope is zero 275 | if (itn>t2) & (itn<=t3): # lateral migration 276 | if np.min(np.abs(slope))!=0: # if slope is not zero 277 | # use the median slope to counterbalance incision: 278 | z = z + kv*dens*9.81*D*slope*dt - kv*dens*9.81*D*np.median(slope)*dt 279 | else: 280 | z = z # no change in z 281 | if (itn>t3): # aggradation 282 | if np.min(np.abs(slope))!=0: # if slope is not zero 283 | # 'aggr_factor' should be larger than 1 so that this leads to overall aggradation: 284 | z = z + kv*dens*9.81*D*slope*dt - aggr_factor*kv*dens*9.81*D*np.mean(slope)*dt 285 | else: 286 | z = z + aggr_factor*dt 287 | if len(xc)>0: # save cutoff data 288 | self.cutoff_times.append(last_cl_time+(itn+1)*dt/(365*24*60*60.0)) 289 | cutoff = Cutoff(xc,yc,zc,W,D) # create cutoff object 290 | self.cutoffs.append(cutoff) 291 | # saving centerlines (with the exception of first channel): 292 | if (np.mod(itn, saved_ts) == 0) and (itn > 0): 293 | self.cl_times.append(last_cl_time+(itn+1)*dt/(365*24*60*60.0)) 294 | channel = Channel(x,y,z,W,D) # create channel object 295 | self.channels.append(channel) 296 | 297 | def plot(self, plot_type, pb_age, ob_age, end_time, n_channels): 298 | """ 299 | Method for plotting ChannelBelt object. 300 | 301 | Parameters 302 | ---------- 303 | plot_type : str 304 | Can be either 'strat' (for stratigraphic plot), 'morph' (for morphologic plot), or 'age' (for age plot). 305 | pb_age : int 306 | Age of point bars (in years) at which they get covered by vegetation. 307 | ob_age : int 308 | Age of oxbow lakes (in years) at which they get covered by vegetation. 309 | end_time : int 310 | Age of last channel to be plotted (in years). 311 | n_channels : int 312 | Total number of channels (used in 'age' plots; can be larger than number of channels being plotted). 313 | 314 | Returns 315 | ------- 316 | fig : matplotlib.figure.Figure 317 | Handle to the figure. 318 | """ 319 | 320 | cot = np.array(self.cutoff_times) 321 | sclt = np.array(self.cl_times) 322 | if end_time>0: 323 | cot = cot[cot<=end_time] 324 | sclt = sclt[sclt<=end_time] 325 | times = np.sort(np.hstack((cot,sclt))) 326 | times = np.unique(times) 327 | order = 0 # variable for ordering objects in plot 328 | # set up min and max x and y coordinates of the plot: 329 | xmin = np.min(self.channels[0].x) 330 | xmax = np.max(self.channels[0].x) 331 | ymax = 0 332 | for i in range(len(self.channels)): 333 | ymax = max(ymax, np.max(np.abs(self.channels[i].y))) 334 | ymax = ymax+2*self.channels[0].W # add a bit of space on top and bottom 335 | ymin = -1*ymax 336 | # size figure so that its size matches the size of the model: 337 | fig = plt.figure(figsize=(20,(ymax-ymin)*20/(xmax-xmin))) 338 | if plot_type == 'morph': 339 | pb_crit = len(times[timestimes[-1]-pb_age: 358 | plt.fill(xm,ym,facecolor=pb_cmap(i/float(len(times)-1)),edgecolor='k',linewidth=0.2) 359 | else: 360 | plt.fill(xm,ym,facecolor=pb_cmap(i/float(len(times)-1))) 361 | if plot_type == 'strat': 362 | order += 1 363 | plt.fill(xm, ym, 'xkcd:light tan', edgecolor='k', linewidth=0.25, zorder=order) 364 | # plt.fill(xm, ym, 'xkcd:light tan', edgecolor='none', linewidth=0.25, zorder=order) 365 | if plot_type == 'age': 366 | order += 1 367 | plt.fill(xm,ym,facecolor=age_cmap(i/float(n_channels-1)),edgecolor='k',linewidth=0.1,zorder=order) 368 | if times[i] in cot: 369 | ind = np.where(cot==times[i])[0][0] 370 | for j in range(0,len(self.cutoffs[ind].x)): 371 | x1 = self.cutoffs[ind].x[j] 372 | y1 = self.cutoffs[ind].y[j] 373 | xm, ym = get_channel_banks(x1,y1,self.cutoffs[ind].W) 374 | if plot_type == 'morph': 375 | plt.fill(xm,ym,color=ob_cmap(i/float(len(times)-1))) 376 | if plot_type == 'strat': 377 | order = order+1 378 | plt.fill(xm, ym, 'xkcd:ocean blue', edgecolor='k', linewidth=0.25, zorder=order) 379 | # plt.fill(xm, ym, 'xkcd:ocean blue', edgecolor='none', linewidth=0.25, zorder=order) 380 | if plot_type == 'age': 381 | order += 1 382 | plt.fill(xm, ym, 'xkcd:sea blue', edgecolor='k', linewidth=0.1, zorder=order) 383 | 384 | x1 = self.channels[len(sclt)-1].x 385 | y1 = self.channels[len(sclt)-1].y 386 | xm, ym = get_channel_banks(x1,y1,self.channels[len(sclt)-1].W) 387 | order = order+1 388 | if plot_type == 'age': 389 | plt.fill(xm, ym, color='xkcd:sea blue', zorder=order, edgecolor='k', linewidth=0.1) 390 | else: 391 | plt.fill(xm, ym, color=(16/255.0,73/255.0,90/255.0), edgecolor='none', zorder=order) #,edgecolor='k') 392 | plt.axis('equal') 393 | # plt.xlim(xmin,xmax) 394 | # plt.ylim(ymin,ymax) 395 | plt.xticks([]) 396 | plt.yticks([]) 397 | plt.tight_layout() 398 | return fig 399 | 400 | def create_movie(self, xmin, xmax, plot_type, filename, dirname, pb_age, ob_age, end_time, n_channels): 401 | """ 402 | Method for creating movie frames (PNG files) that capture the plan-view evolution of a channel belt through time. 403 | The movie has to be assembled from the PNG files after this method is applied. 404 | 405 | Parameters 406 | ---------- 407 | xmin : float 408 | Value of x coordinate on the left side of the frame. 409 | xmax : float 410 | Value of x coordinate on the right side of the frame. 411 | plot_type : str 412 | Plot type; can be either 'strat' (for stratigraphic plot) or 'morph' (for morphologic plot). 413 | filename : str 414 | First few characters of the output filenames. 415 | dirname : str 416 | Name of the directory where output files should be written. 417 | pb_age : int 418 | Age of point bars (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type'). 419 | ob_age : int 420 | Age of oxbow lakes (in years) at which they get covered by vegetation (if the 'morph' option is used for 'plot_type'). 421 | end_time : float or list of float 422 | Time at which the simulation should be stopped. 423 | n_channels : int 424 | Total number of channels + cutoffs for which the simulation is run (usually it is len(chb.cutoffs) + len(chb.channels)). Used when plot_type = 'age'. 425 | 426 | """ 427 | 428 | sclt = np.array(self.cl_times) 429 | if type(end_time) != list: 430 | sclt = sclt[sclt<=end_time] 431 | channels = self.channels[:len(sclt)] 432 | ymax = 0 433 | for i in range(len(channels)): 434 | ymax = max(ymax, np.max(np.abs(channels[i].y))) 435 | ymax = ymax+2*channels[0].W # add a bit of space on top and bottom 436 | ymin = -1*ymax 437 | for i in range(0,len(sclt)): 438 | fig = self.plot(plot_type, pb_age, ob_age, sclt[i], n_channels) 439 | scale = 1 440 | fig_height = scale*fig.get_figheight() 441 | fig_width = (xmax-xmin)*fig_height/(ymax-ymin) 442 | fig.set_figwidth(fig_width) 443 | fig.set_figheight(fig_height) 444 | fig.gca().set_xlim(xmin,xmax) 445 | fig.gca().set_xticks([]) 446 | fig.gca().set_yticks([]) 447 | plt.plot([xmin+200, xmin+200+5000],[ymin+200, ymin+200], 'k', linewidth=2) 448 | plt.text(xmin+200+2000, ymin+200+100, '5 km', fontsize=14) 449 | fname = dirname+filename+'%03d.png'%(i) 450 | fig.savefig(fname, bbox_inches='tight') 451 | plt.close() 452 | 453 | def build_3d_model(chb, model_type, h_mud, h, w, dx, delta_s, dt, starttime, endtime, diff_scale, v_fine, v_coarse, xmin=None, xmax=None, ymin=None, ymax=None, bth=None, dcr=None): 454 | """ 455 | Build 3D model from set of centerlines (that are part of a ChannelBelt object). 456 | 457 | Parameters 458 | ---------- 459 | model_type : str 460 | Model type ('fluvial' or 'submarine'). 461 | h_mud : float 462 | Maximum thickness of overbank deposit. 463 | h : float 464 | Channel depth. 465 | w : float 466 | Channel width. 467 | dx : float 468 | Cell size in x and y directions. 469 | delta_s : float 470 | Sampling distance along centerlines. 471 | starttime : float 472 | Age of centerline that will be used as the first centerline in the model. 473 | endtime : float 474 | Age of centerline that will be used as the last centerline in the model. 475 | xmin : float, optional 476 | Minimum x coordinate that defines the model domain; if xmin is set to zero, 477 | a plot of the centerlines is generated and the model domain has to be defined by clicking its upper left and lower right corners. 478 | xmax : float, optional 479 | Maximum x coordinate that defines the model domain. 480 | ymin : float, optional 481 | Minimum y coordinate that defines the model domain. 482 | ymax : float, optional 483 | Maximum y coordinate that defines the model domain. 484 | diff_scale : float 485 | Diffusion length scale (for overbank deposition). 486 | v_fine : float 487 | Deposition rate of fine sediment, in m/year (for overbank deposition). 488 | v_coarse : float 489 | Deposition rate of coarse sediment, in m/year (for overbank deposition). 490 | bth : float, optional 491 | Thickness of channel sand (only used in submarine models). 492 | dcr : float, optional 493 | Critical channel depth where sand thickness goes to zero (only used in submarine models). 494 | 495 | Returns 496 | ------- 497 | chb_3d : ChannelBelt3D 498 | A ChannelBelt3D object. 499 | xmin : float 500 | Minimum x coordinate that defines the model domain. 501 | xmax : float 502 | Maximum x coordinate that defines the model domain. 503 | ymin : float 504 | Minimum y coordinate that defines the model domain. 505 | ymax : float 506 | Maximum y coordinate that defines the model domain. 507 | """ 508 | 509 | sclt = np.array(chb.cl_times) 510 | ind1 = np.where(sclt >= starttime)[0][0] 511 | ind2 = np.where(sclt <= endtime)[0][-1] 512 | sclt = sclt[ind1:ind2+1] 513 | channels = chb.channels[ind1:ind2+1] 514 | cot = np.array(chb.cutoff_times) 515 | if (len(cot)>0) and (len(np.where(cot >= starttime)[0])>0) and (len(np.where(cot <= endtime)[0])>0): 516 | cfind1 = np.where(cot >= starttime)[0][0] 517 | cfind2 = np.where(cot <= endtime)[0][-1] 518 | cot = cot[cfind1:cfind2+1] 519 | else: 520 | cot = [] 521 | n_steps = len(sclt) # number of events 522 | if xmin is None: # plot centerlines and define model domain 523 | plt.figure(figsize=(15,10)) 524 | for i in range(len(channels)): # plot centerlines 525 | plt.plot(chb.channels[i].x, chb.channels[i].y, 'k') 526 | if i == 0: 527 | maxX = np.max(channels[i].x) 528 | minX = np.min(channels[i].x) 529 | maxY = np.max(channels[i].y) 530 | minY = np.min(channels[i].y) 531 | else: 532 | maxX = max(maxX, np.max(channels[i].x)) 533 | minX = min(minX, np.min(channels[i].x)) 534 | maxY = max(maxY, np.max(channels[i].y)) 535 | minY = min(minY, np.min(channels[i].y)) 536 | plt.axis([minX, maxX, minY-10*w, maxY+10*w]) 537 | plt.gca().set_aspect('equal', adjustable='box') 538 | plt.tight_layout() 539 | pts = np.zeros((2,2)) 540 | for i in range(0,2): 541 | pt = np.asarray(plt.ginput(1)) 542 | pts[i,:] = pt 543 | plt.scatter(pt[0][0], pt[0][1]) 544 | plt.plot([pts[0,0], pts[1,0], pts[1,0], pts[0,0], pts[0,0]], [pts[0,1], pts[0,1], pts[1,1], pts[1,1], pts[0,1]], 'r') 545 | xmin = min(pts[0,0], pts[1,0]) 546 | xmax = max(pts[0,0], pts[1,0]) 547 | ymin = min(pts[0,1], pts[1,1]) 548 | ymax = max(pts[0,1], pts[1,1]) 549 | iwidth = int((xmax-xmin)/dx) 550 | iheight = int((ymax-ymin)/dx) 551 | topo = np.zeros((iheight, iwidth, 3*n_steps)) # array for storing topographic surfaces 552 | dists = np.zeros((iheight, iwidth, n_steps)) 553 | zmaps = np.zeros((iheight, iwidth, n_steps)) 554 | facies = np.zeros((3*n_steps, 1)) 555 | # create initial topography: 556 | x1 = np.linspace(0, iwidth-1, iwidth) 557 | y1 = np.linspace(0, iheight-1, iheight) 558 | xv, yv = np.meshgrid(x1,y1) 559 | z1 = channels[0].z 560 | z1 = z1[(channels[0].x > xmin) & (channels[0].x < xmax)] 561 | topoinit = z1[0] - ((z1[0] - z1[-1]) / (xmax - xmin)) * xv * dx # initial (sloped) topography 562 | topo[:,:,0] = topoinit.copy() 563 | surf = topoinit.copy() 564 | facies[0] = np.NaN 565 | # generate surfaces: 566 | channels3D = [] 567 | for i in trange(n_steps): 568 | x = channels[i].x 569 | y = channels[i].y 570 | z = channels[i].z 571 | cutoff_ind = [] 572 | # check if there were cutoffs during the last time step and collect indices in an array: 573 | for j in range(len(cot)): 574 | if (cot[j] >= sclt[i-1]) & (cot[j] < sclt[i]): 575 | cutoff_ind.append(j) 576 | # create distance map: 577 | cl_dist, x_pix, y_pix, z_pix, s_pix, z_map, x1, y1, z1 = dist_map(x, y, z, xmin, xmax, ymin, ymax, dx, delta_s) 578 | # erosion: 579 | surf = np.minimum(surf, erosion_surface(h,w/dx,cl_dist,z_map)) 580 | topo[:,:,3*i] = surf # erosional surface 581 | dists[:,:,i] = cl_dist # distance map 582 | zmaps[:,:,i] = z_map # map of closest channel elevation 583 | facies[3*i] = np.NaN # array for facies code 584 | 585 | if model_type == 'fluvial': 586 | z_map = gaussian_filter(z_map, sigma=50) # smooth z_map to avoid artefacts in levees 587 | pb = point_bar_surface(cl_dist, z_map, h, w/dx) 588 | th = np.maximum(surf,pb)-surf 589 | th[cl_dist > 1.0 * w/dx] = 0 # eliminate sand outside of channel 590 | th[th<0] = 0 # eliminate negative thickness values 591 | surf = surf+th # update topographic surface with sand thickness 592 | topo[:,:,3*i+1] = surf # top of sand 593 | facies[3*i+1] = 1 # facies code for point bar sand 594 | E_max = z_map + h_mud[i] 595 | if i == n_steps-1: 596 | surf_diff = E_max-surf 597 | surf_diff[surf_diff < 0] = 0 598 | plt.figure() 599 | plt.imshow(surf_diff) 600 | levee = fluvial_levee(cl_dist, surf, E_max, w/dx, diff_scale, v_fine, v_coarse, dt) 601 | if i == n_steps-1: 602 | plt.figure() 603 | plt.imshow(levee) 604 | surf = surf + levee # mud/levee deposition 605 | topo[:,:,3*i+2] = surf # top of levee 606 | facies[3*i+2] = 2 # facies code for overbank 607 | channels3D.append(Channel(x1-xmin, y1-ymin, z1, w, h)) 608 | 609 | if model_type == 'submarine': 610 | z_map = gaussian_filter(z_map, sigma=50) # smooth z_map to avoid artefacts in levees 611 | th, relief = sand_surface(surf, bth, dcr, z_map, h) # sandy channel deposit 612 | th[th < 0] = 0 # eliminate negative thickness values 613 | ws = w * (dcr/h)**0.5 # channel width at the top of the channel deposit 614 | th[cl_dist > 1.0 * ws/dx] = 0 # eliminate sand outside of channel 615 | surf = surf+th # update topographic surface with sand thickness 616 | topo[:,:,3*i+1] = surf # top of sand 617 | facies[3*i+1] = 1 # facies code for channel sand 618 | # need to blur z-map so that levees don't have artefacts: 619 | # blurred = filters.gaussian(z_map, sigma=(50, 50), truncate=3.5, multichannel=False) 620 | E_max = z_map + h_mud[i] 621 | levee = submarine_levee(h_mud[i], cl_dist, surf, E_max, w/dx, diff_scale, v_fine, v_coarse, dt) 622 | surf = surf + levee # mud/levee deposition 623 | topo[:,:,3*i+2] = surf # top of levee 624 | facies[3*i+2] = 2 # facies code for overbank 625 | channels3D.append(Channel(x1-xmin, y1-ymin, z1, w, h)) 626 | 627 | topo = np.concatenate((np.reshape(topoinit,(iheight,iwidth,1)),topo),axis=2) # add initial topography to array 628 | strat = topostrat(topo) # create stratigraphic surfaces 629 | strat = np.delete(strat, np.arange(3*n_steps+1)[1::3], 2) # get rid of unnecessary stratigraphic surfaces (duplicates) 630 | facies = np.delete(facies, np.arange(3*n_steps)[::3]) # get rid of unnecessary facies layers (NaNs) 631 | if model_type == 'fluvial': 632 | facies_code = {1:'point bar', 2:'levee'} 633 | if model_type == 'submarine': 634 | facies_code = {1:'channel sand', 2:'levee'} 635 | chb_3d = ChannelBelt3D(model_type, topo, strat, facies, facies_code, dx, channels3D) 636 | return chb_3d, xmin, xmax, ymin, ymax, dists, zmaps 637 | 638 | def resample_centerline(x,y,z,deltas): 639 | """ 640 | Resample centerline so that 'deltas' is roughly constant, using parametric 641 | spline representation of curve; note that there is *no* smoothing. 642 | 643 | Parameters 644 | ---------- 645 | x : array_like 646 | x-coordinates of centerline. 647 | y : array_like 648 | y-coordinates of centerline. 649 | z : array_like 650 | z-coordinates of centerline. 651 | deltas : float 652 | Distance between points on centerline. 653 | 654 | Returns 655 | ------- 656 | x : ndarray 657 | x-coordinates of resampled centerline. 658 | y : ndarray 659 | y-coordinates of resampled centerline. 660 | z : ndarray 661 | z-coordinates of resampled centerline. 662 | dx : ndarray 663 | dx of resampled centerline. 664 | dy : ndarray 665 | dy of resampled centerline. 666 | dz : ndarray 667 | dz of resampled centerline. 668 | ds : ndarray 669 | ds of resampled centerline. 670 | s : ndarray 671 | s-coordinates of resampled centerline. 672 | """ 673 | 674 | dx, dy, dz, ds, s = compute_derivatives(x,y,z) # compute derivatives 675 | tck, u = scipy.interpolate.splprep([x,y,z],s=0) 676 | unew = np.linspace(0,1,1+int(round(s[-1]/deltas))) # vector for resampling 677 | out = scipy.interpolate.splev(unew,tck) # resampling 678 | x, y, z = out[0], out[1], out[2] # assign new coordinate values 679 | dx, dy, dz, ds, s = compute_derivatives(x,y,z) # recompute derivatives 680 | return x,y,z,dx,dy,dz,ds,s 681 | 682 | def migrate_one_step(x,y,z,W,kl,dt,k,Cf,D,pad,pad1,omega=-1.0,gamma=2.5): 683 | """ 684 | Migrate centerline during one time step, using the migration computed as in Howard & Knutson (1984). 685 | 686 | Parameters 687 | ---------- 688 | x : array_like 689 | x-coordinates of centerline. 690 | y : array_like 691 | y-coordinates of centerline. 692 | z : array_like 693 | z-coordinates of centerline. 694 | W : float 695 | Channel width. 696 | kl : float 697 | Migration rate (or erodibility) constant (m/s). 698 | dt : float 699 | Duration of time step (s). 700 | k : float 701 | Constant for calculating the exponent alpha (= 1.0). 702 | Cf : float 703 | Dimensionless Chezy friction factor. 704 | D : float 705 | Channel depth. 706 | pad : int 707 | Padding parameter for migration rate computation. 708 | pad1 : int 709 | Padding parameter for centerline adjustment. 710 | omega : float 711 | Constant in Howard & Knutson equation (= -1.0). 712 | gamma : float 713 | Constant in Howard & Knutson equation (= 2.5). 714 | 715 | Returns 716 | ------- 717 | x : array_like 718 | New x-coordinates of centerline after migration. 719 | y : array_like 720 | New y-coordinates of centerline after migration. 721 | """ 722 | 723 | ns=len(x) 724 | curv = compute_curvature(x,y) 725 | dx, dy, dz, ds, s = compute_derivatives(x,y,z) 726 | # sinuosity = s[-1]/(x[-1]-x[0]) 727 | sinuosity = s[-1]/(np.sqrt((x[-1]-x[0])**2 + (y[-1]-y[0])**2)) 728 | curv = W*curv # dimensionless curvature 729 | R0 = kl*curv # simple linear relationship between curvature and nominal migration rate 730 | alpha = k*2*Cf/D # exponent for convolution function G 731 | R1 = compute_migration_rate(pad,ns,ds,alpha,R0) 732 | R1 = sinuosity**(-2/3.0)*R1 733 | # calculate new centerline coordinates: 734 | dy_ds = dy[pad1:ns-pad+1]/ds[pad1:ns-pad+1] 735 | dx_ds = dx[pad1:ns-pad+1]/ds[pad1:ns-pad+1] 736 | # adjust x and y coordinates (this *is* the migration): 737 | x[pad1:ns-pad+1] = x[pad1:ns-pad+1] + R1[pad1:ns-pad+1]*dy_ds*dt 738 | y[pad1:ns-pad+1] = y[pad1:ns-pad+1] - R1[pad1:ns-pad+1]*dx_ds*dt 739 | return x,y 740 | 741 | def migrate_one_step_w_bias(x,y,z,W,kl,dt,k,Cf,D,pad,pad1,omega=-1.0,gamma=2.5): 742 | ns=len(x) 743 | curv = compute_curvature(x,y) 744 | dx, dy, dz, ds, s = compute_derivatives(x,y,z) 745 | sinuosity = s[-1]/(x[-1]-x[0]) 746 | curv = W*curv # dimensionless curvature 747 | R0 = kl*curv # simple linear relationship between curvature and nominal migration rate 748 | alpha = k*2*Cf/D # exponent for convolution function G 749 | R1 = compute_migration_rate(pad,ns,ds,alpha,R0) 750 | R1 = sinuosity**(-2/3.0)*R1 751 | pad = -1 752 | # calculate new centerline coordinates: 753 | dy_ds = dy[pad1:ns-pad+1]/ds[pad1:ns-pad+1] 754 | dx_ds = dx[pad1:ns-pad+1]/ds[pad1:ns-pad+1] 755 | tilt_factor = 0.2 756 | T = kl*tilt_factor*np.ones(np.shape(x)) 757 | angle = 90.0 758 | # adjust x and y coordinates (this *is* the migration): 759 | x[pad1:ns-pad+1] = x[pad1:ns-pad+1] + R1[pad1:ns-pad+1] * dy_ds * dt + T[pad1:ns-pad+1] * dy_ds * dt * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds) 760 | y[pad1:ns-pad+1] = y[pad1:ns-pad+1] - R1[pad1:ns-pad+1] * dx_ds * dt - T[pad1:ns-pad+1] * dx_ds * dt * (np.sin(np.deg2rad(angle)) * dx_ds + np.cos(np.deg2rad(angle)) * dy_ds) 761 | return x,y 762 | 763 | def generate_initial_channel(W,D,Sl,deltas,pad,n_bends): 764 | """ 765 | Generate a straight Channel object with some noise added that can serve 766 | as input for initializing a ChannelBelt object. 767 | 768 | Parameters 769 | ---------- 770 | W : float 771 | Channel width. 772 | D : float 773 | Channel depth. 774 | Sl : float 775 | Channel gradient. 776 | deltas : float 777 | Distance between nodes on centerline. 778 | pad : int 779 | Padding (number of node points along centerline). 780 | n_bends : int 781 | Approximate number of bends to be simulated. 782 | 783 | Returns 784 | ------- 785 | Channel 786 | A Channel object initialized with the generated coordinates and dimensions. 787 | """ 788 | noisy_len = n_bends*10*W/2.0 # length of noisy part of initial centerline 789 | pad1 = int(pad/10.0) # padding at upstream end can be shorter than padding on downstream end 790 | if pad1<5: 791 | pad1 = 5 792 | x = np.linspace(0, noisy_len+(pad+pad1)*deltas, int(noisy_len/deltas+pad+pad1)+1) # x coordinate 793 | y = 2.0 * (2*np.random.random_sample(int(noisy_len/deltas)+1,)-1) 794 | y = np.hstack((np.zeros((pad1),),y,np.zeros((pad),))) # y coordinate 795 | deltaz = Sl * deltas*(len(x)-1) 796 | z = np.linspace(0,deltaz,len(x))[::-1] # z coordinate 797 | return Channel(x,y,z,W,D) 798 | 799 | @numba.jit(nopython=True) # use Numba to speed up the heaviest computation 800 | def compute_migration_rate(pad, ns, ds, alpha, R0, omega=-1.0, gamma=2.5): 801 | """ 802 | Compute migration rate as weighted sum of upstream curvatures. 803 | 804 | Parameters 805 | ---------- 806 | pad : int 807 | Padding (number of nodepoints along centerline). 808 | ns : int 809 | Number of points in centerline. 810 | ds : ndarray 811 | Distances between points in centerline. 812 | alpha : float 813 | Exponent for convolution function G. 814 | R0 : ndarray 815 | Nominal migration rate (dimensionless curvature * migration rate constant). 816 | omega : float, optional 817 | Constant in HK model, by default -1.0. 818 | gamma : float, optional 819 | Constant in HK model, by default 2.5. 820 | 821 | Returns 822 | ------- 823 | ndarray 824 | Adjusted channel migration rate. 825 | """ 826 | R1 = np.zeros(ns) # preallocate adjusted channel migration rate 827 | pad1 = int(pad / 10.0) # padding at upstream end can be shorter than padding on downstream end 828 | if pad1 < 5: 829 | pad1 = 5 830 | for i in range(pad1, ns - pad): 831 | si2 = np.hstack((np.array([0]), np.cumsum(ds[i - 1::-1]))) # distance along centerline, backwards from current point 832 | G = np.exp(-alpha * si2) # convolution vector 833 | R1[i] = omega * R0[i] + gamma * np.sum(R0[i::-1] * G) / np.sum(G) # main equation 834 | return R1 835 | 836 | def compute_derivatives(x,y,z): 837 | """ 838 | Compute first derivatives of a curve (centerline). 839 | 840 | Parameters 841 | ---------- 842 | x : array_like 843 | Cartesian x-coordinates of the curve. 844 | y : array_like 845 | Cartesian y-coordinates of the curve. 846 | z : array_like 847 | Cartesian z-coordinates of the curve. 848 | 849 | Returns 850 | ------- 851 | dx : ndarray 852 | First derivative of x coordinate. 853 | dy : ndarray 854 | First derivative of y coordinate. 855 | dz : ndarray 856 | First derivative of z coordinate. 857 | ds : ndarray 858 | Distances between consecutive points along the curve. 859 | s : ndarray 860 | Cumulative distance along the curve. 861 | """ 862 | dx = np.gradient(x) # first derivatives 863 | dy = np.gradient(y) 864 | dz = np.gradient(z) 865 | ds = np.sqrt(dx**2+dy**2+dz**2) 866 | s = np.hstack((0,np.cumsum(ds[1:]))) 867 | return dx, dy, dz, ds, s 868 | 869 | def compute_curvature(x,y): 870 | """ 871 | Compute the first derivatives and curvature of a curve (centerline). 872 | 873 | Parameters 874 | ---------- 875 | x : array_like 876 | Cartesian coordinates of the curve along the x-axis. 877 | y : array_like 878 | Cartesian coordinates of the curve along the y-axis. 879 | 880 | Returns 881 | ------- 882 | curvature : ndarray 883 | Curvature of the curve (in 1/units of x and y). 884 | 885 | Notes 886 | ----- 887 | The function calculates the first and second derivatives of the input coordinates 888 | and uses them to compute the curvature of the curve. 889 | """ 890 | dx = np.gradient(x) # first derivatives 891 | dy = np.gradient(y) 892 | ddx = np.gradient(dx) # second derivatives 893 | ddy = np.gradient(dy) 894 | curvature = (dx*ddy-dy*ddx)/((dx**2+dy**2)**1.5) 895 | return curvature 896 | 897 | def make_colormap(seq): 898 | """ 899 | Return a LinearSegmentedColormap. 900 | 901 | Parameters 902 | ---------- 903 | seq : list of tuple 904 | A sequence of floats and RGB-tuples. The floats should be increasing 905 | and in the interval (0, 1). 906 | 907 | Returns 908 | ------- 909 | LinearSegmentedColormap 910 | A colormap object that can be used in matplotlib plotting. 911 | 912 | References 913 | ---------- 914 | .. [1] https://stackoverflow.com/questions/16834861/create-own-colormap-using-matplotlib-and-plot-color-scale 915 | """ 916 | seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3] 917 | cdict = {'red': [], 'green': [], 'blue': []} 918 | for i, item in enumerate(seq): 919 | if isinstance(item, float): 920 | r1, g1, b1 = seq[i - 1] 921 | r2, g2, b2 = seq[i + 1] 922 | cdict['red'].append([item, r1, r2]) 923 | cdict['green'].append([item, g1, g2]) 924 | cdict['blue'].append([item, b1, b2]) 925 | return mcolors.LinearSegmentedColormap('CustomMap', cdict) 926 | 927 | def kth_diag_indices(a,k): 928 | """ 929 | Find the indices of the k-th diagonal of a 2D array. 930 | 931 | Parameters 932 | ---------- 933 | a : array_like 934 | Input array. Must be 2-dimensional. 935 | k : int 936 | Diagonal offset. If k=0, the main diagonal is returned. 937 | If k>0, the k-th upper diagonal is returned. 938 | If k<0, the k-th lower diagonal is returned. 939 | 940 | Returns 941 | ------- 942 | tuple of ndarray 943 | A tuple of arrays (rows, cols) containing the row and column indices 944 | of the k-th diagonal. 945 | 946 | Notes 947 | ----- 948 | This function is adapted from a solution on Stack Overflow: 949 | https://stackoverflow.com/questions/10925671/numpy-k-th-diagonal-indices 950 | """ 951 | rows, cols = np.diag_indices_from(a) 952 | if k<0: 953 | return rows[:k], cols[-k:] 954 | elif k>0: 955 | return rows[k:], cols[:-k] 956 | else: 957 | return rows, cols 958 | 959 | def find_cutoffs(x,y,crdist,deltas): 960 | """ 961 | Identify locations of cutoffs along a centerline and the indices of the segments 962 | that will become part of the oxbows. 963 | 964 | Parameters 965 | ---------- 966 | x : array_like 967 | x-coordinates of the centerline. 968 | y : array_like 969 | y-coordinates of the centerline. 970 | crdist : float 971 | Critical cutoff distance. 972 | deltas : float 973 | Distance between neighboring points along the centerline. 974 | 975 | Returns 976 | ------- 977 | ind1 : ndarray 978 | Indices of the first set of cutoff points. 979 | ind2 : ndarray 980 | Indices of the second set of cutoff points. 981 | """ 982 | diag_blank_width = int((crdist+20*deltas)/deltas) 983 | # distance matrix for centerline points: 984 | dist = distance.cdist(np.array([x,y]).T,np.array([x,y]).T) 985 | dist[dist>crdist] = np.NaN # set all values that are larger than the cutoff threshold to NaN 986 | # set matrix to NaN along the diagonal zone: 987 | for k in range(-diag_blank_width,diag_blank_width+1): 988 | rows, cols = kth_diag_indices(dist,k) 989 | dist[rows,cols] = np.NaN 990 | i1, i2 = np.where(~np.isnan(dist)) 991 | ind1 = i1[np.where(i10: 1034 | xc.append(x[ind1[0]:ind2[0]+1]) # x coordinates of cutoff 1035 | yc.append(y[ind1[0]:ind2[0]+1]) # y coordinates of cutoff 1036 | zc.append(z[ind1[0]:ind2[0]+1]) # z coordinates of cutoff 1037 | x = np.hstack((x[:ind1[0]+1],x[ind2[0]:])) # x coordinates after cutoff 1038 | y = np.hstack((y[:ind1[0]+1],y[ind2[0]:])) # y coordinates after cutoff 1039 | z = np.hstack((z[:ind1[0]+1],z[ind2[0]:])) # z coordinates after cutoff 1040 | ind1, ind2 = find_cutoffs(x,y,crdist,deltas) 1041 | return x,y,z,xc,yc,zc 1042 | 1043 | def get_channel_banks(x,y,W): 1044 | """ 1045 | Find coordinates of channel banks, given a centerline and a channel width. 1046 | 1047 | Parameters 1048 | ---------- 1049 | x : array_like 1050 | x-coordinates of the centerline. 1051 | y : array_like 1052 | y-coordinates of the centerline. 1053 | W : float 1054 | Channel width. 1055 | 1056 | Returns 1057 | ------- 1058 | xm : ndarray 1059 | x-coordinates of the channel banks (both left and right banks). 1060 | ym : ndarray 1061 | y-coordinates of the channel banks (both left and right banks). 1062 | """ 1063 | x1 = x.copy() 1064 | y1 = y.copy() 1065 | x2 = x.copy() 1066 | y2 = y.copy() 1067 | ns = len(x) 1068 | dx = np.diff(x); dy = np.diff(y) 1069 | ds = np.sqrt(dx**2+dy**2) 1070 | x1[:-1] = x[:-1] + 0.5*W*np.diff(y)/ds 1071 | y1[:-1] = y[:-1] - 0.5*W*np.diff(x)/ds 1072 | x2[:-1] = x[:-1] - 0.5*W*np.diff(y)/ds 1073 | y2[:-1] = y[:-1] + 0.5*W*np.diff(x)/ds 1074 | x1[ns-1] = x[ns-1] + 0.5*W*(y[ns-1]-y[ns-2])/ds[ns-2] 1075 | y1[ns-1] = y[ns-1] - 0.5*W*(x[ns-1]-x[ns-2])/ds[ns-2] 1076 | x2[ns-1] = x[ns-1] - 0.5*W*(y[ns-1]-y[ns-2])/ds[ns-2] 1077 | y2[ns-1] = y[ns-1] + 0.5*W*(x[ns-1]-x[ns-2])/ds[ns-2] 1078 | xm = np.hstack((x1,x2[::-1])) 1079 | ym = np.hstack((y1,y2[::-1])) 1080 | return xm, ym 1081 | 1082 | def dist_map(x, y, z, xmin, xmax, ymin, ymax, dx, delta_s): 1083 | """ 1084 | Function for centerline rasterization and distance map calculation. 1085 | 1086 | Parameters 1087 | ---------- 1088 | x : array_like 1089 | x coordinates of centerline. 1090 | y : array_like 1091 | y coordinates of centerline. 1092 | z : array_like 1093 | z coordinates of centerline. 1094 | xmin : float 1095 | Minimum x coordinate that defines the area of interest. 1096 | xmax : float 1097 | Maximum x coordinate that defines the area of interest. 1098 | ymin : float 1099 | Minimum y coordinate that defines the area of interest. 1100 | ymax : float 1101 | Maximum y coordinate that defines the area of interest. 1102 | dx : float 1103 | Grid cell size (m). 1104 | delta_s : float 1105 | Distance between points along centerline (m). 1106 | 1107 | Returns 1108 | ------- 1109 | cl_dist : ndarray 1110 | Distance map (distance from centerline). 1111 | x_pix : ndarray 1112 | x pixel coordinates of the centerline. 1113 | y_pix : ndarray 1114 | y pixel coordinates of the centerline. 1115 | z_pix : ndarray 1116 | z pixel coordinates of the centerline. 1117 | s_pix : ndarray 1118 | Along-channel distance in pixels. 1119 | z_map : ndarray 1120 | Map of reference channel thalweg elevation (elevation of closest point along centerline). 1121 | x : ndarray 1122 | x centerline coordinates clipped to the 3D model domain. 1123 | y : ndarray 1124 | y centerline coordinates clipped to the 3D model domain. 1125 | z : ndarray 1126 | z centerline coordinates clipped to the 3D model domain. 1127 | """ 1128 | y = y[(x>xmin) & (xxmin) & (xxmin) & (xymin) & (yymin) & (yymin) & (y2*delta_s)[0])>0: 1136 | inds = np.where(ds>2*delta_s)[0] 1137 | inds = np.hstack((0,inds,len(x))) 1138 | lengths = np.diff(inds) 1139 | long_segment = np.where(lengths==max(lengths))[0][0] 1140 | start_ind = inds[long_segment]+1 1141 | end_ind = inds[long_segment+1] 1142 | if end_ind0: 1176 | x_pix, y_pix = eliminate_bad_pixels(img, img1) 1177 | x_pix,y_pix = order_cl_pixels(x_pix, y_pix, img) 1178 | img1 = morphology.binary_dilation(img, np.array([[1,0,1], [1,1,1]], dtype=np.uint8)).astype(np.uint8) 1179 | if len(np.where(img1==0)[0])>0: 1180 | x_pix, y_pix = eliminate_bad_pixels(img,img1) 1181 | x_pix,y_pix = order_cl_pixels(x_pix, y_pix, img) 1182 | img1 = morphology.binary_dilation(img, np.array([[1,0,1], [0,1,0], [1,0,1]], dtype=np.uint8)).astype(np.uint8) 1183 | if len(np.where(img1==0)[0])>0: 1184 | x_pix, y_pix = eliminate_bad_pixels(img, img1) 1185 | x_pix,y_pix = order_cl_pixels(x_pix, y_pix, img) 1186 | #redo the distance calculation (because x_pix and y_pix do not always contain all the points in cl): 1187 | cl[cl==0] = 1 1188 | cl[y_pix,x_pix] = 0 1189 | cl_dist, inds = ndimage.distance_transform_edt(cl, return_indices=True) 1190 | dx,dy,dz,ds,s = compute_derivatives(x,y,z) 1191 | dx_pix = np.diff(x_pix) 1192 | dy_pix = np.diff(y_pix) 1193 | ds_pix = np.sqrt(dx_pix**2 + dy_pix**2) 1194 | s_pix = np.hstack((0,np.cumsum(ds_pix))) 1195 | f = scipy.interpolate.interp1d(s, z) 1196 | snew = s_pix*s[-1]/s_pix[-1] 1197 | if snew[-1] > s[-1]: 1198 | snew[-1] = s[-1] 1199 | snew[snew0]=1 1404 | y_pix,x_pix = np.where(cl==1) 1405 | return x_pix, y_pix 1406 | 1407 | def order_cl_pixels(x_pix, y_pix, img): 1408 | """ 1409 | Function for ordering pixels along a channel centerline, starting on the left side. 1410 | 1411 | Parameters 1412 | ---------- 1413 | x_pix : array_like 1414 | Unordered x pixel coordinates of the centerline. 1415 | y_pix : array_like 1416 | Unordered y pixel coordinates of the centerline. 1417 | img : array_like 1418 | Image array used to determine the distances from the edges. 1419 | 1420 | Returns 1421 | ------- 1422 | x_pix : array_like 1423 | Ordered x pixel coordinates of the centerline. 1424 | y_pix : array_like 1425 | Ordered y pixel coordinates of the centerline. 1426 | """ 1427 | dist = distance.cdist(np.array([x_pix,y_pix]).T,np.array([x_pix,y_pix]).T) 1428 | dist[np.diag_indices_from(dist)]=100.0 1429 | # ind = np.argmin(x_pix) # select starting point on left side of image 1430 | x_pix_dist_from_edges = np.shape(img)[1] - max(x_pix) + min(x_pix) 1431 | y_pix_dist_from_edges = np.shape(img)[0] - max(y_pix) + min(y_pix) 1432 | if x_pix_dist_from_edges <= y_pix_dist_from_edges: 1433 | ind = np.argmin(x_pix) # select starting point on left side of image 1434 | else: 1435 | ind = np.argmin(y_pix) # select starting point on lower side of image 1436 | clinds = [ind] 1437 | count = 0 1438 | while count2: 1441 | t[clinds[-2]]=t[clinds[-2]]+100.0 1442 | t[clinds[-3]]=t[clinds[-3]]+100.0 1443 | ind = np.argmin(t) 1444 | clinds.append(ind) 1445 | count=count+1 1446 | x_pix = x_pix[clinds] 1447 | y_pix = y_pix[clinds] 1448 | return x_pix,y_pix 1449 | 1450 | def save_3d_chb_to_hdf5(chb_3d, fname): 1451 | """ 1452 | Save a 3D channelbelt model as an HDF5 file. 1453 | 1454 | Parameters 1455 | ---------- 1456 | chb_3d : ChannelBelt3D 1457 | The ChannelBelt3D object to be saved. 1458 | fname : str 1459 | The filename for the HDF5 file. 1460 | """ 1461 | f = h5py.File(fname,'w') 1462 | grp = f.create_group('model') 1463 | grp.create_dataset('dx', data = chb_3d.dx) 1464 | grp.create_dataset('topo', data = chb_3d.topo) 1465 | grp.create_dataset('strat', data = chb_3d.strat) 1466 | grp.create_dataset('facies', data = chb_3d.facies) 1467 | for key in chb_3d.facies_code.keys(): 1468 | grp.create_dataset(chb_3d.facies_code[key], data = key) 1469 | grp = f.create_group('channels') 1470 | depths = []; widths = []; xcoords = []; ycoords = []; zcoords = []; lengths = [] 1471 | for channel in chb_3d.channels: 1472 | depths.append(channel.D) 1473 | widths.append(channel.W) 1474 | xcoords.append(channel.x) 1475 | ycoords.append(channel.y) 1476 | zcoords.append(channel.z) 1477 | lengths.append(len(channel.x)) 1478 | x = np.nan * np.ones((len(xcoords), max(lengths))) 1479 | for i in range(len(xcoords)): 1480 | x[i, :len(xcoords[i])] = xcoords[i] 1481 | y = np.nan * np.ones((len(ycoords), max(lengths))) 1482 | for i in range(len(ycoords)): 1483 | y[i, :len(ycoords[i])] = ycoords[i] 1484 | z = np.nan * np.ones((len(zcoords), max(lengths))) 1485 | for i in range(len(zcoords)): 1486 | z[i, :len(zcoords[i])] = zcoords[i] 1487 | grp.create_dataset('depths', data = depths) 1488 | grp.create_dataset('widths', data = widths) 1489 | grp.create_dataset('x', data = x) 1490 | grp.create_dataset('y', data = y) 1491 | grp.create_dataset('z', data = z) 1492 | f.close() 1493 | 1494 | def read_3d_chb_from_hdf5(model_type, fname): 1495 | """ 1496 | Function for reading 3D channelbelt model from an HDF5 file (that was saved using 'save_3d_chb_to_hdf5'). 1497 | 1498 | Parameters 1499 | ---------- 1500 | model_type : str 1501 | Model type (can be 'fluvial' or 'submarine'). 1502 | fname : str 1503 | Filename of the HDF5 file. 1504 | 1505 | Returns 1506 | ------- 1507 | ChannelBelt3D 1508 | ChannelBelt3D object that was created from the HDF5 file. 1509 | """ 1510 | f = h5py.File(fname, 'r') 1511 | model = f['model'] 1512 | topo = np.array(model['topo']) 1513 | strat = np.array(model['strat']) 1514 | facies = np.array(model['facies']) 1515 | facies_code = {} 1516 | facies_code[int(np.array(model['point bar']))] = 'point bar' 1517 | facies_code[int(np.array(model['levee']))] = 'levee' 1518 | dx = float(np.array(model['dx'])) 1519 | x = np.array(f['channels']['x']) 1520 | y = np.array(f['channels']['y']) 1521 | z = np.array(f['channels']['z']) 1522 | depths = np.array(f['channels']['depths']) 1523 | widths = np.array(f['channels']['widths']) 1524 | channels = [] 1525 | for i in range(x.shape[0]): 1526 | x1 = x[i, :] 1527 | x1 = x1[np.isnan(x1) == 0] 1528 | y1 = y[i, :] 1529 | y1 = y1[np.isnan(y1) == 0] 1530 | z1 = z[i, :] 1531 | z1 = z1[np.isnan(z1) == 0] 1532 | channels.append(Channel(x1, y1, z1, widths[i], depths[i])) 1533 | chb_3d = ChannelBelt3D(model_type, topo, strat, facies, facies_code, dx, channels) 1534 | f.close() 1535 | return chb_3d 1536 | 1537 | def create_poro_perm(chb_3d, poro_max): 1538 | """ 1539 | Generate porosity and permeability fields from 3D channelbelt model. 1540 | 1541 | Parameters 1542 | ---------- 1543 | chb_3d : ChannelBelt3D 1544 | A ChannelBelt3D object with these attributes: 1545 | - strat: 3D numpy array representing stratigraphy. 1546 | - topo: 3D numpy array representing topography. 1547 | poro_max : float 1548 | Maximum porosity value. 1549 | 1550 | Returns 1551 | ------- 1552 | porosity : numpy.ndarray 1553 | 3D numpy array of porosity values. 1554 | permeability : numpy.ndarray 1555 | 3D numpy array of permeability values. 1556 | 1557 | Notes 1558 | ----- 1559 | The function calculates porosity as a function of height above the thalweg (HAT) and assigns 1560 | permeability based on the calculated porosity. Areas with zero thickness are assigned zero porosity. 1561 | """ 1562 | ny, nx, nz = np.shape(chb_3d.strat) 1563 | porosity = np.zeros((ny-1, nx-1, nz - 1)) 1564 | for i in range(int((chb_3d.strat.shape[2] - 1)/2)): # only working with channel sands 1565 | hat = np.abs(chb_3d.topo[:, :, 3*i + 1] - np.min(chb_3d.topo[:, :, 3*i + 1])) # height above thalweg 1566 | th = chb_3d.topo[:, :, 3*i + 2] - chb_3d.topo[:, :, 3*i + 1] # thickness of channel deposit 1567 | hat[th == 0] = 100 # set a large HAT value where thickness is zero (we want zero porosity here) 1568 | t = 0.25*(hat[1:,1:]+hat[1:,:-1]+hat[:-1,1:]+hat[:-1,:-1]) # average HAT for porosity grid 1569 | t = t - np.min(t) 1570 | t[t > 30.0] = 30.0 1571 | # porosity is a function of elevation (the higher the elevation, the lower the porosity): 1572 | t = poro_max - poro_max*(t/30.0) 1573 | porosity[:, :, 2*i] = t # assign porosity 1574 | porosity[porosity > poro_max] = poro_max 1575 | permeability = 10**(17*porosity - 3) 1576 | permeability[porosity == 0] = 0 1577 | return porosity, permeability 1578 | 1579 | def save_3d_chb_to_generic_hdf5(chb_3d, props, prop_names, fname): 1580 | """ 1581 | Save a 3D channelbelt model as an HDF5 file. 1582 | 1583 | Parameters 1584 | ---------- 1585 | chb_3d : ChannelBelt3D 1586 | ChannelBelt3D object to be saved. 1587 | props : list of ndarray 1588 | List of property arrays (e.g., porosity, permeability). 1589 | prop_names : list of str 1590 | List of property names corresponding to the property arrays. 1591 | fname : str 1592 | Filename for the HDF5 file. 1593 | 1594 | Returns 1595 | ------- 1596 | None 1597 | """ 1598 | f = h5py.File(fname,'w') 1599 | grp = f.create_group('model') 1600 | grp.create_dataset('dx', data = chb_3d.dx) 1601 | grp.create_dataset('topo', data = chb_3d.topo) 1602 | grp.create_dataset('strat', data = chb_3d.strat) 1603 | grp.create_dataset('facies', data = chb_3d.facies) 1604 | count = 0 1605 | for prop in props: 1606 | grp.create_dataset(prop_names[count], data = prop) 1607 | count += 1 1608 | for key in chb_3d.facies_code.keys(): 1609 | grp.create_dataset(chb_3d.facies_code[key], data = key) 1610 | f.close() 1611 | 1612 | def write_eclipse_grid(strat, porosity, permeability, dx, fname): 1613 | """ 1614 | Function for exporting an Eclipse file ('.grdecl' format) from an array of stratigraphic surfaces ('strat') and an array of porosity ('porosity'). 1615 | Additional 'keywords' like ACTNUM and SATNUM can be added the same way as porosity. 1616 | 1617 | Ordering of cornerpoints for first (uppermost) surface in Eclipse: 1618 | ---------------------------------------- 1619 | | 1 2 | 3 4 | 5 6 | 1620 | | | | | 1621 | | 7 8 | 9 10 | 11 12 | 1622 | ---------------------------------------- 1623 | | 13 14 | 15 16 | 17 18 | 1624 | | | | | 1625 | | 19 20 | 21 22 | 23 24 | 1626 | ---------------------------------------- 1627 | 1628 | Parameters 1629 | ---------- 1630 | strat : numpy.ndarray 1631 | Stratigraphic surfaces (outputs from channel model). 1632 | porosity : numpy.ndarray 1633 | Porosity grid. 1634 | dx : float 1635 | Grid cell size (in meters). 1636 | fname : str 1637 | Filename of the Eclipse file to be written. 1638 | """ 1639 | 1640 | # these swaps have to be done because the logic below was written for this ordering of the axes 1641 | surfs = np.swapaxes(strat, 0, 2) 1642 | surfs = np.swapaxes(surfs, 1, 2) 1643 | porosity = np.swapaxes(porosity, 0, 2) 1644 | porosity = np.swapaxes(porosity, 1, 2) 1645 | permeability = np.swapaxes(permeability, 0, 2) 1646 | permeability = np.swapaxes(permeability, 1, 2) 1647 | 1648 | nz,ny,nx = np.shape(surfs); 1649 | nz=nz-1 # number of cells in z direction 1650 | ny=ny-1 # number of cells in y direction 1651 | nx=nx-1 # number of cells in x direction 1652 | 1653 | dy=dx # size of cells in x and y directions 1654 | 1655 | print('creating cornerpoint array(ZCORN)') 1656 | zcorn = np.zeros((8*nx*ny*nz,)) 1657 | for k in range(nz): 1658 | # write cornerpoints for the top of layer 'k': 1659 | surf = np.squeeze(surfs[k,:,:]) 1660 | zc = np.zeros((2*ny,2*nx)) 1661 | zc[::2,::2] = surf[:-1,:-1] 1662 | zc[1::2,1::2] = surf[1:,1:] 1663 | zc[1::2,::2] = surf[1:,:-1] 1664 | zc[::2,1::2] = surf[:-1,1:] 1665 | zc = np.reshape(zc,(1,4*nx*ny)) 1666 | zcorn[(2*k)*4*nx*ny : (2*k+1)*4*nx*ny] = zc; 1667 | 1668 | # write cornerpoints for the bottom of layer 'k': 1669 | surf = np.squeeze(surfs[k+1,:,:]) 1670 | zc = np.zeros((2*ny,2*nx)); 1671 | zc[::2,::2] = surf[:-1,:-1] 1672 | zc[1::2,1::2] = surf[1:,1:] 1673 | zc[1::2,::2] = surf[1:,:-1] 1674 | zc[::2,1::2] = surf[:-1,1:] 1675 | zc = np.reshape(zc,(1,4*nx*ny)) 1676 | zcorn[(2*k+1)*4*nx*ny : (2*k+2)*4*nx*ny] = zc 1677 | zcorn = np.reshape(zcorn, (int(len(zcorn)/8), 8)) 1678 | zcorn = 100 * zcorn # convert meters to centimeters 1679 | 1680 | print('creating pillar matrix (COORD)') 1681 | coord = np.zeros(((nx+1)*(ny+1),6)); 1682 | for j in range(ny+1): 1683 | for i in range(nx+1): 1684 | coord[j*(nx+1)+i,0] = i*dx 1685 | coord[j*(nx+1)+i,1] = j*dy 1686 | coord[j*(nx+1)+i,2] = surfs[0,j,i] 1687 | coord[j*(nx+1)+i,3] = i*dx 1688 | coord[j*(nx+1)+i,4] = j*dy 1689 | coord[j*(nx+1)+i,5] = surfs[-1,j,i] 1690 | coord = 100 * coord # convert meters to centimeters 1691 | 1692 | print('creating porosity array (PORO)') 1693 | poro = np.zeros((nx*ny*nz,)) 1694 | [i,j,k] = np.meshgrid(np.arange(nx),np.arange(ny),np.arange(nz)) 1695 | ind1 = k*nx*ny + j*nx+i 1696 | ind2 = np.ravel_multi_index((k,j,i),(nz,ny,nx)) 1697 | poro[ind1] = porosity.flatten()[ind2] 1698 | if np.mod(len(poro),8)==0: # if length of porosity array is a multiple of 8 1699 | poro_1 = np.reshape(poro, (int(len(poro)/8), 8)) 1700 | else: 1701 | poro_1 = np.reshape(poro[:-np.mod(len(poro),8)], (int(len(poro)/8), 8)) 1702 | 1703 | print('creating permeability array (PERM)') 1704 | perm = np.zeros((nx*ny*nz,)) 1705 | perm[ind1] = permeability.flatten()[ind2] 1706 | if np.mod(len(perm),8)==0: # if length of porosity array is a multiple of 8 1707 | perm_1 = np.reshape(perm, (int(len(perm)/8), 8)) 1708 | else: 1709 | perm_1 = np.reshape(perm[:-np.mod(len(perm),8)], (int(len(perm)/8), 8)) 1710 | 1711 | # write file: 1712 | fid = open(fname, 'a') 1713 | fid.write('SPECGRID\n') 1714 | fid.write('%d %d %d' %(nx, ny, nz) + ' 1 F /\n') 1715 | fid.write('COORD\n') 1716 | print('writing pillars...') 1717 | for i in range(coord.shape[0]): 1718 | fid.write('%6.3f %6.3f %6.3f %6.3f %6.3f %6.3f\n' %tuple(coord[i,:])) 1719 | fid.write('/\n') 1720 | fid.write(' ') 1721 | fid.write('\n') 1722 | fid.write('ZCORN\n') 1723 | print('writing zcorns...') 1724 | for i in range(zcorn.shape[0]): 1725 | fid.write('%6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f\n' %tuple(zcorn[i,:])) 1726 | fid.write('/\n') 1727 | fid.write(' ') 1728 | fid.write('\n') 1729 | fid.write('PORO\n') 1730 | print('writing porosity...') 1731 | for i in range(poro_1.shape[0]): 1732 | fid.write('%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f\n' %tuple(poro_1[i,:])) 1733 | if np.mod(len(poro),8)!=0: # if length of porosity array is not a multiple of 8 1734 | for i in range(np.mod(len(poro),8)): 1735 | fid.write('%6.4f ' %poro[-np.mod(len(poro),8):][i]) 1736 | fid.write('\n') 1737 | fid.write('/\n') 1738 | fid.write(' ') 1739 | fid.write('\n') 1740 | fid.write('PERMX\n') 1741 | print('writing permeability...') 1742 | for i in range(perm_1.shape[0]): 1743 | fid.write('%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f\n' %tuple(perm_1[i,:])) 1744 | if np.mod(len(perm),8)!=0: # if length of porosity array is not a multiple of 8 1745 | for i in range(np.mod(len(perm),8)): 1746 | fid.write('%6.4f ' %perm[-np.mod(len(perm),8):][i]) 1747 | fid.write('\n') 1748 | fid.write('/\n') 1749 | fid.close() -------------------------------------------------------------------------------- /meanderpy_sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsylvester/meanderpy/10938b0eafab77830304185d380dd1751c758d8b/meanderpy_sketch.png -------------------------------------------------------------------------------- /meanderpy_strat_vs_morph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zsylvester/meanderpy/10938b0eafab77830304185d380dd1751c758d8b/meanderpy_strat_vs_morph.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_namespace_packages 2 | 3 | VERSION="0.1.9" 4 | DESCRIPTION = "meanderpy: a simple model of meandering" 5 | with open("README.md", "r") as f: 6 | long_description_readme = f.read() 7 | 8 | setup( 9 | name="meanderpy", 10 | version=VERSION, 11 | author="Zoltan Sylvester", 12 | author_email="zoltan.sylvester@beg.utexas.edu", 13 | description=DESCRIPTION, 14 | keywords = 'rivers, meandering, geomorphology, stratigraphy', 15 | long_description=long_description_readme, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/zsylvester/meanderpy", 18 | download_url="https://github.com/zsylvester/meanderpy/archive/refs/tags/v{0}tar.gz".format(VERSION), 19 | packages=find_namespace_packages(include=['meanderpy', 'meanderpy.*']), 20 | install_requires=['numpy','matplotlib', 'scipy','numba','pillow','scikit-image','tqdm'], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "Topic :: Scientific/Engineering :: Physics", 24 | "Topic :: Scientific/Engineering :: Visualization", 25 | 'Intended Audience :: Science/Research', 26 | "License :: OSI Approved :: Apache Software License", 27 | "Operating System :: OS Independent", 28 | ], 29 | ) 30 | --------------------------------------------------------------------------------