Cluster Lens Simulation Interface

This notebook shows the current progress in the group and cluster scale simulations.

Implemented:

  • NFWCluster deflector class: a NFW halo and subhalos that can be any deflector, accesible through the Deflector interface

  • Integration with Lens interface to simulate lens related quanities.

Not (yet) implemented:

  • Draw halo and subhalo population

  • Catalog of cluster halos and cluster members

  • Integration with LensPop

Setup

[1]:
from slsim.Deflectors.deflector import Deflector
from slsim.lens import Lens
from slsim.lens_pop import LensPop
from slsim.image_simulation import simulate_image, rgb_image_from_image_list

from astropy.cosmology import FlatLambdaCDM

import numpy as np
import matplotlib.pyplot as plt
from astropy.table import Table, vstack, hstack
from astropy import units as u
from lenstronomy.Util import param_util
from scipy.spatial.distance import cdist

from matplotlib.patches import Ellipse

Generate a population of sources and elliptical deflectors

[2]:
np.random.seed(0)
[3]:
# define a cosmology
cosmo = FlatLambdaCDM(H0=70, Om0=0.3)

# define a sky area
sky_area = u.Quantity(value=0.1, unit="deg2")


# define limits in the intrinsic deflector and source population (in addition to the skypy config
# file)
kwargs_deflector_cut = {"band": "g", "band_max": 26, "z_min": 0.01, "z_max": 2.5}
kwargs_source_cut = {"band": "g", "band_max": 26, "z_min": 0.1, "z_max": 5.0}

# run skypy pipeline and make galaxy-galaxy population class using LensPop
gg_lens_pop = LensPop(
    deflector_type="elliptical",
    source_type="galaxies",
    kwargs_deflector_cut=kwargs_deflector_cut,
    kwargs_source_cut=kwargs_source_cut,
    kwargs_mass2light=None,
    skypy_config=None,
    sky_area=sky_area,
    cosmo=cosmo,
)

Basic Example

This example shows the implemented features. It generates a lens from an slsim sampled source, and a halo + subhalos with manually set parameters. It is not intended to be a realistic cluster.

Set seed for reproduction

[4]:
np.random.seed(3)

Halo parameters:

[5]:
halo_dict = {
    "halo_mass": 1e14,
    "concentration": 5,
    "e1_mass": 0.1,
    "e2_mass": -0.1,
    "z": 0.42,
}

A random sample of deflectors as subhalos:

