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/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
- if operation == 'PERLND':
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
- if operation == 'IMPLND':
63
+ elif operation == 'IMPLND':
64
64
  t_cons = {'TSS' :['SLDS'],
65
- 'TKN' :['POQUALNH3+NH4'],
66
- 'N' :['POQUALNO3'],
67
- 'OP' :['POQUALORTHO P'],
68
- 'BOD' :['POQUALBOD'],
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 validate_uci(self):
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
- #self.hbns[file_path.name.split('.')[0]] = None
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,inplace=True)
50
- schematic.loc[:,'TMEMSB2'].replace('',pd.NA,inplace=True)
51
- schematic.loc[:,'MLNO'].replace('',pd.NA,inplace=True)
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,reach_id):
173
- node_id = get_node_id(G,'RCHRES',reach_id)
174
- return G.subgraph([node_id] + list(nx.ancestors(G,node_id))).copy()
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,reach_id,upstream_reach_ids = None):
181
- G = upstream_network(G,reach_id)
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
- for upstream_reach_id in upstream_reach_ids:
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
- from itertools import chain
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
- node_ids = set([get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids if reach_id > 0])
312
- nodes_to_exclude = set([get_node_id(G,'RCHRES',abs(reach_id)) for reach_id in reach_ids if reach_id < 0])
313
- node_ids = node_ids - nodes_to_exclude
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 = [list(nx.ancestors(G,node_id)) for node_id in node_ids]
317
- nodes.append(node_ids)
318
- nodes = list(set(chain.from_iterable(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
- return
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,reach_id,upstream_reach_ids = None):
461
- return calibration_order(self.G,reach_id,upstream_reach_ids)
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,reach_id, upstream_reach_ids = None):
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=operation,reach_id = reach_id, upstream_reach_ids = upstream_reach_ids)
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
- # Merge source node attributes into edge attributes
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
- return pd.DataFrame(edges)
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,reach_id,group = True):
527
- reach_ids = self._upstream(reach_id)
528
- areas = pd.concat([self.subwatershed(reach_id) for reach_id in reach_ids]).groupby(['SVOL','SVOLNO'])['AFACTR'].sum()
529
-
530
- if group:
531
- areas = pd.concat([areas[operation].groupby(self.uci.opnid_dict[operation].loc[areas[operation].index,'LSID'].values).sum() for operation in ['PERLND','IMPLND']])
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 calibration_order(G,reach_id,upstream_reach_ids = None):
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 specified reaches should be calibrated to
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
- Gsub = subgraph(G,get_node_ids(G,'RCHRES'))
559
- while(len(Gsub.nodes)) > 0:
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
- Gsub.remove_nodes_from(nodes_to_remove)
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':defaultParser,
330
+ parserSelector = {'GLOBAL':globalParser,
308
331
  'FILES':standardParser,
309
332
  'OPN SEQUENCE':opnsequenceParser,
310
333
  'PERLND':operationsParser,