Exemplo n.º 1
0
    def __init__(
        self,
        *,
        mygrad_func: Callable[[Tensor], Tensor],
        true_func: Callable[[np.ndarray], np.ndarray],
        num_arrays: Optional[int] = None,
        shapes: Optional[hnp.MutuallyBroadcastableShapesStrategy] = None,
        index_to_bnds: Dict[int, Tuple[int, int]] = None,
        default_bnds: Tuple[float, float] = (-1e6, 1e6),
        index_to_no_go: Dict[int, Sequence[int]] = None,
        kwargs: Union[Callable,
                      Dict[str, Union[Any, Callable[[Any],
                                                    SearchStrategy]]]] = None,
        index_to_arr_shapes: Dict[int, Union[Sequence[int],
                                             SearchStrategy]] = None,
        atol: float = 1e-7,
        rtol: float = 1e-7,
        assumptions: Optional[Callable[..., bool]] = None,
        permit_0d_array_as_float: bool = True,
    ):
        """
        Parameters
        ----------
        mygrad_func : Callable[[numpy.ndarray, ..., bool], mygrad.Tensor]
            The mygrad function whose forward pass validity is being checked.

        true_func : Callable[[numpy.ndarray, ...], numpy.ndarray]
            A known correct version of the function

        num_arrays : Optional[int]
            The number of arrays to be fed to the function

        shapes : Optional[hnp.MutuallyBroadcastableShapesStrategy]
            A strategy that generates all of the input shapes to feed to the function.

        index_to_bnds : Dict[int, Tuple[int, int]]
            Indicate the lower and upper bounds from which the elements
            for array-i is drawn.

        default_bnds : Tuple[float, float]
            Default lower and upper bounds from which all array elements are drawn

        index_to_no_go : Dict[int, Sequence[int]]
            Values that array-i cannot possess. By default, no values are
            excluded.

        index_to_arr_shapes : Dict[int, Union[Sequence[int], hypothesis.searchstrategy.SearchStrategy]]
            The shape for array-i. This can be an exact shape or a hypothesis search
            strategy that draws shapes.
                Default for array-0: `hnp.array_shapes(max_side=3, max_dims=3)`
                Default for array-i: `broadcastable_shapes(arr-0.shape)`

        kwargs : Union[Callable, Dict[str, Union[Any, Callable[[Any], SearchStrategy]]]]
            Keyword arguments and their values to be passed to the functions.
            The values can be hypothesis search-strategies, in which case
            a value when be drawn at test time for that argument using the provided
            strategy.

            Note that any search strategy must be "wrapped" in a function, which
            will be called, passing it the list of arrays as an input argument, such
            that the strategy can draw based on those particular arrays.

        assumptions : Optional[Callable[[arrs, **kwargs], bool]]
            A callable that is fed the generated arrays and keyword arguments that will
            be fed to ``mygrad_func``. If ``assumptions`` returns ``False``, that test
            case will be marked as skipped by hypothesis.

        permit_0d_array_as_float : bool, optional (default=True)
            If True, drawn 0D arrays will potentially be cast to numpy-floats.
        """
        self.tolerances = dict(atol=atol, rtol=rtol)
        index_to_bnds = _to_dict(index_to_bnds)
        index_to_no_go = _to_dict(index_to_no_go)
        kwargs = _to_dict(kwargs)
        index_to_arr_shapes = _to_dict(index_to_arr_shapes)

        if not ((num_arrays is not None) ^ (shapes is not None)):
            raise ValueError(
                f"Either `num_arrays`(={num_arrays}) must be specified "
                f"xor `shapes`(={shapes}) must be specified")

        if shapes is not None:
            if not isinstance(shapes, st.SearchStrategy):
                raise TypeError(
                    f"`shapes` should be "
                    f"Optional[hnp.MutuallyBroadcastableShapesStrategy]"
                    f", got {shapes}")

            shapes_type = (shapes.wrapped_strategy if isinstance(
                shapes, LazyStrategy) else shapes)

            if not isinstance(shapes_type,
                              hnp.MutuallyBroadcastableShapesStrategy):
                raise TypeError(
                    f"`shapes` should be "
                    f"Optional[hnp.MutuallyBroadcastableShapesStrategy]"
                    f", got {shapes}")
            num_arrays = shapes_type.num_shapes

        assert num_arrays > 0

        self.op = mygrad_func
        self.true_func = true_func

        self.index_to_bnds = index_to_bnds
        self.default_bnds = default_bnds
        self.index_to_no_go = index_to_no_go
        self.index_to_arr_shapes = index_to_arr_shapes
        self.kwargs = kwargs
        self.num_arrays = num_arrays
        self.shapes = shapes
        self.assumptions = assumptions
        self.permit_0d_array_as_float = permit_0d_array_as_float

        # stores the indices of the unspecified array shapes
        self.missing_shapes = set(range(self.num_arrays)) - set(
            self.index_to_arr_shapes)

        if shapes is None:
            self.shapes = (hnp.mutually_broadcastable_shapes(
                num_shapes=len(self.missing_shapes))
                           if self.missing_shapes else st.just(
                               hnp.BroadcastableShapes(input_shapes=(),
                                                       result_shape=())))
        else:
            self.shapes = shapes
Exemplo n.º 2
0
    def __init__(
        self,
        *,
        mygrad_func: Callable[[Tensor], Tensor],
        true_func: Callable[[np.ndarray], np.ndarray],
        num_arrays: Optional[int] = None,
        shapes: Optional[hnp.MutuallyBroadcastableShapesStrategy] = None,
        index_to_bnds: Optional[Dict[int, Tuple[int, int]]] = None,
        default_bnds: Tuple[float, float] = (-1e6, 1e6),
        index_to_no_go: Optional[Dict[int, Sequence[int]]] = None,
        index_to_arr_shapes: Optional[Dict[int, Union[Sequence[int],
                                                      SearchStrategy]]] = None,
        index_to_unique: Optional[Union[Dict[int, bool], bool]] = None,
        elements_strategy: Optional[SearchStrategy] = None,
        kwargs: Optional[Union[Callable,
                               Dict[str,
                                    Union[Any,
                                          Callable[[Any],
                                                   SearchStrategy]]]]] = None,
        arrs_from_kwargs: Optional[Dict[int, str]] = None,
        h: float = 1e-20,
        rtol: float = 1e-8,
        atol: float = 1e-8,
        vary_each_element: bool = False,
        use_finite_difference=False,
        assumptions: Optional[Callable[..., bool]] = None,
    ):
        """
        Parameters
        ----------
        mygrad_func : Callable[[numpy.ndarray, ...], mygrad.Tensor]
            The mygrad function whose backward pass validity is being checked.

        true_func : Callable[[numpy.ndarray, ...], numpy.ndarray]
            A known correct version of the function, which is used to compute
            numerical derivatives.

        num_arrays : Optional[int]
            The number of arrays that must be passed to ``mygrad_func``

        shapes : Optional[hnp.MutuallyBroadcastableShapesStrategy]
            A strategy that generates all of the input shapes to feed to the function.

        index_to_bnds : Optional[Dict[int, Tuple[int, int]]]
            Indicate the lower and upper bounds from which the elements
            for array-i is drawn. By default, [-100, 100].

        default_bnds : Tuple[float, float]
            Default lower and upper bounds from which all array elements are drawn.

        index_to_no_go : Optional[Dict[int, Sequence[int]]]
            Values that array-i cannot possess. By default, no values are
            excluded.

        index_to_arr_shapes : Optional[Dict[int, Union[Sequence[int], SearchStrategy]]]
            The shape for array-i. This can be an exact shape or a hypothesis search
            strategy that draws shapes.
                Default for array-0: `hnp.array_shapes(max_side=3, max_dims=3)`
                Default for array-i: `broadcastable_shapes(arr-0.shape)`

        index_to_unique : Optional[Union[Dict[int, bool], bool]]
            Determines whether the elements drawn for each of the input-arrays are
            required to be unique or not. By default this is `False` for each array.
            If a single boolean value is supplied, this is applied for every array.

        elements_strategy : Optional[Union[SearchStrategy]
            The hypothesis-type-strategy used to draw the array elements.
            The default value is ``hypothesis.strategies.floats``.

        kwargs : Optional[Dict[str, Union[Any, Callable[[Any], SearchStrategy]]]]
            Keyword arguments and their values to be passed to the functions.
            The values can be hypothesis search strategies, in which case
            a value when be drawn at test time for that argument.

            Note that any search strategy must be "wrapped" in a function, which
            will be called, passing it the list of arrays as an input argument, such
            that the strategy can draw based on those particular arrays.

        arrs_from_kwargs : Optional[Dict[int, str]]
            The mapping i (int) -> k (str) indicates that array-i should be
            derived from kwargs[k], which must be a numpy array or MyGrad
            tensor.

        vary_each_element : bool, optional (default=False)
            If False, then use a faster numerical derivative that varies entire
            arrays at once: arr -> arr + h; valid only for functions that map over
            entries, like 'add' and 'sum'. Otherwise, the gradient is constructed
            by varying each element of each array independently.

        use_finite_difference : bool, optional (default=False)
            If True, the finite-difference method will be used to compute the numerical
            derivative instead of the complex step method (default). This is necessary
            if the function being tested is not analytic or does not have a complex-value
            implementation.

        assumptions : Optional[Callable[[arrs, **kwargs], bool]]
            A callable that is fed the generated arrays and keyword arguments that will
            be fed to ``mygrad_func``. If ``assumptions`` returns ``False``, that test
            case will be marked as skipped by hypothesis.
        """

        index_to_bnds = _to_dict(index_to_bnds)
        index_to_no_go = _to_dict(index_to_no_go)
        index_to_arr_shapes = _to_dict(index_to_arr_shapes)
        index_to_unique = _to_dict(index_to_unique)
        self.elements_strategy = (elements_strategy if elements_strategy
                                  is not None else st.floats)
        kwargs = _to_dict(kwargs)
        arrs_from_kwargs = _to_dict(arrs_from_kwargs)

        if not set(arrs_from_kwargs) <= (set(range(num_arrays))
                                         if num_arrays is not None else set()):

            raise ValueError(
                "`kwargs_to_arr` must map an array-ID to a kwarg-name. "
                "Got invalid key(s): " +
                ", ".join(k for k in set(arrs_from_kwargs) -
                          (set(range(num_arrays)
                               ) if num_arrays is not None else set())))

        if any(not isinstance(v, str) for v in arrs_from_kwargs.values()):
            raise ValueError(
                "`kwargs_to_arr` must map an array-ID to a kwarg-name."
                "Got invalid key(s): " +
                ", ".join(v for v in arrs_from_kwargs.values()
                          if not isinstance(v, str)))

        self.arrs_from_kwargs = arrs_from_kwargs

        if not ((num_arrays is not None) ^ (shapes is not None)):
            raise ValueError(
                f"Either `num_arrays`(={num_arrays}) must be specified "
                f"xor `shapes`(={shapes}) must be specified")

        if shapes is not None:
            if not isinstance(shapes, st.SearchStrategy):
                raise TypeError(
                    f"`shapes` should be "
                    f"Optional[hnp.MutuallyBroadcastableShapesStrategy]"
                    f", got {shapes}")

            shapes_type = (shapes.wrapped_strategy if isinstance(
                shapes, LazyStrategy) else shapes)

            if not isinstance(shapes_type,
                              hnp.MutuallyBroadcastableShapesStrategy):
                raise TypeError(
                    f"`shapes` should be "
                    f"Optional[hnp.MutuallyBroadcastableShapesStrategy]"
                    f", got {shapes}")
            num_arrays = shapes_type.num_shapes

        assert num_arrays > 0

        self.op = mygrad_func
        self.true_func = true_func

        self.default_bnds = default_bnds
        if isinstance(index_to_bnds, (tuple, list, np.ndarray)):
            index_to_bnds = {k: index_to_bnds for k in range(num_arrays)}
        self.index_to_bnds = index_to_bnds

        if isinstance(index_to_no_go, (tuple, list, np.ndarray)):
            index_to_no_go = {k: index_to_no_go for k in range(num_arrays)}
        self.index_to_no_go = index_to_no_go

        if isinstance(index_to_arr_shapes,
                      (tuple, list, np.ndarray, st.SearchStrategy)):
            index_to_arr_shapes = {
                k: index_to_arr_shapes
                for k in range(num_arrays)
            }
            self.index_to_arr_shapes = index_to_arr_shapes
        self.index_to_arr_shapes = index_to_arr_shapes

        if isinstance(index_to_unique, bool):
            index_to_unique = {k: index_to_unique for k in range(num_arrays)}
        self.index_to_unique = index_to_unique
        self.kwargs = kwargs
        self.num_arrays = num_arrays

        assert isinstance(h, Real) and h > 0
        self.h = h

        self.tolerances = dict(rtol=rtol, atol=atol)

        assert isinstance(vary_each_element, bool)
        self.vary_each_element = vary_each_element

        assert assumptions is None or callable(assumptions)
        self.assumptions = assumptions

        assert isinstance(use_finite_difference, bool)
        self.use_finite_difference = use_finite_difference

        if use_finite_difference and vary_each_element:
            raise NotImplementedError(
                "`finite_difference` does not have an implementation supporting "
                "\n`vary_each_element=True`")

        if use_finite_difference and h < 1e-8:
            from warnings import warn

            warn(
                f"The `finite_difference` method is being used with an h-value of {h}."
                f"\nThis is likely too small, and was intended for use with the complex-step "
                f"\nmethod. Please update `h` in this call to `backprop_test_factory`"
            )

        # stores the indices of the unspecified array shapes
        self.missing_shapes = set(range(self.num_arrays)) - set(
            self.index_to_arr_shapes)

        if shapes is None:
            self.shapes = (hnp.mutually_broadcastable_shapes(
                num_shapes=len(self.missing_shapes))
                           if self.missing_shapes else st.just(
                               hnp.BroadcastableShapes(input_shapes=(),
                                                       result_shape=())))
        else:
            self.shapes = shapes