def move_direction(self, direction='north', seconds=1.0): """ Move mount in specified `direction` for given amount of `seconds` """ seconds = float(seconds) assert direction in ['north', 'south', 'east', 'west'] move_command = 'move_{}'.format(direction) self.logger.debug("Move command: {}".format(move_command)) try: now = current_time() self.logger.debug("Moving {} for {} seconds. ".format( direction, seconds)) self.query(move_command) time.sleep(seconds) self.logger.debug("{} seconds passed before stop".format( (current_time() - now).sec)) self.query('stop_moving') self.logger.debug("{} seconds passed total".format( (current_time() - now).sec)) except KeyboardInterrupt: self.logger.warning("Keyboard interrupt, stopping movement.") except Exception as e: self.logger.warning( "Problem moving command!! Make sure mount has stopped moving: {}" .format(e)) finally: # Note: We do this twice. That's fine. self.logger.debug("Stopping movement") self.query('stop_moving')
def current_observation(self, new_observation): if self.current_observation is None: # If we have no current observation but do have a new one, set seq_time # and add to the list if new_observation is not None: # Set the new seq_time for the observation new_observation.seq_time = current_time(flatten=True) # Add the new observation to the list self.observed_list[new_observation.seq_time] = new_observation else: # If no new observation, simply reset the current if new_observation is None: self.current_observation.reset() else: # If we have a new observation, check if same as old observation if self.current_observation.name != new_observation.name: self.current_observation.reset() new_observation.seq_time = current_time(flatten=True) # Add the new observation to the list self.observed_list[ new_observation.seq_time] = new_observation self.logger.info( "Setting new observation to {}".format(new_observation)) self._current_observation = new_observation
def test_pretty_time(): t0 = '2016-08-13 10:00:00' os.environ['POCSTIME'] = t0 t1 = current_time(pretty=True) assert t1 == t0 # This will increment one second - see docs t2 = current_time(flatten=True) assert t2 != t0 assert t2 == '20160813T100001' # This will increment one second - see docs t3 = current_time(datetime=True) assert t3 == dt(2016, 8, 13, 10, 0, 2, tzinfo=tz.utc)
def obj(): return { "name": "Testing PANOPTES Unit", "pan_id": "PAN000", "location": { "name": "Mauna Loa Observatory", "latitude": 19.54 * u.degree, # Astropy unit "longitude": "-155.58 deg", # String unit "elevation": "3400.0 m", "horizon": 30 * u.degree, "flat_horizon": -6 * u.degree, "focus_horizon": -12 * u.degree, "observe_horizon": -18 * u.degree, "timezone": "US/Hawaii", "gmt_offset": -600, }, "directories": { "base": "/var/panoptes", "images": "images", "data": "data", "resources": "POCS/resources/", "targets": "POCS/resources/targets", "mounts": "POCS/resources/mounts", }, "db": { "name": "panoptes", "type": "file" }, "empty": {}, "current_time": current_time(), "bool": True, "exception": TypeError, "panoptes_exception": error.InvalidObservation }
def observe(self): """Take individual images for the current observation This method gets the current observation and takes the next corresponding exposure. """ # Get observatory metadata headers = self.get_standard_headers() # All cameras share a similar start time headers['start_time'] = current_time(flatten=True) # List of camera events to wait for to signal exposure is done # processing observing_events = dict() # Take exposure with each camera for cam_name, camera in self.cameras.items(): self.logger.debug(f"Exposing for camera: {cam_name}") try: # Start the exposures camera_observe_event = camera.take_observation( self.current_observation, headers) observing_events[cam_name] = camera_observe_event except Exception as e: self.logger.error(f"Problem waiting for images: {e!r}") return observing_events
def is_dark(self, horizon='observe', default_dark=-18 * u.degree, at_time=None): """If sun is below horizon. Args: horizon (str, optional): Which horizon to use, 'flat', 'focus', or 'observe' (default). default_dark (`astropy.unit.Quantity`, optional): The default horizon for when it is considered "dark". Default is astronomical twilight, -18 degrees. at_time (None or `astropy.time.Time`, optional): Time at which to check if dark, defaults to now. Returns: bool: If it is dark or not. """ if at_time is None: at_time = current_time() horizon_deg = self.get_config(f'location.{horizon}_horizon', default=default_dark) is_dark = self.observer.is_night(at_time, horizon=horizon_deg) self._local_sun_pos = self.observer.altaz(at_time, target=get_sun(at_time)).alt self.logger.debug( f"Sun {self._local_sun_pos:.02f} > {horizon_deg} [{horizon}]") return is_dark
def altaz_to_radec(alt=35, az=90, location=None, obstime=None, verbose=False): """Convert alt/az degrees to RA/Dec SkyCoord. Args: alt (int, optional): Altitude, defaults to 35 az (int, optional): Azimute, defaults to 90 (east) location (None|astropy.coordinates.EarthLocation, required): A valid location. obstime (None, optional): Time for object, defaults to `current_time` verbose (bool, optional): Verbose, default False. Returns: astropy.coordinates.SkyCoord: Coordinates corresponding to the AltAz. """ assert location is not None if obstime is None: obstime = current_time() if verbose: print("Getting coordinates for Alt {} Az {}, from {} at {}".format( alt, az, location, obstime)) altaz = AltAz(obstime=obstime, location=location, alt=alt * u.deg, az=az * u.deg) return SkyCoord(altaz.transform_to(ICRS))
def capture(self, store_result=True): """Read JSON from endpoint url and capture data. Note: Currently this doesn't do any processing or have a callback. Returns: sensor_data (dict): Dictionary of sensors keyed by sensor name. """ self.logger.debug( f'Capturing data from remote url: {self.endpoint_url}') sensor_data = requests.get(self.endpoint_url).json() if isinstance(sensor_data, list): sensor_data = sensor_data[0] self.logger.debug(f'Captured on {self.sensor_name}: {sensor_data!r}') sensor_data['date'] = current_time(flatten=True) if store_result and len(sensor_data) > 0: self.db.insert_current(self.sensor_name, sensor_data) # Make a separate power entry if 'power' in sensor_data: self.db.insert_current('power', sensor_data['power']) self.logger.debug(f'Remote data: {sensor_data}') return sensor_data
def has_ac_power(self, stale=90): """Check for system AC power. Power readings are done by the arduino and are placed in the metadata database. This method looks for entries saved with type `power` and key `main` the `current` collection. The method will also return False if the record is older than `stale` seconds. Args: stale (int, optional): Number of seconds before record is stale, defaults to 90 seconds. Returns: bool: True if system AC power is present. """ # Always assume False self.logger.debug("Checking for AC power") has_power = False if self._in_simulator('power'): return True # Get current power readings from database try: record = self.db.get_current('power') if record is None: self.logger.warning( f'No mains "power" reading found in database.') has_power = False # Assume not for power_key in ['main', 'mains']: # Legacy control boards have `main`. with suppress(KeyError): has_power = bool(record['data'][power_key]) timestamp = record['date'].replace( tzinfo=None) # current_time is timezone naive age = (current_time().datetime - timestamp).total_seconds() self.logger.debug( f"Power Safety: {has_power} [{age:.0f} sec old - {timestamp:%Y-%m-%d %H:%M:%S}]" ) except (TypeError, KeyError) as e: self.logger.warning(f"No record found in DB: {e!r}") except Exception as e: # pragma: no cover self.logger.error(f"Error checking weather: {e!r}") else: if age > stale: self.logger.warning( "Power record looks stale, marking unsafe.") has_power = False if not has_power: self.logger.critical('AC power not detected.') return has_power
def test_countdown_timer_bad_input(): with pytest.raises(ValueError): assert CountdownTimer('d') with pytest.raises(ValueError): assert CountdownTimer(current_time()) with pytest.raises(AssertionError): assert CountdownTimer(-1)
def __init__(self, cameras=None, scheduler=None, dome=None, mount=None, *args, **kwargs): """Main Observatory class Starts up the observatory. Reads config file, sets up location, dates and weather station. Adds cameras, scheduler, dome and mount. """ super().__init__(*args, **kwargs) self.scheduler = None self.dome = None self.mount = None self.logger.info('Initializing observatory') # Setup information about site location self.logger.info('Setting up location') site_details = create_location_from_config() self.location = site_details['location'] self.earth_location = site_details['earth_location'] self.observer = site_details['observer'] # Do some one-time calculations now = current_time() self._local_sun_pos = self.observer.altaz( now, target=get_sun(now)).alt # Re-calculated self._local_sunrise = self.observer.sun_rise_time(now) self._local_sunset = self.observer.sun_set_time(now) self._evening_astro_time = self.observer.twilight_evening_astronomical( now, which='next') self._morning_astro_time = self.observer.twilight_morning_astronomical( now, which='next') # Set up some of the hardware. self.set_mount(mount) self.cameras = OrderedDict() self._primary_camera = None if cameras: self.logger.info(f'Adding cameras to the observatory: {cameras}') for cam_name, camera in cameras.items(): self.add_camera(cam_name, camera) # TODO(jamessynge): Figure out serial port validation behavior here compared to that for # the mount. self.set_dome(dome) self.set_scheduler(scheduler) self.current_offset_info = None self._image_dir = self.get_config('directories.images') self.logger.success('Observatory initialized')
def status(self): """Get status information for various parts of the observatory.""" status = {'can_observe': self.can_observe} now = current_time() try: if self.mount and self.mount.is_initialized: status['mount'] = self.mount.status current_coords = self.mount.get_current_coordinates() status['mount'][ 'current_ha'] = self.observer.target_hour_angle( now, current_coords) if self.mount.has_target: target_coords = self.mount.get_target_coordinates() status['mount'][ 'mount_target_ha'] = self.observer.target_hour_angle( now, target_coords) except Exception as e: # pragma: no cover self.logger.warning(f"Can't get mount status: {e!r}") try: if self.dome: status['dome'] = self.dome.status except Exception as e: # pragma: no cover self.logger.warning(f"Can't get dome status: {e!r}") try: if self.current_observation: status['observation'] = self.current_observation.status status['observation'][ 'field_ha'] = self.observer.target_hour_angle( now, self.current_observation.field) except Exception as e: # pragma: no cover self.logger.warning(f"Can't get observation status: {e!r}") try: status['observer'] = { 'siderealtime': str(self.sidereal_time), 'utctime': now, 'localtime': datetime.now(), 'local_evening_astro_time': self._evening_astro_time, 'local_morning_astro_time': self._morning_astro_time, 'local_sun_set_time': self._local_sunset, 'local_sun_rise_time': self._local_sunrise, 'local_sun_position': self._local_sun_pos, 'local_moon_alt': self.observer.moon_altaz(now).alt, 'local_moon_illumination': self.observer.moon_illumination(now), 'local_moon_phase': self.observer.moon_phase(now), } except Exception as e: # pragma: no cover self.logger.warning(f"Can't get time status: {e!r}") return status
def _update_status(self): self.logger.debug("Getting mount simulator status") status = dict() status['timestamp'] = current_time() status['tracking_rate_ra'] = self.tracking_rate status['state'] = self.state return status
def get_standard_headers(self, observation=None): """Get a set of standard headers Args: observation (`~pocs.scheduler.observation.Observation`, optional): The observation to use for header values. If None is given, use the `current_observation`. Returns: dict: The standard headers """ if observation is None: observation = self.current_observation assert observation is not None, self.logger.warning( "No observation, can't get headers") field = observation.field self.logger.debug("Getting headers for : {}".format(observation)) t0 = current_time() moon = get_moon(t0, self.observer.location) headers = { 'airmass': self.observer.altaz(t0, field).secz.value, 'creator': "POCSv{}".format(self.__version__), 'elevation': self.location.get('elevation').value, 'ha_mnt': self.observer.target_hour_angle(t0, field).value, 'latitude': self.location.get('latitude').value, 'longitude': self.location.get('longitude').value, 'moon_fraction': self.observer.moon_illumination(t0), 'moon_separation': field.coord.separation(moon).value, 'observer': self.get_config('name', default=''), 'origin': 'Project PANOPTES', 'tracking_rate_ra': self.mount.tracking_rate, } # Add observation metadata headers.update(observation.status) # Explicitly convert EQUINOX to float try: equinox = float(headers['equinox'].replace('J', '')) except BaseException: equinox = 2000. # We assume J2000 headers['equinox'] = equinox return headers
def altaz_to_radec(alt=None, az=None, location=None, obstime=None, **kwargs): """Convert alt/az degrees to RA/Dec SkyCoord. >>> from panoptes.utils import altaz_to_radec >>> from astropy.coordinates import EarthLocation >>> from astropy import units as u >>> keck = EarthLocation.of_site('Keck Observatory') ... >>> altaz_to_radec(alt=75, az=180, location=keck, obstime='2020-02-02T20:20:02.02') <SkyCoord (ICRS): (ra, dec) in deg (281.78..., 4.807...)> >>> # Can use quantities or not. >>> alt = 4500 * u.arcmin >>> az = 180 * u.degree >>> altaz_to_radec(alt=alt, az=az, location=keck, obstime='2020-02-02T20:20:02.02') <SkyCoord (ICRS): (ra, dec) in deg (281.78..., 4.807...)> >>> # Will use current time if none given. >>> altaz_to_radec(alt=35, az=90, location=keck) <SkyCoord (ICRS): (ra, dec) in deg (..., ...)> >>> # Must pass a `location` instance. >>> altaz_to_radec() Traceback (most recent call last): File "<stdin>", line 1, in <module> ... assert location is not None AssertionError Args: alt (astropy.units.Quantity or scalar): Altitude. az (astropy.units.Quantity or scalar): Azimuth. location (astropy.coordinates.EarthLocation, required): A valid location. obstime (None, optional): Time for object, defaults to `current_time` Returns: astropy.coordinates.SkyCoord: Coordinates corresponding to the AltAz. """ assert location is not None if obstime is None: obstime = current_time() alt = get_quantity_value(alt, 'degree') * u.degree az = get_quantity_value(az, 'degree') * u.degree altaz = AltAz(obstime=obstime, location=location, alt=alt, az=az) return SkyCoord(altaz.transform_to(ICRS))
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 _setup_location_for_mount(self): """ Sets the mount up to the current location. Mount must be initialized first. This uses mount.location (an astropy.coords.EarthLocation) to set most of the params and the rest is read from a config file. Users should not call this directly. Includes: * Latitude set_long * Longitude set_lat * Daylight Savings disable_daylight_savings * Universal Time Offset set_gmt_offset * Current Date set_local_date * Current Time set_local_time """ assert self.is_initialized, self.logger.warning( 'Mount has not been initialized') assert self.location is not None, self.logger.warning( 'Please set a location before attempting setup') self.logger.info('Setting up mount for location') # Location # Adjust the lat/long for format expected by iOptron lat = '{:+07.0f}'.format(self.location.lat.to(u.arcsecond).value) lon = '{:+07.0f}'.format(self.location.lon.to(u.arcsecond).value) self.query('set_long', lon) self.query('set_lat', lat) # Time self.query('disable_daylight_savings') gmt_offset = self.get_config('location.gmt_offset', default=0) self.query('set_gmt_offset', gmt_offset) now = current_time() + gmt_offset * u.minute self.query('set_local_time', now.datetime.strftime("%H%M%S")) self.query('set_local_date', now.datetime.strftime("%y%m%d"))
def is_weather_safe(self, stale=180): """Determines whether current weather conditions are safe or not. Args: stale (int, optional): Number of seconds before record is stale, defaults to 180 Returns: bool: Conditions are safe (True) or unsafe (False) """ # Always assume False self.logger.debug("Checking weather safety") if self._in_simulator('weather'): return True # Get current weather readings from database is_safe = False try: record = self.db.get_current('weather') if record is None: return False is_safe = record['data'].get('safe', False) timestamp = record['date'].replace( tzinfo=None) # current_time is timezone naive age = (current_time().datetime - timestamp).total_seconds() self.logger.debug( f"Weather Safety: {is_safe} [{age:.0f} sec old - {timestamp:%Y-%m-%d %H:%M:%S}]" ) except Exception as e: # pragma: no cover self.logger.error(f"No weather record in database: {e!r}") else: if age >= stale: self.logger.warning( "Weather record looks stale, marking unsafe.") is_safe = False return is_safe
def set_park_coordinates(self, ha=-170 * u.degree, dec=-10 * u.degree): """ Calculates the RA-Dec for the the park position. This method returns a location that points the optics of the unit down toward the ground. The RA is calculated from subtracting the desired hourangle from the local sidereal time. This requires a proper location be set. Note: Mounts usually don't like to track or slew below the horizon so this will most likely require a configuration item be set on the mount itself. Args: ha (Optional[astropy.units.degree]): Hourangle of desired parking position. Defaults to -165 degrees. dec (Optional[astropy.units.degree]): Declination of desired parking position. Defaults to -165 degrees. Returns: park_skycoord (astropy.coordinates.SkyCoord): A SkyCoord object representing current parking position. """ self.logger.debug('Setting park position') park_time = current_time() park_time.location = self.location lst = park_time.sidereal_time('apparent') self.logger.debug("LST: {}".format(lst)) self.logger.debug("HA: {}".format(ha)) ra = lst - ha self.logger.debug("RA: {}".format(ra)) self.logger.debug("Dec: {}".format(dec)) self._park_coordinates = SkyCoord(ra, dec) self.logger.debug("Park Coordinates RA-Dec: {}".format( self._park_coordinates))
def do_polar_alignment_test(self, *arg): """Capture images of the pole and compute alignment of mount.""" if self.ready is False: return args, kwargs = string_to_params(*arg) # Default to 30 seconds exptime = kwargs.get('exptime', 30) start_time = current_time(flatten=True) base_dir = '{}/images/drift_align/{}'.format( os.getenv('PANDIR'), start_time) plot_fn = '{}/{}_center_overlay.jpg'.format(base_dir, start_time) mount = self.pocs.observatory.mount print_info("Moving to home position") self.pocs.say("Moving to home position") mount.slew_to_home(blocking=True) # Polar Rotation pole_fn = polar_rotation(self.pocs, exptime=exptime, base_dir=base_dir) pole_fn = pole_fn.replace('.cr2', '.fits') # Mount Rotation rotate_fn = mount_rotation(self.pocs, base_dir=base_dir) rotate_fn = rotate_fn.replace('.cr2', '.fits') print_info("Moving back to home") self.pocs.say("Moving back to home") mount.slew_to_home(blocking=True) print_error(f'NO POLAR UTILS RIGHT NOW') return print_info("Done with polar alignment test") self.pocs.say("Done with polar alignment test")
def create_storage_obj(collection, data, obj_id): """Wrap the data in a dict along with the id and a timestamp.""" return dict(_id=obj_id, data=data, type=collection, date=current_time(datetime=True))
def target_down(location): return altaz_to_radec(obstime=current_time(), location=location, alt=5, az=90)
def _setup_observation(self, observation, headers, filename, **kwargs): headers = headers or None # Move the filterwheel if necessary if self.filterwheel is not None: if observation.filter_name is not None: try: # Move the filterwheel self.logger.debug( f'Moving filterwheel={self.filterwheel} to filter_name=' f'{observation.filter_name}') self.filterwheel.move_to(observation.filter_name, blocking=True) except Exception as e: self.logger.error(f'Error moving filterwheel on {self} to' f' {observation.filter_name}: {e!r}') raise (e) else: self.logger.info( f'Filter {observation.filter_name} requested by' f' observation but {self.filterwheel} is missing that filter, ' f'using' f' {self.filter_type}.') if headers is None: start_time = current_time(flatten=True) else: start_time = headers.get('start_time', current_time(flatten=True)) if not observation.seq_time: self.logger.debug(f'Setting observation seq_time={start_time}') observation.seq_time = start_time # Get the filename self.logger.debug( f'Setting image_dir={observation.directory}/{self.uid}/{observation.seq_time}' ) image_dir = os.path.join(observation.directory, self.uid, observation.seq_time) # Get full file path if filename is None: file_path = os.path.join(image_dir, f'{start_time}.{self.file_extension}') else: # Add extension if '.' not in filename: filename = f'{filename}.{self.file_extension}' # Add directory if '/' not in filename: filename = os.path.join(image_dir, filename) file_path = filename self.logger.debug(f'Setting file_path={file_path}') unit_id = self.get_config('pan_id') # Make the IDs. sequence_id = f'{unit_id}_{self.uid}_{observation.seq_time}' image_id = f'{unit_id}_{self.uid}_{start_time}' self.logger.debug(f"sequence_id={sequence_id} image_id={image_id}") # Make the sequence_id # The exptime header data is set as part of observation but can # be override by passed parameter so update here. exptime = kwargs.get('exptime', observation.exptime.value) # Camera metadata metadata = { 'camera_name': self.name, 'camera_uid': self.uid, 'field_name': observation.field.field_name, 'file_path': file_path, 'filter': self.filter_type, 'image_id': image_id, 'is_primary': self.is_primary, 'sequence_id': sequence_id, 'start_time': start_time, 'exptime': exptime } if observation.filter_name is not None: metadata['filter_request'] = observation.filter_name if headers is not None: self.logger.trace( f'Updating {file_path} metadata with provided headers') metadata.update(headers) self.logger.debug( f'Observation setup: exptime={exptime!r} file_path={file_path!r} image_id={image_id!r} metadata=' f'{metadata!r}') return exptime, file_path, image_id, metadata
def get_observation(self, time=None, show_all=False, reread_fields_file=False): """Get a valid observation Args: time (astropy.time.Time, optional): Time at which scheduler applies, defaults to time called show_all (bool, optional): Return all valid observations along with merit value, defaults to False to only get top value reread_fields_file (bool, optional): If the fields file should be reread before scheduling occurs, defaults to False. Returns: tuple or list: A tuple (or list of tuples) with name and score of ranked observations """ if reread_fields_file: self.logger.debug("Rereading fields file") self.read_field_list() if time is None: time = current_time() valid_obs = {obs: 0.0 for obs in self.observations} best_obs = [] self.set_common_properties(time) for constraint in listify(self.constraints): self.logger.info("Checking Constraint: {}".format(constraint)) for obs_name, observation in self.observations.items(): if obs_name in valid_obs: current_score = valid_obs[obs_name] self.logger.debug( f"\t{obs_name}\tCurrent score: {current_score:.03f}") veto, score = constraint.get_score( time, self.observer, observation, **self.common_properties) self.logger.debug( f"\t\tConstraint Score: {score:.03f}\tVeto: {veto}") if veto: self.logger.debug(f"\t\tVetoed by {constraint}") del valid_obs[obs_name] continue valid_obs[obs_name] += score self.logger.debug( f"\t\tTotal score: {valid_obs[obs_name]:.03f}") self.logger.debug(f'Multiplying final scores by priority') for obs_name, score in valid_obs.items(): priority = self.observations[obs_name].priority new_score = score * priority self.logger.debug( f'{obs_name}: {priority:7.2f} *{score:7.2f} = {new_score:7.2f}' ) valid_obs[obs_name] = new_score if len(valid_obs) > 0: # Sort the list by highest score (reverse puts in correct order) best_obs = sorted(valid_obs.items(), key=lambda x: x[1])[::-1] top_obs_name, top_obs_score = best_obs[0] self.logger.info( f'Best observation: {top_obs_name}\tScore: {top_obs_score:.02f}' ) # Check new best against current_observation if self.current_observation is not None \ and top_obs_name != self.current_observation.name: # Favor the current observation if still available end_of_next_set = time + self.current_observation.set_duration if self.observation_available(self.current_observation, end_of_next_set): # If current is better or equal to top, use it if self.current_observation.merit >= top_obs_score: best_obs.insert(0, (self.current_observation, self.current_observation.merit)) # Set the current self.current_observation = self.observations[top_obs_name] self.current_observation.merit = top_obs_score else: if self.current_observation is not None: # Favor the current observation if still available end_of_next_set = time + self.current_observation.set_duration if end_of_next_set < self.common_properties['end_of_night'] and \ self.observation_available(self.current_observation, end_of_next_set): self.logger.debug("Reusing {}".format( self.current_observation)) best_obs = [(self.current_observation.name, self.current_observation.merit)] else: self.logger.warning("No valid observations found") self.current_observation = None if not show_all and len(best_obs) > 0: best_obs = best_obs[0] return best_obs
def _autofocus(self, seconds, focus_range, focus_step, cutout_size, keep_files, take_dark, merit_function, merit_function_kwargs, mask_dilations, make_plots, coarse, focus_event, *args, **kwargs): """Private helper method for calling autofocus in a Thread. See public `autofocus` for information about the parameters. """ focus_type = 'fine' if coarse: focus_type = 'coarse' initial_focus = self.position self.logger.debug( f"Beginning {focus_type} autofocus of {self._camera} - " f"initial position: {initial_focus}") # Set up paths for temporary focus files, and plots if requested. image_dir = self.get_config('directories.images') start_time = current_time(flatten=True) file_path_root = os.path.join(image_dir, 'focus', self._camera.uid, start_time) self._autofocus_error = None dark_cutout = None if take_dark: dark_path = os.path.join(file_path_root, f'dark.{self._camera.file_extension}') self.logger.debug( f'Taking dark frame {dark_path} on camera {self._camera}') try: dark_cutout = self._camera.get_cutout(seconds, dark_path, cutout_size, keep_file=True, dark=True) # Mask 'saturated' with a low threshold to remove hot pixels dark_cutout = mask_saturated(dark_cutout, threshold=0.3, bit_depth=self.camera.bit_depth) except Exception as err: self.logger.error(f"Error taking dark frame: {err!r}") self._autofocus_error = repr(err) focus_event.set() raise err # Take an image before focusing, grab a cutout from the centre and add it to the plot initial_fn = f"{initial_focus}-{focus_type}-initial.{self._camera.file_extension}" initial_path = os.path.join(file_path_root, initial_fn) try: initial_cutout = self._camera.get_cutout(seconds, initial_path, cutout_size, keep_file=True) initial_cutout = mask_saturated(initial_cutout, bit_depth=self.camera.bit_depth) if dark_cutout is not None: initial_cutout = initial_cutout.astype(np.int32) - dark_cutout except Exception as err: self.logger.error(f"Error taking initial image: {err!r}") self._autofocus_error = repr(err) focus_event.set() raise err # Set up encoder positions for autofocus sweep, truncating at focus travel # limits if required. if coarse: focus_range = focus_range[1] focus_step = focus_step[1] else: focus_range = focus_range[0] focus_step = focus_step[0] # Get focus steps. focus_positions = np.arange( max(initial_focus - focus_range / 2, self.min_position), min(initial_focus + focus_range / 2, self.max_position) + 1, focus_step, dtype=np.int) n_positions = len(focus_positions) # Set up empty array holders cutouts = np.zeros((n_positions, cutout_size, cutout_size), dtype=initial_cutout.dtype) masks = np.empty((n_positions, cutout_size, cutout_size), dtype=np.bool) metrics = np.empty(n_positions) # Take and store an exposure for each focus position. for i, position in enumerate(focus_positions): # Move focus, updating focus_positions with actual encoder position after move. focus_positions[i] = self.move_to(position) focus_fn = f"{focus_positions[i]}-{i:02d}.{self._camera.file_extension}" file_path = os.path.join(file_path_root, focus_fn) # Take exposure. try: cutouts[i] = self._camera.get_cutout(seconds, file_path, cutout_size, keep_file=keep_files) except Exception as err: self.logger.error(f"Error taking image {i + 1}: {err!r}") self._autofocus_error = repr(err) focus_event.set() raise err masks[i] = mask_saturated(cutouts[i], bit_depth=self.camera.bit_depth).mask self.logger.debug( f'Making master mask with binary dilation for {self._camera}') master_mask = masks.any(axis=0) master_mask = binary_dilation(master_mask, iterations=mask_dilations) # Apply the master mask and then get metrics for each frame. for i, cutout in enumerate(cutouts): self.logger.debug(f'Applying focus metric to cutout {i:02d}') if dark_cutout is not None: cutout = cutout.astype(np.float32) - dark_cutout cutout = np.ma.array(cutout, mask=np.ma.mask_or(master_mask, np.ma.getmask(cutout))) metrics[i] = focus_utils.focus_metric(cutout, merit_function, **merit_function_kwargs) self.logger.debug(f'Focus metric for cutout {i:02d}: {metrics[i]}') # Only fit a fine focus. fitted = False fitting_indices = [None, None] # Find maximum metric values. imax = metrics.argmax() if imax == 0 or imax == (n_positions - 1): # TODO: have this automatically switch to coarse focus mode if this happens self.logger.warning( f"Best focus outside sweep range, stopping focus and using" f" {focus_positions[imax]}") best_focus = focus_positions[imax] elif not coarse: # Fit data around the maximum value to determine best focus position. # Initialise models shift = models.Shift(offset=-focus_positions[imax]) # Small initial coeffs with expected sign. Helps fitting start in the right direction. poly = models.Polynomial1D(degree=4, c0=1, c1=0, c2=-1e-2, c3=0, c4=-1e-4, fixed={ 'c0': True, 'c1': True, 'c3': True }) scale = models.Scale(factor=metrics[imax]) # https://docs.astropy.org/en/stable/modeling/compound-models.html?#model-composition reparameterised_polynomial = shift | poly | scale # Initialise fitter fitter = fitting.LevMarLSQFitter() # Select data range for fitting. Tries to use 2 points either side of max, if in range. fitting_indices = (max(imax - 2, 0), min(imax + 2, n_positions - 1)) # Fit models to data fit = fitter( reparameterised_polynomial, focus_positions[fitting_indices[0]:fitting_indices[1] + 1], metrics[fitting_indices[0]:fitting_indices[1] + 1]) # Get the encoder position of the best focus. best_focus = np.abs(fit.offset_0) fitted = True # Guard against fitting failures, force best focus to stay within sweep range. min_focus = focus_positions[0] max_focus = focus_positions[-1] if best_focus < min_focus: self.logger.warning( f"Fitting failure: best focus {best_focus} below sweep limit" f" {min_focus}") best_focus = focus_positions[1] if best_focus > max_focus: self.logger.warning( f"Fitting failure: best focus {best_focus} above sweep limit" f" {max_focus}") best_focus = focus_positions[-2] else: # Coarse focus, just use max value. best_focus = focus_positions[imax] # Move the focuser to best focus position. final_focus = self.move_to(best_focus) # Get final cutout. final_fn = f"{final_focus}-{focus_type}-final.{self._camera.file_extension}" file_path = os.path.join(file_path_root, final_fn) try: final_cutout = self._camera.get_cutout(seconds, file_path, cutout_size, keep_file=True) final_cutout = mask_saturated(final_cutout, bit_depth=self.camera.bit_depth) if dark_cutout is not None: final_cutout = final_cutout.astype(np.int32) - dark_cutout except Exception as err: self.logger.error(f"Error taking final image: {err!r}") self._autofocus_error = repr(err) focus_event.set() raise err if make_plots: line_fit = None if fitted: focus_range = np.arange( focus_positions[fitting_indices[0]], focus_positions[fitting_indices[1]] + 1) fit_line = fit(focus_range) line_fit = [focus_range, fit_line] plot_title = f'{self._camera} {focus_type} focus at {start_time}' # Make the plots plot_path = os.path.join(file_path_root, f'{focus_type}-focus.png') plot_path = make_autofocus_plot(plot_path, initial_cutout, final_cutout, initial_focus, final_focus, focus_positions, metrics, merit_function, plot_title=plot_title, line_fit=line_fit) self.logger.info( f"{focus_type.capitalize()} focus plot for {self._camera} written to " f" {plot_path}") self.logger.debug(f"Autofocus of {self._camera} complete - final focus" f" position: {final_focus}") if focus_event: focus_event.set() return initial_focus, final_focus
def _make_pretty_from_fits(fname=None, title=None, figsize=(10, 10 / 1.325), dpi=150, alpha=0.2, number_ticks=7, clip_percent=99.9, **kwargs): data = mask_saturated(fits_utils.getdata(fname)) header = fits_utils.getheader(fname) wcs = WCS(header) if not title: field = header.get('FIELD', 'Unknown field') exptime = header.get('EXPTIME', 'Unknown exptime') filter_type = header.get('FILTER', 'Unknown filter') try: date_time = header['DATE-OBS'] except KeyError: # If we don't have DATE-OBS, check filename for date try: basename = os.path.splitext(os.path.basename(fname))[0] date_time = date_parse(basename).isoformat() except Exception: # pragma: no cover # Otherwise use now date_time = current_time(pretty=True) date_time = date_time.replace('T', ' ', 1) title = f'{field} ({exptime}s {filter_type}) {date_time}' norm = ImageNormalize(interval=PercentileInterval(clip_percent), stretch=LogStretch()) fig = Figure() FigureCanvas(fig) fig.set_size_inches(*figsize) fig.dpi = dpi if wcs.is_celestial: ax = fig.add_subplot(1, 1, 1, projection=wcs) ax.coords.grid(True, color='white', ls='-', alpha=alpha) ra_axis = ax.coords['ra'] ra_axis.set_axislabel('Right Ascension') ra_axis.set_major_formatter('hh:mm') ra_axis.set_ticks(number=number_ticks, color='white', exclude_overlapping=True) dec_axis = ax.coords['dec'] dec_axis.set_axislabel('Declination') dec_axis.set_major_formatter('dd:mm') dec_axis.set_ticks(number=number_ticks, color='white', exclude_overlapping=True) else: ax = fig.add_subplot(111) ax.grid(True, color='white', ls='-', alpha=alpha) ax.set_xlabel('X / pixels') ax.set_ylabel('Y / pixels') im = ax.imshow(data, norm=norm, cmap=get_palette(), origin='lower') add_colorbar(im) fig.suptitle(title) new_filename = re.sub(r'.fits(.fz)?', '.jpg', fname) fig.savefig(new_filename, bbox_inches='tight') # explicitly close and delete figure fig.clf() del fig return new_filename
def sidereal_time(self): return self.observer.local_sidereal_time(current_time())