hspf 2.0.3__py3-none-any.whl → 2.1.0__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.
Binary file
hspf/hbn.py CHANGED
@@ -176,6 +176,12 @@ class hbnInterface:
176
176
  def get_multiple_timeseries(self,t_opn,t_code,t_con,opnids = None,activity = None,axis = 1):
177
177
  return pd.concat([hbn.get_multiple_timeseries(t_opn,t_code,t_con,opnids,activity) for hbn in self.hbns],axis = 1)
178
178
 
179
+ def get_perlnd_constituent(self,constituent,perlnd_ids = None,time_step = 5):
180
+ return get_simulated_perlnd_constituent(self,constituent,time_step)
181
+
182
+ def get_implnd_constituent(self,constituent,implnd_ids = None,time_step = 5):
183
+ return get_simulated_implnd_constituent(self,constituent,time_step)
184
+
179
185
  def get_reach_constituent(self,constituent,reach_ids,time_step,unit = None):
180
186
  if constituent == 'Q':
181
187
  df = get_simulated_flow(self,time_step,reach_ids,unit = unit)
@@ -209,48 +215,17 @@ class hbnInterface:
209
215
 
210
216
  return df
211
217
 
212
-
218
+
213
219
  def get_rchres_data(self,constituent,reach_ids,units = 'mg/l',t_code = 'daily'):
214
220
  '''
215
221
  Convience function for accessing the hbn time series associated with our current
216
222
  calibration method. Assumes you are summing across all dataframes.
217
-
218
- Parameters
219
- ----------
220
- hbn : TYPE
221
- DESCRIPTION.
222
- nutrient_id : TYPE
223
- DESCRIPTION.
224
- reach_ids : TYPE
225
- DESCRIPTION.
226
- flux : TYPE, optional
227
- DESCRIPTION. The default is None.
228
-
229
- Returns
230
- -------
231
- df : TYPE
232
- DESCRIPTION.
233
-
234
- '''
235
-
236
-
223
+ '''
237
224
 
238
- t_cons = helpers.get_tcons(constituent,'RCHRES',units)
239
-
240
-
241
-
242
- df = pd.concat([self.get_multiple_timeseries(t_opn = 'RCHRES',
243
- t_code =t_code,
244
- t_con = t_con,
245
- opnids = reach_ids)
246
- for t_con in t_cons],axis = 1).sum(1).to_frame()
247
-
248
- if (constituent == 'Q') & (units == 'cfs'):
249
- df = df/CF2CFS[t_code]*43560 #Acrfeet/invl to cubic feet/s
250
-
225
+ df = pd.concat([self.get_reach_constituent(constituent,[reach_id],t_code,units) for reach_id in reach_ids], axis = 1)
226
+ df.columns = reach_ids
251
227
  df.attrs['unit'] = units
252
228
  df.attrs['constituent'] = constituent
253
- df.attrs['reach_ids'] = reach_ids
254
229
  return df
255
230
 
256
231
 
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
 
@@ -259,8 +300,8 @@ def routing_reachs(G):
259
300
  def is_routing(G,reach_id):
260
301
  return all([node['type'] not in ['PERLND', 'IMPLND'] for node in adjacent_nodes(G,reach_id)])
261
302
 
262
- def watershed_area(G,reach_ids):
263
- return float(np.nansum(list(nx.get_edge_attributes(make_watershed(G,reach_ids),'area').values())))
303
+ def watershed_area(G,reach_ids,upstream_reach_ids = None):
304
+ return float(np.nansum(list(nx.get_edge_attributes(make_watershed(G,reach_ids,upstream_reach_ids),'area').values())))
264
305
 
265
306
  def catchment_area(G,reach_id):
266
307
  return float(np.nansum(list(nx.get_edge_attributes(make_catchment(G,reach_id),'area').values())))
@@ -300,22 +341,47 @@ def make_catchment(G,reach_id):
300
341
  nx.set_node_attributes(catchment,node_id,'catchment_id')
301
342
  return catchment
302
343
 
303
- from itertools import chain
304
-
305
- def make_watershed(G,reach_ids):
344
+ def make_watershed(G,reach_ids,upstream_reach_ids = None):
306
345
  '''
