QuizGenerator 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/__init__.py +27 -0
  3. QuizGenerator/__main__.py +7 -0
  4. QuizGenerator/canvas/__init__.py +13 -0
  5. QuizGenerator/canvas/canvas_interface.py +627 -0
  6. QuizGenerator/canvas/classes.py +235 -0
  7. QuizGenerator/constants.py +149 -0
  8. QuizGenerator/contentast.py +1955 -0
  9. QuizGenerator/generate.py +253 -0
  10. QuizGenerator/logging.yaml +55 -0
  11. QuizGenerator/misc.py +579 -0
  12. QuizGenerator/mixins.py +548 -0
  13. QuizGenerator/performance.py +202 -0
  14. QuizGenerator/premade_questions/__init__.py +0 -0
  15. QuizGenerator/premade_questions/basic.py +103 -0
  16. QuizGenerator/premade_questions/cst334/__init__.py +1 -0
  17. QuizGenerator/premade_questions/cst334/languages.py +391 -0
  18. QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
  19. QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
  20. QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
  21. QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
  22. QuizGenerator/premade_questions/cst334/process.py +648 -0
  23. QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  24. QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
  25. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
  26. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
  27. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
  28. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
  29. QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
  30. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
  31. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
  32. QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  33. QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
  34. QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
  35. QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
  36. QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
  37. QuizGenerator/premade_questions/cst463/models/text.py +203 -0
  38. QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
  39. QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
  40. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
  41. QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
  42. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
  43. QuizGenerator/qrcode_generator.py +293 -0
  44. QuizGenerator/question.py +715 -0
  45. QuizGenerator/quiz.py +467 -0
  46. QuizGenerator/regenerate.py +472 -0
  47. QuizGenerator/typst_utils.py +113 -0
  48. quizgenerator-0.4.2.dist-info/METADATA +265 -0
  49. quizgenerator-0.4.2.dist-info/RECORD +52 -0
  50. quizgenerator-0.4.2.dist-info/WHEEL +4 -0
  51. quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
  52. quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,1400 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+ import collections
