Skip to content

Commit

Permalink
troute network abstractions for NHD and Hy Features based networks (#577
Browse files Browse the repository at this point in the history
)

* 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
hellkite500 and David Mattern authored Aug 18, 2022
1 parent 7927a0d commit f6293e2
Show file tree
Hide file tree
Showing 7 changed files with 1,046 additions and 8 deletions.
13 changes: 7 additions & 6 deletions src/troute-network/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@
extra_compile_args=["-g"],
)

package_data = {"troute.network": ["reach.pxd", "__init__.pxd"],
"troute.network.musking": ["mc_reach.pxd", "__init__.pxd"],
package_data = {"troute": ["__init__.pxd"],
"troute.network": ["reach.pxd", "__init__.pxd", "reach_structs.h", "reach_structs.c"],
"troute.network.musking": ["mc_reach.pxd", "__init__.pxd", "mc_reach_structs.h", "mc_reach_structs.c"],
"troute.network.reservoirs":["__init__.pxd"],
"troute.network.reservoirs.levelpool":["__init__.pxd", "levelpool.pxd"],
#"troute.network.reservoirs.hybrid":["__init__.pxd", "hybrid.pxd"],
"troute.network.reservoirs.rfc":["__init__.pxd", "rfc.pxd"],
"troute.network.reservoirs.levelpool":["__init__.pxd", "levelpool.pxd", "levelpool_structs.h", "levelpool_structs.c"],
"troute.network.reservoirs.hybrid":["__init__.pxd", "hybrid.pxd", "hybrid_structs.h", "hybrid_structs.c"],
"troute.network.reservoirs.rfc":["__init__.pxd", "rfc.pxd", "rfc_structs.h", "rfc_structs.c"],
}
ext_modules = [reach, levelpool_reservoirs, rfc_reservoirs, musk]

Expand All @@ -89,7 +90,7 @@
setup(
name="troute.network",
namespace_packages=["troute"],
packages=find_namespace_packages(include=["troute.*"]),#["troute.network", "troute.network.reservoirs", "troute.network.reservoirs.levelpool"],
packages=['troute']+find_namespace_packages(include=["troute.*"]),#["troute.network", "troute.network.reservoirs", "troute.network.reservoirs.levelpool"],
ext_modules=ext_modules,
ext_package="",
package_data=package_data,
Expand Down
275 changes: 275 additions & 0 deletions src/troute-network/troute/AbstractNetwork.py
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)
Loading

0 comments on commit f6293e2

Please sign in to comment.