QuizGenerator 0.1.4__py3-none-any.whl → 0.3.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/misc.py CHANGED
@@ -6,16 +6,21 @@ import enum
6
6
  import itertools
7
7
  import logging
8
8
  import math
9
+ import numpy as np
9
10
  from typing import List, Dict, Tuple, Any
10
11
 
11
12
  import fractions
12
13
 
14
+ from QuizGenerator.contentast import ContentAST
15
+
13
16
  log = logging.getLogger(__name__)
14
17
 
15
18
 
16
- class OutputFormat(enum.Enum):
17
- LATEX = enum.auto(),
18
- CANVAS = enum.auto()
19
+ def fix_negative_zero(value):
20
+ """Convert -0.0 to 0.0 to avoid confusing display."""
21
+ if isinstance(value, (int, float)):
22
+ return 0.0 if value == 0 else value
23
+ return value
19
24
 
20
25
 
21
26
  class Answer:
@@ -38,6 +43,7 @@ class Answer:
38
43
  AUTOFLOAT = enum.auto()
39
44
  LIST = enum.auto()
40
45
  VECTOR = enum.auto()
46
+ MATRIX = enum.auto()
41
47
 
42
48
 
43
49
  def __init__(
@@ -200,6 +206,7 @@ class Answer:
200
206
  }
201
207
  for possible_state in [self.value] #itertools.permutations(self.value)
202
208
  ]
209
+
203
210
  else:
204
211
  # For string answers, check if value is a list of acceptable alternatives
205
212
  if isinstance(self.value, list):
@@ -256,13 +263,16 @@ class Answer:
256
263
 
257
264
  elif self.variable_kind == Answer.VariableKind.AUTOFLOAT:
258
265
  # Round to default precision for readability
259
- return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
266
+ rounded = round(self.value, self.DEFAULT_ROUNDING_DIGITS)
267
+ return f"{fix_negative_zero(rounded)}"
260
268
 
261
269
  elif self.variable_kind == Answer.VariableKind.FLOAT:
262
270
  # Round to default precision
263
271
  if isinstance(self.value, (list, tuple)):
264
- return f"{round(self.value[0], self.DEFAULT_ROUNDING_DIGITS)}"
265
- return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
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)}"
266
276
 
267
277
  elif self.variable_kind == Answer.VariableKind.INT:
268
278
  return str(int(self.value))
@@ -272,12 +282,17 @@ class Answer:
272
282
 
273
283
  elif self.variable_kind == Answer.VariableKind.VECTOR:
274
284
  # Format as comma-separated rounded values
275
- return ", ".join(str(round(v, self.DEFAULT_ROUNDING_DIGITS)) for v in self.value)
285
+ return ", ".join(str(fix_negative_zero(round(v, self.DEFAULT_ROUNDING_DIGITS))) for v in self.value)
276
286
 
277
287
  else:
278
288
  # Default: use display or value
279
289
  return str(self.display if hasattr(self, 'display') else self.value)
280
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
+
281
296
  # Factory methods for common answer types
282
297
  @classmethod
283
298
  def binary_hex(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
@@ -404,9 +419,18 @@ class Answer:
404
419
  **kwargs
405
420
  )
406
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
+
407
430
  @staticmethod
408
431
  def _to_fraction(x):
409
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__}")
410
434
  if isinstance(x, fractions.Fraction):
411
435
  return x
412
436
  if isinstance(x, int):
@@ -488,4 +512,68 @@ class Answer:
488
512
  whole, rem = divmod(A, b)
489
513
  outs.add(f"{sign}{whole} {rem}/{b}")
490
514
 
491
- return sorted(outs, key=lambda s: (len(s), s))
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 i in range(self.value.shape[0])
568
+ ]
569
+ for j in range(self.value.shape[1])
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
QuizGenerator/mixins.py CHANGED
@@ -32,8 +32,17 @@ class TableQuestionMixin:
32
32
  Returns:
33
33
  ContentAST.Table with the information formatted
