Skip to content

feat: kernel-density CDF transform for adaptive rectangular mesh #373

Description

@Jammy2211

Overview

Replace the empirical point-rank CDF in the adaptive rectangular mesh with a smooth kernel-density CDF (Enzi et al. arXiv:2606.30620 RTU formulation) as a new opt-in mesh variant. The 2026-07-09 JAX gradient audit (autolens_workspace_developer#87) proved the linear rank-CDF makes the likelihood piecewise-constant in mass/shear whenever interp queries coincide with knots — imaging at os_pix=1 and the whole interferometer sparse path have zero usable gradients today, and os_pix=4 carries ~1–3% FD contamination from rank swaps. The kernel CDF is strictly monotone by construction, C^∞ in queries and point positions, and duplicate-safe, so the mesh carries correct smooth gradients in every configuration.

Success criterion: strict FD assertions pass on ALL parameters (mass/shear included) in the jax_grad workspace-test scripts — imaging at os_pix=1 and 4, interferometer sparse path — plus eager-vs-JIT consistency and fit-quality parity with the linear meshes. Sampler (NUTS/HMC) work is explicitly OUT of scope (future task once gradients are green).

Plan

  • Preamble: inspect PyAutoArray stash@{0} (parked 727-line spline-interpolator refactor); salvage anything useful, record keep/drop here, retire the parked.md entry.
  • Implement the kernel-density CDF transform in a new interpolator module mirroring the spline variant's layout, with exact smooth forward transform and table-based inverse.
  • Add opt-in mesh classes RectangularKernelAdaptDensity / RectangularKernelAdaptImage with a bandwidth kwarg (default tied to mesh resolution); export via the mesh package (surfaces as al.mesh.* downstream with no PyAutoGalaxy/PyAutoLens edits).
  • NumPy-only unit tests in test_autoarray/ (monotonicity, round-trip, duplicate-safety, weight-map variant, areas consistency).
  • Certify via autolens_workspace_test/scripts/jax_grad/: strict FD on all params at os_pix=1 AND 4 (imaging) and on the interferometer sparse path; eager-vs-JIT consistency; figure-of-merit parity within a few e-4 of the linear meshes.
  • Update the gradient audit README rows in autolens_workspace_developer once certified.
  • Ship library PR first, then the workspace-test/developer PR (library-first merge gate).
Detailed implementation plan

Affected Repositories

  • PyAutoArray (primary)
  • autolens_workspace_test (certification scripts)
  • autolens_workspace_developer (gradient README rows)

Branch Survey

Repository Current Branch Dirty?
./PyAutoArray main clean
./autolens_workspace_test main clean
./autolens_workspace_developer main clean (untracked local AGENTS.md only)

Suggested branch: feature/rectangular-kernel-cdf-mesh
Worktree: ~/Code/PyAutoLabs-wt/rectangular-kernel-cdf-mesh/

Coordination note

The parked rect-adapt worktree (#372, uncommitted, awaiting ship sign-off) also edits autoarray/inversion/mesh/mesh_geometry/rectangular.py, but only the edges_transformed hunk (node-midpoint edges); this task adds dispatch in areas_transformed / transformed-grid / ctor — distinct hunks, clean merge expected in either order. If #372 merges first, the kernel path inherits the corrected edges automatically.

Implementation Steps

  1. Stash preamble: diff PyAutoArray stash@{0} against current rectangular.py; salvage or reject (likely reject — it predates the audit and belongs to the rejected spline chain); comment the decision here; retire the parked.md rectangular-spline-cdf entry; keep the stash until the decision comment lands.
  2. New module autoarray/inversion/mesh/interpolator/rectangular_kernel.py (mirror rectangular_spline.py's independently-auditable-copy layout):
    • create_transforms_kernel(traced_points, mesh_weight_map=None, bandwidth=None, n_knots=64, xp=np) — same contract as create_transforms (rectangular.py:70). Per axis: weights w_i = 1/N (density) or normalized mesh_weight_map (image); F(x) = Σ_i w_i · Φ((x − x_i)/h), Φ = Gaussian CDF via scipy.special.erf (np) / jax.scipy.special.erf (jax). Forward evaluated exactly at query points, rescaled by [F(lo), F(hi)] to fill [0,1]. Inverse via interp on a fixed K=n_knots knot grid spanning the data range padded by ~4h — inverse queries are fixed U-grid constants, so gradients flow smoothly through the table values. Bandwidth default h = bandwidth_factor × data_range_axis / mesh_pixels_axis, factor ~1.0, overridable via the mesh bandwidth kwarg.
    • adaptive_rectangular_mappings_weights_via_interpolation_from_kernel(...) — copy of steps 3–7 of the linear helper (floor/ceil, flatten, bilinear weights) routed through kernel transforms.
    • adaptive_rectangular_areas_from_kernel(...) + adaptive_rectangular_transformed_grid_from_kernel(...) — kernel versions of the geometry helpers.
    • InterpolatorRectangularKernel(InterpolatorRectangular) — overrides _mappings_sizes_weights and mesh_geometry, mirroring InterpolatorRectangularSpline.
  3. Geometry dispatch in autoarray/inversion/mesh/mesh_geometry/rectangular.py: MeshGeometryRectangular already branches on spline_deg is not None (~lines 478/526); add parallel optional kernel_bandwidth / kernel_knots ctor kwargs (default None) routing areas_transformed and the transformed-grid property through the *_from_kernel helpers.
  4. Mesh classes autoarray/inversion/mesh/mesh/rectangular_kernel_adapt_density.py / rectangular_kernel_adapt_image.pyRectangularKernelAdaptDensity(RectangularAdaptDensity), RectangularKernelAdaptImage(RectangularAdaptImage); template rectangular_spline_adapt_density.py; store bandwidth/n_knots, override interpolator_cls, forward kwargs in interpolator_from. Export from autoarray/inversion/mesh/mesh/__init__.py; mirror any registry mentions the spline classes have in autoarray/inversion/pixelization.py / autoarray/exc.py. Spline meshes stay untouched.
  5. Unit tests (numpy-only): test_autoarray/inversion/pixelization/interpolator/test_rectangular_kernel.py — strict monotonicity; forward/inverse round-trip; duplicate-point safety; weight-map variant; mesh-class plumbing; areas positive + consistency vs linear variant on a smooth cloud. Full pytest test_autoarray/ green.
  6. Certification (autolens_workspace_test/scripts/jax_grad/, same branch name):
    • imaging_pixelization.py: RectangularKernelAdaptDensity at os_pix=1 with strict FD on ALL params (no skip_indices — must NOT be a staircase); both kernel meshes at os_pix=4 strict FD; util.assert_eager_jit_consistent everywhere; eager figure_of_merit within a few e-4 relative of linear AdaptDensity/AdaptImage.
    • interferometer.py: kernel mesh on the sparse-operator path — strict FD on all mass/shear params + eager-vs-JIT.
    • Existing linear/spline variant assertions unchanged (no regression).
  7. Docs row: autolens_workspace_developer/jax_profiling/gradient/README.md — kernel-mesh rows + staircase-section implications.

Key Files

  • PyAutoArray/autoarray/inversion/mesh/interpolator/rectangular.py — linear transform contract being mirrored (create_transforms, bilinear helper).
  • PyAutoArray/autoarray/inversion/mesh/interpolator/rectangular_spline.py — structural template for the new module (do not build on its math).
  • PyAutoArray/autoarray/inversion/mesh/mesh_geometry/rectangular.py — geometry dispatch site.
  • PyAutoArray/autoarray/inversion/mesh/mesh/rectangular_spline_adapt_density.py — mesh-class template.
  • autolens_workspace_test/scripts/jax_grad/{imaging_pixelization,interferometer,util}.py — certification harness.
  • autolens_workspace_developer/jax_profiling/gradient/README.md — audit table to update.

Risks

  • Bandwidth trade-off: h too small re-approaches the staircase; h too large over-smooths geometry and drifts FoM parity. Default tied to mesh resolution + exposed kwarg.
  • Inverse-table resolution: K=64 must resolve the CDF better than the mesh shape (~30×30 in jax_test configs); n_knots kwarg, bump if parity fails.

Original Prompt

Click to expand starting prompt

Kernel-density CDF transform for the adaptive rectangular mesh (differentiable everywhere)

Type: feature
Target: autoarray
Repos:

  • PyAutoArray
  • autolens_workspace_test
  • autolens_workspace_developer
    Difficulty: medium
    Autonomy: supervised
    Priority: normal
    Status: formalised

Replace the empirical point-rank CDF in the adaptive rectangular mesh with a smooth
kernel-density CDF (the Enzi et al. arXiv:2606.30620 RTU formulation) so the mesh
carries correct smooth mass/shear gradients in every configuration — imaging at any
pixelization over-sampling and the interferometer sparse path (which has no
over-sampling and today has zero usable gradients).

Original request (verbatim): "it sounds like you think we can improve the rectangular
gradient method across all use cases? explain how [...]" → recommendation approved:
kernel-CDF leapfrog, one focused task, "drop the nuts stuff for now I only care that
the jax gradient workspace test script shows grad all works and can work on samples
down the line."

Background (from the 2026-07-09 JAX gradient audit, autolens_workspace_developer#87)

  • The linear rank-CDF (create_transforms in
    PyAutoArray/autoarray/inversion/mesh/interpolator/rectangular.py) makes the
    likelihood exactly piecewise-constant in mass/shear whenever interp queries
    coincide with knots: imaging os_pix=1 and the interferometer sparse path
    (jax_grad/interferometer.py variant B documents the staircase). At os_pix=4
    gradients live but every rank swap leaves a micro-kink (~1-3% FD contamination).
  • Prior attempt: RectangularSplineAdapt{Density,Image} (PyAutoArray PR Add RectangularSplineAdapt{Density,Image} meshes for gradient-based samplers #289,
    2026-04-22, opt-in, built for gradient samplers). Measured 2026-07-09: breaks the
    rank invariance (AD live on all 14 params at os_pix=1) BUT eager-vs-JIT LL gap
    ~15 and erratic FD (noisy surface — Add RectangularSplineAdapt{Density,Image} meshes for gradient-based samplers #289's shipped "oscillations" limitation).
    Suspects: _enforce_strict_monotone eps-jitter, deg-11 polyfit conditioning,
    Hermite inversion table. Do not build on this chain; keep the spline meshes
    untouched/opt-in.
  • A 727-line jax-friendly spline-interpolator refactor is PARKED in PyAutoArray
    stash@{0} (parked.md rectangular-spline-cdf, 2026-05-08).

Task

  1. Preamble: inspect PyAutoArray stash@{0} — salvage anything useful, then
    retire the parked.md entry either way (keep the stash until a decision is
    recorded on the issue).
  2. Implement the kernel-density CDF transform as a new opt-in mesh variant (e.g.
    RectangularKernelAdaptDensity / ...AdaptImage), slotting into the existing
    transform/inv_transform machinery: per axis
    F(x) = sum_i w_i * Phi((x - x_i)/h) evaluated on a small fixed knot grid
    (K ~ 64), inverse via interpolation on the same grid. Strictly monotone by
    construction (no jitter hack), C-infinity in queries AND point positions,
    duplicate-safe (no 1/Delta-knot terms). Weight-map variant uses w_i from the
    adapt image. Bandwidth h defaults tied to mesh resolution; expose as a mesh
    kwarg.
  3. Certification is the success criterion (no sampler work in this task):
    • Extend autolens_workspace_test/scripts/jax_grad/imaging_pixelization.py
      with kernel-mesh variants at os_pix=1 AND os_pix=4: strict FD assertions on
      ALL parameters (mass/shear included) using the existing util.py harness —
      the os_pix=1 variant must NOT be a staircase (that is the point).
    • Extend autolens_workspace_test/scripts/jax_grad/interferometer.py with the
      kernel mesh on the sparse-operator path: strict FD on all mass/shear params.
    • Eager-vs-JIT consistency (util.assert_eager_jit_consistent) must hold on
      every variant.
    • Fit-quality parity: eager figure_of_merit on the jax_test config within a few
      e-4 relative of the linear AdaptDensity/AdaptImage meshes (mesh geometry
      changes slightly; reconstruction quality must not degrade).
    • Update autolens_workspace_developer/jax_profiling/gradient/README.md rows +
      the staircase-section implications once certified.
  4. Sampler trials (NUTS/HMC) are explicitly OUT of scope — future task once the
    jax_grad scripts are green.

Constraints

  • PyAutoArray is claimed by nnls-solver-optimization (active.md) at time of
    filing — start when the claim releases or coordinate explicitly (different
    files, adjacent territory).
  • Spline meshes (PR Add RectangularSplineAdapt{Density,Image} meshes for gradient-based samplers #289) stay as-is; no deletion/refactor of them in this task.
  • Library unit tests numpy-only per repo rules; all JAX validation through the
    workspace_test jax_grad scripts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions