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

NeuralEnsemble / PyNN / 968

pending completion
968

cron

travis-ci-com

GitHub
Merge pull request #761 from apdavison/pytest

Migrate test suite from nose to pytest

10 of 10 new or added lines in 5 files covered. (100.0%)

6955 of 9974 relevant lines covered (69.73%)

0.7 hits per line

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

86.6
/pyNN/parameters.py
1
"""
2
Parameter set handling
3

4
:copyright: Copyright 2006-2022 by the PyNN team, see AUTHORS.
5
:license: CeCILL, see LICENSE for details.
6
"""
7

8
import numpy as np
1✔
9
from collections.abc import Sized
1✔
10
from pyNN.core import is_listlike
1✔
11
from pyNN import errors
1✔
12
from pyNN.random import RandomDistribution, NativeRNG
1✔
13
from lazyarray import larray, partial_shape
1✔
14
import numpy as np
1✔
15

16

17
class LazyArray(larray):
1✔
18
    """
19
    Optimises storage of arrays in various ways:
20
      - stores only a single value if all the values in the array are the same
21
      - if the array is created from a :class:`~pyNN.random.RandomDistribution`
22
        or a function `f(i,j)`, then elements are only evaluated when they are
23
        accessed. Any operations performed on the array are also queued up to
24
        be executed on access.
25

26
    The main intention of the latter is to save memory for very large arrays by
27
    accessing them one row or column at a time: the entire array need never be
28
    in memory.
29

30
    Arguments:
31
        `value`:
32
            may be an int, float, bool, NumPy array, iterator, generator
33
            or a function, `f(i)` or `f(i,j)`, depending on the dimensions of
34
            the array. `f(i,j)` should return a single number when `i` and `j`
35
            are integers, and a 1D array when either `i` or `j` or both is a
36
            NumPy array (in the latter case the two arrays must have equal
37
            lengths).
38
        `shape`:
39
            a tuple giving the shape of the array, or `None`
40
        `dtype`:
41
            the NumPy `dtype`.
42
    """
43
    # most of the implementation moved to external lazyarray package
44
    # the plan is ultimately to move everything to lazyarray
45

46
    def __init__(self, value, shape=None, dtype=None):
1✔
47
        if isinstance(value, str):
1✔
48
            errmsg = "Value should be a string expressing a function of d. "
1✔
49
            try:
1✔
50
                value = eval("lambda d: %s" % value)
1✔
51
            except SyntaxError:
×
52
                raise errors.InvalidParameterValueError(errmsg + "Incorrect syntax.")
×
53
            try:
1✔
54
                value(0.0)
1✔
55
            except NameError as err:
1✔
56
                raise errors.InvalidParameterValueError(errmsg + str(err))
1✔
57
        super(LazyArray, self).__init__(value, shape, dtype)
1✔
58

59
    def __setitem__(self, addr, new_value):
1✔
60
        self.check_bounds(addr)
1✔
61
        if (self.is_homogeneous
1✔
62
            and isinstance(new_value, (int, float, bool))
63
            and self.evaluate(simplify=True) == new_value):
64
            pass
1✔
65
        else:
66
            self.base_value = self.evaluate()
1✔
67
            self.base_value[addr] = new_value
1✔
68
            self.operations = []
1✔
69

70
    def by_column(self, mask=None):
1✔
71
        """
72
        Iterate over the columns of the array. Columns will be yielded either
73
        as a 1D array or as a single value (for a flat array).
74

75
        `mask`: either `None` or a boolean array indicating which columns should be included.
76
        """
77
        column_indices = np.arange(self.ncols)
1✔
78
        if mask is not None:
1✔
79
            if not isinstance(mask, slice):
1✔
80
                assert len(mask) == self.ncols
1✔
81
            column_indices = column_indices[mask]
1✔
82
        if isinstance(self.base_value, RandomDistribution) and self.base_value.rng.parallel_safe:
1✔
83
            if mask is None:
1✔
84
                for j in column_indices:
1✔
85
                    yield self._partially_evaluate((slice(None), j), simplify=True)
1✔
86
            else:
87
                column_indices = np.arange(self.ncols)
