Package: release.debian.org Severity: normal User: release.debian....@packages.debian.org Usertags: unblock
Dear release team, please unblock astroplan in the current freeze. It solves #855477 "Failure with broadcasts in schedulers", severity: important. Changelog entry: astroplan (0.2-5) unstable; urgency=medium * Fix broadcasts in schedulers (Closes: #855477) -- Vincent Prat <vincep...@free.fr> Sat, 18 Feb 2017 16:37:34 +0100 The debdiff is attached. Requested commands: unblock astroplan/0.2-5
diff -Nru astroplan-0.2/debian/changelog astroplan-0.2/debian/changelog --- astroplan-0.2/debian/changelog 2017-01-27 20:57:06.000000000 +0100 +++ astroplan-0.2/debian/changelog 2017-02-18 16:37:34.000000000 +0100 @@ -1,3 +1,9 @@ +astroplan (0.2-5) unstable; urgency=medium + + * Fix broadcasts in schedulers (Closes: #855477) + + -- Vincent Prat <vincep...@free.fr> Sat, 18 Feb 2017 16:37:34 +0100 + astroplan (0.2-4) unstable; urgency=medium * Github patches + failures marked as known (Closes: #851437) diff -Nru astroplan-0.2/debian/patches/disable_failing_tests.patch astroplan-0.2/debian/patches/disable_failing_tests.patch --- astroplan-0.2/debian/patches/disable_failing_tests.patch 2017-01-27 20:57:06.000000000 +0100 +++ astroplan-0.2/debian/patches/disable_failing_tests.patch 1970-01-01 01:00:00.000000000 +0100 @@ -1,40 +0,0 @@ -From: Ole Streicher <oleb...@debian.org> -Subject: Mark known failures ---- a/astroplan/tests/test_scheduling.py -+++ b/astroplan/tests/test_scheduling.py -@@ -6,6 +6,7 @@ - from astropy.time import Time - import astropy.units as u - from astropy.coordinates import SkyCoord -+from astropy.tests.helper import pytest - - from ..utils import time_grid_from_range - from ..observer import Observer -@@ -103,7 +104,8 @@ - assert np.abs(schedule.slots[0].end - new_duration - start) < 1*u.second - assert schedule.slots[1].start == schedule.slots[0].end - -- -+# see https://github.com/astropy/astroplan/pull/282 -+@pytest.mark.xfail() - def test_transitioner(): - blocks = [ObservingBlock(t, 55 * u.minute, i) for i, t in enumerate(targets)] - slew_rate = 1 * u.deg / u.second -@@ -132,6 +134,8 @@ - default_transitioner = Transitioner(slew_rate=1 * u.deg / u.second) - - -+# see https://github.com/astropy/astroplan/pull/282 -+@pytest.mark.xfail() - def test_priority_scheduler(): - constraints = [AirmassConstraint(3, boolean_constraint=False)] - blocks = [ObservingBlock(t, 55*u.minute, i) for i, t in enumerate(targets)] -@@ -157,6 +161,8 @@ - scheduler(blocks, schedule) - - -+# see https://github.com/astropy/astroplan/pull/282 -+@pytest.mark.xfail() - def test_sequential_scheduler(): - constraints = [AirmassConstraint(2.5, boolean_constraint=False)] - blocks = [ObservingBlock(t, 55 * u.minute, i) for i, t in enumerate(targets)] diff -Nru astroplan-0.2/debian/patches/pull-285-Fix-broadcasting.patch astroplan-0.2/debian/patches/pull-285-Fix-broadcasting.patch --- astroplan-0.2/debian/patches/pull-285-Fix-broadcasting.patch 1970-01-01 01:00:00.000000000 +0100 +++ astroplan-0.2/debian/patches/pull-285-Fix-broadcasting.patch 2017-02-18 16:37:34.000000000 +0100 @@ -0,0 +1,891 @@ +Author: Vincent Prat <vincep...@free.fr> +Description: Fix the broadcasting issue, that caused tests to fail. + The patch comes from https://github.com/astropy/astroplan/pull/285. +--- a/astroplan/constraints.py ++++ b/astroplan/constraints.py +@@ -404,7 +404,7 @@ + observer.pressure = 0 + + # find solar altitude at these times +- altaz = observer.altaz(times, get_sun(times)) ++ altaz = observer.altaz(times, get_sun(times), grid=False) + altitude = altaz.alt + # cache the altitude + observer._altaz_cache[aakey] = dict(times=times, +@@ -447,7 +447,7 @@ + self.max = max + + def compute_constraint(self, times, observer, targets): +- sunaltaz = observer.altaz(times, get_sun(times)) ++ sunaltaz = observer.altaz(times, get_sun(times), grid=False) + target_coos = [target.coord if hasattr(target, 'coord') else target + for target in targets] + target_altazs = [observer.altaz(times, coo) for coo in target_coos] +--- a/astroplan/observer.py ++++ b/astroplan/observer.py +@@ -11,6 +11,8 @@ + get_moon, Angle, Latitude, Longitude, + UnitSphericalRepresentation) + from astropy.extern.six import string_types ++from astropy.utils import isiterable ++from astropy.utils.compat.numpy import broadcast_to + import astropy.units as u + from astropy.time import Time + from astropy.utils import isiterable +@@ -20,6 +22,8 @@ + # Package + from .exceptions import TargetNeverUpWarning, TargetAlwaysUpWarning + from .moon import moon_illumination, moon_phase_angle ++from .target import get_skycoord ++ + + __all__ = ["Observer", "MAGIC_TIME"] + +@@ -64,6 +68,14 @@ + else: + time_grid = np.linspace(start, end, N)*u.day + ++ # broadcast so grid is first index, and remaining shape of t0 ++ # falls in later indices. e.g. if t0 is shape (10), time_grid ++ # will be shape (N, 10). If t0 is shape (5, 2), time_grid is (N, 5, 2) ++ while time_grid.ndim <= t0.ndim: ++ time_grid = time_grid[:, np.newaxis] ++ # we want to avoid 1D grids since we always want to broadcast against targets ++ if time_grid.ndim == 1: ++ time_grid = time_grid[:, np.newaxis] + return t0 + time_grid + + +@@ -110,20 +122,24 @@ + """ + Parameters + ---------- +- name : str +- A short name for the telescope, observatory or location. +- + location : `~astropy.coordinates.EarthLocation` + The location (latitude, longitude, elevation) of the observatory. + +- longitude : float, str, `~astropy.units.Quantity` (optional) +- The longitude of the observing location. Should be valid input for +- initializing a `~astropy.coordinates.Longitude` object. ++ timezone : str or `datetime.tzinfo` (optional) ++ The local timezone to assume. If a string, it will be passed ++ through ``pytz.timezone()`` to produce the timezone object. ++ ++ name : str ++ A short name for the telescope, observatory or location. + + latitude : float, str, `~astropy.units.Quantity` (optional) + The latitude of the observing location. Should be valid input for + initializing a `~astropy.coordinates.Latitude` object. + ++ longitude : float, str, `~astropy.units.Quantity` (optional) ++ The longitude of the observing location. Should be valid input for ++ initializing a `~astropy.coordinates.Longitude` object. ++ + elevation : `~astropy.units.Quantity` (optional), default = 0 meters + The elevation of the observing location, with respect to sea + level. Defaults to sea level. +@@ -137,10 +153,6 @@ + temperature : `~astropy.units.Quantity` (optional) + The ambient temperature. + +- timezone : str or `datetime.tzinfo` (optional) +- The local timezone to assume. If a string, it will be passed +- through ``pytz.timezone()`` to produce the timezone object. +- + description : str (optional) + A short description of the telescope, observatory or observing + location. +@@ -336,59 +348,64 @@ + + return Time(date_time, location=self.location) + +- def _transform_target_list_to_altaz(self, times, targets): ++ def _is_broadcastable(self, shp1, shp2): ++ """Test if two shape tuples are broadcastable""" ++ if shp1 == shp2: ++ return True ++ for a, b in zip(shp1[::-1], shp2[::-1]): ++ if a == 1 or b == 1 or a == b: ++ pass ++ else: ++ return False ++ return True ++ ++ def _preprocess_inputs(self, time, target=None, grid=True): + """ +- Workaround for transforming a list of coordinates ``targets`` to +- altitudes and azimuths. ++ Preprocess time and target inputs + +- Parameters +- ---------- +- times : `~astropy.time.Time` or list of `~astropy.time.Time` objects +- Time of observation ++ This routine takes the inputs for time and target and attempts to ++ return a single `~astropy.time.Time` and `~astropy.coordinates.SkyCoord` ++ for each argument, which may be non-scalar if necessary. + +- targets : `~astropy.coordinates.SkyCoord` or list of `~astropy.coordinates.SkyCoord` objects +- List of target coordinates ++ time : `~astropy.time.Time` or other (see below) ++ The time(s) to use in the calculation. It can be anything that ++ `~astropy.time.Time` will accept (including a `~astropy.time.Time` object) + +- location : `~astropy.coordinates.EarthLocation` +- Location of observer ++ target : `~astroplan.FixedTarget`, `~astropy.coordinates.SkyCoord`, or list ++ The target(s) to use in the calculation. + +- Returns +- ------- +- altitudes : list +- List of altitudes for each target, at each time ++ grid: bool ++ If True, and the time and target objects cannot be broadcast, ++ the target object will have extra dimensions packed onto the end, ++ so that calculations with M targets and N times will return an (M, N) ++ shaped result. Useful for grid searches for rise/set times etc. + """ +- if times.isscalar: +- times = Time([times]) +- +- if not isinstance(targets, list) and targets.isscalar: +- targets = [targets] +- +- targets_is_unitsphericalrep = [x.data.__class__ is +- UnitSphericalRepresentation for x in targets] +- if all(targets_is_unitsphericalrep) or not any(targets_is_unitsphericalrep): +- repeated_times = np.tile(times, len(targets)) +- ra_list = Longitude([x.icrs.ra for x in targets]) +- dec_list = Latitude([x.icrs.dec for x in targets]) +- repeated_ra = np.repeat(ra_list, len(times)) +- repeated_dec = np.repeat(dec_list, len(times)) +- inner_sc = SkyCoord(ra=repeated_ra, dec=repeated_dec) +- target_SkyCoord = SkyCoord(inner_sc.data.represent_as(UnitSphericalRepresentation), +- representation=UnitSphericalRepresentation) +- transformed_coord = target_SkyCoord.transform_to(AltAz(location=self.location, +- obstime=repeated_times)) +- else: +- # TODO: This is super slow. +- repeated_times = np.tile(times, len(targets)) +- repeated_targets = np.repeat(targets, len(times)) +- target_SkyCoord = SkyCoord(SkyCoord(repeated_targets).data.represent_as( +- UnitSphericalRepresentation), +- representation=UnitSphericalRepresentation) +- +- transformed_coord = target_SkyCoord.transform_to(AltAz(location=self.location, +- obstime=repeated_times)) +- return transformed_coord ++ # make sure we have a non-scalar time ++ if not isinstance(time, Time): ++ time = Time(time) + +- def altaz(self, time, target=None, obswl=None): ++ if target is None: ++ return time, None ++ ++ # convert any kind of target argument to non-scalar SkyCoord ++ target = get_skycoord(target) ++ ++ if grid: ++ # now we broadcast the targets array so that the first index ++ # iterates over targets, any other indices over times ++ if not target.isscalar: ++ if time.isscalar: ++ target = target[:, np.newaxis] ++ while target.ndim <= time.ndim: ++ target = target[:, np.newaxis] ++ if not self._is_broadcastable(target.shape, time.shape): ++ raise ValueError( ++ 'Time and Target arguments cannot be broadcast against each other with shapes {} and {}'.format( ++ time.shape, target.shape ++ )) ++ return time, target ++ ++ def altaz(self, time, target=None, obswl=None, grid=True): + """ + Get an `~astropy.coordinates.AltAz` frame or coordinate. + +@@ -411,6 +428,12 @@ + obswl : `~astropy.units.Quantity` (optional) + Wavelength of the observation used in the calculation. + ++ grid: bool ++ If True, and the time and target objects cannot be broadcast, ++ the target object will have extra dimensions packed onto the end, ++ so that calculations with M targets and N times will return an (M, N) ++ shaped result. Useful for grid searches for rise/set times etc. ++ + Returns + ------- + `~astropy.coordinates.AltAz` +@@ -440,8 +463,8 @@ + + >>> target_altaz = apo.altaz(time, target) # doctest: +SKIP + """ +- if not isinstance(time, Time): +- time = Time(time) ++ if target is not None: ++ time, target = self._preprocess_inputs(time, target, grid) + + altaz_frame = AltAz(location=self.location, obstime=time, + pressure=self.pressure, obswl=obswl, +@@ -451,24 +474,7 @@ + # Return just the frame + return altaz_frame + else: +- # If target is a list of targets: +- if isiterable(target) and not isinstance(target, SkyCoord): +- get_coord = lambda x: x.coord if hasattr(x, 'coord') else x +- transformed_coords = self._transform_target_list_to_altaz(time, +- list(map(get_coord, target))) +- n_targets = len(target) +- new_shape = (n_targets, int(len(transformed_coords)/n_targets)) +- +- for comp in transformed_coords.data.components: +- getattr(transformed_coords.data, comp).resize(new_shape) +- return transformed_coords +- +- # If single target is a FixedTarget or a SkyCoord: +- if hasattr(target, 'coord'): +- coordinate = target.coord +- else: +- coordinate = target +- return coordinate.transform_to(altaz_frame) ++ return target.transform_to(altaz_frame) + + def parallactic_angle(self, time, target): + """ +@@ -496,17 +502,7 @@ + .. [1] https://en.wikipedia.org/wiki/Parallactic_angle + + """ +- if not isinstance(time, Time): +- time = Time(time) +- +- if isiterable(target): +- get_coord = lambda x: x.coord if hasattr(x, 'coord') else x +- coordinate = SkyCoord(list(map(get_coord, target))) +- else: +- if hasattr(target, 'coord'): +- coordinate = target.coord +- else: +- coordinate = target ++ time, coordinate = self._preprocess_inputs(time, target) + + # Eqn (14.1) of Meeus' Astronomical Algorithms + LST = time.sidereal_time('mean', longitude=self.location.longitude) +@@ -530,9 +526,12 @@ + Parameters + ---------- + t : `~astropy.time.Time` +- Grid of times ++ Grid of N times, any shape. Search grid along first axis, e.g (N, ...) + alt : `~astropy.units.Quantity` + Grid of altitudes ++ Depending on broadcasting we either have ndim >=3 and ++ M targets along first axis, e.g (M, N, ...), or ++ ndim = 2 and targets/times in last axis + rise_set : {"rising", "setting"} + Calculate either rising or setting across the horizon + horizon : float +@@ -543,61 +542,88 @@ + Returns + ------- + Returns the lower and upper limits on the time and altitudes +- of the horizon crossing. +- """ +- alt = np.atleast_2d(Latitude(alt)) +- n_targets = alt.shape[0] ++ of the horizon crossing. The altitude limits have shape (M, ...) and the ++ time limits have shape (...). These arrays aresuitable for interpolation ++ to find the horizon crossing time. ++ """ ++ # handle different cases by enforcing standard shapes on ++ # the altitude grid ++ finesse_time_indexes = False ++ if alt.ndim == 1: ++ raise ValueError('Must supply more at least a 2D grid of altitudes') ++ elif alt.ndim == 2: ++ # TODO: this test for ndim=2 doesn't work. if times is e.g (2,5) ++ # then alt will have ndim=3, but shape (100, 2, 5) so grid ++ # is in first index... ++ ntargets = alt.shape[1] ++ ngrid = alt.shape[0] ++ unit = alt.unit ++ alt = broadcast_to(alt, (ntargets, ngrid, ntargets)).T ++ alt = alt*unit ++ extra_dimension_added = True ++ if t.shape[1] == 1: ++ finesse_time_indexes = True ++ else: ++ extra_dimension_added = False ++ output_shape = (alt.shape[0],) + alt.shape[2:] + + if rise_set == 'rising': + # Find index where altitude goes from below to above horizon +- condition = (alt[:, :-1] < horizon) * (alt[:, 1:] > horizon) ++ condition = (alt[:, :-1, ...] < horizon) * (alt[:, 1:, ...] > horizon) + elif rise_set == 'setting': + # Find index where altitude goes from above to below horizon +- condition = (alt[:, :-1] > horizon) * (alt[:, 1:] < horizon) +- +- target_inds, time_inds = np.nonzero(condition) ++ condition = (alt[:, :-1, ...] > horizon) * (alt[:, 1:, ...] < horizon) + +- if np.count_nonzero(condition) < n_targets: +- target_inds, _ = np.nonzero(condition) +- noncrossing_target_ind = np.setdiff1d(np.arange(n_targets), +- target_inds, +- assume_unique=True) # [0] +- +- warnmsg = ('Target(s) index {} does not cross horizon={} within ' +- '24 hours'.format(noncrossing_target_ind, horizon)) +- +- if (alt[noncrossing_target_ind, :] > horizon).all(): +- warnings.warn(warnmsg, TargetAlwaysUpWarning) +- else: +- warnings.warn(warnmsg, TargetNeverUpWarning) +- +- # Fill in missing time with MAGIC_TIME +- target_inds = np.insert(target_inds, noncrossing_target_ind, +- noncrossing_target_ind) +- time_inds = np.insert(time_inds.astype(float), +- noncrossing_target_ind, +- np.nan) +- elif np.count_nonzero(condition) > n_targets: +- old_target_inds = np.copy(target_inds) +- old_time_inds = np.copy(time_inds) +- +- time_inds = [] +- target_inds = [] +- for tgt, tm in zip(old_target_inds, old_time_inds): +- if tgt not in target_inds: +- time_inds.append(tm) +- target_inds.append(tgt) +- target_inds = np.array(target_inds, dtype=int) +- time_inds = np.array(time_inds) +- +- times = [t[int(i):int(i)+2] if not np.isnan(i) else np.nan for i in time_inds] +- altitudes = [alt[int(i), int(j):int(j)+2] if not np.isnan(j) else np.nan +- for i, j in zip(target_inds, time_inds)] ++ noncrossing_indices = np.sum(condition, axis=1, dtype=np.intp) < 1 ++ alt_lims1 = u.Quantity(np.zeros(output_shape), unit=u.deg) ++ alt_lims2 = u.Quantity(np.zeros(output_shape), unit=u.deg) ++ jd_lims1 = np.zeros(output_shape) ++ jd_lims2 = np.zeros(output_shape) ++ if np.any(noncrossing_indices): ++ for target_index in set(np.where(noncrossing_indices)[0]): ++ warnmsg = ('Target with index {} does not cross horizon={} within ' ++ '24 hours'.format(target_index, horizon)) ++ if (alt[target_index, ...] > horizon).all(): ++ warnings.warn(warnmsg, TargetAlwaysUpWarning) ++ else: ++ warnings.warn(warnmsg, TargetNeverUpWarning) + +- return times, altitudes ++ alt_lims1[np.nonzero(noncrossing_indices)] = np.nan ++ alt_lims2[np.nonzero(noncrossing_indices)] = np.nan ++ jd_lims1[np.nonzero(noncrossing_indices)] = np.nan ++ jd_lims2[np.nonzero(noncrossing_indices)] = np.nan ++ ++ before_indices = np.array(np.nonzero(condition)) ++ # we want to add an vector like (0, 1, ...) to get after indices ++ array_to_add = np.zeros(before_indices.shape[0])[:, np.newaxis].astype(int) ++ array_to_add[1] = 1 ++ after_indices = before_indices + array_to_add ++ ++ al1 = alt[tuple(before_indices)] ++ al2 = alt[tuple(after_indices)] ++ # slice the time in the same way, but delete the object index ++ before_time_index_tuple = np.delete(before_indices, 0, 0) ++ after_time_index_tuple = np.delete(after_indices, 0, 0) ++ if finesse_time_indexes: ++ before_time_index_tuple[1:] = 0 ++ after_time_index_tuple[1:] = 0 ++ tl1 = t[tuple(before_time_index_tuple)] ++ tl2 = t[tuple(after_time_index_tuple)] ++ ++ alt_lims1[tuple(np.delete(before_indices, 1, 0))] = al1 ++ alt_lims2[tuple(np.delete(before_indices, 1, 0))] = al2 ++ jd_lims1[tuple(np.delete(before_indices, 1, 0))] = tl1.utc.jd ++ jd_lims2[tuple(np.delete(before_indices, 1, 0))] = tl2.utc.jd ++ ++ if extra_dimension_added: ++ return (alt_lims1.diagonal(), alt_lims2.diagonal(), ++ jd_lims1.diagonal(), jd_lims2.diagonal()) ++ else: ++ return alt_lims1, alt_lims2, jd_lims1, jd_lims2 + + @u.quantity_input(horizon=u.deg) +- def _two_point_interp(self, times, altitudes, horizon=0*u.deg): ++ def _two_point_interp(self, jd_before, jd_after, ++ alt_before, alt_after, horizon=0*u.deg): + """ + Do linear interpolation between two ``altitudes`` at + two ``times`` to determine the time where the altitude +@@ -605,11 +631,17 @@ + + Parameters + ---------- +- times : `~astropy.time.Time` +- Two times for linear interpolation between ++ jd_before : `float` ++ JD(UTC) before crossing event ++ ++ jd_after : `float` ++ JD(UTC) after crossing event + +- altitudes : array of `~astropy.units.Quantity` +- Two altitudes for linear interpolation between ++ alt_before : `~astropy.units.Quantity` ++ altitude before crossing event ++ ++ alt_after : `~astropy.units.Quantity` ++ altitude after crossing event + + horizon : `~astropy.units.Quantity` + Solve for the time when the altitude is equal to +@@ -621,12 +653,10 @@ + Time when target crosses the horizon + + """ +- if not isinstance(times, Time): +- return MAGIC_TIME +- else: +- slope = (altitudes[1] - altitudes[0])/(times[1].jd - times[0].jd) +- return Time(times[1].jd - ((altitudes[1] - horizon)/slope).value, +- format='jd') ++ slope = (alt_after-alt_before)/((jd_after - jd_before)*u.d) ++ crossing_jd = (jd_after*u.d - ((alt_after - horizon)/slope)) ++ crossing_jd[np.isnan(crossing_jd)] = u.d*MAGIC_TIME.jd ++ return np.squeeze(Time(crossing_jd, format='jd')) + + def _altitude_trig(self, LST, target): + """ +@@ -648,6 +678,7 @@ + alt : `~astropy.unit.Quantity` + Array of altitudes + """ ++ LST, target = self._preprocess_inputs(LST, target) + alt = np.arcsin(np.sin(self.location.latitude.radian) * + np.sin(target.dec) + + np.cos(self.location.latitude.radian) * +@@ -677,9 +708,6 @@ + rise_set : str - either 'rising' or 'setting' + Compute prev/next rise or prev/next set + +- location : `~astropy.coordinates.EarthLocation` +- Location of observer +- + horizon : `~astropy.units.Quantity` + Degrees above/below actual horizon to use + for calculating rise/set times (i.e., +@@ -694,30 +722,21 @@ + ret1 : `~astropy.time.Time` + Time of rise/set + """ +- + if not isinstance(time, Time): + time = Time(time) + +- target_is_vector = isiterable(target) +- + if prev_next == 'next': + times = _generate_24hr_grid(time, 0, 1, N) + else: + times = _generate_24hr_grid(time, -1, 0, N) + +- altaz = self.altaz(times, target) ++ altaz = self.altaz(times, target, grid=True) + altitudes = altaz.alt + +- time_limits, altitude_limits = self._horiz_cross(times, altitudes, rise_set, +- horizon) +- if not target_is_vector: +- return self._two_point_interp(time_limits[0], altitude_limits[0], +- horizon=horizon) +- else: +- return Time([self._two_point_interp(time_limit, altitude_limit, +- horizon=horizon) +- for time_limit, altitude_limit in +- zip(time_limits, altitude_limits)]) ++ al1, al2, jd1, jd2 = self._horiz_cross(times, altitudes, rise_set, ++ horizon) ++ return self._two_point_interp(jd1, jd2, al1, al2, ++ horizon=horizon) + + def _calc_transit(self, time, target, prev_next, antitransit=False, N=150): + """ +@@ -742,9 +761,6 @@ + Toggle compute antitransit (below horizon, equivalent to midnight + for the Sun) + +- location : `~astropy.coordinates.EarthLocation` +- Location of observer +- + N : int + Number of altitudes to compute when searching for + rise or set. +@@ -754,11 +770,10 @@ + ret1 : `~astropy.time.Time` + Time of transit/antitransit + """ ++ # TODO FIX BROADCASTING HERE + if not isinstance(time, Time): + time = Time(time) + +- target_is_vector = isiterable(target) +- + if prev_next == 'next': + times = _generate_24hr_grid(time, 0, 1, N, for_deriv=True) + else: +@@ -771,26 +786,22 @@ + else: + rise_set = 'setting' + +- altaz = self.altaz(times, target) +- if target_is_vector: +- d_altitudes = [each_alt.diff() for each_alt in altaz.alt] ++ altaz = self.altaz(times, target, grid=True) ++ altitudes = altaz.alt ++ if altitudes.ndim > 2: ++ # shape is (M, N, ...) where M is targets and N is grid ++ d_altitudes = altitudes.diff(axis=1) + else: +- altitudes = altaz.alt +- d_altitudes = altitudes.diff() ++ # shape is (N, M) where M is targets and N is grid ++ d_altitudes = altitudes.diff(axis=0) + + dt = Time((times.jd[1:] + times.jd[:-1])/2, format='jd') + + horizon = 0*u.degree # Find when derivative passes through zero +- time_limits, altitude_limits = self._horiz_cross(dt, d_altitudes, +- rise_set, horizon) +- if not target_is_vector: +- return self._two_point_interp(time_limits[0], altitude_limits[0], +- horizon=horizon) +- else: +- return Time([self._two_point_interp(time_limit, altitude_limit, +- horizon=horizon) +- for time_limit, altitude_limit in +- zip(time_limits, altitude_limits)]) ++ al1, al2, jd1, jd2 = self._horiz_cross(dt, d_altitudes, ++ rise_set, horizon) ++ return self._two_point_interp(jd1, jd2, al1, al2, ++ horizon=horizon) + + def _determine_which_event(self, function, args_dict): + """ +@@ -828,19 +839,10 @@ + return previous_event + + if which == 'nearest': +- if isiterable(target): +- return_times = [] +- for next_e, prev_e in zip(next_event, previous_event): +- if abs(time - prev_e) < abs(time - next_e): +- return_times.append(prev_e) +- else: +- return_times.append(next_e) +- return Time(return_times) +- else: +- if abs(time - previous_event) < abs(time - next_event): +- return previous_event +- else: +- return next_event ++ mask = abs(time - previous_event) < abs(time - next_event) ++ return Time(np.where(mask, previous_event.utc.jd, ++ next_event.utc.jd), format='jd') ++ + + raise ValueError('"which" kwarg must be "next", "previous" or ' + '"nearest".') +@@ -1467,27 +1469,8 @@ + if not isinstance(time, Time): + time = Time(time) + +- # TODO: when astropy/astropy#5069 is resolved, replace this workaround which +- # handles scalar and non-scalar time inputs differently +- +- if time.isscalar: +- altaz_frame = AltAz(location=self.location, obstime=time) +- sun = get_sun(time).transform_to(altaz_frame) +- +- moon = get_moon(time, location=self.location, ephemeris=ephemeris).transform_to(altaz_frame) +- return moon +- +- else: +- moon_coords = [] +- for t in time: +- altaz_frame = AltAz(location=self.location, obstime=t) +- moon_coord = get_moon(t, location=self.location, ephemeris=ephemeris).transform_to(altaz_frame) +- moon_coords.append(moon_coord) +- obstime = [coord.obstime for coord in moon_coords] +- alts = u.Quantity([coord.alt for coord in moon_coords]) +- azs = u.Quantity([coord.az for coord in moon_coords]) +- dists = u.Quantity([coord.distance for coord in moon_coords]) +- return SkyCoord(AltAz(azs, alts, dists, obstime=obstime, location=self.location)) ++ moon = get_moon(time, location=self.location, ephemeris=ephemeris) ++ return self.altaz(time, moon, grid=False) + + @u.quantity_input(horizon=u.deg) + def target_is_up(self, time, target, horizon=0*u.degree, return_altaz=False): +@@ -1515,7 +1498,7 @@ + + Returns + ------- +- observable : boolean ++ observable : boolean or np.ndarray(bool) + True if ``target`` is above ``horizon`` at ``time``, else False. + + Examples +@@ -1538,10 +1521,13 @@ + time = Time(time) + + altaz = self.altaz(time, target) +- if isiterable(target): +- observable = [bool(alt > horizon) for alt in altaz.alt] ++ observable = altaz.alt > horizon ++ if altaz.isscalar: ++ observable = bool(observable) + else: +- observable = bool(altaz.alt > horizon) ++ # TODO: simply return observable if we move to ++ # a fully broadcasted API ++ observable = [value for value in observable.flat] + + if not return_altaz: + return observable +@@ -1571,7 +1557,7 @@ + + Returns + ------- +- sun_below_horizon : bool ++ sun_below_horizon : bool or np.ndarray(bool) + `True` if sun is below ``horizon`` at ``time``, else `False`. + + Examples +@@ -1590,7 +1576,12 @@ + time = Time(time) + + solar_altitude = self.altaz(time, target=get_sun(time), obswl=obswl).alt +- return bool(solar_altitude < horizon) ++ if solar_altitude.isscalar: ++ return bool(solar_altitude < horizon) ++ else: ++ # TODO: simply return solar_altitude < horizon if we move to ++ # a fully broadcasted API ++ return [val for val in (solar_altitude < horizon).flat] + + def local_sidereal_time(self, time, kind='apparent', model=None): + """ +@@ -1646,21 +1637,8 @@ + hour_angle : `~astropy.coordinates.Angle` + The hour angle(s) of the target(s) at ``time`` + """ +- if not isinstance(time, Time): +- time = Time(time) +- +- if isiterable(target): +- coords = [t.coord if hasattr(t, 'coord') else t +- for t in target] +- +- hour_angle = Longitude([self.local_sidereal_time(time) - coord.ra +- for coord in coords]) +- +- else: +- coord = target.coord if hasattr(target, 'coord') else target +- hour_angle = Longitude(self.local_sidereal_time(time) - coord.ra) +- +- return hour_angle ++ time, target = self._preprocess_inputs(time, target) ++ return Longitude(self.local_sidereal_time(time) - target.ra) + + @u.quantity_input(horizon=u.degree) + def tonight(self, time=None, horizon=0 * u.degree, obswl=None): +@@ -1689,11 +1667,16 @@ + A tuple of times corresponding to the start and end of current night + """ + current_time = Time.now() if time is None else time +- if self.is_night(current_time, horizon=horizon, obswl=obswl): +- start_time = current_time ++ night_mask = self.is_night(current_time, horizon=horizon, obswl=obswl) ++ sun_set_time = self.sun_set_time(current_time, which='next', horizon=horizon) ++ # workaround for NPY <= 1.8, otherwise np.where works even in scalar case ++ if current_time.isscalar: ++ start_time = current_time if night_mask else sun_set_time + else: +- start_time = self.sun_set_time(current_time, which='next', horizon=horizon) +- +- end_time = self.sun_rise_time(current_time, which='next', horizon=horizon) ++ start_time = np.where(night_mask, current_time, sun_set_time) ++ # np.where gives us a list of start Times - convert to Time object ++ if not isinstance(start_time, Time): ++ start_time = Time(start_time) ++ end_time = self.sun_rise_time(start_time, which='next', horizon=horizon) + + return start_time, end_time +--- a/astroplan/tests/test_target.py ++++ b/astroplan/tests/test_target.py +@@ -4,10 +4,13 @@ + + # Third-party + import astropy.units as u +-from astropy.coordinates import SkyCoord ++from astropy.coordinates import SkyCoord, GCRS, ICRS ++from astropy.time import Time ++from astropy.tests.helper import pytest + + # Package +-from ..target import FixedTarget ++from ..target import FixedTarget, get_skycoord ++from ..observer import Observer + + + def test_FixedTarget_from_name(): +@@ -40,3 +43,39 @@ + 'SkyCoord') + assert vega.coord.dec == vega_coords.dec == vega.dec, ('Retrieve Dec from ' + 'SkyCoord') ++ ++ ++def test_get_skycoord(): ++ m31 = SkyCoord(10.6847083*u.deg, 41.26875*u.deg) ++ m31_with_distance = SkyCoord(10.6847083*u.deg, 41.26875*u.deg, 780*u.kpc) ++ subaru = Observer.at_site('subaru') ++ time = Time("2016-01-22 12:00") ++ pos, vel = subaru.location.get_gcrs_posvel(time) ++ gcrs_frame = GCRS(obstime=Time("2016-01-22 12:00"), obsgeoloc=pos, obsgeovel=vel) ++ m31_gcrs = m31.transform_to(gcrs_frame) ++ m31_gcrs_with_distance = m31_with_distance.transform_to(gcrs_frame) ++ ++ coo = get_skycoord(m31) ++ assert coo.is_equivalent_frame(ICRS()) ++ with pytest.raises(TypeError) as exc_info: ++ len(coo) ++ ++ coo = get_skycoord([m31]) ++ assert coo.is_equivalent_frame(ICRS()) ++ assert len(coo) == 1 ++ ++ coo = get_skycoord([m31, m31_gcrs]) ++ assert coo.is_equivalent_frame(ICRS()) ++ assert len(coo) == 2 ++ ++ coo = get_skycoord([m31_with_distance, m31_gcrs_with_distance]) ++ assert coo.is_equivalent_frame(ICRS()) ++ assert len(coo) == 2 ++ ++ coo = get_skycoord([m31, m31_gcrs, m31_gcrs_with_distance, m31_with_distance]) ++ assert coo.is_equivalent_frame(ICRS()) ++ assert len(coo) == 4 ++ ++ coo = get_skycoord([m31_gcrs, m31_gcrs_with_distance]) ++ assert coo.is_equivalent_frame(m31_gcrs.frame) ++ assert len(coo) == 2 +--- a/astroplan/target.py ++++ b/astroplan/target.py +@@ -7,7 +7,7 @@ + + # Third-party + import astropy.units as u +-from astropy.coordinates import SkyCoord ++from astropy.coordinates import SkyCoord, ICRS, UnitSphericalRepresentation + + __all__ = ["Target", "FixedTarget", "NonFixedTarget"] + +@@ -185,3 +185,77 @@ + """ + Placeholder for future function. + """ ++ ++def get_skycoord(targets): ++ """ ++ Return an `~astropy.coordinates.SkyCoord` object. ++ ++ When performing calculations it is usually most efficient to have ++ a single `~astropy.coordinates.SkyCoord` object, rather than a ++ list of `FixedTarget` or `~astropy.coordinates.SkyCoord` objects. ++ ++ This is a convenience routine to do that. ++ ++ Parameters ++ ----------- ++ targets : list, `~astropy.coordinates.SkyCoord`, `Fixedtarget` ++ either a single target or a list of targets ++ ++ Returns ++ -------- ++ coord : `~astropy.coordinates.SkyCoord` ++ a single SkyCoord object, which may be non-scalar ++ """ ++ if not isinstance(targets, list): ++ return getattr(targets, 'coord', targets) ++ ++ # get the SkyCoord object itself ++ coords = [getattr(target, 'coord', target) for target in targets] ++ ++ # are all SkyCoordinate's in equivalent frames? If not, convert to ICRS ++ convert_to_icrs = not all([coord.frame.is_equivalent_frame(coords[0].frame) for coord in coords[1:]]) ++ ++ # we also need to be careful about handling mixtures of UnitSphericalRepresentations and others ++ targets_is_unitsphericalrep = [x.data.__class__ is ++ UnitSphericalRepresentation for x in coords] ++ ++ longitudes = [] ++ latitudes = [] ++ distances = [] ++ get_distances = not all(targets_is_unitsphericalrep) ++ if convert_to_icrs: ++ # mixture of frames ++ for coordinate in coords: ++ icrs_coordinate = coordinate.icrs ++ longitudes.append(icrs_coordinate.ra) ++ latitudes.append(icrs_coordinate.dec) ++ if get_distances: ++ distances.append(icrs_coordinate.distance) ++ frame = ICRS() ++ else: ++ # all the same frame, get the longitude and latitude names ++ lon_name, lat_name = [mapping.framename for mapping in ++ coords[0].frame_specific_representation_info['spherical']] ++ frame = coords[0].frame ++ for coordinate in coords: ++ longitudes.append(getattr(coordinate, lon_name)) ++ latitudes.append(getattr(coordinate, lat_name)) ++ if get_distances: ++ distances.append(coordinate.distance) ++ ++ # now let's deal with the fact that we may have a mixture of coords with distances and ++ # coords with UnitSphericalRepresentations ++ if all(targets_is_unitsphericalrep): ++ return SkyCoord(longitudes, latitudes, frame=frame) ++ elif not any(targets_is_unitsphericalrep): ++ return SkyCoord(longitudes, latitudes, distances, frame=frame) ++ else: ++ """ ++ We have a mixture of coords with distances and without. ++ Since we don't know in advance the origin of the frame where further transformation ++ will take place, it's not safe to drop the distances from those coords with them set. ++ ++ Instead, let's assign large distances to those objects with none. ++ """ ++ distances = [distance if distance != 1 else 100*u.kpc for distance in distances] ++ return SkyCoord(longitudes, latitudes, distances, frame=frame) +\ No newline at end of file +--- a/setup.py ++++ b/setup.py +@@ -102,7 +102,7 @@ + version=VERSION, + description=DESCRIPTION, + scripts=scripts, +- install_requires=['numpy>=1.6', 'astropy>=1.2', 'pytz'], ++ install_requires=['numpy>=1.6', 'astropy>=1.3', 'pytz'], + extras_require=dict( + plotting=['matplotlib>=1.4'], + docs=['sphinx_rtd_theme'] diff -Nru astroplan-0.2/debian/patches/series astroplan-0.2/debian/patches/series --- astroplan-0.2/debian/patches/series 2017-01-27 20:57:06.000000000 +0100 +++ astroplan-0.2/debian/patches/series 2017-02-18 16:27:41.000000000 +0100 @@ -4,4 +4,4 @@ pull-274-Stop-recognizing-scalar-SkyCoord-objects-as-vect.patch pull-273-Change-to-use-new-SkyCoord-prints.patch issues-282-Fix-more-test-failures-in-astroplan.patch -disable_failing_tests.patch +pull-285-Fix-broadcasting.patch