diff --git a/AGENTS.md b/AGENTS.md index 1ce49fa..0dec0a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,13 +69,23 @@ the full figure set. Every other producer tracks only `dataset.png` and `fit.png everything else. Producers with several sub-datasets write one pair per sub-folder (`multi_dataset` per band, the datacube per channel, `cluster` per point-source plane). Many of these figures are the galaxy-scale plots and look poor on point / cluster / weak data — -bespoke figures are a follow-up through the Eyes dashboard. `cluster` calls the two +bespoke figures are a follow-up through the Eyes dashboard. `cluster` calls the `PlotterPoint` methods directly instead of `VisualizerPoint`, skipping the untracked tracer and -galaxy figures; the "Fit Positions" panel still draws the critical curves, which cost ~2 min per -dataset on the 15-galaxy multi-plane lens (the PNGs match the visualizer's). +galaxy figures; the "Fit Positions" and point-solver panels still draw the critical curves, which +cost ~2 min per dataset on the 15-galaxy multi-plane lens (the PNGs match the visualizer's). + +**Point solver.** `point_source_image_chi_squared` (via `visualize(quick_update=False)`) and +`cluster` (per plane) also keep `point_solver.png`: the model's real `PointSolver` refining onto +its images (`fit_point_dataset: subplot_point_solver`, on in this repo's `plots.yaml`, off in +the library). `point_source_image_chi_squared/images/visualization/teaching/` holds the same +figure on a coarse teaching grid from `aplt.plot_point_solver`, plus its looping animation +`point_solver.gif`. **What is tracked.** PNG figures under `scripts//images/**`, `GALLERY.md` and -`gallery/viz_manifest.yaml`. The FITS / +`gallery/viz_manifest.yaml`, plus the teaching GIF animation +(`point_source_image_chi_squared/images/visualization/teaching/point_solver.gif`). The manifest +and `GALLERY.md` list PNGs only (the PyAutoEyes read contract), so the GIF is tracked beside its +PNG still but is not a manifest entry; `render.yml` commits it with the PNGs. The FITS / CSV / JSON data products the visualizers also write are gitignored (bulky, not viewable on GitHub), as are `output/` and `dataset/**/lensed_source.fits`. diff --git a/GALLERY.md b/GALLERY.md index fb44959..d412231 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -2,14 +2,14 @@ -Rendered with PyAutoLens `2026.8.17.1` · 68 figures. +Rendered with PyAutoLens `2026.8.17.1` · 72 figures. Every figure below is what the PyAutoLens visualizers (`VisualizerImaging`, `VisualizerInterferometer`, `VisualizerPoint`, `VisualizerWeak`) write to a model-fit's `image/` folder, rendered on this repo's datasets with the true model. See [README.md](README.md) for how to improve one. ## Contents - [cluster](#cluster) - - [visualization](#cluster--visualization) (4 figures) + - [visualization](#cluster--visualization) (6 figures) - [point_0/](#cluster--visualization--point_0) - [point_1/](#cluster--visualization--point_1) - [group](#group) @@ -38,8 +38,9 @@ Every figure below is what the PyAutoLens visualizers (`VisualizerImaging`, `Vis - [visualization](#multi_galaxy--visualization) (2 figures) - [top level](#multi_galaxy--visualization--top-level) - [point_source_image_chi_squared](#point_source_image_chi_squared) - - [visualization](#point_source_image_chi_squared--visualization) (4 figures) + - [visualization](#point_source_image_chi_squared--visualization) (6 figures) - [top level](#point_source_image_chi_squared--visualization--top-level) + - [teaching/](#point_source_image_chi_squared--visualization--teaching) - [point_source_source_chi_squared](#point_source_source_chi_squared) - [visualization](#point_source_source_chi_squared--visualization) (4 figures) - [top level](#point_source_source_chi_squared--visualization--top-level) @@ -61,6 +62,10 @@ Every figure below is what the PyAutoLens visualizers (`VisualizerImaging`, `Vis ![fit](scripts/cluster/images/visualization/point_0/fit.png) +##### point_solver + +![point_solver](scripts/cluster/images/visualization/point_0/point_solver.png) + #### cluster / visualization / point_1/ ##### dataset @@ -71,6 +76,10 @@ Every figure below is what the PyAutoLens visualizers (`VisualizerImaging`, `Vis ![fit](scripts/cluster/images/visualization/point_1/fit.png) +##### point_solver + +![point_solver](scripts/cluster/images/visualization/point_1/point_solver.png) + ## group ### group / visualization @@ -359,6 +368,16 @@ Every figure below is what the PyAutoLens visualizers (`VisualizerImaging`, `Vis ![image_with_points](scripts/point_source_image_chi_squared/images/visualization/image_with_points.png) +##### point_solver + +![point_solver](scripts/point_source_image_chi_squared/images/visualization/point_solver.png) + +#### point_source_image_chi_squared / visualization / teaching/ + +##### point_solver + +![point_solver](scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.png) + ## point_source_source_chi_squared ### point_source_source_chi_squared / visualization diff --git a/README.md b/README.md index d8e5200..8d76ddc 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ mirroring `autolens_workspace` (a variant gets its own folder): | `imaging` | `visualization.py` | full set: parametric + Delaunay source | | `interferometer` | `visualization.py` | full set: parametric + Delaunay source | | `interferometer_datacube` | `visualization.py` | `dataset.png` + `fit.png` per channel | -| `point_source_image_chi_squared` | `visualization.py` | `dataset.png` + `fit.png` | +| `point_source_image_chi_squared` | `visualization.py` | `dataset.png` + `fit.png` + image-with-points pair + `point_solver.png`; `teaching/point_solver.png` + `.gif` (coarse grid) | | `point_source_source_chi_squared` | `visualization.py` | `dataset.png` + `fit.png` | | `multi_galaxy` | `visualization.py` | `dataset.png` + `fit.png` | | `group` | `visualization.py` | `dataset.png` + `fit.png` | -| `cluster` | `visualization.py` | `dataset.png` + `fit.png` per point-source plane | +| `cluster` | `visualization.py` | `dataset.png` + `fit.png` + `point_solver.png` per point-source plane | | `multi_dataset` | `visualization.py` | `dataset.png` + `fit.png` per band | | `weak` | `visualization.py` | `dataset.png` + `fit.png` | diff --git a/_viz_cli.py b/_viz_cli.py index 1ec67b0..eff6e62 100644 --- a/_viz_cli.py +++ b/_viz_cli.py @@ -220,12 +220,14 @@ def keep_dataset_and_fit( def mirror_to_scratch(domains: list[str] | None = None) -> Path | None: - """Copy freshly rendered PNGs to the opt-in Eyes scratch folder; return its lib folder. + """Copy freshly rendered PNGs (and GIF animations) to the opt-in Eyes scratch folder. + + Returns the scratch lib folder. For on-the-fly figure review before any PR or CI. When ``$PYAUTO_EYES_SCRATCH`` is unset this is a silent no-op returning ``None`` (so CI and other machines are untouched). Otherwise each domain's ``$PYAUTO_EYES_SCRATCH///`` is - deleted and refilled with every PNG under ``scripts//images/`` (subpaths + deleted and refilled with every PNG and GIF under ``scripts//images/`` (subpaths below ``images/`` kept), so no stale figure survives a re-render. ``domains=None`` mirrors every domain that has an ``images/`` folder (``misc`` never does). """ @@ -243,12 +245,12 @@ def mirror_to_scratch(domains: list[str] | None = None) -> Path | None: continue dest = lib_dir / domain shutil.rmtree(dest, ignore_errors=True) - pngs = sorted(images.rglob("*.png")) - for png in pngs: - target = dest / png.relative_to(images) + figures = sorted([*images.rglob("*.png"), *images.rglob("*.gif")]) + for figure in figures: + target = dest / figure.relative_to(images) target.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(png, target) - print(f" [eyes-scratch] {len(pngs)} PNGs -> {dest}") + shutil.copy2(figure, target) + print(f" [eyes-scratch] {len(figures)} figures (PNG/GIF) -> {dest}") return lib_dir diff --git a/config/visualize/plots.yaml b/config/visualize/plots.yaml index 9d8435a..c1318e9 100644 --- a/config/visualize/plots.yaml +++ b/config/visualize/plots.yaml @@ -62,7 +62,8 @@ 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.)? -fit_point_dataset: {} # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). +fit_point_dataset: # Settings for plots of fits to point source datasets (e.g. FitPointDatasetPlotter). + subplot_point_solver: true # PointSolver refinement steps at the best fit (point_solver.png); off by default in the library. 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/gallery/viz_manifest.yaml b/gallery/viz_manifest.yaml index aa25592..6d213a7 100644 --- a/gallery/viz_manifest.yaml +++ b/gallery/viz_manifest.yaml @@ -8,7 +8,7 @@ rendered_with: autogalaxy: 2026.8.17.1 autoarray: 2026.8.17.1 autofit: 2026.8.17.1 -figure_count: 68 +figure_count: 72 figures: - file: scripts/cluster/images/visualization/point_0/dataset.png producer: scripts/cluster/visualization.py @@ -20,8 +20,14 @@ figures: producer: scripts/cluster/visualization.py domain: cluster source: point_0 - bytes: 68264 - sha256: c330570f6373ef9055c8d4dd5b7be3fa8b3f096cdb0117d2aec4b3d5987e43fa + bytes: 68170 + sha256: a77734d4569f512ec14657e5a6fb2c1308a1e49c7b2153e00bf83f2c83f9e860 +- file: scripts/cluster/images/visualization/point_0/point_solver.png + producer: scripts/cluster/visualization.py + domain: cluster + source: point_0 + bytes: 646052 + sha256: b0f21b073ab19c91c629504940ecd8fb631dda85107edabfd6345c60e7fbc1b4 - file: scripts/cluster/images/visualization/point_1/dataset.png producer: scripts/cluster/visualization.py domain: cluster @@ -32,8 +38,14 @@ figures: producer: scripts/cluster/visualization.py domain: cluster source: point_1 - bytes: 65979 - sha256: aaea68e094f6ff3d142c9a24cb92e750781b43b7768c82f3ff527356150c0f35 + bytes: 65871 + sha256: 813846220f1a47d03f88084b92cb93507a91b6410ae72dd0138cf13eda54c667 +- file: scripts/cluster/images/visualization/point_1/point_solver.png + producer: scripts/cluster/visualization.py + domain: cluster + source: point_1 + bytes: 631672 + sha256: 357d5dff2da880f3bd0908dba09624d2ee66d4211548f27dd79807452b153299 - file: scripts/group/images/visualization/dataset.png producer: scripts/group/visualization.py domain: group @@ -368,8 +380,8 @@ figures: producer: scripts/point_source_image_chi_squared/visualization.py domain: point_source_image_chi_squared source: '' - bytes: 59950 - sha256: dcf7b7a60281c49244264678c6c15ab0a5636be9c66fd4e555bba2b9fdecab40 + bytes: 59992 + sha256: 75919308cfe263625a6d71450f2eb83acc2b13a0b08f1edc3ee97720c4f5f75a - file: scripts/point_source_image_chi_squared/images/visualization/fit_image_with_points.png producer: scripts/point_source_image_chi_squared/visualization.py domain: point_source_image_chi_squared @@ -382,6 +394,18 @@ figures: source: '' bytes: 66518 sha256: 2653419882e5f55d2afe24b067e8d5c9865081aee51517dc05520a12d1787468 +- file: scripts/point_source_image_chi_squared/images/visualization/point_solver.png + producer: scripts/point_source_image_chi_squared/visualization.py + domain: point_source_image_chi_squared + source: '' + bytes: 809716 + sha256: 92252350649f9d7a8698a775afcbf02cc0bc0291f842b5365f7ecea0fb4ec25a +- file: scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.png + producer: scripts/point_source_image_chi_squared/visualization.py + domain: point_source_image_chi_squared + source: teaching + bytes: 495158 + sha256: 84f0312c1415db43b8d8b901a2977a24d706697df70523aef122744a7e00a451 - file: scripts/point_source_source_chi_squared/images/visualization/dataset.png producer: scripts/point_source_source_chi_squared/visualization.py domain: point_source_source_chi_squared @@ -392,8 +416,8 @@ figures: producer: scripts/point_source_source_chi_squared/visualization.py domain: point_source_source_chi_squared source: '' - bytes: 60925 - sha256: b9af86e928d6a1e395cc81aca38cffc7b94bb5b3af82484180d37f989b245472 + bytes: 60766 + sha256: 386aaf5cf28aa8b69b71a53962a3edde8d5673769ab7eb7f7ae81f1fe2fe9399 - file: scripts/point_source_source_chi_squared/images/visualization/fit_image_with_points.png producer: scripts/point_source_source_chi_squared/visualization.py domain: point_source_source_chi_squared diff --git a/scripts/cluster/images/visualization/point_0/fit.png b/scripts/cluster/images/visualization/point_0/fit.png index f0ad0cf..55d542e 100644 Binary files a/scripts/cluster/images/visualization/point_0/fit.png and b/scripts/cluster/images/visualization/point_0/fit.png differ diff --git a/scripts/cluster/images/visualization/point_0/point_solver.png b/scripts/cluster/images/visualization/point_0/point_solver.png new file mode 100644 index 0000000..74fbb8e Binary files /dev/null and b/scripts/cluster/images/visualization/point_0/point_solver.png differ diff --git a/scripts/cluster/images/visualization/point_1/fit.png b/scripts/cluster/images/visualization/point_1/fit.png index a276d4b..682e25d 100644 Binary files a/scripts/cluster/images/visualization/point_1/fit.png and b/scripts/cluster/images/visualization/point_1/fit.png differ diff --git a/scripts/cluster/images/visualization/point_1/point_solver.png b/scripts/cluster/images/visualization/point_1/point_solver.png new file mode 100644 index 0000000..fd996c2 Binary files /dev/null and b/scripts/cluster/images/visualization/point_1/point_solver.png differ diff --git a/scripts/cluster/visualization.py b/scripts/cluster/visualization.py index ce3c647..4c9860a 100644 --- a/scripts/cluster/visualization.py +++ b/scripts/cluster/visualization.py @@ -2,7 +2,7 @@ Visualization: Cluster ====================== -Renders the two figures ``VisualizerPoint`` writes for each point-source dataset of a +Renders the figures ``VisualizerPoint`` writes for each point-source dataset of a multi-plane cluster fit (two point sources at z=1.0 and z=2.0 behind a cluster lens), so the latest version of each lives in git (``GALLERY.md``). A cluster model-fit has one ``AnalysisPoint`` per point-source dataset (combined in a factor graph), each visualized @@ -13,14 +13,18 @@ scripts/cluster/images/visualization/ point_0/dataset.png, point_0/fit.png <- PlotterPoint.dataset_point, PlotterPoint.fit_point - point_1/dataset.png, point_1/fit.png + point_0/point_solver.png <- PlotterPoint.point_solver (the model's real + multi-plane PointSolver refining onto this + source's images, steps 0, 1, 2 and final) + point_1/dataset.png, point_1/fit.png, point_1/point_solver.png **Plotter, not Visualizer.** ``VisualizerPoint.visualize`` also renders the tracer and galaxy -figures this repo does not track, so this producer calls the same two ``PlotterPoint`` methods the +figures this repo does not track, so this producer calls the same ``PlotterPoint`` methods the visualizer calls (``dataset_point(solver=...)`` in ``visualize_before_fit``; -``fit_point(quick_update=True)`` in ``visualize``), writing the figures the visualizer would. The -"Fit Positions" panel draws the tracer's critical curves, which ``fit_point`` computes on a 100x100 -grid covering the solver grid: for this 15-galaxy multi-plane lens that costs ~2 min per dataset. +``fit_point(quick_update=True)`` and ``point_solver`` in ``visualize``), writing the figures the +visualizer would. The "Fit Positions" and point-solver panels draw the tracer's critical curves, +computed once per dataset (as the visualizer does) on a 100x100 grid covering the solver grid: for +this 15-galaxy multi-plane lens that costs ~2 min per dataset. The point plotter writes ``dataset_point.png`` (renamed to ``dataset.png``); every other file the visualizer writes is deleted. @@ -62,6 +66,8 @@ def _repo_root() -> Path: import autofit as af import autolens as al +from autolens.imaging.plot.fit_imaging_plots import _compute_critical_curve_lines +from autolens.lens.plot.critical_curves import critical_curve_grid_from from autolens.point.model.plotter import PlotterPoint from _viz_cli import ensure_dataset, keep_dataset_and_fit @@ -121,11 +127,22 @@ def _repo_root() -> Path: t0 = time.perf_counter() plotter = PlotterPoint(image_path=paths.image_path, title_prefix=analysis.title_prefix) plotter.dataset_point(dataset=analysis.dataset, solver=analysis.solver) + fit = analysis.fit_for_visualization(instance=instance) + ip_lines, ip_colors, sp_lines, sp_colors = _compute_critical_curve_lines( + fit.tracer, critical_curve_grid_from(dataset=fit.dataset, solver=solver) + ) plotter.fit_point( - fit=analysis.fit_for_visualization(instance=instance), + fit=fit, quick_update=True, + image_plane_lines=ip_lines if ip_lines is not None else [], + image_plane_line_colors=ip_colors if ip_lines is not None else [], + source_plane_lines=sp_lines, + source_plane_line_colors=sp_colors, + ) + plotter.point_solver(fit=fit, image_plane_lines=ip_lines if ip_lines is not None else []) + keep_dataset_and_fit( + point_path, dataset_png="dataset_point.png", extra_pngs=("point_solver.png",) ) - keep_dataset_and_fit(point_path, dataset_png="dataset_point.png") print(f"visualize [{dataset.name}]: {time.perf_counter() - t0:.1f}s") n_png = len(list(image_path.rglob("*.png"))) diff --git a/scripts/point_source_image_chi_squared/images/visualization/fit.png b/scripts/point_source_image_chi_squared/images/visualization/fit.png index 4b7c0d4..f562144 100644 Binary files a/scripts/point_source_image_chi_squared/images/visualization/fit.png and b/scripts/point_source_image_chi_squared/images/visualization/fit.png differ diff --git a/scripts/point_source_image_chi_squared/images/visualization/point_solver.png b/scripts/point_source_image_chi_squared/images/visualization/point_solver.png new file mode 100644 index 0000000..4a07487 Binary files /dev/null and b/scripts/point_source_image_chi_squared/images/visualization/point_solver.png differ diff --git a/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.gif b/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.gif new file mode 100644 index 0000000..8132c02 Binary files /dev/null and b/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.gif differ diff --git a/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.png b/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.png new file mode 100644 index 0000000..f9c8b42 Binary files /dev/null and b/scripts/point_source_image_chi_squared/images/visualization/teaching/point_solver.png differ diff --git a/scripts/point_source_image_chi_squared/visualization.py b/scripts/point_source_image_chi_squared/visualization.py index 496c31f..2db2b00 100644 --- a/scripts/point_source_image_chi_squared/visualization.py +++ b/scripts/point_source_image_chi_squared/visualization.py @@ -2,24 +2,32 @@ Visualization: Point Source (Image-Plane Chi-Squared) ===================================================== -Renders the two figures ``VisualizerPoint`` writes for a point-source fit that uses the +Renders the figures ``VisualizerPoint`` writes for a point-source fit that uses the image-plane chi-squared (``FitPositionsImagePair``: the ``PointSolver`` finds the model's multiple images, which are paired one-to-one with the observed positions), so the latest -version of each lives in git (``GALLERY.md``). +version of each lives in git (``GALLERY.md``), plus a coarse-grid teaching version of the +``PointSolver`` refinement figure and its animation. Output tree (wiped at the start of every run):: scripts/point_source_image_chi_squared/images/visualization/ dataset.png <- visualize_before_fit (the plotter writes dataset_point.png; renamed) - fit.png <- visualize(quick_update=True) + fit.png <- visualize image_with_points.png <- visualize_before_fit: the observed positions over ``data.fits`` (dataset only, no model) - fit_image_with_points.png <- visualize(quick_update=True): the observed - positions, the model's positions and critical curves over - ``data.fits`` - -Every other file the visualizer writes is deleted: this domain tracks only these four -figures. + fit_image_with_points.png <- visualize: the observed positions, the model's + positions and critical curves over ``data.fits`` + point_solver.png <- visualize (``fit_point_dataset: subplot_point_solver``, + on in this repo's plots.yaml, off in the library): the + model's real ``PointSolver`` refining onto the images, + steps 0, 1, 2 and final + teaching/point_solver.png <- ``aplt.plot_point_solver`` with a coarse teaching grid + (11x11 at 0.5", precision 0.01"), so the triangles stay + visible at every step + teaching/point_solver.gif <- the same, ``animate=True``: one step per frame, looping + (not in the PNG manifest; tracked beside the still) + +Every other file the visualizer writes is deleted: this domain tracks only these figures. Dataset: ``dataset/point_source/simple`` (four image-plane positions, 5 mas noise; shared by both point-source chi-squared producers), from @@ -58,6 +66,7 @@ def _repo_root() -> Path: import autofit as af import autolens as al +import autolens.plot as aplt from autolens.point.model.visualizer import VisualizerPoint from _viz_cli import ensure_dataset, keep_dataset_and_fit @@ -129,14 +138,44 @@ def _repo_root() -> Path: paths=paths, instance=model.instance_from_prior_medians(), during_analysis=False, - quick_update=True, + quick_update=False, ) keep_dataset_and_fit( image_path, dataset_png="dataset_point.png", - extra_pngs=("image_with_points.png", "fit_image_with_points.png"), + extra_pngs=( + "image_with_points.png", + "fit_image_with_points.png", + "point_solver.png", + ), ) print(f"visualize: {time.perf_counter() - t0:.1f}s") + +""" +__Teaching Figure__ + +The model's real solver grid (0.05" triangles) makes every step after step 0 sub-pixel, so a +coarse grid (11x11 at 0.5", precision 0.01") shows the triangles shrinking onto the images. +The GIF plays the same panels one step per frame. +""" +t0 = time.perf_counter() +teaching_solver = al.PointSolver.for_grid( + grid=al.Grid2D.uniform(shape_native=(11, 11), pixel_scales=0.5), + pixel_scale_precision=0.01, + magnification_threshold=0.1, +) +tracer = al.Tracer(galaxies=[tm.lens(), tm.source()]) +for animate in (False, True): + aplt.plot_point_solver( + solver=teaching_solver, + tracer=tracer, + source_plane_coordinate=tm.SOURCE_CENTRE, + positions=dataset.positions, + output_path=image_path / "teaching", + output_format="png", + animate=animate, + ) +print(f"teaching point_solver: {time.perf_counter() - t0:.1f}s") print( f"Wrote {sorted(p.name for p in image_path.iterdir())} under {image_path.relative_to(REPO_ROOT)}" ) diff --git a/scripts/point_source_source_chi_squared/images/visualization/fit.png b/scripts/point_source_source_chi_squared/images/visualization/fit.png index ece87a7..3a9e03b 100644 Binary files a/scripts/point_source_source_chi_squared/images/visualization/fit.png and b/scripts/point_source_source_chi_squared/images/visualization/fit.png differ