From f1f1f412e4c562e9f5393953ae0dc2863e8ce262 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 21 Oct 2026 12:03:53 +0100 Subject: [PATCH] feat: keep dataset-only image_with_points.png; after-fit figure saved as fit_image_with_points.png (#781) Co-Authored-By: Claude Opus 5.5 --- autolens/config/visualize/plots.yaml | 4 +- autolens/point/model/plotter.py | 13 ++--- autolens/point/model/visualizer.py | 4 +- autolens/point/plot/fit_point_plots.py | 17 +++++-- .../point/model/test_plotter_point.py | 48 ++++++++++++++++--- .../point/plot/test_fit_point_plots.py | 46 ++++++++++++++++++ 6 files changed, 114 insertions(+), 18 deletions(-) diff --git a/autolens/config/visualize/plots.yaml b/autolens/config/visualize/plots.yaml index 8ede4b51d..3e34c9281 100644 --- a/autolens/config/visualize/plots.yaml +++ b/autolens/config/visualize/plots.yaml @@ -55,10 +55,10 @@ fit_interferometer: # Settings for plots of fits to inter point_dataset: # Settings for plots of point source datasets (e.g. PointDatasetPlotter). subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the data, noise-map, etc.)? - image_with_points: true # Plot the observed image (if passed to AnalysisPoint) with the observed positions over it? + image_with_points: true # Plot the observed image (if passed to AnalysisPoint) with the observed positions over it (image_with_points.png)? fit_point_dataset: # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). - image_with_points: true # Plot the observed image (if passed to AnalysisPoint) with observed and model positions over it? + fit_image_with_points: true # Plot the observed image (if passed to AnalysisPoint) with observed and model positions and critical curves over it (fit_image_with_points.png)? weak_dataset: # Settings for plots of weak lensing shear catalogues (e.g. PlotterWeak). subplot_dataset: true # Plot subplot containing all dataset quantities (e.g. the shear field, noise-map, etc.)? diff --git a/autolens/point/model/plotter.py b/autolens/point/model/plotter.py index fdaa4523d..ff7da24bf 100644 --- a/autolens/point/model/plotter.py +++ b/autolens/point/model/plotter.py @@ -44,9 +44,10 @@ def image_with_points( """ Output an observed image of the lensed point source with the point-source positions overlaid. - Before the fit (``fit=None``) only the observed positions are drawn, controlled by the - `point_dataset: image_with_points` setting. After the fit the model positions (and critical curves, - if given) are also drawn, controlled by the `fit_point_dataset: image_with_points` setting. + Before the fit (``fit=None``) only the observed positions are drawn, output as `image_with_points.png` + and controlled by the `point_dataset: image_with_points` setting. After the fit the model positions (and + critical curves, if given) are also drawn, output as `fit_image_with_points.png` (so both figures coexist) + and controlled by the `fit_point_dataset: fit_image_with_points` setting. Parameters ---------- @@ -62,11 +63,11 @@ def image_with_points( Colours for each image-plane line. """ if fit is None: - section = ["point_dataset"] + section, name = ["point_dataset"], "image_with_points" else: - section = ["fit", "fit_point_dataset"] + section, name = ["fit", "fit_point_dataset"], "fit_image_with_points" - if not plot_setting(section=section, name="image_with_points"): + if not plot_setting(section=section, name=name): return plot_image_with_points( diff --git a/autolens/point/model/visualizer.py b/autolens/point/model/visualizer.py index 414ce1f6a..2a2c0d93f 100644 --- a/autolens/point/model/visualizer.py +++ b/autolens/point/model/visualizer.py @@ -34,6 +34,7 @@ def visualize_before_fit( plotter.dataset_point(dataset=analysis.dataset) if getattr(analysis, "image", None) is not None: + # Dataset-only figure (observed positions, no model): `image_with_points.png`. plotter.image_with_points(image=analysis.image, dataset=analysis.dataset) @staticmethod @@ -93,7 +94,8 @@ def visualize( source_plane_line_colors=sp_colors, ) - # Output even on `quick_update`, so the observed image with model positions updates on-the-fly. + # Output even on `quick_update`, so the observed image with model positions updates on-the-fly. Saved as + # `fit_image_with_points.png`, so the dataset-only `image_with_points.png` from before the fit is kept. if getattr(analysis, "image", None) is not None: plotter.image_with_points( image=analysis.image, diff --git a/autolens/point/plot/fit_point_plots.py b/autolens/point/plot/fit_point_plots.py index eb2852965..f0d1cf8ea 100644 --- a/autolens/point/plot/fit_point_plots.py +++ b/autolens/point/plot/fit_point_plots.py @@ -104,6 +104,15 @@ def plot_image_with_points( are drawn as red crosses (image-plane fits only, as a source-plane fit's model data lies in the source plane), and critical curves can be overlaid via ``image_plane_lines``. + The function produces two figures, depending on whether a fit is passed: + + - ``fit=None`` (dataset-only): only the observed image and observed positions are drawn, saved as + ``image_with_points``. Use this to display the data before lens modeling, passing no fit and no + ``image_plane_lines``. + - ``fit`` given: the model positions (and critical curves, via ``image_plane_lines``) are added, the title + becomes " Fit Image With Points" and the figure is saved as ``fit_image_with_points``, so it does not + overwrite the dataset-only figure. + The image is used only for visualization: it does not enter the point-source likelihood. Parameters @@ -113,7 +122,8 @@ def plot_image_with_points( dataset : PointDataset The point-source dataset whose observed positions are overlaid. fit : FitPointDataset, optional - If given, its model positions are overlaid as red crosses. + If given, its model positions are overlaid as red crosses and the figure is saved as + ``fit_image_with_points``; if ``None`` it is the dataset-only ``image_with_points``. output_path : str, optional Directory in which to save the figure. If ``None`` the figure is not saved to disk. @@ -135,7 +145,7 @@ def plot_image_with_points( ax=ax, lines=image_plane_lines, line_colors=image_plane_line_colors, - title=f"{_prefix}{dataset.name} Image With Points", + title=f"{_prefix}{dataset.name} {'Image' if fit is None else 'Fit Image'} With Points", ) # Positions are (y,x) arc-seconds: reflect them exactly as `plot_array` does its own overlays, using the @@ -165,4 +175,5 @@ def _yx(grid): ax.legend(loc="upper right", fontsize=8, framealpha=0.8) - save_figure(fig, path=output_path, filename="image_with_points", format=output_format) + filename = "image_with_points" if fit is None else "fit_image_with_points" + save_figure(fig, path=output_path, filename=filename, format=output_format) diff --git a/test_autolens/point/model/test_plotter_point.py b/test_autolens/point/model/test_plotter_point.py index dca090471..024b9bf64 100644 --- a/test_autolens/point/model/test_plotter_point.py +++ b/test_autolens/point/model/test_plotter_point.py @@ -75,6 +75,44 @@ def test__image_with_points(fit_point_dataset_x2_plane, plot_path, plot_patch): plotter.image_with_points(image=_image(), dataset=fit_point_dataset_x2_plane.dataset) assert str(plot_path / "image_with_points.png") in plot_patch.paths + assert str(plot_path / "fit_image_with_points.png") not in plot_patch.paths + + +def test__fit_image_with_points(fit_point_dataset_x2_plane, plot_path, plot_patch): + if plot_path.exists(): + shutil.rmtree(plot_path) + + plotter = PlotterPoint(image_path=plot_path) + + plotter.image_with_points( + image=_image(), + dataset=fit_point_dataset_x2_plane.dataset, + fit=fit_point_dataset_x2_plane, + ) + + assert str(plot_path / "fit_image_with_points.png") in plot_patch.paths + assert str(plot_path / "image_with_points.png") not in plot_patch.paths + + +def test__visualizer__before_fit__only_image_with_points( + fit_point_dataset_x2_plane, plot_path, plot_patch +): + from types import SimpleNamespace + + from autolens.point.model.visualizer import VisualizerPoint + + if plot_path.exists(): + shutil.rmtree(plot_path) + + analysis = SimpleNamespace( + dataset=fit_point_dataset_x2_plane.dataset, image=_image(), title_prefix=None + ) + paths = SimpleNamespace(image_path=plot_path, output_path=plot_path) + + VisualizerPoint.visualize_before_fit(analysis=analysis, paths=paths, model=None) + + assert plot_patch.paths.count(str(plot_path / "image_with_points.png")) == 1 + assert str(plot_path / "fit_image_with_points.png") not in plot_patch.paths @pytest.mark.parametrize("quick_update", [False, True]) @@ -91,12 +129,9 @@ def test__visualizer__image_with_points_before_and_after_fit( quick_update=quick_update, ) - image_with_points = [ - path for path in plot_patch.paths if path == str(plot_path / "image_with_points.png") - ] - - # Once before the fit (observed positions) and once after (with model positions). - assert len(image_with_points) == 2 + # The dataset-only figure before the fit and the fit figure after it are separate files, so both coexist. + assert plot_patch.paths.count(str(plot_path / "image_with_points.png")) == 1 + assert plot_patch.paths.count(str(plot_path / "fit_image_with_points.png")) == 1 def test__visualizer__no_image__no_image_with_points( @@ -111,3 +146,4 @@ def test__visualizer__no_image__no_image_with_points( assert str(plot_path / "fit.png") in plot_patch.paths assert str(plot_path / "image_with_points.png") not in plot_patch.paths + assert str(plot_path / "fit_image_with_points.png") not in plot_patch.paths diff --git a/test_autolens/point/plot/test_fit_point_plots.py b/test_autolens/point/plot/test_fit_point_plots.py index 0c6039e38..e629844e4 100644 --- a/test_autolens/point/plot/test_fit_point_plots.py +++ b/test_autolens/point/plot/test_fit_point_plots.py @@ -35,7 +35,53 @@ def test__plot_image_with_points(point_dataset, fit_point_dataset_x2_plane, plot output_path=plot_path, output_format="png", ) + assert str(plot_path / "fit_image_with_points.png") in plot_patch.paths + assert str(plot_path / "image_with_points.png") not in plot_patch.paths + + +def test__plot_image_with_points__dataset_only__no_model_points_or_lines( + point_dataset, plot_path, plot_patch, monkeypatch +): + import matplotlib.axes + + import autolens as al + import autogalaxy.util.plot_utils as plot_utils + + from autolens.point.plot.fit_point_plots import plot_image_with_points + + scatter_labels = [] + scatter = matplotlib.axes.Axes.scatter + + def counting_scatter(self, *args, **kwargs): + scatter_labels.append(kwargs.get("label")) + return scatter(self, *args, **kwargs) + + monkeypatch.setattr(matplotlib.axes.Axes, "scatter", counting_scatter) + + plot_array_kwargs = [] + plot_array = plot_utils.plot_array + + def recording_plot_array(*args, **kwargs): + plot_array_kwargs.append(kwargs) + return plot_array(*args, **kwargs) + + monkeypatch.setattr(plot_utils, "plot_array", recording_plot_array) + + plot_image_with_points( + image=al.Array2D.ones(shape_native=(20, 20), pixel_scales=0.25), + dataset=point_dataset, + output_path=plot_path, + output_format="png", + ) + assert str(plot_path / "image_with_points.png") in plot_patch.paths + assert str(plot_path / "fit_image_with_points.png") not in plot_patch.paths + assert scatter_labels.count("Observed") == 1 + assert "Model" not in scatter_labels + assert len(plot_array_kwargs) == 1 + assert plot_array_kwargs[0]["lines"] is None + assert plot_array_kwargs[0]["title"].endswith("Image With Points") + assert "Fit Image" not in plot_array_kwargs[0]["title"] @pytest.mark.parametrize(