CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

526 lines
15 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2017 Adam Hartz <hartz@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import os
  17. import ast
  18. import imp
  19. import math
  20. import cmath
  21. import random
  22. from collections import Sequence
  23. smallbox, _ = csm_tutor.question('smallbox')
  24. defaults = {
  25. 'csq_error_on_unknown_variable': False,
  26. 'csq_input_check': lambda raw, tree: None,
  27. 'csq_render_result': True,
  28. 'csq_syntax': 'base',
  29. 'csq_num_trials': 20,
  30. 'csq_threshold': 1e-9,
  31. 'csq_ratio_check': True,
  32. 'csq_soln': ['6', 'sqrt(2)'],
  33. 'csq_npoints': 1,
  34. 'csq_msg_function': lambda sub: (''),
  35. 'csq_show_check': False
  36. }
  37. default_names = {'pi': [math.pi], 'e': [math.e], 'j': [1j], 'i': [1j]}
  38. def _draw_sqrt(x):
  39. out = r"\sqrt{%s}" % (', '.join(x))
  40. if len(x) != 1:
  41. return out, "sqrt takes exactly one argument"
  42. return out
  43. def _draw_default(context, c):
  44. out = r"%s(%s)" % (c[1], ', '.join(c[2]))
  45. if len(c[2]) == 1 and _implicit_multiplication(context):
  46. return out, "Assuming implicit multiplication."
  47. else:
  48. return out, "Unknown function <tt>%s</tt>." % c[1]
  49. def _default_func(context, names, funcs, c):
  50. if _implicit_multiplication(context):
  51. if len(c[2]) == 1:
  52. # implicit multiplication
  53. val1 = eval_expr(context, names, funcs, c[1])
  54. val2 = eval_expr(context, names, funcs, c[2][0])
  55. return val1 * val2
  56. return random.random()
  57. def _draw_func(name):
  58. def _drawer(args):
  59. return r"%s\left(%s\right)" % (name, ', '.join(args))
  60. return _drawer
  61. def _draw_log(x):
  62. if len(x) == 0:
  63. base = ''
  64. elif len(x) == 1:
  65. base = 'e'
  66. else:
  67. base = x[1]
  68. if any((i in x[0]) for i in ' -+'):
  69. arg = r'\left(%s\right)' % x[0]
  70. else:
  71. arg = x[0]
  72. out = r"\log_{%s}{%s}" % (base, arg)
  73. if len(x) > 2:
  74. return out, "log takes at most 2 arguments"
  75. return out
  76. default_funcs = {
  77. 'atan': (cmath.atan, _draw_func(r'\text{tan}^{-1}')),
  78. 'asin': (cmath.asin, _draw_func(r'\text{sin}^{-1}')),
  79. 'acos': (cmath.acos, _draw_func(r'\text{cos}^{-1}')),
  80. 'tan': (cmath.tan, _draw_func(r'\text{tan}')),
  81. 'sin': (cmath.sin, _draw_func(r'\text{sin}')),
  82. 'cos': (cmath.cos, _draw_func(r'\text{cos}')),
  83. 'log': (cmath.log, _draw_log),
  84. 'sqrt': (cmath.sqrt, _draw_sqrt),
  85. '_default': (_default_func, _draw_default)
  86. }
  87. def _contains(l, test):
  88. if not isinstance(l, list):
  89. return False
  90. elif l[0] == test:
  91. return True
  92. elif l[0] == 'CALL':
  93. return _contains(l[1], test) or any(_contains(i, test) for i in l[2])
  94. else:
  95. return any(_contains(i, test) for i in l[1:])
  96. def eval_expr(context, names, funcs, n):
  97. return _eval_map[n[0]](context, names, funcs, n)
  98. def eval_name(context, names, funcs, n):
  99. return names[n[1]]
  100. def eval_number(context, names, funcs, n):
  101. return ast.literal_eval(n[1])
  102. def eval_binop(func):
  103. def _evaler(context, names, funcs, o):
  104. left = eval_expr(context, names, funcs, o[1])
  105. right = eval_expr(context, names, funcs, o[2])
  106. return func(left, right)
  107. return _evaler
  108. def eval_uminus(context, names, funcs, o):
  109. return -eval_expr(context, names, funcs, o[1])
  110. def eval_uplus(context, names, funcs, o):
  111. return +eval_expr(context, names, funcs, o[1])
  112. def eval_call(context, names, funcs, c):
  113. if c[1][0] == 'NAME' and c[1][1] in funcs:
  114. return funcs[c[1][1]][0](*(eval_expr(context, names, funcs, i)
  115. for i in c[2]))
  116. else:
  117. return funcs['_default'][0](context, names, funcs, c)
  118. def _div(x, y):
  119. x = complex(float(x.real), float(x.imag))
  120. return x / y
  121. _eval_map = {
  122. 'NAME': eval_name,
  123. 'NUMBER': eval_number,
  124. '+': eval_binop(lambda x, y: x + y),
  125. '-': eval_binop(lambda x, y: x - y),
  126. '*': eval_binop(lambda x, y: x * y),
  127. '/': eval_binop(_div),
  128. '^': eval_binop(lambda x, y: x**y),
  129. 'u-': eval_uminus,
  130. 'u+': eval_uplus,
  131. 'CALL': eval_call,
  132. }
  133. def _run_one_test(context, sub, soln, funcs, threshold, ratio=True):
  134. _sub_names = _get_all_names(sub)
  135. _sol_names = _get_all_names(soln)
  136. maps_to_try = _get_all_mappings(context, _sub_names, _sol_names)
  137. for m in maps_to_try:
  138. try:
  139. subm = eval_expr(context, m, funcs, sub)
  140. except:
  141. return False
  142. sol = eval_expr(context, m, funcs, soln)
  143. scale_factor = sol if ratio else 1
  144. if abs(subm-sol)>abs(threshold*scale_factor):
  145. return False
  146. return True
  147. def _get_all_names(tree):
  148. if not isinstance(tree, list):
  149. return []
  150. elif tree[0] == 'NAME':
  151. return [tree[1]]
  152. elif tree[0] == 'CALL':
  153. return _get_all_names(tree[1]) + sum((_get_all_names(i)
  154. for i in tree[2]), [])
  155. else:
  156. return sum((_get_all_names(i) for i in tree[1:]), [])
  157. def _get_random_value():
  158. return random.uniform(1, 30)
  159. def _get_all_mappings(context, soln_names, sub_names):
  160. names = dict(default_names)
  161. names.update(context.get('csq_names', {}))
  162. for n in soln_names:
  163. if n not in names:
  164. names[n] = _get_random_value()
  165. for n in sub_names or []:
  166. if n not in names:
  167. names[n] = _get_random_value()
  168. # map each name to a list of values to test
  169. for n in names:
  170. if callable(n):
  171. names[n] = n()
  172. try:
  173. names[n] = [i for i in names[n]]
  174. except:
  175. names[n] = [names[n]]
  176. # get a list of dictionaries, each representing one mapping to test
  177. return _all_mappings_helper(names)
  178. def _all_mappings_helper(m):
  179. lm = len(m)
  180. if lm == 0:
  181. return {}
  182. n = list(m.keys())[0]
  183. test = [{n: i} for i in m[n]]
  184. if lm == 1:
  185. return test
  186. c = dict(m)
  187. del c[n]
  188. o = _all_mappings_helper(c)
  189. out = []
  190. for i in o:
  191. for j in test:
  192. d = dict(i)
  193. d.update(j)
  194. out.append(d)
  195. return out
  196. def total_points(**info):
  197. return info['csq_npoints']
  198. def _get_syntax_module(context):
  199. syntax = context['csq_syntax']
  200. fname = os.path.join(context['cs_fs_root'], '__QTYPES__', 'expression',
  201. '__SYNTAX__', '%s.py' % syntax)
  202. return imp.load_source(syntax, fname)
  203. def _implicit_multiplication(context):
  204. m = _get_syntax_module(context)
  205. if hasattr(m, 'implicit_multiplication'):
  206. return m.implicit_multiplication
  207. else:
  208. return True
  209. def _get_parser(context):
  210. return _get_syntax_module(context).parser(csm_tools.ply.lex, csm_tools.ply.yacc)
  211. def handle_submission(submissions, **info):
  212. _sub = sub = submissions[info['csq_name']]
  213. solns = info['csq_soln']
  214. parser = _get_parser(info)
  215. test_threshold = info['csq_threshold']
  216. funcs = dict(default_funcs)
  217. funcs.update(info.get('csq_funcs', {}))
  218. sub = parser.parse(sub)
  219. _m = None
  220. if sub is None:
  221. result = False
  222. else:
  223. in_check = info['csq_input_check'](_sub, sub)
  224. if in_check is not None:
  225. result = False
  226. _m = in_check
  227. else:
  228. if not isinstance(solns, list):
  229. solns = [solns]
  230. solns = [parser.parse(i) for i in solns]
  231. ratio = info['csq_ratio_check']
  232. result = False
  233. for soln in solns:
  234. for attempt in range(info['csq_num_trials']):
  235. _sub_names = _get_all_names(sub)
  236. _sol_names = _get_all_names(soln)
  237. if info['csq_error_on_unknown_variable']:
  238. _unique_names = set(_sub_names).difference(_sol_names)
  239. if len(_unique_names) > 0:
  240. _s = "s" if len(_unique_names) > 1 else ""
  241. _v = ", ".join(tree2tex(info, funcs, ["NAME", i])[0]
  242. for i in _unique_names)
  243. _m = "Unknown variable%s: $%s$" % (_s, _v)
  244. result = _run_one_test(info, sub, soln, funcs, test_threshold, ratio)
  245. if not result:
  246. break
  247. if result:
  248. break
  249. if info['csq_show_check']:
  250. if result:
  251. msg = '<img src="BASE/images/check.png" />'
  252. else:
  253. msg = '<img src="BASE/images/cross.png" />'
  254. else:
  255. msg = ''
  256. n = info['csq_name']
  257. msg += info['csq_msg_function'](submissions[info['csq_name']])
  258. msg = info['csm_language'].source_transform_string(info, msg)
  259. msg = ("""\n<script type="text/javascript">"""
  260. """$('#image%s').html(%r);</script>\n""") % (n, msg)
  261. if info['csq_render_result']:
  262. msg += get_display(info, n, sub, False, _m or '')
  263. else:
  264. msg += _m or ''
  265. return {'score': float(result), 'msg': msg}
  266. checktext = "Check Syntax"
  267. def handle_check(submission, **info):
  268. last = submission.get(info['csq_name'])
  269. return get_display(info, info['csq_name'], last)
  270. def render_html(last_log, **info):
  271. name = info['csq_name']
  272. out = smallbox['render_html'](last_log, **info)
  273. out += "\n<span id='image%s'></span>" % (name, )
  274. return out
  275. def get_display(info, name, last, reparse=True, extra_msg=''):
  276. try:
  277. if reparse:
  278. parser = _get_parser(info)
  279. tree = parser.parse(last)
  280. else:
  281. tree = last
  282. funcs = dict(default_funcs)
  283. funcs.update(info.get('csq_funcs', {}))
  284. last = '<displaymath>%s</displaymath>' % tree2tex(info, funcs, tree)[0]
  285. except:
  286. last = '<font color="red">ERROR: Could not interpret your input</font>'
  287. last += csm_language.source_transform_string(info, extra_msg)
  288. out = '<div id="expr%s">Your entry was parsed as:<br/>%s</div>' % (name,
  289. last)
  290. out += '<script type="text/javascript">catsoop.render_all_math($("#expr%s"), true)</script>' % name
  291. return out
  292. def answer_display(**info):
  293. parser = _get_parser(info)
  294. funcs = dict(default_funcs)
  295. funcs.update(info.get('csq_funcs', {}))
  296. if isinstance(info['csq_soln'], str):
  297. a = tree2tex(info, funcs, parser.parse(info['csq_soln']))[0]
  298. out = ("<p>Solution: <tt>%s</tt><br>"
  299. "<div id=\"%s_soln\"><displaymath>%s</displaymath></div>"
  300. "<p>") % (info['csq_soln'], info['csq_name'], a)
  301. else:
  302. out = ("<p><div id=\"%s_soln\">"
  303. "<b>Multiple Possible Solutions:</b>") % info['csq_name']
  304. count = 1
  305. for i in info['csq_soln']:
  306. out += '<hr width="80%" />'
  307. a = tree2tex(info, funcs, parser.parse(i))[0]
  308. out += ('<p>Solution %s: <tt>%s</tt><br>'
  309. '<displaymath>%s</displaymath></p>') % (count, i, a)
  310. count += 1
  311. out += '</div>'
  312. out += '<script type="text/javascript">catsoop.render_all_math($("#expr%s"), true)</script>' % info[
  313. 'csq_name']
  314. return out
  315. # LaTeX Conversion
  316. GREEK_LETTERS = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
  317. 'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi',
  318. 'omicron', 'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi',
  319. 'chi', 'psi', 'omega']
  320. GREEK_DICT = {}
  321. for i in GREEK_LETTERS:
  322. GREEK_DICT[i] = "\\%s" % i
  323. GREEK_DICT[i.upper()] = "\\%s" % i.title()
  324. def name2tex(context, funcs, n):
  325. prec = 5
  326. n = n[1]
  327. s = None
  328. if '_' in n:
  329. n, s = n.split('_')
  330. if n in GREEK_DICT:
  331. n = GREEK_DICT[n]
  332. if s is not None:
  333. if s in GREEK_DICT:
  334. s = GREEK_DICT[s]
  335. return ('%s_{%s}' % (n, s)), prec
  336. return n, prec
  337. def plus2tex(context, funcs, n):
  338. prec = 1
  339. left, lprec = tree2tex(context, funcs, n[1])
  340. right, lprec = tree2tex(context, funcs, n[2])
  341. return "%s + %s" % (left, right), prec
  342. def minus2tex(context, funcs, n):
  343. prec = 1
  344. left, lprec = tree2tex(context, funcs, n[1])
  345. right, lprec = tree2tex(context, funcs, n[2])
  346. return "%s - %s" % (left, right), prec
  347. def div2tex(context, funcs, n):
  348. prec = 2
  349. left, lprec = tree2tex(context, funcs, n[1])
  350. right, lprec = tree2tex(context, funcs, n[2])
  351. return (r"\frac{%s}{%s}" % (left, right)), prec
  352. def times2tex(context, funcs, n):
  353. prec = 2
  354. left, lprec = tree2tex(context, funcs, n[1])
  355. if lprec < prec:
  356. left = r"\left(%s\right)" % left
  357. right, rprec = tree2tex(context, funcs, n[2])
  358. if rprec < prec:
  359. right = r"\left(%s\right)" % right
  360. return r"%s \cdot %s" % (left, right), prec
  361. def exp2tex(context, funcs, n):
  362. prec = 4
  363. left, lprec = tree2tex(context, funcs, n[1])
  364. if lprec < prec:
  365. left = r"\left(%s\right)" % left
  366. right, lprec = tree2tex(context, funcs, n[2])
  367. return (r"%s ^ {%s}" % (left, right)), prec
  368. def uminus2tex(context, funcs, n):
  369. prec = 3
  370. operand, oprec = tree2tex(context, funcs, n[1])
  371. if oprec < prec:
  372. operand = r"\left(%s\right)" % operand
  373. return "-%s" % operand, prec
  374. def uplus2tex(context, funcs, n):
  375. prec = 3
  376. operand, oprec = tree2tex(context, funcs, n[1])
  377. if oprec < prec:
  378. operand = r"\left(%s\right)" % operand
  379. return "+%s" % operand, prec
  380. def call2tex(context, funcs, c):
  381. prec = 6
  382. if c[1][0] == 'NAME' and c[1][1] in funcs:
  383. o = funcs[c[1][1]][1]([tree2tex(context, funcs, i)[0] for i in c[2]])
  384. else:
  385. new_c = list(c)
  386. new_c[1] = tree2tex(context, funcs, c[1])[0]
  387. new_c[2] = [tree2tex(context, funcs, i)[0] for i in c[2]]
  388. o = funcs['_default'][1](context, new_c)
  389. if isinstance(o, str):
  390. pass
  391. elif isinstance(o, Sequence) and len(o) > 1:
  392. o = r"{\color{red} \underbrace{%s}_{\text{%s}}}" % tuple(o[:2])
  393. return o, prec
  394. def _opt_clear_dec_part(x):
  395. n = x.split('.', 1)
  396. if len(n) == 1 or all(i == '0' for i in n[1]):
  397. return n[0]
  398. return '.'.join(n)
  399. def number2tex(context, funcs, x):
  400. n = x[1].lower()
  401. if 'e' in n:
  402. return (r'%s\cdot 10^{%s}' % tuple(n.split('e')), 22)
  403. return x[1], 5
  404. _tree_map = {
  405. 'NAME': name2tex,
  406. 'NUMBER': number2tex,
  407. '+': plus2tex,
  408. '-': minus2tex,
  409. '*': times2tex,
  410. '/': div2tex,
  411. '^': exp2tex,
  412. 'u-': uminus2tex,
  413. 'u+': uplus2tex,
  414. 'CALL': call2tex,
  415. }
  416. def tree2tex(context, funcs, tree):
  417. return _tree_map[tree[0]](context, funcs, tree)