Skip to content
Snippets Groups Projects
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))