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))
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)
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
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)
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())
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')