QuizGenerator 0.4.2__py3-none-any.whl → 0.6.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 (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,8 +9,8 @@ import logging
9
9
  import math
10
10
  from typing import List, Optional
11
11
 
12
- from QuizGenerator.contentast import ContentAST
13
- from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
12
+ from QuizGenerator.contentast import ContentAST, AnswerTypes
13
+ from QuizGenerator.question import Question, QuestionRegistry, RegenerableChoiceMixin
14
14
  from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
15
15
 
16
16
  log = logging.getLogger(__name__)
@@ -40,9 +40,9 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
40
40
  self.num_bits_vpn = self.num_bits_va - self.num_bits_offset
41
41
 
42
42
  self.possible_answers = {
43
- self.Target.VA_BITS: Answer.integer("answer__num_bits_va", self.num_bits_va),
44
- self.Target.OFFSET_BITS: Answer.integer("answer__num_bits_offset", self.num_bits_offset),
45
- self.Target.VPN_BITS: Answer.integer("answer__num_bits_vpn", self.num_bits_vpn)
43
+ self.Target.VA_BITS: AnswerTypes.Int(self.num_bits_va, unit="bits"),
44
+ self.Target.OFFSET_BITS: AnswerTypes.Int(self.num_bits_offset, unit="bits"),
45
+ self.Target.VPN_BITS: AnswerTypes.Int(self.num_bits_vpn, unit="bits")
46
46
  }
47
47
 
48
48
  # Select what kind of question we are going to be
@@ -52,22 +52,25 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
52
52
 
53
53
  return
54
54
 
55
- def get_body(self, **kwargs) -> ContentAST.Section:
55
+ def _get_body(self, **kwargs):
56
+ """Build question body and collect answers."""
57
+ answers = [self.answers['answer']] # Collect the answer
58
+
56
59
  # Create table data with one blank cell
57
60
  table_data = [{}]
58
61
  for target in list(self.Target):
59
62
  if target == self.blank_kind:
60
63
  # This cell should be an answer blank
61
- table_data[0][target.value] = ContentAST.Answer(self.possible_answers[target], " bits")
64
+ table_data[0][target.value] = self.possible_answers[target]
62
65
  else:
63
66
  # This cell shows the value
64
67
  table_data[0][target.value] = f"{self.possible_answers[target].display} bits"
65
-
68
+
66
69
  table = self.create_fill_in_table(
67
70
  headers=[t.value for t in list(self.Target)],
68
71
  template_rows=table_data
69
72
  )
70
-
73
+
71
74
  body = ContentAST.Section()
72
75
  body.add_element(
73
76
  ContentAST.Paragraph(
@@ -77,11 +80,17 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
77
80
  )
78
81
  )
79
82
  body.add_element(table)
83
+ return body, answers
84
+
85
+ def get_body(self, **kwargs) -> ContentAST.Section:
86
+ """Build question body (backward compatible interface)."""
87
+ body, _ = self._get_body(**kwargs)
80
88
  return body
81
-
82
- def get_explanation(self, **kwargs) -> ContentAST.Section:
89
+
90
+ def _get_explanation(self, **kwargs):
91
+ """Build question explanation."""
83
92
  explanation = ContentAST.Section()
84
-
93
+
85
94
  explanation.add_element(
86
95
  ContentAST.Paragraph(
87
96
  [
@@ -92,7 +101,7 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
92
101
  ]
93
102
  )
94
103
  )
95
-
104
+
96
105
  explanation.add_element(
97
106
  ContentAST.Paragraph(
98
107
  [
@@ -104,7 +113,12 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
104
113
  ]
105
114
  )
106
115
  )
107
-
116
+
117
+ return explanation, []
118
+
119
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
120
+ """Build question explanation (backward compatible interface)."""
121
+ explanation, _ = self._get_explanation(**kwargs)
108
122
  return explanation
109
123
 
110
124
 
@@ -219,6 +233,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
219
233
  number_of_hits = 0
220
234
  for (request_number, request) in enumerate(self.requests):
221
235
  was_hit, evicted, cache_state = self.cache.query_cache(request, request_number)
236
+ log.debug(f"cache_state: \"{cache_state}\"")
222
237
  if was_hit:
223
238
  number_of_hits += 1
224
239
  self.request_results[request_number] = {
@@ -230,29 +245,29 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
230
245
 
231
246
  self.answers.update(
232
247
  {
233
- f"answer__hit-{request_number}": Answer.string(
234
- f"answer__hit-{request_number}", ('hit' if was_hit else 'miss')
235
- ),
236
- f"answer__evicted-{request_number}": Answer.string(
237
- f"answer__evicted-{request_number}", ('-' if evicted is None else f"{evicted}")
238
- ),
239
- f"answer__cache_state-{request_number}": Answer.list_value(
240
- f"answer__cache_state-{request_number}", copy.copy(cache_state)
241
- ),
248
+ f"answer__hit-{request_number}": AnswerTypes.String(('hit' if was_hit else 'miss')),
249
+ f"answer__evicted-{request_number}": AnswerTypes.String(('-' if evicted is None else f"{evicted}")),
250
+ f"answer__cache_state-{request_number}": AnswerTypes.List(value=copy.copy(cache_state), order_matters=True),
242
251
  }
243
252
  )
244
253
 
245
254
  self.hit_rate = 100 * number_of_hits / (self.num_requests)
246
255
  self.answers.update(
247
256
  {
248
- "answer__hit_rate": Answer.auto_float("answer__hit_rate", self.hit_rate)
257
+ "answer__hit_rate": AnswerTypes.Float(self.hit_rate,
258
+ label=f"Hit rate, excluding non-capacity misses",
259
+ unit="%"
260
+ )
249
261
  }
250
262
  )
251
263
 
252
264
  # Return whether this workload is interesting
253
265
  return self.is_interesting()
254
266
 
255
- def get_body(self, **kwargs) -> ContentAST.Section:
267
+ def _get_body(self, **kwargs):
268
+ """Build question body and collect answers."""
269
+ answers = []
270
+
256
271
  # Create table data for cache simulation
257
272
  table_rows = []
258
273
  for request_number in sorted(self.request_results.keys()):
@@ -264,47 +279,55 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
264
279
  "Cache State": f"answer__cache_state-{request_number}" # Answer key
265
280
  }
266
281
  )
