Scheduler.py 15.8 KB
from operator import attrgetter

from scheduling.templatetags.jdconverter import jdtodate
from .UserManager import UserQuotaManager
from .Interval import *
from django.db.models import Q

from seq_submit.models import Sequence, Schedule

SIMULATION = False
DEBUG_FILE = False

class Scheduler(IntervalManagement):
    REJECTED_ROOM = "Insufficient room for this sequence"

    def __init__(self):
        super().__init__("Scheduler", "Scheduler")
        self.schedule = Schedule.objects.create()
        self.sequences = []

    def log(self, message: str):
        self.logger.info(message)

    def getNightLimits(self) -> tuple:
        start = getNightStart()
        end = getNightEnd()
        return (start, end)

    def setNightLimits(self, plan_start: float, plan_end: float) -> int:
        self.schedule.plan_start = Decimal(plan_start)
        self.schedule.plan_end = Decimal(plan_end)
        return 0

    #TODO:
    def isFirstSchedule(self) -> bool:
        return False

    def determinePlanStart(self, previous_sched):
        start = secondsToPreciseJulianDate(getPreciseCurrentTime())
        if start > previous_sched.plan_start + self.max_overhead:
            return start + self.max_overhead
        return previous_sched.plan_start

    '''
        Copy information from current night previous schedule
    '''
    def copyFromPrevious(self) -> int:
        if len(Schedule.objects.all()) == 1:
            print("only 1 schedule available")
            self.schedule.plan_night_start = self.schedule.plan_start
            if DEBUG_FILE:
                self.log("No schedule found")
            return 1
        try:
            previous_sched = Schedule.objects.order_by('-created')[1]
            previous_exc_seq = previous_sched.sequences.filter(Q(status=Sequence.EXECUTED) |
                                                               Q(status=Sequence.EXECUTING))
        except:
            self.schedule.plan_night_start = self.schedule.plan_start
            if DEBUG_FILE:
                self.debug("Scheduler could not get information from previous schedule")
            return 1
        for seq in previous_exc_seq:
            shs = seq.shs.latest("schedule__created")
            shs.pk = None
            shs.schedule = self.schedule
            shs.save()
        adder = 0
        try:
            executing = Sequence.objects.filter(status=Sequence.EXECUTING).get()
            if executing:
                s = executing.shs.latest("schedule__created")
                adder = s.tep - secondsToPreciseJulianDate(getPreciseCurrentTime())
        except Exception as e:
            self.log("No executing sequence found " + str(e))
        self.schedule.plan_night_start = previous_sched.plan_night_start
        self.schedule.plan_end = previous_sched.plan_end
        self.schedule.plan_start = self.determinePlanStart(previous_sched) + adder
        return 0

    '''
        used for tests (entry point)
    '''
    def simulateSchedule(self, sequences) -> tuple:
        global SIMULATION
        SIMULATION = True

        self.schedule.plan_night_start = self.schedule.plan_start
        self.sequences = list(sequences)
        shs_list = []
        for sequence in self.sequences:
            shs_list.append(ScheduleHasSequences(sequence=sequence, schedule=self.schedule))
        self.sequences = [(sequence, shs_list[index]) for index, sequence in enumerate(self.sequences)]
        self.computeSchedule()
        return (self.schedule, self.sequences)

    def computeSchedule(self) -> int:
        #print("In computeSchedule")
        interval = Interval(self.schedule.plan_start, self.schedule.plan_end)
        self.intervals.append(interval)
        if DEBUG_FILE:
            self.log("Interval created : " + str(interval.__dict__))
        self.removeInvalidSequences()
        self.determinePriorities()
        print("Sequences bef removeNonEligible are:", len(self.sequences))
        # EP le pb est ici !!!
        #TODO: bugfix time computation EP a virer (remettre)
        self.removeNonEligible()
        print("Sequences aft removeNonEligible are:", len(self.sequences))
        self.sortSequences()
        self.placeSequences()
        return 0

    '''
        Default entry point (called from scheduler/tasks.py/scheduling/run())
    '''
    def makeSchedule(self) -> Schedule:
        print("In makeSchedule()")
        global SIMULATION
        # WHY ???
        SIMULATION = False

        if self.isFirstSchedule():
            #print("It is the first schedule")
            self.schedule.plan_night_start = self.schedule.plan_start
        else:
            #print("It is not the first schedule")
            self.copyFromPrevious()          #TODO trycatch a faire
            self.schedule.plan_night_start = self.schedule.plan_start
            print("night start is", self.schedule.plan_night_start)

        #EP: avirer (add 2 hours)
        #self.schedule.plan_night_start += float( (1/24)*2 )
        #self.schedule.plan_start += float( (1/24)*2 )

        # Get all sequences which are PLANNED, TOBEPLANNED, or PENDING
        self.sequences = list(Sequence.objects.filter(Q(status=Sequence.PLANNED) | Q(status=Sequence.TOBEPLANNED)
                                                      | Q(status=Sequence.PENDING)))
        print("**** nb of sequences already available for scheduling", len(self.sequences))

        # List of tuples (sequence, ScheduleHasSequences) for each sequence above and for current schedule
        self.sequences = [
            (
                sequence,
                ScheduleHasSequences(sequence=sequence, schedule=self.schedule)
            )
            for sequence in self.sequences
        ]
        print("Sequences BEFORE are:", len(self.sequences))
        if DEBUG_FILE:
            self.log(str(len(self.sequences)) + " sequences found")
        self.computeSchedule()
        print("Sequences AFTER are:", len(self.sequences))
        self.saveSchedule()
        if DEBUG_FILE:
            self.log("Saving schedule with " + str(len(self.schedule.sequences.all())) + " sequences")
        return self.schedule

    def saveSchedule(self) -> int:
        self.schedule.save()
        for sequence, shs in self.sequences:
            sequence.status = Sequence.PLANNED
            shs.schedule = self.schedule
            sequence.save()
            shs.save()
        if DEBUG_FILE:
            self.logSchedule()
        return 0


    '''
        JB: Using : list(self.sequences) makes a copy.
    '''
    def removeNonEligible(self) -> int:
        for sequence, shs in list(self.sequences):
            overlap = Decimal(min(self.schedule.plan_end, sequence.jd2)) - Decimal(max(self.schedule.plan_start, sequence.jd1)) - self.max_overhead
            print()
            print("- sequence: ", sequence)
            print("plan start - plan end:", jdtodate(self.schedule.plan_start), "-", jdtodate(self.schedule.plan_end))
            print("jd1-jd2:", jdtodate(sequence.jd1), "-", jdtodate(sequence.jd2))
            print("overlap < duration ?:", overlap, sequence.duration)
            #print("- sequence: ", sequence, "plan start, end, jd1, jd2, overlap", self.schedule.plan_start, self.schedule.plan_end, sequence.jd1, sequence.jd2, overlap)
            #print("- sequence: ", sequence, "plan start", jdtodate(self.schedule.plan_start))
            print()
            if overlap < sequence.duration:
                if sequence.jd1 < self.schedule.plan_start:
                    sequence.status = Sequence.UNPLANNABLE
                    if not SIMULATION:
                        sequence.save()
                self.sequences.remove((sequence, shs))
                if DEBUG_FILE:
                    self.log("Removing non eligible sequence")
        return 0

    def removeInvalidSequences(self):
        for sequence,shs in list(self.sequences):
            if (sequence.jd1 < 0 or sequence.jd2 < 0 or
                    is_nearby_less_or_equal(sequence.duration, Decimal(0)) or
                    sequence.jd2 - sequence.jd1 < sequence.duration):
                if DEBUG_FILE:
                    self.log("Removing sequence in removeInvalidSequences")
                self.sequences.remove((sequence, shs))
                sequence.status = Sequence.INVALID
                if not SIMULATION:
                    sequence.save()
        return 0

    def updateOtherDeltas(self, sequence: Sequence, shs: ScheduleHasSequences, interval: Interval) -> int:
        seq_before, shs_b_i = self.findSequenceBefore(interval)
        seq_after, shs_a_i = self.findSequenceAfter(interval)

        if seq_before and shs_b_i:
            shs_b_i.deltaTR = min(shs.tsp - self.max_overhead, seq_before.jd2) - shs_b_i.tep
        if seq_after and shs_a_i:
            shs_a_i.deltaTL = shs_a_i.tsp - self.max_overhead - max(shs.tep, seq_after.jd1)
        return 0

    '''
        Function who place all the sequences in intervals
    '''
    def placeSequences(self) -> int:
        #print("In placeSequences()")
        #print("sequences are", self.sequences)
        for sequence, shs in list(self.sequences):
            #print("placeSequences() sequence is", sequence)
            quota = UserQuotaManager.determineQuota(sequence)
            #print("quota is", quota)
            if not UserQuotaManager.isSufficient(quota, sequence):
                shs.status = Sequence.REJECTED
                shs.desc = UserQuotaManager.REJECTED
                continue
            matching_intervals = self.getMatchingIntervals(sequence)
            if len(matching_intervals) > 0:
                inter = self.placeSequence(sequence, shs, matching_intervals)
                if inter:
                    self.updateOtherDeltas(sequence, shs, inter)
                sequence_placed = True
            else:
                sequence_placed = self.tryShiftingSequences(sequence, shs)
            if sequence_placed:
                shs.status = Sequence.PLANNED
                self.decreaseQuota(sequence, sequence.duration)
            else:
                if DEBUG_FILE:
                    self.log("Removing sequence in place_sequences")
                shs.status = Sequence.REJECTED
                shs.desc = self.REJECTED_ROOM
        return 0

    def findSequenceBefore(self, interval: Interval):
        for seq, s in self.sequences:
            if s.status == Sequence.PLANNED:
                if is_nearby_equal(s.tep, interval.start):
                    return (seq, s)
        return (None, None)

    def findSequenceAfter(self, interval: Interval):
        for seq, s in self.sequences:
            if s.status == Sequence.PLANNED:
                if is_nearby_equal(s.tsp - self.max_overhead, interval.end):
                    return (seq, s)
        return (None, None)

    '''
        pm(l/r) = Possible Move (Left / Right)
        shs_(b/a)_i = shs (before/after) interval
    '''
    def tryShiftingSequences(self, sequence: Sequence, shs: ScheduleHasSequences) -> bool:
        potential = self.getPotentialIntervals(sequence)
        potential.sort(key=attrgetter("duration"), reverse=True) #sort the list by decreasing duration
        pml = Decimal(0)
        pmr = Decimal(0)

        for interval in potential:
            seq_before, shs_b_i = self.findSequenceBefore(interval)
            seq_after, shs_a_i = self.findSequenceAfter(interval)

            available = min(interval.end, sequence.jd2) - max(interval.start, sequence.jd1)
            missing = sequence.duration - available + self.max_overhead
            if seq_before and shs_b_i:
                pml = min(shs_b_i.deltaTL, interval.start - sequence.jd1)
            if seq_after and shs_a_i:
                pmr = min(shs_a_i.deltaTR, sequence.jd2 - interval.end)

            if is_nearby_sup_or_equal(pml, missing):
                self.moveSequence(seq_before, shs_b_i, missing, "LEFT")
            elif is_nearby_sup_or_equal(pmr, missing):
                self.moveSequence(seq_after, shs_a_i, missing, "RIGHT")
            elif is_nearby_sup_or_equal(pml + pmr, missing):
                self.moveSequence(seq_before, shs_b_i, pml, "LEFT")
                self.moveSequence(seq_after, shs_a_i, missing - pml, "RIGHT")
            else:
                continue
            matching = self.getMatchingIntervals(sequence)
            if len(matching) != 1:
                raise ValueError("There should be one and only one matching interval after shifting")
            inter = self.placeSequence(sequence, shs, matching)
            if inter:
                self.updateOtherDeltas(sequence, shs, inter)
            return True
        return False

    '''
        Move the sequence tsp (time start planned) and tep (time end planned) left or right
    '''
    def moveSequence(self, sequence: Sequence, shs: ScheduleHasSequences, time_shift: Decimal, direction: str) -> int:
        interval_before = None
        interval_after = None

        if direction not in ["LEFT", "RIGHT"]:
            return 1
        if time_shift > (shs.deltaTL if direction == "LEFT" else shs.deltaTR):
            return 1
        for interval in self.intervals:
            if is_nearby_equal(interval.end, shs.tsp - self.max_overhead):
                interval_before = interval
            elif is_nearby_equal(interval.start, shs.tep):
                interval_after = interval
        if direction == "LEFT":
            interval_before.end -= time_shift
            if interval_after:
                interval_after.start -= time_shift
            shs.tsp -= time_shift
            shs.tep -= time_shift
            shs.deltaTL -= time_shift
            shs.deltaTR += time_shift
        else:
            if interval_before:
                interval_before.end += time_shift
            interval_after.start += time_shift
            shs.tsp += time_shift
            shs.tep += time_shift
            shs.deltaTL += time_shift
            shs.deltaTR -= time_shift
        if interval_after:
            if is_nearby_less_or_equal(interval_after.duration, self.max_overhead):
                self.intervals.remove(interval_after)
        if interval_before:
            if is_nearby_less_or_equal(interval_before.duration, self.max_overhead):
                self.intervals.remove(interval_before)
        return 0

    '''
        Sort by jd2 and priority -> (main sorting value)
    '''
    def sortSequences(self) -> int:
        self.sequences.sort(key=lambda x: x[0].jd2)
        self.sequences.sort(key=lambda x: x[0].priority if x[0].priority else 0)
        return 0

    def determinePriorities(self) -> int: #TODO
        return 0

    def decreaseQuota(self, sequence: Sequence, quota: float) -> int:
        user = UserQuotaManager(sequence.request.pyros_user)
        if SIMULATION:
            return 0
        return user.decreaseQuota(Decimal(quota))

    def isEmptySchedule(self) -> bool:
        if len(self.sequences) == 0:
            return True
        return False

    '''
        DEBUG FUNCTIONS
    '''
    def logSequence(self, sequence):
        self.log("Logging sequence : ")
        s = sequence.shs.latest("schedule__created")
        if s.schedule == self.schedule:
            self.log("--> name: %r, start: %f, end: %f, duration: %f, deltaTL: %f, deltaTR: %f"
                  % (sequence.name, s.tsp, s.tep, sequence.duration, s.deltaTL, s.deltaTR))
        self.log("------ end ------")
        return 0

    def logSchedule(self) -> int:
        sequences = Sequence.objects.filter(shs__status=Sequence.PLANNED).order_by('shs__tsp').distinct()
        self.log("There are %d sequence(s) planned" % len(sequences))
        for sequence in sequences:
            s = sequence.shs.latest("schedule__created")
            self.log("--> Pk: %d name: %r, shs PK: %d, start: %f, end: %f, duration: %f, deltaTL: %f, deltaTR: %f"
                  % (sequence.pk, sequence.name, s.pk, s.tsp, s.tep, sequence.duration, s.deltaTL, s.deltaTR))
        self.log("There are %d free intervals" % len(self.intervals))
        for interval in self.intervals:
            self.log("--> start: %f, end: %f" % (interval.start, interval.end))
        return 0