• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NeuralEnsemble / lazyarray / 10723608798

05 Sep 2024 03:37PM UTC coverage: 92.611% (+0.2%) from 92.424%
10723608798

push

github

web-flow
Merge pull request #23 from apdavison/coveralls

Reenable Coveralls

376 of 406 relevant lines covered (92.61%)

4.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.61
/lazyarray.py
1
# encoding: utf-8
2
"""
3
lazyarray is a Python package that provides a lazily-evaluated numerical array
4
class, ``larray``, based on and compatible with NumPy arrays.
5

6
Copyright Andrew P. Davison, Joël Chavas, Elodie Legouée (CNRS) and Ankur Sinha, 2012-2022
7
"""
8

9
import numbers
5✔
10
import operator
5✔
11
from copy import deepcopy
5✔
12
from functools import wraps, reduce
5✔
13
import logging
5✔
14

15
import numpy as np
5✔
16
try:
5✔
17
    from scipy import sparse
5✔
18
    have_scipy = True
5✔
19
except ImportError:
×
20
    have_scipy = False
×
21

22
try:
5✔
23
    from collections.abc import Sized
5✔
24
    from collections.abc import Mapping
5✔
25
    from collections.abc import Iterator
5✔
26
except ImportError:
×
27
    from collections import Sized
×
28
    from collections import Mapping
×
29
    from collections import Iterator
×
30

31

32
__version__ = "0.5.2"
5✔
33

34
logger = logging.getLogger("lazyarray")
5✔
35

36

37
def check_shape(meth):
5✔
38
    """
39
    Decorator for larray magic methods, to ensure that the operand has
40
    the same shape as the array.
41
    """
42
    @wraps(meth)
5✔
43
    def wrapped_meth(self, val):
5✔
44
        if isinstance(val, (larray, np.ndarray)) and val.shape:
5✔
45
            if val.shape != self._shape:
5✔
46
                raise ValueError("shape mismatch: objects cannot be broadcast to a single shape")
5✔
47
        return meth(self, val)
5✔
48
    return wrapped_meth
5✔
49

50

51
def requires_shape(meth):
5✔
52
    @wraps(meth)
5✔
53
    def wrapped_meth(self, *args, **kwargs):
5✔
54
        if self._shape is None:
5✔
55
            raise ValueError("Shape of larray not specified")
5✔
56
        return meth(self, *args, **kwargs)
5✔
57
    return wrapped_meth
5✔
58

59

60
def full_address(addr, full_shape):
5✔
61
    if not (isinstance(addr, np.ndarray) and addr.dtype == bool and addr.ndim == len(full_shape)):
5✔
62
        if not isinstance(addr, tuple):
5✔
63
            addr = (addr,)
5✔
64
        if len(addr) < len(full_shape):
5✔
65
            full_addr = [slice(None)] * len(full_shape)
5✔
66
            for i, val in enumerate(addr):
5✔
67
                full_addr[i] = val
5✔
68
            addr = full_addr
5✔
69
    return addr
5✔
70

71

72
def partial_shape(addr, full_shape):
5✔
73
    """
74
    Calculate the size of the sub-array represented by `addr`
75
    """
76
    def size(x, max):
5✔
77
        if isinstance(x, (int, np.integer)):
5✔
78
            return None
5✔
79
        elif isinstance(x, slice):
5✔
80
            y = min(max, x.stop or max)  # slice limits can go past the bounds
5✔
81
            return 1 + (y - (x.start or 0) - 1) // (x.step or 1)
5✔
82
        elif isinstance(x, Sized):
5✔
83
            if hasattr(x, 'dtype') and x.dtype == bool:
5✔
84
                return x.sum()
5✔
85
            else:
86
                return len(x)
5✔
87
        else:
88
            raise TypeError("Unsupported index type %s" % type(x))
×
89

90
    addr = full_address(addr, full_shape)
5✔
91
    if isinstance(addr, np.ndarray) and addr.dtype == bool:
5✔
92
        return (addr.sum(),)
5✔
93
    elif all(isinstance(x, Sized) for x in addr):
