Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tutorial for Scheduling #218

Merged
merged 30 commits into from Aug 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
16cbd33
start to a scheduling tutorial
kvyh Jul 29, 2016
49b4216
wrote the section on defining targets
kvyh Aug 3, 2016
3262a1c
defined ObservingBlocks
kvyh Aug 3, 2016
04801d6
wrote up the transitioner and scheduling and added a plot
kvyh Aug 3, 2016
36b321f
fixed a few Warnings
kvyh Aug 4, 2016
79c37f0
making doctests skip
kvyh Aug 4, 2016
6065e0f
removed the now-unnecessary example notebooks
kvyh Aug 4, 2016
443fd8b
tests for blocks, transitioner and non-schedulable. Fixed transitione…
kvyh Aug 1, 2016
85063e0
additions to a few tests and fixing a flaw in a scheduler
kvyh Aug 2, 2016
de11781
added some assertions
kvyh Aug 2, 2016
0bef1cf
moved _components=None to before components=components
kvyh Aug 2, 2016
9943d29
added tests for methods inside the Schedule
kvyh Aug 3, 2016
48680ed
changed np.abs to abs and changed line-break formatting
kvyh Aug 3, 2016
e579ce6
removed extra line
kvyh Aug 3, 2016
091875c
fixed an issue with rounding Quantities
kvyh Aug 4, 2016
f8d19c2
made the plot better
kvyh Aug 4, 2016
03b89f5
added a .. code-block:: where it was needed
kvyh Aug 5, 2016
47c04d4
fixed a few issues that can occur because of the Transitioner
kvyh Aug 5, 2016
4d1ce09
starting changing the targets
kvyh Aug 9, 2016
6af09bc
changed unit order
kvyh Aug 10, 2016
dd65e67
fixing sphinx formatting and reducing time resolution in tests
kvyh Aug 11, 2016
fd9eb2f
added doctest skips and mock targets
kvyh Aug 12, 2016
4c938c3
changed scheduling tests to not take as long
kvyh Aug 12, 2016
221d0d6
changed default time back to defining the time
kvyh Aug 13, 2016
dbb06fb
changed the tests to the new call format
kvyh Aug 15, 2016
4526dac
fixed scheduler calls in the tutorial
kvyh Aug 15, 2016
2f4386f
added a space to fix plotting
kvyh Aug 15, 2016
8cb4458
fixed times and switched constraints
kvyh Aug 17, 2016
ab87cbc
one legend entry per target
kvyh Aug 17, 2016
e693ec5
adding to the schedule table and making transitions more visible
kvyh Aug 17, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions astroplan/plots/time_dependent.py
Expand Up @@ -243,7 +243,7 @@ def plot_schedule_airmass(schedule, show_night=False):
targ_to_color = {}
color_idx = np.linspace(0, 1, len(targets))
# lighter, bluer colors indicate higher priority
for target, ci in zip(targets, color_idx):
for target, ci in zip(set(targets), color_idx):
plot_airmass(target, schedule.observer, ts, style_kwargs=dict(color=plt.cm.cool(ci)))
targ_to_color[target.name] = plt.cm.cool(ci)
if show_night:
Expand All @@ -267,7 +267,8 @@ def plot_schedule_airmass(schedule, show_night=False):
fc=targ_to_color[block.target.name], lw=0, alpha=.6)
else:
plt.axvspan(block.start_time.plot_date, block.end_time.plot_date,
color='k', lw=0, alpha=.6)
color='k')
plt.axhline(3, color='k', label='Transitions')
# TODO: make this output a `axes` object


Expand Down
91 changes: 63 additions & 28 deletions astroplan/scheduling.py
Expand Up @@ -15,6 +15,7 @@
from astropy.table import Table

from .utils import time_grid_from_range, stride_array
from .constraints import AltitudeConstraint