6
+ import copy
7
+ import enum
8
+ import logging
9
+ import math
10
+ from typing import List, Optional
11
+
12
+ from QuizGenerator.contentast import ContentAST
13
+ from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
14
+ from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ class MemoryQuestion(Question, abc.ABC):
20
+ def __init__(self, *args, **kwargs):
21
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.MEMORY)
22
+ super().__init__(*args, **kwargs)
23
+
24
+
25
+ @QuestionRegistry.register("VirtualAddressParts")
26
+ class VirtualAddressParts(MemoryQuestion, TableQuestionMixin):
27
+ MAX_BITS = 64
28
+
29
+ class Target(enum.Enum):
30
+ VA_BITS = "# VA Bits"
31
+ VPN_BITS = "# VPN Bits"
32
+ OFFSET_BITS = "# Offset Bits"
33
+
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: 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)
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) -> ContentAST.Section:
56
+ # Create table data with one blank cell
57
+ table_data = [{}]
58
+ for target in list(self.Target):
59
+ if target == self.blank_kind:
60
+ # This cell should be an answer blank
61
+ table_data[0][target.value] = ContentAST.Answer(self.possible_answers[target], " bits")
62
+ else:
63
+ # This cell shows the value
64
+ table_data[0][target.value] = f"{self.possible_answers[target].display} bits"
65
+
66
+ table = self.create_fill_in_table(
67
+ headers=[t.value for t in list(self.Target)],
68
+ template_rows=table_data
69
+ )
70
+
71
+ body = ContentAST.Section()
72
+ body.add_element(
73
+ ContentAST.Paragraph(
74
+ [
75
+ "Given the information in the below table, please complete the table as appropriate."
76
+ ]
77
+ )
78
+ )
79
+ body.add_element(table)
80
+ return body
81
+
82
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
83
+ explanation = ContentAST.Section()
84
+
85
+ explanation.add_element(
86
+ ContentAST.Paragraph(
87
+ [
88
+ "Remember, when we are calculating the size of virtual address spaces, "
89
+ "the number of bits in the overall address space is equal to the number of bits in the VPN "
90
+ "plus the number of bits for the offset.",
91
+ "We don't waste any bits!"
92
+ ]
93
+ )
94
+ )
95
+
96
+ explanation.add_element(
97
+ ContentAST.Paragraph(
98
+ [
99
+ ContentAST.Text(f"{self.num_bits_va}", emphasis=(self.blank_kind == self.Target.VA_BITS)),
100
+ ContentAST.Text(" = "),
101
+ ContentAST.Text(f"{self.num_bits_vpn}", emphasis=(self.blank_kind == self.Target.VPN_BITS)),
102
+ ContentAST.Text(" + "),
103
+ ContentAST.Text(f"{self.num_bits_offset}", emphasis=(self.blank_kind == self.Target.OFFSET_BITS))
104
+ ]
105
+ )
106
+ )
107
+
108
+ return explanation
109
+
110
+
111
+ @QuestionRegistry.register()
112
+ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin, BodyTemplatesMixin):
113
+ class Kind(enum.Enum):
114
+ FIFO = enum.auto()
115
+ LRU = enum.auto()
116
+ BELADY = enum.auto()
117
+
118
+ def __str__(self):
119
+ return self.name
120
+
121
+ class Cache:
122
+ def __init__(self, kind: CachingQuestion.Kind, cache_size: int, all_requests: List[int] = None):
123
+ self.kind = kind
124
+ self.cache_size = cache_size
125
+ self.all_requests = all_requests
126
+
127
+ self.cache_state = []
128
+ self.last_used = collections.defaultdict(lambda: -math.inf)
129
+ self.frequency = collections.defaultdict(lambda: 0)
130
+
131
+ def query_cache(self, request, request_number):
132
+ was_hit = request in self.cache_state
133
+
134
+ evicted = None
135
+ if was_hit:
136
+ # hit!
137
+ pass
138
+ else:
139
+ # miss!
140
+ if len(self.cache_state) == self.cache_size:
141
+ # Then we are full and need to evict
142
+ evicted = self.cache_state[0]
143
+ self.cache_state = self.cache_state[1:]
144
+
145
+ # Add to cache
146
+ self.cache_state.append(request)
147
+
148
+ # update state variable
149
+ self.last_used[request] = request_number
150
+ self.frequency[request] += 1
151
+
152
+ # update cache state
153
+ if self.kind == CachingQuestion.Kind.FIFO:
154
+ pass
155
+ elif self.kind == CachingQuestion.Kind.LRU:
156
+ self.cache_state = sorted(
157
+ self.cache_state,
158
+ key=(lambda e: self.last_used[e]),
159
+ reverse=False
160
+ )
161
+ # elif self.kind == CachingQuestion.Kind.LFU:
162
+ # self.cache_state = sorted(
163
+ # self.cache_state,
164
+ # key=(lambda e: (self.frequency[e], e)),
165
+ # reverse=False
166
+ # )
167
+ elif self.kind == CachingQuestion.Kind.BELADY:
168
+ upcoming_requests = self.all_requests[request_number + 1:]
169
+ self.cache_state = sorted(
170
+ self.cache_state,
171
+ # key=(lambda e: (upcoming_requests.index(e), e) if e in upcoming_requests else (-math.inf, e)),
172
+ key=(lambda e: (upcoming_requests.index(e), -e) if e in upcoming_requests else (math.inf, -e)),
173
+ reverse=True
174
+ )
175
+
176
+ return (was_hit, evicted, self.cache_state)
177
+
178
+ def __init__(self, *args, **kwargs):
179
+ # Store parameters in kwargs for config_params BEFORE calling super().__init__()
180
+ kwargs['num_elements'] = kwargs.get("num_elements", 5)
181
+ kwargs['cache_size'] = kwargs.get("cache_size", 3)
182
+ kwargs['num_requests'] = kwargs.get("num_requests", 10)
183
+
184
+ # Register the regenerable choice using the mixin
185
+ policy_str = (kwargs.get("policy") or kwargs.get("algo"))
186
+ self.register_choice('policy', self.Kind, policy_str, kwargs)
187
+
188
+ super().__init__(*args, **kwargs)
189
+
190
+ self.num_elements = self.config_params.get("num_elements", 5)
191
+ self.cache_size = self.config_params.get("cache_size", 3)
192
+ self.num_requests = self.config_params.get("num_requests", 10)
193
+
194
+ self.hit_rate = 0. # placeholder
195
+
196
+ def refresh(self, previous: Optional[CachingQuestion] = None, *args, hard_refresh: bool = False, **kwargs):
197
+ # Call parent refresh which seeds RNG and calls is_interesting()
198
+ # Note: We ignore the parent's return value since we need to generate the workload first
199
+ super().refresh(*args, **kwargs)
200
+
201
+ # Use the mixin to get the cache policy (randomly selected or fixed)
202
+ self.cache_policy = self.get_choice('policy', self.Kind)
203
+
204
+ self.requests = (
205
+ list(range(self.cache_size)) # Prime the cache with the capacity misses
206
+ + self.rng.choices(
207
+ population=list(range(self.cache_size - 1)), k=1
208
+ ) # Add in one request to an earlier that will differentiate clearly between FIFO and LRU
209
+ + self.rng.choices(
210
+ population=list(range(self.cache_size, self.num_elements)), k=1
211
+ ) ## Add in the rest of the requests
212
+ + self.rng.choices(population=list(range(self.num_elements)), k=(self.num_requests - 2))
213
+ ## Add in the rest of the requests
214
+ )
215
+
216
+ self.cache = CachingQuestion.Cache(self.cache_policy, self.cache_size, self.requests)
217
+
218
+ self.request_results = {}
219
+ number_of_hits = 0
220
+ for (request_number, request) in enumerate(self.requests):
221
+ was_hit, evicted, cache_state = self.cache.query_cache(request, request_number)
222
+ if was_hit:
223
+ number_of_hits += 1
224
+ self.request_results[request_number] = {
225
+ "request": (f"[answer__request]", request),
226
+ "hit": (f"[answer__hit-{request_number}]", ('hit' if was_hit else 'miss')),
227
+ "evicted": (f"[answer__evicted-{request_number}]", ('-' if evicted is None else f"{evicted}")),
228
+ "cache_state": (f"[answer__cache_state-{request_number}]", ','.join(map(str, cache_state)))
229
+ }
230
+
231
+ self.answers.update(
232
+ {
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
+ ),
242
+ }
243
+ )
244
+
245
+ self.hit_rate = 100 * number_of_hits / (self.num_requests)
246
+ self.answers.update(
247
+ {
248
+ "answer__hit_rate": Answer.auto_float("answer__hit_rate", self.hit_rate)
249
+ }
250
+ )
251
+
252
+ # Return whether this workload is interesting
253
+ return self.is_interesting()
254
+
255
+ def get_body(self, **kwargs) -> ContentAST.Section:
256
+ # Create table data for cache simulation
257
+ table_rows = []
258
+ for request_number in sorted(self.request_results.keys()):
259
+ table_rows.append(
260
+ {
261
+ "Page Requested": f"{self.requests[request_number]}",
262
+ "Hit/Miss": f"answer__hit-{request_number}", # Answer key
263
+ "Evicted": f"answer__evicted-{request_number}", # Answer key
264
+ "Cache State": f"answer__cache_state-{request_number}" # Answer key
265
+ }
266
+ )
267
+
268
+ # Create table using mixin - automatically handles answer conversion
269
+ cache_table = self.create_answer_table(
270
+ headers=["Page Requested", "Hit/Miss", "Evicted", "Cache State"],
271
+ data_rows=table_rows,
272
+ answer_columns=["Hit/Miss", "Evicted", "Cache State"]
273
+ )
274
+
275
+ # 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
+
284
+ # Use mixin to create complete body
285
+ 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>. "
287
+ "Given the below series of requests please fill in the table. "
288
+ "For the hit/miss column, please write either \"hit\" or \"miss\". "
289
+ "For the eviction column, please write either the number of the evicted page or simply a dash (e.g. \"-\")."
290
+ )
291
+
292
+ instructions = ContentAST.OnlyHtml([
293
+ "For the cache state, please enter the cache contents in the order suggested in class, "
294
+ "which means separated by commas with spaces (e.g. \"1, 2, 3\") "
295
+ "and with the left-most being the next to be evicted. "
296
+ "In the case where there is a tie, order by increasing number."
297
+ ])
298
+
299
+ body = self.create_fill_in_table_body(intro_text, instructions, cache_table)
300
+ body.add_element(hit_rate_block)
301
+ return body
302
+
303
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
304
+ explanation = ContentAST.Section()
305
+
306
+ explanation.add_element(ContentAST.Paragraph(["The full caching table can be seen below."]))
307
+
308
+ explanation.add_element(
309
+ ContentAST.Table(
310
+ headers=["Page", "Hit/Miss", "Evicted", "Cache State"],
311
+ data=[
312
+ [
313
+ self.request_results[request]["request"][1],
314
+ self.request_results[request]["hit"][1],
315
+ f'{self.request_results[request]["evicted"][1]}',
316
+ f'{self.request_results[request]["cache_state"][1]}',
317
+ ]
318
+ for (request_number, request) in enumerate(sorted(self.request_results.keys()))
319
+ ]
320
+ )
321
+ )
322
+
323
+ explanation.add_element(
324
+ ContentAST.Paragraph(
325
+ [
326
+ "To calculate the hit rate we calculate the percentage of requests "
327
+ "that were cache hits out of the total number of requests. "
328
+ f"In this case we are counting only all but {self.cache_size} requests, "
329
+ f"since we are excluding capacity misses."
330
+ ]
331
+ )
332
+ )
333
+
334
+ return explanation
335
+
336
+ def is_interesting(self) -> bool:
337
+ # todo: interesting is more likely based on whether I can differentiate between it and another algo,
338
+ # so maybe rerun with a different approach but same requests?
339
+ return (self.hit_rate / 100.0) < 0.7
340
+
341
+
342
+ class MemoryAccessQuestion(MemoryQuestion, abc.ABC):
343
+ PROBABILITY_OF_VALID = .875
344
+
345
+
346
+ @QuestionRegistry.register()
347
+ class BaseAndBounds(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
348
+ MAX_BITS = 32
349
+ MIN_BOUNDS_BIT = 5
350
+ MAX_BOUNDS_BITS = 16
351
+
352
+ def refresh(self, rng_seed=None, *args, **kwargs):
353
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
354
+
355
+ max_bound_bits = kwargs.get("max_bound_bits")
356
+
357
+ bounds_bits = self.rng.randint(
358
+ self.MIN_BOUNDS_BIT,
359
+ self.MAX_BOUNDS_BITS
360
+ )
361
+ base_bits = self.MAX_BITS - bounds_bits
362
+
363
+ self.bounds = int(math.pow(2, bounds_bits))
364
+ self.base = self.rng.randint(1, int(math.pow(2, base_bits))) * self.bounds
365
+ self.virtual_address = self.rng.randint(1, int(self.bounds / self.PROBABILITY_OF_VALID))
366
+
367
+ if self.virtual_address < self.bounds:
368
+ self.answers["answer"] = Answer.binary_hex(
369
+ "answer__physical_address",
370
+ self.base + self.virtual_address,
371
+ length=math.ceil(math.log2(self.base + self.virtual_address))
372
+ )
373
+ else:
374
+ self.answers["answer"] = Answer.string("answer__physical_address", "INVALID")
375
+
376
+ def get_body(self) -> ContentAST.Section:
377
+ # Use mixin to create parameter table with answer
378
+ parameter_info = {
379
+ "Base": f"0x{self.base:X}",
380
+ "Bounds": f"0x{self.bounds:X}",
381
+ "Virtual Address": f"0x{self.virtual_address:X}"
382
+ }
383
+
384
+ table = self.create_parameter_answer_table(
385
+ parameter_info=parameter_info,
386
+ answer_label="Physical Address",
387
+ answer_key="answer",
388
+ transpose=True
389
+ )
390
+
391
+ return self.create_parameter_calculation_body(
392
+ intro_text=(
393
+ "Given the information in the below table, "
394
+ "please calcuate the physical address associated with the given virtual address. "
395
+ "If the virtual address is invalid please simply write ***INVALID***."
396
+ ),
397
+ parameter_table=table
398
+ )
399
+
400
+ def get_explanation(self) -> ContentAST.Section:
401
+ explanation = ContentAST.Section()
402
+
403
+ explanation.add_element(
404
+ ContentAST.Paragraph(
405
+ [
406
+ "There's two steps to figuring out base and bounds.",
407
+ "1. Are we within the bounds?\n",
408
+ "2. If so, add to our base.\n",
409
+ "",
410
+ ]
411
+ )
412
+ )
413
+
414
+ explanation.add_element(
415
+ ContentAST.Paragraph(
416
+ [
417
+ f"Step 1: 0x{self.virtual_address:X} < 0x{self.bounds:X} "
418
+ f"--> {'***VALID***' if (self.virtual_address < self.bounds) else 'INVALID'}"
419
+ ]
420
+ )
421
+ )
422
+
423
+ if self.virtual_address < self.bounds:
424
+ explanation.add_element(
425
+ ContentAST.Paragraph(
426
+ [
427
+ f"Step 2: Since the previous check passed, we calculate "
428
+ f"0x{self.base:X} + 0x{self.virtual_address:X} "
429
+ f"= ***0x{self.base + self.virtual_address:X}***.",
430
+ "If it had been invalid we would have simply written INVALID"
431
+ ]
432
+ )
433
+ )
434
+ else:
435
+ explanation.add_element(
436
+ ContentAST.Paragraph(
437
+ [
438
+ f"Step 2: Since the previous check failed, we simply write ***INVALID***.",
439
+ "***If*** it had been valid, we would have calculated "
440
+ f"0x{self.base:X} + 0x{self.virtual_address:X} "
441
+ f"= 0x{self.base + self.virtual_address:X}.",
442
+ ]
443
+ )
444
+ )
445
+
446
+ return explanation
447
+
448
+
449
+ @QuestionRegistry.register()
450
+ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
451
+ MAX_BITS = 20
452
+ MIN_VIRTUAL_BITS = 5
453
+ MAX_VIRTUAL_BITS = 10
454
+
455
+ def __within_bounds(self, segment, offset, bounds):
456
+ if segment == "unallocated":
457
+ return False
458
+ elif bounds < offset:
459
+ return False
460
+ else:
461
+ return True
462
+
463
+ def refresh(self, *args, **kwargs):
464
+ super().refresh(*args, **kwargs)
465
+
466
+ # Pick how big each of our address spaces will be
467
+ self.virtual_bits = self.rng.randint(self.MIN_VIRTUAL_BITS, self.MAX_VIRTUAL_BITS)
468
+ self.physical_bits = self.rng.randint(self.virtual_bits + 1, self.MAX_BITS)
469
+
470
+ # Start with blank base and bounds
471
+ self.base = {
472
+ "code": 0,
473
+ "heap": 0,
474
+ "stack": 0,
475
+ }
476
+ self.bounds = {
477
+ "code": 0,
478
+ "heap": 0,
479
+ "stack": 0,
480
+ }
481
+
482
+ min_bounds = 4
483
+ max_bounds = int(2 ** (self.virtual_bits - 2))
484
+
485
+ def segment_collision(base, bounds):
486
+ # lol, I think this is probably silly, but should work
487
+ return 0 != len(
488
+ set.intersection(
489
+ *[
490
+ set(range(base[segment], base[segment] + bounds[segment] + 1))
491
+ for segment in base.keys()
492
+ ]
493
+ )
494
+ )
495
+
496
+ self.base["unallocated"] = 0
497
+ self.bounds["unallocated"] = 0
498
+
499
+ # Make random placements and check to make sure they are not overlapping
500
+ while (segment_collision(self.base, self.bounds)):
501
+ for segment in self.base.keys():
502
+ self.bounds[segment] = self.rng.randint(min_bounds, max_bounds - 1)
503
+ self.base[segment] = self.rng.randint(0, (2 ** self.physical_bits - self.bounds[segment]))
504
+
505
+ # Pick a random segment for us to use
506
+ self.segment = self.rng.choice(list(self.base.keys()))
507
+ self.segment_bits = {
508
+ "code": 0,
509
+ "heap": 1,
510
+ "unallocated": 2,
511
+ "stack": 3
512
+ }[self.segment]
513
+
514
+ # Try to pick a random address within that range
515
+ try:
516
+ self.offset = self.rng.randint(
517
+ 1,
518
+ min(
519
+ [
520
+ max_bounds - 1,
521
+ int(self.bounds[self.segment] / self.PROBABILITY_OF_VALID)
522
+ ]
523
+ )
524
+ )
525
+ except KeyError:
526
+ # If we are in an unallocated section, we'll get a key error (I think)
527
+ self.offset = self.rng.randint(0, max_bounds - 1)
528
+
529
+ # Calculate a virtual address based on the segment and the offset
530
+ self.virtual_address = (
531
+ (self.segment_bits << (self.virtual_bits - 2))
532
+ + self.offset
533
+ )
534
+
535
+ # Calculate physical address based on offset
536
+ self.physical_address = self.base[self.segment] + self.offset
537
+
538
+ # Set answers based on whether it's in bounds or not
539
+ 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",
542
+ self.physical_address,
543
+ length=self.physical_bits
544
+ )
545
+ 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)
552
+
553
+ def get_body(self) -> ContentAST.Section:
554
+ body = ContentAST.Section()
555
+
556
+ body.add_element(
557
+ ContentAST.Paragraph(
558
+ [
559
+ f"Given a virtual address space of {self.virtual_bits}bits, "
560
+ f"and a physical address space of {self.physical_bits}bits, "
561
+ "what is the physical address associated with the virtual address "
562
+ f"0b{self.virtual_address:0{self.virtual_bits}b}?",
563
+ "If it is invalid simply type INVALID.",
564
+ "Note: assume that the stack grows in the same way as the code and the heap."
565
+ ]
566
+ )
567
+ )
568
+
569
+ # Create segment table using mixin
570
+ segment_rows = [
571
+ {"": "code", "base": f"0b{self.base['code']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['code']:0b}"},
572
+ {"": "heap", "base": f"0b{self.base['heap']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['heap']:0b}"},
573
+ {"": "stack", "base": f"0b{self.base['stack']:0{self.physical_bits}b}", "bounds": f"0b{self.bounds['stack']:0b}"}
574
+ ]
575
+
576
+ segment_table = self.create_answer_table(
577
+ headers=["", "base", "bounds"],
578
+ data_rows=segment_rows,
579
+ answer_columns=[] # No answer columns in this table
580
+ )
581
+
582
+ body.add_element(segment_table)
583
+
584
+ 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
+ )
597
+ )
598
+ return body
599
+
600
+ def get_explanation(self) -> ContentAST.Section:
601
+ explanation = ContentAST.Section()
602
+
603
+ explanation.add_element(
604
+ ContentAST.Paragraph(
605
+ [
606
+ "The core idea to keep in mind with segmentation is that you should always check ",
607
+ "the first two bits of the virtual address to see what segment it is in and then go from there."
608
+ "Keep in mind, "
609
+ "we also may need to include padding if our virtual address has a number of leading zeros left off!"
610
+ ]
611
+ )
612
+ )
613
+
614
+ explanation.add_element(
615
+ ContentAST.Paragraph(
616
+ [
617
+ f"In this problem our virtual address, "
618
+ f"converted to binary and including padding, is 0b{self.virtual_address:0{self.virtual_bits}b}.",
619
+ f"From this we know that our segment bits are 0b{self.segment_bits:02b}, "
620
+ f"meaning that we are in the ***{self.segment}*** segment.",
621
+ ""
622
+ ]
623
+ )
624
+ )
625
+
626
+ if self.segment == "unallocated":
627
+ explanation.add_element(
628
+ ContentAST.Paragraph(
629
+ [
630
+ "Since this is the unallocated segment there are no possible valid translations, so we enter ***INVALID***."
631
+ ]
632
+ )
633
+ )
634
+ else:
635
+ explanation.add_element(
636
+ ContentAST.Paragraph(
637
+ [
638
+ f"Since we are in the {self.segment} segment, "
639
+ f"we see from our table that our bounds are {self.bounds[self.segment]}. "
640
+ f"Remember that our check for our {self.segment} segment is: ",
641
+ f"`if (offset >= bounds({self.segment})) : INVALID`",
642
+ "which becomes"
643
+ f"`if ({self.offset:0b} > {self.bounds[self.segment]:0b}) : INVALID`"
644
+ ]
645
+ )
646
+ )
647
+
648
+ if not self.__within_bounds(self.segment, self.offset, self.bounds[self.segment]):
649
+ # then we are outside of bounds
650
+ explanation.add_element(
651
+ ContentAST.Paragraph(
652
+ [
653
+ "We can therefore see that we are outside of bounds so we should put ***INVALID***.",
654
+ "If we <i>were</i> requesting a valid memory location we could use the below steps to do so."
655
+ "<hr>"
656
+ ]
657
+ )
658
+ )
659
+ else:
660
+ explanation.add_element(
661
+ ContentAST.Paragraph(
662
+ [
663
+ "We are therefore in bounds so we can calculate our physical address, as we do below."
664
+ ]
665
+ )
666
+ )
667
+
668
+ explanation.add_element(
669
+ ContentAST.Paragraph(
670
+ [
671
+ "To find the physical address we use the formula:",
672
+ "<code>physical_address = base(segment) + offset</code>",
673
+ "which becomes",
674
+ f"<code>physical_address = {self.base[self.segment]:0b} + {self.offset:0b}</code>.",
675
+ ""
676
+ ]
677
+ )
678
+ )
679
+
680
+ explanation.add_element(
681
+ ContentAST.Paragraph(
682
+ [
683
+ "Lining this up for ease we can do this calculation as:"
684
+ ]
685
+ )
686
+ )
687
+ explanation.add_element(
688
+ ContentAST.Code(
689
+ f" 0b{self.base[self.segment]:0{self.physical_bits}b}\n"
690
+ f"<u>+ 0b{self.offset:0{self.physical_bits}b}</u>\n"
691
+ f" 0b{self.physical_address:0{self.physical_bits}b}\n"
692
+ )
693
+ )
694
+
695
+ return explanation
696
+
697
+
698
+ @QuestionRegistry.register()
699
+ class Paging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
700
+ MIN_OFFSET_BITS = 3
701
+ MIN_VPN_BITS = 3
702
+ MIN_PFN_BITS = 3
703
+
704
+ MAX_OFFSET_BITS = 8
705
+ MAX_VPN_BITS = 8
706
+ MAX_PFN_BITS = 16
707
+
708
+ def refresh(self, rng_seed=None, *args, **kwargs):
709
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
710
+
711
+ self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
712
+ self.num_bits_vpn = self.rng.randint(self.MIN_VPN_BITS, self.MAX_VPN_BITS)
713
+ self.num_bits_pfn = self.rng.randint(max([self.MIN_PFN_BITS, self.num_bits_vpn]), self.MAX_PFN_BITS)
714
+
715
+ self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
716
+
717
+ # Calculate these two
718
+ self.offset = self.virtual_address % (2 ** (self.num_bits_offset))
719
+ self.vpn = self.virtual_address // (2 ** (self.num_bits_offset))
720
+
721
+ # Generate this randomly
722
+ self.pfn = self.rng.randint(0, 2 ** (self.num_bits_pfn))
723
+
724
+ # Calculate this
725
+ self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
726
+
727
+ if self.rng.choices([True, False], weights=[(self.PROBABILITY_OF_VALID), (1 - self.PROBABILITY_OF_VALID)], k=1)[0]:
728
+ self.is_valid = True
729
+ # Set our actual entry to be in the table and valid
730
+ self.pte = self.pfn + (2 ** (self.num_bits_pfn))
731
+ # 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)
732
+ # self.pfn_var = VariableHex("PFN", self.pfn, num_bits=self.num_pfn_bits, default_presentation=VariableHex.PRESENTATION.BINARY)
733
+ else:
734
+ self.is_valid = False
735
+ # Leave it as invalid
736
+ self.pte = self.pfn
737
+ # self.physical_address_var = Variable("Physical Address", "INVALID")
738
+ # self.pfn_var = Variable("PFN", "INVALID")
739
+
740
+ # self.pte_var = VariableHex("PTE", self.pte, num_bits=(self.num_pfn_bits+1), default_presentation=VariableHex.PRESENTATION.BINARY)
741
+
742
+ # Generate page table (moved from get_body to ensure deterministic generation)
743
+ table_size = self.rng.randint(5, 8)
744
+
745
+ lowest_possible_bottom = max([0, self.vpn - table_size])
746
+ highest_possible_bottom = min([2 ** self.num_bits_vpn - table_size, self.vpn])
747
+
748
+ table_bottom = self.rng.randint(lowest_possible_bottom, highest_possible_bottom)
749
+ table_top = table_bottom + table_size
750
+
751
+ self.page_table = {}
752
+ self.page_table[self.vpn] = self.pte
753
+
754
+ # Fill in the rest of the table
755
+ for vpn in range(table_bottom, table_top):
756
+ if vpn == self.vpn: continue
757
+ pte = self.page_table[self.vpn]
758
+ while pte in self.page_table.values():
759
+ pte = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
760
+ if self.rng.choices([True, False], weights=[(1 - self.PROBABILITY_OF_VALID), self.PROBABILITY_OF_VALID], k=1)[0]:
761
+ # Randomly set it to be valid
762
+ pte += (2 ** (self.num_bits_pfn))
763
+ # Once we have a unique random entry, put it into the Page Table
764
+ self.page_table[vpn] = pte
765
+
766
+ self.answers.update(
767
+ {
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)),
771
+ }
772
+ )
773
+
774
+ if self.is_valid:
775
+ self.answers.update(
776
+ {
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)
781
+ ),
782
+ }
783
+ )
784
+ else:
785
+ self.answers.update(
786
+ {
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"),
790
+ }
791
+ )
792
+
793
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
794
+ body = ContentAST.Section()
795
+
796
+ body.add_element(
797
+ ContentAST.Paragraph(
798
+ [
799
+ "Given the below information please calculate the equivalent physical address of the given virtual address, filling out all steps along the way.",
800
+ "Remember, we typically have the MSB representing valid or invalid."
801
+ ]
802
+ )
803
+ )
804
+
805
+ # Create parameter info table using mixin
806
+ parameter_info = {
807
+ "Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
808
+ "# VPN bits": f"{self.num_bits_vpn}",
809
+ "# PFN bits": f"{self.num_bits_pfn}"
810
+ }
811
+
812
+ body.add_element(self.create_info_table(parameter_info))
813
+
814
+ # Use the page table generated in refresh() for deterministic output
815
+ # Add in ellipses before and after page table entries, if appropriate
816
+ value_matrix = []
817
+
818
+ if min(self.page_table.keys()) != 0:
819
+ value_matrix.append(["...", "..."])
820
+
821
+ value_matrix.extend(
822
+ [
823
+ [f"0b{vpn:0{self.num_bits_vpn}b}", f"0b{pte:0{(self.num_bits_pfn + 1)}b}"]
824
+ for vpn, pte in sorted(self.page_table.items())
825
+ ]
826
+ )
827
+
828
+ if (max(self.page_table.keys()) + 1) != 2 ** self.num_bits_vpn:
829
+ value_matrix.append(["...", "..."])
830
+
831
+ body.add_element(
832
+ ContentAST.Table(
833
+ headers=["VPN", "PTE"],
834
+ data=value_matrix
835
+ )
836
+ )
837
+
838
+ 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
+ )
850
+ )
851
+
852
+ return body
853
+
854
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
855
+ explanation = ContentAST.Section()
856
+
857
+ explanation.add_element(
858
+ ContentAST.Paragraph(
859
+ [
860
+ "The core idea of Paging is we want to break the virtual address into the VPN and the offset. "
861
+ "From here, we get the Page Table Entry corresponding to the VPN, and check the validity of the entry. "
862
+ "If it is valid, we clear the metadata and attach the PFN to the offset and have our physical address.",
863
+ ]
864
+ )
865
+ )
866
+
867
+ explanation.add_element(
868
+ ContentAST.Paragraph(
869
+ [
870
+ "Don't forget to pad with the appropriate number of 0s (the appropriate number is the number of bits)!",
871
+ ]
872
+ )
873
+ )
874
+
875
+ explanation.add_element(
876
+ ContentAST.Paragraph(
877
+ [
878
+ f"Virtual Address = VPN | offset",
879
+ f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> "
880
+ f"= <tt>0b{self.vpn:0{self.num_bits_vpn}b}</tt> | <tt>0b{self.offset:0{self.num_bits_offset}b}</tt>",
881
+ ]
882
+ )
883
+ )
884
+
885
+ explanation.add_element(
886
+ ContentAST.Paragraph(
887
+ [
888
+ "We next use our VPN to index into our page table and find the corresponding entry."
889
+ f"Our Page Table Entry is ",
890
+ f"<tt>0b{self.pte:0{(self.num_bits_pfn + 1)}b}</tt>"
891
+ f"which we found by looking for our VPN in the page table.",
892
+ ]
893
+ )
894
+ )
895
+
896
+ if self.is_valid:
897
+ explanation.add_element(
898
+ ContentAST.Paragraph(
899
+ [
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>"
901
+ ]
902
+ )
903
+ )
904
+ else:
905
+ explanation.add_element(
906
+ ContentAST.Paragraph(
907
+ [
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>.",
909
+ "Therefore, we just write \"INVALID\" as our answer.",
910
+ "If it were valid we would complete the below steps.",
911
+ "<hr>"
912
+ ]
913
+ )
914
+ )
915
+
916
+ explanation.add_element(
917
+ ContentAST.Paragraph(
918
+ [
919
+ "Next, we convert our PTE to our PFN by removing our metadata. "
920
+ "In this case we're just removing the leading bit. We can do this by applying a binary mask.",
921
+ f"PFN = PTE & mask",
922
+ f"which is,"
923
+ ]
924
+ )
925
+ )
926
+ explanation.add_element(
927
+ ContentAST.Equation(
928
+ f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} "
929
+ f"= \\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} "
930
+ f"\\& \\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
931
+ )
932
+ )
933
+
934
+ explanation.add_elements(
935
+ [
936
+ ContentAST.Paragraph(
937
+ [
938
+ "We then add combine our PFN and offset, "
939
+ "Physical Address = PFN | offset",
940
+ ]
941
+ ),
942
+ ContentAST.Equation(
943
+ 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}}}"
944
+ )
945
+ ]
946
+ )
947
+
948
+ explanation.add_elements(
949
+ [
950
+ ContentAST.Paragraph(["Note: Strictly speaking, this calculation is:", ]),
951
+ ContentAST.Equation(
952
+ 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}}}"
953
+ ),
954
+ ContentAST.Paragraph(["But that's a lot of extra 0s, so I'm splitting them up for succinctness"])
955
+ ]
956
+ )
957
+
958
+ return explanation
959
+
960
+
961
+ @QuestionRegistry.register()
962
+ class HierarchicalPaging(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin):
963
+ MIN_OFFSET_BITS = 3
964
+ MIN_PDI_BITS = 2
965
+ MIN_PTI_BITS = 2
966
+ MIN_PFN_BITS = 4
967
+
968
+ MAX_OFFSET_BITS = 5
969
+ MAX_PDI_BITS = 3
970
+ MAX_PTI_BITS = 3
971
+ MAX_PFN_BITS = 6
972
+
973
+ def refresh(self, rng_seed=None, *args, **kwargs):
974
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
975
+
976
+ # Set up bit counts
977
+ self.num_bits_offset = self.rng.randint(self.MIN_OFFSET_BITS, self.MAX_OFFSET_BITS)
978
+ self.num_bits_pdi = self.rng.randint(self.MIN_PDI_BITS, self.MAX_PDI_BITS)
979
+ self.num_bits_pti = self.rng.randint(self.MIN_PTI_BITS, self.MAX_PTI_BITS)
980
+ self.num_bits_pfn = self.rng.randint(self.MIN_PFN_BITS, self.MAX_PFN_BITS)
981
+
982
+ # Total VPN bits = PDI + PTI
983
+ self.num_bits_vpn = self.num_bits_pdi + self.num_bits_pti
984
+
985
+ # Generate a random virtual address
986
+ self.virtual_address = self.rng.randint(1, 2 ** (self.num_bits_vpn + self.num_bits_offset))
987
+
988
+ # Extract components from virtual address
989
+ self.offset = self.virtual_address % (2 ** self.num_bits_offset)
990
+ vpn = self.virtual_address // (2 ** self.num_bits_offset)
991
+
992
+ self.pti = vpn % (2 ** self.num_bits_pti)
993
+ self.pdi = vpn // (2 ** self.num_bits_pti)
994
+
995
+ # Generate PFN randomly
996
+ self.pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
997
+
998
+ # Calculate physical address
999
+ self.physical_address = self.pfn * (2 ** self.num_bits_offset) + self.offset
1000
+
1001
+ # Determine validity at both levels
1002
+ # PD entry can be valid or invalid
1003
+ self.pd_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
1004
+
1005
+ # PT entry only matters if PD is valid
1006
+ if self.pd_valid:
1007
+ self.pt_valid = self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]
1008
+ else:
1009
+ self.pt_valid = False # Doesn't matter, won't be checked
1010
+
1011
+ # Generate a page table number (PTBR - Page Table Base Register value in the PD entry)
1012
+ # This represents which page table to use
1013
+ self.page_table_number = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1014
+
1015
+ # Create PD entry: valid bit + page table number
1016
+ if self.pd_valid:
1017
+ self.pd_entry = (2 ** self.num_bits_pfn) + self.page_table_number
1018
+ else:
1019
+ self.pd_entry = self.page_table_number # Invalid, no valid bit set
1020
+
1021
+ # Create PT entry: valid bit + PFN
1022
+ if self.pt_valid:
1023
+ self.pte = (2 ** self.num_bits_pfn) + self.pfn
1024
+ else:
1025
+ self.pte = self.pfn # Invalid, no valid bit set
1026
+
1027
+ # Overall validity requires both levels to be valid
1028
+ self.is_valid = self.pd_valid and self.pt_valid
1029
+
1030
+ # Build page directory - show 3-4 entries
1031
+ pd_size = self.rng.randint(3, 4)
1032
+ lowest_pd_bottom = max([0, self.pdi - pd_size])
1033
+ highest_pd_bottom = min([2 ** self.num_bits_pdi - pd_size, self.pdi])
1034
+ pd_bottom = self.rng.randint(lowest_pd_bottom, highest_pd_bottom)
1035
+ pd_top = pd_bottom + pd_size
1036
+
1037
+ self.page_directory = {}
1038
+ self.page_directory[self.pdi] = self.pd_entry
1039
+
1040
+ # Fill in other PD entries
1041
+ for pdi in range(pd_bottom, pd_top):
1042
+ if pdi == self.pdi:
1043
+ continue
1044
+ # Generate random PD entry
1045
+ pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1046
+ while pt_num == self.page_table_number: # Make sure it's different
1047
+ pt_num = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1048
+
1049
+ # Randomly valid or invalid
1050
+ if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1051
+ pd_val = (2 ** self.num_bits_pfn) + pt_num
1052
+ else:
1053
+ pd_val = pt_num
1054
+
1055
+ self.page_directory[pdi] = pd_val
1056
+
1057
+ # Build 2-3 page tables to show
1058
+ # Always include the one we need, plus 1-2 others
1059
+ num_page_tables_to_show = self.rng.randint(2, 3)
1060
+
1061
+ # Get unique page table numbers from the PD entries (extract PT numbers from valid entries)
1062
+ shown_pt_numbers = set()
1063
+ for pdi, pd_val in self.page_directory.items():
1064
+ pt_num = pd_val % (2 ** self.num_bits_pfn) # Extract PT number (remove valid bit)
1065
+ shown_pt_numbers.add(pt_num)
1066
+
1067
+ # Ensure our required page table is included
1068
+ shown_pt_numbers.add(self.page_table_number)
1069
+
1070
+ # Limit to requested number, but ALWAYS keep the required page table
1071
+ shown_pt_numbers_list = list(shown_pt_numbers)
1072
+ if self.page_table_number in shown_pt_numbers_list:
1073
+ # Remove it temporarily so we can add it back first
1074
+ shown_pt_numbers_list.remove(self.page_table_number)
1075
+ # Start with required page table, then add others up to the limit
1076
+ shown_pt_numbers = [self.page_table_number] + shown_pt_numbers_list[:num_page_tables_to_show - 1]
1077
+
1078
+ # Build each page table
1079
+ self.page_tables = {} # Dict mapping PT number -> dict of PTI -> PTE
1080
+
1081
+ # Use consistent size for all page tables for cleaner presentation
1082
+ pt_size = self.rng.randint(2, 4)
1083
+
1084
+ # Determine the PTI range that all tables will use (based on target PTI)
1085
+ # This ensures all tables show the same PTI values for consistency
1086
+ lowest_pt_bottom = max([0, self.pti - pt_size + 1])
1087
+ highest_pt_bottom = min([2 ** self.num_bits_pti - pt_size, self.pti])
1088
+ pt_bottom = self.rng.randint(lowest_pt_bottom, highest_pt_bottom)
1089
+ pt_top = pt_bottom + pt_size
1090
+
1091
+ # Generate all page tables using the SAME PTI range
1092
+ for pt_num in shown_pt_numbers:
1093
+ self.page_tables[pt_num] = {}
1094
+
1095
+ for pti in range(pt_bottom, pt_top):
1096
+ if pt_num == self.page_table_number and pti == self.pti:
1097
+ # Use the actual answer for the target page table entry
1098
+ self.page_tables[pt_num][pti] = self.pte
1099
+ else:
1100
+ # Generate random PTE for all other entries
1101
+ pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1102
+ if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1103
+ pte_val = (2 ** self.num_bits_pfn) + pfn
1104
+ else:
1105
+ pte_val = pfn
1106
+
1107
+ self.page_tables[pt_num][pti] = pte_val
1108
+
1109
+ # Set up answers
1110
+ 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"),
1116
+ })
1117
+
1118
+ # PTE answer: if PD is valid, accept the actual PTE value from the table
1119
+ # (regardless of whether that PTE is valid or invalid)
1120
+ if self.pd_valid:
1121
+ self.answers.update({
1122
+ "answer__pte": Answer.binary_hex("answer__pte", self.pte, length=(self.num_bits_pfn + 1)),
1123
+ })
1124
+ else:
1125
+ # If PD is invalid, student can't look up the page table
1126
+ # Accept both "INVALID" (for consistency) and "N/A" (for accuracy)
1127
+ self.answers.update({
1128
+ "answer__pte": Answer.string("answer__pte", ["INVALID", "N/A"]),
1129
+ })
1130
+
1131
+ # Validity, PFN, and Physical Address depend on BOTH levels being valid
1132
+ if self.pd_valid and self.pt_valid:
1133
+ 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)
1138
+ ),
1139
+ })
1140
+ else:
1141
+ 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"),
1145
+ })
1146
+
1147
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
1148
+ body = ContentAST.Section()
1149
+
1150
+ body.add_element(
1151
+ ContentAST.Paragraph([
1152
+ "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>.",
1154
+ "Remember, we typically have the MSB representing valid or invalid."
1155
+ ])
1156
+ )
1157
+
1158
+ # Create parameter info table using mixin (same format as Paging question)
1159
+ parameter_info = {
1160
+ "Virtual Address": f"0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}",
1161
+ "# PDI bits": f"{self.num_bits_pdi}",
1162
+ "# PTI bits": f"{self.num_bits_pti}",
1163
+ "# Offset bits": f"{self.num_bits_offset}",
1164
+ "# PFN bits": f"{self.num_bits_pfn}"
1165
+ }
1166
+
1167
+ body.add_element(self.create_info_table(parameter_info))
1168
+
1169
+ # Page Directory table
1170
+ pd_matrix = []
1171
+ if min(self.page_directory.keys()) != 0:
1172
+ pd_matrix.append(["...", "..."])
1173
+
1174
+ pd_matrix.extend([
1175
+ [f"0b{pdi:0{self.num_bits_pdi}b}", f"0b{pd_val:0{self.num_bits_pfn + 1}b}"]
1176
+ for pdi, pd_val in sorted(self.page_directory.items())
1177
+ ])
1178
+
1179
+ if (max(self.page_directory.keys()) + 1) != 2 ** self.num_bits_pdi:
1180
+ pd_matrix.append(["...", "..."])
1181
+
1182
+ # Use a simple text paragraph - the bold will come from markdown conversion
1183
+ body.add_element(
1184
+ ContentAST.Paragraph([
1185
+ "**Page Directory:**"
1186
+ ])
1187
+ )
1188
+ body.add_element(
1189
+ ContentAST.Table(
1190
+ headers=["PDI", "PD Entry"],
1191
+ data=pd_matrix
1192
+ )
1193
+ )
1194
+
1195
+ # Page Tables - use TableGroup for side-by-side display
1196
+ table_group = ContentAST.TableGroup()
1197
+
1198
+ for pt_num in sorted(self.page_tables.keys()):
1199
+ pt_matrix = []
1200
+ pt_entries = self.page_tables[pt_num]
1201
+
1202
+ min_pti = min(pt_entries.keys())
1203
+ max_pti = max(pt_entries.keys())
1204
+ max_possible_pti = 2 ** self.num_bits_pti - 1
1205
+
1206
+ # Smart leading ellipsis: only if there are 2+ hidden entries before
1207
+ # (if only 1 hidden, we should just show it)
1208
+ if min_pti > 1:
1209
+ pt_matrix.append(["...", "..."])
1210
+ elif min_pti == 1:
1211
+ # Show the 0th entry instead of "..."
1212
+ pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1213
+ if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1214
+ pte_val = (2 ** self.num_bits_pfn) + pfn
1215
+ else:
1216
+ pte_val = pfn
1217
+ pt_matrix.append([f"0b{0:0{self.num_bits_pti}b}", f"0b{pte_val:0{self.num_bits_pfn + 1}b}"])
1218
+
1219
+ # Add actual entries
1220
+ pt_matrix.extend([
1221
+ [f"0b{pti:0{self.num_bits_pti}b}", f"0b{pte:0{self.num_bits_pfn + 1}b}"]
1222
+ for pti, pte in sorted(pt_entries.items())
1223
+ ])
1224
+
1225
+ # Smart trailing ellipsis: only if there are 2+ hidden entries after
1226
+ hidden_after = max_possible_pti - max_pti
1227
+ if hidden_after > 1:
1228
+ pt_matrix.append(["...", "..."])
1229
+ elif hidden_after == 1:
1230
+ # Show the last entry instead of "..."
1231
+ pfn = self.rng.randint(0, 2 ** self.num_bits_pfn - 1)
1232
+ if self.rng.choices([True, False], weights=[self.PROBABILITY_OF_VALID, 1 - self.PROBABILITY_OF_VALID], k=1)[0]:
1233
+ pte_val = (2 ** self.num_bits_pfn) + pfn
1234
+ else:
1235
+ pte_val = pfn
1236
+ 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}"])
1237
+
1238
+ table_group.add_table(
1239
+ label=f"PTC 0b{pt_num:0{self.num_bits_pfn}b}:",
1240
+ table=ContentAST.Table(headers=["PTI", "PTE"], data=pt_matrix)
1241
+ )
1242
+
1243
+ body.add_element(table_group)
1244
+
1245
+ # Answer block
1246
+ body.add_element(
1247
+ 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"),
1257
+ ])
1258
+ )
1259
+
1260
+ return body
1261
+
1262
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
1263
+ explanation = ContentAST.Section()
1264
+
1265
+ explanation.add_element(
1266
+ ContentAST.Paragraph([
1267
+ "Two-level paging requires two lookups: first in the Page Directory, then in a Page Table.",
1268
+ "The virtual address is split into three parts: PDI | PTI | Offset."
1269
+ ])
1270
+ )
1271
+
1272
+ explanation.add_element(
1273
+ ContentAST.Paragraph([
1274
+ "Don't forget to pad with the appropriate number of 0s!"
1275
+ ])
1276
+ )
1277
+
1278
+ # Step 1: Extract PDI, PTI, Offset
1279
+ explanation.add_element(
1280
+ ContentAST.Paragraph([
1281
+ f"<b>Step 1: Extract components from Virtual Address</b>",
1282
+ f"Virtual Address = PDI | PTI | Offset",
1283
+ f"<tt>0b{self.virtual_address:0{self.num_bits_vpn + self.num_bits_offset}b}</tt> = "
1284
+ f"<tt>0b{self.pdi:0{self.num_bits_pdi}b}</tt> | "
1285
+ f"<tt>0b{self.pti:0{self.num_bits_pti}b}</tt> | "
1286
+ f"<tt>0b{self.offset:0{self.num_bits_offset}b}</tt>"
1287
+ ])
1288
+ )
1289
+
1290
+ # Step 2: Look up PD Entry
1291
+ explanation.add_element(
1292
+ ContentAST.Paragraph([
1293
+ f"<b>Step 2: Look up Page Directory Entry</b>",
1294
+ 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
+ ])
1296
+ )
1297
+
1298
+ # Step 3: Check PD validity
1299
+ pd_valid_bit = self.pd_entry // (2 ** self.num_bits_pfn)
1300
+ explanation.add_element(
1301
+ 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>."
1304
+ ])
1305
+ )
1306
+
1307
+ if not self.pd_valid:
1308
+ explanation.add_element(
1309
+ ContentAST.Paragraph([
1310
+ "Since the Page Directory Entry is invalid, the translation fails here.",
1311
+ "We write <b>INVALID</b> for all remaining fields.",
1312
+ "If it were valid, we would continue with the steps below.",
1313
+ "<hr>"
1314
+ ])
1315
+ )
1316
+
1317
+ # Step 4: Extract PT number (if PD valid)
1318
+ explanation.add_element(
1319
+ ContentAST.Paragraph([
1320
+ f"<b>Step 4: Extract Page Table Number</b>",
1321
+ "We remove the valid bit from the PD Entry to get the Page Table Number:"
1322
+ ])
1323
+ )
1324
+
1325
+ explanation.add_element(
1326
+ ContentAST.Equation(
1327
+ f"\\texttt{{{self.page_table_number:0{self.num_bits_pfn}b}}} = "
1328
+ f"\\texttt{{0b{self.pd_entry:0{self.num_bits_pfn + 1}b}}} \\& "
1329
+ f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
1330
+ )
1331
+ )
1332
+
1333
+ if self.pd_valid:
1334
+ explanation.add_element(
1335
+ ContentAST.Paragraph([
1336
+ f"This tells us to use <b>Page Table #{self.page_table_number}</b>."
1337
+ ])
1338
+ )
1339
+
1340
+ # Step 5: Look up PTE
1341
+ explanation.add_element(
1342
+ ContentAST.Paragraph([
1343
+ f"<b>Step 5: Look up Page Table Entry</b>",
1344
+ f"Using PTI = <tt>0b{self.pti:0{self.num_bits_pti}b}</tt> in Page Table #{self.page_table_number}, "
1345
+ f"we find PTE = <tt>0b{self.pte:0{self.num_bits_pfn + 1}b}</tt>"
1346
+ ])
1347
+ )
1348
+
1349
+ # Step 6: Check PT validity
1350
+ pt_valid_bit = self.pte // (2 ** self.num_bits_pfn)
1351
+ explanation.add_element(
1352
+ 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>."
1355
+ ])
1356
+ )
1357
+
1358
+ if not self.pt_valid:
1359
+ explanation.add_element(
1360
+ ContentAST.Paragraph([
1361
+ "Since the Page Table Entry is invalid, the translation fails.",
1362
+ "We write <b>INVALID</b> for PFN and Physical Address.",
1363
+ "If it were valid, we would continue with the steps below.",
1364
+ "<hr>"
1365
+ ])
1366
+ )
1367
+
1368
+ # Step 7: Extract PFN
1369
+ explanation.add_element(
1370
+ ContentAST.Paragraph([
1371
+ f"<b>Step 7: Extract PFN</b>",
1372
+ "We remove the valid bit from the PTE to get the PFN:"
1373
+ ])
1374
+ )
1375
+
1376
+ explanation.add_element(
1377
+ ContentAST.Equation(
1378
+ f"\\texttt{{{self.pfn:0{self.num_bits_pfn}b}}} = "
1379
+ f"\\texttt{{0b{self.pte:0{self.num_bits_pfn + 1}b}}} \\& "
1380
+ f"\\texttt{{0b{(2 ** self.num_bits_pfn) - 1:0{self.num_bits_pfn + 1}b}}}"
1381
+ )
1382
+ )
1383
+
1384
+ # Step 8: Construct physical address
1385
+ explanation.add_element(
1386
+ ContentAST.Paragraph([
1387
+ f"<b>Step 8: Construct Physical Address</b>",
1388
+ "Physical Address = PFN | Offset"
1389
+ ])
1390
+ )
1391
+
1392
+ explanation.add_element(
1393
+ ContentAST.Equation(
1394
+ 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 ''} = "
1395
+ f"\\mathtt{{0b{self.pfn:0{self.num_bits_pfn}b}}} \\mid "
1396
+ f"\\mathtt{{0b{self.offset:0{self.num_bits_offset}b}}}"
1397
+ )
1398
+ )
1399
+
1400
+ return explanation