hspf 2.0.3__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hspf/Masslink_Timeseries.csv +240 -0
- hspf/bin/WinHSPFLt/WinHspfLt.exe +0 -0
- hspf/build_warehouse.py +545 -0
- hspf/data/HSPFParameterRanges.csv +492 -0
- hspf/data/LandUseNames_Mappings.csv +3330 -0
- hspf/hbn.py +27 -39
- hspf/hbn2.py +316 -0
- hspf/hbn_cy.c +14450 -0
- hspf/hbn_cy.html +1540 -0
- hspf/hbn_cy.pyx +107 -0
- hspf/helpers.py +8 -7
- hspf/hspfModel.py +32 -12
- hspf/parser/graph.py +174 -86
- hspf/parser/parsers.py +25 -2
- hspf/reports.py +265 -459
- hspf/uci.py +62 -5
- hspf/validations.py +211 -0
- hspf/warehouse.py +275 -0
- {hspf-2.0.3.dist-info → hspf-2.1.1.dist-info}/METADATA +1 -1
- {hspf-2.0.3.dist-info → hspf-2.1.1.dist-info}/RECORD +21 -10
- {hspf-2.0.3.dist-info → hspf-2.1.1.dist-info}/WHEEL +1 -1
hspf/hbn_cy.pyx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True
|
|
2
|
+
# hbn_cy.pyx - Cython helpers for reading HBN binary files
|
|
3
|
+
from cpython.bytes cimport PyBytes_AsStringAndSize
|
|
4
|
+
cimport cython
|
|
5
|
+
import numpy as np
|
|
6
|
+
cimport numpy as cnp
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
@cython.inline
|
|
10
|
+
cdef unsigned int _read_uint32_le(const unsigned char* buf, Py_ssize_t offset) nogil:
|
|
11
|
+
"""Reads a little-endian unsigned 32-bit integer."""
|
|
12
|
+
return buf[offset] | (buf[offset+1] << 8) | (buf[offset+2] << 16) | (buf[offset+3] << 24)
|
|
13
|
+
|
|
14
|
+
@cython.boundscheck(False)
|
|
15
|
+
@cython.wraparound(False)
|
|
16
|
+
def map_hbn_file(str file_path):
|
|
17
|
+
"""
|
|
18
|
+
Parses an HBN file from a file path to produce mapn and mapd dictionaries.
|
|
19
|
+
Returns (mapn, mapd, data_bytes).
|
|
20
|
+
"""
|
|
21
|
+
cdef:
|
|
22
|
+
bytes data_bytes
|
|
23
|
+
const unsigned char* cbuf
|
|
24
|
+
Py_ssize_t buf_len, index = 1, i, slen, ln
|
|
25
|
+
unsigned int rectype, tcode, idval, reclen
|
|
26
|
+
unsigned char rc1, rc2, rc3, rc
|
|
27
|
+
dict mapn = {}
|
|
28
|
+
dict mapd = {}
|
|
29
|
+
|
|
30
|
+
with open(file_path, 'rb') as f:
|
|
31
|
+
data_bytes = f.read()
|
|
32
|
+
if not data_bytes:
|
|
33
|
+
raise ValueError(f"File is empty: {file_path}")
|
|
34
|
+
|
|
35
|
+
PyBytes_AsStringAndSize(data_bytes, <char **>&cbuf, &buf_len)
|
|
36
|
+
if cbuf[0] != 0xFD:
|
|
37
|
+
raise ValueError("BAD HBN FILE - must start with magic number 0xFD")
|
|
38
|
+
|
|
39
|
+
while index < buf_len:
|
|
40
|
+
if index + 28 > buf_len: break
|
|
41
|
+
rc1 = cbuf[index]; rc2 = cbuf[index+1]; rc3 = cbuf[index+2]; rc = cbuf[index+3]
|
|
42
|
+
rectype = _read_uint32_le(cbuf, index + 4)
|
|
43
|
+
idval = _read_uint32_le(cbuf, index + 16)
|
|
44
|
+
reclen = (<unsigned int>(rc) * 4194304) + (<unsigned int>(rc3) * 16384) + (<unsigned int>(rc2) * 64) + (<unsigned int>(rc1) >> 2) - 24
|
|
45
|
+
|
|
46
|
+
operation = data_bytes[index+8:index+16].decode('ascii', 'ignore').strip()
|
|
47
|
+
activity = data_bytes[index+20:index+28].decode('ascii', 'ignore').strip()
|
|
48
|
+
|
|
49
|
+
if rectype == 1: # data record
|
|
50
|
+
if index + 36 > buf_len: break
|
|
51
|
+
tcode = _read_uint32_le(cbuf, index + 32)
|
|
52
|
+
key = (operation, idval, activity, int(tcode))
|
|
53
|
+
if key not in mapd: mapd[key] = []
|
|
54
|
+
mapd[key].append((index, reclen))
|
|
55
|
+
elif rectype == 0: # data names record
|
|
56
|
+
key = (operation, idval, activity)
|
|
57
|
+
if key not in mapn: mapn[key] = []
|
|
58
|
+
i = index + 28
|
|
59
|
+
slen = 0
|
|
60
|
+
while slen < reclen:
|
|
61
|
+
if i + slen + 4 > buf_len: break
|
|
62
|
+
ln = _read_uint32_le(cbuf, i + slen)
|
|
63
|
+
if i + slen + 4 + ln > buf_len: break
|
|
64
|
+
name = data_bytes[i + slen + 4 : i + slen + 4 + ln].decode('ascii', 'ignore').strip().replace('-', '')
|
|
65
|
+
mapn[key].append(name)
|
|
66
|
+
slen += 4 + ln
|
|
67
|
+
|
|
68
|
+
if reclen < 36: index += reclen + 29
|
|
69
|
+
else: index += reclen + 30
|
|
70
|
+
|
|
71
|
+
return mapn, mapd, data_bytes
|
|
72
|
+
|
|
73
|
+
@cython.boundscheck(False)
|
|
74
|
+
@cython.wraparound(False)
|
|
75
|
+
def read_data_entries(bytes data_bytes, list entries, int nvals):
|
|
76
|
+
"""
|
|
77
|
+
Reads data entries from the file's bytes. Returns (times, rows_array).
|
|
78
|
+
"""
|
|
79
|
+
cdef:
|
|
80
|
+
const unsigned char* cbuf
|
|
81
|
+
Py_ssize_t buf_len, num_entries = len(entries), k, idx
|
|
82
|
+
unsigned int yr, mo, dy, hr, mn
|
|
83
|
+
cnp.ndarray[cnp.float32_t, ndim=2] rows2d = np.empty((num_entries, nvals), dtype=np.float32)
|
|
84
|
+
list times = [None] * num_entries
|
|
85
|
+
|
|
86
|
+
PyBytes_AsStringAndSize(data_bytes, <char **>&cbuf, &buf_len)
|
|
87
|
+
|
|
88
|
+
for k in range(num_entries):
|
|
89
|
+
idx = entries[k][0] # Get just the index from the (index, reclen) tuple
|
|
90
|
+
|
|
91
|
+
# Boundary check for safety
|
|
92
|
+
if idx + 56 + (nvals * 4) > buf_len: continue
|
|
93
|
+
|
|
94
|
+
yr = _read_uint32_le(cbuf, idx + 36)
|
|
95
|
+
mo = _read_uint32_le(cbuf, idx + 40)
|
|
96
|
+
dy = _read_uint32_le(cbuf, idx + 44)
|
|
97
|
+
hr = _read_uint32_le(cbuf, idx + 48)
|
|
98
|
+
mn = _read_uint32_le(cbuf, idx + 52)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
times[k] = datetime(int(yr), int(mo), int(dy), int(hr) - 1, int(mn))
|
|
102
|
+
except ValueError:
|
|
103
|
+
times[k] = datetime(1900, 1, 1) # Fallback for bad date data
|
|
104
|
+
|
|
105
|
+
rows2d[k] = np.frombuffer(data_bytes, dtype=np.float32, count=nvals, offset=idx + 56)
|
|
106
|
+
|
|
107
|
+
return times, rows2d
|
hspf/helpers.py
CHANGED
|
@@ -53,21 +53,22 @@ def get_tcons(nutrient_name,operation,units = 'mg/l'):
|
|
|
53
53
|
'acrft' : {'Q': ['ROVOL']}}
|
|
54
54
|
|
|
55
55
|
t_cons = MAP[units]
|
|
56
|
-
|
|
56
|
+
elif operation == 'PERLND':
|
|
57
57
|
t_cons = {'TSS' :['SOSED'],
|
|
58
58
|
'TKN' :['POQUALNH3+NH4'],
|
|
59
59
|
'N' :['POQUALNO3'],
|
|
60
60
|
'OP' :['POQUALORTHO P'],
|
|
61
61
|
'BOD' :['POQUALBOD'],
|
|
62
62
|
'Q' : ['PERO']} # BOD is the difference of ptot and ortho
|
|
63
|
-
|
|
63
|
+
elif operation == 'IMPLND':
|
|
64
64
|
t_cons = {'TSS' :['SLDS'],
|
|
65
|
-
'TKN' :['
|
|
66
|
-
'N' :['
|
|
67
|
-
'OP' :['
|
|
68
|
-
'BOD' :['
|
|
65
|
+
'TKN' :['SOQUALNH3+NH4'],
|
|
66
|
+
'N' :['SOQUALNO3'],
|
|
67
|
+
'OP' :['SOQUALORTHO P'],
|
|
68
|
+
'BOD' :['SOQUALBOD'],
|
|
69
69
|
'Q' : ['SURO']} # BOD is the difference of ptot and ortho
|
|
70
|
-
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f'Operation {operation} not recognized for nutrient time constituent lookup.')
|
|
71
72
|
return t_cons[nutrient_name]
|
|
72
73
|
|
|
73
74
|
|
hspf/hspfModel.py
CHANGED
|
@@ -30,7 +30,7 @@ class hspfModel():
|
|
|
30
30
|
|
|
31
31
|
# Imposed structures of an hspf model:
|
|
32
32
|
# 1. all model files are located in the same directory as the uci file.
|
|
33
|
-
def __init__(self,uci_file:str):
|
|
33
|
+
def __init__(self,uci_file:str,run_model:bool = False):
|
|
34
34
|
#wdm_files:list = None,
|
|
35
35
|
#hbn_files:str = None):
|
|
36
36
|
# Inputs
|
|
@@ -39,7 +39,7 @@ class hspfModel():
|
|
|
39
39
|
self.wdm_paths = []
|
|
40
40
|
self.uci_file = Path(uci_file).resolve()
|
|
41
41
|
# Validate and load binary data
|
|
42
|
-
self.validate_uci()
|
|
42
|
+
self.validate_uci(run_model = run_model)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
self.hbns = hbn.hbnInterface(self.hbn_paths)
|
|
@@ -51,8 +51,28 @@ class hspfModel():
|
|
|
51
51
|
# Compositions
|
|
52
52
|
self.reports = Reports(self.uci,self.hbns,self.wdms)
|
|
53
53
|
|
|
54
|
+
|
|
55
|
+
def validate_wdms(self):
|
|
56
|
+
# Ensure wdm files exist and the folders for the other file types exist relative
|
|
57
|
+
# to the uci path
|
|
58
|
+
|
|
59
|
+
for index, row in self.uci.table('FILES',drop_comments = False).iterrows():
|
|
60
|
+
file_path = self.uci_file.parent.joinpath(Path(row['FILENAME']))
|
|
61
|
+
if file_path.suffix.lower() == '.wdm':
|
|
62
|
+
assert file_path.exists(),'File Specified in the UCI does not exist:' + file_path.as_posix()
|
|
63
|
+
self.wdm_paths.append(file_path)
|
|
54
64
|
|
|
55
|
-
def
|
|
65
|
+
def validate_pltgens(self):
|
|
66
|
+
raise NotImplementedError()
|
|
67
|
+
|
|
68
|
+
def validate_folders(self):
|
|
69
|
+
for index, row in self.uci.table('FILES',drop_comments = False).iterrows():
|
|
70
|
+
file_path = self.uci_file.parent.joinpath(Path(row['FILENAME']))
|
|
71
|
+
assert file_path.parent.exists(),'File folder Specified in the UCI does not exist: ' + file_path.as_posix()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_uci(self,run_model:bool = False):
|
|
56
76
|
# Ensure wdm files exist and the folders for the other file types exist relative
|
|
57
77
|
# to the uci path
|
|
58
78
|
|
|
@@ -63,15 +83,15 @@ class hspfModel():
|
|
|
63
83
|
self.wdm_paths.append(file_path)
|
|
64
84
|
elif file_path.suffix.lower() == '.hbn':
|
|
65
85
|
assert file_path.parent.exists(),'File folder Specified in the UCI does not exist: ' + file_path.as_posix()
|
|
66
|
-
|
|
67
|
-
if file_path.exists():
|
|
68
|
-
#self.hbns[file_path.name.split('.')[0]] = hbn.hbnClass(file_path)
|
|
69
|
-
self.hbn_paths.append(file_path)
|
|
70
|
-
else:
|
|
71
|
-
self.run_model()
|
|
86
|
+
self.hbn_paths.append(file_path)
|
|
72
87
|
else:
|
|
73
88
|
assert file_path.parent.exists(),'File folder Specified in the UCI does not exist: ' + file_path.as_posix()
|
|
74
89
|
|
|
90
|
+
if (all(file_path.exists() for file_path in self.hbn_paths)) & (run_model == False):
|
|
91
|
+
pass
|
|
92
|
+
else:
|
|
93
|
+
self.run_model()
|
|
94
|
+
|
|
75
95
|
def run_model(self,new_uci_file = None):
|
|
76
96
|
|
|
77
97
|
if new_uci_file is None:
|
|
@@ -80,14 +100,14 @@ class hspfModel():
|
|
|
80
100
|
# new_uci_file = self.model_path.joinpath(uci_name)
|
|
81
101
|
# self.uci.write(new_uci_file)
|
|
82
102
|
subprocess.run([self.winHSPF,self.uci_file.as_posix()]) #, stdout=subprocess.PIPE, creationflags=0x08000000)
|
|
83
|
-
self.load_uci(new_uci_file)
|
|
103
|
+
self.load_uci(new_uci_file,run_model = False)
|
|
84
104
|
|
|
85
105
|
def load_hbn(self,hbn_name):
|
|
86
106
|
self.hbns[hbn_name] = hbn.hbnClass(self.uci_file.parent.joinpath(hbn_name).as_posix())
|
|
87
107
|
|
|
88
|
-
def load_uci(self,uci_file):
|
|
108
|
+
def load_uci(self,uci_file,run_model:bool = False):
|
|
89
109
|
self.uci = UCI(uci_file)
|
|
90
|
-
self.validate_uci()
|
|
110
|
+
self.validate_uci(run_model = run_model)
|
|
91
111
|
|
|
92
112
|
def convert_wdms(self):
|
|
93
113
|
for wdm_file in self.wdm_paths:
|
hspf/parser/graph.py
CHANGED
|
@@ -4,10 +4,12 @@ Created on Thu Feb 6 14:50:45 2025
|
|
|
4
4
|
|
|
5
5
|
@author: mfratki
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import networkx as nx
|
|
8
9
|
import pandas as pd
|
|
9
10
|
import numpy as np
|
|
10
11
|
import math
|
|
12
|
+
from itertools import chain
|
|
11
13
|
|
|
12
14
|
class Node(object):
|
|
13
15
|
nodes = []
|
|
@@ -18,7 +20,37 @@ class Node(object):
|
|
|
18
20
|
def __str__(self):
|
|
19
21
|
return self._label
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# G = nx.MultiDiGraph()
|
|
26
|
+
# reach_nodes = schematic[['TVOL','TVOLNO']].drop_duplicates().reset_index(drop=True).reset_index()
|
|
27
|
+
# nodes = schematic.loc[schematic['SVOL'].isin(['IMPLND','PERLND','GENER'])][['SVOL','SVOLNO']].reset_index(drop=True).reset_index()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# reach_nodes.rename(columns = {'index':'TNODE'},inplace=True)
|
|
31
|
+
# nodes.rename(columns = {'index':'SNODE','TVOL':'OPERATION','TVOLNO':'OPNID'},inplace=True)
|
|
32
|
+
# [G.add_node(row['TNODE'], id = row['TNODE'], category = 'OPERATION', type_id = row['TVOLNO'], type = row['RCHRES'] ) for node,label in reach_nodes.iterrows()]
|
|
33
|
+
|
|
34
|
+
# df = pd.merge(schematic,reach_nodes,right_on = ['TVOL','TVOLNO'],left_on = ['TVOL','TVOLNO']).reset_index()
|
|
35
|
+
# df.rename(columns = {'index':'SNODE'},inplace=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# for index, row in df.iterrows():
|
|
39
|
+
# if row['SVOL'] == 'GENER':
|
|
40
|
+
# G.add_edge(row['SNODE'],row['TNODE'],
|
|
41
|
+
# mlno = row['MLNO'],
|
|
42
|
+
# count = row['AFACTR'],
|
|
43
|
+
# tmemsb1 = row['TMEMSB1'],
|
|
44
|
+
# tmemsb2 = row['TMEMSB2'])
|
|
45
|
+
# else:
|
|
46
|
+
# G.add_edge(row['SNODE'],row['TNODE'],
|
|
47
|
+
# mlno = row['MLNO'],
|
|
48
|
+
# area = row['AFACTR'],
|
|
49
|
+
# tmemsb1 = row['TMEMSB1'],
|
|
50
|
+
# tmemsb2 = row['TMEMSB2'])
|
|
51
|
+
|
|
52
|
+
# G = nx.from_pandas_edgelist(df,'SNODE','TNODE',edge_attr = True,edge_key = 'SNODE', create_using=nx.MultiDiGraph())
|
|
53
|
+
|
|
22
54
|
def create_graph(uci):
|
|
23
55
|
|
|
24
56
|
|
|
@@ -46,9 +78,9 @@ def create_graph(uci):
|
|
|
46
78
|
# Nodes in the schematic block that are missing from the opn sequence block (usually the outlet reach)
|
|
47
79
|
#schematic.loc[schematic.index.map(labels).isna()]
|
|
48
80
|
schematic = schematic.loc[schematic[['snode','tnode']].dropna().index] # For now remove that missing node
|
|
49
|
-
schematic.loc[:,'TMEMSB1'].replace('',pd.NA
|
|
50
|
-
schematic.loc[:,'TMEMSB2'].replace('',pd.NA
|
|
51
|
-
schematic.loc[:,'MLNO'].replace('',pd.NA
|
|
81
|
+
schematic.loc[:,'TMEMSB1'] = schematic['TMEMSB1'].replace('',pd.NA)
|
|
82
|
+
schematic.loc[:,'TMEMSB2'] = schematic['TMEMSB2'].replace('',pd.NA)
|
|
83
|
+
schematic.loc[:,'MLNO'] = schematic['MLNO'].replace('',pd.NA)
|
|
52
84
|
|
|
53
85
|
schematic = schematic.astype({'snode': int,'tnode':int,'MLNO':pd.Int64Dtype(),'TMEMSB1':pd.Int64Dtype(),'TMEMSB2':pd.Int64Dtype()})
|
|
54
86
|
for index, row in schematic.iterrows():
|
|
@@ -169,19 +201,28 @@ def nodes(G,node_type,node_type_id,adjacent_node_type):
|
|
|
169
201
|
|
|
170
202
|
#%% Methods using node_type, node_type_id interface
|
|
171
203
|
|
|
172
|
-
def upstream_network(G,
|
|
173
|
-
|
|
174
|
-
|
|
204
|
+
def upstream_network(G,reach_ids):
|
|
205
|
+
node_ids = [get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids]
|
|
206
|
+
# Initialize an empty set to store all unique ancestors
|
|
207
|
+
|
|
208
|
+
all_ancestors = set()
|
|
209
|
+
# Iterate through the target nodes and find ancestors for each
|
|
210
|
+
for node_id in node_ids:
|
|
211
|
+
ancestors_of_node = nx.ancestors(G, node_id)
|
|
212
|
+
all_ancestors.update(ancestors_of_node) # Add ancestors to the combined set
|
|
213
|
+
|
|
214
|
+
all_ancestors.update(node_ids) # Include the target nodes themselves
|
|
215
|
+
return G.subgraph(all_ancestors).copy()
|
|
216
|
+
#return G.subgraph([node_id] + list(nx.ancestors(G,node_id))).copy()
|
|
175
217
|
|
|
176
218
|
def downstream_network(G,reach_id):
|
|
177
219
|
node_id = get_node_id(G,'RCHRES',reach_id)
|
|
178
220
|
return G.subgraph([node_id] + list(nx.descendants(G,node_id))).copy()
|
|
179
221
|
|
|
180
|
-
def subset_network(G,
|
|
181
|
-
G = upstream_network(G,
|
|
222
|
+
def subset_network(G,reach_ids,upstream_reach_ids = None):
|
|
223
|
+
G = upstream_network(G,reach_ids)
|
|
182
224
|
if upstream_reach_ids is not None:
|
|
183
|
-
|
|
184
|
-
G.remove_nodes_from(get_node_ids(upstream_network(G,upstream_reach_id),'RCHRES'))
|
|
225
|
+
G.remove_nodes_from(get_node_ids(upstream_network(G,upstream_reach_ids),'RCHRES'))
|
|
185
226
|
#assert([len(sinks(G)) == 0,sinks(G)[0] == reach_id])
|
|
186
227
|
return G
|
|
187
228
|
|
|
@@ -222,6 +263,9 @@ def get_implnd_node(G,implnd_id):
|
|
|
222
263
|
def get_node_type_ids(G,node_type = 'RCHRES'):
|
|
223
264
|
return [data['type_id'] for node, data in G.nodes(data = True) if data['type'] == node_type]
|
|
224
265
|
|
|
266
|
+
def get_node_type_id(G,node_id):
|
|
267
|
+
return G.nodes[node_id]['type_id']
|
|
268
|
+
|
|
225
269
|
def get_reaches(G):
|
|
226
270
|
return get_node_type_ids(G, node_type = 'RCHRES')
|
|
227
271
|
|
|
@@ -259,8 +303,8 @@ def routing_reachs(G):
|
|
|
259
303
|
def is_routing(G,reach_id):
|
|
260
304
|
return all([node['type'] not in ['PERLND', 'IMPLND'] for node in adjacent_nodes(G,reach_id)])
|
|
261
305
|
|
|
262
|
-
def watershed_area(G,reach_ids):
|
|
263
|
-
return float(np.nansum(list(nx.get_edge_attributes(make_watershed(G,reach_ids),'area').values())))
|
|
306
|
+
def watershed_area(G,reach_ids,upstream_reach_ids = None):
|
|
307
|
+
return float(np.nansum(list(nx.get_edge_attributes(make_watershed(G,reach_ids,upstream_reach_ids),'area').values())))
|
|
264
308
|
|
|
265
309
|
def catchment_area(G,reach_id):
|
|
266
310
|
return float(np.nansum(list(nx.get_edge_attributes(make_catchment(G,reach_id),'area').values())))
|
|
@@ -300,22 +344,48 @@ def make_catchment(G,reach_id):
|
|
|
300
344
|
nx.set_node_attributes(catchment,node_id,'catchment_id')
|
|
301
345
|
return catchment
|
|
302
346
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def make_watershed(G,reach_ids):
|
|
347
|
+
def make_watershed(G,reach_ids,upstream_reach_ids = None):
|
|
306
348
|
'''
|
|
307
349
|
Creates a sugraph representing the the catchments upstream of the specified hspf model reaches. Note that a negative reach_ids indicate to subtract that area from the total.
|
|
308
350
|
|
|
309
351
|
|
|
310
352
|
'''
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
353
|
+
|
|
354
|
+
node_ids = set(get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids)
|
|
355
|
+
|
|
356
|
+
# Initialize an empty set to store all unique ancestors
|
|
314
357
|
|
|
358
|
+
# Iterate through the target nodes and find ancestors for each
|
|
359
|
+
all_upstream_reaches = set()
|
|
360
|
+
for node_id in node_ids:
|
|
361
|
+
ancestors_of_node = [node['id'] for node in ancestors(G, node_id,'RCHRES')]
|
|
362
|
+
all_upstream_reaches.update(ancestors_of_node) # Add ancestors to the combined set
|
|
363
|
+
all_upstream_reaches.update(node_ids) # Include the target nodes themselves
|
|
364
|
+
|
|
365
|
+
if upstream_reach_ids is not None:
|
|
366
|
+
upstream_node_ids = set(get_node_id(G,'RCHRES',reach_id) for reach_id in upstream_reach_ids)
|
|
367
|
+
for node_id in upstream_node_ids:
|
|
368
|
+
ancestors_of_node = [node['id'] for node in ancestors(G, node_id,'RCHRES')]
|
|
369
|
+
all_upstream_reaches = all_upstream_reaches - set(ancestors_of_node)
|
|
370
|
+
else:
|
|
371
|
+
upstream_node_ids = set()
|
|
372
|
+
|
|
373
|
+
nodes = set(chain.from_iterable([list(G.predecessors(node_id)) for node_id in all_upstream_reaches])) | node_ids
|
|
374
|
+
nodes = nodes - upstream_node_ids # Include the target nodes themselves
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
return G.subgraph(nodes).copy()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# node_ids = set([get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids if reach_id > 0])
|
|
382
|
+
# nodes_to_exclude = set([get_node_id(G,'RCHRES',abs(reach_id)) for reach_id in reach_ids if reach_id < 0])
|
|
383
|
+
# node_ids = node_ids - nodes_to_exclude
|
|
315
384
|
|
|
316
|
-
nodes = [
|
|
317
|
-
nodes
|
|
318
|
-
nodes
|
|
385
|
+
#nodes = get_opnids(G,'RCHRES',reach_ids,upstream_reach_ids) #[ancestors(G,node_id,'RCHRES')) for node_id in node_ids]
|
|
386
|
+
nodes = subset_network(G,reach_ids,upstream_reach_ids)
|
|
387
|
+
#nodes.append(node_ids)
|
|
388
|
+
#nodes = list(set(chain.from_iterable(nodes)))
|
|
319
389
|
watershed = subgraph(G, nodes)
|
|
320
390
|
catchment_id = '_'.join([str(reach_id) for reach_id in reach_ids])
|
|
321
391
|
nx.set_node_attributes(watershed,node_ids,catchment_id)
|
|
@@ -401,8 +471,17 @@ class Catchment():
|
|
|
401
471
|
def dsn(self,tmemn):
|
|
402
472
|
return [self.catchment.nodes[k[0]]['id'] for k,v in nx.get_edge_attributes(self.catchment,'tmemn').items() if v == tmemn]
|
|
403
473
|
|
|
404
|
-
def to_dataframe():
|
|
405
|
-
|
|
474
|
+
def to_dataframe(self):
|
|
475
|
+
edges = []
|
|
476
|
+
for u, v, edge_data in self.catchment.edges(data=True):
|
|
477
|
+
source_node_attributes = self.catchment.nodes[u]
|
|
478
|
+
# Add or update edge attributes with source node attributes
|
|
479
|
+
edge_data["source_type"] = source_node_attributes.get("type")
|
|
480
|
+
edge_data["source_name"] = source_node_attributes.get("name")
|
|
481
|
+
edge_data["source_type_id"] = source_node_attributes.get("type_id")
|
|
482
|
+
edges.append(edge_data)
|
|
483
|
+
|
|
484
|
+
return pd.DataFrame(edges)
|
|
406
485
|
# def _watershed(G,reach_id):
|
|
407
486
|
|
|
408
487
|
# predecessors = (list(G.predecessors(node)))
|
|
@@ -423,7 +502,17 @@ class Catchment():
|
|
|
423
502
|
|
|
424
503
|
# {source:[node for node in nx.shortest_path(G,source,reach_id)] for source in nx.ancestors(G,reach_id)}
|
|
425
504
|
|
|
426
|
-
|
|
505
|
+
def to_dataframe(G):
|
|
506
|
+
edges = []
|
|
507
|
+
for u, v, edge_data in G.edges(data=True):
|
|
508
|
+
source_node_attributes = G.nodes[u]
|
|
509
|
+
# Add or update edge attributes with source node attributes
|
|
510
|
+
edge_data["source_type"] = source_node_attributes.get("type")
|
|
511
|
+
edge_data["source_name"] = source_node_attributes.get("name")
|
|
512
|
+
edge_data["source_type_id"] = source_node_attributes.get("type_id")
|
|
513
|
+
edges.append(edge_data)
|
|
514
|
+
|
|
515
|
+
return pd.DataFrame(edges)
|
|
427
516
|
|
|
428
517
|
|
|
429
518
|
#%% Legacy Methods for Backwards compatability
|
|
@@ -435,10 +524,20 @@ class reachNetwork():
|
|
|
435
524
|
self.routing_reaches = self._routing_reaches()
|
|
436
525
|
self.lakes = self._lakes()
|
|
437
526
|
self.schematic = uci.table('SCHEMATIC').astype({'TVOLNO': int, "SVOLNO": int, 'AFACTR':float})
|
|
438
|
-
|
|
527
|
+
#self.subwatersheds = self._subwatersheds(self.uci)
|
|
528
|
+
|
|
439
529
|
def get_node_type_ids(self,node_type):
|
|
440
530
|
return get_node_type_ids(self.G, node_type)
|
|
441
531
|
|
|
532
|
+
def watershed_outlets(self):
|
|
533
|
+
reach_ids = []
|
|
534
|
+
for reach_id in self.get_node_type_ids('RCHRES'):
|
|
535
|
+
upstream = self.upstream(reach_id)
|
|
536
|
+
reach_ids.append([reach_id])
|
|
537
|
+
if len(upstream) > 1:
|
|
538
|
+
reach_ids.append(upstream)
|
|
539
|
+
return reach_ids
|
|
540
|
+
|
|
442
541
|
def _upstream(self,reach_id,node_type = 'RCHRES'):
|
|
443
542
|
'''
|
|
444
543
|
Returns list of model reaches upstream of inclusive of reach_id
|
|
@@ -457,8 +556,16 @@ class reachNetwork():
|
|
|
457
556
|
downstream.insert(0,reach_id)
|
|
458
557
|
return downstream
|
|
459
558
|
|
|
460
|
-
def calibration_order(self,
|
|
461
|
-
|
|
559
|
+
def calibration_order(self,reach_ids,upstream_reach_ids = None):
|
|
560
|
+
'''
|
|
561
|
+
Calibration order of reaches to prevent upstream influences. Equivalent to iteritivlye pruning the network remving nodes with no upstream connections.
|
|
562
|
+
A list of lists is returned where each sublist contains reaches that can be calibrated in parallel.
|
|
563
|
+
|
|
564
|
+
:param self: Description
|
|
565
|
+
:param reach_ids: Description
|
|
566
|
+
:param upstream_reach_ids: Description
|
|
567
|
+
'''
|
|
568
|
+
return calibration_order(make_watershed(self.G,reach_ids,upstream_reach_ids))
|
|
462
569
|
|
|
463
570
|
def station_order(self,reach_ids):
|
|
464
571
|
raise NotImplementedError()
|
|
@@ -478,30 +585,30 @@ class reachNetwork():
|
|
|
478
585
|
'''
|
|
479
586
|
return [node['type_id'] for node in predecessors(self.G,'RCHRES',get_node_id(self.G,'RCHRES',reach_id))]
|
|
480
587
|
|
|
481
|
-
def get_opnids(self,operation,
|
|
588
|
+
def get_opnids(self,operation,reach_ids, upstream_reach_ids = None):
|
|
482
589
|
'''
|
|
483
590
|
Operation IDs with a path to reach_id. Operations upstream of upstream_reach_ids will not be included
|
|
484
591
|
|
|
485
592
|
'''
|
|
486
|
-
return get_opnids(self.G,operation
|
|
487
|
-
|
|
593
|
+
return get_opnids(self.G,operation,reach_ids,upstream_reach_ids)
|
|
488
594
|
def operation_area(self,operation,opnids = None):
|
|
595
|
+
'''
|
|
596
|
+
Area of operation type for specified operation IDs. If None returns all operation areas.
|
|
597
|
+
Equivalent to the schematic table filtered by operation and opnids.
|
|
598
|
+
'''
|
|
599
|
+
|
|
489
600
|
return operation_area(self.uci,operation)
|
|
490
601
|
|
|
491
602
|
def drainage(self,reach_id):
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
edges = []
|
|
495
|
-
for u, v, edge_data in make_catchment(self.G,reach_id).edges(data=True):
|
|
496
|
-
source_node_attributes = self.G.nodes[u]
|
|
497
|
-
# Add or update edge attributes with source node attributes
|
|
498
|
-
edge_data["source_type"] = source_node_attributes.get("type")
|
|
499
|
-
edge_data["source_name"] = source_node_attributes.get("name")
|
|
500
|
-
edge_data["source_type_id"] = source_node_attributes.get("type_id")
|
|
501
|
-
edges.append(edge_data)
|
|
603
|
+
'''
|
|
604
|
+
Docstring for drainage
|
|
502
605
|
|
|
503
|
-
|
|
504
|
-
|
|
606
|
+
:param self: Network class instance
|
|
607
|
+
:param reach_id: Target reach id
|
|
608
|
+
'''
|
|
609
|
+
# Merge source node attributes into edge attributes
|
|
610
|
+
return to_dataframe(make_catchment(self.G,reach_id))
|
|
611
|
+
|
|
505
612
|
def subwatersheds(self,reach_ids = None):
|
|
506
613
|
df = subwatersheds(self.uci)
|
|
507
614
|
if reach_ids is None:
|
|
@@ -520,15 +627,16 @@ class reachNetwork():
|
|
|
520
627
|
def reach_contributions(self,operation,opnids):
|
|
521
628
|
return reach_contributions(self.uci,operation,opnids)
|
|
522
629
|
|
|
523
|
-
def drainage_area(self,reach_ids):
|
|
524
|
-
return watershed_area(self.G,reach_ids)
|
|
630
|
+
def drainage_area(self,reach_ids,upstream_reach_ids = None):
|
|
631
|
+
return watershed_area(self.G,reach_ids,upstream_reach_ids)
|
|
525
632
|
|
|
526
|
-
def drainage_area_landcover(self,
|
|
527
|
-
|
|
528
|
-
areas =
|
|
529
|
-
|
|
530
|
-
if group:
|
|
531
|
-
areas = pd.concat([areas[operation].groupby(
|
|
633
|
+
def drainage_area_landcover(self,reach_ids,upstream_reach_ids = None, group = True):
|
|
634
|
+
areas = to_dataframe(make_watershed(self.G,reach_ids,upstream_reach_ids))
|
|
635
|
+
areas = areas.groupby(['source_type','source_type_id','source_name'])['area'].sum()[['PERLND','IMPLND']]
|
|
636
|
+
|
|
637
|
+
if group:
|
|
638
|
+
areas = pd.concat([areas[operation].groupby('source_name').sum() for operation in ['PERLND','IMPLND']])
|
|
639
|
+
#areas = pd.concat([areas[operation].groupby(self.uci.opnid_dict[operation].loc[areas[operation].index,'LSID'].values).sum() for operation in ['PERLND','IMPLND']])
|
|
532
640
|
return areas
|
|
533
641
|
|
|
534
642
|
def outlets(self):
|
|
@@ -546,49 +654,28 @@ class reachNetwork():
|
|
|
546
654
|
def paths(self,reach_id):
|
|
547
655
|
return paths(self.G,reach_id)
|
|
548
656
|
|
|
549
|
-
|
|
550
|
-
def
|
|
657
|
+
|
|
658
|
+
def get_opnids(G,operation,reach_ids, upstream_reach_ids = None):
|
|
659
|
+
return get_node_type_ids(make_watershed(G,reach_ids,upstream_reach_ids),operation)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def calibration_order(G):
|
|
551
663
|
'''
|
|
552
|
-
Determines the order in which the
|
|
664
|
+
Determines the order in which the model reaches should be calibrated to
|
|
553
665
|
prevent upstream influences. Primarily helpful when calibrating sediment and
|
|
554
666
|
adjusting in channel erosion rates.
|
|
555
667
|
'''
|
|
556
668
|
|
|
669
|
+
nodes = get_node_ids(G,'RCHRES')
|
|
670
|
+
G = G.subgraph(nodes).copy()
|
|
557
671
|
order = []
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
nodes_to_remove = [node for node, in_degree in Gsub.in_degree() if in_degree == 0]
|
|
672
|
+
while(len(nodes)) > 0:
|
|
673
|
+
nodes_to_remove = [node for node in nodes if G.in_degree(node) == 0]
|
|
562
674
|
order.append([G.nodes[node]['type_id'] for node in nodes_to_remove])
|
|
563
|
-
|
|
675
|
+
nodes = [node for node in nodes if node not in nodes_to_remove]
|
|
676
|
+
G.remove_nodes_from(nodes_to_remove)
|
|
564
677
|
return order
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
def get_opnids(G,operation,reach_id = None, upstream_reach_ids = None):
|
|
570
|
-
G = subset_network(G,reach_id,upstream_reach_ids)
|
|
571
|
-
return ancestors(G,get_node_id(G,'RCHRES',reach_id),operation)
|
|
572
|
-
perlnds = [node['type_id'] for node in get_nodes(G,'PERLND')]
|
|
573
|
-
implnds = [node['type_id'] for node in get_nodes(G,'IMPLND')]
|
|
574
|
-
reachs = [node['type_id'] for node in get_nodes(G,'RCHRES')]
|
|
575
|
-
return {'RCHRES':reachs,'PERLND':perlnds,'IMPLND':implnds}[operation]
|
|
576
|
-
#return reachs,perlnds,implnds
|
|
577
|
-
|
|
578
|
-
def drainage(uci,reach_ids):
|
|
579
|
-
return subwatersheds(uci).loc[reach_ids].reset_index()[['SVOL','LSID','AFACTR']].groupby(['LSID','SVOL']).sum()
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def drainage_area(uci,reach_ids,drng_area = 0):
|
|
584
|
-
if len(reach_ids) == 0:
|
|
585
|
-
return drng_area
|
|
586
|
-
else:
|
|
587
|
-
sign = math.copysign(1,reach_ids[0])
|
|
588
|
-
reach_id = int(reach_ids[0]*sign)
|
|
589
|
-
drng_area = drng_area + sign*uci.network.drainage_area(reach_id)
|
|
590
|
-
drainage_area(uci,reach_ids[1:],drng_area)
|
|
591
|
-
|
|
678
|
+
|
|
592
679
|
|
|
593
680
|
def reach_contributions(uci,operation,opnids):
|
|
594
681
|
schematic = uci.table('SCHEMATIC').set_index('SVOL')
|
|
@@ -617,6 +704,7 @@ def subwatersheds(uci):
|
|
|
617
704
|
|
|
618
705
|
df = pd.concat(dfs).reset_index()
|
|
619
706
|
df = df.set_index('TVOLNO')
|
|
707
|
+
|
|
620
708
|
return df
|
|
621
709
|
|
|
622
710
|
def subwatershed(uci,reach_id):
|
hspf/parser/parsers.py
CHANGED
|
@@ -6,6 +6,7 @@ Created on Fri Oct 7 12:13:23 2022
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from abc import abstractmethod
|
|
9
|
+
from multiprocessing.util import info
|
|
9
10
|
import numpy as np
|
|
10
11
|
import pandas as pd
|
|
11
12
|
from pathlib import Path
|
|
@@ -289,7 +290,29 @@ class masslinkParser(Parser):
|
|
|
289
290
|
table_lines[index] = line[-1]
|
|
290
291
|
|
|
291
292
|
return table_lines
|
|
292
|
-
|
|
293
|
+
|
|
294
|
+
class globalParser(Parser):
|
|
295
|
+
def parse(block,table_name,table_lines):
|
|
296
|
+
table_lines = [line for line in table_lines if '***' not in line]
|
|
297
|
+
data = {
|
|
298
|
+
'description' : table_lines[0].strip(),
|
|
299
|
+
'start_date' : table_lines[1].split('END')[0].split()[1],
|
|
300
|
+
'start_hour' : int(table_lines[1].split('END')[0].split()[2][:2])-1,
|
|
301
|
+
'end_date' : table_lines[1].strip().split('END')[1].split()[0],
|
|
302
|
+
'end_hour' : int(table_lines[1].strip().split('END')[1].split()[1][:2])-1,
|
|
303
|
+
'echo_flag1' : int(table_lines[2].split()[-2]),
|
|
304
|
+
'echo_flag2' : int(table_lines[3].split()[-1]),
|
|
305
|
+
'units_flag' : int(table_lines[3].split()[5]),
|
|
306
|
+
'resume_flag': int(table_lines[3].split()[1]),
|
|
307
|
+
'run_flag': int(table_lines[3].split()[3])
|
|
308
|
+
}
|
|
309
|
+
df = pd.DataFrame([data])
|
|
310
|
+
df['comments'] = ''
|
|
311
|
+
return df
|
|
312
|
+
|
|
313
|
+
def write(block,table_name,table):
|
|
314
|
+
raise NotImplementedError()
|
|
315
|
+
|
|
293
316
|
class specactionsParser(Parser):
|
|
294
317
|
def parse(block,table,lines):
|
|
295
318
|
raise NotImplementedError()
|
|
@@ -304,7 +327,7 @@ class externalsourcesParser():
|
|
|
304
327
|
def write(block,table,lines):
|
|
305
328
|
raise NotImplementedError()
|
|
306
329
|
|
|
307
|
-
parserSelector = {'GLOBAL':
|
|
330
|
+
parserSelector = {'GLOBAL':globalParser,
|
|
308
331
|
'FILES':standardParser,
|
|
309
332
|
'OPN SEQUENCE':opnsequenceParser,
|
|
310
333
|
'PERLND':operationsParser,
|