Source code for autogalaxy.galaxy.mass_field

"""
The `MassField` class is the redshift-bearing container for the mass components which describe the tidal field
of everything *outside* the system being modelled.

A strong lens model does not only contain the mass of the galaxies being fitted: line-of-sight structure and the
group or cluster environment the lens sits in also deflect light. These contributions are described by mass
components with no light of their own — `ExternalShear`, `MassSheet` and `ExternalPotential` are the intended
ones, though any `MassProfile` is accepted (e.g. an NFW "environment" halo standing in for a nearby group).

A `MassField` is its own thing, not a `Galaxy` subclass. It contributes nothing to light and everything to mass,
so a `Galaxies` plane can hold one beside its galaxies and every aggregate mass calculation (deflections,
convergence, potential) picks it up while every light calculation sees zeros.

The galaxy-attached form remains fully supported and is not deprecated: writing
`Galaxy(redshift=0.5, shear=ExternalShear(gamma_1=0.05, gamma_2=0.05))` behaves exactly as it always has. The
`MassField` simply gives the external field a home of its own, so it need not be bolted onto a galaxy whose
light and mass describe something physically different.

In **PyAutoLens** the consumer is `Tracer(fields=...)`, which places each `MassField` at its own redshift in the
multi-plane ray-tracing calculation. That is phase 2 of this work and lives in PyAutoLens, not in this repo.
"""

from typing import Dict, List, Optional

import numpy as np

from autonerves.dictable import instance_as_dict, to_dict

import autoarray as aa
import autofit as af

from autogalaxy import exc
from autogalaxy.galaxy.mass_aggregate import MassProfileAggregate
from autogalaxy.profiles.geometry_profiles import GeometryProfile
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
from autogalaxy.profiles import validate


[docs] class MassField(af.ModelObject, MassProfileAggregate): """ The mass components describing the tidal field of everything outside the modelled system, at a redshift. A `MassField` holds its components as named keyword-argument attributes, exactly like a `Galaxy`, so the user accesses them by name (e.g. `field.shear`, `field.mass_sheet`). Only `MassProfile` objects are accepted: light profiles, pixelizations and regularizations belong on a `Galaxy`. The intended components are the ones which describe external structure rather than a galaxy — `ExternalShear`, `MassSheet` and `ExternalPotential` — but any `MassProfile` may be used, for example an NFW halo representing the group environment. Because it has no light, its image is zeros everywhere and its light-profile list is empty. A `Galaxies([galaxy, field])` therefore sums mass contributions from both and light from the galaxy alone. @DynamicAttrs """ def __init__(self, redshift: float, **kwargs): """ Class representing the external mass field at a given redshift. Parameters ---------- redshift The redshift of the external field. kwargs The named mass components of the field (e.g. `shear`, `mass_sheet`, `potential`), each of which must be a `MassProfile`. """ # `label` is an `af.ModelObject` attribute, not a component of the field. It rides in the `arguments` # of `dict()` and therefore comes back as a keyword argument on a `from_dict` round-trip, so it is # handed to `ModelObject` rather than validated as a mass profile. super().__init__(label=kwargs.pop("label", None)) validate.validate_redshift(redshift=redshift) self.redshift = redshift for name, val in kwargs.items(): if isinstance(val, list): raise exc.GalaxyException( "One or more of the input mass profiles has been passed to the MassField object" "as a list." "" "The MassField object cannot accept a list of mass profiles. " "" "Instead, pass these objects as a dictionary, where the key of each dictionary entry is" "the name of the profile and the value is the profile, e.g.:" "" "{shear : al.mp.ExternalShear()}" "" ) if not isinstance(val, MassProfile): raise exc.GalaxyException( f"MassField received '{name}' = {type(val).__name__}, which is not a MassProfile. A " f"MassField holds only the mass components describing the tidal field of everything " f"outside the modelled system (ExternalShear, MassSheet, ExternalPotential, or any other " f"MassProfile); put light profiles, pixelizations and regularizations on a Galaxy." ) setattr(self, name, val) def __hash__(self): return int(self.id) def __repr__(self): profile_names = ", ".join(self.profile_dict.keys()) if profile_names: return f"MassField(redshift={self.redshift}, {profile_names})" return f"MassField(redshift={self.redshift})" def __eq__(self, other): return self.dict() == other.dict() @property def profile_dict(self) -> Dict: return { key: value for key, value in self.__dict__.items() if isinstance(value, GeometryProfile) }
[docs] def dict(self) -> Dict: d = instance_as_dict(self) d["arguments"] = { **d.get("arguments", {}), **{name: to_dict(profile) for name, profile in self.profile_dict.items()}, } return d
[docs] def image_2d_list_from( self, grid: aa.type.Grid2DLike, xp=np, operated_only: Optional[bool] = None ) -> List[aa.Array2D]: """ Returns an empty list, because a `MassField` holds no light profiles. The `Galaxies` class builds its list of images by calling this on each member, so the field simply contributes nothing to it. """ return []
[docs] def image_2d_list_unbinned_from( self, grid: aa.Grid2D, xp=np, operated_only: Optional[bool] = None ) -> List[np.ndarray]: """Returns an empty list: a `MassField` holds no light profiles to evaluate before binning.""" return []
[docs] @aa.decorators.to_array def image_2d_from( self, grid: aa.type.Grid2DLike, xp=np, operated_only: Optional[bool] = None ) -> np.ndarray: """ Returns a 2D image of zeros, because a `MassField` has no light. This mirrors the zeros returned by `Galaxy.image_2d_from` for a galaxy with no light profiles, so the sum performed by `Galaxies.image_2d_from` is unaffected by the presence of a field. """ return xp.zeros((grid.shape[0],))
[docs] def image_2d_unbinned_from( self, grid: aa.Grid2D, xp=np, operated_only: Optional[bool] = None ) -> np.ndarray: """Returns zeros on the over-sampled grid, mirroring the light-less path of `Galaxy`.""" return xp.zeros((grid.over_sampled.shape[0],))