import os
import sys
import petsc4py
import numpy as np
from mpi4py import MPI
from scipy import spatial
from time import process_time
from gospl.tools.constants import MISSING_DATA_SENTINEL, MISSING_LARGE_SENTINEL
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
MPIrank = petsc4py.PETSc.COMM_WORLD.Get_rank()
MPIsize = petsc4py.PETSc.COMM_WORLD.Get_size()
[docs]
class Tectonics(object):
"""
This class defines how 2D and spherical mesh surfaces are changing given a set of horizontal and vertical rates.
.. note::
Three advection approaches are proposed, a standard upwind scheme, an Inflow-Implicit/Outflow-Explicit scheme and a semi-lagrangian approach based on nearest neighbour search based on a kdTree-search where interpolation is weighted by distance.
"""
def __init__(self):
"""
The initialisation of the `Tectonics` class.
"""
self.hdisp = None
self.tecNb = -1
self.paleoZ = None
self.plateStep = False
self.fiso = self.hGlobal.duplicate()
# Cached horizontal-advection operator (FV upwind / IIOE). The matrix
# coefficients depend only on the face velocities (refreshed when the
# tectonic interval advances) and on `self.dt`, so within an interval at
# fixed dt the operator is constant: build it once and reuse it, along
# with its KSP (so the block-Jacobi PCSetUp is factorised once too).
self._advMatLeft = None
self._advMatRight = None
self._advKSP = None
self._advNbOut = None
self._advDt = None
self._advRebuild = True
return
[docs]
def getTectonics(self):
"""
Finds the current tectonic regimes (horizontal and vertical) for the considered time interval.
.. 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.at[nb + 1, "start"] < 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.at[nb, "end"] - self.tecdata.at[nb, "start"]
else:
timer = self.tEnd - self.tecdata.at[nb, "start"]
# Horizontal displacements
# was iloc[nb, -1]; named access is safer — see AGENTS.md
# Forcing DataFrame layout contract
if self.tecdata.at[nb, "hMap"] != "empty":
fname = self.tecdata.at[nb, "hMap"][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.at[nb, "hMap"][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.at[nb, "end"]
else:
# Get the velocity from the input file.
nodeVel = np.zeros((self.lpoints, 3))
if self.cyclicBC:
# Cyclic 2D model: the mesh is a cylinder. The user keeps
# supplying the displacement in the flat (vx, vy) frame
# (as for a planar model); map it onto the cylinder
# tangent so the periodic-axis component advects AROUND
# the seam instead of pushing radially through it.
nodeVel = self._cylinderVelocity(self.hdisp)
elif self.flatModel:
# Planar 2D model: the displacement lives in the (x, y)
# plane (the z component is zero).
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. The face velocities just
# changed, so the cached advection operator must be rebuilt.
getfacevelocity(self.lpoints, nodeVel)
self._advRebuild = True
else:
self.hdisp = None
# Vertical displacements
if self.tecdata.at[nb, "tMap"] != "empty":
fname = self.tecdata.at[nb, "tMap"][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.at[nb, "tMap"][1]
self.upsub = mdata[key][self.locIDs]
else:
self.upsub = None
# Paleo-elevation fitting
self.paleoZ = None
if self.tecdata.at[nb, "zMap"] != "empty":
fname = self.tecdata.at[nb, "zMap"][0] + ".npz"
mdata = np.load(fname)
key = self.tecdata.at[nb, "zMap"][1]
if len(self.tecdata.at[nb, "zMap"]) == 3:
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 _cylinderVelocity(self, hdisp):
"""
Map a flat ``(vx, vy)`` horizontal displacement onto the tangent of the
cylinder used for a cyclic (periodic) 2D model.
A cyclic mesh is a cylinder: the periodic flat axis is wrapped into a
circle lying in the plane that contains the synthetic ``z`` dimension,
while the other flat axis stays the (straight) cylinder axis. A flat
velocity in the periodic direction must therefore become a velocity
*around* the cylinder — tangent to that circle (the local ``θ̂``) — not a
Cartesian translation, which would push radially through the surface and
be discarded by the face-normal dot product. The non-periodic component
is unchanged (it is already tangent, along the cylinder axis).
"""
coords = self.lcoords
vel = np.zeros((self.lpoints, 3))
if self.east == 2 or self.west == 2:
# Periodic in x -> circle in (x, z), cylinder axis = y.
rad = np.sqrt(coords[:, 0] ** 2 + coords[:, 2] ** 2)
rad[rad == 0.0] = 1.0
cos, sin = coords[:, 0] / rad, coords[:, 2] / rad
vel[:, 0] = -hdisp[:, 0] * sin # around-seam component (θ̂)
vel[:, 2] = hdisp[:, 0] * cos
vel[:, 1] = hdisp[:, 1] # axial component (unchanged)
else:
# Periodic in y -> circle in (y, z), cylinder axis = x.
rad = np.sqrt(coords[:, 1] ** 2 + coords[:, 2] ** 2)
rad[rad == 0.0] = 1.0
cos, sin = coords[:, 1] / rad, coords[:, 2] / rad
vel[:, 1] = -hdisp[:, 1] * sin # around-seam component (θ̂)
vel[:, 2] = hdisp[:, 1] * cos
vel[:, 0] = hdisp[:, 0] # axial component (unchanged)
return vel
[docs]
def _buildAdvecMat(self, iioe, lCoeffs, rCoeffs=None):
"""
Build the advection operator(s) from the per-cell FV coefficients
``(lpoints, 1+maxnb)`` (col 0 = diagonal, cols ``1..maxnb`` = neighbour
entries for ``self.FVmesh_ngbID``) in a single-pass CSR assembly
(``_assembleDiffMatCSR``) rather than the old ``maxnb``-matrix ``axpy``
loop.
:arg iioe: True for the IIOE scheme (build both left/right operators);
False for the implicit upwind scheme (left operator only).
:arg lCoeffs: left (implicit) coefficients.
:arg rCoeffs: right (explicit) coefficients (IIOE only).
:return: ``(advMat_left, advMat_right)`` if ``iioe`` else ``advMat_left``.
"""
if self.flatModel:
# Pin the (non-periodic) domain edges with a Dirichlet row. A cyclic
# seam stays free so material advects across it (advectBorders
# excludes the seam nodes).
lCoeffs[self.advectBorders, 1:] = 0.0
lCoeffs[self.advectBorders, 0] = 1.0
if iioe:
rCoeffs[self.advectBorders, 1:] = 0.0
rCoeffs[self.advectBorders, 0] = 1.0
advMat_left = self._assembleDiffMatCSR(lCoeffs)
if iioe:
advMat_right = self._assembleDiffMatCSR(rCoeffs)
return advMat_left, advMat_right
return advMat_left
def _advSolve(self, rhs, sol):
"""
Solve one advection system with the cached operator + KSP. ``sol`` must
carry the warm-start guess on entry (the field's pre-advection value),
which is close to the solution since advection moves it only slightly.
Falls back to the fgmres/asm solver if the cached KSP diverges.
"""
ksp = self._advKSP
ksp.solve(rhs, sol)
if ksp.getConvergedReason() < 0:
sol = self._solve_KSP2(self._advMatLeft, rhs, sol)
return sol
[docs]
def _advectorIIOE2(self, gvec, vL, newv, vmin, vmax, nbOut):
"""
Perform the advection for the Inflow-Implicit/Outflow-Explicit Scheme 2:
an anti-diffusion correction applied where the Scheme-1 result
(``newv``) over/undershoots the local neighbourhood range
``[vmin, vmax]``. The correction operator depends on the per-field range,
so it is built fresh each call (not cached) and writes the corrected
result back into ``self.tmp``.
:arg gvec: the field's pre-advection global Vec (the explicit operator
multiplies THIS field — previously hard-wired to ``self.hGlobal``,
which corrupted the correction for cumED/flexure/soil).
:arg vL: the field's pre-advection local array.
:arg newv: the Scheme-1 advected local array.
:arg vmin, vmax: local neighbourhood min/max of the pre-advection field.
:arg nbOut: outflow-neighbour count from ``adveciioe``.
"""
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(gvec, 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, erosion (and flexure and soil production if defined) by solving the advection equation based on the finite volume space discretization and a semi-implicit temporal discretization.
The approach relies on an 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::
Here, the 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 a M-matrix yielding favourable solvability and stability properties.
.. important::
The method allows for large time steps without losing too much stability and precision.
It is formally second order accurate in space and time for 1D advection problems with variable velocity. In addition, 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 intersecting the middle of the nodes edges)).
Similarly, we consider that the advected variables at the face are defined by linear interpolation from each connected vertex.
"""
t0 = process_time()
iioe = self.advscheme != 1
# Build (or reuse) the cached advection operator + KSP. The coefficients
# depend only on the face velocities (refreshed on tectonic-interval
# change, which sets `_advRebuild`) and on `self.dt`, so within an
# interval at fixed dt the operator is constant: assemble it once and
# reuse it (and its block-Jacobi PCSetUp) across every step.
if self._advRebuild or self._advDt != self.dt:
if self._advMatLeft is not None:
self._advMatLeft.destroy()
if self._advMatRight is not None:
self._advMatRight.destroy()
self._advMatRight = None
if iioe:
self._advNbOut, lCoeffs, rCoeffs = adveciioe(self.lpoints, self.dt)
self._advMatLeft, self._advMatRight = self._buildAdvecMat(
True, lCoeffs, rCoeffs
)
else:
lCoeffs = advecupwind(self.lpoints, self.dt)
self._advMatLeft = self._buildAdvecMat(False, lCoeffs)
if self._advKSP is None:
self._advKSP = self._makeDiffusionKSP("advect_")
self._advKSP.setOperators(self._advMatLeft, self._advMatLeft)
self._advDt = self.dt
self._advRebuild = False
nbOut = self._advNbOut if iioe else None
# Each field is advected the same way; only the boundary-edge treatment
# differs (elevation/cumED are re-extrapolated, flexure/soil are not).
self._advectField(self.hGlobal, iioe, nbOut)
self.dm.globalToLocal(self.hGlobal, self.hLocal)
self._resetEdges(self.hLocal, self.hGlobal, MISSING_DATA_SENTINEL)
# cumED uses the large-magnitude sentinel (consistent with
# `_advectPlates`): cumulative erosion/deposition can exceed the
# MISSING_DATA_SENTINEL magnitude, so a smaller marker could collide.
self._advectField(self.cumED, iioe, nbOut)
self.dm.globalToLocal(self.cumED, self.cumEDLocal)
self._resetEdges(self.cumEDLocal, self.cumED, MISSING_LARGE_SENTINEL)
# Flexural isostasy (carried in the numpy `localFlex`; no edge reset).
if self.flexOn:
self.tmpL.setArray(self.localFlex)
self.dm.localToGlobal(self.tmpL, self.fiso)
self._advectField(self.fiso, iioe, nbOut)
self.dm.globalToLocal(self.fiso, self.tmpL)
self.localFlex = self.tmpL.getArray().copy()
# Soil thickness (no edge reset; clamped to a physical range after).
if self.cptSoil:
self._advectField(self.Gsoil, iioe, nbOut)
self.dm.globalToLocal(self.Gsoil, self.Lsoil)
lsoil = self.Lsoil.getArray().copy()
np.clip(lsoil, 0.0, self.soil_transition, out=lsoil)
self.Lsoil.setArray(lsoil)
self.dm.localToGlobal(self.Lsoil, self.Gsoil)
if MPIrank == 0 and self.verbose:
print(
"Compute Advection Processes (%0.02f seconds)" % (process_time() - t0),
flush=True,
)
return
def _advectField(self, gvec, iioe, nbOut):
"""
Advect one global field Vec **in place** with the cached advection
operator (and, for IIOE Scheme 2, the anti-diffusion correction).
The field's current value is used as the warm-start guess (advection
moves it only slightly, so this converges in very few iterations).
Scratch used: ``self.tmp`` (solution), ``self.tmp1`` (RHS), ``self.tmpL``
(local views for the Scheme-2 range/correction).
:arg gvec: global field Vec (overwritten with the advected field).
:arg iioe: True for the IIOE schemes, False for implicit upwind.
:arg nbOut: outflow-neighbour count (IIOE Scheme 2 only; else None).
"""
if iioe and self.advscheme == 3:
# Neighbourhood min/max of the pre-advection field for Scheme 2.
self.dm.globalToLocal(gvec, self.tmpL)
fL = self.tmpL.getArray().copy()
fmin, fmax = getrange(self.lpoints, fL)
gvec.copy(result=self.tmp) # warm-start guess
if iioe:
self._advMatRight.mult(gvec, self.tmp1)
self._advSolve(self.tmp1, self.tmp)
if self.advscheme == 3:
self.dm.globalToLocal(self.tmp, self.tmpL)
self._advectorIIOE2(gvec, fL, self.tmpL.getArray(), fmin, fmax, nbOut)
else:
self._advSolve(gvec, self.tmp)
self.tmp.copy(result=gvec)
return
def _resetEdges(self, lvec, gvec, sentinel):
"""
Flat-model only: mark the (non-periodic) domain-edge nodes with
``sentinel`` and re-extrapolate them from their interior neighbours with
``fitedges``, then sync the global Vec. No-op on a global/sphere mesh.
:arg lvec: local Vec of the field (read + rewritten).
:arg gvec: matching global Vec (rewritten from ``lvec``).
:arg sentinel: edge marker (``fitedges`` treats any value ``<= -1e7`` as
missing, so both data/large sentinels are recognised).
"""
if not self.flatModel:
return
arr = lvec.getArray().copy()
arr[self.advectBorders] = sentinel
arr = fitedges(arr)
lvec.setArray(arr)
self.dm.localToGlobal(lvec, gvec)
return
[docs]
def _readAdvectionData(self, hdisp, timer):
"""
From a tectonic input file, this function 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, cumulative erosion deposition, flexural response and soil production. The interpolation is done on the spherical mesh on a single processor. 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) + MISSING_DATA_SENTINEL
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) + MISSING_LARGE_SENTINEL
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) + MISSING_LARGE_SENTINEL
gFI[self.locIDs] = self.localFlex
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, gFI, op=MPI.MAX)
# Send local soil thickness globally
if self.cptSoil:
lsoil = self.Lsoil.getArray().copy()
gSL = np.zeros(self.mpoints, dtype=np.float64) + MISSING_LARGE_SENTINEL
gSL[self.locIDs] = lsoil
MPI.COMM_WORLD.Allreduce(MPI.IN_PLACE, gSL, op=MPI.MAX)
if MPIrank == 0 and self.verbose:
print(
"Transfer local elevation, erosion deposition, soil 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 self.cptSoil:
tmp = np.sum(self.tec_weights * gSL[self.tec_IDs], axis=1)
nsoil = 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 self.cptSoil:
nsoil[self.tec_onIDs] = gSL[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()
if self.cptSoil:
nsoil[nsoil < 0.] = 0.
nsoil[nsoil > self.soil_transition] = self.soil_transition
self.Gsoil.setArray(nsoil[self.glbIDs])
self.dm.globalToLocal(self.Gsoil, self.Lsoil)
# 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.isin(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]
# tec_onIDs holds global-mesh indices where the advected node
# coincides exactly. Remap to local-mesh indices and keep only
# entries this rank owns.
g2l = np.full(self.mpoints, -1, dtype=int)
g2l[self.locIDs] = np.arange(len(self.locIDs))
onID = g2l[self.tec_onIDs]
onID = onID[onID >= 0]
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) + MISSING_DATA_SENTINEL
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.sealevel)[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