Source code for terrainbento.boundary_handlers.single_node_baselevel_handler
# coding: utf8
# !/usr/env/python
"""**SingleNodeBaselevelHandler** changes elevation for a boundary node."""
import os
import numpy as np
from scipy.interpolate import interp1d
_OTHER_FIELDS = ["bedrock__elevation", "lithology_contact__elevation"]
[docs]class SingleNodeBaselevelHandler(object):
"""Control the elevation of a single open boundary node.
The **SingleNodeBaselevelHandler** controls the elevation of a single open
boundary node, referred to here as the *outlet*. The outlet lowering rate
is specified either as a constant or through a time or through a text file
that specifies the elevation change through time.
The **SingleNodeBaselevelHandler** expects that ``topographic__elevation``
is an at-node model grid field. It will modify this field and, if it
exists, the field ``bedrock__elevation``.
Note that **SingleNodeBaselevelHandler** increments time at the end of the
**run_one_step** method.
"""
[docs] def __init__(
self,
grid,
outlet_id=0,
modify_outlet_id=True,
lowering_rate=None,
lowering_file_path=None,
model_end_elevation=None,
**kwargs
):
"""
Parameters
----------
grid : landlab model grid
outlet_id : int, optional
Node ID of the outlet node. Default value is 0.
modify_outlet_id : boolean, optional
Flag to indicate if the outlet node or all other nodes will be
modified. Default is True, indicating that the outlet node will
be modified.
lowering_rate : float, optional
Lowering rate of the outlet node. One of ``lowering_rate`` and
``lowering_file_path`` is required. Units are implied by the
model grids spatial scale and the time units of ``step``. Negative
values mean that the outlet lowers.
lowering_file_path : str, optional
Lowering lowering history file path. One of ``lowering_rate``
and ``lowering_file_path`` is required. Units are implied by
the model grids spatial scale and the time units of ``step``.
This file should be readable with
``np.loadtxt(filename, skiprows=1, delimiter=",")``
Its first column is time and its second column is the elevation
change at the outlet since the onset of the model run. Negative
values mean the outlet lowers.
model_end_elevation : float, optional
Elevation of the outlet at the end of the model run duration. When
the outlet is lowered based on a lowering_file_path, a
``model_end_elevation`` can be set such that lowering is scaled
based on the starting and ending outlet elevation. Default behavior
is to not scale the lowering pattern.
Examples
--------
Start by creating a landlab model grid.
>>> from landlab import RasterModelGrid
>>> mg = RasterModelGrid((5, 5))
>>> z = mg.add_zeros("node", "topographic__elevation")
>>> print(z.reshape(mg.shape))
[[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]]
Now import the **SingleNodeBaselevelHandler** and instantiate.
>>> from terrainbento.boundary_handlers import (
... SingleNodeBaselevelHandler)
>>> bh = SingleNodeBaselevelHandler(mg,
... outlet_id = 0,
... lowering_rate = -0.1)
>>> bh.run_one_step(10.0)
We should expect that node 0 has lowered by one, to an elevation of -1.
>>> print(z.reshape(mg.shape))
[[-1. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0.]]
More complex baselevel histories can be provided with a
``lowering_file_path``.
"""
# ensure that the outlet has a node status of FIXED_VALUE_BOUNDARY.
grid.status_at_node[outlet_id] = grid.BC_NODE_IS_FIXED_VALUE
self.model_time = 0.0
self.grid = grid
self.outlet_id = outlet_id
self.z = self.grid.at_node["topographic__elevation"]
# determine which nodes to lower
# based on which are lowering, set the prefactor correctly.
self.modify_outlet_id = modify_outlet_id
node_ids = np.arange(grid.number_of_nodes)
if self.modify_outlet_id:
self.nodes_to_lower = node_ids == outlet_id
self.prefactor = 1.0
else:
self.nodes_to_lower = node_ids != outlet_id
self.prefactor = -1.0
self._outlet_start_values = {
"topographic__elevation": self.z[self.outlet_id]
}
for of in _OTHER_FIELDS:
if of in self.grid.at_node:
self._outlet_start_values[of] = self.grid.at_node[of][
self.outlet_id
]
if (lowering_file_path is None) and (lowering_rate is None):
raise ValueError(
(
"SingleNodeBaselevelHandler requires one of "
"lowering_rate and lowering_file_path"
)
)
else:
if lowering_rate is None:
if self.modify_outlet_id is False:
raise ValueError(
"SingleNodeBaselevelHandler currently does not "
"support using a filepath for lowering and "
"'modify_outlet_id'=False'. If this is something you "
"need in your research please create at an issue to "
"discuss developing it."
)
# initialize outlet elevation object
if os.path.exists(lowering_file_path):
model_start_elevation = self.z[self.outlet_id]
elev_change_df = np.loadtxt(
lowering_file_path, skiprows=1, delimiter=","
)
time = elev_change_df[:, 0]
elev_change = elev_change_df[:, 1]
if model_end_elevation is None:
scaling_factor = 1.0
else:
scaling_factor = np.abs(
model_start_elevation - model_end_elevation
) / np.abs(elev_change[0] - elev_change[-1])
outlet_elevation = (
scaling_factor * elev_change_df[:, 1]
) + model_start_elevation
self.outlet_elevation_obj = interp1d(
time, outlet_elevation
)
self.lowering_rate = None
self._outlet_start_z = model_start_elevation
self._outlet_effective_z = model_start_elevation
else:
raise ValueError(
(
"The lowering_file_path provided "
"to SingleNodeBaselevelHandler does not "
"exist."
)
)
elif lowering_file_path is None:
self.lowering_rate = lowering_rate
self.outlet_elevation_obj = None
else:
raise ValueError(
(
"Both an lowering_rate and a "
"lowering_file_path have been provided "
"to SingleNodeBaselevelHandler. Please provide "
"only one."
)
)
[docs] def run_one_step(self, step):
"""Run **SingleNodeBaselevelHandler** to update outlet node elevation.
The **run_one_step** method provides a consistent interface to update
the terrainbento boundary condition handlers.
In the **run_one_step** routine, the **SingleNodeBaselevelHandler**
will change the elevation of the outlet node based on inputs specified
at instantiation.
Note that **SingleNodeBaselevelHandler** increments time at the end of
the **run_one_step** method.
Parameters
----------
step : float
Duration of model time to advance forward.
"""
# first, if we do not have an outlet elevation object
if self.outlet_elevation_obj is None:
# calculate lowering amount and subtract
self.z[self.nodes_to_lower] += (
self.prefactor * self.lowering_rate * step
)
# if bedrock__elevation exists as a field, lower it also
for of in _OTHER_FIELDS:
if of in self.grid.at_node:
self.grid.at_node[of][self.nodes_to_lower] += (
self.prefactor * self.lowering_rate * step
)
if self.modify_outlet_id is False:
for key in self._outlet_start_values.keys():
self.grid.at_node[key][
self.outlet_id
] = self._outlet_start_values[key]
# if there is an outlet elevation object
else:
# if bedrock__elevation exists as a field, lower it also
# calcuate the topographic change required to match the current
# time"s value for outlet elevation. This must be done in case
# bedrock elevation exists, and must
# be done before the topography is lowered
topo_change = self.z[self.outlet_id] - self.outlet_elevation_obj(
self.model_time
)
other_fields = [
"bedrock__elevation",
"lithology_contact__elevation",
]
for of in other_fields:
if of in self.grid.at_node:
self.grid.at_node[of][self.outlet_id] -= topo_change
# lower topography
self.z[self.outlet_id] -= topo_change
# increment model time
self.model_time += step