Repository navigation
fix: measure PointSolver magnification at the plane being solved for (#480) - #712
Merged
Merged
Conversation
…480) PointSolver.solve returned no images for a source on an intermediate plane of a multi-plane tracer. The triangle search honoured plane_redshift and traced to the requested plane, but _filter_low_magnification did not: it built LensCalc.from_mass_obj(tracer), whose deflection callable is the whole tracer, i.e. the last plane. Candidate images of an intermediate-plane source are heavily de-magnified measured that way (~1e-3-1e-5 in the issue's configuration), so magnification_threshold discarded every one and the solve returned an empty grid. point_solver.py had plane_redshift in scope and did not forward it; it now does, and the filter measures via LensCalc.from_tracer(..., plane_j=plane_index) -- the same callable the search uses. This is the pattern the fit side already uses: AbstractFitPoint's magnifications_at_positions and solved.py's _lens_calc_for both measure with from_tracer(..., plane_j=...) at the source's own plane. The two halves of one likelihood evaluation therefore disagreed, since positions/image/abstract.py already passed plane_redshift into solver.solve. The argument was plumbed end-to-end; only the last consumer ignored it. No-op for the default case, verified numerically rather than argued: Tracer's deflections_yx_2d_from dispatches to deflections_between_planes_from with plane_i=0, plane_j=-1 whenever the tracer has more than one plane, so from_mass_obj(tracer) and from_tracer(tracer, plane_j=-1) wrap the same callable. Magnifications compare bit-identical (max|diff| = 0.0) for multi-plane and single-plane tracers alike. Also resolves the plane index once, shared by the search and the filter. The bug was those two disagreeing about the plane, so a second copy of the resolution invites the same drift back. That resolution now raises a ValueError naming the tracer's actual redshifts when plane_redshift matches no plane, where plane_index_via_redshift_from's None previously reached traced_grids_list[None] and raised a bare TypeError. Adds test_solver_multi_plane.py. Nothing passed plane_redshift to the solver in any test before this, which is why the bug shipped -- the multi-plane solve path had no coverage. Four of the seven new tests fail against the unfixed solver; the other three pin the behaviour that must not change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188mKnNzXptgLi1CUZ4H7NZ
#480) Two checks on the #480 fix, answering different questions. Consistency. The solver and the fit reach the plane by different keys: the fit from the profile name via extract_plane_index_of_profile, the solver from a redshift via plane_index_via_redshift_from, with AbstractFitPoint.plane_redshift carrying the fit's answer across as planes[plane_index].redshift. That is a name -> index -> redshift -> index round trip spanning two modules, so it is pinned rather than assumed, through the solver's own _plane_index helper rather than a re-derivation of it. Both land on the same plane, and magnifications_at_positions then matches the solver's measurement to 1e-12. Accuracy. Agreement between the two is not evidence either is right, so the magnification is also cross-checked against one derived from ray-tracing alone: central differences of theta -> beta_j via traced_grid_2d_list_from, with mu = 1/det(J), sharing no code with the Hessian path. The two agree to ~1e-8. Both were confirmed against exact JAX autodiff (float64) during development, which agrees with the ray-traced Jacobian to 1.8e-09 at the solved plane. Five of the nine tests in this module now fail against the unfixed solver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188mKnNzXptgLi1CUZ4H7NZ
Merged
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #480.
PointSolver.solvereturned no images for a source on an intermediate plane of a multi-plane tracer, making any point-source modeling beyond "all sources at the same redshift" impossible.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. Candidate images of an intermediate-plane source are heavily de-magnified measured that way (~1e-3–1e-5 in the issue's configuration), somagnification_thresholddiscarded every one and the solve returned an empty grid.point_solver.pyhadplane_redshiftin scope and did not forward it.This is the pattern the fit side already uses.
AbstractFitPoint.magnifications_at_positionsandsolved.py's_lens_calc_forboth measure withLensCalc.from_tracer(..., plane_j=...)at the source's own plane — andpositions/image/abstract.pyalready passedplane_redshiftintosolver.solve. So 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 right. The argument was plumbed end-to-end through the callers; only the last consumer ignored it.No-op for the default case, verified numerically rather than argued.
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 alike.API Changes
No public API removed or renamed. Behaviour changes:
PointSolver.solve(..., plane_redshift=z)now returns the correct images for an intermediate plane, where it previously returned an empty grid. Results forplane_redshift=Noneand for the last plane are unchanged.plane_redshiftmatching no plane now raisesValueErrornaming the tracer's actual redshifts. It previously reachedtraced_grids_list[None]and raised a bareTypeError.The plane-index resolution is now shared by the search and the filter (
ShapeSolver._plane_index) — the bug was those two disagreeing about the plane, so a second copy of the resolution invites the same drift back.Test Plan
test_autolens/point/triangles/test_solver_multi_plane.py(9 tests). Nothing passedplane_redshiftto the solver in any test before this — the multi-plane solve path had no coverage, which is why the bug shipped.test_autolens: 544 passed, 1 skipped (coolestnot installed).Magnification accuracy was cross-checked three ways at the solved plane, 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.99681364Full API Changes (for automation & release notes)
Added
ShapeSolver._plane_index(tracer, plane_redshift=None) -> int— resolves the plane being solved for, shared by_plane_gridand_filter_low_magnification. RaisesValueErrorwhen no plane matches.Changed
ShapeSolver._filter_low_magnification(tracer, points, xp, plane_redshift=None)— gainedplane_redshift; now constructs viaag.LensCalc.from_tracer(..., plane_j=plane_index)instead offrom_mass_obj(tracer).PointSolver._solve_array— forwardsplane_redshiftto_filter_low_magnification.Migration
None. Both changed methods are private, and the public
solvesignature is unchanged.Follow-ups filed (not in this PR)
Cross-checking this fix surfaced three separate items, filed as PyAutoMind prompts rather than widened into here:
LensCalc's NumPy Richardson Hessian uses a hardcoded 0.01″ step. 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. Pre-existing and independent of bug: PointSolver magnification filter uses last-plane magnification instead of plane_redshift #480, but it is the default path.tracer_util.time_delays_fromraises on >2 planes, andLensCalc.from_tracerbinds the deflection callable to the plane while leavingpotential_2d_fromas the tracer's all-planes sum.Generated by the PyAutoLabs agent workflow.
Generated by Claude Code