Commit 06241f058d4410332bf709412bcf4abe68fb39b4
1 parent
7a1effdd
Exists in
master
and in
1 other branch
Class scheduler finished <== TO BE TESTED
Showing
3 changed files
with
512 additions
and
10 deletions
Show diff stats
src/pyros/settings.py
src/pyrosapp/models.py
... | ... | @@ -267,8 +267,8 @@ class Request(models.Model): |
267 | 267 | |
268 | 268 | class Schedule(models.Model): |
269 | 269 | created = models.DateTimeField(blank=True, null=True, auto_now_add=True) |
270 | - day_start = models.DateTimeField(blank=True, null=True) | |
271 | - day_stop = models.DateTimeField(blank=True, null=True) | |
270 | + plan_start = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
271 | + plan_end = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
272 | 272 | flag = models.CharField(max_length=45, blank=True, null=True) |
273 | 273 | |
274 | 274 | class Meta: |
... | ... | @@ -282,8 +282,8 @@ class Schedule(models.Model): |
282 | 282 | class ScheduleHistory(models.Model): |
283 | 283 | sequences = models.ManyToManyField('Sequence', related_name='schedulehistories') |
284 | 284 | created = models.DateTimeField(blank=True, null=True, auto_now_add=True) |
285 | - day_start = models.DateTimeField(blank=True, null=True) | |
286 | - day_stop = models.DateTimeField(blank=True, null=True) | |
285 | + plan_start = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
286 | + plan_end = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
287 | 287 | flag = models.CharField(max_length=45, blank=True, null=True) |
288 | 288 | |
289 | 289 | class Meta: |
... | ... | @@ -310,6 +310,24 @@ class ScientificProgram(models.Model): |
310 | 310 | return (str(self.name)) |
311 | 311 | |
312 | 312 | class Sequence(models.Model): |
313 | + | |
314 | + """ Definition of Status enum values """ | |
315 | + | |
316 | + TOBEPLANNED = "TBP" | |
317 | + OBSERVABLE = "OBS" | |
318 | + UNPLANNABLE = "UNPLN" | |
319 | + PLANNED = "PLND" | |
320 | + EXECUTED = "EXD" | |
321 | + REJECTED = "RJTD" | |
322 | + STATUS_CHOICES = ( | |
323 | + (TOBEPLANNED, "To be planned"), | |
324 | + (OBSERVABLE, "Observable"), | |
325 | + (UNPLANNABLE, "Unplannable"), | |
326 | + (PLANNED, "Planned"), | |
327 | + (EXECUTED, "Executed"), | |
328 | + (REJECTED, "Rejected") | |
329 | + ) | |
330 | + | |
313 | 331 | request = models.ForeignKey(Request, models.CASCADE, related_name="sequences") |
314 | 332 | sequencetype = models.ForeignKey('SequenceType', models.DO_NOTHING, related_name="sequences") |
315 | 333 | schedule = models.ForeignKey(Schedule, models.DO_NOTHING, related_name="sequences") |
... | ... | @@ -318,14 +336,11 @@ class Sequence(models.Model): |
318 | 336 | created = models.DateTimeField(blank=True, null=True, auto_now_add=True) |
319 | 337 | updated = models.DateTimeField(blank=True, null=True, auto_now=True) |
320 | 338 | is_alert = models.BooleanField(default=False) |
321 | - status = models.CharField(max_length=11, blank=True, null=True) | |
322 | - duration = models.FloatField(blank=True, null=True) | |
339 | + status = models.CharField(max_length=11, blank=True, null=True, choices=STATUS_CHOICES) | |
323 | 340 | pointing = models.CharField(max_length=45, blank=True, null=True) |
324 | 341 | with_drift = models.BooleanField(default=False) |
325 | 342 | priority = models.IntegerField(blank=True, null=True) |
326 | 343 | analysis_method = models.CharField(max_length=45, blank=True, null=True) |
327 | - exec_start = models.DateTimeField() | |
328 | - exec_stop = models.DateTimeField(blank=True, null=True) | |
329 | 344 | moon_min = models.IntegerField(blank=True, null=True) |
330 | 345 | alt_min = models.IntegerField(blank=True, null=True) |
331 | 346 | type = models.CharField(max_length=6, blank=True, null=True) |
... | ... | @@ -335,6 +350,14 @@ class Sequence(models.Model): |
335 | 350 | obsolete = models.BooleanField(default=False) |
336 | 351 | processing = models.BooleanField(default=False) |
337 | 352 | flag = models.CharField(max_length=45, blank=True, null=True) |
353 | + tsp = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
354 | + tep = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
355 | + jd1 = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
356 | + jd2 = models.DecimalField(default=0.0, max_digits=15, decimal_places=8) | |
357 | + deltaTL = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
358 | + deltaTR = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
359 | + t_prefered = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
360 | + duration = models.DecimalField(default=-1.0, max_digits=15, decimal_places=8) | |
338 | 361 | |
339 | 362 | |
340 | 363 | class Meta: | ... | ... |
src/scheduler/models.py
1 | 1 | from django.db import models |
2 | +from pyrosapp.models import Schedule, Sequence | |
3 | +from operator import attrgetter | |
4 | +from decimal import * | |
2 | 5 | |
3 | -# Create your models here. | |
6 | +DEFAULT_PLAN_START = 2457485.250000 # April 6th 2016, 18:00:00.0 UT | |
7 | +DEFAULT_PLAN_END = 2457485.916667 # April 7th 2016, 10:00:00.0 UT | |
8 | + | |
9 | +class Interval: | |
10 | + """ | |
11 | + Simple class that represents an interval of time | |
12 | + Julian days should be used | |
13 | + """ | |
14 | + | |
15 | + def __init__(self, start, end): | |
16 | + self._start = Decimal(start) | |
17 | + self._end = Decimal(end) | |
18 | + self.duration = Decimal(end - start) | |
19 | + | |
20 | + | |
21 | + def __str__(self): | |
22 | + print("["+str(self.start)+" - "+str(self.end)+"]") | |
23 | + | |
24 | + def _set_start(self, start): | |
25 | + if start > self._end: | |
26 | + raise ValueError("Cannot set start (%d): must be lower than end (%d)" % (start, self._end)) | |
27 | + self._start = start | |
28 | + self.duration = self._end - self._start | |
29 | + | |
30 | + def _set_end(self, end): | |
31 | + if end < self._start: | |
32 | + raise ValueError("Cannot set end (%d): must be bigger than start (%d)" % (end, self._start)) | |
33 | + self._end = end | |
34 | + self.duration = self._end - self._start | |
35 | + | |
36 | + start = property(_set_start) | |
37 | + end = property(_set_end) | |
38 | + | |
39 | +class Scheduler(): | |
40 | + """ | |
41 | + Role : create a planning for the following/current night | |
42 | + | |
43 | + Read in DB : Sequence, PyrosUser, ScheduleHistory and parents | |
44 | + Create in DB : Schedule, ScheduleHistory ? (not sure) | |
45 | + Update in DB : Schedule, Sequence | |
46 | + Delete in DB : None | |
47 | + | |
48 | + Entry point(s) : | |
49 | + - make_schedule | |
50 | + """ | |
51 | + | |
52 | + """ | |
53 | + TODO: | |
54 | + - voir TODOs dans les commentaires | |
55 | + - fonction de scheduling ( et descendance) | |
56 | + - gestion du re-scheduling (en cas de nouvelle requete) | |
57 | + - tests | |
58 | + - remplissage des espaces libres | |
59 | + - gestion de l'historique (je sais pas si c'est ce module qui va s'en charger au final mais bon ...) | |
60 | + | |
61 | + """ | |
62 | + | |
63 | + def __init__(self): | |
64 | + self.schedule = Schedule() | |
65 | + # TODO: quel est le "flag" dans le schedule ?? | |
66 | + # il faudrait peut-être appeler ces deux fonctions dans la méthode make_schedule ... ou recevoir plan_start et plan_end en param | |
67 | + self.get_night_limits() # TODO | |
68 | + self.intervals = [Interval(self.schedule.plan_start, self.schedule.plan_end) ,] | |
69 | + | |
70 | + | |
71 | + def get_night_limits(self): | |
72 | + ''' | |
73 | + determines and set plan_start and plan_end (beginning & end of the observation night) | |
74 | + ''' | |
75 | + | |
76 | + # TODO: définir comment on calcule plan_start et plan_end (via quels moyens) | |
77 | + self.schedule.plan_start = DEFAULT_PLAN_START # default value | |
78 | + self.schedule.plan_end = DEFAULT_PLAN_END # default value | |
79 | + | |
80 | + | |
81 | + def make_schedule(self): | |
82 | + ''' | |
83 | + ENTRY POINT | |
84 | + | |
85 | + Check all 'OBSERVABLE' sequences to create the most optimized planning for the following/current night | |
86 | + | |
87 | + It is assumed that all sequences that MUST and CAN be analyse have the OBSERVABLE status (e.g. : there must not be any PLANNED sequence at this point) | |
88 | + | |
89 | + :side-effect : | |
90 | + - modify sequences status and dates in DB | |
91 | + ''' | |
92 | + | |
93 | + self.sequences = list(Sequence.objects.filter(status=Sequence.OBSERVABLE)) | |
94 | + self.determine_priorities() | |
95 | + self.remove_not_eligible_sequences() | |
96 | + self.sort_by_jd2_and_priorities() | |
97 | + self.organize_sequences() | |
98 | + self.save_sequences() | |
99 | + | |
100 | + | |
101 | + def determine_priorities(self): | |
102 | + ''' | |
103 | + Computes sequences priority according to the user, the scientific program, ... | |
104 | + ''' | |
105 | + | |
106 | + # TODO: définir comment on calcule la priorité | |
107 | + pass | |
108 | + | |
109 | + | |
110 | + def remove_not_eligible_sequences(self): | |
111 | + ''' | |
112 | + Computes overlap between [jd1; jd2] and [plan_start; plan_end] | |
113 | + Removes from self.sequences all the sequences that cannot be observed between plan_start and plan_end | |
114 | + Set UNPLANNABLE sequences if jd2 < plan_start | |
115 | + | |
116 | + :side-effect : | |
117 | + - remove unwanted sequences from self.sequences | |
118 | + ''' | |
119 | + | |
120 | + ''' Note (1) ''' | |
121 | + for sequence in list(self.sequences): | |
122 | + overlap = min(self.schedule.plan_end, sequence.jd2) - max(self.schedule.plan_end, sequence) | |
123 | + if overlap < sequence.duration: | |
124 | + if sequence.jd2 < self.schedule.plan_start: | |
125 | + """ Note (2) """ | |
126 | + sequence.status = Sequence.UNPLANNABLE | |
127 | + sequence.save() | |
128 | + self.sequences.remove(sequence) | |
129 | + | |
130 | + | |
131 | + def sort_by_jd2_and_priorities(self): | |
132 | + ''' | |
133 | + Sort by priority and jd2, priority being the main sorting parameter | |
134 | + ''' | |
135 | + | |
136 | + self.sequences.sort(key=attrgetter('priority', 'jd2')) | |
137 | + | |
138 | + | |
139 | + def organize_sequences(self): | |
140 | + ''' | |
141 | + Main function of the Scheduler | |
142 | + Arrange a maximum of observable sequences in the planning | |
143 | + | |
144 | + Algorithm (for each sequence) : | |
145 | + - check quota (remove sequence from list if quota is too low) | |
146 | + - select matching intervals | |
147 | + - IF matching intervals => place sequence according to tPrefered | |
148 | + - IF NO matching intervals => try to move other sequences to place this one | |
149 | + | |
150 | + :side-effect : | |
151 | + - remove unwanted sequences from self.sequences | |
152 | + - change status and dates of sequences in self.sequences (but not in DB yet) | |
153 | + ''' | |
154 | + | |
155 | + ''' Note (1) ''' | |
156 | + for sequence in list(self.sequences): | |
157 | + quota = self.determine_quota(sequence) | |
158 | + if quota < sequence.duration: | |
159 | + self.sequences.remove(sequence) | |
160 | + continue | |
161 | + | |
162 | + matching_intervals = self.get_matching_intervals(sequence) | |
163 | + if len(matching_intervals) > 0: | |
164 | + self.place_sequence(sequence, matching_intervals) | |
165 | + sequence_placed = True | |
166 | + else: | |
167 | + sequence_placed = self.try_shifting_sequences(sequence) | |
168 | + | |
169 | + if sequence_placed == True: | |
170 | + sequence.status = Sequence.PLANNED | |
171 | + self.update_quota(sequence) | |
172 | + | |
173 | + def determine_quota(self, sequence:Sequence) -> float: | |
174 | + ''' | |
175 | + Determines the quota (in minutes) according to the current planning duration and the quota of the user and scientific program associated | |
176 | + | |
177 | + :returns : The quota (float) | |
178 | + ''' | |
179 | + # TODO: définir comment on calcule le quota | |
180 | + | |
181 | + return sequence.request.pyros_user.quota # default value | |
182 | + | |
183 | + | |
184 | + def get_matching_intervals(self, sequence:Sequence): | |
185 | + ''' | |
186 | + Find the intervals where the sequence could be inserted | |
187 | + | |
188 | + :returns : list of matching Intervals | |
189 | + ''' | |
190 | + | |
191 | + matching_intervals = [] | |
192 | + | |
193 | + for interval in self.intervals: | |
194 | + overlap = min(sequence.jd2, interval.end) - max(sequence.jd1, interval.start) | |
195 | + if overlap >= sequence.duration: | |
196 | + matching_intervals.append(interval) | |
197 | + | |
198 | + return matching_intervals | |
199 | + | |
200 | + | |
201 | + def place_sequence(self, sequence:Sequence, matching_intervals) : | |
202 | + ''' | |
203 | + Place the sequence in the better interval, according to the t_prefered | |
204 | + | |
205 | + :type matching_intervals: list [Interval] | |
206 | + :param matching_intervals: Intervals in which the sequence can be placed | |
207 | + | |
208 | + :side-effect : | |
209 | + - changes self.intervals | |
210 | + - change the sequence if it it placed | |
211 | + ''' | |
212 | + | |
213 | + if len(matching_intervals) == 0: | |
214 | + raise ValueError("matching_intervals shall not be empty") | |
215 | + | |
216 | + prefered_interval = self.get_prefered_interval(sequence, matching_intervals) | |
217 | + sequence_position_in_interval = self.get_sequence_position_in_interval(sequence, prefered_interval) | |
218 | + self.insert_sequence_in_interval(sequence, prefered_interval, sequence_position_in_interval) | |
219 | + self.cut_interval(sequence, prefered_interval) | |
220 | + self.update_other_deltas(sequence, prefered_interval) | |
221 | + | |
222 | + | |
223 | + def get_prefered_interval(self, sequence:Sequence, matching_intervals) -> Interval: | |
224 | + ''' | |
225 | + Find the better interval, according to the t_prefered | |
226 | + | |
227 | + :type matching_intervals: list [Interval] | |
228 | + :param matching_intervals: Intervals in which the sequence can be placed | |
229 | + | |
230 | + :returns : An Interval that fits sequence.t_prefered at most | |
231 | + ''' | |
232 | + | |
233 | + if len(matching_intervals) == 0: | |
234 | + raise ValueError("matching_intervals shall not be empty") | |
235 | + | |
236 | + if sequence.t_prefered == 0: | |
237 | + prefered_interval = matching_intervals[0] | |
238 | + else: | |
239 | + for index, interval in enumerate(matching_intervals): | |
240 | + if interval.start <= sequence.t_prefered <= interval.end: | |
241 | + prefered_interval = interval | |
242 | + break | |
243 | + elif sequence.t_prefered < interval.start: | |
244 | + if index == 0: | |
245 | + prefered_interval = interval | |
246 | + else: | |
247 | + prefered_interval = matching_intervals[index - 1] | |
248 | + break | |
249 | + return prefered_interval | |
250 | + | |
251 | + | |
252 | + def get_sequence_position_in_interval(self, sequence:Sequence, interval:Interval) -> str: | |
253 | + ''' | |
254 | + Determines where the sequence will be inserted in the interval, regarding sequence.t_prefered | |
255 | + | |
256 | + :returns : A string in ["START", "END", "PREFERED"] describing where the sequence will be inserted in the interval | |
257 | + ''' | |
258 | + | |
259 | + if interval.start <= sequence.t_prefered <= interval.end: | |
260 | + if (sequence.t_prefered - 0.5 * sequence.duration) <= interval.start: | |
261 | + position_in_interval = "START" | |
262 | + elif (sequence.t_prefered + 0.5 * sequence.duration) >= interval.end: | |
263 | + position_in_interval = "END" | |
264 | + else: | |
265 | + position_in_interval = "PREFERED" | |
266 | + else: | |
267 | + if sequence.t_prefered < interval.start: | |
268 | + position_in_interval = "START" | |
269 | + else: | |
270 | + position_in_interval = "END" | |
271 | + return position_in_interval | |
272 | + | |
273 | + | |
274 | + def insert_sequence_in_interval(self, sequence:Sequence, interval:Interval, position:str): | |
275 | + ''' | |
276 | + Inserts the sequence in the interval: | |
277 | + - sets sequence.tsp and sequence.tep | |
278 | + - sets sequence.deltaTL and sequence.deltaTR | |
279 | + | |
280 | + :param interval: Interval in which the sequence will be inserted | |
281 | + :param position: String describing where the sequence will be inserted in the interval | |
282 | + | |
283 | + :side-effect : | |
284 | + - modify sequence attributes (tsp, tep, deltaTL, deltaTR) | |
285 | + ''' | |
286 | + | |
287 | + if position not in ["START", "END", "PREFERED"]: | |
288 | + raise ValueError("position must be either 'START', 'END' or 'PREFERED'") | |
289 | + | |
290 | + if position == "START": | |
291 | + sequence.tsp = interval.start | |
292 | + sequence.tep = interval.start + sequence.duration | |
293 | + sequence.deltaTL = 0 | |
294 | + sequence.deltaTR = interval.end - sequence.tep | |
295 | + elif position == "END": | |
296 | + sequence.tsp = interval.end | |
297 | + sequence.tep = interval.end - sequence.duration | |
298 | + sequence.deltaTL = sequence.tsp - interval.start | |
299 | + sequence.deltaTR = 0 | |
300 | + else: | |
301 | + sequence.tsp = sequence.t_prefered - 0.5 * sequence.duration | |
302 | + sequence.tep = sequence.t_prefered + 0.5 * sequence.duration | |
303 | + sequence.deltaTL = sequence.tsp - interval.start | |
304 | + sequence.deltaTR = interval.end - sequence.tep | |
305 | + | |
306 | + | |
307 | + def cut_interval(self, sequence:Sequence, interval:Interval): | |
308 | + ''' | |
309 | + Separates the interval in two parts regarding to the sequence position | |
310 | + Sorts the interval list in time order | |
311 | + | |
312 | + :param interval : the interval in which the sequence was added | |
313 | + | |
314 | + :side-effect : | |
315 | + - removes interval from self.intervals | |
316 | + - add created intervals to self.intervals | |
317 | + - sorts self.intervals | |
318 | + ''' | |
319 | + | |
320 | + interval_before_sequence = Interval(interval.start, sequence.tsp) | |
321 | + interval_after_sequence = Interval(sequence.tep, interval.end) | |
322 | + | |
323 | + self.intervals.remove(interval) | |
324 | + if interval_before_sequence.duration > 0: | |
325 | + self.intervals.append(interval_before_sequence) | |
326 | + if interval_after_sequence.duration > 0: | |
327 | + self.intervals.append(interval_after_sequence) | |
328 | + self.intervals.sort(key=lambda interval: interval.start, reverse=False) | |
329 | + | |
330 | + | |
331 | + def update_other_deltas(self, sequence:Sequence, interval:Interval): | |
332 | + ''' | |
333 | + Update deltaTL and deltaTR of sequences planned near this sequence | |
334 | + | |
335 | + :param interval: Interval in wich the sequence was added | |
336 | + | |
337 | + :side-effect : | |
338 | + - modify deltaTL and deltaTR of sequences before and after the interval | |
339 | + ''' | |
340 | + | |
341 | + for sequence_ in self.sequences: | |
342 | + if sequence_.status == Sequence.PLANNED: | |
343 | + if sequence_.tep == interval.start: | |
344 | + sequence_before_interval = sequence_ | |
345 | + elif sequence_.tsp == interval.end: | |
346 | + sequence_after_interval = sequence_ | |
347 | + | |
348 | + sequence_before_interval.deltaTR = sequence.tsp - sequence_before_interval.tep | |
349 | + sequence_after_interval.deltaTL = sequence_after_interval.tsp - sequence.tep | |
350 | + | |
351 | + | |
352 | + def try_shifting_sequences(self, sequence:Sequence) -> bool: | |
353 | + ''' | |
354 | + Tries to find a place in the planning for the sequence, moving the other sequences | |
355 | + | |
356 | + :returns : A boolean -> True if the sequence was placed, False otherwise | |
357 | + | |
358 | + :side-effect: | |
359 | + - might change some sequences' deltaTL and/or deltaTR | |
360 | + ''' | |
361 | + | |
362 | + potential_intervals = self.get_potential_intervals() | |
363 | + for interval in potential_intervals: | |
364 | + ''' we get the adjacent sequences ''' | |
365 | + for sequence_ in self.sequences: | |
366 | + if sequence_.status == Sequence.PLANNED: | |
367 | + if sequence_.tep == interval.start: | |
368 | + sequence_before_interval = sequence_ | |
369 | + elif sequence_.tsp == interval.end: | |
370 | + sequence_after_interval = sequence_ | |
371 | + | |
372 | + available_duration = min(interval.end, sequence.jd2) - max(interval.start, sequence.jd1) | |
373 | + missing_duration = sequence.duration - available_duration | |
374 | + | |
375 | + possible_move_to_left = max(sequence_before_interval.deltaTL, interval.start - sequence.jd1) | |
376 | + possible_move_to_right = max(sequence_after_interval.deltaTR, sequence.jd2 - interval.end) | |
377 | + | |
378 | + if possible_move_to_left >= missing_duration: | |
379 | + self.move_sequence(sequence_before_interval, missing_duration, "LEFT") | |
380 | + elif possible_move_to_right >= missing_duration: | |
381 | + self.move_sequence(sequence_after_interval, missing_duration, "RIGHT") | |
382 | + elif possible_move_to_left + possible_move_to_right >= missing_duration: | |
383 | + self.move_sequence(sequence_before_interval, possible_move_to_left, "LEFT") | |
384 | + self.move_sequence(sequence_after_interval, missing_duration - possible_move_to_left, "RIGHT") | |
385 | + | |
386 | + matching_intervals = self.get_matching_intervals(sequence) | |
387 | + if len(matching_intervals) != 1: | |
388 | + raise ValueError("There should be one and only one matching interval after shifting") | |
389 | + self.place_sequence(sequence, matching_intervals) | |
390 | + | |
391 | + return False | |
392 | + | |
393 | + | |
394 | + def get_potential_intervals(self, sequence:Sequence): | |
395 | + ''' | |
396 | + Find the intervals where a part of the sequence could be inserted | |
397 | + | |
398 | + :returns : list of partially-matching Intervals | |
399 | + ''' | |
400 | + | |
401 | + potential_intervals = [] | |
402 | + | |
403 | + for interval in self.intervals: | |
404 | + overlap = min(sequence.jd2, interval.end) - max(sequence.jd1, interval.start) | |
405 | + if overlap > 0: | |
406 | + potential_intervals.append(interval) | |
407 | + | |
408 | + return potential_intervals | |
409 | + | |
410 | + | |
411 | + def move_sequence(self, sequence:Sequence, time_shift:float, direction:str): | |
412 | + ''' | |
413 | + Moves the sequence in the wanted direction, decreasing its deltaTL or deltaTR. | |
414 | + | |
415 | + :param sequence: sequence to be moved | |
416 | + :param time_shift: amplitude of the shift | |
417 | + :param direction: "LEFT" or "RIGHT" | |
418 | + | |
419 | + :side-effect : | |
420 | + - modify the sequence in self.sequences | |
421 | + - changes the interval before and the interval after the sequence | |
422 | + ''' | |
423 | + | |
424 | + if direction not in ["LEFT", "RIGHT"]: | |
425 | + raise ValueError("direction must be 'LEFT' or 'RIGHT'") | |
426 | + if time_shift > (sequence.deltaTL if direction == "LEFT" else sequence.deltaTR): | |
427 | + raise ValueError("Shift value is bigger than deltaT(R/L)") | |
428 | + | |
429 | + for interval in self.intervals: | |
430 | + if interval.end == sequence.jsp: | |
431 | + interval_before = interval | |
432 | + elif interval.start == sequence.jep: | |
433 | + interval_after = interval | |
434 | + | |
435 | + if direction == "LEFT": | |
436 | + interval_before.end -= time_shift | |
437 | + interval_after.start -= time_shift | |
438 | + sequence.jsp -= time_shift | |
439 | + sequence.jep -= time_shift | |
440 | + sequence.deltaTL -= time_shift | |
441 | + sequence.deltaTR += time_shift | |
442 | + else: | |
443 | + interval_before.end += time_shift | |
444 | + interval_after.start += time_shift | |
445 | + sequence.jsp += time_shift | |
446 | + sequence.jep += time_shift | |
447 | + sequence.deltaTL += time_shift | |
448 | + sequence.deltaTR -= time_shift | |
449 | + | |
450 | + | |
451 | + def update_quota(self, sequence:Sequence): | |
452 | + ''' | |
453 | + Update the quota of the user / scientific program / whatever by substracting the sequence duration to the quotas | |
454 | + | |
455 | + :side-effect: | |
456 | + - Modify User quota in DB | |
457 | + ''' | |
458 | + | |
459 | + # TODO: faire les vrais calculs de quota | |
460 | + user = sequence.request.pyros_user | |
461 | + user.quota -= sequence.duration # action par défaut qui correspond au code de self.determine_quota | |
462 | + user.save() | |
463 | + | |
464 | + def save_sequences(self): | |
465 | + ''' | |
466 | + Final function : save in the db all modifications done to sequences | |
467 | + | |
468 | + :side-effect : | |
469 | + - change sequences status and dates in DB | |
470 | + ''' | |
471 | + | |
472 | + for sequence in self.sequences: | |
473 | + sequence.save() | |
474 | + | |
475 | + | |
476 | + | |
477 | +''' Notes | |
478 | + | |
479 | +(1) list(self.sequences) creates a copy in order to modify self.sequences and still iterate on it without unexpected behavior | |
480 | +(2) UNPLANNABLE is a definitive status meaning that the sequence will never be able to be scheduled | |
481 | + | |
482 | +''' | |
4 | 483 | \ No newline at end of file | ... | ... |