Source code for opal.visualization.OptimizerPlotter

# Copyright (c) 2019, Matthias Frey, Paul Scherrer Institut, Villigen PSI, Switzerland
# All rights reserved
#
# Implemented as part of the PhD thesis
# "Precise Simulations of Multibunches in High Intensity Cyclotrons"
#
# This file is part of pyOPALTools.
#
# pyOPALTools is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# You should have received a copy of the GNU General Public License
# along with pyOPALTools. If not, see <https://www.gnu.org/licenses/>.

from .BasePlotter import *
import numpy as np
import matplotlib.gridspec as gridspec
import bisect
from opal import config as config
import re


[docs]class OptimizerPlotter(BasePlotter):
[docs] def __init__(self): pass
# 2. Mai 2018 # https://stackoverflow.com/questions/4836710/does-python-have-a-built-in-function-for-string-natural-sort def __natural_sort(self, l): convert = lambda text: int(text) if text.isdigit() else text.lower() alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] return sorted(l, key = alphanum_key) # 2. Mai 2018 # https://stackoverflow.com/questions/4391697/find-the-index-of-a-dict-within-a-list-by-matching-the-dicts-value def __find(self, lst, key, value): for i, dic in enumerate(lst): if dic[key] == value: return i return None def __sort_list(self, names, dimension, key): natsort_names = self.__natural_sort(names) natsort_dimension = [] for name in natsort_names: idx = self.__find(dimension, 'label', name) natsort_dimension.append(dimension[idx]) return natsort_dimension
[docs] def plot_parallel_coordinates(self, gen, opt=0, **kwargs): """Plotting function for multiobjective optimizer output. Parameters ---------- gen : int Generation to plot opt : int, optional Optimizer number (default: 0) Notes ----- (30. April 2018) https://plot.ly/python/static-image-export/ https://plot.ly/python/parallel-coordinates-plot/ https://stackoverflow.com/questions/40243446/how-to-save-plotly-offline-graph-in-format-png """ try: import plotly.plotly as py import plotly.graph_objs as go if config.opal['style'] == 'jupyter': from plotly.offline import iplot as pyplot else: from plotly.offline import plot as pyplot except: print ( "Install plotly: pip install plotly" ) basename = self.ds.getGenerationBasename(gen, opt) dvar_names = self.ds.design_variables dvar_bounds = self.ds.bounds obj_names = self.ds.objectives ids = self.ds.individuals(gen, opt) dvar_dimension = [] obj_dimension = [] for dvar in dvar_names: dvar_dimension.append({ 'range': dvar_bounds[dvar], 'label': dvar, 'values': [] } ) for obj in obj_names: obj_dimension.append({ 'label': obj, 'values': [] } ) nDvars = len(dvar_names) nObjs = len(obj_names) for i in ids: data = self.ds.getData('', gen=gen, ind=i, opt=opt) for j, d in enumerate(data): if j < nDvars: dvar_dimension[j]['values'].append(d) elif j < nDvars + nObjs: obj_dimension[j - nDvars]['values'].append(d) # make a natural ordering of labels dvar_dimension = self.__sort_list(dvar_names, dvar_dimension, 'label') obj_dimension = self.__sort_list(obj_names, obj_dimension, 'label') dimension = dvar_dimension + obj_dimension data = [ go.Parcoords( line = dict(color = ids, colorscale = 'Jet', showscale = True, reversescale = True, cmin = min(ids), cmax = max(ids)), dimensions = dimension ) ] layout = go.Layout( height=800, width=1600, font=dict(size=18), title = 'Generation ' + str(gen) ) fig = go.Figure(data = data, layout = layout) pyplot(fig, #image='png', #image_filename=filename, #output_type='file', filename='generation_' + str(gen) + '.html')
#auto_open=False)
[docs] def plot_objectives(self, opt=0, **kwargs): """Plotting function for multiobjective optimizer output. Show the trend of the sum of the objectives with the generation. Parameters ---------- opt : int, optional Optimizer number (default: 0) xscale : str, optional 'linear' or 'log', default: linear yscale : str, optional 'linear' or 'log', default: linear grid : bool, optional Show grid, default: False avg : bool, optional Take averaged sum over all objectives default: true Returns ------- matplotlib.pyplot Plot handle """ try: gens = range(1, self.ds.num_generations + 1) objs = self.ds.objectives avg = kwargs.pop('avg', True) result = [] for g in gens: s = 0.0 for obj in objs: if avg: s += np.mean(self.ds.getData(obj, gen=g, opt=opt)) else: s += sum(self.ds.getData(obj, gen=g, opt=opt)) result.append( s ) plt.plot(gens, result) plt.xscale(kwargs.pop('xscale', 'linear')) plt.yscale(kwargs.pop('yscale', 'linear')) plt.grid(kwargs.pop('grid', True), which='both') plt.xlabel('generation') plt.ylabel('sum of objectives (all individuals)') return plt except Exception as ex: opal_logger.exception(ex) return plt.figure()
[docs] def plot_objective_evolution(self, opt=0, objs=[], op=min, **kwargs): """Plot the improvement of the objectives with generation. The operator 'op' is executed between individuals per population Parameters ---------- opt : int, optional Optimizer number (default: 0) objs : list of str, optional List of objectives, if not specified all are plotted op : callable, optional Operator, e.g. max, min, etc xscale : str, optional 'linear', 'log' yscale : str, optional 'linear', 'log' grid : bool, optional total : bool, optional Show sum of objectives label_rep : dict, optional Replace labels by as_bar : bool, optional colorlist : list of str, optional """ try: objectives = self.ds.objectives ngen = self.ds.num_generations xscale = kwargs.pop('xscale', 'linear') yscale = kwargs.pop('yscale', 'linear') grid = kwargs.pop('grid', True) label_rep = kwargs.pop('label_rep', {}) t = kwargs.pop('total', False) objmean = kwargs.pop('objmean', False) objmeandict = kwargs.pop('objmeandict', { 'linewidth': 2, 'linestyle': 'dashed', 'color': 'black', 'label': 'objective mean' }) indmean = kwargs.pop('indmean', False) indmeandict = kwargs.pop('indmeandict', { 'linewidth': 2, 'linestyle': 'dashed', 'color': 'black', 'label': 'individual mean' }) totaldict = kwargs.pop('totaldict', { 'linewidth': 2, 'linestyle': 'dashed', 'color': 'black', 'label': 'objective sum' }) legenddict = kwargs.pop('legenddict', { 'fontsize': 18, 'ncol': 4, 'labelspacing': 0.5, 'bbox_to_anchor': (0.25,0.65 + (indmean) * 0.2, 0.5, 0.5) }) as_bar = kwargs.pop('asbar', False) colorlist = kwargs.pop('colorlist', []) if objs: for obj in objs: if not obj in objectives: raise ValueError(self.ds.filename + ' does only has following objectives: ' + str(objectives)) else: objs = objectives result = np.zeros((len(objs), ngen)) ind_mean = np.zeros(ngen) for j in range(0, ngen): ids = self.ds.individuals(j+1, opt=opt) # val, id cur = [0, -1] for idx in ids: s = 0 for obj in objectives: val = self.ds.getData(var=obj, gen=j+1, ind=idx, all=False, opt=opt) s += val if indmean: ind_mean[j] += val if cur[1] < 0: cur = [s, idx] else: cur = op([s, idx], cur) if indmean: ind_mean[j] /= len(ids) for i, obj in enumerate(objs): result[i, j] = self.ds.getData(obj, gen=j+1, ind=cur[1], all=False, opt=opt) label = [] for i, obj in enumerate(objs): label.append( obj ) if obj in label_rep.keys(): label[-1] = label_rep[obj] if indmean: plt.subplot(2, 1, 1) plt.semilogy(range(1, ngen + 1), ind_mean, **indmeandict) plt.xscale(xscale) plt.xlabel('generation') plt.ylabel('individual mean') plt.grid(grid, which='both') plt.subplot(2, 1, 2) if as_bar: # bar plot bottom = [0] * np.shape(result)[1] for i, obj in enumerate(objs): if i < len(colorlist) - 1: kwargs['color'] = colorlist[i] else: kwargs.pop('color', 'black') plt.bar(range(1, ngen + 1), result[i, :], label=label[i], bottom=bottom, **kwargs) bottom += result[i, :] else: for i, obj in enumerate(objs): if i < len(colorlist) - 1: kwargs['color'] = colorlist[i] else: kwargs.pop('color', 'black') plt.plot(range(1, ngen + 1), result[i, :], label=label[i], **kwargs) if t: total = result.sum(axis=0) plt.plot(range(1, ngen + 1), total, **totaldict) if objmean: mean = result.mean(axis=0) plt.plot(range(1, ngen + 1), mean, **objmeandict) plt.xlabel('generation') plt.xscale(xscale) plt.yscale(yscale) plt.grid(grid, which='both') if len(objs) > 1: plt.ylabel(op.__name__) plt.legend() else: plt.ylabel(obj) plt.legend(loc = 'upper center', **legenddict) if indmean: plt.subplots_adjust(hspace = 10) return plt except Exception as ex: opal_logger.exception(ex) return plt.figure()
[docs] def plot_dvar_evolution(self, opt=0, dvars=[], op=min, **kwargs): """Plot the evolution of the design variable values dependent on the improvement of the objectives with generation. The operator 'op' is executed on two objective value sums of two individuals. Parameters ---------- opt : int, optional Optimizer number (default: 0) dvars : list of str, optional List of design variables, if not specified all are plotted op : callable, optional Operator, e.g. max, min, etc xscale : str, optional 'linear', 'log' yscale : str, optional 'linear', 'log' grid : bool, optional """ try: objs = self.ds.objectives ngen = self.ds.num_generations dvs = self.ds.design_variables if dvars: for dvar in dvars: if not dvar in dvs: raise ValueError(self.ds.filename + ' does only has following design variables: ' + str(dvs)) else: dvars = dvs result = np.zeros((len(dvars), ngen)) for j in range(0, ngen): ids = self.ds.individuals(j+1, opt=opt) # val, id cur = [0, -1] for idx in ids: s = 0 for obj in objs: s += self.ds.getData(var=obj, gen=j+1, ind=idx, all=False, opt=opt) if cur[1] < 0: cur = [s, idx] else: cur = op([s, idx], cur) for i, dvar in enumerate(dvars): result[i, j] = self.ds.getData(dvar, gen=j+1, ind=cur[1], all=False, opt=opt) for i, dvar in enumerate(dvars): plt.plot(range(1, ngen + 1), result[i, :], label=dvar) plt.xlabel('generation') plt.xscale(kwargs.pop('xscale', 'linear')) plt.yscale(kwargs.pop('yscale', 'linear')) plt.grid(kwargs.pop('grid', True), which='both') if len(dvars) > 1: plt.ylabel(op.__name__) plt.legend() else: plt.ylabel(dvar) return plt except Exception as ex: opal_logger.exception(ex) return plt.figure()
[docs] def plot_pareto_front(self, xdvar, ydvar, opt=0, **kwargs): """Plot the Pareto front Parameters ---------- xdvar : str Design variable on x-axis ydvar : str Design variable on y-axis opt : int, optional Optimizer number (default: 0) Returns ------- matplotlib.pyplot Plot handle """ try: x = self.ds.getData(var=xdvar, opt=opt, pareto=True) y = self.ds.getData(var=ydvar, opt=opt, pareto=True) ind = np.argsort(x) plt.scatter(x[ind], y[ind], **kwargs) plt.xlabel(self.ds.getLabel(xdvar)) plt.ylabel(self.ds.getLabel(ydvar)) return plt except Exception as ex: opal_logger.exception(ex) return plt.figure()
[docs] def plot_individual_bounds(self, n, opt=0, **kwargs): """Plot all design variables and their bounds. This will show if a design variable is close to one of its bounds. Parameters ---------- n : int Take the first n-th best individuals opt : int, optional Optimizer number (default: 0) Returns ------- matplotlib.pyplot Plot handle """ # 02. Nov. 2018 # https://stackoverflow.com/questions/8024571/insert-an-item-into-sorted-list-in-python try: objs = self.ds.objectives ids = self.ds.individuals(1, opt=opt) # restrict if n < 1: n = 1 if n > len(ids): n = len(ids) nbests = [] values = [] for gen in range(1, self.ds.num_generations + 1): ids = self.ds.individuals(gen, opt=opt) for ind in ids: s = 0.0 for obj in objs: s += self.ds.getData(var=obj, gen=gen, ind=ind, all=False, opt=opt) # [sum, generation, individual id] if s not in values: bisect.insort(nbests, [gen, ind]) bisect.insort(values, s) if len(nbests) > n: del nbests[-1] del values[-1] dvars = self.ds.design_variables if not dvars: raise IndexError('No design variables found.') ncols = kwargs.pop('ncols', 4) nrows = int(np.ceil(len(dvars) / ncols + 0.5)) gs = gridspec.GridSpec(nrows, ncols) # each row is an individual # each col is a design variable value data = np.zeros([n, len(dvars)]) for i, best in enumerate(nbests): # each list index has a dictionary # that contains just 1 entry, i.e. a list of 2 values (gen, ind) gen, ind = best for j, dvar in enumerate(dvars): data[i][j] = self.ds.getData(var=dvar, gen=gen, ind=ind, all=False, opt=opt) bnds = self.ds.bounds axes = [] xticks = np.linspace(1, n, num=n) xtickstep = kwargs.pop('xtickstep', 1) for i, dvar in enumerate(dvars): ax = plt.subplot(gs[i]) axes.append( ax ) sc = ax.scatter(xticks, data[:, i], c=values, marker='o') ax.set_xlabel('n-th best individual') ax.set_ylabel(dvar) ax.set_xticks(np.arange(1, n+1, step=xtickstep)) ax.axhline(y=bnds[dvar][0], linestyle='dashed', color='black') ax.axhline(y=bnds[dvar][1], linestyle='dashed', color='black', label='design variable upper and lower bound') uplim = bnds[dvar][1] * 1.01 lowlim = bnds[dvar][0] * 0.99 if bnds[dvar][0] < 0: lowlim = bnds[dvar][0] * 1.01 if bnds[dvar][1] < 0: uplim = bnds[dvar][1] * 0.99 ax.set_ylim([lowlim, uplim]) cbar = plt.colorbar(sc, ax=axes) cbar.set_label('sum of objective values') # 3. Nov. 2018 # https://stackoverflow.com/questions/9834452/how-do-i-make-a-single-legend-for-many-subplots-with-matplotlib plt.figlegend(loc = 'lower center', ncol=ncols, labelspacing=0. ) return plt except Exception as ex: opal_logger.exception(ex) return plt.figure()