Repository navigation
Description
Summary
PointSolver._filter_low_magnification discards every candidate image position when solving for an intermediate source plane in a multi-plane tracer. This makes any point-source modeling that involves a source whose plane is not the last plane impossible: the simulator returns 0 positions, and AnalysisPoint likelihood evaluations fail.
Repro
import autolens as al
lens = al.Galaxy(
redshift=0.5,
mass=al.mp.Isothermal(
centre=(0.0, 0.0),
einstein_radius=1.6,
ell_comps=al.convert.ell_comps_from(axis_ratio=0.9, angle=45.0),
),
)
source_0 = al.Galaxy(
redshift=1.0,
mass=al.mp.Isothermal(
centre=(0.02, 0.03),
einstein_radius=0.2,
ell_comps=al.convert.ell_comps_from(axis_ratio=0.8, angle=60.0),
),
point_0=al.ps.Point(centre=(0.02, 0.03)),
)
source_1 = al.Galaxy(redshift=2.0, point_1=al.ps.Point(centre=(0.0, 0.0)))
tracer = al.Tracer(galaxies=[lens, source_0, source_1])
grid = al.Grid2D.uniform(shape_native=(200, 200), pixel_scales=0.05)
solver = al.PointSolver.for_grid(grid=grid, pixel_scale_precision=0.001, magnification_threshold=0.1)
print(solver.solve(tracer=tracer, source_plane_coordinate=(0.02, 0.03), plane_redshift=1.0).shape)
# (0, 2) ← expected (4, 2)
print(solver.solve(tracer=tracer, source_plane_coordinate=(0.0, 0.0), plane_redshift=2.0).shape)
# (4, 2) ← OK, this is the last planeSetting magnification_threshold=0.0 recovers 5 candidate positions for source_0, confirming the triangle search itself is fine — only the magnification post-filter rejects them.
Root cause
autolens/point/solver/shape_solver.py::_filter_low_magnification:
magnifications = ag.LensCalc.from_mass_obj(
tracer
).magnification_2d_via_hessian_from(
grid=aa.Grid2DIrregular(points).array, xp=xp
)The hessian-based magnification implicitly uses the tracer's last plane (z=2.0 in the repro) as the source plane. Inspected at the candidate positions, magnifications come back as ~1e-3–1e-5 (i.e. de-magnified for a z=2.0 source at those locations). The 0.1 threshold then discards everything.
When solving for an intermediate plane, the magnification used by the filter should match the requested plane_redshift, not the tracer's final plane.
Suggested fix
Plumb plane_redshift through from PointSolver.solve → _filter_low_magnification and use it to compute the hessian magnification at the correct plane (e.g. by ray-tracing using plane_index_via_redshift_from, or by building a sub-tracer that ends at plane_redshift).
For the single-plane and last-plane cases the current behaviour is correct, so the fix should be a no-op when plane_redshift is None or equals the last plane's redshift.
Impact
- Simulator scripts can't generate datasets for intermediate-plane sources in a multi-plane lens (e.g. the original
point_source/features/double_einstein_cross/simulator.py). AnalysisPointlog-likelihood evaluation fails when a fittedPointis at any plane other than the last one — everysolver.solve()call returns 0 positions andFitPositions*cannot pair model images to data.- This blocks any factor-graph / multi-plane point-source modeling beyond the simplest "all sources at the same redshift" case.
Workaround used downstream
autolens_workspace issue PyAutoLabs/autolens_workspace#97 needed a multi-source point-source modeling tutorial. To unblock it the workspace example was simplified to drop the intermediate source's own mass profile (so the only deflector is the foreground lens). Once this PyAutoLens bug is fixed, the workspace simulator and model should be reverted to a richer multi-plane configuration where the intermediate source itself acts as a deflector for the further source — see the follow-up prompt in PyAutoPrompt/autolens/restore_multiple_sources_lensing_of_lens.md.
Activity
test: pin solver/fit plane agreement and cross-check the magnification (
Merge pull request #712 from PyAutoLabs/claude/point-source-json-regi…
Jammy2211 commentedon Aug 27, 2026
Shipped
Fixed in #712, merged as
c1bba66.Root cause. The triangle search honoured
plane_redshiftand traced to the requested plane, but_filter_low_magnificationdid not — it builtLensCalc.from_mass_obj(tracer), whose deflection callable is the whole tracer, i.e. the last plane.point_solver.pyhadplane_redshiftin scope and simply did not forward it.Your suggested fix was right, and cheaper than it looked:
LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=...)already existed in PyAutoGalaxy and already bindsdeflections_between_planes_fromfor a given plane. It is also the pattern the fit side was already using —AbstractFitPoint.magnifications_at_positionsandpoint/fit/solved.py::_lens_calc_forboth measure at the source's own plane. Sincepoint/fit/positions/image/abstract.pyalready passedplane_redshiftintosolver.solve, the two halves of one likelihood evaluation disagreed:model_datameasured at the last plane and returned nothing, whilemagnifications_at_positionsmeasured at the source's plane and was correct. The argument was plumbed end-to-end; only the last consumer ignored it.The no-op requirement holds exactly, as you asked.
Tracer.deflections_yx_2d_fromdispatches todeflections_between_planes_fromwithplane_i=0, plane_j=-1whenever the tracer has more than one plane, sofrom_mass_obj(tracer)andfrom_tracer(tracer, plane_j=-1)wrap the same callable. Magnifications compare bit-identical (max|diff| = 0.0) for multi-plane and single-plane tracers.Verification. Your repro now returns
(4, 2)at z=1.0 where it returned(0, 2). Magnification at the solved plane was cross-checked three ways, agreeing to 1.7e-08:-27.89795126 -9.05806212 23.12205173 15.99681364-27.89795173 -9.05806221 23.12205188 15.99681364LensCalc)-27.89795168 -9.05806221 23.12205188 15.99681364New
test_autolens/point/triangles/test_solver_multi_plane.py— 9 tests, 5 of which fail against the unfixed solver. Nothing passedplane_redshiftto the solver in any test before this, which is why the bug survived. Full suite 544 passed, 1 skipped; CI green on all four legs (3.12, 3.13, nojax, docs).Two things folded in. The plane-index resolution is now shared by the search and the filter — the bug was those two disagreeing, so a second copy invites the drift back. And an unmatched
plane_redshiftnow raisesValueErrornaming the tracer's actual redshifts, where it previously reachedtraced_grids_list[None]and raised a bareTypeError.Follow-ups filed, deliberately not widened into this PR:
LensCalc's NumPy Richardson Hessian uses a hardcoded 0.01″ step, and at the last plane of this same tracer it disagrees with both JAX autodiff and the ray-traced Jacobian by 122%, with the wrong sign on three of four points. It is the default path andmagnifications_at_positionsuses it.tracer_util.time_delays_fromraises on >2 planes (which this fix now makes reachable), andLensCalc.from_tracerbinds the deflection callable to the plane while leavingpotential_2d_fromas the tracer's all-planes sum.Still open downstream, as your issue notes: reverting the
autolens_workspacemulti-source example to a richer multi-plane configuration, and removing the twomultiple_sourcesentries fromconfig/build/no_run.yaml.Generated by Claude Code
fix: measure PointSolver magnification at the plane being solved for (P…
test: cross-validate multi-plane ray tracing against independent orac…