diff --git a/examples/solver_comparison.py b/examples/solver_comparison.py index e69de29..b4bfdc8 100644 --- a/examples/solver_comparison.py +++ b/examples/solver_comparison.py @@ -0,0 +1,136 @@ +import os +import pickle +import numpy as np +from math import pi +import pandas as pd +import matplotlib.pyplot as plt + + + +from hyppopy.SolverPool import SolverPool +from hyppopy.HyppopyProject import HyppopyProject +from hyppopy.VirtualFunction import VirtualFunction +from hyppopy.BlackboxFunction import BlackboxFunction + +def make_spider(results, row, title, groundtruth): + categories = ["axis_00", "axis_01", "axis_02", "axis_03", "axis_04"] + N = len(categories) + + angles = [n / float(N) * 2 * pi for n in range(N)] + angles += angles[:1] + + ax = plt.subplot(2, 2, row+1, polar=True, ) + + ax.set_theta_offset(pi / 2) + ax.set_theta_direction(-1) + + plt.xticks(angles[:-1], categories, color='grey', size=8) + + ax.set_rlabel_position(0) + plt.yticks([0.2, 0.4, 0.6, 0.8, 1.0], ["0.2", "0.4", "0.6", "0.8", "1.0"], color="grey", size=7) + plt.ylim(0, 1) + + gt = [] + for i in range(5): + gt.append(groundtruth[i]) + gt += gt[:1] + ax.fill(angles, gt, color=(0.2, 0.8, 0.2), alpha=0.2) + + colors = [(0.8, 0.8, 0.0, 0.8), (0.7, 0.2, 0.2, 0.8), (0.2, 0.2, 0.7, 0.8)] + for iter, data in results["iteration"].items(): + values = [] + for i in range(5): + values.append(data["axis_0{}".format(i)][row]) + values += values[:1] + ax.plot(angles, values, color=colors.pop(0), linewidth=2, linestyle='solid', label="iterations {}".format(iter)) + + ax.plot(angles, gt, color=(0.2, 0.8, 0.2, 0.8), linewidth=2, linestyle='solid', label="groundtruth") + plt.title(title, size=11, color=(0.1, 0.1, 0.1), y=1.1) + plt.legend(bbox_to_anchor=(0.2, 1.2)) + + + + +for vfunc_id in ["5D3"]: + OUTPUTDIR = "C:\\Users\\s635r\\Desktop\\solver_comparison" + EXPERIMENT = {"iterations": [100, 200, 300], + "solver": ["randomsearch", "hyperopt", "optunity"], + "repeat": 1, + "output_dir": os.path.join(OUTPUTDIR, vfunc_id)} + + if not os.path.isdir(EXPERIMENT["output_dir"]): + os.makedirs(EXPERIMENT["output_dir"]) + + project = HyppopyProject() + project.add_hyperparameter(name="axis_00", domain="uniform", data=[0, 1], dtype="float") + project.add_hyperparameter(name="axis_01", domain="uniform", data=[0, 1], dtype="float") + project.add_hyperparameter(name="axis_02", domain="uniform", data=[0, 1], dtype="float") + project.add_hyperparameter(name="axis_03", domain="uniform", data=[0, 1], dtype="float") + project.add_hyperparameter(name="axis_04", domain="uniform", data=[0, 1], dtype="float") + project.add_settings(section="solver", name="max_iterations", value=100) + project.add_settings(section="custom", name="use_solver", value="randomsearch") + + if os.path.isfile(os.path.join(EXPERIMENT["output_dir"], "results")): + file = open(os.path.join(EXPERIMENT["output_dir"], "results"), 'rb') + results = pickle.load(file) + file.close() + else: + vfunc = VirtualFunction() + vfunc.load_default(vfunc_id) + for i in range(5): + vfunc.plot(i) + + + def my_loss_function(data, params): + return vfunc(**params) + + + results = {"group": EXPERIMENT["solver"], + "groundtruth": [], + 'iteration': {}} + + minima = vfunc.minima() + for mini in minima: + results["groundtruth"].append(np.median(mini[0])) + + + for iter in EXPERIMENT["iterations"]: + results["iteration"][iter] = {"axis_00": [], + "axis_01": [], + "axis_02": [], + "axis_03": [], + "axis_04": []} + for solver_name in EXPERIMENT["solver"]: + axis_minima = [0, 0, 0, 0, 0] + for n in range(EXPERIMENT["repeat"]): + print("\rSolver={} iteration={} round={}".format(solver_name, iter, n), end="") + project.add_settings(section="solver", name="max_iterations", value=iter) + project.add_settings(section="custom", name="use_solver", value=solver_name) + + blackbox = BlackboxFunction(data=[], blackbox_func=my_loss_function) + + solver = SolverPool.get(project=project) + solver.blackbox = blackbox + solver.run(print_stats=False) + df, best = solver.get_results() + + best_row = df['losses'].idxmin() + best_loss = df['losses'][best_row] + for i in range(5): + axis_minima[i] += df['axis_0{}'.format(i)][best_row]/EXPERIMENT["repeat"] + for i in range(5): + results["iteration"][iter]["axis_0{}".format(i)].append(axis_minima[i]) + print("") + print("\n\n") + + file = open(os.path.join(EXPERIMENT["output_dir"], "results"), 'wb') + pickle.dump(results, file) + file.close() + + my_dpi = 96 + plt.figure(figsize=(1100/my_dpi, 1100/my_dpi), dpi=my_dpi) + for row in range(3): + make_spider(results, row=row, title=results['group'][row], groundtruth=results["groundtruth"]) + #plt.show() + plt.savefig(os.path.join(EXPERIMENT["output_dir"], "radar_plots.svg")) + plt.savefig(os.path.join(EXPERIMENT["output_dir"], "radar_plots.png")) diff --git a/hyppopy/Solver/HyppopySolver.py b/hyppopy/Solver/HyppopySolver.py index 1c35b41..0d8b1e0 100644 --- a/hyppopy/Solver/HyppopySolver.py +++ b/hyppopy/Solver/HyppopySolver.py @@ -1,223 +1,223 @@ # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # All rights reserved. # # This software is distributed WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. # # See LICENSE.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) import abc import os import types import logging import datetime import numpy as np import pandas as pd from ..globals import DEBUGLEVEL from ..HyppopyProject import HyppopyProject from ..BlackboxFunction import BlackboxFunction from ..VirtualFunction import VirtualFunction from hyppopy.globals import DEBUGLEVEL, DEFAULTITERATIONS LOG = logging.getLogger(os.path.basename(__file__)) LOG.setLevel(DEBUGLEVEL) class HyppopySolver(object): def __init__(self, project=None): self._best = None self._trials = None self._blackbox = None self._max_iterations = None self._project = project self._total_duration = None self._solver_overhead = None self._time_per_iteration = None self._accumulated_blackbox_time = None self._has_maxiteration_field = True @abc.abstractmethod def execute_solver(self, searchspace): raise NotImplementedError('users must define execute_solver to use this class') @abc.abstractmethod def convert_searchspace(self, hyperparameter): raise NotImplementedError('users must define convert_searchspace to use this class') def run(self, print_stats=True): if self._has_maxiteration_field: if 'solver_max_iterations' not in self.project.__dict__: msg = "Missing max_iteration entry in project, use default {}!".format(DEFAULTITERATIONS) LOG.warning(msg) print("WARNING: {}".format(msg)) setattr(self.project, 'solver_max_iterations', DEFAULTITERATIONS) self._max_iterations = self.project.solver_max_iterations start_time = datetime.datetime.now() try: self.execute_solver(self.convert_searchspace(self.project.hyperparameter)) except Exception as e: raise e end_time = datetime.datetime.now() dt = end_time - start_time days = divmod(dt.total_seconds(), 86400) hours = divmod(days[1], 3600) minutes = divmod(hours[1], 60) seconds = divmod(minutes[1], 1) milliseconds = divmod(seconds[1], 0.001) self._total_duration = [int(days[0]), int(hours[0]), int(minutes[0]), int(seconds[0]), int(milliseconds[0])] if print_stats: self.print_best() self.print_timestats() def get_results(self): results = {'duration': [], 'losses': []} pset = self.trials.trials[0]['misc']['vals'] for p in pset.keys(): results[p] = [] for n, trial in enumerate(self.trials.trials): t1 = trial['book_time'] t2 = trial['refresh_time'] results['duration'].append((t2 - t1).microseconds / 1000.0) results['losses'].append(trial['result']['loss']) losses = np.array(results['losses']) results['losses'] = list(losses) pset = trial['misc']['vals'] for p in pset.items(): results[p[0]].append(p[1][0]) return pd.DataFrame.from_dict(results), self.best def print_best(self): print("\n") print("#" * 40) print("### Best Parameter Choice ###") print("#" * 40) for name, value in self.best.items(): print(" - {}\t:\t{}".format(name, value)) print("\n - number of iterations\t:\t{}".format(self.trials.trials[-1]['tid']+1)) print(" - total time\t:\t{}d:{}h:{}m:{}s:{}ms".format(self._total_duration[0], self._total_duration[1], self._total_duration[2], self._total_duration[3], self._total_duration[4])) print("#" * 40) def compute_time_statistics(self): dts = [] for trial in self._trials.trials: if 'book_time' in trial.keys() and 'refresh_time' in trial.keys(): dt = trial['refresh_time'] - trial['book_time'] dts.append(dt.total_seconds()) self._time_per_iteration = np.mean(dts) * 1e3 self._accumulated_blackbox_time = np.sum(dts) * 1e3 tmp = self.total_duration - self._accumulated_blackbox_time - self._solver_overhead = int(np.round(100.0 / self.total_duration * tmp)) + self._solver_overhead = int(np.round(100.0 / (self.total_duration+1e-12) * tmp)) def print_timestats(self): print("\n") print("#" * 40) print("### Timing Statistics ###") print("#" * 40) print(" - per iteration: {}ms".format(int(self.time_per_iteration*1e4)/10000)) print(" - total time: {}d:{}h:{}m:{}s:{}ms".format(self._total_duration[0], self._total_duration[1], self._total_duration[2], self._total_duration[3], self._total_duration[4])) print(" - solver overhead: {}%".format(self.solver_overhead)) print("#" * 40) @property def project(self): return self._project @project.setter def project(self, value): if not isinstance(value, HyppopyProject): msg = "Input error, project_manager of type: {} not allowed!".format(type(value)) LOG.error(msg) raise IOError(msg) self._project = value @property def blackbox(self): return self._blackbox @blackbox.setter def blackbox(self, value): if isinstance(value, types.FunctionType) or isinstance(value, BlackboxFunction) or isinstance(value, VirtualFunction): self._blackbox = value else: self._blackbox = None msg = "Input error, blackbox of type: {} not allowed!".format(type(value)) LOG.error(msg) raise IOError(msg) @property def best(self): return self._best @best.setter def best(self, value): if not isinstance(value, dict): msg = "Input error, best of type: {} not allowed!".format(type(value)) LOG.error(msg) raise IOError(msg) self._best = value @property def trials(self): return self._trials @trials.setter def trials(self, value): self._trials = value @property def max_iterations(self): return self._max_iterations @max_iterations.setter def max_iterations(self, value): if not isinstance(value, int): msg = "Input error, max_iterations of type: {} not allowed!".format(type(value)) LOG.error(msg) raise IOError(msg) if value < 1: msg = "Precondition violation, max_iterations < 1!" LOG.error(msg) raise IOError(msg) self._max_iterations = value @property def total_duration(self): return (self._total_duration[0] * 86400 + self._total_duration[1] * 3600 + self._total_duration[2] * 60 + self._total_duration[3]) * 1000 + self._total_duration[4] @property def solver_overhead(self): if self._solver_overhead is None: self.compute_time_statistics() return self._solver_overhead @property def time_per_iteration(self): if self._time_per_iteration is None: self.compute_time_statistics() return self._time_per_iteration @property def accumulated_blackbox_time(self): if self._accumulated_blackbox_time is None: self.compute_time_statistics() return self._accumulated_blackbox_time diff --git a/hyppopy/VirtualFunction.py b/hyppopy/VirtualFunction.py index f588c37..044c3c7 100644 --- a/hyppopy/VirtualFunction.py +++ b/hyppopy/VirtualFunction.py @@ -1,222 +1,225 @@ # DKFZ # # # Copyright (c) German Cancer Research Center, # Division of Medical and Biological Informatics. # All rights reserved. # # This software is distributed WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR # A PARTICULAR PURPOSE. # # See LICENSE.txt or http://www.mitk.org for details. # # Author: Sven Wanner (s.wanner@dkfz.de) ######################################################################################################################## # USAGE # # The class VirtualFunction is meant to be a virtual energy function with an arbitrary dimensionality. The user can # simply scribble functions as a binary image using e.g. Gimp, defining their ranges using .cfg file and loading them # into the VirtualFunction. An instance of the class can then be used like a normal function returning the sampling of # each dimension loaded. # # 1. create binary images (IMPORTANT same shape for each), background black the function signature white, ensure that # each column has a white pixel. If more than one pixel appears in a column, only the lowest will be used. # # 2. create a .cfg file, see an example in hyppopy/virtualparameterspace # # 3. vfunc = VirtualFunction() # vfunc.load_images(path/of/your/binaryfiles/and/the/configfile) # # 4. use vfunc like a normal function, if you loaded 4 dimension binary images use it like f = vfunc(a,b,c,d) ######################################################################################################################## import os import sys import numpy as np import configparser from glob import glob import matplotlib.pyplot as plt import matplotlib.image as mpimg from hyppopy.globals import VFUNCDATAPATH class VirtualFunction(object): def __init__(self): self.config = None self.data = None self.axis = [] def __call__(self, *args, **kwargs): if len(kwargs) == self.dims(): args = [0]*len(kwargs) for key, value in kwargs.items(): index = int(key.split("_")[1]) args[index] = value assert len(args) == self.dims(), "wrong number of arguments!" for i in range(len(args)): assert self.axis[i][0] <= args[i] <= self.axis[i][1], "out of range access on axis {}!".format(i) lpos, rpos, fracs = self.pos_to_indices(args) fl = self.data[(list(range(self.dims())), lpos)] fr = self.data[(list(range(self.dims())), rpos)] return np.sum(fl*np.array(fracs) + fr*(1-np.array(fracs))) def clear(self): self.axis.clear() self.data = None self.config = None def dims(self): return self.data.shape[0] def size(self): return self.data.shape[1] + def range(self, dim): + return np.abs(self.axis[dim][1] - self.axis[dim][0]) + def minima(self): glob_mins = [] for dim in range(self.dims()): x = [] fmin = np.min(self.data[dim, :]) for _x in range(self.size()): if self.data[dim, _x] <= fmin: x.append(_x/self.size()*(self.axis[dim][1]-self.axis[dim][0])+self.axis[dim][0]) glob_mins.append([x, fmin]) return glob_mins def pos_to_indices(self, positions): lpos = [] rpos = [] pfracs = [] for n in range(self.dims()): pos = positions[n] pos -= self.axis[n][0] pos /= np.abs(self.axis[n][1]-self.axis[n][0]) pos *= self.data.shape[1]-1 lp = int(np.floor(pos)) if lp < 0: lp = 0 rp = int(np.ceil(pos)) if rp > self.data.shape[1]-1: rp = self.data.shape[1]-1 pfracs.append(1.0-(pos-np.floor(pos))) lpos.append(lp) rpos.append(rp) return lpos, rpos, pfracs def plot(self, dim=None, title=""): if dim is None: dim = list(range(self.dims())) else: dim = [dim] fig = plt.figure(figsize=(10, 8)) for i in range(len(dim)): width = np.abs(self.axis[dim[i]][1]-self.axis[dim[i]][0]) ax = np.arange(self.axis[dim[i]][0], self.axis[dim[i]][1], width/self.size()) plt.plot(ax, self.data[dim[i], :], '.', label='axis_{}'.format(str(dim[i]).zfill(2))) plt.legend() plt.grid() plt.title(title) plt.show() def add_dimension(self, data, x_range): if self.data is None: self.data = data if len(self.data.shape) == 1: self.data = self.data.reshape((1, self.data.shape[0])) else: if len(data.shape) == 1: data = data.reshape((1, data.shape[0])) assert self.data.shape[1] == data.shape[1], "shape mismatch while adding dimension!" dims = self.data.shape[0] size = self.data.shape[1] tmp = np.append(self.data, data) self.data = tmp.reshape((dims+1, size)) self.axis.append(x_range) - def load_default(self, dim=3): - path = os.path.join(VFUNCDATAPATH, "{}D".format(dim)) + def load_default(self, name="3D"): + path = os.path.join(VFUNCDATAPATH, "{}".format(name)) if os.path.exists(path): self.load_images(path) else: - raise FileExistsError("No virtualfunction of dimension {} available".format(dim)) + raise FileExistsError("No virtualfunction of dimension {} available".format(name)) def load_images(self, path): self.config = None self.data = None self.axis.clear() img_fnames = [] for f in glob(path + os.sep + "*"): if f.endswith(".png"): img_fnames.append(f) elif f.endswith(".cfg"): self.config = self.read_config(f) else: print("WARNING: files of type {} not supported, the file {} is ignored!".format(f.split(".")[-1], os.path.basename(f))) if self.config is None: print("Aborted, failed to read configfile!") sys.exit() sections = self.config.sections() if len(sections) != len(img_fnames): print("Aborted, inconsistent number of image tmplates and axis specifications!") sys.exit() img_fnames.sort() size_x = None size_y = None for n, fname in enumerate(img_fnames): img = mpimg.imread(fname) if len(img.shape) > 2: img = img[:, :, 0] if size_x is None: size_x = img.shape[1] if size_y is None: size_y = img.shape[0] self.data = np.zeros((len(img_fnames), size_x), dtype=np.float32) assert img.shape[0] == size_y, "Shape mismatch in dimension y {} is not {}".format(img.shape[0], size_y) assert img.shape[1] == size_x, "Shape mismatch in dimension x {} is not {}".format(img.shape[1], size_x) self.sample_image(img, n) def sample_image(self, img, dim): sec_name = "axis_{}".format(str(dim).zfill(2)) assert sec_name in self.config.sections(), "config section {} not found!".format(sec_name) settings = self.get_axis_settings(sec_name) self.axis.append([float(settings['min_x']), float(settings['max_x'])]) y_range = [float(settings['min_y']), float(settings['max_y'])] for x in range(img.shape[1]): candidates = np.where(img[:, x] > 0) assert len(candidates[0]) > 0, "non function value in image detected, ensure each column has at least one value > 0!" y_pos = candidates[0][0]/img.shape[0] self.data[dim, x] = 1-y_pos self.data[dim, :] *= np.abs(y_range[1] - y_range[0]) self.data[dim, :] += y_range[0] def read_config(self, fname): try: config = configparser.ConfigParser() config.read(fname) return config except Exception as e: print(e) return None def get_axis_settings(self, section): dict1 = {} options = self.config.options(section) for option in options: try: dict1[option] = self.config.get(section, option) if dict1[option] == -1: print("skip: %s" % option) except: print("exception on %s!" % option) dict1[option] = None return dict1 diff --git a/hyppopy/virtualparameterspace/5D/axis.cfg b/hyppopy/virtualparameterspace/5D/axis.cfg index 59fb831..ceb0641 100644 --- a/hyppopy/virtualparameterspace/5D/axis.cfg +++ b/hyppopy/virtualparameterspace/5D/axis.cfg @@ -1,29 +1,29 @@ [axis_00] min_x:0 max_x:1 min_y:0 max_y:1 [axis_01] min_x:0 -max_x:800 +max_x:1 min_y:0 max_y:1 [axis_02] min_x:0 -max_x:5 +max_x:1 min_y:0 max_y:1 [axis_03] min_x:0 -max_x:10000 +max_x:1 min_y:0 max_y:1 [axis_04] min_x:0 -max_x:10 +max_x:1 min_y:0 max_y:1 \ No newline at end of file diff --git a/hyppopy/virtualparameterspace/5D/axis.cfg b/hyppopy/virtualparameterspace/5D2/axis.cfg similarity index 79% copy from hyppopy/virtualparameterspace/5D/axis.cfg copy to hyppopy/virtualparameterspace/5D2/axis.cfg index 59fb831..ceb0641 100644 --- a/hyppopy/virtualparameterspace/5D/axis.cfg +++ b/hyppopy/virtualparameterspace/5D2/axis.cfg @@ -1,29 +1,29 @@ [axis_00] min_x:0 max_x:1 min_y:0 max_y:1 [axis_01] min_x:0 -max_x:800 +max_x:1 min_y:0 max_y:1 [axis_02] min_x:0 -max_x:5 +max_x:1 min_y:0 max_y:1 [axis_03] min_x:0 -max_x:10000 +max_x:1 min_y:0 max_y:1 [axis_04] min_x:0 -max_x:10 +max_x:1 min_y:0 max_y:1 \ No newline at end of file diff --git a/hyppopy/virtualparameterspace/5D2/axis_00.png b/hyppopy/virtualparameterspace/5D2/axis_00.png new file mode 100644 index 0000000..074094b Binary files /dev/null and b/hyppopy/virtualparameterspace/5D2/axis_00.png differ diff --git a/hyppopy/virtualparameterspace/5D2/axis_01.png b/hyppopy/virtualparameterspace/5D2/axis_01.png new file mode 100644 index 0000000..fa1ea8e Binary files /dev/null and b/hyppopy/virtualparameterspace/5D2/axis_01.png differ diff --git a/hyppopy/virtualparameterspace/5D2/axis_02.png b/hyppopy/virtualparameterspace/5D2/axis_02.png new file mode 100644 index 0000000..aaccb3f Binary files /dev/null and b/hyppopy/virtualparameterspace/5D2/axis_02.png differ diff --git a/hyppopy/virtualparameterspace/5D2/axis_03.png b/hyppopy/virtualparameterspace/5D2/axis_03.png new file mode 100644 index 0000000..21f8020 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D2/axis_03.png differ diff --git a/hyppopy/virtualparameterspace/5D2/axis_04.png b/hyppopy/virtualparameterspace/5D2/axis_04.png new file mode 100644 index 0000000..383eb77 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D2/axis_04.png differ diff --git a/hyppopy/virtualparameterspace/5D/axis.cfg b/hyppopy/virtualparameterspace/5D3/axis.cfg similarity index 79% copy from hyppopy/virtualparameterspace/5D/axis.cfg copy to hyppopy/virtualparameterspace/5D3/axis.cfg index 59fb831..ceb0641 100644 --- a/hyppopy/virtualparameterspace/5D/axis.cfg +++ b/hyppopy/virtualparameterspace/5D3/axis.cfg @@ -1,29 +1,29 @@ [axis_00] min_x:0 max_x:1 min_y:0 max_y:1 [axis_01] min_x:0 -max_x:800 +max_x:1 min_y:0 max_y:1 [axis_02] min_x:0 -max_x:5 +max_x:1 min_y:0 max_y:1 [axis_03] min_x:0 -max_x:10000 +max_x:1 min_y:0 max_y:1 [axis_04] min_x:0 -max_x:10 +max_x:1 min_y:0 max_y:1 \ No newline at end of file diff --git a/hyppopy/virtualparameterspace/5D3/axis_00.png b/hyppopy/virtualparameterspace/5D3/axis_00.png new file mode 100644 index 0000000..cb3d349 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D3/axis_00.png differ diff --git a/hyppopy/virtualparameterspace/5D3/axis_01.png b/hyppopy/virtualparameterspace/5D3/axis_01.png new file mode 100644 index 0000000..3a62bd1 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D3/axis_01.png differ diff --git a/hyppopy/virtualparameterspace/5D3/axis_02.png b/hyppopy/virtualparameterspace/5D3/axis_02.png new file mode 100644 index 0000000..1d9b8d4 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D3/axis_02.png differ diff --git a/hyppopy/virtualparameterspace/5D3/axis_03.png b/hyppopy/virtualparameterspace/5D3/axis_03.png new file mode 100644 index 0000000..9d95f11 Binary files /dev/null and b/hyppopy/virtualparameterspace/5D3/axis_03.png differ diff --git a/hyppopy/virtualparameterspace/5D3/axis_04.png b/hyppopy/virtualparameterspace/5D3/axis_04.png new file mode 100644 index 0000000..c9da9fe Binary files /dev/null and b/hyppopy/virtualparameterspace/5D3/axis_04.png differ