Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions autogalaxy/profiles/light/linear/shapelets/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ def __init__(
n: int,
m: int,
centre: Tuple[float, float] = (0.0, 0.0),
ell_comps: Tuple[float, float] = (0.0, 0.0),
q: float = 1.0,
phi: float = 0.0,
beta: float = 1.0,
):
"""
Expand All @@ -33,8 +34,11 @@ def __init__(
The m order of the shapelets basis function in the x-direction.
centre
The (y,x) arc-second coordinates of the profile (shapelet) centre.
ell_comps
The first and second ellipticity components of the elliptical coordinate system.
q
The axis-ratio of the elliptical coordinate system, where a perfect circle has q=1.0.
phi
The position angle (in degrees) of the elliptical coordinate system, measured counter-clockwise from the
positive x-axis.
intensity
Overall intensity normalisation of the light profile (units are dimensionless and derived from the data
the light profile's image is compared too, which is expected to be electrons per second).
Expand All @@ -43,7 +47,7 @@ def __init__(
"""

super().__init__(
n=n, m=m, centre=centre, ell_comps=ell_comps, beta=beta, intensity=1.0
n=n, m=m, centre=centre, q=q, phi=phi, beta=beta, intensity=1.0
)


Expand All @@ -53,6 +57,7 @@ def __init__(
n: int,
m: int,
centre: Tuple[float, float] = (0.0, 0.0),
phi: float = 0.0,
beta: float = 1.0,
):
"""
Expand All @@ -74,8 +79,11 @@ def __init__(
The order of the shapelets basis function in the x-direction.
centre
The (y,x) arc-second coordinates of the profile (shapelet) centre.
phi
The position angle (in degrees) of the elliptical coordinate system, measured counter-clockwise from the
positive x-axis.
beta
The characteristic length scale of the shapelet basis function, defined in arc-seconds.
"""

super().__init__(n=n, m=m, centre=centre, ell_comps=(0.0, 0.0), beta=beta)
super().__init__(n=n, m=m, centre=centre, q=1.0, phi=phi, beta=beta)
98 changes: 82 additions & 16 deletions autogalaxy/profiles/light/standard/shapelets/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,73 @@

import autoarray as aa


from autogalaxy.profiles.light.decorators import (
check_operated_only,
)
from autogalaxy.profiles.light.standard.shapelets.abstract import AbstractShapelet

import jax.numpy as jnp
from jax.scipy.special import gammaln

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded import of jax.numpy as jnp at the module level forces a dependency on JAX for all users of this module, even if they are not using JAX. This breaks the existing pattern in the codebase where xp is used as a parameter to switch between numpy and JAX. Consider making JAX an optional dependency and importing it conditionally, or implementing a fallback to scipy.special.genlaguerre when JAX is not available.

Copilot uses AI. Check for mistakes.

def genlaguerre_jax(n, alpha, x):
"""
Generalized (associated) Laguerre polynomial L_n^alpha(x)
calculated using the explicit summation formula, optimized for JAX vectorization.

Parameters:
n (int): Degree of the polynomial (static Python integer).
alpha (Numeric): Parameter alpha > -1.
x (Array): Input array (evaluation points).
"""
# 0. Input Validation (Requires static Python int n)
if not isinstance(n, int) or n < 0:
# Use Python's math.isnan/isinf check if n is float, otherwise type error
raise ValueError(f"Degree n must be a non-negative Python integer (static), got {n}.")

# Base Case L0
if n == 0:
return jnp.ones_like(x)

# 1. Generate k values for summation range [0, 1, 2, ..., n]
k_values = jnp.arange(n + 1) # (n+1,)

# 2. Reshape inputs for broadcasting (x: (M, 1), k: (1, n+1))
x_expanded = jnp.expand_dims(x, axis=-1)
k_values_expanded = jnp.expand_dims(k_values, axis=0)

# --- A. Binomial Factor (BF) Calculation ---
# BF = exp( log( (n+alpha)! / ((n-k)! * (alpha+k)!) ) )

