QuizGenerator 0.7.1__py3-none-any.whl → 0.8.1__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 (30) hide show
  1. QuizGenerator/contentast.py +48 -15
  2. QuizGenerator/generate.py +2 -1
  3. QuizGenerator/mixins.py +14 -100
  4. QuizGenerator/premade_questions/basic.py +24 -29
  5. QuizGenerator/premade_questions/cst334/languages.py +100 -99
  6. QuizGenerator/premade_questions/cst334/math_questions.py +112 -122
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +621 -621
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +137 -163
  9. QuizGenerator/premade_questions/cst334/process.py +312 -328
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +34 -35
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +41 -36
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +48 -41
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +285 -521
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +149 -126
  15. QuizGenerator/premade_questions/cst463/models/attention.py +44 -50
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +43 -47
  17. QuizGenerator/premade_questions/cst463/models/matrices.py +61 -11
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +48 -50
  19. QuizGenerator/premade_questions/cst463/models/text.py +65 -67
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +47 -46
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +100 -156
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +93 -141
  23. QuizGenerator/question.py +310 -202
  24. QuizGenerator/quiz.py +8 -5
  25. QuizGenerator/regenerate.py +14 -6
  26. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/METADATA +30 -2
  27. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/RECORD +30 -30
  28. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/WHEEL +0 -0
  29. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/entry_points.txt +0 -0
  30. {quizgenerator-0.7.1.dist-info → quizgenerator-0.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ import collections
6
6
  import copy
7
7
  import enum
8
8
  import logging
9
+ import random
9
10
  import math
10
11
  from typing import List, Optional
11
12
 
@@ -31,43 +32,45 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
31
32
  VPN_BITS = "# VPN Bits"
32
33
  OFFSET_BITS = "# Offset Bits"
33
34
 
34
- def refresh(self, rng_seed=None, *args, **kwargs):
35
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
36
-
37
- # Generate baselines, if not given
38
- self.num_bits_va = kwargs.get("num_bits_va", self.rng.randint(2, self.MAX_BITS))
39
- self.num_bits_offset = self.rng.randint(1, self.num_bits_va - 1)
40
- self.num_bits_vpn = self.num_bits_va - self.num_bits_offset
41
-
42
- self.possible_answers = {
43
- self.Target.VA_BITS: ca.AnswerTypes.Int(self.num_bits_va, unit="bits"),
44
- self.Target.OFFSET_BITS: ca.AnswerTypes.Int(self.num_bits_offset, unit="bits"),
45
- self.Target.VPN_BITS: ca.AnswerTypes.Int(self.num_bits_vpn, unit="bits")
35
+ @classmethod
36
+ def _build_context(cls, *, rng_seed=None, **kwargs):
37
+ rng = random.Random(rng_seed)
38
+ num_bits_va = kwargs.get("num_bits_va", rng.randint(2, cls.MAX_BITS))
39
+ num_bits_offset = rng.randint(1, num_bits_va - 1)
40
+ num_bits_vpn = num_bits_va - num_bits_offset
41
+
42
+ possible_answers = {
43
+ cls.Target.VA_BITS: ca.AnswerTypes.Int(num_bits_va, unit="bits"),
44
+ cls.Target.OFFSET_BITS: ca.AnswerTypes.Int(num_bits_offset, unit="bits"),
45
+ cls.Target.VPN_BITS: ca.AnswerTypes.Int(num_bits_vpn, unit="bits")
46
46
  }
47
-
48
- # Select what kind of question we are going to be
49
- self.blank_kind = self.rng.choice(list(self.Target))
50
-
51
- self.answers['answer'] = self.possible_answers[self.blank_kind]
52
-
53
- return
54
-
55
- def _get_body(self, **kwargs):
47
+ blank_kind = rng.choice(list(cls.Target))
48
+
49
+ return {
50
+ "num_bits_va": num_bits_va,
51
+ "num_bits_offset": num_bits_offset,
52
+ "num_bits_vpn": num_bits_vpn,
53
+ "possible_answers": possible_answers,
54
+ "blank_kind": blank_kind,
55
+ }
56
+
57
+ @classmethod
58
+ def _build_body(cls, context):
56
59
  """Build question body and collect answers."""
57
- answers = [self.answers['answer']] # Collect the answer
60
+ answer = context["possible_answers"][context["blank_kind"]]
58
61
 
59
62
  # Create table data with one blank cell
60
63
  table_data = [{}]
61
- for target in list(self.Target):
62
- if target == self.blank_kind:
64
+ for target in list(cls.Target):
65
+ if target == context["blank_kind"]:
63
66
  # This cell should be an answer blank
64
- table_data[0][target.value] = self.possible_answers[target]
67
+ table_data[0][target.value] = context["possible_answers"][target]
65
68
  else:
66
69
  # This cell shows the value
67
- table_data[0][target.value] = f"{self.possible_answers[target].display} bits"
70
+ table_data[0][target.value] = f"{context['possible_answers'][target].display} bits"
68
71
 
69
- table = self.create_fill_in_table(
70
- headers=[t.value for t in list(self.Target)],
72
+ table = cls.create_fill_in_table(
73
+ headers=[t.value for t in list(cls.Target)],
71
74
  template_rows=table_data
72
75
  )
73
76
 
@@ -80,14 +83,10 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
80
83
  )
81
84
  )
82
85
  body.add_element(table)
83
- return body, answers
84
-
85
- def get_body(self, **kwargs) -> ca.Section:
86
- """Build question body (backward compatible interface)."""
87
- body, _ = self._get_body(**kwargs)
88
86
  return body
89
87
 
90
- def _get_explanation(self, **kwargs):
88
+ @classmethod
89
+ def _build_explanation(cls, context):
91
90
  """Build question explanation."""
92
91
  explanation = ca.Section()
93
92
 
@@ -105,20 +104,15 @@ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
105
104
  explanation.add_element(
106
105
  ca.Paragraph(
107
106
  [
108
- ca.Text(f"{self.num_bits_va}", emphasis=(self.blank_kind == self.Target.VA_BITS)),
107
+ ca.Text(f"{context['num_bits_va']}", emphasis=(context["blank_kind"] == cls.Target.VA_BITS)),
109
108
  ca.Text(" = "),
110
- ca.Text(f"{self.num_bits_vpn}", emphasis=(self.blank_kind == self.Target.VPN_BITS)),
109
+ ca.Text(f"{context['num_bits_vpn']}", emphasis=(context["blank_kind"] == cls.Target.VPN_BITS)),
111
110
  ca.Text(" + "),
112
- ca.Text(f"{self.num_bits_offset}", emphasis=(self.blank_kind == self.Target.OFFSET_BITS))
111
+ ca.Text(f"{context['num_bits_offset']}", emphasis=(context["blank_kind"] == cls.Target.OFFSET_BITS))
113
112
  ]
114
113
  )
115
114
  )
116
115
 
117
- return explanation, []
118
-
119
- def get_explanation(self, **kwargs) -> ca.Section:
120
- """Build question explanation (backward compatible interface)."""
121
- explanation, _ = self._get_explanation(**kwargs)
122
116
  return explanation
123
117
 
124
118
 
@@ -207,100 +201,116 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
207
201
 
208
202
  self.hit_rate = 0. # placeholder
209
203
 
210
- def refresh(self, previous: Optional[CachingQuestion] = None, *args, hard_refresh: bool = False, **kwargs):
211
- # Call parent refresh which seeds RNG and calls is_interesting()
212
- # Note: We ignore the parent's return value since we need to generate the workload first
213
- super().refresh(*args, **kwargs)
214
-
215
- # Use the mixin to get the cache policy (randomly selected or fixed)
216
- self.cache_policy = self.get_choice('policy', self.Kind)
217
-
218
- self.requests = (
219
- list(range(self.cache_size)) # Prime the cache with the capacity misses
220
- + self.rng.choices(
221
- population=list(range(self.cache_size - 1)), k=1
222
- ) # Add in one request to an earlier that will differentiate clearly between FIFO and LRU
223
- + self.rng.choices(
224
- population=list(range(self.cache_size, self.num_elements)), k=1
225
- ) ## Add in the rest of the requests
226
- + self.rng.choices(population=list(range(self.num_elements)), k=(self.num_requests - 2))
227
- ## Add in the rest of the requests
204
+ @classmethod
205
+ def _build_context(cls, *, rng_seed=None, **kwargs):
206
+ rng = random.Random(rng_seed)
207
+ num_elements = kwargs.get("num_elements", 5)
208
+ cache_size = kwargs.get("cache_size", 3)
209
+ num_requests = kwargs.get("num_requests", 10)
210
+
211
+ policy = kwargs.get("policy") or kwargs.get("algo")
212
+ if policy is None:
213
+ cache_policy = rng.choice(list(cls.Kind))
214
+ config_params = {"policy": cache_policy.name}
215
+ else:
216
+ if isinstance(policy, cls.Kind):
217
+ cache_policy = policy
218
+ else:
219
+ try:
220
+ cache_policy = cls.Kind[str(policy)]
221
+ except KeyError:
222
+ cache_policy = cls.Kind.FIFO
223
+ config_params = {"policy": cache_policy.name}
224
+
225
+ requests = (
226
+ list(range(cache_size))
227
+ + rng.choices(population=list(range(cache_size - 1)), k=1)
228
+ + rng.choices(population=list(range(cache_size, num_elements)), k=1)
229
+ + rng.choices(population=list(range(num_elements)), k=(num_requests - 2))
228
230
  )
229
-
230
- self.cache = CachingQuestion.Cache(self.cache_policy, self.cache_size, self.requests)
231
-
232
- self.request_results = {}
231
+
232
+ cache = cls.Cache(cache_policy, cache_size, requests)
233
+ request_results = {}
233
234
  number_of_hits = 0
234
- for (request_number, request) in enumerate(self.requests):
235
- was_hit, evicted, cache_state = self.cache.query_cache(request, request_number)
236
- log.debug(f"cache_state: \"{cache_state}\"")
235
+ for (request_number, request) in enumerate(requests):
236
+ was_hit, evicted, cache_state = cache.query_cache(request, request_number)
237
237
  if was_hit:
238
238
  number_of_hits += 1
239
- self.request_results[request_number] = {
240
- "request": (f"[answer__request]", request),
241
- "hit": (f"[answer__hit-{request_number}]", ('hit' if was_hit else 'miss')),
242
- "evicted": (f"[answer__evicted-{request_number}]", ('-' if evicted is None else f"{evicted}")),
243
- "cache_state": (f"[answer__cache_state-{request_number}]", ','.join(map(str, cache_state)))
244
- }
245
-
246
- self.answers.update(
247
- {
248
- f"answer__hit-{request_number}": ca.AnswerTypes.String(('hit' if was_hit else 'miss')),
249
- f"answer__evicted-{request_number}": ca.AnswerTypes.String(('-' if evicted is None else f"{evicted}")),
250
- f"answer__cache_state-{request_number}": ca.AnswerTypes.List(value=copy.copy(cache_state), order_matters=True),
251
- }
252
- )
253
-
254
- self.hit_rate = 100 * number_of_hits / (self.num_requests)
255
- self.answers.update(
256
- {
257
- "answer__hit_rate": ca.AnswerTypes.Float(self.hit_rate,
258
- label=f"Hit rate, excluding non-capacity misses",
259
- unit="%"
260
- )
239
+ hit_value = 'hit' if was_hit else 'miss'
240
+ evicted_value = '-' if evicted is None else f"{evicted}"
241
+ cache_state_value = copy.copy(cache_state)
242
+
243
+ request_results[request_number] = {
244
+ "request": request,
245
+ "hit_value": hit_value,
246
+ "evicted_value": evicted_value,
247
+ "cache_state_value": cache_state_value,
248
+ "hit_answer": ca.AnswerTypes.String(hit_value),
249
+ "evicted_answer": ca.AnswerTypes.String(evicted_value),
250
+ "cache_state_answer": ca.AnswerTypes.List(
251
+ value=cache_state_value,
252
+ order_matters=True
253
+ ),
261
254
  }
255
+
256
+ hit_rate = 100 * number_of_hits / num_requests
257
+ hit_rate_answer = ca.AnswerTypes.Float(
258
+ hit_rate,
259
+ label="Hit rate, excluding non-capacity misses",
260
+ unit="%"
262
261
  )
263
262
 
264
- # Return whether this workload is interesting
265
- return self.is_interesting()
266
-
267
- def _get_body(self, **kwargs):
263
+ return {
264
+ "num_elements": num_elements,
265
+ "cache_size": cache_size,
266
+ "num_requests": num_requests,
267
+ "cache_policy": cache_policy,
268
+ "requests": requests,
269
+ "request_results": request_results,
270
+ "hit_rate": hit_rate,
271
+ "hit_rate_answer": hit_rate_answer,
272
+ "_config_params": config_params,
273
+ }
274
+
275
+ @classmethod
276
+ def is_interesting_ctx(cls, context) -> bool:
277
+ return (context["hit_rate"] / 100.0) < 0.7
278
+
279
+ @classmethod
280
+ def _build_body(cls, context):
268
281
  """Build question body and collect answers."""
269
282
  answers = []
270
283
 
271
284
  # Create table data for cache simulation
272
285
  table_rows = []
273
- for request_number in sorted(self.request_results.keys()):
286
+ for request_number in sorted(context["request_results"].keys()):
287
+ result = context["request_results"][request_number]
274
288
  table_rows.append(
275
289
  {
276
- "Page Requested": f"{self.requests[request_number]}",
277
- "Hit/Miss": f"answer__hit-{request_number}", # Answer key
278
- "Evicted": f"answer__evicted-{request_number}", # Answer key
279
- "Cache State": f"answer__cache_state-{request_number}" # Answer key
290
+ "Page Requested": f"{context['requests'][request_number]}",
291
+ "Hit/Miss": result["hit_answer"],
292
+ "Evicted": result["evicted_answer"],
293
+ "Cache State": result["cache_state_answer"]
280
294
  }
281
295
  )
282
296
  # 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}"])
297
+ answers.append(result["hit_answer"])
298
+ answers.append(result["evicted_answer"])
299
+ answers.append(result["cache_state_answer"])
286
300
 
287
301
  # Create table using mixin - automatically handles answer conversion
288
- cache_table = self.create_answer_table(
302
+ cache_table = cls.create_answer_table(
289
303
  headers=["Page Requested", "Hit/Miss", "Evicted", "Cache State"],
290
304
  data_rows=table_rows,
291
305
  answer_columns=["Hit/Miss", "Evicted", "Cache State"]
292
306
  )
293
307
 
294
- # Collect hit rate answer
295
- hit_rate_answer = self.answers["answer__hit_rate"]
296
- answers.append(hit_rate_answer)
297
-
298
308
  # Create hit rate answer block
299
- hit_rate_block = ca.AnswerBlock(hit_rate_answer)
309
+ hit_rate_block = ca.AnswerBlock(context["hit_rate_answer"])
300
310
 
301
311
  # Use mixin to create complete body
302
312
  intro_text = (
303
- f"Assume we are using a **{self.cache_policy}** caching policy and a cache size of **{self.cache_size}**. "
313
+ f"Assume we are using a **{context['cache_policy']}** caching policy and a cache size of **{context['cache_size']}**. "
304
314
  "Given the below series of requests please fill in the table. "
305
315
  "For the hit/miss column, please write either \"hit\" or \"miss\". "
306
316
  "For the eviction column, please write either the number of the evicted page or simply a dash (e.g. \"-\")."
@@ -313,16 +323,12 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
313
323
  "In the case where there is a tie, order by increasing number."
314
324
  ])
315
325
 
316
- body = self.create_fill_in_table_body(intro_text, instructions, cache_table)
326
+ body = cls.create_fill_in_table_body(intro_text, instructions, cache_table)
317
327
  body.add_element(hit_rate_block)
318
- return body, answers
319
-
320
- def get_body(self, **kwargs) -> ca.Section:
321
- """Build question body (backward compatible interface)."""
322
- body, _ = self._get_body(**kwargs)
323
328
  return body
324
329
 
325
- def _get_explanation(self, **kwargs):
330
+ @classmethod
331
+ def _build_explanation(cls, context):
326
332
  """Build question explanation."""
327
333
  explanation = ca.Section()
328
334
 
@@ -333,12 +339,12 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
333
339
  headers=["Page", "Hit/Miss", "Evicted", "Cache State"],
334
340
  data=[
335
341
  [
336
- self.request_results[request]["request"][1],
337
- self.request_results[request]["hit"][1],
338
- f'{self.request_results[request]["evicted"][1]}',
339
- f'{self.request_results[request]["cache_state"][1]}',
342
+ context["request_results"][request]["request"],
343
+ context["request_results"][request]["hit_value"],
344
+ f'{context["request_results"][request]["evicted_value"]}',
345
+ f'{",".join(map(str, context["request_results"][request]["cache_state_value"]))}',
340
346
  ]
341
- for (request_number, request) in enumerate(sorted(self.request_results.keys()))
347
+ for (request_number, request) in enumerate(sorted(context["request_results"].keys()))
342
348
  ]
343
349
  )
344
350
  )
@@ -348,23 +354,13 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
348
354
  [
349
355
  "To calculate the hit rate we calculate the percentage of requests "
350
356
  "that were cache hits out of the total number of requests. "
351
- f"In this case we are counting only all but {self.cache_size} requests, "
357
+ f"In this case we are counting only all but {context['cache_size']} requests, "
352
358
  f"since we are excluding capacity misses."
353
359
  ]
354
360
  )
355
361
  )
