Ejemplo n.º 1
0
class TestNGC2997FromCPT(object):
    '''Integration test: rise/set/transit of a Southern star with telescope horizon.
       Should rise and set, *not* be circumpolar. This test asserts #5969.'''
    def setup(self):

        self.ngc2997 = {
            'ra': RightAscension(degrees=146.4116375),
            'dec': Declination(degrees=-31.19108888)
        }

        self.cpt = {
            'latitude': Angle(degrees=-32.3805542),
            'longitude': Angle(degrees=20.8101815),
        }

        self.date = datetime(2013, 3, 26)

        self.horizon = Angle(degrees=30)

        self.star = Star(self.cpt['latitude'], self.ngc2997,
                         self.horizon.in_degrees())

    def test_not_circumpolar(self):
        assert (not self.star.is_always_up(self.date))
        assert (not self.star.is_always_down(self.date))
Ejemplo n.º 2
0
    def test_apparent_position(self):
        dt_tdb = gregorian_to_ut_mjd(datetime(2012, 1, 3))

        assert_equal(dt_tdb, 55929.0)

        (apparent_ra, apparent_dec,
         diameter) = apparent_planet_pos("moon", dt_tdb, self.site)

        # values from JPL Horizons
        expected_ra = Angle(degrees=26.63848)
        expected_dec = Angle(degrees=15.61778)

        assert_less(abs(apparent_ra.in_degrees() - expected_ra.in_degrees()),
                    self.tolerance)
        assert_less(abs(apparent_dec.in_degrees() - expected_dec.in_degrees()),
                    self.tolerance)
Ejemplo n.º 3
0
def apply_refraction_to_horizon(horizon):
    '''If using a horizon of zero, adds an average refraction term to improve
       the effective horizon. If no horizon is provided, a horizon of zero is
       assumed.

       Non-zero horizons fall through and are not modified.
    '''

    # For non-zero horizons, we shall ignore refraction effects
    std_alt_of_stars = Angle(degrees=0.0)

    if not horizon:
        # Default to the Earth's horizon
        horizon = Angle(degrees=0.0)

    # Approximate the effect of refraction if we are using the true horizon
    if horizon.in_degrees() == 0.0:
        std_alt_of_stars = Angle(degrees=-0.5667)

    effective_horizon_in_deg = std_alt_of_stars.in_degrees(
    ) + horizon.in_degrees()
    effective_horizon = Angle(degrees=effective_horizon_in_deg)

    return effective_horizon