267
-
282
+ # Collect answers for this request
283
+ answers.append(self.answers[f"answer__hit-{request_number}"])
284
+ answers.append(self.answers[f"answer__evicted-{request_number}"])
285
+ answers.append(self.answers[f"answer__cache_state-{request_number}"])
286
+
268
287
  # Create table using mixin - automatically handles answer conversion
269
288
  cache_table = self.create_answer_table(
270
289
  headers=["Page Requested", "Hit/Miss", "Evicted", "Cache State"],
271
290
  data_rows=table_rows,
272
291
  answer_columns=["Hit/Miss", "Evicted", "Cache State"]
273
292
  )
274
-
293
+
294
+ # Collect hit rate answer
295
+ hit_rate_answer = self.answers["answer__hit_rate"]
296
+ answers.append(hit_rate_answer)
297
+
275
298
  # Create hit rate answer block
276
- hit_rate_block = ContentAST.AnswerBlock(
277
- ContentAST.Answer(
278
- answer=self.answers["answer__hit_rate"],
279
- label=f"Hit rate, excluding capacity misses. If appropriate, round to {Answer.DEFAULT_ROUNDING_DIGITS} decimal digits.",
280
- unit="%"
281
- )
282
- )
283
-
299
+ hit_rate_block = ContentAST.AnswerBlock(hit_rate_answer)
300
+
284
301
  # Use mixin to create complete body
285
302
  intro_text = (
286
- f"Assume we are using a <b>{self.cache_policy}</b> caching policy and a cache size of <b>{self.cache_size}</b>. "
303
+ f"Assume we are using a **{self.cache_policy}** caching policy and a cache size of **{self.cache_size}**. "
287
304
  "Given the below series of requests please fill in the table. "
288
305
  "For the hit/miss column, please write either \"hit\" or \"miss\". "
289
306
  "For the eviction column, please write either the number of the evicted page or simply a dash (e.g. \"-\")."
290
307
  )
291
-
308
+
292
309
  instructions = ContentAST.OnlyHtml([
293
310
  "For the cache state, please enter the cache contents in the order suggested in class, "
294
311
  "which means separated by commas with spaces (e.g. \"1, 2, 3\") "
295
312
  "and with the left-most being the next to be evicted. "
296
313
  "In the case where there is a tie, order by increasing number."
297
314
  ])
298
-
315
+
299
316
  body = self.create_fill_in_table_body(intro_text, instructions, cache_table)
300
317
  body.add_element(hit_rate_block)
318
+ return body, answers
319
+
320
+ def get_body(self, **kwargs) -> ContentAST.Section:
321
+ """Build question body (backward compatible interface)."""
322
+ body, _ = self._get_body(**kwargs)
301
323
  return body
302
-
303
- def get_explanation(self, **kwargs) -> ContentAST.Section:
324
+
325
+ def _get_explanation(self, **kwargs):
326
+ """Build question explanation."""
304
327
  explanation = ContentAST.Section()
305
-
328
+
306
329
  explanation.add_element(ContentAST.Paragraph(["The full caching table can be seen below."]))
307
-
330
+
308
331
  explanation.add_element(
309
332
  ContentAST.Table(
310
333
  headers=["Page", "Hit/Miss", "Evicted", "Cache State"],
@@ -319,7 +342,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
319
342
  ]
320
343
  )
321
344
  )
