1095 lines
36 KiB
Python
1095 lines
36 KiB
Python
# Copyright (c) 2014, Almar Klein and Wade Brainerd
|
|
# tinynumpy is distributed under the terms of the MIT License.
|
|
#
|
|
# Original code by Wade Brainerd (https://github.com/wadetb/tinyndarray)
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
"""
|
|
A lightweight, pure Python, numpy compliant ndarray class.
|
|
|
|
The documenation in this module is rather compact. For details on each
|
|
function, see the corresponding documentation at:
|
|
http://docs.scipy.org/doc/numpy/reference/index.html Be aware that the
|
|
behavior of tinynumpy may deviate in some ways from numpy, or that
|
|
certain features may not be supported.
|
|
"""
|
|
|
|
# todo: keep track of readonly better
|
|
# todo: mathematical operators
|
|
# todo: more methods?
|
|
# todo: logspace, meshgrid
|
|
# todo: Fortran order?
|
|
|
|
from __future__ import division
|
|
|
|
import sys
|
|
|
|
###GOBBO
|
|
###This is a fix for ctypes operations like ctypes.c_ulong * 3 that requires __main__
|
|
import __builtin__
|
|
sys.modules["__main__"]=__builtin__
|
|
__main__ = __builtin__
|
|
|
|
###TODO
|
|
# a = array([[1, 2, 3, 4],[5, 6, 7, 8]])
|
|
# a[:] generates the following error:
|
|
# AttributeError: 'module' object has no attribute 'Array'
|
|
|
|
|
|
import ctypes
|
|
|
|
from math import sqrt
|
|
|
|
# Python 2/3 compat
|
|
if sys.version_info >= (3, ):
|
|
xrange = range
|
|
|
|
# Define version numer
|
|
__version__ = '0.0.1dev'
|
|
|
|
# Define dtypes: struct name, short name, numpy name, ctypes type
|
|
_dtypes = [('B', 'b1', 'bool', ctypes.c_bool),
|
|
('b', 'i1', 'int8', ctypes.c_int8),
|
|
('B', 'u1', 'uint8', ctypes.c_uint8),
|
|
('h', 'i2', 'int16', ctypes.c_int16),
|
|
('H', 'u2', 'uint16', ctypes.c_uint16),
|
|
('i', 'i4', 'int32', ctypes.c_int32),
|
|
('I', 'u4', 'uint32', ctypes.c_uint32),
|
|
('q', 'i8', 'int64', ctypes.c_int64),
|
|
('Q', 'u8', 'uint64', ctypes.c_uint64),
|
|
('f', 'f4', 'float32', ctypes.c_float),
|
|
('d', 'f8', 'float64', ctypes.c_double),
|
|
]
|
|
|
|
# Inject common dtype names
|
|
_known_dtypes = [d[2] for d in _dtypes]
|
|
for d in _known_dtypes:
|
|
globals()[d] = d
|
|
|
|
newaxis = None
|
|
|
|
|
|
def _convert_dtype(dtype, to='numpy'):
|
|
""" Convert dtype, if could not find, pass as it was.
|
|
"""
|
|
if dtype is None:
|
|
return dtype
|
|
dtype = str(dtype)
|
|
index = {'array':0, 'short':1, 'numpy':2, 'ctypes':3}[to]
|
|
for dd in _dtypes:
|
|
if dtype in dd:
|
|
return dd[index]
|
|
return dtype # Otherwise return original
|
|
|
|
|
|
def _ceildiv(a, b):
|
|
return -(-a // b)
|
|
|
|
|
|
def _get_step(view):
|
|
""" Return step to walk over array. If 1, the array is fully
|
|
C-contiguous. If 0, the striding is such that one cannot
|
|
step through the array.
|
|
"""
|
|
cont_strides = _strides_for_shape(view.shape, view.itemsize)
|
|
|
|
step = view.strides[-1] // cont_strides[-1]
|
|
corrected_strides = tuple([i * step for i in cont_strides])
|
|
|
|
almost_cont = view.strides == corrected_strides
|
|
if almost_cont:
|
|
return step
|
|
else:
|
|
return 0 # not contiguous
|
|
|
|
|
|
def _strides_for_shape(shape, itemsize):
|
|
strides = []
|
|
stride_product = 1
|
|
for s in reversed(shape):
|
|
strides.append(stride_product)
|
|
stride_product *= s
|
|
return tuple([i * itemsize for i in reversed(strides)])
|
|
|
|
|
|
def _size_for_shape(shape):
|
|
stride_product = 1
|
|
for s in shape:
|
|
stride_product *= s
|
|
return stride_product
|
|
|
|
|
|
def squeeze_strides(s):
|
|
""" Pop strides for singular dimensions. """
|
|
return tuple([s[0]] + [s[i] for i in range(1, len(s)) if s[i] != s[i-1]])
|
|
|
|
def _shape_from_object(obj):
|
|
|
|
shape = []
|
|
# todo: make more efficient, use len() etc
|
|
def _shape_from_object_r(index, element, axis):
|
|
try:
|
|
for i, e in enumerate(element):
|
|
_shape_from_object_r(i, e, axis+1)
|
|
while len(shape) <= axis:
|
|
shape.append(0)
|
|
l = i + 1
|
|
s = shape[axis]
|
|
if l > s:
|
|
shape[axis] = l
|
|
except TypeError:
|
|
pass
|
|
|
|
_shape_from_object_r(0, obj, 0)
|
|
return tuple(shape)
|
|
|
|
|
|
def _assign_from_object(array, obj):
|
|
key = []
|
|
# todo: make more efficient, especially the try-except
|
|
def _assign_from_object_r(element):
|
|
try:
|
|
for i, e in enumerate(element):
|
|
key.append(i)
|
|
_assign_from_object_r(e)
|
|
key.pop()
|
|
except TypeError:
|
|
array[tuple(key)] = element
|
|
|
|
_assign_from_object_r(obj)
|
|
|
|
|
|
def _increment_mutable_key(key, shape):
|
|
for axis in reversed(xrange(len(shape))):
|
|
key[axis] += 1
|
|
if key[axis] < shape[axis]:
|
|
return True
|
|
if axis == 0:
|
|
return False
|
|
key[axis] = 0
|
|
|
|
|
|
def _key_for_index(index, shape):
|
|
key = []
|
|
cumshape = [1]
|
|
for i in reversed(shape):
|
|
cumshape.insert(0, cumshape[0] * i)
|
|
for s in cumshape[1:-1]:
|
|
n = index // s
|
|
key.append(n)
|
|
index -= n * s
|
|
key.append(index)
|
|
return tuple(key)
|
|
|
|
|
|
def _zerositer(n):
|
|
for i in xrange(n):
|
|
yield 0
|
|
|
|
|
|
|
|
## Public functions
|
|
|
|
|
|
def array(obj, dtype=None, copy=True, order=None):
|
|
""" array(obj, dtype=None, copy=True, order=None)
|
|
|
|
Create a new array. If obj is an ndarray, and copy=False, a view
|
|
of that array is returned. For details see:
|
|
http://docs.scipy.org/doc/numpy/reference/generated/numpy.array.html
|
|
"""
|
|
dtype = _convert_dtype(dtype)
|
|
|
|
if isinstance(obj, ndarray):
|
|
# From existing array
|
|
a = obj.view()
|
|
if dtype is not None and dtype != a.dtype:
|
|
a = a.astype(dtype)
|
|
elif copy:
|
|
a = a.copy()
|
|
return a
|
|
if hasattr(obj, '__array_interface__'):
|
|
# From something that looks like an array, we can create
|
|
# the ctypes array for this and use that as a buffer
|
|
D = obj.__array_interface__
|
|
# Get dtype
|
|
dtype_orig = _convert_dtype(D['typestr'][1:])
|
|
# Create array
|
|
if D['strides']:
|
|
itemsize = int(D['typestr'][-1])
|
|
bufsize = D['strides'][0] * D['shape'][0] // itemsize
|
|
else:
|
|
bufsize = _size_for_shape(D['shape'])
|
|
|
|
BufType = (_convert_dtype(dtype_orig, 'ctypes') * bufsize)
|
|
buffer = BufType.from_address(D['data'][0])
|
|
a = ndarray(D['shape'], dtype_orig,
|
|
buffer=buffer, strides=D['strides'], order=order)
|
|
# Convert or copy?
|
|
if dtype is not None and dtype != dtype_orig:
|
|
a = a.astype(dtype)
|
|
elif copy:
|
|
a = a.copy()
|
|
return a
|
|
else:
|
|
# From some kind of iterable
|
|
shape = _shape_from_object(obj)
|
|
# Try to derive dtype
|
|
if dtype is None:
|
|
el = obj
|
|
while isinstance(el, (tuple, list)) and el:
|
|
el = el[0]
|
|
if isinstance(el, int):
|
|
dtype = 'int64'
|
|
# Create array
|
|
a = ndarray(shape, dtype, order=None)
|
|
_assign_from_object(a, obj)
|
|
return a
|
|
|
|
|
|
def zeros_like(a, dtype=None, order=None):
|
|
""" Return an array of zeros with the same shape and type as a given array.
|
|
"""
|
|
dtype = a.dtype if dtype is None else dtype
|
|
return zeros(a.shape, dtype, order)
|
|
|
|
|
|
def ones_like(a, dtype=None, order=None):
|
|
""" Return an array of ones with the same shape and type as a given array.
|
|
"""
|
|
dtype = a.dtype if dtype is None else dtype
|
|
return ones(a.shape, dtype, order)
|
|
|
|
|
|
def empty_like(a, dtype=None, order=None):
|
|
""" Return a new array with the same shape and type as a given array.
|
|
"""
|
|
dtype = a.dtype if dtype is None else dtype
|
|
return empty(a.shape, dtype, order)
|
|
|
|
|
|
def zeros(shape, dtype=None, order=None):
|
|
"""Return a new array of given shape and type, filled with zeros
|
|
"""
|
|
return empty(shape, dtype, order)
|
|
|
|
|
|
def ones(shape, dtype=None, order=None):
|
|
"""Return a new array of given shape and type, filled with ones
|
|
"""
|
|
a = empty(shape, dtype, order)
|
|
a.fill(1)
|
|
return a
|
|
|
|
|
|
def eye(size):
|
|
"""Return a new 2d array with given dimensions, filled with ones on the
|
|
diagonal and zeros elsewhere.
|
|
"""
|
|
a = zeros((size,size))
|
|
for i in xrange(size):
|
|
a[i,i] = 1
|
|
return a
|
|
|
|
|
|
def empty(shape, dtype=None, order=None):
|
|
"""Return a new array of given shape and type, without initializing entries
|
|
"""
|
|
return ndarray(shape, dtype, order=order)
|
|
|
|
|
|
def arange(*args, **kwargs):
|
|
""" arange([start,] stop[, step,], dtype=None)
|
|
|
|
Return evenly spaced values within a given interval.
|
|
|
|
Values are generated within the half-open interval ``[start, stop)``
|
|
(in other words, the interval including `start` but excluding `stop`).
|
|
For integer arguments the function is equivalent to the Python built-in
|
|
`range <http://docs.python.org/lib/built-in-funcs.html>`_ function,
|
|
but returns an ndarray rather than a list.
|
|
|
|
When using a non-integer step, such as 0.1, the results will often not
|
|
be consistent. It is better to use ``linspace`` for these cases.
|
|
"""
|
|
# Get dtype
|
|
dtype = kwargs.pop('dtype', None)
|
|
if kwargs:
|
|
x = list(kwargs.keys())[0]
|
|
raise TypeError('arange() got an unexpected keyword argument %r' % x)
|
|
# Parse start, stop, step
|
|
if len(args) == 0:
|
|
raise TypeError('Required argument "start" not found')
|
|
elif len(args) == 1:
|
|
start, stop, step = 0, int(args[0]), 1
|
|
elif len(args) == 2:
|
|
start, stop, step = int(args[0]), int(args[1]), 1
|
|
elif len(args) == 3:
|
|
start, stop, step = int(args[0]), int(args[1]), int(args[2])
|
|
else:
|
|
raise TypeError('Too many input arguments')
|
|
# Init
|
|
iter = xrange(start, stop, step)
|
|
a = empty((len(iter),), dtype=dtype)
|
|
a[:] = list(iter)
|
|
return a
|
|
|
|
|
|
def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None):
|
|
""" linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
|
|
|
|
Return evenly spaced numbers over a specified interval. Returns num
|
|
evenly spaced samples, calculated over the interval [start, stop].
|
|
The endpoint of the interval can optionally be excluded.
|
|
"""
|
|
# Prepare
|
|
start, stop = float(start), float(stop)
|
|
ra = stop - start
|
|
if endpoint:
|
|
step = ra / (num-1)
|
|
else:
|
|
step = ra / num
|
|
# Create
|
|
a = empty((num,), dtype)
|
|
a[:] = [start + i * step for i in xrange(num)]
|
|
# Return
|
|
if retstep:
|
|
return a, step
|
|
else:
|
|
return a
|
|
|
|
|
|
## The class
|
|
|
|
class ndarray(object):
|
|
""" ndarray(shape, dtype='float64', buffer=None, offset=0,
|
|
strides=None, order=None)
|
|
|
|
Array class similar to numpy's ndarray, implemented in pure Python.
|
|
This class can be distinguished from a real numpy array in that
|
|
the repr always shows the dtype as a string, and for larger arrays
|
|
(more than 100 elements) it shows a short one-line repr.
|
|
|
|
An array object represents a multidimensional, homogeneous array
|
|
of fixed-size items. An associated data-type property describes the
|
|
format of each element in the array.
|
|
|
|
Arrays should be constructed using `array`, `zeros` or `empty` (refer
|
|
to the See Also section below). The parameters given here refer to
|
|
a low-level method (`ndarray(...)`) for instantiating an array.
|
|
|
|
Parameters
|
|
----------
|
|
shape : tuple of ints
|
|
Shape of created array.
|
|
dtype : data-type, optional
|
|
Any object that can be interpreted as a numpy data type.
|
|
buffer : object contaning data, optional
|
|
Used to fill the array with data. If another ndarray is given,
|
|
the underlying data is used. Can also be a ctypes.Array or any
|
|
object that exposes the buffer interface.
|
|
offset : int, optional
|
|
Offset of array data in buffer.
|
|
strides : tuple of ints, optional
|
|
Strides of data in memory.
|
|
order : {'C', 'F'}, optional NOT SUPPORTED
|
|
Row-major or column-major order.
|
|
|
|
Attributes
|
|
----------
|
|
T : ndarray
|
|
Transpose of the array. In tinynumpy only supported for ndim <= 3.
|
|
data : buffer
|
|
The array's elements, in memory. In tinynumpy this is a ctypes array.
|
|
dtype : str
|
|
Describes the format of the elements in the array. In tinynumpy
|
|
this is a string.
|
|
flags : dict
|
|
Dictionary containing information related to memory use, e.g.,
|
|
'C_CONTIGUOUS', 'OWNDATA', 'WRITEABLE', etc.
|
|
flat : iterator object
|
|
Flattened version of the array as an iterator. In tinynumpy
|
|
the iterator cannot be indexed.
|
|
size : int
|
|
Number of elements in the array.
|
|
itemsize : int
|
|
The memory use of each array element in bytes.
|
|
nbytes : int
|
|
The total number of bytes required to store the array data,
|
|
i.e., ``itemsize * size``.
|
|
ndim : int
|
|
The array's number of dimensions.
|
|
shape : tuple of ints
|
|
Shape of the array.
|
|
strides : tuple of ints
|
|
The step-size required to move from one element to the next in
|
|
memory. For example, a contiguous ``(3, 4)`` array of type
|
|
``int16`` in C-order has strides ``(8, 2)``. This implies that
|
|
to move from element to element in memory requires jumps of 2 bytes.
|
|
To move from row-to-row, one needs to jump 8 bytes at a time
|
|
(``2 * 4``).
|
|
base : ndarray
|
|
If the array is a view into another array, that array is its `base`
|
|
(unless that array is also a view). The `base` array is where the
|
|
array data is actually stored.
|
|
__array_interface__ : dict
|
|
Dictionary with low level array information. Used by numpy to
|
|
turn into a real numpy array. Can also be used to give C libraries
|
|
access to the data via ctypes.
|
|
|
|
See Also
|
|
--------
|
|
array : Construct an array.
|
|
zeros : Create an array, each element of which is zero.
|
|
empty : Create an array, but leave its allocated memory unchanged (i.e.,
|
|
it contains "garbage").
|
|
|
|
Notes
|
|
-----
|
|
There are two modes of creating an array:
|
|
|
|
1. If `buffer` is None, then only `shape`, `dtype`, and `order`
|
|
are used.
|
|
2. If `buffer` is an object exposing the buffer interface, then
|
|
all keywords are interpreted.
|
|
|
|
"""
|
|
|
|
__slots__ = ['_dtype', '_shape', '_strides', '_itemsize',
|
|
'_offset', '_base', '_data']
|
|
|
|
def __init__(self, shape, dtype='float64', buffer=None, offset=0,
|
|
strides=None, order=None):
|
|
# Check order
|
|
if order is not None:
|
|
raise RuntimeError('ndarray order parameter is not supported')
|
|
# Check and set shape
|
|
assert isinstance(shape, tuple)
|
|
assert all([isinstance(x, int) for x in shape])
|
|
self._shape = shape
|
|
# Check and set dtype
|
|
dtype = _convert_dtype(dtype) if (dtype is not None) else 'float64'
|
|
if dtype not in _known_dtypes:
|
|
raise TypeError('data type %r not understood' % dtype)
|
|
self._dtype = dtype
|
|
# Itemsize is directly derived from dtype
|
|
self._itemsize = int(_convert_dtype(dtype, 'short')[-1])
|
|
|
|
if buffer is None:
|
|
# New array
|
|
self._base = None
|
|
# Check and set offset and strides
|
|
assert offset == 0
|
|
self._offset = 0
|
|
assert strides is None
|
|
self._strides = _strides_for_shape(self._shape, self.itemsize)
|
|
|
|
else:
|
|
# Existing array
|
|
if isinstance(buffer, ndarray) and buffer.base is not None:
|
|
buffer = buffer.base
|
|
# Keep a reference to avoid memory cleanup
|
|
self._base = buffer
|
|
# for ndarray we use the data property
|
|
if isinstance(buffer, ndarray):
|
|
buffer = buffer.data
|
|
# Check and set offset
|
|
assert isinstance(offset, int) and offset >= 0
|
|
self._offset = offset
|
|
# Check and set strides
|
|
if strides is None:
|
|
strides = _strides_for_shape(shape, self.itemsize)
|
|
assert isinstance(strides, tuple)
|
|
assert all([isinstance(x, int) for x in strides])
|
|
assert len(strides) == len(shape)
|
|
self._strides = strides
|
|
|
|
# Define our buffer class
|
|
buffersize = self._strides[0] * self._shape[0] // self._itemsize
|
|
buffersize += self._offset
|
|
BufferClass = _convert_dtype(dtype, 'ctypes') * buffersize
|
|
# Create buffer
|
|
if buffer is None:
|
|
self._data = BufferClass()
|
|
elif isinstance(buffer, ctypes.Array):
|
|
self._data = BufferClass.from_address(ctypes.addressof(buffer))
|
|
else:
|
|
self._data = BufferClass.from_buffer(buffer)
|
|
|
|
@property
|
|
def __array_interface__(self):
|
|
""" Allow converting to real numpy array, or pass pointer to C library
|
|
http://docs.scipy.org/doc/numpy/reference/arrays.interface.html
|
|
"""
|
|
readonly = False
|
|
# typestr
|
|
typestr = '<' + _convert_dtype(self.dtype, 'short')
|
|
# Pointer
|
|
if isinstance(self._data, ctypes.Array):
|
|
ptr = ctypes.addressof(self._data)
|
|
elif hasattr(self._data, '__array_interface__'):
|
|
ptr, readonly = self._data.__array_interface__['data']
|
|
elif hasattr(self._data, 'buffer_info'): # Python's array.array
|
|
ptr = self._data.buffer_info()[0]
|
|
elif isinstance(self._data, bytes):
|
|
ptr = ctypes.cast(self._data, ctypes.c_void_p).value
|
|
readonly = True
|
|
else:
|
|
raise TypeError('Cannot get address to underlying array data')
|
|
ptr += self._offset * self.itemsize
|
|
#
|
|
return dict(version=3,
|
|
shape=self.shape,
|
|
typestr=typestr,
|
|
descr=[('', typestr)],
|
|
data=(ptr, readonly),
|
|
strides=self.strides,
|
|
#offset=self._offset,
|
|
#mask=None,
|
|
)
|
|
|
|
def __len__(self):
|
|
return self.shape[0]
|
|
|
|
def __getitem__(self, key):
|
|
offset, shape, strides = self._index_helper(key)
|
|
if not shape:
|
|
# Return scalar
|
|
return self._data[offset]
|
|
else:
|
|
# Return view
|
|
return ndarray(shape, self.dtype,
|
|
offset=offset, strides=strides, buffer=self)
|
|
|
|
def __setitem__(self, key, value):
|
|
|
|
# Get info for view
|
|
offset, shape, strides = self._index_helper(key)
|
|
|
|
# Is this easy?
|
|
if not shape:
|
|
self._data[offset] = value
|
|
return
|
|
|
|
# Create view to set data to
|
|
view = ndarray(shape, self.dtype,
|
|
offset=offset, strides=strides, buffer=self)
|
|
|
|
# Get data to set as a list (because getting slices from ctype
|
|
# arrays yield lists anyway). The list is our "contiguous array"
|
|
if isinstance(value, (float, int)):
|
|
value_list = [value] * view.size
|
|
elif isinstance(value, (tuple, list)):
|
|
value_list = value
|
|
else:
|
|
if not isinstance(value, ndarray):
|
|
value = array(value, copy=False)
|
|
value_list = value._toflatlist()
|
|
|
|
# Check if size match
|
|
if view.size != len(value_list):
|
|
raise ValueError('Number of elements in source does not match ' +
|
|
'number of elements in target.')
|
|
|
|
# Assign data in most efficient way that we can. This code
|
|
# looks for the largest semi-contiguous block: the block that
|
|
# we can access as a 1D array with a stepsize.
|
|
subviews = [view]
|
|
value_index = 0
|
|
count = 0
|
|
while subviews:
|
|
subview = subviews.pop(0)
|
|
step = _get_step(subview)
|
|
if step:
|
|
block = value_list[value_index:value_index+subview.size]
|
|
s = slice(subview._offset,
|
|
subview._offset + subview.size * step,
|
|
step)
|
|
view._data[s] = block
|
|
value_index += subview.size
|
|
count += 1
|
|
else:
|
|
for i in range(subview.shape[0]):
|
|
subviews.append(subview[i])
|
|
assert value_index == len(value_list)
|
|
|
|
def __float__(self):
|
|
if self.size == 1:
|
|
return float(self.data[self._offset])
|
|
else:
|
|
raise TypeError('Only length-1 arrays can be converted to scalar')
|
|
|
|
def __int__(self):
|
|
if self.size == 1:
|
|
return int(self.data[self._offset])
|
|
else:
|
|
raise TypeError('Only length-1 arrays can be converted to scalar')
|
|
|
|
def __repr__(self):
|
|
# If more than 100 elements, show short repr
|
|
if self.size > 100:
|
|
shapestr = 'x'.join([str(i) for i in self.shape])
|
|
return '<ndarray %s %s at 0x%x>' % (shapestr, self.dtype, id(self))
|
|
# Otherwise, try to show in nice way
|
|
def _repr_r(s, axis, offset):
|
|
axisindent = min(2, max(0, (self.ndim - axis - 1)))
|
|
if axis < len(self.shape):
|
|
s += '['
|
|
for k_index, k in enumerate(xrange(self.shape[axis])):
|
|
if k_index > 0:
|
|
s += ('\n ' + ' ' * axis) * axisindent
|
|
offset_ = offset + k * self._strides[axis] // self.itemsize
|
|
s = _repr_r(s, axis+1, offset_)
|
|
if k_index < self.shape[axis] - 1:
|
|
s += ', '
|
|
s += ']'
|
|
else:
|
|
r = repr(self.data[offset])
|
|
if '.' in r:
|
|
r = ' ' + r
|
|
if r.endswith('.0'):
|
|
r = r[:-1]
|
|
s += r
|
|
return s
|
|
|
|
s = _repr_r('', 0, self._offset)
|
|
if self.dtype != 'float64':
|
|
return "array(" + s + ", dtype='%s')" % self.dtype
|
|
else:
|
|
return "array(" + s + ")"
|
|
|
|
def __eq__(self, other):
|
|
if other.__module__.split('.')[0] == 'numpy':
|
|
return other == self
|
|
else:
|
|
out = empty(self.shape, 'bool')
|
|
out[:] = [i1==i2 for (i1, i2) in zip(self.flat, other.flat)]
|
|
return out
|
|
|
|
## Private helper functions
|
|
|
|
def _index_helper(self, key):
|
|
|
|
# Indexing spec is located at:
|
|
# http://docs.scipy.org/doc/numpy/reference/arrays.indexing.html
|
|
|
|
# Promote to tuple.
|
|
if not isinstance(key, tuple):
|
|
key = (key,)
|
|
|
|
axis = 0
|
|
shape = []
|
|
strides = []
|
|
offset = self._offset
|
|
|
|
for k in key:
|
|
axissize = self._shape[axis]
|
|
if isinstance(k, int):
|
|
if k >= axissize:
|
|
raise IndexError('index %i is out of bounds for axis %i ' +
|
|
'with size %s' % (k, axis, axissize))
|
|
offset += k * self._strides[axis] // self.itemsize
|
|
axis += 1
|
|
elif isinstance(k, slice):
|
|
start, stop, step = k.indices(self.shape[axis])
|
|
shape.append(_ceildiv(stop - start, step))
|
|
strides.append(step * self._strides[axis])
|
|
offset += start * self._strides[axis] // self.itemsize
|
|
axis += 1
|
|
elif k is Ellipsis:
|
|
raise TypeError("ellipsis are not supported.")
|
|
elif k is None:
|
|
shape.append(1)
|
|
stride = 1
|
|
for s in self._strides[axis:]:
|
|
stride *= s
|
|
strides.append(stride)
|
|
else:
|
|
raise TypeError("key elements must be instaces of int or slice.")
|
|
|
|
shape.extend(self.shape[axis:])
|
|
strides.extend(self._strides[axis:])
|
|
|
|
return offset, tuple(shape), tuple(strides)
|
|
|
|
def _toflatlist(self):
|
|
value_list = []
|
|
subviews = [self]
|
|
count = 0
|
|
while subviews:
|
|
subview = subviews.pop(0)
|
|
step = _get_step(subview)
|
|
if step:
|
|
s = slice(subview._offset,
|
|
subview._offset + subview.size * step,
|
|
step)
|
|
value_list += self._data[s]
|
|
count += 1
|
|
else:
|
|
for i in range(subview.shape[0]):
|
|
subviews.append(subview[i])
|
|
return value_list
|
|
|
|
## Properties
|
|
|
|
@property
|
|
def ndim(self):
|
|
return len(self._shape)
|
|
|
|
@property
|
|
def size(self):
|
|
return _size_for_shape(self._shape)
|
|
|
|
@property
|
|
def nbytes(self):
|
|
return _size_for_shape(self._shape) * self.itemsize
|
|
|
|
def _get_shape(self):
|
|
return self._shape
|
|
|
|
def _set_shape(self, newshape):
|
|
if newshape == self.shape:
|
|
return
|
|
if self.size != _size_for_shape(newshape):
|
|
raise ValueError('Total size of new array must be unchanged')
|
|
if _get_step(self) == 1:
|
|
# Contiguous, hooray!
|
|
self._shape = tuple(newshape)
|
|
self._strides = _strides_for_shape(self._shape, self.itemsize)
|
|
return
|
|
|
|
# Else, try harder ... This code supports adding /removing
|
|
# singleton dimensions. Although it may sometimes be possible
|
|
# to split a dimension in two if the contiguous blocks allow
|
|
# this, we don't bother with such complex cases for now.
|
|
# Squeeze shape / strides
|
|
N = self.ndim
|
|
shape = [self.shape[i] for i in range(N) if self.shape[i] > 1]
|
|
strides = [self.strides[i] for i in range(N) if self.shape[i] > 1]
|
|
# Check if squeezed shapes match
|
|
newshape_ = [newshape[i] for i in range(len(newshape))
|
|
if newshape[i] > 1]
|
|
if newshape_ != shape:
|
|
raise AttributeError('incompatible shape for non-contiguous array')
|
|
# Modify to make this data work in loop
|
|
strides.append(strides[-1])
|
|
shape.append(1)
|
|
# Form new strides
|
|
i = -1
|
|
newstrides = []
|
|
try:
|
|
for s in reversed(newshape):
|
|
if s == 1:
|
|
newstrides.append(strides[i] * shape[i])
|
|
else:
|
|
i -= 1
|
|
newstrides.append(strides[i])
|
|
except IndexError:
|
|
# Fail
|
|
raise AttributeError('incompatible shape for non-contiguous array')
|
|
else:
|
|
# Success
|
|
newstrides.reverse()
|
|
self._shape = tuple(newshape)
|
|
self._strides = tuple(newstrides)
|
|
|
|
shape = property(_get_shape, _set_shape) # Python 2.5 compat (e.g. Jython)
|
|
|
|
@property
|
|
def strides(self):
|
|
return self._strides
|
|
|
|
@property
|
|
def dtype(self):
|
|
return self._dtype
|
|
|
|
@property
|
|
def itemsize(self):
|
|
return self._itemsize
|
|
|
|
@property
|
|
def base(self):
|
|
return self._base
|
|
|
|
@property
|
|
def data(self):
|
|
return self._data
|
|
|
|
@property
|
|
def flat(self):
|
|
subviews = [self]
|
|
count = 0
|
|
while subviews:
|
|
subview = subviews.pop(0)
|
|
step = _get_step(subview)
|
|
if step:
|
|
s = slice(subview._offset,
|
|
subview._offset + subview.size * step,
|
|
step)
|
|
for i in self._data[s]:
|
|
yield i
|
|
else:
|
|
for i in range(subview.shape[0]):
|
|
subviews.append(subview[i])
|
|
|
|
@property
|
|
def T(self):
|
|
if self.ndim < 2:
|
|
return self
|
|
else:
|
|
return self.transpose()
|
|
|
|
@property
|
|
def flags(self):
|
|
|
|
c_cont = _get_step(self) == 1
|
|
return dict(C_CONTIGUOUS=c_cont,
|
|
F_CONTIGUOUS=(c_cont and self.ndim < 2),
|
|
OWNDATA=(self._base is None),
|
|
WRITEABLE=True, # todo: fix this
|
|
ALIGNED=c_cont, # todo: different from contiguous?
|
|
UPDATEIFCOPY=False, # We don't support this feature
|
|
)
|
|
|
|
## Methods - managemenet
|
|
|
|
def fill(self, value):
|
|
assert isinstance(value, (int, float))
|
|
self[:] = value
|
|
|
|
def clip(self, a_min, a_max, out=None):
|
|
if out is None:
|
|
out = empty(self.shape, self.dtype)
|
|
L = self._toflatlist()
|
|
L = [min(a_max, max(a_min, x)) for x in L]
|
|
out[:] = L
|
|
return out
|
|
|
|
def copy(self):
|
|
out = empty(self.shape, self.dtype)
|
|
out[:] = self
|
|
return out
|
|
|
|
def flatten(self):
|
|
out = empty((self.size,), self.dtype)
|
|
out[:] = self
|
|
return out
|
|
|
|
def ravel(self):
|
|
return self.reshape((self.size, ))
|
|
|
|
def repeat(self, repeats, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
out = empty((self.size * repeats,), self.dtype)
|
|
for i in range(repeats):
|
|
out[i*self.size:(i+1)*self.size] = self
|
|
return out
|
|
|
|
def reshape(self, newshape):
|
|
out = self.view()
|
|
try:
|
|
out.shape = newshape
|
|
except AttributeError:
|
|
out = self.copy()
|
|
out.shape = newshape
|
|
return out
|
|
|
|
def transpose(self):
|
|
# Numpy returns a view, but we cannot do that since we do not
|
|
# support Fortran ordering
|
|
ndim = self.ndim
|
|
if ndim < 2:
|
|
return self.view()
|
|
shape = self.shape[::-1]
|
|
out = empty(shape, self.dtype)
|
|
#
|
|
if ndim == 2:
|
|
for i in xrange(self.shape[0]):
|
|
out[:, i] = self[i, :]
|
|
elif ndim == 3:
|
|
for i in xrange(self.shape[0]):
|
|
for j in xrange(self.shape[1]):
|
|
out[:, j, i] = self[i, j, :]
|
|
else:
|
|
raise ValueError('Tinynumpy supports transpose up to ndim=3')
|
|
return out
|
|
|
|
def astype(self, dtype):
|
|
out = empty(self.shape, dtype)
|
|
out[:] = self
|
|
|
|
def view(self, dtype=None, type=None):
|
|
if dtype is None:
|
|
dtype = self.dtype
|
|
if dtype == self.dtype:
|
|
return ndarray(self.shape, dtype, buffer=self,
|
|
offset=self._offset, strides=self.strides)
|
|
elif self.ndim == 1:
|
|
itemsize = int(_convert_dtype(dtype, 'short')[-1])
|
|
size = self.nbytes // itemsize
|
|
offsetinbytes = self._offset * self.itemsize
|
|
offset = offsetinbytes // itemsize
|
|
return ndarray((size, ), dtype, buffer=self, offset=offset)
|
|
else:
|
|
raise ValueError('new type not compatible with array.')
|
|
|
|
## Methods - statistics
|
|
|
|
# We use the self.flat generator here. self._toflatlist() would be
|
|
# faster, but it might take up significantly more memory.
|
|
|
|
def all(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return all(self.flat)
|
|
|
|
def any(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return any(self.flat)
|
|
|
|
def min(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return min(self.flat)
|
|
|
|
def max(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return max(self.flat)
|
|
#return max(self._toflatlist()) # almost twice as fast
|
|
|
|
def sum(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return sum(self.flat)
|
|
|
|
def prod(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
p = 1.0
|
|
for i in self.flat:
|
|
p *= float(i)
|
|
return p
|
|
|
|
def ptp(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
mn = self.data[self._offset]
|
|
mx = mn
|
|
for i in self.flat:
|
|
if i > mx:
|
|
mx = i
|
|
if i < mn:
|
|
mn = i
|
|
return mx - mn
|
|
|
|
def mean(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
return self.sum() / self.size
|
|
|
|
def argmax(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
r = self.data[self._offset]
|
|
r_index = 0
|
|
for i_index, i in enumerate(self.flat):
|
|
if i > r:
|
|
r = i
|
|
r_index = i_index
|
|
return r_index
|
|
|
|
def argmin(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
r = self.data[self._offset]
|
|
r_index = 0
|
|
for i_index, i in enumerate(self.flat):
|
|
if i < r:
|
|
r = i
|
|
r_index = i_index
|
|
return r_index
|
|
|
|
def cumprod(self, axis=None, out=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
if out is None:
|
|
out = empty((self.size,), self.dtype)
|
|
p = 1
|
|
L = []
|
|
for x in self.flat:
|
|
p *= x
|
|
L.append(p)
|
|
out[:] = L
|
|
return out
|
|
|
|
def cumsum(self, axis=None, out=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
if out is None:
|
|
out = empty((self.size,), self.dtype)
|
|
p = 0
|
|
L = []
|
|
for x in self.flat:
|
|
p += x
|
|
L.append(p)
|
|
out[:] = L
|
|
return out
|
|
|
|
def var(self, axis=None):
|
|
if axis:
|
|
raise (TypeError, "axis argument is not supported")
|
|
m = self.mean()
|
|
acc = 0
|
|
for x in self.flat:
|
|
acc += abs(x - m) ** 2
|
|
return acc / self.size
|
|
|
|
def std(self, axis=None):
|
|
return sqrt(self.var(axis))
|
|
|
|
class nditer:
|
|
def __init__(self, array):
|
|
self.array = array
|
|
self.key = [0] * len(self.array.shape)
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __len__(self):
|
|
return _size_for_shape(self.array.shape)
|
|
|
|
def __getitem__(self, index):
|
|
key = _key_for_index(index, self.array.shape)
|
|
return self.array[key]
|
|
|
|
def __next__(self):
|
|
if self.key is None:
|
|
raise StopIteration
|
|
value = self.array[tuple(self.key)]
|
|
if not _increment_mutable_key(self.key, self.array.shape):
|
|
self.key = None
|
|
return value
|
|
|
|
def next(self):
|
|
return self.__next__()
|