Source code for tools.addprocess

import os
import gc
import sys
import petsc4py
import numpy as np
import pandas as pd

from mpi4py import MPI
from scipy import spatial
from time import process_time

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

if "READTHEDOCS" not in os.environ:
    from gflex.f2d import F2D
    from gospl._fortran import flexure
    libisoglob = True
    try:
        import isoflex
    except ModuleNotFoundError:
        libisoglob = False
        pass
    if libisoglob:
        from isoflex.model import Model as iflex

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


[docs] class GridProcess(object): """ When running goSPL on a 2D grid (*i.e.* not a global simulation), this class defines two processes operating on a regular grid: 1. **Flexural isostasy**: it allows to compute isostatic deflections of Earth's lithosphere with uniform or non-uniform flexural rigidity. Evolving surface loads are defined from erosion/deposition values associated to modelled surface processes. 2. **Orographic rain**: it accounts for change in rainfall patterns associated to change in topography. The orographic precipitation function is based on `Smith & Barstad (2004) <https://journals.ametsoc.org/view/journals/atsc/61/12/1520-0469_2004_061_1377_altoop_2.0.co_2.xml>`_ linear model. For global simulation, the library `isoFlex <https://github.com/Geodels/isoFlex>`_ provides a wrapper around `gFlex <https://github.com/awickert/gFlex>`_ to estimate global-scale flexural isostasy based on tiles distribution and projection in parallel. .. note:: Better implementation would likely provide better performance than the one proposed here for computing flexural isostasy... """ def __init__(self): """ Initialisation of the `gridProcess` class. """ self.flexIDs = None self.flex = None self.xIndices = None if self.flexOn: self.rho_water = 1030.0 self.localFlex = np.zeros(self.lpoints) if self.flex_method == 'FFT': self.boundflex = self.boundCond.replace('0', '2') self.boundflex = self.boundflex.replace('1', '0') self.boundflex = self.boundflex.replace('2', '1') self.globalfData = None if self.oroOn: self.oroEPS = np.finfo(float).eps if self.flexOn or self.oroOn: # self.flex_ngbID, self.fmaxnb = stencil(self.lpoints) # Build regular grid for flexure or orographic precipitation calculation if MPIrank == 0 and self.flex_method != 'global': self._buildRegGrid() return
[docs] def _regInterp(self, field): """ Perform bilinear interpolation of ``field`` on the regular grid to unstructured 2D mesh. :arg field: data to interpolate of size m x n :return: ufield ``field`` interpolated to unstructured nodes """ ufield = \ (1. - self.xFrac) * (1. - self.yFrac) * field[self.yIndices, self.xIndices] + \ self.xFrac * (1. - self.yFrac) * field[self.yIndices, self.xIndices + 1] + \ (1. - self.xFrac) * self.yFrac * field[self.yIndices + 1, self.xIndices] + \ self.xFrac * self.yFrac * field[self.yIndices + 1, self.xIndices + 1] return ufield
[docs] def _buildRegGrid(self): """ Builds the regular grid based on nodes coordinates and instantiates two interpolation objects. The first one uses `SciPy cKDTree <https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.html>`_ to interpolate values from the unstructured mesh onto the regular grid based on an inverse weighting distance approach. The second performs a bilinear interpolation from the regular grid to unstructured 2D mesh. .. note:: Here that the KDTree is not kept in memory, instead we store the interpolation information, namely the indices of the neighbouring nodes, and the weights of each node in the neighborhood (based on the distance). """ # Build regular grid for flexure and orographic precipitation calculation xmin = self.mCoords[:, 0].min() xmax = self.mCoords[:, 0].max() ymin = self.mCoords[:, 1].min() ymax = self.mCoords[:, 1].max() newx = np.arange(xmin, xmax + self.reg_dx, self.reg_dx) newy = np.arange(ymin, ymax + self.reg_dx, self.reg_dx) rx, ry = np.meshgrid(newx, newy) rPts = np.stack((rx.ravel(), ry.ravel())).T xmin, xmax = newx[0], newx[-1] ymin, ymax = newy[0], newy[-1] self.reg_xl = xmax - xmin self.reg_yl = ymax - ymin self.reg_nx = int(self.reg_xl / self.reg_dx + 1) self.reg_ny = int(self.reg_yl / self.reg_dx + 1) assert np.all(self.mCoords[:, 0] >= newx[0]) assert np.all(self.mCoords[:, 0] <= newx[-1]) assert np.all(self.mCoords[:, 1] >= newy[0]) assert np.all(self.mCoords[:, 1] <= newy[-1]) self.xFrac = np.interp(self.mCoords[:, 0], newx, np.arange(self.reg_nx)) self.yFrac = np.interp(self.mCoords[:, 1], newy, np.arange(self.reg_ny)) self.xIndices = np.array(self.xFrac, dtype=int) self.xFrac -= self.xIndices self.yIndices = np.array(self.yFrac, dtype=int) self.yFrac -= self.yIndices mask = self.xIndices == self.reg_nx - 1 self.xIndices[mask] -= 1 self.xFrac[mask] += 1. mask = self.yIndices == self.reg_ny - 1 self.yIndices[mask] -= 1 self.yFrac[mask] += 1. treeT = spatial.cKDTree(self.mCoords[:, :2], leafsize=10) distances, self.regIDs = treeT.query(rPts, k=self.rgrd_interp) # Inverse weighting distance... self.regWeights = np.divide( 1.0, distances ** 2, out=np.zeros_like(distances), where=distances != 0 ) self.regSumWeights = np.sum(self.regWeights, axis=1) self.regOnIDs = np.where(self.regSumWeights == 0)[0] self.regSumWeights[self.regSumWeights == 0] = 1.0e-4 del treeT, distances, mask, rPts, newx, newy gc.collect() return
[docs] def _updateTe(self): """ Finds current elastic thickness map for the considered time interval. """ nb = self.teNb if nb < len(self.tedata) - 1: if self.tedata.iloc[nb + 1, 0] <= self.tNow: nb += 1 if nb > self.teNb or nb == -1: if nb == -1: nb = 0 self.teNb = nb if self.flex_method != 'global' and self.tedata["tUni"][nb] == 0.: loadData = np.load(self.tedata.iloc[nb, 2]) teVal = loadData[self.tedata.iloc[nb, 3]] del loadData self.flexTe = np.sum(self.regWeights * teVal[self.regIDs][:, :], axis=1) / self.regSumWeights if len(self.regOnIDs) > 0: self.flexTe[self.regOnIDs] = teVal[self.regIDs[self.regOnIDs, 0]] self.flexTe = self.flexTe.reshape(self.reg_ny, self.reg_nx) if self.flex_method == 'global': loadData = np.load(self.tedata.iloc[nb, 2]) self.flexTe = loadData[self.tedata.iloc[nb, 3]] del loadData else: self.flexTe = self.tedata.iloc[nb, 1] * np.ones((self.reg_ny, self.reg_nx)) return
def _cptFlex2D(self, dZ): """ Compute the flexural response for 2D cases. """ # Build regular grid for flexure calculation if self.xIndices is None: self._buildRegGrid() # Interpolate values on the flexural regular grid regDiff = np.sum(self.regWeights * dZ[self.regIDs][:, :], axis=1) / self.regSumWeights if len(self.regOnIDs) > 0: regDiff[self.regOnIDs] = dZ[self.regIDs[self.regOnIDs, 0]] regDiff = regDiff.reshape(self.reg_ny, self.reg_nx) if self.flex_method == 'FFT': nFlex = flexure(regDiff, self.reg_ny, self.reg_nx, self.reg_yl, self.reg_xl, self.young, self.nu, self.flex_rhos, self.flex_rhoa, self.flex_eet, self.gravity, int(self.boundflex)) # Interpolate back to goSPL mesh flexZ = self._regInterp(nFlex) elif self.flex_method == 'FD': simflex = F2D() simflex.Quiet = True simflex.Method = "FD" simflex.PlateSolutionType = "vWC1994" simflex.Solver = "direct" # gFlex parameters simflex.g = self.gravity simflex.E = self.young simflex.nu = self.nu simflex.rho_m = self.flex_rhoa simflex.rho_fill = 0. simflex.dx = self.reg_dx simflex.dy = self.reg_dx # Boundary conditions simflex.BC_E = self.flex_bcE simflex.BC_W = self.flex_bcW simflex.BC_S = self.flex_bcN simflex.BC_N = self.flex_bcS # Assign elastic thickness grid if self.tedata is not None: self._updateTe() simflex.Te = self.flexTe.copy() else: simflex.Te = self.flex_eet * np.ones(regDiff.shape) # Compute loads simflex.qs = self.flex_rhos * self.gravity * regDiff # Run gFlex simflex.initialize() simflex.run() simflex.finalize() # Interpolate back to goSPL mesh flexZ = self._regInterp(simflex.w) del simflex gc.collect() return flexZ
[docs] def applyFlexure(self): r""" This function computes the flexural isostasy equilibrium based on topographic change. It is a simple routine that accounts for flexural isostatic rebound associated with erosional loading/unloading. The function takes an initial (at time t) and final topography (at time t+Dt) (i.e. before and after erosion/deposition) and returns a corrected final topography that includes the effect of erosional/depositional unloading/loading. It uses a spectral method to solve the bi-harmonic equation governing the bending/flexure of a thin elastic plate floating on an inviscid fluid (the asthenosphere). .. math:: D (d^4 w / d^4 x ) + \Delta \rho g w = q where :math:`D` is the flexural rigidity, :math:`w` is vertical deflection of the plate, :math:`q` is the applied surface load, and :math:`\Delta \rho = \rho_m − \rho_f` is the density of the mantle minus the density of the infilling material. """ t0 = process_time() # Get elevations from time of equilibrium and after erosion deposition hl = self.hLocal.getArray().copy() dZ = np.zeros(self.mpoints, dtype=np.float64) - 1.0e8 dZ[self.locIDs] = hl - self.hOldFlex.getArray() MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, dZ, op=MPI.MAX) flexZ = None if self.flex_method == 'global' and libisoglob: if self.globalfData is None: self.globalfData = np.zeros((len(self.mCoords), 5)) self.globalfData[:, :3] = self.mCoords[:, :3] if self.tedata is not None: self._updateTe() Te = self.flexTe.copy() else: Te = self.flex_eet * np.ones(len(self.mCoords)) self.globalfData[:, 3] = dZ self.globalfData[:, 4] = Te fmodel = iflex(None, None, self.globalfData, None, self.young, self.nu, self.flex_rhoa, self.flex_rhos, self.gravity, 2, self.verbose) fmodel.runFlex() if MPIrank == 0: flexZ = fmodel.simflex.copy() del fmodel gc.collect() if self.flex_method == 'global' and not libisoglob and MPIrank == 0: flexZ = np.zeros(len(self.mCoords)) if MPIrank == 0 and self.flex_method != 'global': flexZ = self._cptFlex2D(dZ) # Send flexural response globally flexZ = MPI.COMM_WORLD.bcast(flexZ, root=0) tmpFlex = flexZ[self.locIDs] if self.flex_method != 'global': # Local flexural isostasy if self.south == 1: tmpFlex[self.southPts] = 0. if self.east == 1: tmpFlex[self.eastPts] = 0. if self.north == 1: tmpFlex[self.northPts] = 0. if self.west == 1: tmpFlex[self.westPts] = 0. self.localFlex += tmpFlex # Update elevation self.hLocal.setArray(hl + tmpFlex) self.dm.localToGlobal(self.hLocal, self.hGlobal) if MPIrank == 0 and self.verbose: print( "Compute Flexural Isostasy (%0.02f seconds)" % (process_time() - t0) ) return
[docs] def cptOrography(self): """ Linear Theory of Orographic Precipitation following Smith & Barstad (2004). The model includes airflow dynamics, condensed water advection, and downslope evaporation. It consists of two vertically-integrated steady-state advection equations describing: (i) the cloud water density and (ii) the hydrometeor density. Solving these equations using Fourier transform techniques, derives a single formula relating terrain and precipitation. .. note:: Please refer to the original manuscript of Smith and Barstad (2004) to understand the model physics and limitations. Common bounds: - latitude : 0-90 [degrees] - precip_base : 0-10 [mm/h] - precip_min : 0.001 - 1 [mm/h] - conv_time : 200-2000 [s] - fall_time : 200-2000 [s] - nm : 0-0.1 [1/s] - hw : 1000-5000 [m] - cw : 0.001-0.02 [kg/m^3] - rainfall_frequency : 0.1 - 24 [number of storms of 1 hour duration per day] """ t0 = process_time() # Get elevations from the unstructured mesh structure hl = self.hLocal.getArray().copy() newZ = np.zeros(self.mpoints, dtype=np.float64) - 1.0e8 newZ[self.locIDs] = hl MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, newZ, op=MPI.MAX) if MPIrank == 0: # Build regular grid for flexure calculation if self.xIndices is None: self._buildRegGrid() # Interpolate values on the regular grid regNewZ = np.sum(self.regWeights * newZ[self.regIDs][:, :], axis=1) / self.regSumWeights if len(self.regOnIDs) > 0: regNewZ[self.regOnIDs] = newZ[self.regIDs[self.regOnIDs, 0]] regNewZ = regNewZ.reshape(self.reg_ny, self.reg_nx) # Wind components u0 = -np.sin(self.wind_dir * 2 * np.pi / 360) * self.wind_speed v0 = np.cos(self.wind_dir * 2 * np.pi / 360) * self.wind_speed # Coriolis factors f_coriolis = 2 * 7.2921e-5 * np.sin(self.wind_latitude * np.pi / 180) # Pad raster boundaries prior to FFT calc_pad = int(np.ceil(((sum(regNewZ.shape))) / 2) / 100 * 100) pad = min([calc_pad, 200]) h = np.pad(regNewZ, pad, 'constant') nx, ny = h.shape # FFT hhat = np.fft.fft2(h) x_n_value = np.fft.fftfreq(ny, (1. / ny)) y_n_value = np.fft.fftfreq(nx, (1. / nx)) x_len = nx * self.reg_dx y_len = ny * self.reg_dx kx_line = 2 * np.pi * x_n_value / x_len ky_line = 2 * np.pi * y_n_value / y_len kx = np.tile(kx_line, (nx, 1)) ky = np.tile(ky_line[:, None], (1, ny)) # Vertical wave number (m) sigma = kx * u0 + ky * v0 mf_num = self.oro_nm ** 2 - sigma ** 2 mf_den = sigma ** 2 - f_coriolis ** 2 # Numerical stability mf_num[mf_num < 0] = 0. mf_den[(mf_den < self.oroEPS) & (mf_den >= 0)] = self.oroEPS mf_den[(mf_den > -self.oroEPS) & (mf_den < 0)] = -self.oroEPS sign = np.where(sigma >= 0, 1, -1) m = sign * np.sqrt(np.abs(mf_num / mf_den * (kx ** 2 + ky ** 2))) # Transfer function P_karot = ((self.oro_cw * 1j * sigma * hhat) / ((1 - (self.oro_hw * m * 1j)) * (1 + (sigma * self.oro_conv_time * 1j)) * (1 + (sigma * self.oro_fall_time * 1j)))) # Inverse FFT, de-pad, convert units, add uniform rate oroRain = np.fft.ifft2(P_karot) oroRain = np.real(oroRain[pad:-pad, pad:-pad]) oroRain *= 3600. # mm hr-1 oroRain += self.oro_precip_base # Precipitation rate must be a value greater than minimum precipitation/runoff to avoid errors when precip_rate <= 0 oroRain[oroRain <= 0] = self.oro_precip_min # Conversion from mm/hr to m/yr oroRain *= 0.366 * self.rainfall_frequency # Interpolate back to goSPL mesh oRain = self._regInterp(oroRain) else: oRain = None # Send orographic rain globally oroR = MPI.COMM_WORLD.bcast(oRain, root=0) # Local orographic rain values self.rainVal = oroR[self.locIDs] self.bL.setArray(self.rainVal * self.larea) self.dm.localToGlobal(self.bL, self.bG) if MPIrank == 0 and self.verbose: print( "Compute Orographic Rain (%0.02f seconds)" % (process_time() - t0) ) return