QuizGenerator 0.4.2__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 (52) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/__init__.py +27 -0
  3. QuizGenerator/__main__.py +7 -0
  4. QuizGenerator/canvas/__init__.py +13 -0
  5. QuizGenerator/canvas/canvas_interface.py +627 -0
  6. QuizGenerator/canvas/classes.py +235 -0
  7. QuizGenerator/constants.py +149 -0
  8. QuizGenerator/contentast.py +1955 -0
  9. QuizGenerator/generate.py +253 -0
  10. QuizGenerator/logging.yaml +55 -0
  11. QuizGenerator/misc.py +579 -0
  12. QuizGenerator/mixins.py +548 -0
  13. QuizGenerator/performance.py +202 -0
  14. QuizGenerator/premade_questions/__init__.py +0 -0
  15. QuizGenerator/premade_questions/basic.py +103 -0
  16. QuizGenerator/premade_questions/cst334/__init__.py +1 -0
  17. QuizGenerator/premade_questions/cst334/languages.py +391 -0
  18. QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
  19. QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
  20. QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
  21. QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
  22. QuizGenerator/premade_questions/cst334/process.py +648 -0
  23. QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  24. QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
  25. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
  26. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
  27. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
  28. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
  29. QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
  30. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
  31. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
  32. QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  33. QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
  34. QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
  35. QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
  36. QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
  37. QuizGenerator/premade_questions/cst463/models/text.py +203 -0
  38. QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
  39. QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
  40. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
  41. QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
  42. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
  43. QuizGenerator/qrcode_generator.py +293 -0
  44. QuizGenerator/question.py +715 -0
  45. QuizGenerator/quiz.py +467 -0
  46. QuizGenerator/regenerate.py +472 -0
  47. QuizGenerator/typst_utils.py +113 -0
  48. quizgenerator-0.4.2.dist-info/METADATA +265 -0
  49. quizgenerator-0.4.2.dist-info/RECORD +52 -0
  50. quizgenerator-0.4.2.dist-info/WHEEL +4 -0
  51. quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
  52. quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,648 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+ import collections
