Source code for sed.sedplex

import os
import gc
import sys
import petsc4py
import numpy as np
import numpy_indexed as npi

from mpi4py import MPI
from time import process_time

if "READTHEDOCS" not in os.environ:
    from gospl._fortran import setmaxnb
    from gospl._fortran import sethillslopecoeff
    from gospl._fortran import scale_volume

petsc4py.init(sys.argv)
MPIrank = petsc4py.PETSc.COMM_WORLD.Get_rank()
MPIcomm = petsc4py.PETSc.COMM_WORLD


[docs] class SEDMesh(object): """ This class encapsulates all the functions related to sediment transport, production and deposition in the continental domain. The following processes are considered: - inland river deposition in depressions and enclosed sea - hillslope processes in both marine and inland areas .. note:: All these functions are run in parallel using the underlying PETSc library. """ def __init__(self, *args, **kwargs): """ The initialisation of `SEDMesh` class consists in the declaration of several PETSc vectors. """ # Petsc vectors self.rhs = self.hGlobal.duplicate() self.tmp = self.hGlobal.duplicate() self.tmpL = self.hLocal.duplicate() self.tmp1 = self.hGlobal.duplicate() self.Qs = self.hGlobal.duplicate() self.QsL = self.hLocal.duplicate() self.nQs = self.hLocal.duplicate() self.vSed = self.hGlobal.duplicate() self.vSedLocal = self.hLocal.duplicate() # Get the maximum number of neighbours on the mesh maxnb = np.zeros(1, dtype=np.int64) maxnb[0] = setmaxnb(self.lpoints) MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, maxnb, op=MPI.MAX) self.maxnb = maxnb[0] return
[docs] def getSedFlux(self): """ This function computes sediment flux in cubic metres per year from incoming rivers. Like for the computation of the flow discharge and erosion rates, the sediment flux is solved by an implicit time integration method, the matrix system is the one obtained from the receiver distributions over the unfilled elevation mesh for the flow discharge (`fMat`). The PETSc *scalable linear equations solvers* (**KSP**) is used here again with an iterative method obtained from PETSc Richardson solver (`richardson`) with block Jacobian preconditioning (`bjacobi`). """ t0 = process_time() # Stratigraphic layers exist if self.stratNb > 0: # Get erosion rate (m/yr) to volume self.tmpL.setArray(self.thCoarse) self.dm.localToGlobal(self.tmpL, self.tmp) else: # Get erosion rate (m/yr) to volume self.Eb.copy(result=self.tmp) # Get the volume of sediment transported in m3 per year self.tmp.pointwiseMult(self.tmp, self.areaGlobal) self._solve_KSP(False, self.fMati, self.tmp, self.vSed) self.fMati.destroy() # Update local vector self.dm.globalToLocal(self.vSed, self.vSedLocal) if MPIrank == 0 and self.verbose: print( "Update Sediment Load (%0.02f seconds)" % (process_time() - t0), flush=True, ) return
[docs] def _moveDownstream(self, vSed, step): """ In cases where river sediment fluxes drain into depressions, they might fill the sink completely and overspill or be deposited in it. This function computes the excess of sediment (if any) able to flow dowstream. .. important:: The excess sediment volume is then added to the downstream sediment flux (`vSed`). :arg vSed: excess sediment volume array :arg step: downstream distribution step :return: excess (boolean set to True is excess sediment fluxes remain to be distributed) """ excess = False # Remove points belonging to other processors vSed = np.multiply(vSed, self.inIDs) # Get volume incoming in each depression grp = npi.group_by(self.pitIDs[self.lsink]) uID = grp.unique _, vol = grp.sum(vSed[self.lsink]) inV = np.zeros(len(self.pitParams), dtype=np.float64) ids = uID > -1 inV[uID[ids]] = vol[ids] # Combine incoming volume globally MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, inV, op=MPI.SUM) # Get excess volume to distribute downstream eV = inV - self.pitVol if (eV > 0.0).any(): eIDs = eV > 0.0 self.pitVol[eIDs] = 0.0 spillIDs = self.pitInfo[eIDs, 0] localSpill = np.where(self.pitInfo[eIDs, 1] == MPIrank)[0] localPts = spillIDs[localSpill] nvSed = np.zeros(self.lpoints, dtype=np.float64) nvSed[localPts] = eV[eIDs][localSpill] ids = np.in1d(self.pitIDs, np.where(eV > 0.0)[0]) self.sedFilled[ids] = self.lFill[ids] # Update unfilled depressions volumes eIDs = eV < 0 self.pitVol[eIDs] -= inV[eIDs] self.pitVol[self.pitVol < 0] = 0.0 # In case there is still remaining sediment flux to distribute downstream if (eV > 1.0e-3).any(): if step == 100: self._buildFlowDirection(self.lFill) else: self._buildFlowDirection(self.sedFilled) self.tmpL.setArray(nvSed) self.dm.localToGlobal(self.tmpL, self.tmp) if self.tmp.sum() > 0.5 * self.maxarea[0]: excess = True self._solve_KSP(True, self.fMat, self.tmp, self.tmp1) self.dm.globalToLocal(self.tmp1, self.tmpL) self.fMat.destroy() return excess
[docs] def _distributeSediment(self, hl): """ This function finds the continental sediment volumes to distribute downstream (down to the ocean or sinks) for a given iteration. :arg hl: local elevation prior deposition """ t0 = process_time() # Define the volume to distribute self.vSedLocal.copy(result=self.QsL) self.dm.localToGlobal(self.QsL, self.Qs) # Get the volumetric sediment rate (m3/yr) to distribute during the time step and convert it in volume (m3) vSed = self.QsL.getArray().copy() * self.dt step = 0 excess = True self.fMat.destroy() while excess: t1 = process_time() excess = self._moveDownstream(vSed, step) if excess: vSed = self.tmpL.getArray().copy() nvSed = vSed.copy() nvSed[hl < self.sedFilled] = 0.0 self.tmpL.setArray(nvSed / self.dt) vSed[self.idBorders] = 0.0 self.vSedLocal.axpy(1.0, self.tmpL) if MPIrank == 0 and self.verbose: print( "Downstream sediment flux computation step %d (%0.02f seconds)" % (step, process_time() - t1), flush=True, ) step += 1 self.dm.localToGlobal(self.vSedLocal, self.vSed) if MPIrank == 0 and self.verbose: print( "Distribute Continental Sediments (%0.02f seconds)" % (process_time() - t0), flush=True, ) return
[docs] def _updateSinks(self, hl): """ This function updates depressions elevations based on incoming sediment volumes. The function runs until all inland sediments have reached either a sink which is not completely filled or the open ocean. :arg hl: local elevation prior deposition """ # Difference between initial volume and remaining one depo = self.pitParams[:, 0] - self.pitVol depo[depo < 0] = 0.0 with np.errstate(divide="ignore", over="ignore"): scaleV = np.divide( depo, self.pitParams[:, 0], out=np.zeros_like(self.pitParams[:, 0]), where=self.pitParams[:, 0] != 0, ) scaleV[scaleV > 1.0] = 1.0 scale = scale_volume(self.pitIDs, scaleV) # Update cumulative erosion and deposition as well as elevation self.tmpL.setArray((self.lFill - hl) * scale) self.dm.localToGlobal(self.tmpL, self.tmp) self.cumED.axpy(1.0, self.tmp) self.hGlobal.axpy(1.0, self.tmp) self.dm.globalToLocal(self.cumED, self.cumEDLocal) self.dm.globalToLocal(self.hGlobal, self.hLocal) # Update stratigraphic layer parameters if self.stratNb > 0: self.deposeStrat() self.elevStrat() # In case there is other sediment type self.pitParams[:, 0] = self.pitVol.copy() return
[docs] def sedChange(self): """ This function is the main entry point to perform both continental and marine river-induced deposition. It calls the private function: - _distributeSediment - _updateSinks """ # Find Continental Sediment Fluxes self.getSedFlux() # Compute depressions information as elevations changed due to erosion self.fillElevation(sed=True) hl = self.hLocal.getArray().copy() self.lsink = self.lsinki.copy() self.pitVol = self.pitParams[:, 0].copy() # Distribute inland sediments self.sedFilled = hl.copy() self._distributeSediment(hl) self._updateSinks(hl) return
[docs] def getHillslope(self): r""" This function computes hillslope processes. The code assumes that gravity is the main driver for transport and states that the flux of sediment is proportional to the gradient of topography. As a result, we use a linear diffusion law commonly referred to as **soil creep**: .. math:: \frac{\partial z}{\partial t}= \kappa_{D} \nabla^2 z in which :math:`\kappa_{D}` is the diffusion coefficient and can be defined with different values for the marine and land environments (set with `hillslopeKa` and `hillslopeKm` in the YAML input file). It encapsulates, in a simple formulation, processes operating on superficial sedimentary layers. Main controls on variations of :math:`\kappa_{D}` include substrate, lithology, soil depth, climate and biological activity. """ # Compute Hillslope Diffusion Law h = self.hLocal.getArray().copy() self.seaID = np.where(h <= self.sealevel)[0] self._hillSlope(smooth=0) # Update layer elevation if self.stratNb > 0: self.elevStrat() if self.memclear: del h gc.collect() # Update erosion/deposition rates self.dm.globalToLocal(self.tmp, self.tmpL) add_rate = self.tmpL.getArray() / self.dt self.tmpL.setArray(add_rate) self.EbLocal.axpy(1.0, self.tmpL) if self.memclear: del add_rate gc.collect() return
[docs] def _hillSlope(self, smooth=0): r""" This function computes hillslope using a linear diffusion law commonly referred to as **soil creep**: .. math:: \frac{\partial z}{\partial t}= \kappa_{D} \nabla^2 z in which :math:`\kappa_{D}` is the diffusion coefficient and can be defined with different values for the marine and land environments (set with `hillslopeKa` and `hillslopeKm` in the YAML input file). .. note:: The hillslope processes in `gospl` are considered to be happening at the same rate for coarse and fine sediment sizes. :arg smooth: integer specifying if the diffusion equation is used for ice flow (1) and marine deposits (2). """ if smooth == 0: if self.Cda == 0.0 and self.Cdm == 0.0: return t0 = process_time() # Diffusion matrix construction if smooth == 1: Cd = np.full(self.lpoints, self.gaussIce, dtype=np.float64) Cd[~self.iceIDs] = 0.0 elif smooth == 2: # Hard-coded coefficients here, used to generate a smooth surface # for computing marine flow directions... Cd = np.full(self.lpoints, 1.e5, dtype=np.float64) Cd[self.seaID] = 5.e6 else: Cd = np.full(self.lpoints, self.Cda, dtype=np.float64) Cd[self.seaID] = self.Cdm diffCoeffs = sethillslopecoeff(self.lpoints, Cd * self.dt) if self.flatModel: diffCoeffs[self.idBorders, 1:] = 0.0 diffCoeffs[self.idBorders, 0] = 1.0 diffMat = self._matrix_build_diag(diffCoeffs[:, 0]) indptr = np.arange(0, self.lpoints + 1, dtype=petsc4py.PETSc.IntType) for k in range(0, self.maxnb): tmpMat = self._matrix_build() indices = self.FVmesh_ngbID[:, k].copy() data = diffCoeffs[:, k + 1] ids = np.nonzero(data == 0.0) indices[ids] = ids tmpMat.assemblyBegin() tmpMat.setValuesLocalCSR( indptr, indices.astype(petsc4py.PETSc.IntType), data, ) tmpMat.assemblyEnd() diffMat.axpy(1.0, tmpMat) tmpMat.destroy() # Get elevation values for considered time step if smooth == 1: if self.tmp1.max()[1] > 0: self._solve_KSP(True, diffMat, self.tmp1, self.tmp) else: self.tmp1.copy(result=self.tmp) diffMat.destroy() self.dm.globalToLocal(self.tmp, self.tmpL) return self.tmpL.getArray().copy() elif smooth == 2: self._solve_KSP(True, diffMat, self.hGlobal, self.tmp) diffMat.destroy() self.dm.globalToLocal(self.tmp, self.tmpL) return self.tmpL.getArray().copy() else: self.hGlobal.copy(result=self.hOld) self._solve_KSP(True, diffMat, self.hOld, self.hGlobal) diffMat.destroy() # Update cumulative erosion/deposition and elevation self.tmp.waxpy(-1.0, self.hOld, self.hGlobal) self.cumED.axpy(1.0, self.tmp) self.dm.globalToLocal(self.cumED, self.cumEDLocal) self.dm.globalToLocal(self.hGlobal, self.hLocal) if self.memclear: del ids, indices, indptr, diffCoeffs, Cd gc.collect() if self.stratNb > 0: self.erodeStrat() self.deposeStrat() if MPIrank == 0 and self.verbose: print( "Compute Hillslope Processes (%0.02f seconds)" % (process_time() - t0), flush=True, ) return