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