QuizGenerator 0.4.1__py3-none-any.whl → 0.4.3__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.
@@ -850,7 +850,7 @@ class ContentAST:
850
850
  if self.inline:
851
851
  return f"${self.latex}$"
852
852
  else:
853
- return r"$$ \displaystyle " + f"{self.latex}" + r" \; $$"
853
+ return r"$\displaystyle " + f"{self.latex}" + r"$"
854
854
 
855
855
  def render_html(self, **kwargs):
856
856
  if self.inline:
@@ -1048,7 +1048,7 @@ class ContentAST:
1048
1048
  if self.inline and self.bracket_type == "p":
1049
1049
  return f"$\\big(\\begin{{{matrix_env}}} {matrix_content} \\end{{{matrix_env}}}\\big)$"
1050
1050
  else:
1051
- return f"$$\\begin{{{matrix_env}}} {matrix_content} \\end{{{matrix_env}}}$$"
1051
+ return f"$\\begin{{{matrix_env}}} {matrix_content} \\end{{{matrix_env}}}$"
1052
1052
 
1053
1053
  def render_html(self, **kwargs):
1054
1054
  matrix_env = "smallmatrix" if self.inline else f"{self.bracket_type}matrix"
@@ -222,6 +222,4 @@ class WeightCounting_RNN(WeightCounting):
222
222
  return model, ["units", "return_sequences"]
223
223
 
224
224
 
225
- @QuestionRegistry.register()
226
- class ConvolutionCalculation(Question):
227
- pass
225
+ # ConvolutionCalculation is implemented in cnns.py
@@ -49,6 +49,7 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
49
49
  # Configuration
50
50
  self.activation_function = None
51
51
  self.use_bias = kwargs.get("use_bias", True)
52
+ self.param_digits = kwargs.get("param_digits", 1) # Precision for weights/biases
52
53
 
53
54
  # Network parameters (weights and biases)
54
55
  self.W1 = None # Input to hidden weights (num_hidden x num_inputs)
@@ -75,16 +76,19 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
75
76
  def _generate_network(self, weight_range=(-2, 2), input_range=(-3, 3)):
76
77
  """Generate random network parameters and input."""
77
78
  # Generate weights using MatrixQuestion's rounded matrix method
79
+ # Use param_digits to match display precision in tables and explanations
78
80
  self.W1 = self.get_rounded_matrix(
79
81
  (self.num_hidden, self.num_inputs),
80
82
  low=weight_range[0],
81
- high=weight_range[1]
83
+ high=weight_range[1],
84
+ digits_to_round=self.param_digits
82
85
  )
83
86
 
84
87
  self.W2 = self.get_rounded_matrix(
85
88
  (self.num_outputs, self.num_hidden),
86
89
  low=weight_range[0],
87
- high=weight_range[1]
90
+ high=weight_range[1],
91
+ digits_to_round=self.param_digits
88
92
  )
89
93
 
90
94
  # Generate biases
@@ -92,12 +96,14 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
92
96
  self.b1 = self.get_rounded_matrix(
93
97
  (self.num_hidden,),
94
98
  low=weight_range[0],
95
- high=weight_range[1]
99
+ high=weight_range[1],
100
+ digits_to_round=self.param_digits
96
101
  )
97
102
  self.b2 = self.get_rounded_matrix(
98
103
  (self.num_outputs,),
99
104
  low=weight_range[0],
100
- high=weight_range[1]
105
+ high=weight_range[1],
106
+ digits_to_round=self.param_digits
101
107
  )
102
108
  else:
103
109
  self.b1 = np.zeros(self.num_hidden)
@@ -256,7 +262,7 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
256
262
  for i in range(self.num_inputs):
257
263
  left_data.append([
258
264
  ContentAST.Equation(f"x_{i+1}", inline=True),
259
- f"{self.X[i]:.1f}"
265
+ f"{self.X[i]:.1f}" # Inputs are always integers or 1 decimal
260
266
  ])
261
267
 
262
268
  # Weights from input to hidden
@@ -264,14 +270,14 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
264
270
  for i in range(self.num_inputs):
