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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +622 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1809 -0
- QuizGenerator/generate.py +362 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +480 -0
- QuizGenerator/mixins.py +539 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +395 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1398 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +396 -0
- QuizGenerator/premade_questions/cst334/process.py +649 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1264 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +657 -0
- QuizGenerator/quiz.py +468 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.1.0.dist-info/METADATA +263 -0
- quizgenerator-0.1.0.dist-info/RECORD +44 -0
- quizgenerator-0.1.0.dist-info/WHEEL +4 -0
- quizgenerator-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
|