322
-
345
+
323
346
  explanation.add_element(
324
347
  ContentAST.Paragraph(
325
348
  [
@@ -330,7 +353,12 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
330
353
  ]
331
354
  )
332
355
  )
333
-
356
+
357
+ return explanation, []
358
+
359
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
360
+ """Build question explanation (backward compatible interface)."""
361
+ explanation, _ = self._get_explanation(**kwargs)
334
362
  return explanation
335
363
 
336
364
  def is_interesting(self) -> bool:
@@ -365,30 +393,32 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
365
393
  self.virtual_address = self.rng.randint(1, int(self.bounds / self.PROBABILITY_OF_VALID))
366
394
 
367
395
  if self.virtual_address < self.bounds:
368
- self.answers["answer"] = Answer.binary_hex(
369
- "answer__physical_address",
396
+ self.answers["answer"] = AnswerTypes.Hex(
370
397
  self.base + self.virtual_address,
371
398
  length=math.ceil(math.log2(self.base + self.virtual_address))
372
399
  )
373
400
  else:
374
- self.answers["answer"] = Answer.string("answer__physical_address", "INVALID")
401
+ self.answers["answer"] = AnswerTypes.String("INVALID")
375
402
 
376
- def get_body(self) -> ContentAST.Section:
403
+ def _get_body(self):
404
+ """Build question body and collect answers."""
405
+ answers = [self.answers["answer"]]
406
+
377
407
  # Use mixin to create parameter table with answer
378
408
  parameter_info = {
379
409
  "Base": f"0x{self.base:X}",
380
410
  "Bounds": f"0x{self.bounds:X}",
381
411
  "Virtual Address": f"0x{self.virtual_address:X}"
382
412
  }
383
-
413
+
384
414
  table = self.create_parameter_answer_table(
385
415
  parameter_info=parameter_info,
386
416
  answer_label="Physical Address",
387
417
  answer_key="answer",
388
418
  transpose=True
389
419
  )
390
-
391
- return self.create_parameter_calculation_body(
420
+
421
+ body = self.create_parameter_calculation_body(
392
422
  intro_text=(
393
423
  "Given the information in the below table, "
394
424
  "please calcuate the physical address associated with the given virtual address. "
@@ -396,10 +426,17 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
396
426
  ),
397
427
  parameter_table=table
398
428
  )
399
-
400
- def get_explanation(self) -> ContentAST.Section:
429
+ return body, answers
430
+
431
+ def get_body(self) -> ContentAST.Section:
432
+ """Build question body (backward compatible interface)."""
433
+ body, _ = self._get_body()
434
+ return body
435
+
436
+ def _get_explanation(self):
437
+ """Build question explanation."""
401
438
  explanation = ContentAST.Section()
402
-
439
+
403
440
  explanation.add_element(
404
441
  ContentAST.Paragraph(
405
442
  [
@@ -410,7 +447,7 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
410
447
  ]
411
448
  )
412
449
  )
413
-
450
+
414
451
  explanation.add_element(
415
452
  ContentAST.Paragraph(
416
453
  [
@@ -419,7 +456,7 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
419
456
  ]
420
457
  )
421
458
  )
422
-
459
+
423
460
  if self.virtual_address < self.bounds:
424
461
  explanation.add_element(
425
462
  ContentAST.Paragraph(
@@ -442,7 +479,12 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
442
479
  ]
443
480
  )
444
481
  )
445
-
482
+
483
+ return explanation, []
484
+
485
+ def get_explanation(self) -> ContentAST.Section:
486
+ """Build question explanation (backward compatible interface)."""
487
+ explanation, _ = self._get_explanation()
446
488
  return explanation
447
489
 
448
490
 
@@ -537,22 +579,25 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
537
579
 
538
580
  # Set answers based on whether it's in bounds or not
539
581
  if self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
540
- self.answers["answer__physical_address"] = Answer.binary_hex(
541
- "answer__physical_address",
582
+ self.answers["answer__physical_address"] = AnswerTypes.Binary(
542
583
  self.physical_address,
543
- length=self.physical_bits
584
+ length=self.physical_bits,
585
+ label="Physical Address"
544
586
  )
545
587
  else:
546
- self.answers["answer__physical_address"] = Answer.string(
547
- "answer__physical_address",
548
- "INVALID"
549
- )
550
-
551
- self.answers["answer__segment"] = Answer.string("answer__segment", self.segment)
588
+ self.answers["answer__physical_address"] = AnswerTypes.String("INVALID", label="Physical Address")
589
+
590
+ self.answers["answer__segment"] = AnswerTypes.String(self.segment, label="Segment name")
552
591
 
553
- def get_body(self) -> ContentAST.Section:
592
+ def _get_body(self):
593
+ """Build question body and collect answers."""
594
+ answers = [
595
+ self.answers["answer__segment"],
596
+ self.answers["answer__physical_address"]
597
+ ]
598
+
554
599
  body = ContentAST.Section()
555
-
600
+
556
601
  body.add_element(
557
602
  ContentAST.Paragraph(
558
603
  [
@@ -565,39 +610,36 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
565
610
  ]
566
611
  )
567
612
  )
568
-
613
+
569
614
  # Create segment table using mixin
570
615
  segment_rows = [
571
616
  {"": "code", "base": f"0b{self.base['code']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['code']:0b}"},
572
617
  {"": "heap", "base": f"0b{self.base['heap']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['heap']:0b}"},
573
618
  {"": "stack", "base": f"0b{self.base['stack']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['stack']:0b}"}
574
619
  ]
575
-
620
+
576
621
  segment_table = self.create_answer_table(
577
622
  headers=["", "base", "bounds"],
578
623
  data_rows=segment_rows,
579
624
  answer_columns=[] # No answer columns in this table
580
625
  )
581
-
626
+
582
627
  body.add_element(segment_table)
583
-
628
+
584
629
  body.add_element(
585
- ContentAST.AnswerBlock(
586
- [
587
- ContentAST.Answer(
588
- self.answers["answer__segment"],
589
- label="Segment name"
590
- ),
591
- ContentAST.Answer(
592
- self.answers["answer__physical_address"],
593
- label="Physical Address"
594
- )
595
- ]
596
- )
630
+ ContentAST.AnswerBlock([
631
+ self.answers["answer__segment"],
632
+ self.answers["answer__physical_address"]
633
+ ])
597
634
  )
635
+ return body, answers
636
+
637
+ def get_body(self) -> ContentAST.Section:
638
+ """Build question body (backward compatible interface)."""
639
+ body, _ = self._get_body()
598
640
  return body
599
-
600
- def get_explanation(self) -> ContentAST.Section:
641
+
642
+ def _get_explanation(self):
601
643
  explanation = ContentAST.Section()
602
644
 
603
645
  explanation.add_element(
@@ -691,7 +733,12 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
691
733
  f" 0b{self.physical_address:0{self.physical_bits}b}\n"
692
734
  )
693
735
  )
694
-
736
+
737
+ return explanation, []
738
+
739
+ def get_explanation(self) -> ContentAST.Section:
740
+ """Build question explanation (backward compatible interface)."""
741
+ explanation, _ = self._get_explanation()
695
742
  return explanation
696
743
 
697
744
 
@@ -765,34 +812,43 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
765
812
 
766
813
  self.answers.update(
767
814
  {
768
- "answer__vpn": Answer.binary_hex("answer__vpn", self.vpn, length=self.num_bits_vpn),
769
- "answer__offset": Answer.binary_hex("answer__offset", self.offset, length=self.num_bits_offset),
770
- "answer__pte": Answer.binary_hex("answer__pte", self.pte, length=(self.num_bits_pfn + 1)),
815
+ "answer__vpn": AnswerTypes.Binary(self.vpn, length=self.num_bits_vpn, label="VPN"),
816
+ "answer__offset": AnswerTypes.Binary(self.offset, length=self.num_bits_offset, label="Offset"),
817
+ "answer__pte": AnswerTypes.Binary(self.pte, length=(self.num_bits_pfn + 1), label="PTE"),
771
818
  }
772
819
  )
773
820
 
774
821
  if self.is_valid:
775
822
  self.answers.update(
776
823
  {
777
- "answer__is_valid": Answer.string("answer__is_valid", "VALID"),
778
- "answer__pfn": Answer.binary_hex("answer__pfn", self.pfn, length=self.num_bits_pfn),
779
- "answer__physical_address": Answer.binary_hex(
780
- "answer__physical_address", self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset)
824
+ "answer__is_valid": AnswerTypes.String("VALID", label="VALID or INVALID?"),
825
+ "answer__pfn": AnswerTypes.Binary(self.pfn, length=self.num_bits_pfn, label="PFN"),
826
+ "answer__physical_address": AnswerTypes.Binary(self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset), label="Physical Address"
781
827
  ),
782
828
  }
783
829
  )