265
271
  left_data.append([
266
272
  ContentAST.Equation(f"w_{{{j+1}{i+1}}}", inline=True),
267
- f"{self.W1[j, i]:.1f}"
273
+ f"{self.W1[j, i]:.{self.param_digits}f}"
268
274
  ])
269
275
 
270
276
  # Weights from hidden to output
271
277
  for i in range(self.num_hidden):
272
278
  left_data.append([
273
279
  ContentAST.Equation(f"w_{i+3}", inline=True),
274
- f"{self.W2[0, i]:.1f}"
280
+ f"{self.W2[0, i]:.{self.param_digits}f}"
275
281
  ])
276
282
 
277
283
  # Right table: Biases, Activations, Training context
@@ -283,14 +289,14 @@ class SimpleNeuralNetworkBase(MatrixQuestion, abc.ABC):
283
289
  for j in range(self.num_hidden):
284
290
  right_data.append([
285
291
  ContentAST.Equation(f"b_{j+1}", inline=True),
286
- f"{self.b1[j]:.1f}"
292
+ f"{self.b1[j]:.{self.param_digits}f}"
287
293
  ])
288
294
 
289
295
  # Output bias
290
296
  if self.use_bias:
291
297
  right_data.append([
292
298
  ContentAST.Equation(r"b_{out}", inline=True),
293
- f"{self.b2[0]:.1f}"
299
+ f"{self.b2[0]:.{self.param_digits}f}"
294
300
  ])
295
301
 
296
302
  # Hidden layer activations (if computed and requested)
@@ -628,11 +634,11 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
628
634
  # Build equation for z_i
629
635
  terms = []
630
636
  for j in range(self.num_inputs):
631
- terms.append(f"({self.W1[i,j]:.1f})({self.X[j]:.1f})")
637
+ terms.append(f"({self.W1[i,j]:.{self.param_digits}f})({self.X[j]:.1f})")
632
638
 
633
639
  z_calc = " + ".join(terms)
634
640
  if self.use_bias:
635
- z_calc += f" + {self.b1[i]:.1f}"
641
+ z_calc += f" + {self.b1[i]:.{self.param_digits}f}"
636
642
 
