Model Cookbook#
The model cookbook provides a concise reference to lens model composition tools, specifically the Model
and Collection objects.
Examples using different PyAutoLens API’s for model composition are provided, which produce more concise and readable code for different use-cases.
Simple Lens Model#
A simple lens model has a lens galaxy with a linear Sersic light profile, Isothermal mass profile and source galaxy with a linear Sersic light profile:
# Lens:
bulge = af.Model(al.lp_linear.Sersic)
mass = af.Model(al.mp.Isothermal)
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
mass=mass,
)
# Source:
source = af.Model(al.Galaxy, redshift=1.0, bulge=al.lp_linear.SersicCore)
# Overall Lens Model:
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
The redshifts in the above model are used to determine which galaxy is the lens and which is the source.
The model total_free_parameters attribute tells us the total number of free parameters (which are fitted for
via a non-linear search), which in this case is 17 (6 from the lens Sersic, 5 from the lens Isothermal and 6 from
the source SersicCore). The intensity of each linear light profile is not one of them: it is solved for by the
inversion during every likelihood evaluation.
print(f"Model Total Free Parameters = {model.total_free_parameters}")
If we print the info attribute of the model we get information on all of the parameters and their priors.
print(model.info)
This gives the following output:
Total Free Parameters = 17
model Collection (N=17)
galaxies Collection (N=17)
lens Galaxy (N=11)
bulge Sersic (N=6)
mass Isothermal (N=5)
source Galaxy (N=6)
bulge SersicCore (N=6)
galaxies
lens
redshift 0.5
bulge
centre
centre_0 GaussianPrior [0], mean = 0.0, sigma = 0.3
centre_1 GaussianPrior [1], mean = 0.0, sigma = 0.3
ell_comps
ell_comps_0 TruncatedGaussianPrior [2], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
ell_comps_1 TruncatedGaussianPrior [3], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
effective_radius UniformPrior [4], lower_limit = 0.0, upper_limit = 30.0
sersic_index UniformPrior [5], lower_limit = 0.8, upper_limit = 5.0
mass
centre
centre_0 GaussianPrior [6], mean = 0.0, sigma = 0.1
centre_1 GaussianPrior [7], mean = 0.0, sigma = 0.1
ell_comps
ell_comps_0 TruncatedGaussianPrior [8], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
ell_comps_1 TruncatedGaussianPrior [9], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
einstein_radius UniformPrior [10], lower_limit = 0.0, upper_limit = 8.0
source
redshift 1.0
bulge
centre
centre_0 GaussianPrior [11], mean = 0.0, sigma = 0.3
centre_1 GaussianPrior [12], mean = 0.0, sigma = 0.3
ell_comps
ell_comps_0 TruncatedGaussianPrior [13], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
ell_comps_1 TruncatedGaussianPrior [14], mean = 0.0, sigma = 0.3, lower_limit = -1.0, upper_limit = 1.0
effective_radius UniformPrior [15], lower_limit = 0.0, upper_limit = 30.0
sersic_index UniformPrior [16], lower_limit = 0.8, upper_limit = 5.0
radius_break 0.025
gamma 0.25
alpha 3.0
The same model can also be visualized as a figure, making its structure easier to understand at a glance.
The figure shows how the model is organized: which parameters belong to each component, and whether they are free,
fixed, shared, linked by an expression, solved during the fit, or not configured. model.info provides the
corresponding numerical details, including the prior assigned to each free parameter and the value of each fixed
parameter.
af.ModelPlotter(model).figure()
Every stage below prints model.info in the same way; from here on only the figure is shown.
More Complex Lens Models#
The API above can be easily extended to compose lens models where each galaxy has multiple light or mass profiles:
# Lens:
bulge = af.Model(al.lp_linear.Sersic)
disk = af.Model(al.lp_linear.Exponential)
mass = af.Model(al.mp.Isothermal)
shear = af.Model(al.mp.ExternalShear)
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
disk=disk,
mass=mass,
shear=shear,
)
# Source:
bulge = af.Model(al.lp_linear.SersicCore)
disk = af.Model(al.lp_linear.ExponentialCore)
source = af.Model(al.Galaxy, redshift=1.0, bulge=bulge, disk=disk)
# Overall Lens Model:
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
af.ModelPlotter(model).figure()
The use of the words bulge, disk, mass and shear above are arbitrary. They can be replaced with any name you
like, e.g. bulge_0, bulge_1, mass_0, mass_1, and the model will still behave in the same way.
The API can also be extended to compose lens models where there are multiple galaxies:
bulge = af.Model(al.lp_linear.Sersic)
mass = af.Model(al.mp.Isothermal)
lens_0 = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
mass=mass,
)
bulge = af.Model(al.lp_linear.Sersic)
mass = af.Model(al.mp.Isothermal)
lens_1 = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
mass=mass,
)
# Source 0:
bulge = af.Model(al.lp_linear.SersicCore)
source_0 = af.Model(al.Galaxy, redshift=1.0, bulge=bulge)
# Source 1 :
bulge = af.Model(al.lp_linear.SersicCore)
source_1 = af.Model(al.Galaxy, redshift=1.0, bulge=bulge)
# Overall Lens Model:
model = af.Collection(
galaxies=af.Collection(
lens_0=lens_0,
lens_1=lens_1,
source_0=source_0,
source_1=source_1
)
)
af.ModelPlotter(model).figure()
The two lens galaxies are identical in structure, and so are the two source galaxies, but their parameters are independent: two separate priors with the same configuration, which is not the same thing as one shared prior.
The above lens model consists of only two planes (an image-plane and source-plane), but has four galaxies in total. This is because the lens galaxies have the same redshift and the source galaxies have the same redshift.
If we gave one of the lens galaxies a different redshift, it would be included in a third plane, and the model would perform multi-plane ray tracing when the model-fit is performed.
Concise API#
If a light or mass profile is passed directly to the af.Model of a galaxy, it is automatically assigned to be a
af.Model component of the galaxy.
This means we can write the model above comprising multiple light and mass profiles more concisely as follows (also removing the comments reading Lens / Source / Overall Lens Model to make the code more readable):
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=al.lp_linear.Sersic,
disk=al.lp_linear.Sersic,
mass=al.mp.Isothermal,
shear=al.mp.ExternalShear,
)
source = af.Model(
al.Galaxy,
redshift=1.0,
bulge=al.lp_linear.SersicCore,
disk=al.lp_linear.ExponentialCore,
)
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
af.ModelPlotter(model).figure()
The concise API is a shorthand for writing a model, not a different model. Note that this stage gives the lens
galaxy a Sersic bulge and a Sersic disk, and each of the two has its own priors rather than sharing one.
Prior Customization#
We can customize the priors of the lens model component individual parameters, using the following three types of priors:
UniformPrior: The values of a parameter are randomly drawn between a lower_limit and upper_limit. For example,
the effective radius of elliptical Sersic profiles typically assumes a uniform prior between 0.0” and 30.0”.
LogUniformPrior: Like a UniformPrior this randomly draws values between a lower_limit and upper_limit, but the
values are drawn from a distribution with base 10. This is used for the intensity of a light profile, as the
luminosity of galaxies follows a log10 distribution.
GaussianPrior: The values of a parameter are randomly drawn from a Gaussian distribution with a mean and width
sigma. For example, the $y$ and $x$ centre values in a light profile typically assume a mean of 0.0” and a
sigma of 0.3”, indicating that we expect the profile centre to be located near the centre of the image.
# Lens:
bulge = af.Model(al.lp_linear.Sersic)
bulge.sersic_index = af.TruncatedGaussianPrior(
mean=4.0, sigma=1.0, lower_limit=1.0, upper_limit=8.0
)
mass = af.Model(al.mp.Isothermal)
mass.centre.centre_0 = af.TruncatedGaussianPrior(
mean=0.0, sigma=0.1, lower_limit=-0.5, upper_limit=0.5
)
mass.centre.centre_1 = af.TruncatedGaussianPrior(
mean=0.0, sigma=0.1, lower_limit=-0.5, upper_limit=0.5
)
mass.einstein_radius = af.UniformPrior(lower_limit=0.0, upper_limit=8.0)
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
mass=mass,
)
# Source
bulge = af.Model(al.lp_linear.SersicCore)
source = af.Model(al.Galaxy, redshift=1.0, bulge=bulge)
source.effective_radius = af.TruncatedGaussianPrior(
mean=0.1, sigma=0.05, lower_limit=0.0, upper_limit=1.0
)
# Overall Lens Model:
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
af.ModelPlotter(model).figure()
Customizing a prior does not change a parameter’s state — a parameter with a customized prior is still sampled.
Print model.info, or call af.ModelPlotter(model).figure(detail="priors"), to read the prior on each parameter.
One thing in the code above is easy to misread: the last line sets effective_radius on the source galaxy, not on
the source galaxy’s bulge. A Galaxy accepts any attribute, so this adds a new free effective_radius parameter
to the source galaxy itself rather than overriding the bulge’s own prior. Writing
source.bulge.effective_radius = ... is what customizes the bulge.
Model Customization#
We can customize the lens model parameters in a number of different ways, as shown below:
# Lens:
bulge = af.Model(al.lp_linear.Sersic)
disk = af.Model(al.lp_linear.Exponential)
# Parameter Pairing: Pair the centre of the bulge and disk together, reducing
# the complexity of non-linear parameter space by N = 2
bulge.centre = disk.centre
# Parameter Fixing: Fix the sersic_index of the bulge to a value of 4, reducing
# the complexity of non-linear parameter space by N = 1
bulge.sersic_index = 4.0
mass = af.Model(al.mp.Isothermal)
# Parameter Offsets: Make the mass model centre parameters the same value as
# the bulge / disk but with an offset.
mass.centre.centre_0 = bulge.centre.centre_0 + 0.1
mass.centre.centre_1 = bulge.centre.centre_1 + 0.1
shear = af.Model(al.mp.ExternalShear)
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=bulge,
disk=disk,
mass=mass,
shear=shear,
)
# Source:
bulge = af.Model(al.lp_linear.SersicCore)
disk = af.Model(al.lp_linear.ExponentialCore)
source = af.Model(al.Galaxy, redshift=1.0, bulge=bulge, disk=disk)
# Overall Lens Model:
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
# Assert that the effective radius of the bulge is larger than that of the disk.
# (Assertions can only be added at the end of model composition, after all components
# have been brought together in a `Collection`.
model.add_assertion(model.galaxies.lens.bulge.effective_radius > model.galaxies.lens.disk.effective_radius)
# Assert that the Einstein Radius is below 3.0":
model.add_assertion(model.galaxies.lens.mass.einstein_radius < 3.0)
af.ModelPlotter(model).figure()
The disk reuses the bulge’s centre rather than having one of its own, the sersic_index is fixed, and the
model carries two assertions relating its parameters.
The two offset centre components of the mass profile are a relation: they are defined by an expression rather
than sampled. A relation on one component of a tuple parameter such as centre is not yet annotated on the model
figure, so model.info is the place to read it.
Redshift Free#
The redshift of a galaxy can be treated as a free parameter in the model-fit by using the following API:
redshift = af.Model(al.Redshift)
redshift.redshift = af.UniformPrior(lower_limit=0.0, upper_limit=2.0)
lens = af.Model(
al.Galaxy,
redshift=redshift,
mass=al.mp.Isothermal
)
source = af.Model(al.Galaxy, redshift=1.0, bulge=al.lp_linear.SersicCore)
model = af.Collection(galaxies=af.Collection(lens=lens, source=source))
af.ModelPlotter(model).figure()
A free redshift is an ordinary sampled parameter of the galaxy. A fixed redshift is not a parameter of the model at all, and the source galaxy above shows this.
The model-fit will automatically enable multi-plane ray tracing and alter the ordering of the planes depending on the redshifts of the galaxies.
NOTE: For strong lenses with just two planes (an image-plane and source-plane) the redshifts of the galaxies do not impact the model-fit. You should therefore never make the redshifts free if you are only modeling a two-plane lens system. This is because lensing calculations can be defined in arc-second coordinates, which do not change as a function of redshift.
Redshifts should be made free when modeling three or more planes, as the multi-plane ray-tracing calculations have an obvious dependence on the redshifts of the galaxies which could be inferred by the model-fit.
Solved and Unconfigured Parameters#
Some parameters of a lens model are neither sampled by the non-linear search nor fixed by the user: they are solved for during the fit, at every likelihood evaluation. The model below contains all three of them:
# Lens: a linear light profile, whose `intensity` is solved for by the inversion.
lens = af.Model(
al.Galaxy,
redshift=0.5,
bulge=af.Model(al.lp_linear.Sersic),
mass=af.Model(al.mp.Isothermal),
)
# Source: a pixelization, whose source `reconstruction` is solved for by the inversion.
pixelization = af.Model(
al.Pixelization,
mesh=af.Model(al.mesh.Delaunay, pixels=500, zeroed_pixels=0),
regularization=af.Model(al.reg.ConstantSplit),
)
source = af.Model(al.Galaxy, redshift=1.0, pixelization=pixelization)
# Point source: a `PointSolved`, whose `centre` is solved for analytically.
point_source = af.Model(al.Galaxy, redshift=1.0, point=af.Model(al.ps.PointSolved))
model = af.Collection(
galaxies=af.Collection(lens=lens, source=source, point_source=point_source)
)
af.ModelPlotter(model).figure()
Three parameters of this model are solved for during the fit:
the
intensityof every linear light profile (al.lp_linear.*, and every member of anal.lp_basis.Basisbuilt from them), which the inversion solves for by linear algebra rather than the search sampling it. ABasishas no solved amplitude of its own: the solved intensities belong to its member profiles, one per member.the
reconstructionof anal.Pixelization, which is the solved surface brightness of every source pixel. The only sampled parameters of a pixelization are its regularization coefficients.the
centreof anal.ps.PointSolved, which is solved for analytically by the*Solvedpoint-source fit classes.al.ps.Point, by contrast, samples its centre as an ordinary free parameter.
These three have no counterpart in model.info at all, because they are not sampled parameters of the model.
A different thing again is areas_factor, a parameter of the Delaunay mesh for which no prior is configured, so the
model cannot be fitted until one is supplied (via config/priors or by setting it in the script). It is unset
configuration — not solved, and not absent from the model.
PyAutoLens’ own default configuration ships no prior for areas_factor. The autolens_workspace supplies one in
config/priors/mesh/delaunay.yaml, as a constant 0.5, so the same code run from the workspace has an ordinary fixed
parameter instead: unset configuration is what you see when your own configuration has a gap.
Available Model Components#
The light profiles, mass profiles and other components that can be used for lens modeling are given at the following API documentation pages:
JSon Outputs#
After a model is composed, it can easily be output to a .json file on hard-disk in a readable structure:
import os
import json
model_path = path.join("path", "to", "model", "json")
os.makedirs(model_path, exist_ok=True)
model_file = path.join(model_path, "model.json")
with open(model_file, "w+") as f:
json.dump(model.dict(), f, indent=4)
We can load the model from its .json file.
model = af.Model.from_json(file=model_file)
This means in PyAutoLens one can write a model in a script, save it to hard disk and load it elsewhere, as well as manually customize it in the .json file directory.
This is used for composing complex models of group scale lenses.
Multi Galaxy, Group and Cluster Models#
Above galaxy scale, models are composed for three regimes (see the New User Guide’s “What Scale System?” ladder — all groups and clusters are multi-galaxy systems, but not vice versa):
Multi-galaxy lenses (2+ co-dominant deflectors, no host halo): one free light + mass model per deflector, composed in a loop with untruncated isothermals (no host halo means no tidal truncation):
lens_dict = {}
for i, centre in enumerate(main_lens_centres):
mass = af.Model(al.mp.Isothermal)
mass.centre = (centre[0], centre[1])
lens_dict[f"lens_{i}"] = af.Model(al.Galaxy, redshift=0.5, mass=mass)
model = af.Collection(galaxies=af.Collection(**lens_dict, source=source))
Group-scale lenses add two things as explicit modelling choices: an optional dark-matter group halo, and faint members whose masses are tied to their luminosities through a shared scaling relation, so N galaxies cost one free parameter (composed via prior arithmetic on a shared prior):
einstein_radius_ref = af.UniformPrior(lower_limit=0.0, upper_limit=1.0)
for i, (centre, luminosity) in enumerate(zip(centres, luminosities)):
mass = af.Model(al.mp.IsothermalSph)
mass.centre = (centre[0], centre[1])
mass.einstein_radius = einstein_radius_ref * (luminosity / reference_luminosity) ** 0.5
Cluster-scale lenses keep the group mass framework (halo + tidally truncated dPIE
members on scaling relations) but change the source strategy: many point-source multiple-image
position datasets, each at its own redshift, fitted via AnalysisPoint.
The full group-scale composition — one main lens galaxy with an external shear, a SersicCore source and one
extra galaxy per detected group member, each with a SersicSph light profile and a dPIEMassSph mass profile whose
centre is fixed to its measured position — is the script
autolens_workspace/scripts/group/modeling.py. Drawn as a figure, with eight extra galaxies, it looks like this:
The eight extra galaxies are identical in structure, but each has its own fixed centre rather than one centre
common to all, and each has its own sigma prior, because eight separate priors with the same configuration is not
the same thing as one shared prior.
The following example notebooks show each regime’s full model composition:
https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/multi_galaxy/modeling.ipynb https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/group/modeling.ipynb https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/group/start_here.ipynb https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/group/features/group_halo/modeling.ipynb https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/cluster/start_here.ipynb
Many Profile Models (Advanced)#
Features such as the Multi Gaussian Expansion (MGE) and shapelets compose models consisting of 50 - 500+ light profiles.
An MGE lens light model of 2 x 30 linear Gaussians, fitted alongside a pixelized source, draws like this:
The 60 Gaussians are split across two bases, because each basis holds its own ell_comps pair while all 60 share
one centre. Every member’s intensity is solved for by the inversion, as is the source’s reconstruction, and the
mesh’s areas_factor has no prior configured.
The following example notebooks show how to compose and fit these models:
https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/imaging/features/multi_gaussian_expansion/modeling.ipynb https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/imaging/features/advanced/shapelets/modeling.ipynb
Model Linking (Advanced)#
When performing non-linear search chaining, the inferred model of one phase can be linked to the model.
For a model with a separate external field, carry the mass and field together:
mass, field = al.util.chaining.mass_and_fields_from(
mass=af.Model(al.mp.PowerLaw),
mass_result=result_1.model.galaxies.lens.mass,
fields_result=result_1.model.fields,
)
model_2 = af.Collection(
galaxies=af.Collection(lens=af.Model(al.Galaxy, redshift=0.5, mass=mass), source=source),
fields=field,
)
result_1.model.fields passes posterior priors into the next stage; use
result_1.instance.fields to fix the field to the previous fit’s values.
The following example notebooks show how to compose and fit these models:
https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/guides/modeling/chaining.ipynb
Across Datasets (Advanced)#
When fitting multiple datasets, model can be composed where the same model component are used across the datasets but certain parameters are free to vary across the datasets.
The following example notebooks show how to compose and fit these models:
https://github.com/PyAutoLabs/autolens_workspace/blob/main/notebooks/multi_dataset/start_here.ipynb
Relations (Advanced)#
We can compose models where the free parameter(s) vary according to a user-specified function (e.g. y = mx +c -> intensity = (m * wavelength) + c across the datasets.
The following example notebooks show how to compose and fit these models:
PyAutoFit API#
PyAutoFit is a general model composition library which offers even more ways to compose lens models not detailed in this cookbook.
The PyAutoFit model composition cookbooks detail this API in more detail:
https://pyautofit.readthedocs.io/en/latest/cookbooks/model.html https://pyautofit.readthedocs.io/en/latest/cookbooks/multi_level_model.html
Wrap Up#
This cookbook shows how to compose simple lens models using the af.Model() and af.Collection() objects.