"""Module for the Telescope class.
This defines a very simple Telescope class to hold telescope-related info.
"""
from collections.abc import Sequence
from pathlib import Path
from typing import Self
import attrs
import h5py
import numpy as np
from astropy import units as un
from astropy.coordinates import Angle, EarthLocation
from attrs import field
from .attrs import unit_validator
from .types import TimeType
def _pol_converter(pols: Sequence[str]) -> tuple[str]:
pols = [
pol[0].lower() + pol[1].upper() if "p" in pol.lower() else pol.upper()
for pol in pols
]
return tuple(pols)
[docs]
@attrs.define(slots=False, kw_only=True)
class Telescope:
"""Class representing a telescope.
Parameters
----------
name
The name of the telescope.
location
The location of the telescope.
pols
The polarizations that the telescope can measure. This should be all
of the available polarizations that the telescope can measure, not just those
that are in a particular observation. The polarizations should be given as a
tuple of strings, where each string is one of "XX", "XY", "YX", "YY", "pI",
"pQ", "pU", or "pV".
integration_time
The integration time of the telescope. This should be a scalar Quantity.
In principle each observation made by a telescope may have different integration
time, but this is the default value.
x_orientation
The orientation of the X polarization. This should be an Angle, and represents
the orientation with respect to East. The default is 0 degrees, which means that
the X polarization is aligned with East (the angle rotates towards North).
"""
# This version indicates the version of the Telescope object, not this entire
# package. The semantic meaning here is that the version is {major}.{minor}.
# Minor version increases indicate simple bug-fixes that don't change the essential
# file format. Major version increases indicate changes to the file format, but
# can often still be backwards-compatible with some caveats.
__version__ = "1.0"
name: str = attrs.field(converter=str)
location: EarthLocation = attrs.field(
validator=attrs.validators.instance_of(EarthLocation)
)
pols: tuple[str] = field(converter=_pol_converter)
integration_time: TimeType = field(default=1 * un.s, validator=unit_validator(un.s))
x_orientation: Angle = field(
default=0 * un.deg,
converter=Angle,
)
@pols.validator
def _pols_vld(self, _, value):
if len(value) < 1:
raise ValueError("Telescope must have at least one polarization")
if len(value) > 4:
raise ValueError("Telescope must have 4 or fewer polarizations")
possible_pols = ("XX", "XY", "YX", "YY", "pI", "pQ", "pU", "pV")
for pol in value:
if pol.upper() not in possible_pols:
raise ValueError(f"Invalid polarization: {pol}")
@integration_time.validator
def _integration_time_vld(self, _, value):
if value.size != 1:
raise ValueError("Integration time must be a scalar")
if value.value <= 0:
raise ValueError("Integration time must be positive")
[docs]
def write(self, fname: str | Path | h5py.File | h5py.Group):
"""Write the telescope to an HDF5 file."""
if isinstance(fname, str | Path):
with h5py.File(fname, "a") as fl:
self.write(fl)
elif isinstance(fname, h5py.File | h5py.Group):
if not fname.name.endswith("/telescope"):
grp = fname.create_group("telescope")
else:
grp = fname
grp.attrs["version"] = self.__version__
grp.attrs["name"] = self.name
grp.attrs["integration_time"] = self.integration_time.to(un.s).value
grp.attrs["x_orientation"] = self.x_orientation.to(un.deg).value
grp["location"] = np.array(
[x.to_value("m") for x in self.location.to_geocentric()]
)
grp["pols"] = self.pols
else:
raise TypeError(f"Invalid type for fname: {type(fname)}")
[docs]
@classmethod
def from_hdf5(cls, fname: str | Path | h5py.File | h5py.Group) -> Self:
"""Read a telescope from an HDF5 file."""
if isinstance(fname, str | Path):
with h5py.File(fname, "r") as fl:
return Telescope.from_hdf5(fl)
elif isinstance(fname, h5py.File | h5py.Group):
grp = fname["telescope"] if not fname.name.endswith("/telescope") else fname
# Check the file-format version
version = grp.attrs["version"]
major = version.split(".")[0]
reader = getattr(cls, f"_read_hdf5_version_{major}", None)
if reader is None:
raise ValueError(f"Unsupported file format version: {version}")
return reader(grp)
else:
raise TypeError(f"Invalid type for fname: {type(fname)}")
@classmethod
def _read_hdf5_version_1(cls, grp: h5py.Group):
return cls(
name=grp.attrs["name"],
location=EarthLocation.from_geocentric(*(grp["location"][()] * un.m)),
pols=tuple(x.decode() for x in grp["pols"][:]),
integration_time=grp.attrs["integration_time"] * un.s,
x_orientation=grp.attrs["x_orientation"] * un.deg,
)