from typing import Dict, List, Optional, Type, Union
import numpy as np
import autoarray as aa
import autofit as af
from autoconf.dictable import instance_as_dict, to_dict
from autogalaxy import exc
from autogalaxy.operate.deflections import OperateDeflections
from autogalaxy.operate.image import OperateImageList
from autogalaxy.profiles.geometry_profiles import GeometryProfile
from autogalaxy.profiles.light.abstract import LightProfile
from autogalaxy.profiles.light.linear import LightProfileLinear
from autogalaxy.profiles.light.snr.abstract import LightProfileSNR
from autogalaxy.profiles.mass.abstract.abstract import MassProfile
[docs]
class Galaxy(af.ModelObject, OperateImageList, OperateDeflections):
"""
@DynamicAttrs
"""
def __init__(self, redshift: float, **kwargs):
"""
Class representing a galaxy, which is composed of attributes used for fitting galaxies (e.g. light profiles,
mass profiles, pixelizations, etc.).
All *has_* methods retun `True` if galaxy has that attribute, `False` if not.
Parameters
----------
redshift
The redshift of the galaxy.
pixelization
The pixelization of the galaxy used to reconstruct an observed image using an inversion.
"""
super().__init__()
self.redshift = redshift
for name, val in kwargs.items():
if isinstance(val, list):
raise exc.GalaxyException(
"One or more of the input light / mass profiles has been passed to the Galaxy object"
"as a list."
""
"The Galaxy object cannot accept a list of light / 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.:"
""
"{bulge : al.lp.Sersic()}"
""
)
setattr(self, name, val)
def __hash__(self):
return int(self.id)
def __repr__(self):
string = "Redshift: {}".format(self.redshift)
if self.has(cls=LightProfile):
string += "\nLight Profiles:\n{}".format(
"\n".join(map(str, self.cls_list_from(cls=LightProfile)))
)
if self.has(cls=MassProfile):
string += "\nMass Profiles:\n{}".format(
"\n".join(map(str, self.cls_list_from(cls=MassProfile)))
)
if self.has(cls=aa.Pixelization):
string += "\nPixelization:\n{}".format(str(self.pixelization))
return string
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) or isinstance(value, aa.Pixelization)
}
[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 cls_list_from(self, cls: Type, cls_filtered: Optional[Type] = None) -> List:
"""
Returns a list of objects in the galaxy which are an instance of the input `cls`.
The optional `cls_filtered` input removes classes of an input instance type.
For example:
- If the input is `cls=ag.LightProfile`, a list containing all light profiles in the galaxy is returned.
- If `cls=ag.LightProfile` and `cls_filtered=ag.LightProfileLinear`, a list of all light profiles
excluding those which are linear light profiles will be returned.
Parameters
----------
cls
The type of class that a list of instances of this class in the galaxy are returned for.
cls_filtered
A class type which is filtered and removed from the class list.
Returns
-------
The list of objects in the galaxy that inherit from input `cls`.
"""
return aa.util.misc.cls_list_from(
values=self.__dict__.values(), cls=cls, cls_filtered=cls_filtered
)
def _radial_projected_shape_slim_from(self, grid: aa.type.Grid1D2DLike) -> int:
"""
To make 1D plots (e.g. `image_1d_from()`) from an input 2D grid, one uses that 2D grid to radially project
the coordinates across the profile's major-axis.
This function computes the distance from the profile centre to the edge of this 2D grid.
Because the centres of the galaxy's light and mass profiles can be offset from one another, thsi means the
radially grid computed for each profile can have different shapes. Therefore plots using a `Galaxy` object
use the biggest radial grid.
If a 1D grid is input it returns the shape of this grid, as the grid itself defines the radial coordinates.
Parameters
----------
grid
A 1D or 2D grid from which a 1D plot of the profile is to be created.
"""
return max(
[
profile._radial_projected_shape_slim_from(grid=grid)
for key, profile in self.profile_dict.items()
]
)
def grid_radial_from(self, grid, centre, angle):
if isinstance(grid, aa.Grid1D) or isinstance(grid, aa.Grid2DIrregular):
return grid
radial_projected_shape_slim = self._radial_projected_shape_slim_from(grid=grid)
return grid.grid_2d_radial_projected_from(
centre=centre, angle=angle + 90, shape_slim=radial_projected_shape_slim
)
[docs]
def image_2d_list_from(
self, grid: aa.type.Grid2DLike, operated_only: Optional[bool] = None
) -> List[aa.Array2D]:
"""
Returns a list of the 2D images of the galaxy's light profiles from a 2D grid of Cartesian (y,x) coordinates.
If the galaxy has no light profiles, a numpy array of zeros is returned.
The images output by this function do not include instrument operations, such as PSF convolution (for imaging
data) or a Fourier transform (for interferometer data).
Inherited methods in the `autogalaxy.operate.image` package can apply these operations to the images.
These functions may have the `operated_only` input passed to them, which is why this function includes
the `operated_only` input.
If the `operated_only` input is included, the function omits light profiles which are parents of
the `LightProfileOperated` object, which signifies that the light profile represents emission that has
already had the instrument operations (e.g. PSF convolution, a Fourier transform) applied to it and therefore
that operation is not performed again.
See the `autogalaxy.profiles.light` package for details of how images are computed from a light
profile.
Parameters
----------
grid
The 2D (y, x) coordinates where values of the image are evaluated.
operated_only
By default, the returned list contains all light profile images (irrespective of whether they have been
operated on or not). If this input is included as a bool, only images which are or are not already
operated are included in the list, with the images of other light profiles created as a numpy array of
zeros.
"""
return [
light_profile.image_2d_from(grid=grid, operated_only=operated_only)
for light_profile in self.cls_list_from(
cls=LightProfile, cls_filtered=LightProfileLinear
)
]
[docs]
@aa.grid_dec.to_array
def image_2d_from(
self, grid: aa.type.Grid2DLike, operated_only: Optional[bool] = None
) -> Union[np.ndarray, aa.Array2D]:
"""
Returns the 2D image of all galaxy light profiles summed from a 2D grid of Cartesian (y,x) coordinates.
This function first computes the image of each galaxy, via the function `image_2d_list_from`. The
images are then summed to give the overall image of the galaxies.
Refer to the function `image_2d_list_from` for a full description of the calculation and how the `operated_only`
input is used.
Parameters
----------
grid
The 2D (y, x) coordinates where values of the image are evaluated.
operated_only
The returned list from this function contains all light profile images, and they are never operated on
(e.g. via the imaging PSF). However, inherited methods in the `autogalaxy.operate.image` package can
apply these operations to the images, which may have the `operated_only` input passed to them. This input
therefore is used to pass the `operated_only` input to these methods.
"""
if (
len(self.cls_list_from(cls=LightProfile, cls_filtered=LightProfileLinear))
> 0
):
return sum(self.image_2d_list_from(grid=grid, operated_only=operated_only))
return np.zeros((grid.shape[0],))
[docs]
@aa.grid_dec.to_projected
def image_1d_from(self, grid: aa.type.Grid2DLike) -> np.ndarray:
"""
Returns the summed 1D image of the galaxy's light profiles using a grid of Cartesian (y,x) coordinates.
If the galaxy has no light profiles, a grid of zeros is returned.
See `profiles.light` module for details of how this is performed.
The decorator `to_projected` converts the output arrays from ndarrays to an `Array1D` data
structure using the input `grid`'s attributes.
Parameters
----------
grid
The 1D (x,) coordinates where values of the image are evaluated.
"""
if self.has(cls=LightProfile):
image_1d_list = []
for light_profile in self.cls_list_from(
cls=LightProfile, cls_filtered=LightProfileLinear
):
grid_radial = self.grid_radial_from(
grid=grid, centre=light_profile.centre, angle=light_profile.angle
)
image_1d_list.append(light_profile.image_1d_from(grid=grid_radial))
return sum(image_1d_list)
return np.zeros((grid.shape[0],))
[docs]
@aa.grid_dec.to_vector_yx
def deflections_yx_2d_from(self, grid: aa.type.Grid2DLike, **kwargs) -> np.ndarray:
"""
Returns the summed 2D deflection angles of the galaxy's mass profiles from a 2D grid of Cartesian (y,x)
coordinates.
If the galaxy has no mass profiles, a numpy array of zeros is returned.
See the `autogalaxy.profiles.mass` package for details of how deflection angles are computed from a
mass profile.
The decorator `to_vector_yx` converts the output arrays from ndarrays to a `VectorYX2D` data structure
using the input `grid`'s attributes.
Parameters
----------
grid
The 2D (y, x) coordinates where values of the deflection angles are evaluated.
"""
if self.has(cls=MassProfile):
return sum(
map(
lambda p: p.deflections_yx_2d_from(grid=grid),
self.cls_list_from(cls=MassProfile),
)
)
return np.zeros((grid.shape[0], 2))
[docs]
@aa.grid_dec.to_array
def convergence_2d_from(self, grid: aa.type.Grid2DLike, **kwargs) -> np.ndarray:
"""
Returns the summed 2D convergence of the galaxy's mass profiles from a 2D grid of Cartesian (y,x) coordinates.
If the galaxy has no mass profiles, a numpy array of zeros is returned.
See the `autogalaxy.profiles.mass` package for details of how convergences are computed from a mass
profile.
The decorator `grid_2d_to_structure` converts the output arrays from ndarrays to an `Array2D` data structure
using the input `grid`'s attributes.
Parameters
----------
grid
The 2D (y, x) coordinates where values of the convergence are evaluated.
"""
if self.has(cls=MassProfile):
return sum(
map(
lambda p: p.convergence_2d_from(grid=grid),
self.cls_list_from(cls=MassProfile),
)
)
return np.zeros((grid.shape[0],))
[docs]
@aa.grid_dec.to_grid
def traced_grid_2d_from(self, grid: aa.type.Grid2DLike) -> aa.type.Grid2DLike:
"""
Trace an input grid using the galaxy's its deflection angles.
"""
return grid - self.deflections_yx_2d_from(grid=grid)
[docs]
@aa.grid_dec.to_projected
def convergence_1d_from(self, grid: aa.type.Grid1D2DLike) -> np.ndarray:
"""
Returns the summed 1D convergence of the galaxy's mass profiles using a grid of Cartesian (y,x) coordinates.
If the galaxy has no mass profiles, a grid of zeros is returned.
See `profiles.mass` module for details of how this is performed.
The decorator `to_projected` converts the output arrays from ndarrays to an `Array1D` data
structure using the input `grid`'s attributes.
Parameters
----------
grid
The 1D (x,) coordinates where values of the convergence are evaluated.
"""
if self.has(cls=MassProfile):
convergence_1d_list = []
for mass_profile in self.cls_list_from(cls=MassProfile):
grid_radial = self.grid_radial_from(
grid=grid, centre=mass_profile.centre, angle=mass_profile.angle
)
convergence_1d_list.append(
mass_profile.convergence_1d_from(grid=grid_radial)
)
return sum(convergence_1d_list)
return np.zeros((grid.shape[0],))
[docs]
@aa.grid_dec.to_array
def potential_2d_from(self, grid: aa.type.Grid2DLike, **kwargs) -> np.ndarray:
"""
Returns the summed 2D potential of the galaxy's mass profiles from a 2D grid of Cartesian (y,x) coordinates.
If the galaxy has no mass profiles, a numpy array of zeros is returned.
See the `autogalaxy.profiles.mass` package for details of how potentials are computed from a mass
profile.
The decorator `grid_2d_to_structure` converts the output arrays from ndarrays to an `Array2D` data structure
using the input `grid`'s attributes.
Parameters
----------
grid
The 2D (y, x) coordinates where values of the potential are evaluated.
"""
if self.has(cls=MassProfile):
return sum(
map(
lambda p: p.potential_2d_from(grid=grid),
self.cls_list_from(cls=MassProfile),
)
)
return np.zeros((grid.shape[0],))
[docs]
@aa.grid_dec.to_projected
def potential_1d_from(self, grid: aa.type.Grid2DLike) -> np.ndarray:
"""
Returns the summed 1D potential of the galaxy's mass profiles using a grid of Cartesian (y,x) coordinates.
If the galaxy has no mass profiles, a grid of zeros is returned.
See `profiles.mass` module for details of how this is performed.
The decorator `to_projected` converts the output arrays from ndarrays to an `Array1D` data
structure using the input `grid`'s attributes.
Parameters
----------
grid
The 1D (x,) coordinates where values of the potential are evaluated.
"""
if self.has(cls=MassProfile):
potential_1d_list = []
for mass_profile in self.cls_list_from(cls=MassProfile):
grid_radial = self.grid_radial_from(
grid=grid, centre=mass_profile.centre, angle=mass_profile.angle
)
potential_1d_list.append(
mass_profile.potential_1d_from(grid=grid_radial)
)
return sum(potential_1d_list)
return np.zeros((grid.shape[0],))
@property
def half_light_radius(self):
return None
[docs]
def luminosity_within_circle_from(self, radius: float):
"""
Returns the total luminosity of the galaxy's light profiles within a circle of specified radius.
See `light_profile.luminosity_within_circle` for details of how this is performed.
Parameters
----------
radius
The radius of the circle to compute the dimensionless mass within.
unit_luminosity
The unit_label the luminosity is returned in {esp, counts}.
exposure_time
The exposure time of the observation, which converts luminosity from electrons per second unit_label to counts.
"""
if self.has(cls=LightProfile):
return sum(
map(
lambda p: p.luminosity_within_circle_from(radius=radius),
self.cls_list_from(
cls=LightProfile, cls_filtered=LightProfileLinear
),
)
)
[docs]
def mass_angular_within_circle_from(self, radius: float):
"""
Integrate the mass profiles's convergence profile to compute the total mass within a circle of \
specified radius. This is centred on the mass profile.
The following unit_label for mass can be specified and output:
- Dimensionless angular unit_label (default) - 'angular'.
- Solar masses - 'angular' (multiplies the angular mass by the critical surface mass density).
Parameters
----------
radius : dim.Length
The radius of the circle to compute the dimensionless mass within.
unit_mass
The unit_label the mass is returned in {angular, angular}.
critical_surface_density or None
The critical surface mass density of the strong lens configuration, which converts mass from angulalr \
unit_label to phsical unit_label (e.g. solar masses).
"""
if self.has(cls=MassProfile):
return sum(
map(
lambda p: p.mass_angular_within_circle_from(radius=radius),
self.cls_list_from(cls=MassProfile),
)
)
else:
raise exc.GalaxyException(
"You cannot perform a mass-based calculation on a galaxy which does not have a mass-profile"
)
[docs]
def set_snr_of_snr_light_profiles(
self,
grid: aa.type.Grid2DLike,
exposure_time: float,
background_sky_level: float = 0.0,
psf: Optional[aa.Kernel2D] = None,
):
"""
Iterate over every galaxy finding all `LightProfileSNR` light profiles and set their `intensity` values to
values which give their input `signal_to_noise_ratio` value, which is performed as follows:
- Evaluate the image of each light profile on the input grid.
- Blur this image with a PSF, if included.
- Take the value of the brightest pixel.
- Use an input `exposure_time` and `background_sky` (e.g. from the `SimulatorImaging` object) to determine
what value of `intensity` gives the desired signal to noise ratio for the image.
Parameters
----------
grid
The (y, x) coordinates in the original reference frame of the grid.
exposure_time
The exposure time of the simulated imaging.
background_sky_level
The level of the background sky of the simulated imaging.
psf
The psf of the simulated imaging which can change the S/N of the light profile due to spreading out
the emission.
"""
for light_profile in self.cls_list_from(cls=LightProfile):
if isinstance(light_profile, LightProfileSNR):
light_profile.set_intensity_from(
grid=grid,
exposure_time=exposure_time,
background_sky_level=background_sky_level,
psf=psf,
)