6
+ import dataclasses
7
+ import enum
8
+ import io
9
+ import logging
10
+ import math
11
+ import os
12
+ import queue
13
+ import uuid
14
+ from typing import List
15
+
16
+ import matplotlib.pyplot as plt
17
+
18
+ from QuizGenerator.contentast import ContentAST
19
+ from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
20
+ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+
25
+ class ProcessQuestion(Question, abc.ABC):
26
+ def __init__(self, *args, **kwargs):
27
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.PROCESS)
28
+ super().__init__(*args, **kwargs)
29
+
30
+
31
+ @QuestionRegistry.register()
32
+ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionMixin, BodyTemplatesMixin):
33
+ class Kind(enum.Enum):
34
+ FIFO = enum.auto()
35
+ ShortestDuration = enum.auto()
36
+ ShortestTimeRemaining = enum.auto()
37
+ RoundRobin = enum.auto()
38
+
39
+ def __str__(self):
40
+ display_names = {
41
+ self.FIFO: "First In First Out",
42
+ self.ShortestDuration: "Shortest Job First",
43
+ self.ShortestTimeRemaining: "Shortest Time To Completion",
44
+ self.RoundRobin: "Round Robin"
45
+ }
46
+ return display_names.get(self, self.name)
47
+
48
+ @staticmethod
49
+ def get_kind_from_string(kind_str: str) -> SchedulingQuestion.Kind:
50
+ try:
51
+ return SchedulingQuestion.Kind[kind_str]
52
+ except KeyError:
53
+ return SchedulingQuestion.Kind.FIFO
54
+
55
+ MAX_JOBS = 4
56
+ MAX_ARRIVAL_TIME = 20
57
+ MIN_JOB_DURATION = 2
58
+ MAX_JOB_DURATION = 10
59
+
60
+ ANSWER_EPSILON = 1.0
61
+
62
+ scheduler_algorithm = None
63
+ SELECTOR = None
64
+ PREEMPTABLE = False
65
+ TIME_QUANTUM = None
66
+
67
+ ROUNDING_DIGITS = 2
68
+
69
+ @dataclasses.dataclass
70
+ class Job:
71
+ job_id: int
72
+ arrival_time: float
73
+ duration: float
74
+ elapsed_time: float = 0
75
+ response_time: float = None
76
+ turnaround_time: float = None
77
+ unpause_time: float | None = None
78
+ last_run: float = 0 # When were we last scheduled
79
+
80
+ state_change_times: List[float] = dataclasses.field(default_factory=lambda: [])
81
+
82
+ SCHEDULER_EPSILON = 1e-09
83
+
84
+ def run(self, curr_time, is_rr=False) -> None:
85
+ if self.response_time is None:
86
+ # Then this is the first time running
87
+ self.mark_start(curr_time)
88
+ self.unpause_time = curr_time
89
+ if not is_rr:
90
+ self.state_change_times.append(curr_time)
91
+
92
+ def stop(self, curr_time, is_rr=False) -> None:
93
+ self.elapsed_time += (curr_time - self.unpause_time)
94
+ if self.is_complete(curr_time):
95
+ self.mark_end(curr_time)
96
+ self.unpause_time = None
97
+ self.last_run = curr_time
98
+ if not is_rr:
99
+ self.state_change_times.append(curr_time)
100
+
101
+ def mark_start(self, curr_time) -> None:
102
+ self.start_time = curr_time
103
+ self.response_time = curr_time - self.arrival_time + self.SCHEDULER_EPSILON
104
+
105
+ def mark_end(self, curr_time) -> None:
106
+ self.end_time = curr_time
107
+ self.turnaround_time = curr_time - self.arrival_time + self.SCHEDULER_EPSILON
108
+
109
+ def time_remaining(self, curr_time) -> float:
110
+ time_remaining = self.duration
111
+ time_remaining -= self.elapsed_time
112
+ if self.unpause_time is not None:
113
+ time_remaining -= (curr_time - self.unpause_time)
114
+ return time_remaining
115
+
116
+ def is_complete(self, curr_time) -> bool:
117
+ return self.duration <= self.elapsed_time + self.SCHEDULER_EPSILON # self.time_remaining(curr_time) <= 0
118
+
119
+ def has_started(self) -> bool:
120
+ return self.response_time is None
121
+
122
+ def get_workload(self, num_jobs, *args, **kwargs) -> List[SchedulingQuestion.Job]:
123
+ """Makes a guaranteed interesting workload by following rules
124
+ 1. First job to arrive is the longest
125
+ 2. At least 2 other jobs arrive in its runtime
126
+ 3. Those jobs arrive in reverse length order, with the smaller arriving 2nd
127
+
128
+ This will clearly show when jobs arrive how they are handled, since FIFO will be different than SJF, and STCF will cause interruptions
129
+ """
130
+
131
+ workload = []
132
+
133
+ # First create a job that is relatively long-running and arrives first.
134
+ # Set arrival time to something fairly low
135
+ job0_arrival = self.rng.randint(0, int(0.25 * self.MAX_ARRIVAL_TIME))
136
+ # Set duration to something fairly long
137
+ job0_duration = self.rng.randint(int(self.MAX_JOB_DURATION * 0.75), self.MAX_JOB_DURATION)
138
+
139
+ # Next, let's create a job that will test whether we are preemptive or not.
140
+ # The core characteristics of this job are that it:
141
+ # 1) would also finish _before_ the end of job0 if selected to run immediately. This tests STCF
142
+ # The bounds for arrival and completion will be:
143
+ # arrival:
144
+ # lower: (job0_arrival + 1) so we have a definite first job
145
+ # upper: (job0_arrival + job0_duration - self.MIN_JOB_DURATION) so we have enough time for a job to run
146
+ # duration:
147
+ # lower: self.MIN_JOB_DURATION
148
+ # upper:
149
+ job1_arrival = self.rng.randint(
150
+ job0_arrival + 1, # Make sure we start _after_ job0
151
+ job0_arrival + job0_duration - self.MIN_JOB_DURATION - 2 # Make sure we always have enough time for job1 & job2
152
+ )
153
+ job1_duration = self.rng.randint(
154
+ self.MIN_JOB_DURATION + 1, # default minimum and leave room for job2
155
+ job0_arrival + job0_duration - job1_arrival - 1 # Make sure our job ends _at least_ before job0 would end
156
+ )
157
+
158
+ # Finally, we want to differentiate between STCF and SJF
159
+ # So, if we don't preempt job0 we want to make it be a tough choice between the next 2 jobs when it completes.
160
+ # This means we want a job that arrives _before_ job0 finishes, after job1 enters, and is shorter than job1
161
+ job2_arrival = self.rng.randint(
162
+ job1_arrival + 1, # Make sure we arrive after job1 so we subvert FIFO
163
+ job0_arrival + job0_duration - 1 # ...but before job0 would exit the system
164
+ )
165
+ job2_duration = self.rng.randint(
166
+ self.MIN_JOB_DURATION, # Make sure it's at least the minimum.
167
+ job1_duration - 1, # Make sure it's shorter than job1
168
+ )
169
+
170
+ # Package them up so we can add more jobs as necessary
171
+ job_tuples = [
172
+ (job0_arrival, job0_duration),
173
+ (job1_arrival, job1_duration),
174
+ (job2_arrival, job2_duration),
175
+ ]
176
+
177
+ # Add more jobs as necessary, if more than 3 are requested
178
+ if num_jobs > 3:
179
+ job_tuples.extend([
180
+ (self.rng.randint(0, self.MAX_ARRIVAL_TIME), self.rng.randint(self.MIN_JOB_DURATION, self.MAX_JOB_DURATION))
181
+ for _ in range(num_jobs - 3)
182
+ ])
183
+
184
+ # Shuffle jobs so they are in a random order
185
+ self.rng.shuffle(job_tuples)
186
+
187
+ # Make workload from job tuples
188
+ workload = []
189
+ for i, (arr, dur) in enumerate(job_tuples):
190
+ workload.append(
191
+ SchedulingQuestion.Job(
192
+ job_id=i,
193
+ arrival_time=arr,
194
+ duration=dur
195
+ )
196
+ )
197
+
198
+ return workload
199
+
200
+ def run_simulation(self, jobs_to_run: List[SchedulingQuestion.Job], selector, preemptable, time_quantum=None):
201
+ curr_time = 0
202
+ selected_job: SchedulingQuestion.Job | None = None
203
+
204
+ self.timeline = collections.defaultdict(list)
205
+ self.timeline[curr_time].append("Simulation Start")
206
+ for job in jobs_to_run:
207
+ self.timeline[job.arrival_time].append(f"Job{job.job_id} arrived")
208
+
209
+ while len(jobs_to_run) > 0:
210
+ possible_time_slices = []
211
+
212
+ # Get the jobs currently in the system
213
+ available_jobs = list(filter(
214
+ (lambda j: j.arrival_time <= curr_time),
215
+ jobs_to_run
216
+ ))
217
+
218
+ # Get the jobs that will enter the system in the future
219
+ future_jobs : List[SchedulingQuestion.Job] = list(filter(
220
+ (lambda j: j.arrival_time > curr_time),
221
+ jobs_to_run
222
+ ))
223
+
224
+ # Check whether there are jobs in the system already
225
+ if len(available_jobs) > 0:
226
+ # Use the selector to identify what job we are going to run
227
+ selected_job : SchedulingQuestion.Job = min(
228
+ available_jobs,
229
+ key=(lambda j: selector(j, curr_time))
230
+ )
231
+ if selected_job.has_started():
232
+ self.timeline[curr_time].append(f"Starting Job{selected_job.job_id} (resp = {curr_time - selected_job.arrival_time:0.{self.ROUNDING_DIGITS}f}s)")
233
+ # We start the job that we selected
234
+ selected_job.run(curr_time, (self.scheduler_algorithm == self.Kind.RoundRobin))
235
+
236
+ # We could run to the end of the job
237
+ possible_time_slices.append(selected_job.time_remaining(curr_time))
238
+
239
+ # Check if we are preemptable or if we haven't found any time slices yet
240
+ if preemptable or len(possible_time_slices) == 0:
241
+ # Then when a job enters we could stop the current task
242
+ if len(future_jobs) != 0:
243
+ next_arrival : SchedulingQuestion.Job = min(
244
+ future_jobs,
245
+ key=(lambda j: j.arrival_time)
246
+ )
247
+ possible_time_slices.append( (next_arrival.arrival_time - curr_time))
248
+
249
+ if time_quantum is not None:
250
+ possible_time_slices.append(time_quantum)
251
+
252
+
253
+ ## Now we pick the minimum
254
+ try:
255
+ next_time_slice = min(possible_time_slices)
256
+ except ValueError:
257
+ log.error("No jobs available to schedule")
258
+ break
259
+ if self.scheduler_algorithm != SchedulingQuestion.Kind.RoundRobin:
260
+ if selected_job is not None:
261
+ self.timeline[curr_time].append(f"Running Job{selected_job.job_id} for {next_time_slice:0.{self.ROUNDING_DIGITS}f}s")
262
+ else:
263
+ self.timeline[curr_time].append(f"(No job running)")
264
+ curr_time += next_time_slice
265
+
266
+ # We stop the job we selected, and potentially mark it as complete
267
+ if selected_job is not None:
268
+ selected_job.stop(curr_time, (self.scheduler_algorithm == self.Kind.RoundRobin))
269
+ if selected_job.is_complete(curr_time):
270
+ self.timeline[curr_time].append(f"Completed Job{selected_job.job_id} (TAT = {selected_job.turnaround_time:0.{self.ROUNDING_DIGITS}f}s)")
271
+ selected_job = None
272
+
273
+ # Filter out completed jobs
274
+ jobs_to_run : List[SchedulingQuestion.Job] = list(filter(
275
+ (lambda j: not j.is_complete(curr_time)),
276
+ jobs_to_run
277
+ ))
278
+ if len(jobs_to_run) == 0:
279
+ break
280
+
281
+ def __init__(self, num_jobs=3, scheduler_kind=None, *args, **kwargs):
282
+ # Preserve question-specific params for QR code config BEFORE calling super().__init__()
283
+ kwargs['num_jobs'] = num_jobs
284
+
285
+ # Register the regenerable choice using the mixin
286
+ self.register_choice('scheduler_kind', SchedulingQuestion.Kind, scheduler_kind, kwargs)
287
+
288
+ super().__init__(*args, **kwargs)
289
+ self.num_jobs = num_jobs
290
+
291
+ def refresh(self, *args, **kwargs):
292
+ # Initialize job_stats before calling super().refresh() since parent's refresh
293
+ # will call is_interesting() which needs this attribute to exist
294
+ self.job_stats = {}
295
+
296
+ # Call parent refresh which seeds RNG and calls is_interesting()
297
+ # Note: We ignore the parent's return value since we need to generate the workload first
298
+ super().refresh(*args, **kwargs)
299
+
300
+ # Use the mixin to get the scheduler (randomly selected or fixed)
301
+ self.scheduler_algorithm = self.get_choice('scheduler_kind', SchedulingQuestion.Kind)
302
+
303
+ # Get workload jobs
304
+ jobs = self.get_workload(self.num_jobs)
305
+
306
+ # Run simulations different depending on which algorithm we chose
307
+ match self.scheduler_algorithm:
308
+ case SchedulingQuestion.Kind.ShortestDuration:
309
+ self.run_simulation(
310
+ jobs_to_run=jobs,
311
+ selector=(lambda j, curr_time: (j.duration, j.job_id)),
312
+ preemptable=False,
313
+ time_quantum=None
314
+ )
315
+ case SchedulingQuestion.Kind.ShortestTimeRemaining:
316
+ self.run_simulation(
317
+ jobs_to_run=jobs,
318
+ selector=(lambda j, curr_time: (j.time_remaining(curr_time), j.job_id)),
319
+ preemptable=True,
320
+ time_quantum=None
321
+ )
322
+ case SchedulingQuestion.Kind.RoundRobin:
323
+ self.run_simulation(
324
+ jobs_to_run=jobs,
325
+ selector=(lambda j, curr_time: (j.last_run, j.job_id)),
326
+ preemptable=True,
327
+ time_quantum=1e-04
328
+ )
329
+ case _:
330
+ self.run_simulation(
331
+ jobs_to_run=jobs,
332
+ selector=(lambda j, curr_time: (j.arrival_time, j.job_id)),
333
+ preemptable=False,
334
+ time_quantum=None
335
+ )
336
+
337
+ # Collate stats
338
+ self.job_stats = {
339
+ i : {
340
+ "arrival_time" : job.arrival_time, # input
341
+ "duration" : job.duration, # input
342
+ "Response" : job.response_time, # output
343
+ "TAT" : job.turnaround_time, # output
344
+ "state_changes" : [job.arrival_time] + job.state_change_times + [job.arrival_time + job.turnaround_time],
345
+ }
346
+ for (i, job) in enumerate(jobs)
347
+ }
348
+ self.overall_stats = {
349
+ "Response" : sum([job.response_time for job in jobs]) / len(jobs),
350
+ "TAT" : sum([job.turnaround_time for job in jobs]) / len(jobs)
351
+ }
352
+
353
+ # todo: make this less convoluted
354
+ self.average_response = self.overall_stats["Response"]
355
+ self.average_tat = self.overall_stats["TAT"]
356
+
357
+ for job_id in sorted(self.job_stats.keys()):
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
+ ),
367
+ })
368
+ 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)
372
+ ),
373
+ "answer__average_turnaround_time": Answer.auto_float(
374
+ "answer__average_turnaround_time",
375
+ sum([job.turnaround_time for job in jobs]) / len(jobs)
376
+ )
377
+ })
378
+
379
+ # Return whether this workload is interesting
380
+ return self.is_interesting()
381
+
382
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
383
+ # Create table data for scheduling results
384
+ table_rows = []
385
+ for job_id in sorted(self.job_stats.keys()):
386
+ table_rows.append({
387
+ "Job ID": f"Job{job_id}",
388
+ "Arrival": self.job_stats[job_id]["arrival_time"],
389
+ "Duration": self.job_stats[job_id]["duration"],
390
+ "Response Time": f"answer__response_time_job{job_id}", # Answer key
391
+ "TAT": f"answer__turnaround_time_job{job_id}" # Answer key
392
+ })
393
+
394
+ # Create table using mixin
395
+ scheduling_table = self.create_answer_table(
396
+ headers=["Job ID", "Arrival", "Duration", "Response Time", "TAT"],
397
+ data_rows=table_rows,
398
+ answer_columns=["Response Time", "TAT"]
399
+ )
400
+
401
+ # 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
+ ])
406
+
407
+ # Use mixin to create complete body
408
+ intro_text = (
409
+ f"Given the below information, compute the required values if using <b>{self.scheduler_algorithm}</b> scheduling. "
410
+ f"Break any ties using the job number."
411
+ )
412
+
413
+ 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. "
415
+ "Examples of appropriately formatted answers would be `0`, `3/2`, `1 1/3`, `1.6667`, and `1.25`. "
416
+ "Note that answers that can be rounded to whole numbers should be, rather than being left in fractional form."
417
+ ])])
418
+
419
+ body = self.create_fill_in_table_body(intro_text, instructions, scheduling_table)
420
+ body.add_element(average_block)
421
+ return body
422
+
423
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
424
+ explanation = ContentAST.Section()
425
+
426
+ explanation.add_element(
427
+ ContentAST.Paragraph([
428
+ f"To calculate the overall Turnaround and Response times using {self.scheduler_algorithm} "
429
+ f"we want to first start by calculating the respective target and response times of all of our individual jobs."
430
+ ])
431
+ )
432
+
433
+ explanation.add_elements([
434
+ ContentAST.Paragraph([
435
+ "We do this by subtracting arrival time from either the completion time or the start time. That is:"
436
+ ]),
437
+ ContentAST.Equation("Job_{TAT} = Job_{completion} - Job_{arrival\_time}"),
438
+ ContentAST.Equation("Job_{response} = Job_{start} - Job_{arrival\_time}"),
439
+ ])
440
+
441
+ explanation.add_element(
442
+ ContentAST.Paragraph([
443
+ f"For each of our {len(self.job_stats.keys())} jobs, we can make these calculations.",
444
+ ])
445
+ )
446
+
447
+ ## Add in TAT
448
+ explanation.add_element(
449
+ ContentAST.Paragraph([
450
+ "For turnaround time (TAT) this would be:"
451
+ ] + [
452
+ f"Job{job_id}_TAT "
453
+ f"= {self.job_stats[job_id]['arrival_time'] + self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f} "
454
+ f"- {self.job_stats[job_id]['arrival_time']:0.{self.ROUNDING_DIGITS}f} "
455
+ f"= {self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f}"
456
+ for job_id in sorted(self.job_stats.keys())
457
+ ])
458
+ )
459
+
460
+ summation_line = ' + '.join([
461
+ f"{self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f}" for job_id in sorted(self.job_stats.keys())
462
+ ])
463
+ explanation.add_element(
464
+ ContentAST.Paragraph([
465
+ f"We then calculate the average of these to find the average TAT time",
466
+ f"Avg(TAT) = ({summation_line}) / ({len(self.job_stats.keys())}) "
467
+ f"= {self.overall_stats['TAT']:0.{self.ROUNDING_DIGITS}f}",
468
+ ])
469
+ )
470
+
471
+
472
+ ## Add in Response
473
+ explanation.add_element(
474
+ ContentAST.Paragraph([
475
+ "For response time this would be:"
476
+ ] + [
477
+ f"Job{job_id}_response "
478
+ f"= {self.job_stats[job_id]['arrival_time'] + self.job_stats[job_id]['Response']:0.{self.ROUNDING_DIGITS}f} "
479
+ f"- {self.job_stats[job_id]['arrival_time']:0.{self.ROUNDING_DIGITS}f} "
480
+ f"= {self.job_stats[job_id]['Response']:0.{self.ROUNDING_DIGITS}f}"
481
+ for job_id in sorted(self.job_stats.keys())
482
+ ])
483
+ )
484
+
485
+ summation_line = ' + '.join([
486
+ f"{self.job_stats[job_id]['Response']:0.{self.ROUNDING_DIGITS}f}" for job_id in sorted(self.job_stats.keys())
487
+ ])
488
+ explanation.add_element(
489
+ ContentAST.Paragraph([
490
+ f"We then calculate the average of these to find the average Response time",
491
+ f"Avg(Response) "
492
+ f"= ({summation_line}) / ({len(self.job_stats.keys())}) "
493
+ f"= {self.overall_stats['Response']:0.{self.ROUNDING_DIGITS}f}",
494
+ "\n",
495
+ ])
496
+ )
497
+
498
+ explanation.add_element(
499
+ ContentAST.Table(
500
+ headers=["Time", "Events"],
501
+ data=[
502
+ [f"{t:02.{self.ROUNDING_DIGITS}f}s"] + ['\n'.join(self.timeline[t])]
503
+ for t in sorted(self.timeline.keys())
504
+ ]
505
+ )
506
+ )
507
+
508
+ explanation.add_element(
509
+ ContentAST.Picture(
510
+ img_data=self.make_image(),
511
+ caption="Process Scheduling Overview"
512
+ )
513
+ )
514
+
515
+ return explanation
516
+
517
+ def is_interesting(self) -> bool:
518
+ duration_sum = sum([self.job_stats[job_id]['duration'] for job_id in self.job_stats.keys()])
519
+ tat_sum = sum([self.job_stats[job_id]['TAT'] for job_id in self.job_stats.keys()])
520
+ return (tat_sum >= duration_sum * 1.1)
521
+
522
+ def make_image(self):
523
+
524
+ fig, ax = plt.subplots(1, 1)
525
+
526
+ 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
+ ax.axvline(x_loc, zorder=0)
528
+ plt.text(x_loc + 0, len(self.job_stats.keys())-0.3, f'{x_loc:0.{self.ROUNDING_DIGITS}f}s', rotation=90)
529
+
530
+ if self.scheduler_algorithm != self.Kind.RoundRobin:
531
+ for y_loc, job_id in enumerate(sorted(self.job_stats.keys(), reverse=True)):
532
+ for i, (start, stop) in enumerate(zip(self.job_stats[job_id]["state_changes"], self.job_stats[job_id]["state_changes"][1:])):
533
+ ax.barh(
534
+ y = [y_loc],
535
+ left = [start],
536
+ width = [stop - start],
537
+ edgecolor='black',
538
+ linewidth = 2,
539
+ color = 'white' if (i % 2 == 1) else 'black'
540
+ )
541
+ else:
542
+ job_deltas = collections.defaultdict(int)
543
+ for job_id in self.job_stats.keys():
544
+ job_deltas[self.job_stats[job_id]["state_changes"][0]] += 1
545
+ job_deltas[self.job_stats[job_id]["state_changes"][1]] -= 1
546
+
547
+ regimes_ranges = zip(sorted(job_deltas.keys()), sorted(job_deltas.keys())[1:])
548
+
549
+ for (low, high) in regimes_ranges:
550
+ jobs_in_range = [
551
+ i for i, job_id in enumerate(list(self.job_stats.keys())[::-1])
552
+ if
553
+ (self.job_stats[job_id]["state_changes"][0] <= low)
554
+ and
555
+ (self.job_stats[job_id]["state_changes"][1] >= high)
556
+ ]
557
+
558
+ if len(jobs_in_range) == 0: continue
559
+
560
+ ax.barh(
561
+ y = jobs_in_range,
562
+ left = [low for _ in jobs_in_range],
563
+ width = [high - low for _ in jobs_in_range],
564
+ color=f"{ 1 - ((len(jobs_in_range) - 1) / (len(self.job_stats.keys())))}",
565
+ )
566
+
567
+ # Plot the overall TAT
568
+ ax.barh(
569
+ y = [i for i in range(len(self.job_stats))][::-1],
570
+ left = [self.job_stats[job_id]["arrival_time"] for job_id in sorted(self.job_stats.keys())],
571
+ width = [self.job_stats[job_id]["TAT"] for job_id in sorted(self.job_stats.keys())],
572
+ tick_label = [f"Job{job_id}" for job_id in sorted(self.job_stats.keys())],
573
+ color=(0,0,0,0),
574
+ edgecolor='black',
575
+ linewidth=2,
576
+ )
577
+
578
+ ax.set_xlim(xmin=0)
579
+
580
+ # Save to BytesIO object instead of a file
581
+ buffer = io.BytesIO()
582
+ plt.savefig(buffer, format='png')
583
+ plt.close(fig)
584
+
585
+ # Reset buffer position to the beginning
586
+ buffer.seek(0)
587
+ return buffer
588
+
589
+ def make_image_file(self, image_dir="imgs"):
590
+
591
+ image_buffer = self.make_image()
592
+
593
+ # Original file-saving logic
594
+ if not os.path.exists(image_dir): os.mkdir(image_dir)
595
+ image_path = os.path.join(image_dir, f"{str(self.scheduler_algorithm).replace(' ', '_')}-{uuid.uuid4()}.png")
596
+
597
+ with open(image_path, 'wb') as fid:
598
+ fid.write(image_buffer.getvalue())
599
+ return image_path
600
+
601
+
602
+ class MLFQ_Question(ProcessQuestion):
603
+
604
+ MIN_DURATION = 10
605
+ MAX_DURATION = 100
606
+ MIN_ARRIVAL = 0
607
+ MAX_ARRIVAL = 100
608
+
609
+ @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
+
623
+ def refresh(self, *args, **kwargs):
624
+ 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)
635
+ }
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
+
643
+ )
644
+ ]
645
+
646
+ curr_time = -1.0
647
+ while True:
648
+ pass
File without changes
@@ -0,0 +1,3 @@
1
+ from .gradient_descent_questions import GradientDescentWalkthrough
2
+ from .gradient_calculation import DerivativeBasic, DerivativeChain
3
+ from .loss_calculations import LossQuestion_Linear, LossQuestion_Logistic, LossQuestion_MulticlassLogistic