5✔
94
        return (len(addr[0]),)
5✔
95
    else:
96
        shape = [size(x, max) for (x, max) in zip(addr, full_shape)]
5✔
97
        return tuple([x for x in shape if x is not None])  # remove empty dimensions
5✔
98

99

100
def reverse(func):
5✔
101
    """Given a function f(a, b), returns f(b, a)"""
102
    @wraps(func)
5✔
103
    def reversed_func(a, b):
5✔
104
        return func(b, a)
5✔
105
    reversed_func.__doc__ = "Reversed argument form of %s" % func.__doc__
5✔
106
    reversed_func.__name__ = "reversed %s" % func.__name__
5✔
107
    return reversed_func
5✔
108
# "The hash of a function object is hash(func_code) ^ id(func_globals)" ?
109
# see http://mail.python.org/pipermail/python-dev/2000-April/003397.html
110

111

112
def lazy_operation(name, reversed=False):
5✔
113
    def op(self, val):
5✔
114
        new_map = deepcopy(self)
5✔
115
        f = getattr(operator, name)
5✔
116
        if reversed:
5✔
117
            f = reverse(f)
5✔
118
        new_map.operations.append((f, val))
5✔
119
        return new_map
5✔
120
    return check_shape(op)
5✔
121

122

123
def lazy_inplace_operation(name):
5✔
124
    def op(self, val):
5✔
125
        self.operations.append((getattr(operator, name), val))
5✔
126
        return self
5✔
127
    return check_shape(op)
5✔
128

129

130
def lazy_unary_operation(name):
5✔
131
    def op(self):
5✔
132
        new_map = deepcopy(self)
5✔
133
        new_map.operations.append((getattr(operator, name), None))
5✔
134
        return new_map
5✔
135
    return op
5✔
136

137

138
def is_array_like(value):
5✔
139
    # False for numbers, generators, functions, iterators
140
    if not isinstance(value, Sized):
5✔
141
        return False
5✔
142
    if have_scipy and sparse.issparse(value):
5✔
143
        return True
5✔
144
    if isinstance(value, Mapping):
5✔
145
        # because we may wish to have lazy arrays in which each
146
        # item is a dict, for example
147
        return False
×
148
    if getattr(value, "is_lazyarray_scalar", False):
5✔
149
        # for user-defined classes that are "Sized" but that should
150
        # be treated as individual elements in a lazy array
151
        # the attribute "is_lazyarray_scalar" can be defined with value
152
        # True.
153
        return False
×
154
    return True
5✔
155

156

157

158
class larray(object):
5✔
159
    """
160
    Optimises storage of and operations on arrays in various ways:
161
      - stores only a single value if all the values in the array are the same;
162
      - if the array is created from a function `f(i)` or `f(i,j)`, then
163
        elements are only evaluated when they are accessed. Any operations
164
        performed on the array are also queued up to be executed on access.
165

166
    Two use cases for the latter are:
167
      - to save memory for very large arrays by accessing them one row or
168
        column at a time: the entire array need never be in memory.
169
      - in parallelized code, different rows or columns may be evaluated
170
        on different nodes or in different threads.
171
    """
172

173

174
    def __init__(self, value, shape=None, dtype=None):
5✔
175
        """
176
        Create a new lazy array.
177

178
        `value` : may be an int, float, bool, NumPy array, iterator,
179
                  generator or a function, `f(i)` or `f(i,j)`, depending on the
180
                  dimensions of the array.
181

182
        `f(i,j)` should return a single number when `i` and `j` are integers,
183
        and a 1D array when either `i` or `j` or both is a NumPy array (in the
184
        latter case the two arrays must have equal lengths).
185
        """
186

187
        self.dtype = dtype
5✔
188
        self.operations = []
5✔
189
        if isinstance(value, str):
5✔
190
            raise TypeError("An larray cannot be created from a string")
5✔
191
        elif isinstance(value, larray):
5✔
192
            if shape is not None and value.shape is not None:
5✔
193
                assert shape == value.shape