Ejemplo n.º 4
0
def refine_day_fraction(app_sidereal_time, m_0, m_1, m_2, tdb, target, site,
                        std_altitude):
    '''Take an approximate value for transit, rise and set, and interpolate
       across the date boundary to obtain corrections to the values. The
       refined times are accurate to the nearest minute.
    '''

    # Find the sidereal time at Greenwich (in degrees)
    sidereal_time_transit = sidereal_time_at_greenwich(app_sidereal_time, m_0)
    sidereal_time_rise = sidereal_time_at_greenwich(app_sidereal_time, m_1)
    sidereal_time_set = sidereal_time_at_greenwich(app_sidereal_time, m_2)

    _log.debug('gwich sidereal_time (rise): %s', sidereal_time_rise)
    _log.debug('gwich sidereal_time (transit): %s', sidereal_time_transit)
    _log.debug('gwich sidereal_time (set): %s', sidereal_time_set)

    _log.debug('m_0: %s', m_0)
    _log.debug('m_1: %s', m_1)
    _log.debug('m_2: %s', m_2)

    # Calculate 'n' as per book instructions
    n_0 = calc_tabular_interval(m_0, tdb)
    n_1 = calc_tabular_interval(m_1, tdb)
    n_2 = calc_tabular_interval(m_2, tdb)

    # Calculate RA/Dec over 3 days for interpolation
    if ('planet' in target):
        (alpha_1, delta_1,
         diameter_1) = apparent_planet_pos(target['planet'], tdb - 1, site)
        (alpha_2, delta_2,
         diameter_2) = apparent_planet_pos(target['planet'], tdb, site)
        (alpha_3, delta_3,
         diameter_3) = apparent_planet_pos(target['planet'], tdb + 1, site)
    else:
        (alpha_1, delta_1) = mean_to_apparent(target, tdb - 1)
        (alpha_2, delta_2) = mean_to_apparent(target, tdb)
        (alpha_3, delta_3) = mean_to_apparent(target, tdb + 1)

    _log.debug('alpha_1 (yesterday): %s', alpha_1.in_degrees())
    _log.debug('alpha_2 (today): %s', alpha_2.in_degrees())
    _log.debug('alpha_3 (tomorrow): %s', alpha_3.in_degrees())

    # Handle wrapping across 24 hr boundary
    # Why do we use 350 degrees? That's a very good question.
    # We need to determine when the alpha has wrapped, but we can't do
    # the obvious test, because of valid cases.
    # Instead, we use the fact that the differences between the alphas
    # need to be large, otherwise the object is moving extremely fast,
    # to determine the wrapping scenario.
    if alpha_2.in_degrees() < alpha_1.in_degrees():
        if alpha_2.in_degrees() - alpha_1.in_degrees() < -350:
            norm_alpha2 = alpha_2.in_degrees() + 360
            alpha_2 = Angle(degrees=norm_alpha2)

    if alpha_3.in_degrees() < alpha_1.in_degrees():
        if alpha_3.in_degrees() - alpha_1.in_degrees() < -350:
            norm_alpha3 = alpha_3.in_degrees() + 360
            alpha_3 = Angle(degrees=norm_alpha3)

    _log.debug('alpha_1 normalised (yesterday): %s', alpha_1.in_degrees())
    _log.debug('alpha_2 normalised (today): %s', alpha_2.in_degrees())
    _log.debug('alpha_3 normalised (tomorrow): %s', alpha_3.in_degrees())

    # Construct the first and second differences
    a = alpha_2.in_degrees() - alpha_1.in_degrees()
    b = alpha_3.in_degrees() - alpha_2.in_degrees()
    c = b - a

    interp_alpha_2_transit = interpolate(alpha_2.in_degrees(), n_0, a, b, c)
    interp_alpha_2_rise = interpolate(alpha_2.in_degrees(), n_1, a, b, c)
    interp_alpha_2_set = interpolate(alpha_2.in_degrees(), n_2, a, b, c)

    # Construct the first and second differences
    a = delta_2.in_degrees() - delta_1.in_degrees()
    b = delta_3.in_degrees() - delta_2.in_degrees()
    c = b - a

    interp_delta_2_rise = interpolate(delta_2.in_degrees(), n_1, a, b, c)
    interp_delta_2_set = interpolate(delta_2.in_degrees(), n_2, a, b, c)

    # Calculate the local hour angle (in degrees)
    local_hour_angle_transit = (sidereal_time_transit +
                                site['longitude'].in_degrees() -
                                interp_alpha_2_transit)

    local_hour_angle_rise = (sidereal_time_rise +
                             site['longitude'].in_degrees() -
                             interp_alpha_2_rise)

    local_hour_angle_set = (sidereal_time_set +
                            site['longitude'].in_degrees() -
                            interp_alpha_2_set)

    while local_hour_angle_transit > 180:
        local_hour_angle_transit -= 360.0
    while local_hour_angle_rise > 180:
        local_hour_angle_rise -= 360.0
    while local_hour_angle_set > 180:
        local_hour_angle_set -= 360.0

    _log.debug('local_hour_angle_rise: %s', local_hour_angle_rise)
    _log.debug('local_hour_angle_transit: %s', local_hour_angle_transit)
    _log.debug('local_hour_angle_set: %s', local_hour_angle_set)

    refined_m_0 = correct_transit(m_0, local_hour_angle_transit)

    refined_m_1 = correct_rise_set(m_1, site['latitude'].in_degrees(),
                                   interp_delta_2_rise, local_hour_angle_rise,
                                   std_altitude)

    refined_m_2 = correct_rise_set(m_2, site['latitude'].in_degrees(),
                                   interp_delta_2_set, local_hour_angle_set,
                                   std_altitude)

    refined_m_0 = normalise_day(refined_m_0)
    refined_m_1 = normalise_day(refined_m_1)
    refined_m_2 = normalise_day(refined_m_2)

    return (refined_m_0, refined_m_1, refined_m_2)