[8]:
subhalos_table = Table.read("../../tests/TestData/subhalos_table.fits")
subhalos_table
[8]:
Table length=10
zMcoeffellipticityphysical_sizestellar_massangular_sizemag_gmag_rmag_imag_zmag_Yvel_dispe1_lighte2_lighte1_masse2_massn_sersiccenter_xcenter_y
kpcrad
float64float64float64[5]float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64float64
0.40961161750606623-17.746593519483130.014461312431650886 .. 0.00175536714388915540.29106380998963070.61264684655328856134707325.9852485.449207115062017e-0724.98005252507366723.59594083261140423.0705908595308922.7676610485441422.57663477819682238.6881226614444-0.045284975537510.1416914374289071-0.00198916045032267430.0268295555564165324.02.19919979271609873.919514530899428
0.4116468964498543-21.334848979344310.23285635798664925 .. 0.00069416686982186370.151746528690044763.7017292439367973240837649679.13573.282729405263374e-0621.56749919504121520.02644532933734219.36318722238075218.98162002127137518.75254269621525275.0041773084429-0.052361870548927840.05551789525462122-0.065114204725414320.082825811031440964.00.86931810534148240.8279203242106586
0.357742197708797-17.700164743142040.11749415182192821 .. 0.00099637059188949850.092475424652424680.225172354120096458172885735.9472442.1789243925349325e-0724.55832075353514623.17465455234582422.6035016878848922.27300729138005822.085215345311457239.485347812618130.0234164866441432080.039984807160732470.088181949612030640.23935392522010144.0-1.4313058824457068-1.2971386999655654
0.43321408312382476-21.6248905977036760.08464660764530811 .. 0.00090064451612373720.210601429726903852.3919434702572593260035388352.96562.058095711291011e-0621.00055784490024319.92680322324476819.3304985662180719.01099168621368618.79413875740036276.910218445291430.105905813832651810.0111862716764030.012359011772878920.00086614016059798524.00.59133352874650819.373582206571212
0.40664485199633355-20.3239237199192860.04239681022192635 .. 0.0006429789184526460.32249553431383681.404950486715550282384629856.098111.2551227098076372e-0622.47027430045213220.99032540007169720.3952837248807920.06514454751023619.87646729406098256.2982137470725-0.0290372786047146770.1631091572224996-0.019279750040648650.100694377964713384.07.3695161120113845.00295994058848
0.4376178752381092-18.79360062524790.14206729280250247 .. 0.00166254099609517070.16470232034672170.425400109516700920230857612.758853.6388064941698217e-0724.2187230609285122.79910454619301422.14635237863257821.7939598266516321.565756843217763243.85574045984882-0.07541363864848340.03446838704341557-0.0181109454921803350.0089485049393253574.0-5.005510169566389-2.681860992222449
0.4099796727538578-16.5121415752633640.030875540197497997 .. 0.00022036147333511390.085138936003368050.116863255401985122792649800.87518071.038882899755727e-0726.3954932072787324.8302983651722324.18221028577788823.84050309375899323.65161860291757236.99130742917959-0.04249659567228350.0035772494809909310.09694801706010811-0.0171008073404092144.07.6440465411053559.268773948886318
0.4127768279274331-15.7213440320292770.07559942601411682 .. 0.000274496065645578760.266686682809988750.29850396733228581283273119.24104832.642817163990263e-0727.0426296093457825.6501365708085725.02227924190363224.6809954994505124.48110194359954235.93973169995965-0.03820392112039080.1303179954461028-0.0163117961459994140.075707783508774544.016.833942804162128-6.2521801083342154
0.43371538809999644-19.9064968339270860.1330919972384299 .. 0.00098325749157584230.081637128296556340.638891858618305746629527232.03935.493502154531822e-0722.8063612356777221.64547454324097221.08360473043468620.77394510871699420.535144539362967249.688294408392660.025357417048465660.0320738516738493660.112672804866140650.160643885459985654.0-3.0834953283783717-6.801424536872165
0.4470202859737583-21.258083492089640.03378577389732713 .. 0.0005074037326770160.342141000470442052.381466544421677219084869701.218542.012277977759689e-0621.85988572141898720.42502138134415519.70774715777286819.3527367324795619.15103583581944273.013723750120850.16408640886164540.064732085375738340.108433362761888610.0526784278237035154.0-4.1362056818869293.5504779276895233
[9]:
subhalos_list = [
    Deflector(deflector_type="EPL", deflector_dict=subhalo)
    for subhalo in subhalos_table
]
len(subhalos_list)
[9]:
10

Initialize a ClusterLens object to combine the components into a Lenstronomy model.

Draw sources until find a valid lens

[10]:
max_try = 100
i = 0
while i < max_try:
    source_dict = gg_lens_pop._sources.draw_source()

    cluster_lens = Lens(
        source_dict=source_dict,
        deflector_dict=halo_dict,
        deflector_kwargs={"subhalos_list": subhalos_list},
        deflector_type="NFW_CLUSTER",
        lens_equation_solver="lenstronomy_default",
        cosmo=cosmo,
    )
    i += 1
    if cluster_lens.validity_test(max_image_separation=50.0):
        print("valid!")
        break
valid!
[11]:
cluster_lens.einstein_radius_deflector
[11]:
21.033852434910667

Visualize the result

[12]:
img_g = simulate_image(cluster_lens, "g", 200)
img_r = simulate_image(cluster_lens, "r", 200)
img_i = simulate_image(cluster_lens, "i", 200)
img_rgb = rgb_image_from_image_list([img_i, img_r, img_g], 2)
plt.imshow(img_rgb, origin="lower")
plt.show()
../_images/notebooks_cluster_lens_19_0.png

Example drawing redMaPPer cluster

Draw a cluster from DES redMaPPer catalog and assign an slsim deflector for each cluster member and a cluster-sized halo based in scaling relations.

This feature is not yet implemented in slsim, but is a possible way of making realistic cluster lenses.

Set seed for reproduction

[54]:
np.random.seed(0)

