Merge pull request #20101 from stvstnfrd/split-core/calc
Split calc package out into an independent package
This commit is contained in:
@@ -4,7 +4,6 @@ data_file = reports/.coverage
|
||||
source =
|
||||
cms
|
||||
common/djangoapps
|
||||
common/lib/calc
|
||||
common/lib/capa
|
||||
common/lib/xmodule
|
||||
lms
|
||||
|
||||
@@ -4,7 +4,6 @@ data_file = reports/.coverage
|
||||
source =
|
||||
cms
|
||||
common/djangoapps
|
||||
common/lib/calc
|
||||
common/lib/capa
|
||||
common/lib/xmodule
|
||||
lms
|
||||
|
||||
@@ -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 *
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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))")
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,6 @@ sys.path.insert(0, root)
|
||||
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 @@ autodoc_mock_imports = [
|
||||
# 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',
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -15,11 +15,11 @@ numpy==1.6.2 # Numeric array processing utilities; used b
|
||||
pyparsing==2.2.0 # Python Parsing module
|
||||
scipy==0.14.0 # Math, science, and engineering library
|
||||
sympy==0.7.1 # Symbolic math library
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
|
||||
# Install these packages from the edx-platform working tree
|
||||
# NOTE: if you change code in these packages, you MUST change the version
|
||||
# number in its setup.py or the code WILL NOT be installed during deploy.
|
||||
-e common/lib/calc
|
||||
-e common/lib/chem
|
||||
-e common/lib/sandbox-packages
|
||||
-e common/lib/symmath
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
common/lib/calc
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
common/lib/chem
|
||||
common/lib/sandbox-packages
|
||||
common/lib/symmath
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e common/lib/calc
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
-e common/lib/capa
|
||||
-e common/lib/chem
|
||||
-e git+https://github.com/edx/codejail.git@a320d43ce6b9c93b17636b2491f724d9e433be47#egg=codejail
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e common/lib/calc
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
-e common/lib/capa
|
||||
-e common/lib/chem
|
||||
-e git+https://github.com/edx/codejail.git@a320d43ce6b9c93b17636b2491f724d9e433be47#egg=codejail
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
|
||||
|
||||
# Python libraries to install directly from github
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
|
||||
# Third-party:
|
||||
-e git+https://github.com/edx/django-wiki.git@v0.0.21#egg=django-wiki
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Python libraries to install that are local to the edx-platform repo
|
||||
-e .
|
||||
-e common/lib/calc
|
||||
-e common/lib/capa
|
||||
-e common/lib/chem
|
||||
-e common/lib/safe_lxml
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
-e common/lib/calc
|
||||
git+https://github.com/edx/openedx-calc.git@e9b698c85ad1152002bc0868f475f153dce88952#egg=calc==0.4
|
||||
-e common/lib/capa
|
||||
-e common/lib/chem
|
||||
-e git+https://github.com/edx/codejail.git@a320d43ce6b9c93b17636b2491f724d9e433be47#egg=codejail
|
||||
|
||||
@@ -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 <username> <ticket-name> <subdirectory>\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
|
||||
|
||||
Reference in New Issue
Block a user