def solve_field(fname, timeout=15, solve_opts=None, *args, **kwargs): """ Plate solves an image. Note: This is a low-level wrapper around the underlying `solve-field` program. See `get_solve_field` for more typical usage and examples. Args: fname(str, required): Filename to solve in .fits extension. timeout(int, optional): Timeout for the solve-field command, defaults to 60 seconds. solve_opts(list, optional): List of options for solve-field. """ solve_field_script = shutil.which('solve-field') if solve_field_script is None: # pragma: no cover raise error.InvalidSystemCommand( f"Can't find solve-field, is astrometry.net installed?") # Add the options for solving the field if solve_opts is not None: options = solve_opts else: # Default options options = [ '--guess-scale', '--cpulimit', str(timeout), '--no-verify', '--crpix-center', '--temp-axy', '--index-xyls', 'none', '--solved', 'none', '--match', 'none', '--rdls', 'none', '--corr', 'none', '--downsample', '4', '--no-plots', ] if 'ra' in kwargs: options.append('--ra') options.append(str(kwargs.get('ra'))) if 'dec' in kwargs: options.append('--dec') options.append(str(kwargs.get('dec'))) if 'radius' in kwargs: options.append('--radius') options.append(str(kwargs.get('radius'))) # Gather all the kwargs that start with `--` and are not already present. logger.debug(f'Adding kwargs: {kwargs!r}') def _modify_opt(opt, val): if isinstance(val, bool): opt_string = str(opt) else: opt_string = f'{opt}={val}' return opt_string options.extend([ _modify_opt(opt, val) for opt, val in kwargs.items() if opt.startswith('--') and opt not in options and not isinstance(val, bool) ]) cmd = [solve_field_script] + options + [fname] logger.debug(f'Solving with: {cmd}') try: proc = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except Exception as e: raise error.PanError(f"Problem plate-solving in solve_field: {e!r}") return proc
def solve_field(fname, timeout=15, solve_opts=None, **kwargs): """ Plate solves an image. Args: fname(str, required): Filename to solve in .fits extension. timeout(int, optional): Timeout for the solve-field command, defaults to 60 seconds. solve_opts(list, optional): List of options for solve-field. verbose(bool, optional): Show output, defaults to False. """ verbose = kwargs.get('verbose', False) if verbose: print("Entering solve_field") solve_field_script = shutil.which('panoptes-solve-field') if solve_field_script is None: # pragma: no cover raise error.InvalidSystemCommand( "Can't find panoptes-solve-field: {}".format(solve_field_script)) # Add the options for solving the field if solve_opts is not None: options = solve_opts else: options = [ '--guess-scale', '--cpulimit', str(timeout), '--no-verify', '--no-plots', '--crpix-center', '--match', 'none', '--corr', 'none', '--wcs', 'none', '--downsample', '4', ] if kwargs.get('overwrite', False): options.append('--overwrite') if kwargs.get('skip_solved', False): options.append('--skip-solved') if 'ra' in kwargs: options.append('--ra') options.append(str(kwargs.get('ra'))) if 'dec' in kwargs: options.append('--dec') options.append(str(kwargs.get('dec'))) if 'radius' in kwargs: options.append('--radius') options.append(str(kwargs.get('radius'))) if fname.endswith('.fz'): options.append('--extension=1') cmd = [solve_field_script] + options + [fname] if verbose: print("Cmd:", cmd) try: proc = subprocess.Popen(cmd, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: raise error.InvalidCommand( "Can't send command to panoptes-solve-field: {} \t {}".format( e, cmd)) except ValueError as e: raise error.InvalidCommand( "Bad parameters to solve_field: {} \t {}".format(e, cmd)) except Exception as e: raise error.PanError("Timeout on plate solving: {}".format(e)) if verbose: print("Returning proc from solve_field") return proc
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 take_exposure(self, seconds=1.0 * u.second, filename=None, dark=False, blocking=False, timeout=None, *args, **kwargs): """Take an exposure for given number of seconds and saves to provided filename. Args: seconds (u.second, optional): Length of exposure. filename (str, optional): Image is saved to this filename. dark (bool, optional): Exposure is a dark frame, default False. On cameras that support taking dark frames internally (by not opening a mechanical shutter) this will be done, for other cameras the light must be blocked by some other means. In either case setting dark to True will cause the `IMAGETYP` FITS header keyword to have value 'Dark Frame' instead of 'Light Frame'. Set dark to None to disable the `IMAGETYP` keyword entirely. blocking (bool, optional): If False (default) returns immediately after starting the exposure, if True will block until it completes and file exists. timeout (astropy.Quantity): The timeout to use for the exposure. If None, will be calculated automatically. Returns: threading.Thread: The readout thread, which joins when readout has finished. """ self._exposure_error = None if not self.is_connected: err = AssertionError("Camera must be connected for take_exposure!") self.logger.error(str(err)) self._exposure_error = repr(err) raise err if not filename: err = AssertionError("Must pass filename for take_exposure") self.logger.error(str(err)) self._exposure_error = repr(err) raise err if not self.can_take_internal_darks: if dark: try: # Can't take internal dark, so try using an opaque filter in a filterwheel self.filterwheel.move_to_dark_position(blocking=True) self.logger.debug("Taking dark exposure using filter: " + self.filterwheel.filter_name( self.filterwheel._dark_position)) except (AttributeError, error.NotFound): # No filterwheel, or no opaque filter (dark_position not set) self.logger.warning( "Taking dark exposure without shutter or opaque filter." " Is the lens cap on?") else: with suppress(AttributeError, error.NotFound): # Ignoring exceptions from no filterwheel, or no last light position self.filterwheel.move_to_light_position(blocking=True) # Check that the camera (and subcomponents) is ready if not self.is_ready: # Work out why the camera isn't ready. self.logger.warning(f'Cameras not ready: {self.readiness!r}') raise error.PanError( f"Attempt to start exposure on {self} while not ready.") if not isinstance(seconds, u.Quantity): seconds = seconds * u.second self.logger.debug( f'Taking seconds={seconds!r} exposure on {self.name}: filename={filename!r}' ) header = self._create_fits_header(seconds, dark) if self.is_exposing: err = error.PanError( f"Attempt to take exposure on {self} while one already in progress." ) self._exposure_error = repr(err) raise err try: # Camera type specific exposure set up and start self._is_exposing_event.set() readout_args = self._start_exposure(seconds, filename, dark, header, *args, *kwargs) except Exception as err: err = error.PanError(f"Error starting exposure on {self}: {err!r}") self._exposure_error = repr(err) self._is_exposing_event.clear() raise err # Start polling thread that will call camera type specific _readout method when done readout_thread = threading.Thread(target=self._poll_exposure, args=(readout_args, seconds), kwargs=dict(timeout=timeout)) readout_thread.start() if blocking: self.logger.debug(f"Blocking on exposure event for {self}") readout_thread.join() while self.is_exposing: time.sleep(0.5) self.logger.trace( f'Exposure blocking complete, waiting for file to exist') while not os.path.exists(filename): time.sleep(0.1) self.logger.debug( f"Blocking complete on {self} for filename={filename!r}") return readout_thread
def make_timelapse(directory, fn_out=None, glob_pattern='20[1-9][0-9]*T[0-9]*.jpg', overwrite=False, timeout=60, **kwargs): """Create a timelapse. A timelapse is created from all the images in given ``directory`` Args: directory (str): Directory containing image files. fn_out (str, optional): Full path to output file name, if not provided, defaults to `directory` basename. glob_pattern (str, optional): A glob file pattern of images to include, default '20[1-9][0-9]*T[0-9]*.jpg', which corresponds to the observation images but excludes any pointing images. The pattern should be relative to the local directory. overwrite (bool, optional): Overwrite timelapse if exists, default False. timeout (int): Timeout for making movie, default 60 seconds. **kwargs (dict): Returns: str: Name of output file Raises: error.InvalidSystemCommand: Raised if ffmpeg command is not found. FileExistsError: Raised if fn_out already exists and overwrite=False. """ if fn_out is None: head, tail = os.path.split(directory) if tail == '': head, tail = os.path.split(head) field_name = head.split('/')[-2] cam_name = head.split('/')[-1] fname = f'{field_name}_{cam_name}_{tail}.mp4' fn_out = os.path.normpath(os.path.join(directory, fname)) if os.path.exists(fn_out) and not overwrite: raise FileExistsError("Timelapse exists. Set overwrite=True if needed") ffmpeg = shutil.which('ffmpeg') if ffmpeg is None: raise error.InvalidSystemCommand( "ffmpeg not found, can't make timelapse") inputs_glob = os.path.join(directory, glob_pattern) try: ffmpeg_cmd = [ ffmpeg, '-r', '3', '-pattern_type', 'glob', '-i', inputs_glob, '-s', 'hd1080', '-vcodec', 'libx264', ] if overwrite: ffmpeg_cmd.append('-y') ffmpeg_cmd.append(fn_out) logger.debug(ffmpeg_cmd) proc = subprocess.Popen(ffmpeg_cmd, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: # Don't wait forever outs, errs = proc.communicate(timeout=timeout) except subprocess.TimeoutExpired: proc.kill() outs, errs = proc.communicate() finally: logger.debug(f"Output: {outs}") logger.debug(f"Errors: {errs}") # Double-check for file existence if not os.path.exists(fn_out): fn_out = None except Exception as e: raise error.PanError(f"Problem creating timelapse in {fn_out}: {e!r}") return fn_out
def create_location_from_config(): """ Sets up the site and location details. These items are read from the 'site' config directive and include: * name * latitude * longitude * timezone * pressure * elevation * horizon """ logger.debug('Setting up site details') try: config_site = get_config('location', default=None) if config_site is None: raise error.PanError( msg='location information not found in config.') name = config_site.get('name', 'Nameless Location') latitude = config_site.get('latitude') longitude = config_site.get('longitude') timezone = config_site.get('timezone') pressure = config_site.get('pressure', 0.680) * u.bar elevation = config_site.get('elevation', 0 * u.meter) horizon = config_site.get('horizon', 30 * u.degree) flat_horizon = config_site.get('flat_horizon', -6 * u.degree) focus_horizon = config_site.get('focus_horizon', -12 * u.degree) observe_horizon = config_site.get('observe_horizon', -18 * u.degree) location = { 'name': name, 'latitude': latitude, 'longitude': longitude, 'elevation': elevation, 'timezone': timezone, 'pressure': pressure, 'horizon': horizon, 'flat_horizon': flat_horizon, 'focus_horizon': focus_horizon, 'observe_horizon': observe_horizon, } logger.debug(f"Location: {location}") # Create an EarthLocation for the mount earth_location = EarthLocation(lat=latitude, lon=longitude, height=elevation) observer = Observer(location=earth_location, name=name, timezone=timezone) site_details = { "location": location, "earth_location": earth_location, "observer": observer } return site_details except Exception as e: raise error.PanError(msg=f'Bad site information: {e!r}')