From 8238c3b4658f3b29d207de0d410ac04586e6656c Mon Sep 17 00:00:00 2001 From: "David E. Bernal Neira" Date: Wed, 5 Aug 2026 13:08:12 -0400 Subject: [PATCH] Import matplotlib only when plotting lyopronto/__init__.py imports high_level, which imported matplotlib at module scope, so every `import lyopronto` initialized matplotlib and its backend even for callers that only run simulations. That costs import time for everyone and makes a missing or misconfigured backend fail at import rather than at the point of use, which matters for headless batch and server-side callers. matplotlib is used only by generate_visualizations and the four _plot_ helpers, so load it there and pass plt to the helpers rather than relying on a module global. `import lyopronto` no longer pulls matplotlib. Full test suite: 144 passed, 1 skipped. --- lyopronto/high_level.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/lyopronto/high_level.py b/lyopronto/high_level.py index 84984653..03b51b62 100644 --- a/lyopronto/high_level.py +++ b/lyopronto/high_level.py @@ -12,14 +12,25 @@ from warnings import warn import numpy as np import csv -import matplotlib.pyplot as plt -from matplotlib import rc as matplotlibrc from scipy.optimize import curve_fit, brentq from ruamel.yaml import YAML yaml = YAML() +def _load_matplotlib(): + """Import matplotlib on demand. + + Only the plotting helpers need it, so importing it at module scope makes + every ``import lyopronto`` pay for a backend initialization that a headless + or simulation-only caller never uses. + """ + import matplotlib.pyplot as plt + from matplotlib import rc as matplotlibrc + + return plt, matplotlibrc + + def execute_simulation(inputs): """ Run the selected simulation tool with the provided inputs. @@ -419,6 +430,7 @@ def generate_visualizations(output_data, inputs, timestamp): """ Create and save publication-quality plots based on simulation results. """ + plt, matplotlibrc = _load_matplotlib() # TODO: move these to kwargs for the function figure_props = { @@ -433,19 +445,19 @@ def generate_visualizations(output_data, inputs, timestamp): plt.rcParams["font.family"] = "Arial" if tool == "Freezing Calculator": - _plot_freezing_results(output_data, figure_props, timestamp) + _plot_freezing_results(output_data, figure_props, timestamp, plt) elif tool in ["Primary Drying Calculator", "Optimizer"]: if tool == "Primary Drying Calculator" and not inputs["sim"]["Rp_known"]: - _plot_rp_results(output_data, figure_props, timestamp) + _plot_rp_results(output_data, figure_props, timestamp, plt) data = output_data[0] # There are extra returns for Rp fitting else: data = output_data # for all but unknown Rp, output_data is the only return - _plot_drying_results(data, figure_props, timestamp) + _plot_drying_results(data, figure_props, timestamp, plt) elif tool == "Design Space Generator": - _plot_design_space(output_data, inputs, figure_props, timestamp) + _plot_design_space(output_data, inputs, figure_props, timestamp, plt) -def _plot_freezing_results(data, props, timestamp): +def _plot_freezing_results(data, props, timestamp, plt): """Generate freezing process visualization.""" fig, ax = plt.subplots(figsize=(props["figwidth"], props["figheight"])) ax.plot( @@ -469,7 +481,7 @@ def _plot_freezing_results(data, props, timestamp): plt.close() -def _plot_drying_results(data, props, timestamp): +def _plot_drying_results(data, props, timestamp, plt): """Generate primary drying process visualizations.""" figwidth = props["figwidth"] @@ -551,7 +563,7 @@ def _plot_drying_results(data, props, timestamp): plt.close() -def _plot_rp_results(data, props, timestamp): +def _plot_rp_results(data, props, timestamp, plt): product_res = data[1] params = data[2] figwidth = props["figwidth"] @@ -583,7 +595,7 @@ def _plot_rp_results(data, props, timestamp): plt.close() -def _plot_design_space(data, inputs, props, timestamp): +def _plot_design_space(data, inputs, props, timestamp, plt): """Generate design space boundary visualization.""" # Implementation for design space plotting