Repository navigation
audit: Delaunay pixel areas and magnification paths (euclid-dr1-prep phase 8) #522
Description
Overview
Phase 8 of the euclid-dr1-prep epic (was 6c; renumbered in the 2026-09-01 Cortex split). The Delaunay source model's magnification has never been validated and the epic's owner suspects the pixel areas. This is a source-code audit of every Delaunay area/magnification path in PyAutoArray, with known-answer tests landed regardless of outcome. It is the software half of the question; the empirical half is Cortex phase 7 (PyAutoCortex/phases/euclid/magnification_robustness.md), which this audit must brief. Any real defect is filed as a separate bug prompt — this task does not grow into the fix.
Plan
- Map every Delaunay area/magnification code path — Voronoi cell areas behind
areas_for_magnification, barycentric dual areas behind the split-point weights, thetripcolorplotting path, the workspacesource_science.pyconsumer, and the Euclid pipeline'smagnificationlatent — and state what each computes. - Prove or drop each suspicion with known-answer tests (analytic lattices, convex-hull totals, exact integrals of a linear field), not resemblance.
- Land the missing direct tests for
areas_for_magnificationandbarycentric_dual_area_from, and correct theareas_for_magnificationdocstring to what is actually zeroed. - Check whether the Euclid pipeline's
magnificationlatent survives a Delaunayvis_pix/full_modelstage at all. - Write the verdict here and in the epic ledger, with an explicit statement for Cortex phase 7: are Delaunay magnification numbers trustworthy, and which area must they use?
- Any proven defect → separate
draft/bug/autoarray/prompt with the reproduced failing case.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary — tests + docstring)
- PyAutoLens (read-only:
autolens/analysis/latent.py::magnificationreachability check; no edits unless a defect is found, then a separate prompt)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoArray | main | clean |
| ./PyAutoLens | main | clean |
Suggested branch: feature/delaunay-area-magnification-audit
What the pre-plan survey established
- Two different "areas" exist for a Delaunay mesh on two different paths:
MeshGeometryDelaunay.areas_for_magnification(autoarray/inversion/mesh/mesh_geometry/delaunay.py:195) =voronoi_areas_numpy(shoelace over scipy Voronoi cells). Only unbounded cells are marked-1and zeroed; bounded-but-huge boundary cells are kept (the existingtest__voronoi_areas_via_delaunay_fromshows index 3 = 29.8 kept, index 4 = −1). The docstring says boundary pixels are zeroed — it overstates.barycentric_dual_area_from(autoarray/inversion/mesh/interpolator/delaunay.py:342, plus an in-graph JAX scatter-add copy at ~:309) = Σ(triangle area / 3) per vertex; used only to weight split-regularisation points viaareas_factor * sqrt(areas). Not used for flux or magnification.
- Plotting (
autoarray/plot/inversion.py::_plot_delaunay) usestripcolorGouraud — linear interpolation over triangles, whose integral is the barycentric dual area, not the Voronoi cell. Picture and number come from different geometry. areas_for_magnificationhas zero library callers; it serves only the four pixelizedsource_science.pyworkspace scripts (Σ reconstruction × mesh_areasas source-plane flux). Those scripts are what Cortex phase 7's Delaunay leg would use.- The Euclid pipeline's magnification latent (
autolens/analysis/latent.py::magnification) is a flux ratio viagalaxies[-1].image_2d_from(...)and never touchesareas_for_magnification. Whether it is finite for aPixelizationsource galaxy is unverified. sqrtis not on the magnification path; the known NaN-gradient hazard is confined to the interpolator's split-point weights.- The cluster-epic prompt
draft/test/workspaces/mesh_magnification_correctness.mdowns the library-level magnification API, non-Delaunay meshes and the simulate-and-recover campaign. This task stays Delaunay-only.
Implementation Steps
test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py— add:test__areas_for_magnification__uniform_lattice: n×n unit lattice → every interior Voronoi cell area == 1.0; every hull point unbounded → 0.0; Σ == (n−2)². Pins the real semantics.test__areas_for_magnification__bounded_boundary_cells_are_kept: the existing 6-point configuration → index 3 (≈29.8) is kept, only index 4 zeroed — documents the bias candidate explicitly.test__areas_for_magnification__repeat_calls_agree: two calls agree; the-1sentinel is not written back into any cached state.
test_autoarray/inversion/mesh/interpolator/test_delaunay.py(create if absent, mirroring the existing interpolator test layout) — add:test__barycentric_dual_area__single_triangle: each vertex == A/3.test__barycentric_dual_area__sums_to_convex_hull_area: random points → Σ dual ==scipy.spatial.ConvexHull(points).volume; assert Σareas_for_magnificationdiffers for the same points (records the divergence).test__linear_field_integral__dual_areas_exact: f = a + b·y + c·x at the vertices; exact integral over the hull equals Σ f_i·dual_i to rounding; Σ f_i·voronoi_i differs on a non-uniform mesh.- NumPy vs JAX parity:
barycentric_dual_area_from(xp=np)equals the in-graph scatter-add on the same padded simplices.
autoarray/inversion/mesh/mesh_geometry/delaunay.py— rewrite theareas_for_magnificationdocstring to the proven semantics (unbounded cells → 0; bounded boundary cells kept; these are Voronoi cells, not the interpolant's dual areas). No behavioural change.- Audit evidence (scratch script, numbers reported here, not shipped): a Gaussian source strictly inside the hull of a Delaunay mesh; compare Σ reconstruction·voronoi areas, Σ reconstruction·dual areas, and the mapped-reconstruction image-plane flux against the analytic source flux. Report which denominator recovers μ and the sign/size of the boundary effect.
- Euclid reachability (read-only): look for
magnificationin the latent outputs of thevis_pix/full_modelstages undereuclid_strong_lens_modeling_pipeline/output/ral_*; if absent (NaN latents are dropped fromlatent_summary.json), confirm by evaluating the latent on a stored Delaunay fit in test mode. Outcome is a finding. - If a defect is proven (expected candidate: the magnification denominator should integrate the interpolant with barycentric dual areas, or bounded boundary cells bias the Voronoi sum), file
draft/bug/autoarray/<name>.mdvia/intakewith the reproduced case andEpic: euclid-dr1-prep; cross-link from the ledger and here. Do not fix in this PR. - Write the per-path audit table (path, what it computes, verdict, evidence) and the Cortex-phase-7 statement into this issue and the epic ledger.
Key Files
autoarray/inversion/mesh/mesh_geometry/delaunay.py—voronoi_areas_numpy,voronoi_areas,areas_for_magnificationautoarray/inversion/mesh/interpolator/delaunay.py—barycentric_dual_area_from, in-graph dual areas, split-point weightsautoarray/plot/inversion.py—_plot_delaunaytest_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py— existingtest__voronoi_areas_via_delaunay_fromautolens_workspace/scripts/imaging/features/pixelization/source_science.py— the only consumer ofareas_for_magnificationPyAutoLens/autolens/analysis/latent.py—magnification,total_source_flux
Verification
pytest test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py test_autoarray/inversion/mesh -qgreen in the worktree; fullpytest test_autoarray -qbefore the PR.- Every Delaunay area path either matched to an analytic answer in a landed test or flagged with a reproduced failing case here.
Original Prompt
Click to expand starting prompt
Audit the Delaunay pixel-area and magnification source code for correctness bugs
Type: bug
Target: autoarray
Repos:
- PyAutoArray
- PyAutoLens
Themes: - pixelization
- euclid
Difficulty: small-medium
Autonomy: supervised
Priority: normal
Status: formalised
Consequence: judge
Review-minutes: 25
Unattended: ready
Epic: euclid-dr1-prep
Phase: 8
Parent: draft/feature/euclid/euclid_dr1_prep_epic.md
Filed: 2026-08-28
Phase 8 of the Euclid DR1 preparation epic (was 6c; renumbered 2026-09-01 in the Cortex split,
where the old 6b became PyAutoCortex phases/euclid/magnification_robustness). Can run
alongside the magnification-robustness phase and does not gate on it — that phase is the
empirical half (does μ come out right on data?), this one is the
source-code half (does the code look right?). Either can find the defect first.
User request (verbatim, the follow-up clause of 6b):
"""
We have also never really validated or tested the magnifcitations using
the Delaunay source model, and this could even have bugs (E.g due to pixel areas not being quite right). So, for this
Euclid epic do all of the magnification comparisons you can on the 10 lenses in this spirit, but also have a follow up issue
which checks if the soruce code itself looks like it might have a bug or issue with the Delaunay area and thus magnification calculations.
"""
Where to look
PyAutoArraymesh geometry: the Delaunayareas_for_magnificationimplementation
(voronoi_areas, with boundary cells zeroed via a-1sentinel) and its
rectangular-adaptive sibling. The cluster epic's audit
(draft/test/workspaces/mesh_magnification_correctness.md) already established that
areas_for_magnificationexists for only two mesh geometries, has zero library
callers (it serves only workspace scripts), and has no direct test — only its
delegates are tested. That is exactly the shape a latent bug hides in.- The boundary-cell semantics. Zeroing boundary cells changes the summed source-plane
area and therefore μ directly. Is the zeroing correct, or does it bias μ high? PyAutoArray/autoarray/plot/inversion.py::_plot_delaunayand the Delaunay mapper —
check whether the areas used for plotting and the areas used for magnification
come from the same computation. Divergence there is a classic source of "the picture
looks right but the number is wrong".- The interaction with
sqrton dual areas — there is a known NaN hazard in the Delaunay
dual-area gradient path; check whether the same expression is on the magnification
path.
Method
This is an audit, not a rewrite. Read the code, then prove each suspicion or drop it:
- Construct known-answer configurations (a uniform triangulation of known total area,
a mesh with an analytically computable source-plane area) and check the code returns
the analytic value. A resemblance verdict is not verification. - Where a suspected bug is found, reproduce it in a minimal test before proposing a
fix, and check whether it is reachable from the paths Euclid actually uses. - Do not exempt a case because it "looks intentional" — if boundary zeroing is
deliberate, find the commit or comment that says so.
Deliverables
- A written audit: each area/magnification code path, what it computes, and whether it
is correct. - Known-answer tests for Delaunay
areas_for_magnification(the missing direct test),
landed regardless of whether a bug is found — the absence of a test is itself the
defect the cluster-epic audit flagged. - If a real defect is found: file a separate bug prompt for the fix rather than
growing this one, and tell phase 6b immediately so its Delaunay leg is interpreted
correctly.
Acceptance / gate
- Every Delaunay area/magnification path either verified against an analytic answer or
flagged with a reproduced failing case. - Direct tests exist where there were none.
- A clear statement to 6b: are its Delaunay magnification numbers trustworthy?
Activity
Jammy2211 commentedon Sep 4, 2026
Audit deliverable (phase 8)
PR: #523 (tests + docstring only, no behavioural change)
Reproduction scripts are in the session scratchpad (
part1_flux_integral.py…part3b_fit_latent.py); they were not committed.Phase 8 audit — Delaunay pixel areas and the magnification calculation
Read-only source audit + numerical experiments. No repository was modified.
All canonical checkouts on
main(PyAutoArray, PyAutoLens, PyAutoGalaxy,PyAutoFit, autolens_workspace, euclid_strong_lens_modeling_pipeline).
Everything below is reproducible from the scripts in this directory
(
part1_flux_integral.py,part1b_irregular_hull.py,part1c_pedestal.py,part1d_zeroed_ring.py,part1e_rectangular_control.py,part2_plot_consistency.py,part3_latent_reach.py,part3b_fit_latent.py)run as
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib python <script>,numpy path (
xp=np) throughout.Answer to the epic owner's question in one line: yes, the source code has a
real bug on the Delaunay magnification path.
areas_for_magnificationreturnsVoronoi cell areas, but the Delaunay mapper is a barycentric-linear
interpolant whose exact quadrature weights are the barycentric dual areas.
The two differ by up to five orders of magnitude on individual boundary cells,
and the resulting magnification is biased by −13 % to −99 % on realistic
irregular source meshes.
1. Per-path table
MeshGeometryDelaunay.areas_for_magnificationPyAutoArray/autoarray/inversion/mesh/mesh_geometry/delaunay.py:195-207voronoi_areaswith-1entries set to0.0source_science.pyscripts (autolens_workspace/scripts/{imaging:399,group:404,multi_galaxy:411,interferometer:387}/features/pixelization/source_science.py) — only if the user swaps the mesh to Delaunay; nothing in PyAutoLens/PyAutoGalaxy/Euclidgrep areas_for_magnificationoverPyAutoArray/test_autoarray,PyAutoLens/test_autolens,PyAutoGalaxy/test_autogalaxy,autolens_workspace_test→ zero hitsvoronoi_areas_numpy.../mesh_geometry/delaunay.py:13-91Voronoi+ shoelace per cell; unbounded regions marked-1.0at:89voronoi_areas(:191) →areas_for_magnificationPyAutoArray/test_autoarray/inversion/pixelization/mesh_geometry/test_delaunay.py:41-57, pins the pathology: it assertsvoronoi_areas[3] == 29.836324on a mesh whose real cells are O(1)areas[areas == -1] = 0.0.../mesh_geometry/delaunay.py:205barycentric_dual_area_fromPyAutoArray/autoarray/inversion/mesh/interpolator/delaunay.py:342-398scipy_delaunay:39andjax_delaunay:317→ only theareas_factor·sqrt(areas)split-point offsetsF_dual / F_map = 1.000000to ≤ 2 × 10⁻⁵ across 5 source configurations and 3 mesh resolutions (Part 1)pixel_weights_delaunay_from.../interpolator/delaunay.py:509-586:546-560); nearest-vertex[1,0,0]fallback outside the hull (:568-580)Mapper.mapping_matrixMapper.mapping_matrixPyAutoArray/autoarray/inversion/mappers/abstract.py:255-279mapped_reconstructed_(operated_)data, i.e. the magnification numerator_plot_delaunayPyAutoArray/autoarray/plot/inversion.py:269-322ax.tripcolor(x, y, vals)— notriangles=, noshading=F_dual=1.00000000; /F_vor= 18.47 (Part 2)MeshGeometryRectangular.areas_for_magnification.../mesh_geometry/rectangular.py:472-481→areas_transformedsource_science.pyscripts (RectangularBilinearAdaptDensity)RectangularUniform), +28.5 % (RectangularBilinearAdaptDensity); per-pixeleffective/areais a tight scale factor (median 1.061 / 1.279), consistent with a guard-ring node-count mismatch — see caveat in §5latent.magnificationPyAutoLens/autolens/analysis/latent.py:218-229total_lensed_source_flux_mujy / total_source_flux_mujyvis_lp,vis_pix,full_modelareas_for_magnification)total_source_flux = 0.0→magnification = inf; 9/9 productionvis_pixresults recordmagnification = 0.0Galaxy.image_2d_fromPyAutoGalaxy/autogalaxy/galaxy/galaxy.py:245-278return xp.zeros(...)at:278when the galaxy has no non-linearLightProfiletotal_source_flux(_mujy)sum = 0.000000e+00; Sersic galaxy →7.941301e+01areas_factor · sqrt(areas).../interpolator/delaunay.py:45(numpy),:323(JAX)ConstantSplit/AdaptSplitregularisation matrices2. PART 1 — which area integrates the reconstruction?
Setup
Identity lens (source-plane data grid = image-plane data grid), so the true
magnification is exactly 1.0. A Delaunay mapper is built from a jittered
24×24 lattice; a Gaussian (or random positive vector) is sampled at the mesh
vertices as
reconstruction. Four integrals:F_map= Σ over data pixels of(mapping_matrix @ s) × pixel_area— what themapper paints on the sky, i.e. what
mapped_reconstructed_datasums toF_vor= Σs · areas_for_magnification— the current workspace denominatorF_dual= Σs · barycentric_dual_area_from(points, simplices)F_true= analytic Gaussian flux2πσ²ATwo confounds were removed by construction so the numbers mean what they say:
(
interpolator/delaunay.py:568-580) and dump flux onto boundary vertices.The outer ring of mesh vertices is therefore pinned un-jittered to the data
boundary, and
n_fallback == 0is reported in every run.F_mapintegrates the data-grid footprint whileF_dualintegrates the hull.Pinning the hull to the data square makes them the same region — reported as
hull area 4.000000 vs data footprint 4.000000 (match to 0.0000 %).Only interior vertices are jittered, so the triangulation stays irregular.
Result (
part1_output.txt)F_mapF_vorF_dualF_trueResolution check (σ=0.20″):
F_dual/F_map= 0.99999083 / 0.99999959 / 1.00000751at
n_side= 16 / 24 / 40.Which denominator gives 1.0:
F_dual. It reproduces the identity-lens truthto ≤ 2 × 10⁻⁵ in every case, including the random reconstruction — the identity
is geometric, not a property of the source.
F_vornever does, and its errorgrows monotonically with how much reconstruction weight sits near the boundary
(0.089 % → 9.3 % as the outer-ring weight goes 0.0000 → 0.4723).
F_map/F_trueis a separate, honest discretisation error of the mesh itself(0.998 → 0.466 as the source outgrows the mesh); it is not a denominator defect,
which is exactly why
μand notF_trueis the right yardstick here.The realistic case is far worse (
part1b_irregular_hull.py,part1b_output.txt)Because
F_dual == F_mapis now an established identity,F_dual / F_voristhe magnification bias, with no matched-domain bookkeeping needed. On a fully
irregular hull (outer ring jittered too — what an adaptive source mesh looks
like after tracing), scipy's Voronoi hands some bounded boundary sites
enormous cells, because their far vertices are the circumcentres of very flat
boundary triangles.
areas_for_magnificationzeroes only the unbounded ones.Direction: negative — the inflated boundary cells swell the denominator, so
source_science.pyunder-estimates the magnification whenever thereconstruction is non-zero at the mesh edge. (The positive branch, +0.03 %…+9.3 %,
occurs in the opposite regime, where the boundary cells are unbounded and get
zeroed, so their flux is discarded from the denominator instead.)
The "compact source is fine" case is a knife edge (
part1c_pedestal.py,part1c_output.txt)The ±0.4 % agreement for a compact source is an accident: the pathological cells
multiply a reconstruction value of ≈ 0. A real inversion does not put exactly 0
there. Adding a random pedestal to the vertices, as a fraction of the peak:
A pedestal of one part in ten thousand of the peak already costs several per
cent. There is no regime in which this denominator is safe by design.
zeroed_pixelsdoes not rescue it (part1d_zeroed_ring.py,part1d_output.txt)The Euclid pipeline builds its Delaunay source mesh with
zeroed_pixels=edge_pixels_total(30), a ring of image-plane points appended byal.image_mesh.append_with_circle_edge_points(
euclid_strong_lens_modeling_pipeline/scripts/full_model.py:730-734, 786, 828-832, 863;scripts/initial_lens_model.py:592) whose source values are fixed to exactly 0.Peeling convex hulls shows why that is not enough:
The pathology lives one ring inside the convex hull, which
zeroed_pixelsnever touches. Zeroing one hull ring moves the flat-source bias from −97.63 % to
−97.69 % (seed 0); −97.03 → −97.09 (seed 1); −97.40 → −97.46 (seed 2);
−97.93 → −97.97 (seed 3). For a compact source with a 0.1 % pedestal, zeroing one
ring moves −7.76 % → −13.65 % (seed 0), −19.38 % → −18.88 % (seed 1) — i.e. it
does nothing useful in either direction. Two rings are needed before the number
comes back (−1.68 %, −0.62 %, −0.90 %, −1.97 %), and three before it is clean.
Is the mapping barycentric-linear?
Yes, and this is what makes
Σ sᵢ · dualᵢthe exact integral rather than anapproximation.
pixel_weights_delaunay_from(
PyAutoArray/autoarray/inversion/mesh/interpolator/delaunay.py:509-586)computes the three sub-triangle areas
a0, a1, a2at:546-552and normalisesby their sum at
:556-560— textbook barycentric coordinates. For a piecewiselinear function on a triangulation,
∫_hull f = Σ_tri area_tri · mean(3 vertices) = Σ_i s_i · (Σ area/3) = Σ_i s_i · dual_i,which is precisely
barycentric_dual_area_from(:342-398). The one departureis the out-of-hull nearest-vertex fallback at
:568-580, which is piecewiseconstant, not linear — a separate hazard, quantified above.
Incidental:
scipy_delaunaypadssimplicesto(2N, 3)with-1rows forstatic JAX shapes (
:24).-1indexing wraps to the last vertex, giving adegenerate zero-area triangle, so passing the padded array to
barycentric_dual_area_fromis harmless — verified identical to the unpaddedresult (
part2_output.txt).3. PART 2 — plotting vs magnification consistency
_plot_delaunay(PyAutoArray/autoarray/plot/inversion.py:269-322) callsax.tripcolor(x, y, vals, cmap=colormap, norm=norm)at:319with neither atriangles=nor ashading=argument. Two things follow. First, matplotlib3.10's
tripcolordefaults toshading='flat'— not Gouraud, contradictingthe docstring at
:272; with point-valuedC, flat shading paints each trianglewith the mean of its three vertices, whose area-weighted sum is
Σ_tri area·mean(3) = Σ_i s_i·dual_i— identical to the Gouraud integral bylinearity, so the docstring inaccuracy is cosmetic here but should not be relied
on. Second, without
triangles=, matplotlib rebuilds its own Qhulltriangulation of the same points instead of reusing
mapper.interpolator.delaunay.simplices; on the test mesh the two simplex setswere identical (780 vs 780 triangles, 0 differences), so this is a latent
fragility (co-circular points could diverge) rather than an active defect.
The measured consequence is unambiguous: the integral of the plotted surface
equals
F_dualto1.00000000and exceedsF_vorby a factor of18.47(
part2_output.txt). The picture the user looks at and the mapper's own fluxalready agree with each other and disagree with the magnification denominator;
areas_for_magnificationis the odd one out, not the plot.4. PART 4 — the
sqrt/ NaN pathgrep -n sqrt PyAutoArray/autoarray/inversion/mesh/mesh_geometry/delaunay.pyreturns nothing — there is no square root anywhere on the
voronoi_areas_numpy→voronoi_areas→areas_for_magnificationpath, so noNaN can be manufactured there by a negative argument (the shoelace at
:83takes
np.absbefore any use, and-1.0at:89is a sentinel that:205maps to
0.0, never a sqrt input). The onlysqrtin the Delaunay package isareas_factor * np.sqrt(areas)atinterpolator/delaunay.py:45(numpy) and:323(JAX), and it is provably not on the flux path: its output isarea_weights, whichsplit_points_from(
PyAutoArray/autoarray/inversion/regularization/regularization_util.py:33-84)uses solely as an offset distance to place four cross points around each
vertex; those points feed
splitted_mappings/splitted_weights, which areconsumed only by the split regularisation schemes
(
autoarray/inversion/regularization/adapt_split.py:127-139,reg_split_np_from).They build the regularisation matrix
H, which shapes which reconstruction theinversion prefers but never rescales a reconstruction that has already been
solved, and they appear nowhere in
mapping_matrix,mapped_reconstructed_data, orareas_for_magnification. So the split-pointweighting cannot change the integral of the reconstructed values — it can only
change the values themselves via the fit, which is a modelling choice, not an
arithmetic defect.
5. PART 3 — Euclid reachability
(a) Latent outputs
euclid_strong_lens_modeling_pipeline/output/does not exist in the canonicalcheckout —
.gitignore:13-14excludesoutput/andoutput_*/, and the RALresults are not present there. Production outputs were found on this machine
under
/tmp/scoped_output,/tmp/scoped_output_sed,/tmp/scoped_323738(read-only; 523
latent_summary.jsonfiles across thedr1_prelim_grade_absample). Only two
initial_lens_modelstages exist in them —there are no
full_modeloutputs on this machine. Values are themedian_pdf_sampleentry oflatent_summary.json:magnificationpresentinitial_lens_model/vis_lp(Sersic source)initial_lens_model/vis_pix(Delaunay source)total_source_flux = 0.0sersic_lens_model/{vis,nir_y,nir_j,nir_h,decam_grizt}So the
vis_pixmagnification is not merely absent — it is present as ameaningless
0.0sentinel that a downstream catalogue would silently ingest.The
vis_pixkey set is otherwise complete (9 keys, same asvis_lp), so theNaN-drop signature does not fire on these particular files.
(b) Analytic reading, confirmed empirically
total_source_flux_mujy(PyAutoLens/autolens/analysis/latent.py:189-215)computes
tracer.galaxies[-1].image_2d_from(grid=fit.dataset.grids.lp)onfit.tracer_linear_light_profiles_to_light_profiles.Galaxy.image_2d_from(
PyAutoGalaxy/autogalaxy/galaxy/galaxy.py:245-278) is:A
Pixelizationis not aLightProfile, so a source galaxy whose only lightcomponent is a pixelization takes the
return zerosbranch. Measured(
part3_output.txt): pixelization-only galaxy →sum(image_2d_from) = 0.000000e+00;Sersic galaxy →
7.941301e+01. Thenab_mag_via_flux_from(0.0, magzero=25) = infand
flux_mujy_via_ab_mag_from(inf) = 0.0exactly, so the denominator is a hardzero and
magnification = lensed / 0.0 = +inf.I confirmed this empirically rather than resting on the reading.
part3b_fit_latent.pybuilds a realal.FitImagingon the pipeline's shippeddataset/simulated/euclid_dr1_like(magzero 24.6), with the truth lens fromtruth.json, and evaluates the library latents directly — no search:total_source_fluxtotal_source_flux_mujymagnificationvis_lpcontrol)truth.json's15.146298766165936zeroed_pixels=30(vis_pix/full_model)infinfis non-finite, soPyAutoFit/autofit/non_linear/analysis/latent.py:258-263(
if np.isfinite(all_values[:, i]).any()) drops the wholemagnificationcolumn: absence of the key is the signature under the current code, and
0.0under whatever version produced the archived
vis_pixresults. Either waythe Euclid
magnificationlatent can never report a magnification for aDelaunay stage, and it never touches
areas_for_magnificationat all — the twoDelaunay-magnification defects are independent and neither masks the other.
The Sersic control also validates the latent itself: it reproduces the
simulator's recorded truth exactly, so the definition is sound and only the
pixelized-source route is broken.
Note also
scripts/tools/diagnose_latent_vis_pix.py's docstring claim that"the library latents that do not depend on the source reconstruction
(
magnification, …) are identical between the two stages" is incorrect formagnification: it depends on the source model, which is exactly what changesat
vis_pix, and the production numbers (6.27/10.08/18.43 vs 0.0/0.0/0.0)disprove it directly.
Caveat on the rectangular control (
part1e_rectangular_control.py)The shipped
source_science.pyscripts all useal.mesh.RectangularBilinearAdaptDensity, whoseareas_for_magnificationisareas_transformed(mesh_geometry/rectangular.py:472-481) — a different codepath from the Delaunay one this audit was scoped to. Running the same
identity-lens test there gives μ = +6.16 % (
RectangularUniform) and+28.47 % (
RectangularBilinearAdaptDensity) at σ=0.15″, with the per-source-pixelratio
effective_mapper_area / areas_for_magnificationa tight scale factor(median 1.0607 and 1.2785, spread ~0.3 % away from the boundary) rather than the
Delaunay path's wild per-cell scatter. That signature is consistent with a
guard-ring node-count mismatch — the rectangular docstring at
:490-497notes aguard ring occupies the border, so an
nvsn−3denominator would give(24/21)² ≈ 1.31. I did not establish a matched-domain identity for therectangular meshes and I am not claiming a bug there — I ran it only so the
Delaunay verdict is stated against a control rather than in isolation. It is a
lead for the cluster epic's
mesh_magnification_correctnessprompt, not afinding of this audit.
6. Statement to Cortex phase 7 (
magnification_robustness)Delaunay magnification numbers produced by the
source_science.pyrecipe arenot trustworthy as they stand, and the defect is in the library, not the
script.
MeshGeometryDelaunay.areas_for_magnification(
PyAutoArray/autoarray/inversion/mesh/mesh_geometry/delaunay.py:195-207)returns Voronoi cell areas, but the Delaunay mapper is a barycentric-linear
interpolant whose exact quadrature weights are the barycentric dual areas
already computed one module away in
barycentric_dual_area_from(
inversion/mesh/interpolator/delaunay.py:342-398) and currently used only toposition regularisation split points. The area that should be used is the
barycentric dual area: with an identity lens it recovers μ = 1.0 to ≤ 2 × 10⁻⁵
for every source shape tested, including a random reconstruction, and it is also
exactly what the plotted
tripcolorsurface integrates to — so switching thedenominator makes the number, the picture and the mapper agree, where today only
the first disagrees. The expected bias direction is negative — magnification
systematically under-estimated — whenever the reconstruction is non-zero near
the mesh edge, because
areas_for_magnificationzeroes only the unboundedboundary cells while the pathology sits in the first ring inside the convex
hull, where bounded Voronoi cells reach 10⁵× their dual area (one cell measured
at 234× the entire mesh hull). Magnitude: −13 % to −53 % on adaptive-style
meshes, −95 % to −99 % when the source fills the hull, and −3 % to −5 % even for
a compact central source once its edge pixels carry a pedestal of one part in
ten thousand of the peak; the apparent ±0.4 % accuracy for a perfectly compact
source is a knife edge that a real non-negative solver will not sit on. Three
operational consequences for the phase. (1) The Delaunay rung (rung 5) cannot be
scored against a
latent.magnificationnumber from the pipeline: for anypixelized source
total_source_fluxis a hard0.0(
PyAutoGalaxy/autogalaxy/galaxy/galaxy.py:278), so the latent evaluates toinfand is dropped, and 9/9 archivedvis_pixresults record it as ameaningless
0.0— that leg needs the area-basedA_img/A_srcquantitycomputed explicitly, and the phase's "state which magnification is compared at
each rung" instruction is therefore load-bearing, not bookkeeping. (2) The
Sersic rungs (1-4) are unaffected by this bug and their latent is sound — it
reproduced the simulator truth
15.146298766165936bit-for-bit — so amatched-vs-mismatched comparison across rungs 1-4 is interpretable today.
(3)
areas_for_magnificationhas no test in any repository, and the onlytest that touches
voronoi_areaspins a 29.8 arcsec² boundary cell as expectedbehaviour, so any fix needs a regression test built from the identity-lens
construction in
part1_flux_integral.pyrather than a re-pin of the existingsnapshot. A separate, un-established lead worth a line in the phase's runs: the
rectangular meshes that
source_science.pyactually ships with show aconsistent +6 %/+28 % scale offset with a guard-ring signature; that belongs to
the cluster epic's
mesh_magnification_correctnessprompt, and this auditneither confirms nor rules it out.
(No fix is proposed or implemented here, per the audit's brief.)
Merge pull request #523 from PyAutoLabs/feature/delaunay-area-magnifi…
Jammy2211 commentedon Sep 4, 2026
Shipped
PR #523 merged into
main(merge commit548ff1e, head8c2e0d18) — CI green on all three legs (unittest 3.12, 3.13, nojax).What landed: the audit's test deliverable and one docstring correction, no behaviour change.
MeshGeometryDelaunay.areas_for_magnification(unit lattice: interior cells 1.0, hull cells zeroed, sum(n-2)²; bounded boundary cells kept; repeat calls agree without writing the-1sentinel back).barycentric_dual_area_from(single triangleA/3; sum equals the convex-hull area; exact integration of a linear field; NumPy-vs-JAX parity).areas_for_magnificationdocstring now states the proven semantics: Voronoi cell areas, only unbounded cells zeroed, not the interpolant's barycentric dual areas.Audit verdict (full table in the comment above): two real defects, deliberately not fixed here, filed as separate PyAutoMind bug prompts —
draft/bug/autoarray/delaunay_magnification_uses_voronoi_not_dual_areas.md— the Voronoi denominator biases μ; the exact quadrature weight for the barycentric-linear mapper is the dual area.draft/bug/autolens/magnification_latent_zero_for_pixelized_source.md— the pipeline'smagnificationlatent is a hard 0/0 for any pixelization-only source.The two tests that pin the current Voronoi semantics carry comments so the follow-up fix flips them deliberately.
Closing as completed; the Mind record is
PyAutoMind/complete/2026/09/delaunay-area-magnification-audit.md.pending-releasestays on the PR until a PyAutoArray release publishes it.Generated by Claude Code