QuizGenerator 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/contentast.py +809 -117
- QuizGenerator/generate.py +219 -11
- QuizGenerator/misc.py +0 -556
- QuizGenerator/mixins.py +50 -29
- QuizGenerator/premade_questions/basic.py +3 -3
- QuizGenerator/premade_questions/cst334/languages.py +183 -175
- QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
- QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
- QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
- QuizGenerator/premade_questions/cst334/process.py +558 -79
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
- QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
- QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
- QuizGenerator/premade_questions/cst463/models/text.py +29 -15
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
- QuizGenerator/question.py +114 -20
- QuizGenerator/quiz.py +81 -24
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,16 +7,14 @@ import dataclasses
|
|
|
7
7
|
import enum
|
|
8
8
|
import io
|
|
9
9
|
import logging
|
|
10
|
-
import math
|
|
11
10
|
import os
|
|
12
|
-
import queue
|
|
13
11
|
import uuid
|
|
14
12
|
from typing import List
|
|
15
13
|
|
|
16
14
|
import matplotlib.pyplot as plt
|
|
17
15
|
|
|
18
|
-
from QuizGenerator.contentast import ContentAST
|
|
19
|
-
from QuizGenerator.question import Question,
|
|
16
|
+
from QuizGenerator.contentast import ContentAST, AnswerTypes
|
|
17
|
+
from QuizGenerator.question import Question, QuestionRegistry, RegenerableChoiceMixin
|
|
20
18
|
from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
|
|
21
19
|
|
|
22
20
|
log = logging.getLogger(__name__)
|
|
@@ -65,6 +63,8 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
65
63
|
TIME_QUANTUM = None
|
|
66
64
|
|
|
67
65
|
ROUNDING_DIGITS = 2
|
|
66
|
+
IMAGE_DPI = 140
|
|
67
|
+
IMAGE_FIGSIZE = (9.5, 5.5)
|
|
68
68
|
|
|
69
69
|
@dataclasses.dataclass
|
|
70
70
|
class Job:
|
|
@@ -356,30 +356,32 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
356
356
|
|
|
357
357
|
for job_id in sorted(self.job_stats.keys()):
|
|
358
358
|
self.answers.update({
|
|
359
|
-
f"answer__response_time_job{job_id}":
|
|
360
|
-
|
|
361
|
-
self.job_stats[job_id]["Response"]
|
|
362
|
-
),
|
|
363
|
-
f"answer__turnaround_time_job{job_id}": Answer.auto_float(
|
|
364
|
-
f"answer__turnaround_time_job{job_id}",
|
|
365
|
-
self.job_stats[job_id]["TAT"]
|
|
366
|
-
),
|
|
359
|
+
f"answer__response_time_job{job_id}": AnswerTypes.Float(self.job_stats[job_id]["Response"]),
|
|
360
|
+
f"answer__turnaround_time_job{job_id}": AnswerTypes.Float(self.job_stats[job_id]["TAT"]),
|
|
367
361
|
})
|
|
368
362
|
self.answers.update({
|
|
369
|
-
"answer__average_response_time":
|
|
370
|
-
|
|
371
|
-
|
|
363
|
+
"answer__average_response_time": AnswerTypes.Float(
|
|
364
|
+
sum([job.response_time for job in jobs]) / len(jobs),
|
|
365
|
+
label="Overall average response time"
|
|
372
366
|
),
|
|
373
|
-
"answer__average_turnaround_time":
|
|
374
|
-
|
|
375
|
-
|
|
367
|
+
"answer__average_turnaround_time": AnswerTypes.Float(
|
|
368
|
+
sum([job.turnaround_time for job in jobs]) / len(jobs),
|
|
369
|
+
label="Overall average TAT"
|
|
376
370
|
)
|
|
377
371
|
})
|
|
378
372
|
|
|
379
373
|
# Return whether this workload is interesting
|
|
380
374
|
return self.is_interesting()
|
|
381
375
|
|
|
382
|
-
def
|
|
376
|
+
def _get_body(self, *args, **kwargs):
|
|
377
|
+
"""
|
|
378
|
+
Build question body and collect answers.
|
|
379
|
+
Returns:
|
|
380
|
+
Tuple of (body_ast, answers_list)
|
|
381
|
+
"""
|
|
382
|
+
from typing import List
|
|
383
|
+
answers: List[ContentAST.Answer] = []
|
|
384
|
+
|
|
383
385
|
# Create table data for scheduling results
|
|
384
386
|
table_rows = []
|
|
385
387
|
for job_id in sorted(self.job_stats.keys()):
|
|
@@ -390,6 +392,9 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
390
392
|
"Response Time": f"answer__response_time_job{job_id}", # Answer key
|
|
391
393
|
"TAT": f"answer__turnaround_time_job{job_id}" # Answer key
|
|
392
394
|
})
|
|
395
|
+
# Collect answers for this job
|
|
396
|
+
answers.append(self.answers[f"answer__response_time_job{job_id}"])
|
|
397
|
+
answers.append(self.answers[f"answer__turnaround_time_job{job_id}"])
|
|
393
398
|
|
|
394
399
|
# Create table using mixin
|
|
395
400
|
scheduling_table = self.create_answer_table(
|
|
@@ -398,38 +403,51 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
398
403
|
answer_columns=["Response Time", "TAT"]
|
|
399
404
|
)
|
|
400
405
|
|
|
406
|
+
# Collect average answers
|
|
407
|
+
avg_response_answer = self.answers["answer__average_response_time"]
|
|
408
|
+
avg_tat_answer = self.answers["answer__average_turnaround_time"]
|
|
409
|
+
answers.append(avg_response_answer)
|
|
410
|
+
answers.append(avg_tat_answer)
|
|
411
|
+
|
|
401
412
|
# Create average answer block
|
|
402
|
-
average_block = ContentAST.AnswerBlock([
|
|
403
|
-
ContentAST.Answer(self.answers["answer__average_response_time"], label="Overall average response time"),
|
|
404
|
-
ContentAST.Answer(self.answers["answer__average_turnaround_time"], label="Overall average TAT")
|
|
405
|
-
])
|
|
413
|
+
average_block = ContentAST.AnswerBlock([avg_response_answer, avg_tat_answer])
|
|
406
414
|
|
|
407
415
|
# Use mixin to create complete body
|
|
408
416
|
intro_text = (
|
|
409
|
-
f"Given the below information, compute the required values if using
|
|
417
|
+
f"Given the below information, compute the required values if using **{self.scheduler_algorithm}** scheduling. "
|
|
410
418
|
f"Break any ties using the job number."
|
|
411
419
|
)
|
|
412
420
|
|
|
413
421
|
instructions = ContentAST.OnlyHtml([ContentAST.Paragraph([
|
|
414
|
-
f"Please format answer as fractions, mixed numbers, or numbers rounded to a maximum of {Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal. "
|
|
422
|
+
f"Please format answer as fractions, mixed numbers, or numbers rounded to a maximum of {ContentAST.Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal. "
|
|
415
423
|
"Examples of appropriately formatted answers would be `0`, `3/2`, `1 1/3`, `1.6667`, and `1.25`. "
|
|
416
424
|
"Note that answers that can be rounded to whole numbers should be, rather than being left in fractional form."
|
|
417
425
|
])])
|
|
418
426
|
|
|
419
427
|
body = self.create_fill_in_table_body(intro_text, instructions, scheduling_table)
|
|
420
428
|
body.add_element(average_block)
|
|
429
|
+
return body, answers
|
|
430
|
+
|
|
431
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
432
|
+
"""Build question body (backward compatible interface)."""
|
|
433
|
+
body, _ = self._get_body(*args, **kwargs)
|
|
421
434
|
return body
|
|
422
435
|
|
|
423
|
-
def
|
|
436
|
+
def _get_explanation(self, **kwargs):
|
|
437
|
+
"""
|
|
438
|
+
Build question explanation.
|
|
439
|
+
Returns:
|
|
440
|
+
Tuple of (explanation_ast, answers_list)
|
|
441
|
+
"""
|
|
424
442
|
explanation = ContentAST.Section()
|
|
425
|
-
|
|
443
|
+
|
|
426
444
|
explanation.add_element(
|
|
427
445
|
ContentAST.Paragraph([
|
|
428
446
|
f"To calculate the overall Turnaround and Response times using {self.scheduler_algorithm} "
|
|
429
447
|
f"we want to first start by calculating the respective target and response times of all of our individual jobs."
|
|
430
448
|
])
|
|
431
449
|
)
|
|
432
|
-
|
|
450
|
+
|
|
433
451
|
explanation.add_elements([
|
|
434
452
|
ContentAST.Paragraph([
|
|
435
453
|
"We do this by subtracting arrival time from either the completion time or the start time. That is:"
|
|
@@ -437,13 +455,13 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
437
455
|
ContentAST.Equation("Job_{TAT} = Job_{completion} - Job_{arrival\_time}"),
|
|
438
456
|
ContentAST.Equation("Job_{response} = Job_{start} - Job_{arrival\_time}"),
|
|
439
457
|
])
|
|
440
|
-
|
|
458
|
+
|
|
441
459
|
explanation.add_element(
|
|
442
460
|
ContentAST.Paragraph([
|
|
443
461
|
f"For each of our {len(self.job_stats.keys())} jobs, we can make these calculations.",
|
|
444
462
|
])
|
|
445
463
|
)
|
|
446
|
-
|
|
464
|
+
|
|
447
465
|
## Add in TAT
|
|
448
466
|
explanation.add_element(
|
|
449
467
|
ContentAST.Paragraph([
|
|
@@ -456,7 +474,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
456
474
|
for job_id in sorted(self.job_stats.keys())
|
|
457
475
|
])
|
|
458
476
|
)
|
|
459
|
-
|
|
477
|
+
|
|
460
478
|
summation_line = ' + '.join([
|
|
461
479
|
f"{self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f}" for job_id in sorted(self.job_stats.keys())
|
|
462
480
|
])
|
|
@@ -467,8 +485,8 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
467
485
|
f"= {self.overall_stats['TAT']:0.{self.ROUNDING_DIGITS}f}",
|
|
468
486
|
])
|
|
469
487
|
)
|
|
470
|
-
|
|
471
|
-
|
|
488
|
+
|
|
489
|
+
|
|
472
490
|
## Add in Response
|
|
473
491
|
explanation.add_element(
|
|
474
492
|
ContentAST.Paragraph([
|
|
@@ -481,7 +499,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
481
499
|
for job_id in sorted(self.job_stats.keys())
|
|
482
500
|
])
|
|
483
501
|
)
|
|
484
|
-
|
|
502
|
+
|
|
485
503
|
summation_line = ' + '.join([
|
|
486
504
|
f"{self.job_stats[job_id]['Response']:0.{self.ROUNDING_DIGITS}f}" for job_id in sorted(self.job_stats.keys())
|
|
487
505
|
])
|
|
@@ -494,7 +512,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
494
512
|
"\n",
|
|
495
513
|
])
|
|
496
514
|
)
|
|
497
|
-
|
|
515
|
+
|
|
498
516
|
explanation.add_element(
|
|
499
517
|
ContentAST.Table(
|
|
500
518
|
headers=["Time", "Events"],
|
|
@@ -504,14 +522,19 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
504
522
|
]
|
|
505
523
|
)
|
|
506
524
|
)
|
|
507
|
-
|
|
525
|
+
|
|
508
526
|
explanation.add_element(
|
|
509
527
|
ContentAST.Picture(
|
|
510
528
|
img_data=self.make_image(),
|
|
511
529
|
caption="Process Scheduling Overview"
|
|
512
530
|
)
|
|
513
531
|
)
|
|
514
|
-
|
|
532
|
+
|
|
533
|
+
return explanation, []
|
|
534
|
+
|
|
535
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
536
|
+
"""Build question explanation (backward compatible interface)."""
|
|
537
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
515
538
|
return explanation
|
|
516
539
|
|
|
517
540
|
def is_interesting(self) -> bool:
|
|
@@ -521,7 +544,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
521
544
|
|
|
522
545
|
def make_image(self):
|
|
523
546
|
|
|
524
|
-
fig, ax = plt.subplots(1, 1)
|
|
547
|
+
fig, ax = plt.subplots(1, 1, figsize=self.IMAGE_FIGSIZE, dpi=self.IMAGE_DPI)
|
|
525
548
|
|
|
526
549
|
for x_loc in set([t for job_id in self.job_stats.keys() for t in self.job_stats[job_id]["state_changes"] ]):
|
|
527
550
|
ax.axvline(x_loc, zorder=0)
|
|
@@ -579,7 +602,8 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
579
602
|
|
|
580
603
|
# Save to BytesIO object instead of a file
|
|
581
604
|
buffer = io.BytesIO()
|
|
582
|
-
plt.
|
|
605
|
+
plt.tight_layout()
|
|
606
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight', pad_inches=0.2)
|
|
583
607
|
plt.close(fig)
|
|
584
608
|
|
|
585
609
|
# Reset buffer position to the beginning
|
|
@@ -599,50 +623,505 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
599
623
|
return image_path
|
|
600
624
|
|
|
601
625
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
MIN_DURATION =
|
|
605
|
-
MAX_DURATION =
|
|
626
|
+
@QuestionRegistry.register()
|
|
627
|
+
class MLFQQuestion(ProcessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
628
|
+
MIN_DURATION = 4
|
|
629
|
+
MAX_DURATION = 12
|
|
606
630
|
MIN_ARRIVAL = 0
|
|
607
|
-
MAX_ARRIVAL =
|
|
608
|
-
|
|
631
|
+
MAX_ARRIVAL = 10
|
|
632
|
+
DEFAULT_NUM_JOBS = 3
|
|
633
|
+
DEFAULT_NUM_QUEUES = 3
|
|
634
|
+
ROUNDING_DIGITS = 2
|
|
635
|
+
IMAGE_DPI = 140
|
|
636
|
+
IMAGE_FIGSIZE = (9.5, 6.5)
|
|
637
|
+
|
|
609
638
|
@dataclasses.dataclass
|
|
610
|
-
class Job
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
639
|
+
class Job:
|
|
640
|
+
job_id: int
|
|
641
|
+
arrival_time: int
|
|
642
|
+
duration: int
|
|
643
|
+
remaining_time: int
|
|
644
|
+
queue_level: int = 0
|
|
645
|
+
time_in_queue: int = 0
|
|
646
|
+
response_time: float | None = None
|
|
647
|
+
turnaround_time: float | None = None
|
|
648
|
+
remaining_quantum: int | None = None
|
|
649
|
+
run_intervals: List[tuple] = dataclasses.field(default_factory=list)
|
|
650
|
+
max_queue_level: int = 0
|
|
651
|
+
|
|
652
|
+
def __init__(
|
|
653
|
+
self,
|
|
654
|
+
num_jobs: int = DEFAULT_NUM_JOBS,
|
|
655
|
+
num_queues: int = DEFAULT_NUM_QUEUES,
|
|
656
|
+
min_job_length: int = MIN_DURATION,
|
|
657
|
+
max_job_length: int = MAX_DURATION,
|
|
658
|
+
boost_interval: int | None = None,
|
|
659
|
+
boost_interval_range: List[int] | None = None,
|
|
660
|
+
*args,
|
|
661
|
+
**kwargs
|
|
662
|
+
):
|
|
663
|
+
kwargs["num_jobs"] = num_jobs
|
|
664
|
+
kwargs["num_queues"] = num_queues
|
|
665
|
+
kwargs["min_job_length"] = min_job_length
|
|
666
|
+
kwargs["max_job_length"] = max_job_length
|
|
667
|
+
if boost_interval is not None:
|
|
668
|
+
kwargs["boost_interval"] = boost_interval
|
|
669
|
+
if boost_interval_range is not None:
|
|
670
|
+
kwargs["boost_interval_range"] = boost_interval_range
|
|
671
|
+
super().__init__(*args, **kwargs)
|
|
672
|
+
self.num_jobs = num_jobs
|
|
673
|
+
self.num_queues = num_queues
|
|
674
|
+
self.min_job_length = min_job_length
|
|
675
|
+
self.max_job_length = max_job_length
|
|
676
|
+
self.boost_interval = boost_interval
|
|
677
|
+
self.boost_interval_range = boost_interval_range
|
|
678
|
+
|
|
679
|
+
def get_workload(self, num_jobs: int) -> List[MLFQQuestion.Job]:
|
|
680
|
+
arrivals = [0]
|
|
681
|
+
if num_jobs > 1:
|
|
682
|
+
arrivals.extend(
|
|
683
|
+
self.rng.randint(self.MIN_ARRIVAL, self.MAX_ARRIVAL)
|
|
684
|
+
for _ in range(num_jobs - 1)
|
|
685
|
+
)
|
|
686
|
+
if max(arrivals) == 0:
|
|
687
|
+
arrivals[-1] = self.rng.randint(1, self.MAX_ARRIVAL)
|
|
688
|
+
|
|
689
|
+
durations = [
|
|
690
|
+
self.rng.randint(self.min_job_length, self.max_job_length)
|
|
691
|
+
for _ in range(num_jobs)
|
|
692
|
+
]
|
|
693
|
+
|
|
694
|
+
jobs = []
|
|
695
|
+
for i in range(num_jobs):
|
|
696
|
+
jobs.append(
|
|
697
|
+
MLFQQuestion.Job(
|
|
698
|
+
job_id=i,
|
|
699
|
+
arrival_time=arrivals[i],
|
|
700
|
+
duration=durations[i],
|
|
701
|
+
remaining_time=durations[i],
|
|
702
|
+
)
|
|
703
|
+
)
|
|
704
|
+
return jobs
|
|
705
|
+
|
|
706
|
+
def _normalize_queue_params(self, values: List[int] | None, num_queues: int) -> List[int]:
|
|
707
|
+
if values is None:
|
|
708
|
+
return []
|
|
709
|
+
values = list(values)
|
|
710
|
+
while len(values) < num_queues:
|
|
711
|
+
values.append(values[-1])
|
|
712
|
+
return values[:num_queues]
|
|
713
|
+
|
|
714
|
+
def run_simulation(
|
|
715
|
+
self,
|
|
716
|
+
jobs: List[MLFQQuestion.Job],
|
|
717
|
+
queue_quantums: List[int],
|
|
718
|
+
queue_allotments: List[int | None],
|
|
719
|
+
boost_interval: int | None,
|
|
720
|
+
) -> None:
|
|
721
|
+
self.timeline = collections.defaultdict(list)
|
|
722
|
+
self.boost_times = []
|
|
723
|
+
pending = sorted(jobs, key=lambda j: (j.arrival_time, j.job_id))
|
|
724
|
+
queues = [collections.deque() for _ in range(len(queue_quantums))]
|
|
725
|
+
completed = set()
|
|
726
|
+
|
|
727
|
+
curr_time = pending[0].arrival_time if pending else 0
|
|
728
|
+
self.timeline[curr_time].append("Simulation Start")
|
|
729
|
+
next_boost_time = None
|
|
730
|
+
if boost_interval is not None:
|
|
731
|
+
next_boost_time = boost_interval
|
|
732
|
+
|
|
733
|
+
def enqueue_arrivals(up_to_time: int) -> None:
|
|
734
|
+
nonlocal pending
|
|
735
|
+
while pending and pending[0].arrival_time <= up_to_time:
|
|
736
|
+
job = pending.pop(0)
|
|
737
|
+
job.queue_level = len(queues) - 1
|
|
738
|
+
job.time_in_queue = 0
|
|
739
|
+
job.remaining_quantum = None
|
|
740
|
+
queues[-1].append(job)
|
|
741
|
+
self.timeline[job.arrival_time].append(
|
|
742
|
+
f"Job{job.job_id} arrived (dur = {job.duration})"
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def apply_boost(curr_time: int, running_job: MLFQQuestion.Job | None = None) -> None:
|
|
746
|
+
jobs_to_boost = []
|
|
747
|
+
for q in queues:
|
|
748
|
+
while q:
|
|
749
|
+
jobs_to_boost.append(q.popleft())
|
|
750
|
+
if running_job is not None and running_job.remaining_time > 0:
|
|
751
|
+
jobs_to_boost.append(running_job)
|
|
752
|
+
if not jobs_to_boost:
|
|
753
|
+
self.boost_times.append(curr_time)
|
|
754
|
+
return
|
|
755
|
+
for job in sorted(jobs_to_boost, key=lambda j: j.job_id):
|
|
756
|
+
job.queue_level = len(queues) - 1
|
|
757
|
+
job.time_in_queue = 0
|
|
758
|
+
job.remaining_quantum = None
|
|
759
|
+
queues[-1].append(job)
|
|
760
|
+
self.timeline[curr_time].append(
|
|
761
|
+
f"Boosted all jobs to Q{len(queues) - 1}"
|
|
762
|
+
)
|
|
763
|
+
self.boost_times.append(curr_time)
|
|
764
|
+
|
|
765
|
+
enqueue_arrivals(curr_time)
|
|
766
|
+
|
|
767
|
+
while len(completed) < len(jobs):
|
|
768
|
+
q_idx = next(
|
|
769
|
+
(i for i in range(len(queues) - 1, -1, -1) if queues[i]),
|
|
770
|
+
None
|
|
771
|
+
)
|
|
772
|
+
if q_idx is None:
|
|
773
|
+
next_times = []
|
|
774
|
+
if pending:
|
|
775
|
+
next_times.append(pending[0].arrival_time)
|
|
776
|
+
if next_boost_time is not None:
|
|
777
|
+
next_times.append(next_boost_time)
|
|
778
|
+
if next_times:
|
|
779
|
+
next_time = min(next_times)
|
|
780
|
+
if next_time > curr_time:
|
|
781
|
+
self.timeline[curr_time].append("CPU idle")
|
|
782
|
+
curr_time = next_time
|
|
783
|
+
enqueue_arrivals(curr_time)
|
|
784
|
+
while next_boost_time is not None and curr_time >= next_boost_time:
|
|
785
|
+
apply_boost(curr_time)
|
|
786
|
+
next_boost_time += boost_interval
|
|
787
|
+
continue
|
|
788
|
+
break
|
|
789
|
+
|
|
790
|
+
job = queues[q_idx].popleft()
|
|
791
|
+
quantum = queue_quantums[q_idx]
|
|
792
|
+
if job.remaining_quantum is None or job.remaining_quantum <= 0:
|
|
793
|
+
job.remaining_quantum = quantum
|
|
794
|
+
|
|
795
|
+
slice_duration = min(job.remaining_quantum, job.remaining_time)
|
|
796
|
+
preempted = False
|
|
797
|
+
if q_idx < len(queues) - 1 and pending:
|
|
798
|
+
next_arrival = pending[0].arrival_time
|
|
799
|
+
if next_arrival < curr_time + slice_duration:
|
|
800
|
+
slice_duration = next_arrival - curr_time
|
|
801
|
+
preempted = True
|
|
802
|
+
if next_boost_time is not None and next_boost_time < curr_time + slice_duration:
|
|
803
|
+
slice_duration = next_boost_time - curr_time
|
|
804
|
+
preempted = True
|
|
805
|
+
|
|
806
|
+
if job.response_time is None:
|
|
807
|
+
job.response_time = curr_time - job.arrival_time
|
|
808
|
+
|
|
809
|
+
if slice_duration > 0:
|
|
810
|
+
self.timeline[curr_time].append(
|
|
811
|
+
f"Running Job{job.job_id} in Q{q_idx} for {slice_duration}"
|
|
812
|
+
)
|
|
813
|
+
job.run_intervals.append((curr_time, curr_time + slice_duration, q_idx))
|
|
814
|
+
curr_time += slice_duration
|
|
815
|
+
job.remaining_time -= slice_duration
|
|
816
|
+
job.time_in_queue += slice_duration
|
|
817
|
+
job.remaining_quantum -= slice_duration
|
|
818
|
+
|
|
819
|
+
enqueue_arrivals(curr_time)
|
|
820
|
+
boosted_current_job = False
|
|
821
|
+
while next_boost_time is not None and curr_time >= next_boost_time:
|
|
822
|
+
apply_boost(curr_time, running_job=job if not boosted_current_job else None)
|
|
823
|
+
boosted_current_job = True
|
|
824
|
+
next_boost_time += boost_interval
|
|
825
|
+
|
|
826
|
+
if job.remaining_time <= 0:
|
|
827
|
+
job.turnaround_time = curr_time - job.arrival_time
|
|
828
|
+
job.remaining_quantum = None
|
|
829
|
+
completed.add(job.job_id)
|
|
830
|
+
self.timeline[curr_time].append(
|
|
831
|
+
f"Completed Job{job.job_id} (TAT = {job.turnaround_time})"
|
|
832
|
+
)
|
|
833
|
+
continue
|
|
834
|
+
if boosted_current_job:
|
|
835
|
+
continue
|
|
836
|
+
|
|
837
|
+
allotment = queue_allotments[q_idx]
|
|
838
|
+
if (
|
|
839
|
+
allotment is not None
|
|
840
|
+
and job.time_in_queue >= allotment
|
|
841
|
+
and q_idx > 0
|
|
842
|
+
):
|
|
843
|
+
job.queue_level = q_idx - 1
|
|
844
|
+
job.max_queue_level = max(job.max_queue_level, job.queue_level)
|
|
845
|
+
job.time_in_queue = 0
|
|
846
|
+
job.remaining_quantum = None
|
|
847
|
+
queues[q_idx - 1].append(job)
|
|
848
|
+
self.timeline[curr_time].append(
|
|
849
|
+
f"Demoted Job{job.job_id} to Q{q_idx - 1}"
|
|
850
|
+
)
|
|
851
|
+
continue
|
|
852
|
+
|
|
853
|
+
if preempted and job.remaining_quantum > 0:
|
|
854
|
+
queues[q_idx].appendleft(job)
|
|
855
|
+
else:
|
|
856
|
+
if job.remaining_quantum <= 0:
|
|
857
|
+
job.remaining_quantum = None
|
|
858
|
+
queues[q_idx].append(job)
|
|
859
|
+
|
|
623
860
|
def refresh(self, *args, **kwargs):
|
|
624
861
|
super().refresh(*args, **kwargs)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
862
|
+
|
|
863
|
+
self.num_jobs = kwargs.get("num_jobs", self.num_jobs)
|
|
864
|
+
self.num_queues = kwargs.get("num_queues", self.num_queues)
|
|
865
|
+
self.min_job_length = kwargs.get("min_job_length", self.min_job_length)
|
|
866
|
+
self.max_job_length = kwargs.get("max_job_length", self.max_job_length)
|
|
867
|
+
self.boost_interval = kwargs.get("boost_interval", self.boost_interval)
|
|
868
|
+
self.boost_interval_range = kwargs.get(
|
|
869
|
+
"boost_interval_range",
|
|
870
|
+
self.boost_interval_range
|
|
871
|
+
)
|
|
872
|
+
if self.boost_interval is None and self.boost_interval_range:
|
|
873
|
+
low, high = self.boost_interval_range
|
|
874
|
+
self.boost_interval = self.rng.randint(low, high)
|
|
875
|
+
|
|
876
|
+
jobs = self.get_workload(self.num_jobs)
|
|
877
|
+
|
|
878
|
+
queue_quantums = [2**(self.num_queues - 1 - i) for i in range(self.num_queues)]
|
|
879
|
+
queue_quantums = self._normalize_queue_params(queue_quantums, self.num_queues)
|
|
880
|
+
queue_quantums = [int(q) for q in queue_quantums]
|
|
881
|
+
|
|
882
|
+
queue_allotments = [None] + [
|
|
883
|
+
queue_quantums[i] * 2 for i in range(1, self.num_queues)
|
|
884
|
+
]
|
|
885
|
+
queue_allotments = self._normalize_queue_params(queue_allotments, self.num_queues)
|
|
886
|
+
queue_allotments = [
|
|
887
|
+
int(allotment) if allotment is not None else None
|
|
888
|
+
for allotment in queue_allotments
|
|
889
|
+
]
|
|
890
|
+
queue_allotments[0] = None
|
|
891
|
+
|
|
892
|
+
self.queue_quantums = queue_quantums
|
|
893
|
+
self.queue_allotments = queue_allotments
|
|
894
|
+
|
|
895
|
+
self.run_simulation(jobs, queue_quantums, queue_allotments, self.boost_interval)
|
|
896
|
+
|
|
897
|
+
self.job_stats = {
|
|
898
|
+
job.job_id: {
|
|
899
|
+
"arrival_time": job.arrival_time,
|
|
900
|
+
"duration": job.duration,
|
|
901
|
+
"Response": job.response_time,
|
|
902
|
+
"TAT": job.turnaround_time,
|
|
903
|
+
"run_intervals": list(job.run_intervals),
|
|
904
|
+
}
|
|
905
|
+
for job in jobs
|
|
635
906
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
907
|
+
|
|
908
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
909
|
+
self.answers.update({
|
|
910
|
+
f"answer__turnaround_time_job{job_id}": AnswerTypes.Float(self.job_stats[job_id]["TAT"])
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
return self.is_interesting()
|
|
914
|
+
|
|
915
|
+
def _get_body(self, *args, **kwargs):
|
|
916
|
+
answers: List[ContentAST.Answer] = []
|
|
917
|
+
|
|
918
|
+
queue_rows = []
|
|
919
|
+
for i in reversed(range(self.num_queues)):
|
|
920
|
+
allotment = self.queue_allotments[i]
|
|
921
|
+
queue_rows.append([
|
|
922
|
+
f"Q{i}",
|
|
923
|
+
self.queue_quantums[i],
|
|
924
|
+
"infinite" if allotment is None else allotment
|
|
925
|
+
])
|
|
926
|
+
queue_table = ContentAST.Table(
|
|
927
|
+
headers=["Queue", "Quantum", "Allotment"],
|
|
928
|
+
data=queue_rows
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
table_rows = []
|
|
932
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
933
|
+
table_rows.append({
|
|
934
|
+
"Job ID": f"Job{job_id}",
|
|
935
|
+
"Arrival": self.job_stats[job_id]["arrival_time"],
|
|
936
|
+
"Duration": self.job_stats[job_id]["duration"],
|
|
937
|
+
"TAT": f"answer__turnaround_time_job{job_id}",
|
|
938
|
+
})
|
|
939
|
+
answers.append(self.answers[f"answer__turnaround_time_job{job_id}"])
|
|
940
|
+
|
|
941
|
+
scheduling_table = self.create_answer_table(
|
|
942
|
+
headers=["Job ID", "Arrival", "Duration", "TAT"],
|
|
943
|
+
data_rows=table_rows,
|
|
944
|
+
answer_columns=["TAT"]
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
intro_text = (
|
|
948
|
+
"Assume an MLFQ scheduler with round-robin inside each queue. "
|
|
949
|
+
f"New jobs enter the highest-priority queue (Q{self.num_queues - 1}) "
|
|
950
|
+
"and a job is demoted after using its total allotment for that queue. "
|
|
951
|
+
"If a higher-priority job arrives, it preempts any lower-priority job."
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
instructions = (
|
|
955
|
+
f"Compute the turnaround time (TAT) for each job. "
|
|
956
|
+
f"Round to at most {ContentAST.Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal."
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
body = ContentAST.Section()
|
|
960
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
961
|
+
body.add_element(queue_table)
|
|
962
|
+
if self.boost_interval is not None:
|
|
963
|
+
body.add_element(ContentAST.Paragraph([
|
|
964
|
+
f"Every {self.boost_interval} time units, all jobs are boosted to "
|
|
965
|
+
f"Q{self.num_queues - 1}. After a boost, scheduling restarts with the "
|
|
966
|
+
"lowest job number in that queue."
|
|
967
|
+
]))
|
|
968
|
+
body.add_element(ContentAST.Paragraph([instructions]))
|
|
969
|
+
body.add_element(scheduling_table)
|
|
970
|
+
return body, answers
|
|
971
|
+
|
|
972
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
973
|
+
body, _ = self._get_body(*args, **kwargs)
|
|
974
|
+
return body
|
|
975
|
+
|
|
976
|
+
def _get_explanation(self, **kwargs):
|
|
977
|
+
explanation = ContentAST.Section()
|
|
978
|
+
|
|
979
|
+
explanation.add_element(
|
|
980
|
+
ContentAST.Paragraph([
|
|
981
|
+
"Turnaround time (TAT) is the completion time minus the arrival time.",
|
|
982
|
+
"We calculate it for each job after simulating the schedule."
|
|
983
|
+
])
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
explanation.add_element(
|
|
987
|
+
ContentAST.Paragraph([
|
|
988
|
+
"For each job:"
|
|
989
|
+
] + [
|
|
990
|
+
f"Job{job_id}_TAT = "
|
|
991
|
+
f"{self.job_stats[job_id]['arrival_time'] + self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f} "
|
|
992
|
+
f"- {self.job_stats[job_id]['arrival_time']:0.{self.ROUNDING_DIGITS}f} "
|
|
993
|
+
f"= {self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f}"
|
|
994
|
+
for job_id in sorted(self.job_stats.keys())
|
|
995
|
+
])
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
explanation.add_element(
|
|
999
|
+
ContentAST.Table(
|
|
1000
|
+
headers=["Time", "Events"],
|
|
1001
|
+
data=[
|
|
1002
|
+
[f"{t:0.{self.ROUNDING_DIGITS}f}s"] + ['\n'.join(events)]
|
|
1003
|
+
for t in sorted(self.timeline.keys())
|
|
1004
|
+
if (events := [
|
|
1005
|
+
event for event in self.timeline[t]
|
|
1006
|
+
if (
|
|
1007
|
+
"arrived" in event
|
|
1008
|
+
or "Demoted" in event
|
|
1009
|
+
or "Boosted" in event
|
|
1010
|
+
or "Completed" in event
|
|
1011
|
+
or "Simulation Start" in event
|
|
1012
|
+
or "CPU idle" in event
|
|
1013
|
+
)
|
|
1014
|
+
])
|
|
1015
|
+
]
|
|
1016
|
+
)
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
explanation.add_element(
|
|
1020
|
+
ContentAST.Picture(
|
|
1021
|
+
img_data=self.make_image(),
|
|
1022
|
+
caption="MLFQ Scheduling Overview"
|
|
1023
|
+
)
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
return explanation, []
|
|
1027
|
+
|
|
1028
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
1029
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
1030
|
+
return explanation
|
|
1031
|
+
|
|
1032
|
+
def make_image(self):
|
|
1033
|
+
fig, ax = plt.subplots(1, 1, figsize=self.IMAGE_FIGSIZE, dpi=self.IMAGE_DPI)
|
|
1034
|
+
|
|
1035
|
+
num_jobs = len(self.job_stats)
|
|
1036
|
+
if num_jobs == 0:
|
|
1037
|
+
buffer = io.BytesIO()
|
|
1038
|
+
plt.tight_layout()
|
|
1039
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight')
|
|
1040
|
+
plt.close(fig)
|
|
1041
|
+
buffer.seek(0)
|
|
1042
|
+
return buffer
|
|
1043
|
+
|
|
1044
|
+
job_colors = {
|
|
1045
|
+
job_id: str(0.15 + 0.7 * (idx / max(1, num_jobs - 1)))
|
|
1046
|
+
for idx, job_id in enumerate(sorted(self.job_stats.keys()))
|
|
1047
|
+
}
|
|
1048
|
+
job_lane = {
|
|
1049
|
+
job_id: idx
|
|
1050
|
+
for idx, job_id in enumerate(sorted(self.job_stats.keys(), reverse=True))
|
|
1051
|
+
}
|
|
1052
|
+
lanes_per_queue = num_jobs
|
|
1053
|
+
|
|
1054
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
1055
|
+
for start, stop, queue_level in self.job_stats[job_id]["run_intervals"]:
|
|
1056
|
+
y_loc = queue_level * lanes_per_queue + job_lane[job_id]
|
|
1057
|
+
ax.barh(
|
|
1058
|
+
y=[y_loc],
|
|
1059
|
+
left=[start],
|
|
1060
|
+
width=[stop - start],
|
|
1061
|
+
edgecolor='black',
|
|
1062
|
+
linewidth=1.5,
|
|
1063
|
+
color=job_colors[job_id]
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
for queue_idx in range(self.num_queues):
|
|
1067
|
+
if queue_idx % 2 == 1:
|
|
1068
|
+
ax.axhspan(
|
|
1069
|
+
queue_idx * lanes_per_queue - 0.5,
|
|
1070
|
+
(queue_idx + 1) * lanes_per_queue - 0.5,
|
|
1071
|
+
facecolor='0.97',
|
|
1072
|
+
edgecolor='none',
|
|
1073
|
+
zorder=-1
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
arrival_times = sorted({
|
|
1077
|
+
self.job_stats[job_id]["arrival_time"]
|
|
1078
|
+
for job_id in self.job_stats.keys()
|
|
1079
|
+
})
|
|
1080
|
+
bottom_label_y = -0.1
|
|
1081
|
+
for arrival_time in arrival_times:
|
|
1082
|
+
ax.axvline(arrival_time, color='0.2', linestyle=':', linewidth=1.2, zorder=0)
|
|
1083
|
+
ax.text(
|
|
1084
|
+
arrival_time + 0.2,
|
|
1085
|
+
bottom_label_y,
|
|
1086
|
+
f"{arrival_time:0.{self.ROUNDING_DIGITS}f}s",
|
|
1087
|
+
color='0.2',
|
|
1088
|
+
rotation=90,
|
|
1089
|
+
ha='left',
|
|
1090
|
+
va='bottom'
|
|
643
1091
|
)
|
|
1092
|
+
|
|
1093
|
+
completion_times = sorted({
|
|
1094
|
+
self.job_stats[job_id]["arrival_time"] + self.job_stats[job_id]["TAT"]
|
|
1095
|
+
for job_id in self.job_stats.keys()
|
|
1096
|
+
})
|
|
1097
|
+
for completion_time in completion_times:
|
|
1098
|
+
ax.axvline(completion_time, color='red', linewidth=1.5, zorder=0)
|
|
1099
|
+
ax.text(
|
|
1100
|
+
completion_time - 0.6,
|
|
1101
|
+
self.num_queues * lanes_per_queue - 0.5,
|
|
1102
|
+
f"{completion_time:0.{self.ROUNDING_DIGITS}f}s",
|
|
1103
|
+
color='red',
|
|
1104
|
+
rotation=90,
|
|
1105
|
+
ha='center',
|
|
1106
|
+
va='top'
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
for boost_time in sorted(set(self.boost_times)):
|
|
1110
|
+
ax.axvline(boost_time, color='tab:blue', linestyle='--', linewidth=1.2, zorder=0)
|
|
1111
|
+
|
|
1112
|
+
tick_positions = [
|
|
1113
|
+
q * lanes_per_queue + (lanes_per_queue - 1) / 2
|
|
1114
|
+
for q in range(self.num_queues)
|
|
644
1115
|
]
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1116
|
+
ax.set_yticks(tick_positions)
|
|
1117
|
+
ax.set_yticklabels([f"Q{i}" for i in range(self.num_queues)])
|
|
1118
|
+
ax.set_ylim(-0.5, self.num_queues * lanes_per_queue - 0.5)
|
|
1119
|
+
ax.set_xlim(xmin=0)
|
|
1120
|
+
ax.set_xlabel("Time")
|
|
1121
|
+
|
|
1122
|
+
buffer = io.BytesIO()
|
|
1123
|
+
plt.tight_layout()
|
|
1124
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight')
|
|
1125
|
+
plt.close(fig)
|
|
1126
|
+
buffer.seek(0)
|
|
1127
|
+
return buffer
|