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,451 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+ import difflib
6
+ import logging
7
+
8
+ from QuizGenerator.question import Question, Answer, QuestionRegistry
9
+ from QuizGenerator.contentast import ContentAST
10
+ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ class IOQuestion(Question, abc.ABC):
16
+ def __init__(self, *args, **kwargs):
17
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.IO)
18
+ super().__init__(*args, **kwargs)
19
+
20
+
21
+ @QuestionRegistry.register()
22
+ class HardDriveAccessTime(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
23
+
24
+ def refresh(self, *args, **kwargs):
25
+ super().refresh(*args, **kwargs)
26
+
27
+ self.hard_drive_rotation_speed = 100 * self.rng.randint(36, 150) # e.g. 3600rpm to 15000rpm
28
+ self.seek_delay = float(round(self.rng.randrange(3, 20), 2))
29
+ self.transfer_rate = self.rng.randint(50, 300)
30
+ self.number_of_reads = self.rng.randint(1, 20)
31
+ self.size_of_reads = self.rng.randint(1, 10)
32
+
33
+ self.rotational_delay = (1 / self.hard_drive_rotation_speed) * (60 / 1) * (1000 / 1) * (1/2)
34
+ self.access_delay = self.rotational_delay + self.seek_delay
35
+ self.transfer_delay = 1000 * (self.size_of_reads * self.number_of_reads) / 1024 / self.transfer_rate
36
+ self.disk_access_delay = self.access_delay * self.number_of_reads + self.transfer_delay
37
+
38
+ self.answers.update({
39
+ "answer__rotational_delay": Answer.float_value(
40
+ "answer__rotational_delay",
41
+ self.rotational_delay
42
+ ),
43
+ "answer__access_delay": Answer.float_value(
44
+ "answer__access_delay",
45
+ self.access_delay
46
+ ),
47
+ "answer__transfer_delay": Answer.float_value(
48
+ "answer__transfer_delay",
49
+ self.transfer_delay
50
+ ),
51
+ "answer__disk_access_delay": Answer.float_value(
52
+ "answer__disk_access_delay",
53
+ self.disk_access_delay
54
+ ),
55
+ })
56
+
57
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
58
+ # Create parameter info table using mixin
59
+ parameter_info = {
60
+ "Hard Drive Rotation Speed": f"{self.hard_drive_rotation_speed}RPM",
61
+ "Seek Delay": f"{self.seek_delay}ms",
62
+ "Transfer Rate": f"{self.transfer_rate}MB/s",
63
+ "Number of Reads": f"{self.number_of_reads}",
64
+ "Size of Reads": f"{self.size_of_reads}KB"
65
+ }
66
+
67
+ parameter_table = self.create_info_table(parameter_info)
68
+
69
+ # Create answer table with multiple rows using mixin
70
+ answer_rows = [
71
+ {"Variable": "Rotational Delay", "Value": "answer__rotational_delay"},
72
+ {"Variable": "Access Delay", "Value": "answer__access_delay"},
73
+ {"Variable": "Transfer Delay", "Value": "answer__transfer_delay"},
74
+ {"Variable": "Total Disk Access Delay", "Value": "answer__disk_access_delay"}
75
+ ]
76
+
77
+ answer_table = self.create_answer_table(
78
+ headers=["Variable", "Value"],
79
+ data_rows=answer_rows,
80
+ answer_columns=["Value"]
81
+ )
82
+
83
+ # Use mixin to create complete body with both tables
84
+ intro_text = "Given the information below, please calculate the following values."
85
+
86
+ instructions = (
87
+ f"Make sure your answers are rounded to {Answer.DEFAULT_ROUNDING_DIGITS} decimal points "
88
+ f"(even if they are whole numbers), and do so after you finish all your calculations! "
89
+ f"(i.e. don't use your rounded answers to calculate your overall answer)"
90
+ )
91
+
92
+ body = self.create_parameter_calculation_body(
93
+ intro_text=intro_text,
94
+ parameter_table=parameter_table,
95
+ answer_table=answer_table,
96
+ additional_instructions=instructions
97
+ )
98
+
99
+ return body
100
+
101
+ def get_explanation(self) -> ContentAST.Section:
102
+ explanation = ContentAST.Section()
103
+
104
+ explanation.add_element(
105
+ ContentAST.Paragraph([
106
+ "To calculate the total disk access time (or \"delay\"), "
107
+ "we should first calculate each of the individual parts.",
108
+ r"Since we know that $t_{total} = (\text{# of reads}) \cdot t_{access} + t_{transfer}$"
109
+ r"we therefore need to calculate $t_{access}$ and $t_{transfer}$, where "
110
+ r"$t_{access} = t_{rotation} + t_{seek}$.",
111
+ ])
112
+ )
113
+
114
+ explanation.add_elements([
115
+ ContentAST.Paragraph(["Starting with the rotation delay, we calculate:"]),
116
+ ContentAST.Equation(
117
+ "t_{rotation} = "
118
+ + f"\\frac{{1 minute}}{{{self.hard_drive_rotation_speed}revolutions}}"
119
+ + r"\cdot \frac{60 seconds}{1 minute} \cdot \frac{1000 ms}{1 second} \cdot \frac{1 revolution}{2} = "
120
+ + f"{self.rotational_delay:0.2f}ms",
121
+ )
122
+ ])
123
+
124
+ explanation.add_elements([
125
+ ContentAST.Paragraph([
126
+ "Now we can calculate:",
127
+ ]),
128
+ ContentAST.Equation(
129
+ f"t_{{access}} "
130
+ f"= t_{{rotation}} + t_{{seek}} "
131
+ f"= {self.rotational_delay:0.2f}ms + {self.seek_delay:0.2f}ms = {self.access_delay:0.2f}ms"
132
+ )
133
+ ])
134
+
135
+ explanation.add_elements([
136
+ ContentAST.Paragraph([r"Next we need to calculate our transfer delay, $t_{transfer}$, which we do as:"]),
137
+ ContentAST.Equation(
138
+ f"t_{{transfer}} "
139
+ f"= \\frac{{{self.number_of_reads} \\cdot {self.size_of_reads}KB}}{{1}} \\cdot \\frac{{1MB}}{{1024KB}} "
140
+ f"\\cdot \\frac{{1 second}}{{{self.transfer_rate}MB}} \\cdot \\frac{{1000ms}}{{1second}} "
141
+ f"= {self.transfer_delay:0.2}ms"
142
+ )
143
+ ])
144
+
145
+ explanation.add_elements([
146
+ ContentAST.Paragraph(["Putting these together we get:"]),
147
+ ContentAST.Equation(
148
+ f"t_{{total}} "
149
+ f"= \\text{{(# reads)}} \\cdot t_{{access}} + t_{{transfer}} "
150
+ f"= {self.number_of_reads} \\cdot {self.access_delay:0.2f} + {self.transfer_delay:0.2f} "
151
+ f"= {self.disk_access_delay:0.2f}ms")
152
+ ])
153
+ return explanation
154
+
155
+
156
+ @QuestionRegistry.register()
157
+ class INodeAccesses(IOQuestion, TableQuestionMixin, BodyTemplatesMixin):
158
+
159
+ def refresh(self, *args, **kwargs):
160
+ super().refresh(*args, **kwargs)
161
+
162
+ # Calculating this first to use blocksize as an even multiple of it
163
+ self.inode_size = 2**self.rng.randint(6, 10)
164
+
165
+ self.block_size = self.inode_size * self.rng.randint(8, 20)
166
+ self.inode_number = self.rng.randint(0, 256)
167
+ self.inode_start_location = self.block_size * self.rng.randint(2, 5)
168
+
169
+ self.inode_address = self.inode_start_location + self.inode_number * self.inode_size
170
+ self.inode_block = self.inode_address // self.block_size
171
+ self.inode_address_in_block = self.inode_address % self.block_size
172
+ self.inode_index_in_block = int(self.inode_address_in_block / self.inode_size)
173
+
174
+ self.answers.update({
175
+ "answer__inode_address": Answer.integer("answer__inode_address", self.inode_address),
176
+ "answer__inode_block": Answer.integer("answer__inode_block", self.inode_block),
177
+ "answer__inode_address_in_block": Answer.integer("answer__inode_address_in_block", self.inode_address_in_block),
178
+ "answer__inode_index_in_block": Answer.integer("answer__inode_index_in_block", self.inode_index_in_block),
179
+ })
180
+
181
+ def get_body(self) -> ContentAST.Section:
182
+ # Create parameter info table using mixin
183
+ parameter_info = {
184
+ "Block Size": f"{self.block_size} Bytes",
185
+ "Inode Number": f"{self.inode_number}",
186
+ "Inode Start Location": f"{self.inode_start_location} Bytes",
187
+ "Inode size": f"{self.inode_size} Bytes"
188
+ }
189
+
190
+ parameter_table = self.create_info_table(parameter_info)
191
+
192
+ # Create answer table with multiple rows using mixin
193
+ answer_rows = [
194
+ {"Variable": "Inode address", "Value": "answer__inode_address"},
195
+ {"Variable": "Block containing inode", "Value": "answer__inode_block"},
196
+ {"Variable": "Inode address (offset) within block", "Value": "answer__inode_address_in_block"},
197
+ {"Variable": "Inode index within block", "Value": "answer__inode_index_in_block"}
198
+ ]
199
+
200
+ answer_table = self.create_answer_table(
201
+ headers=["Variable", "Value"],
202
+ data_rows=answer_rows,
203
+ answer_columns=["Value"]
204
+ )
205
+
206
+ # Use mixin to create complete body with both tables
207
+ intro_text = "Given the information below, please calculate the following values."
208
+
209
+ instructions = (
210
+ "(hint: they should all be round numbers). "
211
+ "Remember, demonstrating you know the equations and what goes into them is generally sufficient."
212
+ )
213
+
214
+ body = self.create_parameter_calculation_body(
215
+ intro_text=intro_text,
216
+ parameter_table=parameter_table,
217
+ answer_table=answer_table,
218
+ additional_instructions=instructions
219
+ )
220
+
221
+ return body
222
+
223
+ def get_explanation(self) -> ContentAST.Section:
224
+ explanation = ContentAST.Section()
225
+
226
+ explanation.add_element(
227
+ ContentAST.Paragraph([
228
+ "If we are given an inode number, there are a few steps that we need to take to load the actual inode. "
229
+ "These consist of determining the address of the inode, which block would contain it, "
230
+ "and then its address within the block.",
231
+ "To find the inode address, we calculate:",
232
+ ])
233
+ )
234
+
235
+ explanation.add_element(
236
+ ContentAST.Equation.make_block_equation__multiline_equals(
237
+ r"(\text{Inode address})",
238
+ [
239
+ r"(\text{Inode Start Location}) + (\text{inode #}) \cdot (\text{inode size})",
240
+ f"{self.inode_start_location} + {self.inode_number} \\cdot {self.inode_size}",
241
+ f"{self.inode_address}"
242
+ ])
243
+ )
244
+
245
+ explanation.add_element(
246
+ ContentAST.Paragraph([
247
+ "Next, we us this to figure out what block the inode is in. "
248
+ "We do this directly so we know what block to load, "
249
+ "thus minimizing the number of loads we have to make.",
250
+ ])
251
+ )
252
+ explanation.add_element(ContentAST.Equation.make_block_equation__multiline_equals(
253
+ r"\text{Block containing inode}",
254
+ [
255
+ r"(\text{Inode address}) \mathbin{//} (\text{block size})",
256
+ f"{self.inode_address} \\mathbin{{//}} {self.block_size}",
257
+ f"{self.inode_block}"
258
+ ]
259
+ ))
260
+
261
+ explanation.add_element(
262
+ ContentAST.Paragraph([
263
+ "When we load this block, we now have in our system memory "
264
+ "(remember, blocks on the hard drive are effectively useless to us until they're in main memory!), "
265
+ "the inode, so next we need to figure out where it is within that block."
266
+ "This means that we'll need to find the offset into this block. "
267
+ "We'll calculate this both as the offset in bytes, and also in number of inodes, "
268
+ "since we can use array indexing.",
269
+ ])
270
+ )
271
+
272
+ explanation.add_element(ContentAST.Equation.make_block_equation__multiline_equals(
273
+ r"\text{offset within block}",
274
+ [
275
+ r"(\text{Inode address}) \bmod (\text{block size})",
276
+ f"{self.inode_address} \\bmod {self.block_size}",
277
+ f"{self.inode_address_in_block}"
278
+ ]
279
+ ))
280
+
281
+ explanation.add_element(
282
+ ContentAST.Text("Remember that `mod` is the same as `%`, the modulo operation.")
283
+ )
284
+
285
+ explanation.add_element(ContentAST.Paragraph(["and"]))
286
+
287
+ explanation.add_element(ContentAST.Equation.make_block_equation__multiline_equals(
288
+ r"\text{index within block}",
289
+ [
290
+ r"\dfrac{\text{offset within block}}{\text{inode size}}",
291
+ f"\\dfrac{{{self.inode_address_in_block}}}{{{self.inode_size}}}",
292
+ f"{self.inode_index_in_block}"
293
+ ]
294
+ ))
295
+
296
+ return explanation
297
+
298
+
299
+ @QuestionRegistry.register()
300
+ class VSFS_states(IOQuestion):
301
+
302
+ from .ostep13_vsfs import fs as vsfs
303
+
304
+ def __init__(self, *args, **kwargs):
305
+ super().__init__(*args, **kwargs)
306
+ self.answer_kind = Answer.AnswerKind.MULTIPLE_DROPDOWN
307
+
308
+ self.num_steps = kwargs.get("num_steps", 10)
309
+
310
+ def refresh(self, *args, **kwargs):
311
+ super().refresh(*args, **kwargs)
312
+
313
+ fs = self.vsfs(4, 4, self.rng)
314
+ operations = fs.run_for_steps(self.num_steps)
315
+
316
+ self.start_state = operations[-1]["start_state"]
317
+ self.end_state = operations[-1]["end_state"]
318
+
319
+ wrong_answers = list(filter(
320
+ lambda o: o != operations[-1]["cmd"],
321
+ map(
322
+ lambda o: o["cmd"],
323
+ operations
324
+ )
325
+ ))
326
+ self.rng.shuffle(wrong_answers)
327
+
328
+ self.answers["answer__cmd"] = Answer(
329
+ "answer__cmd",
330
+ f"{operations[-1]['cmd']}",
331
+ kind=Answer.AnswerKind.MULTIPLE_DROPDOWN,
332
+ correct=True,
333
+ baffles=list(set([op['cmd'] for op in operations[:-1] if op != operations[-1]['cmd']]))
334
+ )
335
+
336
+ def get_body(self) -> ContentAST.Section:
337
+ body = ContentAST.Section()
338
+
339
+ body.add_element(ContentAST.Paragraph(["What operation happens between these two states?"]))
340
+
341
+ body.add_element(
342
+ ContentAST.Code(
343
+ self.start_state,
344
+ make_small=True
345
+ )
346
+ )
347
+
348
+ body.add_element(
349
+ ContentAST.AnswerBlock(
350
+ ContentAST.Answer(
351
+ self.answers["answer__cmd"],
352
+ label="Command"
353
+ )
354
+ )
355
+ )
356
+
357
+ body.add_element(
358
+ ContentAST.Code(
359
+ self.end_state,
360
+ make_small=True
361
+ )
362
+ )
363
+
364
+ return body
365
+
366
+ def get_explanation(self) -> ContentAST.Section:
367
+ explanation = ContentAST.Section()
368
+
369
+ log.debug(f"self.start_state: {self.start_state}")
370
+ log.debug(f"self.end_state: {self.end_state}")
371
+
372
+ explanation.add_elements([
373
+ ContentAST.Paragraph([
374
+ "The key thing to pay attention to when solving these problems is where there are differences between the start state and the end state.",
375
+ "In this particular problem, we can see that these lines are different:"
376
+ ])
377
+ ])
378
+
379
+ chunk_to_add = []
380
+ lines_that_changed = []
381
+ for start_line, end_line in zip(self.start_state.split('\n'), self.end_state.split('\n')):
382
+ if start_line == end_line:
383
+ continue
384
+ lines_that_changed.append((start_line, end_line))
385
+ chunk_to_add.append(
386
+ f" - `{start_line}` -> `{end_line}`"
387
+ )
388
+
389
+ explanation.add_element(
390
+ ContentAST.Paragraph(chunk_to_add)
391
+ )
392
+
393
+ chunk_to_add = [
394
+ "A great place to start is to check to see if the bitmaps have changed as this can quickly tell us a lot of information"
395
+ ]
396
+
397
+ inode_bitmap_lines = list(filter(lambda s: "inode bitmap" in s[0], lines_that_changed))
398
+ data_bitmap_lines = list(filter(lambda s: "data bitmap" in s[0], lines_that_changed))
399
+
400
+ def get_bitmap(line: str) -> str:
401
+ log.debug(f"line: {line}")
402
+ return line.split()[-1]
403
+
404
+ def highlight_changes(a: str, b: str) -> str:
405
+ matcher = difflib.SequenceMatcher(None, a, b)
406
+ result = []
407
+
408
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
409
+ if tag == "equal":
410
+ result.append(b[j1:j2])
411
+ elif tag in ("insert", "replace"):
412
+ result.append(f"***{b[j1:j2]}***")
413
+ # for "delete", do nothing since text is removed
414
+
415
+ return "".join(result)
416
+
417
+ if len(inode_bitmap_lines) > 0:
418
+ inode_bitmap_lines = inode_bitmap_lines[0]
419
+ chunk_to_add.append(f"The inode bitmap lines have changed from {get_bitmap(inode_bitmap_lines[0])} to {get_bitmap(inode_bitmap_lines[1])}.")
420
+ if get_bitmap(inode_bitmap_lines[0]).count('1') < get_bitmap(inode_bitmap_lines[1]).count('1'):
421
+ chunk_to_add.append("We can see that we have added an inode, so we have either called `creat` or `mkdir`.")
422
+ else:
423
+ chunk_to_add.append("We can see that we have removed an inode, so we have called `unlink`.")
424
+
425
+ if len(data_bitmap_lines) > 0:
426
+ data_bitmap_lines = data_bitmap_lines[0]
427
+ chunk_to_add.append(f"The inode bitmap lines have changed from {get_bitmap(data_bitmap_lines[0])} to {get_bitmap(data_bitmap_lines[1])}.")
428
+ if get_bitmap(data_bitmap_lines[0]).count('1') < get_bitmap(data_bitmap_lines[1]).count('1'):
429
+ chunk_to_add.append("We can see that we have added a data block, so we have either called `mkdir` or `write`.")
430
+ else:
431
+ chunk_to_add.append("We can see that we have removed a data block, so we have `unlink`ed a file.")
432
+
433
+ if len(data_bitmap_lines) == 0 and len(inode_bitmap_lines) == 0:
434
+ chunk_to_add.append("If they have not changed, then we know we must have eithered called `link` or `unlink` and must check the references.")
435
+
436
+ explanation.add_element(
437
+ ContentAST.Paragraph(chunk_to_add)
438
+ )
439
+
440
+ explanation.add_elements([
441
+ ContentAST.Paragraph(["The overall changes are highlighted with `*` symbols below"])
442
+ ])
443
+
444
+ explanation.add_element(
445
+ ContentAST.Code(
446
+ highlight_changes(self.start_state, self.end_state)
447
+ )
448
+ )
449
+
450
+ return explanation
451
+