Update vfc_ci code

This commit is contained in:
Aurélien Delval 2021-05-03 15:47:44 +02:00
parent d81777e347
commit 44f0fc1f51
20 changed files with 398 additions and 477 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,24 +3,24 @@
import os
def serve(show, git_directory, git_url, port, allow_origin, logo_url):
# Prepare arguments
show = "--show" if show else ""
git = ""
if git_directory != None:
if git_directory is not None:
git = "git directory %s" % git_directory
if git_url != None:
if git_url is not None:
git = "git url %s" % git_url
logo = ""
if logo_url != None:
if logo_url is not None:
logo = "logo %s" % logo_url
dirname = os.path.dirname(__file__)
# Call the "bokeh serve" command on the system
command = "bokeh serve %s/vfc_ci_report %s --allow-websocket-origin=%s:%s --port %s --args %s %s" \
% (dirname, show, allow_origin, port, port, git, logo)

View File

@ -6,11 +6,11 @@ import sys
import os
from jinja2 import Environment, FileSystemLoader
################################################################################
##########################################################################
# Helper functions
def gen_readme(dev_branch, ci_branch):
# Init template loader
@ -26,17 +26,16 @@ def gen_readme(dev_branch, ci_branch):
fh.write(render)
def gen_workflow(git_host, dev_branch, ci_branch, repo):
# Init template loader
path = os.path.dirname(os.path.abspath(__file__))
env = Environment(loader=FileSystemLoader(path))
if git_host == "github":
# Load template
template = env.get_template("workflow_templates/vfc_test_workflow.j2.yml")
template = env.get_template(
"workflow_templates/vfc_test_workflow.j2.yml")
# Render it
render = template.render(dev_branch=dev_branch, ci_branch=ci_branch)
@ -47,13 +46,14 @@ def gen_workflow(git_host, dev_branch, ci_branch, repo):
with open(filename, "w") as fh:
fh.write(render)
if git_host == "gitlab":
template = env.get_template("workflow_templates/gitlab-ci.j2.yml")
# Ask for the user who will run the jobs (Gitlab specific)
username = input("[vfc_ci] Enter the name of the user who will run the CI jobs:")
email = input("[vfc_ci] Enter the e-mail of the user who will run the CI jobs:")
username = input(
"[vfc_ci] Enter the name of the user who will run the CI jobs:")
email = input(
"[vfc_ci] Enter the e-mail of the user who will run the CI jobs:")
remote_url = repo.remotes[0].config_reader.get("url")
remote_url = remote_url.replace("http://", "")
@ -72,12 +72,10 @@ def gen_workflow(git_host, dev_branch, ci_branch, repo):
fh.write(render)
################################################################################
##########################################################################
def setup(git_host):
# Init repo and make sure that the workflow setup is possible
repo = git.Repo(".")
@ -91,8 +89,9 @@ def setup(git_host):
dev_branch_name = str(dev_branch)
dev_remote = dev_branch.tracking_branch()
# Make sure that the active branch (on which to setup the workflow) has a remote
assert(dev_remote != None), "Error [vfc_ci]: The current branch doesn't " \
# Make sure that the active branch (on which to setup the workflow) has a
# remote
assert(dev_remote is not None), "Error [vfc_ci]: The current branch doesn't " \
"have a remote."
# Make sure that we are not behind the remote (so we can push safely later)
@ -101,8 +100,6 @@ def setup(git_host):
assert(commits_behind == []), "Error [vfc_ci]: The local branch seems " \
"to be at least one commit behind remote."
# Commit the workflow on the current (dev) branch
ci_branch_name = "vfc_ci_%s" % dev_branch_name
@ -111,8 +108,6 @@ def setup(git_host):
repo.index.commit("[auto] Set up Verificarlo CI on this branch")
repo.remote(name="origin").push()
# Create the CI branch (orphan branch with a readme on it)
# (see : https://github.com/gitpython-developers/GitPython/issues/615)
@ -133,23 +128,19 @@ def setup(git_host):
# Force checkout back to the original (dev) branch
repo.git.checkout(dev_branch_name, force=True)
# Print termination messages
print(
"Info [vfc_ci]: A Verificarlo CI workflow has been setup on " \
"Info [vfc_ci]: A Verificarlo CI workflow has been setup on "
"%s." % dev_branch_name
)
print(
"Info [vfc_ci]: Make sure that you have a \"vfc_tests_config.json\" on " \
"this branch. You can also perform a \"vfc_ci test\" dry run before "\
"pushing other commits."
)
"Info [vfc_ci]: Make sure that you have a \"vfc_tests_config.json\" on "
"this branch. You can also perform a \"vfc_ci test\" dry run before "
"pushing other commits.")
if git_host == "gitlab":
print(
"Info [vfc_ci]: Since you are using GitLab, make sure that you " \
"have created an access token for the user you specified (registered "\
"as a variable called \"CI_PUSH_TOKEN\" in your repository)."
)
"Info [vfc_ci]: Since you are using GitLab, make sure that you "
"have created an access token for the user you specified (registered "
"as a variable called \"CI_PUSH_TOKEN\" in your repository).")

View File

