Repository: pyccel/pyccel (python)

Issue:
error when printing the minimum / maximum of an array of int
### Describe the bug

A Fortran runtime error occurs when printing the minimum value of an array of int
(I am not fully sure this is a bug, but certainly a strange behaviour of the print function)

### To Reproduce
Provide code to reproduce the behavior:
```python
import numpy as np


def test_print_float():
    ii = np.zeros(shape=2)
    ii[:] = 1
    print(" ii = ", ii)
    a = np.amin(ii)
    print(a)
    print(np.amin(ii))

def test_print_int():
    ii = np.empty(shape=2, dtype='int')
    ii[:] = 1
    print(" ii = ", ii)
    a = np.amin(ii)    
    if a == np.amin(ii):
        print('OK')
    else:
        print('NOT OK')
    print(a)
    print(np.amin(ii))
```

### Error details
The generated fortran function is 
```fortran
module debug_pyccel_module


  use, intrinsic :: ISO_C_Binding, only : f64 => C_DOUBLE , i64 => &
        C_INT64_T
  use, intrinsic :: ISO_FORTRAN_ENV, only : stdout => output_unit
  implicit none

  contains

  !........................................
  subroutine test_print_float() 

    implicit none

    real(f64), allocatable :: ii(:)
    real(f64) :: a
    integer(i64) :: i

    allocate(ii(0:1_i64))
    ii = 0.0_f64
    ii(:) = 1_i64
    write(stdout, '(A A)', advance="no") ' ii = ' , ' '
    write(stdout, '(A)', advance="no") '['
    do i = 0_i64, 0_i64, 1_i64
      write(stdout, '(F0.12 A)', advance="no") ii(i) , ' '
    end do
    write(stdout, '(F0.12 A)', advance="no") ii(1_i64) , ']'
    write(stdout, '()', advance="yes") 
    a = minval(ii)
    write(stdout, '(F0.12)', advance="yes") a
    write(stdout, '(F0.12)', advance="yes") minval(ii)
    if (allocated(ii)) then
      deallocate(ii)
    end if

  end subroutine test_print_float
  !........................................

  !........................................
  subroutine test_print_int() 

    implicit none

    integer(i64), allocatable :: ii(:)
    integer(i64) :: a
    integer(i64) :: i

    allocate(ii(0:1_i64))
    ii(:) = 1_i64
    write(stdout, '(A A)', advance="no") ' ii = ' , ' '
    write(stdout, '(A)', advance="no") '['
    do i = 0_i64, 0_i64, 1_i64
      write(stdout, '(I0 A)', advance="no") ii(i) , ' '
    end do
    write(stdout, '(I0 A)', advance="no") ii(1_i64) , ']'
    write(stdout, '()', advance="yes") 
    a = minval(Real(ii, f64))
    if (a == minval(Real(ii, f64))) then
      write(stdout, '(A)', advance="yes") 'OK'
    else
      write(stdout, '(A)', advance="yes") 'NOT OK'
    end if
    write(stdout, '(I0)', advance="yes") a
    write(stdout, '(I0)', advance="yes") minval(Real(ii, f64))
    if (allocated(ii)) then
      deallocate(ii)
    end if

  end subroutine test_print_int
  !........................................

end module debug_pyccel_module
```

The error message when running `test_print_int()` is
```sh
 ii =  [1 1]
OK
1
At line 65 of file debug_pyccel_module.f90 (unit = 6, file = 'stdout')
Fortran runtime error: Expected INTEGER for item 1 in formatted transfer, got REAL
(I0)
 ^
 ```

### Expected behavior
The print command `print(np.amin(ii))` should not give an error, but just print `1`
Note: the function `test_print_float()` yields no error.

### Language
Fortran

### Additional context
I am using pyccel 1.7.3 and python 3.11
The same error is obtained with np.min, np.amax and np.max

Pull request description:
Add `numpy.min` to C printer
Add support for `numpy.min` in the C printer:

- introduce the function `_print_NumpyAmin` in `ccode.py` and `fcode.py`;
- add the macro function `NUMPY_AMIN_[TYPE]` to `ndarrays.c`, and update `ndarrays.h` accordingly;
- add Fortran support for complex numbers thanks to new functions in `pyc_math_f90.f90`.
- add unit tests.

Fixes #1376.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 751637c0..62b8eb82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
 
 ### Added
 
+-   #1649 : Add support for `np.min` in C code.
 -   #1621 : Add support for `np.max` in C code.
 -   #1571 : Add support for the function `tuple`.
 -   #1493 : Add preliminary support for importing classes.
diff --git a/pyccel/ast/numpyext.py b/pyccel/ast/numpyext.py
index 0219d8ea..4f81f90a 100644
--- a/pyccel/ast/numpyext.py
+++ b/pyccel/ast/numpyext.py
@@ -1942,21 +1942,36 @@ class NumpyMod(NumpyUfuncBinary):
             self._dtype = type_info.dtype
             self._precision = type_info.precision
 
-class NumpyAmin(NumpyUfuncUnary):
-    """Represent a call to the amin function in the Numpy library"""
-    __slots__ = ()
-    name = 'amin'
-    def _set_shape_rank(self, x):
-        self._shape     = None
-        self._rank      = 0
+class NumpyAmin(PyccelInternalFunction):
+    """
+    Represents a call to  numpy.min for code generation.
 
-    def _set_dtype_precision(self, x):
-        self._dtype     = x.dtype
-        self._precision = get_final_precision(x)
+    Represents a custom class for handling minimum operations.
+
+    Parameters
+    ----------
+    arg : array_like
+        The input array for which the minimum argument is calculated.
+    """
+    __slots__ = ('_dtype', '_precision', '_class_type')
+    name = 'amin'
+    _rank = 0
+    _shape = None
+    _order = None
+    def __init__(self, arg):
+        super().__init__(arg)
+        self._dtype = arg.dtype
+        self._class_type = self._dtype
+        self._precision = max(arg.precision, default_precision[self._dtype])
 
     @property
-    def is_elemental(self):
-        return False
+    def arg(self):
+        """
+        Get the argument to the min function.
+        
+        This method retrieves the argument used in the min function.
+        """
+        return self._args[0]
 
 class NumpyAmax(PyccelInternalFunction):
     """
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 7169fc7e..ac0c0d4b 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -68,7 +68,6 @@ __all__ = ["CCodePrinter", "ccode"]
 numpy_ufunc_to_c_float = {
     'NumpyAbs'  : 'fabs',
     'NumpyFabs' : 'fabs',
-    'NumpyMin'  : 'minval',
     'NumpyFloor': 'floor',  # TODO: might require special treatment with casting
     # ---
     'NumpyExp' : 'exp',
@@ -92,8 +91,6 @@ numpy_ufunc_to_c_float = {
 
 numpy_ufunc_to_c_complex = {
     'NumpyAbs'  : 'cabs',
-    'NumpyMin'  : 'minval',
-    'NumpyMax'  : 'maxval',
     # ---
     'NumpyExp' : 'cexp',
     'NumpyLog' : 'clog',
@@ -1761,6 +1758,22 @@ class CCodePrinter(CodePrinter):
         elif isinstance(dtype, NativeBool):
             return f'numpy_amax_bool({name})'
 
+    def _print_NumpyAmin(self, expr):
+        '''
+        Convert a call to numpy.min to the equivalent function in C.
+        '''
+        dtype = expr.arg.dtype
+        prec  = expr.arg.precision
+        name  = self._print(expr.arg)
+        if isinstance(dtype, NativeInteger):
+            return f'numpy_amin_int{prec * 8}({name})'
+        elif isinstance(dtype, NativeFloat):
+            return f'numpy_amin_float{prec * 8}({name})'
+        elif isinstance(dtype, NativeComplex):
+            return f'numpy_amin_complex{prec * 16}({name})'
+        elif isinstance(dtype, NativeBool):
+            return f'numpy_amin_bool({name})'
+
     def _print_NumpyLinspace(self, expr):
         template = '({start} + {index}*{step})'
         if not isinstance(expr.endpoint, LiteralFalse):
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 14544de9..683db2b0 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -82,8 +82,6 @@ __all__ = ["FCodePrinter", "fcode"]
 numpy_ufunc_to_fortran = {
     'NumpyAbs'  : 'abs',
     'NumpyFabs'  : 'abs',
-    'NumpyMin'  : 'minval',
-    'NumpyAmin'  : 'minval',
     'NumpyFloor': 'floor',  # TODO: might require special treatment with casting
     # ---
     'NumpyExp' : 'exp',
@@ -1279,7 +1277,6 @@ class FCodePrinter(CodePrinter):
         return init_value
     
     def _print_NumpyAmax(self, expr):
-        # Assuming expr.arg is an array
         array_arg = expr.arg
         if array_arg.dtype is NativeBool():
             arg_code = self._print(NumpyInt32(array_arg))
@@ -1291,7 +1288,20 @@ class FCodePrinter(CodePrinter):
             return f'amax({array_arg})'
         else:
             return f'maxval({arg_code})'
+    
+    def _print_NumpyAmin(self, expr):
+        array_arg = expr.arg
+        if array_arg.dtype is NativeBool():
+            arg_code = self._print(NumpyInt32(array_arg))
+        else:
+            arg_code = self._print(array_arg)
 
+        if array_arg.dtype is NativeComplex():
+            self._additional_imports.add(Import('pyc_math_f90', Module('pyc_math_f90',(),())))
+            return f'amin({array_arg})'
+        else:
+            return f'minval({arg_code})'
+        
     def _print_PythonMin(self, expr):
         args = expr.args
         if len(args) == 1:
diff --git a/pyccel/stdlib/math/pyc_math_f90.f90 b/pyccel/stdlib/math/pyc_math_f90.f90
index 3b64507e..593cead8 100644
--- a/pyccel/stdlib/math/pyc_math_f90.f90
+++ b/pyccel/stdlib/math/pyc_math_f90.f90
@@ -27,8 +27,13 @@ interface pyc_lcm
 end interface pyc_lcm
 
 interface amax
-    module procedure amax_00
-    module procedure amax_01
+    module procedure amax_4
+    module procedure amax_8
+end interface
+
+interface amin
+    module procedure amin_4
+    module procedure amin_8
 end interface
 
 contains
@@ -162,48 +167,93 @@ pure function pyc_degrees(rad) result(deg)
 end function pyc_degrees
 
 ! Implementation of numpy amax function
-function amax_00(arr) result(result_0001)
+function amax_4(arr) result(max_value)
 
     implicit none
 
-    complex( C_FLOAT_COMPLEX)                :: result_0001
+    complex( C_FLOAT_COMPLEX)                :: max_value
     complex( C_FLOAT_COMPLEX), intent(in)    :: arr(0:)
     complex( C_FLOAT_COMPLEX)                :: a
-    integer(C_INT64_T)                :: Dummy_0000
+    integer(C_INT64_T)                       :: current_value
+
+    max_value = arr(0_C_INT64_T)
+    do current_value = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
+      a = arr(current_value)
+      if (Real(a, C_FLOAT) > Real(max_value, C_FLOAT) .or. (Real(a, C_FLOAT) == &
+            Real(max_value, C_FLOAT) .and. aimag(a) > aimag(max_value &
+            ))) then
+        max_value = a
+      end if
+    end do
+    return
+
+  end function amax_4
+  
+  function amax_8(arr) result(max_value)
+
+    implicit none
+
+    complex(C_DOUBLE_COMPLEX)                :: max_value
+    complex(C_DOUBLE_COMPLEX), intent(in)    :: arr(0:)
+    complex(C_DOUBLE_COMPLEX)                :: a
+    integer(C_INT64_T)                       :: current_value
 
-    result_0001 = arr(0_C_INT64_T)
-    do Dummy_0000 = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
-      a = arr(Dummy_0000)
-      if (Real(a, C_FLOAT) > Real(result_0001, C_FLOAT) .or. (Real(a, C_FLOAT) == &
-            Real(result_0001, C_FLOAT) .and. aimag(a) > aimag(result_0001 &
+    max_value = arr(0_C_INT64_T)
+    do current_value = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
+      a = arr(current_value)
+      if (Real(a, C_DOUBLE) > Real(max_value, C_DOUBLE) .or. (Real(a, C_DOUBLE) == &
+            Real(max_value, C_DOUBLE) .and. aimag(a) > aimag(max_value &
             ))) then
-        result_0001 = a
+        max_value = a
       end if
     end do
     return
 
-  end function amax_00
+  end function amax_8
   
-  function amax_01(arr) result(result_0001)
+! Implementation of numpy amin function
+function amin_4(arr) result(min_value)
+
+    implicit none
+
+    complex( C_FLOAT_COMPLEX)                :: min_value
+    complex( C_FLOAT_COMPLEX), intent(in)    :: arr(0:)
+    complex( C_FLOAT_COMPLEX)                :: a
+    integer(C_INT64_T)                       :: current_value
+
+    min_value = arr(0_C_INT64_T)
+    do current_value = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
+      a = arr(current_value)
+      if (Real(a, C_FLOAT) < Real(min_value, C_FLOAT) .or. (Real(a, C_FLOAT) == &
+            Real(min_value, C_FLOAT) .and. aimag(a) < aimag(min_value &
+            ))) then
+        min_value = a
+      end if
+    end do
+    return
+
+  end function amin_4
+
+  function amin_8(arr) result(min_value)
 
     implicit none
 
-    complex(C_DOUBLE_COMPLEX)                :: result_0001
+    complex(C_DOUBLE_COMPLEX)                :: min_value
     complex(C_DOUBLE_COMPLEX), intent(in)    :: arr(0:)
     complex(C_DOUBLE_COMPLEX)                :: a
-    integer(C_INT64_T)                :: Dummy_0000
+    integer(C_INT64_T)                       :: current_value
 
-    result_0001 = arr(0_C_INT64_T)
-    do Dummy_0000 = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
-      a = arr(Dummy_0000)
-      if (Real(a, C_DOUBLE) > Real(result_0001, C_DOUBLE) .or. (Real(a, C_DOUBLE) == &
-            Real(result_0001, C_DOUBLE) .and. aimag(a) > aimag(result_0001 &
+    min_value = arr(0_C_INT64_T)
+    do current_value = 1_C_INT64_T, size(arr, kind=C_INT64_T) - 1_C_INT64_T
+      a = arr(current_value)
+      if (Real(a, C_DOUBLE) < Real(min_value, C_DOUBLE) .or. (Real(a, C_DOUBLE) == &
+            Real(min_value, C_DOUBLE) .and. aimag(a) < aimag(min_value &
             ))) then
-        result_0001 = a
+        min_value = a
       end if
     end do
     return
 
-  end function amax_01
+  end function amin_8
 
 end module pyc_math_f90
diff --git a/pyccel/stdlib/ndarrays/ndarrays.c b/pyccel/stdlib/ndarrays/ndarrays.c
index f18c03d9..98e99901 100644
--- a/pyccel/stdlib/ndarrays/ndarrays.c
+++ b/pyccel/stdlib/ndarrays/ndarrays.c
@@ -620,3 +620,37 @@ NUMPY_AMAX_(float64, double, double)
 NUMPY_AMAX_(complex64, float complex, cfloat)
 NUMPY_AMAX_(complex128, double complex, cdouble)
 
+#define NUMPY_AMIN_(NAME, TYPE, CTYPE) \
+    TYPE numpy_amin_##NAME(t_ndarray arr) \
+    { \
+        int64_t nd_indices[arr.nd]; \
+        memset(nd_indices, 0, sizeof(int64_t) * arr.nd); \
+        TYPE output = arr.nd_##CTYPE[get_index_from_array(arr, nd_indices)]; \
+        for (int32_t i = 0; i < arr.length; i++) \
+        { \
+            TYPE current_value = arr.nd_##CTYPE[get_index_from_array(arr, nd_indices)]; \
+            if (creal(current_value) < creal(output) || \
+                (creal(current_value) == creal(output) && cimag(current_value) < cimag(output))) \
+            { \
+                output = current_value; \
+            } \
+            nd_indices[0]++; \
+            for (int32_t j = 0; j < arr.nd - 1; j++) \
+                if (nd_indices[j] == arr.shape[j]) \
+                { \
+                    nd_indices[j] = 0; \
+                    nd_indices[j + 1]++; \
+                } \
+        } \
+        return output; \
+    }
+
+NUMPY_AMIN_(bool, int64_t, bool)
+NUMPY_AMIN_(int8, int64_t, int8)
+NUMPY_AMIN_(int16, int64_t, int16)
+NUMPY_AMIN_(int32, int64_t, int32)
+NUMPY_AMIN_(int64, int64_t, int64)
+NUMPY_AMIN_(float32, float, float)
+NUMPY_AMIN_(float64, double, double)
+NUMPY_AMIN_(complex64, float complex, cfloat)
+NUMPY_AMIN_(complex128, double complex, cdouble)
diff --git a/pyccel/stdlib/ndarrays/ndarrays.h b/pyccel/stdlib/ndarrays/ndarrays.h
index 08a806c4..fa113c18 100644
--- a/pyccel/stdlib/ndarrays/ndarrays.h
+++ b/pyccel/stdlib/ndarrays/ndarrays.h
@@ -167,7 +167,8 @@ double             numpy_sum_float64(t_ndarray arr);
 float complex      numpy_sum_complex64(t_ndarray arr);
 double complex     numpy_sum_complex128(t_ndarray arr);
 
-/*numpy max*/
+/*numpy max/amax */
+
 int64_t            numpy_amax_bool(t_ndarray arr);
 int64_t            numpy_amax_int8(t_ndarray arr);
 int64_t            numpy_amax_int16(t_ndarray arr);
@@ -178,4 +179,16 @@ double             numpy_amax_float64(t_ndarray arr);
 float complex      numpy_amax_complex64(t_ndarray arr);
 double complex     numpy_amax_complex128(t_ndarray arr);
 
+/* numpy min/amin */
+
+int64_t            numpy_amin_bool(t_ndarray arr);
+int64_t            numpy_amin_int8(t_ndarray arr);
+int64_t            numpy_amin_int16(t_ndarray arr);
+int64_t            numpy_amin_int32(t_ndarray arr);
+int64_t            numpy_amin_int64(t_ndarray arr);
+float              numpy_amin_float32(t_ndarray arr);
+double             numpy_amin_float64(t_ndarray arr);
+float complex      numpy_amin_complex64(t_ndarray arr);
+double complex     numpy_amin_complex128(t_ndarray arr);
+
 #endif


Test patch:
diff --git a/tests/epyccel/recognised_functions/test_numpy_funcs.py b/tests/epyccel/recognised_functions/test_numpy_funcs.py
index 2a1cd941..c51fbf2a 100644
--- a/tests/epyccel/recognised_functions/test_numpy_funcs.py
+++ b/tests/epyccel/recognised_functions/test_numpy_funcs.py
@@ -2181,15 +2181,6 @@ def test_sum_property(language):
     x = randint(99,size=10)
     assert(f1(x) == sum_call(x))
 
-@pytest.mark.parametrize( 'language', (
-        pytest.param("fortran", marks = pytest.mark.fortran),
-        pytest.param("c", marks = [
-            pytest.mark.skip(reason="amin not implemented"),
-            pytest.mark.c]
-        ),
-        pytest.param("python", marks = pytest.mark.python)
-    )
-)
 def test_min_int(language):
     def min_call(x : 'int[:]'):
         from numpy import amin
@@ -2199,15 +2190,6 @@ def test_min_int(language):
     x = randint(99,size=10)
     assert(f1(x) == min_call(x))
 
-@pytest.mark.parametrize( 'language', (
-        pytest.param("fortran", marks = pytest.mark.fortran),
-        pytest.param("c", marks = [
-            pytest.mark.skip(reason="amin not implemented"),
-            pytest.mark.c]
-        ),
-        pytest.param("python", marks = pytest.mark.python)
-    )
-)
 def test_min_real(language):
     def min_call(x : 'float[:]'):
         from numpy import amin
@@ -2217,6 +2199,28 @@ def test_min_real(language):
     x = rand(10)
     assert(isclose(f1(x), min_call(x), rtol=RTOL, atol=ATOL))
 
+def test_min_complex(language):
+    def min_call(x: 'complex128[:]'):
+        from numpy import amin
+        return amin(x)
+
+    f1 = epyccel(min_call, language=language)
+    x = randn(10) + 1j * randn(10)  
+    assert np.allclose(f1(x), min_call(x))
+    x = randn(10) + 1j  
+    assert np.allclose(f1(x), min_call(x))
+    x = 10 + 1j * randn(10) 
+    assert np.allclose(f1(x), min_call(x))
+
+def test_min_bool(language):
+    def min_call(x: 'bool[:]'):
+        from numpy import amin
+        return amin(x)
+
+    f1 = epyccel(min_call, language=language)
+    x = np.array([True, False, True, False])  # Generating a boolean array
+    assert f1(x) == min_call(x)
+
 @pytest.mark.parametrize( 'language', (
         pytest.param("fortran", marks = pytest.mark.fortran),
         pytest.param("c", marks = [