307
346
  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
347
 
309
348
 
310
349
  '''
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
350
+
351
+ node_ids = set(get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids)
352
+
353
+ # Initialize an empty set to store all unique ancestors
314
354
 
355
+ # Iterate through the target nodes and find ancestors for each
356
+ all_upstream_reaches = set()
357
+ for node_id in node_ids:
358
+ ancestors_of_node = [node['id'] for node in ancestors(G, node_id,'RCHRES')]
359
+ all_upstream_reaches.update(ancestors_of_node) # Add ancestors to the combined set
360
+ all_upstream_reaches.update(node_ids) # Include the target nodes themselves
361
+
362
+ if upstream_reach_ids is not None:
363
+ upstream_node_ids = set(get_node_id(G,'RCHRES',reach_id) for reach_id in upstream_reach_ids)
364
+ for node_id in upstream_node_ids:
365
+ ancestors_of_node = [node['id'] for node in ancestors(G, node_id,'RCHRES')]
366
+ all_upstream_reaches = all_upstream_reaches - set(ancestors_of_node)
367
+ else:
368
+ upstream_node_ids = set()
369
+
370
+ nodes = set(chain.from_iterable([list(G.predecessors(node_id)) for node_id in all_upstream_reaches])) | node_ids
371
+ nodes = nodes - upstream_node_ids # Include the target nodes themselves
372
+
373
+
374
+ return G.subgraph(nodes).copy()
375
+
376
+
377
+ # node_ids = set([get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids if reach_id > 0])
378
+ # nodes_to_exclude = set([get_node_id(G,'RCHRES',abs(reach_id)) for reach_id in reach_ids if reach_id < 0])
379
+ # node_ids = node_ids - nodes_to_exclude
315
380
 
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)))
381
+ #nodes = get_opnids(G,'RCHRES',reach_ids,upstream_reach_ids) #[ancestors(G,node_id,'RCHRES')) for node_id in node_ids]
382
+ nodes = subset_network(G,reach_ids,upstream_reach_ids)
383
+ #nodes.append(node_ids)
384
+ #nodes = list(set(chain.from_iterable(nodes)))
319
385
  watershed = subgraph(G, nodes)
320
386
  catchment_id = '_'.join([str(reach_id) for reach_id in reach_ids])
321
387
  nx.set_node_attributes(watershed,node_ids,catchment_id)
@@ -401,8 +467,17 @@ class Catchment():
401
467
  def dsn(self,tmemn):
402
468
  return [self.catchment.nodes[k[0]]['id'] for k,v in nx.get_edge_attributes(self.catchment,'tmemn').items() if v == tmemn]
403
469
 
404
- def to_dataframe():
405
- return
470
+ def to_dataframe(self):
471
+ edges = []
472
+ for u, v, edge_data in self.catchment.edges(data=True):
473
+ source_node_attributes = self.catchment.nodes[u]
474
+ # Add or update edge attributes with source node attributes
475
+ edge_data["source_type"] = source_node_attributes.get("type")
476
+ edge_data["source_name"] = source_node_attributes.get("name")
477
+ edge_data["source_type_id"] = source_node_attributes.get("type_id")
478
+ edges.append(edge_data)
479
+
480
+ return pd.DataFrame(edges)
406
481
  # def _watershed(G,reach_id):
407
482
 
408
483
  # predecessors = (list(G.predecessors(node)))
@@ -423,7 +498,17 @@ class Catchment():
423
498
 
424
499
  # {source:[node for node in nx.shortest_path(G,source,reach_id)] for source in nx.ancestors(G,reach_id)}
425
500
 
426
-
501
+ def to_dataframe(G):
502
+ edges = []
503
+ for u, v, edge_data in G.edges(data=True):
504
+ source_node_attributes = G.nodes[u]
505
+ # Add or update edge attributes with source node attributes
506
+ edge_data["source_type"] = source_node_attributes.get("type")
507
+ edge_data["source_name"] = source_node_attributes.get("name")
508
+ edge_data["source_type_id"] = source_node_attributes.get("type_id")
509
+ edges.append(edge_data)
510
+
511
+ return pd.DataFrame(edges)
427
512
 
428
513
 
429
514
  #%% Legacy Methods for Backwards compatability
@@ -457,8 +542,16 @@ class reachNetwork():
457
542
  downstream.insert(0,reach_id)
458
543
  return downstream
459
544
 
460
- def calibration_order(self,reach_id,upstream_reach_ids = None):
461
- return calibration_order(self.G,reach_id,upstream_reach_ids)
545
+ def calibration_order(self,reach_ids,upstream_reach_ids = None):
546
+ '''
547
+ Calibration order of reaches to prevent upstream influences. Equivalent to iteritivlye pruning the network remving nodes with no upstream connections.
548
+ A list of lists is returned where each sublist contains reaches that can be calibrated in parallel.
549
+
550
+ :param self: Description
551
+ :param reach_ids: Description
552
+ :param upstream_reach_ids: Description
553
+ '''
554
+ return calibration_order(make_watershed(self.G,reach_ids,upstream_reach_ids))
462
555
 
463
556
  def station_order(self,reach_ids):
464
557
  raise NotImplementedError()
@@ -478,30 +571,30 @@ class reachNetwork():
478
571
  '''
479
572
  return [node['type_id'] for node in predecessors(self.G,'RCHRES',get_node_id(self.G,'RCHRES',reach_id))]
480
573
 
481
- def get_opnids(self,operation,reach_id, upstream_reach_ids = None):
574
+ def get_opnids(self,operation,reach_ids, upstream_reach_ids = None):
482
575
  '''
483
576
  Operation IDs with a path to reach_id. Operations upstream of upstream_reach_ids will not be included
484
577
 
485
578
  '''
486
- return get_opnids(self.G,operation=operation,reach_id = reach_id, upstream_reach_ids = upstream_reach_ids)
487
-
579
+ return get_opnids(self.G,operation,reach_ids,upstream_reach_ids)
488
580
  def operation_area(self,operation,opnids = None):
581
+ '''
582
+ Area of operation type for specified operation IDs. If None returns all operation areas.
583
+ Equivalent to the schematic table filtered by operation and opnids.
584
+ '''
585
+
489
586
  return operation_area(self.uci,operation)
490
587
 
491
588
  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)
589
+ '''
590
+ Docstring for drainage
502
591
 
503
- return pd.DataFrame(edges)
504
-
592
+ :param self: Network class instance
593
+ :param reach_id: Target reach id
594
+ '''
595
+ # Merge source node attributes into edge attributes
596
+ return to_dataframe(make_catchment(self.G,reach_id))
597
+
505
598
  def subwatersheds(self,reach_ids = None):
506
599
  df = subwatersheds(self.uci)
507
600
  if reach_ids is None:
@@ -520,15 +613,16 @@ class reachNetwork():
520
613
  def reach_contributions(self,operation,opnids):
521
614
  return reach_contributions(self.uci,operation,opnids)
522
615
 
523
- def drainage_area(self,reach_ids):
524
- return watershed_area(self.G,reach_ids)
616
+ def drainage_area(self,reach_ids,upstream_reach_ids = None):
617
+ return watershed_area(self.G,reach_ids,upstream_reach_ids)
525
618
 
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']])
619
+ def drainage_area_landcover(self,reach_ids,upstream_reach_ids = None, group = True):
620
+ areas = to_dataframe(make_watershed(self.G,reach_ids,upstream_reach_ids))
621
+ areas = areas.groupby(['source_type','source_type_id','source_name'])['area'].sum()[['PERLND','IMPLND']]
622
+
623
+ if group:
624
+ areas = pd.concat([areas[operation].groupby('source_name').sum() for operation in ['PERLND','IMPLND']])
625
+ #areas = pd.concat([areas[operation].groupby(self.uci.opnid_dict[operation].loc[areas[operation].index,'LSID'].values).sum() for operation in ['PERLND','IMPLND']])
532
626
  return areas
533
627
 
534
628
  def outlets(self):
@@ -546,49 +640,28 @@ class reachNetwork():
546
640
  def paths(self,reach_id):
547
641
  return paths(self.G,reach_id)
548
642
 