356
362
 
357
- return explanation, []
358
-
359
- def get_explanation(self, **kwargs) -> ca.Section:
360
- """Build question explanation (backward compatible interface)."""
361
- explanation, _ = self._get_explanation(**kwargs)
362
363
  return explanation
363
-
364
- def is_interesting(self) -> bool:
365
- # todo: interesting is more likely based on whether I can differentiate between it and another algo,
366
- # so maybe rerun with a different approach but same requests?
367
- return (self.hit_rate / 100.0) < 0.7
368
364
 
369
365
 
370
366
  class MemoryAccessQuestion(MemoryQuestion, abc.ABC):
@@ -377,48 +373,51 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
377
373
  MIN_BOUNDS_BIT = 5
378
374
  MAX_BOUNDS_BITS = 16
379
375
 
380
- def refresh(self, rng_seed=None, *args, **kwargs):
381
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
382
-
383
- max_bound_bits = kwargs.get("max_bound_bits")
384
-
385
- bounds_bits = self.rng.randint(
386
- self.MIN_BOUNDS_BIT,
387
- self.MAX_BOUNDS_BITS
376
+ @classmethod
377
+ def _build_context(cls, *, rng_seed=None, **kwargs):
378
+ rng = random.Random(rng_seed)
379
+ bounds_bits = rng.randint(
380
+ cls.MIN_BOUNDS_BIT,
381
+ cls.MAX_BOUNDS_BITS
388
382
  )
389
- base_bits = self.MAX_BITS - bounds_bits
390
-
391
- self.bounds = int(math.pow(2, bounds_bits))
392
- self.base = self.rng.randint(1, int(math.pow(2, base_bits))) * self.bounds
393
- self.virtual_address = self.rng.randint(1, int(self.bounds / self.PROBABILITY_OF_VALID))
394
-
395
- if self.virtual_address < self.bounds:
396
- self.answers["answer"] = ca.AnswerTypes.Hex(
397
- self.base + self.virtual_address,
398
- length=math.ceil(math.log2(self.base + self.virtual_address))
383
+ base_bits = cls.MAX_BITS - bounds_bits
384
+
385
+ bounds = int(math.pow(2, bounds_bits))
386
+ base = rng.randint(1, int(math.pow(2, base_bits))) * bounds
387
+ virtual_address = rng.randint(1, int(bounds / cls.PROBABILITY_OF_VALID))
388
+
389
+ return {
390
+ "bounds": bounds,
391
+ "base": base,
392
+ "virtual_address": virtual_address,
393
+ }
394
+
395
+ @classmethod
396
+ def _build_body(cls, context):
397
+ """Build question body and collect answers."""
398
+ if context["virtual_address"] < context["bounds"]:
399
+ answer = ca.AnswerTypes.Hex(
400
+ context["base"] + context["virtual_address"],
401
+ length=math.ceil(math.log2(context["base"] + context["virtual_address"]))
399
402
  )
400
403
  else:
401
- self.answers["answer"] = ca.AnswerTypes.String("INVALID")
402
-
403
- def _get_body(self):
404
- """Build question body and collect answers."""
405
- answers = [self.answers["answer"]]
404
+ answer = ca.AnswerTypes.String("INVALID")
406
405
 
407
406
  # Use mixin to create parameter table with answer
408
407
  parameter_info = {
409
- "Base": f"0x{self.base:X}",
410
- "Bounds": f"0x{self.bounds:X}",
411
- "Virtual Address": f"0x{self.virtual_address:X}"
408
+ "Base": f"0x{context['base']:X}",
409
+ "Bounds": f"0x{context['bounds']:X}",
410
+ "Virtual Address": f"0x{context['virtual_address']:X}"
412
411
  }
413
412
 
414
- table = self.create_parameter_answer_table(
413
+ table = cls.create_parameter_answer_table(
415
414
  parameter_info=parameter_info,
416
415
  answer_label="Physical Address",
417
- answer_key="answer",
416
+ answer=answer,
418
417
  transpose=True
419
418
  )
420
419
 
421
- body = self.create_parameter_calculation_body(
420
+ body = cls.create_parameter_calculation_body(
422
421
  intro_text=(
423
422
  "Given the information in the below table, "
424
423
  "please calcuate the physical address associated with the given virtual address. "
@@ -426,14 +425,10 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
426
425
  ),
427
426
  parameter_table=table
428
427
  )
429
- return body, answers
430
-
431
- def get_body(self) -> ca.Section:
432
- """Build question body (backward compatible interface)."""
433
- body, _ = self._get_body()
434
428
  return body
435
429
 
436
- def _get_explanation(self):
430
+ @classmethod
431
+ def _build_explanation(cls, context):
437
432
  """Build question explanation."""
438
433
  explanation = ca.Section()
439
434
 
@@ -451,19 +446,19 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
451
446
  explanation.add_element(
452
447
  ca.Paragraph(
453
448
  [
454
- f"Step 1: 0x{self.virtual_address:X} < 0x{self.bounds:X} "
455
- f"--> {'***VALID***' if (self.virtual_address < self.bounds) else 'INVALID'}"
449
+ f"Step 1: 0x{context['virtual_address']:X} < 0x{context['bounds']:X} "
450
+ f"--> {'***VALID***' if (context['virtual_address'] < context['bounds']) else 'INVALID'}"
456
451
  ]
457
452
  )
458
453
  )
459
454
 
460
- if self.virtual_address < self.bounds:
455
+ if context["virtual_address"] < context["bounds"]:
461
456
  explanation.add_element(
462
457
  ca.Paragraph(
463
458
  [
464
459
  f"Step 2: Since the previous check passed, we calculate "
465
- f"0x{self.base:X} + 0x{self.virtual_address:X} "
466
- f"= ***0x{self.base + self.virtual_address:X}***.",
460
+ f"0x{context['base']:X} + 0x{context['virtual_address']:X} "
461
+ f"= ***0x{context['base'] + context['virtual_address']:X}***.",
467
462
  "If it had been invalid we would have simply written INVALID"
468
463
  ]
469
464
  )
@@ -474,17 +469,12 @@ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin
474
469
  [
475
470
  f"Step 2: Since the previous check failed, we simply write ***INVALID***.",
476
471
  "***If*** it had been valid, we would have calculated "
477
- f"0x{self.base:X} + 0x{self.virtual_address:X} "
478
- f"= 0x{self.base + self.virtual_address:X}.",
472
+ f"0x{context['base']:X} + 0x{context['virtual_address']:X} "
473
+ f"= 0x{context['base'] + context['virtual_address']:X}.",
479
474
  ]
480
475
  )
481
476
  )
482
477
 
483
- return explanation, []
484
-
485
- def get_explanation(self) -> ca.Section:
486
- """Build question explanation (backward compatible interface)."""
487
- explanation, _ = self._get_explanation()
488
478
  return explanation
489
479
 
490
480
 
@@ -494,7 +484,8 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
494
484
  MIN_VIRTUAL_BITS = 5
495
485
  MAX_VIRTUAL_BITS = 10
496
486
 
497
- def __within_bounds(self, segment, offset, bounds):
487
+ @staticmethod
488
+ def _within_bounds(segment, offset, bounds):
498
489
  if segment == "unallocated":
499
490
  return False
500
491
  elif bounds < offset:
@@ -502,28 +493,29 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
502
493
  else:
503
494
  return True
504
495
 
505
- def refresh(self, *args, **kwargs):
506
- super().refresh(*args, **kwargs)
507
-
496
+ @classmethod
497
+ def _build_context(cls, *, rng_seed=None, **kwargs):
498
+ rng = random.Random(rng_seed)
499
+
508
500
  # Pick how big each of our address spaces will be
509
- self.virtual_bits = self.rng.randint(self.MIN_VIRTUAL_BITS, self.MAX_VIRTUAL_BITS)
510
- self.physical_bits = self.rng.randint(self.virtual_bits + 1, self.MAX_BITS)
511
-
501
+ virtual_bits = rng.randint(cls.MIN_VIRTUAL_BITS, cls.MAX_VIRTUAL_BITS)
502
+ physical_bits = rng.randint(virtual_bits + 1, cls.MAX_BITS)
503
+
512
504
  # Start with blank base and bounds
513
- self.base = {
505
+ base = {
514
506
  "code": 0,
515
507
  "heap": 0,
516
508
  "stack": 0,
517
509
  }
518
- self.bounds = {
510
+ bounds = {
519
511
  "code": 0,
520
512
  "heap": 0,
521
513
  "stack": 0,
522
514
  }
523
-
515
+
524
516
  min_bounds = 4
525
- max_bounds = int(2 ** (self.virtual_bits - 2))
526
-
517
+ max_bounds = int(2 ** (virtual_bits - 2))
518
+
527
519
  def segment_collision(base, bounds):
528
520
  # lol, I think this is probably silly, but should work
529
521
  return 0 != len(
@@ -534,77 +526,81 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
534
526
  ]
535
527
  )
536
528
  )
537
-
538
- self.base["unallocated"] = 0
539
- self.bounds["unallocated"] = 0
540
-
529
+
530
+ base["unallocated"] = 0
531
+ bounds["unallocated"] = 0
532
+
541
533
  # Make random placements and check to make sure they are not overlapping
542
- while (segment_collision(self.base, self.bounds)):
543
- for segment in self.base.keys():
544
- self.bounds[segment] = self.rng.randint(min_bounds, max_bounds - 1)
545
- self.base[segment] = self.rng.randint(0, (2 ** self.physical_bits - self.bounds[segment]))
546
-
534
+ while (segment_collision(base, bounds)):
535
+ for segment in base.keys():
536
+ bounds[segment] = rng.randint(min_bounds, max_bounds - 1)
537
+ base[segment] = rng.randint(0, (2 ** physical_bits - bounds[segment]))
538
+
547
539
  # Pick a random segment for us to use
548
- self.segment = self.rng.choice(list(self.base.keys()))
549
- self.segment_bits = {
540
+ segment = rng.choice(list(base.keys()))
541
+ segment_bits = {
550
542
  "code": 0,
551
543
  "heap": 1,
552
544
  "unallocated": 2,
553
545
  "stack": 3
554
- }[self.segment]
555
-
546
+ }[segment]
547
+
556
548
  # Try to pick a random address within that range
557
- try:
558
- self.offset = self.rng.randint(
559
- 1,
560
- min(
561
- [
562
- max_bounds - 1,
563
- int(self.bounds[self.segment] / self.PROBABILITY_OF_VALID)
564
- ]
565
- )
549
+ if segment == "unallocated":
550
+ offset = rng.randint(0, max_bounds - 1)
551
+ else:
552
+ max_offset = min(
553
+ [
554
+ max_bounds - 1,
555
+ max(1, int(bounds[segment] / cls.PROBABILITY_OF_VALID))
556
+ ]
566
557
  )
567
- except KeyError:
568
- # If we are in an unallocated section, we'll get a key error (I think)
569
- self.offset = self.rng.randint(0, max_bounds - 1)
570
-
558
+ offset = rng.randint(1, max_offset)
559
+
571
560
  # Calculate a virtual address based on the segment and the offset
572
- self.virtual_address = (
573
- (self.segment_bits << (self.virtual_bits - 2))
574
- + self.offset
561
+ virtual_address = (
562
+ (segment_bits << (virtual_bits - 2))
563
+ + offset
575
564
  )
576
-
565
+
577
566
  # Calculate physical address based on offset
578
- self.physical_address = self.base[self.segment] + self.offset
579
-
580
- # Set answers based on whether it's in bounds or not
581
- if self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
582
- self.answers["answer__physical_address"] = ca.AnswerTypes.Binary(
583
- self.physical_address,
584
- length=self.physical_bits,
567
+ physical_address = base[segment] + offset
568
+
569
+ return {
570
+ "virtual_bits": virtual_bits,
571
+ "physical_bits": physical_bits,
572
+ "base": base,
573
+ "bounds": bounds,
574
+ "segment": segment,
575
+ "segment_bits": segment_bits,
576
+ "offset": offset,
577
+ "virtual_address": virtual_address,
578
+ "physical_address": physical_address,
579
+ }
580
+
581
+ @classmethod
582
+ def _build_body(cls, context):
583
+ """Build question body and collect answers."""
584
+ segment_answer = ca.AnswerTypes.String(context["segment"], label="Segment name")
585
+ if cls._within_bounds(context["segment"], context["offset"], context["bounds"][context["segment"]]):
586
+ physical_answer = ca.AnswerTypes.Binary(
587
+ context["physical_address"],
588
+ length=context["physical_bits"],
585
589
  label="Physical Address"
586
590
  )
587
591
  else:
588
- self.answers["answer__physical_address"] = ca.AnswerTypes.String("INVALID", label="Physical Address")
589
-
590
- self.answers["answer__segment"] = ca.AnswerTypes.String(self.segment, label="Segment name")
591
-
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
- ]
592
+ physical_answer = ca.AnswerTypes.String("INVALID", label="Physical Address")
593
+ answers = [segment_answer, physical_answer]
598
594
 
599
595
  body = ca.Section()
600
596
 
601
597
  body.add_element(
602
598
  ca.Paragraph(
603
599
  [
604
- f"Given a virtual address space of {self.virtual_bits}bits, "
605
- f"and a physical address space of {self.physical_bits}bits, "
600
+ f"Given a virtual address space of {context['virtual_bits']}bits, "
601
+ f"and a physical address space of {context['physical_bits']}bits, "
606
602
  "what is the physical address associated with the virtual address "
607
- f"0b{self.virtual_address:0{self.virtual_bits}b}?",
603
+ f"0b{context['virtual_address']:0{context['virtual_bits']}b}?",
608
604
  "If it is invalid simply type INVALID.",
609
605
  "Note: assume that the stack grows in the same way as the code and the heap."
610
606
  ]
@@ -613,12 +609,24 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
613
609
 
614
610
  # Create segment table using mixin
615
611
  segment_rows = [
616
- {"": "code", "base": f"0b{self.base['code']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['code']:0b}"},
617
- {"": "heap", "base": f"0b{self.base['heap']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['heap']:0b}"},
618
- {"": "stack", "base": f"0b{self.base['stack']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['stack']:0b}"}
612
+ {
613
+ "": "code",
614
+ "base": f"0b{context['base']['code']:0{context['physical_bits']}b}",
615
+ "bounds": f"0b{context['bounds']['code']:0b}"
616
+ },
617
+ {
618
+ "": "heap",
619
+ "base": f"0b{context['base']['heap']:0{context['physical_bits']}b}",
620
+ "bounds": f"0b{context['bounds']['heap']:0b}"
621
+ },
622
+ {
623
+ "": "stack",
624
+ "base": f"0b{context['base']['stack']:0{context['physical_bits']}b}",
625
+ "bounds": f"0b{context['bounds']['stack']:0b}"
626
+ }
619
627
  ]
620
628
 
621
- segment_table = self.create_answer_table(
629
+ segment_table = cls.create_answer_table(
622
630
  headers=["", "base", "bounds"],
623
631
  data_rows=segment_rows,
624
632
  answer_columns=[] # No answer columns in this table
@@ -628,18 +636,14 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
628
636
 
629
637
  body.add_element(
630
638
  ca.AnswerBlock([
631
- self.answers["answer__segment"],
632
- self.answers["answer__physical_address"]
639
+ segment_answer,
640
+ physical_answer
633
641
  ])
634
642
  )
635
643
  return body, answers
636
644
 
637
- def get_body(self) -> ca.Section:
638
- """Build question body (backward compatible interface)."""
639
- body, _ = self._get_body()
640
- return body
641
-
642
- def _get_explanation(self):
645
+ @classmethod
646
+ def _build_explanation(cls, context):
643
647
  explanation = ca.Section()
644
648
 
645
649
  explanation.add_element(
@@ -657,15 +661,15 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
657
661
  ca.Paragraph(
658
662
  [
659
663
  f"In this problem our virtual address, "
660
- f"converted to binary and including padding, is 0b{self.virtual_address:0{self.virtual_bits}b}.",
661
- f"From this we know that our segment bits are 0b{self.segment_bits:02b}, "
662
- f"meaning that we are in the ***{self.segment}*** segment.",
664
+ f"converted to binary and including padding, is 0b{context['virtual_address']:0{context['virtual_bits']}b}.",
665
+ f"From this we know that our segment bits are 0b{context['segment_bits']:02b}, "
666
+ f"meaning that we are in the ***{context['segment']}*** segment.",
663
667
  ""
664
668
  ]
665
669
  )
666
670
  )
667
671
 
668
- if self.segment == "unallocated":
672
+ if context["segment"] == "unallocated":
669
673
  explanation.add_element(
670
674
  ca.Paragraph(
671
675
  [
@@ -677,17 +681,17 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
677
681
  explanation.add_element(
678
682
  ca.Paragraph(
679
683
  [
680
- f"Since we are in the {self.segment} segment, "
681
- f"we see from our table that our bounds are {self.bounds[self.segment]}. "
682
- f"Remember that our check for our {self.segment} segment is: ",
683
- f"`if (offset >= bounds({self.segment})) : INVALID`",
684
+ f"Since we are in the {context['segment']} segment, "
685
+ f"we see from our table that our bounds are {context['bounds'][context['segment']]}. "
686
+ f"Remember that our check for our {context['segment']} segment is: ",
687
+ f"`if (offset >= bounds({context['segment']})) : INVALID`",
684
688
  "which becomes"
685
- f"`if ({self.offset:0b} > {self.bounds[self.segment]:0b}) : INVALID`"
689
+ f"`if ({context['offset']:0b} > {context['bounds'][context['segment']]:0b}) : INVALID`"
686
690
  ]
687
691
  )
688
692
  )
689
693
 
690
- if not self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
694
+ if not cls._within_bounds(context["segment"], context["offset"], context["bounds"][context["segment"]]):
691
695
  # then we are outside of bounds
692
696
  explanation.add_element(
693
697
  ca.Paragraph(
@@ -713,7 +717,7 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
713
717
  "To find the physical address we use the formula:",
714
718
  "<code>physical_address = base(segment) + offset</code>",
715
719
  "which becomes",
716
- f"<code>physical_address = {self.base[self.segment]:0b} + {self.offset:0b}</code>.",
720
+ f"<code>physical_address = {context['base'][context['segment']]:0b} + {context['offset']:0b}</code>.",
717
721
  ""
718
722
  ]
719
723
  )
@@ -728,19 +732,14 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
728
732
  )
729
733
  explanation.add_element(
730
734
  ca.Code(
731
- f" 0b{self.base[self.segment]:0{self.physical_bits}b}\n"
732
- f"<u>+ 0b{self.offset:0{self.physical_bits}b}</u>\n"
733
- f" 0b{self.physical_address:0{self.physical_bits}b}\n"
735
+ f" 0b{context['base'][context['segment']]:0{context['physical_bits']}b}\n"
736
+ f"<u>+ 0b{context['offset']:0{context['physical_bits']}b}</u>\n"
737
+ f" 0b{context['physical_address']:0{context['physical_bits']}b}\n"
734
738
  )
735
739
  )
736
740
 
737
741
  return explanation, []
738
742
 
739
- def get_explanation(self) -> ca.Section:
740
- """Build question explanation (backward compatible interface)."""
741
- explanation, _ = self._get_explanation()
742
- return explanation
743
-
744
743
 
745
744
  @QuestionRegistry.register()
746
745
  class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
@@ -752,99 +751,100 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
752
751
  MAX_VPN_BITS = 8
753
752
  MAX_PFN_BITS = 16
754
753
 
755
- def refresh(self, rng_seed=None, *args, **kwargs):
756
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
754
+ @classmethod
755
+ def _build_context(cls, *, rng_seed=None, **kwargs):
756
+ rng = random.Random(rng_seed)
757
757
 
758
- self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
759
- self.num_bits_vpn = self.rng.randint(self.MIN_VPN_BITS, self.MAX_VPN_BITS)
760
- self.num_bits_pfn = self.rng.randint(max([self.MIN_PFN_BITS, self.num_bits_vpn]), self.MAX_PFN_BITS)
758
+ num_bits_offset = rng.randint(cls.MIN_OFFSET_BITS, cls.MAX_OFFSET_BITS)
759
+ num_bits_vpn = rng.randint(cls.MIN_VPN_BITS, cls.MAX_VPN_BITS)
760
+ num_bits_pfn = rng.randint(max([cls.MIN_PFN_BITS, num_bits_vpn]), cls.MAX_PFN_BITS)
761
761
 
762
- self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
762
+ virtual_address = rng.randint(1, 2 ** (num_bits_vpn + num_bits_offset))
763
763
 
764
764
  # Calculate these two
765
- self.offset = self.virtual_address % (2 ** (self.num_bits_offset))
766
- self.vpn = self.virtual_address // (2 ** (self.num_bits_offset))
765
+ offset = virtual_address % (2 ** (num_bits_offset))
766
+ vpn = virtual_address // (2 ** (num_bits_offset))
767
767
 
768
768
  # Generate this randomly
769
- self.pfn = self.rng.randint(0, 2 ** (self.num_bits_pfn))
769
+ pfn = rng.randint(0, 2 ** (num_bits_pfn))
770
770
 
771
771
  # Calculate this
772
- self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
772
+ physical_address = pfn * (2 ** num_bits_offset) + offset
773
773
 
774
- if self.rng.choices([True, False], weights=[(self.PROBABILITY_OF_VALID), (1 - self.PROBABILITY_OF_VALID)], k=1)[0]:
775
- self.is_valid = True
774
+ if rng.choices([True, False], weights=[(cls.PROBABILITY_OF_VALID), (1 - cls.PROBABILITY_OF_VALID)], k=1)[0]:
775
+ is_valid = True
776
776
  # Set our actual entry to be in the table and valid
777
- self.pte = self.pfn + (2 ** (self.num_bits_pfn))
778
- # self.physical_address_var = VariableHex("Physical Address", self.physical_address, num_bits=(self.num_pfn_bits+self.num_offset_bits), default_presentation=VariableHex.PRESENTATION.BINARY)
779
- # self.pfn_var = VariableHex("PFN", self.pfn, num_bits=self.num_pfn_bits, default_presentation=VariableHex.PRESENTATION.BINARY)
777
+ pte = pfn + (2 ** (num_bits_pfn))
780
778
  else:
781
- self.is_valid = False
779
+ is_valid = False
782
780
  # Leave it as invalid
783
- self.pte = self.pfn
784
- # self.physical_address_var = Variable("Physical Address", "INVALID")
785
- # self.pfn_var = Variable("PFN", "INVALID")
786
-
787
- # self.pte_var = VariableHex("PTE", self.pte, num_bits=(self.num_pfn_bits+1), default_presentation=VariableHex.PRESENTATION.BINARY)
781
+ pte = pfn
788
782
 
789
783
  # Generate page table (moved from get_body to ensure deterministic generation)
790
- table_size = self.rng.randint(5, 8)
784
+ table_size = rng.randint(5, 8)
791
785
 
792
- lowest_possible_bottom = max([0, self.vpn - table_size])
793
- highest_possible_bottom = min([2 ** self.num_bits_vpn - table_size, self.vpn])
786
+ lowest_possible_bottom = max([0, vpn - table_size])
787
+ highest_possible_bottom = min([2 ** num_bits_vpn - table_size, vpn])
794
788
 
795
- table_bottom = self.rng.randint(lowest_possible_bottom, highest_possible_bottom)
789
+ table_bottom = rng.randint(lowest_possible_bottom, highest_possible_bottom)
796
790
  table_top = table_bottom + table_size
797
791
 
798
- self.page_table = {}
799
- self.page_table[self.vpn] = self.pte
792
+ page_table = {}
793
+ page_table[vpn] = pte
800
794
 
801
795
  # Fill in the rest of the table
802
- for vpn in range(table_bottom, table_top):
803
- if vpn == self.vpn: continue
804
- pte = self.page_table[self.vpn]
805
- while pte in self.page_table.values():
806
- pte = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
807
- if self.rng.choices([True, False], weights=[(1 - self.PROBABILITY_OF_VALID), self.PROBABILITY_OF_VALID], k=1)[0]:
796
+ for vpn_idx in range(table_bottom, table_top):
797
+ if vpn_idx == vpn:
798
+ continue
799
+ pte_candidate = page_table[vpn]
800
+ while pte_candidate in page_table.values():
801
+ pte_candidate = rng.randint(0, 2 ** num_bits_pfn - 1)
802
+ if rng.choices([True, False], weights=[(1 - cls.PROBABILITY_OF_VALID), cls.PROBABILITY_OF_VALID], k=1)[0]:
808
803
  # Randomly set it to be valid
809
- pte += (2 ** (self.num_bits_pfn))
804
+ pte_candidate += (2 ** num_bits_pfn)
810
805
  # Once we have a unique random entry, put it into the Page Table
811
- self.page_table[vpn] = pte
812
-
813
- self.answers.update(
814
- {
815
- "answer__vpn": ca.AnswerTypes.Binary(self.vpn, length=self.num_bits_vpn, label="VPN"),
816
- "answer__offset": ca.AnswerTypes.Binary(self.offset, length=self.num_bits_offset, label="Offset"),
817
- "answer__pte": ca.AnswerTypes.Binary(self.pte, length=(self.num_bits_pfn + 1), label="PTE"),
818
- }
819
- )
806
+ page_table[vpn_idx] = pte_candidate
807
+
808
+ return {
809
+ 'num_bits_offset': num_bits_offset,
810
+ 'num_bits_vpn': num_bits_vpn,
811
+ 'num_bits_pfn': num_bits_pfn,
812
+ 'virtual_address': virtual_address,
813
+ 'offset': offset,
814
+ 'vpn': vpn,
815
+ 'pfn': pfn,
816
+ 'physical_address': physical_address,
817
+ 'is_valid': is_valid,
818
+ 'pte': pte,
819
+ 'page_table': page_table,
820
+ }
820
821
 
821
- if self.is_valid:
822
- self.answers.update(
823
- {
824
- "answer__is_valid": ca.AnswerTypes.String("VALID", label="VALID or INVALID?"),
825
- "answer__pfn": ca.AnswerTypes.Binary(self.pfn, length=self.num_bits_pfn, label="PFN"),
826
- "answer__physical_address": ca.AnswerTypes.Binary(self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset), label="Physical Address"
827
- ),
828
- }
822
+ @classmethod
823
+ def _build_body(cls, context):
824
+ """Build question body and collect answers."""
825
+ vpn_answer = ca.AnswerTypes.Binary(context['vpn'], length=context['num_bits_vpn'], label='VPN')
826
+ offset_answer = ca.AnswerTypes.Binary(context['offset'], length=context['num_bits_offset'], label='Offset')
827
+ pte_answer = ca.AnswerTypes.Binary(context['pte'], length=(context['num_bits_pfn'] + 1), label='PTE')
828
+ if context['is_valid']:
829
+ is_valid_answer = ca.AnswerTypes.String('VALID', label='VALID or INVALID?')
830
+ pfn_answer = ca.AnswerTypes.Binary(context['pfn'], length=context['num_bits_pfn'], label='PFN')
831
+ physical_answer = ca.AnswerTypes.Binary(
832
+ context['physical_address'],
833
+ length=(context['num_bits_pfn'] + context['num_bits_offset']),
834
+ label='Physical Address'
829
835
  )
830
836
  else:
831
- self.answers.update(
832
- {
833
- "answer__is_valid": ca.AnswerTypes.String("INVALID", label="VALID or INVALID?"),
834
- "answer__pfn": ca.AnswerTypes.String("INVALID", label="PFN"),
835
- "answer__physical_address": ca.AnswerTypes.String("INVALID", label="Physical Address"),
836
- }
837
- )
838
-
839
- def _get_body(self, *args, **kwargs):
840
- """Build question body and collect answers."""
837
+ is_valid_answer = ca.AnswerTypes.String('INVALID', label='VALID or INVALID?')
838
+ pfn_answer = ca.AnswerTypes.String('INVALID', label='PFN')
839
+ physical_answer = ca.AnswerTypes.String('INVALID', label='Physical Address')
840
+
841
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"],
842
+ vpn_answer,
843
+ offset_answer,
844
+ pte_answer,
845
+ is_valid_answer,
846
+ pfn_answer,
847
+ physical_answer,
848
848
  ]
849
849
 
850
850
  body = ca.Section()
@@ -852,73 +852,69 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
852
852
  body.add_element(
853
853
  ca.Paragraph(
854
854
  [
855
- "Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
856
- "Remember, we typically have the MSB representing valid or invalid."
855
+ 'Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.',
856
+ 'Remember, we typically have the MSB representing valid or invalid.'
857
857
  ]
858
858
  )
859
859
  )
860
860
 
861
861
  # Create parameter info table using mixin
862
862
  parameter_info = {
863
- "Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
864
- "# VPN bits": f"{self.num_bits_vpn}",
865
- "# PFN bits": f"{self.num_bits_pfn}"
863
+ 'Virtual Address': f"0b{context['virtual_address']:0{context['num_bits_vpn'] + context['num_bits_offset']}b}",
864
+ '# VPN bits': f"{context['num_bits_vpn']}",
865
+ '# PFN bits': f"{context['num_bits_pfn']}"
866
866
  }
867
867
 
868
- body.add_element(self.create_info_table(parameter_info))
868
+ body.add_element(cls.create_info_table(parameter_info))
869
869
 
870
- # Use the page table generated in refresh() for deterministic output
870
+ # Use the page table generated in _build_context for deterministic output
871
871
  # Add in ellipses before and after page table entries, if appropriate
872
872
  value_matrix = []
873
873
 
874
- if min(self.page_table.keys()) != 0:
875
- value_matrix.append(["...", "..."])
874
+ if min(context['page_table'].keys()) != 0:
875
+ value_matrix.append(['...', '...'])
876
876
 
877
877
  value_matrix.extend(
878
878
  [
879
- [f"0b{vpn:0{self.num_bits_vpn}b}", f"0b{pte:0{(self.num_bits_pfn + 1)}b}"]
880
- for vpn, pte in sorted(self.page_table.items())
879
+ [f"0b{vpn:0{context['num_bits_vpn']}b}", f"0b{pte:0{(context['num_bits_pfn'] + 1)}b}"]
880
+ for vpn, pte in sorted(context['page_table'].items())
881
881
  ]
882
882
  )
883
883
 
884
- if (max(self.page_table.keys()) + 1) != 2 ** self.num_bits_vpn:
885
- value_matrix.append(["...", "..."])
884
+ if (max(context['page_table'].keys()) + 1) != 2 ** context['num_bits_vpn']:
885
+ value_matrix.append(['...', '...'])
886
886
 
887
887
  body.add_element(
888
888
  ca.Table(
889
- headers=["VPN", "PTE"],
889
+ headers=['VPN', 'PTE'],
890
890
  data=value_matrix
891
891
  )
892
892
  )
893
893
 
894
894
  body.add_element(
895
895
  ca.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"],
896
+ vpn_answer,
897
+ offset_answer,
898
+ pte_answer,
899
+ is_valid_answer,
900
+ pfn_answer,
901
+ physical_answer,
902
902
  ])
903
903
  )
904
904
 
905
905
  return body, answers
906
-
907
- def get_body(self, *args, **kwargs) -> ca.Section:
908
- """Build question body (backward compatible interface)."""
909
- body, _ = self._get_body(*args, **kwargs)
910
- return body
911
906
 
912
- def _get_explanation(self, *args, **kwargs):
907
+ @classmethod
908
+ def _build_explanation(cls, context):
913
909
  """Build question explanation."""
914
910
  explanation = ca.Section()
915
911
 
916
912
  explanation.add_element(
917
913
  ca.Paragraph(
918
914
  [
919
- "The core idea of Paging is we want to break the virtual address into the VPN and the offset. "
920
- "From here, we get the Page Table Entry corresponding to the VPN, and check the validity of the entry. "
921
- "If it is valid, we clear the metadata and attach the PFN to the offset and have our physical address.",
915
+ 'The core idea of Paging is we want to break the virtual address into the VPN and the offset. '
916
+ 'From here, we get the Page Table Entry corresponding to the VPN, and check the validity of the entry. '
917
+ 'If it is valid, we clear the metadata and attach the PFN to the offset and have our physical address.',
922
918
  ]
923
919
  )
924
920
  )
@@ -934,9 +930,9 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
934
930
  explanation.add_element(
935
931
  ca.Paragraph(
936
932
  [
937
- f"Virtual Address = VPN | offset",
938
- f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> "
939
- f"= <tt>0b{self.vpn:0{self.num_bits_vpn}b}</tt> | <tt>0b{self.offset:0{self.num_bits_offset}b}</tt>",
933
+ 'Virtual Address = VPN | offset',
934
+ f"<tt>0b{context['virtual_address']:0{context['num_bits_vpn'] + context['num_bits_offset']}b}</tt> "
935
+ f"= <tt>0b{context['vpn']:0{context['num_bits_vpn']}b}</tt> | <tt>0b{context['offset']:0{context['num_bits_offset']}b}</tt>",
940
936
  ]
941
937
  )
942
938
  )
@@ -944,19 +940,19 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
944
940
  explanation.add_element(
945
941
  ca.Paragraph(
946
942
  [
947
- "We next use our VPN to index into our page table and find the corresponding entry."
948
- f"Our Page Table Entry is ",
949
- f"<tt>0b{self.pte:0{(self.num_bits_pfn + 1)}b}</tt>"
950
- f"which we found by looking for our VPN in the page table.",
943
+ 'We next use our VPN to index into our page table and find the corresponding entry.'
944
+ 'Our Page Table Entry is ',
945
+ f"<tt>0b{context['pte']:0{(context['num_bits_pfn'] + 1)}b}</tt>"
946
+ 'which we found by looking for our VPN in the page table.',
951
947
  ]
952
948
  )
953
949
  )
954
950
 
955
- if self.is_valid:
951
+ if context['is_valid']:
956
952
  explanation.add_element(
957
953
  ca.Paragraph(
958
954
  [
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**"
955
+ f"In our PTE we see that the first bit is **{context['pte'] // (2 ** context['num_bits_pfn'])}** meaning that the translation is **VALID**"
960
956
  ]
961
957
  )
962
958
  )
@@ -964,10 +960,10 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
964
960
  explanation.add_element(
965
961
  ca.Paragraph(
966
962
  [
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**.",
968
- "Therefore, we just write \"INVALID\" as our answer.",
969
- "If it were valid we would complete the below steps.",
970
- "<hr>"
963
+ f"In our PTE we see that the first bit is **{context['pte'] // (2 ** context['num_bits_pfn'])}** meaning that the translation is **INVALID**.",
964
+ 'Therefore, we just write "INVALID" as our answer.',
965
+ 'If it were valid we would complete the below steps.',
966
+ '<hr>'
971
967
  ]
972
968
  )
973
969
  )
@@ -975,18 +971,18 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
975
971
  explanation.add_element(
976
972
  ca.Paragraph(
977
973
  [
978
- "Next, we convert our PTE to our PFN by removing our metadata. "
979
- "In this case we're just removing the leading bit. We can do this by applying a binary mask.",
980
- f"PFN = PTE & mask",
981
- f"which is,"
974
+ 'Next, we convert our PTE to our PFN by removing our metadata. '
975
+ "In this case we are just removing the leading bit. We can do this by applying a binary mask.",
976
+ 'PFN = PTE & mask',
977
+ 'which is,',
982
978
  ]
983
979
  )
984
980
  )
985
981
  explanation.add_element(
986
982
  ca.Equation(
987
- f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} "
988
- f"= \\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} "
989
- f"\\& \\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
983
+ f"\\texttt{{{context['pfn']:0{context['num_bits_pfn']}b}}} "
984
+ f"= \\texttt{{0b{context['pte']:0{context['num_bits_pfn'] + 1}b}}} "
985
+ f"\\& \\texttt{{0b{(2 ** context['num_bits_pfn']) - 1:0{context['num_bits_pfn'] + 1}b}}}"
990
986
  )
991
987
  )
992
988
 
@@ -994,21 +990,21 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
994
990
  [
995
991
  ca.Paragraph(
996
992
  [
997
- "We then add combine our PFN and offset, "
998
- "Physical Address = PFN | offset",
993
+ 'We then add combine our PFN and offset, '
994
+ 'Physical Address = PFN | offset',
999
995
  ]
1000
996
  ),
1001
997
  ca.Equation(
1002
- fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = \mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}}} \mid \mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
998
+ fr"{r'\mathbf{' if context['is_valid'] else ''}\mathtt{{0b{context['physical_address']:0{context['num_bits_pfn'] + context['num_bits_offset']}b}}}{r'}' if context['is_valid'] else ''} = \mathtt{{0b{context['pfn']:0{context['num_bits_pfn']}b}}} \mid \mathtt{{0b{context['offset']:0{context['num_bits_offset']}b}}}"
1003
999
  )
1004
1000
  ]
1005
1001
  )
1006
1002
 
1007
1003
  explanation.add_elements(
1008
1004
  [
1009
- ca.Paragraph(["Note: Strictly speaking, this calculation is:", ]),
1005
+ ca.Paragraph(['Note: Strictly speaking, this calculation is:', ]),
1010
1006
  ca.Equation(
1011
- fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = \mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}{0:0{self.num_bits_offset}}}} + \mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
1007
+ fr"{r'\mathbf{' if context['is_valid'] else ''}\mathtt{{0b{context['physical_address']:0{context['num_bits_pfn'] + context['num_bits_offset']}b}}}{r'}' if context['is_valid'] else ''} = \mathtt{{0b{context['pfn']:0{context['num_bits_pfn']}b}{0:0{context['num_bits_offset']}}}} + \mathtt{{0b{context['offset']:0{context['num_bits_offset']}b}}}"
1012
1008
  ),
1013
1009
  ca.Paragraph(["But that's a lot of extra 0s, so I'm splitting them up for succinctness"])
1014
1010
  ]
@@ -1016,11 +1012,6 @@ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
1016
1012
 
1017
1013
  return explanation, []
1018
1014
 
1019
- def get_explanation(self, *args, **kwargs) -> ca.Section:
1020
- """Build question explanation (backward compatible interface)."""
1021
- explanation, _ = self._get_explanation(*args, **kwargs)
1022
- return explanation
1023
-
1024
1015
 
1025
1016
  @QuestionRegistry.register()
1026
1017
  class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
@@ -1034,246 +1025,270 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1034
1025
  MAX_PTI_BITS = 3
1035
1026
  MAX_PFN_BITS = 6
1036
1027
 
1037
- def refresh(self, rng_seed=None, *args, **kwargs):
1038
- super().refresh(rng_seed=rng_seed, *args, **kwargs)
1028
+ @classmethod
1029
+ def _build_context(cls, *, rng_seed=None, **kwargs):
1030
+ rng = random.Random(rng_seed)
1039
1031
 
1040
1032
  # Set up bit counts
1041
- self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
1042
- self.num_bits_pdi = self.rng.randint(self.MIN_PDI_BITS, self.MAX_PDI_BITS)
1043
- self.num_bits_pti = self.rng.randint(self.MIN_PTI_BITS, self.MAX_PTI_BITS)
1044
- self.num_bits_pfn = self.rng.randint(self.MIN_PFN_BITS, self.MAX_PFN_BITS)
1033
+ num_bits_offset = rng.randint(cls.MIN_OFFSET_BITS, cls.MAX_OFFSET_BITS)
1034
+ num_bits_pdi = rng.randint(cls.MIN_PDI_BITS, cls.MAX_PDI_BITS)
1035
+ num_bits_pti = rng.randint(cls.MIN_PTI_BITS, cls.MAX_PTI_BITS)
1036
+ num_bits_pfn = rng.randint(cls.MIN_PFN_BITS, cls.MAX_PFN_BITS)
1045
1037
 
1046
1038
  # Total VPN bits = PDI + PTI
1047
- self.num_bits_vpn = self.num_bits_pdi + self.num_bits_pti
1048
-
1039
+ num_bits_vpn = num_bits_pdi + num_bits_pti
1040
+
1049
1041
  # Generate a random virtual address
1050
- self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
1042
+ virtual_address = rng.randint(1, 2 ** (num_bits_vpn + num_bits_offset))
1051
1043
 
1052
1044
  # Extract components from virtual address
1053
- self.offset = self.virtual_address % (2 ** self.num_bits_offset)
1054
- vpn = self.virtual_address // (2 ** self.num_bits_offset)
1045
+ offset = virtual_address % (2 ** num_bits_offset)
1046
+ vpn = virtual_address // (2 ** num_bits_offset)
1055
1047
 
1056
- self.pti = vpn % (2 ** self.num_bits_pti)
1057
- self.pdi = vpn // (2 ** self.num_bits_pti)
1048
+ pti = vpn % (2 ** num_bits_pti)
1049
+ pdi = vpn // (2 ** num_bits_pti)
1058
1050
 
1059
1051
  # Generate PFN randomly
1060
- self.pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1052
+ pfn = rng.randint(0, 2 ** num_bits_pfn - 1)
1061
1053
 
1062
1054
  # Calculate physical address
1063
- self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
1055
+ physical_address = pfn * (2 ** num_bits_offset) + offset
1064
1056
 
1065
1057
  # Determine validity at both levels
1066
1058
  # PD entry can be valid or invalid
1067
- self.pd_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
1059
+ pd_valid = rng.choices([True, False], weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID], k=1)[0]
1068
1060
 