__all__ = ['ObservingBlock', 'TransitionBlock', 'Schedule', 'Slot', 'Scheduler',
'SequentialScheduler', 'PriorityScheduler', 'Transitioner', 'Scorer']
Expand Down Expand Up @@ -166,10 +167,10 @@ def __init__(self, components, start_time=None):
start_time : `~astropy.units.Quantity`
Start time of observation
"""
self._components = None
self.duration = None
self.start_time = start_time
self.components = components
self._components = None

def __repr__(self):
orig_repr = object.__repr__(self)
Expand Down Expand Up @@ -203,8 +204,7 @@ def components(self, val):
def from_duration(cls, duration):
# for testing how to put transitions between observations during
# scheduling without considering the complexities of duration
tb = TransitionBlock({None: 0*u.second})
tb.duration = duration
tb = TransitionBlock({'duration': duration})
return tb


Expand Down Expand Up @@ -232,14 +232,12 @@ def __init__(self, start_time, end_time, constraints=None):
self.start_time = start_time
self.end_time = end_time
self.slots = [Slot(start_time, end_time)]
self.slew_duration = 4*u.min
# TODO: replace/overwrite slew_duration with Transitioner calls
self.observer = None

def __repr__(self):
return 'Schedule containing ' + str(len(self.observing_blocks)) + \
' observing blocks between ' + str(self.slots[0].start.iso) + \
' and ' + str(self.slots[-1].end.iso)
return ('Schedule containing ' + str(len(self.observing_blocks)) +
' observing blocks between ' + str(self.slots[0].start.iso) +
' and ' + str(self.slots[-1].end.iso))

@property
def observing_blocks(self):
Expand All @@ -261,6 +259,7 @@ def to_table(self, show_transitions=True, show_unused=False):
durations = []
ra = []
dec = []
config = []
for slot in self.slots:
if hasattr(slot.block, 'target'):
start_times.append(slot.start.iso)
Expand All @@ -269,23 +268,28 @@ def to_table(self, show_transitions=True, show_unused=False):
target_names.append(slot.block.target.name)
ra.append(slot.block.target.ra)
dec.append(slot.block.target.dec)
config.append(slot.block.configuration)
elif show_transitions and slot.block:
start_times.append(slot.start.iso)
end_times.append(slot.end.iso)
durations.append(slot.duration.to(u.minute).value)
target_names.append('TransitionBlock')
ra.append('')
dec.append('')
changes = list(slot.block.components.keys())
changes.remove('slew_time')
config.append(changes)
elif slot.block is None and show_unused:
start_times.append(slot.start.iso)
end_times.append(slot.end.iso)
durations.append(slot.duration.to(u.minute).value)
target_names.append('Unused Time')
ra.append('')
dec.append('')
return Table([target_names, start_times, end_times, durations, ra, dec],
config.append('')
return Table([target_names, start_times, end_times, durations, ra, dec, config],
names=('target', 'start time (UTC)', 'end time (UTC)',
'duration (minutes)', 'ra', 'dec'))
'duration (minutes)', 'ra', 'dec', 'configuration'))

def new_slots(self, slot_index, start_time, end_time):
# this is intended to be used such that there aren't consecutive unoccupied slots
Expand All @@ -296,23 +300,23 @@ def insert_slot(self, start_time, block):
# due to float representation, this will change block start time
# and duration by up to 1 second in order to fit in a slot
for j, slot in enumerate(self.slots):
if (slot.start < start_time or np.abs(slot.start-start_time) < 1*u.second) \
and (slot.end > start_time):
if ((slot.start < start_time or abs(slot.start-start_time) < 1*u.second)
and (slot.end > start_time + 1*u.second)):
slot_index = j
if (block.duration - self.slots[slot_index].duration) > 1*u.second:
print(self.slots[slot_index].duration.to(u.second), block.duration)
raise ValueError('longer block than slot')
elif self.slots[slot_index].end - block.duration < start_time:
start_time = self.slots[slot_index].end - block.duration

if np.abs((self.slots[slot_index].duration - block.duration) < 1 * u.second):
if abs((self.slots[slot_index].duration - block.duration) < 1 * u.second):
block.duration = self.slots[slot_index].duration
start_time = self.slots[slot_index].start
end_time = self.slots[slot_index].end
elif np.abs(self.slots[slot_index].start - start_time) < 1*u.second:
elif abs(self.slots[slot_index].start - start_time) < 1*u.second:
start_time = self.slots[slot_index].start
end_time = start_time + block.duration
elif np.abs(self.slots[slot_index].end - start_time - block.duration) < 1*u.second:
elif abs(self.slots[slot_index].end - start_time - block.duration) < 1*u.second:
end_time = self.slots[slot_index].end
else:
end_time = start_time + block.duration
Expand Down Expand Up @@ -443,6 +447,7 @@ def __call__(self, blocks, schedule):
objects with populated ``start_time`` and ``end_time`` or ``duration`` attributes
"""
self.schedule = schedule
self.schedule.observer = self.observer
# these are *shallow* copies
copied_blocks = [copy.copy(block) for block in blocks]
schedule = self._make_schedule(copied_blocks)
Expand Down Expand Up @@ -507,6 +512,12 @@ def _make_schedule(self, blocks):
b._all_constraints = self.constraints
else:
b._all_constraints = self.constraints + b.constraints
# to make sure the scheduler has some constraint to work off of
# and to prevent scheduling of targets below the horizon
if b._all_constraints is None:
b._all_constraints = [AltitudeConstraint(min=0*u.deg)]
elif not any(isinstance(c, AltitudeConstraint) for c in b._all_constraints):
b._all_constraints.append(AltitudeConstraint(min=0*u.deg))
b._duration_offsets = u.Quantity([0*u.second, b.duration/2,
b.duration])
b.observer = self.observer
Expand Down Expand Up @@ -583,6 +594,12 @@ def _make_schedule(self, blocks):
b._all_constraints = self.constraints
else:
b._all_constraints = self.constraints + b.constraints
# to make sure the scheduler has some constraint to work off of
# and to prevent scheduling of targets below the horizon
if b._all_constraints is None:
b._all_constraints = [AltitudeConstraint(min=0*u.deg)]
elif not any(isinstance(c, AltitudeConstraint) for c in b._all_constraints):
b._all_constraints.append(AltitudeConstraint(min=0*u.deg))
b._duration_offsets = u.Quantity([0 * u.second, b.duration / 2, b.duration])
_block_priorities[i] = b.priority
_all_times.append(b.duration)
Expand Down Expand Up @@ -615,11 +632,21 @@ def _make_schedule(self, blocks):
# And then remove any times that are already scheduled
constraint_scores[is_open_time == False] = 0
# Select the most optimal time