@ -1,6 +1,10 @@
# This script reads the vfc_tests_config.json file and executes tests accordingly
# It will also generate a ... .vfcrun.hd5 file with the results of the run
# It will also generate a ... .vfcrunh5 file with the results of the run
import sigdigits as sd
import scipy.stats
import numpy as np
import pandas as pd
import os
import json
@ -12,19 +16,13 @@ import time
import pickle
pickle.HIGHEST_PROTOCOL = 4
import pandas as pd
import numpy as np
import scipy.stats
import sigdigits as sd
# Magic numbers
min_pvalue = 0.05
max_zscore = 3
################################################################################
##########################################################################
# Helper functions
@ -36,7 +34,7 @@ def read_probes_csv(filepath, backend, warnings, execution_data):
except FileNotFoundError:
print(
"Warning [vfc_ci]: Probes not found, your code might have crashed " \
"Warning [vfc_ci]: Probes not found, your code might have crashed "
"or you might have forgotten to call vfc_dump_probes"
)
warnings.append(execution_data)
@ -46,7 +44,7 @@ def read_probes_csv(filepath, backend, warnings, execution_data):
except Exception:
print(
"Warning [vfc_ci]: Your probes could not be read for some unknown " \
"Warning [vfc_ci]: Your probes could not be read for some unknown "
"reason"
)
warnings.append(execution_data)
@ -56,12 +54,11 @@ def read_probes_csv(filepath, backend, warnings, execution_data):
if len(results) == 0:
print(
"Warning [vfc_ci]: Probes empty, it looks like you have dumped " \
"Warning [vfc_ci]: Probes empty, it looks like you have dumped "
"them without calling vfc_put_probe"
)
warnings.append(execution_data)
# Once the CSV has been opened and validated, return its content
results["value"] = results["value"].apply(lambda x: float.fromhex(x))
results.rename(columns={"value": "values"}, inplace=True)
@ -75,6 +72,9 @@ def read_probes_csv(filepath, backend, warnings, execution_data):
def significant_digits(x):
# If the null hypothesis is rejected, call sigdigits with the General
# formula:
if x.pvalue < min_pvalue:
# In a pandas DF, "values" actually refers to the array of columns, and
# not the column named "values"
distribution = x.values[3]
@ -83,41 +83,31 @@ def significant_digits(x):
# The distribution's empirical average will be used as the reference
mu = np.array([x.mu])
# If the null hypothesis is rejected, call sigdigits with General mode:
if x.pvalue < min_pvalue:
method = sd.Method.General
s = sd.significant_digits(
distribution,
mu,
precision=sd.Precision.Absolute,
method=method
precision=sd.Precision.Relative,
method=sd.Method.General,
probability=0.9,
confidence=0.95
)
# Else, manually compute sMCA which is equivalent to a 66% confidence interval
else:
method = sd.Method.CNH
s = sd.significant_digits(
distribution,
mu,
precision=sd.Precision.Absolute,
method=method,
probability=0.66,
confidence=0.66,
)
# s is returned as a size 1 list
# s is returned inside a list
return s[0]
# Else, manually compute sMCA (Stott-Parker formula)
else:
return -np.log2(np.absolute(x.sigma / x.mu))
def significant_digits_lower_bound(x):
# If the null hypothesis is rejected, no lower bound
if x.pvalue < min_pvalue:
return x.s2
# Else, the lower bound will be a 95% confidence interval
# Else, the lower bound will be computed with p= .9 alpha-1=.95
else:
distribution = x.values[3]
distribution = distribution.reshape(len(distribution), 1)
@ -126,16 +116,17 @@ def significant_digits_lower_bound(x):
s = sd.significant_digits(
distribution,
mu,
precision=sd.Precision.Absolute,
precision=sd.Precision.Relative,
method=sd.Method.CNH,
probability=0.9,
confidence=0.95
)
return s[0]
################################################################################
##########################################################################
# Main functions
@ -154,7 +145,6 @@ def read_config():
return json.loads(data)
# Set up metadata
def generate_metadata(is_git_commit):
@ -167,7 +157,6 @@ def generate_metadata(is_git_commit):
"message": ""
}
if is_git_commit:
print("Fetching metadata from last commit...")
from git import Repo
@ -185,7 +174,6 @@ def generate_metadata(is_git_commit):
return metadata
# Execute tests and collect results in a Pandas dataframe (+ dataprocessing)
def run_tests(config):
@ -204,10 +192,10 @@ def run_tests(config):
# not get any data
warnings = []
# Tests execution loop
for executable in config["executables"]:
print("Info [vfc_ci]: Running executable :", executable["executable"], "...")
print("Info [vfc_ci]: Running executable :",
executable["executable"], "...")
parameters = ""
if "parameters" in executable:
@ -245,26 +233,23 @@ def run_tests(config):
n_files = n_files + 1
# Clean CSV output files (by deleting the tmp folder)
os.system("rm -rf .vfcruns.tmp")
# Combine all separate executions in one dataframe
data = pd.concat(data, sort=False, ignore_index=True)
data = data.groupby(["test", "vfc_backend", "variable"])\
.values.apply(list).reset_index()
# Make sure we have some data to work on
assert(len(data) != 0), "Error [vfc_ci]: No data have been generated " \
"by your tests executions, aborting run without writing results file"
return data, warnings
# Data processing
def data_processing(data):
data["values"] = data["values"].apply(lambda x: np.array(x).astype(float))
@ -272,8 +257,8 @@ def data_processing(data):
# Get empirical average, standard deviation and p-value
data["mu"] = data["values"].apply(np.average)
data["sigma"] = data["values"].apply(np.std)
data["pvalue"] = data["values"].apply(lambda x: scipy.stats.shapiro(x).pvalue)
data["pvalue"] = data["values"].apply(
lambda x: scipy.stats.shapiro(x).pvalue)
# Significant digits
data["s2"] = data.apply(significant_digits, axis=1)
@ -281,8 +266,8 @@ def data_processing(data):
# Lower bound of the confidence interval using the sigdigits module
data["s2_lower_bound"] = data.apply(significant_digits_lower_bound, axis=1)
data["s10_lower_bound"] = data["s2_lower_bound"].apply(lambda x: sd.change_base(x, 10))
data["s10_lower_bound"] = data["s2_lower_bound"].apply(
lambda x: sd.change_base(x, 10))
# Compute moments of the distribution
# (including a new distribution obtained by filtering outliers)
@ -297,13 +282,13 @@ def data_processing(data):
data["nsamples"] = data["values"].apply(len)
# Display all executions that resulted in a warning
def show_warnings(warnings):
if len(warnings) > 0:
print(
"Warning [vfc_ci]: Some of your runs could not generate any data " \
"Warning [vfc_ci]: Some of your runs could not generate any data "
"(for instance because your code crashed) and resulted in "
"warnings. Here is the complete list :"
)
@ -316,9 +301,7 @@ def show_warnings(warnings):
print(" Repetition: %s" % warnings[i]["repetition"])
################################################################################
##########################################################################
# Entry point
@ -334,54 +317,51 @@ def run(is_git_commit, export_raw_values, dry_run):
data, warnings = run_tests(config)
show_warnings(warnings)
# Data processing
print("Info [vfc_ci]: Processing data...")
data_processing(data)
# Prepare data for export (by creating a proper index and linking run timestamp)
# Prepare data for export (by creating a proper index and linking run
# timestamp)
data = data.set_index(["test", "variable", "vfc_backend"]).sort_index()
data["timestamp"] = metadata["timestamp"]
filename = metadata["hash"] if is_git_commit else str(metadata["timestamp"])
filename = metadata["hash"] if is_git_commit else str(
metadata["timestamp"])
# Prepare metadata for export
metadata = pd.DataFrame.from_dict([metadata])
metadata = metadata.set_index("timestamp")
# NOTE : Exporting to HDF5 requires to install "tables" on the system
# Export raw data if needed
if export_raw_values and not dry_run:
data.to_hdf(filename + ".vfcraw.hd5", key="data")
metadata.to_hdf(filename + ".vfcraw.hd5", key="metadata")
data.to_hdf(filename + ".vfcraw.h5", key="data")
metadata.to_hdf(filename + ".vfcraw.h5", key="metadata")
# Export data
del data["values"]
if not dry_run:
data.to_hdf(filename + ".vfcrun.hd5", key="data")
metadata.to_hdf(filename + ".vfcrun.hd5", key="metadata")
data.to_hdf(filename + ".vfcrun.h5", key="data")
metadata.to_hdf(filename + ".vfcrun.h5", key="metadata")
# Print termination messages
print(
"Info [vfc_ci]: The results have been successfully written to " \
"%s.vfcrun.hd5." \
"Info [vfc_ci]: The results have been successfully written to "
"%s.vfcrun.h5."
% filename
)
if export_raw_values:
print(
"Info [vfc_ci]: A file containing the raw values has also been " \
"created : %s.vfcraw.hd5."
"Info [vfc_ci]: A file containing the raw values has also been "
"created : %s.vfcraw.h5."
% filename
)
if dry_run:
print(
"Info [vfc_ci]: The dry run flag was enabled, so no files were " \
"Info [vfc_ci]: The dry run flag was enabled, so no files were "
"actually created."
)

