1
# ----------------------------------------------------------------------------------
2
# Copyright ENS, INRIA, CNRS
3
# Contributors: Romain Brette (brette@di.ens.fr) and Dan Goodman (goodman@di.ens.fr)
5
# Brian is a computer program whose purpose is to simulate models
6
# of biological neural networks.
8
# This software is governed by the CeCILL license under French law and
9
# abiding by the rules of distribution of free software. You can use,
10
# modify and/ or redistribute the software under the terms of the CeCILL
11
# license as circulated by CEA, CNRS and INRIA at the following URL
12
# "http://www.cecill.info".
14
# As a counterpart to the access to the source code and rights to copy,
15
# modify and redistribute granted by the license, users are provided only
16
# with a limited warranty and the software's author, the holder of the
17
# economic rights, and the successive licensors have only limited
20
# In this respect, the user's attention is drawn to the risks associated
21
# with loading, using, modifying and/or developing or reproducing the
22
# software by the user in light of its specific status of free software,
23
# that may mean that it is complicated to manipulate, and that also
24
# therefore means that it is reserved for developers and experienced
25
# professionals having in-depth computer knowledge. Users are therefore
26
# encouraged to load and test the software's suitability as regards their
27
# requirements in conditions enabling the security of their systems and/or
28
# data to be ensured and, more generally, to use and operate it in the
29
# same conditions as regards security.
31
# The fact that you are presently reading this means that you have had
32
# knowledge of the CeCILL license and that you accept its terms.
33
# ----------------------------------------------------------------------------------
38
__all__ = ['Network', 'MagicNetwork', 'NetworkOperation', 'network_operation', 'run',
39
'reinit', 'stop', 'clear', 'forget', 'recall']
41
from Queue import Queue
42
from connections import *
43
from neurongroup import NeuronGroup
44
from clock import guess_clock, Clock
47
from operator import isSequenceType
49
from itertools import chain
50
from collections import defaultdict
53
from units import second
55
from utils.progressreporting import *
56
from globalprefs import *
59
globally_stopped = False
62
class Network(object):
64
Contains simulation objects and runs simulations
66
**Initialised as:** ::
70
with ``...`` any collection of objects that should be added to the :class:`Network`.
71
You can also pass lists of objects, lists of lists of objects, etc. Objects
72
that need to passed to the :class:`Network` object are:
74
* :class:`NeuronGroup` and anything derived from it such as :class:`PoissonGroup`.
75
* :class:`Connection` and anything derived from it.
76
* Any monitor such as :class:`SpikeMonitor` or :class:`StateMonitor`.
77
* Any network operation defined with the :func:`network_operation` decorator.
79
Models, equations, etc. do not need to be passed to the :class:`Network` object.
81
The most important method is the ``run(duration)`` method which runs the simulation
82
for the given length of time (see below for details about what happens when you
95
Add additional objects after initialisation, works the same way
97
``run(duration[, report[, report_period]])``
98
Runs the network for the given duration. See below for details about
99
what happens when you do this. See documentation for :func:`run` for
100
an explanation of the ``report`` and ``report_period`` keywords.
102
Reinitialises the network, runs each object's ``reinit()`` and each
103
clock's ``reinit()`` method (resetting them to 0).
105
Can be called from a :func:`network_operation` for example to stop the
106
network from running.
108
Returns the number of neurons in the network.
110
Similar to ``add``, but you can only pass one object and that
111
object is returned. You would only need this in obscure
112
circumstances where objects needed to be added to the network
113
but were either not stored elsewhere or were stored in a way
114
that made them difficult to extract, for example below the
115
NeuronGroup object is only added to the network if certain
120
x = net(NeuronGroup(...))
122
**What happens when you run**
124
For an overview, see the Concepts chapter of the main documentation.
126
When you run the network, the first thing that happens is that it
127
checks if it has been prepared and calls the ``prepare()`` method
128
if not. This just does various housekeeping tasks and optimisations
129
to make the simulation run faster. Also, an update schedule is
130
built at this point (see below).
132
Now the ``update()`` method is repeatedly called until every clock
133
has run for the given length of time. After each call of the
134
``update()`` method, the clock is advanced by one tick, and if
135
multiple clocks are being used, the next clock is determined (this
136
is the clock whose value of ``t`` is minimal amongst all the clocks).
137
For example, if you had two clocks in operation, say ``clock1`` with
138
``dt=3*ms`` and ``clock2`` with ``dt=5*ms`` then this will happen:
140
1. ``update()`` for ``clock1``, tick ``clock1`` to ``t=3*ms``, next
141
clock is ``clock2`` with ``t=0*ms``.
142
2. ``update()`` for ``clock2``, tick ``clock2`` to ``t=5*ms``, next
143
clock is ``clock1`` with ``t=3*ms``.
144
3. ``update()`` for ``clock1``, tick ``clock1`` to ``t=6*ms``, next
145
clock is ``clock2`` with ``t=5*ms``.
146
4. ``update()`` for ``clock2``, tick ``clock2`` to ``t=10*ms``, next
147
clock is ``clock1`` with ``t=6*ms``.
148
5. ``update()`` for ``clock1``, tick ``clock1`` to ``t=9*ms``, next
149
clock is ``clock1`` with ``t=9*ms``.
150
6. ``update()`` for ``clock1``, tick ``clock1`` to ``t=12*ms``, next
151
clock is ``clock2`` with ``t=10*ms``. etc.
153
The ``update()`` method simply runs each operation in the current clock's
154
update schedule. See below for details on the update schedule.
158
An update schedule is the sequence of operations that are
159
called for each ``update()`` step. The standard update schedule is:
161
* Network operations with ``when = 'start'``
162
* Network operations with ``when = 'before_groups'``
163
* Call ``update()`` method for each :class:`NeuronGroup`, this typically
164
performs an integration time step for the differential equations
165
defining the neuron model.
166
* Network operations with ``when = 'after_groups'``
167
* Network operations with ``when = 'middle'``
168
* Network operations with ``when = 'before_connections'``
169
* Call ``do_propagate()`` method for each :class:`Connection`, this
170
typically adds a value to the target state variable of each neuron
171
that a neuron that has fired is connected to. See Tutorial 2: Connections for
172
a more detailed explanation of this.
173
* Network operations with ``when = 'after_connections'``
174
* Network operations with ``when = 'before_resets'``
175
* Call ``reset()`` method for each :class:`NeuronGroup`, typically resets a
176
given state variable to a given reset value for each neuron that fired
178
* Network operations with ``when = 'after_resets'``
179
* Network operations with ``when = 'end'``
181
There is one predefined alternative schedule, which you can choose by calling
182
the ``update_schedule_groups_resets_connections()`` method before running the
183
network for the first time. As the name suggests, the reset operations are
184
done before connections (and the appropriately named network operations are
185
called relative to this rearrangement). You can also define your own update
186
schedule with the ``set_update_schedule`` method (see that method's API documentation for
187
details). This might be useful for example if you have a sequence of network
188
operations which need to be run in a given order.
191
operations = property(fget=lambda self:self._all_operations)
193
def __init__(self, *args, **kwds):
194
self.clock = None # Initialized later
196
self.connections = []
197
# The following dict keeps a copy of which operations are in which slot
198
self._operations_dict = defaultdict(list)
199
self._all_operations = []
200
self.update_schedule_standard()
201
self.prepared = False
202
for o in chain(args, kwds.itervalues()):
205
def add(self, *objs):
207
Add an object or container of objects to the network
210
if isinstance(obj, NeuronGroup):
211
if obj not in self.groups:
212
self.groups.append(obj)
213
elif isinstance(obj, Connection):
214
if obj not in self.connections:
215
self.connections.append(obj)
216
elif isinstance(obj, NetworkOperation):
217
if obj not in self._all_operations:
218
self._operations_dict[obj.when].append(obj)
219
self._all_operations.append(obj)
220
elif isSequenceType(obj):
224
raise TypeError('Only the following types of objects can be added to a network: NeuronGroup, Connection or NetworkOperation')
227
gco = obj.contained_objects
230
except AttributeError:
233
def __call__(self, obj):
235
Add an object to the network and return it
242
Resets the objects and clocks.
244
objs = self.groups + self.connections + self.operations
245
if self.clock is not None:
246
objs.append(self.clock)
248
guess_clock(None).reinit()
249
if hasattr(self, 'clocks'):
250
objs.extend(self.clocks)
252
if hasattr(P, 'reinit'):
257
Prepares the network for simulation:
258
+ Checks the clocks of the neuron groups
259
+ Gather connections with identical subgroups
260
+ Compresses the connection matrices for faster simulation
261
Calling this function is not mandatory but speeds up the simulation.
264
if self.same_clocks():
267
self.set_many_clocks()
269
# Gather connections with identical subgroups
270
# 'subgroups' maps subgroups to connections (initialize with immutable object (not [])!)
271
subgroups = dict.fromkeys([(C.source, C.delay) for C in self.connections], None)
272
for C in self.connections:
273
if subgroups[(C.source, C.delay)] == None:
274
subgroups[(C.source, C.delay)] = [C]
276
subgroups[(C.source, C.delay)].append(C)
277
self.connections = subgroups.values()
278
cons = self.connections # just for readability
279
for i in range(len(cons)):
280
if len(cons[i]) > 1: # at least 2 connections with the same subgroup
281
cons[i] = MultiConnection(cons[i][0].source, cons[i])
285
# Compress connections
286
for C in self.connections:
289
# Experimental support for new propagation code
290
if get_global_preference('usenewpropagate') and get_global_preference('useweave'):
291
from experimental.new_c_propagate import make_new_connection
292
for C in self.connections:
293
make_new_connection(C)
295
# build operations list for each clock
296
self._build_update_schedule()
300
def update_schedule_standard(self):
301
self._schedule = ['ops start',
306
'ops before_connections',
308
'ops after_connections',
314
self._build_update_schedule()
316
def update_schedule_groups_resets_connections(self):
317
self._schedule = ['ops start',
325
'ops before_connections',
327
'ops after_connections',
330
self._build_update_schedule()
332
def set_update_schedule(self, schedule):
334
Defines a custom update schedule
336
A custom update schedule is a list of schedule items. Each update
337
step of the network, the schedule items will be run in turn. A
338
schedule item can be defined as a string or tuple. The following
339
string definitions are possible:
342
Calls the 'update' function of each group in turn, this is
343
typically the integration step of the simulation.
345
Calls the 'do_propagate' function of each connection in
346
turn, this is typically propagating spikes forward (and
347
backward in the case of STDP).
349
Calls the 'reset' function of each group in turn.
351
Calls each operation in turn whose 'when' parameter is
352
set to 'name'. The standard set of 'when' names is
353
start, before_groups, after_groups, before_resets,
354
after_resets, before_connections, after_connections,
355
end, but you can use any you like.
357
If a tuple is provided, it should be of the form:
359
(objset, func, allclocks)
364
a list of objects to be processed
366
Either None or a string. In the case of none, each
367
object in objset must be callable and will be called.
368
In the case of a string, obj.func will be called for
371
Either True or False. If it's set to True, then the
372
object will be placed in the update schedule of
373
every clock in the network. If False, it will be
374
placed in the update schedule only of the clock
377
self._schedule = schedule
378
self._build_update_schedule()
380
def _build_update_schedule(self):
382
Defines what the update step does
384
For each clock we build a list self._update_schedule[id(clock)]
385
of functions which are called at the update step if that clock
386
is active. This is generic and works for single or multiple
389
See documentation for set_update_schedule for an explanation of
390
the self._schedule object.
392
self._update_schedule = defaultdict(list)
393
if hasattr(self, 'clocks'):
396
clocks = [self.clock]
398
for item in self._schedule:
399
# we define some simple names for common schedule items
400
if isinstance(item, str):
405
elif item == 'resets':
409
elif item == 'connections':
410
objset = self.connections
411
objfun = 'do_propagate'
413
# Connections do not define their own clock, but they should
414
# be updated on the schedule of their source group
416
obj.clock = obj.source.clock
417
elif len(item) > 4 and item[0:3] == 'ops': # the item is of the forms 'ops when'
418
objset = self._operations_dict[item[4:]]
422
# we allow the more general form of usage as well
423
objset, objfun, allclocks = item
428
f = getattr(obj, objfun)
430
useclockset = [obj.clock]
432
useclockset = clockset
433
for clock in useclockset:
434
self._update_schedule[id(clock)].append(f)
437
for f in self._update_schedule[id(self.clock)]:
441
def update_threaded(self,queue):
443
EXPERIMENTAL (not useful for the moment)
444
Parallel update of the network (using threads).
446
# Update groups: one group = one thread
447
for P in self.groups:
449
queue.join() # Wait until job is done
451
# The following is done serially
453
for C in self.connections:
454
C.propagate(C.source.get_spikes(C.delay))
456
# Miscellanous operations
457
for op in self.operations:
461
def run(self, duration, threads=1, report=None, report_period=10 * second):
463
Runs the simulation for the given duration.
465
global globally_stopped
467
globally_stopped = False
468
if not self.prepared:
470
self.clock.set_duration(duration)
472
for c in self.clocks:
473
c.set_duration(duration)
474
except AttributeError:
476
if report is not None:
477
start_time = time.time()
478
if not isinstance(report, ProgressReporter):
479
report = ProgressReporter(report, report_period)
480
next_report_time = start_time + float(report_period)
482
report_period = report.period
483
next_report_time = report.next_report_time
485
if self.clock.still_running() and not self.stopped and not globally_stopped:
486
not_same_clocks = not self.same_clocks()
487
while self.clock.still_running() and not self.stopped and not globally_stopped:
488
if report is not None:
489
cur_time = time.time()
490
if cur_time > next_report_time:
491
next_report_time = cur_time + float(report_period)
492
report.update((self.clock.t - self.clock.start) / duration)
496
# Find the next clock to update
497
self.clock = min([(clock.t, clock) for clock in self.clocks])[1]
498
if report is not None:
503
Stops the network from running, this is reset the next time ``run()`` is called.
507
def same_clocks(self):
509
Returns True if the clocks of all groups and operations are the same.
511
clock = self.groups[0].clock
512
return all([obj.clock == clock for obj in self.groups + self.operations])
516
Sets the clock and checks that clocks of all groups are synchronized.
518
if self.same_clocks():
519
self.clock = self.groups[0].clock
521
raise TypeError, 'Clocks are not synchronized!' # other error type?
523
def set_many_clocks(self):
525
Sets a list of clocks.
526
self.clock points to the current clock between considered.
528
self.clocks = list(set([obj.clock for obj in self.groups + self.operations]))
529
self.clock = min([(clock.t, clock) for clock in self.clocks])[1]
533
Number of neurons in the network
536
for P in self.groups:
537
n += len(P) # use compact iterator function?
541
return 'Network of' + str(len(self)) + 'neurons'
543
# TODO: obscure custom update schedules might still lead to unpicklable Network object
544
def __reduce__(self):
545
# This code might need some explanation:
547
# The problem with pickling the Network object is that you cannot pickle
548
# 'instance methods', that is a copy of a method of an instance. The
549
# Network object does this because the _update_schedule attribute stores
550
# a copy of all the functions that need to be called each time step, and
551
# these are all instance methods (of NeuronGroup, Reset, etc.). So, we
552
# solve the problem by deleting this attribute at pickling time, and then
553
# rebuilding it at unpickling time. The function unpickle_network defined
554
# below does the unpickling.
556
# We basically want to make a copy of the current object, and delete the
557
# update schedule from it, and then pickle that. Some weird recursive
558
# stuff happens if you try to do this in the obvious way, so we take the
559
# seemingly mad step of converting the object to a general 'heap' class
560
# (that is, a new-style class with no methods or anything, in this case
561
# the NetworkNoMethods class defined below), do all our operations on
562
# this, store a copy of the actual class of the object (which may not be
563
# Network for derived classes), work with this, and then restore
564
# everything back to the way it was when everything is done.
566
oldclass = self.__class__ # class may be derived from Network
567
self.__class__ = NetworkNoMethods # stops recursion in copy.copy
568
net = copy.copy(self) # we make a copy because after returning from this function we can't restore the class
569
self.__class__ = oldclass # restore the class of the original, which is now back in its original state
570
net._update_schedule = None # remove the problematic element from the copy
571
return (unpickle_network, (oldclass, net)) # the unpickle_network function called with arguments oldclass, net restores it as it was
573
# This class just used as a general 'heap' class - has no methods but can have attributes
574
class NetworkNoMethods(object):
577
def unpickle_network(oldclass, net):
578
# See Network.__reduce__ for an explanation, basically the _update_schedule
579
# cannot be pickled because it contains instance methods, but it can just be
581
net.__class__ = oldclass
582
net._build_update_schedule()
586
class NetworkOperation(magic.InstanceTracker, ObjectContainer):
587
"""Callable class for operations that should be called every update step
589
Typically, you should just use the :func:`network_operation` decorator, but if you
590
can't for whatever reason, use this. Note: current implementation only works for
591
functions, not any callable object.
593
**Initialisation:** ::
595
NetworkOperation(function[,clock])
597
If your function takes an argument, the clock will be passed
600
def __init__(self, function, clock=None, when='end'):
601
self.clock = guess_clock(clock)
603
self.function = function
606
if self.function.func_code.co_argcount == 1:
607
self.function(self.clock)
612
def network_operation(*args, **kwds):
613
"""Decorator to make a function into a :class:`NetworkOperation`
615
A :class:`NetworkOperation` is a callable class which is called every
616
time step by the :class:`Network` ``run`` method. Sometimes it is useful
617
to just define a function which is to be run every update step. This
618
decorator can be used to turn a function into a :class:`NetworkOperation`
619
to be added to a :class:`Network` object.
623
Operation doesn't need a clock::
629
Automagically detect clock::
637
@network_operation(specifiedclock)
641
Specify when the network operation is run (default is ``'end'``)::
643
@network_operation(when='start')
647
Then add to a network as follows::
651
# Notes on this decorator:
652
# Normally, a decorator comes in two types, with or without arguments. If
653
# it has no arguments, e.g.
657
# then the decorator function is defined with an argument, and that
658
# argument is the function f. In this case, the decorator function
659
# returns a new function in place of f.
661
# However, you can also define:
665
# in which case the argument to the decorator function is arg, and the
666
# decorator function returns a 'function factory', that is a callable
667
# object that takes a function as argument and returns a new function.
669
# It might be clearer just to note that the first form above is equivalent
673
# f = decorator(arg)(f)
675
# In this case, we're allowing the decorator to be called either with or
676
# without an argument, so we have to look at the arguments and determine
677
# if it's a function argument (in which case we do the first case above),
678
# or if the arguments are arguments to the decorator, in which case we
679
# do the second case above.
681
# Here, the 'function factory' is the locally defined class
682
# do_network_operation, which is a callable object that takes a function
683
# as argument and returns a NetworkOperation object.
684
class do_network_operation(object):
685
def __init__(self, clock=None, when='end'):
688
def __call__(self, f, level=1):
689
new_network_operation = NetworkOperation(f, self.clock, self.when)
690
# Depending on whether we were called as @network_operation or
691
# @network_operation(...) we need different levels, the level is
692
# 2 in the first case and 1 in the second case (because in the
693
# first case we go originalcaller->network_operation->do_network_operation
694
# and in the second case we go originalcaller->do_network_operation
695
# at the time when this method is called).
696
new_network_operation.set_instance_id(level=level)
697
new_network_operation.__name__ = f.__name__
698
new_network_operation.__doc__ = f.__doc__
699
new_network_operation.__dict__.update(f.__dict__)
700
return new_network_operation
701
if len(args) == 1 and callable(args[0]):
702
# We're in case (1), the user has written:
706
# and the single argument to the decorator is the function f
707
return do_network_operation()(args[0], level=2)
709
# We're in case (2), the user has written:
710
# @network_operation(...)
713
# and the arguments might be clocks or strings, and may have been
714
# called with or without names, so we check both the variable length
715
# argument list *args, and the keyword dictionary **kwds, falling
716
# back on the default values if nothing is given.
720
if isinstance(arg, Clock):
722
elif isinstance(arg, str):
724
for key, val in kwds.iteritems():
725
if key == 'clock': clk = val
726
if key == 'when': when = val
727
return do_network_operation(clock=clk, when=when)
728
#raise TypeError, "Decorator must be used as @network_operation or @network_operation(clock)"
731
class MagicNetwork(Network):
733
Creates a :class:`Network` object from any suitable objects
735
**Initialised as:** ::
739
The object returned can then be used just as a regular
740
:class:`Network` object. It works by finding any object in
741
the ''execution frame'' (i.e. in the same function, script
742
or section of module code where the :class:`MagicNetwork` was
743
created) derived from :class:`NeuronGroup`, :class:`Connection` or
744
:class:`NetworkOperation`.
755
Each of the objects ``G``, ``C`` and ``f`` are added to ``net``.
757
**Advanced usage:** ::
759
MagicNetwork([verbose=False[,level=1]])
764
Set to ``True`` to print out a list of objects that were
765
added to the network, for debugging purposes.
767
Where to find objects. ``level=1`` means look for objects
768
where the :class:`MagicNetwork` object was created. The ``level``
769
argument says how many steps back in the stack to look.
771
def __init__(self, verbose=False, level=1):
773
Set verbose=False to turn off comments.
774
The level variable contains the location of the namespace.
776
(groups, groupnames) = magic.find_instances(NeuronGroup)
777
groups = [g for g in groups if g._owner is g]
778
groupnames = [gn for g, gn in zip(groups, groupnames) if g._owner is g]
779
(connections, connectionnames) = magic.find_instances(Connection)
780
(operations, operationnames) = magic.find_instances(NetworkOperation)
782
print "[MagicNetwork] Groups:", groupnames
783
print "[MagicNetwork] Connections:", connectionnames
784
print "[MagicNetwork] Operations:", operationnames
785
# Use set() to discard duplicates
786
Network.__init__(self, list(set(groups)), list(set(connections)), list(set(operations)))
789
def run(duration, threads=1, report=None, report_period=10 * second):
791
Run a network created from any suitable objects that can be found
796
the length of time to run the network for.
798
How to report progress, the default ``None`` doesn't report the
799
progress. Some standard values for ``report``:
802
Prints progress to the standard output.
804
Prints progress to the standard error output stderr.
805
``graphical``, ``tkinter``
806
Uses the Tkinter module to show a graphical progress bar,
807
this may interfere with any other GUI code you have.
809
Alternatively, you can provide your own callback function by
810
setting ``report`` to be a function ``report(elapsed, complete)``
811
of two variables ``elapsed``, the amount of time elapsed in
812
seconds, and ``complete`` the proportion of the run duration
813
simulated (between 0 and 1). The ``report`` function is
814
guaranteed to be called at the end of the run with
815
``complete=1.0`` so this can be used as a condition for
816
reporting that the computation is finished.
818
How often the progress is reported (by default, every 10s).
820
Works by constructing a :class:`MagicNetwork` object from all the suitable
821
objects that could be found (:class:`NeuronGroup`, :class:`Connection`, etc.) and
822
then running that network. Not suitable for repeated runs or situations
823
in which you need precise control.
825
MagicNetwork(verbose=False, level=2).run(duration, threads=threads,
826
report=report, report_period=report_period)
831
Reinitialises any suitable objects that can be found
837
Works by constructing a :class:`MagicNetwork` object from all the suitable
838
objects that could be found (:class:`NeuronGroup`, :class:`Connection`, etc.) and
839
then calling ``reinit()`` for each of them. Not suitable for repeated
840
runs or situations in which you need precise control.
842
MagicNetwork(verbose=False, level=2).reinit()
847
Globally stops any running network, this is reset the next time a network is run
849
global globally_stopped
850
globally_stopped = True
852
def clear(erase=True, all=False):
854
Clears all Brian objects.
856
Specifically, it stops all existing Brian objects from being collected by
857
:class:`MagicNetwork` (objects created after clearing will still be collected).
858
If ``erase`` is ``True`` then it will also delete all data from these objects.
859
This is useful in, for example, ``ipython`` which stores persistent references
860
to objects in any given session, stopping the data and memory from being freed
861
up. If ``all=True`` then all Brian objects will be cleared. See also
865
net = MagicNetwork(level=2)
866
objs = net.groups + net.connections + net.operations
868
groups, _ = magic.find_instances(NeuronGroup, all=True)
869
connections, _ = magic.find_instances(Connection, all=True)
870
operations, _ = magic.find_instances(NetworkOperation, all=True)
871
objs = groups+connections+operations
873
o.set_instance_id(-1)
875
for k, v in o.__dict__.iteritems():
876
object.__setattr__(o, k, None)
881
Forgets the list of objects passed
883
Forgetting means that :class:`MagicNetwork` will not pick up these objects,
884
but all data is retained. You can pass objects or lists of objects. Forgotten
885
objects can be recalled with :func:`recall`. See also :func:`clear`.
888
if isinstance(obj, (NeuronGroup, Connection, NetworkOperation)):
889
obj._forgotten_instance_id = obj.get_instance_id()
890
obj.set_instance_id(-1)
891
elif isSequenceType(obj):
895
raise TypeError('Only the following types of objects can be forgotten: NeuronGroup, Connection or NetworkOperation')
899
Recalls previously forgotten objects
901
See :func:`forget` and :func:`clear`.
904
if isinstance(obj, (NeuronGroup, Connection, NetworkOperation)):
905
if hasattr(obj, '_forgotten_instance_id'):
906
obj.set_instance_id(obj._forgotten_instance_id)
907
elif isSequenceType(obj):
911
raise TypeError('Only the following types of objects can be recalled: NeuronGroup, Connection or NetworkOperation')