784
830
  else:
785
831
  self.answers.update(
786
832
  {
787
- "answer__is_valid": Answer.string("answer__is_valid", "INVALID"),
788
- "answer__pfn": Answer.string("answer__pfn", "INVALID"),
789
- "answer__physical_address": Answer.string("answer__physical_address", "INVALID"),
833
+ "answer__is_valid": AnswerTypes.String("INVALID", label="VALID or INVALID?"),
834
+ "answer__pfn": AnswerTypes.String("INVALID", label="PFN"),
835
+ "answer__physical_address": AnswerTypes.String("INVALID", label="Physical Address"),
790
836
  }
791
837
  )
792
838
 
793
- def get_body(self, *args, **kwargs) -> ContentAST.Section:
839
+ def _get_body(self, *args, **kwargs):
840
+ """Build question body and collect answers."""
841
+ answers = [
842
+ self.answers["answer__vpn"],
843
+ self.answers["answer__offset"],
844
+ self.answers["answer__pte"],
845
+ self.answers["answer__is_valid"],
846
+ self.answers["answer__pfn"],
847
+ self.answers["answer__physical_address"],
848
+ ]
849
+
794
850
  body = ContentAST.Section()
795
-
851
+
796
852
  body.add_element(
797
853
  ContentAST.Paragraph(
798
854
  [
@@ -801,14 +857,14 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
801
857
  ]
802
858
  )
803
859
  )