637
643
  explanation.add_element(ContentAST.Equation(
638
644
  f"z_{i+1} = {z_calc} = {self.z1[i]:.4f}",
@@ -668,11 +674,11 @@ class ForwardPassQuestion(SimpleNeuralNetworkBase):
668
674
 
669
675
  terms = []
670
676
  for j in range(self.num_hidden):
671
- terms.append(f"({self.W2[0,j]:.1f})({self.a1[j]:.4f})")
677
+ terms.append(f"({self.W2[0,j]:.{self.param_digits}f})({self.a1[j]:.4f})")
672
678
 
673
679
  z_out_calc = " + ".join(terms)
674
680
  if self.use_bias:
675
- z_out_calc += f" + {self.b2[0]:.1f}"
681
+ z_out_calc += f" + {self.b2[0]:.{self.param_digits}f}"
676
682
 
677
683
  explanation.add_element(ContentAST.Equation(
678
684
  f"z_{{out}} = {z_out_calc} = {self.z2[0]:.4f}",
@@ -1197,7 +1203,7 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1197
1203
  # Hidden layer
1198
1204
  z1_0 = self.W1[0, 0] * self.X[0] + self.W1[0, 1] * self.X[1] + self.b1[0]
1199
1205
  explanation.add_element(ContentAST.Equation(
1200
- f"z_1 = w_{{11}} x_1 + w_{{12}} x_2 + b_1 = {self.W1[0,0]:.1f} \\cdot {self.X[0]:.1f} + {self.W1[0,1]:.1f} \\cdot {self.X[1]:.1f} + {self.b1[0]:.1f} = {self.z1[0]:.4f}",
1206
+ f"z_1 = w_{{11}} x_1 + w_{{12}} x_2 + b_1 = {self.W1[0,0]:.{self.param_digits}f} \\cdot {self.X[0]:.1f} + {self.W1[0,1]:.{self.param_digits}f} \\cdot {self.X[1]:.1f} + {self.b1[0]:.{self.param_digits}f} = {self.z1[0]:.4f}",
1201
1207
  inline=False
1202
1208
  ))
1203
1209
 
@@ -1215,7 +1221,7 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1215
1221
  # Output (pre-activation)
1216
1222
  z2 = self.W2[0, 0] * self.a1[0] + self.W2[0, 1] * self.a1[1] + self.b2[0]
1217
1223
  explanation.add_element(ContentAST.Equation(
1218
- f"z_{{out}} = w_3 h_1 + w_4 h_2 + b_2 = {self.W2[0,0]:.1f} \\cdot {self.a1[0]:.4f} + {self.W2[0,1]:.1f} \\cdot {self.a1[1]:.4f} + {self.b2[0]:.1f} = {self.z2[0]:.4f}",
1224
+ f"z_{{out}} = w_3 h_1 + w_4 h_2 + b_2 = {self.W2[0,0]:.{self.param_digits}f} \\cdot {self.a1[0]:.4f} + {self.W2[0,1]:.{self.param_digits}f} \\cdot {self.a1[1]:.4f} + {self.b2[0]:.{self.param_digits}f} = {self.z2[0]:.4f}",
1219
1225
  inline=False
1220
1226
  ))
1221
1227
 
@@ -1291,13 +1297,13 @@ class EndToEndTrainingQuestion(SimpleNeuralNetworkBase):
1291
1297
 
1292
1298
  new_w3 = self.new_W2[0, 0]
1293
1299
  explanation.add_element(ContentAST.Equation(
1294
- f"w_3^{{new}} = w_3 - \\alpha \\frac{{\\partial L}}{{\\partial w_3}} = {self.W2[0,0]:.1f} - {self.learning_rate} \\cdot {grad_w3:.4f} = {new_w3:.4f}",
1300
+ f"w_3^{{new}} = w_3 - \\alpha \\frac{{\\partial L}}{{\\partial w_3}} = {self.W2[0,0]:.{self.param_digits}f} - {self.learning_rate} \\cdot {grad_w3:.4f} = {new_w3:.4f}",
1295
1301
  inline=False
1296
1302
  ))
1297
1303
 
1298
1304
  new_w11 = self.new_W1[0, 0]
1299
1305
  explanation.add_element(ContentAST.Equation(
1300
- f"w_{{11}}^{{new}} = w_{{11}} - \\alpha \\frac{{\\partial L}}{{\\partial w_{{11}}}} = {self.W1[0,0]:.1f} - {self.learning_rate} \\cdot {grad_w11:.4f} = {new_w11:.4f}",
1306
+ f"w_{{11}}^{{new}} = w_{{11}} - \\alpha \\frac{{\\partial L}}{{\\partial w_{{11}}}} = {self.W1[0,0]:.{self.param_digits}f} - {self.learning_rate} \\cdot {grad_w11:.4f} = {new_w11:.4f}",
1301
1307
  inline=False
1302
1308
  ))
1303
1309
 
QuizGenerator/question.py CHANGED
@@ -76,14 +76,21 @@ def parse_spacing(spacing_value) -> float:
76
76
 
77
77
  class QuestionRegistry:
78
78
  _registry = {}
79
+ _class_name_to_registered_name = {} # Reverse mapping: ClassName -> registered_name
79
80
  _scanned = False
80
-
81
+
81
82
  @classmethod
82
83
  def register(cls, question_type=None):
83
84
  def decorator(subclass):
84
85
  # Use the provided name or fall back to the class name
85
86
  name = question_type.lower() if question_type else subclass.__name__.lower()
86
87
  cls._registry[name] = subclass
88
+
89
+ # Build reverse mapping from class name to registered name
90
+ # This allows looking up by class name when QR codes store the class name
91
+ class_name = subclass.__name__.lower()
92
+ cls._class_name_to_registered_name[class_name] = name
93
+
87
94
  return subclass
88
95
  return decorator
89
96
 
@@ -97,29 +104,40 @@ class QuestionRegistry:
97
104
  # Check to see if it's in the registry
98
105
  question_key = question_type.lower()
99
106
  if question_key not in cls._registry:
100
- # Try stripping common course prefixes and module paths for backward compatibility
101
- for prefix in ['cst334.', 'cst463.']:
102
- if question_key.startswith(prefix):
103
- stripped_name = question_key[len(prefix):]
104
- if stripped_name in cls._registry:
105
- question_key = stripped_name
106
- break
107
- # Also try extracting just the final class name after dots
108
- if '.' in stripped_name:
109
- final_name = stripped_name.split('.')[-1]
107
+ # Try the reverse mapping from class name to registered name
108
+ # This handles cases where QR codes store class name (e.g., "RNNForwardPass")
109
+ # but the question is registered with a custom name (e.g., "cst463.rnn.forward-pass")
110
+ if question_key in cls._class_name_to_registered_name:
111
+ question_key = cls._class_name_to_registered_name[question_key]
112
+ log.debug(f"Resolved class name '{question_type}' to registered name '{question_key}'")
113
+ else:
114
+ # Try stripping common course prefixes and module paths for backward compatibility
115
+ for prefix in ['cst334.', 'cst463.']:
116
+ if question_key.startswith(prefix):
117
+ stripped_name = question_key[len(prefix):]
118
+ if stripped_name in cls._registry:
119
+ question_key = stripped_name
120
+ break
121
+ # Also try extracting just the final class name after dots
122
+ if '.' in stripped_name:
123
+ final_name = stripped_name.split('.')[-1]
124
+ if final_name in cls._registry:
125
+ question_key = final_name
126
+ break
127
+ else:
128
+ # As a final fallback, try just the last part after dots
129
+ if '.' in question_key:
130
+ final_name = question_key.split('.')[-1]
110
131
  if final_name in cls._registry:
111
132
  question_key = final_name
112
- break
113
- else:
114
- # As a final fallback, try just the last part after dots
115
- if '.' in question_key:
116
- final_name = question_key.split('.')[-1]
117
- if final_name in cls._registry:
118
- question_key = final_name
133
+ elif final_name in cls._class_name_to_registered_name:
134
+ # Try the class name reverse mapping on the final part
135
+ question_key = cls._class_name_to_registered_name[final_name]
136
+ log.debug(f"Resolved class name '{final_name}' to registered name '{question_key}'")
137
+ else:
138
+ raise ValueError(f"Unknown question type: {question_type}")
119
139
  else:
120
140
  raise ValueError(f"Unknown question type: {question_type}")
121
- else:
122
- raise ValueError(f"Unknown question type: {question_type}")
123
141
 
124
142
  new_question : Question = cls._registry[question_key](**kwargs)
125
143
  # Note: Don't call refresh() here - it will be called by get_question()
@@ -530,7 +548,8 @@ class Question(abc.ABC):
530
548
  )
531
549
 
532
550
  # Attach regeneration metadata to the question AST
533
- question_ast.question_class_name = self.__class__.__name__
551
+ # Use the registered name instead of class name for better QR code regeneration
552
+ question_ast.question_class_name = self._get_registered_name()
534
553
  question_ast.generation_seed = actual_seed
535
554
  question_ast.question_version = self.VERSION
536
555
  # Make a copy of config_params so each question AST has its own
@@ -642,6 +661,26 @@ class Question(abc.ABC):
642
661
  return True
643
662
  return False
644
663
 
664
+ def _get_registered_name(self) -> str:
665
+ """
666
+ Get the registered name for this question class.
667
+
668
+ Returns the name used when registering the question with @QuestionRegistry.register(),
669
+ which may be different from the class name (e.g., "cst463.rnn.forward-pass" vs "RNNForwardPass").
670
+
671
+ This is used for QR code generation to ensure regeneration works correctly.
672
+ Falls back to class name if not found in registry (shouldn't happen in practice).
673
+ """
674
+ class_name_lower = self.__class__.__name__.lower()
675
+ registered_name = QuestionRegistry._class_name_to_registered_name.get(class_name_lower)
676
+
677
+ if registered_name is None:
678
+ # Fallback to class name if not found (shouldn't happen but be defensive)
679
+ log.warning(f"Question {self.__class__.__name__} not found in registry reverse mapping, using class name")
680
+ return self.__class__.__name__
681
+
682
+ return registered_name
683
+
645
684
  class QuestionGroup():
646
685
 
647
686
  def __init__(self, questions_in_group: List[Question], pick_once : bool):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
5
5
  Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
6
6
  Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
@@ -2,14 +2,14 @@ QuizGenerator/README.md,sha256=4n16gKyhIAKRBX4VKlpfcK0pyUYJ6Ht08MUsnwgxrZo,145
2
2
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
3
3
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
4
4
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
5
- QuizGenerator/contentast.py,sha256=Em4cnA64Y8_07VruJk_MXwiWcJwqT4-YVf-Lw7uIvYY,68327
5
+ QuizGenerator/contentast.py,sha256=nx0-y3LTyInzX9QheweHhA90zNsdtXie7X-ckYu1_rM,68318
6
6
  QuizGenerator/generate.py,sha256=o2XezoSE0u-qjxYu1_Ofm9Lpkza7M2Tg47C-ClMcPsE,7197
7
7
  QuizGenerator/logging.yaml,sha256=VJCdh26D8e_PNUs4McvvP1ojz9EVjQNifJzfhEk1Mbo,1114
8
8
  QuizGenerator/misc.py,sha256=2vEztj-Kt_0Q2OnynJKC4gL_w7l1MqWsBhhIDOuVD1s,18710
9
9
  QuizGenerator/mixins.py,sha256=zUKTkswq7aoDZ_nGPUdRuvnza8iH8ZCi6IH2Uw-kCvs,18492
10
10
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
11
11
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
12
- QuizGenerator/question.py,sha256=uxDYyrq17JFXQ11S03Px5cyRuPYn4qKT3z7TZn9XSjg,26093
12
+ QuizGenerator/question.py,sha256=pyDQJzqqyEwhnqCl0PqdTNvM7bTPy7m31qdzGfYC0j4,28269
13
13
  QuizGenerator/quiz.py,sha256=toPodXea2UYGgAf4jyor3Gz-gtXYN1YUJFJFQ5u70v4,18718
14
14
  QuizGenerator/regenerate.py,sha256=EvtFhDUXYaWEBCGJ4RW-zN65qj3cMxWa_Y_Rn44WU6c,14282
15
15
  QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
@@ -40,13 +40,13 @@ QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=M0_9wlPhQICje1UdwIb
40
40
  QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
41
41
  QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=-tXeGgqPkctBBUy4RvEPqhv2kfPqoyO2wk-lNJLNWmY,6697
42
42
  QuizGenerator/premade_questions/cst463/models/text.py,sha256=bUiDIzOBEzilUKQjm2yO9ufcvJGY6Gt3qfeNP9UZOrc,6400
43
- QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=acygK-MobvdmwS4UYKVVL4Ey59M1qmq8dITWOT6V-aI,6793
43
+ QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=k_FVS3JYP8MUmghHzpSR5DKJ973TGRa43MRMs3GzSTo,6768
44
44
  QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
45
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=pyTSvibCaLuT0LnYAZfjUoZlzywaPWWAaSZ5VepH04E,44148
45
+ QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=gd5zI-6gmXUiK8GaCOCq0IlOwDg4YS0NlOdoN9v_flo,44810
46
46
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
47
47
  QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=dPn8Sj0yk4m02np62esMKZ7CvcljhYq3Tq51nY9aJnA,29781
48
- quizgenerator-0.4.1.dist-info/METADATA,sha256=Yo9okMZOhafNPqYyPT9V-wovJs8YcSvxeTXCr3U7ILs,7212
49
- quizgenerator-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
- quizgenerator-0.4.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
51
- quizgenerator-0.4.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
52
- quizgenerator-0.4.1.dist-info/RECORD,,
48
+ quizgenerator-0.4.3.dist-info/METADATA,sha256=VZSWAE0DkhzAn4gqwZ_IjpOI_asXF0lyltJL8494sUs,7212
49
+ quizgenerator-0.4.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
+ quizgenerator-0.4.3.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
51
+ quizgenerator-0.4.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
52
+ quizgenerator-0.4.3.dist-info/RECORD,,