QuizGenerator 0.4.2__py3-none-any.whl → 0.6.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/contentast.py +809 -117
- QuizGenerator/generate.py +219 -11
- QuizGenerator/misc.py +0 -556
- QuizGenerator/mixins.py +50 -29
- QuizGenerator/premade_questions/basic.py +3 -3
- QuizGenerator/premade_questions/cst334/languages.py +183 -175
- QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
- QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
- QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
- QuizGenerator/premade_questions/cst334/process.py +558 -79
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
- QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
- QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
- QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
- QuizGenerator/premade_questions/cst463/models/text.py +29 -15
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
- QuizGenerator/question.py +114 -20
- QuizGenerator/quiz.py +81 -24
- QuizGenerator/regenerate.py +98 -29
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
- QuizGenerator/README.md +0 -5
- QuizGenerator/logging.yaml +0 -55
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
- {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/misc.py
CHANGED
|
@@ -21,559 +21,3 @@ def fix_negative_zero(value):
|
|
|
21
21
|
if isinstance(value, (int, float)):
|
|
22
22
|
return 0.0 if value == 0 else value
|
|
23
23
|
return value
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Answer:
|
|
27
|
-
DEFAULT_ROUNDING_DIGITS = 4
|
|
28
|
-
|
|
29
|
-
class AnswerKind(enum.Enum):
|
|
30
|
-
BLANK = "fill_in_multiple_blanks_question"
|
|
31
|
-
MULTIPLE_ANSWER = "multiple_answers_question"
|
|
32
|
-
ESSAY = "essay_question"
|
|
33
|
-
MULTIPLE_DROPDOWN = "multiple_dropdowns_question"
|
|
34
|
-
NUMERICAL_QUESTION = "numerical_question" # note: these can only be single answers as far as I can tell
|
|
35
|
-
|
|
36
|
-
class VariableKind(enum.Enum):
|
|
37
|
-
STR = enum.auto()
|
|
38
|
-
INT = enum.auto()
|
|
39
|
-
FLOAT = enum.auto()
|
|
40
|
-
BINARY = enum.auto()
|
|
41
|
-
HEX = enum.auto()
|
|
42
|
-
BINARY_OR_HEX = enum.auto()
|
|
43
|
-
AUTOFLOAT = enum.auto()
|
|
44
|
-
LIST = enum.auto()
|
|
45
|
-
VECTOR = enum.auto()
|
|
46
|
-
MATRIX = enum.auto()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def __init__(
|
|
50
|
-
self, key:str,
|
|
51
|
-
value,
|
|
52
|
-
kind : Answer.AnswerKind = AnswerKind.BLANK,
|
|
53
|
-
variable_kind : Answer.VariableKind = VariableKind.STR,
|
|
54
|
-
display=None,
|
|
55
|
-
length=None,
|
|
56
|
-
correct=True,
|
|
57
|
-
baffles=None,
|
|
58
|
-
pdf_only=False
|
|
59
|
-
):
|
|
60
|
-
self.key = key
|
|
61
|
-
self.value = value
|
|
62
|
-
self.kind = kind
|
|
63
|
-
self.variable_kind = variable_kind
|
|
64
|
-
# For list values in display, show the first option (or join them with /)
|
|
65
|
-
if display is not None:
|
|
66
|
-
self.display = display
|
|
67
|
-
elif isinstance(value, list) and variable_kind == Answer.VariableKind.STR:
|
|
68
|
-
self.display = value[0] if len(value) == 1 else " / ".join(value)
|
|
69
|
-
else:
|
|
70
|
-
self.display = value
|
|
71
|
-
self.length = length # Used for bits and hex to be printed appropriately
|
|
72
|
-
self.correct = correct
|
|
73
|
-
self.baffles = baffles
|
|
74
|
-
self.pdf_only = pdf_only
|
|
75
|
-
|
|
76
|
-
def get_for_canvas(self, single_answer=False) -> List[Dict]:
|
|
77
|
-
# If this answer is marked as PDF-only, don't send it to Canvas
|
|
78
|
-
if self.pdf_only:
|
|
79
|
-
return []
|
|
80
|
-
|
|
81
|
-
canvas_answers : List[Dict] = []
|
|
82
|
-
if self.variable_kind == Answer.VariableKind.BINARY:
|
|
83
|
-
canvas_answers = [
|
|
84
|
-
{
|
|
85
|
-
"blank_id": self.key,
|
|
86
|
-
"answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
|
|
87
|
-
"answer_weight": 100 if self.correct else 0,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"blank_id": self.key,
|
|
91
|
-
"answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
|
|
92
|
-
"answer_weight": 100 if self.correct else 0,
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
elif self.variable_kind == Answer.VariableKind.HEX:
|
|
96
|
-
canvas_answers = [
|
|
97
|
-
{
|
|
98
|
-
"blank_id": self.key,
|
|
99
|
-
"answer_text": f"{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
|
|
100
|
-
"answer_weight": 100 if self.correct else 0,
|
|
101
|
-
},{
|
|
102
|
-
"blank_id": self.key,
|
|
103
|
-
"answer_text": f"0x{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
|
|
104
|
-
"answer_weight": 100 if self.correct else 0,
|
|
105
|
-
}
|
|
106
|
-
]
|
|
107
|
-
elif self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
|
|
108
|
-
canvas_answers = [
|
|
109
|
-
{
|
|
110
|
-
"blank_id": self.key,
|
|
111
|
-
"answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
|
|
112
|
-
"answer_weight": 100 if self.correct else 0,
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"blank_id": self.key,
|
|
116
|
-
"answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
|
|
117
|
-
"answer_weight": 100 if self.correct else 0,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"blank_id": self.key,
|
|
121
|
-
"answer_text": f"{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
|
|
122
|
-
"answer_weight": 100 if self.correct else 0,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"blank_id": self.key,
|
|
126
|
-
"answer_text": f"0x{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
|
|
127
|
-
"answer_weight": 100 if self.correct else 0,
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"blank_id": self.key,
|
|
131
|
-
"answer_text": f"{self.value}",
|
|
132
|
-
"answer_weight": 100 if self.correct else 0,
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
]
|
|
136
|
-
elif self.variable_kind in [
|
|
137
|
-
Answer.VariableKind.AUTOFLOAT,
|
|
138
|
-
Answer.VariableKind.FLOAT,
|
|
139
|
-
Answer.VariableKind.INT
|
|
140
|
-
]:
|
|
141
|
-
if single_answer:
|
|
142
|
-
canvas_answers = [
|
|
143
|
-
{
|
|
144
|
-
"numerical_answer_type": "exact_answer",
|
|
145
|
-
"answer_text": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
|
|
146
|
-
"answer_exact": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
|
|
147
|
-
"answer_error_margin": 0.1,
|
|
148
|
-
"answer_weight": 100 if self.correct else 0,
|
|
149
|
-
}
|
|
150
|
-
]
|
|
151
|
-
else:
|
|
152
|
-
# Use the accepted_strings helper with settings that match the original AUTOFLOAT behavior
|
|
153
|
-
answer_strings = self.__class__.accepted_strings(
|
|
154
|
-
self.value,
|
|
155
|
-
allow_integer=True,
|
|
156
|
-
allow_simple_fraction=True,
|
|
157
|
-
max_denominator=3*4*5, # For process questions, these are the numbers of jobs we'd have
|
|
158
|
-
allow_mixed=True,
|
|
159
|
-
include_spaces=False,
|
|
160
|
-
include_fixed_even_if_integer=True
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
canvas_answers = [
|
|
164
|
-
{
|
|
165
|
-
"blank_id": self.key,
|
|
166
|
-
"answer_text": answer_string,
|
|
167
|
-
"answer_weight": 100 if self.correct else 0,
|
|
168
|
-
}
|
|
169
|
-
for answer_string in answer_strings
|
|
170
|
-
]
|
|
171
|
-
|
|
172
|
-
elif self.variable_kind == Answer.VariableKind.VECTOR:
|
|
173
|
-
|
|
174
|
-
# Get all answer variations
|
|
175
|
-
answer_variations = [
|
|
176
|
-
self.__class__.accepted_strings(dimension_value)
|
|
177
|
-
for dimension_value in self.value
|
|
178
|
-
]
|
|
179
|
-
|
|
180
|
-
canvas_answers = []
|
|
181
|
-
for combination in itertools.product(*answer_variations):
|
|
182
|
-
# Add parentheses format for all vectors: (1, 2, 3)
|
|
183
|
-
canvas_answers.append({
|
|
184
|
-
"blank_id" : self.key,
|
|
185
|
-
"answer_weight": 100 if self.correct else 0,
|
|
186
|
-
"answer_text": f"({', '.join(list(combination))})",
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
# Add non-parentheses format only for single-element vectors: 5
|
|
190
|
-
if len(combination) == 1:
|
|
191
|
-
canvas_answers.append(
|
|
192
|
-
{
|
|
193
|
-
"blank_id": self.key,
|
|
194
|
-
"answer_weight": 100 if self.correct else 0,
|
|
195
|
-
"answer_text": f"{', '.join(combination)}",
|
|
196
|
-
}
|
|
197
|
-
)
|
|
198
|
-
return canvas_answers
|
|
199
|
-
|
|
200
|
-
elif self.variable_kind == Answer.VariableKind.LIST:
|
|
201
|
-
canvas_answers = [
|
|
202
|
-
{
|
|
203
|
-
"blank_id": self.key,
|
|
204
|
-
"answer_text": ', '.join(map(str, possible_state)),
|
|
205
|
-
"answer_weight": 100 if self.correct else 0,
|
|
206
|
-
}
|
|
207
|
-
for possible_state in [self.value] #itertools.permutations(self.value)
|
|
208
|
-
]
|
|
209
|
-
|
|
210
|
-
else:
|
|
211
|
-
# For string answers, check if value is a list of acceptable alternatives
|
|
212
|
-
if isinstance(self.value, list):
|
|
213
|
-
canvas_answers = [
|
|
214
|
-
{
|
|
215
|
-
"blank_id": self.key,
|
|
216
|
-
"answer_text": str(alt),
|
|
217
|
-
"answer_weight": 100 if self.correct else 0,
|
|
218
|
-
}
|
|
219
|
-
for alt in self.value
|
|
220
|
-
]
|
|
221
|
-
else:
|
|
222
|
-
canvas_answers = [{
|
|
223
|
-
"blank_id": self.key,
|
|
224
|
-
"answer_text": self.value,
|
|
225
|
-
"answer_weight": 100 if self.correct else 0,
|
|
226
|
-
}]
|
|
227
|
-
|
|
228
|
-
if self.baffles is not None:
|
|
229
|
-
for baffle in self.baffles:
|
|
230
|
-
canvas_answers.append({
|
|
231
|
-
"blank_id": self.key,
|
|
232
|
-
"answer_text": baffle,
|
|
233
|
-
"answer_weight": 0,
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
return canvas_answers
|
|
237
|
-
|
|
238
|
-
def get_display_string(self) -> str:
|
|
239
|
-
"""
|
|
240
|
-
Get the formatted display string for this answer (for grading/answer keys).
|
|
241
|
-
|
|
242
|
-
Returns the answer in the most appropriate format for human readability:
|
|
243
|
-
- BINARY_OR_HEX: hex format (0x...)
|
|
244
|
-
- BINARY: binary format (0b...)
|
|
245
|
-
- HEX: hex format (0x...)
|
|
246
|
-
- AUTOFLOAT/FLOAT: rounded to DEFAULT_ROUNDING_DIGITS
|
|
247
|
-
- INT: integer
|
|
248
|
-
- STR/LIST/VECTOR: as-is
|
|
249
|
-
"""
|
|
250
|
-
if self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
|
|
251
|
-
# For binary_hex answers, show hex format (more compact and readable)
|
|
252
|
-
hex_digits = math.ceil(self.length / 4) if self.length is not None else 0
|
|
253
|
-
return f"0x{self.value:0{hex_digits}X}"
|
|
254
|
-
|
|
255
|
-
elif self.variable_kind == Answer.VariableKind.BINARY:
|
|
256
|
-
# Show binary format
|
|
257
|
-
return f"0b{self.value:0{self.length if self.length is not None else 0}b}"
|
|
258
|
-
|
|
259
|
-
elif self.variable_kind == Answer.VariableKind.HEX:
|
|
260
|
-
# Show hex format
|
|
261
|
-
hex_digits = (self.length // 4) + 1 if self.length is not None else 0
|
|
262
|
-
return f"0x{self.value:0{hex_digits}X}"
|
|
263
|
-
|
|
264
|
-
elif self.variable_kind == Answer.VariableKind.AUTOFLOAT:
|
|
265
|
-
# Round to default precision for readability
|
|
266
|
-
rounded = round(self.value, self.DEFAULT_ROUNDING_DIGITS)
|
|
267
|
-
return f"{fix_negative_zero(rounded)}"
|
|
268
|
-
|
|
269
|
-
elif self.variable_kind == Answer.VariableKind.FLOAT:
|
|
270
|
-
# Round to default precision
|
|
271
|
-
if isinstance(self.value, (list, tuple)):
|
|
272
|
-
rounded = round(self.value[0], self.DEFAULT_ROUNDING_DIGITS)
|
|
273
|
-
return f"{fix_negative_zero(rounded)}"
|
|
274
|
-
rounded = round(self.value, self.DEFAULT_ROUNDING_DIGITS)
|
|
275
|
-
return f"{fix_negative_zero(rounded)}"
|
|
276
|
-
|
|
277
|
-
elif self.variable_kind == Answer.VariableKind.INT:
|
|
278
|
-
return str(int(self.value))
|
|
279
|
-
|
|
280
|
-
elif self.variable_kind == Answer.VariableKind.LIST:
|
|
281
|
-
return ", ".join(str(v) for v in self.value)
|
|
282
|
-
|
|
283
|
-
elif self.variable_kind == Answer.VariableKind.VECTOR:
|
|
284
|
-
# Format as comma-separated rounded values
|
|
285
|
-
return ", ".join(str(fix_negative_zero(round(v, self.DEFAULT_ROUNDING_DIGITS))) for v in self.value)
|
|
286
|
-
|
|
287
|
-
else:
|
|
288
|
-
# Default: use display or value
|
|
289
|
-
return str(self.display if hasattr(self, 'display') else self.value)
|
|
290
|
-
|
|
291
|
-
def get_ast_element(self, label=None):
|
|
292
|
-
from QuizGenerator.contentast import ContentAST
|
|
293
|
-
|
|
294
|
-
return ContentAST.Answer(answer=self, label=label) # todo fix label
|
|
295
|
-
|
|
296
|
-
# Factory methods for common answer types
|
|
297
|
-
@classmethod
|
|
298
|
-
def binary_hex(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
299
|
-
"""Create an answer that accepts binary or hex format"""
|
|
300
|
-
return cls(
|
|
301
|
-
key=key,
|
|
302
|
-
value=value,
|
|
303
|
-
variable_kind=cls.VariableKind.BINARY_OR_HEX,
|
|
304
|
-
length=length,
|
|
305
|
-
**kwargs
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
@classmethod
|
|
309
|
-
def auto_float(cls, key: str, value: float, **kwargs) -> 'Answer':
|
|
310
|
-
"""Create an answer that accepts multiple float formats (decimal, fraction, mixed)"""
|
|
311
|
-
return cls(
|
|
312
|
-
key=key,
|
|
313
|
-
value=value,
|
|
314
|
-
variable_kind=cls.VariableKind.AUTOFLOAT,
|
|
315
|
-
**kwargs
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
@classmethod
|
|
319
|
-
def integer(cls, key: str, value: int, **kwargs) -> 'Answer':
|
|
320
|
-
"""Create an integer answer"""
|
|
321
|
-
return cls(
|
|
322
|
-
key=key,
|
|
323
|
-
value=value,
|
|
324
|
-
variable_kind=cls.VariableKind.INT,
|
|
325
|
-
**kwargs
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
@classmethod
|
|
329
|
-
def string(cls, key: str, value: str, **kwargs) -> 'Answer':
|
|
330
|
-
"""Create a string answer"""
|
|
331
|
-
return cls(
|
|
332
|
-
key=key,
|
|
333
|
-
value=value,
|
|
334
|
-
variable_kind=cls.VariableKind.STR,
|
|
335
|
-
**kwargs
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
@classmethod
|
|
339
|
-
def binary(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
340
|
-
"""Create a binary-only answer"""
|
|
341
|
-
return cls(
|
|
342
|
-
key=key,
|
|
343
|
-
value=value,
|
|
344
|
-
variable_kind=cls.VariableKind.BINARY,
|
|
345
|
-
length=length,
|
|
346
|
-
**kwargs
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
@classmethod
|
|
350
|
-
def hex_value(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
351
|
-
"""Create a hex-only answer"""
|
|
352
|
-
return cls(
|
|
353
|
-
key=key,
|
|
354
|
-
value=value,
|
|
355
|
-
variable_kind=cls.VariableKind.HEX,
|
|
356
|
-
length=length,
|
|
357
|
-
**kwargs
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
@classmethod
|
|
361
|
-
def float_value(cls, key: str, value: Tuple[float], **kwargs) -> 'Answer':
|
|
362
|
-
"""Create a simple float answer (no fraction conversion)"""
|
|
363
|
-
return cls(
|
|
364
|
-
key=key,
|
|
365
|
-
value=value,
|
|
366
|
-
variable_kind=cls.VariableKind.FLOAT,
|
|
367
|
-
**kwargs
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
@classmethod
|
|
371
|
-
def list_value(cls, key: str, value: list, **kwargs) -> 'Answer':
|
|
372
|
-
"""Create a list answer (comma-separated values)"""
|
|
373
|
-
return cls(
|
|
374
|
-
key=key,
|
|
375
|
-
value=value,
|
|
376
|
-
variable_kind=cls.VariableKind.LIST,
|
|
377
|
-
**kwargs
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
@classmethod
|
|
381
|
-
def vector_value(cls, key: str, value: List[float], **kwargs) -> 'Answer':
|
|
382
|
-
"""Create a simple float answer (no fraction conversion)"""
|
|
383
|
-
return cls(
|
|
384
|
-
key=key,
|
|
385
|
-
value=value,
|
|
386
|
-
variable_kind=cls.VariableKind.VECTOR,
|
|
387
|
-
**kwargs
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
@classmethod
|
|
391
|
-
def dropdown(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
|
|
392
|
-
"""Create a dropdown answer with wrong answer choices (baffles)"""
|
|
393
|
-
return cls(
|
|
394
|
-
key=key,
|
|
395
|
-
value=value,
|
|
396
|
-
kind=cls.AnswerKind.MULTIPLE_DROPDOWN,
|
|
397
|
-
baffles=baffles,
|
|
398
|
-
**kwargs
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
@classmethod
|
|
402
|
-
def multiple_choice(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
|
|
403
|
-
"""Create a multiple choice answer with wrong answer choices (baffles)"""
|
|
404
|
-
return cls(
|
|
405
|
-
key=key,
|
|
406
|
-
value=value,
|
|
407
|
-
kind=cls.AnswerKind.MULTIPLE_ANSWER,
|
|
408
|
-
baffles=baffles,
|
|
409
|
-
**kwargs
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
@classmethod
|
|
413
|
-
def essay(cls, key: str, **kwargs) -> 'Answer':
|
|
414
|
-
"""Create an essay question (no specific correct answer)"""
|
|
415
|
-
return cls(
|
|
416
|
-
key=key,
|
|
417
|
-
value="", # Essays don't have predetermined answers
|
|
418
|
-
kind=cls.AnswerKind.ESSAY,
|
|
419
|
-
**kwargs
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
@classmethod
|
|
423
|
-
def matrix(cls, key: str, value: np.array|List, **kwargs ):
|
|
424
|
-
return MatrixAnswer(
|
|
425
|
-
key=key,
|
|
426
|
-
value=value,
|
|
427
|
-
variable_kind=cls.VariableKind.MATRIX
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
@staticmethod
|
|
431
|
-
def _to_fraction(x):
|
|
432
|
-
"""Convert int/float/decimal.Decimal/fractions.Fraction/str('a/b' or decimal) to fractions.Fraction exactly."""
|
|
433
|
-
log.debug(f"x: {x} {x.__class__}")
|
|
434
|
-
if isinstance(x, fractions.Fraction):
|
|
435
|
-
return x
|
|
436
|
-
if isinstance(x, int):
|
|
437
|
-
return fractions.Fraction(x, 1)
|
|
438
|
-
if isinstance(x, decimal.Decimal):
|
|
439
|
-
# exact conversion of decimal.Decimal to fractions.Fraction
|
|
440
|
-
sign, digits, exp = x.as_tuple()
|
|
441
|
-
n = 0
|
|
442
|
-
for d in digits:
|
|
443
|
-
n = n * 10 + d
|
|
444
|
-
n = -n if sign else n
|
|
445
|
-
if exp >= 0:
|
|
446
|
-
return fractions.Fraction(n * (10 ** exp), 1)
|
|
447
|
-
else:
|
|
448
|
-
return fractions.Fraction(n, 10 ** (-exp))
|
|
449
|
-
if isinstance(x, str):
|
|
450
|
-
s = x.strip()
|
|
451
|
-
if '/' in s:
|
|
452
|
-
a, b = s.split('/', 1)
|
|
453
|
-
return fractions.Fraction(int(a.strip()), int(b.strip()))
|
|
454
|
-
return fractions.Fraction(decimal.Decimal(s))
|
|
455
|
-
# float or other numerics
|
|
456
|
-
return fractions.Fraction(decimal.Decimal(str(x)))
|
|
457
|
-
|
|
458
|
-
@staticmethod
|
|
459
|
-
def accepted_strings(
|
|
460
|
-
value,
|
|
461
|
-
*,
|
|
462
|
-
allow_integer=True, # allow "whole numbers as whole numbers"
|
|
463
|
-
allow_simple_fraction=True, # allow simple a/b when denominator small
|
|
464
|
-
max_denominator=720, # how "simple" the fraction is
|
|
465
|
-
allow_mixed=False, # also allow "1 1/2" for 3/2
|
|
466
|
-
include_spaces=False, # also accept "1 / 2"
|
|
467
|
-
include_fixed_even_if_integer=False # include "1.0000" when value is 1 and fixed_decimals is set
|
|
468
|
-
):
|
|
469
|
-
"""
|
|
470
|
-
Return a sorted list of strings you can paste into Canvas as alternate correct answers.
|
|
471
|
-
"""
|
|
472
|
-
decimal.getcontext().prec = max(34, (Answer.DEFAULT_ROUNDING_DIGITS or 0) + 10)
|
|
473
|
-
f = Answer._to_fraction(value)
|
|
474
|
-
outs = set()
|
|
475
|
-
|
|
476
|
-
# Integer form
|
|
477
|
-
if f.denominator == 1 and allow_integer:
|
|
478
|
-
outs.add(str(f.numerator))
|
|
479
|
-
if include_fixed_even_if_integer:
|
|
480
|
-
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS) # 1e-<fixed_decimals>
|
|
481
|
-
d = decimal.Decimal(f.numerator).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
482
|
-
outs.add(format(d, 'f'))
|
|
483
|
-
|
|
484
|
-
# Fixed-decimal form (exactly N decimals)
|
|
485
|
-
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
|
|
486
|
-
d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
487
|
-
outs.add(format(d, 'f'))
|
|
488
|
-
|
|
489
|
-
# Trimmed decimal (no trailing zeros; up to max_trimmed_decimals)
|
|
490
|
-
if Answer.DEFAULT_ROUNDING_DIGITS:
|
|
491
|
-
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
|
|
492
|
-
d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
493
|
-
s = format(d, 'f').rstrip('0').rstrip('.')
|
|
494
|
-
# ensure we keep leading zero like "0.5"
|
|
495
|
-
if s.startswith('.'):
|
|
496
|
-
s = '0' + s
|
|
497
|
-
if s == '-0': # tidy negative zero
|
|
498
|
-
s = '0'
|
|
499
|
-
outs.add(s)
|
|
500
|
-
|
|
501
|
-
# Simple fraction (reduced, with small denominator)
|
|
502
|
-
if allow_simple_fraction:
|
|
503
|
-
fr = f.limit_denominator(max_denominator)
|
|
504
|
-
if fr == f:
|
|
505
|
-
a, b = fr.numerator, fr.denominator
|
|
506
|
-
outs.add(f"{a}/{b}")
|
|
507
|
-
if include_spaces:
|
|
508
|
-
outs.add(f"{a} / {b}")
|
|
509
|
-
if allow_mixed and b != 1 and abs(a) > b:
|
|
510
|
-
sign = '-' if a < 0 else ''
|
|
511
|
-
A = abs(a)
|
|
512
|
-
whole, rem = divmod(A, b)
|
|
513
|
-
outs.add(f"{sign}{whole} {rem}/{b}")
|
|
514
|
-
|
|
515
|
-
return sorted(outs, key=lambda s: (len(s), s))
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
class MatrixAnswer(Answer):
|
|
519
|
-
def get_for_canvas(self, single_answer=False) -> List[Dict]:
|
|
520
|
-
canvas_answers = []
|
|
521
|
-
|
|
522
|
-
"""
|
|
523
|
-
The core idea is that we will walk through and generate each one for X_i,j
|
|
524
|
-
|
|
525
|
-
The big remaining question is how we will get all these names to the outside world.
|
|
526
|
-
It might have to be a pretty big challenge, or rather re-write.
|
|
527
|
-
"""
|
|
528
|
-
|
|
529
|
-
# The core idea is that we will be walking through and generating a per-index set of answers.
|
|
530
|
-
# Boy will this get messy. Poor canvas.
|
|
531
|
-
for i, j in np.ndindex(self.value.shape):
|
|
532
|
-
entry_strings = self.__class__.accepted_strings(
|
|
533
|
-
self.value[i,j],
|
|
534
|
-
allow_integer=True,
|
|
535
|
-
allow_simple_fraction=True,
|
|
536
|
-
max_denominator=3 * 4 * 5,
|
|
537
|
-
allow_mixed=True,
|
|
538
|
-
include_spaces=False,
|
|
539
|
-
include_fixed_even_if_integer=True
|
|
540
|
-
)
|
|
541
|
-
canvas_answers.extend(
|
|
542
|
-
[
|
|
543
|
-
{
|
|
544
|
-
"blank_id": f"{self.key}_{i}_{j}", # Give each an index associated with it so we can track it
|
|
545
|
-
"answer_text": answer_string,
|
|
546
|
-
"answer_weight": 100 if self.correct else 0,
|
|
547
|
-
}
|
|
548
|
-
for answer_string in entry_strings
|
|
549
|
-
]
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
return canvas_answers
|
|
553
|
-
|
|
554
|
-
def get_ast_element(self, label=None):
|
|
555
|
-
from QuizGenerator.contentast import ContentAST
|
|
556
|
-
|
|
557
|
-
log.debug(f"self.value: {self.value}")
|
|
558
|
-
|
|
559
|
-
data = [
|
|
560
|
-
[
|
|
561
|
-
ContentAST.Answer(
|
|
562
|
-
Answer.float_value(
|
|
563
|
-
key=f"{self.key}_{i}_{j}",
|
|
564
|
-
value=self.value[i,j]
|
|
565
|
-
)
|
|
566
|
-
)
|
|
567
|
-
for j in range(self.value.shape[1])
|
|
568
|
-
]
|
|
569
|
-
for i in range(self.value.shape[0])
|
|
570
|
-
]
|
|
571
|
-
table = ContentAST.Table(data)
|
|
572
|
-
|
|
573
|
-
if label is not None:
|
|
574
|
-
return ContentAST.Container([
|
|
575
|
-
ContentAST.Text(f"{label} = "),
|
|
576
|
-
table
|
|
577
|
-
])
|
|
578
|
-
else:
|
|
579
|
-
return table
|