5✔
194
            self._shape = shape or value.shape
5✔
195
            self.base_value = value.base_value
5✔
196
            self.dtype = dtype or value.dtype
5✔
197
            self.operations = value.operations  # should deepcopy?
5✔
198

199
        elif is_array_like(value):  # False for numbers, generators, functions, iterators
5✔
200
            if have_scipy and sparse.issparse(value):  # For sparse matrices
5✔
201
                self.dtype = dtype or value.dtype
5✔
202
            elif not isinstance(value, np.ndarray):
5✔
203
                value = np.array(value, dtype=dtype)
5✔
204
            elif dtype is not None:
5✔
205
               assert np.can_cast(value.dtype, dtype, casting='safe')  # or could convert value to the provided dtype
5✔
206
            if shape and value.shape and value.shape != shape:
5✔
207
                raise ValueError("Array has shape %s, value has shape %s" % (shape, value.shape))
5✔
208
            if value.shape:
5✔
209
                self._shape = value.shape
5✔
210
            else:
211
                self._shape = shape
×
212
            self.base_value = value
5✔
213

214
        else:
215
            assert np.isreal(value)  # also True for callables, generators, iterators
5✔
216
            self._shape = shape
5✔
217
            if dtype is None or isinstance(value, dtype):
5✔
218
                self.base_value = value
5✔
219
            else:
220
                try:
5✔
221
                    self.base_value = dtype(value)
5✔
222
                except TypeError:
5✔
223
                    self.base_value = value
5✔
224

225
    def __eq__(self, other):
5✔
226
        if isinstance(other, self.__class__):
5✔
227
            return self.base_value == other.base_value and self.operations == other.operations and self._shape == other.shape
5✔
228
        elif isinstance(other, numbers.Number):
5✔
229
            if len(self.operations) == 0:
5✔
230
                if isinstance(self.base_value, numbers.Number):
5✔
231
                    return self.base_value == other
5✔
232
                elif isinstance(self.base_value, np.ndarray):
5✔
233
                    return (self.base_value == other).all()
5✔
234
            # todo: we could perform the evaluation ourselves, but that could have a performance hit
235
            raise Exception("You will need to evaluate this lazyarray before checking for equality")
5✔
236
        else:
237
            # todo: add support for NumPy arrays
238
            raise TypeError("Cannot at present compare equality of lazyarray and {}".format(type(other)))
5✔
239

240
    def __deepcopy__(self, memo):
5✔
241
        obj = type(self).__new__(type(self))
5✔
242
        if isinstance(self.base_value, VectorizedIterable):  # special case, but perhaps need to rethink
5✔
243
            obj.base_value = self.base_value                 # whether deepcopy is appropriate everywhere
5✔
244
        else:
245
            try:
5✔
246
                obj.base_value = deepcopy(self.base_value)
5✔
247
            except TypeError:  # base_value cannot be copied, e.g. is a generator (but see generator_tools from PyPI)
5✔
248
                obj.base_value = self.base_value  # so here we create a reference rather than deepcopying - could cause problems
5✔
249
        obj._shape = self._shape
5✔
250
        obj.dtype = self.dtype
5✔
251
        obj.operations = []
5✔
252
        for f, arg in self.operations:
5✔
253
            if isinstance(f, np.ufunc):
5✔
254
                obj.operations.append((f, deepcopy(arg)))
5✔
255
            else:
256
                obj.operations.append((deepcopy(f), deepcopy(arg)))
5✔
257
        return obj
5✔
258

259
    def __repr__(self):
5✔
260
        return "<larray: base_value=%r shape=%r dtype=%r, operations=%r>" % (self.base_value,
×
261
                                                                             self.shape,
262
                                                                             self.dtype,
263
                                                                             self.operations)
264

265
    def _set_shape(self, value):
5✔
266
        if (hasattr(self.base_value, "shape") and
5✔
267
                self.base_value.shape and   # values of type np.float have an empty shape
268
                    self.base_value.shape != value):
