From 3fff3c85c82ae9784a812e29b3650313766c8f56 Mon Sep 17 00:00:00 2001 From: Marcel Spitz Date: Wed, 13 Aug 2025 17:47:06 +0200 Subject: [PATCH 1/4] Remove hard matplotlib Dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b176c251..a3f2308a 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", From b61747b592ec8fb01ba43ffb8f85734d177c7d56 Mon Sep 17 00:00:00 2001 From: Marcel Spitz Date: Thu, 14 Aug 2025 17:15:40 +0200 Subject: [PATCH 2/4] Conditional Definition for RunningMetricAnimation Class --- pymoo/util/running_metric.py | 95 ++++++++++++++++--------------- pymoo/visualization/matplotlib.py | 4 +- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/pymoo/util/running_metric.py b/pymoo/util/running_metric.py index 2a3c8128..13373f7f 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,69 @@ def update(self, algorithm): self.delta_ideal, self.delta_nadir, self.delta_f = delta_ideal, delta_nadir, delta_f +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): + class RunningMetricAnimation(AnimationCallback): - def __init__(self, - delta_gen, - n_plots=4, - key_press=True, - **kwargs) -> None: + 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) + 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): + 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) + 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) + 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) + 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_yscale("symlog") + ax.legend() - ax.set_xlabel("Generation") - ax.set_ylabel("$\Delta \, f$", rotation=0) + ax.set_xlabel("Generation") + ax.set_ylabel("$\Delta \, f$", rotation=0) - def do(self, _, algorithm, force_plot=False, **kwargs): - running = self.running + def do(self, _, algorithm, force_plot=False, **kwargs): + running = self.running - # update the running metric to have the most recent information - running.update(algorithm) + # update the running metric to have the most recent information + running.update(algorithm) - tau = algorithm.n_gen + tau = algorithm.n_gen - if (tau > 0 and tau % self.delta_gen == 0) or force_plot: + 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)) + 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) + 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 + if self.key_press: + def press(event): + if event.key == 'q': + algorithm.termination.force_termination = True - fig.canvas.mpl_connect('key_press_event', press) + fig.canvas.mpl_connect('key_press_event', press) - plt.draw() - plt.waitforbuttonpress() - plt.close('all') + plt.draw() + plt.waitforbuttonpress() + plt.close('all') diff --git a/pymoo/visualization/matplotlib.py b/pymoo/visualization/matplotlib.py index f654e418..e8caf2b3 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 From 373cc4a3b1c7ca75eac98478319d1da8a937f4a6 Mon Sep 17 00:00:00 2001 From: Marcel Spitz Date: Thu, 14 Aug 2025 17:16:04 +0200 Subject: [PATCH 3/4] Change matplotlib import to local Import --- pymoo/util/value_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymoo/util/value_functions.py b/pymoo/util/value_functions.py index c1d5a32c..5d3198fe 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 ) From eb871cbe8346826bb04f40dd80709f99cf4057f3 Mon Sep 17 00:00:00 2001 From: Marcel Spitz Date: Thu, 14 Aug 2025 17:20:52 +0200 Subject: [PATCH 4/4] Integrate Warning Dummy for RunningMetricAnimation --- pymoo/util/running_metric.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pymoo/util/running_metric.py b/pymoo/util/running_metric.py index 13373f7f..ba05d196 100644 --- a/pymoo/util/running_metric.py +++ b/pymoo/util/running_metric.py @@ -129,3 +129,19 @@ def press(event): 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