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.
Files changed (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {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, Answer, QuestionRegistry, RegenerableChoiceMixin
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}": Answer.auto_float(
360
- f"answer__response_time_job{job_id}",
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": Answer.auto_float(
370
- "answer__average_response_time",
371
- sum([job.response_time for job in jobs]) / len(jobs)
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": Answer.auto_float(
374
- "answer__average_turnaround_time",
375
- sum([job.turnaround_time for job in jobs]) / len(jobs)
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 get_body(self, *args, **kwargs) -> ContentAST.Section:
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 <b>{self.scheduler_algorithm}</b> scheduling. "
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 get_explanation(self, **kwargs) -> ContentAST.Section:
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.savefig(buffer, format='png')
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
- class MLFQ_Question(ProcessQuestion):
603
-
604
- MIN_DURATION = 10
605
- MAX_DURATION = 100
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 = 100
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
- arrival_time: float
612
- duration: float
613
- elapsed_time: float = 0.0
614
- response_time: float = None
615
- turnaround_time: float = None
616
-
617
- def run_for_slice(self, slice_duration):
618
- self.elapsed_time += slice_duration
619
-
620
- def is_complete(self):
621
- return math.isclose(self.duration, self.elapsed_time)
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
- # Set up defaults
627
- # todo: allow for per-queue specification of durations, likely through dicts
628
- num_queues = kwargs.get("num_queues", 2)
629
- num_jobs = kwargs.get("num_jobs", 2)
630
-
631
- # Set up queues that we will be using
632
- mlfq_queues = {
633
- priority : queue.Queue()
634
- for priority in range(num_queues)
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
- # Set up jobs that we'll be using
638
- jobs = [
639
- MLFQ_Question.Job(
640
- arrival_time=self.rng.randint(self.MIN_ARRIVAL, self.MAX_ARRIVAL),
641
- duration=self.rng.randint(self.MIN_DURATION, self.MAX_DURATION),
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
- curr_time = -1.0
647
- while True:
648
- pass
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