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

Reply via email to