File: base.py

package info (click to toggle)
brian 2.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,872 kB
  • sloc: python: 51,820; cpp: 2,033; makefile: 108; sh: 72
file content (413 lines) | stat: -rw-r--r-- 14,332 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
"""
All Brian objects should derive from `BrianObject`.
"""

import functools
import os
import sys
import traceback
import weakref

from brian2.core.names import Nameable
from brian2.units.allunits import second
from brian2.units.fundamentalunits import check_units
from brian2.utils.logger import get_logger

__all__ = [
    "BrianObject",
    "BrianObjectException",
]

logger = get_logger(__name__)


class BrianObject(Nameable):
    """
    All Brian objects derive from this class, defines magic tracking and update.

    See the documentation for `Network` for an explanation of which
    objects get updated in which order.

    Parameters
    ----------
    dt : `Quantity`, optional
        The time step to be used for the simulation. Cannot be combined with
        the `clock` argument.
    clock : `Clock`, optional
        The update clock to be used. If neither a clock, nor the `dt` argument
        is specified, the `defaultclock` will be used.
    when : str, optional
        In which scheduling slot to simulate the object during a time step.
        Defaults to ``'start'``. See :ref:`scheduling` for possible values.
    order : int, optional
        The priority of this object for operations occurring at the same time
        step and in the same scheduling slot. Defaults to 0.
    namespace: dict, optional
        A dictionary mapping identifier names to objects. If not given, the
        namespace will be filled in at the time of the call of `Network.run`,
        with either the values from the ``namespace`` argument of the
        `Network.run` method or from the local context, if no such argument is
        given.
    name : str, optional
        A unique name for the object - one will be assigned automatically if
        not provided (of the form ``brianobject_1``, etc.).
    Notes
    -----

    The set of all `BrianObject` objects is stored in ``BrianObject.__instances__()``.
    """

    @check_units(dt=second)
    def __init__(
        self,
        dt=None,
        clock=None,
        when="start",
        order=0,
        namespace=None,
        name="brianobject*",
    ):
        # Setup traceback information for this object
        creation_stack = []
        bases = []
        for modulename in ["brian2"]:
            if modulename in sys.modules:
                base, _ = os.path.split(sys.modules[modulename].__file__)
                bases.append(base)
        for fname, linenum, funcname, line in traceback.extract_stack():
            if all(base not in fname for base in bases):
                s = f"  File '{fname}', line {linenum}, in {funcname}\n    {line}"
                creation_stack.append(s)
        creation_stack = [""] + creation_stack
        #: A string indicating where this object was created (traceback with any parts of Brian code removed)
        self._creation_stack = creation_stack[-1]
        self._full_creation_stack = "\n".join(creation_stack)

        if dt is not None and clock is not None:
            raise ValueError("Can only specify either a dt or a clock, not both.")

        if not isinstance(when, str):
            from brian2.core.clocks import Clock

            # Give some helpful error messages for users coming from the alpha
            # version
            if isinstance(when, Clock):
                raise TypeError(
                    "Do not use the 'when' argument for "
                    "specifying a clock, either provide a "
                    "timestep for the 'dt' argument or a Clock "
                    "object for 'clock'."
                )
            if isinstance(when, tuple):
                raise TypeError(
                    "Use the separate keyword arguments, 'dt' (or "
                    "'clock'), 'when', and 'order' instead of "
                    "providing a tuple for 'when'. Only use the "
                    "'when' argument for the scheduling slot."
                )
            # General error
            raise TypeError(
                "The 'when' argument has to be a string "
                "specifying the scheduling slot (e.g. 'start')."
            )

        Nameable.__init__(self, name)

        #: The clock used for simulating this object
        self._clock = clock
        if clock is None:
            from brian2.core.clocks import Clock, defaultclock

            if dt is not None:
                self._clock = Clock(dt=dt, name=self.name + "_clock*")
            else:
                self._clock = defaultclock

        if getattr(self._clock, "_is_proxy", False):
            from brian2.devices.device import get_device

            self._clock = get_device().defaultclock

        #: Used to remember the `Network` in which this object has been included
        #: before, to raise an error if it is included in a new `Network`
        self._network = None

        #: The ID string determining when the object should be updated in `Network.run`.
        self.when = when

        #: The order in which objects with the same clock and ``when`` should be updated
        self.order = order

        self._dependencies = set()
        self._contained_objects = []
        self._code_objects = []

        self._active = True

        #: The scope key is used to determine which objects are collected by magic
        self._scope_key = self._scope_current_key

        # Make sure that keys in the namespace are valid
        if namespace is None:
            # Do not overwrite namespace if already set (e.g. in StateMonitor)
            namespace = getattr(self, "namespace", {})
        for key in namespace:
            if key.startswith("_"):
                raise ValueError(
                    "Names starting with underscores are "
                    "reserved for internal use an cannot be "
                    "defined in the namespace argument."
                )
        #: The group-specific namespace
        self.namespace = namespace

        logger.diagnostic(
            f"Created BrianObject with name {self.name}, "
            f"clock={self._clock}, "
            f"when={self.when}, order={self.order}"
        )

    #: Global key value for ipython cell restrict magic
    _scope_current_key = 0

    #: Whether or not `MagicNetwork` is invalidated when a new `BrianObject` of this type is added
    invalidates_magic_network = True

    #: Whether or not the object should be added to a `MagicNetwork`. Note that
    #: all objects in `BrianObject.contained_objects` are automatically added
    #: when the parent object is added, therefore e.g. `NeuronGroup` should set
    #: `add_to_magic_network` to ``True``, but it should not be set for all the
    #: dependent objects such as `StateUpdater`
    add_to_magic_network = False

    def __del__(self):
        # For objects that get garbage collected, raise a warning if they have
        # never been part of a network
        if (
            getattr(self, "_network", "uninitialized") is None
            and getattr(self, "group", None) is None
        ):
            logger.warn(
                f"The object '{self.name}' is getting deleted, but was never included in a network. "
                "This probably means that you did not store the object reference in a variable, "
                "or that the variable was not used to construct the network.\n"
                "The object was created here (most recent call only):\n"
                + self._creation_stack,
                name_suffix="unused_brian_object",
            )

    def add_dependency(self, obj):
        """
        Add an object to the list of dependencies. Takes care of handling
        subgroups correctly (i.e., adds its parent object).

        Parameters
        ----------
        obj : `BrianObject`
            The object that this object depends on.
        """
        from brian2.groups.subgroup import Subgroup

        if isinstance(obj, Subgroup):
            self._dependencies.add(obj.source.id)
        else:
            self._dependencies.add(obj.id)

    def before_run(self, run_namespace):
        """
        Optional method to prepare the object before a run.

        Called by `Network.after_run` before the main simulation loop starts.
        """
        for codeobj in self._code_objects:
            codeobj.before_run()

    def after_run(self):
        """
        Optional method to do work after a run is finished.

        Called by `Network.after_run` after the main simulation loop terminated.
        """
        for codeobj in self._code_objects:
            codeobj.after_run()

    def run(self):
        for codeobj in self._code_objects:
            codeobj()

    contained_objects = property(
        fget=lambda self: self._contained_objects,
        doc="""
        The list of objects contained within the `BrianObject`.

        When a `BrianObject` is added to a `Network`, its contained objects will
        be added as well. This allows for compound objects which contain
        a mini-network structure.

        Note that this attribute cannot be set directly, you need to modify
        the underlying list, e.g. ``obj.contained_objects.extend([A, B])``.
        """,
    )

    code_objects = property(
        fget=lambda self: self._code_objects,
        doc="""
        The list of `CodeObject` contained within the `BrianObject`.

        TODO: more details.

        Note that this attribute cannot be set directly, you need to modify
        the underlying list, e.g. ``obj.code_objects.extend([A, B])``.
        """,
    )

    updaters = property(
        fget=lambda self: self._updaters,
        doc="""
        The list of `Updater` that define the runtime behaviour of this object.

        TODO: more details.

        Note that this attribute cannot be set directly, you need to modify
        the underlying list, e.g. ``obj.updaters.extend([A, B])``.
        """,
    )

    clock = property(
        fget=lambda self: self._clock,
        doc="""
        The `Clock` determining when the object should be updated.

        Note that this cannot be changed after the object is
        created.
        """,
    )

    def _set_active(self, val):
        val = bool(val)
        self._active = val
        for obj in self.contained_objects:
            obj.active = val

    active = property(
        fget=lambda self: self._active,
        fset=_set_active,
        doc="""
        Whether or not the object should be run.

        Inactive objects will not have their `update`
        method called in `Network.run`. Note that setting or
        unsetting the `active` attribute will set or unset
        it for all `contained_objects`.
        """,
    )

    def __repr__(self):
        classname = self.__class__.__name__
        description = (
            f"{classname}(clock={self._clock}, when={self.when}, "
            f"order={self.order}, name={self.name!r})"
        )
        return description

    # This is a repeat from Nameable.name, but we want to get the documentation
    # here again
    name = Nameable.name


