Note
This page is generated from the Jupyter notebook
notebooks/07_sersic_host.ipynb in the
repository. You can download it and run it interactively.
Sersic-profile hosts: a transient on a galaxy
A scene host is normally either unresolved (an integrated mag; it rides the source PSF) or a uniform surface brightness (surface_brightness=True; the same value in every pixel). A real transient host is neither: a TDE sits on the cusp of a bulge, a supernova somewhere out on a disk, and how much host light lands under the aperture depends on the galaxy’s shape and on where the source sits in it.
host_prop={"profile": "sersic", ...} renders the host as an elliptical Sersic profile,
convolves it with the PSF, and shares that one image with every Simulation method: simulate, get_snr, get_peak_pixel, is_saturated and get_image_exptime_for_snr.
key |
unit |
default |
meaning |
|---|---|---|---|
|
str |
required |
|
|
arcsec |
required |
half-light radius along the major axis |
|
1 |
Sersic index: 1 is an exponential disk, 4 a de Vaucouleurs bulge |
|
|
0 |
ellipticity, 1 - b/a |
|
|
deg |
0 |
major-axis position angle, counter-clockwise from +x |
|
arcsec |
0 |
offset of the host centre from the source (+x right, +y up) |
mag keeps its usual meaning: with the default surface_brightness=False it is the total integrated host magnitude; with surface_brightness=True it is mu_e, the surface brightness at r_eff in mag/arcsec^2. The parameters are angular only, so convert physical sizes and absolute magnitudes yourself. Typical r-band values are n = 1 to 4, r_e of 0.3 to 1 arcsec for hosts at z of 0.5 to 1 and 1 to 10 arcsec for nearby galaxies, and mu_e of 20 to 24 mag/arcsec^2.
[1]:
import matplotlib.pyplot as plt
import numpy as np
import wcc_etc
from wcc_etc import Simulation, get_scene
from wcc_etc.psfsim import ImageSimulator
wcc_etc.set_wcc_style()
SENSOR = "zwo:r" # in-focus r band on the IMX455
TIME = 60.0 # seconds
NPIX = 256 # 256 px = 4.3 arcsec on a side
def host_scene(source_mag=21, **host):
"""A mag-21 G5V transient on a G5V Sersic host; kwargs override host defaults."""
prop = {"mag": 16, "profile": "sersic", "r_eff": 0.5, "n": 4} | host
return get_scene(
"G5V", mag=source_mag, host="G5V", host_prop=prop, background="zodi"
)
1. What the host looks like on the detector
Four hosts of total magnitude 16 with the mag-21 transient (white cross) at the grid centre. Noiseless 60 s images on a log stretch, through the simulated image’s own plot, with the axes in arcsec to match the host parameters.
[2]:
cases = [
(dict(n=1, r_eff=0.5), 'n=1 disk, $r_e$ = 0.5"'),
(dict(n=4, r_eff=0.5), 'n=4 bulge, $r_e$ = 0.5"'),
(dict(n=1, r_eff=1.0, ellip=0.5, pa=45), 'n=1, $r_e$ = 1.0", ellip 0.5, PA 45'),
(
dict(n=4, r_eff=0.3, ellip=0.3, pa=120, dx=0.6),
'n=4, $r_e$ = 0.3", source 0.6" off',
),
]
fig, axes = plt.subplots(1, 4, figsize=(16, 4.2))
for ax, (kw, title) in zip(axes, cases):
imsim = ImageSimulator.from_sensorfilter(SENSOR, host_scene(**kw), npix=NPIX)
img = imsim.simulate(TIME, add_noise=False)
img.plot(
ax=ax, noise=False, units="arcsec", cmap="magma", colorbar=False, title=title
)
ax.plot(0, 0, "+", color="w", ms=10, mew=1)
fig.suptitle("Noiseless 60 s images; host total mag 16, transient mag 21 (+)", y=1.02)
fig.tight_layout()
plt.show()
[wcc_etc] note: even n_pixels is rounded up by 1 to center the PSF peak (shown once).
One noisy realization of the last case. The transient is the faint point 0.6 arcsec to the left of the nucleus.
[3]:
imsim = ImageSimulator.from_sensorfilter(
SENSOR, host_scene(n=4, r_eff=0.3, ellip=0.3, pa=120, dx=0.6), npix=NPIX
)
imsim.simulate(TIME, seed=1).plot(
units="arcsec", title='n=4 host, source 0.6" off the nucleus, 60 s'
)
plt.show()
2. The two normalizations
With the default total-magnitude mode, a compact host (r_eff = 0.1 arcsec, well inside the grid) must carry the same count rate as a point source of the same magnitude. With surface_brightness=True the pixel at r_eff must read the same per-pixel rate as a uniform host of that surface brightness. The two checks below read the rendered host image through _image_render_bundle and _count_rate_components. Those are internal methods, shown here only to verify the normalization;
they are not part of the public API and may change.
[4]:
# total-magnitude mode vs a point source of the same magnitude
sim = Simulation.from_sensorfilter(SENSOR, host_scene(r_eff=0.1, n=1))
b = sim._image_render_bundle(sim.default_psf, None, 128, 11)
point = Simulation.from_sensorfilter(SENSOR, get_scene("G5V", mag=16))
point_rate = point._count_rate_components()["source_rate_total"]
host_rate = b["extended_rate_image"].sum()
print(f"point source, mag 16 : {point_rate:9.1f} e-/s")
print(
f"Sersic host, mag 16 : {host_rate:9.1f} e-/s ratio {host_rate / point_rate:.4f}"
)
# mu_e mode vs the uniform surface-brightness host
sim_sb = Simulation.from_sensorfilter(
SENSOR, host_scene(mag=21, surface_brightness=True, r_eff=0.5, n=1)
)
b_sb = sim_sb._image_render_bundle(sim_sb.default_psf, None, 128, 11)
uniform = Simulation.from_sensorfilter(
SENSOR,
get_scene(
"G5V", mag=21, host="G5V", host_prop={"mag": 21, "surface_brightness": True}
),
)._count_rate_components()["diffuse_rate_per_pix"]
c = 63
r_pix = int(round(0.5 / (b_sb["plate_scale_mas"] / 1000)))
at_re = b_sb["extended_rate_image"][c, c + r_pix]
print(f"uniform SB, mu = 21 : {uniform:9.4f} e-/s/pix")
print(f"Sersic at r_eff : {at_re:9.4f} e-/s/pix ratio {at_re / uniform:.3f}")
point source, mag 16 : 17921.5 e-/s
Sersic host, mag 16 : 17911.6 e-/s ratio 0.9994
uniform SB, mu = 21 : 0.0510 e-/s/pix
Sersic at r_eff : 0.0500 e-/s/pix ratio 0.981
3. Nuclear vs offset transient
SNR of the mag-21 transient in 60 s against the host’s total magnitude, with the source at the nucleus (TDE-like) and offset by 0.5 and 1.5 arcsec (SN-like), on an n=4, r_eff = 0.5 arcsec bulge. plot_sweep("host__mag", ...) runs each curve and restores the host magnitude afterwards; host__dx is set by hand before each sweep. The dashed line is the SNR with no host.
[5]:
host_mags = np.arange(13, 20.5, 0.5)
no_host = Simulation.from_sensorfilter(SENSOR, get_scene("G5V", mag=21))
hostless = no_host.get_snr(TIME)["snr"]
sim = Simulation.from_sensorfilter(SENSOR, host_scene())
fig, ax = plt.subplots(figsize=(7, 4.5))
for dx in (0.0, 0.5, 1.5):
sim.update(host__dx=dx)
sim.plot_sweep(
"host__mag",
host_mags,
time=TIME,
ax=ax,
label=f'source {dx:.1f}" from the nucleus',
)
ax.axhline(hostless, color="0.4", ls="--", label="no host")
ax.set_xlabel("host total magnitude (r)")
ax.set_ylabel("SNR of a mag-21 transient in 60 s")
ax.set_title('n=4 bulge, $r_e$ = 0.5"')
ax.legend()
plt.show()
for dx in (0.0, 1.5):
sim.update(host__mag=13.0, host__dx=dx)
ratio = sim.get_snr(TIME)["snr"] / hostless
print(f'mag-13 host, source {dx:.1f}" out: SNR is {ratio:.2f} of the no-host value')
sim.update(host__mag=16.0, host__dx=0.0)
/home/docs/checkouts/readthedocs.org/user_builds/schmidt-observatories-wcc-etc/envs/latest/lib/python3.11/site-packages/wcc_etc/simulation.py:1210: UserWarning: detector saturates — up to 61 pixel(s) at or above full well / ADC clip (per-frame, n_reads=1); SNR is unreliable. See is_saturated/get_peak_pixel.
return _maybe_warn(_snr_at(time.to(u.second).value, source_scale))
mag-13 host, source 0.0" out: SNR is 0.08 of the no-host value
mag-13 host, source 1.5" out: SNR is 0.84 of the no-host value
4. How compact is the host?
At fixed total magnitude, a more compact host puts more light under the nuclear aperture, so the SNR of a nuclear transient falls as r_eff shrinks.
[6]:
r_effs = [0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 1.5, 2.0]
fig, ax = plt.subplots(figsize=(7, 4.5))
for n in (1, 4):
sim = Simulation.from_sensorfilter(SENSOR, host_scene(n=n))
sim.plot_sweep(
"host__r_eff", r_effs, time=TIME, ax=ax, marker="o", label=f"n = {n}"
)
ax.axhline(hostless, color="0.4", ls="--", label="no host")
ax.set_xlabel("host $r_e$ [arcsec]")
ax.set_ylabel("SNR of a nuclear mag-21 transient in 60 s")
ax.set_title("host total mag 16, source at the nucleus")
ax.legend()
plt.show()
5. One charge budget
The same host image feeds the saturation and exposure-time APIs. A bright compact bulge saturates the nucleus long before the transient would, and get_image_exptime_for_snr inverts get_snr with the host included.
[7]:
bright = Simulation.from_sensorfilter(SENSOR, host_scene(mag=12, r_eff=0.3, n=4))
print(
"peak pixel in 60 s with a mag-12 n=4 host:",
bright.get_peak_pixel(TIME, units="e-"),
)
print("saturated?", bool(bright.is_saturated(TIME)))
sim = Simulation.from_sensorfilter(SENSOR, host_scene())
target = sim.get_snr(TIME)["snr"]
t_back = sim.get_image_exptime_for_snr(target)["time_s"]
print(f"get_snr(60 s) = {target:.3f} -> get_image_exptime_for_snr = {t_back:.2f} s")
peak pixel in 60 s with a mag-12 n=4 host: 245804.7465461498 electron
saturated? True
get_snr(60 s) = 28.813 -> get_image_exptime_for_snr = 60.00 s
Summary
host_prop={"profile": "sersic", "r_eff": ..., "n": ..., "ellip": ..., "pa": ..., "dx": ..., "dy": ...}with angular parameters;magis the total host magnitude, or mu_e withsurface_brightness=True.One PSF-convolved host image is shared by
simulate,get_snr,get_image_exptime_for_snr,get_peak_pixelandis_saturated.The profile parameters are mutable, so
plot_sweep("host__mag", ...)andplot_sweep("host__r_eff", ...)give the SNR curves directly.Host shot noise is what costs SNR: a mag-13 bulge cuts the mag-21 transient’s SNR to under a tenth of the no-host value at the nucleus, and by about 16 percent 1.5 arcsec out. A bright compact bulge can also saturate the nucleus on its own.