269
            raise ValueError("Lazy array has fixed shape %s, cannot be changed to %s" % (self.base_value.shape, value))
×
270
        self._shape = value
5✔
271
        for op in self.operations:
5✔
272
            if isinstance(op[1], larray):
5✔
273
                op[1].shape = value
5✔
274
    shape = property(fget=lambda self: self._shape,
5✔
275
                     fset=_set_shape, doc="Shape of the array")
276

277
    @property
5✔
278
    @requires_shape
5✔
279
    def nrows(self):
5✔
280
        """Size of the first dimension of the array."""
281
        return self._shape[0]
5✔
282

283
    @property
5✔
284
    @requires_shape
5✔
285
    def ncols(self):
5✔
286
        """Size of the second dimension (if it exists) of the array."""
287
        if len(self.shape) > 1:
5✔
288
            return self._shape[1]
5✔
289
        else:
290
            return 1
5✔
291

292
    @property
5✔
293
    @requires_shape
5✔
294
    def size(self):
5✔
295
        """Total number of elements in the array."""
296
        return reduce(operator.mul, self._shape)
5✔
297

298
    @property
5✔
299
    def is_homogeneous(self):
5✔
300
        """True if all the elements of the array are the same."""
301
        hom_base = isinstance(self.base_value, (int, np.integer, float, bool)) \
5✔
302
                   or type(self.base_value) == self.dtype \
303
                   or (isinstance(self.dtype, type) and isinstance(self.base_value, self.dtype))
304
        hom_ops = all(obj.is_homogeneous for f, obj in self.operations if isinstance(obj, larray))
5✔
305
        return hom_base and hom_ops
5✔
306

307
    def _partial_shape(self, addr):
5✔
308
        """
309
        Calculate the size of the sub-array represented by `addr`
310
        """
311
        return partial_shape(addr, self._shape)
5✔
312

313
    def _homogeneous_array(self, addr):
5✔
314
        self.check_bounds(addr)
5✔
315
        shape = self._partial_shape(addr)
5✔
316
        return np.ones(shape, type(self.base_value))
5✔
317

318
    def _full_address(self, addr):
5✔
319
        return full_address(addr, self._shape)
5✔
320

321
    def _array_indices(self, addr):
5✔
322
        self.check_bounds(addr)
5✔
323

324
        def axis_indices(x, max):
5✔
325
            if isinstance(x, (int, np.integer)):
5✔
326
                return x
5✔
327
            elif isinstance(x, slice):  # need to handle negative values in slice
5✔
328
                return np.arange((x.start or 0),
5✔
329
                                    (x.stop or max),
330
                                    (x.step or 1),
331
                                    dtype=int)
332
            elif isinstance(x, Sized):
5✔
333
                if hasattr(x, 'dtype') and x.dtype == bool:
5✔
334
                    return np.arange(max)[x]
5✔
335
                else:
336
                    return np.array(x)
5✔
337
            else:
338
                raise TypeError("Unsupported index type %s" % type(x))
×
339
        addr = self._full_address(addr)
5✔
340
        if isinstance(addr, np.ndarray) and addr.dtype == bool:
5✔
341
            if addr.ndim == 1:
5✔
342
                return (np.arange(self._shape[0])[addr],)
5✔
343
            else:
344
                raise NotImplementedError()
×
345
        elif all(isinstance(x, Sized) for x in addr):
5✔
346
            indices = [np.array(x) for x in addr]
5✔
347
            return indices
5✔
348
        else:
349
            indices = [axis_indices(x, max) for (x, max) in zip(addr, self._shape)]
5✔
350
            if len(indices) == 1:
5✔
351
                return indices
5✔
352
            elif len(indices) == 2:
5✔
353
                if isinstance(indices[0], Sized):
5✔
354
                    if isinstance(indices[1], Sized):
5✔
355
                        mesh_xy = np.meshgrid(*indices)
5✔
356
                        return (mesh_xy[0].T, mesh_xy[1].T)  # meshgrid works on (x,y), not (i,j)
5✔
357
                return indices
5✔
358
            else:
359
                raise NotImplementedError("Only 1D and 2D arrays supported")
5✔
360

361
    @requires_shape
5✔
362
    def __getitem__(self, addr):
5✔
363
        """
364
        Return one or more items from the array, as for NumPy arrays.
365

366
        `addr` may be a single integer, a slice, a NumPy boolean array or a
367
        NumPy integer array.
368
        """
369
        return self._partially_evaluate(addr, simplify=False)
5✔
370

371
    def _partially_evaluate(self, addr, simplify=False):
5✔
372
        """
373
        Return part of the lazy array.
374
        """
375
        if self.is_homogeneous:
5✔
376
            if simplify:
5✔
377
                base_val = self.base_value
×
378
            else:
379
                base_val = self._homogeneous_array(addr) * self.base_value
5✔
380
        elif isinstance(self.base_value, (int, np.integer, float, bool)):
5✔
381
            base_val = self._homogeneous_array(addr) * self.base_value
×
382
        elif isinstance(self.base_value, np.ndarray):
5✔
383
            base_val = self.base_value[addr]
5✔
384
        elif have_scipy and sparse.issparse(self.base_value):  # For sparse matrices larr[2, :]
5✔
385
            base_val = self.base_value[addr]
×
386
        elif callable(self.base_value):
5✔
387
            indices = self._array_indices(addr)
5✔
388
            base_val = self.base_value(*indices)
5✔
389
            if isinstance(base_val, np.ndarray) and base_val.shape == (1,):
5✔
390
                base_val = base_val[0]
×
391
        elif hasattr(self.base_value, "lazily_evaluate"):
5✔
392
            base_val = self.base_value.lazily_evaluate(addr, shape=self._shape)
×
393
        elif isinstance(self.base_value, VectorizedIterable):
5✔
394
            partial_shape = self._partial_shape(addr)
5✔
395
            if partial_shape:
5✔
396
                n = reduce(operator.mul, partial_shape)
5✔
397
            else:
398
                n = 1
5✔
399
            base_val = self.base_value.next(n)  # note that the array contents will depend on the order of access to elements
5✔
400
            if n == 1:
5✔
401
                base_val = base_val[0]
5✔
402
            elif partial_shape and base_val.shape != partial_shape:
5✔
403
                base_val = base_val.reshape(partial_shape)
5✔
404
        elif isinstance(self.base_value, Iterator):
5✔
405
            raise NotImplementedError("coming soon...")
5✔
406
        else:
407
            raise ValueError("invalid base value for array (%s)" % self.base_value)
5✔
408
        return self._apply_operations(base_val, addr, simplify=simplify)
5✔
409

410
    @requires_shape
5✔
411
    def check_bounds(self, addr):
5✔
412
        """
413
        Check whether the given address is within the array bounds.
414
        """
415
        def is_boolean_array(arr):
5✔
416
            return hasattr(arr, 'dtype') and arr.dtype == bool
5✔
417

418
        def check_axis(x, size):
5✔
419
            if isinstance(x, (int, np.integer)):
5✔
420
                lower = upper = x
5✔
421
            elif isinstance(x, slice):
5✔
422
                lower = x.start or 0
5✔
423
                upper = min(x.stop or size - 1, size - 1)  # slices are allowed to go past the bounds
5✔
424
            elif isinstance(x, Sized):
5✔
425
                if is_boolean_array(x):
5✔
426
                    lower = 0
5✔
427
                    upper = x.size - 1
5✔
428
                else:
429
                    if len(x) == 0:
5✔
430
                        raise ValueError("Empty address component (address was %s)" % str(addr))
5✔
431
                    if hasattr(x, "min"):
5✔
432
                        lower = x.min()
5✔
433
                    else:
434
                        lower = min(x)
5✔
435
                    if hasattr(x, "max"):
5✔
436
                        upper = x.max()
5✔
437
                    else:
438
                        upper = max(x)
5✔
439
            else:
440
                raise TypeError("Invalid array address: %s (element of type %s)" % (str(addr), type(x)))
5✔
441
            if (lower < -size) or (upper >= size):
5✔
442
                raise IndexError("Index out of bounds")
5✔
443
        full_addr = self._full_address(addr)
5✔
444
        if isinstance(addr, np.ndarray) and addr.dtype == bool:
5✔
445
            if len(addr.shape) > len(self._shape):
5✔
446
                raise IndexError("Too many indices for array")
×
447
            for xmax, size in zip(addr.shape, self._shape):
5✔
448
                upper = xmax - 1
5✔
449
                if upper >= size:
5✔
450
                    raise IndexError("Index out of bounds")
×
451
        else:
452
            for i, size in zip(full_addr, self._shape):
5✔
453
                check_axis(i, size)
5✔
454

455
    def apply(self, f):
5✔
456
        """
457
        Add the function `f(x)` to the list of the operations to be performed,
458
        where `x` will be a scalar or a numpy array.
459

460
        >>> m = larray(4, shape=(2,2))
461
        >>> m.apply(np.sqrt)
462
        >>> m.evaluate()
463
        array([[ 2.,  2.],
464
               [ 2.,  2.]])
465
        """
466
        self.operations.append((f, None))
5✔
467

468
    def _apply_operations(self, x, addr=None, simplify=False):
5✔
469
        for f, arg in self.operations:
5✔
470
            if arg is None:
5✔
471
                x = f(x)
5✔
472
            elif isinstance(arg, larray):
5✔
473
                if addr is None:
5✔
474
                    x = f(x, arg.evaluate(simplify=simplify))
5✔
475
                else:
476
                    x = f(x, arg._partially_evaluate(addr, simplify=simplify))
5✔
477

478
            else:
479
                x = f(x, arg)
5✔
480
        return x
5✔
481

482
    @requires_shape
5✔
483
    def evaluate(self, simplify=False, empty_val=0):
5✔
484
        """
485
        Return the lazy array as a real NumPy array.
486

487
        If the array is homogeneous and ``simplify`` is ``True``, return a
488
        single numerical value.
489
        """
490
        # need to catch the situation where a generator-based larray is evaluated a second time
491
        if self.is_homogeneous:
5✔
492
            if simplify:
5✔
493
                x = self.base_value
5✔
494
            else:
495
                x = self.base_value * np.ones(self._shape, dtype=self.dtype)
5✔
496
        elif isinstance(self.base_value, (int, np.integer, float, bool, np.bool_)):
5✔
497
            x = self.base_value * np.ones(self._shape, dtype=self.dtype)
5✔
498
        elif isinstance(self.base_value, np.ndarray):
5✔
499
            if self.base_value.shape == (1,):
5✔
500
                x = self.base_value[0]
5✔
501
            else:
502
                x = self.base_value
5✔
503
        elif callable(self.base_value):
5✔
504
            x = np.array(np.fromfunction(self.base_value, shape=self._shape, dtype=int), dtype=self.dtype)
5✔
505
        elif hasattr(self.base_value, "lazily_evaluate"):
5✔
506
            x = self.base_value.lazily_evaluate(shape=self._shape)
×
507
        elif isinstance(self.base_value, VectorizedIterable):
5✔
508
            x = self.base_value.next(self.size)
5✔
509
            if x.shape != self._shape:
5✔
510
                x = x.reshape(self._shape)
5✔
511
        elif have_scipy and sparse.issparse(self.base_value):  # For sparse matrices
5✔
512
            if empty_val!=0:
×
513
                x = self.base_value.toarray((sparse.csc_matrix))
×
514
                x = np.where(x, x, np.nan)
×
515
            else:
516
                x = self.base_value.toarray((sparse.csc_matrix))
×
517
        elif isinstance(self.base_value, Iterator):
5✔
518
            x = np.fromiter(self.base_value, dtype=self.dtype or float, count=self.size)
5✔
519
            if x.shape != self._shape:
5✔
520
                x = x.reshape(self._shape)
5✔
521
        else:
522
            raise ValueError("invalid base value for array")
5✔
523
        return self._apply_operations(x, simplify=simplify)
