diff --git a/.coveragerc b/.coveragerc index f385998076d9..fab115cb37f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,6 @@ data_file = reports/.coverage source = cms common/djangoapps - common/lib/calc common/lib/capa common/lib/xmodule lms diff --git a/.coveragerc-local b/.coveragerc-local index fa3191ced9c9..e2342d8bdab3 100644 --- a/.coveragerc-local +++ b/.coveragerc-local @@ -4,7 +4,6 @@ data_file = reports/.coverage source = cms common/djangoapps - common/lib/calc common/lib/capa common/lib/xmodule lms diff --git a/common/lib/calc/calc/__init__.py b/common/lib/calc/calc/__init__.py deleted file mode 100644 index d79035ca09a3..000000000000 --- a/common/lib/calc/calc/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Ideally, we wouldn't need to pull in all the calc symbols here, -but courses were using 'import calc', so we need this for -backwards compatibility -""" -from __future__ import absolute_import -from .calc import * diff --git a/common/lib/calc/calc/calc.py b/common/lib/calc/calc/calc.py deleted file mode 100644 index 47066e9c0f01..000000000000 --- a/common/lib/calc/calc/calc.py +++ /dev/null @@ -1,504 +0,0 @@ -""" -Parser and evaluator for FormulaResponse and NumericalResponse - -Uses pyparsing to parse. Main function as of now is evaluator(). -""" - -from __future__ import absolute_import -import math -import numbers -import operator - -import numpy -from pyparsing import ( - CaselessLiteral, - Combine, - Forward, - Group, - Literal, - MatchFirst, - Optional, - ParseResults, - Suppress, - Word, - ZeroOrMore, - alphanums, - alphas, - nums, - stringEnd -) - -from . import functions -import six -from functools import reduce - -# Functions available by default -# We use scimath variants which give complex results when needed. For example: -# np.sqrt(-4+0j) = 2j -# np.sqrt(-4) = nan, but -# np.lib.scimath.sqrt(-4) = 2j -DEFAULT_FUNCTIONS = { - 'sin': numpy.sin, - 'cos': numpy.cos, - 'tan': numpy.tan, - 'sec': functions.sec, - 'csc': functions.csc, - 'cot': functions.cot, - 'sqrt': numpy.lib.scimath.sqrt, - 'log10': numpy.lib.scimath.log10, - 'log2': numpy.lib.scimath.log2, - 'ln': numpy.lib.scimath.log, - 'exp': numpy.exp, - 'arccos': numpy.lib.scimath.arccos, - 'arcsin': numpy.lib.scimath.arcsin, - 'arctan': numpy.arctan, - 'arcsec': functions.arcsec, - 'arccsc': functions.arccsc, - 'arccot': functions.arccot, - 'abs': numpy.abs, - 'fact': math.factorial, - 'factorial': math.factorial, - 'sinh': numpy.sinh, - 'cosh': numpy.cosh, - 'tanh': numpy.tanh, - 'sech': functions.sech, - 'csch': functions.csch, - 'coth': functions.coth, - 'arcsinh': numpy.arcsinh, - 'arccosh': numpy.arccosh, - 'arctanh': numpy.lib.scimath.arctanh, - 'arcsech': functions.arcsech, - 'arccsch': functions.arccsch, - 'arccoth': functions.arccoth -} - -DEFAULT_VARIABLES = { - 'i': numpy.complex(0, 1), - 'j': numpy.complex(0, 1), - 'e': numpy.e, - 'pi': numpy.pi, -} - -SUFFIXES = { - '%': 0.01, -} - - -class UndefinedVariable(Exception): - """ - Indicate when a student inputs a variable which was not expected. - """ - pass - - -class UnmatchedParenthesis(Exception): - """ - Indicate when a student inputs a formula with mismatched parentheses. - """ - pass - - -def lower_dict(input_dict): - """ - Convert all keys in a dictionary to lowercase; keep their original values. - - Keep in mind that it is possible (but not useful?) to define different - variables that have the same lowercase representation. It would be hard to - tell which is used in the final dict and which isn't. - """ - return {k.lower(): v for k, v in six.iteritems(input_dict)} - - -# The following few functions define evaluation actions, which are run on lists -# of results from each parse component. They convert the strings and (previously -# calculated) numbers into the number that component represents. - -def super_float(text): - """ - Like float, but with SI extensions. 1k goes to 1000. - """ - if text[-1] in SUFFIXES: - return float(text[:-1]) * SUFFIXES[text[-1]] - else: - return float(text) - - -def eval_number(parse_result): - """ - Create a float out of its string parts. - - e.g. [ '7.13', 'e', '3' ] -> 7130 - Calls super_float above. - """ - return super_float("".join(parse_result)) - - -def eval_atom(parse_result): - """ - Return the value wrapped by the atom. - - In the case of parenthesis, ignore them. - """ - # Find first number in the list - result = next(k for k in parse_result if isinstance(k, numbers.Number)) - return result - - -def eval_power(parse_result): - """ - Take a list of numbers and exponentiate them, right to left. - - e.g. [ 2, 3, 2 ] -> 2^3^2 = 2^(3^2) -> 512 - (not to be interpreted (2^3)^2 = 64) - """ - # `reduce` will go from left to right; reverse the list. - parse_result = reversed( - [k for k in parse_result - if isinstance(k, numbers.Number)] # Ignore the '^' marks. - ) - # Having reversed it, raise `b` to the power of `a`. - power = reduce(lambda a, b: b ** a, parse_result) - return power - - -def eval_parallel(parse_result): - """ - Compute numbers according to the parallel resistors operator. - - BTW it is commutative. Its formula is given by - out = 1 / (1/in1 + 1/in2 + ...) - e.g. [ 1, 2 ] -> 2/3 - - Return NaN if there is a zero among the inputs. - """ - if len(parse_result) == 1: - return parse_result[0] - if 0 in parse_result: - return float('nan') - reciprocals = [1. / e for e in parse_result - if isinstance(e, numbers.Number)] - return 1. / sum(reciprocals) - - -def eval_sum(parse_result): - """ - Add the inputs, keeping in mind their sign. - - [ 1, '+', 2, '-', 3 ] -> 0 - - Allow a leading + or -. - """ - total = 0.0 - current_op = operator.add - for token in parse_result: - if token == '+': - current_op = operator.add - elif token == '-': - current_op = operator.sub - else: - total = current_op(total, token) - return total - - -def eval_product(parse_result): - """ - Multiply the inputs. - - [ 1, '*', 2, '/', 3 ] -> 0.66 - """ - prod = 1.0 - current_op = operator.mul - for token in parse_result: - if token == '*': - current_op = operator.mul - elif token == '/': - current_op = operator.truediv - else: - prod = current_op(prod, token) - return prod - - -def add_defaults(variables, functions, case_sensitive): - """ - Create dictionaries with both the default and user-defined variables. - """ - all_variables = dict(DEFAULT_VARIABLES) - all_functions = dict(DEFAULT_FUNCTIONS) - all_variables.update(variables) - all_functions.update(functions) - - if not case_sensitive: - all_variables = lower_dict(all_variables) - all_functions = lower_dict(all_functions) - - return (all_variables, all_functions) - - -def evaluator(variables, functions, math_expr, case_sensitive=False): - """ - Evaluate an expression; that is, take a string of math and return a float. - - -Variables are passed as a dictionary from string to value. They must be - python numbers. - -Unary functions are passed as a dictionary from string to function. - """ - # No need to go further. - if math_expr.strip() == "": - return float('nan') - - # Parse the tree. - check_parens(math_expr) - math_interpreter = ParseAugmenter(math_expr, case_sensitive) - math_interpreter.parse_algebra() - - # Get our variables together. - all_variables, all_functions = add_defaults(variables, functions, case_sensitive) - - # ...and check them - math_interpreter.check_variables(all_variables, all_functions) - - # Create a recursion to evaluate the tree. - if case_sensitive: - casify = lambda x: x - else: - casify = lambda x: x.lower() # Lowercase for case insens. - - evaluate_actions = { - 'number': eval_number, - 'variable': lambda x: all_variables[casify(x[0])], - 'function': lambda x: all_functions[casify(x[0])](x[1]), - 'atom': eval_atom, - 'power': eval_power, - 'parallel': eval_parallel, - 'product': eval_product, - 'sum': eval_sum - } - - return math_interpreter.reduce_tree(evaluate_actions) - - -def check_parens(formula): - """ - Check that any open parentheses are closed - - Otherwise, raise an UnmatchedParenthesis exception - """ - count = 0 - delta = { - '(': +1, - ')': -1 - } - for index, char in enumerate(formula): - if char in delta: - count += delta[char] - if count < 0: - msg = "Invalid Input: A closing parenthesis was found after segment " + \ - "{}, but there is no matching opening parenthesis before it." - raise UnmatchedParenthesis(msg.format(formula[0:index])) - if count > 0: - msg = "Invalid Input: Parentheses are unmatched. " + \ - "{} parentheses were opened but never closed." - raise UnmatchedParenthesis(msg.format(count)) - - -class ParseAugmenter(object): - """ - Holds the data for a particular parse. - - Retains the `math_expr` and `case_sensitive` so they needn't be passed - around method to method. - Eventually holds the parse tree and sets of variables as well. - """ - def __init__(self, math_expr, case_sensitive=False): - """ - Create the ParseAugmenter for a given math expression string. - - Do the parsing later, when called like `OBJ.parse_algebra()`. - """ - self.case_sensitive = case_sensitive - self.math_expr = math_expr - self.tree = None - self.variables_used = set() - self.functions_used = set() - - def vpa(tokens): - """ - When a variable is recognized, store it in `variables_used`. - """ - varname = tokens[0][0] - self.variables_used.add(varname) - - def fpa(tokens): - """ - When a function is recognized, store it in `functions_used`. - """ - varname = tokens[0][0] - self.functions_used.add(varname) - - self.variable_parse_action = vpa - self.function_parse_action = fpa - - def parse_algebra(self): - """ - Parse an algebraic expression into a tree. - - Store a `pyparsing.ParseResult` in `self.tree` with proper groupings to - reflect parenthesis and order of operations. Leave all operators in the - tree and do not parse any strings of numbers into their float versions. - - Adding the groups and result names makes the `repr()` of the result - really gross. For debugging, use something like - print OBJ.tree.asXML() - """ - # 0.33 or 7 or .34 or 16. - number_part = Word(nums) - inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part) - # pyparsing allows spaces between tokens--`Combine` prevents that. - inner_number = Combine(inner_number) - - # SI suffixes and percent. - number_suffix = MatchFirst(Literal(k) for k in SUFFIXES.keys()) - - # 0.33k or 17 - plus_minus = Literal('+') | Literal('-') - number = Group( - Optional(plus_minus) + - inner_number + - Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) + - Optional(number_suffix) - ) - number = number("number") - - # Predefine recursive variables. - expr = Forward() - - # Handle variables passed in. They must start with a letter - # and may contain numbers and underscores afterward. - inner_varname = Combine(Word(alphas, alphanums + "_") + ZeroOrMore("'")) - # Alternative variable name in tensor format - # Tensor name must start with a letter, continue with alphanums - # Indices may be alphanumeric - # e.g., U_{ijk}^{123} - upper_indices = Literal("^{") + Word(alphanums) + Literal("}") - lower_indices = Literal("_{") + Word(alphanums) + Literal("}") - tensor_lower = Combine(Word(alphas, alphanums) + lower_indices + ZeroOrMore("'")) - tensor_mixed = Combine(Word(alphas, alphanums) + Optional(lower_indices) + upper_indices + ZeroOrMore("'")) - # Test for mixed tensor first, then lower tensor alone, then generic variable name - varname = Group(tensor_mixed | tensor_lower | inner_varname)("variable") - varname.setParseAction(self.variable_parse_action) - - # Same thing for functions. - function = Group(inner_varname + Suppress("(") + expr + Suppress(")"))("function") - function.setParseAction(self.function_parse_action) - - atom = number | function | varname | "(" + expr + ")" - atom = Group(atom)("atom") - - # Do the following in the correct order to preserve order of operation. - pow_term = atom + ZeroOrMore("^" + atom) - pow_term = Group(pow_term)("power") - - par_term = pow_term + ZeroOrMore('||' + pow_term) # 5k || 4k - par_term = Group(par_term)("parallel") - - prod_term = par_term + ZeroOrMore((Literal('*') | Literal('/')) + par_term) # 7 * 5 / 4 - prod_term = Group(prod_term)("product") - - sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3 - sum_term = Group(sum_term)("sum") - - # Finish the recursion. - expr << sum_term # pylint: disable=pointless-statement - self.tree = (expr + stringEnd).parseString(self.math_expr)[0] - - def reduce_tree(self, handle_actions, terminal_converter=None): - """ - Call `handle_actions` recursively on `self.tree` and return result. - - `handle_actions` is a dictionary of node names (e.g. 'product', 'sum', - etc&) to functions. These functions are of the following form: - -input: a list of processed child nodes. If it includes any terminal - nodes in the list, they will be given as their processed forms also. - -output: whatever to be passed to the level higher, and what to - return for the final node. - `terminal_converter` is a function that takes in a token and returns a - processed form. The default of `None` just leaves them as strings. - """ - def handle_node(node): - """ - Return the result representing the node, using recursion. - - Call the appropriate `handle_action` for this node. As its inputs, - feed it the output of `handle_node` for each child node. - """ - if not isinstance(node, ParseResults): - # Then treat it as a terminal node. - if terminal_converter is None: - return node - else: - return terminal_converter(node) - - node_name = node.getName() - if node_name not in handle_actions: # pragma: no cover - raise Exception(u"Unknown branch name '{}'".format(node_name)) - - action = handle_actions[node_name] - handled_kids = [handle_node(k) for k in node] - return action(handled_kids) - - # Find the value of the entire tree. - return handle_node(self.tree) - - def check_variables(self, valid_variables, valid_functions): - """ - Confirm that all the variables used in the tree are valid/defined. - - Otherwise, raise an UndefinedVariable containing all bad variables. - """ - if self.case_sensitive: - casify = lambda x: x - else: - casify = lambda x: x.lower() # Lowercase for case insens. - - bad_vars = set(var for var in self.variables_used - if casify(var) not in valid_variables) - - if bad_vars: - varnames = ", ".join(sorted(bad_vars)) - message = "Invalid Input: {} not permitted in answer as a variable".format(varnames) - - # Check to see if there is a different case version of the variables - caselist = set() - if self.case_sensitive: - for var2 in bad_vars: - for var1 in valid_variables: - if var2.lower() == var1.lower(): - caselist.add(var1) - if len(caselist) > 0: - betternames = ', '.join(sorted(caselist)) - message += " (did you mean " + betternames + "?)" - - raise UndefinedVariable(message) - - bad_funcs = set(func for func in self.functions_used - if casify(func) not in valid_functions) - if bad_funcs: - funcnames = ', '.join(sorted(bad_funcs)) - message = "Invalid Input: {} not permitted in answer as a function".format(funcnames) - - # Check to see if there is a corresponding variable name - if any(casify(func) in valid_variables for func in bad_funcs): - message += " (did you forget to use * for multiplication?)" - - # Check to see if there is a different case version of the function - caselist = set() - if self.case_sensitive: - for func2 in bad_funcs: - for func1 in valid_functions: - if func2.lower() == func1.lower(): - caselist.add(func1) - if len(caselist) > 0: - betternames = ', '.join(sorted(caselist)) - message += " (did you mean " + betternames + "?)" - - raise UndefinedVariable(message) diff --git a/common/lib/calc/calc/functions.py b/common/lib/calc/calc/functions.py deleted file mode 100644 index 830e6faa8563..000000000000 --- a/common/lib/calc/calc/functions.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Provide the mathematical functions that numpy doesn't. - -Specifically, the secant/cosecant/cotangents and their inverses and -hyperbolic counterparts -""" -from __future__ import absolute_import -import numpy - - -# Normal Trig -def sec(arg): - """ - Secant - """ - return 1 / numpy.cos(arg) - - -def csc(arg): - """ - Cosecant - """ - return 1 / numpy.sin(arg) - - -def cot(arg): - """ - Cotangent - """ - return 1 / numpy.tan(arg) - - -# Inverse Trig -# http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Relationships_among_the_inverse_trigonometric_functions -def arcsec(val): - """ - Inverse secant - """ - return numpy.arccos(1. / val) - - -def arccsc(val): - """ - Inverse cosecant - """ - return numpy.arcsin(1. / val) - - -def arccot(val): - """ - Inverse cotangent - """ - if numpy.real(val) < 0: - return -numpy.pi / 2 - numpy.arctan(val) - else: - return numpy.pi / 2 - numpy.arctan(val) - - -# Hyperbolic Trig -def sech(arg): - """ - Hyperbolic secant - """ - return 1 / numpy.cosh(arg) - - -def csch(arg): - """ - Hyperbolic cosecant - """ - return 1 / numpy.sinh(arg) - - -def coth(arg): - """ - Hyperbolic cotangent - """ - return 1 / numpy.tanh(arg) - - -# And their inverses -def arcsech(val): - """ - Inverse hyperbolic secant - """ - return numpy.arccosh(1. / val) - - -def arccsch(val): - """ - Inverse hyperbolic cosecant - """ - return numpy.arcsinh(1. / val) - - -def arccoth(val): - """ - Inverse hyperbolic cotangent - """ - return numpy.arctanh(1. / val) diff --git a/common/lib/calc/calc/preview.py b/common/lib/calc/calc/preview.py deleted file mode 100644 index 5c04177ac3c3..000000000000 --- a/common/lib/calc/calc/preview.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -Provide a `latex_preview` method similar in syntax to `evaluator`. - -That is, given a math string, parse it and render each branch of the result, -always returning valid latex. - -Because intermediate values of the render contain more data than simply the -string of latex, store it in a custom class `LatexRendered`. -""" - -from __future__ import absolute_import -from .calc import DEFAULT_FUNCTIONS, DEFAULT_VARIABLES, SUFFIXES, ParseAugmenter -from functools import reduce - - -class LatexRendered(object): - """ - Data structure to hold a typeset representation of some math. - - Fields: - -`latex` is a generated, valid latex string (as if it were standalone). - -`sans_parens` is usually the same as `latex` except without the outermost - parens (if applicable). - -`tall` is a boolean representing if the latex has any elements extending - above or below a normal height, specifically things of the form 'a^b' and - '\frac{a}{b}'. This affects the height of wrapping parenthesis. - """ - def __init__(self, latex, parens=None, tall=False): - """ - Instantiate with the latex representing the math. - - Optionally include parenthesis to wrap around it and the height. - `parens` must be one of '(', '[' or '{'. - `tall` is a boolean (see note above). - """ - self.latex = latex - self.sans_parens = latex - self.tall = tall - - # Generate parens and overwrite `self.latex`. - if parens is not None: - left_parens = parens - if left_parens == '{': - left_parens = r'\{' - - pairs = {'(': ')', - '[': ']', - r'\{': r'\}'} - if left_parens not in pairs: - raise Exception( - u"Unknown parenthesis '{}': coder error".format(left_parens) - ) - right_parens = pairs[left_parens] - - if self.tall: - left_parens = r"\left" + left_parens - right_parens = r"\right" + right_parens - - self.latex = u"{left}{expr}{right}".format( - left=left_parens, - expr=latex, - right=right_parens - ) - - def __repr__(self): # pragma: no cover - """ - Give a sensible representation of the object. - - If `sans_parens` is different, include both. - If `tall` then have '<[]>' around the code, otherwise '<>'. - """ - if self.latex == self.sans_parens: - latex_repr = u'"{}"'.format(self.latex) - else: - latex_repr = u'"{}" or "{}"'.format(self.latex, self.sans_parens) - - if self.tall: - wrap = u'<[{}]>' - else: - wrap = u'<{}>' - - return wrap.format(latex_repr) - - -def render_number(children): - """ - Combine the elements forming the number, escaping the suffix if needed. - """ - children_latex = [k.latex for k in children] - - suffix = "" - if children_latex[-1] in SUFFIXES: - suffix = children_latex.pop() - suffix = u"\\text{{{s}}}".format(s=suffix) - - # Exponential notation-- the "E" splits the mantissa and exponent - if "E" in children_latex: - pos = children_latex.index("E") - mantissa = "".join(children_latex[:pos]) - exponent = "".join(children_latex[pos + 1:]) - latex = u"{m}\\!\\times\\!10^{{{e}}}{s}".format( - m=mantissa, e=exponent, s=suffix - ) - return LatexRendered(latex, tall=True) - else: - easy_number = "".join(children_latex) - return LatexRendered(easy_number + suffix) - - -def enrich_varname(varname): - """ - Prepend a backslash if we're given a greek character. - """ - greek = ("alpha beta gamma delta epsilon varepsilon zeta eta theta " - "vartheta iota kappa lambda mu nu xi pi rho sigma tau upsilon " - "phi varphi chi psi omega").split() - - # add capital greek letters - greek += [x.capitalize() for x in greek] - - # add hbar for QM - greek.append('hbar') - - # add infinity - greek.append('infty') - - if varname in greek: - return u"\\{letter}".format(letter=varname) - else: - return varname.replace("_", r"\_") - - -def variable_closure(variables, casify): - """ - Wrap `render_variable` so it knows the variables allowed. - """ - def render_variable(children): - """ - Replace greek letters, otherwise escape the variable names. - """ - varname = children[0].latex - if casify(varname) not in variables: - pass # TODO turn unknown variable red or give some kind of error - - first, _, second = varname.partition("_") - - if second: - # Then 'a_b' must become 'a_{b}' - varname = u"{a}_{{{b}}}".format( - a=enrich_varname(first), - b=enrich_varname(second) - ) - else: - varname = enrich_varname(varname) - - return LatexRendered(varname) # .replace("_", r"\_")) - return render_variable - - -def function_closure(functions, casify): - """ - Wrap `render_function` so it knows the functions allowed. - """ - def render_function(children): - """ - Escape function names and give proper formatting to exceptions. - - The exceptions being 'sqrt', 'log2', and 'log10' as of now. - """ - fname = children[0].latex - if casify(fname) not in functions: - pass # TODO turn unknown function red or give some kind of error - - # Wrap the input of the function with parens or braces. - inner = children[1].latex - if fname == "sqrt": - inner = u"{{{expr}}}".format(expr=inner) - else: - if children[1].tall: - inner = u"\\left({expr}\\right)".format(expr=inner) - else: - inner = u"({expr})".format(expr=inner) - - # Correctly format the name of the function. - if fname == "sqrt": - fname = u"\\sqrt" - elif fname == "log10": - fname = u"\\log_{10}" - elif fname == "log2": - fname = u"\\log_2" - else: - fname = u"\\text{{{fname}}}".format(fname=fname) - - # Put it together. - latex = fname + inner - return LatexRendered(latex, tall=children[1].tall) - # Return the function within the closure. - return render_function - - -def render_power(children): - """ - Combine powers so that the latex is wrapped in curly braces correctly. - - Also, if you have 'a^(b+c)' don't include that last set of parens: - 'a^{b+c}' is correct, whereas 'a^{(b+c)}' is extraneous. - """ - if len(children) == 1: - return children[0] - - children_latex = [k.latex for k in children if k.latex != "^"] - children_latex[-1] = children[-1].sans_parens - - raise_power = lambda x, y: u"{}^{{{}}}".format(y, x) - latex = reduce(raise_power, reversed(children_latex)) - return LatexRendered(latex, tall=True) - - -def render_parallel(children): - """ - Simply join the child nodes with a double vertical line. - """ - if len(children) == 1: - return children[0] - - children_latex = [k.latex for k in children if k.latex != "||"] - latex = r"\|".join(children_latex) - tall = any(k.tall for k in children) - return LatexRendered(latex, tall=tall) - - -def render_frac(numerator, denominator): - r""" - Given a list of elements in the numerator and denominator, return a '\frac' - - Avoid parens if they are unnecessary (i.e. the only thing in that part). - """ - if len(numerator) == 1: - num_latex = numerator[0].sans_parens - else: - num_latex = r"\cdot ".join(k.latex for k in numerator) - - if len(denominator) == 1: - den_latex = denominator[0].sans_parens - else: - den_latex = r"\cdot ".join(k.latex for k in denominator) - - latex = u"\\frac{{{num}}}{{{den}}}".format(num=num_latex, den=den_latex) - return latex - - -def render_product(children): - r""" - Format products and division nicely. - - Group bunches of adjacent, equal operators. Every time it switches from - denominator to the next numerator, call `render_frac`. Join these groupings - together with '\cdot's, ending on a numerator if needed. - - Examples: (`children` is formed indirectly by the string on the left) - 'a*b' -> 'a\cdot b' - 'a/b' -> '\frac{a}{b}' - 'a*b/c/d' -> '\frac{a\cdot b}{c\cdot d}' - 'a/b*c/d*e' -> '\frac{a}{b}\cdot \frac{c}{d}\cdot e' - """ - if len(children) == 1: - return children[0] - - position = "numerator" # or denominator - fraction_mode_ever = False - numerator = [] - denominator = [] - latex = "" - - for kid in children: - if position == "numerator": - if kid.latex == "*": - pass # Don't explicitly add the '\cdot' yet. - elif kid.latex == "/": - # Switch to denominator mode. - fraction_mode_ever = True - position = "denominator" - else: - numerator.append(kid) - else: - if kid.latex == "*": - # Switch back to numerator mode. - # First, render the current fraction and add it to the latex. - latex += render_frac(numerator, denominator) + r"\cdot " - - # Reset back to beginning state - position = "numerator" - numerator = [] - denominator = [] - elif kid.latex == "/": - pass # Don't explicitly add a '\frac' yet. - else: - denominator.append(kid) - - # Add the fraction/numerator that we ended on. - if position == "denominator": - latex += render_frac(numerator, denominator) - else: - # We ended on a numerator--act like normal multiplication. - num_latex = r"\cdot ".join(k.latex for k in numerator) - latex += num_latex - - tall = fraction_mode_ever or any(k.tall for k in children) - return LatexRendered(latex, tall=tall) - - -def render_sum(children): - """ - Concatenate elements, including the operators. - """ - if len(children) == 1: - return children[0] - - children_latex = [k.latex for k in children] - latex = "".join(children_latex) - tall = any(k.tall for k in children) - return LatexRendered(latex, tall=tall) - - -def render_atom(children): - """ - Properly handle parens, otherwise this is trivial. - """ - if len(children) == 3: - return LatexRendered( - children[1].latex, - parens=children[0].latex, - tall=children[1].tall - ) - else: - return children[0] - - -def add_defaults(var, fun, case_sensitive=False): - """ - Create sets with both the default and user-defined variables. - - Compare to calc.add_defaults - """ - var_items = set(DEFAULT_VARIABLES) - fun_items = set(DEFAULT_FUNCTIONS) - - var_items.update(var) - fun_items.update(fun) - - if not case_sensitive: - var_items = set(k.lower() for k in var_items) - fun_items = set(k.lower() for k in fun_items) - - return var_items, fun_items - - -def latex_preview(math_expr, variables=(), functions=(), case_sensitive=False): - """ - Convert `math_expr` into latex, guaranteeing its parse-ability. - - Analagous to `evaluator`. - """ - # No need to go further - if math_expr.strip() == "": - return "" - - # Parse tree - latex_interpreter = ParseAugmenter(math_expr, case_sensitive) - latex_interpreter.parse_algebra() - - # Get our variables together. - variables, functions = add_defaults(variables, functions, case_sensitive) - - # Create a recursion to evaluate the tree. - if case_sensitive: - casify = lambda x: x - else: - casify = lambda x: x.lower() # Lowercase for case insens. - - render_actions = { - 'number': render_number, - 'variable': variable_closure(variables, casify), - 'function': function_closure(functions, casify), - 'atom': render_atom, - 'power': render_power, - 'parallel': render_parallel, - 'product': render_product, - 'sum': render_sum - } - - backslash = "\\" - wrap_escaped_strings = lambda s: LatexRendered( - s.replace(backslash, backslash * 2) - ) - - output = latex_interpreter.reduce_tree( - render_actions, - terminal_converter=wrap_escaped_strings - ) - return output.latex diff --git a/common/lib/calc/calc/tests/__init__.py b/common/lib/calc/calc/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/common/lib/calc/calc/tests/test_calc.py b/common/lib/calc/calc/tests/test_calc.py deleted file mode 100644 index dfdc721ae8e9..000000000000 --- a/common/lib/calc/calc/tests/test_calc.py +++ /dev/null @@ -1,558 +0,0 @@ -""" -Unit tests for calc.py -""" - -from __future__ import absolute_import -import unittest -import numpy -import calc -from pyparsing import ParseException -from six.moves import zip - -# numpy's default behavior when it evaluates a function outside its domain -# is to raise a warning (not an exception) which is then printed to STDOUT. -# To prevent this from polluting the output of the tests, configure numpy to -# ignore it instead. -# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html -numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise' - - -class EvaluatorTest(unittest.TestCase): - """ - Run tests for calc.evaluator - Go through all functionalities as specifically as possible-- - work from number input to functions and complex expressions - Also test custom variable substitutions (i.e. - `evaluator({'x':3.0}, {}, '3*x')` - gives 9.0) and more. - """ - - def test_number_input(self): - """ - Test different kinds of float inputs - - See also - test_trailing_period (slightly different) - test_exponential_answer - test_si_suffix - """ - easy_eval = lambda x: calc.evaluator({}, {}, x) - - self.assertEqual(easy_eval("13"), 13) - self.assertEqual(easy_eval("3.14"), 3.14) - self.assertEqual(easy_eval(".618033989"), 0.618033989) - - self.assertEqual(easy_eval("-13"), -13) - self.assertEqual(easy_eval("-3.14"), -3.14) - self.assertEqual(easy_eval("-.618033989"), -0.618033989) - - def test_period(self): - """ - The string '.' should not evaluate to anything. - """ - with self.assertRaises(ParseException): - calc.evaluator({}, {}, '.') - with self.assertRaises(ParseException): - calc.evaluator({}, {}, '1+.') - - def test_trailing_period(self): - """ - Test that things like '4.' will be 4 and not throw an error - """ - self.assertEqual(4.0, calc.evaluator({}, {}, '4.')) - - def test_exponential_answer(self): - """ - Test for correct interpretation of scientific notation - """ - answer = 50 - correct_responses = [ - "50", "50.0", "5e1", "5e+1", - "50e0", "50.0e0", "500e-1" - ] - incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] - - for input_str in correct_responses: - result = calc.evaluator({}, {}, input_str) - fail_msg = "Expected '{0}' to equal {1}".format( - input_str, answer - ) - self.assertEqual(answer, result, msg=fail_msg) - - for input_str in incorrect_responses: - result = calc.evaluator({}, {}, input_str) - fail_msg = "Expected '{0}' to not equal {1}".format( - input_str, answer - ) - self.assertNotEqual(answer, result, msg=fail_msg) - - def test_si_suffix(self): - """ - Test calc.py's unique functionality of interpreting si 'suffixes'. - - For instance '%' stand for 1/100th so '1%' should be 0.01 - """ - test_mapping = [ - ('4.2%', 0.042) - ] - - for (expr, answer) in test_mapping: - tolerance = answer * 1e-6 # Make rel. tolerance, because of floats - fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}" - fail_msg = fail_msg.format(expr[-1], expr, answer) - self.assertAlmostEqual( - calc.evaluator({}, {}, expr), answer, - delta=tolerance, msg=fail_msg - ) - - def test_operator_sanity(self): - """ - Test for simple things like '5+2' and '5/2' - """ - var1 = 5.0 - var2 = 2.0 - operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)] - - for (operator, answer) in operators: - input_str = "{0} {1} {2}".format(var1, operator, var2) - result = calc.evaluator({}, {}, input_str) - fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format( - operator, input_str, answer - ) - self.assertEqual(answer, result, msg=fail_msg) - - def test_raises_zero_division_err(self): - """ - Ensure division by zero gives an error - """ - with self.assertRaises(ZeroDivisionError): - calc.evaluator({}, {}, '1/0') - with self.assertRaises(ZeroDivisionError): - calc.evaluator({}, {}, '1/0.0') - with self.assertRaises(ZeroDivisionError): - calc.evaluator({'x': 0.0}, {}, '1/x') - - def test_parallel_resistors(self): - """ - Test the parallel resistor operator || - - The formula is given by - a || b || c ... - = 1 / (1/a + 1/b + 1/c + ...) - It is the resistance of a parallel circuit of resistors with resistance - a, b, c, etc&. See if this evaulates correctly. - """ - self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5) - self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) - self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) - - def test_parallel_resistors_with_zero(self): - """ - Check the behavior of the || operator with 0 - """ - self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1'))) - self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1'))) - self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1'))) - - def assert_function_values(self, fname, ins, outs, tolerance=1e-3): - """ - Helper function to test many values at once - - Test the accuracy of evaluator's use of the function given by fname - Specifically, the equality of `fname(ins[i])` against outs[i]. - This is used later to test a whole bunch of f(x) = y at a time - """ - - for (arg, val) in zip(ins, outs): - input_str = "{0}({1})".format(fname, arg) - result = calc.evaluator({}, {}, input_str) - fail_msg = "Failed on function {0}: '{1}' was not {2}".format( - fname, input_str, val - ) - self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) - - def test_trig_functions(self): - """ - Test the trig functions provided in calc.py - - which are: sin, cos, tan, arccos, arcsin, arctan - """ - - angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] - sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] - cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j] - tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j] - # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... - - self.assert_function_values('sin', angles, sin_values) - self.assert_function_values('cos', angles, cos_values) - self.assert_function_values('tan', angles, tan_values) - - # Include those where the real part is between -pi/2 and pi/2 - arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j', '-1.1', '1.1'] - arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j, -1.570 + 0.443j, 1.570 + 0.443j] - self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles) - - # Include those where the real part is between 0 and pi - arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j', '-1.1', '1.1'] - arccos_angles = [0, 0.524, 0.628, 1 + 1j, 3.141 - 0.443j, -0.443j] - self.assert_function_values('arccos', arccos_inputs, arccos_angles) - - # Has the same range as arcsin - arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j'] - arctan_angles = arcsin_angles - self.assert_function_values('arctan', arctan_inputs, arctan_angles) - - def test_reciprocal_trig_functions(self): - """ - Test the reciprocal trig functions provided in calc.py - - which are: sec, csc, cot, arcsec, arccsc, arccot - """ - angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] - sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498 + 0.591j] - csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622 - 0.304j] - cot_values = [-1, 1.732, 1.376, 1, 1, 0.218 - 0.868j] - - self.assert_function_values('sec', angles, sec_values) - self.assert_function_values('csc', angles, csc_values) - self.assert_function_values('cot', angles, cot_values) - - arcsec_inputs = ['1.1547', '1.2361', '2', '-2', '-1.4142', '0.4983+0.5911*j'] - arcsec_angles = [0.524, 0.628, 1.047, 2.094, 2.356, 1 + 1j] - self.assert_function_values('arcsec', arcsec_inputs, arcsec_angles) - - arccsc_inputs = ['-1.1547', '-1.4142', '2', '1.7013', '1.1547', '0.6215-0.3039*j'] - arccsc_angles = [-1.047, -0.785, 0.524, 0.628, 1.047, 1 + 1j] - self.assert_function_values('arccsc', arccsc_inputs, arccsc_angles) - - # Has the same range as arccsc - arccot_inputs = ['-0.5774', '-1', '1.7321', '1.3764', '0.5774', '(0.2176-0.868*j)'] - arccot_angles = arccsc_angles - self.assert_function_values('arccot', arccot_inputs, arccot_angles) - - def test_hyperbolic_functions(self): - """ - Test the hyperbolic functions - - which are: sinh, cosh, tanh, sech, csch, coth - """ - inputs = ['0', '0.5', '1', '2', '1+j'] - neg_inputs = ['0', '-0.5', '-1', '-2', '-1-j'] - negate = lambda x: [-k for k in x] - - # sinh is odd - sinh_vals = [0, 0.521, 1.175, 3.627, 0.635 + 1.298j] - self.assert_function_values('sinh', inputs, sinh_vals) - self.assert_function_values('sinh', neg_inputs, negate(sinh_vals)) - - # cosh is even - do not negate - cosh_vals = [1, 1.128, 1.543, 3.762, 0.834 + 0.989j] - self.assert_function_values('cosh', inputs, cosh_vals) - self.assert_function_values('cosh', neg_inputs, cosh_vals) - - # tanh is odd - tanh_vals = [0, 0.462, 0.762, 0.964, 1.084 + 0.272j] - self.assert_function_values('tanh', inputs, tanh_vals) - self.assert_function_values('tanh', neg_inputs, negate(tanh_vals)) - - # sech is even - do not negate - sech_vals = [1, 0.887, 0.648, 0.266, 0.498 - 0.591j] - self.assert_function_values('sech', inputs, sech_vals) - self.assert_function_values('sech', neg_inputs, sech_vals) - - # the following functions do not have 0 in their domain - inputs = inputs[1:] - neg_inputs = neg_inputs[1:] - - # csch is odd - csch_vals = [1.919, 0.851, 0.276, 0.304 - 0.622j] - self.assert_function_values('csch', inputs, csch_vals) - self.assert_function_values('csch', neg_inputs, negate(csch_vals)) - - # coth is odd - coth_vals = [2.164, 1.313, 1.037, 0.868 - 0.218j] - self.assert_function_values('coth', inputs, coth_vals) - self.assert_function_values('coth', neg_inputs, negate(coth_vals)) - - def test_hyperbolic_inverses(self): - """ - Test the inverse hyperbolic functions - - which are of the form arc[X]h - """ - results = [0, 0.5, 1, 2, 1 + 1j] - - sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j'] - self.assert_function_values('arcsinh', sinh_vals, results) - - cosh_vals = ['1', '1.1276', '1.5431', '3.7622', '0.8337+0.9889*j'] - self.assert_function_values('arccosh', cosh_vals, results) - - tanh_vals = ['0', '0.4621', '0.7616', '0.964', '1.0839+0.2718*j'] - self.assert_function_values('arctanh', tanh_vals, results) - - sech_vals = ['1.0', '0.8868', '0.6481', '0.2658', '0.4983-0.5911*j'] - self.assert_function_values('arcsech', sech_vals, results) - - results = results[1:] - csch_vals = ['1.919', '0.8509', '0.2757', '0.3039-0.6215*j'] - self.assert_function_values('arccsch', csch_vals, results) - - coth_vals = ['2.164', '1.313', '1.0373', '0.868-0.2176*j'] - self.assert_function_values('arccoth', coth_vals, results) - - def test_other_functions(self): - """ - Test the non-trig functions provided in calc.py - - Specifically: - sqrt, log10, log2, ln, abs, - fact, factorial - """ - - # Test sqrt - self.assert_function_values( - 'sqrt', - [0, 1, 2, 1024], # -1 - [0, 1, 1.414, 32] # 1j - ) - # sqrt(-1) is NAN not j (!!). - - # Test logs - self.assert_function_values( - 'log10', - [0.1, 1, 3.162, 1000000, '1+j'], - [-1, 0, 0.5, 6, 0.151 + 0.341j] - ) - self.assert_function_values( - 'log2', - [0.5, 1, 1.414, 1024, '1+j'], - [-1, 0, 0.5, 10, 0.5 + 1.133j] - ) - self.assert_function_values( - 'ln', - [0.368, 1, 1.649, 2.718, 42, '1+j'], - [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j] - ) - - # Test abs - self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) - - # Test factorial - fact_inputs = [0, 1, 3, 7] - fact_values = [1, 1, 6, 5040] - self.assert_function_values('fact', fact_inputs, fact_values) - self.assert_function_values('factorial', fact_inputs, fact_values) - - self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)") - self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)") - self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)") - self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)") - - def test_constants(self): - """ - Test the default constants provided in calc.py - - which are: j (complex number), e, pi - """ - - # Of the form ('expr', python value, tolerance (or None for exact)) - default_variables = [ - ('i', 1j, None), - ('j', 1j, None), - ('e', 2.7183, 1e-4), - ('pi', 3.1416, 1e-4), - ] - for (variable, value, tolerance) in default_variables: - fail_msg = "Failed on constant '{0}', not within bounds".format( - variable - ) - result = calc.evaluator({}, {}, variable) - if tolerance is None: - self.assertEqual(value, result, msg=fail_msg) - else: - self.assertAlmostEqual( - value, result, - delta=tolerance, msg=fail_msg - ) - - def test_complex_expression(self): - """ - Calculate combinations of operators and default functions - """ - - self.assertAlmostEqual( - calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), - 10.180, - delta=1e-3 - ) - self.assertAlmostEqual( - calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"), - 1.6, - delta=1e-3 - ) - self.assertAlmostEqual( - calc.evaluator({}, {}, "10||sin(7+5)"), - -0.567, delta=0.01 - ) - self.assertAlmostEqual( - calc.evaluator({}, {}, "sin(e)"), - 0.41, delta=0.01 - ) - self.assertAlmostEqual( - calc.evaluator({}, {}, "e^(j*pi)"), - -1, delta=1e-5 - ) - - def test_explicit_sci_notation(self): - """ - Expressions like 1.6*10^-3 (not 1.6e-3) it should evaluate. - """ - self.assertEqual( - calc.evaluator({}, {}, "-1.6*10^-3"), - -0.0016 - ) - self.assertEqual( - calc.evaluator({}, {}, "-1.6*10^(-3)"), - -0.0016 - ) - - self.assertEqual( - calc.evaluator({}, {}, "-1.6*10^3"), - -1600 - ) - self.assertEqual( - calc.evaluator({}, {}, "-1.6*10^(3)"), - -1600 - ) - - def test_simple_vars(self): - """ - Substitution of variables into simple equations - """ - variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4, "f_0'": 2.0, "T_{ijk}^{123}''": 5.2} - - # Should not change value of constant - # even with different numbers of variables... - self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13) - self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13) - self.assertEqual(calc.evaluator(variables, {}, '13'), 13) - - # Easy evaluation - self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72) - self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91) - self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4) - self.assertEqual(calc.evaluator(variables, {}, "f_0'"), 2.0) - self.assertEqual(calc.evaluator(variables, {}, "T_{ijk}^{123}''"), 5.2) - - # Test a simple equation - self.assertAlmostEqual( - calc.evaluator(variables, {}, '3*x-y'), - 21.25, delta=0.01 # = 3 * 9.72 - 7.91 - ) - self.assertAlmostEqual( - calc.evaluator(variables, {}, 'x*y'), - 76.89, delta=0.01 - ) - - self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13) - self.assertEqual(calc.evaluator(variables, {}, "13"), 13) - self.assertEqual( - calc.evaluator( - {'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.6600949841121}, - {}, "5" - ), - 5 - ) - - def test_variable_case_sensitivity(self): - """ - Test the case sensitivity flag and corresponding behavior - """ - self.assertEqual( - calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), - 8.0 - ) - - variables = {'E': 1.0} - self.assertEqual( - calc.evaluator(variables, {}, "E", case_sensitive=True), - 1.0 - ) - # Recall 'e' is a default constant, with value 2.718 - self.assertAlmostEqual( - calc.evaluator(variables, {}, "e", case_sensitive=True), - 2.718, delta=0.02 - ) - - def test_simple_funcs(self): - """ - Subsitution of custom functions - """ - variables = {'x': 4.712} - functions = {'id': lambda x: x} - self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) - self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) - self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712) - - functions.update({'f': numpy.sin}) - self.assertAlmostEqual( - calc.evaluator(variables, functions, 'f(x)'), - -1, delta=1e-3 - ) - - def test_function_case_insensitive(self): - """ - Test case insensitive evaluation - - Normal functions with some capitals should be fine - """ - self.assertAlmostEqual( - -0.28, - calc.evaluator({}, {}, 'SiN(6)', case_sensitive=False), - delta=1e-3 - ) - - def test_function_case_sensitive(self): - """ - Test case sensitive evaluation - - Incorrectly capitilized should fail - Also, it should pick the correct version of a function. - """ - with self.assertRaisesRegexp(calc.UndefinedVariable, 'SiN'): - calc.evaluator({}, {}, 'SiN(6)', case_sensitive=True) - - # With case sensitive turned on, it should pick the right function - functions = {'f': lambda x: x, 'F': lambda x: x + 1} - self.assertEqual( - 6, calc.evaluator({}, functions, 'f(6)', case_sensitive=True) - ) - self.assertEqual( - 7, calc.evaluator({}, functions, 'F(6)', case_sensitive=True) - ) - - def test_undefined_vars(self): - """ - Check to see if the evaluator catches undefined variables - """ - variables = {'R1': 2.0, 'R3': 4.0} - - with self.assertRaisesRegexp(calc.UndefinedVariable, r'QWSEKO'): - calc.evaluator({}, {}, "5+7*QWSEKO") - with self.assertRaisesRegexp(calc.UndefinedVariable, r'r2'): - calc.evaluator({'r1': 5}, {}, "r1+r2") - with self.assertRaisesRegexp(calc.UndefinedVariable, r'r1, r3'): - calc.evaluator(variables, {}, "r1*r3", case_sensitive=True) - with self.assertRaisesRegexp(calc.UndefinedVariable, r'did you forget to use \*'): - calc.evaluator(variables, {}, "R1(R3 + 1)") - - def test_mismatched_parens(self): - """ - Check to see if the evaluator catches mismatched parens - """ - with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'opened but never closed'): - calc.evaluator({}, {}, "(1+2") - with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'no matching opening parenthesis'): - calc.evaluator({}, {}, "(1+2))") diff --git a/common/lib/calc/calc/tests/test_preview.py b/common/lib/calc/calc/tests/test_preview.py deleted file mode 100644 index df3913c25f50..000000000000 --- a/common/lib/calc/calc/tests/test_preview.py +++ /dev/null @@ -1,241 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Unit tests for preview.py -""" - -from __future__ import absolute_import -import unittest -from calc import preview -import pyparsing - - -class LatexRenderedTest(unittest.TestCase): - """ - Test the initializing code for LatexRendered. - - Specifically that it stores the correct data and handles parens well. - """ - def test_simple(self): - """ - Test that the data values are stored without changing. - """ - math = 'x^2' - obj = preview.LatexRendered(math, tall=True) - self.assertEquals(obj.latex, math) - self.assertEquals(obj.sans_parens, math) - self.assertEquals(obj.tall, True) - - def _each_parens(self, with_parens, math, parens, tall=False): - """ - Helper method to test the way parens are wrapped. - """ - obj = preview.LatexRendered(math, parens=parens, tall=tall) - self.assertEquals(obj.latex, with_parens) - self.assertEquals(obj.sans_parens, math) - self.assertEquals(obj.tall, tall) - - def test_parens(self): - """ Test curvy parens. """ - self._each_parens('(x+y)', 'x+y', '(') - - def test_brackets(self): - """ Test brackets. """ - self._each_parens('[x+y]', 'x+y', '[') - - def test_squiggles(self): - """ Test curly braces. """ - self._each_parens(r'\{x+y\}', 'x+y', '{') - - def test_parens_tall(self): - """ Test curvy parens with the tall parameter. """ - self._each_parens(r'\left(x^y\right)', 'x^y', '(', tall=True) - - def test_brackets_tall(self): - """ Test brackets, also tall. """ - self._each_parens(r'\left[x^y\right]', 'x^y', '[', tall=True) - - def test_squiggles_tall(self): - """ Test tall curly braces. """ - self._each_parens(r'\left\{x^y\right\}', 'x^y', '{', tall=True) - - def test_bad_parens(self): - """ Check that we get an error with invalid parens. """ - with self.assertRaisesRegexp(Exception, 'Unknown parenthesis'): - preview.LatexRendered('x^2', parens='not parens') - - -class LatexPreviewTest(unittest.TestCase): - """ - Run integrative tests for `latex_preview`. - - All functionality was tested `RenderMethodsTest`, but see if it combines - all together correctly. - """ - def test_no_input(self): - """ - With no input (including just whitespace), see that no error is thrown. - """ - self.assertEquals('', preview.latex_preview('')) - self.assertEquals('', preview.latex_preview(' ')) - self.assertEquals('', preview.latex_preview(' \t ')) - - def test_number_simple(self): - """ Simple numbers should pass through. """ - self.assertEquals(preview.latex_preview('3.1415'), '3.1415') - - def test_number_suffix(self): - """ Suffixes should be escaped. """ - self.assertEquals(preview.latex_preview('1.618%'), r'1.618\text{%}') - - def test_number_sci_notation(self): - """ Numbers with scientific notation should display nicely """ - self.assertEquals( - preview.latex_preview('6.0221413E+23'), - r'6.0221413\!\times\!10^{+23}' - ) - self.assertEquals( - preview.latex_preview('-6.0221413E+23'), - r'-6.0221413\!\times\!10^{+23}' - ) - - def test_variable_simple(self): - """ Simple valid variables should pass through. """ - self.assertEquals(preview.latex_preview('x', variables=['x']), 'x') - - def test_greek(self): - """ Variable names that are greek should be formatted accordingly. """ - self.assertEquals(preview.latex_preview('pi'), r'\pi') - - def test_variable_subscript(self): - """ Things like 'epsilon_max' should display nicely """ - self.assertEquals( - preview.latex_preview('epsilon_max', variables=['epsilon_max']), - r'\epsilon_{max}' - ) - - def test_function_simple(self): - """ Valid function names should be escaped. """ - self.assertEquals( - preview.latex_preview('f(3)', functions=['f']), - r'\text{f}(3)' - ) - - def test_function_tall(self): - r""" Functions surrounding a tall element should have \left, \right """ - self.assertEquals( - preview.latex_preview('f(3^2)', functions=['f']), - r'\text{f}\left(3^{2}\right)' - ) - - def test_function_sqrt(self): - """ Sqrt function should be handled specially. """ - self.assertEquals(preview.latex_preview('sqrt(3)'), r'\sqrt{3}') - - def test_function_log10(self): - """ log10 function should be handled specially. """ - self.assertEquals(preview.latex_preview('log10(3)'), r'\log_{10}(3)') - - def test_function_log2(self): - """ log2 function should be handled specially. """ - self.assertEquals(preview.latex_preview('log2(3)'), r'\log_2(3)') - - def test_power_simple(self): - """ Powers should wrap the elements with braces correctly. """ - self.assertEquals(preview.latex_preview('2^3^4'), '2^{3^{4}}') - - def test_power_parens(self): - """ Powers should ignore the parenthesis of the last math. """ - self.assertEquals(preview.latex_preview('2^3^(4+5)'), '2^{3^{4+5}}') - - def test_parallel(self): - r""" Parallel items should combine with '\|'. """ - self.assertEquals(preview.latex_preview('2||3'), r'2\|3') - - def test_product_mult_only(self): - r""" Simple products should combine with a '\cdot'. """ - self.assertEquals(preview.latex_preview('2*3'), r'2\cdot 3') - - def test_product_big_frac(self): - """ Division should combine with '\frac'. """ - self.assertEquals( - preview.latex_preview('2*3/4/5'), - r'\frac{2\cdot 3}{4\cdot 5}' - ) - - def test_product_single_frac(self): - """ Division should ignore parens if they are extraneous. """ - self.assertEquals( - preview.latex_preview('(2+3)/(4+5)'), - r'\frac{2+3}{4+5}' - ) - - def test_product_keep_going(self): - """ - Complex products/quotients should split into many '\frac's when needed. - """ - self.assertEquals( - preview.latex_preview('2/3*4/5*6'), - r'\frac{2}{3}\cdot \frac{4}{5}\cdot 6' - ) - - def test_sum(self): - """ Sums should combine its elements. """ - # Use 'x' as the first term (instead of, say, '1'), so it can't be - # interpreted as a negative number. - self.assertEquals( - preview.latex_preview('-x+2-3+4', variables=['x']), - '-x+2-3+4' - ) - - def test_sum_tall(self): - """ A complicated expression should not hide the tallness. """ - self.assertEquals( - preview.latex_preview('(2+3^2)'), - r'\left(2+3^{2}\right)' - ) - - def test_complicated(self): - """ - Given complicated input, ensure that exactly the correct string is made. - """ - self.assertEquals( - preview.latex_preview('11*f(x)+x^2*(3||4)/sqrt(pi)'), - r'11\cdot \text{f}(x)+\frac{x^{2}\cdot (3\|4)}{\sqrt{\pi}}' - ) - - self.assertEquals( - preview.latex_preview('log10(1+3/4/Cos(x^2)*(x+1))', - case_sensitive=True), - (r'\log_{10}\left(1+\frac{3}{4\cdot \text{Cos}\left(x^{2}\right)}' - r'\cdot (x+1)\right)') - ) - - def test_syntax_errors(self): - """ - Test a lot of math strings that give syntax errors - - Rather than have a lot of self.assertRaises, make a loop and keep track - of those that do not throw a `ParseException`, and assert at the end. - """ - bad_math_list = [ - '11+', - '11*', - 'f((x)', - 'sqrt(x^)', - '3f(x)', # Not 3*f(x) - '3|4', - '3|||4' - ] - bad_exceptions = {} - for math in bad_math_list: - try: - preview.latex_preview(math) - except pyparsing.ParseException: - pass # This is what we were expecting. (not excepting :P) - except Exception as error: # pragma: no cover - bad_exceptions[math] = error - else: # pragma: no cover - # If there is no exception thrown, this is a problem - bad_exceptions[math] = None - - self.assertEquals({}, bad_exceptions) diff --git a/common/lib/calc/setup.py b/common/lib/calc/setup.py deleted file mode 100644 index 8d4df62f4f90..000000000000 --- a/common/lib/calc/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import absolute_import -from setuptools import setup - -setup( - name="calc", - version="0.3", - packages=["calc"], - install_requires=[ - "pyparsing==2.2.0", - "numpy", - "scipy", - ], -) diff --git a/docs/common_lib.rst b/docs/common_lib.rst index e761153f145d..c1cc729fabc3 100644 --- a/docs/common_lib.rst +++ b/docs/common_lib.rst @@ -8,7 +8,6 @@ out from edx-platform into separate packages at some point. .. toctree:: :maxdepth: 2 - common/lib/calc/modules common/lib/capa/modules common/lib/chem/modules common/lib/safe_lxml/modules diff --git a/docs/conf.py b/docs/conf.py index f20186059c81..8050e1f50b6f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,6 @@ sys.path.append(root / "docs") sys.path.append(root / "cms/djangoapps") sys.path.append(root / "common/djangoapps") -sys.path.append(root / "common/lib/calc") sys.path.append(root / "common/lib/capa") sys.path.append(root / "common/lib/chem") sys.path.append(root / "common/lib/safe_lxml") @@ -237,7 +236,6 @@ # the generated *.rst files modules = { 'cms': 'cms', - 'common/lib/calc/calc': 'common/lib/calc', 'common/lib/capa/capa': 'common/lib/capa', 'common/lib/chem/chem': 'common/lib/chem', 'common/lib/safe_lxml/safe_lxml': 'common/lib/safe_lxml', diff --git a/docs/testing.rst b/docs/testing.rst index 149a3e1e0d30..a47b3378c9e4 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -246,7 +246,7 @@ after the first failure. common/lib tests are tested with the ``test_lib`` task, which also accepts the ``--failed`` and ``--exitfirst`` options:: - paver test_lib -l common/lib/calc + paver test_lib -l common/lib/xmodule paver test_lib -l common/lib/xmodule --failed For example, this command runs a single python unit test file:: diff --git a/scripts/py2_to_py3_convert_and_create_pr.sh b/scripts/py2_to_py3_convert_and_create_pr.sh index d31affe6dca0..68faa94e1b0f 100644 --- a/scripts/py2_to_py3_convert_and_create_pr.sh +++ b/scripts/py2_to_py3_convert_and_create_pr.sh @@ -8,10 +8,10 @@ # 2) On the command line, go into your edx-platform repo checkout # 3) Make sure you are on the master branchof edx-platform with no changes # 4) Run this script from the root of the repo, handing it your username, ticketname, and subdirectory to convert: -# ./scripts/py2_to_py3_convert_and_create_pr.sh cpappas INCR-1234 common/lib/calc +# ./scripts/py2_to_py3_convert_and_create_pr.sh cpappas INCR-1234 common/lib/xmodule help_text="\nUsage: ./scripts/py2_to_py3_convert_and_create_pr.sh \n"; -help_text+="Example: ./scripts/py2_to_py3_convert_and_create_pr.sh cpappas INCR-1234 common/lib/calc\n\n"; +help_text+="Example: ./scripts/py2_to_py3_convert_and_create_pr.sh cpappas INCR-1234 common/lib/xmodule\n\n"; for i in "$@" ; do if [[ $i == "--help" ]] ; then