549
-
550
- def calibration_order(G,reach_id,upstream_reach_ids = None):
643
+
644
+ def get_opnids(G,operation,reach_ids, upstream_reach_ids = None):
645
+ return get_node_type_ids(make_watershed(G,reach_ids,upstream_reach_ids),operation)
646
+
647
+
648
+ def calibration_order(G):
551
649
  '''
552
- Determines the order in which the specified reaches should be calibrated to
650
+ Determines the order in which the model reaches should be calibrated to
553
651
  prevent upstream influences. Primarily helpful when calibrating sediment and
554
652
  adjusting in channel erosion rates.
555
653
  '''
556
654
 
655
+ nodes = get_node_ids(G,'RCHRES')
656
+ G = G.subgraph(nodes).copy()
557
657
  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]
658
+ while(len(nodes)) > 0:
659
+ nodes_to_remove = [node for node in nodes if G.in_degree(node) == 0]
562
660
  order.append([G.nodes[node]['type_id'] for node in nodes_to_remove])
563
- Gsub.remove_nodes_from(nodes_to_remove)
661
+ nodes = [node for node in nodes if node not in nodes_to_remove]
662
+ G.remove_nodes_from(nodes_to_remove)
564
663
  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
-
664
+
592
665
 
593
666
  def reach_contributions(uci,operation,opnids):
594
667
  schematic = uci.table('SCHEMATIC').set_index('SVOL')
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,
hspf/reports.py CHANGED
@@ -325,6 +325,7 @@ def ann_avg_subwatershed_loading(constituent,uci,hbn):
325
325
  return df
326
326
 
327
327
  def ann_avg_watershed_loading(constituent,reach_ids,uci,hbn, by_landcover = False):
328
+ reach_ids = [item for sublist in [uci.network._upstream(reach_id) for reach_id in reach_ids] for item in sublist]
328
329
  df = ann_avg_constituent_loading(constituent,uci,hbn)
329
330
  df = df.loc[df['TVOLNO'].isin(reach_ids)]
330
331
  if by_landcover:
hspf/uci.py CHANGED
@@ -8,6 +8,7 @@ Created on Mon Jul 11 08:39:57 2022
8
8
 
9
9
  #lines = reader('C:/Users/mfratki/Documents/Projects/LacQuiParle/ucis/LacQuiParle_0.uci')
10
10
  import subprocess
11
+ import sys
11
12
  import numpy as np
12
13
  import pandas as pd
13
14
  from .parser.parsers import Table
@@ -78,7 +79,7 @@ class UCI():
78
79
 
79
80
  def table(self,block,table_name = 'na',table_id = 0,drop_comments = True):
80
81
  # Dynamic parsing of tables when called by user
81
- assert block in ['FILES','PERLND','IMPLND','RCHRES','SCHEMATIC','OPN SEQUENCE','MASS-LINK','EXT SOURCES','NETWORK','GENER','MONTH-DATA','EXT TARGETS','COPY','FTABLES']
82
+ assert block in ['GLOBAL','FILES','PERLND','IMPLND','RCHRES','SCHEMATIC','OPN SEQUENCE','MASS-LINK','EXT SOURCES','NETWORK','GENER','MONTH-DATA','EXT TARGETS','COPY','FTABLES']
82
83
 
83
84
  table = self.uci[(block,table_name,table_id)] #[block][table_name][table_id]
84
85
  #TODO move the format_opnids into the Table class?
@@ -103,7 +104,7 @@ class UCI():
103
104
  self.uci[(block,table_name,table_id)].replace(table)
104
105
 
105
106
  def table_lines(self,block,table_name = 'na',table_id = 0):
106
- return self.uci[(block,table_name,table_id)].lines
107
+ return self.uci[(block,table_name,table_id)].lines.copy()
107
108
 
108
109
  def comments(block,table_name = None,table_id = 0): # comments of a table
109
110
  raise NotImplementedError()
@@ -177,6 +178,43 @@ class UCI():
177
178
  lines += ['END RUN']
178
179
  self.lines = lines
179
180
 
