class FastscapelibContext: """This process takes care of proper initialization, update and clean-up of fastscapelib-fortran internal state. """ shape = xs.foreign(UniformRectilinearGrid2D, 'shape') length = xs.foreign(UniformRectilinearGrid2D, 'length') elevation = xs.foreign(SurfaceTopography, 'elevation') context = xs.variable( intent='out', description='accessor to fastscapelib-fortran internal variables') def initialize(self): fs.fastscape_init() fs.fastscape_set_nx_ny(*np.flip(self.shape)) fs.fastscape_setup() fs.fastscape_set_xl_yl(*np.flip(self.length)) self.context = fs.fastscapecontext @xs.runtime(args='step_delta') def run_step(self, dt): # fastscapelib-fortran runtime routines use dt from context self.context.dt = dt def finalize(self): fs.fastscape_destroy()
class FastscapelibContext: """This process takes care of proper initialization, update and clean-up of fastscapelib-fortran internal state. """ shape = xs.foreign(UniformRectilinearGrid2D, 'shape') length = xs.foreign(UniformRectilinearGrid2D, 'length') ibc = xs.foreign(BorderBoundary, 'ibc') context = xs.any_object( description='accessor to fastscapelib-fortran internal variables') def initialize(self): fs.fastscape_init() fs.fastscape_set_nx_ny(*np.flip(self.shape)) fs.fastscape_setup() fs.fastscape_set_xl_yl(*np.flip(self.length)) fs.fastscape_set_bc(self.ibc) self.context = SerializableFastscapeContext() @xs.runtime(args='step_delta') def run_step(self, dt): # fastscapelib-fortran runtime routines use dt from context self.context["dt"] = dt def finalize(self): fs.fastscape_destroy()
class TerrainDerivatives: """Compute, on demand, terrain derivatives such as slope or curvature. """ shape = xs.foreign(UniformRectilinearGrid2D, 'shape') spacing = xs.foreign(UniformRectilinearGrid2D, 'spacing') elevation = xs.foreign(SurfaceTopography, 'elevation') slope = xs.on_demand(dims=('y', 'x'), description='terrain local slope') curvature = xs.on_demand(dims=('y', 'x'), description='terrain local curvature') @slope.compute def _slope(self): slope = np.empty_like(self.elevation) ny, nx = self.shape dy, dx = self.spacing fs.slope(self.elevation.ravel(), slope.ravel(), nx, ny, dx, dy) return slope @curvature.compute def _curvature(self): curv = np.empty_like(self.elevation) ny, nx = self.shape dy, dx = self.spacing fs.curvature(self.elevation.ravel(), curv.ravel(), nx, ny, dx, dy) return curv
class TwoBlocksUplift: """Set two blocks separated by a clip plane, with different uplift rates. """ x_position = xs.variable( description='position of the clip plane along the x-axis') rate_left = xs.variable(description='uplift rate of the left block') rate_right = xs.variable(description='uplift rate of the right block') shape = xs.foreign(UniformRectilinearGrid2D, 'shape') x = xs.foreign(UniformRectilinearGrid2D, 'x') # TODO: group=['bedrock_forcing_upward', 'surface_forcing_upward'] # see https://github.com/benbovy/xarray-simlab/issues/64 uplift = xs.variable(dims=[(), ('y', 'x')], intent='out', group='any_forcing_upward', description='imposed vertical uplift') def initialize(self): # align clip plane position self._x_idx = np.argmax(self.x > self.x_position) @xs.runtime(args='step_delta') def run_step(self, dt): rate = np.full(self.shape, self.rate_left) rate[:, self._x_idx:] = self.rate_right self.uplift = rate * dt
class LinearDiffusion: """Hillslope erosion by diffusion.""" diffusivity = xs.variable( dims=[(), ('y', 'x')], description='diffusivity (transport coefficient)' ) erosion = xs.variable( dims=('y', 'x'), intent='out', groups='erosion' ) shape = xs.foreign(UniformRectilinearGrid2D, 'shape') elevation = xs.foreign(SurfaceToErode, 'elevation') fs_context = xs.foreign(FastscapelibContext, 'context') def run_step(self): kd = np.broadcast_to(self.diffusivity, self.shape).flatten() self.fs_context["kd"] = kd # we don't use the kdsed fastscapelib-fortran feature directly # see class DifferentialLinearDiffusion self.fs_context["kdsed"] = -1. # bypass fastscapelib-fortran global state self.fs_context["h"] = self.elevation.flatten() fs.diffusion() erosion_flat = self.elevation.ravel() - self.fs_context["h"] self.erosion = erosion_flat.reshape(self.shape)
class OrographicDrainageDischarge(FlowAccumulator): """Accumulate orographic precipitation from upstream to downstream. For use in the context of landscape evolution modeling, ``flowacc`` values are converted from mm^3 h^-1 to m^3 yr^-1. For convenience, the ``discharge`` on demand variable still returns the values in mm^3 h^-1. """ runoff = xs.foreign(OrographicPrecipitation, 'precip_rate') rainfall_frequency = xs.foreign(OrographicPrecipitation, 'rainfall_frequency') discharge = xs.on_demand( dims=('y', 'x'), description='discharge from orographic precipitation', attrs={"units": "mm^3/h"}) def run_step(self): super().run_step() # scale mm m^2/h to m^3/yr assuming that # the rainfall frequency is the number of storm of 1 hour duration per day self.flowacc *= 8.76 * self.rainfall_frequency @discharge.compute def _discharge(self): return self.flowacc / (8.76 * self.rainfall_frequency)
class ExampleProcess: """A process with complete interface for testing.""" in_var = xs.variable(dims=["x", ("x", "y")], description="input variable") out_var = xs.variable(groups="example_group", intent="out") inout_var = xs.variable(intent="inout", converter=int) od_var = xs.on_demand() obj_var = xs.any_object(description="arbitrary object") in_foreign_var = xs.foreign(SomeProcess, "some_var") in_foreign_var2 = xs.foreign(AnotherProcess, "some_var") out_foreign_var = xs.foreign(AnotherProcess, "another_var", intent="out") in_foreign_od_var = xs.foreign(SomeProcess, "some_od_var") in_global_var = xs.global_ref("some_global_var") out_global_var = xs.global_ref("another_global_var", intent="out") group_var = xs.group("some_group") group_dict_var = xs.group_dict("some_group") other_attrib = attr.attrib(init=False, repr=False) other_attr = "this is not a xsimlab variable attribute" @od_var.compute def compute_od_var(self): return 0
class UniformSedimentLayer: """Uniform sediment (or regolith, or soil) layer. This layer has uniform properties (undefined in this class) and generally undergo under active erosion, transport and deposition processes. """ surf_elevation = xs.foreign(SurfaceTopography, 'elevation') bedrock_elevation = xs.foreign(Bedrock, 'elevation') thickness = xs.variable( dims=('y', 'x'), intent='out', description='sediment layer thickness' ) @thickness.compute def _get_thickness(self): return self.surf_elevation - self.bedrock_elevation def initialize(self): self.thickness = self._get_thickness() def run_step(self): self.thickness = self._get_thickness()
class VariableTwoBlockUplift: """compute diferent linear gradient uplift rate along the x or y axis for two blocks separated by a clip plane""" x_position = xsimlab.variable( description='position of the clip plane along the x-axis') uplift_rate_left = xsimlab.variable( description='uplift rate of the left box') uplift_rate_right = xsimlab.variable( description='uplift rate of the right box') gradient_left = xsimlab.variable( description='gradient of the left box uplift') gradient_right = xsimlab.variable( description='gradient of the right box uplift') grid_shape = xsimlab.foreign(RasterGrid2D, 'shape') rate = xsimlab.foreign(BlockUplift, 'rate', intent='out') def run_step(self): mask = np.ones((self.grid_shape[0], self.grid_shape[1])) mask[:, 0:self.x_position] = self.uplift_rate_left mask[:, self.x_position:self.grid_shape[1]] = self.uplift_rate_right gradient = np.ones((self.grid_shape[1])) gradient[0:self.x_position] = np.linspace(self.gradient_left, 1, self.x_position) gradient[self.x_position:self.grid_shape[1]] = np.linspace( self.gradient_right, 1, np.abs(self.grid_shape[1] - self.x_position)) self.rate = mask * gradient
class FixedGridParams: spacing = xs.foreign(UniformGrid1D, "spacing", intent="out") length = xs.foreign(UniformGrid1D, "length", intent="out") def initialize(self): self.spacing = 0.01 self.length = 1.0
class Bedrock: """Update the elevation of bedrock (i.e., land and/or submarine basement). """ elevation = xs.variable(dims=('y', 'x'), intent='inout', description='bedrock elevation') depth = xs.on_demand(dims=('y', 'x'), description='bedrock depth below topographic surface') bedrock_motion_up = xs.foreign(TotalVerticalMotion, 'bedrock_upward') surface_motion_up = xs.foreign(TotalVerticalMotion, 'surface_upward') surface_elevation = xs.foreign(SurfaceTopography, 'elevation') @depth.compute def _depth(self): return self.surf_elevation - self.elevation def initialize(self): if np.any(self.elevation > self.surface_elevation): raise ValueError("Encountered bedrock elevation higher than " "topographic surface elevation.") def run_step(self): self._elevation_next = np.minimum( self.elevation + self.bedrock_motion_up, self.surface_elevation + self.surface_motion_up) def finalize_step(self): self.elevation = self._elevation_next
class InitUFlat: """Flat initial profile of `u`.""" x = xs.foreign(UniformGrid1D, "x") u = xs.foreign(ProfileU, "u", intent="out") def initialize(self): self.u = np.zeros_like(self.x)
class InitUFlat(object): """Flat initial profile of `u`.""" x = xs.foreign(UniformGrid1D, 'x') u = xs.foreign(ProfileU, 'u', intent='out') def initialize(self): self.u = np.zeros_like(self.x)
class BareRockSurface: """Initialize topographic surface as a bare rock surface.""" surf_elevation = xs.foreign(SurfaceTopography, 'elevation') bedrock_elevation = xs.foreign(Bedrock, 'elevation', intent='out') def initialize(self): self.bedrock_elevation = self.surf_elevation.copy()
class P2: var = xs.foreign(P1, "var") cached_var = xs.foreign(P1, "cached_var") view = xs.variable(dims="x", intent="out") cached_view = xs.variable(dims="x", intent="out") def run_step(self): self.view = self.var self.cached_view = self.cached_var
class InitUGauss(object): """Initialize `u` profile using a Gaussian pulse.""" loc = xs.variable(description='location of initial pulse') scale = xs.variable(description='scale of initial pulse') x = xs.foreign(UniformGrid1D, 'x') u = xs.foreign(ProfileU, 'u', intent='out') def initialize(self): self.u = np.exp(-1 / self.scale**2 * (self.x - self.loc)**2)
class InitUGauss: """Initialize `u` profile using a Gaussian pulse.""" loc = xs.variable(description="location of initial pulse", static=True) scale = xs.variable(description="scale of initial pulse", static=True) x = xs.foreign(UniformGrid1D, "x") u = xs.foreign(ProfileU, "u", intent="out") def initialize(self): self.u = np.exp(-1 / self.scale ** 2 * (self.x - self.loc) ** 2)
class FlatSurface: """Initialize surface topography as a flat surface at sea-level with random perturbations (white noise). """ shape = xs.foreign(UniformRectilinearGrid2D, 'shape') elevation = xs.foreign(SurfaceTopography, 'elevation', intent='out') def initialize(self): self.elevation = np.random.rand(*self.shape)
class StreamPowerChannel(ChannelErosion): """Stream-Power channel erosion.""" k_coef = xs.variable(dims=[(), ('y', 'x')], description='bedrock channel incision coefficient') area_exp = xs.variable(description='drainage area exponent') slope_exp = xs.variable(description='slope exponent') shape = xs.foreign(UniformRectilinearGrid2D, 'shape') elevation = xs.foreign(FlowRouter, 'elevation') receivers = xs.foreign(FlowRouter, 'receivers') flowacc = xs.foreign(FlowAccumulator, 'flowacc') fs_context = xs.foreign(FastscapelibContext, 'context') chi = xs.on_demand(dims=('y', 'x'), description='integrated drainage area (chi)') def _set_g_in_context(self): # transport/deposition feature is exposed in subclasses self.fs_context.g1 = 0. self.fs_context.g2 = 0. def run_step(self): kf = np.broadcast_to(self.k_coef, self.shape).flatten() self.fs_context.kf = kf # we don't use kfsed fastscapelib-fortran feature directly self.fs_context.kfsed = -1. self._set_g_in_context() self.fs_context.m = self.area_exp self.fs_context.n = self.slope_exp # bypass fastscapelib_fortran global state self.fs_context.h = self.elevation.flatten() # TODO: https://github.com/fastscape-lem/fastscapelib-fortran/pull/25 # this has no effect yet. self.fs_context.a = self.flowacc.flatten() if self.receivers.ndim == 1: fs.streampowerlawsingleflowdirection() else: fs.streampowerlaw() erosion_flat = self.elevation.ravel() - self.fs_context.h self.erosion = erosion_flat.reshape(self.shape) @chi.compute def _chi(self): chi_arr = np.empty_like(self.elevation, dtype='d') self.fs_context.copychi(chi_arr.ravel()) return chi_arr
class EscarpmentWithPertubation(Escarpment): y = xsimlab.foreign(UniformRectilinearGrid2D, 'y') grid_lenght = xsimlab.foreign(UniformRectilinearGrid2D, 'length') def initialize(self): super(EscarpmentWithPertubation, self).initialize() perturb = np.cos(self.x / self.grid_lenght[1] * 2. * np.pi) self.elevation += perturb
class LocalIsostasyErosionTectonics(BaseLocalIsostasy): """Local isostatic effect of both erosion and tectonic forcing. This process makes no distinction between the density of rock and the density of eroded material (one single coefficient is used). """ erosion = xs.foreign(TotalErosion, 'height') surface_upward = xs.foreign(TectonicForcing, 'surface_upward') def run_step(self): self.rebound = self.i_coef * (self.erosion - self.surface_upward)
class StratigraphicHorizons: """Generate a fixed number of stratigraphic horizons. A horizon is active, i.e., it tracks the evolution of the land/submarine topographic surface until it is "frozen" at a given time. Beyond this freezing (or deactivation) time, the horizon will only be affected by tectonic deformation and/or erosion. To compute diagnostics on those horizons, you can create a subclass where you can add "on_demand" variables. """ freeze_time = xs.variable( dims='horizon', description='horizon freezing (deactivation) time') active = xs.variable(dims='horizon', intent='out', description='whether the horizon is active or not') surf_elevation = xs.foreign(SurfaceTopography, 'elevation') elevation_motion = xs.foreign(TotalVerticalMotion, 'surface_upward') bedrock_motion = xs.foreign(TotalVerticalMotion, 'bedrock_upward') elevation = xs.variable(dims=('horizon', 'y', 'x'), intent='out', description='elevation of horizon surfaces') @xs.runtime(args='sim_start') def initialize(self, start_time): if np.any(self.freeze_time < start_time): raise ValueError("'freeze_time' value must be greater than the " "time of the beginning of the simulation") self.elevation = np.repeat(self.surf_elevation[None, :, :], self.freeze_time.size, axis=0) self.active = np.full_like(self.freeze_time, True, dtype=bool) @xs.runtime(args='step_start') def run_step(self, current_time): self.active = current_time < self.freeze_time def finalize_step(self): elevation_next = self.surf_elevation + self.elevation_motion self.elevation[self.active] = elevation_next self.elevation[~self.active] = np.minimum( self.elevation[~self.active] + self.bedrock_motion, elevation_next)
class InitBasinGeom: """ Give initial basin elevation field as a function of x: z = exp(- (x - a) / b) + c """ init_br = xs.variable(dims="x", description="shift parameter", static=True) x = xs.foreign(UniformGrid1D, "x") z = xs.foreign(ProfileZ, "z", intent="out") h = xs.foreign(ProfileZ, "h", intent="out") def initialize(self): self.h = np.zeros(len(self.x)) #initial sediment thickness is 0 self.z = np.zeros(len(self.x)) + self.init_br
class SurfaceAfterTectonics(SurfaceToErode): """Used for the computation erosion processes after applying tectonic forcing. """ topo_elevation = xs.foreign(SurfaceTopography, 'elevation') forced_motion = xs.foreign(TectonicForcing, 'surface_upward') elevation = xs.variable(dims=('y', 'x'), intent='out', description='surface elevation before erosion') def run_step(self): self.elevation = self.topo_elevation + self.forced_motion
class LocalIsostasyErosion(BaseLocalIsostasy): """Local isostasic effect of erosion.""" erosion = xs.foreign(TotalErosion, 'height') def run_step(self): self.rebound = self.i_coef * self.erosion
class BorderBoundary: status = xs.variable(dims=[(), 'border'], description='node status at borders') border = xs.variable(dims='border', intent='out', description='4-border boundaries coordinate') border_status = xs.variable( dims='border', intent='out', description='node status at the 4-border boundaries') fs_context = xs.foreign(FastscapelibContext, 'context') ibc = xs.variable(intent='out', description='boundary code used by fastscapelib-fortran') def initialize(self): self.border = np.array(['left', 'right', 'top', 'bottom']) self.border_status = np.broadcast_to(self.status, 4) # convert to fastscapelib-fortran ibc code arr_bc = np.array( [1 if st == 'fixed_value' else 0 for st in self.border_status]) # different border order self.ibc = sum(arr_bc * np.array([1, 100, 10, 1000])) self.fs_context.ibc = self.ibc
class SourcePoint(object): """Source point for quantity `u`. The location of the source point is adjusted to coincide with the nearest node the grid. """ loc = xs.variable(description='source location') flux = xs.variable(description='source flux') x = xs.foreign(UniformGrid1D, 'x') u_source = xs.variable(dims='x', intent='out', group='u_vars') @property def nearest_node(self): idx = np.abs(self.x - self.loc).argmin() return idx @property def source_rate(self): src_array = np.zeros_like(self.x) src_array[self.nearest_node] = self.flux return src_array def run_step(self, dt): self.u_source = self.source_rate * dt
class LocalIsostasyTectonics(BaseLocalIsostasy): """Local isostasic effect of tectonic forcing.""" bedrock_upward = xs.foreign(TectonicForcing, 'bedrock_upward') def run_step(self): self.rebound = -1. * self.i_coef * self.bedrock_upward
class DifferentialStreamPowerChannel(StreamPowerChannel): """Stream-Power channel (differential) erosion. Channel incision coefficient may vary depending on whether the topographic surface is bare rock or covered by a soil (sediment) layer. """ k_coef_bedrock = xs.variable( dims=[(), ('y', 'x')], description='bedrock channel incision coefficient') k_coef_soil = xs.variable( dims=[(), ('y', 'x')], description='soil (sediment) channel incision coefficient') k_coef = xs.variable( dims=('y', 'x'), intent='out', description='differential channel incision coefficient') active_layer_thickness = xs.foreign(UniformSedimentLayer, 'thickness') def run_step(self): self.k_coef = np.where(self.active_layer_thickness <= 0., self.k_coef_bedrock, self.k_coef_soil) super(DifferentialStreamPowerChannel, self).run_step()
class SourcePoint: """Source point for quantity `u`. The location of the source point is adjusted to coincide with the nearest node the grid. """ loc = xs.variable(description="source location") flux = xs.variable(description="source flux") x = xs.foreign(UniformGrid1D, "x") u_source = xs.variable(dims="x", intent="out", groups="u_vars") @property def nearest_node(self): idx = np.abs(self.x - self.loc).argmin() return idx @property def source_rate(self): src_array = np.zeros_like(self.x) src_array[self.nearest_node] = self.flux return src_array @xs.runtime(args="step_delta") def run_step(self, dt): self.u_source = self.source_rate * dt