astra.nonsidereal#

Non-sidereal (moving target) tracking for observatory imaging sequences.

This module tracks solar system objects and Earth-orbiting objects. Their sky coordinates change over the length of an exposure. The module reads positions from a pre-computed ephemeris and sends differential tracking rates to the mount through ASCOM.

What it does:
  • Reads the ephemeris that ObjectActionConfig computed at schedule load time, from Astropy or from JPL Horizons.

  • Interpolates RA/Dec and their rates at any moment without new lookups.

  • Sends the ASCOM RightAscensionRate and DeclinationRate the mount needs to follow the target (open-loop tracking).

  • Re-slews the mount to the current ephemeris position at a set interval, to remove drift that the rates cannot correct.

Notes

  • Autoguiding does not work with non-sidereal tracking. The observatory disables guiding when this tracking is active.

  • The mount must report CanSetRightAscensionRate and CanSetDeclinationRate.

Classes

NonSiderealManager(action, logger[, ...])

Manages non-sidereal tracking for a single imaging sequence.

class astra.nonsidereal.NonSiderealManager(action: Action, logger: Logger, telescope: AlpacaDevice | None = None, to_mount_frame: Callable[[float, float], tuple[float, float]] | None = None)[source]#

Bases: object

Manages non-sidereal tracking for a single imaging sequence.

This class computes the tracking rates and the re-center positions from the pre-computed ephemeris. The observatory loop only controls the hardware.

Usage:

manager = NonSiderealManager(action, logger)
if manager.is_active:
    manager.apply_rates(telescope)

try:
    # ... exposure loop ...
    if manager.should_recenter():
        manager.recenter(paired_devices, wait_for_slew_fn)
finally:
    manager.reset_rates(telescope)
Parameters:
  • action – The scheduled action for this sequence.

  • logger – Observatory logger.

  • telescope – Mount this sequence will run on. When given, its ASCOM CanSetRightAscensionRate / CanSetDeclinationRate flags decide whether non-sidereal tracking can run at all. This is the authoritative check: schedule validation only knows whether some mount in the observatory supports differential rates, not which one the action is paired with.

  • to_mount_frame – Optional (ra_deg, dec_deg) -> (ra_deg, dec_deg) that converts an ICRS position into the mount’s frame. The ephemeris is ICRS; a JNow mount needs the conversion before every re-centering slew. None sends ICRS unchanged. The observatory supplies to_mount_coordinates for the paired telescope.

property is_active: bool#
apply_rates(telescope: AlpacaDevice) None[source]#

Push current differential RA/Dec rates to the mount.

prepoint_coordinates(lead_time_seconds: float = 0.0) tuple[float, float] | None[source]#

Return RA/Dec for an initial lead-pointing slew.

Parameters:

lead_time_seconds – Seconds after sequence start used as the pre-pointing target. Defaults to 0, which points at the target’s position at the start of the sequence.

Returns:

Tuple of (ra_deg, dec_deg) or None when non-sidereal tracking is inactive.

tracking_activation_time(lead_time_seconds: float = 0.0) Time | None[source]#

Return the time when non-sidereal rates and imaging should begin.

should_recenter() bool[source]#

Return True if the recenter interval has elapsed since tracking started.

The interval is counted from the first rate command, not from when the manager was built. Sequence setup (slew, filter, focus, lead-time wait) can take longer than a short interval, and a re-center on the first exposure would waste two slews on a mount that just arrived on target.

drift_between(start: Time, end: Time) float | None[source]#

Angular distance the target moves between two times, in arcsec.

Returns None when non-sidereal tracking is inactive.

recenter_if_late(activation_time: Time, paired_devices: PairedDevices, wait_for_slew_fn: Callable[[PairedDevices], None], can_slew: Callable[[], bool] | None = None) bool[source]#

Re-point the mount if the target has drifted since activation_time.

The mount is pre-pointed at where the target would be at activation_time. If the sequence reaches that point late, the target has moved on, and how much that matters depends entirely on the target: the same delay is imperceptible for a comet and puts a satellite degrees outside the field. The decision is therefore made on the distance the target has actually travelled, not on how late the sequence is.

Returns True if a re-centre was performed.

recenter(paired_devices: PairedDevices, wait_for_slew_fn: Callable[[PairedDevices], None], can_slew: Callable[[], bool] | None = None) bool[source]#

Slew to the current ephemeris position and refresh the tracking rates.

The first slew goes to the target’s position at the moment the slew is commanded. A fast target moves on while the mount is slewing, so a second slew corrects for that motion. The second slew is skipped when the target moved less than _RECENTER_CORRECTION_MIN_ARCSEC during the first one.

Parameters:
  • paired_devices – PairedDevices for the sequence.

  • wait_for_slew_fn – Callable(paired_devices) that blocks until slew completes.

  • can_slew – Optional predicate checked before each slew. Conditions can change between the two slews. A weather alert parks the mount, and a parked mount rejects a slew. So it is checked before each slew, not only on entry.

Returns:

True if re-centering was performed.

reset_rates(telescope: AlpacaDevice) None[source]#

Reset differential tracking rates to zero.

Safe to call even when not active (no-op if non-sidereal tracking was never started, so the telescope is never touched); always call this in a finally block.