1069
1061
  # PT entry only matters if PD is valid
1070
- if self.pd_valid:
1071
- self.pt_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
1062
+ if pd_valid:
1063
+ pt_valid = rng.choices([True, False], weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID], k=1)[0]
1072
1064
  else:
1073
- self.pt_valid = False # Doesn't matter, won't be checked
1065
+ pt_valid = False # Doesn't matter, won't be checked
1074
1066
 
1075
1067
  # Generate a page table number (PTBR - Page Table Base Register value in the PD entry)
1076
1068
  # This represents which page table to use
1077
- self.page_table_number = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1069
+ page_table_number = rng.randint(0, 2 ** num_bits_pfn - 1)
1078
1070
 
1079
1071
  # Create PD entry: valid bit + page table number
1080
- if self.pd_valid:
1081
- self.pd_entry = (2 ** self.num_bits_pfn) + self.page_table_number
1072
+ if pd_valid:
1073
+ pd_entry = (2 ** num_bits_pfn) + page_table_number
1082
1074
  else:
1083
- self.pd_entry = self.page_table_number # Invalid, no valid bit set
1075
+ pd_entry = page_table_number # Invalid, no valid bit set
1084
1076
 
1085
1077
  # Create PT entry: valid bit + PFN
1086
- if self.pt_valid:
1087
- self.pte = (2 ** self.num_bits_pfn) + self.pfn
1078
+ if pt_valid:
1079
+ pte = (2 ** num_bits_pfn) + pfn
1088
1080
  else:
1089
- self.pte = self.pfn # Invalid, no valid bit set
1081
+ pte = pfn # Invalid, no valid bit set
1090
1082
 
1091
1083
  # Overall validity requires both levels to be valid
1092
- self.is_valid = self.pd_valid and self.pt_valid
1084
+ is_valid = pd_valid and pt_valid
1093
1085
 
1094
1086
  # Build page directory - show 3-4 entries
1095
- pd_size = self.rng.randint(3, 4)
1096
- lowest_pd_bottom = max([0, self.pdi - pd_size])
1097
- highest_pd_bottom = min([2 ** self.num_bits_pdi - pd_size, self.pdi])
1098
- pd_bottom = self.rng.randint(lowest_pd_bottom, highest_pd_bottom)
1087
+ pd_size = rng.randint(3, 4)
1088
+ lowest_pd_bottom = max([0, pdi - pd_size])
1089
+ highest_pd_bottom = min([2 ** num_bits_pdi - pd_size, pdi])
1090
+ pd_bottom = rng.randint(lowest_pd_bottom, highest_pd_bottom)
1099
1091
  pd_top = pd_bottom + pd_size
1100
1092
 
1101
- self.page_directory = {}
1102
- self.page_directory[self.pdi] = self.pd_entry
1093
+ page_directory = {}
1094
+ page_directory[pdi] = pd_entry
1103
1095
 
