Author: Maciej Fijalkowski <fij...@gmail.com> Branch: Changeset: r50201:ea547b8be18f Date: 2011-12-06 10:48 +0200 http://bitbucket.org/pypy/pypy/changeset/ea547b8be18f/
Log: merge matrix-reshape-merge branch. Thanks mattip for doing that. Adds settable shape of an array as well as reshape method/function. diff --git a/pypy/module/micronumpy/__init__.py b/pypy/module/micronumpy/__init__.py --- a/pypy/module/micronumpy/__init__.py +++ b/pypy/module/micronumpy/__init__.py @@ -77,4 +77,5 @@ 'inf': 'app_numpy.inf', 'e': 'app_numpy.e', 'arange': 'app_numpy.arange', + 'reshape': 'app_numpy.reshape', } diff --git a/pypy/module/micronumpy/app_numpy.py b/pypy/module/micronumpy/app_numpy.py --- a/pypy/module/micronumpy/app_numpy.py +++ b/pypy/module/micronumpy/app_numpy.py @@ -36,3 +36,39 @@ j += 1 i += step return arr + +def reshape(a, shape): + '''reshape(a, newshape) + Gives a new shape to an array without changing its data. + + Parameters + ---------- + a : array_like + Array to be reshaped. + newshape : int or tuple of ints + The new shape should be compatible with the original shape. If + an integer, then the result will be a 1-D array of that length. + One shape dimension can be -1. In this case, the value is inferred + from the length of the array and remaining dimensions. + + Returns + ------- + reshaped_array : ndarray + This will be a new view object if possible; otherwise, it will + be a copy. + + + See Also + -------- + ndarray.reshape : Equivalent method. + + Notes + ----- + + It is not always possible to change the shape of an array without + copying the data. If you want an error to be raise if the data is copied, + you should assign the new shape to the shape attribute of the array +''' + if not hasattr(a, 'reshape'): + a = numpypy.array(a) + return a.reshape(shape) diff --git a/pypy/module/micronumpy/interp_numarray.py b/pypy/module/micronumpy/interp_numarray.py --- a/pypy/module/micronumpy/interp_numarray.py +++ b/pypy/module/micronumpy/interp_numarray.py @@ -98,6 +98,107 @@ endshape[i] = remainder[i] return endshape +def get_shape_from_iterable(space, old_size, w_iterable): + new_size = 0 + new_shape = [] + if space.isinstance_w(w_iterable, space.w_int): + new_size = space.int_w(w_iterable) + if new_size < 0: + new_size = old_size + new_shape = [new_size, ] + else: + neg_dim = -1 + batch = space.listview(w_iterable) + #Allow for shape = (1,2,3) or shape = ((1,2,3)) + if len(batch) > 1 and space.issequence_w(batch[0]): + batch = space.listview(batch[0]) + new_size = 1 + if len(batch) < 1: + if old_size == 1: + #Scalars can have an empty size. + new_size = 1 + else: + new_size = 0 + new_shape = [] + i = 0 + for elem in batch: + s = space.int_w(elem) + if s < 0: + if neg_dim >= 0: + raise OperationError(space.w_ValueError, space.wrap( + "can only specify one unknown dimension")) + s = 1 + neg_dim = i + new_size *= s + new_shape.append(s) + i += 1 + if neg_dim >= 0: + new_shape[neg_dim] = old_size / new_size + new_size *= new_shape[neg_dim] + if new_size != old_size: + raise OperationError(space.w_ValueError, + space.wrap("total size of new array must be unchanged")) + return new_shape + +#Recalculating strides. Find the steps that the iteration does for each +#dimension, given the stride and shape. Then try to create a new stride that +#fits the new shape, using those steps. If there is a shape/step mismatch +#(meaning that the realignment of elements crosses from one step into another) +#return None so that the caller can raise an exception. +def calc_new_strides(new_shape, old_shape, old_strides): + #Return the proper strides for new_shape, or None + # if the mapping crosses stepping boundaries + + #Assumes that prod(old_shape) ==prod(new_shape), len(old_shape) > 1 and + # len(new_shape) > 0 + steps = [] + last_step = 1 + oldI = 0 + new_strides = [] + if old_strides[0] < old_strides[-1]: + for i in range(len(old_shape)): + steps.append(old_strides[i] / last_step) + last_step *= old_shape[i] + cur_step = steps[0] + n_new_elems_used = 1 + n_old_elems_to_use = old_shape[0] + for s in new_shape: + new_strides.append(cur_step * n_new_elems_used) + n_new_elems_used *= s + while n_new_elems_used > n_old_elems_to_use: + oldI += 1 + if steps[oldI] != steps[oldI - 1]: + return None + n_old_elems_to_use *= old_shape[oldI] + if n_new_elems_used == n_old_elems_to_use: + oldI += 1 + if oldI >= len(old_shape): + break + cur_step = steps[oldI] + n_old_elems_to_use *= old_shape[oldI] + else: + for i in range(len(old_shape) - 1, -1, -1): + steps.insert(0, old_strides[i] / last_step) + last_step *= old_shape[i] + cur_step = steps[-1] + n_new_elems_used = 1 + oldI = -1 + n_old_elems_to_use = old_shape[-1] + for s in new_shape[::-1]: + new_strides.insert(0, cur_step * n_new_elems_used) + n_new_elems_used *= s + while n_new_elems_used > n_old_elems_to_use: + oldI -= 1 + if steps[oldI] != steps[oldI + 1]: + return None + n_old_elems_to_use *= old_shape[oldI] + if n_new_elems_used == n_old_elems_to_use: + oldI -= 1 + if oldI < -len(old_shape): + break + cur_step = steps[oldI] + n_old_elems_to_use *= old_shape[oldI] + return new_strides # Iterators for arrays # -------------------- @@ -444,6 +545,7 @@ return False i = i.next(shapelen) return True + def descr_all(self, space): return space.wrap(self._all()) @@ -459,6 +561,7 @@ return True i = i.next(shapelen) return False + def descr_any(self, space): return space.wrap(self._any()) @@ -483,6 +586,12 @@ def descr_get_shape(self, space): return space.newtuple([space.wrap(i) for i in self.shape]) + def descr_set_shape(self, space, w_iterable): + concrete = self.get_concrete() + new_shape = get_shape_from_iterable(space, + concrete.find_size(), w_iterable) + concrete.setshape(space, new_shape) + def descr_get_size(self, space): return space.wrap(self.find_size()) @@ -735,6 +844,40 @@ return NDimSlice(self, new_sig, start, strides[:], backstrides[:], shape[:]) + def descr_reshape(self, space, w_args): + """reshape(...) + a.reshape(shape) + + Returns an array containing the same data with a new shape. + + Refer to `%s.reshape` for full documentation. + + See Also + -------- + numpy.reshape : equivalent function +""" % 'numpypy' + concrete = self.get_concrete() + new_shape = get_shape_from_iterable(space, + concrete.find_size(), w_args) + #Since we got to here, prod(new_shape) == self.size + new_strides = calc_new_strides(new_shape, + concrete.shape, concrete.strides) + if new_strides: + #We can create a view, strides somehow match up. + new_sig = signature.Signature.find_sig([ + NDimSlice.signature, self.signature, ]) + ndims = len(new_shape) + new_backstrides = [0] * ndims + for nd in range(ndims): + new_backstrides[nd] = (new_shape[nd] - 1) * new_strides[nd] + arr = NDimSlice(self, new_sig, self.start, new_strides, + new_backstrides, new_shape) + else: + #Create copy with contiguous data + arr = concrete.copy() + arr.setshape(space, new_shape) + return arr + def descr_mean(self, space): return space.div(self.descr_sum(space), space.wrap(self.find_size())) @@ -830,6 +973,11 @@ def debug_repr(self): return 'Scalar' + def setshape(self, space, new_shape): + # In order to get here, we already checked that prod(new_shape)==1, + # so in order to have a consistent API, let it go through. + pass + class VirtualArray(BaseArray): """ Class for representing virtual arrays, such as binary ops or ufuncs @@ -1022,6 +1170,39 @@ return space.wrap(self.shape[0]) return space.wrap(1) + def setshape(self, space, new_shape): + if len(self.shape) < 1: + return + elif len(self.shape) < 2: + #TODO: this code could be refactored into calc_strides + #but then calc_strides would have to accept a stepping factor + strides = [] + backstrides = [] + s = self.strides[0] + if self.order == 'C': + new_shape.reverse() + for sh in new_shape: + strides.append(s) + backstrides.append(s * (sh - 1)) + s *= sh + if self.order == 'C': + strides.reverse() + backstrides.reverse() + new_shape.reverse() + self.strides = strides[:] + self.backstrides = backstrides[:] + self.shape = new_shape[:] + return + new_strides = calc_new_strides(new_shape, self.shape, self.strides) + if new_strides is None: + raise OperationError(space.w_AttributeError, space.wrap( + "incompatible shape for a non-contiguous array")) + new_backstrides = [0] * len(new_shape) + for nd in range(len(new_shape)): + new_backstrides[nd] = (new_shape[nd] - 1) * new_strides[nd] + self.strides = new_strides[:] + self.backstrides = new_backstrides[:] + self.shape = new_shape[:] class NDimSlice(ViewArray): signature = signature.BaseSignature() @@ -1077,9 +1258,11 @@ def copy(self): array = W_NDimArray(self.size, self.shape[:], self.find_dtype()) iter = self.start_iter() + a_iter = array.start_iter() while not iter.done(): - array.setitem(iter.offset, self.getitem(iter.offset)) + array.setitem(a_iter.offset, self.getitem(iter.offset)) iter = iter.next(len(self.shape)) + a_iter = a_iter.next(len(array.shape)) return array class W_NDimArray(BaseArray): @@ -1137,6 +1320,10 @@ return ArrayIterator(self.size) raise NotImplementedError # use ViewIterator simply, test it + def setshape(self, space, new_shape): + self.shape = new_shape + self.calc_strides(new_shape) + def debug_repr(self): return 'Array' @@ -1261,7 +1448,8 @@ __debug_repr__ = interp2app(BaseArray.descr_debug_repr), dtype = GetSetProperty(BaseArray.descr_get_dtype), - shape = GetSetProperty(BaseArray.descr_get_shape), + shape = GetSetProperty(BaseArray.descr_get_shape, + BaseArray.descr_set_shape), size = GetSetProperty(BaseArray.descr_get_size), T = GetSetProperty(BaseArray.descr_get_transpose), @@ -1279,6 +1467,7 @@ dot = interp2app(BaseArray.descr_dot), copy = interp2app(BaseArray.descr_copy), + reshape = interp2app(BaseArray.descr_reshape), ) diff --git a/pypy/module/micronumpy/test/test_numarray.py b/pypy/module/micronumpy/test/test_numarray.py --- a/pypy/module/micronumpy/test/test_numarray.py +++ b/pypy/module/micronumpy/test/test_numarray.py @@ -158,6 +158,13 @@ assert shape_agreement(self.space, [5, 2], [4, 3, 5, 2]) == [4, 3, 5, 2] + def test_calc_new_strides(self): + from pypy.module.micronumpy.interp_numarray import calc_new_strides + assert calc_new_strides([2, 4], [4, 2], [4, 2]) == [8, 2] + assert calc_new_strides([2, 4, 3], [8, 3], [1, 16]) == [1, 2, 16] + assert calc_new_strides([2, 3, 4], [8, 3], [1, 16]) is None + assert calc_new_strides([24], [2, 4, 3], [48, 6, 1]) is None + assert calc_new_strides([24], [2, 4, 3], [24, 6, 2]) == [2] class AppTestNumArray(BaseNumpyAppTest): def test_ndarray(self): @@ -216,8 +223,8 @@ assert a[2] == 4 def test_copy(self): - from numpypy import array - a = array(range(5)) + from numpypy import arange, array + a = arange(5) b = a.copy() for i in xrange(5): assert b[i] == a[i] @@ -227,6 +234,11 @@ a = array(1) assert a.copy() == a + a = arange(8) + b = a[::2] + c = b.copy() + assert (c == b).all() + def test_iterator_init(self): from numpypy import array a = array(range(5)) @@ -339,6 +351,76 @@ c = a[:3] assert c.shape == (3,) + def test_set_shape(self): + from numpypy import array, zeros + a = array([]) + a.shape = [] + a = array(range(12)) + a.shape = (3, 4) + assert (a == [range(4), range(4, 8), range(8, 12)]).all() + a.shape = (3, 2, 2) + assert a[1, 1, 1] == 7 + a.shape = (3, -1, 2) + assert a.shape == (3, 2, 2) + a.shape = 12 + assert a.shape == (12, ) + exc = raises(ValueError, "a.shape = 10") + assert str(exc.value) == "total size of new array must be unchanged" + a = array(3) + a.shape = () + #numpy allows this + a.shape = (1,) + + def test_reshape(self): + from numpypy import array, zeros + a = array(range(12)) + exc = raises(ValueError, "b = a.reshape((3, 10))") + assert str(exc.value) == "total size of new array must be unchanged" + b = a.reshape((3, 4)) + assert b.shape == (3, 4) + assert (b == [range(4), range(4, 8), range(8, 12)]).all() + b[:, 0] = 1000 + assert (a == [1000, 1, 2, 3, 1000, 5, 6, 7, 1000, 9, 10, 11]).all() + a = zeros((4, 2, 3)) + a.shape = (12, 2) + + def test_slice_reshape(self): + from numpypy import zeros, arange + a = zeros((4, 2, 3)) + b = a[::2, :, :] + b.shape = (2, 6) + exc = raises(AttributeError, "b.shape = 12") + assert str(exc.value) == \ + "incompatible shape for a non-contiguous array" + b = a[::2, :, :].reshape((2, 6)) + assert b.shape == (2, 6) + b = arange(20)[1:17:2] + b.shape = (4, 2) + assert (b == [[1, 3], [5, 7], [9, 11], [13, 15]]).all() + c = b.reshape((2, 4)) + assert (c == [[1, 3, 5, 7], [9, 11, 13, 15]]).all() + + z = arange(96).reshape((12, -1)) + assert z.shape == (12, 8) + y = z.reshape((4, 3, 8)) + v = y[:, ::2, :] + w = y.reshape(96) + u = v.reshape(64) + assert y[1, 2, 1] == z[5, 1] + y[1, 2, 1] = 1000 + #z, y, w, v are views of eachother + assert z[5, 1] == 1000 + assert v[1, 1, 1] == 1000 + assert w[41] == 1000 + #u is not a view, it is a copy! + assert u[25] == 41 + + def test_reshape_varargs(self): + skip("How do I do varargs in rpython? reshape should accept a" + " variable number of arguments") + z = arange(96).reshape(12, -1) + y = z.reshape(4, 3, 8) + def test_add(self): from numpypy import array a = array(range(5)) @@ -1155,3 +1237,14 @@ a = arange(0, 0.8, 0.1) assert len(a) == 8 assert arange(False, True, True).dtype is dtype(int) + + +class AppTestRanges(BaseNumpyAppTest): + def test_app_reshape(self): + from numpypy import arange, array, dtype, reshape + a = arange(12) + b = reshape(a, (3, 4)) + assert b.shape == (3, 4) + a = range(12) + b = reshape(a, (3, 4)) + assert b.shape == (3, 4) diff --git a/pypy/module/micronumpy/test/test_zjit.py b/pypy/module/micronumpy/test/test_zjit.py --- a/pypy/module/micronumpy/test/test_zjit.py +++ b/pypy/module/micronumpy/test/test_zjit.py @@ -186,7 +186,8 @@ # sure it was optimized correctly. # XXX the comment above is wrong now. We need preferrably a way to # count the two loops separately - self.check_resops({'setinteriorfield_raw': 4, 'guard_nonnull': 1, 'getfield_gc': 41, + py.test.skip("counting exact number of classes is nonsense") + self.check_resops({'setarrayitem_raw': 4, 'guard_nonnull': 1, 'getfield_gc': 35, 'guard_class': 22, 'int_add': 8, 'float_mul': 2, 'guard_isnull': 2, 'jump': 4, 'int_ge': 4, 'getinteriorfield_raw': 4, 'float_add': 2, 'guard_false': 4, _______________________________________________ pypy-commit mailing list pypy-commit@python.org http://mail.python.org/mailman/listinfo/pypy-commit