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
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