1104
1096
  # Fill in other PD entries
1105
- for pdi in range(pd_bottom, pd_top):
1106
- if pdi == self.pdi:
1097
+ for pdi_idx in range(pd_bottom, pd_top):
1098
+ if pdi_idx == pdi:
1107
1099
  continue
1108
1100
  # Generate random PD entry
1109
- pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1110
- while pt_num == self.page_table_number: # Make sure it's different
1111
- pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1101
+ pt_num = rng.randint(0, 2 ** num_bits_pfn - 1)
1102
+ while pt_num == page_table_number: # Make sure it's different
1103
+ pt_num = rng.randint(0, 2 ** num_bits_pfn - 1)
1112
1104
 
1113
1105
  # Randomly valid or invalid
1114
- if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1115
- pd_val = (2 ** self.num_bits_pfn) + pt_num
1106
+ if rng.choices([True, False], weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID], k=1)[0]:
1107
+ pd_val = (2 ** num_bits_pfn) + pt_num
1116
1108
  else:
1117
1109
  pd_val = pt_num
1118
1110
 
1119
- self.page_directory[pdi] = pd_val
1111
+ page_directory[pdi_idx] = pd_val
1120
1112
 
1121
1113
  # Build 2-3 page tables to show
1122
1114
  # Always include the one we need, plus 1-2 others
