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.
Files changed (31) hide show
  1. QuizGenerator/contentast.py +952 -82
  2. QuizGenerator/generate.py +45 -9
  3. QuizGenerator/misc.py +4 -554
  4. QuizGenerator/mixins.py +47 -25
  5. QuizGenerator/premade_questions/cst334/languages.py +139 -125
  6. QuizGenerator/premade_questions/cst334/math_questions.py +78 -66
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +258 -144
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +71 -33
  9. QuizGenerator/premade_questions/cst334/process.py +554 -64
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +32 -6
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +59 -34
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +27 -8
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +53 -32
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +228 -88
  15. QuizGenerator/premade_questions/cst463/models/attention.py +26 -10
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -19
  17. QuizGenerator/premade_questions/cst463/models/rnns.py +25 -12
  18. QuizGenerator/premade_questions/cst463/models/text.py +26 -11
  19. QuizGenerator/premade_questions/cst463/models/weight_counting.py +36 -22
  20. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +89 -109
  21. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +117 -51
  22. QuizGenerator/question.py +110 -15
  23. QuizGenerator/quiz.py +81 -24
  24. QuizGenerator/regenerate.py +98 -29
  25. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.1.dist-info}/METADATA +1 -1
  26. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.1.dist-info}/RECORD +29 -31
  27. QuizGenerator/README.md +0 -5
  28. QuizGenerator/logging.yaml +0 -55
  29. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.1.dist-info}/WHEEL +0 -0
  30. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.1.dist-info}/entry_points.txt +0 -0
  31. {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 get_body(self, *args, **kwargs) -> ContentAST.Section:
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 <b>{self.scheduler_algorithm}</b> scheduling. "
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 get_explanation(self, **kwargs) -> ContentAST.Section:
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.savefig(buffer, format='png')
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
- class MLFQ_Question(ProcessQuestion):
603
-
604
- MIN_DURATION = 10
605
- MAX_DURATION = 100
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 = 100
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
- 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
-
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
- # 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)
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
- # 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
-
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
- curr_time = -1.0
647
- while True:
648
- pass
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