Added symbolic response tests
This commit is contained in:
committed by
Ned Batchelder
parent
e61a6fe787
commit
f62dad2f57
@@ -46,7 +46,7 @@ import sys
|
||||
import pyparsing
|
||||
|
||||
from .registry import TagRegistry
|
||||
from capa.chem import chemcalc
|
||||
from chem import chemcalc
|
||||
import xqueue_interface
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from collections import namedtuple
|
||||
from shapely.geometry import Point, MultiPoint
|
||||
|
||||
# specific library imports
|
||||
from .calc import evaluator, UndefinedVariable
|
||||
from calc import evaluator, UndefinedVariable
|
||||
from .correctmap import CorrectMap
|
||||
from datetime import datetime
|
||||
from .util import *
|
||||
@@ -1043,7 +1043,7 @@ class CustomResponse(LoncapaResponse):
|
||||
messages = self.context['messages']
|
||||
correct_map = CorrectMap()
|
||||
|
||||
overall_message = self.clean_message_html(self.context['overall_message']))
|
||||
overall_message = self.clean_message_html(self.context['overall_message'])
|
||||
correct_map.set_overall_message(overall_message)
|
||||
|
||||
for k in range(len(idset)):
|
||||
@@ -1195,12 +1195,24 @@ class SymbolicResponse(CustomResponse):
|
||||
"""
|
||||
|
||||
response_tag = 'symbolicresponse'
|
||||
max_inputfields = 1
|
||||
|
||||
def setup_response(self):
|
||||
# Symbolic response always uses symmath_check()
|
||||
# If the XML did not specify this, then set it now
|
||||
# Otherwise, we get an error from the superclass
|
||||
self.xml.set('cfn', 'symmath_check')
|
||||
|
||||
# Let CustomResponse do its setup
|
||||
super(SymbolicResponse, self).setup_response()
|
||||
|
||||
def execute_check_function(self, idset, submission):
|
||||
from symmath import symmath_check
|
||||
fn = self.code
|
||||
try:
|
||||
answer_given = submission[0] if (len(idset) == 1) else submission
|
||||
# Since we have limited max_inputfields to 1,
|
||||
# we can assume that there is only one submission
|
||||
answer_given = submission[0]
|
||||
|
||||
ret = symmath_check(
|
||||
self.expect, answer_given,
|
||||
dynamath=self.context.get('dynamath'),
|
||||
|
||||
@@ -734,3 +734,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
|
||||
option_element.text = description
|
||||
|
||||
return input_element
|
||||
|
||||
|
||||
class SymbolicResponseXMLFactory(ResponseXMLFactory):
|
||||
""" Factory for producing <symbolicresponse> xml """
|
||||
|
||||
def create_response_element(self, **kwargs):
|
||||
""" Build the <symbolicresponse> XML element.
|
||||
|
||||
Uses **kwargs:
|
||||
|
||||
*expect*: The correct answer (a sympy string)
|
||||
|
||||
*options*: list of option strings to pass to symmath_check
|
||||
(e.g. 'matrix', 'qbit', 'imaginary', 'numerical')"""
|
||||
|
||||
# Retrieve **kwargs
|
||||
expect = kwargs.get('expect', '')
|
||||
options = kwargs.get('options', [])
|
||||
|
||||
# Symmath check expects a string of options
|
||||
options_str = ",".join(options)
|
||||
|
||||
# Construct the <symbolicresponse> element
|
||||
response_element = etree.Element('symbolicresponse')
|
||||
|
||||
if expect:
|
||||
response_element.set('expect', str(expect))
|
||||
|
||||
if options_str:
|
||||
response_element.set('options', str(options_str))
|
||||
|
||||
return response_element
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
return ResponseXMLFactory.textline_input_xml(**kwargs)
|
||||
|
||||
@@ -10,6 +10,7 @@ import random
|
||||
import unittest
|
||||
import textwrap
|
||||
import mock
|
||||
import textwrap
|
||||
|
||||
from . import test_system
|
||||
|
||||
@@ -184,107 +185,151 @@ class ImageResponseTest(ResponseTest):
|
||||
self.assert_answer_format(problem)
|
||||
|
||||
|
||||
class SymbolicResponseTest(unittest.TestCase):
|
||||
class SymbolicResponseTest(ResponseTest):
|
||||
from response_xml_factory import SymbolicResponseXMLFactory
|
||||
xml_factory_class = SymbolicResponseXMLFactory
|
||||
|
||||
def test_symbolic_response_grade(self):
|
||||
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system)
|
||||
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mn>1</mn>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mn>0</mn>
|
||||
</mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
''',
|
||||
}
|
||||
wrong_answers = {'1_2_1': '2',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
</mstyle>
|
||||
</math>''',
|
||||
}
|
||||
def test_grade_single_input(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="2*x+3*y")
|
||||
|
||||
# Correct answers
|
||||
correct_inputs = [
|
||||
('2x+3y', textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
|
||||
</mstyle></math>""")),
|
||||
|
||||
('x+x+3y', textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mi>x</mi><mo>+</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
|
||||
</mstyle></math>""")),
|
||||
]
|
||||
|
||||
for (input_str, input_mathml) in correct_inputs:
|
||||
self._assert_symbolic_grade(problem, input_str, input_mathml, 'correct')
|
||||
|
||||
# Incorrect answers
|
||||
incorrect_inputs = [
|
||||
('0', ''),
|
||||
('4x+3y', textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>4</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
|
||||
</mstyle></math>""")),
|
||||
]
|
||||
|
||||
for (input_str, input_mathml) in incorrect_inputs:
|
||||
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
|
||||
|
||||
|
||||
def test_complex_number_grade(self):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
options=["matrix", "imaginary"])
|
||||
|
||||
# For LaTeX-style inputs, symmath_check() will try to contact
|
||||
# a server to convert the input to MathML.
|
||||
# We mock out the server, simulating the response that it would give
|
||||
# for this input.
|
||||
import requests
|
||||
d = os.path.dirname(__file__)
|
||||
correct_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_correct.html")).read().decode('utf8')
|
||||
wrong_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_wrong.html")).read().decode('utf8')
|
||||
dirpath = os.path.dirname(__file__)
|
||||
correct_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_correct.html")).read().decode('utf8')
|
||||
wrong_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_wrong.html")).read().decode('utf8')
|
||||
|
||||
# Correct answer
|
||||
with mock.patch.object(requests, 'post') as mock_post:
|
||||
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# send for the correct response input
|
||||
mock_post.return_value.text = correct_snuggletex_response
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
|
||||
self._assert_symbolic_grade(problem,
|
||||
"cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]",
|
||||
textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mrow><mo>(</mo><mi>θ</mi><mo>)</mo></mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mi>i</mi>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo><mi>θ</mi><mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</mo>
|
||||
<mrow>
|
||||
<mo>[</mo>
|
||||
<mtable>
|
||||
<mtr>
|
||||
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<mo>]</mo>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>
|
||||
"""),
|
||||
'correct')
|
||||
|
||||
# Incorrect answer
|
||||
with mock.patch.object(requests, 'post') as mock_post:
|
||||
|
||||
# Simulate what the LaTeX-to-MathML server would
|
||||
# send for the incorrect response input
|
||||
mock_post.return_value.text = wrong_snuggletex_response
|
||||
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
self._assert_symbolic_grade(problem, "2",
|
||||
textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true"><mn>2</mn></mstyle>
|
||||
</math>
|
||||
"""),
|
||||
'incorrect')
|
||||
|
||||
def test_multiple_inputs_exception(self):
|
||||
|
||||
# Should not allow multiple inputs, since we specify
|
||||
# only one "expect" value
|
||||
with self.assertRaises(Exception):
|
||||
problem = self.build_problem(math_display=True,
|
||||
expect="2*x+3*y",
|
||||
num_inputs=3)
|
||||
|
||||
def _assert_symbolic_grade(self, problem,
|
||||
student_input,
|
||||
dynamath_input,
|
||||
expected_correctness):
|
||||
input_dict = {'1_2_1': str(student_input),
|
||||
'1_2_1_dynamath': str(dynamath_input) }
|
||||
|
||||
correct_map = problem.grade_answers(input_dict)
|
||||
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'),
|
||||
expected_correctness)
|
||||
|
||||
|
||||
class OptionResponseTest(ResponseTest):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .calc import evaluator, UndefinedVariable
|
||||
from calc import evaluator
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user