181
+ def set_simulation_period(self,start_year,end_year):
182
+ # Update GLOBAL table with new start and end dates very janky implementation but not a priority.
183
+
184
+ # if start_hour < 10:
185
+ # start_hour = f'0{int(start_hour+1)}:00'
186
+ # else:
187
+ # start_hour = f'{int(start_hour+1)}:00'
188
+
189
+ # if end_hour < 10:
190
+ # end_hour = f'0{int(end_hour+1)}:00'
191
+ # else:
192
+ # end_hour = f'{int(end_hour+1)}:00'
193
+
194
+ table_lines = self.table_lines('GLOBAL')
195
+ for index, line in enumerate(table_lines):
196
+ if '***' in line: #in case there are comments in the global block
197
+ continue
198
+ elif line.strip().startswith('START'):
199
+ table_lines[index] = line[0:14] + f'{start_year}/01/01 00:00 ' + f'END {end_year}/12/31 24:00'
200
+ else:
201
+ continue
202
+
203
+ self.uci[('GLOBAL','na',0)].lines = table_lines
204
+
205
+ def set_echo_flags(self,flag1,flag2):
206
+ table_lines = self.table_lines('GLOBAL')
207
+ for index, line in enumerate(table_lines):
208
+ if '***' in line: #in case there are comments in the global block
209
+ continue
210
+ elif line.strip().startswith('RUN INTERP OUTPT LEVELS'):
211
+ table_lines[index] = f' RUN INTERP OUTPT LEVELS {flag1} {flag2}'
212
+ else:
213
+ continue
214
+
215
+
216
+ self.uci[('GLOBAL','na',0)].lines = table_lines
217
+
180
218
 
181
219
  def _write(self,filepath):
182
220
  with open(filepath, 'w') as the_file:
@@ -211,6 +249,9 @@ class UCI():
211
249
  self.merge_lines()
212
250
  self._write(new_uci_path)
213
251
 
252
+ def _run(self,wait_for_completion=True):
253
+ run_model(self.filepath, wait_for_completion=wait_for_completion)
254
+
214
255
  def update_bino(self,name):
215
256
  #TODO: Move up to busniess/presentation layer
216
257
  table = self.table('FILES',drop_comments = False) # initialize the table
@@ -325,9 +366,25 @@ class UCI():
325
366
 
326
367
  #TODO: More conveince methods that should probably be in a separate module
327
368
 
328
- def run_model(uci_file):
329
- winHSPF = str(Path(__file__).resolve().parent.parent) + '\\bin\\WinHSPFLt\\WinHspfLt.exe'
330
- subprocess.run([winHSPF,uci_file.as_posix()]) #, stdout=subprocess.PIPE, creationflags=0x08000000)
369
+ def run_model(uci_file, wait_for_completion=True):
370
+ winHSPF = str(Path(__file__).resolve().parent.parent) + '\\bin\\WinHSPFlt\\WinHspfLt.exe'
371
+
372
+ # Arguments for the subprocess
373
+ args = [winHSPF, uci_file.as_posix()]
374
+
375
+ if wait_for_completion:
376
+ # Use subprocess.run to wait for the process to complete (original behavior)
377
+ subprocess.run(args)
378
+ else:
379
+ # Use subprocess.Popen to run the process in the background without waiting
380
+ # On Windows, you can use creationflags to prevent a console window from appearing
381
+ if sys.platform.startswith('win'):
382
+ # Use a variable for the flag to ensure it's only used on Windows
383
+ creationflags = subprocess.CREATE_NO_WINDOW
384
+ subprocess.Popen(args, creationflags=creationflags)
385
+ else:
386
+ # For other platforms (like Linux/macOS), Popen without special flags works fine
387
+ subprocess.Popen(args)
331
388
 
332
389
  def get_filepaths(uci,file_extension):
333
390
  files = uci.table('FILES')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hspf
3
- Version: 2.0.3
3
+ Version: 2.1.0
4
4
  Summary: Python package for downloading and running HSPF models
5
5
  Project-URL: Homepage, https://github.com/mfratkin1/pyHSPF
6
6
  Author-email: Mulu Fratkin <michael.fratkin@state.mn.us>
@@ -1,11 +1,12 @@
1
1
  hspf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- hspf/hbn.py,sha256=SQMxWllZy5OxWGMkhmjiardb8vbSjrmENJrorLBqTDI,19476
