From d0c2e32f2a9d4bd160ce22f0761be122b2015ee6 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Mon, 18 May 2026 13:44:07 +0100 Subject: [PATCH] docs: add light profiles guide at scripts/guides/profiles/light.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new single-page tour of every light profile shipped by PyAutoGalaxy: the five namespaces (ag.lp, ag.lp_linear, ag.lp_operated, ag.lp_basis, ag.lp_snr), a detailed Sersic example, a dedicated section for the newly merged SersicMultipole and GaussianMultipole profiles (PRs #420 / #421), a Basis section demonstrating a 4-Gaussian MGE composition, and a compact image_2d_from walkthrough of every remaining standard profile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .script_sizes.json | 217 +++++----- scripts/guides/profiles/__init__.py | 0 scripts/guides/profiles/light.py | 600 ++++++++++++++++++++++++++++ 3 files changed, 717 insertions(+), 100 deletions(-) create mode 100644 scripts/guides/profiles/__init__.py create mode 100644 scripts/guides/profiles/light.py diff --git a/.script_sizes.json b/.script_sizes.json index 99bd716b..99cf01e2 100644 --- a/.script_sizes.json +++ b/.script_sizes.json @@ -1,149 +1,166 @@ { "scripts/ellipse/__init__.py": 0, - "scripts/ellipse/database.py": 10772, - "scripts/ellipse/fit.py": 12802, - "scripts/ellipse/modeling.py": 22570, - "scripts/ellipse/multipoles.py": 15674, - "scripts/ellipse/simulator.py": 4855, + "scripts/ellipse/database.py": 10744, + "scripts/ellipse/fit.py": 12822, + "scripts/ellipse/modeling.py": 23259, + "scripts/ellipse/multipoles.py": 15704, + "scripts/ellipse/simulator.py": 4868, "scripts/guides/__init__.py": 0, "scripts/guides/advanced/__init__.py": 0, - "scripts/guides/advanced/over_sampling.py": 16023, - "scripts/guides/data_structures.py": 12563, - "scripts/guides/galaxies.py": 15890, + "scripts/guides/advanced/over_sampling.py": 16006, + "scripts/guides/data_structures.py": 12585, + "scripts/guides/galaxies.py": 15920, "scripts/guides/hpc/__init__.py": 0, - "scripts/guides/hpc/example_cpu_and_gpu.py": 13124, + "scripts/guides/hpc/example_cpu_and_gpu.py": 13106, "scripts/guides/modeling/__init__.py": 0, - "scripts/guides/modeling/bug_fix.py": 4868, - "scripts/guides/modeling/chaining.py": 12759, - "scripts/guides/modeling/cookbook.py": 9185, - "scripts/guides/modeling/customize.py": 6958, - "scripts/guides/modeling/searches.py": 11764, + "scripts/guides/modeling/bug_fix.py": 4837, + "scripts/guides/modeling/chaining.py": 12779, + "scripts/guides/modeling/cookbook.py": 9193, + "scripts/guides/modeling/customize.py": 6964, + "scripts/guides/modeling/searches.py": 11776, "scripts/guides/plot/__init__.py": 0, "scripts/guides/plot/advanced/__init__.py": 0, - "scripts/guides/plot/advanced/plotters_pixelization.py": 4848, + "scripts/guides/plot/advanced/plotters_pixelization.py": 4862, "scripts/guides/plot/examples/__init__.py": 0, "scripts/guides/plot/examples/mat_plot.py": 4907, - "scripts/guides/plot/examples/plotters.py": 9753, + "scripts/guides/plot/examples/plotters.py": 9777, "scripts/guides/plot/examples/searches.py": 18690, - "scripts/guides/plot/examples/visuals.py": 5793, - "scripts/guides/plot/simulator.py": 4450, - "scripts/guides/plot/start_here.py": 4422, + "scripts/guides/plot/examples/visuals.py": 5811, + "scripts/guides/plot/simulator.py": 4463, + "scripts/guides/plot/start_here.py": 4418, + "scripts/guides/profiles/__init__.py": 0, + "scripts/guides/profiles/light.py": 23900, "scripts/guides/results/__init__.py": 0, + "scripts/guides/results/_quick_fit.py": 2554, "scripts/guides/results/aggregator/__init__.py": 0, - "scripts/guides/results/aggregator/data_fitting.py": 8026, - "scripts/guides/results/aggregator/galaxies_fit.py": 5596, + "scripts/guides/results/aggregator/data_fitting.py": 8037, + "scripts/guides/results/aggregator/galaxies_fit.py": 6457, "scripts/guides/results/aggregator/interferometer.py": 1850, - "scripts/guides/results/aggregator/models.py": 10129, - "scripts/guides/results/aggregator/queries.py": 6008, - "scripts/guides/results/aggregator/samples.py": 20061, - "scripts/guides/results/aggregator/samples_via_aggregator.py": 19322, + "scripts/guides/results/aggregator/models.py": 10140, + "scripts/guides/results/aggregator/queries.py": 6019, + "scripts/guides/results/aggregator/samples.py": 20948, + "scripts/guides/results/aggregator/samples_via_aggregator.py": 19358, "scripts/guides/results/database/__init__.py": 0, "scripts/guides/results/database/simulators/__init__.py": 0, - "scripts/guides/results/database/simulators/light_sersic_exp__0.py": 6730, - "scripts/guides/results/database/simulators/light_sersic_exp__1.py": 6729, - "scripts/guides/results/database/simulators/light_sersic_exp__2.py": 6728, - "scripts/guides/results/database/start_here.py": 14256, - "scripts/guides/results/start_here.py": 25225, + "scripts/guides/results/database/simulators/light_sersic_exp__0.py": 6747, + "scripts/guides/results/database/simulators/light_sersic_exp__1.py": 6746, + "scripts/guides/results/database/simulators/light_sersic_exp__2.py": 6745, + "scripts/guides/results/database/start_here.py": 14279, + "scripts/guides/results/start_here.py": 26414, "scripts/guides/results/workflow/__init__.py": 0, - "scripts/guides/results/workflow/csv_make.py": 14185, - "scripts/guides/results/workflow/fits_make.py": 15320, - "scripts/guides/results/workflow/png_make.py": 15313, + "scripts/guides/results/workflow/csv_make.py": 14207, + "scripts/guides/results/workflow/fits_make.py": 15338, + "scripts/guides/results/workflow/png_make.py": 15335, "scripts/guides/units/__init__.py": 0, - "scripts/guides/units/cosmology.py": 4330, - "scripts/guides/units/flux.py": 5445, + "scripts/guides/units/cosmology.py": 4338, + "scripts/guides/units/flux.py": 5449, "scripts/imaging/__init__.py": 0, - "scripts/imaging/data_preparation.py": 15824, + "scripts/imaging/data_preparation.py": 15844, "scripts/imaging/data_preparation/__init__.py": 0, "scripts/imaging/data_preparation/examples/__init__.py": 0, - "scripts/imaging/data_preparation/examples/data.py": 9289, - "scripts/imaging/data_preparation/examples/noise_map.py": 5499, + "scripts/imaging/data_preparation/examples/data.py": 9298, + "scripts/imaging/data_preparation/examples/noise_map.py": 5506, "scripts/imaging/data_preparation/examples/optional/__init__.py": 0, - "scripts/imaging/data_preparation/examples/optional/extra_galaxies_centres.py": 4593, - "scripts/imaging/data_preparation/examples/optional/info.py": 2844, + "scripts/imaging/data_preparation/examples/optional/extra_galaxies_centres.py": 4599, + "scripts/imaging/data_preparation/examples/optional/info.py": 2838, "scripts/imaging/data_preparation/examples/optional/light_centre.py": 3311, "scripts/imaging/data_preparation/examples/optional/mask.py": 5094, - "scripts/imaging/data_preparation/examples/optional/mask_extra_galaxies.py": 5533, - "scripts/imaging/data_preparation/examples/psf.py": 6845, + "scripts/imaging/data_preparation/examples/optional/mask_extra_galaxies.py": 5537, + "scripts/imaging/data_preparation/examples/psf.py": 6550, "scripts/imaging/data_preparation/gui/__init__.py": 0, - "scripts/imaging/data_preparation/gui/extra_galaxies_centres.py": 3524, - "scripts/imaging/data_preparation/gui/light_centre.py": 3113, - "scripts/imaging/data_preparation/gui/mask.py": 2169, - "scripts/imaging/data_preparation/gui/mask_extra_galaxies.py": 3435, + "scripts/imaging/data_preparation/gui/extra_galaxies_centres.py": 3532, + "scripts/imaging/data_preparation/gui/light_centre.py": 3121, + "scripts/imaging/data_preparation/gui/mask.py": 2175, + "scripts/imaging/data_preparation/gui/mask_extra_galaxies.py": 3444, "scripts/imaging/data_preparation/manual/__init__.py": 0, "scripts/imaging/data_preparation/manual/mask_irregular.py": 2910, - "scripts/imaging/data_preparation/start_here.py": 15373, + "scripts/imaging/data_preparation/start_here.py": 15392, "scripts/imaging/features/__init__.py": 0, "scripts/imaging/features/extra_galaxies/__init__.py": 0, - "scripts/imaging/features/extra_galaxies/modeling.py": 17244, - "scripts/imaging/features/extra_galaxies/simulator.py": 8906, + "scripts/imaging/features/extra_galaxies/modeling.py": 19978, + "scripts/imaging/features/extra_galaxies/simulator.py": 9584, "scripts/imaging/features/linear_light_profiles/__init__.py": 0, - "scripts/imaging/features/linear_light_profiles/fit.py": 8180, - "scripts/imaging/features/linear_light_profiles/likelihood_function.py": 23944, - "scripts/imaging/features/linear_light_profiles/modeling.py": 15394, + "scripts/imaging/features/linear_light_profiles/fit.py": 8193, + "scripts/imaging/features/linear_light_profiles/likelihood_function.py": 23982, + "scripts/imaging/features/linear_light_profiles/modeling.py": 15470, "scripts/imaging/features/multi_gaussian_expansion/__init__.py": 0, - "scripts/imaging/features/multi_gaussian_expansion/fit.py": 12775, - "scripts/imaging/features/multi_gaussian_expansion/likelihood_function.py": 30231, - "scripts/imaging/features/multi_gaussian_expansion/modeling.py": 15367, - "scripts/imaging/features/multi_gaussian_expansion/simulator.py": 6811, + "scripts/imaging/features/multi_gaussian_expansion/fit.py": 12791, + "scripts/imaging/features/multi_gaussian_expansion/likelihood_function.py": 30273, + "scripts/imaging/features/multi_gaussian_expansion/modeling.py": 15404, + "scripts/imaging/features/multi_gaussian_expansion/simulator.py": 6824, "scripts/imaging/features/operated_light_profile/__init__.py": 0, - "scripts/imaging/features/operated_light_profile/modeling.py": 9082, - "scripts/imaging/features/operated_light_profile/simulator.py": 4803, + "scripts/imaging/features/operated_light_profile/modeling.py": 9104, + "scripts/imaging/features/operated_light_profile/simulator.py": 4816, "scripts/imaging/features/pixelization/__init__.py": 0, - "scripts/imaging/features/pixelization/fit.py": 16827, - "scripts/imaging/features/pixelization/likelihood_function.py": 35526, - "scripts/imaging/features/pixelization/modeling.py": 20012, - "scripts/imaging/features/pixelization/source_science.py": 6288, + "scripts/imaging/features/pixelization/fit.py": 16845, + "scripts/imaging/features/pixelization/likelihood_function.py": 35586, + "scripts/imaging/features/pixelization/modeling.py": 20038, + "scripts/imaging/features/pixelization/source_science.py": 6308, "scripts/imaging/features/shapelets/__init__.py": 0, - "scripts/imaging/features/shapelets/fit.py": 15693, - "scripts/imaging/features/shapelets/modeling.py": 16653, - "scripts/imaging/features/simulator_manual_signal_to_noise.py": 6379, + "scripts/imaging/features/shapelets/fit.py": 15718, + "scripts/imaging/features/shapelets/modeling.py": 16682, + "scripts/imaging/features/simulator_manual_signal_to_noise.py": 6392, "scripts/imaging/features/sky_background/__init__.py": 0, - "scripts/imaging/features/sky_background/fit.py": 6074, - "scripts/imaging/features/sky_background/modeling.py": 8561, - "scripts/imaging/features/sky_background/simulator.py": 4934, - "scripts/imaging/fit.py": 15512, - "scripts/imaging/likelihood_function.py": 13685, - "scripts/imaging/modeling.py": 23205, - "scripts/imaging/simulator.py": 6446, - "scripts/imaging/simulator_sample.py": 6356, - "scripts/imaging/simulator_sersic.py": 4932, - "scripts/imaging/start_here.py": 19926, + "scripts/imaging/features/sky_background/fit.py": 6086, + "scripts/imaging/features/sky_background/modeling.py": 8585, + "scripts/imaging/features/sky_background/simulator.py": 4947, + "scripts/imaging/fit.py": 15542, + "scripts/imaging/likelihood_function.py": 13717, + "scripts/imaging/modeling.py": 24010, + "scripts/imaging/simulator.py": 6461, + "scripts/imaging/simulator_sample.py": 6371, + "scripts/imaging/simulator_sersic.py": 4946, + "scripts/imaging/start_here.py": 19954, "scripts/interferometer/__init__.py": 0, "scripts/interferometer/casa_reduction.py": 7259, - "scripts/interferometer/data_preparation.py": 12068, + "scripts/interferometer/data_preparation.py": 12090, "scripts/interferometer/data_preparation/__init__.py": 0, "scripts/interferometer/features/__init__.py": 0, + "scripts/interferometer/features/extra_galaxies/__init__.py": 0, + "scripts/interferometer/features/extra_galaxies/modeling.py": 12463, + "scripts/interferometer/features/extra_galaxies/simulator.py": 6626, + "scripts/interferometer/features/linear_light_profiles/__init__.py": 0, + "scripts/interferometer/features/linear_light_profiles/fit.py": 8791, + "scripts/interferometer/features/linear_light_profiles/likelihood_function.py": 19662, + "scripts/interferometer/features/linear_light_profiles/modeling.py": 18523, + "scripts/interferometer/features/multi_gaussian_expansion/__init__.py": 0, + "scripts/interferometer/features/multi_gaussian_expansion/fit.py": 7794, + "scripts/interferometer/features/multi_gaussian_expansion/likelihood_function.py": 16610, + "scripts/interferometer/features/multi_gaussian_expansion/modeling.py": 13800, "scripts/interferometer/features/pixelization/__init__.py": 0, - "scripts/interferometer/features/pixelization/fit.py": 20545, - "scripts/interferometer/features/pixelization/likelihood_function.py": 38065, - "scripts/interferometer/features/pixelization/many_visibilities_preparation.py": 8624, - "scripts/interferometer/features/pixelization/modeling.py": 19565, - "scripts/interferometer/features/pixelization/source_science.py": 6571, - "scripts/interferometer/fit.py": 10918, - "scripts/interferometer/likelihood_function.py": 14952, - "scripts/interferometer/modeling.py": 18250, - "scripts/interferometer/simulator.py": 6552, - "scripts/interferometer/start_here.py": 17543, + "scripts/interferometer/features/pixelization/fit.py": 20587, + "scripts/interferometer/features/pixelization/likelihood_function.py": 38127, + "scripts/interferometer/features/pixelization/many_visibilities_preparation.py": 8638, + "scripts/interferometer/features/pixelization/modeling.py": 19610, + "scripts/interferometer/features/pixelization/source_science.py": 6577, + "scripts/interferometer/features/shapelets/__init__.py": 0, + "scripts/interferometer/features/shapelets/fit.py": 6070, + "scripts/interferometer/features/shapelets/modeling.py": 11385, + "scripts/interferometer/fit.py": 10865, + "scripts/interferometer/likelihood_function.py": 14986, + "scripts/interferometer/modeling.py": 18769, + "scripts/interferometer/simulator.py": 6867, + "scripts/interferometer/start_here.py": 18641, "scripts/multi/__init__.py": 0, "scripts/multi/features/__init__.py": 0, "scripts/multi/features/dataset_offsets/__init__.py": 0, - "scripts/multi/features/dataset_offsets/modeling.py": 10113, - "scripts/multi/features/dataset_offsets/simulator.py": 7709, + "scripts/multi/features/dataset_offsets/modeling.py": 10139, + "scripts/multi/features/dataset_offsets/simulator.py": 7726, "scripts/multi/features/imaging_and_interferometer/__init__.py": 0, - "scripts/multi/features/imaging_and_interferometer/modeling.py": 6761, - "scripts/multi/features/imaging_and_interferometer/simulator.py": 5696, + "scripts/multi/features/imaging_and_interferometer/modeling.py": 6781, + "scripts/multi/features/imaging_and_interferometer/simulator.py": 5707, "scripts/multi/features/one_by_one/__init__.py": 0, - "scripts/multi/features/one_by_one/modeling.py": 9926, + "scripts/multi/features/one_by_one/modeling.py": 9952, "scripts/multi/features/pixelization/__init__.py": 0, - "scripts/multi/features/pixelization/modeling.py": 7793, + "scripts/multi/features/pixelization/modeling.py": 7815, "scripts/multi/features/same_wavelength/__init__.py": 0, - "scripts/multi/features/same_wavelength/modeling.py": 7006, - "scripts/multi/features/same_wavelength/simulator.py": 6332, + "scripts/multi/features/same_wavelength/modeling.py": 7024, + "scripts/multi/features/same_wavelength/simulator.py": 6345, "scripts/multi/features/wavelength_dependence/__init__.py": 0, - "scripts/multi/features/wavelength_dependence/modeling.py": 9732, - "scripts/multi/features/wavelength_dependence/simulator.py": 8609, - "scripts/multi/modeling.py": 12831, - "scripts/multi/plot.py": 3841, - "scripts/multi/simulator.py": 6581, - "scripts/multi/start_here.py": 14972 + "scripts/multi/features/wavelength_dependence/modeling.py": 9756, + "scripts/multi/features/wavelength_dependence/simulator.py": 8628, + "scripts/multi/modeling.py": 12861, + "scripts/multi/plot.py": 3851, + "scripts/multi/simulator.py": 6596, + "scripts/multi/start_here.py": 14998 } diff --git a/scripts/guides/profiles/__init__.py b/scripts/guides/profiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/guides/profiles/light.py b/scripts/guides/profiles/light.py new file mode 100644 index 00000000..938c740d --- /dev/null +++ b/scripts/guides/profiles/light.py @@ -0,0 +1,600 @@ +""" +Light Profiles +============== + +This guide is the single-page tour of every light profile **PyAutoGalaxy** ships with: how to +construct each one, how to evaluate its image on a grid, how to compose it into a model, and how +to retrieve an instance from that model. Once you have read this guide you should be able to +recognise every profile referenced by the modelling examples and the API reference, and you +should know which family (standard / linear / operated / multipole / basis) any given profile +belongs to. + +The guide is deliberately broad rather than deep — for each family it shows the *shape* of the +API and points you at the relevant `features/` package for the workflow details. + +__Contents__ + +- **Overview & Docs URL:** Where the canonical API reference lives. +- **All Light Profiles (Survey):** A high-level run-through of every profile in `ag.lp.*` and + the related namespaces, without yet evaluating any images. +- **Detailed Example: Sersic Image:** Build a `Grid2D`, instantiate `ag.lp.Sersic`, evaluate + `image_2d_from`, plot it. +- **Linear Light Profiles:** One-line API for `ag.lp_linear.*` — intensity solved by inversion. +- **Operated Light Profiles:** One-line API for `ag.lp_operated.*` — emission post PSF. +- **Multipole Light Profiles:** The newer `SersicMultipole` and `GaussianMultipole`, with the + m=3 / m=4 Fourier perturbation on the eccentric radius explained and plotted. +- **Basis:** The grouping object that lets many profiles behave as a single composite — the + building block of Multi-Gaussian Expansion (MGE) and shapelet decompositions. +- **Light Profile in a Model:** Wrap a profile in `af.Model`, compose it into an `af.Collection` + via a `Galaxy`, inspect the model info. +- **Model Instance from Light Profile:** Realise an instance from the model's prior medians and + evaluate `image_2d_from` on it. +- **Remaining Profiles Walkthrough:** Compact `image_2d_from` block for every standard profile + not yet shown, emphasising the API is the same as the Sersic example above. + +__Units__ + +In this guide, all quantities use **PyAutoGalaxy**'s internal unit coordinates: spatial +coordinates in arc-seconds, luminosities in electrons per second, and mass quantities (e.g. +convergence) are dimensionless. + +The `guides/units_and_cosmology.ipynb` guide illustrates how to convert these to physical +quantities (kiloparsecs, magnitudes, solar masses). + +__Data Structures__ + +Images returned by `image_2d_from` are wrapped in **PyAutoGalaxy**'s `Array2D` data structure +with `slim` and `native` views. The `guides/data_structures.py` guide covers this in detail; +here we only use the default `slim` 1D representation when printing values. + +__Docs URL__ + +The published API reference for these classes lives at: + + https://pyautogalaxy.readthedocs.io/en/latest/api/light.html + +The autosummary on that page is the authoritative list of every public light-profile class. +This guide mirrors it section-by-section, so a class shown here as `ag.lp.SersicCore` is +documented there under the `Standard [ag.lp]` autosummary, and so on for `ag.lp_linear`, +`ag.lp_operated`, `ag.lp_basis`. +""" + +# from autoconf import setup_notebook; setup_notebook() + +import autofit as af +import autogalaxy as ag +import autogalaxy.plot as aplt + + +""" +__Grid__ + +To evaluate the image of any light profile we need a 2D Cartesian grid of (y,x) coordinates. +We build a 100x100 grid here at a 0.05" pixel scale — used by every section below. +""" +grid = ag.Grid2D.uniform( + shape_native=(100, 100), + pixel_scales=0.05, +) + +""" +__All Light Profiles (Survey)__ + +**PyAutoGalaxy** groups light profiles into five namespaces, each with a clear purpose: + +- `ag.lp.*` — *Standard* parametric profiles. `intensity` is a free model parameter. +- `ag.lp_linear.*` — *Linear* profiles. `intensity` is removed from the model and instead + solved analytically via a linear matrix inversion during each likelihood evaluation. +- `ag.lp_operated.*` — *Operated* profiles representing emission that has already had an + instrument operation (e.g. PSF convolution) applied to it; `operated_only` on the fit + classes controls inclusion. +- `ag.lp_basis.Basis` — A grouping object that bundles multiple light profiles into a single + composite profile (e.g. an MGE built from many Gaussians). +- `ag.lp_snr.*` — Standard profiles parameterised by *signal-to-noise ratio* rather than + intensity; useful when simulating a dataset with a target SNR. Not covered further in this + guide, but shares the API of the Standard profiles. + +Below we construct each standard profile with default parameters. No `image_2d_from` is +evaluated yet — that comes in the next section. The goal here is purely a catalogue of what +is available, in the same order they appear in the API reference. +""" +# Sersic family +sersic = ag.lp.Sersic() +sersic_sph = ag.lp.SersicSph() +sersic_core = ag.lp.SersicCore() +sersic_core_sph = ag.lp.SersicCoreSph() +sersic_multipole = ag.lp.SersicMultipole() + +# Exponential family (Sersic with sersic_index fixed to 1) +exponential = ag.lp.Exponential() +exponential_sph = ag.lp.ExponentialSph() +exponential_core = ag.lp.ExponentialCore() +exponential_core_sph = ag.lp.ExponentialCoreSph() + +# de Vaucouleurs (Sersic with sersic_index fixed to 4) +dev_vaucouleurs = ag.lp.DevVaucouleurs() +dev_vaucouleurs_sph = ag.lp.DevVaucouleursSph() + +# Gaussian / Moffat / Multipole-Gaussian +gaussian = ag.lp.Gaussian() +gaussian_sph = ag.lp.GaussianSph() +gaussian_multipole = ag.lp.GaussianMultipole() +moffat = ag.lp.Moffat() +moffat_sph = ag.lp.MoffatSph() + +# Specialised: Chameleon (NFW-like double-isothermal) and Elson-Free-Fall (King-like) +chameleon = ag.lp.Chameleon() +chameleon_sph = ag.lp.ChameleonSph() +eff = ag.lp.ElsonFreeFall() +eff_sph = ag.lp.ElsonFreeFallSph() + +# Shapelets — n_y, n_x (Cartesian) or n, m (Polar) pick the basis index +shapelet_cartesian = ag.lp.ShapeletCartesian(n_y=0, n_x=0) +shapelet_polar = ag.lp.ShapeletPolar(n=0, m=0) +shapelet_exponential = ag.lp.ShapeletExponential(n=0, m=0) + +# Basis — bundles a list of light profiles into a single composite +basis = ag.lp_basis.Basis(profile_list=[ag.lp_linear.Gaussian(sigma=0.5)]) + +""" +Two things worth knowing about this list before we move on: + +1. Every elliptical profile (e.g. `Sersic`, `Gaussian`) has a spherical sibling whose name + ends in `Sph` (e.g. `SersicSph`, `GaussianSph`). The spherical variant fixes the + ellipticity components `ell_comps` to `(0, 0)`, which is useful when you want to model a + round galaxy and avoid two redundant parameters in the non-linear search. +2. The `Multipole` variants (`SersicMultipole`, `GaussianMultipole`) only exist as + *elliptical* profiles — the m=3 / m=4 perturbations are angular distortions and are not + meaningful without an underlying elliptical reference frame. + +We now move on to seeing what these profiles actually produce when evaluated on a grid. + +__Detailed Example: Sersic Image__ + +The `Sersic` profile is the canonical galaxy light profile, controlled by: + +- `centre` — the (y, x) arc-second coordinate of the profile's centre. +- `ell_comps` — the two ellipticity components `(e1, e2)`. Use + `ag.convert.ell_comps_from(axis_ratio=..., angle=...)` to convert from human-friendly + axis ratio and position angle. +- `intensity` — overall brightness normalisation. +- `effective_radius` — the half-light radius (arc-seconds). +- `sersic_index` — the Sersic concentration. `n=1` reduces to an exponential disc and + `n=4` reduces to a de Vaucouleurs profile. + +Build a Sersic and evaluate its image on our grid: +""" +sersic = ag.lp.Sersic( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + effective_radius=0.6, + sersic_index=3.0, +) + +image = sersic.image_2d_from(grid=grid) + +aplt.plot_array(array=image, title="Sersic Image") + +""" +The returned `image` is an `Array2D` — the `slim` view is a 1D numpy array of length +`total_pixels`, and `native` gives a 2D `(shape_native_y, shape_native_x)` array. + +This same `image_2d_from(grid=grid)` call exists on every light profile in this guide, +returning an image of identical shape and units. Every section below is a small variation +on this one — the API is uniform. + +__Linear Light Profiles__ + +For a non-linear search, the `intensity` parameter of a standard light profile is a free +parameter sampled by the fitter. This works fine for one or two profiles, but adds a free +dimension to the parameter space for every extra profile you bolt on. + +Linear light profiles solve this by removing `intensity` from the model entirely and instead +recovering it analytically via a linear matrix inversion at each likelihood evaluation. This +keeps the non-linear parameter space small even when you combine many profiles, and is the +default in our modern modelling examples. + +The API is identical to the standard profile, just without `intensity`: +""" +linear_sersic = ag.lp_linear.Sersic( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + effective_radius=0.6, + sersic_index=3.0, +) + +""" +Every standard profile in `ag.lp.*` has a `ag.lp_linear.*` counterpart, **including the +newer `SersicMultipole` and `GaussianMultipole`** — `ag.lp_linear.SersicMultipole` and +`ag.lp_linear.GaussianMultipole` both exist and behave the same way (the multipole comps +are non-linear parameters; only the overall intensity is solved by inversion). + +The full workflow (likelihood function, fits, modeling) is documented in: + + scripts/imaging/features/linear_light_profiles/ + +That folder contains `fit.py`, `modeling.py`, and `likelihood_function.py` showing how to +build models with linear profiles and what the likelihood looks like under the hood. + +__Operated Light Profiles__ + +Some emission components — chiefly the unresolved bright cores of AGN — are already PSF- +convolved by the time you receive the image. Standard profiles get PSF-convolved during the +fit, so applying a PSF a second time double-blurs them. + +Operated light profiles tell the fit "this profile's emission has already had the PSF +operation applied; do not blur it again". The fit classes expose an `operated_only` flag +that controls whether these profiles are included or excluded from a given image computation. +""" +operated_gaussian = ag.lp_operated.Gaussian( + centre=(0.0, 0.0), + ell_comps=(0.0, 0.0), + intensity=0.3, + sigma=0.05, +) + +""" +Three operated profiles are available: `ag.lp_operated.Gaussian`, `ag.lp_operated.Moffat`, +and `ag.lp_operated.Sersic`. + +The full workflow (simulating with operated profiles, modeling with them) is documented in: + + scripts/imaging/features/operated_light_profile/ + +That folder contains `simulator.py` and `modeling.py`. + +__Multipole Light Profiles__ + +`SersicMultipole` and `GaussianMultipole` are recent additions that bolt m=3 and m=4 Fourier +angular perturbations onto the eccentric radius of a base profile. The perturbed radius is + + r' = r * (1 + c3 cos(3 theta) + s3 sin(3 theta) + + c4 cos(4 theta) + s4 sin(4 theta)) + +where `theta` is the polar angle in the profile's elliptical reference frame, and the +`multipole_3_comps = (c3, s3)` and `multipole_4_comps = (c4, s4)` parameters control the +amplitude of each perturbation. + +When both `multipole_*_comps` are `(0.0, 0.0)` (the defaults), the profile reduces exactly to +the base profile. This is by design — you can swap a `Sersic` for a `SersicMultipole` in any +model without changing its predictions, and the multipole comps simply add four extra free +parameters that can capture boxy / discy / lopsided morphologies. + +Build a `SersicMultipole` with non-zero multipole components, alongside the unperturbed +Sersic that produced our reference image above: +""" +sersic_multipole = ag.lp.SersicMultipole( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + effective_radius=0.6, + sersic_index=3.0, + multipole_3_comps=(0.05, 0.00), + multipole_4_comps=(0.00, 0.04), +) + +aplt.plot_array( + array=sersic_multipole.image_2d_from(grid=grid), + title="SersicMultipole Image (m=3 + m=4 perturbation)", +) + +""" +For comparison, here is the unperturbed Sersic image produced earlier in the guide — the +two should look almost identical with the perturbation showing as a subtle azimuthal +modulation: +""" +aplt.plot_array( + array=sersic.image_2d_from(grid=grid), + title="Sersic Image (no multipole perturbation)", +) + +""" +The `GaussianMultipole` profile applies the same perturbation to a Gaussian base — handy +when you want a multipole component inside a Multi-Gaussian Expansion (more on that +shortly): +""" +gaussian_multipole = ag.lp.GaussianMultipole( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + sigma=0.4, + multipole_3_comps=(0.05, 0.00), + multipole_4_comps=(0.00, 0.04), +) + +aplt.plot_array( + array=gaussian_multipole.image_2d_from(grid=grid), + title="GaussianMultipole Image", +) + +""" +Two practical notes on the multipole variants: + +- There is **no spherical (`*Sph`) variant** of either multipole. The perturbation is an + angular distortion measured in the elliptical reference frame, so it only makes sense for + an elliptical profile (a spherical profile has no preferred angle). +- Both multipoles exist as **linear variants** too: `ag.lp_linear.SersicMultipole` and + `ag.lp_linear.GaussianMultipole`. In the linear form the multipole comps remain non- + linear parameters but the overall intensity is solved by inversion, just like for the + ordinary linear profiles. + +__Basis__ + +A `Basis` is not a profile in its own right but a *grouping* of profiles that behave as a +single composite. The classic application is the Multi-Gaussian Expansion (MGE), where a +galaxy's light is decomposed into a sum of many concentric Gaussians at fixed centres and +ellipticities but with increasing widths — together they reproduce arbitrary radial profiles +the standard parametric forms cannot capture. + +The `Basis` constructor takes a `profile_list` of any light or mass profiles: +""" +basis = ag.lp_basis.Basis( + profile_list=[ + ag.lp_linear.Gaussian( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + sigma=0.05, + ), + ag.lp_linear.Gaussian( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + sigma=0.15, + ), + ag.lp_linear.Gaussian( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + sigma=0.4, + ), + ag.lp_linear.Gaussian( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + sigma=1.0, + ), + ] +) + +""" +Because the constituents are `LightProfileLinear` instances they have no concrete +`intensity` value yet — the intensity is the thing the inversion would solve for during a +fit. To plot what the basis looks like *as a model component* we wrap it as the `bulge` of +a `Galaxy` and plot the galaxy's image, which is how the basis is used in practice: +""" +basis_galaxy = ag.Galaxy(redshift=0.5, bulge=basis) + +aplt.plot_array( + array=basis_galaxy.image_2d_from(grid=grid), + title="Basis Image (4-Gaussian MGE, plotted via Galaxy)", +) + +""" +Two things make `Basis` powerful: + +- It slots into a `Galaxy` exactly like a `Sersic` would — once wrapped, the rest of the + modelling code doesn't have to know it's looking at four Gaussians under the hood. +- When *every* constituent profile is a `LightProfileLinear` (as in the example above), all + of their `intensity` values are solved together in a **single combined inversion** at each + likelihood evaluation. This means an MGE built from, say, 30 Gaussians adds only the + shared geometric parameters to the non-linear search rather than 30 extra intensities. + +The full MGE workflow — choosing how many Gaussians to use, how to space their `sigma` +values, and how the inversion plays with regularisation — is documented in: + + scripts/imaging/features/multi_gaussian_expansion/ + +Shapelet decompositions follow the same `Basis` pattern, using `ag.lp.ShapeletPolar` / +`ag.lp.ShapeletCartesian` / `ag.lp.ShapeletExponential` (and their linear counterparts). +The full shapelets workflow is documented in: + + scripts/imaging/features/shapelets/ + +__Light Profile in a Model__ + +So far we have been instantiating profiles with concrete parameter values. When fitting a +real dataset we instead build a *model* of the profile and let the non-linear search find the +best-fit parameters. This is what `af.Model` is for. +""" +sersic_model = af.Model(ag.lp.Sersic) + +""" +The `af.Model` wraps the profile class. Every constructor argument that has a numerical +default now becomes a *prior* — by default the priors are `UniformPriors` covering a sensible +range for each parameter (see `autogalaxy/config/priors/light.yaml` for the configured +ranges). + +You can override individual priors before fitting: +""" +sersic_model.sersic_index = af.UniformPrior(lower_limit=0.5, upper_limit=8.0) +sersic_model.effective_radius = af.UniformPrior(lower_limit=0.01, upper_limit=10.0) + +""" +A model profile by itself is not yet a complete model — it has to be associated with a +`Galaxy` and an `af.Collection` so the search knows what dataset it's fitting: +""" +galaxy_model = af.Model(ag.Galaxy, redshift=0.5, bulge=sersic_model) +model = af.Collection(galaxies=af.Collection(galaxy=galaxy_model)) + +print(model.info) + +""" +Printing `model.info` prints the full priors-and-defaults summary — useful before kicking off +a long fit to confirm the model looks the way you expect. + +The model API is the same for **every** light profile in this guide — swap `ag.lp.Sersic` +for `ag.lp.SersicMultipole`, `ag.lp_linear.Gaussian`, `ag.lp_basis.Basis`, etc., and the rest +of the snippet is unchanged. Multipole comps and Basis constituent lists are wired into the +prior machinery automatically. + +Full modeling end-to-end examples live in `scripts/imaging/modeling.py` and the topic- +specific guides under `scripts/imaging/features/`. + +__Model Instance from Light Profile__ + +A model is a description of *possible* profiles. To get an actual profile back out — for +example to plot what the prior medians look like before running a fit — call +`instance_from_prior_medians()`: +""" +sersic_instance = sersic_model.instance_from_prior_medians() +print(type(sersic_instance)) # autogalaxy.profiles.light.standard.sersic.Sersic + +image = sersic_instance.image_2d_from(grid=grid) +aplt.plot_array(array=image, title="Sersic Instance from Prior Medians") + +""" +The instance returned from `instance_from_prior_medians()` is a real `ag.lp.Sersic` — the +same class we constructed by hand at the top of the guide — and supports the full API +including `image_2d_from`. + +The same flow works at the galaxies level: realise an instance of the full model and pull +the light profile back out of it. +""" +model_instance = model.instance_from_prior_medians() + +galaxies = ag.Galaxies(galaxies=[model_instance.galaxies.galaxy]) + +aplt.plot_array( + array=galaxies.image_2d_from(grid=grid), + title="Galaxies Instance Image", +) + +bulge_instance = model_instance.galaxies.galaxy.bulge +print(type(bulge_instance)) # autogalaxy.profiles.light.standard.sersic.Sersic + +""" +After a fit completes, `result.max_log_likelihood_instance` returns the same shape of +object, with the prior medians replaced by the fitted parameter values. See +`scripts/guides/results/start_here.py` for the full results-introspection guide. + +__Remaining Profiles Walkthrough__ + +We have shown the full `image_2d_from` → `af.Model` → `instance` flow for the `Sersic` +profile. Every remaining standard profile uses the **same API** — the only thing that +changes is which parameters appear in the constructor. + +The compact tour below builds each remaining profile with sensible parameter values and +plots its image, so you can see what each looks like. When you want to use any of these in +a model, repeat the `af.Model(...)` / `af.Collection(...)` pattern from the previous section. +""" + +aplt.plot_array( + array=ag.lp.SersicCore( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + effective_radius=0.6, + sersic_index=3.0, + radius_break=0.05, + gamma=0.2, + alpha=3.0, + ).image_2d_from(grid=grid), + title="SersicCore Image", +) + +aplt.plot_array( + array=ag.lp.Exponential( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.7, angle=30.0), + intensity=0.5, + effective_radius=1.6, + ).image_2d_from(grid=grid), + title="Exponential Image", +) + +aplt.plot_array( + array=ag.lp.ExponentialCore( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.7, angle=30.0), + intensity=0.5, + effective_radius=1.6, + radius_break=0.05, + gamma=0.2, + alpha=3.0, + ).image_2d_from(grid=grid), + title="ExponentialCore Image", +) + +aplt.plot_array( + array=ag.lp.DevVaucouleurs( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + effective_radius=0.6, + ).image_2d_from(grid=grid), + title="DevVaucouleurs Image", +) + +aplt.plot_array( + array=ag.lp.Gaussian( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + sigma=0.4, + ).image_2d_from(grid=grid), + title="Gaussian Image", +) + +aplt.plot_array( + array=ag.lp.Moffat( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + alpha=0.4, + beta=2.5, + ).image_2d_from(grid=grid), + title="Moffat Image", +) + +aplt.plot_array( + array=ag.lp.Chameleon( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + core_radius_0=0.05, + core_radius_1=0.3, + ).image_2d_from(grid=grid), + title="Chameleon Image", +) + +aplt.plot_array( + array=ag.lp.ElsonFreeFall( + centre=(0.0, 0.0), + ell_comps=ag.convert.ell_comps_from(axis_ratio=0.8, angle=45.0), + intensity=1.0, + effective_radius=0.6, + eta=2.0, + ).image_2d_from(grid=grid), + title="ElsonFreeFall Image", +) + +""" +The spherical variants (`SersicSph`, `GaussianSph`, etc.) are constructed identically with +the `ell_comps` argument removed — they look like a rotationally symmetric version of the +corresponding elliptical plot. + +The shapelet profiles are normally used inside a `Basis` rather than individually, but for +completeness here is the lowest-order Cartesian shapelet on its own: +""" +aplt.plot_array( + array=ag.lp.ShapeletCartesian( + n_y=0, + n_x=0, + centre=(0.0, 0.0), + ell_comps=(0.0, 0.0), + intensity=1.0, + beta=0.2, + ).image_2d_from(grid=grid), + title="ShapeletCartesian (n_y=0, n_x=0) Image", +) + +""" +And that completes the tour. If you arrived here from the API reference and now want to use +any of these profiles in an actual fit, the next step is `scripts/imaging/modeling.py`, +which sets up an `AnalysisImaging` and runs a non-linear search end-to-end. The +`scripts/imaging/features/` subpackages handle the family-specific workflows referenced +throughout this guide: + +- `linear_light_profiles/` — using `ag.lp_linear.*` in a fit. +- `operated_light_profile/` — using `ag.lp_operated.*` in a fit. +- `multi_gaussian_expansion/` — building and fitting an MGE-style `Basis`. +- `shapelets/` — building and fitting a shapelet-style `Basis`. +"""