Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 1 addition & 5 deletions autolens/aggregator/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ def _tracer_from(

# Mirrors the `fields` fold in `AnalysisLens.tracer_via_instance_from`: a model may declare an
# external field alongside its galaxies, and a `MassField` is not a galaxy.
fields = (
list(instance.fields)
if getattr(instance, "fields", None) is not None
else None
)
fields = tracer_util.fields_list_from(getattr(instance, "fields", None))

tracer = Tracer(galaxies=galaxy_list, cosmology=cosmology, fields=fields)

Expand Down
6 changes: 1 addition & 5 deletions autolens/analysis/analysis/lens.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,7 @@ def tracer_via_instance_from(
# A model may declare an external field alongside its galaxies, as
# `af.Collection(galaxies=..., fields=af.Collection(field=af.Model(al.MassField, ...)))`.
# It is folded here rather than into `galaxy_list`, because a `MassField` is not a galaxy.
fields = (
list(instance.fields)
if getattr(instance, "fields", None) is not None
else None
)
fields = tracer_util.fields_list_from(getattr(instance, "fields", None))

return Tracer(
galaxies=galaxy_list,
Expand Down
9 changes: 8 additions & 1 deletion autolens/analysis/model_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ def mass_field_from(
fields=af.Collection(field=al.model_util.mass_field_from(lens=lens)),
)

For a single field, ``fields=al.model_util.mass_field_from(lens=lens)`` is also
supported. This shortens prior paths (``fields.shear.gamma_1`` instead of
``fields.field.shear.gamma_1``), so the two forms have different result identifiers
by design. The collection form above remains the primary example and retains its
existing identifier.

The ``MassSheet`` and ``ExternalPotential`` have a meaningful centre (unlike an
``ExternalShear``, whose deflection field is constant), and that centre is physically
the centre of the system the field is expanded about. It is therefore **shared** with
Expand Down Expand Up @@ -177,7 +183,8 @@ def mass_field_from(

Returns
-------
The ``af.Model`` of the ``MassField``, to be placed in the model's ``fields`` collection.
The ``af.Model`` of the ``MassField``, to be placed in the model's ``fields`` collection
or supplied directly as ``fields`` for a single field.
"""
if not hasattr(lens, "mass"):
raise ValueError(
Expand Down
8 changes: 2 additions & 6 deletions autolens/analysis/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
SourceMaxSeparation,
)
from autolens.lens.tracer import Tracer
from autolens.lens import tracer_util
from autolens.point.solver import PointSolver
from autonerves.test_mode import is_test_mode, skip_checks

Expand All @@ -54,12 +55,7 @@ def max_log_likelihood_fields(self) -> List[ag.MassField]:
Mirrors `max_log_likelihood_galaxies` (which reads the galaxies off the same instance), and returns an empty
list for the overwhelming majority of models, which declare no `fields` collection at all.
"""
fields = getattr(self.instance, "fields", None)

if fields is None:
return []

return list(fields)
return tracer_util.fields_list_from(getattr(self.instance, "fields", None))

def source_plane_light_profile_centre_from(
self, plane_redshift: Optional[float] = None
Expand Down
20 changes: 14 additions & 6 deletions autolens/lens/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ def _validate_fields(fields):
Raise if ``fields`` is not something the tracer can treat as a collection of
``MassField`` objects.

This mirrors ``_validate_galaxies`` exactly — same container checks, same
A bare ``MassField`` is accepted as a one-element collection. Otherwise this
mirrors ``_validate_galaxies`` — same container checks, same
string trap, same ``af.ModelInstance`` escape hatch — but names ``MassField``
in every message, because the two arguments are not interchangeable and the
most likely mistake is passing one where the other belongs.
Expand All @@ -259,6 +260,9 @@ def _validate_fields(fields):
if isinstance(fields, af.ModelInstance):
return

if isinstance(fields, MassField):
fields = [fields]

if isinstance(fields, (str, bytes)):
raise TypeError(
f"fields must be an iterable of MassField objects, but a "
Expand Down Expand Up @@ -297,7 +301,7 @@ def __init__(
self,
galaxies: Union[List[ag.Galaxy], af.ModelInstance],
cosmology: ag.cosmo.LensingCosmology = None,
fields: Optional[Union[List[MassField], af.ModelInstance]] = None,
fields: Optional[Union[MassField, List[MassField], af.ModelInstance]] = None,
):
"""
Performs gravitational lensing ray-tracing calculations based on an input list of galaxies and a cosmology.
Expand Down Expand Up @@ -330,7 +334,8 @@ def __init__(
The cosmology used to perform ray-tracing calculations.
fields
The `MassField` objects describing the tidal field of everything outside the modelled system (e.g. an
external shear, a mass sheet, an external potential). Each is placed at its own redshift in the
external shear, a mass sheet, an external potential). Pass a list or a single
`MassField`; the tracer stores a fresh list in either case. Each is placed at its own redshift in the
multi-plane calculation exactly like a galaxy's mass, but contributes no light. Attaching these
components to a `Galaxy` instead remains fully supported and is not deprecated.
"""
Expand All @@ -343,13 +348,15 @@ def __init__(
if fields is not None:
_validate_fields(fields=fields)

fields = tracer_util.fields_list_from(fields)

_warn_if_no_light_is_behind_any_mass(galaxies=galaxies, fields=fields)

self.galaxies = galaxies

# Stored as a plain list (never None) so every member-walking property below can concatenate it
# unconditionally, and so a tracer with no fields serialises as `fields: []` rather than a null.
self.fields = list(fields) if fields is not None else []
self.fields = fields

self.cosmology = cosmology or ag.cosmo.Planck15()

Expand Down Expand Up @@ -492,7 +499,7 @@ def sliced_tracer_from(
source_galaxies: List[ag.Galaxy],
planes_between_lenses: List[int],
cosmology: ag.cosmo.LensingCosmology = None,
fields: Optional[List[MassField]] = None,
fields: Optional[Union[MassField, List[MassField]]] = None,
):
"""
Returns a tracer where the lens system is split into planes with specified redshift distances between them.
Expand Down Expand Up @@ -541,6 +548,7 @@ def sliced_tracer_from(
fields
The `MassField` objects describing the external tidal field. Each has its redshift snapped to the
nearest sliced plane redshift, exactly as the `line_of_sight_galaxies` do.
Accepts a list or a single `MassField`.
"""
cosmology = cosmology or ag.cosmo.Planck15()

Expand All @@ -556,7 +564,7 @@ def sliced_tracer_from(

galaxies = lens_galaxies + line_of_sight_galaxies + source_galaxies

fields = list(fields) if fields is not None else []
fields = tracer_util.fields_list_from(fields)

# Fields are snapped exactly as `line_of_sight_galaxies` are: a field is a redshift-bearing member of
# the system, so leaving it off a sliced plane would defeat the whole point of the slicing.
Expand Down
13 changes: 13 additions & 0 deletions autolens/lens/tracer_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
This module contains lower-level helpers that are called by ``Tracer`` but kept separate
to avoid cluttering the main class. Key functions:

- ``fields_list_from`` — normalizes an optional single field or collection to a fresh list.
- ``plane_redshifts_from`` — derives the list of unique plane redshifts from a list of
galaxies, collapsing multiple galaxies at the same redshift into a single plane.
- ``ordered_plane_redshifts_with_slicing_from`` — extends the above with optional
Expand All @@ -21,6 +22,18 @@
from autolens import exc


def fields_list_from(fields) -> List[ag.MassField]:
"""Return a fresh list from no fields, a single MassField, or a field collection.

Container and entry validation remain the responsibility of the caller.
"""
if fields is None:
return []
if isinstance(fields, ag.MassField):
return [fields]
return list(fields)


def _redshift_is_traced(redshift) -> bool:
"""
Return True if ``redshift`` is a JAX traced scalar that cannot be coerced to a
Expand Down
3 changes: 2 additions & 1 deletion docs/api/galaxy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ shear, a mass sheet, an external potential — at its own redshift and with no l
own; pass a list of them as ``Tracer(galaxies=..., fields=[field])`` and each is placed in
the plane at its redshift, contributing its mass to every lensing calculation while every
per-galaxy surface still sees galaxies alone. Attaching these components to a ``Galaxy``
instead remains fully supported.
instead remains fully supported. For one field, ``Tracer(galaxies=..., fields=field)``
is also accepted; ``tracer.fields`` is always stored as a list.

.. currentmodule:: autolens

Expand Down
4 changes: 4 additions & 0 deletions docs/api/modeling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Convenience functions which compose commonly used ``af.Model`` objects.
``mass_field_from`` builds the ``af.Model(al.MassField, ...)`` external-field model that
goes in a model's ``fields`` collection, sharing the centre of its ``MassSheet`` /
``ExternalPotential`` components with the lens galaxy model's mass centre.
The primary form is ``fields=af.Collection(field=field)``. For a single field,
``fields=field`` is also supported. It shortens prior paths from
``fields.field.shear.gamma_1`` to ``fields.shear.gamma_1`` and therefore gives a
different result identifier by design. Existing collection-form identifiers are unchanged.

.. currentmodule:: autolens.analysis.model_util

Expand Down
41 changes: 41 additions & 0 deletions test_autolens/aggregator/test_tracer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from types import SimpleNamespace

import pytest
import autofit as af
import autolens as al

from autolens.aggregator.tracer import _tracer_from


@pytest.mark.parametrize("form", ["flat", "collection", "absent", "none", "empty"])
@pytest.mark.parametrize("override", [False, True])
def test__tracer_from__normalizes_saved_and_override_fields(form, override):
field = af.Model(al.MassField, redshift=0.5, shear=al.mp.ExternalShear)
model = af.Collection(
galaxies=af.Collection(
lens=al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph()),
source=al.Galaxy(redshift=1.0, bulge=al.lp.SersicSph()),
),
cosmology=al.cosmo.Planck15(),
)
if form == "flat":
model.fields = field
elif form == "collection":
model.fields = af.Collection(field=field)
elif form == "none":
model.fields = None
elif form == "empty":
model.fields = af.Collection()
instance = model.instance_from_prior_medians()
fit = SimpleNamespace(children=[], instance=instance)

tracers = _tracer_from(fit=fit, instance=instance if override else None)

assert len(tracers) == 1
expected = (
[instance.fields]
if form == "flat"
else [instance.fields.field] if form == "collection" else []
)
assert tracers[0].fields == expected
assert len(tracers[0].galaxies) == 2
40 changes: 40 additions & 0 deletions test_autolens/analysis/analysis/test_analysis_lens.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,43 @@ def test__log_likelihood_function__a_field_shear_matches_the_galaxy_attached_she
)

assert field_likelihood == pytest.approx(galaxy_likelihood, 1.0e-8)


def test__tracer_via_instance_from__bare_field_is_folded_into_tracer_fields(
analysis_imaging_7x7,
):
model = af.Collection(
galaxies=af.Collection(
lens=af.Model(al.Galaxy, redshift=0.5, mass=al.mp.Isothermal),
source=af.Model(al.Galaxy, redshift=1.0, bulge=al.lp.Sersic),
),
fields=af.Model(
al.MassField, redshift=0.5, shear=af.Model(al.mp.ExternalShear)
),
)
instance = model.instance_from_prior_medians()

tracer = analysis_imaging_7x7.tracer_via_instance_from(instance=instance)

assert tracer.fields == [instance.fields]
assert all(field is not instance.fields for field in tracer.galaxies)


def test__log_likelihood__bare_and_collection_fields_agree(masked_imaging_7x7):
analysis = al.AnalysisImaging(dataset=masked_imaging_7x7, use_jax=False)
galaxies = af.Collection(
lens=al.Galaxy(redshift=0.5, mass=al.mp.IsothermalSph(einstein_radius=1.0)),
source=al.Galaxy(redshift=1.0, bulge=al.lp.SersicSph(intensity=1.0)),
)
field = af.Model(
al.MassField,
redshift=0.5,
shear=al.mp.ExternalShear(gamma_1=0.05, gamma_2=0.06),
)
flat = af.Collection(galaxies=galaxies, fields=field).instance_from_prior_medians()
collection = af.Collection(
galaxies=galaxies, fields=af.Collection(field=field)
).instance_from_prior_medians()
assert analysis.log_likelihood_function(flat) == pytest.approx(
analysis.log_likelihood_function(collection), rel=0, abs=1.0e-8
)
20 changes: 20 additions & 0 deletions test_autolens/analysis/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,23 @@ def _poison(*args, **kwargs):
assert isinstance(second, al.PositionsLH)
assert second.positions.array == pytest.approx(first.positions.array, 1.0e-8)
assert second.threshold == pytest.approx(first.threshold, 1.0e-8)


@pytest.mark.parametrize("form", ["flat", "collection", "absent", "none", "empty"])
def test__max_log_likelihood_fields__normalizes_model_slot(form, analysis_imaging_7x7):
field = al.MassField(redshift=0.5, shear=al.mp.ExternalShear(gamma_1=0.05))
instance = af.ModelInstance()
if form == "flat":
instance.fields = field
elif form == "collection":
instance.fields = af.ModelInstance({"field": field})
elif form == "none":
instance.fields = None
elif form == "empty":
instance.fields = af.ModelInstance()
result = res.Result(
samples_summary=al.m.MockSamplesSummary(max_log_likelihood_instance=instance),
analysis=analysis_imaging_7x7,
)
expected = [field] if form in ("flat", "collection") else []
assert result.max_log_likelihood_fields == expected
Loading
Loading