hspf 2.0.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.
hspf/parser/graph.py ADDED
@@ -0,0 +1,934 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Thu Feb 6 14:50:45 2025
4
+
5
+ @author: mfratki
6
+ """
7
+ import networkx as nx
8
+ import pandas as pd
9
+ import numpy as np
10
+ import math
11
+
12
+ class Node(object):
13
+ nodes = []
14
+
15
+ def __init__(self, label):
16
+ self._label = label
17
+
18
+ def __str__(self):
19
+ return self._label
20
+
21
+ # class PerlndNode(Node):
22
+ # raise NotImplementedError
23
+
24
+ # class ReachNode(Node):
25
+ # raise NotImplementedError
26
+
27
+ # class ImplndNode(Node):
28
+ # raise NotImplementedError
29
+
30
+ # class SourceNode(Node):
31
+ # raise NotImplementedError
32
+
33
+ # class TargetNode(Node):
34
+ # raise NotImplementedError
35
+
36
+ # class MetNode(Node):
37
+ # raise NotImplementedError
38
+
39
+
40
+
41
+ # class wdmNode(Node):
42
+ # raise NotImplementedError
43
+
44
+
45
+ # # Add Parameter Nodes Add edges at same time since it's expensive to determine associated plern/implnd/reach node
46
+ # keys = [key for key in uci.uci.keys() if key[0] in ['IMPLND','RCHRES','PERLND']]
47
+ # for operation,table_name,table_id in keys:
48
+ # parms = uci.table(operation,table_name,table_id)
49
+ # for opnid, row in parms.iterrows():
50
+ # target_node = graph.get_node(G,operation,opnid)
51
+ # for parameter in row.index:
52
+ # G.add_node(max(G.nodes) + 1, type = 'Parameter', value = row[parameter], name = parameter, operation = operation, table_name = table_name, table_id = table_id)
53
+ # #labels[(operation,parameter,table_id)] = [max(G.nodes)]
54
+ # G.add_edge(max(G.nodes), target_node)
55
+
56
+
57
+
58
+ def create_graph(uci):
59
+
60
+
61
+ # Define Node labels
62
+ opn_sequence = uci.table('OPN SEQUENCE').reset_index(drop=True)
63
+ opn_sequence.set_index(['OPERATION','SEGMENT'],inplace=True)
64
+ opn_sequence_labels = opn_sequence.index.drop_duplicates().to_list()
65
+ G = nx.MultiDiGraph()
66
+ [G.add_node(node, id = node, category = 'OPERATION', type_id = label[1], type = label[0] ) for node,label in enumerate(opn_sequence_labels)]
67
+
68
+ # ext_sources = uci.table('EXT SOURCES').reset_index(drop=True)
69
+ # ext_sources.set_index(['SVOL','SVOLNO'],inplace=True)
70
+ # ext_sources_labels = ext_sources.index.drop_duplicates().to_list()
71
+ # [G.add_node(max(G.nodes) + 1,id = max(G.nodes) + 1, type = label[0], type_id = label[1], category = 'WDM') for node,label in enumerate(ext_sources_labels)]
72
+
73
+ labels = {v: i for i, v in enumerate(opn_sequence_labels)}# + ext_sources_labels)}
74
+
75
+ #Define edges from Schematic Block
76
+ schematic = uci.table('SCHEMATIC').reset_index(drop=True).set_index(['SVOL','SVOLNO'])
77
+ schematic['snode'] = schematic.index.map(labels)
78
+ schematic.reset_index(inplace=True)
79
+ schematic = schematic.set_index(['TVOL','TVOLNO'])
80
+ schematic['tnode'] = schematic.index.map(labels)
81
+ schematic.reset_index(inplace=True)
82
+ # Nodes in the schematic block that are missing from the opn sequence block (usually the outlet reach)
83
+ #schematic.loc[schematic.index.map(labels).isna()]
84
+ schematic = schematic.loc[schematic[['snode','tnode']].dropna().index] # For now remove that missing node
85
+ schematic.loc[:,'TMEMSB1'].replace('',pd.NA,inplace=True)
86
+ schematic.loc[:,'TMEMSB2'].replace('',pd.NA,inplace=True)
87
+ schematic.loc[:,'MLNO'].replace('',pd.NA,inplace=True)
88
+
89
+ schematic = schematic.astype({'snode': int,'tnode':int,'MLNO':pd.Int64Dtype(),'TMEMSB1':pd.Int64Dtype(),'TMEMSB2':pd.Int64Dtype()})
90
+ for index, row in schematic.iterrows():
91
+ if row['SVOL'] == 'GENER':
92
+ G.add_edge(row['snode'],row['tnode'],
93
+ mlno = row['MLNO'],
94
+ count = row['AFACTR'],
95
+ tmemsb1 = row['TMEMSB1'],
96
+ tmemsb2 = row['TMEMSB2'])
97
+ else:
98
+ G.add_edge(row['snode'],row['tnode'],
99
+ mlno = row['MLNO'],
100
+ area = row['AFACTR'],
101
+ tmemsb1 = row['TMEMSB1'],
102
+ tmemsb2 = row['TMEMSB2'])
103
+
104
+ # _ = [G.add_edge(row['snode'],row['tnode'],
105
+ # mlno = row['MLNO'],
106
+ # area = row['AFACTR'],
107
+ # tmemsb1 = row['TMEMSB1'],
108
+ # tmemsb2 = row['TMEMSB2']) for index, row in schematic.iterrows()]
109
+
110
+
111
+
112
+ #Define edges from Ext Sources
113
+ # ext_sources['snode'] = ext_sources.index.map(labels)
114
+ # ext_sources.set_index(['TVOL','TOPFST'],inplace=True)
115
+ # ext_sources['tnode'] = ext_sources.index.map(labels)
116
+ # _ = [G.add_edge(row['snode'],row['tnode'],
117
+ # smemn = row['SMEMN'],
118
+ # smemsb = row['SMEMSB'],
119
+ # mfactor = row['MFACTOR'],
120
+ # tran = row['TRAN'],
121
+ # tmemn = row['TMEMN'],
122
+ # tmemsb1 = row['TMEMSB1'],
123
+ # tmemsb2 = row['TMEMSB2']) for index, row in ext_sources.iterrows()]
124
+
125
+
126
+
127
+ # Add property information
128
+ geninfo = uci.table('PERLND','GEN-INFO')
129
+ for index,row in geninfo.iterrows():
130
+ G.nodes[labels[('PERLND',index)]]['name'] = row['LSID']
131
+
132
+
133
+ geninfo = uci.table('IMPLND','GEN-INFO')
134
+ for index,row in geninfo.iterrows():
135
+ G.nodes[labels[('IMPLND',index)]]['name'] = row['LSID']
136
+
137
+ geninfo = uci.table('RCHRES','GEN-INFO')
138
+ for index,row in geninfo.iterrows():
139
+ G.nodes[labels[('RCHRES',index)]]['name'] = row['RCHID']
140
+ G.nodes[labels[('RCHRES',index)]]['lkfg'] = row['LKFG']
141
+
142
+
143
+ # # Add property information
144
+ # bininfo = uci.table('PERLND','BINARY-INFO')
145
+ # for index,row in geninfo.iterrows():
146
+ # G.nodes[labels[('PERLND',index)]]['name'] = row['LSID']
147
+
148
+ # bininfo = uci.table('IMPLND','BINARY-INFO')
149
+ # for index,row in geninfo.iterrows():
150
+ # G.nodes[labels[('IMPLND',index)]]['name'] = row['LSID']
151
+
152
+ # bininfo = uci.table('RCHRES','BINARY-INFO')
153
+ # for index,row in geninfo.iterrows():
154
+ # G.nodes[labels[('RCHRES',index)]]['name'] = row['RCHID']
155
+ # G.nodes[labels[('RCHRES',index)]]['lkfg'] = row['LKFG']
156
+
157
+
158
+ # labels = {}
159
+ # for n, d in G.nodes(data=True):
160
+ # l = (d['operation'],d['opnid'])
161
+ # labels[l] = labels.get(l, [])
162
+ # labels[l].append(n)
163
+
164
+
165
+
166
+ G.labels = labels
167
+ return G
168
+
169
+
170
+ # def create_subgraph(G,start_node):
171
+ # sub_G = nx.MultDiGraph()
172
+ # for n in G.successors_iter(start_node):
173
+ # sub_G.add_path([start_node,n])
174
+ # create_subgraph(G,sub_G,n)
175
+
176
+ # Binary info
177
+
178
+ """
179
+ CREATE TABLE Operation (
180
+ opn VARCHAR,
181
+ opnid INTEGER,
182
+ PRIMARY KEY (opn, opnid)
183
+
184
+
185
+
186
+ )
187
+
188
+
189
+
190
+
191
+ """
192
+
193
+
194
+
195
+
196
+ """
197
+ CREATE TABLE Files (
198
+ ftype VARCHAR,
199
+ unit INTEGER NOT NULL PRIMARY KEY,,
200
+ filename VARCHAR
201
+ );
202
+
203
+ """
204
+
205
+
206
+ """
207
+ CREATE TABLE GenInfo (
208
+ pk INTEGER NOT NULL,
209
+ opn VARCHAR,
210
+ opnid INTEGER,
211
+ PRIMARY KEY (opn, opnid)
212
+ iunits INTEGER,
213
+ ounits INTEGER,
214
+ punit1 INTEGER,
215
+ punit2 INTEGER,
216
+ BUNIT1 INTEGER,
217
+ BUNIT2 INTEGER
218
+ );
219
+
220
+ """
221
+
222
+ # # Files
223
+ # files = uci.table('FILES')
224
+ # files['FTYPE'] = files['FTYPE'].replace({'WDM': 'WDM1'})
225
+ # dfs = []
226
+
227
+ # # PerlndInfo
228
+ # operation = 'PERLND'
229
+ # geninfo = uci.table(operation,'GEN-INFO')
230
+ # binaryinfo = uci.table(operation,'BINARY-INFO')
231
+ # if operation == 'RCHRES':
232
+ # geninfo = geninfo.rename(columns = {'RCHID':'LSID',
233
+ # 'BUNITE':'BUNIT1',
234
+ # 'BUNITM': 'BUNIT2',
235
+ # 'PUNITE': 'PUNIT1',
236
+ # 'PUNITM': 'PUNIT2'})
237
+ # df = pd.merge(geninfo,binaryinfo, left_index = True, right_index = True, how = 'outer').reset_index()
238
+ # df.insert(0,'OPN',operation)
239
+ # df = pd.merge(df,files, left_on = 'BUNIT1', right_on = 'UNIT')
240
+
241
+ # # Schematic Table
242
+ # schematic = uci.table('SCHEMATIC')
243
+
244
+ # # Masslink Table
245
+ # masslinks = []
246
+ # for table_name in uci.table_names('MASS-LINK'):
247
+ # mlno = table_name.split('MASS-LINK')[1]
248
+ # masslink = uci.table('MASS-LINK',table_name)
249
+ # masslink.insert(0,'MLNO',mlno)
250
+ # masslinks.append(masslink)
251
+ # masslinks = pd.concat(masslinks)
252
+
253
+ # #masslinks['QUALID'] = (masslinks['SMEMSB1'].str.strip().replace('','0').astype(int)-1).replace(-1,pd.NA)
254
+
255
+
256
+
257
+ # hbn_name = uci.table('PERLND','QUAL-PROPS', int(row['SMEMSB1']) - 1).iloc[0]['QUALID']
258
+
259
+ # operation = row['SVOL']
260
+ # activity = row['SGRPN']
261
+ # ts_name = row['SMEMN']
262
+
263
+ # hbn_name = row['SMEMN'] + hbn_name
264
+
265
+
266
+ # schematic = uci.table('SCHEMATIC')
267
+ # schematic = pd.merge(schematic,masslinks,left_on = 'MLNO',right_on = 'MLNO')
268
+
269
+ # all(schematic['SVOL_x'] == schematic['SVOL_y'])
270
+ # all(schematic['TVOL_x'] == schematic['TVOL_y'])
271
+ # schematic.loc[schematic['TMEMSB1_x'] == '', 'TMEMSB1_y'] = schematic['TMEMSB1_x']
272
+ # schematic.loc[schematic['TMEMSB2_x'] == '', 'TMEMSB2_y'] = schematic['TMEMSB2_x']
273
+
274
+ # schematic = schematic.drop(columns=['TMEMSB1_y','TMEMSB2_y','TVOL_y','SVOL_y'])
275
+ # schematic = schematic.rename(columns = {'SVOL_x':'SVOL',
276
+ # 'TVOL_x':'TVOL',
277
+ # 'TMEMSB2_x':'TMEMSB2',
278
+ # 'TMEMSB1_x':'TMEMSB1'})
279
+
280
+
281
+
282
+ # # Watershed Weighted Mean
283
+ # subwatersheds = uci.network.subwatersheds()
284
+ # subwatersheds = subwatersheds.loc[subwatersheds['SVOL'] == 'PERLND'].reset_index()
285
+ # df = cal.model.hbns.get_multiple_timeseries('PERLND',5,'PERO',test['SVOLNO'].values).mean().reset_index()
286
+ # df.columns = ['OPNID','value']
287
+ # weighted_mean = df[['value','AFACTR']].groupby(df['LSID']).apply(lambda x: (x['value'] * x['AFACTR']).sum() / x['AFACTR'].sum())
288
+ # weighted_mean.loc['combined'] = (df['value'] * df['AFACTR']).sum() / df['AFACTR'].sum()
289
+
290
+
291
+ # # annual weighted timeseries watershed
292
+ # reach_ids = [103,119,104,118]
293
+ # subwatersheds = uci.network.subwatersheds().loc[reach_ids]
294
+ # subwatersheds = subwatersheds.loc[subwatersheds['SVOL'] == 'PERLND'].reset_index()
295
+ # df = cal.model.hbns.get_multiple_timeseries('PERLND',5,'PERO',test['SVOLNO'].values).mean().reset_index()
296
+ # df.columns = ['OPNID','value']
297
+ # df = pd.merge(subwatersheds,df,left_on = 'SVOLNO', right_on='OPNID')
298
+ # weighted_mean = (df['value'] * df['AFACTR']).sum() / df['AFACTR'].sum()
299
+ # df[f'weighted_{ts_name}'] = df.groupby('LSID')[parameter].transform(lambda x: (x * df.loc[x.index, 'AFACTR']).sum() / df.loc[x.index, 'AFACTR'].sum())
300
+ # weighted_mean.loc['combined'] = (df['value'] * df['AFACTR']).sum() / df['AFACTR'].sum()
301
+
302
+
303
+
304
+ # # parameter average weighted by landcover area
305
+ # table_name = 'PWAT-PARM2'
306
+ # parameter = 'LZSN'
307
+ # table_id = 0
308
+ # operation = 'PERLND'
309
+
310
+ # subwatersheds = uci.network.subwatersheds()
311
+ # subwatersheds = subwatersheds.loc[subwatersheds['SVOL'] == 'PERLND'].reset_index()
312
+ # df = uci.table(operation,table_name,table_id)[parameter].reset_index()
313
+ # df.columns = ['OPNID',parameter]
314
+ # df = pd.merge(subwatersheds,df,left_on = 'SVOLNO', right_on='OPNID')
315
+ # df[f'weighted_{parameter}'] = df.groupby('TVOLNO')[parameter].transform(lambda x: (x * df.loc[x.index, 'AFACTR']).sum() / df.loc[x.index, 'AFACTR'].sum())
316
+
317
+
318
+ # #df[f'weighted_{parameter}'] = df.groupby('LSID')[parameter].transform(lambda x: (x * df.loc[x.index, 'AFACTR']).sum() / df.loc[x.index, 'AFACTR'].sum())
319
+
320
+
321
+
322
+
323
+ # extsources = uci.table('EXT SOURCES')
324
+ # extsources['SVOL'] = extsources['SVOL'].replace({'WDM': 'WDM1'})
325
+
326
+ # df = pd.merge(extsources,df,left_on = 'SVOL',right_on = 'FTYPE',how = 'right')
327
+
328
+
329
+ # exttargets = uci.table('EXT TARGETS')
330
+ # schematic = uci.table('SCHEMATIC')
331
+
332
+
333
+ #%% Methods using universal node id
334
+
335
+
336
+ def _add_subgraph_labels(G,G_sub):
337
+ G_sub.labels = {label:node for label, node in G.labels.items() if node in G_sub.nodes}
338
+ return G_sub
339
+
340
+ def subgraph(G,nodes):
341
+ def add_subgraph_labels(G,G_sub):
342
+ G_sub.labels = {label:node for label, node in G.labels.items() if node in G_sub.nodes}
343
+ return G_sub
344
+ return add_subgraph_labels(G,G.subgraph(nodes).copy())
345
+
346
+ def _predecessors(G,node_id:int):
347
+ return [G.nodes[node] for node in G.predecessors(node_id)]
348
+
349
+ def _successors(G, node_id:int):
350
+ return [G.nodes[node] for node in G.successors(node_id)]
351
+
352
+ def _ancestors(G,node_id:int):
353
+ '''
354
+ Returns a list of nodes reachable from node node_id
355
+ '''
356
+ #set(nx.get_node_attributes(G,'type').values()) # Get all node types
357
+ return [G.nodes[node] for node in list(nx.ancestors(G,node_id))]
358
+
359
+ def _descendants(G,node_id:int):
360
+ '''
361
+ Returns a list of nodes reachable from node node_id
362
+ '''
363
+ return [G.nodes[node] for node in list(nx.descendants(G,node_id))]
364
+
365
+
366
+ def predecessors(G,node_type:str,node_id:int):
367
+ '''
368
+ Returns a list of nodes of a give type with a direct connection to node_id
369
+ '''
370
+ return [G.nodes[node] for node in list(G.predecessors(node_id)) if G.nodes[node]['type'] == node_type]
371
+
372
+ def successors(G,node_type:str,node_id:int):
373
+ '''
374
+ Returns a list of nodes of a given type that node_id has a direct connection to
375
+ '''
376
+ return [G.nodes[node] for node in list(G.successors(node_id)) if G.nodes[node]['type'] == node_type]
377
+
378
+ def ancestors(G,node_id:int,ancestor_node_type:str):
379
+ '''
380
+ Returns a list of nodes of a give type reachable from node node_id
381
+ '''
382
+ #set(nx.get_node_attributes(G,'type').values()) # Get all node types
383
+ return [G.nodes[node] for node in list(nx.ancestors(G,node_id)) if G.nodes[node]['type'] == ancestor_node_type]
384
+
385
+ def descendants(G,node_id:int,descendant_node_type:str):
386
+ '''
387
+ Returns a list of nodes of a give type reachable from node node_id
388
+ '''
389
+ return [G.nodes[node] for node in list(nx.descendants(G,node_id)) if G.nodes[node]['type'] == descendant_node_type]
390
+
391
+
392
+ def node_types(G):
393
+ return set(nx.get_node_attributes(G,'type').values())
394
+
395
+ def node_categories(G):
396
+ return set(nx.get_node_attributes(G,'category').values())
397
+
398
+ def node_labels(G):
399
+ return {(node['type'],node['type_id']): node['id'] for _, node in G.nodes(data=True)}
400
+
401
+ def get_node_id(G,node_type,node_type_id):
402
+ return node_labels(G)[(node_type,node_type_id)]
403
+
404
+ def get_nodes(G,node_type):
405
+ return [data for node_id, data in G.nodes(data=True) if data['type'] == node_type]
406
+
407
+ def get_node_ids(G,node_type):
408
+ return [node_id for node_id, data in G.nodes(data=True) if data['type'] == node_type]
409
+
410
+
411
+ def nodes(G,node_type,node_type_id,adjacent_node_type):
412
+ return (node for node in predecessors(G,node_type,G.labels[(node_type,node_type_id)]) if G.nodes[node]['type'] == adjacent_node_type)
413
+
414
+
415
+
416
+
417
+ #%% Methods using node_type, node_type_id interface
418
+
419
+ def upstream_network(G,reach_id):
420
+ return G.subgraph(nx.ancestors(G,get_node_id(G,'RCHRES',reach_id))).copy()
421
+
422
+ def downstream_network(G,reach_id):
423
+ return G.subgraph(nx.descendants(G,get_node_id(G,'RCHRES',reach_id))).copy()
424
+
425
+ def subset_network(G,reach_id,upstream_reach_ids = None):
426
+ G = upstream_network(G,reach_id)
427
+ if upstream_reach_ids is not None:
428
+ [G.remove_nodes_from(nx.ancestors(G,upstream_reach_id)) for upstream_reach_id in upstream_reach_ids if upstream_reach_id in G.nodes]
429
+ [G.remove_nodes_from([upstream_reach_id]) for upstream_reach_id in upstream_reach_ids if upstream_reach_id in G.nodes]
430
+ #assert([len(sinks(G)) == 0,sinks(G)[0] == reach_id])
431
+ return G
432
+
433
+ def upstream_nodes(G,reach_id,upstream_node_type):
434
+ return ancestors(G,get_node_id(G,'RCHRES',reach_id),upstream_node_type)
435
+
436
+ def downstream_nodes(G,reach_id,downstream_node_type):
437
+ return descendants(G,get_node_id(G,'RCHRES',reach_id),downstream_node_type)
438
+
439
+ def adjacent_nodes(G,reach_id):
440
+ node_id = get_node_id(G,'RCHRES',reach_id)
441
+ return _predecessors(G,node_id) + _successors(G,node_id)
442
+
443
+ def adjacent_upstream_nodes(G,reach_id,upstream_node_type):
444
+ return predecessors(G,upstream_node_type,get_node_id(G,'RCHRES',reach_id))
445
+
446
+
447
+ def adjacent_downstream_nodes(G,reach_id,downstream_node_type):
448
+ return successors(G,downstream_node_type,get_node_id(G,'RCHRES',reach_id))
449
+
450
+
451
+ def reach_node(G,reach_id):
452
+ return get_node_id(G,'RCHRES',reach_id)
453
+
454
+ def get_perlnd_node(G,perlnd_id):
455
+ return get_node_id(G,'PERLND',perlnd_id)
456
+
457
+ def get_implnd_node(G,implnd_id):
458
+ return get_node_id(G,'IMPLND',implnd_id)
459
+
460
+
461
+
462
+
463
+
464
+
465
+ #%%# Public interfaces
466
+
467
+ def get_node_type_ids(G,node_type = 'RCHRES'):
468
+ return [data['type_id'] for node, data in G.nodes(data = True) if data['type'] == node_type]
469
+
470
+ def get_reaches(G):
471
+ return get_node_type_ids(G, node_type = 'RCHRES')
472
+
473
+ def outlets(G):
474
+ return [G.nodes[node]['type_id'] for node, out_degree in G.out_degree(get_node_ids(G,'RCHRES')) if out_degree == 0]
475
+
476
+ def adjacent_operations(G,operation,reach_id):
477
+ assert operation in ['RCHRES','PERLND','IMPLND']
478
+ return [G.nodes[perlnd_node_id]['id'] for perlnd_node_id in predecessors(G,operation,G.labels[('RCHRES',reach_id)])]
479
+
480
+ def adjacent_perlnds(G,reach_id):
481
+ return [G.nodes[perlnd_node_id] for perlnd_node_id in predecessors(G,'PERLND',G.labels[('RCHRES',reach_id)])]
482
+
483
+ def adjacent_implnds(G,reach_id):
484
+ return [G.nodes[perlnd_node_id] for perlnd_node_id in predecessors(G,'IMPLND',G.labels[('RCHRES',reach_id)])]
485
+
486
+ def adjacent_reaches(G,reach_id):
487
+ return [G.nodes[perlnd_node_id] for perlnd_node_id in predecessors(G,'RCHRES',G.labels[('RCHRES',reach_id)])]
488
+
489
+ def upstream_adjacent_reachs(G,reach_id):
490
+ return [G.nodes[reach_id]['id'] for reach_id in predecessors(G,'RCHRES',G.labels[('RCHRES',reach_id)])]
491
+
492
+ def downstream_adjacent_reachs(G,reach_id):
493
+ return successors(G,'RCHRES',G.labels[('RCHRES',reach_id)])
494
+
495
+ def upstream_reachs(G,reach_id,upstream_reach_ids = None):
496
+ return [node['type_id'] for node in ancestors(G,get_node_id(G,'RCHRES',reach_id),'RCHRES')]
497
+
498
+ def downstream_reachs(G,reach_id, upstream_reach_ids = None):
499
+ return [node['type_id'] for node in descendants(G,get_node_id(G,'RCHRES',reach_id),'RCHRES')]
500
+
501
+ def routing_reachs(G):
502
+ return [reach_id for reach_id in get_reaches(G) if is_routing(G,reach_id)]
503
+
504
+ def is_routing(G,reach_id):
505
+ return all([node['type'] not in ['PERLND', 'IMPLND'] for node in adjacent_nodes(G,reach_id)])
506
+
507
+ def watershed_area(G,reach_ids):
508
+ return float(np.nansum(list(nx.get_edge_attributes(make_watershed(G,reach_ids),'area').values())))
509
+
510
+ def catchment_area(G,reach_id):
511
+ return float(np.nansum(list(nx.get_edge_attributes(make_catchment(G,reach_id),'area').values())))
512
+
513
+
514
+ def paths(G,reach_id,source_type = 'RCHRES'):
515
+ reach_node = get_node_id(G,'RCHRES',reach_id)
516
+ inv_labels = {v: k[1] for k, v in node_labels(G).items()}
517
+ return {inv_labels[source['id']]:[inv_labels[node] for node in nx.shortest_path(G,source['id'],reach_node)] for source in ancestors(G,reach_node,source_type)}
518
+
519
+ def count_ancestors(G,node_type,ancestor_node_type):
520
+ return {node['type_id']:len(ancestors(G,node['id'],ancestor_node_type)) for node in get_nodes(G,node_type)}
521
+
522
+
523
+
524
+ # Catchment constructor
525
+ def make_catchment(G,reach_id):
526
+ node_id = get_node_id(G,'RCHRES',reach_id)
527
+ catchment = G.edge_subgraph(G.in_edges(node_id,keys=True))
528
+ nx.set_node_attributes(catchment,node_id,'catchment_id')
529
+ return catchment
530
+
531
+ from itertools import chain
532
+
533
+ def make_watershed(G,reach_ids):
534
+ '''
535
+ 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.
536
+
537
+
538
+ '''
539
+ node_ids = set([get_node_id(G,'RCHRES',reach_id) for reach_id in reach_ids if reach_id > 0])
540
+ nodes_to_exclude = set([get_node_id(G,'RCHRES',abs(reach_id)) for reach_id in reach_ids if reach_id < 0])
541
+ node_ids = node_ids - nodes_to_exclude
542
+
543
+
544
+ nodes = [list(nx.ancestors(G,node_id)) for node_id in node_ids]
545
+ nodes.append(node_ids)
546
+ nodes = list(set(chain.from_iterable(nodes)))
547
+ watershed = subgraph(G, nodes)
548
+ catchment_id = '_'.join([str(reach_id) for reach_id in reach_ids])
549
+ nx.set_node_attributes(watershed,node_ids,catchment_id)
550
+ return watershed
551
+
552
+
553
+ def catcments(G):
554
+ return None
555
+ # Catchment selectors
556
+
557
+ '''
558
+ Properties of an HSPF catchment
559
+ - area
560
+ - outlet
561
+ - inlets
562
+ - inflows
563
+ - outflows
564
+ - reach
565
+
566
+
567
+
568
+ '''
569
+
570
+
571
+ # def area_by_landcover(G):
572
+ # for k, v in nx.get_edge_attributes(catchment,'aread').items
573
+ # np.nansum([v for k, v in nx.get_edge_attributes(catchment,'area').items()])
574
+ # combined_data = []
575
+ # for u, v, edge_data in test.edges(data=True):
576
+ # row = {
577
+ # 'source': u,
578
+ # 'target': v,
579
+ # **test.nodes[u],
580
+ # **test.nodes[v],
581
+ # **edge_data
582
+ # }
583
+ # combined_data.append(row)
584
+
585
+ def area_by_landcover(catchment):
586
+ return np.nansum([v for k, v in nx.get_edge_attributes(catchment,'area').items()])
587
+
588
+
589
+ def area(catchment):
590
+ return np.nansum([v for k, v in nx.get_edge_attributes(catchment,'area').items()])
591
+
592
+ def operation_ids(catchment,operation):
593
+ return [k[1] for k,v in catchment.labels.items() if k[0] == operation]
594
+
595
+ def dsn(catchment,tmemn):
596
+ return [catchment.nodes[k[0]]['id'] for k,v in nx.get_edge_attributes(catchment,'tmemn').items() if v == tmemn]
597
+
598
+ # catchment is a subset of a networkx graph constructed from a UCI file.
599
+ class Catchment():
600
+ def __init__(self,catchment):
601
+ self.catchment = catchment
602
+
603
+ def area(self):
604
+ return np.nansum([v for k, v in nx.get_edge_attributes(self.catchment,'area').items()])
605
+
606
+ def operation_ids(self,operation):
607
+ return [self.catchment.nodes[node]['type_id'] for node in get_node_ids(self.catchment,operation) if node in self.catchment.nodes]
608
+
609
+ def dsn(self,tmemn):
610
+ return [self.catchment.nodes[k[0]]['id'] for k,v in nx.get_edge_attributes(self.catchment,'tmemn').items() if v == tmemn]
611
+
612
+ def to_dataframe():
613
+ return
614
+ # def _watershed(G,reach_id):
615
+
616
+ # predecessors = (list(G.predecessors(node)))
617
+ # node = G.labels[('RCHRES',reach_id)]
618
+ # subgraph = nx.subgraph(G,nx.ancestors(G,node))
619
+ # return subgraph
620
+
621
+
622
+ # def _catchment(G,reach_id):
623
+
624
+ # node = G.labels[('RCHRES',reach_id)]
625
+
626
+ # subgraph = nx.subgraph(G,nx.ancestors(G,node))
627
+ # return subgraph
628
+
629
+
630
+ # def paths(G,reach_id):
631
+
632
+ # {source:[node for node in nx.shortest_path(G,source,reach_id)] for source in nx.ancestors(G,reach_id)}
633
+
634
+
635
+
636
+
637
+ #%% Legacy Methods for Backwards compatability
638
+ class reachNetwork():
639
+ def __init__(self,uci,reach_id = None):
640
+ self.G = create_graph(uci)
641
+ self.schematic = uci.table('SCHEMATIC').astype({'TVOLNO': int, "SVOLNO": int, 'AFACTR':float})
642
+ self.uci = uci
643
+
644
+ def get_node_type_ids(self,node_type):
645
+ return get_node_type_ids(self.G, node_type)
646
+
647
+ def _upstream(self,reach_id,node_type = 'RCHRES'):
648
+ '''
649
+ Returns list of model reaches upstream of inclusive of reach_id
650
+
651
+ '''
652
+ upstream = [node['type_id'] for node in upstream_nodes(self.G,reach_id,node_type) if node['type'] == 'RCHRES']
653
+ upstream.append(reach_id)
654
+ return upstream
655
+
656
+ def _downstream(self,reach_id,node_type = 'RCHRES'):
657
+ '''
658
+ Returns list of model reaches downstream inclusive of reach_id
659
+
660
+ '''
661
+ downstream = [node['type_id'] for node in downstream_nodes(self.G,reach_id,node_type) if node['type'] == 'RCHRES']
662
+ downstream.insert(0,reach_id)
663
+ return downstream
664
+
665
+ def calibration_order(self,reach_id,upstream_reach_ids = None):
666
+ return calibration_order(self.G,reach_id,upstream_reach_ids)
667
+
668
+ def station_order(self,reach_ids):
669
+ raise NotImplementedError()
670
+
671
+
672
+ def downstream(self,reach_id):
673
+ '''
674
+ Downstream adjacent reaches
675
+
676
+ '''
677
+ return [node['type_id'] for node in successors(self.G,'RCHRES',get_node_id(self.G,'RCHRES',reach_id))]
678
+
679
+ def upstream(self,reach_id):
680
+ '''
681
+ Upstream adjacent reaches
682
+
683
+ '''
684
+ return [node['type_id'] for node in predecessors(self.G,'RCHRES',get_node_id(self.G,'RCHRES',reach_id))]
685
+
686
+ def get_opnids(self,operation,reach_id, upstream_reach_ids = None):
687
+ '''
688
+ Operation IDs with a path to reach_id. Operations upstream of upstream_reach_ids will not be included
689
+
690
+ '''
691
+ return get_opnids(self.G,operation=operation,reach_id = reach_id, upstream_reach_ids = upstream_reach_ids)
692
+
693
+ def operation_area(self,operation,opnids = None):
694
+ return operation_area(self.uci,operation)
695
+
696
+ def drainage(self,reach_id):
697
+ # Merge source node attributes into edge attributes
698
+
699
+ edges = []
700
+ for u, v, edge_data in make_catchment(self.G,reach_id).edges(data=True):
701
+ source_node_attributes = self.G.nodes[u]
702
+ # Add or update edge attributes with source node attributes
703
+ edge_data["source_type"] = source_node_attributes.get("type")
704
+ edge_data["source_name"] = source_node_attributes.get("name")
705
+ edge_data["source_type_id"] = source_node_attributes.get("type_id")
706
+ edges.append(edge_data)
707
+
708
+ return pd.DataFrame(edges)
709
+
710
+ def subwatersheds(self,reach_ids = None):
711
+ df = subwatersheds(self.uci)
712
+ if reach_ids is not None:
713
+ df = df.loc[df.index.intersection(reach_ids)]
714
+ return df
715
+
716
+ def subwatershed(self,reach_id):
717
+ return subwatershed(self.uci,reach_id) #.loc[reach_id]
718
+
719
+ def subwatershed_area(self,reach_id):
720
+ return self.drainage(reach_id).query("source_type in ['PERLND','IMPLND']")['area'].sum()
721
+
722
+ def reach_contributions(self,operation,opnids):
723
+ return reach_contributions(self.uci,operation,opnids)
724
+
725
+ def drainage_area(self,reach_ids):
726
+ return watershed_area(self.G,reach_ids)
727
+
728
+ def drainage_area_landcover(self,reach_id,group = True):
729
+ reach_ids = self._upstream(reach_id)
730
+ areas = pd.concat([self.subwatershed(reach_id) for reach_id in reach_ids]).groupby(['SVOL','SVOLNO'])['AFACTR'].sum()
731
+
732
+ if group:
733
+ areas = pd.concat([areas[operation].groupby(self.uci.opnid_dict[operation].loc[areas[operation].index,'LSID'].values).sum() for operation in ['PERLND','IMPLND']])
734
+ return areas
735
+
736
+ def outlets(self):
737
+ return [self.G.nodes[node]['type_id'] for node, out_degree in self.G.out_degree() if (out_degree == 0) & (self.G.nodes[node]['type'] == 'RCHRES')]
738
+
739
+ def paths(self,reach_id):
740
+ return paths(self.G,reach_id)
741
+
742
+
743
+ def calibration_order(G,reach_id,upstream_reach_ids = None):
744
+ '''
745
+ Determines the order in which the specified reaches should be calibrated to
746
+ prevent upstream influences. Primarily helpful when calibrating sediment and
747
+ adjusting in channel erosion rates.
748
+ '''
749
+
750
+ order = []
751
+ Gsub = subgraph(G,get_node_ids(G,'RCHRES'))
752
+ while(len(Gsub.nodes)) > 0:
753
+
754
+ nodes_to_remove = [node for node, in_degree in Gsub.in_degree() if in_degree == 0]
755
+ order.append([G.nodes[node]['type_id'] for node in nodes_to_remove])
756
+ Gsub.remove_nodes_from(nodes_to_remove)
757
+ return order
758
+
759
+
760
+
761
+
762
+ def get_opnids(G,operation,reach_id = None, upstream_reach_ids = None):
763
+ G = subset_network(G,reach_id,upstream_reach_ids)
764
+ perlnds = [node['type_id'] for node in get_nodes(G,'PERLND')]
765
+ implnds = [node['type_id'] for node in get_nodes(G,'IMPLND')]
766
+ reachs = [node['type_id'] for node in get_nodes(G,'RCHRES')]
767
+ return {'RCHRES':reachs,'PERLND':perlnds,'IMPLND':implnds}[operation]
768
+ #return reachs,perlnds,implnds
769
+
770
+ def drainage(uci,reach_ids):
771
+ return subwatersheds(uci).loc[reach_ids].reset_index()[['SVOL','LSID','AFACTR']].groupby(['LSID','SVOL']).sum()
772
+
773
+
774
+
775
+ def drainage_area(uci,reach_ids,drng_area = 0):
776
+ if len(reach_ids) == 0:
777
+ return drng_area
778
+ else:
779
+ sign = math.copysign(1,reach_ids[0])
780
+ reach_id = int(reach_ids[0]*sign)
781
+ drng_area = drng_area + sign*uci.network.drainage_area(reach_id)
782
+ drainage_area(uci,reach_ids[1:],drng_area)
783
+
784
+
785
+ def reach_contributions(uci,operation,opnids):
786
+ schematic = uci.table('SCHEMATIC').set_index('SVOL')
787
+ schematic = schematic[schematic.index == operation]
788
+ schematic = schematic[schematic['TVOL'] == 'RCHRES'][['SVOLNO','TVOLNO','AFACTR']].astype({'SVOLNO':int,'TVOLNO':int,'AFACTR':float})
789
+ schematic = pd.concat([schematic[['SVOLNO','TVOLNO','AFACTR']][schematic['SVOLNO'] == opnid] for opnid in opnids])
790
+ schematic = schematic.reset_index()
791
+ schematic = schematic.groupby(['SVOL','SVOLNO','TVOLNO']).sum()
792
+ #schematic.columns = [operation,'reach','reachshed']
793
+ #schematic.set_index(operation,drop = True,inplace = True)
794
+ return schematic
795
+
796
+ def subwatersheds(uci):
797
+ schematic = uci.table('SCHEMATIC').set_index('SVOL')
798
+ schematic = schematic[(schematic.index == 'PERLND') | (schematic.index == 'IMPLND')]
799
+ schematic = schematic[schematic['TVOL'] == 'RCHRES'][['SVOLNO','TVOLNO','AFACTR','MLNO']].astype({'SVOLNO':int,'TVOLNO':int,'AFACTR':float,'MLNO':int})
800
+ schematic.reset_index(inplace=True,drop=False)
801
+ schematic.set_index('TVOLNO',inplace=True)
802
+
803
+ dfs = []
804
+ for operation in ['PERLND','IMPLND']:
805
+ df = schematic.loc[schematic['SVOL'] == operation].reset_index()
806
+ df = df.set_index('SVOLNO')
807
+ dfs.append(df.join(uci.table(operation,'GEN-INFO').iloc[:,0]))
808
+
809
+ df = pd.concat(dfs).reset_index()
810
+ df = df.set_index('TVOLNO')
811
+ return df
812
+
813
+ def subwatershed(uci,reach_id):
814
+ return subwatersheds(uci).loc[[reach_id]]
815
+
816
+ def drains_to(uci,opnid,operation):
817
+ schematic = uci.table('SCHEMATIC').set_index('SVOL')
818
+ schematic = schematic[schematic.index == operation]
819
+ schematic = schematic[schematic['TVOL'] == 'RCHRES'][['SVOLNO','TVOLNO','AFACTR']].astype({'SVOLNO':int,'TVOLNO':int,'AFACTR':float})
820
+ schematic = schematic[schematic['TVOLNO'] == opnid]
821
+ return schematic
822
+
823
+ def landcover_area(uci):
824
+ return pd.concat([operation_area(uci,operation) for operation in ['PERLND','IMPLND']])
825
+
826
+ def operation_area(uci,operation):
827
+ # schematic = uci.table('SCHEMATIC').copy()
828
+ # schematic = schematic.astype({'TVOLNO': int, "SVOLNO": int, 'AFACTR':float})
829
+ # schematic = schematic.groupby(['SVOL','SVOLNO']).sum()
830
+ # schematic = schematic.loc[[operation]].droplevel(0)['AFACTR'].to_frame()
831
+ df = subwatersheds(uci)
832
+ df = df.loc[df['SVOL'] == operation,['AFACTR','SVOLNO']]
833
+ df = df.set_index('SVOLNO')
834
+ df['LSID'] = uci.table(operation,'GEN-INFO').iloc[:,0].loc[df.index].values
835
+ return df
836
+
837
+
838
+
839
+
840
+
841
+
842
+
843
+
844
+ # p = paths(G,reach_id,'RCHRES')
845
+ # ptotout = hbn.get_multiple_timeseries('RCHRES',4,'PTOTOUT',reach_ids)
846
+ # ptotin = hbn.get_multiple_timeseries('RCHRES',4,'PTOTIN',reach_ids)
847
+ # reach_losses = 1-(ptotin-ptotout)/ptotin
848
+ # loads = subwatershed_total_phosphorous_loading(uci,hbn,t_code=4)
849
+
850
+ # loss_factors = pd.concat([reach_losses[v].prod(axis=1) for k,v in p.items()],axis=1)
851
+ # loss_factors.columns = list(p.keys())
852
+ # allocations = loads[loss_factors.columns].mul(loss_factors.values,axis=1)
853
+
854
+
855
+
856
+ # def loss_factor(G,reach_id,reach_losses):
857
+ # p = paths(G,reach_id,'RCHRES')
858
+ # loss_factors = pd.concat([reach_losses[v].prod(axis=1) for k,v in p.items()],axis=1)
859
+ # loss_factors.columns = list(p.keys())
860
+
861
+ # return return_loss_factors
862
+
863
+ # def allocation(uci,reach_id):
864
+
865
+
866
+
867
+
868
+ # subwatersheds = uci.network.subwatersheds()
869
+ # load = total_phosphorous(uci,hbn,4)
870
+ # load[subwatersheds.loc[subwatersheds['SVOL'] == 'PERLND']['SVOLNO'].to_list()]
871
+
872
+
873
+ # def catchment_area(G,reach_id):
874
+ # node = G.labels[('RCHRES',reach_id)]
875
+ # return [attributes['area'] for _,_, attributes in G.in_edges(node, data=True) if 'area' in attributes.keys()]
876
+
877
+
878
+
879
+
880
+
881
+
882
+
883
+ # def upstream_reach(G,reach_id):
884
+ # node = G.labels[('RCHRES',reach_id)]
885
+ # upstream_reach_ids = [node for node in list(G.predecessors(node)) if G.nodes[node]['type'] == 'RCHRES']
886
+ # return upstream_reach_ids
887
+
888
+
889
+ # def downstream_reachs(G,reach_id):
890
+ # node = G.labels[('RCHRES',reach_id)]
891
+ # downstream_reach_ids = [node for node in list(G.successors(node)) if G.nodes[node]['type'] == 'RCHRES']
892
+ # return downstream_reach_ids
893
+
894
+
895
+
896
+
897
+
898
+ # neighbors = list(G.predecessors(427))
899
+ # upstream_reach_ids = [node for node,operation in nx.get_node_attributes(G,'operation').items() if (operation == 'RCHRES') & (node in neighbors)]
900
+
901
+ # upstream_reach_nodes = [node for node in nx.neighbors(G,node) if 'operation' in node.keys() &
902
+ # subset_graph
903
+
904
+
905
+
906
+ # def upstream_network(G,reach_id):
907
+ # G = deepcopy(G)
908
+ # ancestors = list(nx.ancestors(G,reach_id))
909
+ # ancestors.insert(0,reach_id)
910
+ # drop = [node for node in G.nodes if node not in ancestors]
911
+ # G.remove_nodes_from(drop)
912
+ # return G
913
+
914
+ # def downstream_network(G,reach_id):
915
+ # G = deepcopy(G)
916
+ # descendants = list(nx.descendants(G,reach_id))
917
+ # drop = [node for node in G.nodes if node not in descendants]
918
+ # G.remove_nodes_from(drop)
919
+ # return G
920
+
921
+
922
+ # def subset_graph(G,reach_id, upstream_reach_ids = None):
923
+ # G = upstream_network(G,reach_id)
924
+ # if upstream_reach_ids is not None:
925
+ # [G.remove_nodes_from(nx.ancestors(G,upstream_reach_id)) for upstream_reach_id in upstream_reach_ids if upstream_reach_id in G.nodes]
926
+ # [G.remove_nodes_from([upstream_reach_id]) for upstream_reach_id in upstream_reach_ids if upstream_reach_id in G.nodes]
927
+ # assert([len(sinks(G)) == 0,sinks(G)[0] == reach_id])
928
+ # return G
929
+
930
+ # def sinks(G):
931
+ # return [node for node in G.nodes if (G.out_degree(node) == 0)]
932
+
933
+
934
+