"""The ``umami.Metric`` class calculates metrics on a Landlab model grid."""
from collections import OrderedDict
from copy import deepcopy
import numpy as np
import yaml
import umami.calculations.metric as calcs
from landlab import RasterModelGrid, create_grid
from umami.utils.create_landlab_components import _create_landlab_components
from umami.utils.io import _read_input, _write_output
from umami.utils.validate import _validate_fields, _validate_func
_VALID_FUNCS = calcs.__dict__
[docs]class Metric(object):
"""Create a ``Metric`` class based on a Landlab model grid."""
_required_fields = ["topographic__elevation"]
[docs] def __init__(
self,
grid,
flow_accumulator_kwds=None,
chi_finder_kwds=None,
metrics=None,
):
"""
Parameters
----------
grid : Landlab model grid
flow_accumulator_kwds : dict
Parameters to pass to the Landlab ``FlowAccumulator`` to specify
flow direction and accumulation.
chi_finder_kwds : dict
Parameters to pass to the Landlab ``ChiFinder`` to specify optional
arguments. `
metrics : dict
A dictionary of desired metrics to calculate. See examples for
required format.
Examples
--------
>>> from io import StringIO
>>> from landlab import RasterModelGrid
>>> from umami import Metric
>>> grid = RasterModelGrid((10, 10))
>>> z = grid.add_zeros("node", "topographic__elevation")
>>> z += grid.x_of_node + grid.y_of_node
>>> file_like=StringIO('''
... me:
... _func: aggregate
... method: mean
... field: topographic__elevation
... ep10:
... _func: aggregate
... method: percentile
... field: topographic__elevation
... q: 10
... oid1_mean:
... _func: watershed_aggregation
... field: topographic__elevation
... method: mean
... outlet_id: 1
... sn1:
... _func: count_equal
... field: drainage_area
... value: 1
... ''')
>>> metric = Metric(grid)
>>> metric.add_from_file(file_like)
>>> metric.names
['me', 'ep10', 'oid1_mean', 'sn1']
>>> metric.calculate()
>>> metric.value('me')
9.0
>>> metric.values
[9.0, 5.0, 5.0, 8]
"""
# verify that apppropriate fields are present.
for field in self._required_fields:
if field not in grid.at_node:
msg = "umami: Required field: {field} is missing.".format(
field=field
)
raise ValueError(msg)
# save a reference to the grid.
self._grid = grid
# run FlowAccumulator and ChiFinder
self._fa, self._cf = _create_landlab_components(
self._grid,
chi_finder_kwds=chi_finder_kwds,
flow_accumulator_kwds=flow_accumulator_kwds,
)
# determine which metrics are desired.
self._metrics = OrderedDict(metrics or {})
self._validate_metrics(self._metrics)
@property
def names(self):
"""Names of metrics in metric order."""
self._names = [key for key in self._metrics]
return self._names
[docs] def value(self, name):
"""Get a specific metric value.
Parameters
----------
name: str
Name of desired metric.
"""
return self._values[name]
@property
def values(self):
"""Metric values in metric order."""
return [self._values[key] for key in self._metrics.keys()]
[docs] def add_from_file(self, file):
"""Add metrics to an ``umami.Metric`` from a file.
Parameters
----------
file_like : file path or StringIO
File will be parsed by ``yaml.safe_load`` and converted to an
``OrderedDict``.
"""
params = _read_input(file)
self.add_from_dict(params)
[docs] def add_from_dict(self, params):
"""Add metrics to an ``umami.Metric`` from a dictionary.
Adding metrics through this method does not overwrite already existing
metrics. New metrics are appended to the existing metric list.
Parameters
----------
params : dict or OrderedDict
Keys are metric names and values are a dictionary describing
the creation of the metric. It will be convereted to an OrderedDict
before metrics are added so as to preserve metric order.
"""
new_metrics = OrderedDict(params)
self._validate_metrics(new_metrics)
for key in new_metrics:
self._metrics[key] = new_metrics[key]
[docs] def calculate(self):
"""Calculate metric values.
Calculated metric values are stored in the attribute
``Metric.values``.
"""
self._values = OrderedDict()
for key in self._metrics.keys():
info = deepcopy(self._metrics[key])
_func = info.pop("_func")
function = calcs.__dict__[_func]
if _func in ("chi_gradient", "chi_intercept"):
self._values[key] = function(self._cf)
else:
self._values[key] = function(self._grid, **info)
[docs] def write_metrics_to_file(self, path, style, decimals=3):
"""Write metrics to a file.
Parameters
----------
path :
style : str
yaml, dakota
decimals: int
Number of decimals to round output to.
Examples
--------
>>> from io import StringIO
>>> from landlab import RasterModelGrid
>>> from umami import Metric
>>> grid = RasterModelGrid((10, 10))
>>> z = grid.add_zeros("node", "topographic__elevation")
>>> z += grid.x_of_node + grid.y_of_node
>>> file_like=StringIO('''
... me:
... _func: aggregate
... method: mean
... field: topographic__elevation
... ep10:
... _func: aggregate
... method: percentile
... field: topographic__elevation
... q: 10
... oid1_mean:
... _func: watershed_aggregation
... field: topographic__elevation
... method: mean
... outlet_id: 1
... sn1:
... _func: count_equal
... field: drainage_area
... value: 1
... ''')
First we ouput in *dakota* style, in which each metric is listed on
its own line with its name as a comment.
>>> metric = Metric(grid)
>>> metric.add_from_file(file_like)
>>> metric.calculate()
>>> out = StringIO()
>>> metric.write_metrics_to_file(out, style="dakota")
>>> file_contents = out.getvalue().splitlines()
>>> for line in file_contents:
... print(line.strip())
9.0 me
5.0 ep10
5.0 oid1_mean
8 sn1
Next we output in *yaml* style, in which each metric is serialized in
YAML format.
>>> out = StringIO()
>>> metric.write_metrics_to_file(out, style="yaml")
>>> file_contents = out.getvalue().splitlines()
>>> for line in file_contents:
... print(line.strip())
me: 9.0
ep10: 5.0
oid1_mean: 5.0
sn1: 8
"""
if style == "dakota":
stream = "\n".join(
[
str(np.round(val, decimals=decimals)) + " " + str(key)
for key, val in self._values.items()
]
)
if style == "yaml":
stream = "\n".join(
[
str(key) + ": " + str(np.round(val, decimals=decimals))
for key, val in self._values.items()
]
)
_write_output(path, stream)
[docs] @classmethod
def from_dict(cls, params):
"""Create an umami ``Metric`` from a dictionary.
Parameters
----------
params : dict or OrderedDict
This dict must contain a key *grid*, the values of which will be
passed to the `Landlab` function ``create_grid`` to create the
model grid. It will be convereted to an OrderedDict before metrics
are added so as to preserve metric order.
Examples
--------
>>> from io import StringIO
>>> from umami import Metric
>>> params = {
... "grid": {
... "RasterModelGrid": [
... [10, 10],
... {
... "fields": {
... "node": {
... "topographic__elevation": {
... "plane": [
... {"point": [0, 0, 0]},
... {"normal": [-1, -1, 1]},
... ]
... }
... }
... }
... },
... ]
... },
... "metrics": {
... "me": {
... "_func": "aggregate",
... "method": "mean",
... "field": "topographic__elevation",
... },
... "ep10": {
... "_func": "aggregate",
... "method": "percentile",
... "field": "topographic__elevation",
... "q": 10,
... },
... "oid1_mean": {
... "_func": "watershed_aggregation",
... "field": "topographic__elevation",
... "method": "mean",
... "outlet_id": 1,
... },
... "sn1": {
... "_func": "count_equal",
... "field": "drainage_area",
... "value": 1,
... },
... },
... }
>>> metric = Metric.from_dict(params)
>>> metric.names
['me', 'ep10', 'oid1_mean', 'sn1']
>>> metric.calculate()
>>> metric.value('me')
9.0
>>> metric.values
[9.0, 5.0, 5.0, 8]
"""
# create grid
grid = create_grid(params.pop("grid"))
return cls(grid, **params)
[docs] @classmethod
def from_file(cls, file_like):
"""Create an umami ``Metric`` from a file-like object.
Parameters
----------
file_like : file path or StringIO
File will be parsed by ``yaml.safe_load`` and converted to an
``OrderedDict``.
Returns
-------
umami.Metric
Examples
--------
>>> from io import StringIO
>>> from umami import Metric
>>> file_like=StringIO('''
... grid:
... RasterModelGrid:
... - [10, 10]
... - fields:
... node:
... topographic__elevation:
... plane:
... - point: [0, 0, 0]
... - normal: [-1, -1, 1]
... metrics:
... me:
... _func: aggregate
... method: mean
... field: topographic__elevation
... ep10:
... _func: aggregate
... method: percentile
... field: topographic__elevation
... q: 10
... oid1_mean:
... _func: watershed_aggregation
... field: topographic__elevation
... method: mean
... outlet_id: 1
... sn1:
... _func: count_equal
... field: drainage_area
... value: 1
... ''')
>>> metric = Metric.from_file(file_like)
>>> metric.names
['me', 'ep10', 'oid1_mean', 'sn1']
>>> metric.calculate()
>>> metric.value('me')
9.0
>>> metric.values
[9.0, 5.0, 5.0, 8]
"""
params = _read_input(file_like)
return cls.from_dict(params)
def _validate_metrics(self, metrics):
# look at all _funcs, ensure that they are valid
for key in metrics:
info = metrics[key]
_validate_func(key, info, _VALID_FUNCS)
_validate_fields(self._grid, info)