diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 598d8cb9f..fb96a418c 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -26,7 +26,7 @@ from PiFinder import state_utils import PiFinder.calc_utils as calc_utils from PiFinder.multiproclogging import MultiprocLogging -from PiFinder.pointing_model.astro_coords import RaDecRoll +from PiFinder.types.coordinates import RaDecRoll from PiFinder.solver import get_initialized_solved_dict from PiFinder.pointing_model.imu_dead_reckoning import ImuDeadReckoning import PiFinder.pointing_model.quaternion_transforms as qt @@ -164,11 +164,11 @@ def update_plate_solve_and_imu(imu_dead_reckoning: ImuDeadReckoning, solved: dic q_x2imu = solved["imu_quat"] # IMU measurement at the time of plate solving # Update: - solved_cam = RaDecRoll() - solved_cam.set_from_deg( + solved_cam = RaDecRoll( solved["camera_center"]["RA"], solved["camera_center"]["Dec"], solved["camera_center"]["Roll"], + deg=True ) imu_dead_reckoning.update_plate_solve_and_imu(solved_cam, q_x2imu) @@ -220,11 +220,11 @@ def update_imu( solved["camera_center"]["RA"], solved["camera_center"]["Dec"], solved["camera_center"]["Roll"], - ) = cam_eq.get_deg(use_none=True) + ) = cam_eq.get(deg=True) # Store the current scope pointing estimate scope_eq = imu_dead_reckoning.get_scope_radec() - solved["RA"], solved["Dec"], solved["Roll"] = scope_eq.get_deg(use_none=True) + solved["RA"], solved["Dec"], solved["Roll"] = scope_eq.get(deg=True) solved["solve_time"] = imu_time solved["solve_source"] = "IMU" @@ -271,17 +271,16 @@ def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): TODO: Do this once at alignment """ # RA, Dec of camera center:: - solved_cam = RaDecRoll() - solved_cam.set_from_deg( + solved_cam = RaDecRoll( solved["camera_center"]["RA"], solved["camera_center"]["Dec"], solved["camera_center"]["Roll"], + deg=True ) # RA, Dec of target (where scope is pointing): solved["Roll"] = 0 # Target roll isn't calculated by Tetra3. Set to zero here - solved_scope = RaDecRoll() - solved_scope.set_from_deg(solved["RA"], solved["Dec"], solved["Roll"]) + solved_scope = RaDecRoll(solved["RA"], solved["Dec"], solved["Roll"], deg=True) # Set alignment in imu_dead_reckoning imu_dead_reckoning.set_cam2scope_alignment(solved_cam, solved_scope) diff --git a/python/PiFinder/pointing_model/astro_coords.py b/python/PiFinder/pointing_model/astro_coords.py deleted file mode 100644 index 1c9de2c8f..000000000 --- a/python/PiFinder/pointing_model/astro_coords.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Various astronomical coordinates functions -""" - -from dataclasses import dataclass -import numpy as np -import quaternion -from typing import Union # When updated to Python 3.10+, remove and use new type hints - -import PiFinder.pointing_model.quaternion_transforms as qt - - -@dataclass -class RaDecRoll: - """ - Data class for equatorial coordinates defined by (RA, Dec, Roll). This - makes it easier for interfacing and convert between radians and degrees. - - The set methods allow values to be float or None but internally, None will - be stored as np.nan so that the type is consistent. the get methods will - return None if the value is np.nan. - - NOTE: All angles are in radians. - """ - - ra: float = np.nan - dec: float = np.nan - roll: float = np.nan - is_set = False - - def reset(self): - """Reset to unset state""" - self.ra = np.nan - self.dec = np.nan - self.roll = np.nan - self.is_set = False - - def set( - self, ra: Union[float, None], dec: Union[float, None], roll: Union[float, None] - ): - """Set using radians""" - self.ra = ra if ra is not None else np.nan - self.dec = dec if dec is not None else np.nan - self.roll = roll if roll is not None else np.nan - self.is_set = True - - def set_from_deg( - self, - ra_deg: Union[float, None], - dec_deg: Union[float, None], - roll_deg: Union[float, None], - ): - """Set using degrees""" - ra = np.deg2rad(ra_deg) if ra_deg is not None else np.nan - dec = np.deg2rad(dec_deg) if dec_deg is not None else np.nan - roll = np.deg2rad(roll_deg) if roll_deg is not None else np.nan - - self.set(ra, dec, roll) - - def set_from_quaternion(self, q_eq: quaternion.quaternion): - """ - Set from a quaternion rotation relative to the Equatorial frame. - Re-using code from quaternion_transforms.q_eq2radec. - """ - ra, dec, roll = qt.q_eq2radec(q_eq) - self.set(ra, dec, roll) - - def get( - self, use_none=False - ) -> tuple[Union[float, None], Union[float, None], Union[float, None]]: - """ - Returns (ra, dec, roll) in radians. If use_none is True, returns None - for any unset (nan) values. - """ - if use_none: - ra = self.ra if not np.isnan(self.ra) else None - dec = self.dec if not np.isnan(self.dec) else None - roll = self.roll if not np.isnan(self.roll) else None - else: - ra, dec, roll = self.ra, self.dec, self.roll - - return ra, dec, roll - - def get_deg( - self, use_none=False - ) -> tuple[Union[float, None], Union[float, None], Union[float, None]]: - """ - Returns (ra, dec, roll) in degrees. If use_none is True, returns None - for any unset (nan) values. - """ - if use_none: - ra = np.rad2deg(self.ra) if not np.isnan(self.ra) else None - dec = np.rad2deg(self.dec) if not np.isnan(self.dec) else None - roll = np.rad2deg(self.roll) if not np.isnan(self.roll) else None - else: - ra, dec, roll = ( - np.rad2deg(self.ra), - np.rad2deg(self.dec), - np.rad2deg(self.roll), - ) - - return ra, dec, roll diff --git a/python/PiFinder/pointing_model/imu_dead_reckoning.py b/python/PiFinder/pointing_model/imu_dead_reckoning.py index 905625de3..8ddc5755e 100644 --- a/python/PiFinder/pointing_model/imu_dead_reckoning.py +++ b/python/PiFinder/pointing_model/imu_dead_reckoning.py @@ -10,7 +10,7 @@ import numpy as np import quaternion -from PiFinder.pointing_model.astro_coords import RaDecRoll +from PiFinder.types.coordinates import RaDecRoll import PiFinder.pointing_model.quaternion_transforms as qt @@ -101,7 +101,7 @@ def update_plate_solve_and_imu( q_x2imu: [quaternion] Raw IMU measurement quaternions. This is the IMU frame orientation wrt unknown drifting reference frame X. """ - if not solved_cam.is_set: + if not solved_cam.valid: return # No update # Update plate-solved coord: Camera frame relative to the Equatorial @@ -142,8 +142,7 @@ def get_cam_radec(self) -> RaDecRoll: dead_reckoning to indicate if the estimate is from dead-reckoning (True) or from plate solving (False). """ - ra_dec_roll = RaDecRoll() - ra_dec_roll.set_from_quaternion(self.q_eq2cam) + ra_dec_roll = RaDecRoll.from_quaternion(self.q_eq2cam) return ra_dec_roll @@ -153,8 +152,7 @@ def get_scope_radec(self) -> RaDecRoll: to indicate if the estimate is from dead-reckoning (True) or from plate solving (False). """ - ra_dec_roll = RaDecRoll() - ra_dec_roll.set_from_quaternion(self.q_eq2scope) + ra_dec_roll = RaDecRoll.from_quaternion(self.q_eq2scope) return ra_dec_roll diff --git a/python/PiFinder/types/__init__.py b/python/PiFinder/types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/PiFinder/types/coordinates.py b/python/PiFinder/types/coordinates.py new file mode 100644 index 000000000..34e56502d --- /dev/null +++ b/python/PiFinder/types/coordinates.py @@ -0,0 +1,130 @@ +""" +Astronomical coordinate types +""" + +from dataclasses import dataclass +import numpy as np +import quaternion +from typing import Union # When updated to Python 3.10+, remove and use new type hints + +from PiFinder.pointing_model.quaternion_transforms import q_eq2radec, radec2q_eq + + +@dataclass +class RaDecRoll: + """ + Data class for equatorial coordinates defined by (RA, Dec, Roll). This + makes it easier for interfacing and convert between radians and degrees. + + The set methods allow values to be float or None but internally, None will + be stored as np.nan so that the type is consistent. the get methods will + return None if the value is np.nan. + """ + ra: float = np.nan # All angles in radians + dec: float = np.nan + roll: float = np.nan + valid = False + + def __init__(self, ra: float, dec: float, roll: float, deg=False): + self.set(ra, dec, roll, deg=deg) + + @classmethod + def from_quaternion(cls, q_eq: quaternion.quaternion): + ra, dec, roll = q_eq2radec(q_eq) + return cls(ra, dec, roll) + + def reset(self): + """Reset to unset state""" + self.ra = np.nan + self.dec = np.nan + self.roll = np.nan + self.valid = False + + def set( + self, + ra: Union[float, None], + dec: Union[float, None], + roll: Union[float, None], + deg=False # If True, input angles are in degrees + ): + """Set using radians""" + self.ra = ra if ra is not None else np.nan + self.dec = dec if dec is not None else np.nan + self.roll = roll if roll is not None else np.nan + + if np.isnan(self.ra) or np.isnan(self.dec) or np.isnan(self.roll): + self.valid = False + else: + self.valid = True + + if deg: + self.ra = np.deg2rad(self.ra) + self.dec = np.deg2rad(self.dec) + self.roll = np.deg2rad(self.roll) + + def set_from_quaternion(self, q_eq: quaternion.quaternion): + """ + Set from a quaternion rotation relative to the Equatorial frame. + """ + ra, dec, roll = q_eq2radec(q_eq) + self.set(ra, dec, roll) + + def as_quaternion(self) -> quaternion.quaternion: + """ + Return the quaternion rotation relative to the Equatorial frame. + """ + return radec2q_eq(self.ra, self.dec, self.roll) + + def get(self, + use_none=True, # If True, returns None instead of np.nan + deg=False # If True, returns degrees + ) -> tuple[Union[float, None], Union[float, None], Union[float, None]]: + """ + Returns (ra, dec, roll) in radians. If use_none is True, returns None + for any unset (nan) values. + """ + if deg: + ra, dec, roll = np.rad2deg(self.ra), np.rad2deg(self.dec), np.rad2deg(self.roll) + else: + ra, dec, roll = self.ra, self.dec, self.roll + + if use_none: + ra = ra if not np.isnan(ra) else None + dec = dec if not np.isnan(dec) else None + roll = roll if not np.isnan(roll) else None + + return ra, dec, roll + + +@dataclass +class RaDec: + """ + Data class for equatorial coordinates defined by (RA, Dec). + + The set methods allow values to be float or None but internally, None will + be stored as np.nan so that the type is consistent. the get methods will + return None if the value is np.nan. + """ + ra: float = np.nan # All angles in radians + dec: float = np.nan + valid = False + + def __init__(self, ra: float, dec: float, roll: float, deg=False): + raise NotImplementedError("Outline for RaDec class") + + +@dataclass +class AltAz: + """ + Data class for horizontal coordinates defined by (Alt, Az). + + The set methods allow values to be float or None but internally, None will + be stored as np.nan so that the type is consistent. the get methods will + return None if the value is np.nan. + """ + alt: float = np.nan # All angles in radians + az: float = np.nan + valid = False + + def __init__(self, alt: float, az: float, deg=False): + raise NotImplementedError("Outline for AltAz class")