Source code for sed.stratplex

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

from mpi4py import MPI
from time import process_time

from gospl.tools.constants import BEDROCK_SENTINEL

if "READTHEDOCS" not in os.environ:
    from gospl._fortran import strataonesed

MPIrank = petsc4py.PETSc.COMM_WORLD.Get_rank()
MPIsize = petsc4py.PETSc.COMM_WORLD.Get_size()


[docs] class STRAMesh(object): """ This class encapsulates all the functions related to underlying stratigraphic information. .. note:: Sediment compaction in stratigraphic layers geometry and properties change are also considered. """ def __init__(self): """ The initialisation of `STRAMesh` class related to stratigraphic informations. """ self.stratH = None self.stratZ = None self.phiS = None # Dual-lithology (coarse/fine) layer fields. Allocated only when # self.stratLith is True (see _extraStrata / DESIGN_DUAL_LITHOLOGY.md). # stratHf = fine-fraction layer thickness (coarse = stratH - stratHf); # phiF = fine porosity per layer (phiS then holds the coarse porosity). self.stratHf = None self.phiF = None # Dual-lithology fine mass-balance diagnostics (m^3, owned-node running # totals over the run; reduced across ranks by the conservation test). # _fineEroded accumulates fine solid removed from the pile (erodeStrat); # _fineDeposited accumulates fine deposited back (deposeStrat, both the # continental and marine calls). On a closed sphere they must match to # within the floor/transit budget — the fine-specific analogue of the # total-cumED mass-conservation check. self._fineEroded = 0.0 self._fineDeposited = 0.0 # Per-layer erodibility multiplier (lpoints, stratNb). Effective K # at the surface = self.K * stratK[<top non-empty layer>]. Fresh # deposits and the bedrock sentinel default to 1.0 (no scaling). # An optional `stratK` key in the npstrata file lets the user # impose a non-uniform multiplier on the initial layers. self.stratK = None # In-model provenance tracers (opt-in `provenance:`; see # DESIGN_PROVENANCE.md §6). stratP[node, layer, class] = thickness of # each source-rock class in each layer (Σ over classes == stratH); # source_class = per-vertex bedrock class. Allocated only when provOn. self.stratP = None self.source_class = None return
[docs] def readStratLayers(self): """ When stratigraphic layers are turned on, this function reads any initial stratigraphic layers provided in the input file (key: `npstrata`). The following variables will be read from the file: - thickness of each stratigrapic layer `strataH` accounting for both erosion & deposition events. - elevation at time of deposition, considered to be to the current elevation for the top stratigraphic layer `strataZ`. - porosity of coarse sediment `phiS` in each stratigraphic layer computed at center of each layer. With dual lithology enabled (`strata: dual: True`), two optional keys let each initial layer carry its own coarse/fine composition: - `strataHf`: fine-fraction bulk thickness per layer (coarse = `strataH - strataHf`); absent -> all-coarse. - `phiF`: fine porosity per layer; absent -> defaults to `phi0f`. """ if self.strataFile is not None: fileData = np.load(self.strataFile) stratVal = fileData["strataH"] self.initLay = stratVal.shape[1] self.stratNb += self.initLay # Create stratigraphic arrays self.stratH = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.stratH[:, 0 : self.initLay] = stratVal[self.locIDs, 0 : self.initLay] stratVal = fileData["strataZ"] self.stratZ = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.stratZ[:, 0 : self.initLay] = stratVal[self.locIDs, 0 : self.initLay] stratVal = fileData["phiS"] self.phiS = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.phiS[:, 0 : self.initLay] = stratVal[self.locIDs, 0 : self.initLay] # Per-layer K multiplier. Loaded from the optional `stratK` # key in the npstrata file (same (mpoints, initLay) shape as # the other layer fields); defaults to 1.0 (use self.K as-is). self.stratK = np.ones((self.lpoints, self.stratNb), dtype=np.float64) if "stratK" in fileData.files: stratVal = fileData["stratK"] self.stratK[:, 0 : self.initLay] = stratVal[ self.locIDs, 0 : self.initLay ] # Dual-lithology fine-fraction layer fields. Optional in the # npstrata file: `strataHf` (per-layer fine bulk thickness, so each # initial layer carries its own coarse/fine composition; coarse = # strataH - strataHf) and `phiF` (per-layer fine porosity). Absent # `strataHf` -> all-coarse, so a single-fraction file still loads. if self.stratLith: self.stratHf = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) if "strataHf" in fileData.files: stratVal = fileData["strataHf"] self.stratHf[:, 0 : self.initLay] = stratVal[ self.locIDs, 0 : self.initLay ] # Keep the partition physical: 0 <= fine <= total thickness. np.clip( self.stratHf[:, 0 : self.initLay], 0.0, self.stratH[:, 0 : self.initLay], out=self.stratHf[:, 0 : self.initLay], ) self.phiF = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) if "phiF" in fileData.files: stratVal = fileData["phiF"] self.phiF[:, 0 : self.initLay] = stratVal[ self.locIDs, 0 : self.initLay ] else: # No fine porosity supplied: default to the fine surface # porosity so fine-bearing initial layers compact sensibly # (irrelevant where strataHf == 0). self.phiF[:, 0 : self.initLay] = self.phi0f elif "strataHf" in fileData.files and MPIrank == 0: print( "Warning: the npstrata file provides 'strataHf' (per-layer " "fine composition) but dual lithology is disabled. Set " "`strata: dual: True` to use it; the field is ignored.", flush=True, ) # All layers in the file are real sediment; no bedrock sentinel. self.bedrockLay = 0 if self.memclear: del fileData, stratVal gc.collect() else: self.stratH = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.phiS = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.stratZ = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.stratK = np.ones((self.lpoints, self.stratNb), dtype=np.float64) # Treat layer 0 as an effectively infinite bedrock reservoir when # no initial stratigraphy file is provided. The 1e6 m sentinel # cancels out in erodeStrat (cumThick / eroVal share the offset) # so it does not contaminate eroded volumes. self.stratH[:, 0] = BEDROCK_SENTINEL self.phiS[:, 0] = self.phi0s self.bedrockLay = 1 # layer 0 is the infinite-bedrock sentinel # Dual-lithology fine-fraction fields. The infinite-bedrock # sentinel layer is split into coarse/fine by bedrock_coarse_frac # so material eroded from bedrock inherits a defined composition. if self.stratLith: self.stratHf = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.phiF = np.zeros((self.lpoints, self.stratNb), dtype=np.float64) self.stratHf[:, 0] = BEDROCK_SENTINEL * (1.0 - self.bedrock_coarse_frac) self.phiF[:, 0] = self.phi0f if getattr(self, "provOn", False): self._initProvenance() return
[docs] def _initProvenance(self): """ Allocate and seed the provenance state (opt-in `provenance:`; Phase 0). ``source_class`` (per-vertex bedrock class) is read from the ``uniform`` scalar or the ``source`` ``[file, key]`` map; ``stratP[node, layer, class]`` is the per-class thickness in each layer, seeded so every initial layer (and the bedrock sentinel) carries the node's bedrock source class (Σ over classes == stratH). A passive tracer — no physics feedback — so a provenance-on run is identical to provenance-off until the erosion/transport/deposition hooks of later phases are added. """ if self._provSourceMap is not None: fname, key = self._provSourceMap[0], self._provSourceMap[1] data = np.load(fname + ".npz") self.source_class = data[key][self.locIDs].astype(np.int64) del data else: self.source_class = np.full( self.lpoints, int(self._provSourceUniform), dtype=np.int64 ) np.clip(self.source_class, 0, self.provNb - 1, out=self.source_class) # Per-layer per-class thickness; each initial layer's full thickness is # assigned to the node's bedrock source class. self.stratP = np.zeros( (self.lpoints, self.stratNb, self.provNb), dtype=np.float64 ) self.stratP[np.arange(self.lpoints), :, self.source_class] = self.stratH return
[docs] def _fillZeroPorosity(self, phiS): """ Where ``phiS == 0`` (layer has been emptied or never deposited), inherit the value from the nearest underlying layer with non-zero porosity. Vectorised forward-fill from low → high index along axis 1. Leading zeros at the column base stay zero (no valid layer below). """ mask = phiS > 0 idx = np.where(mask, np.arange(phiS.shape[1])[None, :], 0) np.maximum.accumulate(idx, axis=1, out=idx) return np.take_along_axis(phiS, idx, axis=1)
[docs] def _surfaceK(self): """ Return the per-node erodibility multiplier of the topmost non-empty stratigraphic layer. Used by the SPL flavours to scale the scalar ``self.K`` according to the bedrock currently exposed at the surface. Returns 1.0 everywhere when stratigraphy is disabled (``stratNb == 0``) or when the column is fully empty at a node, so the SPL evaluation falls back to the YAML-default K. """ if self.stratNb == 0 or self.stratK is None: return np.ones(self.lpoints, dtype=np.float64) sK = self.stratK[:, : self.stratStep + 1] mask = sK > 0 # For each row, locate the highest column index where the layer # has a non-zero multiplier. Reverse-then-argmax picks the first # non-zero starting from the top of the column. rev = mask[:, ::-1] any_valid = rev.any(axis=1) top_from_right = np.argmax(rev, axis=1) top_idx = sK.shape[1] - 1 - top_from_right out = np.ones(self.lpoints, dtype=np.float64) rows = np.arange(sK.shape[0]) out[any_valid] = sK[rows[any_valid], top_idx[any_valid]] return out
[docs] def _surfaceComposition(self): """ Return the per-node coarse fraction ``fc`` (by bulk thickness) of the topmost non-empty stratigraphic layer; the fine fraction is ``1 - fc``. Returns all-ones (all coarse) when dual lithology is disabled or the fine pile is unallocated, so single-fraction callers are unchanged. Shared hook for the SPL erodibility blend (`_surfaceLithoK`) and the hillslope diffusivity blend (see DESIGN_DUAL_LITHOLOGY.md). """ if not self.stratLith or self.stratHf is None or self.stratNb == 0: return np.ones(self.lpoints, dtype=np.float64) H = self.stratH[:, : self.stratStep + 1] Hf = self.stratHf[:, : self.stratStep + 1] mask = H > 0 # Topmost non-empty layer per node (reverse-then-argmax, as _surfaceK). rev = mask[:, ::-1] any_valid = rev.any(axis=1) top_from_right = np.argmax(rev, axis=1) top_idx = H.shape[1] - 1 - top_from_right fc = np.ones(self.lpoints, dtype=np.float64) rows = np.arange(H.shape[0]) Htop = H[rows, top_idx] Hftop = Hf[rows, top_idx] valid = any_valid & (Htop > 0) fc[valid] = 1.0 - (Hftop[valid] / Htop[valid]) np.clip(fc, 0.0, 1.0, out=fc) return fc
[docs] def _surfaceLithoK(self): """ Per-node erodibility multiplier from the exposed surface composition: ``fc + (1 - fc) * fine_k_factor``. Equals 1.0 everywhere when dual lithology is off (or ``fine_k_factor == 1``, i.e. no lithology contrast), so it composes multiplicatively with ``_surfaceK`` in the SPL flavours without altering single-fraction behaviour. """ if not self.stratLith: return np.ones(self.lpoints, dtype=np.float64) fc = self._surfaceComposition() return fc + (1.0 - fc) * self.fine_k_factor
[docs] def _surfaceLithoD(self): """ Per-node diffusivity multiplier from the exposed surface composition: ``fc + (1 - fc) * fine_diff_factor``. Equals 1.0 everywhere when dual lithology is off (or ``fine_diff_factor == 1``, i.e. no contrast), so it composes multiplicatively with the base hillslope coefficients (``Cda``/``Cdm``) without altering single-fraction behaviour. Fines diffuse faster when ``fine_diff_factor > 1`` (see DESIGN_DUAL_LITHOLOGY.md Section 7). """ if not self.stratLith: return np.ones(self.lpoints, dtype=np.float64) fc = self._surfaceComposition() return fc + (1.0 - fc) * self.fine_diff_factor
[docs] def deposeStrat(self): """ Add deposition on top of an existing stratigraphic layer. The following variables will be recorded: - thickness of each stratigrapic layer `stratH` accounting for both erosion & deposition events. - porosity of sediment `phiS` in each stratigraphic layer computed at center of each layer. In dual-lithology mode the deposit is split into a coarse and a fine fraction using the **per-node deposit fine fraction** ``self.depoFineFrac``. It starts each step as ``self.fineFrac`` (the composition of the routed sediment arriving at each node, from ``sedplex._getSedFlux``) and is refined inside continental depressions by ``_pitFineFraction`` (3b: fine biased to the depocenter, coarse to the inlet/margins). """ self.dm.globalToLocal(self.tmp, self.tmpL) depo = self.tmpL.getArray().copy() depo[depo < 1.0e-4] = 0.0 self.stratH[:, self.stratStep] += depo ids = np.where(depo > 0)[0] if self.stratLith: fineDepo = depo * self.depoFineFrac self.stratHf[:, self.stratStep] += fineDepo # Mass-balance diagnostic (owned nodes only, m^3). self._fineDeposited += float( np.sum((fineDepo * self.larea)[self.inIDs == 1]) ) # Fresh deposit carries each lithology's surface porosity. self.phiS[ids, self.stratStep] = self.phi0c self.phiF[ids, self.stratStep] = self.phi0f else: self.phiS[ids, self.stratStep] = self.phi0s if getattr(self, "provOn", False): # Lay the deposit into the layer's provenance composition (the # arriving routed composition; a passive tracer, so no sorting). # Keeps Σ over classes == stratH for the deposited thickness. provDepo = depo[:, None] * self.depoProvFrac self.stratP[:, self.stratStep, :] += provDepo self._provDeposited += np.sum( (provDepo * self.larea[:, None])[self.inIDs == 1], axis=0 ) # Freshly deposited sediment carries the default erodibility (no # multiplier). If you want re-deposited sediment to keep its # source-layer K, this is the line to revisit. self.stratK[ids, self.stratStep] = 1.0 # Cleaning arrays if self.memclear: del depo, ids gc.collect() return
[docs] def erodeStrat(self): """ This function removes eroded sediment thicknesses from the stratigraphic pile. The function takes into account the porosity values of considered lithologies in each eroded stratigraphic layers. It follows the assumptions: - Eroded thicknesses from stream power law and hillslope diffusion are considered to encompass both the solid and void phase. - Only the solid phase will be moved dowstream by surface processes. - The corresponding deposit thicknesses for those freshly eroded sediments correspond to uncompacted thicknesses based on the porosity at surface given from the input file. """ self.dm.globalToLocal(self.tmp, self.tmpL) ero = self.tmpL.getArray().copy() ero[ero > 0] = 0.0 # Nodes experiencing erosion nids = np.where(ero < 0)[0] if len(nids) == 0: self.thCoarse = np.zeros(self.lpoints) if self.stratLith: self.thFine = np.zeros(self.lpoints) if getattr(self, "provOn", False): self.provEro = np.zeros((self.lpoints, self.provNb)) return # Cumulative thickness for each node if self.stratLith: # Inflate the fine pile in proportion to layer 0's CURRENT fine # fraction so the sentinel never alters that layer's composition # (for the infinite-bedrock case this fraction is 1-bedrock_coarse_frac; # for a real basal layer it is its own fine ratio, i.e. 0 if all coarse). H0 = self.stratH[nids, 0] sentFine = BEDROCK_SENTINEL * np.divide( self.stratHf[nids, 0], H0, out=np.zeros_like(H0), where=H0 > 0 ) self.stratH[nids, 0] += BEDROCK_SENTINEL if self.stratLith: self.stratHf[nids, 0] += sentFine provOn = getattr(self, "provOn", False) if provOn: # The infinite-bedrock sentinel is pure bedrock -> the node's source # class (keeps Σ over classes == stratH for layer 0). self.stratP[nids, 0, self.source_class[nids]] += BEDROCK_SENTINEL cumThick = np.cumsum(self.stratH[nids, self.stratStep :: -1], axis=1)[:, ::-1] boolMask = cumThick < -ero[nids].reshape((len(nids), 1)) mask = boolMask.astype(int) if provOn: # Per-class bulk eroded from the FULLY consumed layers (captured # before they are cleared below). provMaskedBulk = ( self.stratP[nids, : self.stratStep + 1, :] * mask[:, :, None] ).sum(axis=1) thickS = self.stratH[nids, 0 : self.stratStep + 1] if self.stratLith: # Each layer's bulk thickness splits into coarse and fine; each # fraction yields its own solid phase (1 - its own porosity). fineTh = self.stratHf[nids, 0 : self.stratStep + 1] coarseTh = thickS - fineTh thCoarse = coarseTh * (1.0 - self.phiS[nids, 0 : self.stratStep + 1]) thFine = fineTh * (1.0 - self.phiF[nids, 0 : self.stratStep + 1]) thCoarse = np.sum((thCoarse * mask), axis=1) thFine = np.sum((thFine * mask), axis=1) else: thCoarse = thickS * (1.0 - self.phiS[nids, 0 : self.stratStep + 1]) thCoarse = np.sum((thCoarse * mask), axis=1) # Clear all stratigraphy points which are eroded cumThick[boolMask] = 0.0 tmp = self.stratH[nids, : self.stratStep + 1] tmp[boolMask] = 0 self.stratH[nids, : self.stratStep + 1] = tmp if self.stratLith: tmpf = self.stratHf[nids, : self.stratStep + 1] tmpf[boolMask] = 0 self.stratHf[nids, : self.stratStep + 1] = tmpf if provOn: tmpp = self.stratP[nids, : self.stratStep + 1, :] tmpp[boolMask] = 0.0 self.stratP[nids, : self.stratStep + 1, :] = tmpp # Erode remaining stratal layers # Get non-zero top layer number. `minlength=len(nids)` is required: a # node whose entire column was consumed (every layer in `boolMask`, i.e. # the erosion demand exceeded the total pile *including* the # BEDROCK_SENTINEL infinite-bedrock floor) contributes no nonzero row to # `np.nonzero(cumThick)[0]`. Without minlength, `bincount` silently # truncates such an all-zero row when it is the LAST node, so `eroLayNb` # comes back one short and the fancy-index below raises a broadcast # IndexError (a middle all-zero row already gets a 0 count, so only # trailing ones crash). The sentinel makes a fully-consumed column # impossible for physical erosion, but a non-converged upstream solve # (e.g. glacial abrasion feeding `dz_ero`) can produce # a pathological demand that reaches here; padding keeps `erodeStrat` # crash-safe and treats those rows like any other fully-eroded node. eroLayNb = np.bincount(np.nonzero(cumThick)[0], minlength=len(nids)) - 1 eroVal = cumThick[np.arange(len(nids)), eroLayNb] + ero[nids] self.thCoarse = np.zeros(self.lpoints) # From the partially eroded top layer extract the solid phase removed. H_old = self.stratH[nids, eroLayNb] tmp = H_old - eroVal tmp[tmp < 1.0e-8] = 0.0 # TODO-REFACTOR: value matches DISCHARGE_FLOOR but distinct role (thickness numerical-noise floor); do not replace if self.stratLith: self.thFine = np.zeros(self.lpoints) Hf_old = self.stratHf[nids, eroLayNb] # Well-mixed layer: coarse/fine bulk are removed in proportion to # the layer composition; the remainder keeps the same ratio. frac = np.divide(tmp, H_old, out=np.zeros_like(tmp), where=H_old > 0) coarse_rm = (H_old - Hf_old) * frac fine_rm = Hf_old * frac thCoarse = thCoarse + coarse_rm * (1.0 - self.phiS[nids, eroLayNb]) thFine = thFine + fine_rm * (1.0 - self.phiF[nids, eroLayNb]) # Uncompacted thickness deposited downstream, per fraction, using # each lithology's surface porosity. self.thCoarse[nids] = thCoarse / (1.0 - self.phi0c) self.thFine[nids] = thFine / (1.0 - self.phi0f) self.thCoarse[self.thCoarse < 0.0] = 0.0 self.thFine[self.thFine < 0.0] = 0.0 # Remaining fine in the partial layer scales with remaining total. self.stratHf[nids, eroLayNb] = Hf_old * np.divide( eroVal, H_old, out=np.zeros_like(eroVal), where=H_old > 0 ) else: # Define the uncompacted sand thickness that will be deposited dowstream thCoarse += tmp * (1.0 - self.phiS[nids, eroLayNb]) self.thCoarse[nids] = thCoarse / (1.0 - self.phi0s) self.thCoarse[self.thCoarse < 0.0] = 0.0 if provOn: # Per-class bulk removed from the partial top layer (proportional to # the bulk removed), plus the fully-eroded layers captured earlier. fracP = np.divide( H_old - eroVal, H_old, out=np.zeros_like(H_old), where=H_old > 0 ) P_part = self.stratP[nids, eroLayNb, :].copy() provBulkEro = provMaskedBulk + P_part * fracP[:, None] # (n, classes) keepP = np.divide(eroVal, H_old, out=np.zeros_like(eroVal), where=H_old > 0) self.stratP[nids, eroLayNb, :] = P_part * keepP[:, None] # Update thickness of top stratigraphic layer self.stratH[nids, eroLayNb] = eroVal self.stratH[nids, 0] -= BEDROCK_SENTINEL if self.stratLith: self.stratHf[nids, 0] -= sentFine if provOn: self.stratP[nids, 0, self.source_class[nids]] -= BEDROCK_SENTINEL neg = self.stratH < 0 self.stratH[neg] = 0.0 self.phiS[neg] = 0.0 self.stratK[neg] = 0.0 self.phiS[:, : self.stratStep + 1] = self._fillZeroPorosity( self.phiS[:, : self.stratStep + 1] ) # Same forward-fill for the K multiplier so an emptied layer # inherits the bedrock K of the layer below it once exposed. self.stratK[:, : self.stratStep + 1] = self._fillZeroPorosity( self.stratK[:, : self.stratStep + 1] ) if self.stratLith: top = self.stratStep + 1 self.stratHf[neg] = 0.0 self.phiF[neg] = 0.0 np.clip(self.stratHf, 0.0, None, out=self.stratHf) # Fine bulk can never exceed the layer total thickness. np.minimum( self.stratHf[:, :top], self.stratH[:, :top], out=self.stratHf[:, :top], ) self.phiF[:, :top] = self._fillZeroPorosity(self.phiF[:, :top]) self.thCoarse /= self.dt if self.stratLith: self.thFine /= self.dt # Mass-balance diagnostic: fine solid removed this step (owned # nodes, m^3). thFine is the uncompacted deposit-equivalent rate, # the same basis transported and deposited downstream. self._fineEroded += float( np.sum((self.thFine * self.dt * self.larea)[self.inIDs == 1]) ) if provOn: top = self.stratStep + 1 np.clip(self.stratP[:, :top, :], 0.0, None, out=self.stratP[:, :top, :]) # Re-impose Σ over classes == stratH per layer (absorbs the sentinel # round-off and the neg-thickness clamp); empty layers stay zero. psum = self.stratP[:, :top, :].sum(axis=2) scale = np.divide( self.stratH[:, :top], psum, out=np.ones_like(psum), where=psum > 0 ) self.stratP[:, :top, :] *= scale[:, :, None] # Per-class eroded sediment as the uncompacted deposit-equivalent # RATE: the total (self.thCoarse[+thFine]) split by the eroded bulk's # provenance composition. Σ over classes == the total, so the class # sub-fluxes routed in transport sum to the total flux. thTot = self.thCoarse.copy() if self.stratLith: thTot = thTot + self.thFine totBulk = provBulkEro.sum(axis=1) provComp = np.divide( provBulkEro, totBulk[:, None], out=np.zeros_like(provBulkEro), where=totBulk[:, None] > 0, ) self.provEro = np.zeros((self.lpoints, self.provNb)) self.provEro[nids] = provComp * thTot[nids][:, None] self._provEroded += np.sum( (self.provEro * self.dt * self.larea[:, None])[self.inIDs == 1], axis=0 ) return
[docs] def elevStrat(self): """ This function updates the current stratigraphic layer elevation. """ self.stratZ[:, self.stratStep] = self.hLocal.getArray() return
[docs] def _depthPorosity(self, depth): """ This function uses the depth-porosity relationships to compute the porosities for each lithology and then the solid phase to get each layer thickness changes due to compaction. .. note:: We assume that porosity cannot increase after unloading. :arg depth: depth below basement for each sedimentary layer :return: newH updated sedimentary layer thicknesses after compaction """ if self.stratLith: return self._depthPorosityDual(depth) # Depth-porosity functions phiS = self.phi0s * np.exp(depth / self.z0s) phiS = np.minimum(phiS, self.phiS[:, : self.stratStep + 1]) # Compute the solid phase in each layers tmpS = self.stratH[:, : self.stratStep + 1].copy() tmpS *= 1.0 - self.phiS[:, : self.stratStep + 1] solidPhase = tmpS # Get new layer thickness after porosity change tot = 1.0 - phiS[:, : self.stratStep + 1] ids = np.where(tot > 0.0) newH = np.zeros(tot.shape) newH[ids] = solidPhase[ids] / tot[ids] newH[newH <= 0] = 0.0 phiS[newH <= 0] = 0.0 # Freeze the bedrock sentinel so it neither compacts nor drives surface drop if self.bedrockLay > 0: b = self.bedrockLay newH[:, :b] = self.stratH[:, :b] # thickness unchanged (1e6 sentinel) phiS[:, :b] = self.phiS[:, :b] # porosity unchanged # Update porosities in each sedimentary layer phiS = self._fillZeroPorosity(phiS) self.phiS[:, : self.stratStep + 1] = phiS if self.memclear: del phiS, solidPhase del ids, tmpS, tot gc.collect() return newH
[docs] def _depthPorosityDual(self, depth): """ Dual-lithology depth-porosity / compaction. Each fraction (coarse, fine) compacts on its own porosity-depth curve; the layer's new thickness is the sum of the two recompacted bulk thicknesses, and the fine pile ``stratHf`` is updated consistently. This is the headline physics of dual lithology: fine and coarse have different compaction, so the same solid load yields a different preserved thickness depending on the layer's grain mix. :arg depth: depth below basement for each sedimentary layer :return: newH updated layer thicknesses after compaction """ top = self.stratStep + 1 phiS_cur = self.phiS[:, :top] phiF_cur = self.phiF[:, :top] H = self.stratH[:, :top] Hf = self.stratHf[:, :top] Hc = H - Hf # Per-fraction depth-porosity, capped at the current value (porosity # cannot increase on unloading — same assumption as single-fraction). phiS_new = np.minimum(self.phi0c * np.exp(depth / self.z0c), phiS_cur) phiF_new = np.minimum(self.phi0f * np.exp(depth / self.z0f), phiF_cur) # Solid phase preserved per fraction. coarseSolid = Hc * (1.0 - phiS_cur) fineSolid = Hf * (1.0 - phiF_cur) # Recompacted bulk thickness per fraction. totC = 1.0 - phiS_new totF = 1.0 - phiF_new Hc_new = np.zeros_like(Hc) Hf_new = np.zeros_like(Hf) idc = totC > 0.0 idf = totF > 0.0 Hc_new[idc] = coarseSolid[idc] / totC[idc] Hf_new[idf] = fineSolid[idf] / totF[idf] Hc_new[Hc_new <= 0] = 0.0 Hf_new[Hf_new <= 0] = 0.0 newH = Hc_new + Hf_new # Emptied layers carry no porosity. empty = newH <= 0 phiS_new[empty] = 0.0 phiF_new[empty] = 0.0 # Freeze the bedrock sentinel layers (thickness + porosities unchanged). if self.bedrockLay > 0: b = self.bedrockLay newH[:, :b] = H[:, :b] Hf_new[:, :b] = Hf[:, :b] phiS_new[:, :b] = phiS_cur[:, :b] phiF_new[:, :b] = phiF_cur[:, :b] phiS_new = self._fillZeroPorosity(phiS_new) phiF_new = self._fillZeroPorosity(phiF_new) self.phiS[:, :top] = phiS_new self.phiF[:, :top] = phiF_new self.stratHf[:, :top] = Hf_new if self.memclear: del phiS_new, phiF_new, coarseSolid, fineSolid del Hc, Hf, Hc_new, Hf_new, totC, totF gc.collect() return newH
[docs] def getCompaction(self): """ This function computes the change in sedimentary layers porosities and thicknesses due to compaction. .. note:: We assume a simple depth-porosiy relationship. """ t0 = process_time() topZ = np.vstack(self.hLocal.getArray()) totH = np.sum(self.stratH[:, : self.stratStep + 1], axis=1) # Height of the sediment column above the center of each layer is given by cumZ = -np.cumsum(self.stratH[:, self.stratStep :: -1], axis=1) + topZ elev = np.append(topZ, cumZ[:, :-1], axis=1) zlay = np.fliplr(elev - np.fliplr(self.stratH[:, : self.stratStep + 1] / 2.0)) # Compute lithologies porosities for each depositional layers # Get depth below basement depth = zlay - topZ # Now using depth-porosity relationships we compute the porosities newH = self._depthPorosity(depth) # Get the total thickness changes induced by compaction and # update the elevation accordingly dz = totH - np.sum(newH, axis=1) dz[dz <= 0] = 0.0 self.hLocal.setArray(topZ.flatten() - dz.flatten()) self.dm.localToGlobal(self.hLocal, self.hGlobal) # Update each layer thicknesses self.stratH[:, : self.stratStep + 1] = newH if self.memclear: del dz, newH, totH, topZ del depth, zlay, cumZ, elev gc.collect() if MPIrank == 0 and self.verbose: print( "Compute Lithology Porosity Values (%0.02f seconds)" % (process_time() - t0), flush=True, ) return
[docs] def stratalRecord(self, indices, weights, onIDs): """ Once the interpolation has been performed, the following function updates the stratigraphic records based on the advected mesh. The function relies on the fortran subroutine strataonesed. In dual-lithology mode the fine pile (`stratHf`, `phiF`) is advected with a second strataonesed call — `stratHf` is a bulk thickness, so the plain weighted interpolation applies directly (the unused `stratathreesed` kernel instead treats its extra fields as 0–1 fractions and renormalises them, which does not match the thickness representation here). :arg indices: indices of the closest nodes used for interpolation :arg weights: weights based on the distances to closest nodes :arg onIDs: index of nodes remaining at the same position. """ # Get local stratal dataset after displacements loc_stratH = self.stratH[:, : self.stratStep] loc_stratZ = self.stratZ[:, : self.stratStep] loc_phiS = self.phiS[:, : self.stratStep] nstratH, nstratZ, nphiS = strataonesed( self.lpoints, self.stratStep, indices, weights, loc_stratH, loc_stratZ, loc_phiS, ) if len(onIDs) > 0: nstratZ[onIDs, :] = loc_stratZ[indices[onIDs, 0], :] nstratH[onIDs, :] = loc_stratH[indices[onIDs, 0], :] nphiS[onIDs, :] = loc_phiS[indices[onIDs, 0], :] if self.stratLith: # Advect the fine pile with the same interpolation (stratHf as # thickness, phiF as porosity); the re-interpolated elevation is # identical to nstratZ above and discarded. loc_stratHf = self.stratHf[:, : self.stratStep] loc_phiF = self.phiF[:, : self.stratStep] nstratHf, _, nphiF = strataonesed( self.lpoints, self.stratStep, indices, weights, loc_stratHf, loc_stratZ, loc_phiF, ) if len(onIDs) > 0: nstratHf[onIDs, :] = loc_stratHf[indices[onIDs, 0], :] nphiF[onIDs, :] = loc_phiF[indices[onIDs, 0], :] provOn = getattr(self, "provOn", False) if provOn: # Advect each provenance class's per-layer thickness with the same # interpolation (a bulk thickness, like stratHf). Linear weights # preserve Σ over classes == stratH (re-imposed below after sync). nstratP = np.zeros((self.lpoints, self.stratStep, self.provNb)) for c in range(self.provNb): loc_P = np.ascontiguousarray(self.stratP[:, : self.stratStep, c]) nP, _, _ = strataonesed( self.lpoints, self.stratStep, indices, weights, loc_P, loc_stratZ, loc_phiS, ) if len(onIDs) > 0: nP[onIDs, :] = loc_P[indices[onIDs, 0], :] nstratP[:, :, c] = nP # Updates stratigraphic records after mesh advection on the edges of each partition # to ensure that all stratigraphic information on the adjacent nodes of the neighbouring # partition are equals on all processors sharing a common number of nodes. for k in range(self.stratStep): self.tmp.setArray(nstratZ[:, k]) self.dm.globalToLocal(self.tmp, self.tmpL) self.stratZ[:, k] = self.tmpL.getArray().copy() self.tmp.setArray(nstratH[:, k]) self.dm.globalToLocal(self.tmp, self.tmpL) self.stratH[:, k] = self.tmpL.getArray().copy() self.tmp.setArray(nphiS[:, k]) self.dm.globalToLocal(self.tmp, self.tmpL) self.phiS[:, k] = self.tmpL.getArray().copy() if self.stratLith: self.tmp.setArray(nstratHf[:, k]) self.dm.globalToLocal(self.tmp, self.tmpL) self.stratHf[:, k] = self.tmpL.getArray().copy() self.tmp.setArray(nphiF[:, k]) self.dm.globalToLocal(self.tmp, self.tmpL) self.phiF[:, k] = self.tmpL.getArray().copy() if provOn: for c in range(self.provNb): self.tmp.setArray(nstratP[:, k, c]) self.dm.globalToLocal(self.tmp, self.tmpL) self.stratP[:, k, c] = self.tmpL.getArray().copy() if provOn: # Re-impose Σ over classes == stratH after the interpolation drift. top = self.stratStep np.clip(self.stratP[:, :top, :], 0.0, None, out=self.stratP[:, :top, :]) psum = self.stratP[:, :top, :].sum(axis=2) scale = np.divide( self.stratH[:, :top], psum, out=np.ones_like(psum), where=psum > 0 ) self.stratP[:, :top, :] *= scale[:, :, None] return