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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +627 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1955 -0
- QuizGenerator/generate.py +253 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +579 -0
- QuizGenerator/mixins.py +548 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +391 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
- QuizGenerator/premade_questions/cst334/process.py +648 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +203 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +715 -0
- QuizGenerator/quiz.py +467 -0
- QuizGenerator/regenerate.py +472 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.4.2.dist-info/METADATA +265 -0
- quizgenerator-0.4.2.dist-info/RECORD +52 -0
- quizgenerator-0.4.2.dist-info/WHEEL +4 -0
- quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
- 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
|