804
-
860
+
805
861
  # Create parameter info table using mixin
806
862
  parameter_info = {
807
863
  "Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
808
864
  "# VPN bits": f"{self.num_bits_vpn}",
809
865
  "# PFN bits": f"{self.num_bits_pfn}"
810
866
  }
811
-
867
+
812
868
  body.add_element(self.create_info_table(parameter_info))
813
869
 
814
870
  # Use the page table generated in refresh() for deterministic output
@@ -827,33 +883,36 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
827
883
 
828
884
  if (max(self.page_table.keys()) + 1) != 2 ** self.num_bits_vpn:
829
885
  value_matrix.append(["...", "..."])
830
-
886
+
831
887
  body.add_element(
832
888
  ContentAST.Table(
833
889
  headers=["VPN", "PTE"],
834
890
  data=value_matrix
835
891
  )
836
892
  )
837
-
893
+
838
894
  body.add_element(
839
- ContentAST.AnswerBlock(
840
- [
841
-
842
- ContentAST.Answer(self.answers["answer__vpn"], label="VPN"),
843
- ContentAST.Answer(self.answers["answer__offset"], label="Offset"),
844
- ContentAST.Answer(self.answers["answer__pte"], label="PTE"),
845
- ContentAST.Answer(self.answers["answer__is_valid"], label="VALID or INVALID?"),
846
- ContentAST.Answer(self.answers["answer__pfn"], label="PFN"),
847
- ContentAST.Answer(self.answers["answer__physical_address"], label="Physical Address"),
848
- ]
849
- )
895
+ ContentAST.AnswerBlock([
896
+ self.answers["answer__vpn"],
897
+ self.answers["answer__offset"],
898
+ self.answers["answer__pte"],
899
+ self.answers["answer__is_valid"],
900
+ self.answers["answer__pfn"],
901
+ self.answers["answer__physical_address"],
902
+ ])
850
903
  )
851
-
904
+
905
+ return body, answers
906
+
907
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
908
+ """Build question body (backward compatible interface)."""
909
+ body, _ = self._get_body(*args, **kwargs)
852
910
  return body
853
911
 
854
- def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
912
+ def _get_explanation(self, *args, **kwargs):
913
+ """Build question explanation."""
855
914
  explanation = ContentAST.Section()
856
-
915
+
857
916
  explanation.add_element(
858
917
  ContentAST.Paragraph(
859
918
  [
@@ -863,7 +922,7 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
863
922
  ]
864
923
  )
865
924
  )
866
-
925
+
867
926
  explanation.add_element(
868
927
  ContentAST.Paragraph(
869
928
  [
@@ -871,7 +930,7 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
871
930
  ]
872
931
  )
873
932
  )
874
-
933
+
875
934
  explanation.add_element(
876
935
  ContentAST.Paragraph(
877
936
  [
@@ -881,7 +940,7 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
881
940
  ]
882
941
  )
883
942
  )
884
-
943
+
885
944
  explanation.add_element(
886
945
  ContentAST.Paragraph(
887
946
  [
@@ -892,12 +951,12 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
892
951
  ]
893
952
  )
894
953
  )
895
-
954
+
896
955
  if self.is_valid:
897
956
  explanation.add_element(
898
957
  ContentAST.Paragraph(
899
958
  [
900
- f"In our PTE we see that the first bit is <b>{self.pte // (2 ** self.num_bits_pfn)}</b> meaning that the translation is <b>VALID</b>"
959
+ f"In our PTE we see that the first bit is **{self.pte // (2 ** self.num_bits_pfn)}** meaning that the translation is **VALID**"
901
960
  ]
902
961
  )
903
962
  )
@@ -905,14 +964,14 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
905
964
  explanation.add_element(
906
965
  ContentAST.Paragraph(
907
966
  [
908
- f"In our PTE we see that the first bit is <b>{self.pte // (2 ** self.num_bits_pfn)}</b> meaning that the translation is <b>INVALID</b>.",
967
+ f"In our PTE we see that the first bit is **{self.pte // (2 ** self.num_bits_pfn)}** meaning that the translation is **INVALID**.",
909
968
  "Therefore, we just write \"INVALID\" as our answer.",
910
969
  "If it were valid we would complete the below steps.",
911
970
  "<hr>"
912
971
  ]
913
972
  )
914
973
  )