1123
- num_page_tables_to_show = self.rng.randint(2, 3)
1115
+ num_page_tables_to_show = rng.randint(2, 3)
1124
1116
 
1125
1117
  # Get unique page table numbers from the PD entries (extract PT numbers from valid entries)
1126
1118
  shown_pt_numbers = set()
1127
- for pdi, pd_val in self.page_directory.items():
1128
- pt_num = pd_val % (2 ** self.num_bits_pfn) # Extract PT number (remove valid bit)
1119
+ for pd_val in page_directory.values():
1120
+ pt_num = pd_val % (2 ** num_bits_pfn) # Extract PT number (remove valid bit)
1129
1121
  shown_pt_numbers.add(pt_num)
1130
1122
 
1131
1123
  # Ensure our required page table is included
1132
- shown_pt_numbers.add(self.page_table_number)
1124
+ shown_pt_numbers.add(page_table_number)
1133
1125
 
1134
1126
  # Limit to requested number, but ALWAYS keep the required page table
1135
1127
  shown_pt_numbers_list = list(shown_pt_numbers)
1136
- if self.page_table_number in shown_pt_numbers_list:
1128
+ if page_table_number in shown_pt_numbers_list:
1137
1129
  # Remove it temporarily so we can add it back first
1138
- shown_pt_numbers_list.remove(self.page_table_number)
1130
+ shown_pt_numbers_list.remove(page_table_number)
1139
1131
  # Start with required page table, then add others up to the limit
1140
- shown_pt_numbers = [self.page_table_number] + shown_pt_numbers_list[:num_page_tables_to_show - 1]
1132
+ shown_pt_numbers = [page_table_number] + shown_pt_numbers_list[:num_page_tables_to_show - 1]
1141
1133
 
1142
1134
  # Build each page table
1143
- self.page_tables = {} # Dict mapping PT number -> dict of PTI -> PTE
1135
+ page_tables = {} # Dict mapping PT number -> dict of PTI -> PTE
1144
1136
 
1145
1137
  # Use consistent size for all page tables for cleaner presentation
1146
- pt_size = self.rng.randint(2, 4)
1138
+ pt_size = rng.randint(2, 4)
1147
1139
 
1148
1140
  # Determine the PTI range that all tables will use (based on target PTI)
1149
1141
  # This ensures all tables show the same PTI values for consistency
1150
- lowest_pt_bottom = max([0, self.pti - pt_size + 1])
1151
- highest_pt_bottom = min([2 ** self.num_bits_pti - pt_size, self.pti])
1152
- pt_bottom = self.rng.randint(lowest_pt_bottom, highest_pt_bottom)
1142
+ lowest_pt_bottom = max([0, pti - pt_size + 1])
1143
+ highest_pt_bottom = min([2 ** num_bits_pti - pt_size, pti])
1144
+ pt_bottom = rng.randint(lowest_pt_bottom, highest_pt_bottom)
1153
1145
  pt_top = pt_bottom + pt_size
1154
1146
 
1155
1147
  # Generate all page tables using the SAME PTI range
1156
1148
  for pt_num in shown_pt_numbers:
1157
- self.page_tables[pt_num] = {}
1149
+ page_tables[pt_num] = {}
1158
1150
 
1159
- for pti in range(pt_bottom, pt_top):
1160
- if pt_num == self.page_table_number and pti == self.pti:
1151
+ for pti_idx in range(pt_bottom, pt_top):
1152
+ if pt_num == page_table_number and pti_idx == pti:
1161
1153
  # Use the actual answer for the target page table entry
1162
- self.page_tables[pt_num][pti] = self.pte
1154
+ page_tables[pt_num][pti_idx] = pte
1163
1155
  else:
1164
1156
  # Generate random PTE for all other entries
1165
- pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1166
- if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1167
- pte_val = (2 ** self.num_bits_pfn) + pfn
1157
+ pfn_rand = rng.randint(0, 2 ** num_bits_pfn - 1)
1158
+ if rng.choices([True, False], weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID], k=1)[0]:
1159
+ pte_val = (2 ** num_bits_pfn) + pfn_rand
1168
1160
  else:
1169
- pte_val = pfn
1170
-
1171
- self.page_tables[pt_num][pti] = pte_val
1172
-
1173
- # Set up answers
1174
- self.answers.update({
1175
- "answer__pdi": ca.AnswerTypes.Binary(self.pdi, length=self.num_bits_pdi,
1176
- label="PDI (Page Directory Index)"),
1177
- "answer__pti": ca.AnswerTypes.Binary(self.pti, length=self.num_bits_pti,
1178
- label="PTI (Page Table Index)"),
1179
- "answer__offset": ca.AnswerTypes.Binary(self.offset, length=self.num_bits_offset,
1180
- label="Offset"),
1181
- "answer__pd_entry": ca.AnswerTypes.Binary(self.pd_entry, length=(self.num_bits_pfn + 1),
1182
- label="PD Entry (from Page Directory)"),
1183
- "answer__pt_number": (
1184
- ca.AnswerTypes.Binary(self.page_table_number, length=self.num_bits_pfn,
1185
- label="Page Table Number")
1186
- if self.pd_valid
1187
- else ca.AnswerTypes.String("INVALID", label="Page Table Number")
1188
- ),
1189
- })
1190
-
1191
- # PTE answer: if PD is valid, accept the actual PTE value from the table
1192
- # (regardless of whether that PTE is valid or invalid)
1193
- if self.pd_valid:
1194
- self.answers.update({
1195
- "answer__pte": ca.AnswerTypes.Binary(self.pte, length=(self.num_bits_pfn + 1),
1196
- label="PTE (from Page Table)"),
1197
- })
1161
+ pte_val = pfn_rand
1162
+
1163
+ page_tables[pt_num][pti_idx] = pte_val
1164
+
1165
+ def random_pte_value():
1166
+ pfn_rand = rng.randint(0, 2 ** num_bits_pfn - 1)
1167
+ if rng.choices([True, False], weights=[cls.PROBABILITY_OF_VALID, 1 - cls.PROBABILITY_OF_VALID], k=1)[0]:
1168
+ return (2 ** num_bits_pfn) + pfn_rand
1169
+ return pfn_rand
1170
+
1171
+ pt_display_extras = {}
1172
+ for pt_num, pt_entries in page_tables.items():
1173
+ min_pti = min(pt_entries.keys())
1174
+ max_pti = max(pt_entries.keys())
1175
+ max_possible_pti = 2 ** num_bits_pti - 1
1176
+ leading = None
1177
+ trailing = None
1178
+ if min_pti == 1:
1179
+ leading = (0, random_pte_value())
1180
+ if (max_possible_pti - max_pti) == 1:
1181
+ trailing = (max_possible_pti, random_pte_value())
1182
+ pt_display_extras[pt_num] = {
1183
+ 'leading': leading,
1184
+ 'trailing': trailing,
1185
+ }
1186
+
1187
+ return {
1188
+ 'num_bits_offset': num_bits_offset,
1189
+ 'num_bits_pdi': num_bits_pdi,
1190
+ 'num_bits_pti': num_bits_pti,
1191
+ 'num_bits_pfn': num_bits_pfn,
1192
+ 'num_bits_vpn': num_bits_vpn,
1193
+ 'virtual_address': virtual_address,
1194
+ 'offset': offset,
1195
+ 'pdi': pdi,
1196
+ 'pti': pti,
1197
+ 'pfn': pfn,
1198
+ 'physical_address': physical_address,
1199
+ 'pd_valid': pd_valid,
1200
+ 'pt_valid': pt_valid,
1201
+ 'page_table_number': page_table_number,
1202
+ 'pd_entry': pd_entry,
1203
+ 'pte': pte,
1204
+ 'is_valid': is_valid,
1205
+ 'page_directory': page_directory,
1206
+ 'page_tables': page_tables,
1207
+ 'pt_display_extras': pt_display_extras,
1208
+ }
1209
+
1210
+ @classmethod
1211
+ def _build_body(cls, context):
1212
+ """Build question body and collect answers."""
1213
+ pdi_answer = ca.AnswerTypes.Binary(context['pdi'], length=context['num_bits_pdi'], label='PDI (Page Directory Index)')
1214
+ pti_answer = ca.AnswerTypes.Binary(context['pti'], length=context['num_bits_pti'], label='PTI (Page Table Index)')
1215
+ offset_answer = ca.AnswerTypes.Binary(context['offset'], length=context['num_bits_offset'], label='Offset')
1216
+ pd_entry_answer = ca.AnswerTypes.Binary(context['pd_entry'], length=(context['num_bits_pfn'] + 1), label='PD Entry (from Page Directory)')
1217
+ if context['pd_valid']:
1218
+ pt_number_answer = ca.AnswerTypes.Binary(context['page_table_number'], length=context['num_bits_pfn'], label='Page Table Number')
1219
+ pte_answer = ca.AnswerTypes.Binary(context['pte'], length=(context['num_bits_pfn'] + 1), label='PTE (from Page Table)')
1198
1220
  else:
1199
- # If PD is invalid, student can't look up the page table
1200
- # Accept both "INVALID" (for consistency) and "N/A" (for accuracy)
1201
- self.answers.update({
1202
- "answer__pte": ca.AnswerTypes.String(["INVALID", "N/A"], label="PTE (from Page Table)"),
1203
- })
1204
-
1205
- # Validity, PFN, and Physical Address depend on BOTH levels being valid
1206
- if self.pd_valid and self.pt_valid:
1207
- self.answers.update({
1208
- "answer__is_valid": ca.AnswerTypes.String("VALID", label="VALID or INVALID?"),
1209
- "answer__pfn": ca.AnswerTypes.Binary(self.pfn, length=self.num_bits_pfn, label="PFN"),
1210
- "answer__physical_address": ca.AnswerTypes.Binary(self.physical_address, length=(self.num_bits_pfn + self.num_bits_offset), label="Physical Address"
1211
- ),
1212
- })
1221
+ pt_number_answer = ca.AnswerTypes.String('INVALID', label='Page Table Number')
1222
+ pte_answer = ca.AnswerTypes.String(['INVALID', 'N/A'], label='PTE (from Page Table)')
1223
+
1224
+ if context['pd_valid'] and context['pt_valid']:
1225
+ is_valid_answer = ca.AnswerTypes.String('VALID', label='VALID or INVALID?')
1226
+ pfn_answer = ca.AnswerTypes.Binary(context['pfn'], length=context['num_bits_pfn'], label='PFN')
1227
+ physical_answer = ca.AnswerTypes.Binary(
1228
+ context['physical_address'],
1229
+ length=(context['num_bits_pfn'] + context['num_bits_offset']),
1230
+ label='Physical Address'
1231
+ )
1213
1232
  else:
1214
- self.answers.update({
1215
- "answer__is_valid": ca.AnswerTypes.String("INVALID", label="VALID or INVALID?"),
1216
- "answer__pfn": ca.AnswerTypes.String("INVALID", label="PFN"),
1217
- "answer__physical_address": ca.AnswerTypes.String("INVALID", label="Physical Address"),
1218
- })
1233
+ is_valid_answer = ca.AnswerTypes.String('INVALID', label='VALID or INVALID?')
1234
+ pfn_answer = ca.AnswerTypes.String('INVALID', label='PFN')
1235
+ physical_answer = ca.AnswerTypes.String('INVALID', label='Physical Address')
1219
1236
 
1220
- def _get_body(self, *args, **kwargs):
1221
- """Build question body and collect answers."""
1222
1237
  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"],
1238
+ pdi_answer,
1239
+ pti_answer,
1240
+ offset_answer,
1241
+ pd_entry_answer,
1242
+ pt_number_answer,
1243
+ pte_answer,
1244
+ is_valid_answer,
1245
+ pfn_answer,
1246
+ physical_answer,
1232
1247
  ]
1233
1248
 
1234
1249
  body = ca.Section()
1235
1250
 
1236
1251
  body.add_element(
1237
1252
  ca.Paragraph([
1238
- "Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
1239
- "This problem uses **two-level (hierarchical) paging**.",
1240
- "Remember, we typically have the MSB representing valid or invalid."
1253
+ 'Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.',
1254
+ 'This problem uses **two-level (hierarchical) paging**.',
1255
+ 'Remember, we typically have the MSB representing valid or invalid.'
1241
1256
  ])
1242
1257
  )
1243
1258
 
1244
1259
  # Create parameter info table using mixin (same format as Paging question)
1245
1260
  parameter_info = {
1246
- "Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
1247
- "# PDI bits": f"{self.num_bits_pdi}",
1248
- "# PTI bits": f"{self.num_bits_pti}",
1249
- "# Offset bits": f"{self.num_bits_offset}",
1250
- "# PFN bits": f"{self.num_bits_pfn}"
1261
+ 'Virtual Address': f"0b{context['virtual_address']:0{context['num_bits_vpn'] + context['num_bits_offset']}b}",
1262
+ '# PDI bits': f"{context['num_bits_pdi']}",
1263
+ '# PTI bits': f"{context['num_bits_pti']}",
1264
+ '# Offset bits': f"{context['num_bits_offset']}",
1265
+ '# PFN bits': f"{context['num_bits_pfn']}"
1251
1266
  }
1252
1267
 
1253
- body.add_element(self.create_info_table(parameter_info))
1268
+ body.add_element(cls.create_info_table(parameter_info))
1254
1269
 
1255
1270
  # Page Directory table
1256
1271
  pd_matrix = []
1257
- if min(self.page_directory.keys()) != 0:
1258
- pd_matrix.append(["...", "..."])
1272
+ if min(context['page_directory'].keys()) != 0:
1273
+ pd_matrix.append(['...', '...'])
1259
1274
 
1260
1275
  pd_matrix.extend([
1261
- [f"0b{pdi:0{self.num_bits_pdi}b}", f"0b{pd_val:0{self.num_bits_pfn + 1}b}"]
1262
- for pdi, pd_val in sorted(self.page_directory.items())
1276
+ [f"0b{pdi:0{context['num_bits_pdi']}b}", f"0b{pd_val:0{context['num_bits_pfn'] + 1}b}"]
1277
+ for pdi, pd_val in sorted(context['page_directory'].items())
1263
1278
  ])
1264
1279
 
1265
- if (max(self.page_directory.keys()) + 1) != 2 ** self.num_bits_pdi:
1266
- pd_matrix.append(["...", "..."])
1280
+ if (max(context['page_directory'].keys()) + 1) != 2 ** context['num_bits_pdi']:
1281
+ pd_matrix.append(['...', '...'])
1267
1282
 
1268
1283
  # Use a simple text paragraph - the bold will come from markdown conversion
1269
1284
  body.add_element(
1270
1285
  ca.Paragraph([
1271
- "**Page Directory:**"
1286
+ '**Page Directory:**'
1272
1287
  ])
1273
1288
  )
1274
1289
  body.add_element(
1275
1290
  ca.Table(
1276
- headers=["PDI", "PD Entry"],
1291
+ headers=['PDI', 'PD Entry'],
1277
1292
  data=pd_matrix
1278
1293
  )
1279
1294
  )
@@ -1281,49 +1296,43 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1281
1296
  # Page Tables - use TableGroup for side-by-side display
1282
1297
  table_group = ca.TableGroup()
1283
1298
 
1284
- for pt_num in sorted(self.page_tables.keys()):
1299
+ for pt_num in sorted(context['page_tables'].keys()):
1285
1300
  pt_matrix = []
1286
- pt_entries = self.page_tables[pt_num]
1301
+ pt_entries = context['page_tables'][pt_num]
1287
1302
 
1288
1303
  min_pti = min(pt_entries.keys())
1289
1304
  max_pti = max(pt_entries.keys())
1290
- max_possible_pti = 2 ** self.num_bits_pti - 1
1305
+ max_possible_pti = 2 ** context['num_bits_pti'] - 1
1291
1306
 
1292
1307
  # Smart leading ellipsis: only if there are 2+ hidden entries before
1293
1308
  # (if only 1 hidden, we should just show it)
1294
1309
  if min_pti > 1:
1295
- pt_matrix.append(["...", "..."])
1310
+ pt_matrix.append(['...', '...'])
1296
1311
  elif min_pti == 1:
1297
- # Show the 0th entry instead of "..."
1298
- pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1299
- if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1300
- pte_val = (2 ** self.num_bits_pfn) + pfn
1301
- else:
1302
- pte_val = pfn
1303
- pt_matrix.append([f"0b{0:0{self.num_bits_pti}b}", f"0b{pte_val:0{self.num_bits_pfn + 1}b}"])
1312
+ leading = context['pt_display_extras'][pt_num]['leading']
1313
+ if leading is not None:
1314
+ leading_pti, leading_pte = leading
1315
+ pt_matrix.append([f"0b{leading_pti:0{context['num_bits_pti']}b}", f"0b{leading_pte:0{context['num_bits_pfn'] + 1}b}"])
1304
1316
 
1305
1317
  # Add actual entries
1306
1318
  pt_matrix.extend([
1307
- [f"0b{pti:0{self.num_bits_pti}b}", f"0b{pte:0{self.num_bits_pfn + 1}b}"]
1319
+ [f"0b{pti:0{context['num_bits_pti']}b}", f"0b{pte:0{context['num_bits_pfn'] + 1}b}"]
1308
1320
  for pti, pte in sorted(pt_entries.items())
1309
1321
  ])
1310
1322
 
1311
1323
  # Smart trailing ellipsis: only if there are 2+ hidden entries after
1312
1324
  hidden_after = max_possible_pti - max_pti
1313
1325
  if hidden_after > 1:
1314
- pt_matrix.append(["...", "..."])
1326
+ pt_matrix.append(['...', '...'])
1315
1327
  elif hidden_after == 1:
1316
- # Show the last entry instead of "..."
1317
- pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1318
- if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1319
- pte_val = (2 ** self.num_bits_pfn) + pfn
1320
- else:
1321
- pte_val = pfn
1322
- pt_matrix.append([f"0b{max_possible_pti:0{self.num_bits_pti}b}", f"0b{pte_val:0{self.num_bits_pfn + 1}b}"])
1328
+ trailing = context['pt_display_extras'][pt_num]['trailing']
1329
+ if trailing is not None:
1330
+ trailing_pti, trailing_pte = trailing
1331
+ pt_matrix.append([f"0b{trailing_pti:0{context['num_bits_pti']}b}", f"0b{trailing_pte:0{context['num_bits_pfn'] + 1}b}"])
1323
1332
 