# need to leave time around the Block for transitions
if self.transitioner.instrument_reconfig_times:
max_config_time = sum([max(value.values()) for value in
self.transitioner.instrument_reconfig_times.values()])
else:
max_config_time = 0*u.second
if self.transitioner.slew_rate:
buffer_time = (160*u.deg/self.transitioner.slew_rate + max_config_time)
else:
buffer_time = max_config_time
# TODO: make it so that this isn't required to prevent errors in slot creation
total_duration = b.duration + self.gap_time
total_duration = b.duration + buffer_time
# calculate the number of time slots needed for this exposure
_stride_by = np.int(np.ceil(total_duration / time_resolution))
_stride_by = np.int(np.ceil(float(total_duration / time_resolution)))

# Stride the score arrays by that number
_strided_scores = stride_array(constraint_scores, _stride_by)
Expand All @@ -644,10 +671,11 @@ def _make_schedule(self, blocks):

if _is_scheduled:
# set duration such that the Block will fit in the strided array
duration_indices = np.int(np.ceil(b.duration / time_resolution))
duration_indices = np.int(np.ceil(float(b.duration / time_resolution)))
b.duration = duration_indices * time_resolution
# add 1 second to the start time to allow for scheduling at the start of a slot
slot_index = [q for q, slot in enumerate(self.schedule.slots)
if slot.start < new_start_time < slot.end][0]
if slot.start < new_start_time + 1*u.second < slot.end][0]
slots_before = self.schedule.slots[:slot_index]
slots_after = self.schedule.slots[slot_index + 1:]
# this has to remake transitions between already existing ObservingBlocks
Expand All @@ -656,14 +684,14 @@ def _make_schedule(self, blocks):
# make a transition object after the previous ObservingBlock
tb = self.transitioner(self.schedule.slots[slot_index - 1].block, b,
self.schedule.slots[slot_index - 1].end, self.observer)
times_indices = np.int(np.ceil(tb.duration / time_resolution))
times_indices = np.int(np.ceil(float(tb.duration / time_resolution)))
tb.duration = times_indices * time_resolution
start_idx = self.schedule.slots[slot_index - 1].block.end_idx
end_idx = times_indices + start_idx
# this may make some OBs get sub-optimal scheduling, but it closes gaps
# TODO: determine a reasonable range inside which it gets shifted
if (new_start_time - tb.start_time < tb.duration or
Copy link
Contributor

@bmorris3 bmorris3 Aug 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lolwut ❓

np.abs(new_start_time - tb.end_time) < self.gap_time):
abs(new_start_time - tb.end_time) < self.gap_time):
new_start_time = tb.end_time
start_time_idx = end_idx
self.schedule.insert_slot(tb.start_time, tb)
Expand All @@ -674,13 +702,13 @@ def _make_schedule(self, blocks):
# change the existing TransitionBlock to what it needs to be now
tb = self.transitioner(self.schedule.slots[slot_index - 2].block, b,
self.schedule.slots[slot_index - 2].end, self.observer)
times_indices = np.int(np.ceil(tb.duration / time_resolution))
times_indices = np.int(np.ceil(float(tb.duration / time_resolution)))
tb.duration = times_indices * time_resolution
start_idx = self.schedule.slots[slot_index - 2].block.end_idx
end_idx = times_indices + start_idx
self.schedule.change_slot_block(slot_index - 1, new_block=tb)
if (new_start_time - tb.start_time < tb.duration or
np.abs(new_start_time - tb.end_time) < self.gap_time):
abs(new_start_time - tb.end_time) < self.gap_time):
new_start_time = tb.end_time
start_time_idx = end_idx
is_open_time[start_idx: end_idx] = False
Expand All @@ -691,7 +719,7 @@ def _make_schedule(self, blocks):
# make a transition object after the new ObservingBlock
tb = self.transitioner(b, self.schedule.slots[slot_index + 1].block,
new_start_time + b.duration, self.observer)
times_indices = np.int(np.ceil(tb.duration / time_resolution))
times_indices = np.int(np.ceil(float(tb.duration / time_resolution)))
tb.duration = times_indices * time_resolution
self.schedule.insert_slot(tb.start_time, tb)
start_idx = end_time_idx
Expand Down Expand Up @@ -729,7 +757,8 @@ def __init__(self, slew_rate=None, instrument_reconfig_times=None):
If not None, gives a mapping from property names to another
dictionary. The second dictionary maps 2-tuples of states to the
time it takes to transition between those states (as an
`~astropy.units.Quantity`).
`~astropy.units.Quantity`), can also take a 'default' key
mapped to a default transition time.
"""
self.slew_rate = slew_rate
self.instrument_reconfig_times = instrument_reconfig_times
Expand Down Expand Up @@ -776,19 +805,25 @@ def __call__(self, oldblock, newblock, start_time, observer):
if components:
return TransitionBlock(components, start_time)
else:
return None
return TransitionBlock.from_duration(0*u.second)

def compute_instrument_transitions(self, oldblock, newblock):
components = {}
for conf_name, old_conf in oldblock.configuration.items():
if conf_name in newblock:
if conf_name in newblock.configuration:
conf_times = self.instrument_reconfig_times.get(conf_name,
None)
if conf_times is not None:
new_conf = newblock[conf_name]
new_conf = newblock.configuration[conf_name]
ctime = conf_times.get((old_conf, new_conf), None)
def_time = conf_times.get('default', None)
if ctime is not None:
s = '{0}:{1} to {2}'.format(conf_name, old_conf,
new_conf)
components[s] = ctime
elif def_time and not old_conf == new_conf:
s = '{0}:{1} to {2}'.format(conf_name, old_conf,
new_conf)
components[s] = def_time

return components
2 changes: 2 additions & 0 deletions astroplan/target.py
Expand Up @@ -162,6 +162,8 @@ def _from_name_mock(cls, query_name, name=None):
"vega": {"ra": 279.23473479*u.deg, "dec": 38.78368896*u.deg},
"aldebaran": {"ra": 68.98016279*u.deg, "dec": 16.50930235*u.deg},
"polaris": {"ra": 37.95456067*u.deg, "dec": 89.26410897*u.deg},
"deneb": {"ra": 310.35797975*u.deg, "dec": 45.28033881*u.deg},
"m13": {"ra": 250.423475*u.deg, "dec": 36.4613194*u.deg},
"altair": {"ra": 297.6958273*u.deg, "dec": 8.8683212*u.deg}
}

Expand Down