915
-
974
+
916
975
  explanation.add_element(
917
976
  ContentAST.Paragraph(
918
977
  [
@@ -930,7 +989,7 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
930
989
  f"\\& \\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
931
990
  )
932
991
  )
933
-
992
+
934
993
  explanation.add_elements(
935
994
  [
936
995
  ContentAST.Paragraph(
@@ -944,7 +1003,7 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
944
1003
  )
945
1004
  ]
946
1005
  )
947
-
1006
+
948
1007
  explanation.add_elements(
949
1008
  [
950
1009
  ContentAST.Paragraph(["Note: Strictly speaking, this calculation is:", ]),
@@ -955,6 +1014,11 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
955
1014
  ]
956
1015
  )
957
1016
 
1017
+ return explanation, []
1018
+
1019
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
1020
+ """Build question explanation (backward compatible interface)."""
1021
+ explanation, _ = self._get_explanation(*args, **kwargs)
958
1022
  return explanation
959
1023
 
960
1024
 
@@ -1108,49 +1172,71 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1108
1172
 
1109
1173
  # Set up answers
1110
1174
  self.answers.update({
1111
- "answer__pdi": Answer.binary_hex("answer__pdi", self.pdi, length=self.num_bits_pdi),
1112
- "answer__pti": Answer.binary_hex("answer__pti", self.pti, length=self.num_bits_pti),
1113
- "answer__offset": Answer.binary_hex("answer__offset", self.offset, length=self.num_bits_offset),
1114
- "answer__pd_entry": Answer.binary_hex("answer__pd_entry", self.pd_entry, length=(self.num_bits_pfn + 1)),
1115
- "answer__pt_number": Answer.binary_hex("answer__pt_number", self.page_table_number, length=self.num_bits_pfn) if self.pd_valid else Answer.string("answer__pt_number", "INVALID"),
1175
+ "answer__pdi": AnswerTypes.Binary(self.pdi, length=self.num_bits_pdi,
1176
+ label="PDI (Page Directory Index)"),
1177
+ "answer__pti": AnswerTypes.Binary(self.pti, length=self.num_bits_pti,
1178
+ label="PTI (Page Table Index)"),
1179
+ "answer__offset": AnswerTypes.Binary(self.offset, length=self.num_bits_offset,
1180
+ label="Offset"),
1181
+ "answer__pd_entry": AnswerTypes.Binary(self.pd_entry, length=(self.num_bits_pfn + 1),
1182
+ label="PD Entry (from Page Directory)"),
1183
+ "answer__pt_number": (
1184
+ AnswerTypes.Binary(self.page_table_number, length=self.num_bits_pfn,
1185
+ label="Page Table Number")
1186
+ if self.pd_valid
1187
+ else AnswerTypes.String("INVALID", label="Page Table Number")
1188
+ ),
1116
1189
  })
1117
1190
 
1118
1191
  # PTE answer: if PD is valid, accept the actual PTE value from the table
1119
1192
  # (regardless of whether that PTE is valid or invalid)
1120
1193
  if self.pd_valid:
1121
1194
  self.answers.update({
1122
- "answer__pte": Answer.binary_hex("answer__pte", self.pte, length=(self.num_bits_pfn + 1)),
1195
+ "answer__pte": AnswerTypes.Binary(self.pte, length=(self.num_bits_pfn + 1),
1196
+ label="PTE (from Page Table)"),
1123
1197
  })
1124
1198
  else:
1125
1199
  # If PD is invalid, student can't look up the page table
1126
1200
  # Accept both "INVALID" (for consistency) and "N/A" (for accuracy)
1127
1201
  self.answers.update({
1128
- "answer__pte": Answer.string("answer__pte", ["INVALID", "N/A"]),
1202
+ "answer__pte": AnswerTypes.String(["INVALID", "N/A"], label="PTE (from Page Table)"),
1129
1203
  })
1130
1204
 
1131
1205
  # Validity, PFN, and Physical Address depend on BOTH levels being valid
1132
1206
  if self.pd_valid and self.pt_valid:
1133
1207
  self.answers.update({
1134
- "answer__is_valid": Answer.string("answer__is_valid", "VALID"),
1135
- "answer__pfn": Answer.binary_hex("answer__pfn", self.pfn, length=self.num_bits_pfn),
1136
- "answer__physical_address": Answer.binary_hex(
1137
- "answer__physical_address", self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset)
1208
+ "answer__is_valid": AnswerTypes.String("VALID", label="VALID or INVALID?"),
1209
+ "answer__pfn": AnswerTypes.Binary(self.pfn, length=self.num_bits_pfn, label="PFN"),
1210
+ "answer__physical_address": AnswerTypes.Binary(self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset), label="Physical Address"
1138
1211
  ),
1139
1212
  })
1140
1213
  else:
1141
1214
  self.answers.update({
1142
- "answer__is_valid": Answer.string("answer__is_valid", "INVALID"),
1143
- "answer__pfn": Answer.string("answer__pfn", "INVALID"),
1144
- "answer__physical_address": Answer.string("answer__physical_address", "INVALID"),
1215
+ "answer__is_valid": AnswerTypes.String("INVALID", label="VALID or INVALID?"),
1216
+ "answer__pfn": AnswerTypes.String("INVALID", label="PFN"),
1217
+ "answer__physical_address": AnswerTypes.String("INVALID", label="Physical Address"),
1145
1218
  })
1146
1219
 
1147
- def get_body(self, *args, **kwargs) -> ContentAST.Section:
1220
+ def _get_body(self, *args, **kwargs):
1221
+ """Build question body and collect answers."""
1222
+ answers = [
1223
+ self.answers["answer__pdi"],
1224
+ self.answers["answer__pti"],
1225
+ self.answers["answer__offset"],
1226
+ self.answers["answer__pd_entry"],
1227
+ self.answers["answer__pt_number"],
1228
+ self.answers["answer__pte"],
1229
+ self.answers["answer__is_valid"],
1230
+ self.answers["answer__pfn"],
1231
+ self.answers["answer__physical_address"],
1232
+ ]
1233
+
1148
1234
  body = ContentAST.Section()
1149
1235
 
1150
1236
  body.add_element(
1151
1237
  ContentAST.Paragraph([
1152
1238
  "Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
1153
- "This problem uses <b>two-level (hierarchical) paging</b>.",
1239
+ "This problem uses **two-level (hierarchical) paging**.",
1154
1240
  "Remember, we typically have the MSB representing valid or invalid."
1155
1241
  ])
1156
1242
  )
@@ -1245,21 +1331,27 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1245
1331
  # Answer block
1246
1332
  body.add_element(
1247
1333
  ContentAST.AnswerBlock([
1248
- ContentAST.Answer(self.answers["answer__pdi"], label="PDI (Page Directory Index)"),
1249
- ContentAST.Answer(self.answers["answer__pti"], label="PTI (Page Table Index)"),
1250
- ContentAST.Answer(self.answers["answer__offset"], label="Offset"),
1251
- ContentAST.Answer(self.answers["answer__pd_entry"], label="PD Entry (from Page Directory)"),
1252
- ContentAST.Answer(self.answers["answer__pt_number"], label="Page Table Number"),
1253
- ContentAST.Answer(self.answers["answer__pte"], label="PTE (from Page Table)"),
1254
- ContentAST.Answer(self.answers["answer__is_valid"], label="VALID or INVALID?"),
1255
- ContentAST.Answer(self.answers["answer__pfn"], label="PFN"),
1256
- ContentAST.Answer(self.answers["answer__physical_address"], label="Physical Address"),
1334
+ self.answers["answer__pdi"],
1335
+ self.answers["answer__pti"],
1336
+ self.answers["answer__offset"],
1337
+ self.answers["answer__pd_entry"],
1338
+ self.answers["answer__pt_number"],
1339
+ self.answers["answer__pte"],
1340
+ self.answers["answer__is_valid"],
1341
+ self.answers["answer__pfn"],
1342
+ self.answers["answer__physical_address"],
1257
1343
  ])
1258
1344
  )
1259
1345
 
1346
+ return body, answers
1347
+
1348
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
1349
+ """Build question body (backward compatible interface)."""
1350
+ body, _ = self._get_body(*args, **kwargs)
1260
1351
  return body
1261
1352
 
1262
- def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
1353
+ def _get_explanation(self, *args, **kwargs):
1354
+ """Build question explanation."""
1263
1355
  explanation = ContentAST.Section()
1264
1356
 