1324
1333
  table_group.add_table(
1325
- label=f"PTC 0b{pt_num:0{self.num_bits_pfn}b}:",
1326
- table=ca.Table(headers=["PTI", "PTE"], data=pt_matrix)
1334
+ label=f"PTC 0b{pt_num:0{context['num_bits_pfn']}b}:",
1335
+ table=ca.Table(headers=['PTI', 'PTE'], data=pt_matrix)
1327
1336
  )
1328
1337
 
1329
1338
  body.add_element(table_group)
@@ -1331,33 +1340,29 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1331
1340
  # Answer block
1332
1341
  body.add_element(
1333
1342
  ca.AnswerBlock([
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"],
1343
+ pdi_answer,
1344
+ pti_answer,
1345
+ offset_answer,
1346
+ pd_entry_answer,
1347
+ pt_number_answer,
1348
+ pte_answer,
1349
+ is_valid_answer,
1350
+ pfn_answer,
1351
+ physical_answer,
1343
1352
  ])
1344
1353
  )
1345
1354
 
1346
1355
  return body, answers
1347
1356
 
1348
- def get_body(self, *args, **kwargs) -> ca.Section:
1349
- """Build question body (backward compatible interface)."""
1350
- body, _ = self._get_body(*args, **kwargs)
1351
- return body
1352
-
1353
- def _get_explanation(self, *args, **kwargs):
1357
+ @classmethod
1358
+ def _build_explanation(cls, context):
1354
1359
  """Build question explanation."""
1355
1360
  explanation = ca.Section()
1356
1361
 
1357
1362
  explanation.add_element(
1358
1363
  ca.Paragraph([
1359
- "Two-level paging requires two lookups: first in the Page Directory, then in a Page Table.",
1360
- "The virtual address is split into three parts: PDI | PTI | Offset."
1364
+ 'Two-level paging requires two lookups: first in the Page Directory, then in a Page Table.',
1365
+ 'The virtual address is split into three parts: PDI | PTI | Offset.'
1361
1366
  ])
1362
1367
  )
1363
1368
 
@@ -1370,128 +1375,123 @@ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplates
1370
1375
  # Step 1: Extract PDI, PTI, Offset
1371
1376
  explanation.add_element(
1372
1377
  ca.Paragraph([
1373
- f"**Step 1: Extract components from Virtual Address**",
1374
- f"Virtual Address = PDI | PTI | Offset",
1375
- f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> = "
1376
- f"<tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt> | "
1377
- f"<tt>0b{self.pti:0{self.num_bits_pti}b}</tt> | "
1378
- f"<tt>0b{self.offset:0{self.num_bits_offset}b}</tt>"
1378
+ '**Step 1: Extract components from Virtual Address**',
1379
+ 'Virtual Address = PDI | PTI | Offset',
1380
+ f"<tt>0b{context['virtual_address']:0{context['num_bits_vpn'] + context['num_bits_offset']}b}</tt> = "
1381
+ f"<tt>0b{context['pdi']:0{context['num_bits_pdi']}b}</tt> | "
1382
+ f"<tt>0b{context['pti']:0{context['num_bits_pti']}b}</tt> | "
1383
+ f"<tt>0b{context['offset']:0{context['num_bits_offset']}b}</tt>"
1379
1384
  ])
1380
1385
  )
1381
1386
 
1382
1387
  # Step 2: Look up PD Entry
1383
1388
  explanation.add_element(
1384
1389
  ca.Paragraph([
1385
- f"**Step 2: Look up Page Directory Entry**",
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>"
1390
+ '**Step 2: Look up Page Directory Entry**',
1391
+ f"Using PDI = <tt>0b{context['pdi']:0{context['num_bits_pdi']}b}</tt>, we find PD Entry = <tt>0b{context['pd_entry']:0{context['num_bits_pfn'] + 1}b}</tt>"
1387
1392
  ])
1388
1393
  )
1389
1394
 
1390
1395
  # Step 3: Check PD validity
1391
- pd_valid_bit = self.pd_entry // (2 ** self.num_bits_pfn)
1396
+ pd_valid_bit = context['pd_entry'] // (2 ** context['num_bits_pfn'])
1392
1397
  explanation.add_element(
1393
1398
  ca.Paragraph([
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'}**."
1399
+ '**Step 3: Check Page Directory Entry validity**',
1400
+ f"The MSB (valid bit) is **{pd_valid_bit}**, so this PD Entry is **{'VALID' if context['pd_valid'] else 'INVALID'}**."
1396
1401
  ])
1397
1402
  )
1398
1403
 
1399
- if not self.pd_valid:
1404
+ if not context['pd_valid']:
1400
1405
  explanation.add_element(
1401
1406
  ca.Paragraph([
1402
- "Since the Page Directory Entry is invalid, the translation fails here.",
1403
- "We write **INVALID** for all remaining fields.",
1404
- "If it were valid, we would continue with the steps below.",
1405
- "<hr>"
1407
+ 'Since the Page Directory Entry is invalid, the translation fails here.',
1408
+ 'We write **INVALID** for all remaining fields.',
1409
+ 'If it were valid, we would continue with the steps below.',
1410
+ '<hr>'
1406
1411
  ])
1407
1412
  )
1408
1413
 
1409
1414
  # Step 4: Extract PT number (if PD valid)
1410
1415
  explanation.add_element(
1411
1416
  ca.Paragraph([
1412
- f"**Step 4: Extract Page Table Number**",
1413
- "We remove the valid bit from the PD Entry to get the Page Table Number:"
1417
+ '**Step 4: Extract Page Table Number**',
1418
+ 'We remove the valid bit from the PD Entry to get the Page Table Number:'
1414
1419
  ])
1415
1420
  )
1416
1421
 
1417
1422
  explanation.add_element(
1418
1423
  ca.Equation(
1419
- f"\\texttt{{{self.page_table_number:0{self.num_bits_pfn}b}}} = "
1420
- f"\\texttt{{0b{self.pd_entry:0{self.num_bits_pfn + 1}b}}} \\& "
1421
- f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
1424
+ f"\\texttt{{{context['page_table_number']:0{context['num_bits_pfn']}b}}} = "
1425
+ f"\\texttt{{0b{context['pd_entry']:0{context['num_bits_pfn'] + 1}b}}} \\& "
1426
+ f"\\texttt{{0b{(2 ** context['num_bits_pfn']) - 1:0{context['num_bits_pfn'] + 1}b}}}"
1422
1427
  )
1423
1428
  )
1424
1429
 
1425
- if self.pd_valid:
1430
+ if context['pd_valid']:
1426
1431
  explanation.add_element(
1427
1432
  ca.Paragraph([
1428
- f"This tells us to use **Page Table #{self.page_table_number}**."
1433
+ f"This tells us to use **Page Table #{context['page_table_number']}**."
1429
1434
  ])
1430
1435
  )
1431
1436
 
1432
1437
  # Step 5: Look up PTE
1433
1438
  explanation.add_element(
1434
1439
  ca.Paragraph([
1435
- f"**Step 5: Look up Page Table Entry**",
1436
- f"Using PTI = <tt>0b{self.pti:0{self.num_bits_pti}b}</tt> in Page Table #{self.page_table_number}, "
1437
- f"we find PTE = <tt>0b{self.pte:0{self.num_bits_pfn + 1}b}</tt>"
1440
+ '**Step 5: Look up Page Table Entry**',
1441
+ f"Using PTI = <tt>0b{context['pti']:0{context['num_bits_pti']}b}</tt> in Page Table #{context['page_table_number']}, "
1442
+ f"we find PTE = <tt>0b{context['pte']:0{context['num_bits_pfn'] + 1}b}</tt>"
1438
1443
  ])
1439
1444
  )
1440
1445
 
1441
1446
  # Step 6: Check PT validity
1442
- pt_valid_bit = self.pte // (2 ** self.num_bits_pfn)
1447
+ pt_valid_bit = context['pte'] // (2 ** context['num_bits_pfn'])
1443
1448
  explanation.add_element(
1444
1449
  ca.Paragraph([
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'}**."
1450
+ '**Step 6: Check Page Table Entry validity**',
1451
+ f"The MSB (valid bit) is **{pt_valid_bit}**, so this PTE is **{'VALID' if context['pt_valid'] else 'INVALID'}**."
1447
1452
  ])
1448
1453
  )
1449
1454
 
1450
- if not self.pt_valid:
1455
+ if not context['pt_valid']:
1451
1456
  explanation.add_element(
1452
1457
  ca.Paragraph([
1453
- "Since the Page Table Entry is invalid, the translation fails.",
1454
- "We write **INVALID** for PFN and Physical Address.",
1455
- "If it were valid, we would continue with the steps below.",
1456
- "<hr>"
1458
+ 'Since the Page Table Entry is invalid, the translation fails.',
1459
+ 'We write **INVALID** for PFN and Physical Address.',
1460
+ 'If it were valid, we would continue with the steps below.',
1461
+ '<hr>'
1457
1462
  ])
1458
1463
  )
1459
1464
 
1460
1465
  # Step 7: Extract PFN
1461
1466
  explanation.add_element(
1462
1467
  ca.Paragraph([
1463
- f"**Step 7: Extract PFN**",
1464
- "We remove the valid bit from the PTE to get the PFN:"
1468
+ '**Step 7: Extract PFN**',
1469
+ 'We remove the valid bit from the PTE to get the PFN:'
1465
1470
  ])
1466
1471
  )
1467
1472
 
1468
1473
  explanation.add_element(
1469
1474
  ca.Equation(
1470
- f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} = "
1471
- f"\\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} \\& "
1472
- f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
1475
+ f"\\texttt{{{context['pfn']:0{context['num_bits_pfn']}b}}} = "
1476
+ f"\\texttt{{0b{context['pte']:0{context['num_bits_pfn'] + 1}b}}} \\& "
1477
+ f"\\texttt{{0b{(2 ** context['num_bits_pfn']) - 1:0{context['num_bits_pfn'] + 1}b}}}"
1473
1478
  )
1474
1479
  )
1475
1480
 
1476
1481
  # Step 8: Construct physical address
1477
1482
  explanation.add_element(
1478
1483
  ca.Paragraph([
1479
- f"**Step 8: Construct Physical Address**",
1480
- "Physical Address = PFN | Offset"
1484
+ '**Step 8: Construct Physical Address**',
1485
+ 'Physical Address = PFN | Offset'
1481
1486
  ])
1482
1487
  )
1483
1488
 
1484
1489
  explanation.add_element(
1485
1490
  ca.Equation(
1486
- fr"{r'\mathbf{' if self.is_valid else ''}\mathtt{{0b{self.physical_address:0{self.num_bits_pfn + self.num_bits_offset}b}}}{r'}' if self.is_valid else ''} = "
1487
- f"\\mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}}} \\mid "
1488
- f"\\mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
1491
+ fr"{r'\mathbf{' if context['is_valid'] else ''}\mathtt{{0b{context['physical_address']:0{context['num_bits_pfn'] + context['num_bits_offset']}b}}}{r'}' if context['is_valid'] else ''} = "
1492
+ f"\mathtt{{0b{context['pfn']:0{context['num_bits_pfn']}b}}} \mid "
1493
+ f"\mathtt{{0b{context['offset']:0{context['num_bits_offset']}b}}}"
1489
1494
  )
1490
1495
  )
1491
1496
 
1492
1497
  return explanation, []
1493
-
1494
- def get_explanation(self, *args, **kwargs) -> ca.Section:
1495
- """Build question explanation (backward compatible interface)."""
1496
- explanation, _ = self._get_explanation(*args, **kwargs)
1497
- return explanation