From 6d014aa9832cb560d191d00164fa77b22d37b2fd Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Tue, 10 Mar 2026 15:58:01 +0000 Subject: [PATCH] docs: add module docstrings and improve method docstrings in profiles package - Add module-level docstrings to geometry_profiles, light/abstract, light/standard/sersic, light/linear/abstract, light/operated/abstract, mass/abstract/abstract, and basis explaining what each module contains - Add class docstring to LightProfileOperated explaining the operated-only filtering pattern used throughout the codebase - Add docstrings to abstract MassProfile methods (deflections_yx_2d_from, convergence_2d_from, potential_2d_from, convergence_func, potential_func, mass_integral, ellipticity_rescale) including the lensing physics context - Add deflections_2d_via_potential_2d_from docstring explaining the finite-difference fallback implementation - Improve GeometryProfile.has() docstring with Parameters/Returns sections - Add docstrings to GeometryProfile abstract transform methods - Add docstrings to LightProfile.coefficient_tag, half_light_radius, _intensity Co-Authored-By: Claude Sonnet 4.6 --- autogalaxy/profiles/basis.py | 11 ++ autogalaxy/profiles/geometry_profiles.py | 43 +++++- autogalaxy/profiles/light/abstract.py | 30 ++++ autogalaxy/profiles/light/linear/abstract.py | 14 ++ .../profiles/light/operated/abstract.py | 18 +++ autogalaxy/profiles/light/standard/sersic.py | 15 ++ autogalaxy/profiles/mass/abstract/abstract.py | 146 ++++++++++++++++++ 7 files changed, 276 insertions(+), 1 deletion(-) diff --git a/autogalaxy/profiles/basis.py b/autogalaxy/profiles/basis.py index 71ece9439..ef8936fb3 100644 --- a/autogalaxy/profiles/basis.py +++ b/autogalaxy/profiles/basis.py @@ -1,3 +1,14 @@ +""" +The `Basis` class groups multiple light or mass profiles into a single object that acts like a single profile. + +A basis is typically used for multi-component decompositions of a galaxy's light or mass distribution — for +example a multi-Gaussian expansion (MGE), which represents a galaxy as a sum of many Gaussian profiles, or +a shapelet decomposition. Each component of the basis captures a distinct spatial scale of the galaxy. + +When linear light profiles are used in a basis, their individual intensities are solved simultaneously via a +linear inversion (a single matrix solve), making the inference highly efficient regardless of how many basis +components are included. +""" import numpy as np from typing import Dict, List, Optional, Union diff --git a/autogalaxy/profiles/geometry_profiles.py b/autogalaxy/profiles/geometry_profiles.py index 963ed955d..afaaf981c 100644 --- a/autogalaxy/profiles/geometry_profiles.py +++ b/autogalaxy/profiles/geometry_profiles.py @@ -1,3 +1,11 @@ +""" +Geometry profiles define the spatial geometry of light and mass profiles, including their centre coordinates and +elliptical orientation. + +The `GeometryProfile`, `SphProfile` and `EllProfile` classes provide the base geometric transformations that all +light and mass profiles inherit, including translating a grid to the profile centre and rotating it to the +profile's position angle. +""" import numpy as np from typing import Optional, Tuple, Type @@ -34,14 +42,47 @@ def __eq__(self, other): def has(self, cls: Type) -> bool: """ - Does this instance have an attribute which is of type cls? + Returns `True` if any attribute of this profile is an instance of the input class `cls`, else `False`. + + Parameters + ---------- + cls + The class type to search for amongst the profile's attributes. + + Returns + ------- + bool + `True` if any attribute is an instance of `cls`, else `False`. """ return aa.util.misc.has(values=self.__dict__.values(), cls=cls) def transformed_to_reference_frame_grid_from(self, grid, xp=np, **kwargs): + """ + Transform a grid of (y,x) coordinates to the reference frame of the profile. + + Subclasses override this to perform a translation to the profile centre and, for elliptical profiles, + a rotation to the profile's position angle. + + Parameters + ---------- + grid + The (y, x) coordinates in the original reference frame of the grid. + """ raise NotImplemented() def transformed_from_reference_frame_grid_from(self, grid, xp=np, **kwargs): + """ + Transform a grid of (y,x) coordinates from the reference frame of the profile back to the original + observer reference frame. + + Subclasses override this to reverse the translation to the profile centre and, for elliptical profiles, + the rotation to the profile's position angle. + + Parameters + ---------- + grid + The (y, x) coordinates in the reference frame of the profile. + """ raise NotImplemented() diff --git a/autogalaxy/profiles/light/abstract.py b/autogalaxy/profiles/light/abstract.py index 3ae084825..86f915f51 100644 --- a/autogalaxy/profiles/light/abstract.py +++ b/autogalaxy/profiles/light/abstract.py @@ -1,3 +1,13 @@ +""" +Abstract base classes for all light profiles in **PyAutoGalaxy**. + +A light profile has an analytic function that describes the surface brightness of a galaxy as a function of +(y,x) Cartesian coordinates. Each profile is associated with a spatial geometry (centre and ellipticity) and +an `intensity` normalisation that scales the overall brightness. + +The `LightProfile` class is the root of the light profile hierarchy. All concrete profiles (e.g. `Sersic`, +`Exponential`, `Gaussian`) inherit from it and implement `image_2d_from` and `image_2d_via_radii_from`. +""" import numpy as np from typing import Optional, Tuple @@ -39,6 +49,13 @@ def __init__( @property def coefficient_tag(self) -> str: + """ + A short string tag used to label the intensity coefficient when this profile is used inside a `Basis` + object (e.g. for multi-Gaussian expansion or shapelet decomposition). + + Returns an empty string for standard light profiles, and is overridden by linear light profile subclasses + to label their solved-for intensity coefficient. + """ return "" def image_2d_from( @@ -114,9 +131,22 @@ def luminosity_integral(self, x: np.ndarray) -> np.ndarray: @property def half_light_radius(self) -> float: + """ + The radius that contains half of the total light of the profile (the half-light radius). + + For profiles with an `effective_radius` attribute (e.g. Sersic, Exponential) this returns the + `effective_radius`. Returns `None` for profiles that do not define an effective radius. + """ if hasattr(self, "effective_radius"): return self.effective_radius @property def _intensity(self): + """ + The normalisation intensity of the light profile used internally when evaluating the image. + + For standard light profiles this simply returns `self.intensity`. Linear light profiles override this + property to return 1.0, because their intensity is solved for via a linear inversion rather than being + a free parameter. + """ return self.intensity diff --git a/autogalaxy/profiles/light/linear/abstract.py b/autogalaxy/profiles/light/linear/abstract.py index 44eba6abf..c85f8deb6 100644 --- a/autogalaxy/profiles/light/linear/abstract.py +++ b/autogalaxy/profiles/light/linear/abstract.py @@ -1,3 +1,17 @@ +""" +Abstract base classes for linear light profiles in **PyAutoGalaxy**. + +A linear light profile behaves identically to a standard light profile (e.g. `Sersic`, `Exponential`, +`Gaussian`) except that its `intensity` is not a free parameter — instead, it is solved for analytically via +a linear inversion during every likelihood evaluation. + +This reduces the number of non-linear parameters in a model fit by one per linear profile, which can +substantially speed up non-linear sampling without any loss of accuracy. + +The `LightProfileLinear` class is the abstract base from which all linear light profile variants inherit. +The `LightProfileLinearObjFuncList` subclass additionally supports regularization, allowing the solved +intensities to be penalized by a smoothness prior. +""" import inspect import numpy as np from typing import Dict, List, Optional diff --git a/autogalaxy/profiles/light/operated/abstract.py b/autogalaxy/profiles/light/operated/abstract.py index 7f910e679..c800e818f 100644 --- a/autogalaxy/profiles/light/operated/abstract.py +++ b/autogalaxy/profiles/light/operated/abstract.py @@ -1,2 +1,20 @@ class LightProfileOperated: + """ + Mixin class that marks a light profile as already having had an instrument operation applied to it. + + An "operated" light profile represents emission whose image has already had an operation applied, most + commonly a PSF convolution. This means that when the image of an operated light profile is computed, the + PSF convolution step is skipped — the PSF effect is already baked into the profile itself. + + This pattern is useful for modelling point-source emission (e.g. AGN) or other compact emission where the + PSF profile itself is used directly as the light profile. + + The `operated_only` input to `image_2d_from` methods throughout the codebase controls which light profiles + contribute to an image: + + - `operated_only=None` (default): all light profiles contribute regardless of whether they are operated. + - `operated_only=True`: only `LightProfileOperated` instances contribute; non-operated profiles return zeros. + - `operated_only=False`: only non-operated profiles contribute; `LightProfileOperated` instances return zeros. + """ + pass diff --git a/autogalaxy/profiles/light/standard/sersic.py b/autogalaxy/profiles/light/standard/sersic.py index acc5d3465..95c26fae7 100644 --- a/autogalaxy/profiles/light/standard/sersic.py +++ b/autogalaxy/profiles/light/standard/sersic.py @@ -1,3 +1,18 @@ +""" +Sersic light profiles. + +The Sersic profile is one of the most widely used models for describing the surface brightness of galaxies. +It has the functional form: + + I(r) = I_eff * exp{ -b_n * [(r / r_eff)^(1/n) - 1] } + +where `r_eff` is the effective (half-light) radius, `n` is the Sersic index controlling concentration, and +`b_n` is derived from `n` to ensure that `r_eff` encloses half the total flux. + +Special cases: n=1 is the exponential (disk) profile, n=4 is the de Vaucouleurs (bulge) profile. + +This module provides both elliptical (`Sersic`) and spherical (`SersicSph`) variants. +""" import numpy as np from numpy import seterr diff --git a/autogalaxy/profiles/mass/abstract/abstract.py b/autogalaxy/profiles/mass/abstract/abstract.py index 0006f82c4..ffb1fd9df 100644 --- a/autogalaxy/profiles/mass/abstract/abstract.py +++ b/autogalaxy/profiles/mass/abstract/abstract.py @@ -1,3 +1,17 @@ +""" +Abstract base class for all mass profiles in **PyAutoGalaxy** and **PyAutoLens**. + +A mass profile describes the projected mass distribution of a galaxy and exposes three fundamental lensing +quantities: + +- `deflections_yx_2d_from` — the deflection angles α(θ) that describe how light rays are bent. +- `convergence_2d_from` — the dimensionless surface mass density κ(θ) = Σ(θ) / Σ_cr. +- `potential_2d_from` — the lensing (Shapiro) potential ψ(θ). + +Every other lensing observable (shear, magnification, critical curves, Einstein radius, Fermat potential) can +be derived from these three quantities. See the `autogalaxy.operate.lens_calc` module for the `LensCalc` class +that derives these secondary quantities. +""" import numpy as np from typing import Tuple @@ -25,9 +39,53 @@ def __init__( super().__init__(centre=centre, ell_comps=ell_comps) def deflections_yx_2d_from(self, grid): + """ + Returns the 2D deflection angles of the mass profile from a 2D grid of Cartesian (y,x) coordinates. + + The deflection angle α(θ) at image-plane position θ describes how a light ray is bent by the + gravitational field of the lens. The source-plane position β is then: + + β = θ − α(θ) + + Deflection angles are the single most important output of a mass profile — every other lensing quantity + (convergence, shear, magnification, critical curves, caustics) can be derived from them. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where the deflection angles are evaluated. + + Returns + ------- + aa.VectorYX2D + The (y, x) deflection angles at every coordinate on the input grid. + """ raise NotImplementedError def deflections_2d_via_potential_2d_from(self, grid): + """ + Returns the 2D deflection angles of the mass profile by numerically differentiating the lensing + potential on the input grid. + + This is a fallback implementation that computes deflection angles as the gradient of the potential via + finite differences: + + α_y = ∂ψ/∂y, α_x = ∂ψ/∂x + + Most concrete mass profiles override `deflections_yx_2d_from` with an analytic expression. This + method is provided for cross-checking and for profiles where only the potential is known analytically. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where the deflection angles are evaluated. + + Returns + ------- + aa.Grid2D + The (y, x) deflection angles at every coordinate on the input grid, computed via finite differences + of the lensing potential. + """ potential = self.potential_2d_from(grid=grid) deflections_y_2d = np.gradient( @@ -43,22 +101,110 @@ def deflections_2d_via_potential_2d_from(self, grid): ) def convergence_2d_from(self, grid, xp=np): + """ + Returns the 2D convergence of the mass profile from a 2D grid of Cartesian (y,x) coordinates. + + The convergence κ(θ) is the dimensionless surface mass density of the lens, defined as the projected + surface mass density Σ(θ) divided by the critical surface mass density Σ_cr: + + κ(θ) = Σ(θ) / Σ_cr + + Physically, κ = 1 on the Einstein ring. Regions with κ > 1 produce multiple images. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where the convergence is evaluated. + + Returns + ------- + aa.Array2D + The convergence κ(θ) at every coordinate on the input grid. + """ raise NotImplementedError def convergence_func(self, grid_radius: float) -> float: + """ + Returns the convergence of the mass profile as a function of the radial coordinate. + + This is used to integrate the convergence profile to compute enclosed masses and the Einstein radius. + + Parameters + ---------- + grid_radius + The radial distance from the profile centre at which the convergence is evaluated. + + Returns + ------- + float + The convergence at the input radial distance. + """ raise NotImplementedError def potential_2d_from(self, grid): + """ + Returns the 2D lensing potential of the mass profile from a 2D grid of Cartesian (y,x) coordinates. + + The lensing potential ψ(θ) is the gravitational (Shapiro) time-delay term. It quantifies how much the + passage of light through the gravitational field delays its arrival relative to a straight-line path in + empty space. + + The potential enters directly into the Fermat potential: + + φ(θ) = ½ |θ − β|² − ψ(θ) + + which governs time delays between multiple lensed images of the same source. + + Parameters + ---------- + grid + The 2D (y, x) coordinates where the lensing potential is evaluated. + + Returns + ------- + aa.Array2D + The lensing potential ψ(θ) at every coordinate on the input grid. + """ raise NotImplementedError def potential_func(self, u, y, x): + """ + Returns the integrand of the lensing potential at a single point, used in numerical integration schemes + for computing the potential from the mass profile's convergence. + + Parameters + ---------- + u + The integration variable. + y + The y-coordinate of the point at which the potential is evaluated. + x + The x-coordinate of the point at which the potential is evaluated. + """ raise NotImplementedError def mass_integral(self, x, xp=np): + """ + Integrand used by `mass_angular_within_circle_from` to compute the total projected mass within a circle. + + This integrates 2π r κ(r) to give the enclosed convergence (dimensionless mass) at radius `x`. + + Parameters + ---------- + x + The radial coordinate at which the integrand is evaluated. + """ return 2 * xp.pi * x * self.convergence_func(grid_radius=aa.ArrayIrregular(x)) @property def ellipticity_rescale(self): + """ + A rescaling factor applied to account for the ellipticity of the mass profile when computing the + Einstein radius from the average convergence equals unity criterion. + + For a spherical profile this is 1.0. Elliptical profiles return a factor that maps the elliptical + enclosed mass to an equivalent circular Einstein radius. + """ return NotImplementedError() def mass_angular_within_circle_from(self, radius: float):