1265
1357
  explanation.add_element(
@@ -1278,7 +1370,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1278
1370
  # Step 1: Extract PDI, PTI, Offset
1279
1371
  explanation.add_element(
1280
1372
  ContentAST.Paragraph([
1281
- f"<b>Step 1: Extract components from Virtual Address</b>",
1373
+ f"**Step 1: Extract components from Virtual Address**",
1282
1374
  f"Virtual Address = PDI | PTI | Offset",
1283
1375
  f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> = "
1284
1376
  f"<tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt> | "
@@ -1290,7 +1382,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1290
1382
  # Step 2: Look up PD Entry
1291
1383
  explanation.add_element(
1292
1384
  ContentAST.Paragraph([
1293
- f"<b>Step 2: Look up Page Directory Entry</b>",
1385
+ f"**Step 2: Look up Page Directory Entry**",
1294
1386
  f"Using PDI = <tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt>, we find PD Entry = <tt>0b{self.pd_entry:0{self.num_bits_pfn + 1}b}</tt>"
1295
1387
  ])
1296
1388
  )
@@ -1299,8 +1391,8 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1299
1391
  pd_valid_bit = self.pd_entry // (2 ** self.num_bits_pfn)
1300
1392
  explanation.add_element(
1301
1393
  ContentAST.Paragraph([
1302
- f"<b>Step 3: Check Page Directory Entry validity</b>",
1303
- f"The MSB (valid bit) is <b>{pd_valid_bit}</b>, so this PD Entry is <b>{'VALID' if self.pd_valid else 'INVALID'}</b>."
1394
+ f"**Step 3: Check Page Directory Entry validity**",
1395
+ f"The MSB (valid bit) is **{pd_valid_bit}**, so this PD Entry is **{'VALID' if self.pd_valid else 'INVALID'}**."
1304
1396
  ])
1305
1397
  )
1306
1398
 
@@ -1308,7 +1400,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1308
1400
  explanation.add_element(
1309
1401
  ContentAST.Paragraph([
1310
1402
  "Since the Page Directory Entry is invalid, the translation fails here.",
1311
- "We write <b>INVALID</b> for all remaining fields.",
1403
+ "We write **INVALID** for all remaining fields.",
1312
1404
  "If it were valid, we would continue with the steps below.",
1313
1405
  "<hr>"
1314
1406
  ])
@@ -1317,7 +1409,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1317
1409
  # Step 4: Extract PT number (if PD valid)
1318
1410
  explanation.add_element(
1319
1411
  ContentAST.Paragraph([
1320
- f"<b>Step 4: Extract Page Table Number</b>",
1412
+ f"**Step 4: Extract Page Table Number**",
1321
1413
  "We remove the valid bit from the PD Entry to get the Page Table Number:"
1322
1414
  ])
1323
1415
  )
@@ -1333,14 +1425,14 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1333
1425
  if self.pd_valid:
1334
1426
  explanation.add_element(
1335
1427
  ContentAST.Paragraph([
1336
- f"This tells us to use <b>Page Table #{self.page_table_number}</b>."
1428
+ f"This tells us to use **Page Table #{self.page_table_number}**."
1337
1429
  ])
1338
1430
  )
1339
1431
 
1340
1432
  # Step 5: Look up PTE
1341
1433
  explanation.add_element(
1342
1434
  ContentAST.Paragraph([
1343
- f"<b>Step 5: Look up Page Table Entry</b>",
1435
+ f"**Step 5: Look up Page Table Entry**",
1344
1436
  f"Using PTI = <tt>0b{self.pti:0{self.num_bits_pti}b}</tt> in Page Table #{self.page_table_number}, "
1345
1437
  f"we find PTE = <tt>0b{self.pte:0{self.num_bits_pfn + 1}b}</tt>"
1346
1438
  ])
@@ -1350,8 +1442,8 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1350
1442
  pt_valid_bit = self.pte // (2 ** self.num_bits_pfn)
1351
1443
  explanation.add_element(
1352
1444
  ContentAST.Paragraph([
1353
- f"<b>Step 6: Check Page Table Entry validity</b>",
1354
- f"The MSB (valid bit) is <b>{pt_valid_bit}</b>, so this PTE is <b>{'VALID' if self.pt_valid else 'INVALID'}</b>."
1445
+ f"**Step 6: Check Page Table Entry validity**",
1446
+ f"The MSB (valid bit) is **{pt_valid_bit}**, so this PTE is **{'VALID' if self.pt_valid else 'INVALID'}**."
1355
1447
  ])
1356
1448
  )
1357
1449
 
@@ -1359,7 +1451,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1359
1451
  explanation.add_element(
1360
1452
  ContentAST.Paragraph([
1361
1453
  "Since the Page Table Entry is invalid, the translation fails.",
1362
- "We write <b>INVALID</b> for PFN and Physical Address.",
1454
+ "We write **INVALID** for PFN and Physical Address.",
1363
1455
  "If it were valid, we would continue with the steps below.",
1364
1456
  "<hr>"
1365
1457
  ])
@@ -1368,7 +1460,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1368
1460
  # Step 7: Extract PFN
1369
1461
  explanation.add_element(
1370
1462
  ContentAST.Paragraph([
1371
- f"<b>Step 7: Extract PFN</b>",
1463
+ f"**Step 7: Extract PFN**",
1372
1464
  "We remove the valid bit from the PTE to get the PFN:"
1373
1465
  ])
1374
1466
  )
@@ -1384,7 +1476,7 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1384
1476
  # Step 8: Construct physical address
1385
1477
  explanation.add_element(
1386
1478
  ContentAST.Paragraph([
1387
- f"<b>Step 8: Construct Physical Address</b>",
1479
+ f"**Step 8: Construct Physical Address**",
1388
1480
  "Physical Address = PFN | Offset"
1389
1481
  ])
1390
1482
  )
@@ -1397,4 +1489,9 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1397
1489
  )
1398
1490
  )
1399
1491
 
1492
+ return explanation, []
1493
+
1494
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
1495
+ """Build question explanation (backward compatible interface)."""
1496
+ explanation, _ = self._get_explanation(*args, **kwargs)
1400
1497
  return explanation