import astropy.units as u
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from astropy.coordinates import SkyCoord
matplotlib.use("Agg") # Use the Agg backend for non-GUI rendering
import os
from astropy.io import fits
from astropy.table import Table
from astropy.visualization import make_lupton_rgb
from astropy.wcs import WCS
[docs]
def getimages(ra, dec, size=240, filters="grizy", type="stack"):
"""Query ps1filenames.py service to get a list of images.
:param ra: Right ascension of position, in degrees.
:type ra: float
:param dec: Declination of position, in degrees.
:type dec: float
:param size: The image size in pixels (0.25 arcsec/pixel)
:type size: int
:param filters: A string with the filters to include
:type filters: str
:return: The results of the search for relevant images.
:rtype: Astropy Table
"""
service = "https://ps1images.stsci.edu/cgi-bin/ps1filenames.py"
url = ("{service}?ra={ra}&dec={dec}&size={size}&format=fits" "&filters={filters}&type={type}").format(
**locals()
)
table = Table.read(url, format="ascii")
return table
[docs]
def geturl(ra, dec, size=240, output_size=None, filters="grizy", format="jpg", color=False, type="stack"):
"""Get the URL for images in the table.
:param ra: Right ascension of position, in degrees.
:type ra: float
:param dec: Declination of position, in degrees.
:type dec: float
:param size: The extracted image size in pixels (0.25 arcsec/pixel)
:type size: int
:param output_size: output (display) image size in pixels (default = size).
The output_size has no effect for fits format images.
:type output_size: int
:param filters: The string with filters to include.
:type filters: str
:param format: The data format (options are \\"jpg\\", \\"png" or \\"fits\\").
:type format: str
:param color: If True, creates a color image (only for jpg or png format).
If False, return a list of URLs for single-filter grayscale images.
:type color: bool, optional
:return: The url for the image to download.
:rtype: str
"""
if color and format == "fits":
raise ValueError("color images are available only for jpg or png formats")
if format not in ("jpg", "png", "fits"):
raise ValueError("format must be one of jpg, png, fits")
table = getimages(ra, dec, size=size, filters=filters, type=type)
url = (
"https://ps1images.stsci.edu/cgi-bin/fitscut.cgi?" "ra={ra}&dec={dec}&size={size}&format={format}"
).format(**locals())
if output_size:
url = url + f"&output_size={output_size}"
# sort filters from red to blue
flist = ["yzirg".find(x) for x in table["filter"]]
table = table[np.argsort(flist)]
if color:
if len(table) > 3:
# pick 3 filters
table = table[[0, len(table) // 2, len(table) - 1]]
for i, param in enumerate(["red", "green", "blue"]):
url = url + "&{}={}".format(param, table["filename"][i])
else:
urlbase = url + "&red="
url = []
for filename in table["filename"]:
url.append(urlbase + filename)
return url
[docs]
def get_ps1_pic(path, objid, ra, dec, size, band, safe=False, save=False):
"""Downloads PS1 picture (in fits) centered at a given location.
:param path: The filepath where the fits file will be saved.
:type path: str
:param objid: The PS1 objid of the object of interest (to save as filename).
:type objid: int
:param ra: Right ascension of position, in degrees.
:type ra: float
:param dec: Declination of position, in degrees.
:type dec: float
:param size: The extracted image size in pixels (0.25 arcsec/pixel)
:type size: int
:param band: The PS1 band.
:type band: str
:param safe: If True, include the objid of the object of interest in the filename
(useful when saving multiple files at comparable positions).
:type safe: bool, optional
"""
fitsurl = geturl(ra, dec, size=size, filters=f"{band}", format="fits")
fh = fits.open(fitsurl[0])
if save:
if safe:
fh.writeto(path + f"/PS1_{objid}_{int(size*0.25)}arcsec_{band}.fits")
else:
fh.writeto(path + f"/PS1_ra={ra}_dec={dec}_{int(size*0.25)}arcsec_{band}.fits")
else:
return fh
[docs]
def find_all(name, path):
"""Crawls through a directory and all its sub-directories looking for a file matching
\\'name\\'. If found, it is returned.
:param name: The filename for which to search.
:type name: str
:param path: The directory to search.
:type path: str
:return: The list of absolute paths to all files called \\'name\\' in \\'path\\'.
:rtype: list
"""
result = []
for root, _, files in os.walk(path):
if name in files:
result.append(os.path.join(root, name))
return result
[docs]
def plot_match(
host_ra,
host_dec,
true_host_ra,
true_host_dec,
host_z_mean,
host_z_std,
sn_ra,
sn_dec,
sn_name,
sn_z,
bayesflag,
fn,
):
"""Short summary.
Parameters
----------
host_ra : type
Description of parameter `host_ra`.
host_dec : type
Description of parameter `host_dec`.
true_host_ra : type
Description of parameter `true_host_ra`.
true_host_dec : type
Description of parameter `true_host_dec`.
host_z_mean : type
Description of parameter `host_z_mean`.
host_z_std : type
Description of parameter `host_z_std`.
sn_ra : type
Description of parameter `sn_ra`.
sn_dec : type
Description of parameter `sn_dec`.
sn_name : type
Description of parameter `sn_name`.
sn_z : type
Description of parameter `sn_z`.
bayesflag : type
Description of parameter `bayesflag`.
fn : type
Description of parameter `fn`.
Returns
-------
type
Description of returned object.
"""
cols = np.array(
[
"#ff9f1c",
"#2cda9d",
"#f15946",
"#da80dd",
"#f4e76e",
"#b87d4b",
"#ff928b",
"#c73e1d",
"#58b09c",
"#e7e08b",
]
)
bands = "zrg"
if len(host_ra) > 0:
sep = np.nanmax(
SkyCoord(host_ra * u.deg, host_dec * u.deg)
.separation(SkyCoord(sn_ra * u.deg, sn_dec * u.deg))
.arcsec
)
else:
sep = 0
if true_host_ra:
sep_true = (
SkyCoord(true_host_ra * u.deg, true_host_dec * u.deg)
.separation(SkyCoord(sn_ra * u.deg, sn_dec * u.deg))
.arcsec
)
if (true_host_ra) and (true_host_dec) and (sep_true > sep):
sep = sep_true
rad = np.nanmax([30.0, 2 * sep]) # arcsec to pixels, scaled by 1.5x host-SN separation
print(f"Getting img with size len {rad:.2f}...")
pic_data = []
for band in bands:
get_ps1_pic("./", None, sn_ra, sn_dec, int(rad * 4), band)
a = find_all(f"PS1_ra={sn_ra}_dec={sn_dec}_{int(rad)}arcsec_{band}.fits", ".")
pixels = fits.open(a[0])[0].data
pixels = pixels.astype("float32")
# normalize to the range 0-255
pixels *= 255 / np.nanmax(pixels)
# plt.hist(pixels)
pic_data.append(pixels)
hdu = fits.open(a[0])[0]
os.remove(a[0])
lo_val, up_val = np.nanpercentile(
np.array(pic_data).ravel(), (0.5, 99.5)
) # Get the value of lower and upper 0.5% of all pixels
stretch_val = up_val - lo_val
rgb_default = make_lupton_rgb(
pic_data[0], pic_data[1], pic_data[2], minimum=lo_val, stretch=stretch_val, Q=0
)
wcs = WCS(hdu.header)
plt.figure(num=None, figsize=(12, 8), facecolor="w", edgecolor="k")
ax = plt.subplot(projection=wcs)
ax.set_xlabel("RA", fontsize=24)
ax.set_ylabel("DEC", fontsize=24)
# really emphasize the supernova location
plt.axvline(x=int(rad * 2), c="tab:red", alpha=0.5, lw=2)
plt.axhline(y=int(rad * 2), c="tab:red", alpha=0.5, lw=2)
if true_host_ra and true_host_dec:
true_str = ""
ax.scatter(
true_host_ra,
true_host_dec,
transform=ax.get_transform("fk5"),
marker="+",
alpha=0.8,
lw=2,
s=200,
color="magenta",
zorder=100,
)
else:
true_str = "(no true)"
bayesstr = ". "
if bayesflag == 2:
bayesstr += "Strong match!"
# don't plot the second-best host
host_ra = host_ra[:1]
host_dec = host_dec[:1]
elif bayesflag == 1:
bayesstr += "Weak match."
if host_ra and host_dec:
for i in np.arange(len(host_ra)):
# print(f"Plotting host {i}")
ax.scatter(
host_ra[i],
host_dec[i],
transform=ax.get_transform("fk5"),
marker="o",
alpha=0.8,
lw=2,
s=100,
edgecolor="k",
facecolor=cols[i],
zorder=100,
)
if sn_z == sn_z:
plt.title(
f"{sn_name}, z={sn_z:.4f}; Host Match,"
f"z={host_z_mean:.4f}+/-{host_z_std:.4f} {true_str}{bayesstr}"
)
else:
plt.title(
f"{sn_name}, no z; Host Match, "
f"z={host_z_mean:.4f}+/-{host_z_std:.4f} {true_str}{bayesstr}"
)
else:
if sn_z == sn_z:
plt.title(f"{sn_name}, z={sn_z:.4f}; No host found {true_str}")
else:
plt.title(f"{sn_name}, no z; No host found {true_str}")
ax.imshow(rgb_default, origin="lower")
plt.axis("off")
plt.savefig("./%s.png" % fn, bbox_inches="tight")
plt.close()
# Function to diagnose the discrepancy when the top-ranked galaxy is not the true host
[docs]
def diagnose_ranking(
true_index,
post_probs,
galaxy_catalog,
post_offset,
post_z,
post_absmag,
galaxy_ids,
z_sn,
sn_position,
post_offset_true=None,
post_z_true=None,
post_absmag_true=None,
verbose=False,
):
"""Short summary.
Parameters
----------
true_index : type
Description of parameter `true_index`.
post_probs : type
Description of parameter `post_probs`.
galaxy_catalog : type
Description of parameter `galaxy_catalog`.
post_offset : type
Description of parameter `post_offset`.
post_z : type
Description of parameter `post_z`.
post_absmag : type
Description of parameter `post_absmag`.
galaxy_ids : type
Description of parameter `galaxy_ids`.
z_sn : type
Description of parameter `z_sn`.
sn_position : type
Description of parameter `sn_position`.
post_offset_true : type
Description of parameter `post_offset_true`.
post_z_true : type
Description of parameter `post_z_true`.
post_absmag_true : type
Description of parameter `post_absmag_true`.
verbose : type
Description of parameter `verbose`.
Returns
-------
type
Description of returned object.
"""
top_indices = np.argsort(post_probs)[-3:][::-1] # Top 3 ranked galaxies
if verbose:
if true_index > 0:
print(f"True Galaxy: {true_index + 1}")
# Check if the true galaxy is in the top 5
if true_index not in top_indices:
print(f"Warning: True Galaxy {true_index + 1} is not in the top 5!")
# Print top 5 and compare with the true galaxy
for rank, i in enumerate(top_indices, start=1):
is_true = "(True Galaxy)" if i == true_index and true_index > 0 else ""
print(
f"Rank {rank}: ID {galaxy_ids[top_indices[rank-1]]}"
f"has a Posterior probability of being the host: {post_probs[i]:.4f} {is_true}"
)
# Detailed comparison of the top-ranked and true galaxy
print(f"Coords (SN): {sn_position.ra.deg:.4f}, {sn_position.dec.deg:.4f}")
for _, i in enumerate(top_indices, start=1):
top_gal = galaxy_catalog[i]
top_theta = sn_position.separation(
SkyCoord(ra=top_gal["ra"] * u.degree, dec=top_gal["dec"] * u.degree)
).arcsec
if verbose:
print(f"Redshift (SN): {z_sn:.4f}")
print(f"Top Galaxy (Rank {i}): Coords: {top_gal['ra']:.4f}, {top_gal['dec']:.4f}")
print(
f"\t\t\tRedshift = {top_gal['z_best_mean']:.4f}+/-{top_gal['z_best_std']:.4f},"
" Angular Size = {top_gal['angular_size_arcsec']:.4f} arcsec"
)
print(f"\t\t\tFractional Sep. = {top_theta/top_gal['angular_size_arcsec']:.4f} host radii")
print(f'\t\t\tAngular Sep. ("): {top_theta:.2f}')
print(f"\t\t\tRedshift Posterior = {post_z[i]:.4e}," " Offset Posterior = {post_offset[i]:.4e}")
print(f"\t\t\tAbsolute mag Posterior = {post_absmag[i]:.4e}")
if verbose and true_index > 0:
true_gal = galaxy_catalog[true_index]
true_theta = sn_position.separation(
SkyCoord(ra=true_gal["ra"] * u.degree, dec=true_gal["dec"] * u.degree)
).arcsec
print(f"True Galaxy: Fractional Sep. = {true_theta/true_gal['angular_size_arcsec']:.4f} host radii")
print(
f"\t\t\tRedshift = {true_gal['redshift']:.4f}, "
f"Angular Size = {true_gal['angular_size_arcsec']:.4f}\""
)
print(f"\t\t\tRedshift Posterior = {post_z_true:.4e}, Offset Posterior = {post_offset_true:.4e}")
if true_index > 0:
post_offset_true = post_offset[true_index]
if true_index > 0:
post_z_true = post_z[true_index]
ranked_indices = np.argsort(post_probs)[::-1]
# Find the position of the true galaxy's index in the ranked list
true_rank = np.where(ranked_indices == true_index)[0][0] if true_index > 0 else -1
# Return the rank (0-based index) of the true galaxy
return true_rank, post_probs[true_index]