From e72967e349ab767c5b7e0ac3b05237fd92559e49 Mon Sep 17 00:00:00 2001 From: stv Date: Fri, 29 Mar 2019 12:12:53 -0700 Subject: [PATCH] Split calc package out into an independent package This is part of a larger effort to split packages out into independent, composable, PyPI installable Python packages. While this package itself isn't monumental, it represents the bottom of the dependecy tree and can be split out before any other packages. --- .coveragerc | 1 - .coveragerc-local | 1 - common/lib/calc/calc/__init__.py | 7 - common/lib/calc/calc/calc.py | 504 ------------------ common/lib/calc/calc/functions.py | 100 ---- common/lib/calc/calc/preview.py | 401 -------------- common/lib/calc/calc/tests/__init__.py | 0 common/lib/calc/calc/tests/test_calc.py | 558 -------------------- common/lib/calc/calc/tests/test_preview.py | 241 --------- common/lib/calc/setup.py | 13 - docs/common_lib.rst | 1 - docs/conf.py | 2 - docs/testing.rst | 2 +- scripts/py2_to_py3_convert_and_create_pr.sh | 4 +- 14 files changed, 3 insertions(+), 1832 deletions(-) delete mode 100644 common/lib/calc/calc/__init__.py delete mode 100644 common/lib/calc/calc/calc.py delete mode 100644 common/lib/calc/calc/functions.py delete mode 100644 common/lib/calc/calc/preview.py delete mode 100644 common/lib/calc/calc/tests/__init__.py delete mode 100644 common/lib/calc/calc/tests/test_calc.py delete mode 100644 common/lib/calc/calc/tests/test_preview.py delete mode 100644 common/lib/calc/setup.py 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