From bc917452e738c0f5bdb21bd5e9f833fc5e4c3659 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Thu, 2 Apr 2026 20:38:13 +0100 Subject: [PATCH 1/2] more changes --- autofit/non_linear/plot/__init__.py | 9 +- autofit/non_linear/plot/mcmc_plotters.py | 10 - autofit/non_linear/plot/mle_plotters.py | 230 +++++++----------- autofit/non_linear/plot/nest_plotters.py | 164 +++++-------- autofit/non_linear/plot/output.py | 150 ------------ autofit/non_linear/plot/samples_plotters.py | 155 ++---------- .../non_linear/search/mcmc/abstract_mcmc.py | 18 +- autofit/non_linear/search/mle/abstract_mle.py | 76 +++--- .../non_linear/search/nest/abstract_nest.py | 17 +- autofit/plot/__init__.py | 9 +- 10 files changed, 233 insertions(+), 605 deletions(-) delete mode 100644 autofit/non_linear/plot/mcmc_plotters.py delete mode 100644 autofit/non_linear/plot/output.py diff --git a/autofit/non_linear/plot/__init__.py b/autofit/non_linear/plot/__init__.py index 525a81891..d581e9dd7 100644 --- a/autofit/non_linear/plot/__init__.py +++ b/autofit/non_linear/plot/__init__.py @@ -1,5 +1,4 @@ -from autofit.non_linear.plot.samples_plotters import SamplesPlotter -from autofit.non_linear.plot.mcmc_plotters import MCMCPlotter -from autofit.non_linear.plot.mle_plotters import MLEPlotter -from autofit.non_linear.plot.nest_plotters import NestPlotter -from autofit.non_linear.plot.output import Output \ No newline at end of file +from autofit.non_linear.plot.samples_plotters import corner_cornerpy +from autofit.non_linear.plot.nest_plotters import corner_anesthetic +from autofit.non_linear.plot.mle_plotters import subplot_parameters, log_likelihood_vs_iteration +from autofit.non_linear.plot.plot_util import output_figure diff --git a/autofit/non_linear/plot/mcmc_plotters.py b/autofit/non_linear/plot/mcmc_plotters.py deleted file mode 100644 index a3400c4d7..000000000 --- a/autofit/non_linear/plot/mcmc_plotters.py +++ /dev/null @@ -1,10 +0,0 @@ -import logging - -from autofit.non_linear.plot.samples_plotters import SamplesPlotter - -logger = logging.getLogger(__name__) - -class MCMCPlotter(SamplesPlotter): - - pass - diff --git a/autofit/non_linear/plot/mle_plotters.py b/autofit/non_linear/plot/mle_plotters.py index 28a2a22a2..c118d90a1 100644 --- a/autofit/non_linear/plot/mle_plotters.py +++ b/autofit/non_linear/plot/mle_plotters.py @@ -1,136 +1,94 @@ -import logging - -from autofit.non_linear.plot.samples_plotters import SamplesPlotter - -from autofit.non_linear.plot.samples_plotters import skip_plot_in_test_mode - -logger = logging.getLogger(__name__) - -class MLEPlotter(SamplesPlotter): - - def subplot_parameters(self, use_log_y : bool = False, use_last_50_percent : bool = False, **kwargs): - """ - Plots a subplot of every parameter against iteration number. - - The subplot extends over all free parameters in the model-fit, with the number of parameters per subplot - given by the total number of free parameters in the model-fit. - - This often produces a large dynamic range in the y-axis. Plotting the y-axis on a log-scale or only - plotting the last 50% of samples can make the plot easier to inspect. - - Parameters - ---------- - use_log_y - If True, the y-axis is plotted on a log-scale. - use_last_50_percent - If True, only the last 50% of samples are plotted. - kwargs - Additional key word arguments can be passed to the `plt.subplots` method. - - Returns - ------- - - """ - - import matplotlib.pyplot as plt - - parameter_lists = self.samples.parameters_extract - - plt.subplots(self.model.total_free_parameters, 1, figsize=(12, 3 * len(parameter_lists))) - - for i, parameters in enumerate(parameter_lists): - - iteration_list = range(len(parameter_lists[0])) - - plt.subplot(self.model.total_free_parameters, 1, i + 1) - - if use_last_50_percent: - - iteration_list = iteration_list[int(len(iteration_list) / 2) :] - parameters = parameters[int(len(parameters) / 2) :] - - if use_log_y: - plt.semilogy(iteration_list, parameters, c="k") - else: - plt.plot(iteration_list, parameters, c="k") - - plt.xlabel("Iteration", fontsize=16) - plt.ylabel(self.model.parameter_labels_with_superscripts_latex[i], fontsize=16) - plt.xticks(fontsize=16) - plt.yticks(fontsize=16) - - filename = "subplot_parameters" - - if use_log_y: - filename += "_log_y" - - if use_last_50_percent: - filename += "_last_50_percent" - - self.output.subplot_to_figure( - auto_filename=filename - ) - plt.close() - - @skip_plot_in_test_mode - def log_likelihood_vs_iteration(self, use_log_y : bool = False, use_last_50_percent : bool = False, **kwargs): - """ - Plot the log likelihood of a model fit as a function of iteration number. - - For a maximum likelihood estimate, the log likelihood should increase with iteration number. - - This often produces a large dynamic range in the y-axis. Plotting the y-axis on a log-scale or only - plotting the last 50% of samples can make the plot easier to inspect. - - Parameters - ---------- - use_log_y - If True, the y-axis is plotted on a log-scale. - """ - - import matplotlib.pyplot as plt - - log_likelihood_list = self.samples.log_likelihood_list - iteration_list = range(len(log_likelihood_list)) - - if use_last_50_percent: - - iteration_list = iteration_list[int(len(iteration_list) / 2) :] - log_likelihood_list = log_likelihood_list[int(len(log_likelihood_list) / 2) :] - - plt.figure(figsize=(12, 12)) - - if use_log_y: - plt.semilogy(iteration_list, log_likelihood_list, c="k") - else: - plt.plot(iteration_list, log_likelihood_list, c="k") - - plt.xlabel("Iteration", fontsize=16) - plt.ylabel("Log Likelihood", fontsize=16) - plt.xticks(fontsize=16) - plt.yticks(fontsize=16) - - title = "Log Likelihood vs Iteration" - - if use_log_y: - - title += " (Log Scale)" - - if use_last_50_percent: - - title += " (Last 50 Percent)" - - plt.title("Log Likelihood vs Iteration", fontsize=24) - - filename = "log_likelihood_vs_iteration" - - if use_log_y: - filename += "_log_y" - - if use_last_50_percent: - filename += "_last_50_percent" - - self.output.to_figure( - auto_filename=filename, - ) - plt.close() +from autofit.non_linear.plot.plot_util import skip_in_test_mode, output_figure + + +@skip_in_test_mode +def subplot_parameters( + samples, + use_log_y=False, + use_last_50_percent=False, + path=None, + filename="subplot_parameters", + format="show", + **kwargs, +): + import matplotlib.pyplot as plt + + model = samples.model + parameter_lists = samples.parameters_extract + + plt.subplots(model.total_free_parameters, 1, figsize=(12, 3 * len(parameter_lists))) + + for i, parameters in enumerate(parameter_lists): + iteration_list = range(len(parameter_lists[0])) + + plt.subplot(model.total_free_parameters, 1, i + 1) + + if use_last_50_percent: + iteration_list = iteration_list[int(len(iteration_list) / 2) :] + parameters = parameters[int(len(parameters) / 2) :] + + if use_log_y: + plt.semilogy(iteration_list, parameters, c="k") + else: + plt.plot(iteration_list, parameters, c="k") + + plt.xlabel("Iteration", fontsize=16) + plt.ylabel(model.parameter_labels_with_superscripts_latex[i], fontsize=16) + plt.xticks(fontsize=16) + plt.yticks(fontsize=16) + + actual_filename = filename + if use_log_y: + actual_filename += "_log_y" + if use_last_50_percent: + actual_filename += "_last_50_percent" + + output_figure(path=path, filename=actual_filename, format=format) + + +@skip_in_test_mode +def log_likelihood_vs_iteration( + samples, + use_log_y=False, + use_last_50_percent=False, + path=None, + filename="log_likelihood_vs_iteration", + format="show", + **kwargs, +): + import matplotlib.pyplot as plt + + log_likelihood_list = samples.log_likelihood_list + iteration_list = range(len(log_likelihood_list)) + + if use_last_50_percent: + iteration_list = iteration_list[int(len(iteration_list) / 2) :] + log_likelihood_list = log_likelihood_list[int(len(log_likelihood_list) / 2) :] + + plt.figure(figsize=(12, 12)) + + if use_log_y: + plt.semilogy(iteration_list, log_likelihood_list, c="k") + else: + plt.plot(iteration_list, log_likelihood_list, c="k") + + plt.xlabel("Iteration", fontsize=16) + plt.ylabel("Log Likelihood", fontsize=16) + plt.xticks(fontsize=16) + plt.yticks(fontsize=16) + + title = "Log Likelihood vs Iteration" + if use_log_y: + title += " (Log Scale)" + if use_last_50_percent: + title += " (Last 50 Percent)" + + plt.title(title, fontsize=24) + + actual_filename = filename + if use_log_y: + actual_filename += "_log_y" + if use_last_50_percent: + actual_filename += "_last_50_percent" + + output_figure(path=path, filename=actual_filename, format=format) diff --git a/autofit/non_linear/plot/nest_plotters.py b/autofit/non_linear/plot/nest_plotters.py index 36743da7c..b5dc20963 100644 --- a/autofit/non_linear/plot/nest_plotters.py +++ b/autofit/non_linear/plot/nest_plotters.py @@ -1,103 +1,61 @@ -from functools import wraps -import numpy as np -import warnings - -from autoconf import conf - -from autofit.non_linear.plot import SamplesPlotter -from autofit.non_linear.plot.samples_plotters import skip_plot_in_test_mode - - -def log_value_error(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - """ - Prevent an exception terminating the run if visualization fails due to convergence not yet being reached. - - Searches attempt to perform visualization every `iterations_per_full_update`, however these visualization calls - may occur before the search has converged on enough of parameter to successfully perform visualization. - - This can lead the search to raise an exception which terminates the Python script, when we instead - want the code to continue running, to continue the search and perform visualization on a subsequent iteration - once convergence has been achieved. - - This wrapper catches these exceptions, logs them so the user can see visualization failed and then - continues the code without raising an exception in a way that terminates the script. - - This wrapper is specific to Dynesty, which raises a `ValueError` when visualization is performed before - convergence has been achieved. - - Parameters - ---------- - self - An instance of a `SearchPlotter` class. - args - The arguments used to perform a visualization of the search. - kwargs - The keyword arguments used to perform a visualization of the search. - """ - try: - return func(self, *args, **kwargs) - except (ValueError, KeyError, AssertionError, IndexError, TypeError, RuntimeError, np.linalg.LinAlgError): - pass - - return wrapper - - -class NestPlotter(SamplesPlotter): - - @skip_plot_in_test_mode - @log_value_error - def corner_anesthetic(self, **kwargs): - """ - Plots a corner plot via the visualization library `anesthetic`. - - This plots a corner plot including the 1-D and 2-D marginalized posteriors. - """ - - config_dict = conf.instance["visualize"]["plots_settings"]["corner_anesthetic"] - - from anesthetic.samples import NestedSamples - from anesthetic import make_2d_axes - import matplotlib.pylab as pylab - - params = {'font.size' : int(config_dict["fontsize"])} - pylab.rcParams.update(params) - - figsize = ( - self.model.total_free_parameters * config_dict["figsize_per_parammeter"], - self.model.total_free_parameters * config_dict["figsize_per_parammeter"] - ) - - samples = NestedSamples( - np.asarray(self.samples.parameter_lists), - weights=self.samples.weight_list, - columns=self.model.parameter_labels_with_superscripts_latex - ) - - from pandas.errors import SettingWithCopyWarning - warnings.filterwarnings("ignore", category=SettingWithCopyWarning) - - fig, axes = make_2d_axes( - self.model.parameter_labels_with_superscripts_latex, - figsize=figsize, - facecolor=config_dict["facecolor"], - ) - - warnings.filterwarnings("default", category=SettingWithCopyWarning) - - # prior = samples.prior() - # prior.plot_2d(axes, alpha=0.9, label="prior") - samples.plot_2d( - axes, - alpha=config_dict["alpha"], - label="posterior", - ) - axes.iloc[-1, 0].legend( - bbox_to_anchor=(len(axes) / 2, len(axes)), - loc='lower center', - ncols=2 - ) - - self.output.to_figure(auto_filename="corner_anesthetic") - self.close() +import numpy as np +import warnings + +from autoconf import conf + +from autofit.non_linear.plot.plot_util import ( + skip_in_test_mode, + log_plot_exception, + output_figure, +) + + +@skip_in_test_mode +@log_plot_exception +def corner_anesthetic(samples, path=None, filename="corner_anesthetic", format="show", **kwargs): + config_dict = conf.instance["visualize"]["plots_settings"]["corner_anesthetic"] + + from anesthetic.samples import NestedSamples + from anesthetic import make_2d_axes + import matplotlib.pylab as pylab + + params = {"font.size": int(config_dict["fontsize"])} + pylab.rcParams.update(params) + + model = samples.model + + figsize = ( + model.total_free_parameters * config_dict["figsize_per_parammeter"], + model.total_free_parameters * config_dict["figsize_per_parammeter"], + ) + + nested_samples = NestedSamples( + np.asarray(samples.parameter_lists), + weights=samples.weight_list, + columns=model.parameter_labels_with_superscripts_latex, + ) + + from pandas.errors import SettingWithCopyWarning + + warnings.filterwarnings("ignore", category=SettingWithCopyWarning) + + fig, axes = make_2d_axes( + model.parameter_labels_with_superscripts_latex, + figsize=figsize, + facecolor=config_dict["facecolor"], + ) + + warnings.filterwarnings("default", category=SettingWithCopyWarning) + + nested_samples.plot_2d( + axes, + alpha=config_dict["alpha"], + label="posterior", + ) + axes.iloc[-1, 0].legend( + bbox_to_anchor=(len(axes) / 2, len(axes)), + loc="lower center", + ncols=2, + ) + + output_figure(path=path, filename=filename, format=format) diff --git a/autofit/non_linear/plot/output.py b/autofit/non_linear/plot/output.py deleted file mode 100644 index 1c9a5daf5..000000000 --- a/autofit/non_linear/plot/output.py +++ /dev/null @@ -1,150 +0,0 @@ -from pathlib import Path -from os import path -from typing import List, Optional, Union - -from autoconf import conf - - -def set_backend(): - - import matplotlib - - backend = conf.get_matplotlib_backend() - - if backend not in "default": - matplotlib.use(backend) - - try: - hpc_mode = conf.instance["general"]["hpc"]["hpc_mode"] - except KeyError: - hpc_mode = False - - if hpc_mode: - matplotlib.use("Agg") - - -import os - - -class Output: - def __init__( - self, - path: Optional[Path] = None, - filename: Optional[str] = None, - format: Union[str, List[str]] = None, - bypass: bool = False, - ): - """ - Sets how the figure or subplot is output, either by displaying it on the screen or writing it to hard-disk. - - This object wraps the following Matplotlib methods: - - - plt.show: https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.show.html - - plt.savefig: https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.savefig.html - - The default behaviour is the display the figure on the computer screen, as opposed to outputting to hard-disk - as a file. - - Parameters - ---------- - path - If the figure is output to hard-disk the path of the folder it is saved to. - filename - If the figure is output to hard-disk the filename used to save it. - format - The format of the output, 'show' displays on the computer screen, 'png' outputs to .png, 'fits' outputs to - `.fits` format. - bypass - Whether to bypass the `plt.show` or `plt.savefig` methods, used when plotting a subplot. - """ - if path is not None: - path = Path(path) - - self.path = path - - if path is not None and path: - os.makedirs(path, exist_ok=True) - - self.filename = filename - self._format = format - self.bypass = bypass - - @property - def format(self) -> str: - return self._format or "show" - - @property - def format_list(self): - if not isinstance(self.format, list): - return [self.format] - return self.format - - def to_figure( - self, - auto_filename: Optional[str] = None, - ): - """ - Output the figure, by either displaying it on the user's screen or to the hard-disk as a .png or .fits file. - - Parameters - ---------- - structure - The 2D array of image to be output, required for outputting the image as a fits file. - """ - - import matplotlib.pyplot as plt - - filename = auto_filename if self.filename is None else self.filename - - for format in self.format_list: - if os.environ.get("PYAUTOARRAY_OUTPUT_MODE") == "1": - return self.to_figure_output_mode(filename=filename) - if not self.bypass: - if format == "show": - plt.show() - elif format == "png": - plt.savefig(self.path / f"{filename}.png") - elif format == "pdf": - plt.savefig(self.path / f"{filename}.pdf") - - def subplot_to_figure(self, auto_filename=None): - """ - Output a subplot figure, either as an image on the screen or to the hard-disk as a png or fits file. - """ - - import matplotlib.pyplot as plt - - filename = auto_filename if self.filename is None else self.filename - - for format in self.format_list: - if os.environ.get("PYAUTOARRAY_OUTPUT_MODE") == "1": - return self.to_figure_output_mode(filename=filename) - if format == "show": - plt.show() - elif format == "png": - plt.savefig(self.path / f"{filename}.png") - elif format == "pdf": - plt.savefig(self.path / f"{filename}.pdf") - - - def to_figure_output_mode(self, filename: str): - - import matplotlib.pyplot as plt - - global COUNT - - try: - COUNT += 1 - except NameError: - COUNT = 0 - - import sys - - script_name = path.split(sys.argv[0])[-1].replace(".py", "") - - output_path = path.join(os.getcwd(), "output_mode", script_name) - os.makedirs(output_path, exist_ok=True) - - plt.savefig( - path.join(output_path, f"{COUNT}_{filename}.png"), - ) diff --git a/autofit/non_linear/plot/samples_plotters.py b/autofit/non_linear/plot/samples_plotters.py index c908dd99f..85a9389d9 100644 --- a/autofit/non_linear/plot/samples_plotters.py +++ b/autofit/non_linear/plot/samples_plotters.py @@ -1,130 +1,25 @@ -import numpy as np -from functools import wraps -import logging -import os - -from autoconf import conf - -from autofit.non_linear.plot.output import Output - -logger = logging.getLogger(__name__) - -def skip_plot_in_test_mode(func): - """ - Skips visualization plots of non-linear searches if test mode is on. - - Parameters - ---------- - func - A function which plots a result of a non-linear search. - - Returns - ------- - A function that plots a visual, or None if test mode is on. - """ - - @wraps(func) - def wrapper( - *args, - **kwargs - ): - """ - Skips visualization plots of non-linear searches if test mode is on. - - Parameters - ---------- - obj - An plotter object which performs visualization of a non-linear search. - - Returns - ------- - A function that plots a visual, or None if test mode is on. - """ - - if os.environ.get("PYAUTOFIT_TEST_MODE") == "1": - return - - return func(*args, **kwargs) - - return wrapper - - - -class SamplesPlotter: - def __init__( - self, - samples, - output : Output = Output() - ): - - self.samples = samples - self.output = output - - @property - def model(self): - return self.samples.model - - @property - def log_posterior_list(self): - return self.samples.log_posterior_list - - def close(self): - - import matplotlib.pyplot as plt - - if plt.fignum_exists(num=1): - plt.clf() - plt.close() - - def log_plot_exception(self, plot_name : str): - """ - Plotting the results of a ``dynesty`` model-fit before they have converged on an - accurate estimate of the posterior can lead the ``dynesty`` plotting routines - to raise a ``ValueError``. - - This exception is caught in each of the plotting methods below, and this - function is used to log the behaviour. - - Parameters - ---------- - plot_name - The name of the ``dynesty`` plot which raised a ``ValueError`` - """ - - logger.info( - f"""{self.__class__.__name__} unable to produce {plot_name} visual: posterior estimate therefore - not yet sufficient for this model-fit is not yet robust enough to do this. - Visuals should be produced in later update, once posterior estimate is updated. - """ - ) - - def corner_cornerpy(self, **kwargs): - """ - Plots a corner plot via the visualization library `corner.py`. - - This plots a corner plot including the 1-D and 2-D marginalized posteriors. - """ - - if os.environ.get("PYAUTOFIT_TEST_MODE") == "1": - return - - import matplotlib.pylab as pylab - - config_dict = conf.instance["visualize"]["plots_settings"]["corner_cornerpy"] - - params = {'font.size' : int(config_dict["fontsize"])} - pylab.rcParams.update(params) - - import corner - - corner.corner( - data=np.asarray(self.samples.parameter_lists), - weight_list=self.samples.weight_list, - labels=self.model.parameter_labels_with_superscripts_latex, - ) - - self.output.to_figure(auto_filename="corner") - self.close() - - - +import numpy as np + +from autoconf import conf + +from autofit.non_linear.plot.plot_util import skip_in_test_mode, output_figure + + +@skip_in_test_mode +def corner_cornerpy(samples, path=None, filename="corner", format="show", **kwargs): + import matplotlib.pylab as pylab + + config_dict = conf.instance["visualize"]["plots_settings"]["corner_cornerpy"] + + params = {"font.size": int(config_dict["fontsize"])} + pylab.rcParams.update(params) + + import corner + + corner.corner( + data=np.asarray(samples.parameter_lists), + weight_list=samples.weight_list, + labels=samples.model.parameter_labels_with_superscripts_latex, + ) + + output_figure(path=path, filename=filename, format=format) diff --git a/autofit/non_linear/search/mcmc/abstract_mcmc.py b/autofit/non_linear/search/mcmc/abstract_mcmc.py index dea35a845..6dcfffd7a 100644 --- a/autofit/non_linear/search/mcmc/abstract_mcmc.py +++ b/autofit/non_linear/search/mcmc/abstract_mcmc.py @@ -6,8 +6,7 @@ from autofit.non_linear.initializer import Initializer from autofit.non_linear.samples import SamplesMCMC from autofit.non_linear.search.mcmc.auto_correlations import AutoCorrelationsSettings -from autofit.non_linear.plot.mcmc_plotters import MCMCPlotter -from autofit.non_linear.plot.output import Output +from autofit.non_linear.plot import corner_cornerpy class AbstractMCMC(NonLinearSearch): @@ -50,10 +49,6 @@ def config_type(self): def samples_cls(self): return SamplesMCMC - @property - def plotter_cls(self): - return MCMCPlotter - def plot_results(self, samples): if not samples.pdf_converged: @@ -62,10 +57,9 @@ def plot_results(self, samples): def should_plot(name): return conf.instance["visualize"]["plots_search"]["mcmc"][name] - plotter = self.plotter_cls( - samples=samples, - output=Output(path=self.paths.image_path / "search", format="png"), - ) - if should_plot("corner_cornerpy"): - plotter.corner_cornerpy() + corner_cornerpy( + samples=samples, + path=self.paths.image_path / "search", + format="png", + ) diff --git a/autofit/non_linear/search/mle/abstract_mle.py b/autofit/non_linear/search/mle/abstract_mle.py index 2f678991c..36ac52828 100644 --- a/autofit/non_linear/search/mle/abstract_mle.py +++ b/autofit/non_linear/search/mle/abstract_mle.py @@ -1,43 +1,33 @@ -from abc import ABC - -from autoconf import conf -from autofit.non_linear.search.abstract_search import NonLinearSearch -from autofit.non_linear.samples import Samples -from autofit.non_linear.plot.mle_plotters import MLEPlotter -from autofit.non_linear.plot.output import Output - - -class AbstractMLE(NonLinearSearch, ABC): - @property - def config_type(self): - return conf.instance["non_linear"]["mle"] - - @property - def samples_cls(self): - return Samples - - @property - def plotter_cls(self): - return MLEPlotter - - def plot_results(self, samples): - - def should_plot(name): - return conf.instance["visualize"]["plots_search"]["mle"][name] - - plotter = self.plotter_cls( - samples=samples, - output=Output(path=self.paths.image_path / "search", format="png"), - ) - - if should_plot("subplot_parameters"): - - plotter.subplot_parameters() - plotter.subplot_parameters(use_log_y=True) - plotter.subplot_parameters(use_last_50_percent=True) - - if should_plot("log_likelihood_vs_iteration"): - - plotter.log_likelihood_vs_iteration() - plotter.log_likelihood_vs_iteration(use_log_y=True) - plotter.log_likelihood_vs_iteration(use_last_50_percent=True) \ No newline at end of file +from abc import ABC + +from autoconf import conf +from autofit.non_linear.search.abstract_search import NonLinearSearch +from autofit.non_linear.samples import Samples +from autofit.non_linear.plot import subplot_parameters, log_likelihood_vs_iteration + + +class AbstractMLE(NonLinearSearch, ABC): + @property + def config_type(self): + return conf.instance["non_linear"]["mle"] + + @property + def samples_cls(self): + return Samples + + def plot_results(self, samples): + + def should_plot(name): + return conf.instance["visualize"]["plots_search"]["mle"][name] + + plot_path = self.paths.image_path / "search" + + if should_plot("subplot_parameters"): + subplot_parameters(samples=samples, path=plot_path, format="png") + subplot_parameters(samples=samples, use_log_y=True, path=plot_path, format="png") + subplot_parameters(samples=samples, use_last_50_percent=True, path=plot_path, format="png") + + if should_plot("log_likelihood_vs_iteration"): + log_likelihood_vs_iteration(samples=samples, path=plot_path, format="png") + log_likelihood_vs_iteration(samples=samples, use_log_y=True, path=plot_path, format="png") + log_likelihood_vs_iteration(samples=samples, use_last_50_percent=True, path=plot_path, format="png") diff --git a/autofit/non_linear/search/nest/abstract_nest.py b/autofit/non_linear/search/nest/abstract_nest.py index 8b088ebe9..eb89ba983 100644 --- a/autofit/non_linear/search/nest/abstract_nest.py +++ b/autofit/non_linear/search/nest/abstract_nest.py @@ -11,8 +11,7 @@ InitializerParamBounds, ) from autofit.non_linear.samples import SamplesNest -from autofit.non_linear.plot.nest_plotters import NestPlotter -from autofit.non_linear.plot.output import Output +from autofit.non_linear.plot import corner_anesthetic class AbstractNest(NonLinearSearch, ABC): @@ -70,20 +69,16 @@ def config_type(self): def samples_cls(self): return SamplesNest - @property - def plotter_cls(self): - return NestPlotter - def plot_results(self, samples): def should_plot(name): return conf.instance["visualize"]["plots_search"]["nest"][name] - plotter = self.plotter_cls( - samples=samples, - output=Output(path=self.paths.image_path / "search", format="png"), - ) if should_plot("corner_anesthetic"): with warnings.catch_warnings(): warnings.simplefilter("ignore") - plotter.corner_anesthetic() \ No newline at end of file + corner_anesthetic( + samples=samples, + path=self.paths.image_path / "search", + format="png", + ) \ No newline at end of file diff --git a/autofit/plot/__init__.py b/autofit/plot/__init__.py index 525a81891..d581e9dd7 100644 --- a/autofit/plot/__init__.py +++ b/autofit/plot/__init__.py @@ -1,5 +1,4 @@ -from autofit.non_linear.plot.samples_plotters import SamplesPlotter -from autofit.non_linear.plot.mcmc_plotters import MCMCPlotter -from autofit.non_linear.plot.mle_plotters import MLEPlotter -from autofit.non_linear.plot.nest_plotters import NestPlotter -from autofit.non_linear.plot.output import Output \ No newline at end of file +from autofit.non_linear.plot.samples_plotters import corner_cornerpy +from autofit.non_linear.plot.nest_plotters import corner_anesthetic +from autofit.non_linear.plot.mle_plotters import subplot_parameters, log_likelihood_vs_iteration +from autofit.non_linear.plot.plot_util import output_figure From cf760257a33a4984455213e543a6f31363d7fe4b Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Fri, 3 Apr 2026 10:08:12 +0100 Subject: [PATCH 2/2] Add plot_util.py with shared decorators and output helper Co-Authored-By: Claude Opus 4.6 (1M context) --- autofit/non_linear/plot/plot_util.py | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 autofit/non_linear/plot/plot_util.py diff --git a/autofit/non_linear/plot/plot_util.py b/autofit/non_linear/plot/plot_util.py new file mode 100644 index 000000000..7b26f57bf --- /dev/null +++ b/autofit/non_linear/plot/plot_util.py @@ -0,0 +1,52 @@ +import os +import logging +from functools import wraps +from pathlib import Path + +import numpy as np + +logger = logging.getLogger(__name__) + + +def skip_in_test_mode(func): + @wraps(func) + def wrapper(*args, **kwargs): + if os.environ.get("PYAUTOFIT_TEST_MODE") == "1": + return + return func(*args, **kwargs) + + return wrapper + + +def log_plot_exception(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except ( + ValueError, + KeyError, + AssertionError, + IndexError, + TypeError, + RuntimeError, + np.linalg.LinAlgError, + ): + logger.info( + f"Unable to produce {func.__name__} visual: posterior estimate " + f"not yet sufficient. Should succeed in a later update." + ) + + return wrapper + + +def output_figure(path=None, filename="figure", format="show"): + import matplotlib.pyplot as plt + + if format == "show": + plt.show() + elif format in ("png", "pdf"): + if path is not None: + os.makedirs(path, exist_ok=True) + plt.savefig(Path(path) / f"{filename}.{format}") + plt.close()