generated from NOAA-OWP/owp-open-source-project-template
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
troute network abstractions for NHD and Hy Features based networks (#577
) * An abstract network interface and base class * An NHD based network for NWM <= 3.0 Routing * A HY_Features based network for ngen NWM routing * add waterbody reading to HYFeaturesNetwork * deprecate extract_waterbody_connections in nhd_network * Clean up hy_features waterbodies * Add basic DataAssimilation base/interfae with NudginDA subclass * Hack for poor input data, sort indicies\! * added check with error raise for empty list of nexus input files * Fix indent error so waterbody wrangling is in correct block * fix indent bug on waterbody data reading * make sure to package C sources needed by other cython packages * Make sure build_tests.py module is installed with the routing package * Make sure to apply node_key_func to all toID values Co-authored-by: David Mattern <dmattern@cheyenne5.cheyenne.ucar.edu>
- Loading branch information
1 parent
7927a0d
commit f6293e2
Showing
7 changed files
with
1,046 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
from abc import ABC, abstractmethod | ||
from functools import partial | ||
import pandas as pd | ||
from datetime import datetime | ||
import time | ||
|
||
from troute.nhd_network import reverse_dict, extract_connections, replace_waterbodies_connections, reverse_network, reachable_network, split_at_waterbodies_and_junctions, split_at_junction, dfs_decomposition | ||
|
||
__verbose__ = False | ||
__showtiming__ = False | ||
|
||
class AbstractNetwork(ABC): | ||
""" | ||
""" | ||
__slots__ = ["_dataframe", "_waterbody_connections", "_gages", | ||
"_terminal_codes", "_connections", "_waterbody_df", | ||
"_waterbody_types_df", "_independent_networks", | ||
"_reaches_by_tw", "_reverse_network", "_q0", "_t0", | ||
"_qlateral", "_break_segments"] | ||
|
||
def __init__(self, cols=None, terminal_code=None, break_points=None, verbose=False, showtiming=False): | ||
global __verbose__, __showtiming__ | ||
__verbose__ = verbose | ||
__showtiming__ = showtiming | ||
if cols: | ||
self._dataframe = self._dataframe[list(cols.values())] | ||
# Rename parameter columns to standard names: from route-link names | ||
# key: "link" | ||
# downstream: "to" | ||
# dx: "Length" | ||
# n: "n" # TODO: rename to `manningn` | ||
# ncc: "nCC" # TODO: rename to `mannningncc` | ||
# s0: "So" # TODO: rename to `bedslope` | ||
# bw: "BtmWdth" # TODO: rename to `bottomwidth` | ||
# waterbody: "NHDWaterbodyComID" | ||
# gages: "gages" | ||
# tw: "TopWdth" # TODO: rename to `topwidth` | ||
# twcc: "TopWdthCC" # TODO: rename to `topwidthcc` | ||
# alt: "alt" | ||
# musk: "MusK" | ||
# musx: "MusX" | ||
# cs: "ChSlp" # TODO: rename to `sideslope` | ||
self._dataframe = self._dataframe.rename(columns=reverse_dict(cols)) | ||
self.set_index("key") | ||
self.sort_index() | ||
self._waterbody_connections = None | ||
self._gages = None | ||
self._connections = None | ||
self._independent_networks = None | ||
self._reverse_network = None | ||
self._reaches_by_tw = None | ||
self._q0 = None | ||
self._t0 = None | ||
self._qlateral = None | ||
#qlat_const = forcing_parameters.get("qlat_const", 0) | ||
#FIXME qlat_const | ||
""" Figure out a good way to default initialize to qlat_const/c | ||
qlat_const = 1.0 | ||
self._qlateral = pd.DataFrame( | ||
qlat_const, | ||
index=self._dataframe.index, | ||
columns=range(nts // qts_subdivisions), | ||
dtype="float32", | ||
) | ||
""" | ||
# there may be off-domain nodes that are not explicitly identified | ||
# but which are terminal (i.e., off-domain) as a result of a mask or some other | ||
# an interior domain truncation that results in a | ||
# otherwise valid node value being pointed to, but which is masked out or | ||
# being intentionally separated into another domain. | ||
self._terminal_codes = set( | ||
self._dataframe[ | ||
~self._dataframe["downstream"].isin(self._dataframe.index) | ||
]["downstream"].values | ||
) | ||
# There can be an externally determined terminal code -- that's this value | ||
self._terminal_codes.add(terminal_code) | ||
|
||
self._break_segments = set() | ||
if break_points: | ||
if break_points["break_network_at_waterbodies"]: | ||
self._break_segments = self._break_segments | set(self.waterbody_connections.values()) | ||
if break_points["break_network_at_gages"]: | ||
self._break_segments = self._break_segments | set(self.gages.values()) | ||
|
||
@property | ||
def network_break_segments(self): | ||
""" | ||
""" | ||
return self._break_segments | ||
|
||
@property | ||
def reverse_network(self): | ||
""" | ||
""" | ||
if self._reverse_network is None: | ||
self._reverse_network = reverse_network(self.connections) | ||
return self._reverse_network | ||
|
||
@property | ||
def independent_networks(self): | ||
""" | ||
""" | ||
if self._independent_networks is None: | ||
# STEP 2: Identify Independent Networks and Reaches by Network | ||
if __showtiming__: | ||
start_time = time.time() | ||
if __verbose__: | ||
print("organizing connections into reaches ...") | ||
|
||
self._independent_networks = reachable_network(self.reverse_network) | ||
|
||
if __verbose__: | ||
print("reach organization complete") | ||
if __showtiming__: | ||
print("... in %s seconds." % (time.time() - start_time)) | ||
return self._independent_networks | ||
|
||
@property | ||
def reaches_by_tailwater(self): | ||
""" | ||
""" | ||
if self._reaches_by_tw is None: | ||
self._reaches_by_tw = {} | ||
for tw, net in self.independent_networks.items(): | ||
if self.network_break_segments: | ||
path_func = partial( | ||
split_at_waterbodies_and_junctions, self.network_break_segments, net | ||
) | ||
else: | ||
path_func = partial(split_at_junction, net) | ||
|
||
self._reaches_by_tw[tw] = dfs_decomposition(net, path_func) | ||
return self._reaches_by_tw | ||
|
||
@property | ||
def waterbody_dataframe(self): | ||
return self._waterbody_df | ||
|
||
@property | ||
def waterbody_types_dataframe(self): | ||
return self._waterbody_types_df | ||
|
||
@property | ||
def connections(self): | ||
if self._connections is None: | ||
self._connections = extract_connections( | ||
self._dataframe, "downstream", terminal_codes=self._terminal_codes | ||
) | ||
return self._connections | ||
|
||
@property | ||
def qlateral(self): | ||
""" | ||
""" | ||
return self._qlateral | ||
|
||
@property | ||
def q0(self): | ||
""" | ||
Initial channel segment flow values | ||
If not set elsewhere, they are 0 | ||
""" | ||
if self._q0 is None: | ||
self._q0 = pd.DataFrame( | ||
0, index=self._dataframe.index, columns=["qu0", "qd0", "h0"], dtype="float32", | ||
) | ||
return self._q0 | ||
|
||
@property | ||
def t0(self): | ||
""" | ||
Time 0 as a datetime object | ||
If not set elsewhere, it defaults to "2015-08-16_00:00:00" | ||
""" | ||
if self._t0 is None: | ||
self._t0 = datetime.strptime("2015-08-16_00:00:00", "%Y-%m-%d_%H:%M:%S") | ||
return self._t0 | ||
|
||
@t0.setter | ||
def t0(self, value): | ||
""" | ||
""" | ||
if isinstance(value, datetime): | ||
self._t0 = value | ||
else: | ||
self._t0 = datetime.strptime(value, "%Y-%m-%d_%H:%M:%S") | ||
|
||
@property | ||
@abstractmethod | ||
def waterbody_connections(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def waterbody_null(self): | ||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def gages(self): | ||
pass | ||
|
||
@property | ||
def dataframe(self): | ||
return self._dataframe | ||
|
||
@property | ||
def terminal_codes(self): | ||
return self._terminal_codes | ||
|
||
def set_synthetic_wb_segments(self, synthetic_wb_segments, synthetic_wb_id_offset): | ||
""" | ||
""" | ||
self._dataframe.reset_index(inplace=True) #reset index so key is now column | ||
# rename the current key column to key32 | ||
key32_d = {"key":"key32"} | ||
self._dataframe = self._dataframe.rename(columns=key32_d) | ||
# create a key index that is int64 | ||
# copy the links into the new column | ||
self._dataframe["key"] = self._dataframe.key32.astype("int64") | ||
# update the values of the synthetic reservoir segments | ||
fix_idx = self._dataframe.key.isin(set(synthetic_wb_segments)) | ||
self._dataframe.loc[fix_idx,"key"] = (self._dataframe[fix_idx].key + synthetic_wb_id_offset).astype("int64") | ||
#Reset key to index | ||
self.set_index("key") | ||
self.sort_index() | ||
|
||
def replace_waterbodies(self): | ||
""" | ||
""" | ||
#Make sure to update held state self._connections | ||
#but pass the property self.connectionsto ensure it gets properly instansiated | ||
#in case it hasn't already been | ||
self._connections = replace_waterbodies_connections( | ||
self.connections, self._waterbody_connections | ||
) | ||
|
||
def set_index(self, key): | ||
""" | ||
""" | ||
#If the index name is already `key`, don't bother | ||
if self._dataframe.index.name != key: | ||
self._dataframe.set_index(key, inplace=True) | ||
|
||
def sort_index(self): | ||
""" | ||
""" | ||
self._dataframe = self._dataframe.sort_index() | ||
|
||
def drop(self, key, axis=1): | ||
""" | ||
FIXME can be problematic to drop keys | ||
before certain properties are intialized... | ||
""" | ||
self._dataframe.drop(key, axis=axis, inplace=True) | ||
|
||
def astype(self, type, columns=None): | ||
""" | ||
""" | ||
if columns: | ||
self._dataframe[columns] = self._dataframe[columns].astype(type) | ||
else: | ||
self._dataframe = self._dataframe.astype(type) |
Oops, something went wrong.