Esempio n. 1
0
def factorial(z):
    """
    Factorial function over complex numbers, using the gamma function.
    Note that math.factorial will return long ints, which are problematic when running
    into overflow issues. The gamma function just returns inf.

    Usage
    =====

    Non-negative integer input returns floats:
    >>> factorial(4)
    24.0

    Floats and complex numbers use scipy's gamma function:
    >>> import math
    >>> factorial(0.5) # doctest: +ELLIPSIS
    0.8862269...
    >>> math.sqrt(math.pi)/2 # doctest: +ELLIPSIS
    0.8862269...
    >>> factorial(3.2+4.1j) # doctest: +ELLIPSIS
    (1.0703272...-0.3028032...j)
    >>> factorial(2.2+4.1j)*(3.2+4.1j) # doctest: +ELLIPSIS
    (1.0703272...-0.3028032...j)

    Works with numpy arrays:
    >>> np.array_equal(
    ...     factorial(np.array([1, 2, 3, 4])),
    ...     np.array([1, 2, 6, 24])
    ... )
    True

    Really big numbers return inf:
    >>> factorial(500) == float('inf')
    True
    >>> factorial(500.5) == float('inf')
    True

    Throws errors at poles:
    >>> factorial(-2)                           # doctest: +ELLIPSIS
    Traceback (most recent call last):
    FunctionEvalError: Error evaluating factorial() or fact() in input...
    """

    try:
        is_integer = isinstance(z, int) or z.is_integer()
    except AttributeError:
        is_integer = False

    if is_integer and z < 0:
        msg = ("Error evaluating factorial() or fact() in input. These "
               "functions cannot be used at negative integer values.")
        raise FunctionEvalError(msg)

    value = special.gamma(z + 1)
    # value is a numpy array; If it's 0d, we can just get its item:
    try:
        return value.item()
    except ValueError:
        return value
Esempio n. 2
0
def arctan2(x, y):
    """
    Returns the an angle in range (-pi, pi] whose tangent is y/x, taking into
    account the quadrant that (x, y) is in.
    """
    if x == 0 and y == 0:
        raise FunctionEvalError("arctan2(0, 0) is undefined")

    return np.arctan2(y, x)
Esempio n. 3
0
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 eval_function(parse_result, functions):
        """
        Evaluates a function

        Arguments:
            parse_result: ['funcname', arglist]

        Usage
        =====
        Instantiate a parser and some functions:
        >>> import numpy as np
        >>> functions = {"sin": np.sin, "cos": np.cos}

        Single variable functions work:
        >>> MathExpression.eval_function(['sin', [0]], functions)
        0.0
        >>> MathExpression.eval_function(['cos', [0]], functions)
        1.0

        So do multivariable functions:
        >>> def h(x, y): return x + y
        >>> MathExpression.eval_function(['h', [1, 2]], {"h": h})
        3

        Validation:
        ==============================
        By default, eval_function inspects its function's arguments to first
        validate that the correct number of arguments are passed:

        >>> def h(x, y): return x + y
        >>> try:
        ...     MathExpression.eval_function(['h', [1, 2, 3]], {"h": h})
        ... except ArgumentError as error:
        ...     print(error)
        Wrong number of arguments passed to h(...): Expected 2 inputs, but received 3.

        However, if the function to be evaluated has a truthy 'validated'
        property, we assume it does its own validation and we do not check the
        number of arguments.

        >>> from mitxgraders.exceptions import StudentFacingError
        >>> def g(*args):
        ...     if len(args) != 2:
        ...         raise StudentFacingError('I need two inputs!')
        ...     return args[0]*args[1]
        >>> g.validated = True
        >>> try:
        ...     MathExpression.eval_function(['g', [1]], {"g": g})
        ... except StudentFacingError as error:
        ...     print(error)
        I need two inputs!
        """
        # Obtain the function and arguments
        name, args = parse_result
        func = functions[name]

        # If function does not do its own validation, try and validate here.
        if not getattr(func, 'validated', False):
            MathExpression.validate_function_call(func, name, args)

        # Try to call the function
        try:
            return func(*args)
        except StudentFacingError:
            raise
        except ZeroDivisionError:
            # It would be really nice to tell student the symbolic argument as part of this message,
            # but making symbolic argument available would require some nontrivial restructing
            msg = ("There was an error evaluating {name}(...). "
                   "Its input does not seem to be in its domain.").format(
                       name=name)
            raise CalcZeroDivisionError(msg)
        except OverflowError:
            msg = ("There was an error evaluating {name}(...). "
                   "(Numerical overflow).").format(name=name)
            raise CalcOverflowError(msg)
        except Exception:  # pylint: disable=W0703
            # Don't know what this is, or how you want to deal with it
            # Call it a domain issue.
            msg = ("There was an error evaluating {name}(...). "
                   "Its input does not seem to be in its domain.").format(
                       name=name)
            raise FunctionEvalError(msg)