View File

@ -15,8 +15,7 @@ import helper
import plot
################################################################################
##########################################################################
class CompareRuns:
@ -44,43 +43,40 @@ class CompareRuns:
if n == 0 or n > len(timestamps):
n = len(timestamps)
for i in range(0, n):
# Get metadata associated to this run
row_metadata = helper.get_metadata(self.metadata, timestamps[-i-1])
row_metadata = helper.get_metadata(
self.metadata, timestamps[-i - 1])
date = time.ctime(timestamps[-i - 1])
# Fill the x series
str = row_metadata["name"]
x_series.insert(0, helper.get_metadata(self.metadata, timestamps[-i-1])["name"])
x_series.insert(0, helper.get_metadata(
self.metadata, timestamps[-i - 1])["name"])
# Fill the metadata lists
x_metadata["date"].insert(0, date)
x_metadata["is_git_commit"].insert(0, row_metadata["is_git_commit"])
x_metadata["is_git_commit"].insert(
0, row_metadata["is_git_commit"])
x_metadata["hash"].insert(0, row_metadata["hash"])
x_metadata["author"].insert(0, row_metadata["author"])
x_metadata["message"].insert(0, row_metadata["message"])
return x_series, x_metadata
# Plots update function
def update_plots(self):
# Select all data matching current test/var/backend
runs = self.data.loc[
[self.widgets["select_test"].value],
self.widgets["select_var"].value, self.widgets["select_backend"].value
]
runs = self.data.loc[[self.widgets["select_test"].value],
self.widgets["select_var"].value,
self.widgets["select_backend"].value]
timestamps = runs["timestamp"]
x_series, x_metadata = self.gen_x_series(timestamps.sort_values())
# Update source
main_dict = runs.to_dict("series")
@ -93,7 +89,6 @@ class CompareRuns:
n = self.current_n_runs
main_dict = {key: value[-n:] for key, value in main_dict.items()}
# Generate ColumnDataSources for the 3 dotplots
for stat in ["sigma", "s10", "s2"]:
dict = {
@ -111,18 +106,20 @@ class CompareRuns:
}
if stat == "s10" or stat == "s2":
dict["%s_lower_bound" % stat] = main_dict["%s_lower_bound" % stat]
dict["%s_lower_bound" %
stat] = main_dict["%s_lower_bound" %
stat]
# Filter outliers if the box is checked
if len(self.widgets["outliers_filtering_compare"].active) > 0:
outliers = helper.detect_outliers(dict[stat])
dict[stat] = helper.remove_outliers(dict[stat], outliers)
dict["%s_x" % stat] = helper.remove_outliers(dict["%s_x" % stat], outliers)
dict["%s_x" % stat] = helper.remove_outliers(
dict["%s_x" % stat], outliers)
# Assign ColumnDataSource
self.sources["%s_source" % stat].data = dict
# Generate ColumnDataSource for the boxplot
dict = {
"is_git_commit": main_dict["is_git_commit"],
@ -143,29 +140,37 @@ class CompareRuns:
"nsamples": main_dict["nsamples"]
}
self.sources["boxplot_source"].data = dict
# Update x axis
# Update x_ranges
helper.reset_x_range(self.plots["boxplot"], self.sources["boxplot_source"].data["x"])
helper.reset_x_range(self.plots["sigma_plot"], self.sources["sigma_source"].data["sigma_x"])
helper.reset_x_range(self.plots["s10_plot"], self.sources["s10_source"].data["s10_x"])
helper.reset_x_range(self.plots["s2_plot"], self.sources["s2_source"].data["s2_x"])
helper.reset_x_range(
self.plots["boxplot"],
self.sources["boxplot_source"].data["x"]
)
helper.reset_x_range(
self.plots["sigma_plot"],
self.sources["sigma_source"].data["sigma_x"]
)
helper.reset_x_range(
self.plots["s10_plot"],
self.sources["s10_source"].data["s10_x"]
)
helper.reset_x_range(
self.plots["s2_plot"],
self.sources["s2_source"].data["s2_x"]
)
# Widgets' callback functions
def update_test(self, attrname, old, new):
# If the value is updated by the CustomJS, self.widgets["select_var"].value
# won't be updated, so we have to look for that case and assign it manually
# won't be updated, so we have to look for that case and assign it
# manually
# "new" should be a list when updated by CustomJS
if type(new) == list:
if isinstance(new, list):
# If filtering removed all options, we might have an empty list
# (in this case, we just skip the callback and do nothing)
if len(new) > 0:
@ -183,7 +188,6 @@ class CompareRuns:
.index.get_level_values("variable").drop_duplicates().tolist()
self.widgets["select_var"].options = self.vars
# Reset var selection if old one is not available in new vars
if self.widgets["select_var"].value not in self.vars:
self.widgets["select_var"].value = self.vars[0]
@ -194,14 +198,14 @@ class CompareRuns:
# anyway)
self.update_var("", "", self.widgets["select_var"].value)
def update_var(self, attrname, old, new):
# If the value is updated by the CustomJS, self.widgets["select_var"].value
# won't be updated, so we have to look for that case and assign it manually
# won't be updated, so we have to look for that case and assign it
# manually
# new should be a list when updated by CustomJS
if type(new) == list:
if isinstance(new, list):
new = new[0]
if new != self.widgets["select_var"].value:
@ -209,7 +213,6 @@ class CompareRuns:
self.widgets["select_var"].value = new
return
# New list of available backends
self.backends = self.data.loc[self.widgets["select_test"].value, self.widgets["select_var"].value]\
.index.get_level_values("vfc_backend").drop_duplicates().tolist()
@ -225,13 +228,11 @@ class CompareRuns:
# anyway)
self.update_backend("", "", self.widgets["select_backend"].value)
def update_backend(self, attrname, old, new):
# Simply update plots, since no other data is affected
self.update_plots()
def update_n_runs(self, attrname, old, new):
# Simply update runs selection (value and string display)
self.select_n_runs.value = new
@ -239,12 +240,9 @@ class CompareRuns:
self.update_plots()
def update_outliers_filtering(self, attrname, old, new):
self.update_plots()
# Bokeh setup functions
def setup_plots(self):
@ -256,7 +254,6 @@ class CompareRuns:
# (defined inside template to avoid bloating server w/ too much JS code)
js_tap_callback = "goToInspectRuns();"
# Box plot
self.plots["boxplot"] = figure(
name="boxplot", title="Variable distribution over runs",
@ -297,7 +294,6 @@ class CompareRuns:
)
self.doc.add_root(self.plots["boxplot"])
# Sigma plot (bar plot)
self.plots["sigma_plot"] = figure(
name="sigma_plot", title="Standard deviation σ over runs",
@ -324,7 +320,6 @@ class CompareRuns:
)
self.doc.add_root(self.plots["sigma_plot"])
# s plot (bar plot with 2 tabs)
self.plots["s10_plot"] = figure(
name="s10_plot", title="Significant digits s over runs",
@ -353,7 +348,6 @@ class CompareRuns:
)
s10_tab = Panel(child=self.plots["s10_plot"], title="Base 10")
self.plots["s2_plot"] = figure(
name="s2_plot", title="Significant digits s over runs",
plot_width=900, plot_height=400, x_range=[""],
@ -389,7 +383,6 @@ class CompareRuns:
self.doc.add_root(s_tabs)
def setup_widgets(self):
# Initial selections
@ -404,13 +397,11 @@ class CompareRuns:
self.backends = self.data.loc[self.tests[0], self.vars[0]]\
.index.get_level_values("vfc_backend").drop_duplicates().tolist()
# Custom JS callback that will be used client side to filter selections
filter_callback_js = """
selector.options = options.filter(e => e.includes(cb_obj.value));
"""
# Test selector widget
# Number of runs to display
@ -442,13 +433,15 @@ class CompareRuns:
self.widgets["test_filter"] = TextInput(
name="test_filter", title="Tests filter:"
)
self.widgets["test_filter"].js_on_change("value", CustomJS(
args=dict(options=self.tests, selector=self.widgets["select_test"]),
code=filter_callback_js
))
self.widgets["test_filter"].js_on_change(
"value",
CustomJS(
args=dict(
options=self.tests,
selector=self.widgets["select_test"]),
code=filter_callback_js))
self.doc.add_root(self.widgets["test_filter"])
# Number of runs to display
self.widgets["select_n_runs"] = Select(
@ -458,7 +451,6 @@ class CompareRuns:
self.doc.add_root(self.widgets["select_n_runs"])
self.widgets["select_n_runs"].on_change("value", self.update_n_runs)
# Variable selector widget
self.widgets["select_var"] = Select(
@ -469,7 +461,6 @@ class CompareRuns:
self.widgets["select_var"].on_change("value", self.update_var)
self.widgets["select_var"].on_change("options", self.update_var)
# Backend selector widget
self.widgets["select_backend"] = Select(
@ -479,7 +470,6 @@ class CompareRuns:
self.doc.add_root(self.widgets["select_backend"])
self.widgets["select_backend"].on_change("value", self.update_backend)
# Outliers filtering checkbox
self.widgets["outliers_filtering_compare"] = CheckboxGroup(
@ -490,12 +480,11 @@ class CompareRuns:
self.widgets["outliers_filtering_compare"]\
.on_change("active", self.update_outliers_filtering)
# Communication methods
# (to send/receive messages to/from master)
# Callback to change view of Inspect runs when data is selected
def inspect_run_callback(self, new, source_name, x_name):
# In case we just unselected everything, then do nothing
@ -507,7 +496,6 @@ class CompareRuns:
self.master.go_to_inspect(run_name)
# Wrappers for each plot (since new is the index of the clicked element,
# it is dependent of the plot because we could have filtered some outliers)
# There doesn't seem to be an easy way to add custom parameters to a
@ -525,7 +513,6 @@ class CompareRuns:
def inspect_run_callback_s10(self, attr, old, new):
self.inspect_run_callback(new, "s10_source", "s10_x")
# Constructor
def __init__(self, master, doc, data, metadata):
@ -536,7 +523,6 @@ class CompareRuns:
self.data = data
self.metadata = metadata
self.sources = {
"boxplot_source": ColumnDataSource(data={}),
"sigma_source": ColumnDataSource(data={}),

View File

@ -10,7 +10,7 @@ import numpy as np
max_ticks = 15
max_zscore = 3
################################################################################
##########################################################################
# From a timestamp, return the associated metadata as a Pandas serie
@ -39,7 +39,6 @@ def get_run_name(timestamp, hash):
now = calendar.timegm(gmt)
diff = now - timestamp
# Special case : < 1 minute (return string directly)
if diff < 60:
str = "Less than a minute ago"
@ -83,12 +82,10 @@ def get_run_name(timestamp, hash):
str = str % (n, plural)
# We might want to add the git hash
if hash != "":
str = str + " (%s)" % hash
# Finally, check for duplicate with previously generated string
if str == get_run_name.previous:
# Increment the duplicate counter and add it to str
@ -96,12 +93,14 @@ def get_run_name(timestamp, hash):
str = "%s (%s)" % (str, get_run_name.counter)
else:
# No duplicate, reset both previously generated str and duplicate counter
# No duplicate, reset both previously generated str and duplicate
# counter
get_run_name.counter = 0
get_run_name.previous = str
return str
# These external variables will store data about the last generated string to
# avoid duplicates (assuming the runs are sorted by time)
get_run_name.counter = 0
@ -156,11 +155,16 @@ def remove_boxplot_outliers(dict, outliers, prefix):
dict["%s_x" % prefix] = remove_outliers(dict["%s_x" % prefix], outliers)
dict["%s_min" % prefix] = remove_outliers(dict["%s_min" % prefix], outliers)
dict["%s_quantile25" % prefix] = remove_outliers(dict["%s_quantile25" % prefix], outliers)
dict["%s_quantile50" % prefix] = remove_outliers(dict["%s_quantile50" % prefix], outliers)
dict["%s_quantile75" % prefix] = remove_outliers(dict["%s_quantile75" % prefix], outliers)
dict["%s_max" % prefix] = remove_outliers(dict["%s_max" % prefix], outliers)
dict["%s_min" % prefix] = remove_outliers(
dict["%s_min" % prefix], outliers)
dict["%s_quantile25" % prefix] = remove_outliers(
dict["%s_quantile25" % prefix], outliers)
dict["%s_quantile50" % prefix] = remove_outliers(
dict["%s_quantile50" % prefix], outliers)
dict["%s_quantile75" % prefix] = remove_outliers(
dict["%s_quantile75" % prefix], outliers)
dict["%s_max" % prefix] = remove_outliers(
dict["%s_max" % prefix], outliers)
dict["%s_mu" % prefix] = remove_outliers(dict["%s_mu" % prefix], outliers)
dict["nsamples"] = remove_outliers(dict["nsamples"], outliers)

View File

@ -15,8 +15,7 @@ import helper
import plot
################################################################################
##########################################################################
class InspectRuns:
@ -40,7 +39,6 @@ class InspectRuns:
return runs_dict
def gen_boxplot_tooltips(self, prefix):
return [
("Name", "@%s_x" % prefix),
@ -63,26 +61,27 @@ class InspectRuns:
"@%s_mu" % prefix: "printf"
}
# Data processing helper
# (computes new distributions for sigma, s2, s10)
def data_processing(self, dataframe):
# Compute aggragated mu
dataframe["mu"] = np.vectorize(np.average)(dataframe["mu"], weights=dataframe["nsamples"])
dataframe["mu"] = np.vectorize(
np.average)(
dataframe["mu"],
weights=dataframe["nsamples"])
# nsamples is the number of aggregated elements (as well as the number
# of samples for our new sigma and s distributions)
dataframe["nsamples"] = dataframe["nsamples"].apply(lambda x: len(x))
dataframe["mu_x"] = dataframe.index
# Make sure that strings don't excede a certain length
dataframe["mu_x"] = dataframe["mu_x"].apply(
lambda x: x[:17] + "[...]" + x[-17:] if len(x) > 39 else x
)
# Get quantiles and mu for sigma, s10, s2
for prefix in ["sigma", "s10", "s2"]:
@ -91,18 +90,18 @@ class InspectRuns:
dataframe[prefix] = dataframe[prefix].apply(np.sort)
dataframe["%s_min" % prefix] = dataframe[prefix].apply(np.min)
dataframe["%s_quantile25" % prefix] = dataframe[prefix].apply(np.quantile, args=(0.25,))
dataframe["%s_quantile50" % prefix] = dataframe[prefix].apply(np.quantile, args=(0.50,))
dataframe["%s_quantile75" % prefix] = dataframe[prefix].apply(np.quantile, args=(0.75,))
dataframe["%s_quantile25" % prefix] = dataframe[prefix].apply(
np.quantile, args=(0.25,))
dataframe["%s_quantile50" % prefix] = dataframe[prefix].apply(
np.quantile, args=(0.50,))
dataframe["%s_quantile75" % prefix] = dataframe[prefix].apply(
np.quantile, args=(0.75,))
dataframe["%s_max" % prefix] = dataframe[prefix].apply(np.max)
dataframe["%s_mu" % prefix] = dataframe[prefix].apply(np.average)
del dataframe[prefix]
return dataframe
# Plots update function
def update_plots(self):
@ -117,7 +116,6 @@ class InspectRuns:
]
filterby = self.factors_dict[filterby_display]
# Groupby and aggregate lines belonging to the same group in lists
groups = self.run_data[
@ -131,17 +129,16 @@ class InspectRuns:
"sigma": lambda x: x.tolist(),
"s10": lambda x: x.tolist(),
"s2": lambda x: x.tolist(),
"mu": lambda x: x.tolist(),
# Used for mu weighted average first, then will be replaced
"nsamples": lambda x: x.tolist()
})
# Compute the new distributions, ...
groups = self.data_processing(groups).to_dict("list")
# Update source
# Assign each ColumnDataSource, starting with the boxplots
@ -166,7 +163,8 @@ class InspectRuns:
top_outliers = helper.detect_outliers(dict["%s_max" % prefix])
helper.remove_boxplot_outliers(dict, top_outliers, prefix)
bottom_outliers = helper.detect_outliers(dict["%s_min" % prefix])
bottom_outliers = helper.detect_outliers(
dict["%s_min" % prefix])
helper.remove_boxplot_outliers(dict, bottom_outliers, prefix)
self.sources["%s_source" % prefix].data = dict
@ -185,8 +183,8 @@ class InspectRuns:
if len(self.widgets["outliers_filtering_inspect"].active) > 0:
mu_outliers = helper.detect_outliers(groups["mu"])
groups["mu"] = helper.remove_outliers(groups["mu"], mu_outliers)
groups["mu_x"] = helper.remove_outliers(groups["mu_x"], mu_outliers)
groups["mu_x"] = helper.remove_outliers(
groups["mu_x"], mu_outliers)
# Update plots axis/titles
@ -194,42 +192,38 @@ class InspectRuns:
factors_dict = self.factors_dict.copy()
del factors_dict[groupby_display]
del factors_dict[filterby_display]
over_all = list(factors_dict.keys())[0]
for_all = list(factors_dict.keys())[0]
# Update all display strings for plot title (remove caps, plural)
groupby_display = groupby_display.lower()
filterby_display = filterby_display.lower()[:-1]
over_all = over_all.lower()
for_all = for_all.lower()
self.plots["mu_inspect"].title.text = \
"Empirical average μ of %s (groupped by %s, for all %s)" \
% (filterby_display, groupby_display, over_all)
% (filterby_display, groupby_display, for_all)
self.plots["sigma_inspect"].title.text = \
"Standard deviation σ of %s (groupped by %s, for all %s)" \
% (filterby_display, groupby_display, over_all)
% (filterby_display, groupby_display, for_all)
self.plots["s10_inspect"].title.text = \
"Significant digits s of %s (groupped by %s, for all %s)" \
% (filterby_display, groupby_display, over_all)
% (filterby_display, groupby_display, for_all)
self.plots["s2_inspect"].title.text = \
"Significant digits s of %s (groupped by %s, for all %s)" \
% (filterby_display, groupby_display, over_all)
# Update x_ranges
% (filterby_display, groupby_display, for_all)
helper.reset_x_range(self.plots["mu_inspect"], groups["mu_x"])
helper.reset_x_range(self.plots["sigma_inspect"], groups["sigma_x"])
helper.reset_x_range(self.plots["s10_inspect"], groups["s10_x"])
helper.reset_x_range(self.plots["s2_inspect"], groups["s2_x"])
# Widets' callback functions
# Run selector callback
def update_run(self, attrname, old, new):
filterby = self.widgets["filterby_radio"].labels[
@ -260,8 +254,8 @@ class InspectRuns:
# anyway)
self.update_filter("", "", old_value)
# "Group by" radio
def update_groupby(self, attrname, old, new):
# Update "Filter by" radio list
@ -269,7 +263,6 @@ class InspectRuns:
del filterby_list[self.widgets["groupby_radio"].active]
self.widgets["filterby_radio"].labels = filterby_list
filterby = self.widgets["filterby_radio"].labels[
self.widgets["filterby_radio"].active
]
@ -292,8 +285,8 @@ class InspectRuns:
# anyway)
self.update_filter("", "", old_value)
# "Filter by" radio
def update_filterby(self, attrname, old, new):
filterby = self.widgets["filterby_radio"].labels[
@ -318,20 +311,18 @@ class InspectRuns:
# anyway)
self.update_filter("", "", old_value)
# Filter selector callback
def update_filter(self, attrname, old, new):
self.update_plots()
# Filter outliers checkbox callback
def update_outliers_filtering(self, attrname, old, new):
# The status (checked/unchecked) of the checkbox is also verified inside
# self.update_plots(), so calling this function is enough
self.update_plots()
# Bokeh setup functions
# (for both variable and backend selection at once)
@ -339,7 +330,6 @@ class InspectRuns:
tools = "pan, wheel_zoom, xwheel_zoom, ywheel_zoom, reset, save"
# Tooltips and formatters
dotplot_tooltips = [
@ -352,14 +342,16 @@ class InspectRuns:
}
sigma_boxplot_tooltips = self.gen_boxplot_tooltips("sigma")
sigma_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters("sigma")
sigma_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters(
"sigma")
s10_boxplot_tooltips = self.gen_boxplot_tooltips("s10")
s10_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters("s10")
s10_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters(
"s10")
s2_boxplot_tooltips = self.gen_boxplot_tooltips("s2")
s2_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters("s2")
s2_boxplot_tooltips_formatters = self.gen_boxplot_tooltips_formatters(
"s2")
# Plots
@ -377,7 +369,6 @@ class InspectRuns:
)
self.doc.add_root(self.plots["mu_inspect"])
# Sigma plot
self.plots["sigma_inspect"] = figure(
name="sigma_inspect",
@ -386,13 +377,13 @@ class InspectRuns:
tools=tools, sizing_mode="scale_width"
)
plot.fill_boxplot(
self.plots["sigma_inspect"], self.sources["sigma_source"], prefix="sigma",
self.plots["sigma_inspect"],
self.sources["sigma_source"],
prefix="sigma",
tooltips=sigma_boxplot_tooltips,
tooltips_formatters = sigma_boxplot_tooltips_formatters
)
tooltips_formatters=sigma_boxplot_tooltips_formatters)
self.doc.add_root(self.plots["sigma_inspect"])
# s plots
self.plots["s10_inspect"] = figure(
name="s10_inspect",
@ -401,11 +392,14 @@ class InspectRuns:
tools=tools, sizing_mode='scale_width'
)
plot.fill_boxplot(
self.plots["s10_inspect"], self.sources["s10_source"], prefix="s10",
self.plots["s10_inspect"],
self.sources["s10_source"],
prefix="s10",
tooltips=s10_boxplot_tooltips,
tooltips_formatters = s10_boxplot_tooltips_formatters
)
s10_tab_inspect = Panel(child=self.plots["s10_inspect"], title="Base 10")
tooltips_formatters=s10_boxplot_tooltips_formatters)
s10_tab_inspect = Panel(
child=self.plots["s10_inspect"],
title="Base 10")
self.plots["s2_inspect"] = figure(
name="s2_inspect",
@ -426,8 +420,6 @@ class InspectRuns:
)
self.doc.add_root(s_tabs_inspect)
def setup_widgets(self):
# Generation of selectable items
@ -445,7 +437,6 @@ class InspectRuns:
"Tests": "test"
}
# Run selection
# Contains all options strings
@ -457,7 +448,6 @@ class InspectRuns:
# This contains only entries matching the run
self.run_data = self.data[self.data["timestamp"] == self.current_run]
change_run_callback_js = "updateRunMetadata(cb_obj.value);"
self.widgets["select_run"] = Select(
@ -475,7 +465,6 @@ class InspectRuns:
))
))
# Factors selection
# "Group by" radio
@ -491,7 +480,6 @@ class InspectRuns:
self.update_groupby
)
# "Filter by" radio
# Get all possible factors, and remove the one selected in "Group by"
filterby_list = list(self.factors_dict.keys())
@ -509,7 +497,6 @@ class InspectRuns:
self.update_filterby
)
# Filter selector
filterby = self.widgets["filterby_radio"].labels[
@ -530,7 +517,6 @@ class InspectRuns:
self.widgets["select_filter"]\
.on_change("value", self.update_filter)
# Toggle for outliers filtering
self.widgets["outliers_filtering_inspect"] = CheckboxGroup(
@ -541,17 +527,14 @@ class InspectRuns:
self.widgets["outliers_filtering_inspect"]\
.on_change("active", self.update_outliers_filtering)
# Communication methods
# (to send/receive messages to/from master)
# When received, switch to the run_name in parameter
def switch_view(self, run_name):
self.widgets["select_run"].value = run_name
# Constructor
def __init__(self, master, doc, data, metadata):
@ -562,7 +545,6 @@ class InspectRuns:
self.data = data
self.metadata = metadata
self.sources = {
"mu_source": ColumnDataSource(data={}),
"sigma_source": ColumnDataSource(data={}),

View File

@ -1,5 +1,5 @@
# Look for and read all the run files in the current directory (ending with
# .vfcrun.hd5), and lanch a Bokeh server for the visualization of this data.
# .vfcrunh5), and lanch a Bokeh server for the visualization of this data.
import os
import sys
@ -14,18 +14,16 @@ import compare_runs
import inspect_runs
import helper
################################################################################
##########################################################################
# Read vfcrun files, and aggregate them in one dataset
run_files = [ f for f in os.listdir(".") if f.endswith(".vfcrun.hd5") ]
run_files = [f for f in os.listdir(".") if f.endswith(".vfcrun.h5")]
if len(run_files) == 0:
print(
"Warning [vfc_ci]: Could not find any vfcrun files in the directory. " \
"This will result in server errors and prevent you from viewing the report."
)
"Warning [vfc_ci]: Could not find any vfcrun files in the directory. "
"This will result in server errors and prevent you from viewing the report.")
# These are arrays of Pandas dataframes for now
metadata = []
@ -55,12 +53,11 @@ metadata["date"] = metadata.index.to_series().map(
)
################################################################################
##########################################################################
curdoc().title = "Verificarlo Report"
# Read server arguments
# (this is quite easy because Bokeh server is called through a wrapper, so
# we know exactly what the arguments might be)
@ -83,7 +80,6 @@ for i in range(1, len(sys.argv)):
address = sys.argv[i + 2]
url = ""
# Here, address is either the remote URL or the path to the local Git
# repo (depending on the method)
@ -99,12 +95,11 @@ for i in range(1, len(sys.argv)):
else:
raise ValueError(
"Error [vfc_ci]: The specified method to get the Git " \
"repository is invalid. Are you calling Bokeh directly " \
"Error [vfc_ci]: The specified method to get the Git "
"repository is invalid. Are you calling Bokeh directly "
"instead of using the Verificarlo wrapper ?"
)
# At this point, "url" should be set correctly, we can get the repo's
# URL and name, after making sure we're on a Git URL
@ -113,7 +108,7 @@ for i in range(1, len(sys.argv)):
path = parsed_url.path.split("/")
if len(path) < 3:
raise ValueError(
"Error [vfc_ci]: The found URL doesn't seem to be pointing " \
"Error [vfc_ci]: The found URL doesn't seem to be pointing "
"to a Git repository (path is too short)"
)
@ -122,7 +117,6 @@ for i in range(1, len(sys.argv)):
curdoc().template_variables["repo_url"] = url
curdoc().template_variables["repo_name"] = repo_name
# We should have a "github.com" or a "*gitlab*" URL
if parsed_url.netloc == "github.com":
@ -148,8 +142,6 @@ for i in range(1, len(sys.argv)):
git_repo_linked = True
# Look for a logo URL
# If a logo URL is specified, it will be included in the report's header
if sys.argv[i] == "logo":
@ -162,8 +154,7 @@ curdoc().template_variables["git_repo_linked"] = git_repo_linked
curdoc().template_variables["has_logo"] = has_logo
################################################################################
##########################################################################
# Setup report views
@ -179,7 +170,6 @@ class ViewsMaster:
def go_to_inspect(self, run_name):
self.inspect.switch_view(run_name)
# Constructor
def __init__(self, data, metadata, git_repo_linked, commit_link):
@ -190,7 +180,8 @@ class ViewsMaster:
self.commit_link = commit_link
# Pass metadata to the template as a JSON string
curdoc().template_variables["metadata"] = self.metadata.to_json(orient="index")
curdoc().template_variables["metadata"] = self.metadata.to_json(
orient="index")
# Runs comparison
self.compare = compare_runs.CompareRuns(

View File

@ -15,21 +15,19 @@ def fill_dotplot(
):
# (Optional) Tooltip and tooltip formatters
if tooltips != None:
if tooltips is not None:
hover = HoverTool(tooltips=tooltips, mode="vline", names=["circle"])
if tooltips_formatters != None:
if tooltips_formatters is not None:
hover.formatters = tooltips_formatters
plot.add_tools(hover)
# (Optional) Add TapTool (for JS tap callback)
if js_tap_callback != None:
if js_tap_callback is not None:
tap = TapTool(callback=CustomJS(code=js_tap_callback))
plot.add_tools(tap)
# (Optional) Add segment to represent a lower bound
if lower_bound:
lower_segment = plot.segment(
@ -38,24 +36,20 @@ def fill_dotplot(
source=source, line_color="black"
)
# Draw dots (actually Bokeh circles)
circle = plot.circle(
name="circle",
x="%s_x" % data_field, y=data_field, source=source, size=12
)
# (Optional) Draw lines between dots
if lines:
line = plot.line(x="%s_x" % data_field, y=data_field, source=source)
# (Optional) Add server tap callback
if server_tap_callback != None:
if server_tap_callback is not None:
circle.data_source.selected.on_change("indices", server_tap_callback)
# Plot appearance
plot.xgrid.grid_line_color = None
plot.ygrid.grid_line_color = None
@ -67,30 +61,27 @@ def fill_dotplot(
plot.xaxis[0].major_label_orientation = pi / 8
def fill_boxplot(
plot, source,
prefix="",
tooltips=None, tooltips_formatters=None,
js_tap_callback=None, server_tap_callback=None,
js_tap_callback=None, server_tap_callback=None
):
# (Optional) Tooltip and tooltip formatters
if tooltips != None:
if tooltips is not None:
hover = HoverTool(tooltips=tooltips, mode="vline", names=["full_box"])
if tooltips_formatters != None:
if tooltips_formatters is not None:
hover.formatters = tooltips_formatters
plot.add_tools(hover)
# (Optional) Add TapTool (for JS tap callback)
if js_tap_callback != None:
if js_tap_callback is not None:
tap = TapTool(callback=CustomJS(code=js_tap_callback))
plot.add_tools(tap)
# Draw boxes (the prefix argument modifies the fields of ColumnDataSource
# that are used)
@ -128,18 +119,18 @@ def fill_boxplot(
color="black"
)
# (Optional) Add server tap callback
if server_tap_callback != None:
if server_tap_callback is not None:
top_stem.data_source.selected.on_change("indices", server_tap_callback)
bottom_stem.data_source.selected.on_change("indices", server_tap_callback)
bottom_stem.data_source.selected.on_change(
"indices", server_tap_callback)
full_box.data_source.selected.on_change("indices", server_tap_callback)
bottom_box.data_source.selected.on_change("indices", server_tap_callback)
bottom_box.data_source.selected.on_change(
"indices", server_tap_callback)
mu_dot.data_source.selected.on_change("indices", server_tap_callback)
# Plot appearance
plot.xgrid.grid_line_color = None
plot.ygrid.grid_line_color = None

View File

@ -291,19 +291,19 @@
<!-- PLOTS -->
<div class="column is-9">
<h3 class="title is-3">Plots</h3>
<div class="card plot-card">
<div class="card plot-card" style="z-index: 3;">
{{ embed(roots.s_tabs_inspect) }}
</div>
<br>
<div class="card plot-card">
<div class="card plot-card" style="z-index: 2;">
{{ embed(roots.sigma_inspect) }}
</div>
<br>
<div class="card plot-card">
<div class="card plot-card" style="z-index: 1;">
{{ embed(roots.mu_inspect) }}
</div>
</div>
@ -325,6 +325,8 @@
</div>
<!-- JAVASCRIPT -->
<script>
// Listen to clicks on breadcrumb (for responsive header)

View File

@ -29,7 +29,7 @@ run_verificarlo_tests:
- git fetch --all
- git checkout -b {{ci_branch}} origin/{{ci_branch}}
- mkdir -p vfcruns
- mv *.vfcrun.hd5 vfcruns
- mv *.vfcrun.h5 vfcruns
- git add vfcruns/*
- git commit -m "[auto] New test results for commit ${git_hash}"
- git push
@ -39,4 +39,4 @@ run_verificarlo_tests:
artifacts:
paths:
- "*.vfcraw.hd5"
- "*.vfcraw.h5"

View File

@ -26,7 +26,7 @@ jobs:
- name: Install Python requirements
run: |
pip install numpy scipy pandas bokeh jinja2 tables GitPython
apt update
apt install wget
wget https://raw.githubusercontent.com/verificarlo/significantdigits/main/sigdigits.py -P /usr/local/lib/python3.8/dist-packages
@ -45,7 +45,7 @@ jobs:
git checkout {{ci_branch}}
mkdir -p vfcruns
mv *.vfcrun.hd5 vfcruns
mv *.vfcrun.h5 vfcruns
git add vfcruns/*
git commit -m "[auto] New test results for commit ${git_hash}"
git push
@ -54,4 +54,4 @@ jobs:
uses: actions/upload-artifact@v2
with:
{% raw %}name: ${{github.sha}}.vfcraw{% endraw %}
path: ./*.vfcraw.hd5
path: ./*.vfcraw.h5

24
vfc_ci
View File

@ -10,10 +10,11 @@
import argparse
################################################################################
##########################################################################
# Parameters validation helpers
def is_port(string):
value = int(string)
if value < 0 or value > 65535:
@ -31,8 +32,7 @@ def is_directory(string):
return string
################################################################################
##########################################################################
# Subcommand decorator
@ -41,6 +41,7 @@ cli = argparse.ArgumentParser(
)
subparsers = cli.add_subparsers(dest="subcommand")
def subcommand(description="", args=[], parent=subparsers):
def decorator(func):
parser = parent.add_parser(func.__name__, description=description)
@ -54,9 +55,7 @@ def argument(*name_or_flags, **kwargs):
return ([*name_or_flags], kwargs)
################################################################################
##########################################################################
# "setup" subcommand
@ -76,10 +75,9 @@ def setup(args):
import ci.setup
ci.setup.setup(args.git_host)
# "test" subcommand
@subcommand(
description="Execute predefined Verificarlo tests and save their results.",
args=[
@ -112,10 +110,9 @@ def test(args):
import ci.test
ci.test.run(args.is_git_commit, args.export_raw_results, args.dry_run)
# "serve" subcommand
@subcommand(
description="""
Start a server to visualize Verificarlo test results.
@ -174,9 +171,9 @@ def test(args):
def serve(args):
# git_directory and git_url are supposed to be exclusive
if args.git_directory != None and args.git_url != None:
if args.git_directory is not None and args.git_url is not None:
raise argparse.ArgumentTypeError(
"\"-gd\" / \"--git-directory\" and \"-gu\" / \"--git-url\" are "\
"\"-gd\" / \"--git-directory\" and \"-gu\" / \"--git-url\" are "
"mutually exclusive. Please make sure to use at most one of them."
)
@ -191,12 +188,9 @@ def serve(args):
)
###############################################################################
# Main command group and entry point
if __name__ == "__main__":
args = cli.parse_args()
if args.subcommand is None: