Repository: ivy-llc/ivy
Pull request: Solve numpy.vector_norm assertion error in case of 0-dim array input
Solved by converting it to 1-dim array before computation and then convert it back to 0-dim in case of keepdims

Fix #21099 

Problem statement:
numpy.vector_norm has too high rounding errors
Running `test_vector_norm` for the numpy backend with tolerance set to `1e-03` still causes an assertion error:
```
FAILED           [ 25%]WARNING:tensorflow:You are casting an input of type complex128 to an incompatible dtype float64.  This will discard the imaginary part and may not be what you intended.
WARNING:tensorflow:You are casting an input of type complex128 to an incompatible dtype float64.  This will discard the imaginary part and may not be what you intended.

ivy_tests/test_ivy/test_functional/test_core/test_linalg.py:721 (test_vector_norm[cpu-numpy-False-False])
@handle_test(
>       fn_tree="functional.ivy.vector_norm",
        dtype_values_axis=helpers.dtype_values_axis(
            available_dtypes=helpers.get_dtypes("numeric"),
            valid_axis=True,
            min_value=-1e04,
            max_value=1e04,
            abs_smallest_val=1e-04,
            max_axes_size=2,
            allow_neg_axes=True,
        ),
        kd=st.booleans(),
        ord=st.one_of(
            helpers.ints(min_value=-5, max_value=5),
            helpers.floats(min_value=-5, max_value=5.0),
            st.sampled_from((float("inf"), -float("inf"))),
        ),
        dtype=helpers.get_dtypes("numeric", full=False, none=True),
    )

test_linalg.py:723: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
test_linalg.py:749: in test_vector_norm
    helpers.test_function(
../../helpers/function_testing.py:467: in test_function
    value_test(
../../helpers/assertions.py:141: in value_test
    assert_all_close(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

ret_np = 2.236068, ret_from_gt_np = array(2.2379644, dtype=float32)
backend = 'numpy', rtol = 1e-05, atol = 0.001
ground_truth_backend = 'tensorflow'

    def assert_all_close(
        ret_np,
        ret_from_gt_np,
        backend: str,
        rtol=1e-05,
        atol=1e-08,
        ground_truth_backend="TensorFlow",
    ):
        """
        Match the ret_np and ret_from_gt_np inputs element-by-element to ensure that they
        are the same.
    
        Parameters
        ----------
        ret_np
            Return from the framework to test. Ivy Container or Numpy Array.
        ret_from_gt_np
            Return from the ground truth framework. Ivy Container or Numpy Array.
        rtol
            Relative Tolerance Value.
        atol
            Absolute Tolerance Value.
        ground_truth_backend
            Ground Truth Backend Framework.
    
        Returns
        -------
        None if the test passes, else marks the test as failed.
        """
        ret_dtype = str(ret_np.dtype)
        ret_from_gt_dtype = str(ret_from_gt_np.dtype).replace("longlong", "int64")
        assert (
            ret_dtype == ret_from_gt_dtype
        ), (
            "the ground truth framework {} returned a {} datatype while "
            "the backend {} returned a {} datatype".format(
                ground_truth_backend,
                ret_from_gt_dtype,
                backend,
                ret_dtype,
            )
        )
        # TODO eanble
        # if ivy.is_ivy_container(ret_np) and ivy.is_ivy_container(ret_from_gt_np):
        #     ivy.Container.cont_multi_map(assert_all_close, [ret_np, ret_from_gt_np])
        # else:
        if ret_np.dtype == "bfloat16" or ret_from_gt_np.dtype == "bfloat16":
            ret_np = ret_np.astype("float64")
            ret_from_gt_np = ret_from_gt_np.astype("float64")
>       assert np.allclose(
            np.nan_to_num(ret_np), np.nan_to_num(ret_from_gt_np), rtol=rtol, atol=atol
        ), (
            f" the results from backend {backend} "
            f"and ground truth framework {ground_truth_backend} "
            f"do not match\n {ret_np}!={ret_from_gt_np} \n\n"
        )
E       AssertionError:  the results from backend numpy and ground truth framework tensorflow do not match
E        2.2360680103302!=2.237964391708374 
E       
E       
E       Falsifying example: test_vector_norm(
E           backend_fw='numpy',
E           on_device='cpu',
E           dtype_values_axis=(['complex128'], [array(-1.-2.j)], None),
E           kd=False,
E           ord=-3.43322753906251e-05,
E           dtype=['complex64'],
E           fn_name='vector_norm',
E           test_flags=FunctionTestFlags(
E               ground_truth_backend='tensorflow',
E               num_positional_args=1,
E               with_out=False,
E               instance_method=False,
E               test_gradients=None,
E               test_compile=None,
E               as_variable=[False],
E               native_arrays=[False],
E               container=[False],
E           ),
E       )
E       
E       You can reproduce this example by temporarily adding @reproduce_failure('6.82.0', b'AXicY2AAAkYGCGBEYjMxMIK5B96DuSwwcQYAHPcBvQ==') as a decorator on your test case

../../helpers/assertions.py:62: AssertionError
```

Hints from issue discussion:

Commit message: Solve numpy.vector_norm assertion error in case of 0-dim array input

Solved by converting it to 1-dim array before computation and then convert it back to 0-dim in case of keepdims

Patch to review:
diff --git a/ivy/functional/backends/numpy/linear_algebra.py b/ivy/functional/backends/numpy/linear_algebra.py
index b8b1971f77fdd..1aae1f7164ccf 100644
--- a/ivy/functional/backends/numpy/linear_algebra.py
+++ b/ivy/functional/backends/numpy/linear_algebra.py
@@ -388,10 +388,19 @@ def vector_norm(
     elif ord == -inf:
         return np.min(abs_x, axis=axis, keepdims=keepdims, out=out)
     else:
-        return (
+        # There is a rounding error whenever the input is a 0-dim
+        # The solution at the moment is to convert the 0-dim array to 1-dim
+        # and then convert it back to 0-dim in case of keepdims=True
+        abs_x = np.array(np.abs(x), ndmin=1)
+        res = (
             np.sum(abs_x**ord, axis=axis, keepdims=keepdims) ** (1.0 / ord)
         ).astype(abs_x.dtype)
 
+        if keepdims and x.ndim == 0:
+            res = res.reshape(x.shape)
+
+        return res
+
 
 # Extra #
 # ----- #


Code review comments:
diff_hunk: @@ -388,10 +388,19 @@ def vector_norm(
     elif ord == -inf:
         return np.min(abs_x, axis=axis, keepdims=keepdims, out=out)
     else:
-        return (
+        # There is a rounding error whenever the input is a 0-dim
+        # The solution at the moment is to convert the 0-dim array to 1-dim
+        # and then convert it back to 0-dim in case of keepdims=True
+        abs_x = np.array(np.abs(x), ndmin=1)
path: ivy/functional/backends/numpy/linear_algebra.py
text: @user1:
I think we could just do a `np.abs(x).expand_dims` instead based on the dimensions, `np.array` creates a new copy of the array ig
Happy to know what you think, thanks @author 😄

@author:
Good point @user1 
Let me try and test it with `expand_dims`

@author:
Just done!