def weakproxy_with_fallback(obj):
    """
    Attempts to create a `weakproxy` to the object, but falls back to the object if not possible.
    """
    try:
        return weakref.proxy(obj)
    except TypeError:
        return obj


def device_override(name):
    """
    Decorates a function/method to allow it to be overridden by the current `Device`.

    The ``name`` is the function name in the `Device` to use as an override if it exists.

    The returned function has an additional attribute ``original_function``
    which is a reference to the original, undecorated function.
    """

    def device_override_decorator(func):
        def device_override_decorated_function(*args, **kwds):
            from brian2.devices.device import get_device

            curdev = get_device()
            if hasattr(curdev, name):
                return getattr(curdev, name)(*args, **kwds)
            else:
                return func(*args, **kwds)

        device_override_decorated_function.original_function = func
        functools.update_wrapper(device_override_decorated_function, func)

        return device_override_decorated_function

    return device_override_decorator


class BrianObjectException(Exception):
    """
    High level exception that adds extra Brian-specific information to exceptions

    This exception should only be raised at a fairly high level in Brian code to
    pass information back to the user. It adds extra information about where a
    `BrianObject` was defined to better enable users to locate the source of
    problems.

    Parameters
    ----------

    message : str
        Additional error information to add to the original exception.
    brianobj : BrianObject
        The object that caused the error to happen.
    original_exception : Exception
        The original exception that was raised.
    """

    def __init__(self, message, brianobj):
        self._brian_message = message
        self._brian_objname = brianobj.name
        self._brian_objcreate = (
            "Object was created here (most recent call only, full details in "
            "debug log):\n" + brianobj._creation_stack
        )
        full_stack = "Object was created here:\n" + brianobj._full_creation_stack
        logger.diagnostic(
            "Error was encountered with object "
            f"'{self._brian_objname}':\n"
            f"{full_stack}"
        )

    def __str__(self):
        return (
            f"Error encountered with object named '{self._brian_objname}'.\n"
            f"{self._brian_objcreate}\n\n"
            f"{self._brian_message} "
            "(See above for original error message and traceback.)"
        )


def brian_object_exception(message, brianobj, original_exception):
    """
    Returns a `BrianObjectException` derived from the original exception.

    Creates a new class derived from the class of the original exception
    and `BrianObjectException`. This allows exception handling code to
    respond both to the original exception class and `BrianObjectException`.

    See `BrianObjectException` for arguments and notes.
    """

    raise NotImplementedError(
        "The brian_object_exception function is no longer used. "
        "Raise a BrianObjectException directly."
    )