QuizGenerator 0.4.4__py3-none-any.whl → 0.5.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 (31) hide show
  1. QuizGenerator/contentast.py +949 -80
  2. QuizGenerator/generate.py +44 -7
  3. QuizGenerator/misc.py +4 -554
  4. QuizGenerator/mixins.py +47 -25
  5. QuizGenerator/premade_questions/cst334/languages.py +139 -125
  6. QuizGenerator/premade_questions/cst334/math_questions.py +78 -66
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +258 -144
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +71 -33
  9. QuizGenerator/premade_questions/cst334/process.py +51 -20
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +32 -6
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +59 -34
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +27 -8
  13. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +53 -32
  14. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +228 -88
  15. QuizGenerator/premade_questions/cst463/models/attention.py +26 -10
  16. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -19
  17. QuizGenerator/premade_questions/cst463/models/rnns.py +25 -12
  18. QuizGenerator/premade_questions/cst463/models/text.py +26 -11
  19. QuizGenerator/premade_questions/cst463/models/weight_counting.py +36 -22
  20. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +89 -109
  21. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +117 -51
  22. QuizGenerator/question.py +110 -15
  23. QuizGenerator/quiz.py +74 -23
  24. QuizGenerator/regenerate.py +98 -29
  25. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.0.dist-info}/METADATA +1 -1
  26. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.0.dist-info}/RECORD +29 -31
  27. QuizGenerator/README.md +0 -5
  28. QuizGenerator/logging.yaml +0 -55
  29. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.0.dist-info}/WHEEL +0 -0
  30. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.0.dist-info}/entry_points.txt +0 -0
  31. {quizgenerator-0.4.4.dist-info → quizgenerator-0.5.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/mixins.py CHANGED
@@ -68,19 +68,20 @@ class TableQuestionMixin:
68
68
  """
69
69
  answer_columns = answer_columns or []
70
70
 
71
- def format_cell(row_data: Dict, column: str) -> Union[str, ContentAST.Answer]:
71
+ def format_cell(row_data: Dict, column: str) -> Union[str, Answer]:
72
72
  """Format a cell based on whether it should be an answer or plain data"""
73
73
  value = row_data.get(column, "")
74
-
74
+
75
75
  # If this column should contain answers and the value is an Answer object
76
+ # Answer extends ContentAST.Leaf, so it can be used directly
76
77
  if column in answer_columns and isinstance(value, Answer):
77
- return ContentAST.Answer(value)
78
+ return value
78
79
  # If this column should contain answers but we have the answer key
79
80
  elif column in answer_columns and isinstance(value, str) and hasattr(self, 'answers'):
80
81
  answer_obj = self.answers.get(value)
81
82
  if answer_obj:
82
- return ContentAST.Answer(answer_obj)
83
-
83
+ return answer_obj
84
+
84
85
  # Otherwise return as plain data
85
86
  return str(value)
86
87
 
@@ -119,9 +120,9 @@ class TableQuestionMixin:
119
120
  # Build data with parameters plus answer row
120
121
  data = [[key, str(value)] for key, value in parameter_info.items()]
121
122
 
122
- # Add answer row
123
+ # Add answer row - Answer extends ContentAST.Leaf so it can be used directly
123
124
  if hasattr(self, 'answers') and answer_key in self.answers:
124
- data.append([answer_label, ContentAST.Answer(self.answers[answer_key])])
125
+ data.append([answer_label, self.answers[answer_key]])
125
126
  else:
126
127
  data.append([answer_label, f"[{answer_key}]"]) # Fallback
127
128
 
@@ -149,16 +150,17 @@ class TableQuestionMixin:
149
150
  ContentAST.Table with multiple answer blanks
150
151
  """
151
152
 
152
- def process_cell_value(value: Any) -> Union[str, ContentAST.Answer]:
153
+ def process_cell_value(value: Any) -> Union[str, Answer]:
153
154
  """Convert cell values to appropriate display format"""
154
- # If it's already an Answer object, wrap it
155
+ # If it's already an Answer object, use it directly
156
+ # Answer extends ContentAST.Leaf so it can be used in the AST
155
157
  if isinstance(value, Answer):
156
- return ContentAST.Answer(value)
158
+ return value
157
159
  # If it's a string that looks like an answer key, try to resolve it
158
160
  elif isinstance(value, str) and value.startswith("answer__") and hasattr(self, 'answers'):
159
161
  answer_obj = self.answers.get(value)
160
162
  if answer_obj:
161
- return ContentAST.Answer(answer_obj)
163
+ return answer_obj
162
164
  # Otherwise return as-is
163
165
  return str(value)
164
166
 
@@ -493,37 +495,52 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
493
495
  subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
494
496
  return subparts
495
497
 
496
- def get_body(self):
498
+ def _get_body(self):
499
+ """Build question body and collect answers."""
497
500
  body = ContentAST.Section()
498
-
501
+ answers = []
502
+
499
503
  body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
500
-
504
+
501
505
  if self.is_multipart():
502
506
  # Use multipart formatting with repeated problem parts
503
507
  subpart_data = self.generate_subquestion_data()
504
508
  repeated_part = self.create_repeated_problem_part(subpart_data)
505
509
  body.add_element(repeated_part)
510
+ # Collect answers from self.answers dict
511
+ answers = list(self.answers.values())
506
512
  else:
507
513
  # Single equation display
508
514
  equation_latex = self.format_single_equation(self.operand_a, self.operand_b)
509
515
  body.add_element(ContentAST.Equation(f"{equation_latex} = ", inline=False))
510
-
516
+
511
517
  # Canvas-only answer fields (hidden from PDF)
512
- self._add_single_question_answers(body)
513
-
518
+ single_answers = self._add_single_question_answers(body)
519
+ if single_answers:
520
+ answers.extend(single_answers)
521
+
522
+ return body, answers
523
+
524
+ def get_body(self):
525
+ """Build question body (backward compatible interface)."""
526
+ body, _ = self._get_body()
514
527
  return body
515
-
528
+
516
529
  def _add_single_question_answers(self, body):
517
- """Add Canvas-only answer fields for single questions. Subclasses can override."""
530
+ """Add Canvas-only answer fields for single questions. Subclasses can override.
531
+
532
+ Returns:
533
+ List of Answer objects that were added to the body.
534
+ """
518
535
  # Default implementation - subclasses should override for specific answer formats
519
- pass
520
-
521
- def get_explanation(self):
536
+ return []
537
+
538
+ def _get_explanation(self):
522
539
  """Default explanation structure. Subclasses should override for specific explanations."""
523
540
  explanation = ContentAST.Section()
524
-
541
+
525
542
  explanation.add_element(ContentAST.Paragraph([self.get_explanation_intro()]))
526
-
543
+
527
544
  if self.is_multipart():
528
545
  # Handle multipart explanations
529
546
  for i, data in enumerate(self.subquestion_data):
@@ -532,7 +549,12 @@ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
532
549
  else:
533
550
  # Single part explanation
534
551
  explanation.add_element(self.create_single_explanation())
535
-
552
+
553
+ return explanation, []
554
+
555
+ def get_explanation(self):
556
+ """Build question explanation (backward compatible interface)."""
557
+ explanation, _ = self._get_explanation()
536
558
  return explanation
537
559
 
538
560
  def get_explanation_intro(self):
@@ -155,115 +155,115 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
155
155
 
156
156
  super().__init__(*args, **kwargs)
157
157
 
158
- if grammar_str_good is not None and grammar_str_bad is not None:
158
+ # Store whether grammars are fixed (provided) or should be randomized
159
+ self.fixed_grammars = grammar_str_good is not None and grammar_str_bad is not None
160
+ if self.fixed_grammars:
159
161
  self.grammar_str_good = grammar_str_good
160
162
  self.grammar_str_bad = grammar_str_bad
161
163
  self.include_spaces = kwargs.get("include_spaces", False)
162
164
  self.MAX_LENGTH = kwargs.get("max_length", 30)
163
- else:
164
- which_grammar = self.rng.choice(range(4))
165
-
166
- if which_grammar == 0:
167
- # todo: make a few different kinds of grammars that could be picked
168
- self.grammar_str_good = """
169
- <expression> ::= <term> | <expression> + <term> | <expression> - <term>
170
- <term> ::= <factor> | <term> * <factor> | <term> / <factor>
171
- <factor> ::= <number>
172
- <number> ::= <digit> | <number> <digit>
173
- <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
174
- """
175
- # Adding in a plus to number
176
- self.grammar_str_bad = """
177
- <expression> ::= <term> | <expression> + <term> | <expression> - <term>
178
- <term> ::= <factor> | <term> * <factor> | <term> / <factor>
179
- <factor> ::= <number>
180
- <number> ::= <digit> + | <digit> <number>
181
- <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
182
- """
183
- self.include_spaces = False
184
- self.MAX_LENGTH = 30
185
- elif which_grammar == 1:
186
- self.grammar_str_good = """
187
- <sentence> ::= <subject> <verb> <object>
188
- <subject> ::= The cat | A dog | The bird | A child | <adjective> <animal>
189
- <animal> ::= cat | dog | bird | child
190
- <adjective> ::= happy | sad | angry | playful
191
- <verb> ::= chases | sees | hates | loves
192
- <object> ::= the ball | the toy | the tree | <adjective> <object>
193
- """
194
- self.grammar_str_bad = """
195
- <sentence> ::= <subject> <verb> <object>
196
- <subject> ::= The human | The dog | A bird | Some child | A <adjective> <animal>
197
- <animal> ::= cat | dog | bird | child
198
- <adjective> ::= happy | sad | angry | playful
199
- <verb> ::= chases | sees | hates | loves
200
- <object> ::= the ball | the toy | the tree | <adjective> <object>
201
- """
202
- self.include_spaces = True
203
- self.MAX_LENGTH = 100
204
- elif which_grammar == 2:
205
- self.grammar_str_good = """
206
- <poem> ::= <line> | <line> <poem>
207
- <line> ::= <subject> <verb> <object> <modifier>
208
- <subject> ::= whispers | shadows | dreams | echoes | <compound-subject>
209
- <compound-subject> ::= <subject> and <subject>
210
- <verb> ::= dance | dissolve | shimmer | collapse | <compound-verb>
211
- <compound-verb> ::= <verb> then <verb>
212
- <object> ::= beneath | between | inside | around | <compound-object>
213
- <compound-object> ::= <object> through <object>
214
- <modifier> ::= silently | violently | mysteriously | endlessly | <recursive-modifier>
215
- <recursive-modifier> ::= <modifier> and <modifier>
216
- """
217
- self.grammar_str_bad = """
218
- <bad-poem> ::= <almost-valid-line> | <bad-poem> <bad-poem>
219
- <almost-valid-line> ::= <tricky-subject> <tricky-verb> <tricky-object> <tricky-modifier>
220
- <tricky-subject> ::= whispers | shadows and and | <duplicate-subject>
221
- <duplicate-subject> ::= whispers whispers
222
- <tricky-verb> ::= dance | <incorrect-verb-placement> | <verb-verb>
223
- <incorrect-verb-placement> ::= dance dance
224
- <verb-verb> ::= dance whispers
225
- <tricky-object> ::= beneath | <object-verb-swap> | <duplicate-object>
226
- <object-verb-swap> ::= dance beneath
227
- <duplicate-object> ::= beneath beneath
228
- <tricky-modifier> ::= silently | <modifier-subject-swap> | <duplicate-modifier>
229
- <modifier-subject-swap> ::= whispers silently
230
- <duplicate-modifier> ::= silently silently
231
- """
232
- self.include_spaces = True
233
- self.MAX_LENGTH = 100
234
- elif which_grammar == 3:
235
- self.grammar_str_good = """
236
- <A> ::= a <B> a |
237
- <B> ::= b <C> b |
238
- <C> ::= c <A> c |
239
- """
240
- self.grammar_str_bad = """
241
- <A> ::= a <B> c
242
- <B> ::= b <C> a |
243
- <C> ::= c <A> b |
244
- """
245
- self.include_spaces = False
246
- self.MAX_LENGTH = 100
247
-
248
- self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
249
- self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
250
-
251
- self.num_answer_options = kwargs.get("num_answer_options", 4)
252
- self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
253
-
254
- def refresh(self, *args, **kwargs):
255
- super().refresh(*args, **kwargs)
256
-
257
- self.answers = {}
258
-
165
+ self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
166
+ self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
167
+
259
168
  self.num_answer_options = kwargs.get("num_answer_options", 4)
260
169
  self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
261
-
262
- self.refresh()
170
+
171
+ def _select_random_grammar(self):
172
+ """Select and set a random grammar. Called from refresh() to ensure each PDF gets different grammar."""
173
+ which_grammar = self.rng.choice(range(4))
174
+
175
+ if which_grammar == 0:
176
+ # todo: make a few different kinds of grammars that could be picked
177
+ self.grammar_str_good = """
178
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
179
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
180
+ <factor> ::= <number>
181
+ <number> ::= <digit> | <number> <digit>
182
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
183
+ """
184
+ # Adding in a plus to number
185
+ self.grammar_str_bad = """
186
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
187
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
188
+ <factor> ::= <number>
189
+ <number> ::= <digit> + | <digit> <number>
190
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
191
+ """
192
+ self.include_spaces = False
193
+ self.MAX_LENGTH = 30
194
+ elif which_grammar == 1:
195
+ self.grammar_str_good = """
196
+ <sentence> ::= <subject> <verb> <object>
197
+ <subject> ::= The cat | A dog | The bird | A child | <adjective> <animal>
198
+ <animal> ::= cat | dog | bird | child
199
+ <adjective> ::= happy | sad | angry | playful
200
+ <verb> ::= chases | sees | hates | loves
201
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
202
+ """
203
+ self.grammar_str_bad = """
204
+ <sentence> ::= <subject> <verb> <object>
205
+ <subject> ::= The human | The dog | A bird | Some child | A <adjective> <animal>
206
+ <animal> ::= cat | dog | bird | child
207
+ <adjective> ::= happy | sad | angry | playful
208
+ <verb> ::= chases | sees | hates | loves
209
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
210
+ """
211
+ self.include_spaces = True
212
+ self.MAX_LENGTH = 100
213
+ elif which_grammar == 2:
214
+ self.grammar_str_good = """
215
+ <poem> ::= <line> | <line> <poem>
216
+ <line> ::= <subject> <verb> <object> <modifier>
217
+ <subject> ::= whispers | shadows | dreams | echoes | <compound-subject>
218
+ <compound-subject> ::= <subject> and <subject>
219
+ <verb> ::= dance | dissolve | shimmer | collapse | <compound-verb>
220
+ <compound-verb> ::= <verb> then <verb>
221
+ <object> ::= beneath | between | inside | around | <compound-object>
222
+ <compound-object> ::= <object> through <object>
223
+ <modifier> ::= silently | violently | mysteriously | endlessly | <recursive-modifier>
224
+ <recursive-modifier> ::= <modifier> and <modifier>
225
+ """
226
+ self.grammar_str_bad = """
227
+ <bad-poem> ::= <almost-valid-line> | <bad-poem> <bad-poem>
228
+ <almost-valid-line> ::= <tricky-subject> <tricky-verb> <tricky-object> <tricky-modifier>
229
+ <tricky-subject> ::= whispers | shadows and and | <duplicate-subject>
230
+ <duplicate-subject> ::= whispers whispers
231
+ <tricky-verb> ::= dance | <incorrect-verb-placement> | <verb-verb>
232
+ <incorrect-verb-placement> ::= dance dance
233
+ <verb-verb> ::= dance whispers
234
+ <tricky-object> ::= beneath | <object-verb-swap> | <duplicate-object>
235
+ <object-verb-swap> ::= dance beneath
236
+ <duplicate-object> ::= beneath beneath
237
+ <tricky-modifier> ::= silently | <modifier-subject-swap> | <duplicate-modifier>
238
+ <modifier-subject-swap> ::= whispers silently
239
+ <duplicate-modifier> ::= silently silently
240
+ """
241
+ self.include_spaces = True
242
+ self.MAX_LENGTH = 100
243
+ elif which_grammar == 3:
244
+ self.grammar_str_good = """
245
+ <A> ::= a <B> a |
246
+ <B> ::= b <C> b |
247
+ <C> ::= c <A> c |
248
+ """
249
+ self.grammar_str_bad = """
250
+ <A> ::= a <B> c
251
+ <B> ::= b <C> a |
252
+ <C> ::= c <A> b |
253
+ """
254
+ self.include_spaces = False
255
+ self.MAX_LENGTH = 100
256
+
257
+ self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
258
+ self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
263
259
 
264
260
  def refresh(self, *args, **kwargs):
265
261
  super().refresh(*args, **kwargs)
266
-
262
+
263
+ # Re-select random grammar for each refresh if not using fixed grammars
264
+ if not self.fixed_grammars:
265
+ self._select_random_grammar()
266
+
267
267
  self.answers = {}
268
268
 
269
269
  self.answers.update(
@@ -337,46 +337,55 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
337
337
  )
338
338
 
339
339
 
340
- def get_body(self, *args, **kwargs) -> ContentAST.Section:
340
+ def _get_body(self, *args, **kwargs):
341
+ """Build question body and collect answers."""
342
+ answers = list(self.answers.values())
343
+
341
344
  body = ContentAST.Section()
342
-
343
- body.add_elements([
344
- ContentAST.Paragraph([
345
- ContentAST.OnlyHtml([
346
- ContentAST.Text("Given the following grammar, which of the below strings are part of the language?")
347
- ]),
348
- ContentAST.OnlyLatex([
349
- ContentAST.Text(
350
- "Given the following grammar "
351
- "please circle any provided strings that are part of the language (or indicate clearly if there are none), "
352
- "and on each blank line provide generate a new, unique string that is part of the language."
353
- )
345
+
346
+ body.add_element(
347
+ ContentAST.OnlyHtml([
348
+ ContentAST.Paragraph([
349
+ "Given the following grammar, which of the below strings are part of the language?"
354
350
  ])
355
351
  ])
356
- ])
357
-
358
- body.add_element(
359
- ContentAST.Code(self.grammar_good.get_grammar_string())
360
352
  )
361
-
362
- # Add in some answers as latex-only options to be circled
363
353
  body.add_element(
364
354
  ContentAST.OnlyLatex([
365
- ContentAST.Text(f"- `{str(answer)}`")
366
- for answer in self.featured_answers
355
+ ContentAST.Paragraph([
356
+ "Given the following grammar "
357
+ "please circle any provided strings that are part of the language (or indicate clearly if there are none), "
358
+ "and on each blank line provide generate a new, unique string that is part of the language."
359
+ ])
367
360
  ])
368
361
  )
369
-
362
+
363
+ body.add_element(
364
+ ContentAST.Code(self.grammar_good.get_grammar_string())
365
+ )
366
+
367
+ # Add in some answers as latex-only options to be circled
368
+ latex_list = ContentAST.OnlyLatex([])
369
+ for answer in self.featured_answers:
370
+ latex_list.add_element(ContentAST.Paragraph([f"- `{str(answer)}`"]))
371
+ body.add_element(latex_list)
372
+
370
373
  # For Latex-only, ask students to generate some more.
371
374
  body.add_element(
372
375
  ContentAST.OnlyLatex([
373
- ContentAST.AnswerBlock([ContentAST.Answer(Answer.string(f"blank_line_{i}", f"blank_line_{i}"), label=f"blank_line_{i}") for i in range(self.num_answer_blanks)])
376
+ ContentAST.AnswerBlock([Answer.string(f"blank_line_{i}", "", label="") for i in range(self.num_answer_blanks)])
374
377
  ])
375
378
  )
376
-
379
+
380
+ return body, answers
381
+
382
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
383
+ """Build question body (backward compatible interface)."""
384
+ body, _ = self._get_body(*args, **kwargs)
377
385
  return body
378
386
 
379
- def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
387
+ def _get_explanation(self, *args, **kwargs):
388
+ """Build question explanation."""
380
389
  explanation = ContentAST.Section()
381
390
  explanation.add_element(
382
391
  ContentAST.Paragraph([
@@ -384,6 +393,11 @@ class ValidStringsInLanguageQuestion(LanguageQuestion):
384
393
  "Unfortunately, there isn't space here to demonstrate the derivation so please work through them on your own!"
385
394
  ])
386
395
  )
396
+ return explanation, []
397
+
398
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
399
+ """Build question explanation (backward compatible interface)."""
400
+ explanation, _ = self._get_explanation(*args, **kwargs)
387
401
  return explanation
388
402
 
389
403
  def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
@@ -31,11 +31,16 @@ class BitsAndBytes(MathQuestion):
31
31
  self.num_bytes = int(math.pow(2, self.num_bits))
32
32
 
33
33
  if self.from_binary:
34
- self.answers = {"answer" : Answer.integer("num_bytes", self.num_bytes)}
34
+ self.answers = {"answer": Answer.integer("num_bytes", self.num_bytes,
35
+ label="Address space size", unit="Bytes")}
35
36
  else:
36
- self.answers = {"answer" : Answer.integer("num_bits", self.num_bits)}
37
+ self.answers = {"answer": Answer.integer("num_bits", self.num_bits,
38
+ label="Number of bits in address", unit="bits")}
37
39
 
38
- def get_body(self, **kwargs) -> ContentAST.Section:
40
+ def _get_body(self, **kwargs):
41
+ """Build question body and collect answers."""
42
+ answers = [self.answers['answer']]
43
+
39
44
  body = ContentAST.Section()
40
45
  body.add_element(
41
46
  ContentAST.Paragraph([
@@ -45,31 +50,17 @@ class BitsAndBytes(MathQuestion):
45
50
  f"{'do we need to address our memory' if not self.from_binary else 'of memory can be addressed'}?"
46
51
  ])
47
52
  )
48
-
49
- if self.from_binary:
50
- body.add_element(
51
- ContentAST.AnswerBlock(
52
- ContentAST.Answer(
53
- answer=self.answers['answer'],
54
- label="Address space size",
55
- unit="Bytes"
56
- ),
57
- )
58
- )
59
- else:
60
- body.add_element(
61
- ContentAST.AnswerBlock(
62
- ContentAST.Answer(
63
- answer=self.answers['answer'],
64
- label="Number of bits in address",
65
- unit="bits"
66
- ),
67
- )
68
- )
69
-
53
+
54
+ body.add_element(ContentAST.AnswerBlock(self.answers['answer']))
55
+
56
+ return body, answers
57
+
58
+ def get_body(self, **kwargs) -> ContentAST.Section:
59
+ """Build question body (backward compatible interface)."""
60
+ body, _ = self._get_body(**kwargs)
70
61
  return body
71
-
72
- def get_explanation(self, **kwargs) -> ContentAST.Section:
62
+
63
+ def _get_explanation(self, **kwargs):
73
64
  explanation = ContentAST.Section()
74
65
 
75
66
  explanation.add_element(
@@ -94,7 +85,12 @@ class BitsAndBytes(MathQuestion):
94
85
  explanation.add_element(
95
86
  ContentAST.Equation(f"log_{{2}}({self.num_bytes} \\text{{bytes}}) = \\textbf{{{self.num_bits}}}\\text{{bits}}")
96
87
  )
97
-
88
+
89
+ return explanation, []
90
+
91
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
92
+ """Build question explanation (backward compatible interface)."""
93
+ explanation, _ = self._get_explanation(**kwargs)
98
94
  return explanation
99
95
 
100
96
 
@@ -115,13 +111,18 @@ class HexAndBinary(MathQuestion):
115
111
  self.binary_val = f"0b{self.value:0{4*self.number_of_hexits}b}"
116
112
 
117
113
  if self.from_binary:
118
- self.answers['answer'] = Answer.string("hex_val", self.hex_val)
114
+ self.answers['answer'] = Answer.string("hex_val", self.hex_val,
115
+ label="Value in hex")
119
116
  else:
120
- self.answers['answer'] = Answer.string("binary_val", self.binary_val)
117
+ self.answers['answer'] = Answer.string("binary_val", self.binary_val,
118
+ label="Value in binary")
121
119
 
122
- def get_body(self, **kwargs) -> ContentAST.Section:
120
+ def _get_body(self, **kwargs):
121
+ """Build question body and collect answers."""
122
+ answers = [self.answers['answer']]
123
+
123
124
  body = ContentAST.Section()
124
-
125
+
125
126
  body.add_element(
126
127
  ContentAST.Paragraph([
127
128
  f"Given the number {self.hex_val if not self.from_binary else self.binary_val} "
@@ -129,19 +130,17 @@ class HexAndBinary(MathQuestion):
129
130
  "Please include base indicator all padding zeros as appropriate (e.g. 0x01 should be 0b00000001)",
130
131
  ])
131
132
  )
132
-
133
- body.add_element(
134
- ContentAST.AnswerBlock([
135
- ContentAST.Answer(
136
- answer = self.answers['answer'],
137
- label=f"Value in {'hex' if self.from_binary else 'binary'}: ",
138
- )
139
- ])
140
- )
141
-
133
+
134
+ body.add_element(ContentAST.AnswerBlock(self.answers['answer']))
135
+
136
+ return body, answers
137
+
138
+ def get_body(self, **kwargs) -> ContentAST.Section:
139
+ """Build question body (backward compatible interface)."""
140
+ body, _ = self._get_body(**kwargs)
142
141
  return body
143
-
144
- def get_explanation(self, **kwargs) -> ContentAST.Section:
142
+
143
+ def _get_explanation(self, **kwargs):
145
144
  explanation = ContentAST.Section()
146
145
 
147
146
  paragraph = ContentAST.Paragraph([
@@ -188,10 +187,15 @@ class HexAndBinary(MathQuestion):
188
187
  f"Which gives us our binary value of: 0b{binary_str}"
189
188
  ])
190
189
  )
191
-
190
+
191
+ return explanation, []
192
+
193
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
194
+ """Build question explanation (backward compatible interface)."""
195
+ explanation, _ = self._get_explanation(**kwargs)
192
196
  return explanation
193
-
194
-
197
+
198
+
195
199
  @QuestionRegistry.register()
196
200
  class AverageMemoryAccessTime(MathQuestion):
197
201
 
@@ -219,7 +223,8 @@ class AverageMemoryAccessTime(MathQuestion):
219
223
  self.amat = self.hit_rate * self.hit_latency + (1 - self.hit_rate) * self.miss_latency
220
224
 
221
225
  self.answers = {
222
- "amat": Answer.float_value("answer__amat", self.amat)
226
+ "amat": Answer.float_value("answer__amat", self.amat,
227
+ label="Average Memory Access Time", unit="cycles")
223
228
  }
224
229
 
225
230
  # Finally, do the self.rngizing of the question, to avoid these being non-deterministic
@@ -228,9 +233,12 @@ class AverageMemoryAccessTime(MathQuestion):
228
233
  # At this point, everything in the question should be set.
229
234
  pass
230
235
 
231
- def get_body(self, **kwargs) -> ContentAST.Section:
236
+ def _get_body(self, **kwargs):
237
+ """Build question body and collect answers."""
238
+ answers = [self.answers["amat"]]
239
+
232
240
  body = ContentAST.Section()
233
-
241
+
234
242
  # Add in background information
235
243
  body.add_element(
236
244
  ContentAST.Paragraph([
@@ -245,32 +253,31 @@ class AverageMemoryAccessTime(MathQuestion):
245
253
  ["Hit Latency", f"{self.hit_latency} cycles"],
246
254
  ["Miss Latency", f"{self.miss_latency} cycles"]
247
255
  ]
248
-
256
+
249
257
  # Add in either miss rate or hit rate -- we only need one of them
250
258
  if self.show_miss_rate:
251
259
  table_data.append(["Miss Rate", f"{100 * (1 - self.hit_rate): 0.2f}%"])
252
260
  else:
253
261
  table_data.append(["Hit Rate", f"{100 * self.hit_rate: 0.2f}%"])
254
-
262
+
255
263
  body.add_element(
256
264
  ContentAST.Table(
257
265
  data=table_data
258
266
  )
259
267
  )
260
-
261
- body.add_element(
262
- ContentAST.AnswerBlock([
263
- ContentAST.Answer(
264
- answer=self.answers["amat"],
265
- label="Average Memory Access Time",
266
- unit="cycles"
267
- )
268
- ])
269
- )
270
-
268
+
269
+ body.add_element(ContentAST.LineBreak())
270
+
271
+ body.add_element(ContentAST.AnswerBlock(self.answers["amat"]))
272
+
273
+ return body, answers
274
+
275
+ def get_body(self, **kwargs) -> ContentAST.Section:
276
+ """Build question body (backward compatible interface)."""
277
+ body, _ = self._get_body(**kwargs)
271
278
  return body
272
-
273
- def get_explanation(self, **kwargs) -> ContentAST.Section:
279
+
280
+ def _get_explanation(self, **kwargs):
274
281
  explanation = ContentAST.Section()
275
282
 
276
283
  # Add in General explanation
@@ -292,6 +299,11 @@ class AverageMemoryAccessTime(MathQuestion):
292
299
  ]
293
300
  )
294
301
  )
295
-
302
+
303
+ return explanation, []
304
+
305
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
306
+ """Build question explanation (backward compatible interface)."""
307
+ explanation, _ = self._get_explanation(**kwargs)
296
308
  return explanation
297
309