redMaPPer cluster catalog

[55]:
clusters = Table.read("../../data/redMaPPer/clusters_example.fits")
members = Table.read("../../data/redMaPPer/members_example.fits")

Draw a cluster as an example

[56]:
cluster_id = np.random.choice(clusters["ID"])
cluster_data = clusters[clusters["ID"] == cluster_id][0]
members_data = members[members["ID"] == cluster_id]
members_data.sort("R")  # sort by distance to the center
[57]:
cluster_id
[57]:
209
[58]:
_bcgs_ids = [cluster_data[f"ID{i}"] for i in range(4)]
_bcgs_iprobs = [cluster_data[f"PCen{i}"] for i in range(4)]
_bcgs_iprobs = np.array(_bcgs_iprobs) / np.sum(_bcgs_iprobs)
bcg_id = np.random.choice(_bcgs_ids, p=_bcgs_iprobs)
bcg_data = members_data[members_data["ObjID"] == bcg_id]
[59]:
members_data["center_x"] = (members_data["RAJ2000"] - bcg_data["RAJ2000"]) * u.deg.to(
    u.arcsec
)
members_data["center_y"] = (members_data["DEJ2000"] - bcg_data["DEJ2000"]) * u.deg.to(
    u.arcsec
)
[60]:
members_data = members_data[
    "ID", "ObjID", "PMem", "R", "center_x", "center_y", "gmag", "rmag", "imag", "zmag"
]
members_data.rename_columns(
    ["gmag", "rmag", "imag", "zmag"], ["mag_g", "mag_r", "mag_i", "mag_z"]
)

Draw some deflectors

[61]:
n_deflectors = 5000
deflectors_table = []
for _ in range(n_deflectors):
    deflectors_table.append(gg_lens_pop._lens_galaxies.draw_deflector())
deflectors_table = vstack(deflectors_table)

Assign a deflector for each cluster member based on magnitude and color

[62]:
def assing_similar_deflector(
    members_table, deflectors_table, cluster_z, cosmo=None, bands=("g", "r", "i", "z")
):
    mag_members = [members_table[f"mag_{b}"].data for b in bands]
    mag_deflectors = [deflectors_table[f"mag_{b}"].data for b in bands]
    dist_mod_members = (
        -5
        * np.log10(cosmo.luminosity_distance(cluster_z) / (10 * u.pc))
        * np.ones(len(members_table))
    )
    dist_mod_deflectors = -5 * np.log10(
        cosmo.luminosity_distance(deflectors_table["z"].data) / (10 * u.pc)
    )
    distance = cdist(
        np.stack([*mag_members, dist_mod_members], axis=1),
        np.stack([*mag_deflectors, dist_mod_deflectors], axis=1),
        metric="euclidean",
    )
    nearest_neighbors_indices = distance.argmin(axis=1)
    similar_deflectors = deflectors_table[nearest_neighbors_indices]
    return similar_deflectors
[63]:
subhalos_table = assing_similar_deflector(
    members_data,
    deflectors_table,
    cluster_data["zlambda"],
    cosmo=cosmo,
    bands=("g", "r", "i", "z"),
)
subhalos_table = hstack(
    [subhalos_table, members_data["ID", "ObjID", "PMem", "center_x", "center_y"]]
)
[64]:
plt.hist(subhalos_table["z"], bins="auto", label="deflectors")
plt.axvline(cluster_data["zlambda"], c="r", ls="--", label="cluster")
plt.xlabel("redshift")
plt.legend()
plt.show()
../_images/notebooks_cluster_lens_36_0.png
[65]:
plt.scatter(members_data["mag_g"], subhalos_table["mag_g"], label="g")
plt.scatter(members_data["mag_r"], subhalos_table["mag_r"], label="r")
plt.scatter(members_data["mag_i"], subhalos_table["mag_i"], label="i")
plt.scatter(members_data["mag_z"], subhalos_table["mag_z"], label="z")
plt.plot(np.linspace(17, 27), np.linspace(17, 27), "k--")
plt.xlabel("member mag")
plt.ylabel("deflector mag")
plt.legend()
plt.show()
../_images/notebooks_cluster_lens_37_0.png
[66]:
plt.scatter(
    members_data["mag_g"] - members_data["mag_r"],
    subhalos_table["mag_g"] - subhalos_table["mag_r"],
    label="g - r",
)
plt.scatter(
    members_data["mag_r"] - members_data["mag_i"],
    subhalos_table["mag_r"] - subhalos_table["mag_i"],
    label="r - i",
)
plt.scatter(
    members_data["mag_i"] - members_data["mag_z"],
    subhalos_table["mag_i"] - subhalos_table["mag_z"],
    label="i - z",
)
plt.plot(np.linspace(0, 3.5), np.linspace(0, 3.5), "k--")
plt.xlabel("member color")
plt.ylabel("deflector color")
plt.legend()
plt.show()
../_images/notebooks_cluster_lens_38_0.png