Ejemplo n.º 5
0
class Visibility(object):
    """The Visibility class is used to calculate target visibilities for a given site.

    The Visibility class is instantiated with a given site and time range. 
    It can be used repeatedly with different targets to get those targets 
    observable intervals at the given site. It takes ha_limits, horizon limits, 
    airmass limits, moon angular distance limits, and zenith blind spot limits 
    into account when computing over all observable intervals for a target.

    Args:
        site (dict): Dictionary of site properties. Should contain Angles for latitude and longitude
        start_date (datetime): Start datetime over which you want to calculate intervals
        end_date (datetime): End datetime over which you want to calculate intervals
        horizon (float): Horizon angle in degrees for the site
        twilight (str): Type of twilight to use for rise/set calculation. Can be one of `sunrise`, `sunset`, `civil`, `nautical`, `astronomical`
        ha_limit_neg (float): The hour angle negative limit for the telescope
        ha_limit_pos (float): The hour angle positive limit ror the telescope
        zenith_blind_spot (float): blind spot angle in degrees over which the telescope cannot observe
    Raises:
        rise_set.exceptions.InvalidHourAngleLimit: If the positive or negative hour angles provided are out of the possible range.
    """
    def __init__(self,
                 site,
                 start_date,
                 end_date,
                 horizon=0,
                 twilight='sunrise',
                 ha_limit_neg=-4.9,
                 ha_limit_pos=4.9,
                 zenith_blind_spot=0):
        self.site = site
        self.start_date = start_date
        self.end_date = end_date
        self.horizon = Angle(degrees=horizon)
        self.twilight = twilight
        self.zenith_blind_spot = Angle(degrees=zenith_blind_spot)

        if ha_limit_pos > 12.0 or ha_limit_pos < 0.0:
            msg = "Positive hour angle limit must fall between 0 and 12 hours"
            raise InvalidHourAngleLimit(msg)

        if ha_limit_neg < -12.0 or ha_limit_neg > 0.0:
            msg = "Negative hour angle limit must fall between -12 and 0 hours"
            raise InvalidHourAngleLimit(msg)

        self.ha_limit_neg = ha_limit_neg
        self.ha_limit_pos = ha_limit_pos

        self.dark_intervals = []
        self.moon_dark_intervals = []

    def get_dark_intervals(self):
        """Returns the dark intervals for the site.
        
        Returns the night time dark intervals for the given site and date range set
        in this visibility object.

        Returns:
            list: A list of tuples of start/end datetime pairs that make up the dark intervals for this site.
        """
        # Don't compute this again if we've already done it
        if self.dark_intervals:
            return self.dark_intervals

        target = 'sun'

        self.dark_intervals = self.get_target_intervals(target, up=False)

        return self.dark_intervals

    def get_moon_dark_intervals(self):
        """Returns the dark moon intervals for the site.
        
        Returns the dark moon intervals (moon is not visible) for the given
        site and date range set in this visibility object.

        Returns:
            list: A list of tuples of start/end datetime pairs that make up the dark moon intervals for this site.
        """
        # Don't compute this again if we've already done it
        if self.moon_dark_intervals:
            return self.moon_dark_intervals

        target = 'moon'

        self.moon_dark_intervals = self.get_target_intervals(target, up=False)

        return self.moon_dark_intervals

    def _add_moon_interval(self,
                           time,
                           target_app_ra,
                           target_app_dec,
                           constraint=Angle(degrees=30)):
        moon_app_ra, moon_app_dec, diameter = apparent_planet_pos(
            'moon', time['tdb'], self.site)
        # call slalib to get the angular moon distance
        target_moon_dist = angular_distance_between(target_app_ra,
                                                    target_app_dec,
                                                    moon_app_ra, moon_app_dec)
        # if that moon distance is > the constraint, add this interval to final intervals
        return target_moon_dist.in_degrees() >= constraint.in_degrees()

    def _add_zenith_interval(self,
                             time,
                             target_app_ra,
                             target_app_dec,
                             constraint=Angle(degrees=0)):
        ha = calc_local_hour_angle(target_app_ra, self.site['longitude'],
                                   time['time'])
        target_zenith_dist = calculate_zenith_distance(self.site['latitude'],
                                                       target_app_dec, ha)
        # if that zenith distance is > the constraint, add this interval to final intervals
        return target_zenith_dist.in_degrees() >= constraint.in_degrees()

    def _get_chunked_intervals(self,
                               target,
                               target_intervals,
                               compare_func,
                               constraint,
                               chunksize=datetime.timedelta(minutes=30)):
        '''Returns a set of datetime 2-tuples, each of which represents an interval
           of time that the target is greater than the constraint away from the thing-to-be-avoided.
           The supplied compare_func calculates the distance to it's specific obstacle (moon, zenith).
        '''
        intervals = []

        for start, end in target_intervals:
            chunkstart = start
            chunkend = min(chunkstart + chunksize, end)
            while chunkstart != chunkend and chunkend <= end:
                # get the tdb date of the start time of the interval
                tdb = date_to_tdb(chunkstart)
                # get the apparent ra/dec for the target, and for the moon at this timestamp
                if is_sidereal_target(target):
                    target_app_ra, target_app_dec = mean_to_apparent(
                        target, tdb)
                else:
                    target_app_ra, target_app_dec = elem_to_topocentric_apparent(
                        chunkstart, target, self.site, target_to_jform(target))

                if compare_func({
                        'time': chunkstart,
                        'tdb': tdb
                }, target_app_ra, target_app_dec, constraint):
                    intervals.append((chunkstart, chunkend))

                # increment the chunkstart/end up
                chunkstart = chunkend
                chunkend = min(chunkstart + chunksize, end)

        intervals = coalesce_adjacent_intervals(intervals)
        return intervals

    def get_moon_distance_intervals(self,
                                    target,
                                    target_intervals,
                                    moon_distance=Angle(degrees=30),
                                    chunksize=datetime.timedelta(minutes=30)):
        """Returns the moon distance intervals for the given target.
        
        Returns the intervals for which the given target is greater than the angular moon distance
        away from the moon at the given site and date range set in this visibility object.

        Args:
            target (dict): A dictionary of target details in the rise-set library format
            target_intervals (list): A list of datetime tuples that represent the above horizon intervals for the target. Returned by get_target_intervals()
            moon_distance (Angle): The minimum angular moon distance that the target must be away from the moon
            chunksize (timedelta): The time delta over which to calculate if the target intervals are out of range of the zenith.
        Returns:
            list: A list of tuples of start/end datetime pairs that make up the intervals over which this target is greater than angular moon distance away from moon.
        """
        return self._get_chunked_intervals(target, target_intervals,
                                           self._add_moon_interval,
                                           moon_distance, chunksize)

    def get_zenith_distance_intervals(self,
                                      target,
                                      target_intervals,
                                      chunksize=datetime.timedelta(minutes=1)):
        """Returns the zenith distance intervals for the given target.
        
        Returns the intervals for which the given target is greater than zenith distance away from the zenith at the given site 
        and date range set in this visibility object.

        Args:
            target (dict): A dictionary of target details in the rise-set library format
            target_intervals (list): A list of datetime tuples that represent the above horizon intervals for the target.
                                     Returned by get_target_intervals()
            chunksize (timedelta): The time delta over which to calculate if the target intervals are out of range of 
                                   the zenith.
        Returns:
            list: A list of tuples of start/end datetime pairs that make up the intervals over which this target is greater than zenith distance away from zenith.
        """
        return self._get_chunked_intervals(target, target_intervals,
                                           self._add_zenith_interval,
                                           self.zenith_blind_spot, chunksize)

    def get_target_intervals(self, target, up=True, airmass=None):
        """Returns the above or below horizon intervals for the given target.
        
        Returns the above (up=True) or below (up=False) horizon intervals for the given target and given site and date range set in this visibility object.

        Args:
            target (dict): A dictionary of target details in the rise-set library format
            airmass (float): The maximum acceptable airmass for this target to be observable in
            up (boolean): True (default) if you want intervals above the horizon, False for below
        Returns:
            list: A list of tuples of start/end datetime pairs that make up the intervals over which this target is above/below the horizon.
        Raises:
            rise_set.exceptions.RiseSetError: If there was a problem calculating the rise/set/transfer times of the target
        """
        effective_horizon = set_airmass_limit(airmass,
                                              self.horizon.in_degrees())

        # Return dark intervals for static targets
        if is_static_target(target):
            intervals = self.get_dark_intervals()
        # Handle moving objects differently from stars
        elif is_moving_object(target):
            intervals = self._get_moving_object_target_intervals(
                target, effective_horizon)
        # The target has an RA/Dec
        else:
            intervals = self._get_ra_target_intervals(target, up, airmass,
                                                      effective_horizon)

        return intervals

    def _get_moving_object_target_intervals(self, target, effective_horizon):
        window = {
            'start': self.start_date,
            'end': self.end_date,
        }
        site = self.site.copy()
        site['horizon'] = Angle(degrees=effective_horizon)
        intervals, _ = find_moving_object_up_intervals(window, target, site)

        return intervals

    def _get_ra_target_intervals(self, target, up, airmass, effective_horizon):
        star = Star(self.site['latitude'], target, effective_horizon)

        if up:
            day_interval_func = self._find_when_target_is_up
        else:
            day_interval_func = self._find_when_target_is_down

        # Find rise/set/transit for each day
        intervals = []
        current_date = self.start_date
        while current_date < self.end_date + ONE_DAY:
            one_day_intervals = day_interval_func(target, current_date, star,
                                                  airmass)

            # Add today's intervals to the accumulating list of intervals
            intervals.extend(one_day_intervals)

            # Move on to tomorrow
            current_date += ONE_DAY

        # Collapse adjacent intervals into continuous larger intervals
        intervals = coalesce_adjacent_intervals(intervals)
        intervals = intersect_intervals(intervals,
                                        [(self.start_date, self.end_date)])

        return intervals

    def get_ha_intervals(self, target):
        """Returns the hour angle intervals for the given target.

        Returns the hour anle intervals for the given target and given site and date range set in this visibility object.
        The hour angle intervals are uninterupted chunks of time that the target is within the hour angle limits of the
        telescope.

        Args:
            target (dict): A dictionary of target details in the rise-set library format
        Returns:
            list: A list of tuples of start/end datetime pairs that make up the intervals over which this target is within HA limits.
        """
        SIDEREAL_SOLAR_DAY_RATIO = 1.002737909350
        SIDEREAL_SOLAR_DAY = datetime.timedelta(
            seconds=(ONE_DAY.total_seconds() / SIDEREAL_SOLAR_DAY_RATIO))

        earliest_date = self.start_date - SIDEREAL_SOLAR_DAY

        tdb = date_to_tdb(earliest_date)
        mjd = gregorian_to_ut_mjd(earliest_date)
        gmst = ut_mjd_to_gmst(mjd)

        # Need the apparent ra/dec for getting correct ha limits on high dec targets
        apparent_ra, apparent_dec = mean_to_apparent(target, tdb)

        # Flip the neg/pos ha limits if site is in the southern hemisphere
        ha_neg = self.ha_limit_neg
        ha_pos = self.ha_limit_pos
        if self.site['latitude'].in_degrees() < 0:
            ha_neg = -self.ha_limit_pos
            ha_pos = -self.ha_limit_neg

        # the rise time
        hour_rise = ha_neg + apparent_ra.in_hours() - \
            self.site['longitude'].in_hours() - gmst.in_hours()
        hour_rise /= SIDEREAL_SOLAR_DAY_RATIO

        # the set time
        hour_set  = ha_pos + apparent_ra.in_hours() - \
            self.site['longitude'].in_hours() - gmst.in_hours()
        hour_set /= SIDEREAL_SOLAR_DAY_RATIO

        current_rise = earliest_date + datetime.timedelta(hours=hour_rise)
        current_set = earliest_date + datetime.timedelta(hours=hour_set)

        # Find hour angle limits for each day
        intervals = []
        while current_set < (self.end_date + SIDEREAL_SOLAR_DAY):
            intervals.append((current_rise, current_set))
            current_rise += SIDEREAL_SOLAR_DAY
            current_set += SIDEREAL_SOLAR_DAY

        # do not exceed start/end dates
        intervals = coalesce_adjacent_intervals(intervals)
        intervals = intersect_intervals(intervals,
                                        [(self.start_date, self.end_date)])

        return intervals

    def get_observable_intervals(self,
                                 target,
                                 airmass=None,
                                 moon_distance=Angle(degrees=30)):
        """Returns the observable intervals for the given target.
        
        Returns the observable intervals for the given target and given site and date range set in this visibility object.
        The observable intervals are the intersections of the dark intervals at the site, the above horizon target intervals,
        the moon distance intervals of the target, the hour angle intervals of the target, and the zenith blind spot intervals
        of the target.

        Args:
            target (dict): A dictionary of target details in the rise-set library format
            airmass (float): The maximum acceptable airmass for this target to be observable in
            moon_distance (Angle): The minimum acceptable angular distance between the moon and the target
        Returns:
            list: A list of tuples of start/end datetime pairs that make up the intervals over which this target is observable.
        Raises:
            rise_set.exceptions.RiseSetError: If there was a problem calculating the rise/set/transfer times of the target
        """
        # get the intervals of each separately
        dark = self.get_dark_intervals()
        above_horizon = self.get_target_intervals(target, airmass=airmass)

        if moon_distance.in_degrees() <= 0.5 or is_static_target(target):
            moon_avoidance = above_horizon
        else:
            moon_avoidance = self.get_moon_distance_intervals(
                target, above_horizon, moon_distance)
        if is_sidereal_target(target):
            within_hour_angle = self.get_ha_intervals(target)
        else:
            # if the target type is such that there is no 'ra'/'dec' values, then we cannot calculate the ha intervals,
            # so we just use the target intervals instead (since they are all intersected together next). This is true
            # for moving objects and static objects.
            within_hour_angle = above_horizon

        if self.zenith_blind_spot.in_degrees() <= 0.0 or is_static_target(
                target):
            zenith_hole_avoidance = above_horizon
        else:
            zenith_hole_avoidance = self.get_zenith_distance_intervals(
                target, above_horizon)

        # find the overlapping intervals between them
        intervals = intersect_many_intervals(dark, above_horizon,
                                             within_hour_angle, moon_avoidance,
                                             zenith_hole_avoidance)

        return intervals

    def _find_when_target_is_down(self, target, dt, star=None, airmass=None):
        '''Returns a single datetime 2-tuple, representing an interval
           of uninterrupted time below the horizon at the specified site, for the
           requested date.

           Note: Even though this function currently ignores times, the dt object
           must be a datetime, *not* a date.
        '''

        # Ensure we only deal with dates, because our rise/set/transit tuple is
        # day specific.
        # TODO: Extend to arbitrary start/end times
        dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)

        # We will calculate down intervals as the inverse of the up intervals
        _log.debug("dt: %s", dt)
        up_intervals = self._find_when_target_is_up(target, dt, star, airmass)

        if not up_intervals:
            _log.warn("Got no up intervals!")
            _log.warn("dt was: %s", dt)
            _log.warn("target was: %s", target)

        down_intervals = []

        # If the first value has time 00:00:00, then the target starts up
        if up_intervals[0][0].time() == MIDNIGHT:
            pass

        # Otherwise the target starts down - so there's one extra interval at start
        else:
            down_start = dt
            down_end = up_intervals[0][0]

            down_intervals.append((down_start, down_end))

        # Proceed through the intervals, extracting the gaps
        for i in range(len(up_intervals) - 1):
            down_start = up_intervals[i][1]
            down_end = up_intervals[i + 1][0]

            down_intervals.append((down_start, down_end))

        # If the target sets before the end of the day, grab that as an
        # extra down interval
        if up_intervals[-1][1].time() != MIDNIGHT:
            down_start = up_intervals[-1][1]
            down_end = dt + ONE_DAY

            down_intervals.append((down_start, down_end))

        return down_intervals

    def _find_when_target_is_up(self, target, dt, star=None, airmass=None):
        '''Returns a single datetime 2-tuple, representing an interval
           of uninterrupted time above the horizon at the specified
           site, for the requested date.

           Note: Even though this function currently ignores times, the dt
           object must be a datetime, *not* a date.

           TODO: Clean up this unpleasant interface - no star need be passed if
           the sun is the target, but one *must* be passed otherwise. This isn't
           obvious from the method signature.
        '''
        effective_horizon = set_airmass_limit(airmass,
                                              self.horizon.in_degrees())

        # Remove any time component of the provided datetime object
        dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)

        # Get the rise/set/transit times for the target, for this day
        intervals = []
        # TODO: Catch the RiseSetError for circumpolar stars
        # TODO: Return either a complete or empty interval, as appropriate
        # TODO: This requires either introspecting state in the error, or
        # TODO: calling an is_circumpolar method before this calculation

        if target == 'sun':
            transits, rises, sets = calc_sunrise_set(self.site, dt,
                                                     self.twilight)
        elif target == 'moon':
            transits, rises, sets = calc_planet_rise_set(
                self.site, dt, MOON_REFRACTION, 'moon')
        else:
            # Test for circumpolarity
            if star.is_always_up(dt):
                # Return a full interval over the entire day
                intervals.append((dt, dt + ONE_DAY))
                return intervals

            # Catch target never rising
            try:
                effective_horizon_angle = Angle(degrees=effective_horizon)
                transits, rises, sets = calc_rise_set(target, self.site, dt,
                                                      effective_horizon_angle)
            except RiseSetError:
                return intervals

        _log.debug("latitude: %s", self.site['latitude'].in_degrees())
        _log.debug("longitude: %s", self.site['longitude'].in_degrees())
        _log.debug("twilight: %s", self.twilight)
        _log.debug("dt: %s", dt)
        _log.debug("rise: %s (%s)", rises, dt + rises)
        _log.debug("transit: %s (%s)", transits, dt + transits)
        _log.debug("set: %s (%s)", sets, dt + sets)

        #        import ipdb; ipdb.set_trace()

        # Case 1: Overlapping start of day boundary
        # Target rose yesterday, and sets today. Rises again later today.
        #         |       x                                     |
        #         |    x     x                                  |
        #         | x           x                               | x
        #        x|                x                           x|
        #     x   |                   x                     x   |
        #   Rise  0hr  Transit       Set                  Rise 24hr
        if (rises > transits) and (sets > transits):

            # Store the first interval - start of day until target set
            absolute_set = sets + dt
            intervals.append((dt, absolute_set))

            # Store the second interval - target rise until end of day
            absolute_rise = rises + dt
            intervals.append((absolute_rise, dt + ONE_DAY))

        # Case 2: Rise, set and transit all fall within the day, in order
        # Target rises today, transits, and sets before the day ends
        #         |                      x                     |
        #         |                   x     x                  |
        #         |                x           x               |
        #         |             x                 x            |
        #         |          x                       x         |
        #         |       x                             x      |
        #         0hr   Rise           Transit          Set   24hr
        elif (rises < transits) and (sets > transits):
            # Only one interval - rise until target set
            absolute_rise = rises + dt
            absolute_set = sets + dt
            intervals.append((absolute_rise, absolute_set))

        # Case 3: Overlapping end of day boundary
        # Target rose yesterday, and sets today. Rises again later today.
        #                 x    |                                        x    |
        #              x     x |                                     x     x |
        #           x          |x                                 x          |x
        #        x             |   x                           x             |   x
        #     x                |      x                     x                |
        #   Rise       Transit 0hr   Set                  Rise      Transit 24hr
        elif (rises < transits) and (sets < transits):
            # Same code as case 1!
            # Store the first interval - start of day until target set
            absolute_set = sets + dt
            intervals.append((dt, absolute_set))

            # Store the second interval - target rise until end of day
            absolute_rise = rises + dt
            intervals.append((absolute_rise, dt + ONE_DAY))

        return intervals

    def __repr__(self):
        repr_dict = copy.deepcopy(self.__dict__)
        del (repr_dict['dark_intervals'])

        sorted_str_dict = "{" + ", ".join(
            "%s: %s" % (key, self.__dict__[key])
            for key in sorted(self.__dict__)) + "}"
        return "Visibility (%s)" % sorted_str_dict

    def __key(self):
        return (self.site, self.start_date, self.end_date, self.horizon,
                self.twilight)

    def __eq__(self, other):
        return self.__key() == other.__key()

    def __hash__(self):
        return hash(self.__key())
