From d76ec1c6b158fbf9e000bfe94baf49874d7ebc77 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Thu, 30 Apr 2026 22:27:14 +0200 Subject: [PATCH 1/5] Fix: Horizontal coordinate orientation (use -parallactic_angle) --- python/PiFinder/calc_utils.py | 19 +++++++++++++++++++ python/PiFinder/integrator.py | 11 ++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/python/PiFinder/calc_utils.py b/python/PiFinder/calc_utils.py index 112271a3d..af4741436 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 2457f4872..eac6a1602 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -319,6 +319,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,12 +333,16 @@ 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 + 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 + roll_deg = 0.0 # NCP up else: logger.error(f"Unknown mount type: {mount_type}. Cannot set roll.") roll_deg = 0.0 @@ -347,6 +352,6 @@ def get_roll_by_mount_type( # 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 += 180.0 # Southern hemisphere TODO: Verify this return roll_deg From 848ef57e61cb3858608b68eed681a20863fdadad Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Fri, 1 May 2026 22:56:22 +0100 Subject: [PATCH 2/5] Remove duplicate definition of dt --- python/PiFinder/integrator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index eac6a1602..18e6bbf99 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -161,7 +161,6 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # 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 From 29cd601224077aa2d2d353a1fb2e920dd8f34fa0 Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Fri, 1 May 2026 23:45:41 +0100 Subject: [PATCH 3/5] Encapsulate functionality to set Alt, Az, Roll in solved dict to a func --- python/PiFinder/integrator.py | 66 ++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 18e6bbf99..264692005 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -75,6 +75,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 @@ -114,6 +115,7 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # We have a new image solve: Use plate-solving for RA/Dec update_plate_solve_and_imu(imu_dead_reckoning, solved) else: + # TODO: In this case, it should run update_imu() # Failed solve - clear constellation solved["solve_source"] = "CAM_FAILED" solved["constellation"] = "" @@ -130,9 +132,9 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa if imu: update_imu(imu_dead_reckoning, solved, last_image_solve, imu) - # Push IMU updates only if newer than last push + # Update Alt, Az, Roll only if newer than last push if ( - solved["RA"] and solved["solve_time"] > last_solve_time + solved["RA"] is not None 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 @@ -141,31 +143,10 @@ def integrator(shared_state, solver_queue, console_queue, log_queue, is_debug=Fa # 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? - 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) @@ -176,7 +157,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 @@ -274,6 +254,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. From f53401a19de923be7f865d476a2f9d41f5e2205a Mon Sep 17 00:00:00 2001 From: TakKanekoGit <> Date: Sat, 2 May 2026 00:35:00 +0100 Subject: [PATCH 4/5] 1) Ensure that AltAz is calculated when plate solved. 2) Fall back to IMU dead-reckoning if plate-solve fails. --- python/PiFinder/integrator.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 264692005..5c3dfd1ad 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 @@ -101,19 +102,15 @@ 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 @@ -125,20 +122,18 @@ 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 # Update Alt, Az, Roll only if newer than last push - if ( - solved["RA"] is not None 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 - + 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 From b898825f5db6cf2d11f91757f11bc6df0addb496 Mon Sep 17 00:00:00 2001 From: Tak Kaneko <> Date: Sat, 2 May 2026 12:00:44 +0200 Subject: [PATCH 5/5] Fix: No need to add 180 deg to -parallactic_angle for horizontal coordinate chart display in the Southern hemisphere --- python/PiFinder/integrator.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/python/PiFinder/integrator.py b/python/PiFinder/integrator.py index 5c3dfd1ad..a08299b25 100644 --- a/python/PiFinder/integrator.py +++ b/python/PiFinder/integrator.py @@ -342,20 +342,17 @@ def get_roll_by_mount_type( # 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 + # 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 + # 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 TODO: Verify this + roll_deg = 0.0 # NCP up return roll_deg