--- Begin Message ---
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
--- End Message ---