def desired_dome_azimuth(self, dome_target_azimuth, telescope_target, next_telescope_target=None): """Return a new target dome azimuth if dome movement is needed to avoid vignetting, else None. Parameters ---------- dome_target_azimuth : `float` or `None` Dome target azimuth (deg), or `None` if unknown. telescope_target : `ElevationAzimuth` Telescope target elevation and azimuth. next_telescope_target : `ElevationAzimuth` or `None`, optional Next telescope_target target elevation and azimuth, if known, else `None`. Ignored. Returns ------- dome_target_azimuth : `float` or `None` New desired dome azimuth (deg), or `None` if no change. """ if dome_target_azimuth is None: return telescope_target.azimuth.position # scaled_delta_azimuth is the difference multiplied by cos(target alt). scaled_delta_azimuth = utils.angle_diff( telescope_target.azimuth.position, dome_target_azimuth).deg * math.cos( telescope_target.elevation.position * RAD_PER_DEG) if abs(scaled_delta_azimuth) < self.max_delta_azimuth: return None return telescope_target.azimuth.position
async def dome_telemetry(self) -> None: while self.run_telemetry_loop: if (self.controllers.mtdome.evt_summaryState.data.summaryState == salobj.State.ENABLED): dome_az_set = self.controllers.mtdome.tel_azimuth.data.positionCommanded dome_el_set = (self.controllers.mtdome.tel_lightWindScreen. data.positionCommanded) dome_az_pos = self.controllers.mtdome.tel_azimuth.data.positionActual dome_el_pos = (self.controllers.mtdome.tel_lightWindScreen. data.positionActual) error_az = np.random.normal(0.0, 1e-7) error_el = np.random.normal(0.0, 1e-7) diff_az = angle_diff(dome_az_set, dome_az_pos) diff_el = angle_diff(dome_el_set, dome_el_pos) # This next bit is to simulate the MTDomeTrajectory behavior. if (self.controllers.mtdometrajectory.evt_summaryState.data. summaryState == salobj.State.ENABLED): dome_az_set = getattr( self.controllers.mtmount.tel_azimuth.data, self.mtmount_demand_position_name, ) dome_el_set = getattr( self.controllers.mtmount.tel_elevation.data, self.mtmount_demand_position_name, ) self.controllers.mtdome.tel_azimuth.set( positionCommanded=dome_az_set, positionActual=dome_az_pos + error_az + diff_az.deg / 1.1, ) self.controllers.mtdome.tel_lightWindScreen.set( positionCommanded=dome_el_set, positionActual=dome_el_pos + error_el + diff_el.deg / 1.1, ) await self.controllers.mtdome.tel_azimuth.write() await self.controllers.mtdome.tel_lightWindScreen.write() await asyncio.sleep(HEARTBEAT_INTERVAL)
def _get_distance(self) -> float: """Determines the smallest distance [rad] between the initial and target positions assuming motion around a circle. Returns ------- distance: `float` The smallest distance between the initial and target positions. """ distance = utils.angle_diff( math.degrees(self._end_position), math.degrees(self._start_position) ).rad return distance
async def wait_dome_move(self, azimuth): """Wait for an ATDome azimuth move to finish. Parameters ---------- azimuth : `float` Target azimuth for telescope and dome (deg) """ while True: curr_pos = await self.dome_remote.tel_position.next( flush=True, timeout=STD_TIMEOUT) if utils.angle_diff(curr_pos.azimuthPosition, azimuth).deg < 0.1: break
async def move_azimuth_loop(self): """Move the dome to the specified azimuth.""" try: max_az_corr = abs(self.az_vel * self.telemetry_interval) while True: if (self.summary_state == salobj.State.ENABLED and self.cmd_az != self.curr_az): az_err = utils.angle_diff(self.cmd_az, self.curr_az).deg abs_az_corr = min(abs(az_err), max_az_corr) az_corr = abs_az_corr if az_err >= 0 else -abs_az_corr self.curr_az += az_corr await self.tel_position.set_write(azimuthPosition=self.curr_az) await asyncio.sleep(self.telemetry_interval) except asyncio.CancelledError: raise except Exception: self.log.exception("move_azimuth_loop failed") raise
def desired_dome_elevation( self, dome_target_elevation, telescope_target, next_telescope_target=None ): """Return a new target dome elevation if dome movement is needed to avoid vignetting, else None. Parameters ---------- dome_target_elevation : `lsst.ts.simactuators.path.PathSegment` \ or `None` Dome target elevation, or `None` if unknown. telescope_target : `ElevationAzimuth` Telescope target elevation and azimuth. next_telescope_target : `ElevationAzimuth` or `None`, optional Next telescope_target target elevation and azimuth, if known, else `None`. Ignored. Returns ------- dome_target_elevation : `lsst.ts.simactuators.path.PathSegment` \ or `None` New desired dome elevation, or `None` if no change. """ if dome_target_elevation is None: return telescope_target.elevation dome_elevation_shifted = dome_target_elevation.at( telescope_target.elevation.tai ) eldiff = utils.angle_diff( dome_elevation_shifted.position, telescope_target.elevation.position ).deg if abs(eldiff) < self.max_delta_elevation: return None return simactuators.path.PathSegment( position=telescope_target.elevation.position, velocity=0, tai=telescope_target.elevation.tai, )
def desired_dome_azimuth( self, dome_target_azimuth, telescope_target, next_telescope_target=None ): """Return a new target dome azimuth if dome movement is needed to avoid vignetting, else None. Parameters ---------- dome_target_azimuth : `lsst.ts.simactuators.path.PathSegment` \ or `None` Dome target azimuth. telescope_target : `ElevationAzimuth` Telescope target elevation and azimuth. next_telescope_target : `ElevationAzimuth` or `None`, optional Next telescope_target target elevation and azimuth, if known, else `None`. Ignored. Returns ------- dome_target_azimuth : `lsst.ts.simactuators.path.PathSegment` or `None` New desired dome azimuth, or `None` if no change. """ if dome_target_azimuth is None: return telescope_target.azimuth # scaled_delta_azimuth is the difference multiplied by cos(target alt). dome_azimuth_shifted = dome_target_azimuth.at(telescope_target.azimuth.tai) scaled_delta_azimuth = utils.angle_diff( telescope_target.azimuth.position, dome_azimuth_shifted.position ).deg * math.cos(telescope_target.elevation.position * RAD_PER_DEG) if abs(scaled_delta_azimuth) < self.max_delta_azimuth: return None return simactuators.path.PathSegment( position=telescope_target.azimuth.position, velocity=0, tai=telescope_target.azimuth.tai, )
async def monitor_position(self, check: typing.Optional[typing.Any] = None ) -> None: """Monitor MTCS axis position. Monitor/log a selected set of axis from the main telescope. This is useful during slew activities to make sure everything is going as expected. Parameters ---------- check : `types.SimpleNamespace` or `None` Override `self.check` for defining which resources are used. """ assert self._dome_az_in_position is not None assert self._dome_el_in_position is not None # Creates a copy of check so it can be freely modified to control what # needs to be verified at each stage of the process. _check = copy.copy(self.check) if check is None else copy.copy(check) self.log.debug("Monitor position started.") # xml 7.1/8.0 backward compatibility mtmount_actual_position_name = "actualPosition" if _check.mtmount: self.log.debug("Waiting for Target event from mtmount.") try: target = await self.rem.mtmount.evt_target.next( flush=True, timeout=self.long_timeout) self.log.debug(f"Mount target: {target}") except asyncio.TimeoutError: raise RuntimeError( "Not receiving target events from the NewMTMount. " "Check component for errors.") if not hasattr(self.rem.mtmount.tel_azimuth.DataType(), mtmount_actual_position_name): self.log.debug("Running in xml 7.1 compatibility mode.") mtmount_actual_position_name = "angleActual" while True: status = "" if _check.mtmount: target, tel_az, tel_el = await asyncio.gather( self.rem.mtmount.evt_target.next( flush=True, timeout=self.long_timeout), self.rem.mtmount.tel_azimuth.next( flush=True, timeout=self.fast_timeout), self.rem.mtmount.tel_elevation.next( flush=True, timeout=self.fast_timeout), ) tel_az_actual_position = getattr(tel_az, mtmount_actual_position_name) tel_el_actual_position = getattr(tel_el, mtmount_actual_position_name) distance_az = angle_diff(target.azimuth, tel_az_actual_position) distance_el = angle_diff(target.elevation, tel_el_actual_position) status += ( f"[Tel]: Az = {tel_az_actual_position:+08.3f}[{distance_az.deg:+6.1f}]; " f"El = {tel_el_actual_position:+08.3f}[{distance_el.deg:+6.1f}] " ) if _check.mtrotator: rotation_data = await self.rem.mtrotator.tel_rotation.next( flush=True, timeout=self.fast_timeout) distance_rot = angle_diff(rotation_data.demandPosition, rotation_data.actualPosition) status += f"[Rot]: {rotation_data.demandPosition:+08.3f}[{distance_rot.deg:+6.1f}] " if _check.mtdome: dome_az = await self.rem.mtdome.tel_azimuth.next( flush=True, timeout=self.fast_timeout) dome_el = await self.rem.mtdome.tel_lightWindScreen.next( flush=True, timeout=self.fast_timeout) dome_az_diff = angle_diff(dome_az.positionActual, dome_az.positionCommanded) dome_el_diff = angle_diff(dome_el.positionActual, dome_el.positionCommanded) if np.abs(dome_az_diff) < self.dome_slew_tolerance: self._dome_az_in_position.set() if np.abs(dome_el_diff) < self.dome_slew_tolerance: self._dome_el_in_position.set() status += (f"[Dome] Az = {dome_az.positionActual:+08.3f}; " f"El = {dome_el.positionActual:+08.3f} ") if len(status) > 0: self.log.debug(status) await asyncio.sleep(self.fast_timeout)
async def rotator_telemetry(self) -> None: await self.controllers.mtrotator.tel_electrical.set_write() await self.controllers.mtrotator.evt_connected.set_write(connected=True ) await self.controllers.mtrotator.evt_configuration.set_write( positionAngleUpperLimit=90.0, velocityLimit=3.0, accelerationLimit=1.0, positionErrorThreshold=0.0, positionAngleLowerLimit=-90.0, followingErrorThreshold=0.1, trackingSuccessPositionThreshold=0.01, trackingLostTimeout=5.0, ) await self.controllers.mtrotator.evt_controllerState.set_write( controllerState=0, offlineSubstate=1, enabledSubstate=0, applicationStatus=1024, ) await self.controllers.mtrotator.evt_interlock.set_write(engaged=False) while self.run_telemetry_loop: if (self.controllers.mtrotator.evt_summaryState.data.summaryState == salobj.State.ENABLED): # Safely initilize topic data. if not self.controllers.mtrotator.tel_rotation.has_data: self.controllers.mtrotator.tel_rotation.set( demandPosition=0.0, actualPosition=0.0) error = np.random.normal(0.0, 1e-7) demand = self.controllers.mtrotator.tel_rotation.data.demandPosition position = self.controllers.mtrotator.tel_rotation.data.actualPosition dif = angle_diff(demand, position) await self.controllers.mtrotator.tel_rotation.set_write( actualPosition=position + error + dif.deg / 1.1) await self.controllers.mtrotator.tel_motors.set_write( calibrated=np.zeros(2) + self.controllers.mtrotator. tel_rotation.data.actualPosition, raw=27.4e6 * (np.zeros(2) + self.controllers.mtrotator. tel_rotation.data.actualPosition), ) await self.controllers.mtrotator.tel_ccwFollowingError.set_write( timestamp=current_tai()) if self.acting: await self.controllers.mtrotator.evt_target.set_write( position=position, velocity=0.0, tai=current_tai(), ) await self.controllers.mtrotator.evt_tracking.set_write( tracking=True) await self.controllers.mtrotator.evt_inPosition.set_write( inPosition=np.abs(dif) < ROT_IN_POSITION_DELTA) else: await self.controllers.mtrotator.evt_tracking.set_write( tracking=False) await asyncio.sleep(HEARTBEAT_INTERVAL)
async def mount_telemetry(self) -> None: await self.controllers.mtmount.evt_elevationMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) await self.controllers.mtmount.evt_azimuthMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) await self.controllers.mtmount.evt_cameraCableWrapMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) await self.controllers.mtmount.evt_cameraCableWrapFollowing.set_write( enabled=True) await self.controllers.mtmount.evt_connected.set_write(command=True, replies=True) while self.run_telemetry_loop: if (self.controllers.mtmount.evt_summaryState.data.summaryState == salobj.State.ENABLED): # Safely initilize data if not self.controllers.mtmount.tel_azimuth.has_data: self.controllers.mtmount.tel_azimuth.set( **{ self.mtmount_demand_position_name: 0.0, self.mtmount_actual_position_name: 0.0, }) if not self.controllers.mtmount.tel_elevation.has_data: self.controllers.mtmount.tel_elevation.set( **{ self.mtmount_demand_position_name: 0.0, self.mtmount_actual_position_name: 0.0, }) if not self.controllers.mtmount.tel_elevationDrives.has_data: self.controllers.mtmount.tel_elevationDrives.set() if not self.controllers.mtmount.tel_azimuthDrives.has_data: self.controllers.mtmount.tel_azimuthDrives.set() if not self.controllers.mtmount.evt_target.has_data: self.controllers.mtmount.evt_target.set() az_induced_error = np.random.normal(0.0, 1e-7) el_induced_error = np.random.normal(0.0, 1e-7) az_set = getattr( self.controllers.mtmount.tel_azimuth.data, self.mtmount_demand_position_name, ) el_set = getattr( self.controllers.mtmount.tel_elevation.data, self.mtmount_demand_position_name, ) az_actual = getattr( self.controllers.mtmount.tel_azimuth.data, self.mtmount_actual_position_name, ) el_actual = getattr( self.controllers.mtmount.tel_elevation.data, self.mtmount_actual_position_name, ) az_dif = angle_diff(az_set, az_actual) el_dif = angle_diff(el_set, el_actual) in_position_elevation = np.abs(el_dif) < 1e-1 * u.deg in_position_azimuth = np.abs(az_dif) < 1e-1 * u.deg if self.acting: await self.controllers.mtmount.evt_elevationMotionState.set_write( state=MTMount.AxisMotionState.TRACKING) await self.controllers.mtmount.evt_azimuthMotionState.set_write( state=MTMount.AxisMotionState.TRACKING) await self.controllers.mtmount.evt_cameraCableWrapMotionState.set_write( state=MTMount.AxisMotionState.TRACKING) await self.controllers.mtmount.evt_elevationInPosition.set_write( inPosition=in_position_elevation) await self.controllers.mtmount.evt_azimuthInPosition.set_write( inPosition=in_position_azimuth) await self.controllers.mtmount.evt_cameraCableWrapInPosition.set_write( inPosition=True) elif (self.controllers.mtmount.evt_elevationMotionState.data. state == MTMount.AxisMotionState.TRACKING): await self.controllers.mtmount.evt_elevationMotionState.set_write( state=MTMount.AxisMotionState.STOPPING) await self.controllers.mtmount.evt_azimuthMotionState.set_write( state=MTMount.AxisMotionState.STOPPING) await self.controllers.mtmount.evt_cameraCableWrapMotionState.set_write( state=MTMount.AxisMotionState.STOPPING) else: await self.controllers.mtmount.evt_elevationMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) await self.controllers.mtmount.evt_azimuthMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) await self.controllers.mtmount.evt_cameraCableWrapMotionState.set_write( state=MTMount.AxisMotionState.STOPPED) # The following computation of angleActual is to emulate a # trajectory. At every loop it adds three factors: # 1 - the currect position # 2 - a random error factor # 3 - difference between the current and target positions # multiplied by a STEP_FACTOR, where # 0 < STEP_FACTOR <= 1.0 # If STEP_FACTOR == 1 it means the target position will be # achieved in one loop. By reducing STEP_FACTOR we can emulate # a slew that takes a certain number of iterations to be # completed. await self.controllers.mtmount.tel_azimuth.set_write( **{ self.mtmount_actual_position_name: az_actual + az_induced_error + az_dif.deg * STEP_FACTOR }) await self.controllers.mtmount.tel_elevation.set_write( **{ self.mtmount_actual_position_name: el_actual + el_induced_error + el_dif.deg * STEP_FACTOR }) await self.controllers.mtmount.tel_azimuthDrives.set_write( current=np.random.normal( loc=0.5, scale=0.01, size=len(self.controllers.mtmount.tel_azimuthDrives. data.current), ), timestamp=current_tai(), ) await self.controllers.mtmount.tel_elevationDrives.set_write( current=np.random.normal( loc=0.5, scale=0.01, size=len(self.controllers.mtmount.tel_elevationDrives. data.current), ), timestamp=current_tai(), ) await self.controllers.mtmount.tel_cameraCableWrap.set_write( actualPosition=self.controllers.mtrotator.tel_rotation. data.actualPosition, timestamp=current_tai(), ) await self.controllers.mtmount.evt_cameraCableWrapTarget.set_write( position=self.controllers.mtrotator.tel_rotation.data. actualPosition, taiTime=current_tai(), force_output=True, ) await self.controllers.mtmount.evt_target.write() await asyncio.sleep(HEARTBEAT_INTERVAL)