def _move_to(self, position): if self._moving: self._move_event.set() msg = "Attempt to move filter wheel when already moving" self.logger.error(msg) raise RuntimeError(msg) move_distance = position - self.position if self.is_unidirectional: # Filter wheel can only move one way, will have to go the long way around for -ve moves move_distance = move_distance % self._n_positions else: # Filter wheel can move either direction, just need magnitude of the move. move_distance = abs(move_distance) move_duration = move_distance * self._move_time move = threading.Timer(interval=move_duration, function=self._complete_move, args=(position, )) self._position = float('nan') self._moving = True move.start() if move_duration > self._timeout: move.join(timeout=self._timeout) # If still alive then kill and raise timeout if move.is_alive(): self._move_event.set() self._moving = False msg = "Timeout waiting for filter wheel move to complete" self.logger.error(msg) raise error.Timeout(msg)
def slew_to_target(self, blocking=False, timeout=180): """ Slews to the currently assigned target coordinates. Slews the mount to the coordinates that have been assigned by `~set_target_coordinates`. If no coordinates have been set, do nothing and return `False`, otherwise return response from the mount. If `blocking=True` then wait for up to `timeout` seconds for the mount to reach the `is_tracking` state. If a timeout occurs, raise a `pocs.error.Timeout` exception. Args: blocking (bool, optional): If command should block while slewing to home, default False. timeout (int, optional): Maximum time spent slewing to home, default 180 seconds. Returns: bool: indicating success """ success = False if self.is_parked: self.logger.info("Mount is parked") elif not self.has_target: self.logger.info("Target Coordinates not set") else: self.logger.debug('Slewing to target') success = self.query('slew_to_target') self.logger.debug("Mount response: {}".format(success)) if success: if blocking: # Set up the timeout timer self.logger.debug( f'Setting slew timeout timer for {timeout} sec') timeout_timer = CountdownTimer(timeout) block_time = 3 # seconds while self.is_tracking is False: if timeout_timer.expired(): self.logger.warning( f'slew_to_target timout: {timeout} seconds') raise error.Timeout('Problem slewing to target') self.logger.debug( f'Slewing to target, sleeping for {block_time} seconds' ) timeout_timer.sleep(max_sleep=block_time) self.logger.debug(f'Done with slew_to_target block') else: self.logger.warning('Problem with slew_to_target') return success
def _poll_exposure(self, readout_args, exposure_time, timeout=None, interval=0.01): """ Wait until camera is no longer exposing or the timeout is reached. If the timeout is reached, an `error.Timeout` is raised. """ if timeout is None: timer_duration = self._timeout + self._readout_time + exposure_time.to_value( u.second) else: timer_duration = timeout self.logger.debug( f"Polling exposure with timeout of {timer_duration} seconds.") timer = CountdownTimer(duration=timer_duration) try: while self.is_exposing: if timer.expired(): msg = f"Timeout (timer.duration={timer.duration!r}) waiting for exposure on" f" {self} to complete" raise error.Timeout(msg) time.sleep(interval) except Exception as err: # Error returned by driver at some point while polling self.logger.error( f'Error while waiting for exposure on {self}: {err!r}') self._exposure_error = repr(err) raise err else: # Camera type specific readout function try: self._readout(*readout_args) except Exception as err: self.logger.error(f"Error during readout on {self}: {err!r}") self._exposure_error = repr(err) raise err finally: # Make sure this gets set regardless of any errors self._is_exposing_event.clear()
def correct_tracking(self, correction_info, axis_timeout=30.): """ Make tracking adjustment corrections. Args: correction_info (dict[tuple]): Correction info to be applied, see `get_tracking_correction`. axis_timeout (float, optional): Timeout for adjustment in each axis, default 30 seconds. Raises: `error.Timeout`: Timeout error. """ for axis, corrections in correction_info.items(): if not axis or not corrections: continue offset = corrections[0] offset_ms = corrections[1] delta_direction = corrections[2] self.logger.info("Adjusting {}: {} {:0.2f} ms {:0.2f}".format( axis, delta_direction, offset_ms, offset)) self.query('move_ms_{}'.format(delta_direction), '{:05.0f}'.format(offset_ms)) # Adjust tracking for `axis_timeout` seconds then fail if not done. start_tracking_time = current_time() while self.is_tracking is False: if (current_time() - start_tracking_time).sec > axis_timeout: raise error.Timeout( "Tracking adjustment timeout: {}".format(axis)) self.logger.debug( "Waiting for {} tracking adjustment".format(axis)) time.sleep(0.5)
def wait_for_events( events, timeout=600, sleep_delay=5 * u.second, callback=None, ): """Wait for event(s) to be set. This method will wait for a maximum of `timeout` seconds for all of the `events` to complete. Checks every `sleep_delay` seconds for the events to be set. If provided, the `callback` will be called every `sleep_delay` seconds. The callback should return `True` to continue waiting otherwise `False` to interrupt the loop and return from the function. .. doctest:: >>> import time >>> import threading >>> from panoptes.utils.time import wait_for_events >>> # Create some events, normally something like taking an image. >>> event0 = threading.Event() >>> event1 = threading.Event() >>> # Wait for 30 seconds but interrupt after 1 second by returning False from callback. >>> def interrupt_cb(): time.sleep(1); return False >>> # The function will return False if events are not set. >>> wait_for_events([event0, event1], timeout=30, callback=interrupt_cb) False >>> # Timeout will raise an exception. >>> wait_for_events([event0, event1], timeout=1) Traceback (most recent call last): File "<input>", line 1, in <module> File ".../panoptes-utils/src/panoptes/utils/time.py", line 254, in wait_for_events panoptes.utils.error.Timeout: Timeout: Timeout waiting for generic event >>> # Set the events in another thread for normal usage. >>> def set_events(): time.sleep(1); event0.set(); event1.set() >>> threading.Thread(target=set_events).start() >>> wait_for_events([event0, event1], timeout=30) True Args: events (list(`threading.Event`)): An Event or list of Events to wait on. timeout (float|`astropy.units.Quantity`): Timeout in seconds to wait for events, default 600 seconds. sleep_delay (float, optional): Time in seconds between event checks. callback (callable): A periodic callback that should return `True` to continue waiting or `False` to interrupt the loop. Can also be used for e.g. custom logging. Returns: bool: True if events were set, False otherwise. Raises: error.Timeout: Raised if events have not all been set before `timeout` seconds. """ with suppress(AttributeError): sleep_delay = sleep_delay.to_value('second') event_timer = CountdownTimer(timeout) if not isinstance(events, list): events = [events] start_time = current_time() while not all([event.is_set() for event in events]): elapsed_secs = round((current_time() - start_time).to_value('second'), 2) if event_timer.expired(): raise error.Timeout( f"Timeout waiting for {len(events)} events after {elapsed_secs} seconds" ) if callable(callback) and callback() is False: logger.warning( f"Waiting for {len(events)} events has been interrupted after {elapsed_secs} seconds" ) break # Sleep for a little bit. event_timer.sleep(max_sleep=sleep_delay) return all([event.is_set() for event in events])
def get_solve_field(fname, replace=True, overwrite=True, timeout=30, **kwargs): """Convenience function to wait for `solve_field` to finish. This function merely passes the `fname` of the image to be solved along to `solve_field`, which returns a subprocess.Popen object. This function then waits for that command to complete, populates a dictonary with the EXIF informaiton and returns. This is often more useful than the raw `solve_field` function. Example: >>> from panoptes.utils.images import fits as fits_utils >>> # Get our fits filename. >>> fits_fn = getfixture('unsolved_fits_file') >>> # Perform the solve. >>> solve_info = fits_utils.get_solve_field(fits_fn) >>> # Show solved filename. >>> solve_info['solved_fits_file'] '.../unsolved.fits' >>> # Pass a suggested location. >>> ra = 15.23 >>> dec = 90 >>> radius = 5 # deg >>> solve_info = fits_utils.solve_field(fits_fn, ra=ra, dec=dec, radius=radius) >>> # Pass kwargs to `solve-field` program. >>> solve_kwargs = {'--pnm': '/tmp/awesome.bmp', '--overwrite': True} >>> solve_info = fits_utils.get_solve_field(fits_fn, **solve_kwargs, skip_solved=False) >>> assert os.path.exists('/tmp/awesome.bmp') Args: fname ({str}): Name of FITS file to be solved. replace (bool, optional): Saves the WCS back to the original file, otherwise output base filename with `.new` extension. Default True. overwrite (bool, optional): Clobber file, default True. Required if `replace=True`. timeout (int, optional): The timeout for solving, default 30 seconds. **kwargs ({dict}): Options to pass to `solve_field` should start with `--`. Returns: dict: Keyword information from the solved field. """ skip_solved = kwargs.get('skip_solved', True) out_dict = {} output = None errs = None header = getheader(fname) wcs = WCS(header) # Check for solved file if skip_solved and wcs.is_celestial: logger.info( f"Skipping solved file (use skip_solved=False to solve again): {fname}" ) out_dict.update(header) out_dict['solved_fits_file'] = fname return out_dict # Set a default radius of 15 if overwrite: kwargs['--overwrite'] = True # Use unpacked version of file. was_compressed = False if fname.endswith('.fz'): logger.debug(f'Uncompressing {fname}') fname = funpack(fname) logger.debug(f'Using {fname} for solving') was_compressed = True logger.debug(f'Solving with: {kwargs!r}') proc = solve_field(fname, **kwargs) try: output, errs = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired: proc.kill() output, errs = proc.communicate() raise error.Timeout(f'Timeout while solving: {output!r} {errs!r}') else: if proc.returncode != 0: logger.debug(f'Returncode: {proc.returncode}') for log in [output, errs]: if log and log > '': logger.debug(f'Output on {fname}: {log}') if proc.returncode == 3: raise error.SolveError(f'solve-field not found: {output}') new_fname = fname.replace('.fits', '.new') if replace: logger.debug(f'Overwriting original {fname}') os.replace(new_fname, fname) else: fname = new_fname try: header = getheader(fname) header.remove('COMMENT', ignore_missing=True, remove_all=True) header.remove('HISTORY', ignore_missing=True, remove_all=True) out_dict.update(header) except OSError: logger.warning(f"Can't read fits header for: {fname}") # Check it was solved. if WCS(header).is_celestial is False: raise error.SolveError( 'File not properly solved, no WCS header present.') # Remove WCS file. os.remove(fname.replace('.fits', '.wcs')) if was_compressed and replace: logger.debug(f'Compressing plate-solved {fname}') fname = fpack(fname) out_dict['solved_fits_file'] = fname return out_dict
def get_solve_field(fname, replace=True, remove_extras=True, **kwargs): """Convenience function to wait for `solve_field` to finish. This function merely passes the `fname` of the image to be solved along to `solve_field`, which returns a subprocess.Popen object. This function then waits for that command to complete, populates a dictonary with the EXIF informaiton and returns. This is often more useful than the raw `solve_field` function Args: fname ({str}): Name of FITS file to be solved replace (bool, optional): Replace fname the solved file remove_extras (bool, optional): Remove the files generated by solver **kwargs ({dict}): Options to pass to `solve_field` Returns: dict: Keyword information from the solved field """ verbose = kwargs.get('verbose', False) skip_solved = kwargs.get('skip_solved', True) out_dict = {} output = None errs = None file_path, file_ext = os.path.splitext(fname) header = getheader(fname) wcs = WCS(header) # Check for solved file if skip_solved and wcs.is_celestial: if verbose: print("Solved file exists, skipping", "(pass skip_solved=False to solve again):", fname) out_dict.update(header) out_dict['solved_fits_file'] = fname return out_dict if verbose: print("Entering get_solve_field:", fname) # Set a default radius of 15 kwargs.setdefault('radius', 15) proc = solve_field(fname, **kwargs) try: output, errs = proc.communicate(timeout=kwargs.get('timeout', 30)) except subprocess.TimeoutExpired: proc.kill() raise error.Timeout("Timeout while solving") else: if verbose: print("Returncode:", proc.returncode) print("Output:", output) print("Errors:", errs) if proc.returncode == 3: raise error.SolveError('solve-field not found: {}'.format(output)) if not os.path.exists(fname.replace(file_ext, '.solved')): raise error.SolveError('File not solved') try: # Handle extra files created by astrometry.net new = fname.replace(file_ext, '.new') rdls = fname.replace(file_ext, '.rdls') axy = fname.replace(file_ext, '.axy') xyls = fname.replace(file_ext, '-indx.xyls') if replace and os.path.exists(new): # Remove converted fits os.remove(fname) # Rename solved fits to proper extension os.rename(new, fname) out_dict['solved_fits_file'] = fname else: out_dict['solved_fits_file'] = new if remove_extras: for f in [rdls, xyls, axy]: if os.path.exists(f): os.remove(f) except Exception as e: warn('Cannot remove extra files: {}'.format(e)) if errs is not None: warn("Error in solving: {}".format(errs)) else: try: out_dict.update(getheader(fname)) except OSError: if verbose: print("Can't read fits header for:", fname) return out_dict
def improve_wcs(fname, remove_extras=True, replace=True, timeout=30, **kwargs): """Improve the world-coordinate-system (WCS) of a FITS file. This will plate-solve an already-solved field, using a verification process that will also attempt a SIP distortion correction. Args: fname (str): Full path to FITS file. remove_extras (bool, optional): If generated files should be removed, default True. replace (bool, optional): Overwrite existing file, default True. timeout (int, optional): Timeout for the solve, default 30 seconds. **kwargs: Additional keyword args for `solve_field`. Can also include a `verbose` flag. Returns: dict: FITS headers, including solve information. Raises: error.SolveError: Description error.Timeout: Description """ verbose = kwargs.get('verbose', False) out_dict = {} output = None errs = None if verbose: print("Entering improve_wcs: {}".format(fname)) options = [ '--continue', '-t', '3', '-q', '0.01', '--no-plots', '--guess-scale', '--cpulimit', str(timeout), '--no-verify', '--crpix-center', '--match', 'none', '--corr', 'none', '--wcs', 'none', '-V', fname, ] proc = solve_field(fname, solve_opts=options, **kwargs) try: output, errs = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired: proc.kill() raise error.Timeout("Timeout while solving") else: if verbose: print("Output: {}", output) print("Errors: {}", errs) if not os.path.exists(fname.replace('.fits', '.solved')): raise error.SolveError('File not solved') try: # Handle extra files created by astrometry.net new = fname.replace('.fits', '.new') rdls = fname.replace('.fits', '.rdls') axy = fname.replace('.fits', '.axy') xyls = fname.replace('.fits', '-indx.xyls') if replace and os.path.exists(new): # Remove converted fits os.remove(fname) # Rename solved fits to proper extension os.rename(new, fname) out_dict['solved_fits_file'] = fname else: out_dict['solved_fits_file'] = new if remove_extras: for f in [rdls, xyls, axy]: if os.path.exists(f): os.remove(f) except Exception as e: warn('Cannot remove extra files: {}'.format(e)) if errs is not None: warn("Error in solving: {}".format(errs)) else: try: out_dict.update(fits.getheader(fname)) except OSError: if verbose: print("Can't read fits header for {}".format(fname)) return out_dict
def _efw_poll(self, filterwheel_ID, position, move_event, timeout): """ Polls filter wheel until the current move is complete. Also monitors for errors while polling and checks position after the move is complete. Optionally sets a threading.Event to signal the end of the move. Has an optional timeout to raise an TimeoutError is the move takes longer than expected. Args: filterwheel_ID (int): integer ID of the filterwheel that is moving. position (int): position to move the filter wheel. Must be an integer >= 0. move_event (threading.Event, optional): Event to set once the move is complete timeout (u.Quantity, optional): maximum time to wait for the move to complete. Should be a Quantity with time units. If a numeric type without units is given seconds will be assumed. Raises: `panoptes.utils.error.PanError`: raised if the driver returns an error or if the final position is not as expected. `panoptes.utils.error.Timeout`: raised if the move does not end within the period of time specified by the timeout argument. """ if timeout is not None: timer = CountdownTimer(duration=timeout) try: # No status query function in the SDK. Only way to check on progress of move # is to keep issuing the same move command until we stop getting the MOVING # error code back. error_code = self._CDLL.EFWSetPosition( ctypes.c_int(filterwheel_ID), ctypes.c_int(position)) while error_code == ErrorCode.MOVING: if timeout is not None and timer.expired(): msg = "Timeout waiting for filterwheel {} to move to {}".format( filterwheel_ID, position) raise error.Timeout(msg) time.sleep(0.1) error_code = self._CDLL.EFWSetPosition( ctypes.c_int(filterwheel_ID), ctypes.c_int(position)) if error_code != ErrorCode.SUCCESS: # Got some sort of error while polling. msg = "Error while moving filterwheel {} to {}: {}".format( filterwheel_ID, position, ErrorCode(error_code).name) self.logger.error(msg) raise error.PanError(msg) final_position = self.get_position(filterwheel_ID) if final_position != position: msg = "Tried to move filterwheel {} to {}, but ended up at {}.".format( filterwheel_ID, position, final_position) self.logger.error(msg) raise error.PanError(msg) self.logger.debug( f"Filter wheel {filterwheel_ID} moved to {position}.") finally: # Regardless must always set the Event when the move has stopped. if move_event is not None: move_event.set()
def _timeout_move(self): self._move_event.set() msg = "Timeout waiting for filter wheel move to complete" self.logger.error(msg) raise error.Timeout(msg)