def test_array_class_is_correct(): """Tests two forms of MathArray creation actually create MathArray instances""" from_constructor = MathArray([[1, 2, 3], [4, 5, 6]]) assert isinstance(from_constructor, MathArray) from_slice = MathArray([[1, 2, 3], [4, 5, 6]])[:, 2:3] assert isinstance(from_slice, MathArray)
def test_descriptions(): scalar = MathArray(5) assert scalar.shape_name == 'scalar' assert scalar.description == 'scalar' assert scalar.shape == tuple() vector = MathArray([1, 2, 3, 4, 5]) assert vector.shape_name == 'vector' assert vector.description == 'vector of length 5' assert vector.shape == (5,) matrix = MathArray([vector, vector, vector]) assert matrix.shape_name == 'matrix' assert matrix.description == 'matrix of shape (rows: 3, cols: 5)' assert matrix.shape == (3, 5) tensor1 = MathArray([matrix, matrix, matrix, matrix]) assert tensor1.shape_name == 'tensor' assert tensor1.description == 'tensor of shape (4, 3, 5)' assert tensor1.shape == (4, 3, 5) tensor2 = MathArray([tensor1, tensor1]) assert tensor2.shape_name == 'tensor' assert tensor2.description == 'tensor of shape (2, 4, 3, 5)' assert tensor2.shape == (2, 4, 3, 5)
def test_matrix_times_matrix_multiplication(): A = MathArray([ [5, 2, 3], [-4, 3, -1] ]) B = MathArray([ [0, 4, 5, 2], [5, -2, 6, -3], [-7, 5, 8, 4] ]) C = MathArray([ [-11, 31, 61, 16], [22, -27, -10, -21] ]) assert equal_as_arrays(A*B, C) X = random_math_array([3, 5]) Y = random_math_array([4, 2]) match = ("Cannot multiply a matrix of shape \(rows: 3, cols: 5\) with a matrix " "of shape \(rows: 4, cols: 2\)") with raises(ShapeError, match=match): X*Y row = MathArray([[1, 2, 3]]) col = MathArray([[1], [2], [3]]) assert row*col == 14 and isinstance(row*col, Number)
def test_cross(): cross = ARRAY_ONLY_FUNCTIONS['cross'] a = MathArray([2, -1, 3.5]) b = MathArray([1.5, 2.25, -1]) a_cross_b = MathArray([-6.875, 7.25, 6.]) assert equal_as_arrays(cross(a, b), a_cross_b) vec_3 = random_math_array((3, )) vec_4 = random_math_array((4, )) match = ( r"There was an error evaluating function cross\(...\)\n" r"1st input is ok: received a vector of length 3 as expected\n" r"2nd input has an error: received a vector of length 4, expected " r"a vector of length 3") with raises(DomainError, match=match): cross(vec_3, vec_4) match = ( r"There was an error evaluating function cross\(...\)\n" r"1st input has an error: received a vector of length 4, expected " r"a vector of length 3\n" r"2nd input is ok: received a vector of length 3 as expected") with raises(DomainError, match=match): cross(vec_4, vec_3)
def test_subtraction_with_zero(): A = MathArray([[5, 2, 1], [-2, 4, -3]]) assert equal_as_arrays(A - 0, A) assert equal_as_arrays(0 - A, -A) # pylint: disable=invalid-unary-operand-type u = MathArray([1, 2]) assert equal_as_arrays(u - 0, u) assert equal_as_arrays(0 - u, -u) # pylint: disable=invalid-unary-operand-type
def test_vector_times_vector_is_dot_product(): a = MathArray([1, 2, 3]) b = MathArray([-2, 3, 4]) c = MathArray([1, 2, 3, 4, 5]) assert isinstance(a*b, Number) assert a*b == b*a == 16 match = "Cannot calculate the dot product of a vector of length 3 with a vector of length 5" with raises(ShapeError, match=match): a*c
def test_numpy_functions_preserve_class(): A = MathArray([ [1, 2, 3], [4, 5, 6] ]) A_trans = MathArray([ [1, 4], [2, 5], [3, 6] ]) assert equal_as_arrays(np.transpose(A), A_trans) assert equal_as_arrays(A.T, A_trans)
def test_scalar_product(): X = MathArray([[1, 2, 3], [10, 20, 30]]) two = MathArray(2) two3 = MathArray([[[2]]]) Y = MathArray([[2, 4, 6], [20, 40, 60]]) assert equal_as_arrays(2 * X, Y) assert equal_as_arrays(X * 2, Y) assert equal_as_arrays(two * X, Y) assert equal_as_arrays(X * two, Y) # This is ugly, but convenient for logic and implementation assert equal_as_arrays(two3 * X, Y) assert equal_as_arrays(X * two3, Y)
def test_array_abs_input_types(): array_abs = ARRAY_ONLY_FUNCTIONS['abs'] x = random.uniform(0, 10) assert array_abs(x) == x assert array_abs(-x) == x assert array_abs(MathArray([2, -3, 6])) == 7 match = ("The abs\(...\) function expects a scalar or vector. To take the " "norm of a matrix, try norm\(...\) instead.") with raises(FunctionEvalError, match=match): array_abs(MathArray([[1, 2], [3, 4]]))
def test_nonpositive_matrix_powers(): A = MathArray([[1, 5], [-4, 3]]) I2 = MathArray([[1, 0], [0, 1]]) # B is not invertible B = MathArray([[2, 1], [6, 3]]) assert equal_as_arrays(A**0, I2) # Slight numerical errors in the inversion assert approx_equal_as_arrays(A**-2 * A**3, A, tol=1e-15) match = 'Cannot raise singular matrix to negative powers.' with raises(MathArrayError, match=match): B**-1
def test_vector_times_matrix_multiplication(): u = MathArray([1, -2, 3]) A = MathArray([[2, 4], [4, -3], [-1, 0]]) b = MathArray([-9, 10]) assert equal_as_arrays(u * A, b) assert b.ndim == 1 # result is a vector X = random_math_array(5) Y = random_math_array([4, 3]) match = (r"Cannot multiply a vector of length 5 with a matrix of shape " r"\(rows: 4, cols: 3\)") with raises(ShapeError, match=match): X * Y
def test_matrix_times_vector_multiplication(): u = MathArray([1, -2, 3]) A = MathArray([[2, 4, -1], [4, -3, 0]]) b = MathArray([-9, 10]) assert equal_as_arrays(A * u, b) assert b.ndim == 1 # result is a vector X = random_math_array([4, 3]) Y = random_math_array(5) match = ( r"Cannot multiply a matrix of shape \(rows: 4, cols: 3\) with a vector " r"of length 5.") with raises(ShapeError, match=match): X * Y
def test_subtraction_with_shape_mismath(): # shape mismatch A = MathArray([[5, 2, 1], [-2, 4, -3]]) B = MathArray([[2, -1], [3, 0]]) match = ('Cannot add/subtract a matrix of shape \(rows: 2, cols: 3\) ' 'with a matrix of shape \(rows: 2, cols: 2\).') with raises(ShapeError, match=match): A - B # dimension mismatch u = MathArray([1, 2]) match = ('Cannot add/subtract a vector of length 2 with a matrix of ' 'shape \(rows: 2, cols: 2\).') with raises(ShapeError, match=match): u - B
def test_addition_with_zero(): A = MathArray([[5, 2, 1], [-2, 4, -3]]) assert equal_as_arrays(A + 0, A) assert equal_as_arrays(0 + A, A) z0 = MathArray(0) assert equal_as_arrays(A + z0, A) assert equal_as_arrays(z0 + A, A) # This is ugly, but convenient for logic and implementation z3 = MathArray([[[0]]]) assert equal_as_arrays(A + z3, A) assert equal_as_arrays(z3 + A, A) u = MathArray([1, 2]) assert equal_as_arrays(u + 0, u) assert equal_as_arrays(0 + u, u)
def test_in_place_powers(): amounts = [2, -3.0, MathArray(5)] for amount in amounts: A = random_math_array([2, 2]) A_copy = A.copy() A **= amount assert equal_as_arrays(A, A_copy ** amount)
def test_calc_functions_multiple_arguments(): """Tests parse/eval handling functions with multiple arguments correctly""" def h1(x): return x def h2(x, y): return x * y def h3(x, y, z): return x * y * z assert evaluator("h(2)", {}, {"h": h1}, {})[0] == 2.0 assert evaluator("h(2, 3)", {}, {"h": h2}, {})[0] == 6.0 assert evaluator("h(2, 3, 4)", {}, {"h": h3}, {})[0] == 24.0 assert equal_as_arrays( evaluator("h(2, [1, 2, 3])", {}, {"h": h2})[0], MathArray([2, 4, 6])) with raises(ArgumentError): evaluator("h(2, 1)", {}, {"h": h1}, {}) with raises(UnableToParse): evaluator("h()", {}, {"h": h1}, {}) with raises(ArgumentError): evaluator("h(1)", {}, {"h": h2}, {}) with raises(ArgumentError): evaluator("h(1,2,3)", {}, {"h": h2}, {}) with raises(UnableToParse): evaluator("h()", {}, {"h": h3}, {}) with raises(ArgumentError): evaluator("h(1)", {}, {"h": h3}, {}) with raises(ArgumentError): evaluator("h(1,2)", {}, {"h": h3}, {})
def test_in_place_division(): amounts = [5, MathArray(5)] for amount in amounts: A = random_math_array([2, 2]) A_copy = A.copy() A /= amount assert equal_as_arrays(A, A_copy / amount)
def get_shape_description(shape): """ Get shape description from numpy shape tuple or string 'square'. """ if shape == (1, ): return 'scalar' elif shape == 'square': return 'square matrix' return MathArray.get_description(shape)
def test_addition_with_other_types(): A = MathArray([[5, 2], [-2, 4]]) match = "Cannot add/subtract scalars to a matrix." with raises(ShapeError, match=match): A + 1 with raises(ShapeError, match=match): A + 1.0 with raises(ShapeError, match=match): A + (1 + 2j) with raises(TypeError, match="Cannot add/subtract a matrix with object of " "<type 'list'>"): A + [[1, 2], [4, 5]]
def test_subtraction_with_other_types(): A = MathArray([[5, 2], [-2, 4]]) match = "Cannot add/subtract scalars to a matrix." with raises(ShapeError, match=match): A - 1 with raises(ShapeError, match=match): A - 1.0 with raises(ShapeError, match=match): A - (1 + 2j) with raises(TypeError, match="Cannot add/subtract a matrix with object of " "{list_type}".format(list_type=list)): A - [[1, 2], [4, 5]]
def test_division_by_array_raises_error(): A = MathArray([4, 8]) match = "Cannot divide by a vector" with raises(ShapeError, match=match): 2/A match = "Cannot divide a vector by a vector" with raises(ShapeError, match=match): A/A match = "Cannot divide vector by object of <type 'list'>" with raises(TypeError, match=match): A/[1, 2, 3]
def array_abs(obj): """ Takes absolute value of numbers or vectors and suggests norm(...) instead for matrix/tensors. NOTE: The decision to limit abs(...) to scalars and vectors was motivated by pedagogy not software. """ if isinstance(obj, MathArray) and obj.ndim > 1: msg = ("The abs(...) function expects a scalar or vector. To take the " "norm of a {}, try norm(...) instead.".format( MathArray.get_shape_name(obj.ndim))) raise FunctionEvalError(msg) return np.linalg.norm(obj)
def test_scalar_special_cases(): five = MathArray(5) three = MathArray(3) # Addition assert five + three == 8 assert five + 3 == 8 assert 5 + three == 8 # Subtraction assert five - three == 2 assert five - 3 == 2 assert 5 - three == 2 # Powers assert five**3 == 125 assert 5**three == 125 assert five**three == 125 # With MathArrays on the right: match = "Cannot raise a scalar to a <type 'list'> power" with raises(TypeError, match=match): MathArray(2)**[1, 2, 3] match = 'Cannot raise a scalar to power of a vector.' with raises(ShapeError, match=match): MathArray(2)**MathArray([1, 2, 3])
def test_subtraction_with_correct_shapes(): u = MathArray([1, 2, 3]) v = MathArray([10, 20, 30]) w = MathArray([9, 18, 27]) assert equal_as_arrays(v - u, w) A = MathArray([[5, 2, 1], [-2, 4, -3]]) B = MathArray([[2, -1, 4], [3, 0, 1]]) C = MathArray([[3, 3, -3], [-5, 4, -4]]) assert equal_as_arrays(A - B, C)
def test_addition_with_correct_shapes(): u = MathArray([1, 2, 3]) v = MathArray([10, 20, 30]) w = MathArray([11, 22, 33]) assert equal_as_arrays(u + v, w) A = MathArray([[5, 2, 1], [-2, 4, -3]]) B = MathArray([[2, -1, 4], [3, 0, 1]]) C = MathArray([[7, 1, 5], [1, 4, -2]]) assert equal_as_arrays(A + B, C) assert equal_as_arrays(A + B, B + A)
def eval_array(parse_result, metadata_dict): """ Takes in a list of evaluated expressions and returns it as a MathArray. May mutate metadata_dict. If passed a list of numpy arrays, generates a matrix/tensor/etc. Arguments: parse_result: A list containing each element of the array metadata_dict: A dictionary with key 'max_array_dim_used', whose value should be an integer. If the result of eval_array has higher dimension than 'max_array_dim_used', this value will be updated. Usage ===== Returns MathArray instances and updates metadata_dict['max_array_dim_used'] if needed: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([1, 2, 3], metadata_dict) MathArray([1, 2, 3]) >>> metadata_dict['max_array_dim_used'] 1 If metadata_dict['max_array_dim_used'] is larger than returned array value, then metadata_dict is not updated: >>> metadata_dict = { 'max_array_dim_used': 2 } >>> MathExpression.eval_array([1, 2, 3], metadata_dict) MathArray([1, 2, 3]) >>> metadata_dict['max_array_dim_used'] 2 >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... [1 , 2], ... [3, 4] ... ], metadata_dict) MathArray([[1, 2], [3, 4]]) In practice, this is called recursively: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) MathArray([[1, 2, 3], [4, 5, 6]]) >>> metadata_dict['max_array_dim_used'] 2 One complex entry will convert everything to complex: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2j, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) MathArray([[ 1.+0.j, 0.+2.j, 3.+0.j], [ 4.+0.j, 5.+0.j, 6.+0.j]]) We try to detect shape errors: >>> metadata_dict = { 'max_array_dim_used': 0 } >>> try: # doctest: +ELLIPSIS ... MathExpression.eval_array([ ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... 4 ... ], metadata_dict) ... except UnableToParse as error: ... print(error) Unable to parse vector/matrix. If you're trying ... >>> metadata_dict = { 'max_array_dim_used': 0 } >>> try: # doctest: +ELLIPSIS ... MathExpression.eval_array([ ... 2.0, ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... 4 ... ], metadata_dict) ... except UnableToParse as error: ... print(error) Unable to parse vector/matrix. If you're trying ... """ shape_message = ("Unable to parse vector/matrix. If you're trying to " "enter a matrix, this is most likely caused by an " "unequal number of elements in each row.") try: array = MathArray(parse_result) except ValueError: # This happens, for example, with np.array([1, 2, [3]]) # when using numpy version 1.6 raise UnableToParse(shape_message) if array.dtype == 'object': # This happens, for example, with np.array([[1], 2, 3]), # OR with with np.array([1, 2, [3]]) in recent versions of numpy raise UnableToParse(shape_message) if array.ndim > metadata_dict['max_array_dim_used']: metadata_dict['max_array_dim_used'] = array.ndim return array
def test_copying(): A = MathArray([1, 2, 3]) assert equal_as_arrays(A, A.copy())
def cross(a, b): return MathArray([ a[1] * b[2] - b[1] * a[2], a[2] * b[0] - b[2] * a[0], a[0] * b[1] - b[0] * a[1] ])
DEFAULT_SUFFIXES = {'%': 0.01} METRIC_SUFFIXES = { 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12 } pauli = { 'sigma_x': MathArray([[0, 1], [1, 0]]), 'sigma_y': MathArray([[0, -1j], [1j, 0]]), 'sigma_z': MathArray([[1, 0], [0, -1]]) } cartesian_xyz = { 'hatx': MathArray([1, 0, 0]), 'haty': MathArray([0, 1, 0]), 'hatz': MathArray([0, 0, 1]) } cartesian_ijk = { 'hati': MathArray([1, 0, 0]), 'hatj': MathArray([0, 1, 0]), 'hatk': MathArray([0, 0, 1]) }
def test_matrix_power_works_with_floats_and_scalars_if_integral(): A = random_math_array([3, 3]) assert equal_as_arrays(A**2, A**2.0) assert equal_as_arrays(A**2, A**MathArray(2.0))