from operator import attrgetter from .UserManager import UserManager from .Interval import * from django.db.models import Q 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 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: 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: 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() self.removeNonEligible() self.sortSequences() self.placeSequences() return 0 ''' Default entry point (called from scheduler/tasks.py/scheduling/run()) ''' def makeSchedule(self) -> Schedule: global SIMULATION SIMULATION = False if self.isFirstSchedule(): self.schedule.plan_night_start = self.schedule.plan_start else: self.copyFromPrevious() #TODO trycatch a faire self.schedule.plan_night_start = self.schedule.plan_start # List of sequences (PLANNED, TOBEPLANNED, PENDING) self.sequences = list(Sequence.objects.filter(Q(status=Sequence.PLANNED) | Q(status=Sequence.TOBEPLANNED) | Q(status=Sequence.PENDING))) # 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 ] if DEBUG_FILE: self.log(str(len(self.sequences)) + " sequences found") self.computeSchedule() 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 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: for sequence, shs in list(self.sequences): quota = UserManager.determineQuota(sequence) if not UserManager.isSufficient(quota, sequence): shs.status = Sequence.REJECTED shs.desc = UserManager.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 = UserManager(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