QuizGenerator 0.1.0__py3-none-any.whl

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