1✔
88
                for j, local in zip(column_indices, mask):
1✔
89
                    col = self._partially_evaluate((slice(None), j), simplify=True)
1✔
90
                    if local:
1✔
91
                        yield col
1✔
92
        else:
93
            for j in column_indices:
1✔
94
                yield self._partially_evaluate((slice(None), j), simplify=True)
1✔
95

96

97
class ArrayParameter(object):
1✔
98
    """
99
    Represents a parameter whose value consists of multiple values, e.g. a tuple or array.
100

101
    The reason for defining this class rather than just using a NumPy array is
102
    to avoid the ambiguity of "is a given array a single parameter value (e.g.
103
    a spike train for one cell) or an array of parameter values (e.g. one number
104
    per cell)?".
105

106
    Arguments:
107
        `value`:
108
            anything which can be converted to a NumPy array, or another
109
            :class:`ArrayParameter` object.
110
    """
111

112
    def __init__(self, value):
1✔
113
        if isinstance(value, ArrayParameter):
1✔
114
            self.value = value.value
1✔
115
        elif isinstance(value, np.ndarray):
1✔
116
            # dont overwrite dtype of int arrays
117
            self.value = value
1✔
118
        else:
119
            self.value = np.array(value, float)
1✔
120

121
    # def __len__(self):
122
    #     This must not be defined, otherwise ArrayParameter is insufficiently different from NumPy array
123

124
    def max(self):
1✔
125
        """Return the maximum value."""
126
        return self.value.max()
×
127

128
    def __add__(self, val):
1✔
129
        """
130
        Return a new :class:`ArrayParameter` in which all values in the original
131
        :class:`ArrayParameter` have `val` added to them.
132

133
        If `val` is itself an array, return an array of :class:`ArrayParameter`
134
        objects, where ArrayParameter `i` is the original ArrayParameter added to
135
        element `i` of val.
136
        """
137
        if hasattr(val, '__len__'):
×
138
            # reshape if necessary?
139
            return np.array([self.__class__(self.value + x) for x in val], dtype=self.__class__)
×
140
        else:
141
            return self.__class__(self.value + val)
×
142

143
    def __sub__(self, val):
1✔
144
        """
145
        Return a new :class:`ArrayParameter` in which all values in the original
146
        :class:`ArrayParameter` have `val` subtracted from them.
147

148
        If `val` is itself an array, return an array of :class:`ArrayParameter`
149
        objects, where ArrayParameter `i` is the original ArrayParameter with
150
        element `i` of val subtracted from it.
151
        """
152
        if hasattr(val, '__len__'):
1✔
153
            # reshape if necessary?
154
            return np.array([self.__class__(self.value - x) for x in val], dtype=self.__class__)
×
155
        else:
156
            return self.__class__(self.value - val)
1✔
157

158
    def __mul__(self, val):
1✔
159
        """
160
        Return a new :class:`ArrayParameter` in which all values in the original
161
        :class:`ArrayParameter` have been multiplied by `val`.
162

163
        If `val` is itself an array, return an array of :class:`ArrayParameter`
164
        objects, where ArrayParameter `i` is the original ArrayParameter multiplied by
165
        element `i` of `val`.
166
        """
167
        if hasattr(val, '__len__'):
1✔
168
            # reshape if necessary?
169
            return np.array([self.__class__(self.value * x) for x in val], dtype=self.__class__)
1✔
170
        else:
171
            return self.__class__(self.value * val)
1✔
172

173
    __rmul__ = __mul__
1✔
174

175
    def __div__(self, val):
1✔
176
        """
177
        Return a new :class:`ArrayParameter` in which all values in the original
178
        :class:`ArrayParameter` have been divided by `val`.
179

180
        If `val` is itself an array, return an array of :class:`ArrayParameter`
181
        objects, where ArrayParameter `i` is the original ArrayParameter divided by
182
        element `i` of `val`.
183
        """
184
        if hasattr(val, '__len__'):
×
185
            # reshape if necessary?
186
            return np.array([self.__class__(self.value / x) for x in val], dtype=self.__class__)
×
187
        else:
188
            return self.__class__(self.value / val)
×
189

