Overview
Two library bugs surfaced while writing the new scripts/guides/profiles/{light,mass}.py guides in autolens_workspace (#86 / #176 / #178 / #179):
Basis.image_2d_from returns a raw numpy.ndarray when the basis is composed of LightProfileLinear constituents (the standard MGE case), instead of the Array2D its return-type annotation promises.
dPIEPotential.convergence_2d_from returns a VectorYX2D instead of the Array2D its scalar return value should produce.
Both forced workarounds in the workspace guides — neither is structurally serious, but both are tiny library-side fixes that remove the need for those workarounds and tighten the type contract advertised by the methods' annotations.
Plan
- Fix 1 —
Basis.image_2d_from: in autogalaxy/profiles/basis.py, wrap the LightProfileLinear zero placeholder in aa.Array2D so the summed return type is always Array2D, regardless of which constituent profiles the basis was built from.
- Fix 2 —
dPIEPotential.convergence_2d_from: in autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py, swap the @aa.decorators.to_vector_yx decorator on dPIEPotential.convergence_2d_from for @aa.decorators.to_array. The function body returns a scalar field; the vector decorator is a copy-paste typo from the deflections method directly above it.
- Regression tests: add one test per fix asserting the return type is
Array2D for each method, in the existing test_autogalaxy/profiles/test_basis.py and test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py.
Detailed implementation plan
Affected Repositories
- PyAutoGalaxy (primary)
- PyAutoLens — none (re-exports only; will pick up the fix transparently)
Work Classification
Library (code + tests). Ships via /ship_library.
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoGalaxy |
main |
clean |
| ./PyAutoLens |
main |
clean (reference; not modified) |
Suggested branch: feature/profile-return-type-fixes
Worktree root: ~/Code/PyAutoLabs-wt/profile-return-type-fixes/
Routing: library-dev → /start_library.
Implementation Steps
-
autogalaxy/profiles/basis.py — In Basis.image_2d_list_from, replace the LightProfileLinear placeholder
xp.zeros((grid.shape[0],))
with
aa.Array2D(values=xp.zeros((grid.shape[0],)), mask=grid.mask)
This makes Basis.image_2d_from = sum([...]) return an Array2D whenever every constituent is linear (the MGE case), because Array2D.__add__ returns Array2D. The standard-profile path already produces Array2D instances from each image_2d_from call, so the sum is unchanged for non-linear bases.
-
autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py — Change the decorator on dPIEPotential.convergence_2d_from from:
@aa.decorators.to_vector_yx
@aa.decorators.transform
def convergence_2d_from(...):
to:
@aa.decorators.to_array
@aa.decorators.transform
def convergence_2d_from(...):
Matches the dPIEPotentialSph.convergence_2d_from decorator stack directly below it, which has always been correct.
-
Add regression tests in the existing test files:
test_autogalaxy/profiles/test_basis.py — add a test that constructs a Basis of LightProfileLinear Gaussians and asserts isinstance(basis.image_2d_from(grid), aa.Array2D).
test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py — add a test that constructs a dPIEPotential and asserts isinstance(profile.convergence_2d_from(grid), aa.Array2D). (NOT VectorYX2D.)
-
Run the existing test suite to confirm no regressions on the surrounding behaviour:
python -m pytest test_autogalaxy/profiles/test_basis.py test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py -x
Key Files
autogalaxy/profiles/basis.py — wrap linear-profile zero placeholder in Array2D
autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py — fix decorator
test_autogalaxy/profiles/test_basis.py — add regression test
test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py — add regression test
Out of scope
- Workspace cleanup of the workarounds in
autolens_workspace/scripts/guides/profiles/{light,mass}.py — cosmetic, can stay until the next pass through those scripts.
- Any other
lp_basis / dPIE-family quirks not specifically listed here.
JAX considerations
Basis.image_2d_from is consumed by downstream Python (Galaxy → Galaxies → Tracer), never the JIT output, so wrapping in Array2D is safe even in xp=jnp mode (per the CLAUDE.md rule on intermediate-step Array2D wraps).
dPIEPotential.convergence_2d_from is decorated; the decorator stack handles the JAX-vs-numpy path, no extra work needed.
Original Prompt
Click to expand starting prompt
Two library bugs surfaced while writing autolens_workspace/scripts/guides/profiles/light.py (#86 / #176) and mass.py (#178 / #179):
-
Basis.image_2d_from returns a raw numpy.ndarray instead of an Array2D when the basis is composed of LightProfileLinear constituents. The light.py guide had to wrap the basis in a Galaxy and plot via the galaxy's image to make aplt.plot_array work.
Root cause: Basis.image_2d_list_from returns xp.zeros((grid.shape[0],)) for LightProfileLinear profiles (because their intensity will be solved by the inversion later). Summing those zero ndarrays in image_2d_from produces a raw ndarray rather than an Array2D.
-
dPIEPotential.convergence_2d_from returns a VectorYX2D instead of an Array2D. The mass.py guide had to skip plotting convergence for dPIEPotential and use dPIEPotentialSph in the walkthrough instead.
Root cause: a typo / copy-paste of the deflections decorator — @aa.decorators.to_vector_yx is used where @aa.decorators.to_array is correct. The function body returns a scalar field (kappa_circ * (1 - asymm_term) + (alpha_circ / grid_radii) * asymm_term) which is not a (y, x) vector.
Both fixes are tiny library-code changes with regression tests.
Overview
Two library bugs surfaced while writing the new
scripts/guides/profiles/{light,mass}.pyguides in autolens_workspace (#86 / #176 / #178 / #179):Basis.image_2d_fromreturns a rawnumpy.ndarraywhen the basis is composed ofLightProfileLinearconstituents (the standard MGE case), instead of theArray2Dits return-type annotation promises.dPIEPotential.convergence_2d_fromreturns aVectorYX2Dinstead of theArray2Dits scalar return value should produce.Both forced workarounds in the workspace guides — neither is structurally serious, but both are tiny library-side fixes that remove the need for those workarounds and tighten the type contract advertised by the methods' annotations.
Plan
Basis.image_2d_from: inautogalaxy/profiles/basis.py, wrap theLightProfileLinearzero placeholder inaa.Array2Dso the summed return type is alwaysArray2D, regardless of which constituent profiles the basis was built from.dPIEPotential.convergence_2d_from: inautogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py, swap the@aa.decorators.to_vector_yxdecorator ondPIEPotential.convergence_2d_fromfor@aa.decorators.to_array. The function body returns a scalar field; the vector decorator is a copy-paste typo from the deflections method directly above it.Array2Dfor each method, in the existingtest_autogalaxy/profiles/test_basis.pyandtest_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py.Detailed implementation plan
Affected Repositories
Work Classification
Library (code + tests). Ships via
/ship_library.Branch Survey
Suggested branch:
feature/profile-return-type-fixesWorktree root:
~/Code/PyAutoLabs-wt/profile-return-type-fixes/Routing:
library-dev→/start_library.Implementation Steps
autogalaxy/profiles/basis.py— InBasis.image_2d_list_from, replace theLightProfileLinearplaceholderwith
This makes
Basis.image_2d_from = sum([...])return anArray2Dwhenever every constituent is linear (the MGE case), becauseArray2D.__add__returnsArray2D. The standard-profile path already producesArray2Dinstances from eachimage_2d_fromcall, so the sum is unchanged for non-linear bases.autogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py— Change the decorator ondPIEPotential.convergence_2d_fromfrom:to:
Matches the
dPIEPotentialSph.convergence_2d_fromdecorator stack directly below it, which has always been correct.Add regression tests in the existing test files:
test_autogalaxy/profiles/test_basis.py— add a test that constructs aBasisofLightProfileLinearGaussians and assertsisinstance(basis.image_2d_from(grid), aa.Array2D).test_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py— add a test that constructs adPIEPotentialand assertsisinstance(profile.convergence_2d_from(grid), aa.Array2D). (NOTVectorYX2D.)Run the existing test suite to confirm no regressions on the surrounding behaviour:
Key Files
autogalaxy/profiles/basis.py— wrap linear-profile zero placeholder inArray2Dautogalaxy/profiles/mass/total/dual_pseudo_isothermal_potential.py— fix decoratortest_autogalaxy/profiles/test_basis.py— add regression testtest_autogalaxy/profiles/mass/total/test_dual_pseudo_isothermal_potential.py— add regression testOut of scope
autolens_workspace/scripts/guides/profiles/{light,mass}.py— cosmetic, can stay until the next pass through those scripts.lp_basis/ dPIE-family quirks not specifically listed here.JAX considerations
Basis.image_2d_fromis consumed by downstream Python (Galaxy → Galaxies → Tracer), never the JIT output, so wrapping inArray2Dis safe even inxp=jnpmode (per the CLAUDE.md rule on intermediate-step Array2D wraps).dPIEPotential.convergence_2d_fromis decorated; the decorator stack handles the JAX-vs-numpy path, no extra work needed.Original Prompt
Click to expand starting prompt
Two library bugs surfaced while writing
autolens_workspace/scripts/guides/profiles/light.py(#86 / #176) andmass.py(#178 / #179):Basis.image_2d_fromreturns a rawnumpy.ndarrayinstead of anArray2Dwhen the basis is composed ofLightProfileLinearconstituents. The light.py guide had to wrap the basis in a Galaxy and plot via the galaxy's image to makeaplt.plot_arraywork.Root cause:
Basis.image_2d_list_fromreturnsxp.zeros((grid.shape[0],))forLightProfileLinearprofiles (because their intensity will be solved by the inversion later). Summing those zero ndarrays inimage_2d_fromproduces a raw ndarray rather than anArray2D.dPIEPotential.convergence_2d_fromreturns aVectorYX2Dinstead of anArray2D. The mass.py guide had to skip plotting convergence fordPIEPotentialand usedPIEPotentialSphin the walkthrough instead.Root cause: a typo / copy-paste of the deflections decorator —
@aa.decorators.to_vector_yxis used where@aa.decorators.to_arrayis correct. The function body returns a scalar field (kappa_circ * (1 - asymm_term) + (alpha_circ / grid_radii) * asymm_term) which is not a(y, x)vector.Both fixes are tiny library-code changes with regression tests.