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/hbn.py ADDED
@@ -0,0 +1,487 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Wed Mar 30 15:33:52 2022
4
+ Utility functions for accessing data from the hbn files as they relate to the
5
+ nutrients relevant for our current calibration methods. (See calibration_helpers.py)
6
+
7
+ @author: mfratki
8
+ """
9
+ from pyhspf import helpers
10
+ import pandas as pd
11
+ import math
12
+ from struct import unpack
13
+ from numpy import fromfile
14
+ from pandas import DataFrame
15
+ from datetime import datetime, timedelta, timezone
16
+ from collections import defaultdict
17
+ import numpy as np
18
+ #from pathlib import Path
19
+
20
+
21
+ #TIMSERIES_CATALOG = Path('C:/Users/mfratki/Documents/GitHub/hspf_tools/parser/Timeseries Catalog')
22
+
23
+ # catalog = []
24
+ # columns = ['operation','activity','name', 'sub1','sub2','type','units_eng','units_met','comments']
25
+ # for operation in ['PERLND','IMPLND','RCHRES']:
26
+ # files = [file for file in TIMSERIES_CATALOG.joinpath(operation).iterdir()]
27
+ # for file in files:
28
+ # lines = open(file).readlines()
29
+ # for line in lines:
30
+ # values = [col for col in line.split(' ') if col != '']
31
+ # comments = ' '.join(values[6:]).strip('\n')
32
+ # values = values[0:6]
33
+ # values.append(comments)
34
+ # values.insert(0,file.stem)
35
+ # values.insert(0,operation)
36
+ # catalog.append(pd.Series(data = values,index = columns))
37
+
38
+ # df = pd.concat(catalog,axis=1).transpose()
39
+
40
+
41
+ # TIMESERIES_CATALOG = pd.read_csv('C:/Users/mfratki/Documents/GitHub/hspf_tools/parser/Timeseries Catalog/TIMSERIES_CATALOG.csv')
42
+
43
+
44
+ # def timeseries_info(t_opn,t_activity,t_cons):
45
+ # ts_catalog = TIMESERIES_CATALOG.loc[(TIMESERIES_CATALOG['operation'] == t_opn) &
46
+ # (TIMESERIES_CATALOG['activity'] == t_activity)]
47
+
48
+ # ts_info = [row for index,row in ts_catalog.iterrows() if t_cons.startswith(row['name'])]
49
+
50
+ # assert(len(ts_info) <= 1)
51
+ # return ts_info
52
+
53
+ CF2CFS = {'hourly':3600,
54
+ 'daily':86400,
55
+ 'monthly':2592000,
56
+ 'yearly':31536000,
57
+ 'h':3600,
58
+ 'D':86400,
59
+ 'ME':2592000,
60
+ 'Y':31536000,
61
+ 'YE':31536000,
62
+ 2:3600,
63
+ 3:86400,
64
+ 4:2592000,
65
+ 5:31536000}
66
+
67
+ AGG_DEFAULTS = {'cfs':'mean',
68
+ 'mg/l':'mean',
69
+ 'degF': 'mean',
70
+ 'lb':'sum'}
71
+
72
+ UNIT_DEFAULTS = {'Q': 'cfs',
73
+ 'TSS': 'mg/l',
74
+ 'TP' : 'mg/l',
75
+ 'OP' : 'mg/l',
76
+ 'TKN': 'mg/l',
77
+ 'N' : 'mg/l',
78
+ 'WT' : 'degF',
79
+ 'WL' : 'ft'}
80
+
81
+ #agg_func = AGG_DEFAULTS[unit]
82
+ def get_simulated_implnd_constituent(hbn,constituent,time_step):
83
+ t_cons = helpers.get_tcons(constituent,'IMPLND')
84
+ df = sum([hbn.get_multiple_timeseries(t_opn='IMPLND',
85
+ t_con= t_con,
86
+ t_code = time_step) for t_con in t_cons])
87
+ # df.loc[:,'OPN'] = 'IMPLND'
88
+ # df.columns = ['OPNID',constituent,'SVOL']
89
+ if constituent == 'TSS':
90
+ df = df*2000
91
+
92
+ return df
93
+
94
+
95
+ def get_simulated_perlnd_constituent(hbn,constituent,time_step):
96
+ t_cons = helpers.get_tcons(constituent,'PERLND')
97
+ df = sum([hbn.get_multiple_timeseries(t_opn='PERLND',
98
+ t_con= t_con,
99
+ t_code = time_step) for t_con in t_cons])
100
+ # df.loc[:,'OPN'] = 'PERLND'
101
+ # df.columns = ['OPNID',constituent,'SVOL']
102
+ if constituent == 'TSS':
103
+ df = df*2000
104
+
105
+ return df
106
+
107
+ def get_simulated_catchment_constituent(hbn,constituent,time_step):
108
+ return pd.concat([get_simulated_perlnd_constituent(hbn,constituent,time_step),
109
+ get_simulated_implnd_constituent(hbn,constituent,time_step)])
110
+
111
+
112
+ def get_simulated_flow(hbn,time_step,reach_ids,unit = None):
113
+
114
+ if unit is None:
115
+ unit = 'cfs'
116
+ assert unit in ['cfs','acrft']
117
+
118
+ # if sign is None:
119
+ # exclude = [1 for i in enumerate(reach_ids)]
120
+ sign = [math.copysign(1,reach_id) for reach_id in reach_ids]
121
+ reach_ids = [abs(reach_id) for reach_id in reach_ids]
122
+
123
+ flows = hbn.get_multiple_timeseries('RCHRES',time_step,'ROVOL',reach_ids)
124
+ flows = (flows*sign).sum(axis=1) # Correct instances when a flow needs to be subtracted (rare)
125
+
126
+ if unit == 'cfs':
127
+ flows = flows/CF2CFS[time_step]*43560 #Acrfeet/invl to cubic feet/s
128
+
129
+ flows.attrs['unit'] = unit
130
+ return flows
131
+
132
+ def get_simulated_temperature(hbn,units,time_step,reach_ids):
133
+ raise NotImplementedError()
134
+
135
+
136
+ def get_simulated_reach_constituent(hbn,constituent,time_step,reach_ids,unit = None):
137
+ # if exclude is None:
138
+ # exclude = [1 for i in enumerate(reach_ids)]
139
+ sign = [math.copysign(1,reach_id) for reach_id in reach_ids]
140
+
141
+ if unit is None:
142
+ unit = UNIT_DEFAULTS[constituent]
143
+ else:
144
+ assert(unit in ['mg/l','lb','cfs','degF'])
145
+
146
+ t_cons = helpers.get_tcons(constituent,'RCHRES','lb')
147
+
148
+ # Correct instances when a flow needs to be subtracted (rare)
149
+ df = pd.concat([hbn.get_multiple_timeseries('RCHRES',time_step,t_con,[abs(reach_id) for reach_id in reach_ids])*sign for t_con in t_cons],axis=1).sum(axis=1)
150
+
151
+ if constituent == 'TSS':
152
+ df = df*2000
153
+
154
+
155
+ if unit == 'mg/l':
156
+ #if time_step not in ['h','hourly']:
157
+ flow = get_simulated_flow(hbn,time_step,reach_ids,'acrft')*1233481.8375475 #(acrft to Liters)
158
+ df = df*453592.37 # lbs to mg/l
159
+ df = df/flow
160
+
161
+ df.attrs['unit'] = unit
162
+ df.attrs['constituent'] = constituent
163
+ df.attrs['reach_ids'] = reach_ids
164
+ return df
165
+
166
+ class hbnInterface:
167
+ def __init__(self,file_paths,Map = True):
168
+ self.names = [file_path for file_path in file_paths]
169
+ self.hbns = [hbnClass(file_path,Map) for file_path in file_paths]
170
+
171
+ def _clear_cache(self):
172
+ [hbn._clear_cache() for hbn in self.hbns]
173
+
174
+ def get_time_series(self, t_opn, t_cons, t_code, opnid, activity = None):
175
+ return pd.concat([hbn.get_time_series(t_opn, t_cons, t_code, opnid, activity) for hbn in self.hbns],axis = 1)
176
+
177
+ def get_multiple_timeseries(self,t_opn,t_code,t_con,opnids = None,activity = None,axis = 1):
178
+ return pd.concat([hbn.get_multiple_timeseries(t_opn,t_code,t_con,opnids,activity) for hbn in self.hbns],axis = 1)
179
+
180
+ def get_reach_constituent(self,constituent,reach_ids,time_step,unit = None):
181
+ if constituent == 'Q':
182
+ df = get_simulated_flow(self,time_step,reach_ids,unit = unit)
183
+ elif constituent == 'WT':
184
+ df = get_simulated_temperature(self,time_step,reach_ids)
185
+ else:
186
+ df = get_simulated_reach_constituent(self,constituent,time_step,reach_ids,unit)
187
+ return df.to_frame()
188
+
189
+ def output_names(self):
190
+ # dd = defaultdict(list)
191
+ # dics = [hbn.output_names() for hbn in self.hbns]
192
+ # for dic in dics:
193
+ # for key, vals in dic.items():
194
+ # [dd[key].append(val) for val in vals]
195
+ dd = defaultdict(set)
196
+ dics = [hbn.output_names() for hbn in self.hbns]
197
+ for dic in dics:
198
+ for key, vals in dic.items():
199
+ [dd[key].add(val) for val in vals]
200
+ return dd
201
+
202
+ def get_perlnd_data(self,constituent,t_code = 'yearly'):
203
+ t_cons = helpers.get_tcons(constituent,'PERLND')
204
+
205
+ df = pd.concat([self.get_multiple_timeseries(t_opn = 'PERLND',
206
+ t_code = t_code,
207
+ t_con = t_con,
208
+ opnids = None)
209
+ for t_con in t_cons],axis = 0)
210
+
211
+ return df
212
+
213
+
214
+ def get_rchres_data(self,constituent,reach_ids,units = 'mg/l',t_code = 'daily'):
215
+ '''
216
+ Convience function for accessing the hbn time series associated with our current
217
+ calibration method. Assumes you are summing across all dataframes.
218
+
219
+ Parameters
220
+ ----------
221
+ hbn : TYPE
222
+ DESCRIPTION.
223
+ nutrient_id : TYPE
224
+ DESCRIPTION.
225
+ reach_ids : TYPE
226
+ DESCRIPTION.
227
+ flux : TYPE, optional
228
+ DESCRIPTION. The default is None.
229
+
230
+ Returns
231
+ -------
232
+ df : TYPE
233
+ DESCRIPTION.
234
+
235
+ '''
236
+
237
+
238
+
239
+ t_cons = helpers.get_tcons(constituent,'RCHRES',units)
240
+
241
+
242
+
243
+ df = pd.concat([self.get_multiple_timeseries(t_opn = 'RCHRES',
244
+ t_code =t_code,
245
+ t_con = t_con,
246
+ opnids = reach_ids)
247
+ for t_con in t_cons],axis = 1).sum(1).to_frame()
248
+
249
+ if (constituent == 'Q') & (units == 'cfs'):
250
+ df = df/CF2CFS[t_code]*43560 #Acrfeet/invl to cubic feet/s
251
+
252
+ df.attrs['unit'] = units
253
+ df.attrs['constituent'] = constituent
254
+ df.attrs['reach_ids'] = reach_ids
255
+ return df
256
+
257
+
258
+ def reach_losses(self,constituent,t_code):
259
+ inflows = pd.concat([self.get_multiple_timeseries('RCHRES',t_code,t_cons) for t_cons in LOSS_MAP[constituent][0]],axis=1).sum()
260
+ outflows = pd.concat([self.get_multiple_timeseries('RCHRES',t_code,t_cons) for t_cons in LOSS_MAP[constituent][1]],axis=1).sum()
261
+ return inflows/outflows
262
+
263
+ LOSS_MAP = {'Q':(['IVOL'],['ROVOL']),
264
+ 'TSS': (['ISEDTOT'],['ROSEDTOT']),
265
+ 'TP': (['PTOTIN'],['PTOTOUT']),
266
+ 'N': ([ 'NO2INTOT', 'NO3INTOT'],['NO3OUTTOT','NO2OUTTOT']),
267
+ 'TKN':(['TAMINTOT','NTOTORGIN'],['TAMOUTTOT','NTOTORGOUT']),
268
+ 'OP': (['PO4INDIS'],['PO4OUTDIS'])}
269
+ TCODES2FREQ = {1:'min',2:'h',3:'D',4:'M',5:'Y'}
270
+
271
+ class hbnClass:
272
+ def __init__(self,file_name,Map = True):
273
+ self.data(file_name,Map)
274
+ self.tcodes = {'minutely':1,'hourly':2,'daily':3,'monthly':4,'yearly':5,
275
+ 1:'minutely',2:'hourly',3:'daily',4:'monthly',5:'yearly',
276
+ 'min':1,'h':2,'D':3,'M':4,'Y':5,'H':2,'ME':4,'YE':5}
277
+ self.pandas_tcodes = {1:'min',2:'h',3:'D',4:'ME',5:'YE'}
278
+ def data(self,file_name,Map = False):
279
+ self.file_name = file_name
280
+ self.data = fromfile(self.file_name, 'B')
281
+ if self.data[0] != 0xFD:
282
+ print('BAD HBN FILE - must start with magic number 0xFD')
283
+ return
284
+ if Map == True:
285
+ self.map_hbn()
286
+ else:
287
+ self._clear_cache()
288
+
289
+ def map_hbn(self):
290
+ """
291
+ Reads ALL data from hbn_file and return them in DataFrame
292
+ Parameters
293
+ ----------
294
+ hbn_file : str
295
+ Name/path of HBN created by HSPF.
296
+ Returns
297
+ -------
298
+ df_summary : DataFrame
299
+ Summary information of data found in HBN file (also saved to HDF5 file.)
300
+ """
301
+
302
+ self.simulation_duration_count = 0
303
+ self.data_frames = {}
304
+ self.summary = []
305
+ self.summarycols = ['Operation', 'Activity', 'segment', 'Frequency', 'Shape', 'Start', 'Stop']
306
+ self.summaryindx = []
307
+ self.output_dictionary = {}
308
+
309
+ data = self.data
310
+
311
+ # Build layout maps of the file's contents
312
+ mapn = defaultdict(list)
313
+ mapd = defaultdict(list)
314
+ index = 1 # already used first byte (magic number)
315
+ while index < len(data):
316
+ rc1, rc2, rc3, rc, rectype, operation, id, activity = unpack('4BI8sI8s', data[index:index + 28])
317
+ rc1 = int(rc1 >> 2)
318
+ rc2 = int(rc2) * 64 + rc1 # 2**6
319
+ rc3 = int(rc3) * 16384 + rc2 # 2**14
320
+ reclen = int(rc) * 4194304 + rc3 - 24 # 2**22
321
+
322
+ operation = operation.decode('ascii').strip() # Python3 converts to bytearray not string
323
+ activity = activity.decode('ascii').strip()
324
+
325
+ if operation not in {'PERLND', 'IMPLND', 'RCHRES'}:
326
+ print('ALIGNMENT ERROR', operation)
327
+
328
+ if rectype == 1: # data record
329
+ tcode = unpack('I', data[index + 32: index + 36])[0]
330
+ mapd[operation, id, activity, tcode].append((index, reclen))
331
+ elif rectype == 0: # data names record
332
+ i = index + 28
333
+ slen = 0
334
+ while slen < reclen:
335
+ ln = unpack('I', data[i + slen: i + slen + 4])[0]
336
+ n = unpack(f'{ln}s', data[i + slen + 4: i + slen + 4 + ln])[0].decode('ascii').strip()
337
+ mapn[operation, id, activity].append(n.replace('-', ''))
338
+ slen += 4 + ln
339
+ else:
340
+ print('UNKNOW RECTYPE', rectype)
341
+ if reclen < 36:
342
+ index += reclen + 29 # found by trial and error
343
+ else:
344
+ index += reclen + 30
345
+ self.mapn = dict(mapn)
346
+ self.mapd = dict(mapd)
347
+
348
+
349
+ def read_data(self,operation,id,activity,tcode):
350
+ rows = []
351
+ times = []
352
+ nvals = len(self.mapn[operation, id, activity]) # number constituent timeseries
353
+ utc_offset = timezone(timedelta(hours=-6)) #UTC is 6hours ahead of CST
354
+ for (index, reclen) in self.mapd[operation, id, activity, tcode]:
355
+ yr, mo, dy, hr, mn = unpack('5I', self.data[index + 36: index + 56])
356
+ hr = hr-1
357
+ dt = datetime(yr, mo, dy, 0, mn ,tzinfo=utc_offset) + timedelta(hours=hr)
358
+ times.append(dt)
359
+
360
+ index += 56
361
+ row = unpack(f'{nvals}f', self.data[index:index + (4 * nvals)])
362
+ rows.append(row)
363
+ dfname = f'{operation}_{activity}_{id:03d}_{tcode}'
364
+ if self.simulation_duration_count == 0:
365
+ self.simulation_duration_count = len(times)
366
+ df = DataFrame(rows, index=times, columns=self.mapn[operation, id, activity]).sort_index(level = 'index')
367
+ if len(df) > 0:
368
+ #if tcode in ['daily',3]:
369
+ self.summaryindx.append(dfname)
370
+ self.summary.append((operation, activity, str(id), self.tcodes[tcode], str(df.shape), df.index[0], df.index[-1]))
371
+ self.output_dictionary[dfname] = self.mapn[operation, id, activity]
372
+ self.data_frames[dfname] = df.resample(self.pandas_tcodes[tcode]).mean() # sets the hours to 00 for non hourly time steps # an expensive operation probably
373
+ return self.data_frames[dfname]
374
+ else:
375
+ return None
376
+
377
+ def _clear_cache(self):
378
+ self.data_frames = {}
379
+ self.summary = []
380
+ self.summarycols = ['Operation', 'Activity', 'segment', 'Frequency', 'Shape', 'Start', 'Stop']
381
+ self.summaryindx = []
382
+ self.output_dictionary = {}
383
+
384
+ # def read_data2(self,operation,id,activity,tcode):
385
+
386
+ # rows = []
387
+ # times = []
388
+
389
+ # nvals = len(self.mapn[operation, id, activity]) # number of constituent time series
390
+ # #utc_offset = timezone(timedelta(hours=6)) # UTC is 6 hours ahead of CST
391
+
392
+ # indices, reclens = zip(*self.mapd[operation, id, activity, tcode])
393
+ # indices = np.array(indices)
394
+ # data_array = np.frombuffer(self.data, dtype=np.uint8) # Convert raw data to NumPy array
395
+
396
+ # times = [np.frombuffer(data_array[indice+36: indice+56], dtype=np.int32,count=5) for indice in indices]
397
+ # times = [datetime(time[0],time[1],time[2],time[3]-1) for time in times]
398
+ # rows = [np.frombuffer(data_array[indice + 56:indice +56 + (4 * nvals)], dtype=np.float32) for indice in indices]
399
+
400
+ # df = pd.DataFrame(rows, index=times, columns=self.mapn[operation, id, activity]).sort_index(level = 'index')
401
+ # return df
402
+
403
+ def infer_opnids(self,t_opn, t_cons,activity):
404
+ result = [k[-2] for k,v in self.mapn.items() if (t_cons in v) & (k[0] == t_opn) & (k[-1] == activity)]
405
+ if len(result) == 0:
406
+ return print('No Constituent-OPNID relationship found')
407
+ return result
408
+
409
+
410
+ def infer_activity(self,t_opn, t_cons):
411
+ result = [k[-1] for k,v in self.mapn.items() if (t_cons in v) & (k[0] == t_opn)]
412
+ if len(result) == 0:
413
+ return print('No Constituent-Activity relationship found')
414
+ assert(len(set(result)) == 1)
415
+ return result[0]
416
+
417
+
418
+ def get_time_series(self, t_opn, t_cons, t_code, opnid, activity = None):
419
+ """
420
+ get a single time series based on:
421
+ 1. t_opn: RCHRES, IMPLND, PERLND
422
+ 2. t_opn_id: 1, 2, 3, etc
423
+ 3. t_cons: target constituent name
424
+ 4. t_activity: HYDR, IQUAL, etc
425
+ 5. time_unit: yearly, monthly, full (default is 'full' simulation duration)
426
+ """
427
+ if isinstance(t_code,str):
428
+ t_code = self.tcodes[t_code]
429
+
430
+ if activity is None:
431
+ activity = self.infer_activity(t_opn,t_cons)
432
+ if activity is None:
433
+ return None
434
+ summaryindx = f'{t_opn}_{activity}_{opnid:03d}_{t_code}'
435
+ if summaryindx in self.summaryindx:
436
+ df = self.data_frames[summaryindx][t_cons].copy()
437
+ #df.index = df.index.shift(-1,TCODES2FREQ[t_code])
438
+ df = df[df.index >= '1996-01-01']
439
+
440
+ elif (t_opn, opnid, activity,t_code) in self.mapd.keys():
441
+ df = self.read_data(t_opn,opnid,activity,t_code)[t_cons].copy()
442
+ #df.index = df.index.shift(-1,TCODES2FREQ[t_code])
443
+ df = df[df.index >= '1996-01-01']
444
+ else:
445
+ df = None
446
+
447
+ return df
448
+ def get_multiple_timeseries(self,t_opn,t_code,t_con,opnids = None,activity = None):
449
+ # a single constituent but multiple opnids
450
+ if isinstance(t_code,str):
451
+ t_code = self.tcodes[t_code]
452
+
453
+ if activity is None:
454
+ activity = self.infer_activity(t_opn,t_con)
455
+ if activity is None:
456
+ return None
457
+
458
+ if opnids is None:
459
+ opnids = self.infer_opnids(t_opn,t_con,activity)
460
+ if opnids is None:
461
+ return None
462
+
463
+ df = None
464
+ frames = []
465
+ mapd_list = list(self.mapd.keys())
466
+ for opnid in opnids:
467
+ if (t_opn,opnid,activity,t_code) in mapd_list:
468
+ frames.append(self.get_time_series(t_opn,t_con,t_code,opnid,activity).rename(opnid))
469
+ if len(frames)>0:
470
+ df = pd.concat(frames,axis=1)
471
+
472
+ return df
473
+
474
+ def output_names(self):
475
+ activities = set([k[-1] for k,v in self.mapn.items()])
476
+ dic = {}
477
+ for activity in activities:
478
+ t_cons = [v for k,v in self.mapn.items() if k[-1] == activity]
479
+ dic[activity] = set([item for sublist in t_cons for item in sublist])
480
+ return dic
481
+
482
+ @staticmethod
483
+ def get_perlands(summary_indxs):
484
+ perlands = [int(summary_indx.split('_')[-2]) for summary_indx in summary_indxs]
485
+ return perlands
486
+
487
+
hspf/helpers.py ADDED
@@ -0,0 +1,94 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Wed Nov 10 15:48:19 2021
4
+
5
+ @author: mfratki
6
+ """
7
+ import pandas as pd
8
+ import numpy as np
9
+ #import subprocess
10
+ from pathlib import Path
11
+
12
+
13
+
14
+ def decompose_perlands(metzones,landcovers):
15
+ perlands = {}
16
+ for metzone in metzones:
17
+ metzone = int(metzone)
18
+ for landcover in landcovers:
19
+ landcover = int(landcover)
20
+ perlands[metzone+landcover] = (metzone,landcover)
21
+ return perlands
22
+
23
+
24
+
25
+ def get_months(month):
26
+ months = {1:'JAN', 2:'FEB', 3:'MAR', 4:'APR', 5:'MAY', 6:'JUN', 7:'JUL',
27
+ 8:'AUG', 9:'SEP', 10:'OCT', 11:'NOV', 12:'DEC'}
28
+ return months[month]
29
+
30
+ def get_adjacent_month(month,side = 1):
31
+ months = {1:[12,2], 2:[1,3], 3:[2,4], 4:[3,5], 5:[4,6], 6:[5,7], 7:[6,8],
32
+ 8:[7,9], 9:[8,10], 10:[9,11], 11:[10,12], 12:[11,1]}
33
+ return months[month][side]
34
+
35
+ def get_tcons(nutrient_name,operation,units = 'mg/l'):
36
+ ''' Convience function for getting the consntituent time series names associated with the nutrients we are
37
+ calibrating for. Note tehat Qual Prop 4 (BOD)
38
+ '''
39
+
40
+ if operation == 'RCHRES':
41
+ MAP = {'mg/l':{'TSS' :['SSEDTOT'], # TSS
42
+ 'TKN' :['TAMCONCDIS','NTOTORGCONC'], # TKN
43
+ 'N' :['NO2CONCDIS','NO3CONCDIS'], # N
44
+ 'OP' :['PO4CONCDIS'], # Ortho
45
+ 'TP' :['PTOTCONC']},# BOD is the difference of ptot and ortho
46
+ 'lb': {'TSS' :['ROSEDTOT'], # TSS
47
+ 'TKN' :['TAMOUTTOT','NTOTORGOUT'], # TKN
48
+ 'N' :['NO3OUTTOT','NO2OUTTOT'], # N
49
+ 'OP' :['PO4OUTDIS'], # Ortho
50
+ 'TP' :['PTOTOUT'],
51
+ 'BOD' :['BODOUTTOT']},
52
+ 'cfs': {'Q': ['ROVOL']},
53
+ 'acrft' : {'Q': ['ROVOL']}}
54
+
55
+ t_cons = MAP[units]
56
+ if operation == 'PERLND':
57
+ t_cons = {'TSS' :['SOSED'],
58
+ 'TKN' :['POQUALNH3+NH4'],
59
+ 'N' :['POQUALNO3'],
60
+ 'OP' :['POQUALORTHO P'],
61
+ 'BOD' :['POQUALBOD'],
62
+ 'Q' : ['PERO']} # BOD is the difference of ptot and ortho
63
+ if operation == 'IMPLND':
64
+ t_cons = {'TSS' :['SLDS'],
65
+ 'TKN' :['POQUALNH3+NH4'],
66
+ 'N' :['POQUALNO3'],
67
+ 'OP' :['POQUALORTHO P'],
68
+ 'BOD' :['POQUALBOD'],
69
+ 'Q' : ['SURO']} # BOD is the difference of ptot and ortho
70
+
71
+ return t_cons[nutrient_name]
72
+
73
+
74
+ def nutrient_name(nutrient_id: int):
75
+ key = {0:'TSS',
76
+ 1:'TKN',
77
+ 2:'N',
78
+ 3:'OP',
79
+ 4:'TP',# REally this is BOD but you subtract orthophosphate to get BOD
80
+ 5:'ChlA',
81
+ 6:'DO',
82
+ 7: 'Q'}
83
+ return key[nutrient_id]
84
+
85
+ def nutrient_id(nutrient_name: str):
86
+ key = {'TSS' :0,
87
+ 'TKN' :1,
88
+ 'N' :2,
89
+ 'OP' :3,
90
+ 'TP' :4, # REally this is BOD but you subtract orthophosphate to get BOD
91
+ 'ChlA':5,
92
+ 'DO' :6,
93
+ 'Q' :7}
94
+ return key[nutrient_name]