Ejemplo n.º 6
0
class TestAngle(object):
    '''Unit tests for the angle.Angle class.'''

    # Test constructor errors
    @raises(AngleConfigError)
    def test_invalid_angle_none(self):
        self.angle = Angle()

    @raises(AngleConfigError)
    def test_invalid_angle_type(self):
        self.angle = Angle(units = 'time')

    @raises(AngleConfigError)
    def test_invalid_angle_units(self):
        self.angle = Angle(degrees = 45, units = 'kilos')

    @raises(AngleConfigError)
    def test_invalid_multiple_angles(self):
        self.angle = Angle(degrees = 45, radians = pi/4)


    # Test degree functionality
    def test_in_degrees(self):
        self.angle = Angle(degrees=37)
        assert_equal(self.angle.in_degrees(), 37)

    def test_in_degrees_rads_provided(self):
        self.angle = Angle(radians=pi)
        assert_equal(self.angle.in_degrees(), 180)

    def test_in_degrees_negative(self):
        self.angle = Angle(degrees = -37)
        assert_equal(self.angle.in_degrees(), -37)



    # Test radian functionality
    def test_in_radians_rads_provided(self):
        self.angle = Angle(radians=2*pi)
        assert_equal(self.angle.in_radians(), 2*pi)

    def test_in_radians_degrees_provided(self):
        self.angle = Angle(degrees=180)
        assert_equal(self.angle.in_radians(), pi)

    def test_in_radians_degrees_provided_time(self):
        self.angle = Angle(degrees=12, units = 'time')
        assert_equal(self.angle.in_radians(), pi)


    def test_in_radians_negative(self):
        self.angle = Angle(radians = -pi)
        assert_equal(self.angle.in_radians(), -pi)



    # Test valid sexegesimal->degrees conversion given units = time
    def test_from_sexegesimal_hrs_time(self):
        self.angle = Angle(degrees='12:00:00', units= 'time')
        assert_equal(self.angle.in_degrees(), 180)

    def test_from_sexegesimal_more_hrs_time(self):
        self.angle = Angle(degrees='120:00:00', units= 'time')
        assert_equal(self.angle.in_degrees(), 1800)

    def test_from_sexegesimal_hrs_mins_time(self):
        self.angle = Angle(degrees='12:30:00', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.5)

    def test_from_sexegesimal_hrs_secs_time(self):
        self.angle = Angle(degrees='12:30:30', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.625)

    def test_from_sexegesimal_fractional_secs_time(self):
        self.angle = Angle(degrees='12:30:30.1', units = 'time')
        assert_equal(self.angle.in_degrees(), (187.625 + (360/24/36000)))

    def test_from_sexegesimal_negative_time(self):
        self.angle = Angle(degrees='-12:00:00', units = 'time')
        assert_equal(self.angle.in_degrees(), -180)

    def test_from_sexegesimal_positive_time(self):
        self.angle = Angle(degrees='+12:00:00', units = 'time')
        assert_equal(self.angle.in_degrees(), 180)

    def test_from_sexegesimal_zero_time(self):
        self.angle = Angle(degrees='0 0 0', units = 'time')
        assert_equal(self.angle.in_degrees(), 0.0)



    # Test valid sexegesimal->degrees conversion given units = arc
    def test_from_sexegesimal_hrs_arc(self):
        self.angle = Angle(degrees='12:00:00')
        assert_equal(self.angle.in_degrees(), 12)

    def test_from_sexegesimal_more_hrs_arc(self):
        self.angle = Angle(degrees='120:00:00')
        assert_equal(self.angle.in_degrees(), 120)

    def test_from_sexegesimal_hrs_mins_arc(self):
        self.angle = Angle(degrees='12:30:00')
        assert_equal(self.angle.in_degrees(), 12.5)

    def test_from_sexegesimal_hrs_secs_arc(self):
        self.angle = Angle(degrees='12:30:30')
        assert_almost_equal(self.angle.in_degrees(), 12.50833, 5)

    def test_from_sexegesimal_fractional_secs_arc(self):
        self.angle = Angle(degrees='12:30:30.1')
        assert_almost_equal(self.angle.in_degrees(), 12.50836, 5)

    def test_from_sexegesimal_negative_arc(self):
        self.angle = Angle(degrees='-12:00:00')
        assert_equal(self.angle.in_degrees(), -12.0)

    def test_from_sexegesimal_positive_arc(self):
        self.angle = Angle(degrees='+12:00:00')
        assert_equal(self.angle.in_degrees(), 12.0)

    def test_from_sexegesimal_zero_arc(self):
        self.angle = Angle(degrees='0 0 0')
        assert_equal(self.angle.in_degrees(), 0.0)



    # Test various valid input forms for sexegesimal
    def test_from_sexegesimal_valid_format_colons(self):
        self.angle = Angle(degrees='12:30:30', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.625)

    def test_from_sexegesimal_valid_format_one_space(self):
        self.angle = Angle(degrees='12 30 30', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.625)

    def test_from_sexegesimal_valid_format_many_spaces(self):
        self.angle = Angle(degrees='12        30   30', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.625)

    def test_from_sexegesimal_valid_format_weird_delims(self):
        self.angle = Angle(degrees='12$$30$$30', units = 'time')
        assert_equal(self.angle.in_degrees(), 187.625)



    # Test sexegesimal validation on various illegal inputs
    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_number_mins_too_long(self):
        self.angle = Angle(degrees='12:0120:00')

    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_secs_too_long(self):
        self.angle = Angle(degrees='12:00:0032')

    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_min_too_small(self):
        self.angle = Angle(degrees='12:-1:00')

    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_sec_too_small(self):
        self.angle = Angle(degrees='12:00:-1.0')

    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_no_delimiters(self):
        self.angle = Angle(degrees = '123030')

    @raises(InvalidAngleError)
    def test_from_sexegesimal_invalid_format_minuses(self):
        self.angle = Angle(degrees = '12-30-30', units = 'time')



    # Test returning degrees in sexegesimal
    def test_in_sexegesimal_degrees_str_arc(self):
        self.angle = Angle(degrees = '12 30 30')
        assert_equal(self.angle.in_sexegesimal(), '12 30 30')

    def test_in_sexegesimal_degrees_num_arc(self):
        self.angle = Angle(degrees = 45)
        assert_equal(self.angle.in_sexegesimal(), '45 0 0')

    def test_in_sexegesimal_degrees_negative_num_arc(self):
        self.angle = Angle(degrees = -90)
        assert_equal(self.angle.in_sexegesimal(), '-90 0 0')

    def test_in_sexegesimal_degrees_negative_str_arc(self):
        self.angle = Angle(degrees = '-12 00 00')
        assert_equal(self.angle.in_sexegesimal(), '-12 0 0')

    def test_in_sexegesimal_degrees_str_time(self):
        self.angle = Angle(degrees = '12 30 30', units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '12 30 30')

    def test_in_sexegesimal_degrees_num_time(self):
        self.angle = Angle(degrees = 12.5, units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '12 30 0')

    def test_in_sexegesimal_degrees_negative_num_time(self):
        self.angle = Angle(degrees = -12.5, units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '-12 30 0')

    def test_in_sexegesimal_degrees_negative_str_time(self):
        self.angle = Angle(degrees = '-12 00 00', units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '-12 0 0')

    # Test returning radians in sexegesimal
    def test_in_sexegesimal_radians_str_arc(self):
        self.angle = Angle(radians = '12 30 30')
        assert_equal(self.angle.in_sexegesimal(radians = True), '12 30 30')

    def test_in_sexegesimal_radians_negative_str_arc(self):
        self.angle = Angle(radians = '-2 00 00')
        assert_equal(self.angle.in_sexegesimal(radians = True), '-2 0 0')

    def test_in_sexegesimal_radians_str_time(self):
        self.angle = Angle(radians = '12 30 30', units = 'time')
        assert_equal(self.angle.in_sexegesimal(radians = True), '12 30 30')

    def test_in_sexegesimal_radians_negative_str_time(self):
        self.angle = Angle(radians = '-3 00 00', units = 'time')
        assert_equal(self.angle.in_sexegesimal(radians = True), '-3 0 0')


    # Test converting degrees to radians sexegesimal
    def test_in_sexegesimal_degrees_to_radians_arc(self):
        self.angle = Angle(degrees = 180)
        assert_equal(self.angle.in_sexegesimal(radians = True), '3 8 29.7335529233')

    def test_in_sexegesimal__negative_degrees_to_radians_arc(self):
        self.angle = Angle(degrees = -180)
        assert_equal(self.angle.in_sexegesimal(radians = True), '-3 8 29.7335529233')

    def test_in_sexegesimal_degrees_to_radians_time(self):
        self.angle = Angle(degrees = 12, units = 'time')
        assert_equal(self.angle.in_sexegesimal(radians = True), '3 8 29.7335529233')

    def test_in_sexegesimal_negative_degrees_to_radians_time(self):
        self.angle = Angle(degrees = -12, units = 'time')
        assert_equal(self.angle.in_sexegesimal(radians = True), '-3 8 29.7335529233')

    def test_in_sexegesimal_radians_to_degrees_arc(self):
        self.angle = Angle(radians = pi)
        assert_equal(self.angle.in_sexegesimal(), '180 0 0')

    def test_in_sexegesimal_negative_radians_to_degrees_arc(self):
        self.angle = Angle(radians = -2*pi)
        assert_equal(self.angle.in_sexegesimal(), '-360 0 0')

    def test_in_sexegesimal_radians_to_degrees_time(self):
        self.angle = Angle(radians = pi, units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '180 0 0')

    def test_in_sexegesimal_negative_radians_to_degrees_time(self):
        self.angle = Angle(radians = -2*pi, units = 'time')
        assert_equal(self.angle.in_sexegesimal(), '-360 0 0')