-
Jörn Künsemöller authoredJörn Künsemöller authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
template_constraints_v1.py 15.79 KiB
#!/usr/bin/env python3
# dynamic_scheduling.py
#
# Copyright (C) 2020
# ASTRON (Netherlands Institute for Radio Astronomy)
# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands
#
# This file is part of the LOFAR software suite.
# The LOFAR software suite is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# The LOFAR software suite is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>.
#
# $Id: $
"""
"""
import logging
logger = logging.getLogger(__name__)
from datetime import datetime, timedelta
from dateutil import parser
from lofar.sas.tmss.tmss.tmssapp import models
from lofar.sas.tmss.tmss.tmssapp.conversions import create_astroplan_observer_for_station, Time, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies
from . import ScoredSchedulingUnit
def can_run_within_timewindow(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
'''determine if the given scheduling_unit can run withing the given timewindow evaluating all constraints from the "constraints" version 1 template'''
if has_manual_scheduler_constraint(scheduling_unit):
logger.info("### SchedulingUnitBlueprint id=%s has manual scheduler constraint and cannot be dynamically scheduled." % (scheduling_unit.id))
return False
if not can_run_within_timewindow_with_time_constraints(scheduling_unit, lower_bound, upper_bound):
logger.info("### SchedulingUnitBlueprint id=%s does not meet time constraints between %s and %s." % (scheduling_unit.id, lower_bound, upper_bound))
return False
if not can_run_within_timewindow_with_sky_constraints(scheduling_unit, lower_bound, upper_bound):
logger.info("### SchedulingUnitBlueprint id=%s does not meet sky constraints between %s and %s." % (scheduling_unit.id, lower_bound, upper_bound))
return False
if not can_run_within_timewindow_with_daily_constraints(scheduling_unit, lower_bound, upper_bound):
logger.info("### SchedulingUnitBlueprint id=%s does not meet daily constraints between %s and %s." % (scheduling_unit.id, lower_bound, upper_bound))
return False
return True
def can_run_after(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime) -> bool:
'''Check if the given scheduling_unit can run somewhere after the given lowerbound timestamp depending on the sub's constrains-template/doc.'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
if 'before' in constraints['time']:
before = parser.parse(constraints['time']['before'], ignoretz=True)
return before > lower_bound
return True
# only expose the can_run_within_timewindow and can_run_after methods, and keep the details hidden for this module's importers who do not need these implemnetation details
__all__ = ['can_run_within_timewindow', 'can_run_after']
def has_manual_scheduler_constraint(scheduling_unit: models.SchedulingUnitBlueprint) -> bool:
'''evaluate the scheduler contraint. Should this unit be manually scheduled?'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
return constraints.get('scheduler', '') == 'manual'
def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
'''evaluate the daily contraint'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
if constraints['daily']['require_day'] or constraints['daily']['require_night'] or constraints['daily']['avoid_twilight']:
if (upper_bound - lower_bound).days >= 1:
logger.info("### SchedulingUnitBlueprint id=%s has daily constraints, but bounds span %s" % (scheduling_unit.id, (upper_bound - lower_bound)))
return False
if upper_bound < lower_bound:
raise ValueError("Provided upper_bound=%s is earlier than provided lower_bound=%s" % (upper_bound, lower_bound))
stations = scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['stations']
# check contraint and return false on first failure
for station in stations:
# get day/night times for bounds
# we could sample in between bounds, but will instead do some checks
if constraints['daily']['require_day'] and lower_bound.date() != upper_bound.date():
logger.info("### SchedulingUnitBlueprint id=%s cannot meet require_day constraint when starting and ending on different days." % scheduling_unit.id)
return False
timestamps = [lower_bound, upper_bound]
sun_events = timestamps_and_stations_to_sun_rise_and_set(timestamps=tuple(timestamps), stations=(station,))[station]
if constraints['daily']['require_day']:
for i in range(len(timestamps)):
if timestamps[i] < sun_events['day'][i]['start'] or timestamps[i] > sun_events['day'][i]['end']:
logger.info("### %s not between %s and %s" % (timestamps[i], sun_events['day'][i]['start'], sun_events['day'][i]['end']))
logger.info("### SchedulingUnitBlueprint id=%s does not meet require_day constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i]))
return False
else:
logger.info("### %s between %s and %s" % (timestamps[i], sun_events['day'][i]['start'], sun_events['day'][i]['end'] ))
if constraints['daily']['require_night']:
if sun_events['night'][0]['start'].date() != sun_events['night'][1]['start'].date():
logger.info("### SchedulingUnitBlueprint id=%s cannot meet require_night constraint when starting and ending in different nights." % scheduling_unit.id)
return False
for i in range(len(timestamps)):
if timestamps[i] < sun_events['night'][i]['start'] or timestamps[i] > sun_events['night'][i]['end']:
logger.info("### SchedulingUnitBlueprint id=%s does not meet require_night constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i]))
return False
if constraints['daily']['avoid_twilight']:
# Note: the same index for sun_events everywhere is not a typo, but to make sure it's the _same_ night or day for both bounds or obs will span over twilight
if not (timestamps[0] > sun_events['day'][0]['start'] and timestamps[0] < sun_events['day'][0]['end'] and
timestamps[1] > sun_events['day'][0]['start'] and timestamps[1] < sun_events['day'][0]['end']) or \
(timestamps[0] > sun_events['night'][0]['start'] and timestamps[0] < sun_events['night'][0]['end'] and
timestamps[1] > sun_events['night'][0]['start'] and timestamps[1] < sun_events['night'][0]['end']):
logger.info("### SchedulingUnitBlueprint id=%s does not meet avoid_twilight constraint." % scheduling_unit.id)
return False
logger.info('### SchedulingUnitBlueprint id=%s meets all daily constraints. Returning True.')
return True
def can_run_within_timewindow_with_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
'''evaluate the time contraint(s)'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
# TODO: TMSS-244 (and more?), evaluate the constraints in constraints['time']
if has_manual_scheduler_constraint(scheduling_unit):
at = parser.parse(constraints['time']['at'], ignoretz=True)
return at >= lower_bound and at+scheduling_unit.duration <= upper_bound # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration']
if 'before' in constraints['time']:
before = parser.parse(constraints['time']['before'], ignoretz=True)
return before <= upper_bound-scheduling_unit.duration # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration']
if 'after' in constraints['time']:
after = parser.parse(constraints['time']['after'], ignoretz=True)
return lower_bound >= after
# if 'between' in constraints['time']:
# betweens = [ dateutil.parser.parse(constraints['time']['between'])
# return lower_bound >= after
return True # for now, ignore time contraints.
def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
'''evaluate the time contraint(s)'''
logger.info('### can_run_within_timewindow_with_sky_constraints called with lower_bound=%s upper_bound=%s '% (lower_bound, upper_bound))
constraints = scheduling_unit.draft.scheduling_constraints_doc
# TODO: TMSS-245 TMSS-250 (and more?), evaluate the constraints in constraints['sky']
# maybe even split this method into sub methods for the very distinct sky constraints: min_calibrator_elevation, min_target_elevation, transit_offset & min_distance
beam = scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['tile_beam']
angle1 = beam['angle1']
angle2 = beam['angle2']
direction_type = beam['direction_type']
if "sky" in constraints and 'min_distance' in constraints['sky']:
# currently we only check at bounds, we probably want to add some more samples in between later on
distances = coordinates_and_timestamps_to_separation_from_bodies(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=(lower_bound, upper_bound), bodies=tuple(constraints['sky']['min_distance'].keys()))
for body, min_distance in constraints['sky']['min_distance'].items():
timestamps = distances[body]
for timestamp, angle in timestamps.items():
if angle.rad < min_distance:
logger.info('### Distance=%s from body=%s does not meet min_distance=%s constraint at timestamp=%s' % (angle.rad, body, min_distance, timestamp))
return False
return True
def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime) -> datetime:
constraints = scheduling_unit.draft.scheduling_constraints_doc
duration = timedelta(seconds=scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration'])
try:
if has_manual_scheduler_constraint(scheduling_unit) and 'at' in constraints['time']:
at = parser.parse(constraints['time']['at'], ignoretz=True)
return at
if 'after' in constraints['time']:
return parser.parse(constraints['time']['after'], ignoretz=True)
if constraints['daily']['require_day'] or constraints['daily']['require_night'] or constraints['daily']['avoid_twilight']:
stations = scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['stations']
all_sun_events = timestamps_and_stations_to_sun_rise_and_set(timestamps=(lower_bound,lower_bound+timedelta(days=1)), stations=tuple(stations))
start_time_per_station = {}
for station in stations:
sun_events = all_sun_events[station]
day = sun_events['day'][0]
night = sun_events['night'][0]
next_day = sun_events['day'][1]
next_night = sun_events['night'][1]
if constraints['daily']['require_day']:
# TODO: Do we need to check for observations that are too long and can e.g. only be run in summer?
if lower_bound + duration > day['end']:
start_time_per_station[station] = next_day['start']
continue
if lower_bound >= day['start']:
start_time_per_station[station] = lower_bound
continue
start_time_per_station[station] = day['start']
continue
if constraints['daily']['require_night']:
if lower_bound + duration > night['end']:
start_time_per_station[station] = next_night['start']
continue
if lower_bound >= night['start']:
start_time_per_station[station] = lower_bound
continue
start_time_per_station[station] = night['start']
continue
if constraints['daily']['avoid_twilight']:
if lower_bound + duration < day['end']:
if lower_bound >= day['start']:
start_time_per_station[station] = lower_bound
continue
start_time_per_station[station] = day['start']
continue
if lower_bound + duration < night['end']:
if lower_bound >= night['start']:
start_time_per_station[station] = lower_bound
continue
start_time_per_station[station] = night['start']
continue
start_time_per_station[station] = next_day['start']
continue
return max(start_time_per_station.values())
except Exception as e:
logger.exception(str(e))
# no constraints dictating starttime? make a guesstimate.
return lower_bound
def compute_scores(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound:datetime, upper_bound:datetime) -> ScoredSchedulingUnit:
'''Compute the "fitness" scores per constraint for the given scheduling_unit at the given starttime depending on the sub's constrains-template/doc.'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
# TODO: add compute_scores methods for each type of constraint
# TODO: take start_time into account. For example, an LST constraint yields a better score when the starttime is such that the center of the obs is at LST.
# TODO: TMSS-??? (and more?), compute score using the constraints in constraints['daily']
# TODO: TMSS-244 (and more?), compute score using the constraints in constraints['time']
# TODO: TMSS-245 TMSS-250 (and more?), compute score using the constraints in constraints['sky']
# for now (as a proof of concept and sort of example), just return 1's
scores = {'daily': 1.0,
'time': 1.0,
'sky': 1.0 }
# add "common" scores which do not depend on constraints, such as project rank and creation date
# TODO: should be normalized!
scores['project_rank'] = scheduling_unit.draft.scheduling_set.project.priority_rank
#scores['age'] = (datetime.utcnow() - scheduling_unit.created_at).total_seconds()
try:
# TODO: apply weights. Needs some new weight model in django, probably linked to constraints_template.
# for now, just average the scores
weighted_score = sum(scores.values())/len(scores)
except:
weighted_score = 1
return ScoredSchedulingUnit(scheduling_unit=scheduling_unit,
scores=scores,
weighted_score=weighted_score,
start_time=get_earliest_possible_start_time(scheduling_unit, lower_bound))