2
+ hspf/hbn.py,sha256=X-xFTrJ9Z7rM_spMaZxyUIXisUKEm36Gq0gCG-mYuSY,18982
3
3
  hspf/helpers.py,sha256=djKc12ZZkJmB_cHEbFm-mk8sp4GAbBNfjXxfp7YAELU,3132
4
- hspf/hspfModel.py,sha256=8XFPd89niSn9bNTjB2UUpoLNAs6wsD6i6Lb9YKoYjUU,8090
5
- hspf/reports.py,sha256=DfS9DoNwrnD3UvxO879i-bM2gWh5QUMxrV4mdRDgpfE,51878
6
- hspf/uci.py,sha256=towPqQYFO1JC1yNHG5gHoM_8jeO-XueSmClheSth-5k,31612
4
+ hspf/hspfModel.py,sha256=K_xF7HtuMpDMod56Z3IXDCeGsnUi8KGhly_9tm-mxoY,9070
5
+ hspf/reports.py,sha256=ALAeGP0KYdsFUnzAV5BZ784NRDxKgn42GKZpyq3E4xU,51997
6
+ hspf/uci.py,sha256=rsi_KJqdfBFp0rlKCHyhmQGdB_rgNE8k6abTjH26UqE,33982
7
7
  hspf/wdm.py,sha256=q0hNqsMNrTkxHtKEX0q0wWlIZabXv6UX2HjNCF9WEW4,12734
8
8
  hspf/wdmReader.py,sha256=-akKWB9SpUzXvXoQMeHLZNi_u584KaeEOyHB-YolTWM,22848
9
+ hspf/bin/WinHSPFLt/WinHspfLt.exe,sha256=Afs_nJ62r1VnTL2P4XfiRJ1sH2If5DeGTbcCzoqlanE,74752
9
10
  hspf/data/ParseTable.csv,sha256=ExqUaZg_uUPF5XHGLJEk5_jadnDenKjbwqC4d-iNX_M,193609
10
11
  hspf/data/Timeseries Catalog/IMPLND/IQUAL.txt,sha256=r36wt2gYtHKr5SkOcVnpyk5aYZF743AgkJ5o7CvHlIc,1000
11
12
  hspf/data/Timeseries Catalog/IMPLND/IWATER.txt,sha256=JZ03DFMq8e3EcflRSQ_BPYIeKe8TH3WYEUMmTF2OQEs,743
@@ -27,8 +28,8 @@ hspf/data/Timeseries Catalog/RCHRES/OXRX.txt,sha256=NWdRFpJ60LsYzCGHjt8Llay3OI8j
27
28
  hspf/data/Timeseries Catalog/RCHRES/PLANK.txt,sha256=0MAehIrF8leYQt0Po-9h6IiujzoWOlw-ADCV-bPiqs0,3508
28
29
  hspf/data/Timeseries Catalog/RCHRES/SEDTRN.txt,sha256=SiTgD4_YWctTgEfhoMymZfv8ay74xzCRdnI005dXjyE,659
29
30
  hspf/parser/__init__.py,sha256=2HvprGVCaJ9L-egvTj1MI-bekq5CNjtSBZfrCtQi3fs,92
30
- hspf/parser/graph.py,sha256=bAOCkOwubRoETRWlOP_apOFyepV-yHSeCYPYVyuZ2bE,28610
31
- hspf/parser/parsers.py,sha256=xlWB-odGNrArdvd5qwGyvNZ0N8oaVmuNZ6z3gRdHm-g,19796
32
- hspf-2.0.3.dist-info/METADATA,sha256=qyqFAALOQR0L2W62BIsBRD65-CwPF2Ue2iFJEK8-Jdc,605
33
- hspf-2.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- hspf-2.0.3.dist-info/RECORD,,
31
+ hspf/parser/graph.py,sha256=AQkimUP232P3STLFVN2sfrpwiEdGvcc-244TQb8RIgs,32595
32
+ hspf/parser/parsers.py,sha256=x3othxQogUmGNe_ctCU20atDrRM_B4lEbVJb3EMbwto,20850
33
+ hspf-2.1.0.dist-info/METADATA,sha256=SHh9Lng8KdpN2vjaFZe5dFeFI5RsxypQEIrhGlu8G6s,605
34
+ hspf-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
35
+ hspf-2.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any