We have to assign a halo to the cluster. By now we set the halo mass and concentration by hand, the ellipticity is chosen based on the distribution of the subhalos.

Set the halo ellipticity from subhalo distribution, weighted by stellar mass:

[67]:
cov_matrix = np.cov(
    np.stack([subhalos_table["center_x"], subhalos_table["center_y"]], axis=1),
    aweights=subhalos_table["stellar_mass"],
    rowvar=False,
)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
largest_eigenvector = eigenvectors[:, np.argmax(eigenvalues)]
phi = np.arctan2(largest_eigenvector[1], largest_eigenvector[0])
q = np.sqrt(np.min(eigenvalues) / np.max(eigenvalues))
e1, e2 = param_util.phi_q2_ellipticity(phi, q)

Visualize the members distribution, along with an ellipse describing the shape of the halo

[68]:
plt.scatter(
    subhalos_table["center_x"],
    subhalos_table["center_y"],
    s=subhalos_table["physical_size"] * 10,
    c=subhalos_table["stellar_mass"],
    cmap="Oranges",
)
plt.gca().add_patch(
    Ellipse(
        (0, 0), 400, 400 * q, angle=np.rad2deg(phi), facecolor="none", edgecolor="k"
    )
)
plt.xlabel("ra [arcsec]")
plt.ylabel("dec [arcsec]")
plt.show()
../_images/notebooks_cluster_lens_43_0.png

Halo NFW parameters, the mass and concetration are arbitrary:

[69]:
halo_dict = {
    "halo_mass": 2e14,  # TODO: assign mass based on halo mass function + mass-richness relation
    "concentration": 5,  # TODO: set concentration from mass-redshift-concentration relation
    "e1_mass": e1,
    "e2_mass": e2,
    "z": cluster_data["zlambda"],
}

for k, v in halo_dict.items():
    print(f"{k}: {v:1.2e}")
halo_mass: 2.00e+14
concentration: 5.00e+00
e1_mass: 2.29e-01
e2_mass: 1.53e-01
z: 5.74e-01

Initialize the subhalos as deflectors

[70]:
subhalos_list = [
    Deflector(deflector_type="EPL", deflector_dict=subhalo)
    for subhalo in subhalos_table
]
len(subhalos_list)
[70]:
73

Initialize a ClusterLens object to combine the components into a Lenstronomy model.

Draw sources until find a valid lens

[77]:
max_try = 100
i = 0
while i < max_try:
    source_dict = gg_lens_pop._sources.draw_source()

    cluster_lens = Lens(
        source_dict=source_dict,
        deflector_dict=halo_dict,
        deflector_kwargs={"subhalos_list": subhalos_list},
        deflector_type="NFW_CLUSTER",
        lens_equation_solver="lenstronomy_default",
        cosmo=cosmo,
    )
    i += 1
    if cluster_lens.validity_test(max_image_separation=100.0):
        print("valid!")
        break
assert cluster_lens.validity_test(max_image_separation=100.0)
valid!
[78]:
cluster_lens.einstein_radius_deflector
[78]:
8.821158724189685

Visualize the result

[79]:
img_g = simulate_image(cluster_lens, "g", 500)
img_r = simulate_image(cluster_lens, "r", 500)
img_i = simulate_image(cluster_lens, "i", 500)
img_rgb = rgb_image_from_image_list([img_i, img_r, img_g], 2)
plt.imshow(img_rgb, origin="lower")
plt.show()
../_images/notebooks_cluster_lens_52_0.png