import os
import gc
import sys
import petsc4py
import numpy as np
from mpi4py import MPI
from scipy import spatial
from time import process_time
# from geomstats.geometry.hypersphere import Hypersphere
if "READTHEDOCS" not in os.environ:
from gospl._fortran import adveciioe
from gospl._fortran import adveciioe2
from gospl._fortran import advecupwind
from gospl._fortran import fitedges
from gospl._fortran import getrange
from gospl._fortran import getfacevelocity
petsc4py.init(sys.argv)
MPIrank = petsc4py.PETSc.COMM_WORLD.Get_rank()
MPIsize = petsc4py.PETSc.COMM_WORLD.Get_size()
MPIcomm = MPI.COMM_WORLD
[docs]
class Tectonics(object):
"""
This class defines how 2D and spherical mesh surface is changing given a set of horizontal and vertical rates.
.. note::
Three advection approaches are proposed, a standard upwind scheme, a Inflow-Implicit/Outflow-Explicit scheme and a semi-lagrangian approach based on nearest neighbour search based on the tree built from the spherical mesh and is weighted by distance.
"""
def __init__(self):
"""
The initialisation of the `EarthPlate` class.
"""
self.hdisp = None
self.tecNb = -1
self.paleoZ = None
self.minZ = None
self.plateStep = False
self.fiso = self.hGlobal.duplicate()
return
[docs]
def getTectonics(self):
"""
Finds the current tectonic regimes (horizontal and vertical) for the considered time interval.
For horizontal displacements, the mesh variables will have to be first advected over the grid and then reinterpolated on the initial mesh coordinates.
.. note::
The approach here does not allow for mesh refinement in zones of convergence and thus can be limiting.
Yet using a fixed mesh has one main advantage: the mesh and Finite Volume discretisation do not have to be rebuilt each time the mesh is advected.
"""
if self.tecdata is None:
return
nb = self.tecNb
if nb < len(self.tecdata) - 1:
if self.tecdata.iloc[nb + 1, 0] < self.tNow + self.dt:
nb += 1
if nb > self.tecNb or nb == -1:
if nb == -1:
nb = 0
self.tecNb = nb
if nb < len(self.tecdata.index) - 1:
timer = self.tecdata.iloc[nb, 1] - self.tecdata.iloc[nb, 0]
else:
timer = self.tEnd - self.tecdata.iloc[nb, 0]
# Horizontal displacements
if self.tecdata.iloc[nb, -1] != "empty":
fname = self.tecdata.iloc[nb, -1][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.iloc[nb, -1][1]
self.hdisp = mdata[key][self.locIDs, :]
# In case of advection based on interpolation from plate position
if self.advscheme == 0:
self._readAdvectionData(mdata[key], timer)
self.plateStep = True
self.plateTimer = self.tecdata.iloc[nb, 1]
else:
# Get the velocity from the input file.
nodeVel = np.zeros((self.lpoints, 3))
if self.flatModel:
nodeVel[:, :2] = self.hdisp[:, :2]
else:
nodeVel = self.hdisp.copy()
# Store velocity on voronoi edges and dot product based on
# vecocity vector and face normals.
getfacevelocity(self.lpoints, nodeVel)
else:
self.hdisp = None
# Vertical displacements
if self.tecdata.iloc[nb, 2] != "empty":
fname = self.tecdata.iloc[nb, 2][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.iloc[nb, 2][1]
self.upsub = mdata[key][self.locIDs]
else:
self.upsub = None
# Paleo-elevation fitting
self.minZ = None
self.paleoZ = None
if self.tecdata.iloc[nb, 3] != "empty":
fname = self.tecdata.iloc[nb, 3][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.iloc[nb, 3][1]
if len(self.tecdata.iloc[nb, 3]) == 3:
self.minZ = self.tecdata.iloc[nb, 2][2]
self.paleoZ = mdata[key][self.locIDs]
del mdata
# Perform advection based on the flow-Implicit/Outflow-Explicit
# or a `first-order upwind implicitly scheme.
if self.hdisp is not None and self.advscheme > 0:
self._varAdvector()
return
def _buildAdvecMat(self, iioe, lCoeffs, rCoeffs=None):
"""
Create the advection matrix.
"""
if self.flatModel:
lCoeffs[self.idBorders, 1:] = 0.0
lCoeffs[self.idBorders, 0] = 1.0
if iioe:
rCoeffs[self.idBorders, 1:] = 0.0
rCoeffs[self.idBorders, 0] = 1.0
advMat_left = self._matrix_build_diag(lCoeffs[:, 0])
if iioe:
advMat_right = self._matrix_build_diag(rCoeffs[:, 0])
else:
advMat_right = None
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 = lCoeffs[:, k + 1]
ids = np.nonzero(data == 0.0)
indices[ids] = ids
tmpMat.assemblyBegin()
tmpMat.setValuesLocalCSR(
indptr,
indices.astype(petsc4py.PETSc.IntType),
data,
)
tmpMat.assemblyEnd()
advMat_left.axpy(1.0, tmpMat)
tmpMat.destroy()
if iioe:
tmpMat = self._matrix_build()
indices = self.FVmesh_ngbID[:, k].copy()
data = rCoeffs[:, k + 1]
ids = np.nonzero(data == 0.0)
indices[ids] = ids
tmpMat.assemblyBegin()
tmpMat.setValuesLocalCSR(
indptr,
indices.astype(petsc4py.PETSc.IntType),
data,
)
tmpMat.assemblyEnd()
advMat_right.axpy(1.0, tmpMat)
tmpMat.destroy()
if iioe:
return advMat_left, advMat_right
else:
return advMat_left
def _advectorIIOE2(self, vL, newv, vmin, vmax, nbOut):
"""
Perform the advection for the Inflow-Implicit/Outflow-Explicit Scheme 2.
"""
diffmax = newv - vmax
diffmax[diffmax < 0] = 0.
diffmin = newv - vmin
diffmin[diffmin > 0] = 0.
diff = np.abs(diffmax) + np.abs(diffmin)
self.tmpL.setArray(diff)
self.dm.localToGlobal(self.tmpL, self.tmp)
excess = self.tmp.sum()
if excess > 0.:
lCoeffs, rCoeffs = adveciioe2(self.lpoints, self.dt, nbOut, vL, vmin, vmax)
advMat_left2, advMat_right2 = self._buildAdvecMat(True, lCoeffs, rCoeffs)
advMat_right2.mult(self.hGlobal, self.tmp1)
self._solve_KSP(True, advMat_left2, self.tmp1, self.tmp)
advMat_left2.destroy()
advMat_right2.destroy()
return
[docs]
def _varAdvector(self):
"""
Perform the advection of elevation and erosion by solving the advection equation based on the finite volume space discretization and the semi-implicit discretization in time. The approach is based on a Inflow-Implicit/Outflow-Explicit scheme following the work from `Mikula & Ohlberger, 2014 <https://www.math.sk/mikula/mo-FVCA6.pdf>`_ or a `first-order upwind implicitly scheme <https://www.sciencedirect.com/science/article/pii/S0168927414001032>`_ depending on the user configuration.
.. note::
Its basic idea is that outflow from a cell is treated explicitly while inflow is treated implicitly.
Since the matrix of the system is determined by the inflow fluxes it is an M-matrix yielding favourable solvability and stability properties.
.. important::
The method allows large time steps without losing stability and not deteriorating precision.
It is formally second order accurate in space and time for 1D advection problems with variable velocity and numerical experiments indicate its second order accuracy for smooth solutions in general.
.. note::
Velocity at the face is taken to be the linear interpolation for each vertex (in a vertex-centered discretisation the dual of the delaunay triangulation (i.e. the voronoi mesh has its edges on the middle of the nodes edges)).
Similarly we consider that the advected variable at the face is defined by linear interpolation from each connected vertex.
"""
t0 = process_time()
iioe = True
if self.advscheme == 1:
iioe = False
# Advection matrix construction
if iioe:
if self.advscheme == 3:
# Minimum and maximum in a local neighborhood
hL = self.hLocal.getArray().copy()
hmin, hmax = getrange(self.lpoints, hL)
nbOut, lCoeffs, rCoeffs = adveciioe(self.lpoints, self.dt)
advMat_left, advMat_right = self._buildAdvecMat(iioe, lCoeffs, rCoeffs)
else:
lCoeffs = advecupwind(self.lpoints, self.dt)
advMat_left = self._buildAdvecMat(iioe, lCoeffs)
# Advect elevations
if iioe:
# Inflow-Implicit/Outflow-Explicit Scheme 1
advMat_right.mult(self.hGlobal, self.tmp1)
self._solve_KSP(True, advMat_left, self.tmp1, self.tmp)
# Inflow-Implicit/Outflow-Explicit Scheme 2
if self.advscheme == 3:
self.dm.globalToLocal(self.tmp, self.tmpL)
newh = self.tmpL.getArray()
self._advectorIIOE2(hL, newh, hmin, hmax, nbOut)
else:
# Upwind scheme with potentially excessive diffusion solved implicitly
self._solve_KSP(True, advMat_left, self.hGlobal, self.tmp)
# Update elevations
self.tmp.copy(result=self.hGlobal)
self.dm.globalToLocal(self.hGlobal, self.hLocal)
if self.flatModel:
hL = self.hLocal.getArray().copy()
hL[self.idBorders] = -1.e8
nhL = fitedges(hL)
self.hLocal.setArray(nhL)
self.dm.localToGlobal(self.hLocal, self.hGlobal)
# Advect erosion deposition
if iioe:
if self.advscheme == 3:
self.dm.globalToLocal(self.cumED, self.cumEDLocal)
# Minimum and maximum in a local neighborhood
edL = self.cumEDLocal.getArray().copy()
edmin, edmax = getrange(self.lpoints, edL)
# Inflow-Implicit/Outflow-Explicit Scheme 1
advMat_right.mult(self.cumED, self.tmp1)
self._solve_KSP(True, advMat_left, self.tmp1, self.tmp)
# Inflow-Implicit/Outflow-Explicit Scheme 2
if self.advscheme == 3:
self.dm.globalToLocal(self.tmp, self.tmpL)
newed = self.tmpL.getArray()
self._advectorIIOE2(edL, newed, edmin, edmax, nbOut)
else:
# Upwind scheme with potentially excessive diffusion solved implicitly
self._solve_KSP(True, advMat_left, self.cumED, self.tmp)
# Update erosion deposition
self.tmp.copy(result=self.cumED)
self.dm.globalToLocal(self.cumED, self.cumEDLocal)
if self.flatModel:
edL = self.cumEDLocal.getArray().copy()
edL[self.idBorders] = -1.e8
nedL = fitedges(edL)
self.cumEDLocal.setArray(nedL)
self.dm.localToGlobal(self.cumEDLocal, self.cumED)
# Advect flexural isostasy
if self.flexOn:
if iioe:
if self.advscheme == 3:
self.tmpL.setArray(self.localFlex)
self.dm.localToGlobal(self.tmpL, self.fiso)
# Minimum and maximum in a local neighborhood
fimin, fimax = getrange(self.lpoints, self.localFlex)
# Inflow-Implicit/Outflow-Explicit Scheme 1
advMat_right.mult(self.fiso, self.tmp1)
self._solve_KSP(True, advMat_left, self.tmp1, self.tmp)
# Inflow-Implicit/Outflow-Explicit Scheme 2
if self.advscheme == 3:
self.dm.globalToLocal(self.tmp, self.tmpL)
newfi = self.tmpL.getArray()
self._advectorIIOE2(self.localFlex, newfi, fimin, fimax, nbOut)
else:
self.tmpL.setArray(self.localFlex)
self.dm.localToGlobal(self.tmpL, self.fiso)
# Upwind scheme with potentially excessive diffusion solved implicitly
self._solve_KSP(True, advMat_left, self.fiso, self.tmp)
# Update flexural isostasy
self.tmp.copy(result=self.fiso)
self.dm.globalToLocal(self.fiso, self.tmpL)
self.localFlex = self.tmpL.getArray().copy()
if MPIrank == 0 and self.verbose:
print(
"Compute Advection Processes (%0.02f seconds)" % (process_time() - t0),
flush=True,
)
# Clean
if iioe:
advMat_right.destroy()
del rCoeffs
advMat_left.destroy()
del edL, nedL, hL, nhL, lCoeffs
# del edmin, edmax, hmin, hmax
gc.collect()
return
[docs]
def _readAdvectionData(self, hdisp, timer):
"""
From a tectonic input file reads the horizontal displacements information, containing the displacement rates along each axis.
.. note::
A cKDTree is built with the advected coordinates and used to interpolate the mesh variables on the initial local mesh position.
The interpolation is based on a weighting distance function accounting for the 3 closest advected vertices.
:arg hdisp: displacement rates in 3D
:arg timer: displacement time step in years
"""
t0 = process_time()
XYZ = self.mCoords + hdisp * timer
# Build a tree with the advected nodes
tree = spatial.cKDTree(XYZ, leafsize=10)
# Query the distances from unstructured nodes
distances, self.tec_IDs = tree.query(self.mCoords, k=3)
# Inverse weighting distance...
self.tec_weights = np.divide(
1.0, distances, out=np.zeros_like(distances), where=distances != 0
)
self.tec_onIDs = np.where(distances[:, 0] == 0)[0]
self.tec_sumw = np.sum(self.tec_weights, axis=1)
del tree, distances
if MPIrank == 0 and self.verbose:
print(
"Read advection data (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
return
[docs]
def _advectPlates(self):
"""
Advects surface information based on plate evolution and performs interpolation.
.. important::
The interpolated values are the elevation, flexural response and cumulative erosion deposition and the interpolation is done on the spherical mesh on a single provessor. In case the stratigraphic information is also recorded then this is also interpolated.
"""
# Send local elevation globally
t0 = process_time()
hl = self.hLocal.getArray().copy()
gZ = np.zeros(self.mpoints, dtype=np.float64) - 1.0e8
gZ[self.locIDs] = hl
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, gZ, op=MPI.MAX)
# Send local erosion deposition globally
edl = self.cumEDLocal.getArray().copy()
gED = np.zeros(self.mpoints, dtype=np.float64) - 1.0e10
gED[self.locIDs] = edl
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, gED, op=MPI.MAX)
# Send local flexural isostasy globally
if self.flexOn:
gFI = np.zeros(self.mpoints, dtype=np.float64) - 1.0e10
gFI[self.locIDs] = self.localFlex
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, gFI, op=MPI.MAX)
if MPIrank == 0 and self.verbose:
print(
"Transfer local elevation, erosion deposition and flexural information globally (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
# Perform interpolation
t0 = process_time()
tmp = np.sum(self.tec_weights * gZ[self.tec_IDs], axis=1)
nelev = np.divide(tmp, self.tec_sumw, out=np.zeros_like(self.tec_sumw), where=self.tec_sumw != 0)
tmp = np.sum(self.tec_weights * gED[self.tec_IDs], axis=1)
nerodep = np.divide(tmp, self.tec_sumw, out=np.zeros_like(self.tec_sumw), where=self.tec_sumw != 0)
if self.flexOn:
tmp = np.sum(self.tec_weights * gFI[self.tec_IDs], axis=1)
nfi = np.divide(tmp, self.tec_sumw, out=np.zeros_like(self.tec_sumw), where=self.tec_sumw != 0)
if len(self.tec_onIDs) > 0:
nelev[self.tec_onIDs] = gZ[self.tec_IDs[self.tec_onIDs, 0]]
nerodep[self.tec_onIDs] = gED[self.tec_IDs[self.tec_onIDs, 0]]
if self.flexOn:
nfi[self.tec_onIDs] = gFI[self.tec_IDs[self.tec_onIDs, 0]]
if MPIrank == 0 and self.verbose:
print(
"Define local elevation and erosion/deposition after advection (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
self.hGlobal.setArray(nelev[self.glbIDs])
self.dm.globalToLocal(self.hGlobal, self.hLocal)
self.cumED.setArray(nerodep[self.glbIDs])
self.dm.globalToLocal(self.cumED, self.cumEDLocal)
if self.flexOn:
self.localFlex = nfi[self.locIDs].copy()
# Update stratigraphic record
if self.stratNb > 0 and self.stratStep > 0:
self._advectStrati()
return
[docs]
def _findPts2Reduce(self):
"""
Finds the nodes part of another partition that are required for interpolation.
:return: Indices of needed nodes present in other partitions
"""
# Global neighbours for each local partition
lgNghbs = self.idNbghs[self.locIDs, :].flatten()
# Global IDs required locally but part of another partition
gids = lgNghbs[~np.in1d(lgNghbs, self.locIDs)]
# Get all points that have to be transferred
vals = np.zeros(self.mpoints, dtype=int)
vals[gids] = 1
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, vals, op=MPI.MAX)
return np.where(vals > 0)[0]
[docs]
def _advectStrati(self):
"""
From advected stratigraphic information, retrieve points that are part of another partition and perform the interpolation.
"""
# Get lobal point IDs that will need to be transferred globally
t0 = process_time()
redIDs = self._findPts2Reduce()
if MPIrank == 0 and self.verbose:
print(
"Send stratigraphy information to local partition (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
# Transfer the variables accordingly and perform operation
t0 = process_time()
nghs = self.tec_IDs[self.locIDs]
wgts = self.tec_weights[self.locIDs, :, None]
sumw = self.tec_sumw[self.locIDs, :, None]
onID = self.tec_onIDs[self.locIDs]
self.stratH[:, : self.stratStep] = self._updateStratInfo(
redIDs, self.stratH[:, : self.stratStep], sumw, onID, wgts, nghs
)
self.stratZ[:, : self.stratStep] = self._updateStratInfo(
redIDs, self.stratZ[:, : self.stratStep], sumw, onID, wgts, nghs
)
self.phiS[:, : self.stratStep] = self._updateStratInfo(
redIDs, self.phiS[:, : self.stratStep], sumw, onID, wgts, nghs
)
if MPIrank == 0 and self.verbose:
print(
"Define local stratigraphy variables after advection (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
del nghs, wgts, sumw, onID
return
[docs]
def _updateStratInfo(self, reduceIDs, variable, sumw, onIDs, weights, nghs):
"""
Update stratigraphic variables based on interpolated values.
:arg reduceIDs: indices of nodes information that need to be available globally
:arg variable: stratigraphic variable to interpolate
:arg sumw: sum of weights for interpolation
:arg onIDs: nodes that remain fixed
:arg weights: weights for interpolation
:arg nghs: neighbours for interpolation
:return: nvar interpolated stratigraphic variable
"""
# Reduce variable so that all values that need to be read from another partition can be
vals = np.zeros((self.mpoints, self.stratStep), dtype=np.float64) - 1.0e8
vals[self.locIDs, :] = variable
redVals = vals[reduceIDs, :]
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, redVals, op=MPI.MAX)
vals[reduceIDs, :] = redVals
# Inverse weighting distance...
tmp = np.sum(weights * vals[nghs, :], axis=1)
nvar = np.divide(tmp, sumw, where=sumw != 0, out=np.zeros_like(tmp))
if len(onIDs) > 0:
nvar[onIDs, :] = vals[nghs[onIDs, 0], :]
return nvar
[docs]
def updatePaleoZ(self):
"""
Update surface information based on paleo-reconstruction.
:arg paleoZ: paleo-elevation data
:arg minZ: elevation threshold used for fitting
"""
if self.plateStep:
if self.plateTimer == self.tNow:
self._advectPlates()
if self.paleoZ is None:
return
t0 = process_time()
# Send local elevation globally
hl = self.hLocal.getArray().copy()
ids = np.where(hl <= self.minZ)[0]
hl[ids] = self.paleoZ[ids]
# Fit simulated elevations to paleoelevation ones
self.hLocal.setArray(hl)
self.dm.localToGlobal(self.hLocal, self.hGlobal)
if MPIrank == 0 and self.verbose:
print(
"Update model based on paleo-elevations (%0.02f seconds)"
% (process_time() - t0),
flush=True,
)
return