From c902bed966321abbf09cc5087300ecff6e44a70e Mon Sep 17 00:00:00 2001 From: Matthew Hasselfield Date: Tue, 19 Sep 2023 14:50:48 -0400 Subject: [PATCH] Update ACU agent to work with LAT (#526) * ACU: cmdline switch to not start monitor/broadcast * ACU: corotator data are tagged as "Corotator" instead of "Axis3" Other fixes to work on the LAT; some simplifications * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ACU: fix turn-around time computation The scan generator code was adding an extra "step_time" (usually 1s) to each turn-around. * ACU: remove az_min / az_max in preparation for az_drift * ACU: add az_drift parameter to scan generator * ACU: add az_drift to agent * ACU: document startup arg --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- socs/agents/acu/agent.py | 102 +++++++++++++++++++++---------------- socs/agents/acu/drivers.py | 71 +++++++++++++++++--------- 2 files changed, 104 insertions(+), 69 deletions(-) diff --git a/socs/agents/acu/agent.py b/socs/agents/acu/agent.py index 6bf8f5953..0f2a554cd 100644 --- a/socs/agents/acu/agent.py +++ b/socs/agents/acu/agent.py @@ -52,9 +52,14 @@ class ACUAgent: The full path to a scan config file describing motions to cycle through on the ACU. If this is None, the associated process and feed will not be registered. + startup (bool): + If True, immediately start the main monitoring processes + for status and UDP data. + """ - def __init__(self, agent, acu_config='guess', exercise_plan=None): + def __init__(self, agent, acu_config='guess', exercise_plan=None, + startup=False): # Separate locks for exclusive access to az/el, and boresight motions. self.azel_lock = TimeoutLock() self.boresight_lock = TimeoutLock() @@ -100,7 +105,7 @@ def __init__(self, agent, acu_config='guess', exercise_plan=None): 'ACU_failures_errors': {}, 'platform_status': {}, 'ACU_emergency': {}, - 'third_axis': {}, + 'corotator': {}, }, 'broadcast': {}, } @@ -120,12 +125,12 @@ def __init__(self, agent, acu_config='guess', exercise_plan=None): self.monitor, self._simple_process_stop, blocking=False, - startup=True) + startup=startup) agent.register_process('broadcast', self.broadcast, self._simple_process_stop, blocking=False, - startup=True) + startup=startup) agent.register_process('generate_scan', self.generate_scan, self._simple_process_stop, @@ -379,6 +384,7 @@ def monitor(self, session, params): 'Azimuth_mode': None, 'Elevation_mode': None, 'Boresight_mode': None, + 'Corotator_mode': None, } j = yield self.acu_read.http.Values(self.acu8100) @@ -461,9 +467,18 @@ def monitor(self, session, params): self.log.warn('ACU now in remote mode.') if self.data['status']['summary']['ctime'] == prev_checkdata['ctime']: self.log.warn('ACU time has not changed from previous data point!') - for axis_mode in ['Azimuth_mode', 'Elevation_mode', 'Boresight_mode']: - if self.data['status']['summary'][axis_mode] != prev_checkdata[axis_mode]: - self.log.info(axis_mode + ' has changed to ' + self.data['status']['summary'][axis_mode]) + + # Alert on any axis mode change. + for axis_mode in prev_checkdata.keys(): + if 'mode' not in axis_mode: + continue + v = self.data['status']['summary'].get(axis_mode) + if v is None: + v = self.data['status']['corotator'].get(axis_mode) + if v != prev_checkdata[axis_mode]: + self.log.info('{axis_mode} is now "{v}"', + axis_mode=axis_mode, v=v) + prev_checkdata[axis_mode] = v # influx_blocks are constructed based on refers to all # other self.data['status'] keys. Do not add more keys to @@ -577,11 +592,6 @@ def monitor(self, session, params): data_blocks.update(new_blocks) - prev_checkdata = {'ctime': self.data['status']['summary']['ctime'], - 'Azimuth_mode': self.data['status']['summary']['Azimuth_mode'], - 'Elevation_mode': self.data['status']['summary']['Elevation_mode'], - 'Boresight_mode': self.data['status']['summary']['Boresight_mode'], - } return True, 'Acquisition exited cleanly.' @ocs_agent.param('auto_enable', type=bool, default=True) @@ -1158,51 +1168,44 @@ def stop_and_clear(self, session, params): to Stop; also clear the ProgramTrack stack. """ - - session.set_status('running') - i = 0 - while i < 5: + def _read_modes(): modes = [self.data['status']['summary']['Azimuth_mode'], - self.data['status']['summary']['Elevation_mode'], - ] + self.data['status']['summary']['Elevation_mode']] if self.acu_config['platform'] == 'satp': modes.append(self.data['status']['summary']['Boresight_mode']) elif self.acu_config['platform'] in ['ccat', 'lat']: - modes.append(self.data['status']['third_axis']['Axis3_mode']) - if modes != ['Stop', 'Stop', 'Stop']: + modes.append(self.data['status']['corotator']['Corotator_mode']) + return modes + + session.set_status('running') + for i in range(6): + if all([m == 'Stop' for m in _read_modes()]): + self.log.info('All axes in Stop mode') + break + else: yield self.acu_control.stop() self.log.info('Stop called (iteration %i)' % (i + 1)) yield dsleep(0.1) i += 1 - else: - self.log.info('All axes in Stop mode') - i = 5 - modes = [self.data['status']['summary']['Azimuth_mode'], - self.data['status']['summary']['Elevation_mode'], - ] - if self.acu_config['platform'] == 'satp': - modes.append(self.data['status']['summary']['Boresight_mode']) - elif self.acu_config['platform'] in ['ccat', 'lat']: - modes.append(self.data['status']['third_axis']['Axis3_mode']) - if modes != ['Stop', 'Stop', 'Stop']: - self.log.error('Axes could not be set to Stop!') - return False, 'Could not set axes to Stop mode' - j = 0 - while j < 5: + else: + msg = 'Failed to set all axes to Stop mode!' + self.log.error(msg) + return False, msg + + for i in range(6): free_stack = self.data['status']['summary']['Free_upload_positions'] if free_stack < FULL_STACK: yield self.acu_control.http.Command('DataSets.CmdTimePositionTransfer', 'Clear Stack') - self.log.info('Clear Stack called (iteration %i)' % (j + 1)) + self.log.info('Clear Stack called (iteration %i)' % (i + 1)) yield dsleep(0.1) - j += 1 else: self.log.info('Stack cleared') - j = 5 - free_stack = self.data['status']['summary']['Free_upload_positions'] - if free_stack < FULL_STACK: - self.log.warn('Stack not fully cleared!') - return False, 'Could not clear stack' + break + else: + msg = 'Failed to clear the ProgramTrack stack!' + self.log.warn(msg) + return False, msg session.set_status('stopping') return True, 'Job completed' @@ -1287,6 +1290,7 @@ def line_batcher(lines, n=10): @ocs_agent.param('az_start', default='end', choices=['end', 'mid', 'az_endpoint1', 'az_endpoint2', 'mid_inc', 'mid_dec']) + @ocs_agent.param('az_drift', type=float, default=None) @ocs_agent.param('az_only', type=bool, default=True) @ocs_agent.param('scan_upload_length', type=float, default=None) @inlineCallbacks @@ -1297,7 +1301,7 @@ def generate_scan(self, session, params): el_speed=None, \ num_scans=None, start_time=None, \ wait_to_start=None, step_time=None, \ - az_start='end', az_only=True, \ + az_start='end', az_drift=None, az_only=True, \ scan_upload_length=None) **Process** - Scan generator, currently only works for @@ -1337,6 +1341,10 @@ def generate_scan(self, session, params): of the scan use 'mid_inc' (for first half-leg to have positive az velocity), 'mid_dec' (negative az velocity), or 'mid' (velocity oriented towards endpoint2). + az_drift (float): if set, this should be a drift velocity + in deg/s. The scan extrema will move accordingly. This + can be used to better follow compact sources as they + rise or set through the focal plane. az_only (bool): if True (the default), then only the Azimuth axis is put in ProgramTrack mode, and the El axis is put in Stop mode. @@ -1379,7 +1387,8 @@ def generate_scan(self, session, params): scan_upload_len = params.get('scan_upload_length') scan_params = {k: params.get(k) for k in [ 'num_scans', 'num_batches', 'start_time', - 'wait_to_start', 'step_time', 'batch_size', 'az_start'] + 'wait_to_start', 'step_time', 'batch_size', + 'az_start', 'az_drift'] if params.get(k) is not None} el_speed = params.get('el_speed', 0.0) @@ -1682,6 +1691,8 @@ def add_agent_args(parser_in=None): pgroup = parser_in.add_argument_group('Agent Options') pgroup.add_argument("--acu-config") pgroup.add_argument("--exercise-plan") + pgroup.add_argument("--no-processes", action='store_true', + default=False) return parser_in @@ -1691,7 +1702,8 @@ def main(args=None): parser=parser, args=args) agent, runner = ocs_agent.init_site_agent(args) - _ = ACUAgent(agent, args.acu_config, args.exercise_plan) + _ = ACUAgent(agent, args.acu_config, args.exercise_plan, + startup=not args.no_processes) runner.run(agent, auto_reconnect=True) diff --git a/socs/agents/acu/drivers.py b/socs/agents/acu/drivers.py index b260b45dc..3398fdcef 100644 --- a/socs/agents/acu/drivers.py +++ b/socs/agents/acu/drivers.py @@ -225,6 +225,7 @@ def generate_constant_velocity_scan(az_endpoint1, az_endpoint2, az_speed, batch_size=500, az_start='mid_inc', az_first_pos=None, + az_drift=None, ptstack_fmt=True): """Python generator to produce times, azimuth and elevation positions, azimuth and elevation velocities, azimuth and elevation flags for @@ -266,6 +267,9 @@ def generate_constant_velocity_scan(az_endpoint1, az_endpoint2, az_speed, az_first_pos (float): If not None, the first az scan will start at this position (but otherwise proceed in the same starting direction). + az_drift (float): The rate (deg / s) at which to shift the + scan endpoints in time. This can be used to better track + celestial sources in targeted scans. ptstack_fmt (bool): determine whether values are produced with the necessary format to upload to the ACU. If False, this function will produce lists of time, azimuth, elevation, azimuth velocity, @@ -273,26 +277,45 @@ def generate_constant_velocity_scan(az_endpoint1, az_endpoint2, az_speed, True. """ - az_min = min(az_endpoint1, az_endpoint2) - az_max = max(az_endpoint1, az_endpoint2) - if az_max == az_min: + def get_target_az(current_az, current_t, increasing): + # Return the next endpoint azimuth, based on current (az, t) + # and whether to move in +ve or -ve az direction. + # + # Includes the effects of az_drift, to keep the scan endpoints + # (at least at the end of a scan) on the drifted trajectories. + if increasing: + target = max(az_endpoint1, az_endpoint2) + else: + target = min(az_endpoint1, az_endpoint2) + if az_drift is not None: + v = az_speed if increasing else -az_speed + target = target + az_drift / (v - az_drift) * ( + (target - current_az + v * current_t)) + return target + + if az_endpoint1 == az_endpoint2: raise ValueError('Generator requires two different az endpoints!') + + # Note that starting scan direction gets modified, below, + # depending on az_start. + increasing = az_endpoint2 > az_endpoint1 + if az_start in ['az_endpoint1', 'az_endpoint2', 'end']: if az_start in ['az_endpoint1', 'end']: az = az_endpoint1 else: az = az_endpoint2 - increasing = (az == az_min) + increasing = not increasing elif az_start in ['mid_inc', 'mid_dec', 'mid']: az = (az_endpoint1 + az_endpoint2) / 2 if az_start == 'mid': - increasing = az_endpoint2 > az_endpoint1 + pass elif az_start == 'mid_inc': increasing = True else: increasing = False else: - raise ValueError('az_start value not supported. Choose from ' + raise ValueError(f'az_start value "{az_start}" not supported. Choose from ' 'az_endpoint1, az_endpoint2, mid_inc, mid_dec') az_vel = az_speed if increasing else -az_speed @@ -318,7 +341,7 @@ def generate_constant_velocity_scan(az_endpoint1, az_endpoint2, az_speed, stop_iter = float('inf') else: stop_iter = num_batches - batch_size = int(np.ceil((az_max - az_min) / daz)) + batch_size = int(np.ceil(abs(az_endpoint2 - az_endpoint1) / daz)) def dec_num_scans(): nonlocal num_scans @@ -328,6 +351,7 @@ def dec_num_scans(): def check_num_scans(): return num_scans is None or num_scans > 0 + target_az = get_target_az(az, t, increasing) point_group_batch = 0 i = 0 @@ -344,64 +368,63 @@ def check_num_scans(): point_block[6].append(el_flag) point_block[7].append(int(point_group_batch > 0)) - t += step_time if point_group_batch > 0: point_group_batch -= 1 if increasing: - if az <= (az_max - 2 * daz): + if az <= (target_az - 2 * daz): + t += step_time az += daz az_vel = az_speed el_vel = el_speed az_flag = 1 el_flag = 0 - increasing = True - elif az == az_max: + elif az == target_az: + # Turn around. t += turntime az_vel = -1 * az_speed el_vel = el_speed az_flag = 1 el_flag = 0 increasing = False + target_az = get_target_az(az, t, increasing) dec_num_scans() point_group_batch = MIN_GROUP_NEW_LEG - 1 else: - az_remaining = az_max - az - time_remaining = az_remaining / az_speed - az = az_max - t += (time_remaining - step_time) + time_remaining = (target_az - az) / az_speed + az = target_az + t += time_remaining az_vel = az_speed el_vel = el_speed az_flag = 2 el_flag = 0 - increasing = True else: - if az >= (az_min + 2 * daz): + if az >= (target_az + 2 * daz): + t += step_time az -= daz az_vel = -1 * az_speed el_vel = el_speed az_flag = 1 el_flag = 0 - increasing = False - elif az == az_min: + elif az == target_az: + # Turn around. t += turntime az_vel = az_speed el_vel = el_speed az_flag = 1 el_flag = 0 increasing = True + target_az = get_target_az(az, t, increasing) dec_num_scans() point_group_batch = MIN_GROUP_NEW_LEG - 1 else: - az_remaining = az - az_min - time_remaining = az_remaining / az_speed - az = az_min - t += (time_remaining - step_time) + time_remaining = (az - target_az) / az_speed + az = target_az + t += time_remaining az_vel = -1 * az_speed el_vel = el_speed az_flag = 2 el_flag = 0 - increasing = False if not check_num_scans(): # Kill the velocity on the last point and exit -- this