190
    __truediv__ = __div__
1✔
191

192
    def __eq__(self, other):
1✔
193
        if isinstance(other, ArrayParameter):
1✔
194
            return self.value.size == other.value.size and (self.value == other.value).all()
1✔
195
        elif isinstance(other, np.ndarray) and other.size > 0 and isinstance(other[0], ArrayParameter):
1✔
196
            return np.array([(self == seq).all() for seq in other])
1✔
197
        else:
198
            return False
×
199

200
    def __repr__(self):
1✔
201
        return "%s(%s)" % (self.__class__.__name__, self.value)
1✔
202

203

204
class Sequence(ArrayParameter):
1✔
205
    """
206
        Represents a sequence of numerical values.
207

208
        Arguments:
209
            `value`:
210
                anything which can be converted to a NumPy array, or another
211
                :class:`Sequence` object.
212
    """
213
    # should perhaps use neo.SpikeTrain instead of this class, or at least allow a neo SpikeTrain
214
    pass
1✔
215

216

217
class ParameterSpace(object):
1✔
218
    """
219
    Representation of one or more points in a parameter space.
220

221
    i.e. represents one or more parameter sets, where each parameter set has
222
    the same parameter names and types but the parameters may have different
223
    values.
224

225
    Arguments:
226
        `parameters`:
227
            a dict containing values of any type that may be used to construct a
228
            `lazy array`_, i.e. `int`, `float`, NumPy array,
229
            :class:`~pyNN.random.RandomDistribution`, function that accepts a
230
            single argument.
231
        `schema`:
232
            a dict whose keys are the expected parameter names and whose values
233
            are the expected parameter types
234
        `component`:
235
            optional - class for which the parameters are destined. Used in
236
            error messages.
237
        `shape`:
238
            the shape of the lazy arrays that will be constructed.
239

240
    .. _`lazy array`: https://lazyarray.readthedocs.org/
241
    """
242

243
    def __init__(self, parameters, schema=None, shape=None, component=None):
1✔
244
        """
245

246
        """
247
        self._parameters = {}
1✔
248
        self.schema = schema
1✔
249
        self._shape = shape
1✔
250
        self.component = component
1✔
251
        self.update(**parameters)
1✔
252
        self._evaluated = False
1✔
253

254
    def _set_shape(self, shape):
1✔
255
        for value in self._parameters.values():
1✔
256
            value.shape = shape
1✔
257
        self._shape = shape
1✔
258
    shape = property(fget=lambda self: self._shape, fset=_set_shape,
1✔
259
                     doc="Size of the lazy arrays contained within the parameter space")
260

261
    def keys(self):
1✔
262
        """
263
        PS.keys() -> list of PS's keys.
264
        """
265
        return self._parameters.keys()
1✔
266

267
    def items(self):
1✔
268
        """
269
        PS.items() ->  an iterator over the (key, value) items of PS.
270

271
        Note that the values will all be :class:`LazyArray` objects.
272
        """
273
        return self._parameters.items()
1✔
274

275
    def __repr__(self):
1✔
276
        return "<ParameterSpace %s, shape=%s>" % (", ".join(self.keys()), self.shape)
1✔
277

278
    def update(self, **parameters):
1✔
279
        """
280
        Update the contents of the parameter space according to the
281
        `(key, value)` pairs in ``**parameters``. All values will be turned into
282
        lazy arrays.
283

284
        If the :class:`ParameterSpace` has a schema, the keys and the data types
285
        of the values will be checked against the schema.
286
        """
287
        if self.schema:
1✔
288
            for name, value in parameters.items():
1✔
289
                try:
1✔
290
                    expected_dtype = self.schema[name]
1✔
291
                except KeyError:
1✔
292
                    if self.component:
1✔
293
                        model_name = self.component.__name__
1✔
294
                    else:
295
                        model_name = 'unknown'
×
296
                    raise errors.NonExistentParameterError(name,
1✔
297
                                                           model_name,
298
                                                           valid_parameter_names=self.schema.keys())
299
                if issubclass(expected_dtype, ArrayParameter) and isinstance(value, Sized):
1✔
300
                    if len(value) == 0:
1✔
301
                        value = ArrayParameter([])
1✔
302
                    elif not isinstance(value[0], ArrayParameter):  # may be a more generic way to do it, but for now this special-casing seems like the most robust approach
1✔
303
                        if isinstance(value[0], Sized):  # e.g. list of tuples
1✔
304
                            value = type(value)([ArrayParameter(x) for x in value])
1✔
305
                        else:
306
                            value = ArrayParameter(value)
1✔
307
                try:
1✔
308
                    self._parameters[name] = LazyArray(value, shape=self._shape,
1✔
309
                                                       dtype=expected_dtype)
310
                except (TypeError, errors.InvalidParameterValueError):
1✔
311
                    raise errors.InvalidParameterValueError(
1✔
312
                        "For parameter %s expected %s, got %s" % (name, expected_dtype, type(value)))
313
                except ValueError as err:
×
314
                    # maybe put the more specific error classes into lazyarray
315
                    raise errors.InvalidDimensionsError(err)
×
316
        else:
317
            for name, value in parameters.items():
1✔
318
                self._parameters[name] = LazyArray(value, shape=self._shape)
1✔
319

320
    def __getitem__(self, name):
1✔
321
        """x.__getitem__(y) <==> x[y]"""
322
        return self._parameters[name]
1✔
323

324
    def __setitem__(self, name, value):
1✔
325
        # need to add check against schema
326
        self._parameters[name] = value
1✔
327

328
    def pop(self, name, d=None):
1✔
329
        """
330
        Remove the given parameter from the parameter set and from its schema,
331
        and return its value.
332
        """
333
        value = self._parameters.pop(name, d)
1✔
334
        if self.schema:
1✔
335
            self.schema.pop(name, d)
×
336
        return value
1✔
337

338
    @property
1✔
339
    def is_homogeneous(self):
1✔
340
        """
341
        True if all of the lazy arrays within are homogeneous.
342
        """
343
        return all(value.is_homogeneous for value in self._parameters.values())
1✔
344

345
    def evaluate(self, mask=None, simplify=False):
1✔
346
        """
347
        Evaluate all lazy arrays contained in the parameter space, using the
348
        given mask.
349
        """
350
        if self._shape is None:
1✔
351
            raise Exception("Must set shape of parameter space before evaluating")
×
352
        if mask is None:
1✔
353
            for name, value in self._parameters.items():
1✔
354
                self._parameters[name] = value.evaluate(simplify=simplify)
1✔
355
            self._evaluated_shape = self._shape
1✔
356
        else:
357
            for name, value in self._parameters.items():
1✔
358
                try:
1✔
359
                    if isinstance(value.base_value, RandomDistribution) and value.base_value.rng.parallel_safe:
1✔
360
                        value = value.evaluate()  # can't partially evaluate if using parallel safe
1✔
361
                    self._parameters[name] = value[mask]
1✔
362
                except ValueError:
1✔
363
                    raise errors.InvalidParameterValueError(f"{name} should not be of type {type(value)}")
1✔
364
            self._evaluated_shape = partial_shape(mask, self._shape)
1✔
365
        self._evaluated = True
1✔
366
        # should possibly update self.shape according to mask?
367

368
    def as_dict(self):
1✔
369
        """
370
        Return a plain dict containing the same keys and values as the
371
        parameter space. The values must first have been evaluated.
372
        """
373
        if not self._evaluated:
1✔
374
            raise Exception("Must call evaluate() method before calling ParameterSpace.as_dict()")
×
375
        D = {}
1✔
376
        for name, value in self._parameters.items():
1✔
377
            D[name] = value
1✔
378
            assert not isinstance(D[name], LazyArray)  # should all have been evaluated by now
1✔
379
        return D
1✔
380

381
    def __iter__(self):
1✔
382
        r"""
383
        Return an array-element-wise iterator over the parameter space.
384

385
        Each item in the iterator is a dict, containing the same keys as the
386
        :class:`ParameterSpace`. For the `i`\th dict returned by the iterator,
387
        each value is the `i`\th element of the corresponding lazy array in the
388
        parameter space.
389

390
        Example:
391

392
        >>> ps = ParameterSpace({'a': [2, 3, 5, 8], 'b': 7, 'c': lambda i: 3*i+2}, shape=(4,))
393
        >>> ps.evaluate()
394
        >>> for D in ps:
395
        ...     print(D)
396
        ...
397
        {'a': 2, 'c': 2, 'b': 7}
398
        {'a': 3, 'c': 5, 'b': 7}
399
        {'a': 5, 'c': 8, 'b': 7}
400
        {'a': 8, 'c': 11, 'b': 7}
401
        """
402
        if not self._evaluated:
1✔
403
            raise Exception("Must call evaluate() method before iterating over a ParameterSpace")
×
404
        for i in range(self._evaluated_shape[0]):
1✔
405
            D = {}
1✔
406
            for name, value in self._parameters.items():
1✔
407
                if is_listlike(value):
1✔
408
                    D[name] = value[i]
1✔
409
                else:
410
                    D[name] = value
×
411
                assert not isinstance(D[name], LazyArray)  # should all have been evaluated by now
1✔
412
            yield D
1✔
413

414
    def columns(self):
1✔
415
        """
416
        For a 2D space, return a column-wise iterator over the parameter space.
417
        """
418
        if not self._evaluated:
1✔
419
            raise Exception("Must call evaluate() method before iterating over a ParameterSpace")
×
420
        assert len(self.shape) == 2
1✔
421
        if len(self._evaluated_shape) == 1:  # values will be one-dimensional
1✔
422
            yield self._parameters
1✔
423
        else:
424
            for j in range(self._evaluated_shape[1]):
1✔
425
                D = {}
1✔
426
                for name, value in self._parameters.items():
1✔
427
                    if is_listlike(value):
1✔
428
                        D[name] = value[:, j]
1✔
429
                    else:
430
                        D[name] = value
×
431
                    # should all have been evaluated by now
432
                    assert not isinstance(D[name], LazyArray)
1✔
433
                yield D
1✔
434

435
    def __eq__(self, other):
1✔
436
        return (all(a == b for a, b in zip(self._parameters.items(), other._parameters.items()))
×
437
                and self.schema == other.schema
438
                and self._shape == other._shape)
439

440
    @property
1✔
441
    def parallel_safe(self):
1✔
442
        return any(isinstance(value.base_value, RandomDistribution) and value.base_value.rng.parallel_safe
1✔
443
                   for value in self._parameters.values())
444

445
    @property
1✔
446
    def has_native_rngs(self):
1✔
447
        """
448
        Return True if the parameter set contains any NativeRNGs
449
        """
450
        return any(isinstance(rd.base_value.rng, NativeRNG)
1✔
451
                   for rd in self._random_distributions())
452

453
    def _random_distributions(self):
1✔
454
        """
455
        An iterator over those values contained in the PS that are
456
        derived from random distributions.
457
        """
458
        return (value for value in self._parameters.values() if isinstance(value.base_value, RandomDistribution))
1✔
459

460
    def expand(self, new_shape, mask):
1✔
461
        """
462
        Increase the size of the ParameterSpace.
463

464
        Existing array values are mapped to the indices given in mask.
465
        New array values are set to NaN.
466
        """
467
        for name, value in self._parameters.items():
×
468
            if isinstance(value.base_value, np.ndarray):
×
469
                new_base_value = np.ones(new_shape) * np.nan
×
470
                new_base_value[mask] = value.base_value
×
471
                self._parameters[name].base_value = new_base_value
×
472
        self.shape = new_shape
×
473

474

475
def simplify(value):
1✔
476
    """
477
    If `value` is a homogeneous array, return the single value that all elements
478
    share. Otherwise, pass the value through.
479
    """
480
    if isinstance(value, np.ndarray) and len(value.shape) > 0:  #  latter condition is for Brian scalar quantities
1✔
481
        if (value == value[0]).all():
1✔
482
            return value[0]
1✔
483
        else:
484
            return value
1✔
485
    else:
486
        return value
1✔
487
    # alternative - need to benchmark
488
    # if np.any(arr != arr[0]):
489
    #    return arr
490
    # else:
491
    #    return arr[0]
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

© 2025 Coveralls, Inc