34
34
  """
35
+ # Don't convert ContentAST elements to strings - let them render properly
36
+ table_data = []
37
+ for key, value in info_dict.items():
38
+ # Keep ContentAST elements as-is, convert others to strings
39
+ if isinstance(value, ContentAST.Element):
40
+ table_data.append([key, value])
41
+ else:
42
+ table_data.append([key, str(value)])
43
+
35
44
  return ContentAST.Table(
36
- data=[[key, str(value)] for key, value in info_dict.items()],
45
+ data=table_data,
37
46
  transpose=transpose
38
47
  )
39
48
 
@@ -638,7 +638,7 @@ class Segmentation(MemoryAccessQuestion, TableQuestionMixin, BodyTemplatesMixin)
638
638
  f"Since we are in the {self.segment} segment, "
639
639
  f"we see from our table that our bounds are {self.bounds[self.segment]}. "
640
640
  f"Remember that our check for our {self.segment} segment is: ",
641
- f"`if (offset > bounds({self.segment})) : INVALID`",
641
+ f"`if (offset >= bounds({self.segment})) : INVALID`",
642
642
  "which becomes"
643
643
  f"`if ({self.offset:0b} > {self.bounds[self.segment]:0b}) : INVALID`"
644
644
  ]
@@ -15,7 +15,6 @@ from typing import List
15
15
 
16
16
  import matplotlib.pyplot as plt
17
17
 
18
- from QuizGenerator.misc import OutputFormat
19
18
  from QuizGenerator.contentast import ContentAST
20
19
  from QuizGenerator.question import Question, Answer, QuestionRegistry, RegenerableChoiceMixin
21
20
  from QuizGenerator.mixins import TableQuestionMixin, BodyTemplatesMixin
@@ -380,7 +379,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
380
379
  # Return whether this workload is interesting
381
380
  return self.is_interesting()
382
381
 
383
- def get_body(self, output_format: OutputFormat|None = None, *args, **kwargs) -> ContentAST.Section:
382
+ def get_body(self, *args, **kwargs) -> ContentAST.Section:
384
383
  # Create table data for scheduling results
385
384
  table_rows = []
386
385
  for job_id in sorted(self.job_stats.keys()):
@@ -0,0 +1,192 @@
1
+ import abc
2
+ import logging
3
+ import math
4
+ import keras
5
+ import numpy as np
6
+
7
+ from QuizGenerator.misc import MatrixAnswer
8
+ from QuizGenerator.question import Question, QuestionRegistry, Answer
9
+ from QuizGenerator.contentast import ContentAST
10
+ from QuizGenerator.constants import MathRanges
11
+ from QuizGenerator.mixins import TableQuestionMixin
12
+
13
+ from .matrices import MatrixQuestion
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ @QuestionRegistry.register("cst463.attention.forward-pass")
19
+ class AttentionForwardPass(MatrixQuestion, TableQuestionMixin):
20
+
21
+ @staticmethod
22
+ def simple_attention(Q, K, V):
23
+ """
24
+ Q: (seq_len, d_k) - queries
25
+ K: (seq_len, d_k) - keys
26
+ V: (seq_len, d_v) - values
27
+
28
+ Returns: (seq_len, d_v) - attended output
29
+ """
30
+ d_k = Q.shape[1]
31
+
32
+ # Compute attention scores
33
+ scores = Q @ K.T / np.sqrt(d_k)
34
+
35
+ # Softmax to get weights
36
+ attention_weights = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True)
37
+
38
+ # Weighted sum of values
39
+ output = attention_weights @ V
40
+
41
+ return output, attention_weights
42
+
43
+ def refresh(self, *args, **kwargs):
44
+ super().refresh(*args, **kwargs)
45
+
46
+ seq_len = kwargs.get("seq_len", 3)
47
+ d_k = kwargs.get("key_dimension", 1) # key/query dimension
48
+ d_v = kwargs.get("value_dimension", 1) # value dimension
49
+
50
+ # Small integer matrices
51
+ self.Q = self.rng.randint(0, 3, size=(seq_len, d_k))
52
+ self.K = self.rng.randint(0, 3, size=(seq_len, d_k))
53
+ self.V = self.rng.randint(0, 3, size=(seq_len, d_v))
54
+
55
+ self.Q = self.get_rounded_matrix((seq_len, d_k), 0, 3)
56
+ self.K = self.get_rounded_matrix((seq_len, d_k), 0, 3)
57
+ self.V = self.get_rounded_matrix((seq_len, d_v), 0, 3)
58
+
59
+ self.output, self.weights = self.simple_attention(self.Q, self.K, self.V)
60
+
61
+ ## Answers:
62
+ # Q, K, V, output, weights
63
+
64
+ self.answers["weights"] = MatrixAnswer("weights", self.output)
65
+ self.answers["output"] = MatrixAnswer("output", self.output)
66
+
67
+ return True
68
+
69
+ def get_body(self, **kwargs) -> ContentAST.Section:
70
+ body = ContentAST.Section()
71
+
72
+ body.add_element(
73
+ ContentAST.Text("Given the below information about a self attention layer, please calculate the output sequence.")
74
+ )
75
+ body.add_element(
76
+ self.create_info_table(
77
+ {
78
+ "Q": ContentAST.Matrix(self.Q),
79
+ "K": ContentAST.Matrix(self.K),
80
+ "V": ContentAST.Matrix(self.V),
81
+ }
82
+ )
83
+ )
84
+
85
+ body.add_elements([
86
+ ContentAST.LineBreak(),
87
+ self.answers["weights"].get_ast_element(label=f"Weights"),
88
+ ContentAST.LineBreak(),
89
+ self.answers["output"].get_ast_element(label=f"Output"),
90
+ ])
91
+
92
+ return body
93
+
94
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
95
+ explanation = ContentAST.Section()
96
+ digits = Answer.DEFAULT_ROUNDING_DIGITS
97
+
98
+ explanation.add_element(
99
+ ContentAST.Paragraph([
100
+ "Self-attention uses scaled dot-product attention to compute a weighted combination of values based on query-key similarity."
101
+ ])
102
+ )
103
+
104
+ # Step 1: Compute attention scores
105
+ explanation.add_element(
106
+ ContentAST.Paragraph([
107
+ ContentAST.Text("Step 1: Compute attention scores", emphasis=True)
108
+ ])
109
+ )
110
+
111
+ d_k = self.Q.shape[1]
112
+ explanation.add_element(
113
+ ContentAST.Equation(f"\\text{{scores}} = \\frac{{Q K^T}}{{\\sqrt{{d_k}}}} = \\frac{{Q K^T}}{{\\sqrt{{{d_k}}}}}")
114
+ )
115
+
116
+ scores = self.Q @ self.K.T / np.sqrt(d_k)
117
+
118
+ explanation.add_element(
119
+ ContentAST.Paragraph([
120
+ "Raw scores (scaling by ",
121
+ ContentAST.Equation(f'\\sqrt{{{d_k}}}', inline=True),
122
+ " prevents extremely large values):"
123
+ ])
124
+ )
125
+ explanation.add_element(ContentAST.Matrix(np.round(scores, digits)))
126
+
127
+ # Step 2: Apply softmax
128
+ explanation.add_element(
129
+ ContentAST.Paragraph([
130
+ ContentAST.Text("Step 2: Apply softmax to get attention weights", emphasis=True)
131
+ ])
132
+ )
133
+
134
+ explanation.add_element(
135
+ ContentAST.Equation(r"\alpha_{ij} = \frac{\exp(\text{score}_{ij})}{\sum_k \exp(\text{score}_{ik})}")
136
+ )
137
+
138
+ # Show ONE example row
139
+ explanation.add_element(
140
+ ContentAST.Paragraph([
141
+ "Example: Row 0 softmax computation"
142
+ ])
143
+ )
144
+
145
+ row_scores = scores[0]
146
+ exp_scores = np.exp(row_scores)
147
+ sum_exp = exp_scores.sum()
148
+ weights_row = exp_scores / sum_exp
149
+
150
+ exp_terms = " + ".join([f"e^{{{s:.{digits}f}}}" for s in row_scores])
151
+
152
+ explanation.add_element(
153
+ ContentAST.Paragraph([
154
+ f"Denominator = {exp_terms} = {sum_exp:.{digits}f}"
155
+ ])
156
+ )
157
+
158
+ # Format array with proper rounding
159
+ weights_str = "[" + ", ".join([f"{w:.{digits}f}" for w in weights_row]) + "]"
160
+ explanation.add_element(
161
+ ContentAST.Paragraph([
162
+ f"Resulting weights: {weights_str}"
163
+ ])
164
+ )
165
+
166
+ explanation.add_element(
167
+ ContentAST.Paragraph([
168
+ "Complete attention weight matrix:"
169
+ ])
170
+ )
171
+ explanation.add_element(ContentAST.Matrix(np.round(self.weights, digits)))
172
+
173
+ # Step 3: Weighted sum of values
174
+ explanation.add_element(
175
+ ContentAST.Paragraph([
176
+ ContentAST.Text("Step 3: Compute weighted sum of values", emphasis=True)
177
+ ])
178
+ )
179
+
180
+ explanation.add_element(
181
+ ContentAST.Equation(r"\text{output} = \text{weights} \times V")
182
+ )
183
+
184
+ explanation.add_element(
185
+ ContentAST.Paragraph([
186
+ "Final output:"
187
+ ])
188
+ )
189
+ explanation.add_element(ContentAST.Matrix(np.round(self.output, digits)))
190
+
191
+ return explanation
192
+
@@ -0,0 +1,186 @@
1
+ import abc
2
+ import logging
3
+ import math
4
+ import keras
5
+ import numpy as np
6
+
7
+ from QuizGenerator.question import Question, QuestionRegistry
8
+ from QuizGenerator.misc import Answer, MatrixAnswer
9
+ from QuizGenerator.contentast import ContentAST
10
+ from QuizGenerator.constants import MathRanges
11
+ from .matrices import MatrixQuestion
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ @QuestionRegistry.register("cst463.cnn.convolution")
17
+ class ConvolutionCalculation(MatrixQuestion):
18
+
19
+ @staticmethod
20
+ def conv2d_multi_channel(image, kernel, stride=1, padding=0):
21
+ """
22
+ image: (H, W, C_in) - height, width, input channels
23
+ kernel: (K_h, K_w, C_in, C_out) - kernel height, width, input channels, output filters
24
+ Returns: (H_out, W_out, C_out)
25
+ """
26
+ H, W = image.shape
27
+ K_h, K_w, C_out = kernel.shape
28
+
29
+ # Add padding
30
+ if padding > 0:
31
+ image = np.pad(image, ((padding, padding), (padding, padding), (0, 0)), mode='constant')
32
+ H, W = H + 2 * padding, W + 2 * padding
33
+
34
+ # Output dimensions
35
+ H_out = (H - K_h) // stride + 1
36
+ W_out = (W - K_w) // stride + 1
37
+
38
+ output = np.zeros((H_out, W_out, C_out))
39
+
40
+ # Convolve each filter
41
+ for f in range(C_out):
42
+ for i in range(H_out):
43
+ for j in range(W_out):
44
+ h_start = i * stride
45
+ w_start = j * stride
46
+ # Extract receptive field and sum over all input channels
47
+ receptive_field = image[h_start:h_start + K_h, w_start:w_start + K_w]
48
+ output[i, j, f] = np.sum(receptive_field * kernel[:, :, f])
49
+
50
+ return output
51
+
52
+ def refresh(self, *args, **kwargs):
53
+ super().refresh(*args, **kwargs)
54
+
55
+ # num_input_channels = 1
56
+ input_size = kwargs.get("input_size", 4)
57
+ num_filters = kwargs.get("num_filters", 1)
58
+ self.stride = kwargs.get("stride", 1)
59
+ self.padding = kwargs.get("padding", 0)
60
+
61
+ # Small sizes for hand calculation
62
+ self.image = self.get_rounded_matrix((input_size, input_size))
63
+ self.kernel = self.get_rounded_matrix((3, 3, num_filters), -1, 1)
64
+
65
+ self.result = self.conv2d_multi_channel(self.image, self.kernel, stride=self.stride, padding=self.padding)
66
+
67
+ self.answers = {
68
+ f"result_{i}" : MatrixAnswer(f"result_{i}", self.result[:,:,i])
69
+ for i in range(self.result.shape[-1])
70
+ }
71
+
72
+ return True
73
+
74
+ def get_body(self, **kwargs) -> ContentAST.Section:
75
+ body = ContentAST.Section()
76
+
77
+ body.add_elements(
78
+ [
79
+ ContentAST.Text("Given image represented as matrix: "),
80
+ ContentAST.Matrix(self.image, name="image")
81
+ ]
82
+ )
83
+
84
+ body.add_elements(
85
+ [
86
+ ContentAST.Text("And convolution filters: "),
87
+ ] + [
88
+ ContentAST.Matrix(self.kernel[:, :, i], name=f"Filter {i}")
89
+ for i in range(self.kernel.shape[-1])
90
+ ]
91
+ )
92
+
93
+ body.add_element(
94
+ ContentAST.Paragraph(
95
+ [
96
+ f"Calculate the output of the convolution operation. Assume stride = {self.stride} and padding = {self.padding}."
97
+ ]
98
+ )
99
+ )
100
+
101
+ body.add_element(ContentAST.LineBreak())
102
+
103
+ body.add_elements([
104
+ ContentAST.Container([
105
+ self.answers[f"result_{i}"].get_ast_element(label=f"Result of filter {i}"),
106
+ ContentAST.LineBreak()
107
+ ])
108
+ for i in range(self.result.shape[-1])
109
+ ])
110
+
111
+
112
+
113
+ return body
114
+
115
+ def get_explanation(self, **kwargs) -> ContentAST.Section:
116
+ explanation = ContentAST.Section()
117
+ digits = Answer.DEFAULT_ROUNDING_DIGITS
118
+
119
+ explanation.add_element(
120
+ ContentAST.Paragraph([
121
+ "To compute a 2D convolution, we slide the filter across the input image and compute the element-wise product at each position, then sum the results."
122
+ ])
123
+ )
124
+
125
+ explanation.add_element(
126
+ ContentAST.Paragraph([
127
+ f"With stride={self.stride} and padding={self.padding}: ",
128
+ f"stride controls how many pixels the filter moves each step, ",
129
+ f"and padding adds zeros around the border {'(no border in this case)' if self.padding == 0 else f'({self.padding} pixels)'}."
130
+ ])
131
+ )
132
+
133
+ # For each filter, show one detailed example computation
134
+ for f_idx in range(self.kernel.shape[-1]):
135
+ explanation.add_element(
136
+ ContentAST.Paragraph([
137
+ ContentAST.Text(f"Filter {f_idx}:", emphasis=True)
138
+ ])
139
+ )
140
+
141
+ # Show the filter (rounded)
142
+ explanation.add_element(
143
+ ContentAST.Matrix(np.round(self.kernel[:, :, f_idx], digits), name=f"Filter {f_idx}")
144
+ )
145
+
146
+ # Show ONE example computation (position 0,0)
147
+ explanation.add_element(
148
+ ContentAST.Paragraph([
149
+ "Example computation at position (0, 0):"
150
+ ])
151
+ )
152
+
153
+ # Account for padding when extracting receptive field
154
+ if self.padding > 0:
155
+ padded_image = np.pad(self.image, ((self.padding, self.padding), (self.padding, self.padding)), mode='constant')
156
+ receptive_field = padded_image[0:3, 0:3]
157
+ else:
158
+ receptive_field = self.image[0:3, 0:3]
159
+
160
+ computation_steps = []
161
+ for r in range(3):
162
+ row_terms = []
163
+ for c in range(3):
164
+ img_val = receptive_field[r, c]
165
+ kernel_val = self.kernel[r, c, f_idx]
166
+ row_terms.append(f"({img_val:.2f} \\times {kernel_val:.2f})")
167
+ computation_steps.append(" + ".join(row_terms))
168
+
169
+ equation_str = " + ".join(computation_steps)
170
+ result_val = self.result[0, 0, f_idx]
171
+
172
+ explanation.add_element(
173
+ ContentAST.Equation(f"{equation_str} = {result_val:.2f}")
174
+ )
175
+
176
+ # Show the complete output matrix (rounded)
177
+ explanation.add_element(
178
+ ContentAST.Paragraph([
179
+ "Complete output:"
180
+ ])
181
+ )
182
+ explanation.add_element(
183
+ ContentAST.Matrix(np.round(self.result[:, :, f_idx], digits))
184
+ )
185
+
186
+ return explanation
@@ -0,0 +1,24 @@
1
+ #!env python
2
+ import abc
3
+
4
+ import numpy as np
5
+
6
+ from QuizGenerator.question import Question
7
+
8
+
9
+ class MatrixQuestion(Question, abc.ABC):
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ self.default_digits_to_round = kwargs.get("digits_to_round", 2)
13
+
14
+ def refresh(self, rng_seed=None, *args, **kwargs):
15
+ super().refresh(rng_seed=rng_seed, *args, **kwargs)
16
+ self.rng = np.random.RandomState(rng_seed)
17
+
18
+ def get_rounded_matrix(self, shape, low=0, high=1, digits_to_round=None):
19
+ if digits_to_round is None:
20
+ digits_to_round = self.default_digits_to_round
21
+ return np.round(
22
+ (high - low) * self.rng.rand(*shape) + low,
23
+ digits_to_round
24
+ )