5✔
524

525
    def __call__(self, arg):
5✔
526
        if callable(self.base_value):
5✔
527
            if isinstance(arg, larray):
5✔
528
                new_map = deepcopy(arg)
5✔
529
            elif callable(arg):
×
530
                new_map = larray(arg)
×
531
            else:
532
                raise Exception("Argument must be either callable or an larray.")
×
533
            new_map.operations.append((self.base_value, None))
5✔
534
            new_map.operations.extend(self.operations)
5✔
535
            return new_map
5✔
536
        else:
537
            raise Exception("larray is not callable")
×
538

539
    __iadd__ = lazy_inplace_operation('add')
5✔
540
    __isub__ = lazy_inplace_operation('sub')
5✔
541
    __imul__ = lazy_inplace_operation('mul')
5✔
542
    __idiv__ = lazy_inplace_operation('div')
5✔
543
    __ipow__ = lazy_inplace_operation('pow')
5✔
544

545
    __add__ = lazy_operation('add')
5✔
546
    __radd__ = __add__
5✔
547
    __sub__ = lazy_operation('sub')
5✔
548
    __rsub__ = lazy_operation('sub', reversed=True)
5✔
549
    __mul__ = lazy_operation('mul')
5✔
550
    __rmul__ = __mul__
5✔
551
    __div__ = lazy_operation('div')
5✔
552
    __rdiv__ = lazy_operation('div', reversed=True)
5✔
553
    __truediv__ = lazy_operation('truediv')
5✔
554
    __rtruediv__ = lazy_operation('truediv', reversed=True)
5✔
555
    __pow__ = lazy_operation('pow')
5✔
556

557
    __lt__ = lazy_operation('lt')
5✔
558
    __gt__ = lazy_operation('gt')
5✔
559
    __le__ = lazy_operation('le')
5✔
560
    __ge__ = lazy_operation('ge')
5✔
561

562
    __neg__ = lazy_unary_operation('neg')
5✔
563
    __pos__ = lazy_unary_operation('pos')
5✔
564
    __abs__ = lazy_unary_operation('abs')
5✔
565

566

567
class VectorizedIterable(object):
5✔
568
    """
569
    Base class for any class which has a method `next(n)`, i.e., where you
570
    can choose how many values to return rather than just returning one at a
571
    time.
572
    """
573
    pass
5✔
574

575

576
def _build_ufunc(func):
5✔
577
    """Return a ufunc that works with lazy arrays"""
578
    def larray_compatible_ufunc(x):
5✔
579
        if isinstance(x, larray):
5✔
580
            y = deepcopy(x)
5✔
581
            y.apply(func)
5✔
582
            return y
5✔
583
        else:
584
            return func(x)
5✔
585
    return larray_compatible_ufunc
5✔
586

587

588
def _build_ufunc_2nd_arg(func):
5✔
589
    """Return a ufunc taking a second, non-array argument, that works with lazy arrays"""
590
    def larray_compatible_ufunc2(x1, x2):
5✔
591
        if not isinstance(x2, numbers.Number):
5✔
592
            raise TypeError("lazyarry ufuncs do not accept an array as the second argument")
5✔
593
        if isinstance(x1, larray):
5✔
594
            def partial(x):
5✔
595
                return func(x, x2)
5✔
596
            y = deepcopy(x1)
5✔
597
            y.apply(partial)
5✔
598
            return y
5✔
599
        else:
600
            return func(x1, x2)
5✔
601
    return larray_compatible_ufunc2
5✔
602

603

604
# build lazy-array compatible versions of NumPy ufuncs
605
namespace = globals()
5✔
606
for name in dir(np):
5✔
607
    obj = getattr(np, name)
5✔
608
    if isinstance(obj, np.ufunc) and name not in namespace:
5✔
609
        if name in ("power", "fmod", "arctan2, hypot, ldexp, maximum, minimum"):
5✔
610
            namespace[name] = _build_ufunc_2nd_arg(obj)
5✔
611
        else:
612
            namespace[name] = _build_ufunc(obj)
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc