import re
from pathlib import Path
import astropy.units as u
import numpy as np
from astropy.coordinates import SkyCoord
from astropy.table import Table
__all__ = [
"ApertureAIJ",
"MultiApertureAIJ",
"ApertureFileAIJ",
"generate_aij_table",
"parse_aij_table",
"Star",
]
[docs]
class ApertureAIJ:
"""
Represents the aperture information AstroImageJ saves.
Attributes
----------
backplane : bool
Not sure what this does but stellarphot doesn't use it. Default: False
radius : float
Aperture radius. Default: 15.0
rback1 : float
Inner annulus radius. Default: 27.0
rback2 : float
Outer annulus radius. Default: 41.0
removebackstars : bool
Whether to remove stars in the annulus from the photometry. Default: True
"""
def __init__(self):
# Outer annulus radius
self.rback2 = 41.0
# Inner annulus radius
self.rback1 = 27.0
# Aperture radius
self.radius = 15.0
# Defaults for this match the defaults for stellarphot.
self.removebackstars = True
# Not sure what this does but stellarphot doesn't use it.
self.backplane = False
def __setattr__(self, attr, value):
floats = ["rback1", "rback2", "radius"]
bools = ["removebackstars", "backplane"]
if attr in floats:
super().__setattr__(attr, float(value))
elif attr in bools:
if isinstance(value, str):
value = True if value.lower() == "true" else False
super().__setattr__(attr, value)
def __eq__(self, other):
attributes = ["rback1", "rback2", "radius", "removebackstars", "backplane"]
equal = [getattr(self, attr) == getattr(other, attr) for attr in attributes]
return all(equal)
[docs]
class MultiApertureAIJ:
"""
Multi-aperture information that AstroImageJ saves.
Attributes
----------
absmagapertures : list
List of absolute magnitudes of the apertures.
apfwhmfactor : float
Factor to multiply the aperture radius by relative to the FWHM. Default: 1.4
centroidstar : list
List of booleans indicating whether the aperture is a centroid star.
decapertures : list
List of Declinations of the apertures.
isalignstar : list
List of booleans indicating whether the aperture is an alignment star.
isrefstar : list
List of booleans indicating whether the aperture is a reference star.
naperturesmax : float
Maximum number of apertures. Default: 1000
raapertures : list
List of Right Ascensions of the apertures.
usevarsizeap : bool
Whether to use variable size apertures. Default: False
xapertures : list
List of x positions of the apertures.
yapertures : list
List of y positions of the apertures.
"""
def __init__(self):
# Default values for these chosen to match AIJ defaults
# They are not used by stellarphot
self.naperturesmax = 1000
self.apfwhmfactor = 1.4
self.usevarsizeap = False
# Each attribute below should be list-like, all of the same length
self.isrefstar = []
self.centroidstar = []
self.isalignstar = []
self.xapertures = []
self.yapertures = []
self.absmagapertures = []
self.raapertures = []
self.decapertures = []
def __setattr__(self, name, value):
floats = ["naperturesmax", "apfwhmfactor"]
bools = ["usevarsizeap"]
lists = [
"isrefstar",
"centroidstar",
"isalignstar",
"xapertures",
"yapertures",
"absmagapertures",
"raapertures",
"decapertures",
]
list_is_bool = ["isrefstar", "centroidstar", "isalignstar"]
if name not in (floats + bools + lists):
raise AttributeError(f"Attribute {name} does not exist")
if name in floats:
value = float(value)
elif name in bools:
if isinstance(value, str):
value = True if value.lower() == "true" else False
else:
value = bool(value)
elif name in lists:
if (
(name in list_is_bool)
and (len(value) > 0)
and isinstance(value[0], str)
):
value = [True if v.lower() == "true" else False for v in value]
value = list(value)
super().__setattr__(name, value)
def __eq__(self, other):
simple_attrs = [
"naperturesmax",
"apfwhmfactor",
"usevarsizeap",
"isrefstar",
"centroidstar",
"isalignstar",
]
float_attrs = [
"xapertures",
"yapertures",
"absmagapertures",
"raapertures",
"decapertures",
]
simple_eq = [
getattr(self, attr) == getattr(other, attr) for attr in simple_attrs
]
float_eq = [
np.allclose(getattr(self, attr), getattr(other, attr), equal_nan=True)
for attr in float_attrs
]
equal = simple_eq + float_eq
return all(equal)
[docs]
class ApertureFileAIJ:
"""
Class to represent AstroImageJ aperture file.
Attributes
----------
aperture : `~stellarphot.io.ApertureAIJ`
Aperture information.
`
multiaperture : `~stellarphot.io.MultiApertureAIJ`
Multi-aperture information.
"""
def __init__(self):
self.aperture = ApertureAIJ()
self.multiaperture = MultiApertureAIJ()
def __str__(self):
lines = []
top_level_attrib = vars(self)
for name, attrib in top_level_attrib.items():
base_attrib = vars(attrib)
for bname, battrib in base_attrib.items():
try:
value = ",".join([str(v).lower() for v in battrib])
except TypeError:
value = battrib
if value is True:
value = "true"
elif value is False:
value = "false"
lines.append(f".{name}.{bname}={value}")
# Add a trailing blank line
return "\n".join(lines) + "\n"
def __eq__(self, other):
return (self.aperture == other.aperture) and (
self.multiaperture == other.multiaperture
)
[docs]
def write(self, file):
"""
Write the apertures data in class to a file.
Parameters
----------
file : str
Name of the file to write.
"""
p = Path(file)
p.write_text(str(self))
[docs]
@classmethod
def read(cls, file):
"""
Populate ApertureFileAIJ class by reading AstroImageJ apertures datafile.
Each line is basically a path to an attribute name followed by a value.
Parameters
----------
file : str
Name of the file to read.
"""
# Make the instance to return
aij_aps = cls()
with open(file) as f:
for line in f:
class_path, value = line.strip().split("=")
# There is always a leading dot
_, attr1, attr2 = class_path.split(".")
obj = getattr(aij_aps, attr1)
# value is either a single value or a list of values separated by commas
vals = value.split(",")
if len(vals) == 1:
val_to_set = vals[0]
else:
try:
val_to_set = [float(v) for v in vals]
except ValueError:
val_to_set = list(vals)
setattr(obj, attr2, val_to_set)
return aij_aps
[docs]
@classmethod
def from_table(
cls,
aperture_table,
aperture_rad=None,
inner_annulus=None,
outer_annulus=None,
default_absmag=99.999,
default_isalign=True,
default_centroidstar=True,
y_size=4096,
):
"""
Create an `~stellarphot.io.ApertureFileAIJ` instance from a stellarphot aperture
table and info about the aperture sizes.
Parameters
----------
aperture_table : `astropy.table.Table`
Table of aperture information.
aperture_rad : number
Radius of aperture.
inner_annulus : number
Inner radius of annulus.
outer_annulus : number
Outer radius of annulus.
Returns
-------
apAIJ: `~stellarphot.io.ApertureFileAIJ`
ApertureFileAIJ object representing the aperture table.
"""
# Create the instance
apAIJ = cls()
# Populate aperture properties
apAIJ.aperture.rback2 = outer_annulus
apAIJ.aperture.rback1 = inner_annulus
apAIJ.aperture.radius = aperture_rad
# Remaining properties of apAIJ.aperture default to the
# correct values for stellarphot
# Populate multiaperture properties
n_apertures = len(aperture_table)
columns = aperture_table.colnames
if n_apertures > apAIJ.multiaperture.naperturesmax:
apAIJ.multiaperture.naperturesmax = n_apertures + 1
# A boolean column for this would be better, but this will do
# for now.
apAIJ.multiaperture.isrefstar = [
("comparison" in name.lower()) for name in aperture_table["marker name"]
]
# These are not currently in the table but that could change...
if "centroidstar" not in columns:
apAIJ.multiaperture.centroidstar = [default_centroidstar] * n_apertures
else:
apAIJ.multiaperture.centroidstar = aperture_table["centroidstar"]
if "isalign" not in columns:
apAIJ.multiaperture.isalignstar = [default_isalign] * n_apertures
else:
apAIJ.multiaperture.isalignstar = aperture_table["isalign"]
apAIJ.multiaperture.xapertures = aperture_table["x"]
apAIJ.multiaperture.yapertures = y_size - aperture_table["y"]
if "absmag" not in columns:
apAIJ.multiaperture.absmagapertures = [default_absmag] * n_apertures
else:
apAIJ.multiaperture.absmagapertures = aperture_table["absmag"]
apAIJ.multiaperture.raapertures = aperture_table["coord"].ra.degree
apAIJ.multiaperture.decapertures = aperture_table["coord"].dec.degree
return apAIJ
def _is_comp(star_coord, comp_table):
idx, d2d, _ = star_coord.match_to_catalog_sky(comp_table["coord"])
return "comparison" in comp_table["marker name"][idx]
[docs]
def generate_aij_table(table_name, comparison_table):
"""
Generate an AIJ table from a stellarphot table and a comparison table.
Parameters
----------
table_name : `astropy.table.Table`
Table of stellarphot photometry.
comparison_table : `astropy.table.Table`
Table of comparison star photometry.
Returns
-------
base_table : `astropy.table.Table`
Table of photometry in AIJ format.
"""
info_columns = {
"date-obs": "DATE_OBS",
"airmass": "AIRMASS",
"BJD": "BJD_MOBS",
"exposure": "EXPOSURE",
"filter": "FILTER",
"aperture": "Source_Radius",
"annulus_inner": "Sky_Rad(min)",
"annulus_outer": "Sky_Rad(max)",
}
by_source_columns = {
"xcenter": "X(IJ)",
"ycenter": "Y(IJ)",
"aperture_net_counts": "Source-Sky",
"aperture_area": "N_Src_Pixels",
"noise-aij": "Source_Error",
"snr": "Source_SNR",
"sky_per_pix_avg": "Sky/Pixel",
"annulus_area": "N_Sky_Pixels",
"fwhm_x": "X-Width",
"fwhm_y": "Y-Width",
"width": "Width",
"relative_flux": "rel_flux",
"relative_flux_error": "rel_flux_err",
"relative_flux_snr": "rel_flux_SNR",
"comparison counts": "tot_C_cnts",
"comparison error": "tot_C_err",
}
by_star = table_name.group_by("star_id")
base_table = by_star.groups[0][list(info_columns.keys())]
for star_id, sub_table in zip(by_star.groups.keys, by_star.groups, strict=True):
star_co = SkyCoord(
ra=sub_table["RA"][0], dec=sub_table["Dec"][0], unit="degree"
)
if _is_comp(star_co, comparison_table):
char = "C"
else:
char = "T"
new_table = sub_table[list(by_source_columns.keys())] # + ['BJD']
for old_col, new_col in by_source_columns.items():
new_column_name = new_col + f"_{char}{star_id[0]}"
new_table.rename_column(old_col, new_column_name)
# Add individual columns to the existing table instead of hstack
# Turns out hstack is super slow.
for new_col in new_table.colnames:
base_table[new_col] = new_table[new_col]
for old_col, new_col in info_columns.items():
base_table.rename_column(old_col, new_col)
return base_table
[docs]
def parse_aij_table(table_name):
"""
Return a list of objects, one for each source, from an AstroImageJ
photometry table.
Parameters
----------
table_name : str
Name of the table.
Returns
-------
stars : list
List of `Star` objects.
"""
# Read in the raw table.
if table_name.endswith(".csv"):
# The table may have been edited and changed to csv.
raw = Table.read(table_name)
else:
# The default, though, is tab-separated text with a file extension xls.
raw = Table.read(table_name, format="ascii.tab")
# Extract the names of all columns which are not specific to a source.
# Source columns end with _TX or _CX where X is one or more digits.
source_column = r".*_[CT]\d+"
common_columns = []
for name in raw.colnames:
if not re.search(source_column, name):
if name.strip():
common_columns.append(name)
# Get all of the source designations from the names of the net counts
# columns.
flux_columns = [name for name in raw.colnames if name.startswith("Source-Sky")]
source_column_ids = [c.split("_")[1] for c in flux_columns]
# For the first source, grab all of the column names that are specific to
# a source. These will be the same for all sources.
first_source_names = [
name for name in raw.colnames if name.endswith(source_column_ids[0])
]
generic_source_columns = [name.rsplit("_", 1)[0] for name in first_source_names]
# Make a list of star objects. Not sure this is actually better than
# a list of tables or something simple like that.
stars = []
for idx, source in enumerate(source_column_ids):
specifc_column_names = ["_".join([g, source]) for g in generic_source_columns]
all_names = common_columns + specifc_column_names
my_table = raw[all_names]
for spec, gen in zip(specifc_column_names, generic_source_columns, strict=True):
my_table.rename_column(spec, gen)
stars.append(Star(my_table, idx + 1))
return stars
[docs]
class Star:
"""
A class for storing photometry for a single star.
Parameters
----------
table : `astropy.table.Table`
Table of photometry for a single star.
id_num : int
ID number of the star.
Attributes
----------
airmass : `astropy.units.Quantity`
bjd_tdb : `astropy.units.Quantity`
counts : `astropy.units.Quantity`
dec : `astropy.units.Quantity`
error : `astropy.units.Quantity`
exposure : `astropy.units.Quantity`
id : int
ID number of the star.
jd_utc_start : `astropy.units.Quantity`
magnitude : `astropy.units.Quantity`
magnitude_error : `astropy.units.Quantity`
mjd_start : `astropy.units.Quantity`
peak : `astropy.units.Quantity`
ra : `astropy.units.Quantity`
sky_per_pixel : `astropy.units.Quantity`
snr : `astropy.units.Quantity`
"""
def __init__(self, table, id_num):
self._table = table
self._table["DEC"].unit = u.degree
self.id = id_num
@property
def airmass(self):
"""
Airmass at the time of observation.
"""
return self._table["AIRMASS"]
@property
def counts(self):
"""
Net counts in the aperture.
"""
return self._table["Source-Sky"]
@property
def ra(self):
"""
Right ascension of the star.
"""
return self._table["RA"] / 24 * 360 * u.degree
@property
def dec(self):
"""
Declination of the star.
"""
return self._table["DEC"]
@property
def error(self):
"""
Error in the net counts.
"""
return self._table["Source_Error"]
@property
def sky_per_pixel(self):
"""
Sky brightness per pixel.
"""
return self._table["Sky/Pixel"]
@property
def peak(self):
"""
Peak counts in the aperture.
"""
return self._table["Peak"]
@property
def jd_utc_start(self):
"""
Julian date of the start of the observation.
"""
return self._table["JD_UTC"]
@property
def mjd_start(self):
"""
Modified Julian date of the start of the observation.
"""
return self._table["J.D.-2400000"] - 0.5
@property
def exposure(self):
"""
Exposure time of the observation.
"""
try:
return self._table["EXPOSURE"]
except KeyError:
return self._table["EXPTIME"]
@property
def magnitude(self):
"""
Magnitude of the star.
"""
return -2.5 * np.log10(self.counts) + 2.5 * np.log10(self.exposure)
@property
def snr(self):
"""
Signal-to-noise ratio of the star.
"""
return self.counts / self.error
@property
def magnitude_error(self):
"""
Error in the magnitude of the star.
"""
return 1.0857 / self.snr
@property
def bjd_tdb(self):
"""
Midpoint of the exposure as barycentric Julian date
in Barycentric Dynamical Time.
"""
return self._table["BJD_TDB"]