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.
Files changed (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {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