+
+Sometimes, you want to work with non-commuting variables or operations. These can be straightforwardly implemented as matrices, as in the following example. Note that although we secretly have matrix variables, students do not need to know this, and hence we never give error messages that have anything to do with matrices.
+
+[mathjaxinline]A[/mathjaxinline] is a scalar function. Write down its gradient.
+
+Suggested inputs:
+
+
+ nabla * A (correct)
+ A * nabla (incorrect)
+ nabla * A + 1 (incorrect, but looks like a matrix + a scalar)
+
+
+
+
+Gradient of \(A\) =
+
+
+
+
+
+Resources
+
+
+
diff --git a/docs/grading_math/matrix_grader/matrix_grader.md b/docs/grading_math/matrix_grader/matrix_grader.md
index 8baf0c27..be2ec994 100644
--- a/docs/grading_math/matrix_grader/matrix_grader.md
+++ b/docs/grading_math/matrix_grader/matrix_grader.md
@@ -313,6 +313,26 @@ we can use:
```
+### Hiding all messages
+
+MatrixGraders can be used to introduce non-commuting variables. In such a situation, students may not know that the variables they are using are matrices "under the hood", and so we want to suppress all matrix errors and messages. We can do this by setting `suppress_matrix_messages=True`, which overrides `answer_shape_mismatch={'is_raised'}` and `shape_errors`. In the following example, `A` and `B` are secretly matrices that don't commute, but students will never see a matrix error message from typing something like `1+A`.
+
+```
+grader = MatrixGrader(
+ answers=['A*B'],
+ variables=['A', 'B'],
+ sample_from={
+ 'A': RealMatrices(),
+ 'B': RealMatrices()
+ },
+ max_array_dim=0,
+ suppress_matrix_messages=True
+)
+```
+
+Note that this will also suppress error messages from trying to do things like `sin([1, 2])` or `[1, 2]^2`. If your answer needs to take functions of the non-commuting variables, then this option is insufficient.
+
+
## Matrix Functions
MatrixGrader provides all the default functions of `FormulaGrader` (`sin`, `cos`, etc.) plus some extras such as `trans(A)` (transpose) and `det(A)` (determinant). See [Mathematical Functions]('../functions_and_constants.md') for full list.
diff --git a/mitxgraders/formulagrader/matrixgrader.py b/mitxgraders/formulagrader/matrixgrader.py
index c0a14a89..472ca312 100644
--- a/mitxgraders/formulagrader/matrixgrader.py
+++ b/mitxgraders/formulagrader/matrixgrader.py
@@ -11,7 +11,7 @@
from mitxgraders.helpers.validatorfuncs import NonNegative
from mitxgraders.helpers.calc import MathArray, within_tolerance, identity
from mitxgraders.helpers.calc.exceptions import (
- CalcError, MathArrayShapeError as ShapeError)
+ MathArrayShapeError as ShapeError, MathArrayError, DomainError, ArgumentShapeError)
from mitxgraders.helpers.calc.mathfuncs import (
merge_dicts, ARRAY_ONLY_FUNCTIONS)
@@ -55,6 +55,9 @@ class MatrixGrader(FormulaGrader):
'shape': Type and shape information about the expected and
received objects is revealed.
+ suppress_matrix_messages (bool): If True, suppresses all matrix-related
+ error messages from being displayed. Overrides shape_errors=True and
+ is_raised=True. Defaults to False.
"""
# merge_dicts does not mutate the originals
@@ -69,6 +72,7 @@ def schema_config(self):
Required('max_array_dim', default=1): NonNegative(int),
Required('negative_powers', default=True): bool,
Required('shape_errors', default=True): bool,
+ Required('suppress_matrix_messages', default=False): bool,
Required('answer_shape_mismatch', default={
'is_raised': True,
'msg_detail': 'type'
@@ -88,15 +92,27 @@ def check_response(self, answer, student_input, **kwargs):
with MathArray.enable_negative_powers(self.config['negative_powers']):
result = super(MatrixGrader, self).check_response(answer, student_input, **kwargs)
except ShapeError as err:
- if self.config['shape_errors']:
+ if self.config['suppress_matrix_messages']:
+ return {'ok': False, 'msg': '', 'grade_decimal': 0}
+ elif self.config['shape_errors']:
raise
else:
return {'ok': False, 'msg': err.message, 'grade_decimal': 0}
except InputTypeError as err:
- if self.config['answer_shape_mismatch']['is_raised']:
+ if self.config['suppress_matrix_messages']:
+ return {'ok': False, 'msg': '', 'grade_decimal': 0}
+ elif self.config['answer_shape_mismatch']['is_raised']:
raise
else:
return {'ok': False, 'grade_decimal': 0, 'msg': err.message}
+ except (ArgumentShapeError, MathArrayError) as err:
+ # If we're using matrix quantities for noncommutative scalars, we
+ # might get an ArgumentShapeError from using functions of matrices,
+ # or a MathArrayError from taking a funny power of a matrix.
+ # Suppress these too.
+ if self.config['suppress_matrix_messages']:
+ return {'ok': False, 'msg': '', 'grade_decimal': 0}
+ raise
return result
@staticmethod
@@ -137,7 +153,7 @@ def validate_student_input_shape(student_input, expected_shape, detail):
"of incorrect shape".format(expected, received))
else:
msg = ("Expected answer to be a {0}, but input is a {1}"
- .format(expected, received))
+ .format(expected, received))
raise InputTypeError(msg)
@@ -147,6 +163,7 @@ def get_comparer_utils(self):
"""Get the utils for comparer function."""
def _within_tolerance(x, y):
return within_tolerance(x, y, self.config['tolerance'])
+
def _validate_shape(student_input, shape):
detail = self.config['answer_shape_mismatch']['msg_detail']
return self.validate_student_input_shape(student_input, shape, detail)
diff --git a/mitxgraders/helpers/calc/exceptions.py b/mitxgraders/helpers/calc/exceptions.py
index 6984588b..f34d1f28 100644
--- a/mitxgraders/helpers/calc/exceptions.py
+++ b/mitxgraders/helpers/calc/exceptions.py
@@ -54,6 +54,12 @@ class ArgumentError(DomainError):
"""
pass
+class ArgumentShapeError(DomainError):
+ """
+ Raised when the wrong type of argument is passed to a function
+ """
+ pass
+
class MathArrayError(CalcError):
"""
Thrown by MathArray when anticipated errors are made.
diff --git a/mitxgraders/helpers/calc/expressions.py b/mitxgraders/helpers/calc/expressions.py
index 9d1d2612..9b4f58c3 100644
--- a/mitxgraders/helpers/calc/expressions.py
+++ b/mitxgraders/helpers/calc/expressions.py
@@ -800,7 +800,7 @@ def eval_function(parse_result, functions):
>>> def h(x, y): return x + y
>>> MathExpression.eval_function(['h', [1, 2, 3]], {"h": h})
Traceback (most recent call last):
- ArgumentError: Wrong number of arguments passed to h. Expected 2, received 3.
+ ArgumentError: Wrong number of arguments passed to h(...): Expected 2 inputs, but received 3.
However, if the function to be evaluated has a truthy 'validated'
property, we assume it does its own validation and we do not check the
@@ -854,8 +854,8 @@ def validate_function_call(func, name, args):
num_args = len(args)
expected = get_number_of_args(func)
if expected != num_args:
- msg = ("Wrong number of arguments passed to {func}. "
- "Expected {num}, received {num2}.")
+ msg = ("Wrong number of arguments passed to {func}(...): "
+ "Expected {num} inputs, but received {num2}.")
raise ArgumentError(msg.format(func=name, num=expected, num2=num_args))
return True
diff --git a/mitxgraders/helpers/calc/specify_domain.py b/mitxgraders/helpers/calc/specify_domain.py
index 89e4d4a6..1e97b237 100644
--- a/mitxgraders/helpers/calc/specify_domain.py
+++ b/mitxgraders/helpers/calc/specify_domain.py
@@ -8,7 +8,7 @@
from voluptuous import Schema, Invalid, Required, Any
from mitxgraders.helpers.validatorfuncs import is_shape_specification
from mitxgraders.baseclasses import ObjectWithSchema
-from mitxgraders.helpers.calc.exceptions import DomainError
+from mitxgraders.helpers.calc.exceptions import ArgumentShapeError, ArgumentError
from mitxgraders.helpers.calc.math_array import (
MathArray, is_numberlike_array, is_square)
from mitxgraders.helpers.calc.formatters import get_description
@@ -156,17 +156,17 @@ class SpecifyDomain(ObjectWithSchema):
... )
True
- If inputs are bad, student-facing DomainErrors are thrown:
+ If inputs are bad, student-facing ArgumentShapeErrors are thrown:
>>> a = MathArray([2, -1, 3])
>>> b = MathArray([-1, 4])
>>> cross(a, b) # doctest: +ELLIPSIS
Traceback (most recent call last):
- DomainError: There was an error evaluating function cross(...)
+ ArgumentShapeError: There was an error evaluating function cross(...)
1st input is ok: received a vector of length 3 as expected
2nd input has an error: received a vector of length 2, expected a vector of length 3
>>> cross(a)
Traceback (most recent call last):
- DomainError: There was an error evaluating function cross(...): expected 2 inputs, but received 1.
+ ArgumentError: Wrong number of arguments passed to cross(...): Expected 2 inputs, but received 1.
To specify that an input should be a an array of specific size, use a list or tuple
for that shape value. Below, [3, 2] specifies a 3 by 2 matrix (the tuple
@@ -178,7 +178,7 @@ class SpecifyDomain(ObjectWithSchema):
>>> square_mat = MathArray([[1, 2], [3, 4]])
>>> f(1, 2, 3, square_mat) # doctest: +ELLIPSIS
Traceback (most recent call last):
- DomainError: There was an error evaluating function f(...)
+ ArgumentShapeError: There was an error evaluating function f(...)
1st input is ok: received a scalar as expected
2nd input has an error: received a scalar, expected a matrix of shape (rows: 3, cols: 2)
3rd input has an error: received a scalar, expected a vector of length 2
@@ -207,7 +207,7 @@ def make_decorator(*shapes, **kwargs):
"""
Constructs the decorator that validates inputs.
- This method is NOT author-facing; its inputs undero no validation.
+ This method is NOT author-facing; its inputs undergo no validation.
Used internally in mitxgraders library.
"""
@@ -218,14 +218,15 @@ def make_decorator(*shapes, **kwargs):
# can't use @wraps, func might be a numpy ufunc
def decorator(func):
func_name = display_name if display_name else func.__name__
+
def _func(*args):
if len(shapes) != len(args):
- msg = ("There was an error evaluating function "
- "{func_name}(...): expected {expected} inputs, but "
- "received {received}."
+ # Use the same response as in validate_function_call in expressions.py
+ msg = ("Wrong number of arguments passed to {func_name}(...): "
+ "Expected {expected} inputs, but received {received}."
.format(func_name=func_name, expected=len(shapes), received=len(args))
- )
- raise DomainError(msg)
+ )
+ raise ArgumentError(msg)
errors = []
for schema, arg in zip(schemas, args):
@@ -246,16 +247,15 @@ def _func(*args):
else:
expected = get_shape_description(shape)
lines.append('{0} input is ok: received a {1} as expected'
- .format(ordinal, expected))
+ .format(ordinal, expected))
message = "\n".join(lines)
- raise DomainError(message)
+ raise ArgumentShapeError(message)
_func.__name__ = func.__name__
_func.validated = True
return _func
-
return decorator
specify_domain = SpecifyDomain
diff --git a/mitxgraders/helpers/validatorfuncs.py b/mitxgraders/helpers/validatorfuncs.py
index 1b336a12..deb4db14 100644
--- a/mitxgraders/helpers/validatorfuncs.py
+++ b/mitxgraders/helpers/validatorfuncs.py
@@ -30,7 +30,7 @@ def PercentageString(value):
return "{percent}%".format(percent=percent)
except Invalid:
raise
- except:
+ except Exception:
pass
raise Invalid("Not a valid percentage string")
diff --git a/tests/formulagrader/test_matrixgrader.py b/tests/formulagrader/test_matrixgrader.py
index c5ccc722..d084e3f9 100644
--- a/tests/formulagrader/test_matrixgrader.py
+++ b/tests/formulagrader/test_matrixgrader.py
@@ -3,7 +3,7 @@
from mitxgraders import (MatrixGrader, RealMatrices, RealVectors, ComplexRectangle)
from mitxgraders.formulagrader.matrixgrader import InputTypeError
from mitxgraders.helpers.calc.exceptions import (
- DomainError, MathArrayError,
+ DomainError, MathArrayError, ArgumentError,
MathArrayShapeError as ShapeError, UnableToParse
)
from mitxgraders.helpers.calc.math_array import identity, equal_as_arrays
@@ -240,3 +240,25 @@ def test_wrong_answer_type_error_messages_with_scalars():
def test_validate_student_input_shape_edge_case():
with raises(AttributeError):
MatrixGrader.validate_student_input_shape([1, 2], (2,), 'type')
+
+def test_suppress_matrix_messages():
+ grader = MatrixGrader(
+ answers='[1, 2, 3]',
+ answer_shape_mismatch=dict(
+ is_raised=True, # Overridden by suppress_matrix_messages
+ ),
+ shape_errors=True, # Overridden by suppress_matrix_messages
+ suppress_matrix_messages=True
+ )
+ assert grader(None, '10')['ok'] is False
+ assert grader(None, '10')['msg'] == ''
+ assert grader(None, '[1, 2, 3] + 1')['ok'] is False
+ assert grader(None, '[1, 2, 3] + 1')['msg'] == ''
+ assert grader(None, '[1, 2, 3, 4]')['ok'] is False
+ assert grader(None, '[1, 2, 3, 4]')['msg'] == ''
+ assert grader(None, 'sin([1, 2, 3])')['ok'] is False
+ assert grader(None, '[1, 2, 3]^1.3')['ok'] is False
+
+ # Note that we haven't suppressed all errors:
+ with raises(ArgumentError):
+ grader(None, 'sin(1, 2)')
diff --git a/tests/helpers/calc/test_specify_domain.py b/tests/helpers/calc/test_specify_domain.py
index 714eaf3b..3b28ee5d 100644
--- a/tests/helpers/calc/test_specify_domain.py
+++ b/tests/helpers/calc/test_specify_domain.py
@@ -85,7 +85,7 @@ def test_incorrect_arguments_raise_errors():
def test_incorrect_number_of_inputs_raises_useful_error():
f = get_somefunc()
- match = 'There was an error evaluating function somefunc\(...\): expected 4 inputs, but received 2.'
+ match = 'Wrong number of arguments passed to somefunc\(...\): Expected 4 inputs, but received 2.'
with raises(DomainError, match=match):
f(1, 2)