log_N_plus_alpha_fact = gammaln(n + alpha + 1)

log_BF_k = (
log_N_plus_alpha_fact
- gammaln(n - k_values + 1) # log( (n-k)! )
- gammaln(alpha + k_values + 1) # log( (alpha+k)! )
)

BF_k = jnp.exp(log_BF_k) # Shape: (n+1,)

# --- B. Term Factor (TF) Calculation ---
# TF = (-x)^k / k!

# Note: jnp.math.gamma(k_values + 1) is equivalent to k! in log-gamma space
TF_k = jnp.power(-x_expanded, k_values_expanded) / jnp.exp(gammaln(k_values_expanded + 1))
# TF_k Shape: (M, n+1)

# --- C. Final Summation ---
# Sum over the last axis (axis=1), which corresponds to k
# BF_k broadcasts over the M dimension of TF_k
return jnp.sum(BF_k * TF_k, axis=1)
Comment on lines +13 to +69

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function uses hardcoded jnp operations instead of respecting the xp parameter that is passed to image_2d_from. This means the function will fail if numpy is expected (when xp=np). The genlaguerre_jax function should either use the xp parameter or there should be conditional logic to choose between a JAX and NumPy implementation.

Copilot uses AI. Check for mistakes.

class ShapeletPolar(AbstractShapelet):
def __init__(
self,
n: int,
m: int,
centre: Tuple[float, float] = (0.0, 0.0),
ell_comps: Tuple[float, float] = (0.0, 0.0),
q: float = 1.0,
phi: float = 0.0,
intensity: float = 1.0,
beta: float = 1.0,
):
Expand All @@ -39,20 +92,25 @@ def __init__(
The m order of the shapelets basis function in the x-direction.
centre
The (y,x) arc-second coordinates of the profile (shapelet) centre.
ell_comps
The first and second ellipticity components of the elliptical coordinate system.
q
The axis-ratio of the elliptical coordinate system, where a perfect circle has q=1.0.
phi
The position angle (in degrees) of the elliptical coordinate system, measured counter-clockwise from the
positive x-axis.
intensity
Overall intensity normalisation of the light profile (units are dimensionless and derived from the data
the light profile's image is compared too, which is expected to be electrons per second).
beta
The characteristic length scale of the shapelet basis function, defined in arc-seconds.
"""

self.n = n
self.m = m
self.n = int(n)
self.m = int(m)
self.phi = float(phi)
self.q = float(q)

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameters q and phi are stored as instance attributes but they are not being used by the parent class. Since the parent class EllProfile expects ell_comps to define the elliptical geometry and uses it in transformation methods, replacing the parameter convention breaks the integration with the parent class geometry system. The code should either maintain ell_comps compatibility or the AbstractShapelet base class needs to be updated to support the new parameter convention.

Copilot uses AI. Check for mistakes.

super().__init__(
centre=centre, ell_comps=ell_comps, beta=beta, intensity=intensity
centre=centre, beta=beta, intensity=intensity
)
Comment on lines 116 to 118

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent class AbstractShapelet requires ell_comps as a parameter, but this change removes it from the super().init() call. This will cause an error because the parent class LightProfile (through AbstractShapelet) expects ell_comps to be provided. The parent's init signature is def __init__(self, centre, ell_comps, intensity, beta), so omitting ell_comps will result in a TypeError.

Copilot uses AI. Check for mistakes.

@property
Expand All @@ -62,7 +120,6 @@ def coefficient_tag(self) -> str:
@aa.over_sample
@aa.grid_dec.to_array
@check_operated_only
@aa.grid_dec.transform
def image_2d_from(
self,
grid: aa.type.Grid2DLike,
Expand All @@ -86,11 +143,11 @@ def image_2d_from(
image
The image of the Polar Shapelet evaluated at every (y,x) coordinate on the transformed grid.
"""
from scipy.special import genlaguerre
from jax.scipy.special import factorial

laguerre = genlaguerre(n=(self.n - xp.abs(self.m)) / 2.0, alpha=xp.abs(self.m))

grid = aa.util.geometry.transform_grid_2d_to_reference_frame(
grid_2d=grid.array, centre=self.centre, angle=self.phi, xp=xp

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator @aa.grid_dec.transform has been removed from the image_2d_from method. This decorator typically handles the transformation of grid coordinates to the profile's reference frame. By removing it and manually calling transform_grid_2d_to_reference_frame, the code may not properly integrate with the parent class's geometry system and could produce incorrect results when the profile has elliptical components.

Suggested change
grid_2d=grid.array, centre=self.centre, angle=self.phi, xp=xp
grid_2d=grid, centre=self.centre, angle=self.phi, xp=xp

Copilot uses AI. Check for mistakes.
)

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual transformation uses self.phi directly as the rotation angle, but this doesn't account for the elliptical coordinate system transformations that the parent class handles. The removed @aa.grid_dec.transform decorator would have applied the proper transformations including centre translation, rotation, and elliptical scaling based on ell_comps. The manual approach bypasses this and may produce incorrect results.

Copilot uses AI. Check for mistakes.
const = (
((-1) ** ((self.n - xp.abs(self.m)) // 2))
* xp.sqrt(
Expand All @@ -100,10 +157,14 @@ def image_2d_from(
/ self.beta
/ xp.sqrt(xp.pi)
)
rsq = (grid[:, 0] ** 2 + (grid[:, 1]/self.q) ** 2) / self.beta**2

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The axis ratio self.q is applied inconsistently in the calculation. The code divides grid[:, 1] by self.q when computing rsq, but this doesn't account for the full elliptical transformation. In an elliptical coordinate system, both coordinates should be scaled properly, and the transformation should be applied after rotation to the major/minor axis frame. The current implementation may not correctly represent an elliptical shapelet.

Copilot uses AI. Check for mistakes.
theta = xp.arctan2(grid[:, 1], grid[:, 0])

m_abs = abs(self.m)
n_laguerre = (self.n - m_abs) // 2
laguerre_vals = genlaguerre_jax(n=n_laguerre, alpha=m_abs, x=rsq)

rsq = (grid.array[:, 0] ** 2 + grid.array[:, 1] ** 2) / self.beta**2
theta = xp.arctan2(grid.array[:, 1], grid.array[:, 0])
radial = rsq ** (abs(self.m / 2.0)) * xp.exp(-rsq / 2.0) * laguerre(rsq)
radial = rsq ** (xp.abs(self.m) / 2.0) * xp.exp(-rsq / 2.0) * laguerre_vals

if self.m == 0:
azimuthal = 1
Expand All @@ -112,7 +173,7 @@ def image_2d_from(
else:
azimuthal = xp.cos((-1) * self.m * theta)

return const * radial * azimuthal
return self._intensity * const * radial * azimuthal


class ShapeletPolarSph(ShapeletPolar):
Expand All @@ -121,6 +182,7 @@ def __init__(
n: int,
m: int,
centre: Tuple[float, float] = (0.0, 0.0),
phi: float = 0.0,
intensity: float = 1.0,
beta: float = 1.0,
):
Expand All @@ -143,6 +205,9 @@ def __init__(
The order of the shapelets basis function in the x-direction.
centre
The (y,x) arc-second coordinates of the profile (shapelet) centre.
phi
The position angle (in degrees) of the elliptical coordinate system, measured counter-clockwise from the
positive x-axis.
intensity
Overall intensity normalisation of the light profile (units are dimensionless and derived from the data
the light profile's image is compared too, which is expected to be electrons per second).
Expand All @@ -154,7 +219,8 @@ def __init__(
n=n,
m=m,
centre=centre,
ell_comps=(0.0, 0.0),
q=1.0,
phi=phi,
intensity=intensity,
beta=beta,
)
Loading
Loading