Skip to content

Commit 0d2a8cb

Browse files
authored
Implement LIVVkit 3 for EVV (#8)
* Update resources and script to match LIVV3 interface * Update PGN module for LIVV3 * Update ks extension (MVK test) for LIVVkit 3.0 Update to new elements, adds variable description and groups variables into accept/reject groups for figures * Clean up unused code, fix summary table * Fix details table creation, cleanup code in main * Re-order tables, rejected first...should be shorter * Update to use LIVVkit 3 API * Revert to un-grouped validations * Remove Python 2 * Update contact and version info * Fix commas * Add web resources for LIVV3 * Remove unused code from tsc extn * Make LIVVkit 3.0.1 lower bound requirement
1 parent 89d6b22 commit 0d2a8cb

File tree

16 files changed

+614
-675
lines changed

16 files changed

+614
-675
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
EVV is a python-based toolkit for extended verification and validation of Earth
55
system models (ESMs). Currently, it provides a number tests to determine if
6-
modifications to an ESM is *climate changing.*
7-
8-
6+
modifications to an ESM is *climate changing.*
7+
8+
99
Contact
1010
===========
1111

@@ -15,6 +15,9 @@ report bugs, ask questions, or contact us for any reason, use the
1515

1616
Want to send us a private message?
1717

18+
**Michael Kelleher**
19+
* github: @mkstratos
20+
* email: <a href="mailto:[email protected]">kelleherme [at] ornl.gov</a>
1821
**Joseph H. Kennedy**
1922
* github: @jhkennedy
2023
* email: <a href="mailto:[email protected]">kennedyjh [at] ornl.gov</a>

evv4esm/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@
2727
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2828
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929

30-
from __future__ import absolute_import, division, print_function, unicode_literals
3130

32-
__version_info__ = (0, 2, 5)
31+
__version_info__ = (0, 3, 0)
3332
__version__ = '.'.join(str(vi) for vi in __version_info__)
3433

3534
PASS_COLOR = '#389933'

evv4esm/__main__.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2929
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3030

31-
from __future__ import absolute_import, division, print_function, unicode_literals
32-
3331
import os
3432
import sys
3533
import time
@@ -75,8 +73,8 @@ def parse_args(args=None):
7573
from evv4esm import resources
7674
args.livv_resource_dir = livvkit.resource_dir
7775
livvkit.resource_dir = os.sep.join(resources.__path__)
78-
79-
return args
76+
77+
return args
8078

8179

8280
def main(cl_args=None):
@@ -106,11 +104,10 @@ def main(cl_args=None):
106104
from livvkit.components import validation
107105
from livvkit import scheduler
108106
from livvkit.util import functions
109-
from livvkit.util import elements
107+
from livvkit import elements
110108

111109
if args.extensions:
112110
functions.setup_output()
113-
114111
summary_elements = []
115112
validation_config = {}
116113
print(" -----------------------------------------------------------------")
@@ -120,31 +117,24 @@ def main(cl_args=None):
120117
for conf in livvkit.validation_model_configs:
121118
validation_config = functions.merge_dicts(validation_config,
122119
functions.read_json(conf))
123-
summary_elements.extend(scheduler.run_quiet(validation, validation_config,
124-
group=False))
120+
summary_elements.extend(scheduler.run_quiet("validation", validation, validation_config,
121+
group=False))
125122
print(" -----------------------------------------------------------------")
126123
print(" Extensions test suite complete ")
127124
print(" -----------------------------------------------------------------")
128125
print("")
129126

130-
result = elements.page("Summary", "", element_list=summary_elements)
131-
functions.write_json(result, livvkit.output_dir, "index.json")
127+
result = elements.Page("Summary", "", elements=summary_elements)
128+
with open(os.path.join(livvkit.output_dir, "index.json"), "w") as index_data:
129+
index_data.write(result._repr_json())
132130
print("-------------------------------------------------------------------")
133131
print(" Done! Results can be seen in a web browser at:")
134132
print(" " + os.path.join(livvkit.output_dir, 'index.html'))
135133
print("-------------------------------------------------------------------")
136134

137135
if args.serve:
138-
try:
139-
# Python 3
140-
import http.server as server
141-
import socketserver as socket
142-
except ImportError:
143-
# Python 2
144-
# noinspection PyPep8Naming
145-
import SimpleHTTPServer as server
146-
# noinspection PyPep8Naming
147-
import SocketServer as socket
136+
import http.server as server
137+
import socketserver as socket
148138

149139
httpd = socket.TCPServer(('', args.serve), server.SimpleHTTPRequestHandler)
150140

evv4esm/ensembles/e3sm.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030

3131
"""E3SM specific ensemble functions."""
3232

33-
from __future__ import absolute_import, division, print_function, unicode_literals
3433
import six
3534

3635
import os
@@ -102,8 +101,16 @@ def gather_monthly_averages(ensemble_files, variable_set=None):
102101
continue
103102
else:
104103
m = np.mean(data.variables[var][0, ...])
105-
106-
monthly_avgs.append((case, var, '{:04}'.format(inst), date_str, m))
107-
108-
monthly_avgs = pd.DataFrame(monthly_avgs, columns=('case', 'variable', 'instance', 'date', 'monthly_mean'))
104+
try:
105+
_name = f": {data.variables[var].getncattr('long_name')}"
106+
except AttributeError:
107+
_name = ""
108+
try:
109+
_units = f" [{data.variables[var].getncattr('units')}]"
110+
except AttributeError:
111+
_units = ""
112+
desc = f"{_name}{_units}"
113+
monthly_avgs.append((case, var, '{:04}'.format(inst), date_str, m, desc))
114+
115+
monthly_avgs = pd.DataFrame(monthly_avgs, columns=('case', 'variable', 'instance', 'date', 'monthly_mean', 'desc'))
109116
return monthly_avgs

evv4esm/extensions/ks.py

Lines changed: 97 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,25 @@
4444
techniques.
4545
"""
4646

47-
from __future__ import absolute_import, division, print_function, unicode_literals
48-
import six
49-
50-
import os
5147
import argparse
52-
53-
from pprint import pprint
48+
import os
5449
from collections import OrderedDict
55-
56-
import numpy as np
57-
from scipy import stats
50+
from pathlib import Path
51+
from pprint import pprint
5852

5953
import livvkit
60-
from livvkit.util import elements as el
54+
import numpy as np
55+
import pandas as pd
56+
import six
57+
from livvkit import elements as el
6158
from livvkit.util import functions as fn
6259
from livvkit.util.LIVVDict import LIVVDict
60+
from scipy import stats
6361

62+
from evv4esm import EVVException, human_color_names
6463
from evv4esm.ensembles import e3sm
6564
from evv4esm.ensembles.tools import monthly_to_annual_avg, prob_plot
6665
from evv4esm.utils import bib2html
67-
from evv4esm import human_color_names, EVVException
6866

6967

7068
def variable_set(name):
@@ -118,7 +116,7 @@ def parse_args(args=None):
118116
default=13, type=float,
119117
help='The critical value (desired significance level) for rejecting the ' +
120118
'null hypothesis.')
121-
119+
122120
parser.add_argument('--img-dir',
123121
default=os.getcwd(),
124122
help='Image output location.')
@@ -138,13 +136,26 @@ def parse_args(args=None):
138136
args.config['ks'][key] = val
139137

140138
config_arg_list = []
141-
[config_arg_list.extend(['--'+key, str(val)]) for key, val in args.config['ks'].items()
142-
if key != 'config']
139+
_ = [
140+
config_arg_list.extend(['--'+key, str(val)])
141+
for key, val in args.config['ks'].items() if key != 'config'
142+
]
143143
args, _ = parser.parse_known_args(config_arg_list)
144144

145145
return args
146146

147147

148+
def col_fmt(dat):
149+
"""Format results for table output."""
150+
if dat is not None:
151+
try:
152+
_out = "{:.3e}, {:.3e}".format(*dat)
153+
except TypeError:
154+
_out = dat
155+
else:
156+
_out = "-"
157+
return _out
158+
148159
def run(name, config):
149160
"""
150161
Runs the analysis.
@@ -156,7 +167,7 @@ def run(name, config):
156167
Returns:
157168
The result of elements.page with the list of elements to display
158169
"""
159-
170+
160171
config_arg_list = []
161172
[config_arg_list.extend(['--'+key, str(val)]) for key, val in config.items()]
162173

@@ -167,31 +178,52 @@ def run(name, config):
167178

168179
details, img_gal = main(args)
169180

170-
tbl_data = OrderedDict(sorted(details.items()))
171-
tbl_el = {'Type': 'V-H Table',
172-
'Title': 'Validation',
173-
'TableTitle': 'Analyzed variables',
174-
'Headers': ['h0', 'K-S test (D, p)', 'T test (t, p)'],
175-
'Data': {'': tbl_data}
176-
}
181+
table_data = pd.DataFrame(details).T
182+
_hdrs = [
183+
"h0",
184+
"K-S test (D, p)",
185+
"T test (t, p)",
186+
"mean (test case, ref. case)",
187+
"std (test case, ref. case)",
188+
]
189+
table_data = table_data[_hdrs]
190+
for _hdr in _hdrs[1:]:
191+
table_data[_hdr] = table_data[_hdr].apply(col_fmt)
192+
193+
tables = [
194+
el.Table("Rejected", data=table_data[table_data["h0"] == "reject"]),
195+
el.Table("Accepted", data=table_data[table_data["h0"] == "accept"]),
196+
el.Table("Null", data=table_data[~table_data["h0"].isin(["accept", "reject"])])
197+
]
198+
177199
bib_html = bib2html(os.path.join(os.path.dirname(__file__), 'ks.bib'))
178-
tl = [el.tab('Figures', element_list=[img_gal]),
179-
el.tab('Details', element_list=[tbl_el]),
180-
el.tab('References', element_list=[el.html(bib_html)])]
181-
182-
rejects = [var for var, dat in tbl_data.items() if dat['h0'] == 'reject']
183-
results = {'Type': 'Table',
184-
'Title': 'Results',
185-
'Headers': ['Test status', 'Variables analyzed', 'Rejecting', 'Critical value', 'Ensembles'],
186-
'Data': {'Test status': 'pass' if len(rejects) < args.critical else 'fail',
187-
'Variables analyzed': len(tbl_data.keys()),
188-
'Rejecting': len(rejects),
189-
'Critical value': args.critical,
190-
'Ensembles': 'statistically identical' if len(rejects) < args.critical else 'statistically different'}
191-
}
200+
201+
tabs = el.Tabs(
202+
{
203+
"Figures": img_gal,
204+
"Details": tables,
205+
"References": [el.RawHTML(bib_html)]
206+
}
207+
)
208+
rejects = [var for var, dat in details.items() if dat["h0"] == "reject"]
209+
210+
results = el.Table(
211+
title="Results",
212+
data=OrderedDict(
213+
{
214+
'Test status': ['pass' if len(rejects) < args.critical else 'fail'],
215+
'Variables analyzed': [len(details.keys())],
216+
'Rejecting': [len(rejects)],
217+
'Critical value': [int(args.critical)],
218+
'Ensembles': [
219+
'statistically identical' if len(rejects) < args.critical else 'statistically different'
220+
]
221+
}
222+
)
223+
)
192224

193225
# FIXME: Put into a ___ function
194-
page = el.page(name, __doc__.replace('\n\n', '<br><br>'), element_list=[results], tab_list=tl)
226+
page = el.Page(name, __doc__.replace('\n\n', '<br><br>'), elements=[results, tabs])
195227
return page
196228

197229

@@ -232,17 +264,17 @@ def print_details(details):
232264

233265

234266
def summarize_result(results_page):
235-
summary = {'Case': results_page['Title']}
236-
for elem in results_page['Data']['Elements']:
237-
if elem['Type'] == 'Table' and elem['Title'] == 'Results':
238-
summary['Test status'] = elem['Data']['Test status']
239-
summary['Variables analyzed'] = elem['Data']['Variables analyzed']
240-
summary['Rejecting'] = elem['Data']['Rejecting']
241-
summary['Critical value'] = elem['Data']['Critical value']
242-
summary['Ensembles'] = elem['Data']['Ensembles']
267+
summary = {'Case': results_page.title}
268+
269+
for elem in results_page.elements:
270+
if isinstance(elem, el.Table) and elem.title == "Results":
271+
summary['Test status'] = elem.data['Test status'][0]
272+
summary['Variables analyzed'] = elem.data['Variables analyzed'][0]
273+
summary['Rejecting'] = elem.data['Rejecting'][0]
274+
summary['Critical value'] = elem.data['Critical value'][0]
275+
summary['Ensembles'] = elem.data['Ensembles'][0]
243276
break
244-
else:
245-
continue
277+
246278
return {'': summary}
247279

248280

@@ -251,13 +283,13 @@ def populate_metadata():
251283
Generates the metadata responsible for telling the summary what
252284
is done by this module's run method
253285
"""
254-
286+
255287
metadata = {'Type': 'ValSummary',
256288
'Title': 'Validation',
257289
'TableTitle': 'Kolmogorov-Smirnov test',
258290
'Headers': ['Test status', 'Variables analyzed', 'Rejecting', 'Critical value', 'Ensembles']}
259291
return metadata
260-
292+
261293

262294
def main(args):
263295
ens_files, key1, key2 = case_files(args)
@@ -276,7 +308,7 @@ def main(args):
276308
if not common_vars:
277309
raise EVVException('No common variables between {} and {} to analyze!'.format(args.test_case, args.ref_case))
278310

279-
img_list = []
311+
images = {"accept": [], "reject": [], "-": []}
280312
details = LIVVDict()
281313
for var in sorted(common_vars):
282314
annuals_1 = annual_avgs.query('case == @args.test_case & variable == @var').monthly_mean.values
@@ -307,24 +339,32 @@ def main(args):
307339
img_file = os.path.relpath(os.path.join(args.img_dir, var + '.png'), os.getcwd())
308340
prob_plot(annuals_1, annuals_2, 20, img_file, test_name=args.test_case, ref_name=args.ref_case,
309341
pf=details[var]['h0'])
310-
311-
img_desc = 'Mean annual global average of {var} for <em>{testcase}</em> ' \
342+
_desc = monthly_avgs.query('case == @args.test_case & variable == @var').desc.values[0]
343+
img_desc = 'Mean annual global average of {var}{desc} for <em>{testcase}</em> ' \
312344
'is {testmean:.3e} and for <em>{refcase}</em> is {refmean:.3e}. ' \
313345
'Pass (fail) is indicated by {cpass} ({cfail}) coloring of the ' \
314346
'plot markers and bars.'.format(var=var,
347+
desc=_desc,
315348
testcase=args.test_case,
316349
testmean=details[var]['mean (test case, ref. case)'][0],
317350
refcase=args.ref_case,
318351
refmean=details[var]['mean (test case, ref. case)'][1],
319352
cfail=human_color_names['fail'][0],
320353
cpass=human_color_names['pass'][0])
321354

322-
img_link = os.path.join(os.path.basename(args.img_dir), os.path.basename(img_file))
323-
img_list.append(el.image(var, img_desc, img_link))
324-
325-
img_gal = el.gallery('Analyzed variables', img_list)
355+
img_link = Path(*Path(args.img_dir).parts[-2:], Path(img_file).name)
356+
_img = el.Image(var, img_desc, img_link, relative_to="", group=details[var]['h0'])
357+
images[details[var]['h0']].append(_img)
358+
359+
gals = []
360+
for group in ["reject", "accept", "-"]:
361+
_group_name = {
362+
"reject": "Failed variables", "accept": "Passed variables", "-": "Null variables"
363+
}
364+
if images[group]:
365+
gals.append(el.Gallery(_group_name[group], images[group]))
326366

327-
return details, img_gal
367+
return details, gals
328368

329369

330370
if __name__ == '__main__':

0 commit comments

Comments
 (0)