diff --git a/pymoo/util/running_metric.py b/pymoo/util/running_metric.py index 2a3c81282..ba05d1962 100644 --- a/pymoo/util/running_metric.py +++ b/pymoo/util/running_metric.py @@ -1,4 +1,3 @@ -import matplotlib.pyplot as plt import numpy as np from pymoo.core.callback import Callback @@ -7,7 +6,7 @@ from pymoo.termination.ftol import calc_delta_norm from pymoo.util.normalization import normalize from pymoo.util.sliding_window import SlidingWindow -from pymoo.visualization.video.callback_video import AnimationCallback +from pymoo.visualization.matplotlib import is_matplotlib_available class RunningMetric(Callback): @@ -64,65 +63,85 @@ def update(self, algorithm): self.delta_ideal, self.delta_nadir, self.delta_f = delta_ideal, delta_nadir, delta_f - -class RunningMetricAnimation(AnimationCallback): - - def __init__(self, - delta_gen, - n_plots=4, - key_press=True, - **kwargs) -> None: - - super().__init__(**kwargs) - self.running = RunningMetric() - self.delta_gen = delta_gen - self.key_press = key_press - self.data = SlidingWindow(n_plots) - - def draw(self, data, ax): - - for tau, x, f, v in data[:-1]: - ax.plot(x, f, label="t=%s" % tau, alpha=0.6, linewidth=3) - - tau, x, f, v = data[-1] - ax.plot(x, f, label="t=%s (*)" % tau, alpha=0.9, linewidth=3) - - for k in range(len(v)): - if v[k]: - ax.plot([k + 1, k + 1], [0, f[k]], color="black", linewidth=0.5, alpha=0.5) - ax.plot([k + 1], [f[k]], "o", color="black", alpha=0.5, markersize=2) - - ax.set_yscale("symlog") - ax.legend() - - ax.set_xlabel("Generation") - ax.set_ylabel("$\Delta \, f$", rotation=0) - - def do(self, _, algorithm, force_plot=False, **kwargs): - running = self.running - - # update the running metric to have the most recent information - running.update(algorithm) - - tau = algorithm.n_gen - - if (tau > 0 and tau % self.delta_gen == 0) or force_plot: - - f = running.delta_f - x = np.arange(len(f)) + 1 - v = [max(ideal, nadir) > 0.005 for ideal, nadir in zip(running.delta_ideal, running.delta_nadir)] - self.data.append((tau, x, f, v)) - - fig, ax = plt.subplots() - self.draw(self.data, ax) - - if self.key_press: - def press(event): - if event.key == 'q': - algorithm.termination.force_termination = True - - fig.canvas.mpl_connect('key_press_event', press) - - plt.draw() - plt.waitforbuttonpress() - plt.close('all') +if is_matplotlib_available(): + # only conditionally define `RunningMetricAnimation` if matplotlib is available + from pymoo.visualization.matplotlib import plt + from pymoo.visualization.video.callback_video import AnimationCallback + + class RunningMetricAnimation(AnimationCallback): + + def __init__(self, + delta_gen, + n_plots=4, + key_press=True, + **kwargs) -> None: + + super().__init__(**kwargs) + self.running = RunningMetric() + self.delta_gen = delta_gen + self.key_press = key_press + self.data = SlidingWindow(n_plots) + + def draw(self, data, ax): + + for tau, x, f, v in data[:-1]: + ax.plot(x, f, label="t=%s" % tau, alpha=0.6, linewidth=3) + + tau, x, f, v = data[-1] + ax.plot(x, f, label="t=%s (*)" % tau, alpha=0.9, linewidth=3) + + for k in range(len(v)): + if v[k]: + ax.plot([k + 1, k + 1], [0, f[k]], color="black", linewidth=0.5, alpha=0.5) + ax.plot([k + 1], [f[k]], "o", color="black", alpha=0.5, markersize=2) + + ax.set_yscale("symlog") + ax.legend() + + ax.set_xlabel("Generation") + ax.set_ylabel("$\Delta \, f$", rotation=0) + + def do(self, _, algorithm, force_plot=False, **kwargs): + running = self.running + + # update the running metric to have the most recent information + running.update(algorithm) + + tau = algorithm.n_gen + + if (tau > 0 and tau % self.delta_gen == 0) or force_plot: + + f = running.delta_f + x = np.arange(len(f)) + 1 + v = [max(ideal, nadir) > 0.005 for ideal, nadir in zip(running.delta_ideal, running.delta_nadir)] + self.data.append((tau, x, f, v)) + + fig, ax = plt.subplots() + self.draw(self.data, ax) + + if self.key_press: + def press(event): + if event.key == 'q': + algorithm.termination.force_termination = True + + fig.canvas.mpl_connect('key_press_event', press) + + plt.draw() + plt.waitforbuttonpress() + plt.close('all') +else: + class RunningMetricAnimation: + """Helper class that raises informative errors when matplotlib is not available.""" + + def __getattr__(self, name): + raise ImportError( + "Visualization features require matplotlib.\n" + "Install with: pip install pymoo[visualization]" + ) + + def __call__(self, *args, **kwargs): + raise ImportError( + "Visualization features require matplotlib.\n" + "Install with: pip install pymoo[visualization]" + ) + \ No newline at end of file diff --git a/pymoo/util/value_functions.py b/pymoo/util/value_functions.py index c1d5a32c7..5d3198fee 100644 --- a/pymoo/util/value_functions.py +++ b/pymoo/util/value_functions.py @@ -5,7 +5,6 @@ from scipy.optimize import minimize as scimin from scipy.optimize import OptimizeResult from pymoo.core.problem import Problem -import matplotlib.pyplot as plt from scipy.optimize import NonlinearConstraint from scipy.optimize import Bounds from pymoo.algorithms.soo.nonconvex.es import ES @@ -278,7 +277,8 @@ def poly_vf(P, x): def plot_vf(P, vf, show=True): - + # import plt function locally as matplotlib is an optional dependency + from pymoo.visualization.matplotlib import plt plt.scatter(P[:,0], P[:,1], marker=".", color="red", s=200 ) diff --git a/pymoo/visualization/matplotlib.py b/pymoo/visualization/matplotlib.py index f654e4189..e8caf2b3b 100644 --- a/pymoo/visualization/matplotlib.py +++ b/pymoo/visualization/matplotlib.py @@ -24,7 +24,7 @@ # Export all commonly used matplotlib objects __all__ = [ 'matplotlib', 'plt', 'patches', 'colors', 'cm', 'animation', - 'LineCollection', 'PatchCollection', 'ListedColormap', 'is_available' + 'LineCollection', 'PatchCollection', 'ListedColormap', 'is_matplotlib_available' ] except ImportError: @@ -57,7 +57,7 @@ def __call__(self, *args, **kwargs): ListedColormap = _MatplotlibNotAvailable() -def is_available(): +def is_matplotlib_available(): """Check if matplotlib is available for visualization.""" return _MATPLOTLIB_AVAILABLE diff --git a/pyproject.toml b/pyproject.toml index b176c2514..a3f2308a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ requires-python = ">= 3.9" dependencies = [ "numpy>=1.19.3", "scipy>=1.1", - "matplotlib>=3", "autograd>=1.4", "cma>=3.2.2", "moocore>=0.1.7",