Exemple #1
0
    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')
Exemple #2
0
    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
Exemple #3
0
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
    }
Exemple #5
0
    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
Exemple #6
0
    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
Exemple #7
0
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))
Exemple #8
0
    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
Exemple #9
0
    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
Exemple #10
0
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)
Exemple #11
0
    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')
Exemple #12
0
    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
Exemple #13
0
    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
Exemple #14
0
    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
Exemple #15
0
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))
Exemple #16
0
    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)
Exemple #17
0
    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"))
Exemple #18
0
    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
Exemple #19
0
    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))
Exemple #20
0
    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")
Exemple #21
0
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))
Exemple #22
0
def target_down(location):
    return altaz_to_radec(obstime=current_time(), location=location, alt=5, az=90)
Exemple #23
0
    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
Exemple #24
0
    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
Exemple #25
0
    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
Exemple #26
0
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
Exemple #27
0
 def sidereal_time(self):
     return self.observer.local_sidereal_time(current_time())