├── fastscape ├── tests │ ├── __init__.py │ ├── fixtures.py │ ├── test_processes_context.py │ ├── test_processes_grid.py │ ├── test_processes_boundary.py │ ├── test_processes_initial.py │ ├── test_processes_tectonics.py │ └── test_processes_main.py ├── models │ ├── __init__.py │ └── _models.py ├── __init__.py └── processes │ ├── erosion.py │ ├── context.py │ ├── hillslope.py │ ├── __init__.py │ ├── grid.py │ ├── boundary.py │ ├── initial.py │ ├── marine.py │ ├── isostasy.py │ ├── tectonics.py │ ├── channel.py │ ├── flow.py │ └── main.py ├── .gitattributes ├── MANIFEST.in ├── doc ├── source │ ├── _static │ │ ├── favicon.ico │ │ ├── fastscape_logo_midres.png │ │ └── style.css │ ├── release_notes.rst │ ├── cite.rst │ ├── _templates │ │ └── process_class.rst │ ├── install.rst │ ├── index.rst │ ├── examples.rst │ ├── models.rst │ ├── processes.rst │ ├── develop.rst │ └── conf.py ├── environment.yml ├── Makefile └── make.bat ├── readthedocs.yml ├── environment.yml ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── pypipublish.yml │ └── tests.yml ├── LICENSE ├── pyproject.toml ├── .gitignore └── README.rst /fastscape/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | fastscape/_version.py export-subst 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include fastscape/_version.py 3 | -------------------------------------------------------------------------------- /doc/source/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastscape-lem/fastscape/HEAD/doc/source/_static/favicon.ico -------------------------------------------------------------------------------- /doc/source/_static/fastscape_logo_midres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fastscape-lem/fastscape/HEAD/doc/source/_static/fastscape_logo_midres.png -------------------------------------------------------------------------------- /fastscape/models/__init__.py: -------------------------------------------------------------------------------- 1 | from ._models import basic_model, bootstrap_model, marine_model, sediment_model 2 | 3 | __all__ = ("basic_model", "bootstrap_model", "marine_model", "sediment_model") 4 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-20.04" 5 | tools: 6 | python: "mambaforge-4.10" 7 | 8 | conda: 9 | environment: doc/environment.yml 10 | 11 | formats: [] 12 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: fastscape-dev 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python>=3.9 6 | - xarray-simlab 7 | - fastscapelib-f2py 8 | - numba 9 | - pytest 10 | - pip 11 | -------------------------------------------------------------------------------- /doc/source/release_notes.rst: -------------------------------------------------------------------------------- 1 | .. _release_notes: 2 | 3 | Release notes 4 | ============= 5 | 6 | v0.1.0 (25 September 2023) 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | A first proper release after a long time being in beta although stable. 10 | -------------------------------------------------------------------------------- /fastscape/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import PackageNotFoundError, version 2 | 3 | try: 4 | __version__ = version("fastscape") 5 | except PackageNotFoundError: # noqa 6 | # package is not installed 7 | pass 8 | 9 | from fastscape import models, processes 10 | 11 | __all__ = ("processes", "models") 12 | -------------------------------------------------------------------------------- /doc/environment.yml: -------------------------------------------------------------------------------- 1 | name: fastscape-doc 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.10 7 | - xarray-simlab=0.5.0 8 | - xarray 9 | - ipython 10 | - nbconvert 11 | - sphinx 12 | - pandoc 13 | - fastscapelib-f2py=2.8.3 14 | - numba 15 | - sphinx_rtd_theme 16 | - pip 17 | - pip: 18 | - -e .. 19 | -------------------------------------------------------------------------------- /fastscape/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from fastscape.processes.context import FastscapelibContext 4 | 5 | 6 | @contextmanager 7 | def fastscape_context(shape=(3, 4), length=(10.0, 30.0)): 8 | p = FastscapelibContext(shape=shape, length=length) 9 | p.initialize() 10 | 11 | try: 12 | yield p.context 13 | finally: 14 | p.finalize() 15 | -------------------------------------------------------------------------------- /doc/source/cite.rst: -------------------------------------------------------------------------------- 1 | .. _cite: 2 | 3 | How to cite fastscape 4 | ===================== 5 | 6 | If you have used fastscape (this software) and want to cite it in a 7 | scientific publication, we provide citations and DOIs for specific 8 | versions via Zenodo. Click on the badge below to get citation 9 | information for the latest version of fastscape. 10 | 11 | .. image:: https://zenodo.org/badge/133702738.svg 12 | :target: https://zenodo.org/badge/latestdoi/133702738 13 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_context.py: -------------------------------------------------------------------------------- 1 | from fastscape.processes.context import FastscapelibContext 2 | 3 | 4 | def test_fastscapelib_context(): 5 | p = FastscapelibContext(shape=(3, 4), length=(10.0, 30.0), ibc=1111) 6 | 7 | p.initialize() 8 | 9 | assert p.context["nx"] == 4 10 | assert p.context["ny"] == 3 11 | assert p.context["xl"] == 30.0 12 | assert p.context["yl"] == 10.0 13 | assert p.context["h"].size == 3 * 4 14 | assert p.context["bounds_ibc"] == 1111 15 | 16 | p.run_step(10.0) 17 | 18 | assert p.context["dt"] == 10.0 19 | 20 | p.finalize() 21 | 22 | assert p.context["h"] is None 23 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/source/_static/style.css: -------------------------------------------------------------------------------- 1 | @import url("theme.css"); 2 | 3 | .wy-side-nav-search>a img.logo, 4 | .wy-side-nav-search .wy-dropdown>a img.logo { 5 | width: 12rem 6 | } 7 | 8 | .wy-side-nav-search { 9 | background-color: #eee; 10 | } 11 | 12 | .wy-side-nav-search>div.version { 13 | display: none; 14 | } 15 | 16 | .wy-nav-top { 17 | background-color: #555; 18 | } 19 | 20 | .wy-menu-vertical header, 21 | .wy-menu-vertical p.caption { 22 | color: #be9063; 23 | } 24 | 25 | 26 | table.colwidths-given { 27 | table-layout: fixed; 28 | width: 100%; 29 | } 30 | table.docutils td { 31 | white-space: unset; 32 | word-wrap: break-word; 33 | } 34 | 35 | 36 | .highlight { 37 | background: #efefef; 38 | } 39 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # run all checks with `pre-commit run --all-files` 2 | 3 | ci: 4 | autoupdate_schedule: monthly 5 | autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]" 6 | autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" 7 | autofix_prs: false 8 | 9 | repos: 10 | - repo: https://github.com/pre-commit/pre-commit-hooks 11 | rev: v4.4.0 12 | hooks: 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | - id: check-yaml 16 | - id: mixed-line-ending 17 | 18 | - repo: https://github.com/astral-sh/ruff-pre-commit 19 | rev: v0.0.289 20 | hooks: 21 | - id: ruff 22 | args: [--fix] 23 | 24 | - repo: https://github.com/psf/black 25 | rev: 23.9.1 26 | hooks: 27 | - id: black 28 | -------------------------------------------------------------------------------- /doc/source/_templates/process_class.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | {% block methods %} 8 | 9 | {% if methods %} 10 | .. rubric:: Methods 11 | 12 | .. autosummary:: 13 | {% for item in methods %} 14 | ~{{ name }}.{{ item }} 15 | {%- endfor %} 16 | {% endif %} 17 | {% endblock %} 18 | 19 | {% block attributes %} 20 | {% if attributes %} 21 | .. rubric:: Attributes 22 | 23 | .. autosummary:: 24 | {% for item in attributes %} 25 | ~{{ name }}.{{ item }} 26 | {%- endfor %} 27 | {% endif %} 28 | {% endblock %} 29 | 30 | Output of process info: 31 | 32 | .. ipython:: python 33 | 34 | import xsimlab as xs 35 | from fastscape.processes import {{ name }} 36 | xs.process_info({{ name }}) 37 | -------------------------------------------------------------------------------- /.github/workflows/pypipublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package on PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: pypi 12 | url: https://pypi.org/p/fastscape 13 | permissions: 14 | id-token: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - name: Set up Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.9" 22 | - name: Install publish dependencies 23 | run: python -m pip install build 24 | - name: Build package 25 | run: python -m build . -o py_dist 26 | - name: Publish package to PyPI 27 | uses: pypa/gh-action-pypi-publish@release/v1 28 | with: 29 | packages-dir: py_dist/ 30 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main, master] 4 | pull_request: 5 | branches: [main, master] 6 | 7 | name: Tests 8 | 9 | jobs: 10 | tests: 11 | name: Test Python (${{ matrix.os }} / ${{ matrix.python-version }}) 12 | runs-on: ${{ matrix.os }} 13 | defaults: 14 | run: 15 | shell: bash -el {0} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 20 | python-version: ["3.9", "3.10", "3.11"] 21 | steps: 22 | - name: Checkout repo 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup micromamba 26 | uses: mamba-org/setup-micromamba@v1 27 | with: 28 | environment-file: environment.yml 29 | cache-environment: true 30 | cache-downloads: false 31 | create-args: >- 32 | python=${{ matrix.python-version }} 33 | 34 | - name: Install fastscape 35 | run: | 36 | python -m pip install . -v 37 | 38 | - name: Run tests 39 | run: | 40 | pytest . -vv --color=yes 41 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from fastscape.processes import RasterGrid2D, UniformRectilinearGrid2D 4 | 5 | 6 | def test_uniform_rectilinear_grid_2d(): 7 | p = UniformRectilinearGrid2D( 8 | shape=np.array([3, 4]), spacing=np.array([5.0, 10.0]), origin=np.array([0.0, 1.0]) 9 | ) 10 | 11 | p.initialize() 12 | 13 | np.testing.assert_equal(p.shape, (3, 4)) 14 | np.testing.assert_equal(p.spacing, (5.0, 10.0)) 15 | np.testing.assert_equal(p.origin, (0.0, 1.0)) 16 | np.testing.assert_equal(p.length, (10.0, 30.0)) 17 | assert p.size == 12 18 | assert p.area == 600.0 19 | assert p.cell_area == 50.0 20 | assert p.dx == 10.0 21 | assert p.dy == 5.0 22 | assert p.nx == 4 23 | assert p.ny == 3 24 | np.testing.assert_equal(p.x, np.array([1.0, 11.0, 21.0, 31.0])) 25 | np.testing.assert_equal(p.y, np.array([0.0, 5.0, 10.0])) 26 | 27 | 28 | def test_raster_grid_2d(): 29 | p = RasterGrid2D(shape=np.array([3, 4]), length=np.array([10.0, 30.0])) 30 | 31 | p.initialize() 32 | 33 | np.testing.assert_equal(p.spacing, (5.0, 10.0)) 34 | np.testing.assert_equal(p.origin, (0.0, 0.0)) 35 | -------------------------------------------------------------------------------- /fastscape/processes/erosion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xsimlab as xs 3 | 4 | from .grid import UniformRectilinearGrid2D 5 | 6 | 7 | @xs.process 8 | class TotalErosion: 9 | """Sum up all erosion processes.""" 10 | 11 | erosion_vars = xs.group("erosion") 12 | 13 | cumulative_height = xs.variable( 14 | dims=[(), ("y", "x")], intent="inout", description="erosion height accumulated over time" 15 | ) 16 | 17 | height = xs.variable( 18 | dims=[(), ("y", "x")], 19 | intent="out", 20 | description="total erosion height at current step", 21 | groups="surface_downward", 22 | ) 23 | 24 | rate = xs.on_demand(dims=[(), ("y", "x")], description="total erosion rate at current step") 25 | 26 | grid_area = xs.foreign(UniformRectilinearGrid2D, "area") 27 | 28 | domain_rate = xs.on_demand(description="domain-integrated volumetric erosion rate") 29 | 30 | @xs.runtime(args="step_delta") 31 | def run_step(self, dt): 32 | self._dt = dt 33 | 34 | self.height = sum(self.erosion_vars) 35 | self.cumulative_height += self.height 36 | 37 | @rate.compute 38 | def _rate(self): 39 | return self.height / self._dt 40 | 41 | @domain_rate.compute 42 | def _domain_rate(self): 43 | return np.sum(self.height) * self.grid_area / self._dt 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, fastscape-lem 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /fastscape/processes/context.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .boundary import BorderBoundary 6 | from .grid import UniformRectilinearGrid2D 7 | 8 | 9 | class SerializableFastscapeContext: 10 | """Fastscapelib-fortran context getter/setter that is serializable. 11 | 12 | (Fortran objects can't be pickled). 13 | """ 14 | 15 | def __getitem__(self, key): 16 | return getattr(fs.fastscapecontext, key) 17 | 18 | def __setitem__(self, key, value): 19 | setattr(fs.fastscapecontext, key, value) 20 | 21 | 22 | @xs.process 23 | class FastscapelibContext: 24 | """This process takes care of proper initialization, 25 | update and clean-up of fastscapelib-fortran internal 26 | state. 27 | 28 | """ 29 | 30 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 31 | length = xs.foreign(UniformRectilinearGrid2D, "length") 32 | ibc = xs.foreign(BorderBoundary, "ibc") 33 | 34 | context = xs.any_object(description="accessor to fastscapelib-fortran internal variables") 35 | 36 | def initialize(self): 37 | fs.fastscape_init() 38 | fs.fastscape_set_nx_ny(*np.flip(self.shape)) 39 | fs.fastscape_setup() 40 | fs.fastscape_set_xl_yl(*np.flip(self.length)) 41 | 42 | fs.fastscape_set_bc(self.ibc) 43 | 44 | self.context = SerializableFastscapeContext() 45 | 46 | @xs.runtime(args="step_delta") 47 | def run_step(self, dt): 48 | # fastscapelib-fortran runtime routines use dt from context 49 | self.context["dt"] = dt 50 | 51 | def finalize(self): 52 | fs.fastscape_destroy() 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=42", 5 | "setuptools-scm>=7", 6 | ] 7 | 8 | [tool.setuptools.packages.find] 9 | include = [ 10 | "fastscape", 11 | "fastscape.*", 12 | ] 13 | 14 | [tool.setuptools_scm] 15 | fallback_version = "9999" 16 | 17 | [project] 18 | name = "fastscape" 19 | dynamic = ["version"] 20 | authors = [ 21 | {name = "Benoît Bovy", email = "benbovy@gmail.com"}, 22 | ] 23 | maintainers = [ 24 | {name = "Fastscape contributors"}, 25 | ] 26 | license = {text = "BSD-3-Clause"} 27 | description = "A fast, versatile and user-friendly landscape evolution model" 28 | keywords = ["simulation", "toolkit", "modeling", "landscape", "geomorphology"] 29 | readme = "README.rst" 30 | classifiers = [ 31 | "Intended Audience :: Science/Research", 32 | "License :: OSI Approved :: BSD License", 33 | "Programming Language :: Python :: 3", 34 | ] 35 | requires-python = ">=3.9" 36 | dependencies = [ 37 | "xarray-simlab >= 0.5.0", 38 | "numba", 39 | ] 40 | 41 | [project.optional-dependencies] 42 | dev = ["pytest"] 43 | 44 | [project.urls] 45 | Documentation = "https://fastscape.readthedocs.io" 46 | Repository = "https://github.com/fastscape-lem/fastscape" 47 | 48 | [tool.black] 49 | line-length = 100 50 | 51 | [tool.ruff] 52 | # E402: module level import not at top of file 53 | # E501: line too long - let black worry about that 54 | # E731: do not assign a lambda expression, use a def 55 | ignore = [ 56 | "E402", 57 | "E501", 58 | "E731", 59 | ] 60 | select = [ 61 | "F", # Pyflakes 62 | "E", # Pycodestyle 63 | "W", 64 | "I", # isort 65 | "UP", # Pyupgrade 66 | ] 67 | exclude = [".eggs", "doc"] 68 | target-version = "py39" 69 | 70 | [tool.ruff.isort] 71 | known-first-party = ["fastscape"] 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | py_dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | doc/source/_api_generated/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /doc/source/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Install fastscape 4 | ================= 5 | 6 | Required dependencies 7 | --------------------- 8 | 9 | - Python 3 10 | - `xarray `__ 11 | - `xarray-simlab `__ (0.4.0 or later) 12 | - `fastscapelib-fortran 13 | `__ (2.8.2 or later) 14 | 15 | Install using conda 16 | ------------------- 17 | 18 | fastscape can be installed or updated using conda_:: 19 | 20 | $ conda install fastscape -c conda-forge 21 | 22 | This installs fastscape and all the required dependencies. 23 | 24 | the fastscape conda package is maintained on the `conda-forge`_ 25 | channel. 26 | 27 | .. _conda-forge: https://conda-forge.org/ 28 | .. _conda: https://conda.io/docs/ 29 | 30 | Install from source 31 | ------------------- 32 | 33 | Be sure you have the required dependencies installed first. You might 34 | consider using conda_ to install them:: 35 | 36 | $ conda install xarray-simlab fastscapelib-f2py -c conda-forge 37 | 38 | A good practice (especially for development purpose) is to install the packages 39 | in a separate environment, e.g. using conda:: 40 | 41 | $ conda create -n fastscape python xarray-simlab fastscapelib-f2py numba -c conda-forge 42 | $ conda activate fastscape 43 | 44 | Then you can clone the ``fastscape`` git repository and install it 45 | using ``pip`` locally:: 46 | 47 | $ git clone https://github.com/fastscape-lem/fastscape.git 48 | $ cd fastscape 49 | $ pip install . 50 | 51 | For development purpose, use the following command:: 52 | 53 | $ pip install -e . 54 | 55 | Import fastscape 56 | ---------------- 57 | 58 | To make sure that ``fastscape`` is correctly installed, try import it in a 59 | Python console:: 60 | 61 | >>> import fastscape 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Fastscape 2 | ========= 3 | 4 | |Build Status| |Doc Status| |Zenodo| 5 | 6 | A fast, versatile and user-friendly landscape evolution model. 7 | 8 | Fastscape is a Python package that provides a lot a small model 9 | components (i.e., processes) to use with the xarray-simlab_ modeling 10 | framework. Those components can readily be combined together in order 11 | to create custom Landscape Evolution Models (LEMs). 12 | 13 | Routines from the fastscapelib_ library are used for fast model 14 | execution. 15 | 16 | .. |Build Status| image:: https://github.com/fastscape-lem/fastscape/actions/workflows/tests.yml/badge.svg?branch=master 17 | :target: https://github.com/fastscape-lem/fastscape/actions/workflows/tests.yml 18 | :alt: Build Status 19 | .. |Doc Status| image:: https://readthedocs.org/projects/fastscape/badge/?version=latest 20 | :target: https://fastscape.readthedocs.io/en/latest/?badge=latest 21 | :alt: Documentation Status 22 | .. |Zenodo| image:: https://zenodo.org/badge/133702738.svg 23 | :target: https://zenodo.org/badge/latestdoi/133702738 24 | :alt: Citation 25 | 26 | .. _xarray-simlab: https://github.com/benbovy/xarray-simlab 27 | .. _fastscapelib: https://github.com/fastscape-lem/fastscapelib-fortran 28 | 29 | Documentation 30 | ------------- 31 | 32 | Documentation is hosted on ReadTheDocs: 33 | https://fastscape.readthedocs.io 34 | 35 | License 36 | ------- 37 | 38 | 3-clause ("Modified" or "New") BSD license. See the LICENSE file for details. 39 | 40 | Acknowledgment 41 | -------------- 42 | 43 | Fastscape is developed at the `Earth Surface Process Modelling`__ group of 44 | the GFZ Helmholtz Centre Potsdam. 45 | 46 | __ http://www.gfz-potsdam.de/en/section/earth-surface-process-modelling/ 47 | 48 | Citing fastscape 49 | ---------------- 50 | 51 | If you use xarray-simlab in a scientific publication, we would 52 | appreciate a `citation`_. 53 | 54 | .. _`citation`: http://fastscape.readthedocs.io/en/latest/cite.html 55 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | Fastscape: a fast, versatile and user-friendly landscape evolution model 2 | ======================================================================== 3 | 4 | Fastscape is a Python package that provides a lot a small model 5 | components (i.e., processes) to use with the xarray-simlab_ modeling 6 | framework. Those components can readily be combined together in order 7 | to create custom Landscape Evolution Models (LEMs). 8 | 9 | Routines from the fastscapelib_ library are used for fast model 10 | execution. 11 | 12 | .. _xarray-simlab: http://xarray-simlab.readthedocs.io/ 13 | .. _fastscapelib: https://github.com/fastscape-lem/fastscapelib-fortran 14 | 15 | Documentation index 16 | ------------------- 17 | 18 | **Getting Started** 19 | 20 | * :doc:`install` 21 | * :doc:`examples` 22 | 23 | .. toctree:: 24 | :maxdepth: 1 25 | :hidden: 26 | :caption: Getting Started 27 | 28 | install 29 | examples 30 | 31 | **API Reference** 32 | 33 | * :doc:`models` 34 | * :doc:`processes` 35 | 36 | .. toctree:: 37 | :maxdepth: 1 38 | :hidden: 39 | :caption: API Reference 40 | 41 | models 42 | processes 43 | 44 | 45 | **Help & reference** 46 | 47 | * :doc:`release_notes` 48 | * :doc:`cite` 49 | * :doc:`develop` 50 | 51 | .. toctree:: 52 | :maxdepth: 1 53 | :hidden: 54 | :caption: Help & reference 55 | 56 | release_notes 57 | cite 58 | develop 59 | 60 | License 61 | ------- 62 | 63 | 3-clause ("Modified" or "New") BSD license. See the LICENSE file for details. 64 | 65 | Acknowledgment 66 | -------------- 67 | 68 | Fastscape is developed at the `Earth Surface Process Modelling`__ group of 69 | the GFZ Helmholtz Centre Potsdam. 70 | 71 | __ http://www.gfz-potsdam.de/en/section/earth-surface-process-modelling/ 72 | 73 | Citing fastscape 74 | ---------------- 75 | 76 | If you have used fastscape and would like to cite it in a scientific 77 | publication, we would certainly appreciate it (see :doc:`cite` 78 | section). 79 | -------------------------------------------------------------------------------- /fastscape/processes/hillslope.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .context import FastscapelibContext 6 | from .grid import UniformRectilinearGrid2D 7 | from .main import SurfaceToErode, UniformSedimentLayer 8 | 9 | 10 | @xs.process 11 | class LinearDiffusion: 12 | """Hillslope erosion by diffusion.""" 13 | 14 | diffusivity = xs.variable( 15 | dims=[(), ("y", "x")], description="diffusivity (transport coefficient)" 16 | ) 17 | erosion = xs.variable(dims=("y", "x"), intent="out", groups="erosion") 18 | 19 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 20 | elevation = xs.foreign(SurfaceToErode, "elevation") 21 | fs_context = xs.foreign(FastscapelibContext, "context") 22 | 23 | def run_step(self): 24 | kd = np.broadcast_to(self.diffusivity, self.shape).flatten() 25 | self.fs_context["kd"] = kd 26 | 27 | # we don't use the kdsed fastscapelib-fortran feature directly 28 | # see class DifferentialLinearDiffusion 29 | self.fs_context["kdsed"] = -1.0 30 | 31 | # bypass fastscapelib-fortran global state 32 | self.fs_context["h"] = self.elevation.flatten() 33 | 34 | fs.diffusion() 35 | 36 | erosion_flat = self.elevation.ravel() - self.fs_context["h"] 37 | self.erosion = erosion_flat.reshape(self.shape) 38 | 39 | 40 | @xs.process 41 | class DifferentialLinearDiffusion(LinearDiffusion): 42 | """Hillslope differential erosion by diffusion. 43 | 44 | Diffusivity may vary depending on whether the topographic surface 45 | is bare rock or covered by a soil (sediment) layer. 46 | 47 | """ 48 | 49 | diffusivity_bedrock = xs.variable(dims=[(), ("y", "x")], description="bedrock diffusivity") 50 | diffusivity_soil = xs.variable(dims=[(), ("y", "x")], description="soil (sediment) diffusivity") 51 | 52 | diffusivity = xs.variable(dims=("y", "x"), intent="out", description="differential diffusivity") 53 | 54 | soil_thickness = xs.foreign(UniformSedimentLayer, "thickness") 55 | 56 | def run_step(self): 57 | self.diffusivity = np.where( 58 | self.soil_thickness <= 0.0, self.diffusivity_bedrock, self.diffusivity_soil 59 | ) 60 | 61 | super().run_step() 62 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_boundary.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from fastscape.processes import BorderBoundary 5 | 6 | 7 | def test_border_boundary_broadcast(): 8 | p = BorderBoundary(status="fixed_value") 9 | 10 | p.initialize() 11 | np.testing.assert_equal(p.border, np.array(["left", "right", "top", "bottom"])) 12 | np.testing.assert_equal(p.border_status, ["fixed_value"] * 4) 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "status, expected_ibc", 17 | [ 18 | (["fixed_value", "fixed_value", "fixed_value", "fixed_value"], 1111), 19 | (["core", "fixed_value", "fixed_value", "fixed_value"], 1110), 20 | (["fixed_value", "core", "fixed_value", "fixed_value"], 1011), 21 | (["fixed_value", "fixed_value", "core", "fixed_value"], 111), 22 | (["fixed_value", "fixed_value", "fixed_value", "core"], 1101), 23 | (["looped", "looped", "fixed_value", "fixed_value"], 1010), 24 | (["fixed_value", "fixed_value", "looped", "looped"], 101), 25 | ], 26 | ) 27 | def test_border_boundary_ibc(status, expected_ibc): 28 | p = BorderBoundary(status=status) 29 | 30 | p.initialize() 31 | np.testing.assert_equal(p.ibc, expected_ibc) 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "status, error_msg", 36 | [ 37 | (["fixed_value", "fixed_value", "core"], "Border status should be defined for all borders"), 38 | ("invalid_status", "Invalid border status"), 39 | (["looped", "looped", "core", "core"], "There must be at least one border with status"), 40 | ( 41 | ["looped", "fixed_value", "looped", "fixed_value"], 42 | "Periodic boundary conditions must be symmetric", 43 | ), 44 | ], 45 | ) 46 | def test_border_boundary_error(status, error_msg): 47 | with pytest.raises(ValueError, match=error_msg): 48 | BorderBoundary(status=status) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "status, warning_msg", 53 | [ 54 | (["core", "core", "fixed_value", "fixed_value"], "Left and right"), 55 | (["fixed_value", "fixed_value", "core", "core"], "Top and bottom"), 56 | ], 57 | ) 58 | def test_border_boundary_warning(status, warning_msg): 59 | p = BorderBoundary(status=status) 60 | with pytest.warns(UserWarning, match=warning_msg): 61 | p.initialize() 62 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_initial.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from fastscape.processes import ( 5 | BareRockSurface, 6 | Escarpment, 7 | FlatSurface, 8 | NoErosionHistory, 9 | ) 10 | 11 | 12 | def test_flat_surface(): 13 | shape = (3, 2) 14 | rs = np.random.RandomState(seed=1234) 15 | elevation = rs.rand(*shape) 16 | 17 | np.random.default_rng(1234) 18 | p = FlatSurface(shape=shape, seed=1234) 19 | p.initialize() 20 | 21 | np.testing.assert_equal(shape, p.shape) 22 | np.testing.assert_allclose(elevation, p.elevation) 23 | 24 | p2 = FlatSurface(shape=shape, seed=None) 25 | p2.initialize() 26 | assert np.all(p2.elevation > 0.0) 27 | assert np.all(p2.elevation <= 1.0) 28 | 29 | 30 | @pytest.mark.parametrize( 31 | "inputs", 32 | [ 33 | ( 34 | { 35 | "x_left": 10, 36 | "x_right": 20, 37 | "elevation_left": 0.0, 38 | "elevation_right": 100.0, 39 | "shape": (11, 31), 40 | "x": np.linspace(0, 30, 31), 41 | } 42 | ), 43 | ( 44 | { 45 | "x_left": 15, 46 | "x_right": 15, 47 | "elevation_left": 0.0, 48 | "elevation_right": 100.0, 49 | "shape": (11, 31), 50 | "x": np.linspace(0, 30, 31), 51 | } 52 | ), 53 | ], 54 | ) 55 | def test_escarpment(inputs): 56 | p = Escarpment(**inputs) 57 | p.initialize() 58 | 59 | # test invariant elevation along the rows (y-axis) up to random values 60 | assert np.all(np.abs(p.elevation - p.elevation[0, :]) < 1.0) 61 | 62 | # shape and x-coordinate values chosen so that the escaprement limits 63 | # match the grid 64 | assert abs(p.elevation[0, int(p.x_left)] - p.elevation_left) < 1.0 65 | assert abs(p.elevation[0, int(p.x_right) + 1] - p.elevation_right) < 1.0 66 | 67 | 68 | def test_bare_rock_surface(): 69 | elevation = np.array([[2, 3], [4, 1]]) 70 | 71 | p = BareRockSurface(surf_elevation=elevation) 72 | p.initialize() 73 | 74 | np.testing.assert_equal(elevation, p.bedrock_elevation) 75 | # bedrock_elevation must be a copy of surf_elevation 76 | assert p.bedrock_elevation.base is not p.surf_elevation 77 | 78 | 79 | def test_no_erosion_history(): 80 | p = NoErosionHistory() 81 | p.initialize() 82 | 83 | assert p.height == 0 84 | -------------------------------------------------------------------------------- /fastscape/processes/__init__.py: -------------------------------------------------------------------------------- 1 | from .boundary import BorderBoundary 2 | from .channel import ( 3 | ChannelErosion, 4 | DifferentialStreamPowerChannel, 5 | DifferentialStreamPowerChannelTD, 6 | StreamPowerChannel, 7 | StreamPowerChannelTD, 8 | ) 9 | from .erosion import TotalErosion 10 | from .flow import ( 11 | DrainageArea, 12 | FlowAccumulator, 13 | FlowRouter, 14 | MultipleFlowRouter, 15 | SingleFlowRouter, 16 | ) 17 | from .grid import RasterGrid2D, UniformRectilinearGrid2D 18 | from .hillslope import DifferentialLinearDiffusion, LinearDiffusion 19 | from .initial import BareRockSurface, Escarpment, FlatSurface, NoErosionHistory 20 | from .isostasy import ( 21 | BaseIsostasy, 22 | BaseLocalIsostasy, 23 | Flexure, 24 | LocalIsostasyErosion, 25 | LocalIsostasyErosionTectonics, 26 | LocalIsostasyTectonics, 27 | ) 28 | from .main import ( 29 | Bedrock, 30 | StratigraphicHorizons, 31 | SurfaceToErode, 32 | SurfaceTopography, 33 | TerrainDerivatives, 34 | TotalVerticalMotion, 35 | UniformSedimentLayer, 36 | ) 37 | from .marine import MarineSedimentTransport, Sea 38 | from .tectonics import ( 39 | BlockUplift, 40 | HorizontalAdvection, 41 | SurfaceAfterTectonics, 42 | TectonicForcing, 43 | TwoBlocksUplift, 44 | ) 45 | 46 | __all__ = ( 47 | "BorderBoundary", 48 | "ChannelErosion", 49 | "DifferentialStreamPowerChannel", 50 | "DifferentialStreamPowerChannelTD", 51 | "StreamPowerChannel", 52 | "StreamPowerChannelTD", 53 | "TotalErosion", 54 | "DrainageArea", 55 | "FlowAccumulator", 56 | "FlowRouter", 57 | "SingleFlowRouter", 58 | "MultipleFlowRouter", 59 | "RasterGrid2D", 60 | "UniformRectilinearGrid2D", 61 | "LinearDiffusion", 62 | "DifferentialLinearDiffusion", 63 | "BareRockSurface", 64 | "Escarpment", 65 | "FlatSurface", 66 | "NoErosionHistory", 67 | "BaseIsostasy", 68 | "BaseLocalIsostasy", 69 | "Flexure", 70 | "LocalIsostasyErosion", 71 | "LocalIsostasyErosionTectonics", 72 | "LocalIsostasyTectonics", 73 | "Bedrock", 74 | "SurfaceTopography", 75 | "SurfaceToErode", 76 | "StratigraphicHorizons", 77 | "TerrainDerivatives", 78 | "TotalVerticalMotion", 79 | "UniformSedimentLayer", 80 | "MarineSedimentTransport", 81 | "Sea", 82 | "BlockUplift", 83 | "HorizontalAdvection", 84 | "SurfaceAfterTectonics", 85 | "TectonicForcing", 86 | "TwoBlocksUplift", 87 | ) 88 | -------------------------------------------------------------------------------- /fastscape/processes/grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xsimlab as xs 3 | 4 | 5 | @xs.process 6 | class UniformRectilinearGrid2D: 7 | """Create a uniform rectilinear (static) 2-dimensional grid.""" 8 | 9 | shape = xs.variable(dims="shape_yx", description="nb. of grid nodes in (y, x)", static=True) 10 | spacing = xs.variable(dims="shape_yx", description="grid node spacing in (y, x)", static=True) 11 | origin = xs.variable( 12 | dims="shape_yx", description="(y, x) coordinates of grid origin", static=True 13 | ) 14 | 15 | length = xs.variable(dims="shape_yx", intent="out", description="total grid length in (y, x)") 16 | size = xs.variable(intent="out", description="total nb. of nodes") 17 | area = xs.variable(intent="out", description="total grid area") 18 | cell_area = xs.variable(intent="out", description="fixed grid cell area") 19 | 20 | dx = xs.variable(intent="out", description="grid spacing in x (cols)") 21 | dy = xs.variable(intent="out", description="grid spacing in y (rows)") 22 | 23 | nx = xs.variable(intent="out", description="nb. of nodes in x (cols)") 24 | ny = xs.variable(intent="out", description="nb. of nodes in y (rows)") 25 | 26 | x = xs.index(dims="x", description="grid x coordinate") 27 | y = xs.index(dims="y", description="grid y coordinate") 28 | 29 | def _set_length_or_spacing(self): 30 | self.length = (self.shape - 1) * self.spacing 31 | 32 | def initialize(self): 33 | self._set_length_or_spacing() 34 | self.size = np.prod(self.shape) 35 | self.cell_area = np.prod(self.spacing) 36 | self.area = self.cell_area * self.size 37 | 38 | self.dx = self.spacing[1] 39 | self.dy = self.spacing[0] 40 | self.nx = self.shape[1] 41 | self.ny = self.shape[0] 42 | 43 | self.x = np.linspace(self.origin[1], self.origin[1] + self.length[1], self.shape[1]) 44 | self.y = np.linspace(self.origin[0], self.origin[0] + self.length[0], self.shape[0]) 45 | 46 | 47 | @xs.process 48 | class RasterGrid2D(UniformRectilinearGrid2D): 49 | """Create a raster 2-dimensional grid.""" 50 | 51 | length = xs.variable( 52 | dims="shape_yx", intent="in", description="total grid length in (y, x)", static=True 53 | ) 54 | origin = xs.variable( 55 | dims="shape_yx", intent="out", description="(y, x) coordinates of grid origin" 56 | ) 57 | spacing = xs.variable(dims="shape_yx", intent="out", description="grid node spacing in (y, x)") 58 | 59 | def _set_length_or_spacing(self): 60 | self.spacing = self.length / (self.shape - 1) 61 | 62 | def initialize(self): 63 | self.origin = np.array([0.0, 0.0]) 64 | super().initialize() 65 | -------------------------------------------------------------------------------- /doc/source/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | A tutorial and some illustrative examples can be found as jupyter 7 | notebooks in the `fastscape_demo`_ repository. Thanks to `Binder`_, 8 | you can run those notebooks in your browser without the need to 9 | install anything. Just click on the badge below: 10 | 11 | .. image:: https://img.shields.io/badge/launch-binder-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC 12 | :target: https://mybinder.org/v2/gh/fastscape-lem/fastscape-demo/master 13 | 14 | .. _`fastscape_demo`: https://github.com/fastscape-lem/fastscape-demo 15 | .. _`Binder`: https://mybinder.readthedocs.io 16 | -------------------------------------------------------------------------------- /doc/source/models.rst: -------------------------------------------------------------------------------- 1 | .. _models: 2 | 3 | Models 4 | ====== 5 | 6 | Fastscape provides a few landscape evolution model "presets", i.e., 7 | :class:`xsimlab.Model` objects that can be used as-is or as a basis 8 | for building custom models. Those presets are built from a large 9 | collection of :doc:`processes`. 10 | 11 | For help on how to run and further customize those models, see the 12 | :doc:`examples` section or `xarray-simlab's documentation`_. 13 | 14 | .. _`xarray-simlab's documentation`: http://xarray-simlab.readthedocs.io/ 15 | 16 | Bootstrap model 17 | --------------- 18 | 19 | ``bootstrap_model`` has the minimal set of processes required to 20 | simulate on a 2D uniform grid the evolution of topographic surface 21 | under the action of tectonic and erosion processes. 22 | 23 | None of such processes is included. This model only provides the 24 | "skeleton" of a landscape evolution model and might be used as a basis 25 | to create custom models. 26 | 27 | .. ipython:: python 28 | 29 | from fastscape.models import bootstrap_model 30 | bootstrap_model 31 | 32 | Basic model 33 | ----------- 34 | 35 | ``basic_model`` is a "standard" landscape evolution model that 36 | includes block uplift, (bedrock) channel erosion using the stream 37 | power law and hillslope erosion/deposition using linear diffusion. 38 | 39 | Initial topography is a flat surface with random perturbations. 40 | 41 | Flow is routed on the topographic surface using a D8, single flow 42 | direction algorithm. 43 | 44 | All erosion processes are computed on a topographic surface that is 45 | first updated by tectonic forcing processes. 46 | 47 | .. ipython:: python 48 | 49 | from fastscape.models import basic_model 50 | basic_model 51 | 52 | Sediment model 53 | -------------- 54 | 55 | ``sediment_model`` is built on top of ``basic_model`` ; it tracks the 56 | evolution of both the topographic surface and the bedrock, separated 57 | by a uniform, active layer of sediment. 58 | 59 | This model uses an extended version of the stream-power law that also 60 | includes channel transport and deposition. 61 | 62 | Flow is routed using a multiple flow direction algorithm. 63 | 64 | Differential erosion/deposition is enabled for both hillslope and 65 | channel processes, i.e., distinct values may be set for the erosion 66 | and transport coefficients (bedrock vs soil/sediment). 67 | 68 | .. ipython:: python 69 | 70 | from fastscape.models import sediment_model 71 | sediment_model 72 | 73 | Marine model 74 | ------------ 75 | 76 | ``marine_model`` simulates the erosion, transport and deposition of 77 | bedrock or sediment in both continental and submarine environments. 78 | 79 | It is built on top of ``sediment_model`` to which it 80 | adds a process for sediment transport, deposition and compaction in 81 | the submarine domain (under sea level). 82 | 83 | The processes for the initial topography and uplift both allow easy 84 | set-up of the two land vs. marine environments. 85 | 86 | An additional process keeps track of a fixed number of stratigraphic 87 | horizons over time. 88 | 89 | .. ipython:: python 90 | 91 | from fastscape.models import marine_model 92 | marine_model 93 | -------------------------------------------------------------------------------- /fastscape/processes/boundary.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import numpy as np 4 | import xsimlab as xs 5 | 6 | 7 | @xs.process 8 | class BorderBoundary: 9 | """Sets boundary conditions at grid borders. 10 | 11 | Borders are defined in the following order: 12 | 13 | left, right, top, bottom 14 | 15 | Border status can be one of: 16 | 17 | - "core" (open boundary) 18 | - "fixed_value" (closed boundary) 19 | - "looped" (periodic boundary) 20 | 21 | "fixed_value" must be set for at least one border. This is the minimal 22 | constraint in order to make the numerical model solvable. 23 | 24 | "looped" must be symmetric, i.e., defined for (left, right) 25 | or (top, bottom). 26 | 27 | Note that currently if "core" is set for two opposed borders these 28 | will have periodic conditions (this comes from a current limitation in 29 | fastscapelib-fortran which will be solved in a next release). 30 | 31 | """ 32 | 33 | status = xs.variable( 34 | dims=[(), "border"], 35 | default="fixed_value", 36 | description="node status at borders", 37 | static=True, 38 | ) 39 | 40 | border = xs.index(dims="border", description="4-border boundaries coordinate") 41 | border_status = xs.variable( 42 | dims="border", intent="out", description="node status at the 4-border boundaries" 43 | ) 44 | 45 | ibc = xs.variable(intent="out", description="boundary code used by fastscapelib-fortran") 46 | 47 | @status.validator 48 | def _check_status(self, attribute, value): 49 | if not np.isscalar(value) and len(value) != 4: 50 | raise ValueError( 51 | "Border status should be defined for all borders " 52 | f"(left, right, top, bottom), found {value}" 53 | ) 54 | 55 | valid = ["fixed_value", "core", "looped"] 56 | bs = list(np.broadcast_to(value, 4)) 57 | 58 | for s in bs: 59 | if s not in valid: 60 | raise ValueError(f"Invalid border status {s!r}, must be one of {valid}") 61 | 62 | if "fixed_value" not in bs: 63 | raise ValueError( 64 | f"There must be at least one border with status 'fixed_value', found {bs}" 65 | ) 66 | 67 | def invalid_looped(s): 68 | return bool(s[0] == "looped") ^ bool(s[1] == "looped") 69 | 70 | if invalid_looped(bs[:2]) or invalid_looped(bs[2:]): 71 | raise ValueError(f"Periodic boundary conditions must be symmetric, found {bs}") 72 | 73 | def initialize(self): 74 | self.border = np.array(["left", "right", "top", "bottom"]) 75 | 76 | bstatus = np.array(np.broadcast_to(self.status, 4)) 77 | 78 | # TODO: remove when solved in fastscapelib-fortran 79 | w_msg_common = ( 80 | "borders have both 'core' status but periodic conditions " 81 | "are used due to current behavior in fastscapelib-fortran" 82 | ) 83 | 84 | if bstatus[0] == "core" and bstatus[1] == "core": 85 | w_msg = "Left and right " + w_msg_common 86 | warnings.warn(w_msg, UserWarning) 87 | 88 | if bstatus[2] == "core" and bstatus[3] == "core": 89 | w_msg = "Top and bottom " + w_msg_common 90 | warnings.warn(w_msg, UserWarning) 91 | 92 | self.border_status = bstatus 93 | 94 | # convert to fastscapelib-fortran ibc code 95 | arr_bc = np.array([1 if st == "fixed_value" else 0 for st in self.border_status]) 96 | 97 | # different border order 98 | self.ibc = sum(arr_bc * np.array([1, 100, 1000, 10])) 99 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_tectonics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from fastscape.processes import ( 5 | BlockUplift, 6 | SurfaceAfterTectonics, 7 | TectonicForcing, 8 | TwoBlocksUplift, 9 | ) 10 | from fastscape.processes.context import FastscapelibContext 11 | 12 | 13 | def test_tectonic_forcing(): 14 | grid_shape = (3, 2) 15 | uplift = np.full(grid_shape, 1.0) 16 | isostasy = np.full(grid_shape, 2.0) 17 | bedrock_advect = np.full(grid_shape, 3.0) 18 | surf_advect = np.full(grid_shape, 4.0) 19 | area = 300.0 20 | dt = 10.0 21 | 22 | p = TectonicForcing( 23 | bedrock_forcing_vars=[uplift, isostasy, bedrock_advect], 24 | surface_forcing_vars=[uplift, isostasy, surf_advect], 25 | grid_area=area, 26 | ) 27 | 28 | p.run_step(dt) 29 | 30 | # uplift + isostasy + bedrock_advect 31 | np.testing.assert_equal(p.bedrock_upward, np.full(grid_shape, 1.0 + 2.0 + 3.0)) 32 | 33 | # uplift + isostasy + surf_advect 34 | np.testing.assert_equal(p.surface_upward, np.full(grid_shape, 1.0 + 2.0 + 4.0)) 35 | 36 | # test scalar values 37 | p2 = TectonicForcing(surface_forcing_vars=[1.0, 2.0, 3.0], grid_area=area) 38 | p2.run_step(dt) 39 | assert p2.surface_upward == 6.0 40 | assert p2.bedrock_upward == 0.0 # no variables given 41 | assert p2._domain_rate() == 6.0 * area / dt 42 | 43 | 44 | def test_surface_after_tectonics(): 45 | grid_shape = (3, 2) 46 | topo_elevation = np.full(grid_shape, 2.0) 47 | forced_motion = np.full(grid_shape, 3.0) 48 | 49 | p = SurfaceAfterTectonics(topo_elevation=topo_elevation, forced_motion=forced_motion) 50 | 51 | p.run_step() 52 | 53 | expected = topo_elevation + forced_motion 54 | np.testing.assert_equal(p.elevation, expected) 55 | 56 | 57 | @pytest.mark.parametrize( 58 | "b_status, expected_uplift", 59 | [ 60 | ( 61 | np.array(["fixed_value", "fixed_value", "fixed_value", "fixed_value"]), 62 | np.array([[0.0, 0.0, 0.0, 0.0], [0.0, 50.0, 50.0, 0.0], [0.0, 0.0, 0.0, 0.0]]), 63 | ), 64 | ( 65 | np.array(["fixed_value", "core", "core", "fixed_value"]), 66 | np.array([[0.0, 50.0, 50.0, 50.0], [0.0, 50.0, 50.0, 50.0], [0.0, 0.0, 0.0, 0.0]]), 67 | ), 68 | ], 69 | ) 70 | def test_block_uplift(b_status, expected_uplift): 71 | rate = 5 72 | shape = (3, 4) 73 | dt = 10.0 74 | 75 | # dummy context 76 | f = FastscapelibContext(shape=shape, length=(10.0, 30.0), ibc=1010) 77 | f.initialize() 78 | f.run_step(dt) 79 | 80 | p = BlockUplift(rate=rate, shape=shape, status=b_status, fs_context=f) 81 | 82 | p.initialize() 83 | p.run_step(dt) 84 | np.testing.assert_equal(p.uplift, expected_uplift) 85 | 86 | # test variable rate 87 | p2 = BlockUplift(rate=np.full(shape, 5.0), shape=shape, status=b_status, fs_context=f) 88 | p2.initialize() 89 | p2.run_step(dt) 90 | np.testing.assert_equal(p2.uplift, expected_uplift) 91 | 92 | 93 | def test_two_blocks_uplift(): 94 | x = np.array([1, 2, 3]) 95 | x_pos = 1 96 | rate_l = 2 97 | rate_r = 3 98 | grid = (3, 4) 99 | dt = 10.0 100 | 101 | p = TwoBlocksUplift(x_position=x_pos, rate_left=rate_l, rate_right=rate_r, shape=grid, x=x) 102 | 103 | p.initialize() 104 | p.run_step(dt) 105 | 106 | expected = np.array( 107 | [[20.0, 30.0, 30.0, 30.0], [20.0, 30.0, 30.0, 30.0], [20.0, 30.0, 30.0, 30.0]] 108 | ) 109 | np.testing.assert_equal(p.uplift, expected) 110 | -------------------------------------------------------------------------------- /fastscape/processes/initial.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import xsimlab as xs 3 | 4 | from .erosion import TotalErosion 5 | from .grid import UniformRectilinearGrid2D 6 | from .main import Bedrock, SurfaceTopography 7 | 8 | 9 | @xs.process 10 | class FlatSurface: 11 | """Initialize surface topography as a flat surface at sea-level with 12 | random perturbations (white noise). 13 | 14 | """ 15 | 16 | seed = xs.variable(default=None, description="random seed") 17 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 18 | elevation = xs.foreign(SurfaceTopography, "elevation", intent="out") 19 | 20 | def initialize(self): 21 | if self.seed is not None: 22 | if np.isnan(float(self.seed)): 23 | seed = None 24 | else: 25 | seed = int(self.seed) 26 | else: 27 | seed = self.seed 28 | 29 | rs = np.random.RandomState(seed=seed) 30 | self.elevation = rs.rand(*self.shape) 31 | 32 | 33 | @xs.process 34 | class Escarpment: 35 | """Initialize surface topography as an escarpment separating two 36 | nearly flat surfaces. 37 | 38 | The slope of the escarpment is uniform (linear interpolation 39 | between the two plateaus). Random perturbations are added to the 40 | elevation of each plateau. 41 | 42 | """ 43 | 44 | x_left = xs.variable( 45 | description="location of the scarp's left limit on the x-axis", static=True 46 | ) 47 | x_right = xs.variable( 48 | description="location of the scarp's right limit on the x-axis", static=True 49 | ) 50 | 51 | elevation_left = xs.variable(description="elevation on the left side of the scarp") 52 | elevation_right = xs.variable(description="elevation on the right side of the scarp") 53 | 54 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 55 | x = xs.foreign(UniformRectilinearGrid2D, "x") 56 | elevation = xs.foreign(SurfaceTopography, "elevation", intent="out") 57 | 58 | def initialize(self): 59 | self.elevation = np.full(self.shape, self.elevation_left, dtype=np.double) 60 | 61 | # align scarp limit locations 62 | idx_left = np.argmax(self.x > self.x_left) 63 | idx_right = np.argmax(self.x > self.x_right) 64 | 65 | self.elevation[:, idx_right:] = self.elevation_right 66 | 67 | # ensure lower elevation on x-axis limits for nice drainage patterns 68 | self.elevation[:, 1:-1] += np.random.rand(*self.shape)[:, 1:-1] 69 | 70 | # create scarp slope 71 | scarp_width = self.x[idx_right] - self.x[idx_left] 72 | 73 | if scarp_width > 0: 74 | scarp_height = self.elevation_right - self.elevation_left 75 | scarp_slope = scarp_height / scarp_width 76 | scarp_coord = self.x[idx_left:idx_right] - self.x[idx_left] 77 | 78 | self.elevation[:, idx_left:idx_right] = self.elevation_left + scarp_slope * scarp_coord 79 | 80 | 81 | @xs.process 82 | class BareRockSurface: 83 | """Initialize topographic surface as a bare rock surface.""" 84 | 85 | surf_elevation = xs.foreign(SurfaceTopography, "elevation") 86 | bedrock_elevation = xs.foreign(Bedrock, "elevation", intent="out") 87 | 88 | def initialize(self): 89 | self.bedrock_elevation = self.surf_elevation.copy() 90 | 91 | 92 | @xs.process 93 | class NoErosionHistory: 94 | """Initialize erosion to zero (no erosion history).""" 95 | 96 | height = xs.foreign(TotalErosion, "cumulative_height", intent="out") 97 | 98 | def initialize(self): 99 | self.height = 0.0 100 | -------------------------------------------------------------------------------- /fastscape/processes/marine.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import xsimlab as xs 3 | 4 | from .channel import ChannelErosion 5 | from .context import FastscapelibContext 6 | from .grid import UniformRectilinearGrid2D 7 | from .main import SurfaceToErode 8 | 9 | 10 | @xs.process 11 | class Sea: 12 | """Sea level.""" 13 | 14 | # TODO: add diagnostics like shoreline extraction or 15 | # continental area vs. marine masks. 16 | 17 | level = xs.variable(default=0.0, description="sea level (elevation)") 18 | 19 | 20 | @xs.process 21 | class MarineSedimentTransport: 22 | """Marine sediment transport, deposition and compaction. 23 | 24 | The source of sediment used for marine transport originates from 25 | channel erosion and/or transport, which, integrated over the whole 26 | continental area, provides a volume of sediment yielded through 27 | the shoreline. 28 | 29 | A uniform, user-defined ratio of silt/sand is considered for this 30 | sediment yield. Each of these grain size category has its own 31 | properties like porosity, the exponential decreasing of porosity 32 | with depth and the transport coefficient (diffusivity). 33 | 34 | """ 35 | 36 | ss_ratio_land = xs.variable(description="silt fraction of continental sediment source") 37 | ss_ratio_sea = xs.variable( 38 | dims=("y", "x"), intent="out", description="silt fraction of marine sediment layer" 39 | ) 40 | 41 | porosity_sand = xs.variable(description="surface (reference) porosity of sand") 42 | porosity_silt = xs.variable(description="surface (reference) porosity of silt") 43 | 44 | e_depth_sand = xs.variable(description="e-folding depth of exp. porosity curve for sand") 45 | e_depth_silt = xs.variable(description="e-folding depth of exp. porosity curve for silt") 46 | 47 | diffusivity_sand = xs.variable(description="diffusivity (transport coefficient) for sand") 48 | 49 | diffusivity_silt = xs.variable(description="diffusivity (transport coefficient) for silt") 50 | 51 | layer_depth = xs.variable(description="mean depth (thickness) of marine active layer") 52 | 53 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 54 | fs_context = xs.foreign(FastscapelibContext, "context") 55 | elevation = xs.foreign(SurfaceToErode, "elevation") 56 | sediment_source = xs.foreign(ChannelErosion, "erosion") 57 | sea_level = xs.foreign(Sea, "level") 58 | 59 | erosion = xs.variable( 60 | dims=("y", "x"), 61 | intent="out", 62 | groups="erosion", 63 | description="marine erosion or deposition of sand/silt", 64 | ) 65 | 66 | def initialize(self): 67 | # needed so that channel erosion/transport is disabled below sealevel 68 | self.fs_context["runmarine"] = True 69 | 70 | def run_step(self): 71 | self.fs_context["ratio"] = self.ss_ratio_land 72 | 73 | self.fs_context["poro2"] = self.porosity_sand 74 | self.fs_context["poro1"] = self.porosity_silt 75 | 76 | self.fs_context["zporo2"] = self.e_depth_sand 77 | self.fs_context["zporo1"] = self.e_depth_silt 78 | 79 | self.fs_context["kdsea2"] = self.diffusivity_sand 80 | self.fs_context["kdsea1"] = self.diffusivity_silt 81 | 82 | self.fs_context["layer"] = self.layer_depth 83 | 84 | self.fs_context["sealevel"] = self.sea_level 85 | self.fs_context["Sedflux"] = self.sediment_source.ravel() 86 | 87 | # bypass fastscapelib-fortran global state 88 | self.fs_context["h"] = self.elevation.flatten() 89 | 90 | fs.marine() 91 | 92 | erosion_flat = self.elevation.ravel() - self.fs_context["h"] 93 | self.erosion = erosion_flat.reshape(self.shape) 94 | 95 | self.ss_ratio_sea = self.fs_context["fmix"].copy().reshape(self.shape) 96 | -------------------------------------------------------------------------------- /fastscape/processes/isostasy.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .boundary import BorderBoundary 6 | from .erosion import TotalErosion 7 | from .grid import UniformRectilinearGrid2D 8 | from .main import SurfaceTopography 9 | from .tectonics import TectonicForcing 10 | 11 | 12 | @xs.process 13 | class BaseIsostasy: 14 | """Base class for isostasy. 15 | 16 | Do not use this base class directly in a model! Use one of its 17 | subclasses instead. 18 | 19 | However, if you need one or several of the variables declared here 20 | in another process, it is preferable to pass this base class in 21 | :func:`xsimlab.foreign`. 22 | 23 | """ 24 | 25 | rebound = xs.variable( 26 | dims=("y", "x"), 27 | intent="out", 28 | groups=["bedrock_upward", "surface_upward"], 29 | description="isostasic rebound due to material loading/unloading", 30 | ) 31 | 32 | 33 | @xs.process 34 | class BaseLocalIsostasy(BaseIsostasy): 35 | """Base class for local isostasy. 36 | 37 | Do not use this base class directly in a model! Use one of its 38 | subclasses instead. 39 | 40 | However, if you need one or several of the variables declared here 41 | in another process, it is preferable to pass this base class in 42 | :func:`xsimlab.foreign`. 43 | 44 | """ 45 | 46 | i_coef = xs.variable(description="local isostatic coefficient") 47 | 48 | 49 | @xs.process 50 | class LocalIsostasyErosion(BaseLocalIsostasy): 51 | """Local isostasic effect of erosion.""" 52 | 53 | erosion = xs.foreign(TotalErosion, "height") 54 | 55 | def run_step(self): 56 | self.rebound = self.i_coef * self.erosion 57 | 58 | 59 | @xs.process 60 | class LocalIsostasyTectonics(BaseLocalIsostasy): 61 | """Local isostasic effect of tectonic forcing.""" 62 | 63 | bedrock_upward = xs.foreign(TectonicForcing, "bedrock_upward") 64 | 65 | def run_step(self): 66 | self.rebound = -1.0 * self.i_coef * self.bedrock_upward 67 | 68 | 69 | @xs.process 70 | class LocalIsostasyErosionTectonics(BaseLocalIsostasy): 71 | """Local isostatic effect of both erosion and tectonic forcing. 72 | 73 | This process makes no distinction between the density of rock and 74 | the density of eroded material (one single coefficient is used). 75 | 76 | """ 77 | 78 | erosion = xs.foreign(TotalErosion, "height") 79 | surface_upward = xs.foreign(TectonicForcing, "surface_upward") 80 | 81 | def run_step(self): 82 | self.rebound = self.i_coef * (self.erosion - self.surface_upward) 83 | 84 | 85 | @xs.process 86 | class Flexure(BaseIsostasy): 87 | """Flexural isostatic effect of both erosion and tectonic 88 | forcing. 89 | 90 | """ 91 | 92 | lithos_density = xs.variable(dims=[(), ("y", "x")], description="lithospheric rock density") 93 | asthen_density = xs.variable(description="asthenospheric rock density") 94 | e_thickness = xs.variable(description="effective elastic plate thickness") 95 | 96 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 97 | length = xs.foreign(UniformRectilinearGrid2D, "length") 98 | 99 | ibc = xs.foreign(BorderBoundary, "ibc") 100 | 101 | elevation = xs.foreign(SurfaceTopography, "elevation") 102 | 103 | erosion = xs.foreign(TotalErosion, "height") 104 | surface_upward = xs.foreign(TectonicForcing, "surface_upward") 105 | 106 | def run_step(self): 107 | ny, nx = self.shape 108 | yl, xl = self.length 109 | 110 | lithos_density = np.broadcast_to(self.lithos_density, self.shape).flatten() 111 | 112 | elevation_eq = self.elevation.flatten() 113 | diff = (self.surface_upward - self.erosion).ravel() 114 | 115 | # set elevation pre and post rebound 116 | elevation_pre = elevation_eq + diff 117 | elevation_post = elevation_pre.copy() 118 | 119 | fs.flexure( 120 | elevation_post, 121 | elevation_eq, 122 | nx, 123 | ny, 124 | xl, 125 | yl, 126 | lithos_density, 127 | self.asthen_density, 128 | self.e_thickness, 129 | self.ibc, 130 | ) 131 | 132 | self.rebound = (elevation_post - elevation_pre).reshape(self.shape) 133 | -------------------------------------------------------------------------------- /fastscape/models/_models.py: -------------------------------------------------------------------------------- 1 | import xsimlab as xs 2 | 3 | from ..processes.boundary import BorderBoundary 4 | from ..processes.channel import DifferentialStreamPowerChannelTD, StreamPowerChannel 5 | from ..processes.context import FastscapelibContext 6 | from ..processes.erosion import TotalErosion 7 | from ..processes.flow import DrainageArea, MultipleFlowRouter, SingleFlowRouter 8 | from ..processes.grid import RasterGrid2D 9 | from ..processes.hillslope import DifferentialLinearDiffusion, LinearDiffusion 10 | from ..processes.initial import ( 11 | BareRockSurface, 12 | Escarpment, 13 | FlatSurface, 14 | NoErosionHistory, 15 | ) 16 | from ..processes.main import ( 17 | Bedrock, 18 | StratigraphicHorizons, 19 | SurfaceToErode, 20 | SurfaceTopography, 21 | TerrainDerivatives, 22 | TotalVerticalMotion, 23 | UniformSedimentLayer, 24 | ) 25 | from ..processes.marine import MarineSedimentTransport, Sea 26 | from ..processes.tectonics import ( 27 | BlockUplift, 28 | SurfaceAfterTectonics, 29 | TectonicForcing, 30 | TwoBlocksUplift, 31 | ) 32 | 33 | # ``bootstrap_model`` has the minimal set of processes required to 34 | # simulate on a 2D uniform grid the evolution of topographic surface 35 | # under the action of tectonic and erosion processes. None of such 36 | # processes are included. It only provides the "skeleton" of a 37 | # landscape evolution model and might be used as a basis to create 38 | # custom models. 39 | 40 | bootstrap_model = xs.Model( 41 | { 42 | "grid": RasterGrid2D, 43 | "fs_context": FastscapelibContext, 44 | "boundary": BorderBoundary, 45 | "tectonics": TectonicForcing, 46 | "surf2erode": SurfaceToErode, 47 | "erosion": TotalErosion, 48 | "vmotion": TotalVerticalMotion, 49 | "topography": SurfaceTopography, 50 | } 51 | ) 52 | 53 | # ``basic_model`` is a "standard" landscape evolution model that 54 | # includes block uplift, (bedrock) channel erosion using the stream 55 | # power law and hillslope erosion/deposition using linear 56 | # diffusion. Initial topography is a flat surface with random 57 | # perturbations. Flow is routed on the topographic surface using a D8, 58 | # single flow direction algorithm. All erosion processes are computed 59 | # on a topographic surface that is first updated by tectonic forcing 60 | # processes. 61 | 62 | basic_model = bootstrap_model.update_processes( 63 | { 64 | "uplift": BlockUplift, 65 | "surf2erode": SurfaceAfterTectonics, 66 | "flow": SingleFlowRouter, 67 | "drainage": DrainageArea, 68 | "spl": StreamPowerChannel, 69 | "diffusion": LinearDiffusion, 70 | "terrain": TerrainDerivatives, 71 | "init_topography": FlatSurface, 72 | "init_erosion": NoErosionHistory, 73 | } 74 | ) 75 | 76 | # ``sediment_model`` is built on top of ``basic_model`` ; it tracks 77 | # the evolution of both the topographic surface and the bedrock, 78 | # separated by a uniform, active layer of sediment. This model uses an 79 | # extended version of the stream-power law that also includes channel 80 | # transport and deposition. Flow is routed using a multiple flow 81 | # direction algorithm. Differential erosion/deposition is enabled for 82 | # both hillslope and channel processes, i.e., distinct values may be 83 | # set for the erosion and transport coefficients (bedrock vs 84 | # soil/sediment). 85 | 86 | sediment_model = basic_model.update_processes( 87 | { 88 | "bedrock": Bedrock, 89 | "active_layer": UniformSedimentLayer, 90 | "init_bedrock": BareRockSurface, 91 | "flow": MultipleFlowRouter, 92 | "spl": DifferentialStreamPowerChannelTD, 93 | "diffusion": DifferentialLinearDiffusion, 94 | } 95 | ) 96 | 97 | # ``marine_model`` simulates the erosion, transport and deposition of 98 | # bedrock or sediment in both continental and submarine 99 | # environments. It is built on top of ``sediment_model`` to which it 100 | # adds a process for sediment transport, deposition and compaction in 101 | # the submarine domain (under sea level). The processes for the 102 | # initial topography and uplift both allow easy set-up of the two land 103 | # vs. marine environments. An additional process keeps track of a 104 | # fixed number of stratigraphic horizons over time. 105 | 106 | marine_model = sediment_model.update_processes( 107 | { 108 | "init_topography": Escarpment, 109 | "uplift": TwoBlocksUplift, 110 | "sea": Sea, 111 | "marine": MarineSedimentTransport, 112 | "strati": StratigraphicHorizons, 113 | } 114 | ) 115 | -------------------------------------------------------------------------------- /doc/source/processes.rst: -------------------------------------------------------------------------------- 1 | .. _processes: 2 | 3 | Processes 4 | ========= 5 | 6 | Fastscape provides a few dozens of processes, i.e., Python classes 7 | decorated with :func:`xsimlab.process`, serving as building blocks for 8 | the creation of custom landscape evolution models. 9 | 10 | Those processes are presented by thematic below. You can import any of 11 | them from the ``processes`` subpackage, e.g., 12 | 13 | .. code-block:: python 14 | 15 | from fastscape.processes import SurfaceTopography 16 | 17 | Rather than building models from scratch, you might better want to 18 | pick one of the model presets presented in the :doc:`models` section 19 | and customize it using :meth:`xsimlab.Model.update_processes` or 20 | :meth:`xsimlab.Model.drop_processes`. 21 | 22 | For more help on how to use these process classes to create new 23 | :class:`xsimlab.Model` objects, see the :doc:`examples` section or 24 | `xarray-simlab's documentation`_. 25 | 26 | .. _`xarray-simlab's documentation`: http://xarray-simlab.readthedocs.io/ 27 | 28 | Main interfaces & drivers 29 | ------------------------- 30 | 31 | Defined in ``fastscape/processes/main.py`` 32 | 33 | These processes define (update them over time) the main interfaces 34 | used in landscape evolution models, such as the topographic surface, 35 | the bedrock level or several stratigraphic horizons. 36 | 37 | .. currentmodule:: fastscape.processes 38 | .. autosummary:: 39 | :nosignatures: 40 | :template: process_class.rst 41 | :toctree: _api_generated/ 42 | 43 | Bedrock 44 | SurfaceTopography 45 | SurfaceToErode 46 | StratigraphicHorizons 47 | TerrainDerivatives 48 | TotalVerticalMotion 49 | UniformSedimentLayer 50 | 51 | Grid 52 | ---- 53 | 54 | Defined in ``fastscape/processes/grid.py`` 55 | 56 | Processes that define the model grids used in Fastscape and their 57 | properties (shape, spacing, length, etc.). 58 | 59 | .. autosummary:: 60 | :nosignatures: 61 | :template: process_class.rst 62 | :toctree: _api_generated/ 63 | 64 | UniformRectilinearGrid2D 65 | RasterGrid2D 66 | 67 | Boundaries 68 | ---------- 69 | 70 | Defined in ``fastscape/processes/boundary.py`` 71 | 72 | Processes that can be used for setting the boundary conditions. 73 | 74 | .. autosummary:: 75 | :nosignatures: 76 | :template: process_class.rst 77 | :toctree: _api_generated/ 78 | 79 | BorderBoundary 80 | 81 | Initial conditions 82 | ------------------ 83 | 84 | Defined in ``fastscape/processes/initial.py`` 85 | 86 | Processes that mostly serve as common "presets" for various initial 87 | conditions (e.g., initial topography, erosion pre-history, initial 88 | sediment cover). 89 | 90 | .. autosummary:: 91 | :nosignatures: 92 | :template: process_class.rst 93 | :toctree: _api_generated/ 94 | 95 | BareRockSurface 96 | Escarpment 97 | FlatSurface 98 | NoErosionHistory 99 | 100 | Tectonics 101 | --------- 102 | 103 | Defined in ``fastscape/processes/tectonics.py`` 104 | 105 | All processes (generic or specific) about tectonic forcing. 106 | 107 | .. autosummary:: 108 | :nosignatures: 109 | :template: process_class.rst 110 | :toctree: _api_generated/ 111 | 112 | BlockUplift 113 | HorizontalAdvection 114 | SurfaceAfterTectonics 115 | TectonicForcing 116 | TwoBlocksUplift 117 | 118 | Flow routing 119 | ------------ 120 | 121 | Defined in ``fastscape/processes/flow.py`` 122 | 123 | Processes that route flow on the topographic surface. 124 | 125 | .. autosummary:: 126 | :nosignatures: 127 | :template: process_class.rst 128 | :toctree: _api_generated/ 129 | 130 | FlowRouter 131 | SingleFlowRouter 132 | MultipleFlowRouter 133 | FlowAccumulator 134 | DrainageArea 135 | 136 | Erosion / deposition 137 | -------------------- 138 | 139 | Defined in ``fastscape/processes/erosion.py`` 140 | 141 | General erosion (or deposition) processes. 142 | 143 | .. autosummary:: 144 | :nosignatures: 145 | :template: process_class.rst 146 | :toctree: _api_generated/ 147 | 148 | TotalErosion 149 | 150 | Channel processes 151 | ----------------- 152 | 153 | Defined in ``fastscape/processes/channel.py`` 154 | 155 | River channel erosion, transport and/or deposition processes. 156 | 157 | .. autosummary:: 158 | :nosignatures: 159 | :template: process_class.rst 160 | :toctree: _api_generated/ 161 | 162 | ChannelErosion 163 | DifferentialStreamPowerChannel 164 | DifferentialStreamPowerChannelTD 165 | StreamPowerChannel 166 | StreamPowerChannelTD 167 | 168 | Hillslope processes 169 | ------------------- 170 | 171 | Defined in ``fastscape/processes/hillslope.py`` 172 | 173 | Hillslope erosion, transport and/or deposition processes. 174 | 175 | .. autosummary:: 176 | :nosignatures: 177 | :template: process_class.rst 178 | :toctree: _api_generated/ 179 | 180 | LinearDiffusion 181 | DifferentialLinearDiffusion 182 | 183 | Marine processes 184 | ---------------- 185 | 186 | Defined in ``fastscape/processes/marine.py`` 187 | 188 | Generic or specialized processes used to model (sediment or other) 189 | dynamics in submarine environments. 190 | 191 | .. autosummary:: 192 | :nosignatures: 193 | :template: process_class.rst 194 | :toctree: _api_generated/ 195 | 196 | MarineSedimentTransport 197 | Sea 198 | 199 | Isostasy 200 | -------- 201 | 202 | Defined in ``fastscape/processes/isostasy.py`` 203 | 204 | Processes for modeling the local or flexural isostatic effect of 205 | erosion and/or other driving processes (tectonics). 206 | 207 | .. autosummary:: 208 | :nosignatures: 209 | :template: process_class.rst 210 | :toctree: _api_generated/ 211 | 212 | BaseIsostasy 213 | BaseLocalIsostasy 214 | Flexure 215 | LocalIsostasyErosion 216 | LocalIsostasyErosionTectonics 217 | LocalIsostasyTectonics 218 | -------------------------------------------------------------------------------- /fastscape/tests/test_processes_main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from fastscape.processes import ( 5 | Bedrock, 6 | StratigraphicHorizons, 7 | SurfaceToErode, 8 | SurfaceTopography, 9 | TerrainDerivatives, 10 | TotalVerticalMotion, 11 | UniformSedimentLayer, 12 | ) 13 | 14 | 15 | def test_total_vertical_motion(): 16 | grid_shape = (3, 2) 17 | uplift = np.random.uniform(size=grid_shape) 18 | isostasy = np.random.uniform(size=grid_shape) 19 | erosion1 = np.random.uniform(size=grid_shape) 20 | erosion2 = np.random.uniform(size=grid_shape) 21 | bedrock_advect = np.random.uniform(size=grid_shape) 22 | surf_advect = np.random.uniform(size=grid_shape) 23 | 24 | p = TotalVerticalMotion( 25 | bedrock_upward_vars=[uplift, isostasy, bedrock_advect], 26 | surface_upward_vars=[uplift, isostasy, surf_advect], 27 | surface_downward_vars=[erosion1, erosion2], 28 | ) 29 | 30 | p.run_step() 31 | 32 | expected = uplift + isostasy + bedrock_advect 33 | np.testing.assert_equal(p.bedrock_upward, expected) 34 | 35 | expected = uplift + isostasy + surf_advect - (erosion1 + erosion2) 36 | np.testing.assert_equal(p.surface_upward, expected) 37 | 38 | 39 | def test_surface_topography(): 40 | elevation = np.random.uniform(size=(3, 2)) 41 | upward = np.random.uniform(size=(3, 2)) 42 | expected = elevation + upward 43 | 44 | p = SurfaceTopography(elevation=elevation, motion_upward=upward) 45 | 46 | p.finalize_step() 47 | 48 | np.testing.assert_equal(p.elevation, expected) 49 | 50 | 51 | def test_surface_to_erode(): 52 | elevation = np.random.uniform(size=(3, 2)) 53 | 54 | p = SurfaceToErode(topo_elevation=elevation) 55 | 56 | p.run_step() 57 | 58 | np.testing.assert_equal(p.elevation, p.topo_elevation) 59 | 60 | 61 | def test_bedrock_error(): 62 | elevation = np.ones((3, 2)) 63 | surface_elevation = np.zeros_like(elevation) 64 | 65 | with pytest.raises(ValueError, match=r".* bedrock elevation higher .*"): 66 | p = Bedrock( 67 | elevation=elevation, 68 | surface_elevation=surface_elevation, 69 | bedrock_motion_up=np.zeros_like(elevation), 70 | surface_motion_up=np.zeros_like(elevation), 71 | ) 72 | 73 | p.initialize() 74 | 75 | 76 | def test_berock(): 77 | elevation = np.array([[0.0, 0.0, 0.0], [2.0, 2.0, 2.0]]) 78 | surface_elevation = np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]) 79 | 80 | p = Bedrock( 81 | elevation=elevation, 82 | bedrock_motion_up=np.full_like(elevation, 0.5), 83 | surface_motion_up=np.full_like(elevation, -0.1), 84 | surface_elevation=surface_elevation, 85 | ) 86 | 87 | p.initialize() 88 | p.run_step() 89 | 90 | expected = np.array([[1.0, 1.0, 1.0], [0.0, 0.0, 0.0]]) 91 | np.testing.assert_equal(p._depth(), expected) 92 | 93 | p.finalize_step() 94 | 95 | expected = np.array([[0.5, 0.5, 0.5], [1.9, 1.9, 1.9]]) 96 | np.testing.assert_equal(p.elevation, expected) 97 | 98 | 99 | def test_uniform_sediment_layer(): 100 | grid_shape = (3, 2) 101 | bedrock_elevation = np.random.uniform(size=grid_shape) 102 | surf_elevation = np.random.uniform(size=grid_shape) + 1 103 | expected = surf_elevation - bedrock_elevation 104 | 105 | p = UniformSedimentLayer(bedrock_elevation=bedrock_elevation, surf_elevation=surf_elevation) 106 | 107 | p.initialize() 108 | np.testing.assert_equal(p.thickness, expected) 109 | 110 | p.run_step() 111 | np.testing.assert_equal(p.thickness, expected) 112 | 113 | 114 | def test_terrain_derivatives(): 115 | X, Y = np.meshgrid(np.linspace(-5, 5, 11), np.linspace(-5, 5, 21)) 116 | spacing = (0.5, 1.0) # note: dy, dx 117 | 118 | # test slope and curvature using parabola 119 | elevation = X**2 + Y**2 120 | 121 | p = TerrainDerivatives(shape=elevation.shape, spacing=spacing, elevation=elevation) 122 | 123 | expected_slope = np.sqrt((2 * X) ** 2 + (2 * Y) ** 2) 124 | expected_curvature = (2 + 4 * X**2 + 4 * Y**2) / (1 + 4 * X**2 + 4 * Y**2) ** 1.5 125 | 126 | def assert_skip_bounds(actual, expected): 127 | np.testing.assert_allclose(actual[1:-1, 1:-1], expected[1:-1, 1:-1]) 128 | 129 | # note: slope values in degrees, skip boundaries 130 | actual_slope = np.tan(np.radians(p._slope())) 131 | assert_skip_bounds(actual_slope, expected_slope) 132 | 133 | # TODO: figure out why difference of factor 2 134 | actual_curvature = p._curvature() 135 | assert_skip_bounds(actual_curvature, expected_curvature / 2) 136 | 137 | 138 | def test_stratigraphic_horizons(): 139 | freeze_time = np.array([10.0, 20.0, 30.0]) 140 | 141 | surf_elevation = np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0]]) 142 | 143 | p = StratigraphicHorizons( 144 | freeze_time=freeze_time, 145 | surf_elevation=surf_elevation, 146 | bedrock_motion=np.array([[-0.2, -0.2, -0.2], [0.0, 0.0, 0.0]]), 147 | elevation_motion=np.full_like(surf_elevation, -0.1), 148 | ) 149 | 150 | with pytest.raises(ValueError, match=r"'freeze_time' value must be .*"): 151 | p.initialize(100) 152 | 153 | p.initialize(10.0) 154 | assert p.elevation.shape == freeze_time.shape + surf_elevation.shape 155 | np.testing.assert_equal(p.horizon, np.array([0, 1, 2])) 156 | np.testing.assert_equal(p.active, np.array([True, True, True])) 157 | 158 | p.run_step(25.0) 159 | p.finalize_step() 160 | np.testing.assert_equal(p.active, np.array([False, False, True])) 161 | np.testing.assert_equal(p.elevation[2], np.array([[0.9, 0.9, 0.9], [1.9, 1.9, 1.9]])) 162 | for i in [0, 1]: 163 | np.testing.assert_equal(p.elevation[i], np.array([[0.8, 0.8, 0.8], [1.9, 1.9, 1.9]])) 164 | -------------------------------------------------------------------------------- /doc/source/develop.rst: -------------------------------------------------------------------------------- 1 | .. _develop: 2 | 3 | Contributor Guide 4 | ================= 5 | 6 | Fastscape is an open-source project. Contributions are welcome, and they are 7 | greatly appreciated! 8 | 9 | You can contribute in many ways, e.g., by reporting bugs, submitting feedbacks, 10 | contributing to the development of the code and/or the documentation, etc. 11 | 12 | This page provides resources on how best to contribute. 13 | 14 | Issues 15 | ------ 16 | 17 | The `Github Issue Tracker`_ is the right place for reporting bugs and for 18 | discussing about development ideas. Feel free to open a new issue if you have 19 | found a bug or if you have suggestions about new features or changes. 20 | 21 | For now, as the project is still very young, it is also a good place for 22 | asking usage questions. 23 | 24 | .. _`Github Issue Tracker`: https://github.com/fastscape-lem/fastscape/issues 25 | 26 | Development environment 27 | ----------------------- 28 | 29 | If you wish to contribute to the development of the code and/or the 30 | documentation, here are a few steps for setting a development environment. 31 | 32 | Fork the repository and download the code 33 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | To further be able to submit modifications, it is preferable to start by 36 | forking the fastscape repository on GitHub_ (you need to have an account). 37 | 38 | Then clone your fork locally:: 39 | 40 | $ git clone git@github.com:your_name_here/fastscape.git 41 | 42 | Alternatively, if you don't plan to submit any modification, you can clone the 43 | original fastscape git repository:: 44 | 45 | $ git clone git@github.com:fastscape-lem/fastscape.git 46 | 47 | .. _GitHub: https://github.com 48 | 49 | Install 50 | ~~~~~~~ 51 | 52 | To install the dependencies, we recommend using the conda_ package manager with 53 | the conda-forge_ channel. For development purpose, you might consider installing 54 | the packages in a new conda environment:: 55 | 56 | $ conda create -n fastscape_dev xarray-simlab fastscapelib-f2py -c conda-forge 57 | $ source activate fastscape_dev 58 | 59 | Then install fastscape locally using ``pip``:: 60 | 61 | $ cd fastscape 62 | $ pip install -e . 63 | 64 | .. _conda: http://conda.pydata.org/docs/ 65 | .. _conda-forge: https://conda-forge.github.io/ 66 | 67 | Run tests 68 | ~~~~~~~~~ 69 | 70 | To make sure everything behaves as expected, you may want to run 71 | fastscape's unit tests locally using the `pytest`_ package. You 72 | can first install it with conda:: 73 | 74 | $ conda install pytest -c conda-forge 75 | 76 | Then you can run tests from the main fastscape directory:: 77 | 78 | $ pytest fastscape --verbose 79 | 80 | .. _pytest: https://docs.pytest.org/en/latest/ 81 | 82 | Contributing to code 83 | -------------------- 84 | 85 | Below are some useful pieces of information in case you want to contribute 86 | to the code. 87 | 88 | Local development 89 | ~~~~~~~~~~~~~~~~~ 90 | 91 | Once you have setup the development environment, the next step is to create 92 | a new git branch for local development:: 93 | 94 | $ git checkout -b name-of-your-bugfix-or-feature 95 | 96 | Now you can make your changes locally. 97 | 98 | Submit changes 99 | ~~~~~~~~~~~~~~ 100 | 101 | Once you are done with the changes, you can commit your changes to git and 102 | push your branch to your fastscape fork on GitHub:: 103 | 104 | $ git add . 105 | $ git commit -m "Your detailed description of your changes." 106 | $ git push origin name-of-your-bugfix-or-feature 107 | 108 | (note: this operation may be repeated several times). 109 | 110 | We you are ready, you can create a new pull request through the GitHub_ website 111 | (note that it is still possible to submit changes after your created a pull 112 | request). 113 | 114 | Python versions 115 | ~~~~~~~~~~~~~~~ 116 | 117 | Fastscape supports Python versions 3.9 and higher. 118 | 119 | Test 120 | ~~~~ 121 | 122 | fastscape's uses unit tests extensively to make sure that every 123 | part of the code behaves as we expect. Test coverage is required for 124 | all code contributions. 125 | 126 | Unit test are written using `pytest`_ style (i.e., mostly using the 127 | ``assert`` statement directly) in various files located in the 128 | ``xsimlab/tests`` folder. You can run tests locally from the main 129 | fastscape directory:: 130 | 131 | $ pytest fastscape --verbose 132 | 133 | All tests are also executed automatically on continuous integration 134 | platforms on every push to every pull request on GitHub. 135 | 136 | Docstrings 137 | ~~~~~~~~~~ 138 | 139 | Everything (i.e., classes, methods, functions...) that is part of the public API 140 | should follow the numpydoc_ standard when possible. 141 | 142 | .. _numpydoc: https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt 143 | 144 | Coding style 145 | ~~~~~~~~~~~~ 146 | 147 | The fastscape code mostly follows the style conventions defined in PEP8_. 148 | 149 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 150 | 151 | Source code checker 152 | ~~~~~~~~~~~~~~~~~~~ 153 | 154 | To check about any potential error or bad style in your code, you might want 155 | using a source code checker like flake8_. You can install it in your 156 | development environment:: 157 | 158 | $ conda install flake8 -c conda-forge 159 | 160 | .. _flake8: http://flake8.pycqa.org 161 | 162 | Release notes entry 163 | ~~~~~~~~~~~~~~~~~~~ 164 | 165 | Every significative code contribution should be listed in the 166 | :doc:`release_notes` section of this documentation under the 167 | corresponding version. 168 | 169 | Contributing to documentation 170 | ----------------------------- 171 | 172 | fastscape uses Sphinx_ for documentation, hosted on http://readthedocs.org . 173 | Documentation is maintained in the RestructuredText markup language (``.rst`` 174 | files) in ``fastscape/doc``. 175 | 176 | To build the documentation locally, first install requirements (for example here 177 | in a separate conda environment):: 178 | 179 | $ conda env create -n fastscape_doc -f doc/environment.yml 180 | $ source activate fastscape_doc 181 | 182 | Then build documentation with ``make``:: 183 | 184 | $ cd doc 185 | $ make html 186 | 187 | The resulting HTML files end up in the ``build/html`` directory. 188 | 189 | You can now make edits to rst files and run ``make html`` again to update 190 | the affected pages. 191 | 192 | .. _Sphinx: http://www.sphinx-doc.org/ 193 | -------------------------------------------------------------------------------- /fastscape/processes/tectonics.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .boundary import BorderBoundary 6 | from .context import FastscapelibContext 7 | from .grid import UniformRectilinearGrid2D 8 | from .main import Bedrock, SurfaceToErode, SurfaceTopography 9 | 10 | 11 | @xs.process 12 | class TectonicForcing: 13 | """Sum up all tectonic forcing processes and their effect on the 14 | vertical motion of the bedrock surface and the topographic 15 | surface, respectively. 16 | 17 | """ 18 | 19 | bedrock_forcing_vars = xs.group("bedrock_forcing_upward") 20 | surface_forcing_vars = xs.group("surface_forcing_upward") 21 | 22 | bedrock_upward = xs.variable( 23 | dims=[(), ("y", "x")], 24 | intent="out", 25 | groups="bedrock_upward", 26 | description="imposed vertical motion of bedrock surface", 27 | ) 28 | 29 | surface_upward = xs.variable( 30 | dims=[(), ("y", "x")], 31 | intent="out", 32 | groups="surface_upward", 33 | description="imposed vertical motion of topographic surface", 34 | ) 35 | 36 | grid_area = xs.foreign(UniformRectilinearGrid2D, "area") 37 | 38 | domain_rate = xs.on_demand(description="domain-integrated volumetric tectonic rate") 39 | 40 | @xs.runtime(args="step_delta") 41 | def run_step(self, dt): 42 | self._dt = dt 43 | 44 | self.bedrock_upward = sum(self.bedrock_forcing_vars) 45 | self.surface_upward = sum(self.surface_forcing_vars) 46 | 47 | @domain_rate.compute 48 | def _domain_rate(self): 49 | return np.sum(self.surface_upward) * self.grid_area / self._dt 50 | 51 | 52 | @xs.process 53 | class SurfaceAfterTectonics(SurfaceToErode): 54 | """Used for the computation erosion processes after 55 | applying tectonic forcing. 56 | 57 | """ 58 | 59 | topo_elevation = xs.foreign(SurfaceTopography, "elevation") 60 | 61 | forced_motion = xs.foreign(TectonicForcing, "surface_upward") 62 | 63 | elevation = xs.variable( 64 | dims=("y", "x"), intent="out", description="surface elevation before erosion" 65 | ) 66 | 67 | def run_step(self): 68 | self.elevation = self.topo_elevation + self.forced_motion 69 | 70 | 71 | @xs.process 72 | class BlockUplift: 73 | """Vertical tectonic block uplift. 74 | 75 | Automatically resets uplift to zero at grid borders where 76 | 'fixed_value' boundary conditions are set. 77 | 78 | """ 79 | 80 | rate = xs.variable(dims=[(), ("y", "x")], description="uplift rate") 81 | 82 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 83 | status = xs.foreign(BorderBoundary, "border_status") 84 | fs_context = xs.foreign(FastscapelibContext, "context") 85 | 86 | uplift = xs.variable( 87 | dims=[(), ("y", "x")], 88 | intent="out", 89 | groups=["bedrock_forcing_upward", "surface_forcing_upward"], 90 | description="imposed vertical uplift", 91 | ) 92 | 93 | def initialize(self): 94 | # build uplift rate binary mask according to border status 95 | self._mask = np.ones(self.shape) 96 | 97 | _all = slice(None) 98 | slices = [(_all, 0), (_all, -1), (0, _all), (-1, _all)] 99 | 100 | for status, border in zip(self.status, slices): 101 | if status == "fixed_value": 102 | self._mask[border] = 0.0 103 | 104 | @xs.runtime(args="step_delta") 105 | def run_step(self, dt): 106 | rate = np.broadcast_to(self.rate, self.shape) * self._mask 107 | 108 | self.uplift = rate * dt 109 | 110 | 111 | @xs.process 112 | class TwoBlocksUplift: 113 | """Set two blocks separated by a clip plane, with different 114 | uplift rates. 115 | 116 | """ 117 | 118 | x_position = xs.variable(description="position of the clip plane along the x-axis", static=True) 119 | 120 | rate_left = xs.variable(description="uplift rate of the left block") 121 | rate_right = xs.variable(description="uplift rate of the right block") 122 | 123 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 124 | x = xs.foreign(UniformRectilinearGrid2D, "x") 125 | 126 | uplift = xs.variable( 127 | dims=[(), ("y", "x")], 128 | intent="out", 129 | groups=["bedrock_forcing_upward", "surface_forcing_upward"], 130 | description="imposed vertical uplift", 131 | ) 132 | 133 | def initialize(self): 134 | # align clip plane position 135 | self._x_idx = np.argmax(self.x > self.x_position) 136 | 137 | @xs.runtime(args="step_delta") 138 | def run_step(self, dt): 139 | rate = np.full(self.shape, self.rate_left) 140 | 141 | rate[:, self._x_idx :] = self.rate_right 142 | 143 | self.uplift = rate * dt 144 | 145 | 146 | @xs.process 147 | class HorizontalAdvection: 148 | """Horizontal rock advection imposed by a velocity field.""" 149 | 150 | u = xs.variable(dims=[(), ("y", "x")], description="velocity field component in x-direction") 151 | v = xs.variable(dims=[(), ("y", "x")], description="velocity field component in y-direction") 152 | 153 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 154 | fs_context = xs.foreign(FastscapelibContext, "context") 155 | 156 | bedrock_elevation = xs.foreign(Bedrock, "elevation") 157 | surface_elevation = xs.foreign(SurfaceTopography, "elevation") 158 | 159 | bedrock_veffect = xs.variable( 160 | dims=("y", "x"), 161 | intent="out", 162 | groups="bedrock_forcing_upward", 163 | description="vertical effect of advection on bedrock surface", 164 | ) 165 | 166 | surface_veffect = xs.variable( 167 | dims=("y", "x"), 168 | intent="out", 169 | groups="surface_forcing_upward", 170 | description="vertical effect of advection on topographic surface", 171 | ) 172 | 173 | def run_step(self): 174 | self.fs_context["vx"] = np.broadcast_to(self.u, self.shape).flatten() 175 | self.fs_context["vy"] = np.broadcast_to(self.v, self.shape).flatten() 176 | 177 | # bypass fastscapelib-fortran state 178 | self.fs_context["h"] = self.surface_elevation.flatten() 179 | self.fs_context["b"] = self.bedrock_elevation.flatten() 180 | 181 | fs.advect() 182 | 183 | h_advected = self.fs_context["h"].reshape(self.shape) 184 | self.surface_veffect = h_advected - self.surface_elevation 185 | 186 | b_advected = self.fs_context["b"].reshape(self.shape) 187 | self.bedrock_veffect = b_advected - self.bedrock_elevation 188 | -------------------------------------------------------------------------------- /fastscape/processes/channel.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .context import FastscapelibContext 6 | from .flow import FlowAccumulator, FlowRouter 7 | from .grid import UniformRectilinearGrid2D 8 | from .main import UniformSedimentLayer 9 | 10 | 11 | @xs.process 12 | class ChannelErosion: 13 | """Base class for continental channel erosion and/or deposition. 14 | 15 | Do not use this base class directly in a model! Use one of its 16 | subclasses instead. 17 | 18 | However, if you need one or several of the variables declared here 19 | in another process, it is preferable to pass this base class in 20 | :func:`xsimlab.foreign`. 21 | 22 | """ 23 | 24 | erosion = xs.variable( 25 | dims=("y", "x"), 26 | intent="out", 27 | groups="erosion", 28 | description="channel erosion and/or deposition", 29 | ) 30 | 31 | 32 | @xs.process 33 | class StreamPowerChannel(ChannelErosion): 34 | """Stream-Power channel erosion.""" 35 | 36 | k_coef = xs.variable(dims=[(), ("y", "x")], description="bedrock channel incision coefficient") 37 | area_exp = xs.variable(default=0.4, description="drainage area exponent") 38 | slope_exp = xs.variable(default=1, description="slope exponent") 39 | 40 | tol_rel = xs.variable(default=1e-4, description="relative tolerance (Gauss-Siedel convergence)") 41 | tol_abs = xs.variable(default=1e-4, description="absolute tolerance (Gauss-Siedel convergence)") 42 | max_iter = xs.variable( 43 | default=100, description="max nb. of iterations (Gauss-Siedel convergence)" 44 | ) 45 | 46 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 47 | elevation = xs.foreign(FlowRouter, "elevation") 48 | receivers = xs.foreign(FlowRouter, "receivers") 49 | flowacc = xs.foreign(FlowAccumulator, "flowacc") 50 | fs_context = xs.foreign(FastscapelibContext, "context") 51 | 52 | chi = xs.on_demand(dims=("y", "x"), description="integrated drainage area (chi)") 53 | 54 | def _set_g_in_context(self): 55 | # transport/deposition feature is exposed in subclasses 56 | self.fs_context["g1"] = 0.0 57 | self.fs_context["g2"] = 0.0 58 | 59 | def _set_tolerance(self): 60 | self.fs_context["tol_rel"] = self.tol_rel 61 | self.fs_context["tol_abs"] = self.tol_abs 62 | self.fs_context["nGSStreamPowerLawMax"] = self.max_iter 63 | 64 | def run_step(self): 65 | kf = np.broadcast_to(self.k_coef, self.shape).flatten() 66 | self.fs_context["kf"] = kf 67 | 68 | # we don't use kfsed fastscapelib-fortran feature directly 69 | self.fs_context["kfsed"] = -1.0 70 | 71 | self._set_g_in_context() 72 | 73 | self.fs_context["m"] = self.area_exp 74 | self.fs_context["n"] = self.slope_exp 75 | 76 | # note: this is ignored in fastscapelib_fortran <2.8.3 !! 77 | self._set_tolerance() 78 | 79 | # bypass fastscapelib_fortran global state 80 | self.fs_context["h"] = self.elevation.flatten() 81 | self.fs_context["a"] = self.flowacc.flatten() 82 | 83 | if self.receivers.ndim == 1: 84 | fs.streampowerlawsingleflowdirection() 85 | else: 86 | fs.streampowerlaw() 87 | 88 | erosion_flat = self.elevation.ravel() - self.fs_context["h"] 89 | self.erosion = erosion_flat.reshape(self.shape) 90 | 91 | @chi.compute 92 | def _chi(self): 93 | chi_arr = np.empty_like(self.elevation, dtype="d") 94 | self.fs_context["copychi"](chi_arr.ravel()) 95 | 96 | return chi_arr 97 | 98 | 99 | @xs.process 100 | class DifferentialStreamPowerChannel(StreamPowerChannel): 101 | """Stream-Power channel (differential) erosion. 102 | 103 | Channel incision coefficient may vary depending on whether the 104 | topographic surface is bare rock or covered by a soil (sediment) 105 | layer. 106 | 107 | """ 108 | 109 | k_coef_bedrock = xs.variable( 110 | dims=[(), ("y", "x")], description="bedrock channel incision coefficient" 111 | ) 112 | k_coef_soil = xs.variable( 113 | dims=[(), ("y", "x")], description="soil (sediment) channel incision coefficient" 114 | ) 115 | 116 | k_coef = xs.variable( 117 | dims=("y", "x"), intent="out", description="differential channel incision coefficient" 118 | ) 119 | 120 | active_layer_thickness = xs.foreign(UniformSedimentLayer, "thickness") 121 | 122 | def run_step(self): 123 | self.k_coef = np.where( 124 | self.active_layer_thickness <= 0.0, self.k_coef_bedrock, self.k_coef_soil 125 | ) 126 | 127 | super().run_step() 128 | 129 | 130 | @xs.process 131 | class StreamPowerChannelTD(StreamPowerChannel): 132 | """Extended stream power channel erosion, transport and deposition.""" 133 | 134 | # TODO: https://github.com/fastscape-lem/fastscapelib-fortran/pull/25 135 | # - update input var dimensions 136 | # - set self.get_context.g instead of g1 and g2 137 | 138 | g_coef = xs.variable( 139 | # dims=[(), ('y', 'x')], 140 | description="detached bedrock transport/deposition coefficient" 141 | ) 142 | 143 | def _set_g_in_context(self): 144 | # TODO: set g instead 145 | self.fs_context["g1"] = self.g_coef 146 | self.fs_context["g2"] = -1.0 147 | 148 | 149 | @xs.process 150 | class DifferentialStreamPowerChannelTD(DifferentialStreamPowerChannel): 151 | """Extended stream power channel (differential) erosion, transport and 152 | deposition. 153 | 154 | Both channel incision and transport/deposition coefficients may 155 | vary depending on whether the topographic surface is bare rock or 156 | covered by a soil (sediment) layer. 157 | 158 | """ 159 | 160 | # TODO: https://github.com/fastscape-lem/fastscapelib-fortran/pull/25 161 | # - update input var dimensions 162 | # - set self.get_context.g instead of g1 and g2 163 | 164 | g_coef_bedrock = xs.variable( 165 | # dims=[(), ('y', 'x')], 166 | description="detached bedrock transport/deposition coefficient" 167 | ) 168 | 169 | g_coef_soil = xs.variable( 170 | # dims=[(), ('y', 'x')], 171 | description="soil (sediment) transport/deposition coefficient" 172 | ) 173 | 174 | g_coef = xs.variable( 175 | dims=("y", "x"), intent="out", description="differential transport/deposition coefficient" 176 | ) 177 | 178 | def _set_g_in_context(self): 179 | # TODO: set g instead 180 | self.fs_context["g1"] = self.g_coef_bedrock 181 | self.fs_context["g2"] = self.g_coef_soil 182 | 183 | def run_step(self): 184 | self.g_coef = np.where( 185 | self.active_layer_thickness <= 0.0, self.g_coef_bedrock, self.g_coef_soil 186 | ) 187 | 188 | super().run_step() 189 | -------------------------------------------------------------------------------- /fastscape/processes/flow.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numba 3 | import numpy as np 4 | import xsimlab as xs 5 | 6 | from .context import FastscapelibContext 7 | from .grid import UniformRectilinearGrid2D 8 | from .main import SurfaceToErode 9 | 10 | 11 | @xs.process 12 | class FlowRouter: 13 | """Base process class to route flow on the topographic surface. 14 | 15 | Do not use this base class directly in a model! Use one of its 16 | subclasses instead. 17 | 18 | However, if you need one or several of the variables declared here 19 | in another process, it is preferable to pass this base class in 20 | :func:`xsimlab.foreign`. 21 | 22 | """ 23 | 24 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 25 | elevation = xs.foreign(SurfaceToErode, "elevation") 26 | fs_context = xs.foreign(FastscapelibContext, "context") 27 | 28 | stack = xs.variable(dims="node", intent="out", description="DFS ordered grid node indices") 29 | nb_receivers = xs.variable(dims="node", intent="out", description="number of flow receivers") 30 | receivers = xs.variable( 31 | dims=["node", ("node", "nb_rec_max")], 32 | intent="out", 33 | description="flow receiver node indices", 34 | ) 35 | lengths = xs.variable( 36 | dims=["node", ("node", "nb_rec_max")], intent="out", description="out flow path length" 37 | ) 38 | weights = xs.variable( 39 | dims=["node", ("node", "nb_rec_max")], intent="out", description="flow partition weights" 40 | ) 41 | nb_donors = xs.variable(dims="node", intent="out", description="number of flow donors") 42 | donors = xs.variable( 43 | dims=("node", "nb_don_max"), intent="out", description="flow donors node indices" 44 | ) 45 | 46 | basin = xs.on_demand(dims=("y", "x"), description="river catchments") 47 | lake_depth = xs.on_demand(dims=("y", "x"), description="lake depth") 48 | 49 | def route_flow(self): 50 | # must be implemented in sub-classes 51 | pass 52 | 53 | def run_step(self): 54 | # bypass fastscapelib_fortran global state 55 | self.fs_context["h"] = self.elevation.ravel() 56 | 57 | self.route_flow() 58 | 59 | self.nb_donors = self.fs_context["ndon"].astype("int") 60 | # Fortran 1 vs Python 0 index 61 | self.donors = self.fs_context["don"].astype("int").transpose() - 1 62 | 63 | @basin.compute 64 | def _basin(self): 65 | catch = self.fs_context["catch"].reshape(self.shape) 66 | 67 | # storing basin ids as integers is safer 68 | return (catch * catch.size).astype("int") 69 | 70 | @lake_depth.compute 71 | def _lake_depth(self): 72 | return self.fs_context["lake_depth"].reshape(self.shape).copy() 73 | 74 | 75 | @xs.process 76 | class SingleFlowRouter(FlowRouter): 77 | """Single direction (convergent) flow router.""" 78 | 79 | slope = xs.on_demand(dims="node", description="out flow path slope") 80 | 81 | def initialize(self): 82 | # for compatibility 83 | self.nb_receivers = np.ones_like(self.fs_context["rec"]) 84 | self.weights = np.ones_like(self.fs_context["length"]) 85 | 86 | def route_flow(self): 87 | fs.flowroutingsingleflowdirection() 88 | 89 | # Fortran 1 vs Python 0 index 90 | self.stack = self.fs_context["stack"].astype("int") - 1 91 | self.receivers = self.fs_context["rec"] - 1 92 | self.lengths = self.fs_context["length"] 93 | 94 | @slope.compute 95 | def _slope(self): 96 | elev_flat = self.elevation.ravel() 97 | elev_flat_diff = elev_flat - elev_flat[self.receivers] 98 | 99 | # skip base levels 100 | slope = np.zeros_like(self.lengths) 101 | idx = np.argwhere(self.lengths > 0) 102 | 103 | slope[idx] = (elev_flat_diff[idx] / self.lengths[idx],) 104 | 105 | return slope 106 | 107 | 108 | @xs.process 109 | class MultipleFlowRouter(FlowRouter): 110 | """Multiple direction (convergent/divergent) flow router with uniform 111 | slope exponent. 112 | 113 | By default, the slope exponent equals zero, i.e., the amount of flow is 114 | distributed evenly among the flow receivers. 115 | 116 | """ 117 | 118 | slope_exp = xs.variable( 119 | dims=[(), ("y", "x")], default=0.0, description="MFD partioner slope exponent", static=True 120 | ) 121 | 122 | def initialize(self): 123 | self.fs_context["p_mfd_exp"] = np.broadcast_to(self.slope_exp, self.shape).flatten() 124 | 125 | def route_flow(self): 126 | fs.flowrouting() 127 | 128 | # Fortran 1 vs Python 0 index | Fortran col vs Python row layout 129 | self.stack = self.fs_context["mstack"].astype("int") - 1 130 | self.nb_receivers = self.fs_context["mnrec"].astype("int") 131 | self.receivers = self.fs_context["mrec"].astype("int").transpose() - 1 132 | self.lengths = self.fs_context["mlrec"].transpose() 133 | self.weights = self.fs_context["mwrec"].transpose() 134 | 135 | 136 | # TODO: remove when possible to use fastscapelib-fortran 137 | # see https://github.com/fastscape-lem/fastscapelib-fortran/issues/24 138 | @numba.njit 139 | def _flow_accumulate_sd(field, stack, receivers): 140 | for inode in stack[-1::-1]: 141 | if receivers[inode] != inode: 142 | field[receivers[inode]] += field[inode] 143 | 144 | 145 | @numba.njit 146 | def _flow_accumulate_mfd(field, stack, nb_receivers, receivers, weights): 147 | for inode in stack: 148 | if nb_receivers[inode] == 1 and receivers[inode, 0] == inode: 149 | continue 150 | 151 | for k in range(nb_receivers[inode]): 152 | irec = receivers[inode, k] 153 | field[irec] += field[inode] * weights[inode, k] 154 | 155 | 156 | @xs.process 157 | class FlowAccumulator: 158 | """Accumulate the flow from upstream to downstream.""" 159 | 160 | runoff = xs.variable( 161 | dims=[(), ("y", "x")], description="surface runoff (source term) per area unit" 162 | ) 163 | 164 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 165 | cell_area = xs.foreign(UniformRectilinearGrid2D, "cell_area") 166 | stack = xs.foreign(FlowRouter, "stack") 167 | nb_receivers = xs.foreign(FlowRouter, "nb_receivers") 168 | receivers = xs.foreign(FlowRouter, "receivers") 169 | weights = xs.foreign(FlowRouter, "weights") 170 | 171 | flowacc = xs.variable( 172 | dims=("y", "x"), intent="out", description="flow accumulation from up to downstream" 173 | ) 174 | 175 | def run_step(self): 176 | field = np.broadcast_to(self.runoff * self.cell_area, self.shape).flatten() 177 | 178 | if self.receivers.ndim == 1: 179 | _flow_accumulate_sd(field, self.stack, self.receivers) 180 | 181 | else: 182 | _flow_accumulate_mfd(field, self.stack, self.nb_receivers, self.receivers, self.weights) 183 | 184 | self.flowacc = field.reshape(self.shape) 185 | 186 | 187 | @xs.process 188 | class DrainageArea(FlowAccumulator): 189 | """Upstream contributing area.""" 190 | 191 | runoff = xs.variable(dims=[(), ("y", "x")], intent="out") 192 | 193 | # alias of flowacc, for convenience 194 | area = xs.variable(dims=("y", "x"), intent="out", description="drainage area") 195 | 196 | def initialize(self): 197 | self.runoff = 1 198 | 199 | def run_step(self): 200 | super().run_step() 201 | 202 | self.area = self.flowacc 203 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # fastscape documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Jun 19 14:43:39 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | import sys 20 | import os 21 | 22 | print("python exec:", sys.executable) 23 | print("sys.path:", sys.path) 24 | try: 25 | import xsimlab 26 | 27 | print("xsimlab: %s, %s" % (xsimlab.__version__, xsimlab.__file__)) 28 | except ImportError: 29 | print("no xarray-simlab") 30 | try: 31 | import fastscapelib_fortran 32 | 33 | print("fastscapelib_fortran: %s" % (fastscapelib_fortran.__file__)) 34 | except ImportError: 35 | print("no fastscapelib_fortran") 36 | 37 | import fastscape 38 | 39 | print("fastscape: %s, %s" % (fastscape.__version__, fastscape.__file__)) 40 | 41 | # -- General configuration ------------------------------------------------ 42 | 43 | # If your documentation needs a minimal Sphinx version, state it here. 44 | # 45 | # needs_sphinx = '1.0' 46 | 47 | # Add any Sphinx extension module names here, as strings. They can be 48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 49 | # ones. 50 | extensions = [ 51 | "sphinx.ext.autodoc", 52 | "sphinx.ext.autosummary", 53 | "sphinx.ext.intersphinx", 54 | "sphinx.ext.mathjax", 55 | "sphinx.ext.extlinks", 56 | "sphinx.ext.napoleon", 57 | # 'numpydoc', 58 | "IPython.sphinxext.ipython_directive", 59 | "IPython.sphinxext.ipython_console_highlighting", 60 | ] 61 | 62 | autodoc_default_flags = [ 63 | # Make sure that any autodoc declarations show the right members 64 | "members", 65 | "inherited-members", 66 | "show-inheritance", 67 | ] 68 | 69 | autosummary_generate = True 70 | 71 | napoleon_numpy_docstring = True 72 | # numpydoc_class_members_toctree = True 73 | # numpydoc_show_class_members = False 74 | 75 | # Add any paths that contain templates here, relative to this directory. 76 | templates_path = ["_templates"] 77 | 78 | # The suffix(es) of source filenames. 79 | # You can specify multiple suffix as a list of string: 80 | # 81 | # source_suffix = ['.rst', '.md'] 82 | source_suffix = ".rst" 83 | 84 | # The master toctree document. 85 | master_doc = "index" 86 | 87 | # General information about the project. 88 | project = "fastscape" 89 | copyright = "2019, fastscape Developers" 90 | author = "Benoit Bovy and fastscape Developers" 91 | 92 | # The version info for the project you're documenting, acts as replacement for 93 | # |version| and |release|, also used in various other places throughout the 94 | # built documents. 95 | # 96 | # The short X.Y version. 97 | version = fastscape.__version__.split("+")[0] 98 | # The full version, including alpha/beta/rc tags. 99 | release = fastscape.__version__ 100 | 101 | # The language for content autogenerated by Sphinx. Refer to documentation 102 | # for a list of supported languages. 103 | # 104 | # This is also used if you do content translation via gettext catalogs. 105 | # Usually you set "language" from the command line for these cases. 106 | language = None 107 | 108 | # List of patterns, relative to source directory, that match files and 109 | # directories to ignore when looking for source files. 110 | # This patterns also effect to html_static_path and html_extra_path 111 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] 112 | 113 | # The name of the Pygments (syntax highlighting) style to use. 114 | pygments_style = "sphinx" 115 | 116 | # If true, `todo` and `todoList` produce output, else they produce nothing. 117 | todo_include_todos = False 118 | 119 | 120 | # -- Options for HTML output ---------------------------------------------- 121 | 122 | # The theme to use for HTML and HTML Help pages. See the documentation for 123 | # a list of builtin themes. 124 | # 125 | html_theme = "sphinx_rtd_theme" 126 | 127 | # Theme options are theme-specific and customize the look and feel of a theme 128 | # further. For a list of options available for each theme, see the 129 | # documentation. 130 | # 131 | html_theme_options = {"logo_only": True} 132 | 133 | # Additional css files 134 | html_css_files = [ 135 | "style.css", 136 | ] 137 | 138 | # The name of an image file (relative to this directory) to place at the top 139 | # of the sidebar. 140 | html_logo = "_static/fastscape_logo_midres.png" 141 | 142 | # The name of an image file (within the static path) to use as favicon of the 143 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 144 | # pixels large. 145 | html_favicon = "_static/favicon.ico" 146 | 147 | # Add any paths that contain custom static files (such as style sheets) here, 148 | # relative to this directory. They are copied after the builtin static files, 149 | # so a file named "default.css" will overwrite the builtin "default.css". 150 | html_static_path = ["_static"] 151 | 152 | 153 | # -- Options for HTMLHelp output ------------------------------------------ 154 | 155 | # Output file base name for HTML help builder. 156 | htmlhelp_basename = "fastscape-doc" 157 | 158 | 159 | # -- Options for LaTeX output --------------------------------------------- 160 | 161 | latex_elements = { 162 | # The paper size ('letterpaper' or 'a4paper'). 163 | # 164 | # 'papersize': 'letterpaper', 165 | # The font size ('10pt', '11pt' or '12pt'). 166 | # 167 | # 'pointsize': '10pt', 168 | # Additional stuff for the LaTeX preamble. 169 | # 170 | # 'preamble': '', 171 | # Latex figure (float) alignment 172 | # 173 | # 'figure_align': 'htbp', 174 | } 175 | 176 | # Grouping the document tree into LaTeX files. List of tuples 177 | # (source start file, target name, title, 178 | # author, documentclass [howto, manual, or own class]). 179 | latex_documents = [ 180 | (master_doc, "fastscape.tex", "fastscape Documentation", "fastscape Developers", "manual"), 181 | ] 182 | 183 | 184 | # -- Options for manual page output --------------------------------------- 185 | 186 | # One entry per manual page. List of tuples 187 | # (source start file, name, description, authors, manual section). 188 | man_pages = [(master_doc, "fastscape", "fastscape Documentation", [author], 1)] 189 | 190 | 191 | # -- Options for Texinfo output ------------------------------------------- 192 | 193 | # Grouping the document tree into Texinfo files. List of tuples 194 | # (source start file, target name, title, author, 195 | # dir menu entry, description, category) 196 | texinfo_documents = [ 197 | ( 198 | master_doc, 199 | "fastscape", 200 | "fastscape Documentation", 201 | author, 202 | "fastscape", 203 | "One line description of project.", 204 | "Miscellaneous", 205 | ), 206 | ] 207 | 208 | # Example configuration for intersphinx: refer to the Python standard library. 209 | intersphinx_mapping = { 210 | "python": ("https://docs.python.org/3.7/", None), 211 | #'numpy': ('https://docs.scipy.org/doc/numpy/', None), 212 | #'xarray': ('http://xarray.pydata.org/en/stable/', None), 213 | "xsimlab": ("https://xarray-simlab.readthedocs.io/en/latest/", None), 214 | } 215 | -------------------------------------------------------------------------------- /fastscape/processes/main.py: -------------------------------------------------------------------------------- 1 | import fastscapelib_fortran as fs 2 | import numpy as np 3 | import xsimlab as xs 4 | 5 | from .grid import UniformRectilinearGrid2D 6 | 7 | 8 | @xs.process 9 | class TotalVerticalMotion: 10 | """Sum up all vertical motions of bedrock and topographic surface, 11 | respectively. 12 | 13 | Vertical motions may result from external forcing, erosion and/or 14 | feedback of erosion on tectonics (isostasy). 15 | 16 | """ 17 | 18 | bedrock_upward_vars = xs.group("bedrock_upward") 19 | surface_upward_vars = xs.group("surface_upward") 20 | surface_downward_vars = xs.group("surface_downward") 21 | 22 | bedrock_upward = xs.variable( 23 | dims=("y", "x"), intent="out", description="bedrock motion in upward direction" 24 | ) 25 | surface_upward = xs.variable( 26 | dims=("y", "x"), intent="out", description="topographic surface motion in upward direction" 27 | ) 28 | 29 | def run_step(self): 30 | self.bedrock_upward = sum(self.bedrock_upward_vars) 31 | 32 | self.surface_upward = sum(self.surface_upward_vars) - sum(self.surface_downward_vars) 33 | 34 | 35 | @xs.process 36 | class SurfaceTopography: 37 | """Update the elevation of the (land and/or submarine) surface 38 | topography. 39 | 40 | """ 41 | 42 | elevation = xs.variable( 43 | dims=("y", "x"), intent="inout", description="surface topography elevation" 44 | ) 45 | 46 | motion_upward = xs.foreign(TotalVerticalMotion, "surface_upward") 47 | 48 | def finalize_step(self): 49 | self.elevation += self.motion_upward 50 | 51 | 52 | @xs.process 53 | class SurfaceToErode: 54 | """Defines the topographic surface used for the computation of erosion 55 | processes. 56 | 57 | In this process class, it simply corresponds to the topographic 58 | surface, unchanged, at the current time step. 59 | 60 | Sometimes it would make sense to compute erosion processes after 61 | having applied other processes such as tectonic forcing. This 62 | could be achieved by subclassing. 63 | 64 | """ 65 | 66 | topo_elevation = xs.foreign(SurfaceTopography, "elevation") 67 | 68 | elevation = xs.variable( 69 | dims=("y", "x"), intent="out", description="surface elevation before erosion" 70 | ) 71 | 72 | def run_step(self): 73 | self.elevation = self.topo_elevation 74 | 75 | 76 | @xs.process 77 | class Bedrock: 78 | """Update the elevation of bedrock (i.e., land and/or submarine 79 | basement). 80 | 81 | """ 82 | 83 | elevation = xs.variable(dims=("y", "x"), intent="inout", description="bedrock elevation") 84 | 85 | depth = xs.on_demand(dims=("y", "x"), description="bedrock depth below topographic surface") 86 | 87 | bedrock_motion_up = xs.foreign(TotalVerticalMotion, "bedrock_upward") 88 | surface_motion_up = xs.foreign(TotalVerticalMotion, "surface_upward") 89 | 90 | surface_elevation = xs.foreign(SurfaceTopography, "elevation") 91 | 92 | @depth.compute 93 | def _depth(self): 94 | return self.surface_elevation - self.elevation 95 | 96 | def initialize(self): 97 | if np.any(self.elevation > self.surface_elevation): 98 | raise ValueError( 99 | "Encountered bedrock elevation higher than " "topographic surface elevation." 100 | ) 101 | 102 | def run_step(self): 103 | self._elevation_next = np.minimum( 104 | self.elevation + self.bedrock_motion_up, self.surface_elevation + self.surface_motion_up 105 | ) 106 | 107 | def finalize_step(self): 108 | self.elevation = self._elevation_next 109 | 110 | 111 | @xs.process 112 | class UniformSedimentLayer: 113 | """Uniform sediment (or regolith, or soil) layer. 114 | 115 | This layer has uniform properties (undefined in this class) and 116 | generally undergo under active erosion, transport and deposition 117 | processes. 118 | 119 | """ 120 | 121 | surf_elevation = xs.foreign(SurfaceTopography, "elevation") 122 | bedrock_elevation = xs.foreign(Bedrock, "elevation") 123 | 124 | thickness = xs.variable(dims=("y", "x"), intent="out", description="sediment layer thickness") 125 | 126 | @thickness.compute 127 | def _get_thickness(self): 128 | return self.surf_elevation - self.bedrock_elevation 129 | 130 | def initialize(self): 131 | self.thickness = self._get_thickness() 132 | 133 | def run_step(self): 134 | self.thickness = self._get_thickness() 135 | 136 | 137 | @xs.process 138 | class TerrainDerivatives: 139 | """Compute, on demand, terrain derivatives such as slope or 140 | curvature. 141 | 142 | """ 143 | 144 | shape = xs.foreign(UniformRectilinearGrid2D, "shape") 145 | spacing = xs.foreign(UniformRectilinearGrid2D, "spacing") 146 | elevation = xs.foreign(SurfaceTopography, "elevation") 147 | 148 | slope = xs.on_demand(dims=("y", "x"), description="terrain local slope") 149 | curvature = xs.on_demand(dims=("y", "x"), description="terrain local curvature") 150 | 151 | @slope.compute 152 | def _slope(self): 153 | slope = np.empty_like(self.elevation) 154 | ny, nx = self.shape 155 | dy, dx = self.spacing 156 | 157 | fs.slope(self.elevation.ravel(), slope.ravel(), nx, ny, dx, dy) 158 | 159 | return slope 160 | 161 | @curvature.compute 162 | def _curvature(self): 163 | curv = np.empty_like(self.elevation) 164 | ny, nx = self.shape 165 | dy, dx = self.spacing 166 | 167 | fs.curvature(self.elevation.ravel(), curv.ravel(), nx, ny, dx, dy) 168 | 169 | return curv 170 | 171 | 172 | @xs.process 173 | class StratigraphicHorizons: 174 | """Generate a fixed number of stratigraphic horizons. 175 | 176 | A horizon is active, i.e., it tracks the evolution of the 177 | land/submarine topographic surface until it is "frozen" at a given 178 | time. Beyond this freezing (or deactivation) time, the horizon 179 | will only be affected by tectonic deformation and/or erosion. 180 | 181 | To compute diagnostics on those horizons, you can create a 182 | subclass where you can add "on_demand" variables. 183 | 184 | """ 185 | 186 | freeze_time = xs.variable( 187 | dims="horizon", description="horizon freezing (deactivation) time", static=True 188 | ) 189 | 190 | horizon = xs.index(dims="horizon", description="horizon number") 191 | 192 | active = xs.variable( 193 | dims="horizon", intent="out", description="whether the horizon is active or not" 194 | ) 195 | 196 | surf_elevation = xs.foreign(SurfaceTopography, "elevation") 197 | elevation_motion = xs.foreign(TotalVerticalMotion, "surface_upward") 198 | bedrock_motion = xs.foreign(TotalVerticalMotion, "bedrock_upward") 199 | 200 | elevation = xs.variable( 201 | dims=("horizon", "y", "x"), intent="out", description="elevation of horizon surfaces" 202 | ) 203 | 204 | @xs.runtime(args="sim_start") 205 | def initialize(self, start_time): 206 | if np.any(self.freeze_time < start_time): 207 | raise ValueError( 208 | "'freeze_time' value must be greater than the " 209 | "time of the beginning of the simulation" 210 | ) 211 | 212 | self.elevation = np.repeat(self.surf_elevation[None, :, :], self.freeze_time.size, axis=0) 213 | 214 | self.horizon = np.arange(0, len(self.freeze_time)) 215 | 216 | self.active = np.full_like(self.freeze_time, True, dtype=bool) 217 | 218 | @xs.runtime(args="step_start") 219 | def run_step(self, current_time): 220 | self.active = current_time < self.freeze_time 221 | 222 | def finalize_step(self): 223 | elevation_next = self.surf_elevation + self.elevation_motion 224 | 225 | self.elevation[self.active] = elevation_next 226 | 227 | self.elevation[~self.active] = np.minimum( 228 | self.elevation[~self.active] + self.bedrock_motion, elevation_next 229 | ) 230 | --------------------------------------------------------------------------------