Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions course/course/course.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<problem url_name="matrix5"/>
<problem url_name="matrix6"/>
<problem url_name="matrix7"/>
<problem url_name="matrix8"/>
<problem url_name="matrixtech"/>
</vertical>
</sequential>
Expand Down
44 changes: 44 additions & 0 deletions course/problem/matrix8.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<problem display_name="Matrix Grader: Noncommuting Variables" showanswer="always" weight="10" attempts="">

<p>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.</p>

<p>[mathjaxinline]A[/mathjaxinline] is a scalar function. Write down its gradient.</p>

<p>Suggested inputs:</p>

<ul>
<li><code>nabla * A</code> (correct)</li>
<li><code>A * nabla</code> (incorrect)</li>
<li><code>nabla * A + 1</code> (incorrect, but looks like a matrix + a scalar)</li>
</ul>

<script type="text/python" system_path="python_lib">
from mitxgraders import *
grader = MatrixGrader(
variables=['nabla', 'A'],
sample_from={
'nabla': RealMatrices(),
'A': RealMatrices()
},
max_array_dim=0, # Students can't enter their own matrices
suppress_matrix_messages=True # Turns off all matrix error messages
)
</script>

<p style="display:inline">Gradient of \(A\) = </p>
<customresponse cfn="grader" expect="nabla * A" inline="1">
<textline math="true" preprocessorClassName="MJxPrep" preprocessorSrc="/static/MJxPrep.js" size="40" inline="1" />
</customresponse>


<h3>Resources</h3>
<ul>
<li>
<a href="https://github.com/mitodl/mitx-grading-library/tree/master/course/problem/matrix7.xml" target="_blank">View source</a>
</li>
<li>
<a href="https://mitodl.github.io/mitx-grading-library/grading_math/comparer_functions/" target="_blank">Documentation for <code>comparer_functions</code></a>
</li>
</ul>

</problem>
20 changes: 20 additions & 0 deletions docs/grading_math/matrix_grader/matrix_grader.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 21 additions & 4 deletions mitxgraders/formulagrader/matrixgrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions mitxgraders/helpers/calc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions mitxgraders/helpers/calc/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
28 changes: 14 additions & 14 deletions mitxgraders/helpers/calc/specify_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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):
Expand All @@ -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
2 changes: 1 addition & 1 deletion mitxgraders/helpers/validatorfuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
24 changes: 23 additions & 1 deletion tests/formulagrader/test_matrixgrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)')
2 changes: 1 addition & 1 deletion tests/helpers/calc/test_specify_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down