Skip to content

Commit

Permalink
Improve np.array implementation (#1256)
Browse files Browse the repository at this point in the history
- Ensure `np.array` can be called with one variable argument (previously
this caused an infinite loop over the elements of the array).
- Simplify `_print_NumpyArray` to reduce duplication and fix #1241 and
fix duplicate #1257.
- Improve `np.array` implementation to correct the default order and add
the `ndmin` parameter.
- Add tests for new code.
- Clean up `test_arrays.py` so failures reference the relevant issue,
tuples use `==` instead of `np.array_equal`, and a more comprehensive
check is carried out on returned arrays including checking the datatype
and ordering.

---------

Co-authored-by: EmilyBourne <louise.bourne@gmail.com>
Co-authored-by: Yaman Güçlü <yaman.guclu@gmail.com>
  • Loading branch information
3 people committed Aug 25, 2023
1 parent 4371217 commit 536be30
Show file tree
Hide file tree
Showing 7 changed files with 562 additions and 291 deletions.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.

### Added

### Fixed

- #1499 : Fix passing temporary arrays to functions.
- #1241 : Missing transpose when converting from a C-ordered array to F-ordered array.
- #1241 : Incorrect transpose when copying an F-ordered array.
- #1241 : Fix infinite loop when passing an array as the only argument to `np.array`.

### Changed

### Deprecated

## \[1.9.0\] - 2023-08-22

### Added

- #752 : Allow passing array variables to `numpy.array`.
- #1280 : Allow copying arrays using `numpy.array`.
- Allow interfaces in classes.
Expand All @@ -18,8 +33,8 @@ All notable changes to this project will be documented in this file.

- #682 : Wrong data layout when copying a slice of an array.
- #1453 : Fix error-level developer mode output.
- #1499 : Fix passing temporary arrays to functions.
- \[INTERNALS\] Fix string base class selection.
- #1496 : Fix interfaces which differ only by order or rank.

### Changed

Expand Down
31 changes: 22 additions & 9 deletions pyccel/ast/numpyext.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,12 +502,9 @@ def _process_order(rank, order):
return order

#==============================================================================
# TODO [YG, 18.02.2020]: accept Numpy array argument
# TODO [YG, 18.02.2020]: use order='K' as default, like in numpy.array
# TODO [YG, 22.05.2020]: move dtype & prec processing to __init__
class NumpyArray(NumpyNewArray):
"""
Represents a call to numpy.array for code generation.
Represents a call to `numpy.array` for code generation.
A class representing a call to the NumPy `array` function.
Expand All @@ -521,12 +518,16 @@ class NumpyArray(NumpyNewArray):
order : str
The ordering of the array (C/Fortran).
ndmin : LiteralInteger, int, optional
The minimum number of dimensions that the resulting array should
have.
"""
__slots__ = ('_arg','_dtype','_precision','_shape','_rank','_order')
_attribute_nodes = ('_arg',)
name = 'array'

def __init__(self, arg, dtype=None, order='C'):
def __init__(self, arg, dtype=None, order='K', ndmin=None):

if not isinstance(arg, (PythonTuple, PythonList, Variable)):
raise TypeError('Unknown type of %s.' % type(arg))
Expand All @@ -538,6 +539,17 @@ def __init__(self, arg, dtype=None, order='C'):
if not (is_homogeneous_tuple or is_array):
raise TypeError('we only accept homogeneous arguments')

if not isinstance(order, (LiteralString, str)):
raise TypeError("The order must be specified explicitly with a string.")
elif isinstance(order, LiteralString):
order = order.python_value

if ndmin is not None:
if not isinstance(ndmin, (LiteralInteger, int)):
raise TypeError("The minimum number of dimensions must be specified explicitly with an integer.")
elif isinstance(ndmin, LiteralInteger):
ndmin = ndmin.python_value

init_dtype = dtype

# Verify dtype and get precision
Expand All @@ -546,12 +558,14 @@ def __init__(self, arg, dtype=None, order='C'):
prec = get_final_precision(arg)
else:
dtype, prec = process_dtype(dtype)
# ... Determine ordering
order = str(order).strip("\'")

shape = process_shape(False, arg.shape)
rank = len(shape)

if ndmin and ndmin>rank:
shape = (LiteralInteger(1),)*(ndmin-rank) + shape
rank = ndmin

if rank < 2:
order = None
else:
Expand All @@ -561,9 +575,8 @@ def __init__(self, arg, dtype=None, order='C'):
if order not in ('K', 'A', 'C', 'F'):
raise ValueError(f"Cannot recognize '{order}' order")

# TODO [YG, 18.02.2020]: set correct order based on input array
if order in ('K', 'A'):
order = 'C'
order = arg.order or 'C'
# ...

self._arg = arg
Expand Down
81 changes: 40 additions & 41 deletions pyccel/codegen/printing/fcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -1075,50 +1075,49 @@ def _print_NumpyWhere(self, expr):
return stmt

def _print_NumpyArray(self, expr):
"""Fortran print."""

expr_args = (expr.arg,) if isinstance(expr.arg, Variable) else expr.arg
order = expr.order
# If Numpy array is stored with column-major ordering, transpose values
# use reshape with order for rank > 2
if expr.order == 'F':
if expr.rank == 2:
rhs_code = self._print(expr.arg)
rhs_code = 'transpose({})'.format(rhs_code)
elif expr.rank > 2:
args = [self._print(a) for a in expr.arg]
new_args = []
for ac, a in zip(args, expr.arg):
if a.order == 'C':
shape = ', '.join(self._print(i) for i in a.shape)
order = ', '.join(self._print(LiteralInteger(i)) for i in range(a.rank, 0, -1))
ac = 'reshape({}, [{}], order=[{}])'.format(ac, shape, order)
new_args.append(ac)

args = new_args
rhs_code = '[' + ' ,'.join(args) + ']'
shape = ', '.join(self._print(i) for i in expr.shape)
order = [LiteralInteger(i) for i in range(1, expr.rank+1)]
order = order[1:]+ order[:1]
order = ', '.join(self._print(i) for i in order)
rhs_code = 'reshape({}, [{}], order=[{}])'.format(rhs_code, shape, order)
elif expr.order == 'C':
if expr.rank > 2:
args = [self._print(a) for a in expr.arg]
new_args = []
for ac, a in zip(args, expr.arg):
if a.order == 'F':
shape = ', '.join(self._print(i) for i in a.shape[::-1])
order = ', '.join(self._print(LiteralInteger(i)) for i in range(a.rank, 0, -1))
ac = 'reshape({}, [{}], order=[{}])'.format(ac, shape, order)
new_args.append(ac)

args = new_args
rhs_code = '[' + ' ,'.join(args) + ']'
shape = ', '.join(self._print(i) for i in expr.shape[::-1])
rhs_code = 'reshape({}, [{}])'.format(rhs_code, shape)
else:
rhs_code = self._print(expr.arg)
elif expr.order is None:
if expr.rank <= 2:
rhs_code = self._print(expr.arg)
if expr.arg.order and expr.arg.order != expr.order:
rhs_code = f'transpose({rhs_code})'
if expr.arg.rank < expr.rank:
if order == 'F':
shape_code = ', '.join(self._print(i) for i in expr.shape)
else:
shape_code = ', '.join(self._print(i) for i in expr.shape[::-1])
rhs_code = f"reshape({rhs_code}, [{shape_code}])"
else:
new_args = []
inv_order = 'C' if order == 'F' else 'F'
for a in expr_args:
ac = self._print(a)
if a.order == inv_order:
shape = a.shape if a.order == 'C' else a.shape[::-1]
shape_code = ', '.join(self._print(i) for i in shape)
order_code = ', '.join(self._print(LiteralInteger(i)) for i in range(a.rank, 0, -1))
ac = f'reshape({ac}, [{shape_code}], order=[{order_code}])'
new_args.append(ac)

if len(new_args) == 1:
rhs_code = new_args[0]
else:
rhs_code = '[' + ' ,'.join(new_args) + ']'

if len(new_args) != 1 or expr.arg.rank < expr.rank:
if order == 'C':
shape_code = ', '.join(self._print(i) for i in expr.shape[::-1])
rhs_code = f'reshape({rhs_code}, [{shape_code}])'
else:
shape_code = ', '.join(self._print(i) for i in expr.shape)
order_index = [LiteralInteger(i) for i in range(1, expr.rank+1)]
order_index = order_index[1:]+ order_index[:1]
order_code = ', '.join(self._print(i) for i in order_index)
rhs_code = f'reshape({rhs_code}, [{shape_code}], order=[{order_code}])'


return rhs_code

def _print_NumpyFloor(self, expr):
Expand Down
6 changes: 4 additions & 2 deletions pyccel/codegen/printing/pycode.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,11 +704,13 @@ def _print_Deallocate(self, expr):

def _print_NumpyArray(self, expr):
name = self._get_numpy_name(expr)
arg_var = expr.arg

arg = self._print(expr.arg)
arg = self._print(arg_var)
dtype = self._print_dtype_argument(expr, expr.init_dtype)
order = f"order='{expr.order}'" if expr.order else ''
args = ', '.join(a for a in [arg, dtype, order] if a!= '')
ndmin = f"ndmin={expr.rank}" if expr.rank > arg_var.rank else ''
args = ', '.join(a for a in [arg, dtype, order, ndmin] if a!= '')
return f"{name}({args})"

def _print_NumpyAutoFill(self, expr):
Expand Down
28 changes: 28 additions & 0 deletions tests/epyccel/modules/arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,33 @@ def multiple_2d_stack_array_2():
s = s + b[i][j] - a[j] / c[i][j]
return s

#==============================================================================
# TEST: Array with ndmin argument
#==============================================================================
@template('T', ['int[:]', 'int[:,:]', 'int[:,:,:]', 'int[:,:](order=F)', 'int[:,:,:](order=F)'])
def array_ndmin_1(x : 'T'):
from numpy import array
y = array(x, ndmin=1)
return y

@template('T', ['int[:]', 'int[:,:]', 'int[:,:,:]', 'int[:,:](order=F)', 'int[:,:,:](order=F)'])
def array_ndmin_2(x : 'T'):
from numpy import array
y = array(x, ndmin=2)
return y

@template('T', ['int[:]', 'int[:,:]', 'int[:,:,:]', 'int[:,:](order=F)', 'int[:,:,:](order=F)'])
def array_ndmin_4(x : 'T'):
from numpy import array
y = array(x, ndmin=4)
return y

@template('T', ['int[:]', 'int[:,:]', 'int[:,:,:]', 'int[:,:](order=F)', 'int[:,:,:](order=F)'])
def array_ndmin_2_order(x : 'T'):
from numpy import array
y = array(x, ndmin=2, order='F')
return y

#==============================================================================
# TEST: Product and matrix multiplication
#==============================================================================
Expand Down Expand Up @@ -1602,6 +1629,7 @@ def arrs_2d_different_shapes_0():
pm = np.array([[1, 1, 1]])
x = pn + pm
return np.shape(x)[0], np.shape(x)[1]

def arrs_1d_negative_index_1():
import numpy as np
a = np.zeros(10)
Expand Down

0 comments on commit 536be30

Please sign in to comment.