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