#!/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 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): return False if not can_run_within_timewindow_with_time_constraints(scheduling_unit, lower_bound, upper_bound): return False if not can_run_within_timewindow_with_sky_constraints(scheduling_unit, lower_bound, upper_bound): return False if not can_run_within_timewindow_with_daily_constraints(scheduling_unit, 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 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 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 if 'before' in constraints['time']: before = parser.parse(constraints['time']['before'], ignoretz=True) return before <= upper_bound-scheduling_unit.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)''' 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 has_manual_scheduler_constraint(scheduling_unit): 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']: # 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))