diff --git a/python/PiFinder/calc_utils.py b/python/PiFinder/calc_utils.py index 112271a3..af474143 100644 --- a/python/PiFinder/calc_utils.py +++ b/python/PiFinder/calc_utils.py @@ -388,6 +388,25 @@ def ra_to_ha(self, ra_deg, dt): return ha_hrs * 180 / 12 # Hour angle [deg] + def radec_to_pa(self, ra_deg, dec_deg, dt): + """ + Returns the parallactic angle of an object at (ra, dec) as seen from an + observer at latitiude, lat and time, dt. See hadec_to_pa() for how + parallactic angle is defined. + + INPUTS: + ra_deg: Right ascension [deg] + dec_deg: Declination [deg] + dt: Python datetime object (must be timezone-aware) + + RETURNS: + pa_deg: Parallactic angle [deg] + """ + ha_deg = self.ra_to_ha(ra_deg, dt) # Note that HA is in deg + lat_deg = self._observer_geoid.latitude.degrees + pa_deg = hadec_to_pa(ha_deg, dec_deg, lat_deg) + return pa_deg # Parallactic angle [deg] + def radec_to_roll(self, ra_deg, dec_deg, dt): """ Returns the roll (field rotation) of an object at (ra, dec) as diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 2457f487..a08299b2 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -62,9 +62,10 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # This holds the last image solve position info # so we can delta for IMU updates last_image_solve = None - last_solve_time = time.time() + #last_solve_time = time.time() while True: + pointing_updated = False # Flag to track if pointing was updated in this loop state_utils.sleep_for_framerate(shared_state) # Check for new camera solve in queue @@ -75,6 +76,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa pass if type(next_image_solve) is dict: + # TODO: Refactor this bit: # For camera solves, always start from last successful camera solve # NOT from shared_state (which may contain IMU drift) # This prevents IMU noise accumulation during failed solves @@ -100,20 +102,17 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # For failed solves, preserve ALL position data from previous solve # Don't recalculate from GPS (causes drift from GPS noise) - # Set solve_source and push camera solves immediately if solved["RA"] is not None: + # Successfully plate-solved: last_image_solve = copy.deepcopy(solved) solved["solve_source"] = "CAM" - # Calculate constellation for successful solve - solved["constellation"] = ( - calc_utils.sf_utils.radec_to_constellation( - solved["RA"], solved["Dec"] - ) - ) shared_state.set_solve_state(True) # We have a new image solve: Use plate-solving for RA/Dec update_plate_solve_and_imu(imu_dead_reckoning, solved) + #last_solve_time = solved["solve_time"] + pointing_updated = True else: + # TODO: In this case, it should run update_imu() # Failed solve - clear constellation solved["solve_source"] = "CAM_FAILED" solved["constellation"] = "" @@ -123,50 +122,26 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa shared_state.set_solution(solved) shared_state.set_solve_state(False) - elif imu_dead_reckoning.tracking: + if imu_dead_reckoning.tracking and not pointing_updated: # Previous plate-solve exists so use IMU dead-reckoning from # the last plate solved coordinates. imu = shared_state.imu() if imu: + #last_solve_time = time.time() update_imu(imu_dead_reckoning, solved, last_image_solve, imu) + pointing_updated = True - # Push IMU updates only if newer than last push - if ( - solved["RA"] and solved["solve_time"] > last_solve_time - # and solved["solve_source"] == "IMU" - ): - last_solve_time = time.time() # TODO: solve_time is ambiguous because it's also used for IMU dead-reckoning - + # Update Alt, Az, Roll only if newer than last push + if pointing_updated: + #last_solve_time = time.time() # Set location for roll and altaz calculations. # TODO: Is it necessary to set location? # TODO: Altaz doesn't seem to be required for catalogs when in # EQ mode? Could be disabled in future when in EQ mode? - location = shared_state.location() - dt = shared_state.datetime() - if location: - calc_utils.sf_utils.set_location( - location.lat, location.lon, location.altitude - ) - - # Set the roll so that the chart is displayed appropriately for the mount type - solved["Roll"] = get_roll_by_mount_type( - solved["RA"], solved["Dec"], location, dt, mount_type - ) - - # Update remaining solved keys - # Calculate constellation for current position - solved["constellation"] = calc_utils.sf_utils.radec_to_constellation( - solved["RA"], solved["Dec"] - ) # TODO: Can the outer brackets be omitted? - - # Set Alt/Az because it's needed for the catalogs for the - # Alt/Az mount type. TODO: Can this be moved to the catalog? - dt = shared_state.datetime() - if location and dt: - solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( - solved["RA"], solved["Dec"], dt - ) - + update_solved_coords(solved, + location=shared_state.location(), + dt=shared_state.datetime(), + mount_type=mount_type) # Push IMU update shared_state.set_solution(solved) shared_state.set_solve_state(True) @@ -177,7 +152,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # ======== Wrapper and helper functions =============================== - def update_plate_solve_and_imu(imu_dead_reckoning: ImuDeadReckoning, solved: dict): """ Wrapper for ImuDeadReckoning.update_plate_solve_and_imu() to @@ -275,6 +249,36 @@ def update_imu( ) +def update_solved_coords(solved, location, dt, mount_type): + """ + Based on RA/Dec, update the following in dictionary 'solved': + solved["Alt"], solved["Az"], solved["Roll"], solved["constellation"] + """ + if solved["RA"] is None or solved["Dec"] is None: + solved["constellation"] = None + solved["Alt"], solved["Az"] = None, None + solved["Roll"] = None + return + + # Calculate constellation for current position + solved["constellation"] = calc_utils.sf_utils.radec_to_constellation( + solved["RA"], solved["Dec"] + ) + + if location and dt: + # Location needs to be set for Alt/Az and roll calculations + calc_utils.sf_utils.set_location(location.lat, location.lon, location.altitude) + # Set Alt/Az + solved["Alt"], solved["Az"] = calc_utils.sf_utils.radec_to_altaz( + solved["RA"], solved["Dec"], dt) + # Set the roll so that the chart is displayed appropriately for the mount type + solved["Roll"] = get_roll_by_mount_type( + solved["RA"], solved["Dec"], location, dt, mount_type) + else: + solved["Alt"], solved["Az"] = None, None + solved["Roll"] = None + + def set_cam2scope_alignment(imu_dead_reckoning: ImuDeadReckoning, solved: dict): """ Set alignment. @@ -319,6 +323,7 @@ def get_roll_by_mount_type( if mount_type == "Alt/Az": # Altaz mounts: Display chart in horizontal coordinates if location and dt: + """ # We have location and time/date (and assume that location has been set) # Roll at the target RA/Dec in the horizontal frame roll_deg = calc_utils.sf_utils.radec_to_roll(ra_deg, dec_deg, dt) @@ -332,21 +337,22 @@ def get_roll_by_mount_type( roll_deg - np.sign(ha_deg) * 180 ) # In essence, gives: roll_deg = -pa_deg # End of HACK + """ + # chart.py uses roll to rotate the chart around the target center + # by roll in anti-clockwise direction. Use -parallactic_angle + roll_deg = -calc_utils.sf_utils.radec_to_pa(ra_deg, dec_deg, dt) else: - # No position or time/date available, so set roll to 0.0 - roll_deg = 0.0 + # No position or time/date available. Default to display in equatorial coordinate + roll_deg = 0.0 # NCP up elif mount_type == "EQ": - # EQ-mounts: Display chart with NCP up so roll = 0.0 - roll_deg = 0.0 + # EQ-mounts: Display chart in equatorial coordinates + roll_deg = 0.0 # NCP up + # If location is available, adjust roll for hemisphere: + if location: + if location.lat < 0.0: + roll_deg = 180.0 # SCP up (for southern hemisphere) else: logger.error(f"Unknown mount type: {mount_type}. Cannot set roll.") - roll_deg = 0.0 - - # If location is available, adjust roll for hemisphere: - # Altaz: North up in northern hemisphere, South up in southern hemisphere - # EQ mounts: NCP up in northern hemisphere, SCP up in southern hemisphere - if location: - if location.lat < 0.0: - roll_deg += 180.0 # Southern hemisphere + roll_deg = 0.0 # NCP up return roll_deg