-
Jorrit Schaap authoredJorrit Schaap authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
template_constraints_v1.py 9.22 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 lofar.common.datetimeutils import parseDatetime
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
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'''
return all([has_online_scheduler_constraint(scheduling_unit),
can_run_within_timewindow_with_time_constraints(scheduling_unit, lower_bound, upper_bound),
can_run_within_timewindow_with_sky_constraints(scheduling_unit, lower_bound, upper_bound),
can_run_within_timewindow_with_daily_constraints(scheduling_unit, lower_bound, upper_bound)])
# only expose the can_run_within_timewindow method, and keep the details hidden for this module's importers who do not need these implemnetation details
__all__ = ['can_run_within_timewindow']
def has_online_scheduler_constraint(scheduling_unit: models.SchedulingUnitBlueprint) -> bool:
'''evaluate the scheduler contraint'''
constraints = scheduling_unit.draft.scheduling_constraints_doc
return constraints.get('scheduler', '') == 'online'
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 not (constraints['daily']['require_day'] and constraints['daily']['require_night']):
# no day/night restrictions, can run any time
return True
if constraints['daily']['require_day'] or constraints['daily']['require_night']:
# TODO: TMSS-254 and TMSS-255
# TODO: take avoid_twilight into account
# Please note that this first crude proof of concept treats sunset/sunrise as 'events',
# whereas in our definition they are transition periods. See: TMSS-435
# Ugly code. Should be improved. Works for demo.
# create a series of timestamps in the window of opportunity, and evaluate of there are all during day or night
possible_start_time = get_earliest_possible_start_time(scheduling_unit, lower_bound)
# ToDo: use specified total observation duration, and ignore pipelines who don't care about day/night
possible_stop_time = possible_start_time + scheduling_unit.duration
timestamps = [possible_start_time]
while timestamps[-1] < possible_stop_time - timedelta(hours=8):
timestamps.append(timestamps[-1] + timedelta(hours=8))
timestamps.append(possible_stop_time)
LOFAR_CENTER_OBSERVER = create_astroplan_observer_for_station('CS002')
if constraints['daily']['require_night'] and all(LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
return True
if constraints['daily']['require_day'] and all(not LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
return True
return False
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 'before' in constraints['time']:
before = parseDatetime(constraints['time']['before'].replace('T', ' ').replace('Z', ''))
return upper_bound <= before
if 'after' in constraints['time']:
after = parseDatetime(constraints['time']['after'].replace('T', ' ').replace('Z', ''))
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)'''
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
return True # for now, ignore sky contraints.
def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime) -> datetime:
constraints = scheduling_unit.draft.scheduling_constraints_doc
try:
if 'after' in constraints['time']:
return parseDatetime(constraints['time']['after'].replace('T', ' ').replace('Z', ''))
if constraints['daily']['require_day'] or constraints['daily']['require_night']:
# TODO: TMSS-254 and TMSS-255
# TODO: take avoid_twilight into account
# for now, use the incorrect proof of concept which works for the demo
# but... this should be rewritten completely using Joerns new sun_events
LOFAR_CENTER_OBSERVER = create_astroplan_observer_for_station('CS002')
sun_events = timestamps_and_stations_to_sun_rise_and_set(timestamps=[lower_bound], stations=['CS002'])['CS002']
sun_set = sun_events['sunset'][0]['start']
sun_rise = sun_events['sunrise'][0]['end']
if constraints['daily']['require_day']:
if lower_bound+scheduling_unit.duration > sun_set:
return LOFAR_CENTER_OBSERVER.sun_rise_time(time=Time(sun_set), which='next').to_datetime()
if lower_bound >= sun_rise:
return lower_bound
return sun_rise
if constraints['daily']['require_night']:
if lower_bound+scheduling_unit.duration < sun_rise:
return lower_bound
if lower_bound >= sun_set:
return lower_bound
return sun_set
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))