Skip to content

Commit 95b9b4c

Browse files
authored
Merge pull request #51 from LimnoTech/expand-test-cases
Restore automated testing support for Test10
2 parents d712b60 + fd9d118 commit 95b9b4c

File tree

4 files changed

+86
-42
lines changed

4 files changed

+86
-42
lines changed

HSP2/PLANK_Class.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ def baldth(nsfg,no3,tam,po4,paldh,naldh,aldl,aldh,mbal,dox,anaer,oxald,bal,depco
11221122

11231123
# use unit death rate to compute death rate; dthbal is expressed
11241124
# as umoles of phosphorus per liter per interval
1125-
return (ald * bal) + slof # dthbal
1125+
return (ald * bal) + slof, bal # dthbal
11261126

11271127

11281128
def balrx(self, ballit,tw,talgrl,talgrh,talgrm,malgr,cmmp, cmmnp,tamfg,amrfg,nsfg,cmmn,cmmlt,delt60,
@@ -1162,7 +1162,7 @@ def balrx(self, ballit,tw,talgrl,talgrh,talgrm,malgr,cmmp, cmmnp,tamfg,amrfg,nsf
11621162
grobal = self.grochk (po4,no3,tam,phfg,decfg,baco2,cvbpc,cvbpn,nsfg,nmingr,pmingr,cmingr,i0,grtotn,grobal)
11631163

11641164
# calculate benthic algae death, baldth only called here
1165-
dthbal = self.baldth(nsfg,no3,tam,po4,paldh,naldh,aldl,aldh,mbal,dox,anaer,oxald,bal,depcor)
1165+
(dthbal, bal) = self.baldth(nsfg,no3,tam,po4,paldh,naldh,aldl,aldh,mbal,dox,anaer,oxald,bal,depcor)
11661166

11671167
bal += grobal # determine the new benthic algae population
11681168

HSP2tools/HDF5.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,47 @@
1010
from threading import Lock
1111

1212
class HDF5:
13+
14+
REQUIRES_MAPPING = ['GQUAL','CONS']
15+
1316
def __init__(self, file_name:str) -> None:
1417
self.file_name = file_name
1518
self.aliases = self._read_aliases_csv()
1619
self.data = {}
1720
self.lock = Lock()
1821

1922
self.gqual_prefixes = self._read_gqual_mapping()
23+
self.cons_prefixes = self._read_cons_mapping()
24+
self.iqual_prefixes = self._read_iqual_mapping()
2025

21-
def _read_gqual_mapping(self) -> Dict[str,str]:
22-
""""GQUAL is based on number which corresponds to the parameter
23-
however which number is assoicated with which parameter changes
24-
based on the UCI file. Need to read from GQUAL tables
26+
def _read_nqual_mapping(self, key:str, target_col:str, nquals:int = 10) -> Dict[str,str]:
27+
"""Some modules, like GQUAL, allow for number which corresponds to the consistent
28+
being modeled. However which number is assoicated with which parameter changes
29+
based on the UCI file. Need to read from specification tables
2530
"""
26-
27-
gqual_prefixes = {}
28-
for i in range(1,7):
31+
dict_mappings = {}
32+
for i in range(1,nquals):
2933
try:
3034
with pd.HDFStore(self.file_name,'r') as store:
31-
df = pd.read_hdf(store,f'RCHRES/GQUAL/GQUAL{i}')
35+
df = pd.read_hdf(store,f'{key}{i}')
3236
row = df.iloc[0]
33-
gqid = row['GQID']
34-
gqual_prefixes[gqid] = str(i)
37+
gqid = row[target_col]
38+
dict_mappings[gqid] = str(i)
3539
except KeyError:
36-
#Mean no gqual number (e.g. QUAL3) for this run
40+
#Mean no nqual number (e.g. GQUAL3) for this run
3741
pass
38-
return gqual_prefixes
42+
return dict_mappings
3943

44+
def _read_gqual_mapping(self) -> Dict[str,str]:
45+
return self._read_nqual_mapping(R'RCHRES/GQUAL/GQUAL', 'GQID', 7)
46+
47+
def _read_cons_mapping(self) -> Dict[str,str]:
48+
return self._read_nqual_mapping(R'RCHRES/CONS/CONS','CONID', 7)
49+
50+
def _read_iqual_mapping(self) -> Dict[str,str]:
51+
"""placeholder - for implementation similar to gqual - but for current test just assume 1"""
52+
return {'':'1'}
53+
4054
def _read_aliases_csv(self) -> Dict[Tuple[str,str,str],str]:
4155
datapath = os.path.join(HSP2tools.__path__[0], 'data', 'HBNAliases.csv')
4256
df = pd.read_csv(datapath)
@@ -52,11 +66,12 @@ def get_time_series(self, operation:str, id:str, constituent:str, activity:str)
5266

5367
#We still need a special case for IMPLAND/IQUAL and PERLAND/PQUAL
5468
constituent_prefix = ''
55-
if activity == 'GQUAL':
69+
if activity in self.REQUIRES_MAPPING:
5670
constituent_prefix = ''
57-
for key, value in self.gqual_prefixes.items():
71+
prefix_dict = getattr(self, f'{activity.lower()}_prefixes')
72+
for key, value in prefix_dict.items():
5873
if constituent.startswith(key):
59-
constituent_prefix = f'GQUAL{value}_'
74+
constituent_prefix = f'{activity}{value}_'
6075
constituent = constituent.replace(key,'')
6176

6277
key = (operation,id,activity)

HSP2tools/data/HBNAliases.csv

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,14 @@ RCHRES,PLANK,TP,PTOTCONC
127127
RCHRES,PLANK,ROTORN,NTOTORGOUT
128128
RCHRES,PLANK,ROTORP,PTOTORGOUT
129129
RCHRES,PLANK,ROTORC,CTOTORGOUT
130+
IMPLND,IQUAL,IQADDR,IQADDRCOD
131+
IMPLND,IQUAL,IQADEP,IQADEPCOD
132+
IMPLND,IQUAL,IQADWT,IQADWTCOD
133+
IMPLND,IQUAL,SOQC,SOQCCOD
134+
IMPLND,IQUAL,SOQO,SOQOCOD
135+
IMPLND,IQUAL,SOQOC,SOQOCCOD
136+
IMPLND,IQUAL,SOQS,SOQSCOD
137+
IMPLND,IQUAL,SOQSP,SOQSPCOD
138+
IMPLND,IQUAL,SOQUAL,SOQUALCOD
139+
IMPLND,IQUAL,SQO,SQOCOD
140+
RCHRES,CONS,CON,CONC

tests/convert/regression_base.py

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import time
12
import os
23
import inspect
34
import webbrowser
@@ -6,10 +7,14 @@
67
import pandas as pd
78
import numpy as np
89

9-
from typing import Dict, List, Tuple
10+
from typing import Dict, List, Tuple, Union
1011

1112
from concurrent.futures import ThreadPoolExecutor, as_completed, thread
1213

14+
15+
OperationsTuple = Tuple[str,str,str,str,str]
16+
ResultsTuple = Tuple[bool,bool,bool,float]
17+
1318
class RegressTest(object):
1419
def __init__(self, compare_case:str, operations:List[str]=[], activities:List[str]=[],
1520
tcodes:List[str] = ['2'], ids:List[str] = [], threads:int=os.cpu_count() - 1) -> None:
@@ -38,13 +43,22 @@ def _init_files(self):
3843

3944
def _get_hbn_data(self, test_dir: str) -> None:
4045
sub_dir = os.path.join(test_dir, 'HSPFresults')
46+
self.hspf_data_collection = {}
4147
for file in os.listdir(sub_dir):
4248
if file.lower().endswith('.hbn'):
43-
self.hspf_data = HBNOutput(os.path.join(test_dir, sub_dir, file))
44-
break
45-
self.hspf_data.read_data()
46-
47-
def _get_hdf5_data(self, test_dir: str) -> List[HDF5]:
49+
hspf_data = HBNOutput(os.path.join(test_dir, sub_dir, file))
50+
hspf_data.read_data()
51+
for key in hspf_data.output_dictionary.keys():
52+
self.hspf_data_collection[key] = hspf_data
53+
54+
def get_hspf_time_series(self, ops:OperationsTuple) -> Union[pd.Series,None]:
55+
operation, activity, id, constituent, tcode = ops
56+
key = f'{operation}_{activity}_{id}_{tcode}'
57+
hspf_data = self.hspf_data_collection[key]
58+
series = hspf_data.get_time_series(operation, int(id), constituent, activity, 'hourly')
59+
return series
60+
61+
def _get_hdf5_data(self, test_dir: str) -> None:
4862
sub_dir = os.path.join(test_dir, 'HSP2results')
4963
for file in os.listdir(sub_dir):
5064
if file.lower().endswith('.h5') or file.lower().endswith('.hdf'):
@@ -62,26 +76,27 @@ def should_compare(self, operation:str, activity:str, id:str, tcode:str) -> bool
6276
return False
6377
return True
6478

65-
def generate_report(self, file:str, results: Dict[Tuple[str,str,str,str,str],Tuple[bool,bool,bool,float]]) -> None:
79+
def generate_report(self, file:str, results: Dict[OperationsTuple,ResultsTuple]) -> None:
6680
html = self.make_html_report(results)
6781
self.write_html(file,html)
6882
webbrowser.open_new_tab('file://' + file)
6983

70-
def make_html_report(self, results_dict:Dict[Tuple[str,str,str,str,str],Tuple[bool,bool,bool,float]]) -> str:
84+
def make_html_report(self, results_dict:Dict[OperationsTuple,ResultsTuple]) -> str:
7185
"""populates html table"""
7286
style_th = 'style="text-align:left"'
7387
style_header = 'style="border:1px solid; background-color:#EEEEEE"'
7488

7589
html = f'<html><header><h1>CONVERSION TEST REPORT</h1></header><body>\n'
7690
html += f'<table style="border:1px solid">\n'
7791

78-
for key in self.hspf_data.output_dictionary.keys():
92+
for key in self.hspf_data_collection.keys():
7993
operation, activity, opn_id, tcode = key.split('_')
8094
if not self.should_compare(operation, activity, opn_id, tcode):
8195
continue
8296
html += f'<tr><th colspan=5 {style_header}>{key}</th></tr>\n'
8397
html += f'<tr><th></th><th {style_th}>Constituent</th><th {style_th}>Max Diff</th><th>Match</th><th>Note</th></tr>\n'
84-
for cons in self.hspf_data.output_dictionary[key]:
98+
hspf_data = self.hspf_data_collection[key]
99+
for cons in hspf_data.output_dictionary[key]:
85100
result = results_dict[(operation,activity,opn_id, cons, tcode)]
86101
no_data_hsp2, no_data_hspf, match, diff = result
87102
html += self.make_html_comp_row(cons, no_data_hsp2, no_data_hspf, match, diff)
@@ -110,16 +125,17 @@ def write_html(self, file:str, html:str) -> None:
110125
with open(file, 'w') as f:
111126
f.write(html)
112127

113-
def run_test(self) -> Dict[Tuple[str,str,str,str,str],Tuple[bool,bool,bool,float]]:
128+
def run_test(self) -> Dict[OperationsTuple,ResultsTuple]:
114129
futures = {}
115130
results_dict = {}
116131

117132
with ThreadPoolExecutor(max_workers=self.threads) as executor:
118-
for key in self.hspf_data.output_dictionary.keys():
133+
for key in self.hspf_data_collection.keys():
119134
(operation, activity, opn_id, tcode) = key.split('_')
120135
if not self.should_compare(operation, activity, opn_id, tcode):
121136
continue
122-
for cons in self.hspf_data.output_dictionary[key]:
137+
hspf_data = self.hspf_data_collection[key]
138+
for cons in hspf_data.output_dictionary[key]:
123139
params = (operation,activity,opn_id,cons,tcode)
124140
futures[executor.submit(self.check_con, params)] = params
125141

@@ -129,13 +145,13 @@ def run_test(self) -> Dict[Tuple[str,str,str,str,str],Tuple[bool,bool,bool,float
129145

130146
return results_dict
131147

132-
def check_con(self, params:Tuple[str,str,str,str,str]) -> Tuple[bool,bool,bool,float]:
148+
def check_con(self, params:OperationsTuple) -> ResultsTuple:
133149
"""Performs comparision of single constituent"""
134150
operation, activity, id, constituent, tcode = params
135151
print(f' {operation}_{id} {activity} {constituent}\n')
136152

137153
ts_hsp2 = self.hsp2_data.get_time_series(operation, id, constituent, activity)
138-
ts_hspf = self.hspf_data.get_time_series(operation, int(id), constituent, activity, 'hourly')
154+
ts_hspf = self.get_hspf_time_series(params)
139155

140156
no_data_hsp2 = ts_hsp2 is None
141157
no_data_hspf = ts_hspf is None
@@ -157,22 +173,20 @@ def check_con(self, params:Tuple[str,str,str,str,str]) -> Tuple[bool,bool,bool,f
157173
elif constituent == 'QTOTAL' or constituent == 'HTEXCH' :
158174
tolerance = max(abs(ts_hsp2.values.min()), abs(ts_hsp2.values.max())) * 1e-3
159175

160-
ts_hsp2, ts_hspf = self.validate_time_series(ts_hsp2, ts_hspf,
161-
self.hsp2_data, self.hspf_data, operation, activity, id, constituent)
176+
ts_hsp2, ts_hspf = self.validate_time_series(ts_hsp2, ts_hspf, operation, activity, id, constituent)
162177

163178
match, diff = self.compare_time_series(ts_hsp2, ts_hspf, tolerance)
164179

165180
return (no_data_hsp2, no_data_hspf, match, diff)
166181

167182
def fill_nan_and_null(self, timeseries:pd.Series, replacement_value:float = 0.0) -> pd.Series:
168-
"""Replaces any nan or HSPF nulls -1.0e26 with provided replacement_value"""
183+
"""Replaces any nan or HSPF nulls -1.0e30 with provided replacement_value"""
169184
timeseries = timeseries.fillna(replacement_value)
170-
timeseries = timeseries.replace(-1.0e26, replacement_value)
185+
timeseries = timeseries.where(timeseries > -1.0e30, replacement_value)
171186
return timeseries
172187

173-
def validate_time_series(self, ts_hsp2:pd.Series, ts_hspf:pd.Series,
174-
hsp2_data:HDF5, hspf_data:HBNOutput, operation:str, activity:str,
175-
id:str, cons:str) -> Tuple[pd.Series, pd.Series]:
188+
def validate_time_series(self, ts_hsp2:pd.Series, ts_hspf:pd.Series, operation:str,
189+
activity:str, id:str, cons:str) -> Tuple[pd.Series, pd.Series]:
176190
""" validates a corrects time series to avoid false differences """
177191

178192
# In some test cases it looked like HSP2 was executing for a single extra time step
@@ -187,8 +201,11 @@ def validate_time_series(self, ts_hsp2:pd.Series, ts_hspf:pd.Series,
187201
### special cases
188202
# if tiny suro in one and no suro in the other, don't trigger on suro-dependent numbers
189203
if activity == 'PWTGAS' and cons in ['SOTMP', 'SODOX', 'SOCO2']:
190-
ts_suro_hsp2 = hsp2_data.get_time_series(operation, id, "SURO", "PWATER")
191-
ts_suro_hspf = hspf_data.get_time_series(operation, int(id), "SURO", "PWATER", 'hourly')
204+
ts_suro_hsp2 = self.hsp2_data.get_time_series(operation, id, 'SURO', 'PWATER')
205+
ts_suro_hsp2 = self.fill_nan_and_null(ts_suro_hsp2)
206+
ts_suro_hspf = self.get_hspf_time_series((operation, 'PWATER', id, 'SURO', 2))
207+
ts_suro_hsp2 = self.fill_nan_and_null(ts_suro_hspf)
208+
192209

193210
idx_zero_suro_hsp2 = ts_suro_hsp2 == 0
194211
idx_low_suro_hsp2 = ts_suro_hsp2 < 1.0e-8
@@ -200,7 +217,8 @@ def validate_time_series(self, ts_hsp2:pd.Series, ts_hspf:pd.Series,
200217

201218
# if volume in reach is going to zero, small concentration differences are not signficant
202219
if activity == 'SEDTRN' and cons in ['SSEDCLAY', 'SSEDTOT']:
203-
ts_vol_hsp2 = hsp2_data.get_time_series(operation, id, "VOL", "HYDR")
220+
ts_vol_hsp2 = self.hsp2_data.get_time_series(operation, id, "VOL", "HYDR")
221+
ts_vol_hsp2 = self.fill_nan_and_null(ts_vol_hsp2)
204222

205223
idx_low_vol = ts_vol_hsp2 < 1.0e-4
206224
ts_hsp2.loc[idx_low_vol] = ts_hsp2.loc[idx_low_vol] = 0

0 commit comments

Comments
 (0)