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,395 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import abc
5
+ import enum
6
+ import itertools
7
+ from typing import List, Dict, Optional, Tuple, Any
8
+
9
+ from QuizGenerator.question import QuestionRegistry, Question, Answer
10
+
11
+ from QuizGenerator.contentast import ContentAST
12
+
13
+ import logging
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class LanguageQuestion(Question, abc.ABC):
18
+ def __init__(self, *args, **kwargs):
19
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.LANGUAGES)
20
+ super().__init__(*args, **kwargs)
21
+
22
+ class BNF:
23
+
24
+ class Grammar:
25
+ def __init__(self, symbols, start_symbol=None):
26
+ self.start_symbol = start_symbol if start_symbol is not None else symbols[0]
27
+ self.symbols = symbols
28
+
29
+ def generate(self, include_spaces=False, early_exit=False, early_exit_min_iterations=5):
30
+ curr_symbols : List[BNF.Symbol] = [self.start_symbol]
31
+ prev_symbols: List[BNF.Symbol] = curr_symbols
32
+
33
+ iteration_count = 0
34
+ # Check to see if we have any non-terminals left
35
+ while any(map(lambda s: s.kind == BNF.Symbol.Kind.NonTerminal, curr_symbols)):
36
+ # Grab the previous symbols in case we are early exitting
37
+ prev_symbols = curr_symbols
38
+
39
+ # Walk through the current symbols and build a new list of symbols from it
40
+ next_symbols : List[BNF.Symbol] = []
41
+ for symbol in curr_symbols:
42
+ next_symbols.extend(symbol.expand())
43
+ curr_symbols = next_symbols
44
+
45
+ iteration_count += 1
46
+
47
+ if early_exit and iteration_count > early_exit_min_iterations:
48
+ break
49
+
50
+ if early_exit:
51
+ # If we are doing an early exit then we are going to return things with non-terminals
52
+ curr_symbols = prev_symbols
53
+
54
+ # Take all the current symbols and combine them
55
+ return ('' if not include_spaces else ' ').join([str(s) for s in curr_symbols])
56
+
57
+ def print(self):
58
+ for symbol in self.symbols:
59
+ print(symbol.get_full_str())
60
+
61
+ def get_grammar_string(self):
62
+ lines = []
63
+ lines.append('```')
64
+ for symbol in self.symbols:
65
+ lines.append(f"{symbol.get_full_str()}")
66
+
67
+ lines.append('```')
68
+ return '\n'.join(lines)
69
+
70
+ class Symbol:
71
+
72
+ class Kind(enum.Enum):
73
+ NonTerminal = enum.auto()
74
+ Terminal = enum.auto()
75
+
76
+ def __init__(self, symbol : str, kind : Kind, rng):
77
+ self.symbol = symbol
78
+ self.kind = kind
79
+ self.productions : List[BNF.Production] = [] # productions
80
+ self.rng = rng
81
+
82
+ def __str__(self):
83
+ # if self.kind == BNF.Symbol.Kind.NonTerminal:
84
+ # return f"`{self.symbol}`"
85
+ # else:
86
+ # return f"{self.symbol}"
87
+ return f"{self.symbol}"
88
+
89
+ def get_full_str(self):
90
+ return f"{self.symbol} ::= {' | '.join([str(p) for p in self.productions])}"
91
+
92
+ def add_production(self, production: BNF.Production):
93
+ self.productions.append(production)
94
+
95
+ def expand(self) -> List[BNF.Symbol]:
96
+ if self.kind == BNF.Symbol.Kind.Terminal:
97
+ return [self]
98
+ return self.rng.choice(self.productions).production
99
+
100
+ class Production:
101
+ def __init__(self, production_line, nonterminal_symbols: Dict[str, BNF.Symbol], rng):
102
+ if len(production_line.strip()) == 0:
103
+ self.production = []
104
+ else:
105
+ self.production = [
106
+ (nonterminal_symbols.get(symbol, BNF.Symbol(symbol, BNF.Symbol.Kind.Terminal, rng=rng)))
107
+ for symbol in production_line.split(' ')
108
+ ]
109
+
110
+ def __str__(self):
111
+ if len(self.production) == 0:
112
+ return '""'
113
+ return f"{' '.join([str(s) for s in self.production])}"
114
+
115
+
116
+ @staticmethod
117
+ def parse_bnf(grammar_str, rng) -> BNF.Grammar:
118
+
119
+ # Figure out all the nonterminals and create a Token for them
120
+ terminal_symbols = {}
121
+ start_symbol = None
122
+ for line in grammar_str.strip().splitlines():
123
+ if "::=" in line:
124
+ non_terminal_str, _ = line.split("::=", 1)
125
+ non_terminal_str = non_terminal_str.strip()
126
+
127
+ terminal_symbols[non_terminal_str] = BNF.Symbol(non_terminal_str, BNF.Symbol.Kind.NonTerminal, rng=rng)
128
+ if start_symbol is None:
129
+ start_symbol = terminal_symbols[non_terminal_str]
130
+
131
+ # Parse the grammar statement
132
+ for line in grammar_str.strip().splitlines():
133
+ if "::=" in line:
134
+ # Split the line into non-terminal and its expansions
135
+ non_terminal_str, expansions = line.split("::=", 1)
136
+ non_terminal_str = non_terminal_str.strip()
137
+
138
+ non_terminal = terminal_symbols[non_terminal_str]
139
+
140
+ for production_str in expansions.split('|'):
141
+ production_str = production_str.strip()
142
+ non_terminal.add_production(BNF.Production(production_str, terminal_symbols, rng=rng))
143
+ bnf_grammar = BNF.Grammar(list(terminal_symbols.values()), start_symbol)
144
+ return bnf_grammar
145
+
146
+
147
+ @QuestionRegistry.register("LanguageQuestion")
148
+ class ValidStringsInLanguageQuestion(LanguageQuestion):
149
+ MAX_TRIES = 1000
150
+
151
+ def __init__(self, grammar_str_good: Optional[str] = None, grammar_str_bad: Optional[str] = None, *args, **kwargs):
152
+ # Preserve question-specific params for QR code config
153
+ if grammar_str_good is not None:
154
+ kwargs['grammar_str_good'] = grammar_str_good
155
+ if grammar_str_bad is not None:
156
+ kwargs['grammar_str_bad'] = grammar_str_bad
157
+
158
+ super().__init__(*args, **kwargs)
159
+
160
+ if grammar_str_good is not None and grammar_str_bad is not None:
161
+ self.grammar_str_good = grammar_str_good
162
+ self.grammar_str_bad = grammar_str_bad
163
+ self.include_spaces = kwargs.get("include_spaces", False)
164
+ self.MAX_LENGTH = kwargs.get("max_length", 30)
165
+ else:
166
+ which_grammar = self.rng.choice(range(4))
167
+
168
+ if which_grammar == 0:
169
+ # todo: make a few different kinds of grammars that could be picked
170
+ self.grammar_str_good = """
171
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
172
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
173
+ <factor> ::= <number>
174
+ <number> ::= <digit> | <number> <digit>
175
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
176
+ """
177
+ # Adding in a plus to number
178
+ self.grammar_str_bad = """
179
+ <expression> ::= <term> | <expression> + <term> | <expression> - <term>
180
+ <term> ::= <factor> | <term> * <factor> | <term> / <factor>
181
+ <factor> ::= <number>
182
+ <number> ::= <digit> + | <digit> <number>
183
+ <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
184
+ """
185
+ self.include_spaces = False
186
+ self.MAX_LENGTH = 30
187
+ elif which_grammar == 1:
188
+ self.grammar_str_good = """
189
+ <sentence> ::= <subject> <verb> <object>
190
+ <subject> ::= The cat | A dog | The bird | A child | <adjective> <animal>
191
+ <animal> ::= cat | dog | bird | child
192
+ <adjective> ::= happy | sad | angry | playful
193
+ <verb> ::= chases | sees | hates | loves
194
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
195
+ """
196
+ self.grammar_str_bad = """
197
+ <sentence> ::= <subject> <verb> <object>
198
+ <subject> ::= The human | The dog | A bird | Some child | A <adjective> <animal>
199
+ <animal> ::= cat | dog | bird | child
200
+ <adjective> ::= happy | sad | angry | playful
201
+ <verb> ::= chases | sees | hates | loves
202
+ <object> ::= the ball | the toy | the tree | <adjective> <object>
203
+ """
204
+ self.include_spaces = True
205
+ self.MAX_LENGTH = 100
206
+ elif which_grammar == 2:
207
+ self.grammar_str_good = """
208
+ <poem> ::= <line> | <line> <poem>
209
+ <line> ::= <subject> <verb> <object> <modifier>
210
+ <subject> ::= whispers | shadows | dreams | echoes | <compound-subject>
211
+ <compound-subject> ::= <subject> and <subject>
212
+ <verb> ::= dance | dissolve | shimmer | collapse | <compound-verb>
213
+ <compound-verb> ::= <verb> then <verb>
214
+ <object> ::= beneath | between | inside | around | <compound-object>
215
+ <compound-object> ::= <object> through <object>
216
+ <modifier> ::= silently | violently | mysteriously | endlessly | <recursive-modifier>
217
+ <recursive-modifier> ::= <modifier> and <modifier>
218
+ """
219
+ self.grammar_str_bad = """
220
+ <bad-poem> ::= <almost-valid-line> | <bad-poem> <bad-poem>
221
+ <almost-valid-line> ::= <tricky-subject> <tricky-verb> <tricky-object> <tricky-modifier>
222
+ <tricky-subject> ::= whispers | shadows and and | <duplicate-subject>
223
+ <duplicate-subject> ::= whispers whispers
224
+ <tricky-verb> ::= dance | <incorrect-verb-placement> | <verb-verb>
225
+ <incorrect-verb-placement> ::= dance dance
226
+ <verb-verb> ::= dance whispers
227
+ <tricky-object> ::= beneath | <object-verb-swap> | <duplicate-object>
228
+ <object-verb-swap> ::= dance beneath
229
+ <duplicate-object> ::= beneath beneath
230
+ <tricky-modifier> ::= silently | <modifier-subject-swap> | <duplicate-modifier>
231
+ <modifier-subject-swap> ::= whispers silently
232
+ <duplicate-modifier> ::= silently silently
233
+ """
234
+ self.include_spaces = True
235
+ self.MAX_LENGTH = 100
236
+ elif which_grammar == 3:
237
+ self.grammar_str_good = """
238
+ <A> ::= a <B> a |
239
+ <B> ::= b <C> b |
240
+ <C> ::= c <A> c |
241
+ """
242
+ self.grammar_str_bad = """
243
+ <A> ::= a <B> c
244
+ <B> ::= b <C> a |
245
+ <C> ::= c <A> b |
246
+ """
247
+ self.include_spaces = False
248
+ self.MAX_LENGTH = 100
249
+
250
+ self.grammar_good = BNF.parse_bnf(self.grammar_str_good, self.rng)
251
+ self.grammar_bad = BNF.parse_bnf(self.grammar_str_bad, self.rng)
252
+
253
+ self.num_answer_options = kwargs.get("num_answer_options", 4)
254
+ self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
255
+
256
+ def refresh(self, *args, **kwargs):
257
+ super().refresh(*args, **kwargs)
258
+
259
+ self.answers = {}
260
+
261
+ self.num_answer_options = kwargs.get("num_answer_options", 4)
262
+ self.num_answer_blanks = kwargs.get("num_answer_blanks", 4)
263
+
264
+ self.refresh()
265
+
266
+ def refresh(self, *args, **kwargs):
267
+ super().refresh(*args, **kwargs)
268
+
269
+ self.answers = {}
270
+
271
+ self.answers.update(
272
+ {
273
+ "answer_good" : Answer(
274
+ f"answer_good",
275
+ self.grammar_good.generate(self.include_spaces),
276
+ Answer.AnswerKind.MULTIPLE_ANSWER,
277
+ correct=True
278
+ )
279
+ }
280
+ )
281
+
282
+ self.answers.update(
283
+ {
284
+ "answer_bad":
285
+ Answer(
286
+ f"answer_bad",
287
+ self.grammar_bad.generate(self.include_spaces),
288
+ Answer.AnswerKind.MULTIPLE_ANSWER,
289
+ correct=False
290
+ )
291
+ })
292
+ self.answers.update({
293
+ "answer_bad_early":
294
+ Answer(
295
+ f"answer_bad_early",
296
+ self.grammar_bad.generate(self.include_spaces, early_exit=True),
297
+ Answer.AnswerKind.MULTIPLE_ANSWER,
298
+ correct=False
299
+ )
300
+ })
301
+
302
+ answer_text_set = {a.value for a in self.answers.values()}
303
+ num_tries = 0
304
+ while len(self.answers) < 10 and num_tries < self.MAX_TRIES:
305
+
306
+ correct = self.rng.choice([True, False])
307
+ if not correct:
308
+ early_exit = self.rng.choice([True, False])
309
+ else:
310
+ early_exit = False
311
+ new_answer = Answer(
312
+ f"answer_{num_tries}",
313
+ (
314
+ self.grammar_good
315
+ if correct or early_exit
316
+ else self.grammar_bad
317
+ ).generate(self.include_spaces, early_exit=early_exit),
318
+ Answer.AnswerKind.MULTIPLE_ANSWER,
319
+ correct= correct and not early_exit
320
+ )
321
+ if len(new_answer.value) < self.MAX_LENGTH and new_answer.value not in answer_text_set:
322
+ self.answers.update({new_answer.key : new_answer})
323
+ answer_text_set.add(new_answer.value)
324
+ num_tries += 1
325
+
326
+ # Generate answers that will be used only for the latex version.
327
+ self.featured_answers = {
328
+ self.grammar_good.generate(),
329
+ self.grammar_bad.generate(),
330
+ self.grammar_good.generate(early_exit=True)
331
+ }
332
+ while len(self.featured_answers) < self.num_answer_options:
333
+ self.featured_answers.add(
334
+ self.rng.choice([
335
+ lambda: self.grammar_good.generate(),
336
+ lambda: self.grammar_bad.generate(),
337
+ lambda: self.grammar_good.generate(early_exit=True),
338
+ ])()
339
+ )
340
+
341
+
342
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
343
+ body = ContentAST.Section()
344
+
345
+ body.add_element(
346
+ ContentAST.Paragraph([
347
+ ContentAST.OnlyHtml(
348
+ ContentAST.Text("Given the following grammar, which of the below strings are part of the language?")
349
+ ),
350
+ ContentAST.OnlyLatex(
351
+ ContentAST.Text(
352
+ "Given the following grammar "
353
+ "please circle any provided strings that are part of the language (or indicate clearly if there are none), "
354
+ "and on each blank line provide generate a new, unique string that is part of the language."
355
+ )
356
+ )
357
+ ])
358
+ )
359
+
360
+ body.add_element(
361
+ ContentAST.Paragraph([
362
+ self.grammar_good.get_grammar_string()
363
+ ])
364
+ )
365
+
366
+ # Add in some answers as latex-only options to be circled
367
+ body.add_element(
368
+ ContentAST.OnlyLatex([
369
+ ContentAST.Text(f"- `{str(answer)}`")
370
+ for answer in self.featured_answers
371
+ ])
372
+ )
373
+
374
+ # For Latex-only, ask students to generate some more.
375
+ body.add_element(
376
+ ContentAST.OnlyLatex([
377
+ ContentAST.AnswerBlock([ContentAST.Answer() for _ in range(self.num_answer_blanks)])
378
+ ])
379
+ )
380
+
381
+ return body
382
+
383
+ def get_explanation(self, *args, **kwargs) -> ContentAST.Section:
384
+ explanation = ContentAST.Section()
385
+ explanation.add_element(
386
+ ContentAST.Paragraph([
387
+ "Remember, for a string to be part of our language, we need to be able to derive it from our grammar.",
388
+ "Unfortunately, there isn't space here to demonstrate the derivation so please work through them on your own!"
389
+ ])
390
+ )
391
+ return explanation
392
+
393
+ def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
394
+
395
+ return Answer.AnswerKind.MULTIPLE_ANSWER, list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
@@ -0,0 +1,297 @@
1
+ #!env python
2
+ import abc
3
+ import logging
4
+ import math
5
+
6
+ from QuizGenerator.question import Question, QuestionRegistry, Answer
7
+ from QuizGenerator.contentast import ContentAST
8
+ from QuizGenerator.constants import MathRanges
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class MathQuestion(Question, abc.ABC):
14
+ def __init__(self, *args, **kwargs):
15
+ kwargs["topic"] = kwargs.get("topic", Question.Topic.MATH)
16
+ super().__init__(*args, **kwargs)
17
+
18
+
19
+ @QuestionRegistry.register()
20
+ class BitsAndBytes(MathQuestion):
21
+
22
+ MIN_BITS = MathRanges.DEFAULT_MIN_MATH_BITS
23
+ MAX_BITS = MathRanges.DEFAULT_MAX_MATH_BITS
24
+
25
+ def refresh(self, *args, **kwargs):
26
+ super().refresh(*args, **kwargs)
27
+
28
+ # Generate the important parts of the problem
29
+ self.from_binary = (0 == self.rng.randint(0,1))
30
+ self.num_bits = self.rng.randint(self.MIN_BITS, self.MAX_BITS)
31
+ self.num_bytes = int(math.pow(2, self.num_bits))
32
+
33
+ if self.from_binary:
34
+ self.answers = {"answer" : Answer.integer("num_bytes", self.num_bytes)}
35
+ else:
36
+ self.answers = {"answer" : Answer.integer("num_bits", self.num_bits)}
37
+
38
+ def get_body(self, **kwargs) -> ContentAST.Section:
39
+ body = ContentAST.Section()
40
+ body.add_element(
41
+ ContentAST.Paragraph([
42
+ f"Given that we have "
43
+ f"{self.num_bits if self.from_binary else self.num_bytes} {'bits' if self.from_binary else 'bytes'}, "
44
+ f"how many {'bits' if not self.from_binary else 'bytes'} "
45
+ f"{'do we need to address our memory' if not self.from_binary else 'of memory can be addressed'}?"
46
+ ])
47
+ )
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
+
70
+ return body
71
+
72
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
73
+ explanation = ContentAST.Section()
74
+
75
+ explanation.add_element(
76
+ ContentAST.Paragraph([
77
+ "Remember that for these problems we use one of these two equations (which are equivalent)"
78
+ ])
79
+ )
80
+ explanation.add_elements([
81
+ ContentAST.Equation(r"log_{2}(\text{#bytes}) = \text{#bits}"),
82
+ ContentAST.Equation(r"2^{(\text{#bits})} = \text{#bytes}")
83
+ ])
84
+
85
+ explanation.add_element(
86
+ ContentAST.Paragraph(["Therefore, we calculate:"])
87
+ )
88
+
89
+ if self.from_binary:
90
+ explanation.add_element(
91
+ ContentAST.Equation(f"2 ^ {{{self.num_bits}bits}} = \\textbf{{{self.num_bytes}}}\\text{{bytes}}")
92
+ )
93
+ else:
94
+ explanation.add_element(
95
+ ContentAST.Equation(f"log_{{2}}({self.num_bytes} \\text{{bytes}}) = \\textbf{{{self.num_bits}}}\\text{{bits}}")
96
+ )
97
+
98
+ return explanation
99
+
100
+
101
+ @QuestionRegistry.register()
102
+ class HexAndBinary(MathQuestion):
103
+
104
+ MIN_HEXITS = 1
105
+ MAX_HEXITS = 8
106
+
107
+ def refresh(self, **kwargs):
108
+ super().refresh(**kwargs)
109
+
110
+ self.from_binary = self.rng.choice([True, False])
111
+ self.number_of_hexits = self.rng.randint(1, 8)
112
+ self.value = self.rng.randint(1, 16**self.number_of_hexits)
113
+
114
+ self.hex_val = f"0x{self.value:0{self.number_of_hexits}X}"
115
+ self.binary_val = f"0b{self.value:0{4*self.number_of_hexits}b}"
116
+
117
+ if self.from_binary:
118
+ self.answers['answer'] = Answer.string("hex_val", self.hex_val)
119
+ else:
120
+ self.answers['answer'] = Answer.string("binary_val", self.binary_val)
121
+
122
+ def get_body(self, **kwargs) -> ContentAST.Section:
123
+ body = ContentAST.Section()
124
+
125
+ body.add_element(
126
+ ContentAST.Paragraph([
127
+ f"Given the number {self.hex_val if not self.from_binary else self.binary_val} "
128
+ f"please convert it to {'hex' if self.from_binary else 'binary'}.",
129
+ "Please include base indicator all padding zeros as appropriate (e.g. 0x01 should be 0b00000001)",
130
+ ])
131
+ )
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
+
142
+ return body
143
+
144
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
145
+ explanation = ContentAST.Section()
146
+
147
+ paragraph = ContentAST.Paragraph([
148
+ "The core idea for converting between binary and hex is to divide and conquer. "
149
+ "Specifically, each hexit (hexadecimal digit) is equivalent to 4 bits. "
150
+ ])
151
+
152
+ if self.from_binary:
153
+ paragraph.add_line(
154
+ "Therefore, we need to consider each group of 4 bits together and convert them to the appropriate hexit."
155
+ )
156
+ else:
157
+ paragraph.add_line(
158
+ "Therefore, we need to consider each hexit and convert it to the appropriate 4 bits."
159
+ )
160
+
161
+ explanation.add_element(paragraph)
162
+
163
+ # Generate translation table
164
+ binary_str = f"{self.value:0{4*self.number_of_hexits}b}"
165
+ hex_str = f"{self.value:0{self.number_of_hexits}X}"
166
+
167
+ explanation.add_element(
168
+ ContentAST.Table(
169
+ data=[
170
+ ["0b"] + [binary_str[i:i+4] for i in range(0, len(binary_str), 4)],
171
+ ["0x"] + list(hex_str)
172
+ ],
173
+ # alignments='center', #['center' for _ in range(0, 1+len(hex_str))],
174
+ padding=False
175
+
176
+ )
177
+ )
178
+
179
+ if self.from_binary:
180
+ explanation.add_element(
181
+ ContentAST.Paragraph([
182
+ f"Which gives us our hex value of: 0x{hex_str}"
183
+ ])
184
+ )
185
+ else:
186
+ explanation.add_element(
187
+ ContentAST.Paragraph([
188
+ f"Which gives us our binary value of: 0b{binary_str}"
189
+ ])
190
+ )
191
+
192
+ return explanation
193
+
194
+
195
+ @QuestionRegistry.register()
196
+ class AverageMemoryAccessTime(MathQuestion):
197
+
198
+ CHANCE_OF_99TH_PERCENTILE = 0.75
199
+
200
+ def refresh(self, rng_seed=None, *args, **kwargs):
201
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
202
+
203
+ # Figure out how many orders of magnitude different we are
204
+ orders_of_magnitude_different = self.rng.randint(1,4)
205
+ self.hit_latency = self.rng.randint(1,9)
206
+ self.miss_latency = int(self.rng.randint(1, 9) * math.pow(10, orders_of_magnitude_different))
207
+
208
+ # Add in a complication of making it sometimes very, very close
209
+ if self.rng.random() < self.CHANCE_OF_99TH_PERCENTILE:
210
+ # Then let's make it very close to 99%
211
+ self.hit_rate = (99 + self.rng.random()) / 100
212
+ else:
213
+ self.hit_rate = self.rng.random()
214
+
215
+ # Calculate the hit rate
216
+ self.hit_rate = round(self.hit_rate, 4)
217
+
218
+ # Calculate the AverageMemoryAccessTime (which is the answer itself)
219
+ self.amat = self.hit_rate * self.hit_latency + (1 - self.hit_rate) * self.miss_latency
220
+
221
+ self.answers = {
222
+ "amat": Answer.float_value("answer__amat", self.amat)
223
+ }
224
+
225
+ # Finally, do the self.rngizing of the question, to avoid these being non-deterministic
226
+ self.show_miss_rate = self.rng.random() > 0.5
227
+
228
+ # At this point, everything in the question should be set.
229
+ pass
230
+
231
+ def get_body(self, **kwargs) -> ContentAST.Section:
232
+ body = ContentAST.Section()
233
+
234
+ # Add in background information
235
+ body.add_element(
236
+ ContentAST.Paragraph([
237
+ ContentAST.Text("Please calculate the Average Memory Access Time given the below information. "),
238
+ ContentAST.Text(
239
+ f"Please round your answer to {Answer.DEFAULT_ROUNDING_DIGITS} decimal points. ",
240
+ hide_from_latex=True
241
+ )
242
+ ])
243
+ )
244
+ table_data = [
245
+ ["Hit Latency", f"{self.hit_latency} cycles"],
246
+ ["Miss Latency", f"{self.miss_latency} cycles"]
247
+ ]
248
+
249
+ # Add in either miss rate or hit rate -- we only need one of them
250
+ if self.show_miss_rate:
251
+ table_data.append(["Miss Rate", f"{100 * (1 - self.hit_rate): 0.2f}%"])
252
+ else:
253
+ table_data.append(["Hit Rate", f"{100 * self.hit_rate: 0.2f}%"])
254
+
255
+ body.add_element(
256
+ ContentAST.Table(
257
+ data=table_data
258
+ )
259
+ )
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
+
271
+ return body
272
+
273
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
274
+ explanation = ContentAST.Section()
275
+
276
+ # Add in General explanation
277
+ explanation.add_element(
278
+ ContentAST.Paragraph([
279
+ "Remember that to calculate the Average Memory Access Time "
280
+ "we weight both the hit and miss times by their relative likelihood.",
281
+ "That is, we calculate:"
282
+ ])
283
+ )
284
+
285
+ # Add in equations
286
+ explanation.add_element(
287
+ ContentAST.Equation.make_block_equation__multiline_equals(
288
+ lhs="AMAT",
289
+ rhs=[
290
+ r"(hit\_rate)*(hit\_cost) + (1 - hit\_rate)*(miss\_cost)",
291
+ f"({self.hit_rate: 0.{Answer.DEFAULT_ROUNDING_DIGITS}f})*({self.hit_latency}) + ({1 - self.hit_rate: 0.{Answer.DEFAULT_ROUNDING_DIGITS}f})*({self.miss_latency}) = {self.amat: 0.{Answer.DEFAULT_ROUNDING_DIGITS}f}\\text{{cycles}}"
292
+ ]
293
+ )
294
+ )
295
+
296
+ return explanation
297
+