1# -*- coding: utf-8 -*-
2"""
3Implementation of the "low-level" functionality used by the common
4implementation of the API, for the NEST simulator.
5
6Classes and attributes usable by the common implementation:
7
8Classes:
9    ID
10    Connection
11
12Attributes:
13    state -- a singleton instance of the _State class.
14
15All other functions and classes are private, and should not be used by other
16modules.
17
18:copyright: Copyright 2006-2021 by the PyNN team, see AUTHORS.
19:license: CeCILL, see LICENSE for details.
20"""
21
22import nest
23import logging
24import tempfile
25import warnings
26import numpy as np
27from pyNN import common
28from pyNN.core import reraise
29
30logger = logging.getLogger("PyNN")
31name = "NEST"  # for use in annotating output data
32
33# The following constants contain the names of NEST model parameters that
34# relate to simulation time and so may need to be adjusted by a time offset.
35# TODO: Currently contains only parameters that occur in PyNN standard models.
36#       We should add parameters from all models that are distributed with NEST
37#       in case they are used with PyNN as "native" models.
38NEST_VARIABLES_TIME_DIMENSION = ("start", "stop")
39NEST_ARRAY_VARIABLES_TIME_DIMENSION = ("spike_times", "amplitude_times", "rate_times")
40
41
42# --- For implementation of get_time_step() and similar functions --------------
43
44
45def nest_property(name, dtype):
46    """Return a property that accesses a NEST kernel parameter"""
47
48    def _get(self):
49        return nest.GetKernelStatus(name)
50
51    def _set(self, val):
52        try:
53            nest.SetKernelStatus({name: dtype(val)})
54        except nest.kernel.NESTError as e:
55            reraise(e, "%s = %s (%s)" % (name, val, type(val)))
56    return property(fget=_get, fset=_set)
57
58
59def apply_time_offset(parameters, offset):
60    parameters_copy = {}
61    for name, value in parameters.items():
62        if name in NEST_VARIABLES_TIME_DIMENSION:
63            parameters_copy[name] = value + offset
64        elif name in NEST_ARRAY_VARIABLES_TIME_DIMENSION:
65            parameters_copy[name] = [v + offset for v in value]
66        else:
67            parameters_copy[name] = value
68    return parameters_copy
69
70
71class _State(common.control.BaseState):
72    """Represent the simulator state."""
73
74    def __init__(self):
75        super(_State, self).__init__()
76        try:
77            nest.Install('pynn_extensions')
78            self.extensions_loaded = True
79        except nest.kernel.NESTError as err:
80            self.extensions_loaded = False
81        self.initialized = False
82        self.optimize = False
83        self.spike_precision = "off_grid"
84        self.verbosity = "error"
85        self._cache_num_processes = nest.GetKernelStatus()['num_processes']  # avoids blocking if only some nodes call num_processes
86                                                                             # do the same for rank?
87        # allow NEST to erase previously written files (defaut with all the other simulators)
88        nest.SetKernelStatus({'overwrite_files': True})
89        self.tempdirs = []
90        self.recording_devices = []
91        self.populations = []  # needed for reset
92        self.current_sources = []
93        self._time_offset = 0.0
94        self.t_flush = -1
95        self.stale_connection_cache = False
96
97    @property
98    def t(self):
99        # note that we always simulate one min delay past the requested time
100        # we round to try to reduce floating-point problems
101        # longer-term, we should probably work with integers (in units of time step)
102        return max(np.around(self.t_kernel - self.min_delay - self._time_offset, decimals=12), 0.0)
103
104    t_kernel = nest_property("biological_time", float)
105
106    dt = nest_property('resolution', float)
107
108    threads = nest_property('local_num_threads', int)
109
110    rng_seed = nest_property('rng_seed', int)
111    grng_seed = nest_property('rng_seed', int)
112
113    @property
114    def min_delay(self):
115        return nest.GetKernelStatus('min_delay')
116
117    def set_delays(self, min_delay, max_delay):
118        # this assumes we never set max_delay without also setting min_delay
119        if min_delay != 'auto':
120            min_delay = float(min_delay)
121            if max_delay == 'auto':
122                max_delay = 10.0
123            else:
124                max_delay = float(max_delay)
125            nest.SetKernelStatus({'min_delay': min_delay,
126                                  'max_delay': max_delay})
127
128    @property
129    def max_delay(self):
130        return nest.GetKernelStatus('max_delay')
131
132    @property
133    def num_processes(self):
134        return self._cache_num_processes
135
136    @property
137    def mpi_rank(self):
138        return nest.Rank()
139
140    def _get_spike_precision(self):
141        return self._spike_precision
142
143    def _set_spike_precision(self, precision):
144        if nest.off_grid_spiking and precision == "on_grid":
145            raise ValueError("The option to use off-grid spiking cannot be turned off once enabled")
146        if precision == 'off_grid':
147            self.default_recording_precision = 15
148        elif precision == 'on_grid':
149            self.default_recording_precision = 3
150        else:
151            raise ValueError("spike_precision must be 'on_grid' or 'off_grid'")
152        self._spike_precision = precision
153    spike_precision = property(fget=_get_spike_precision, fset=_set_spike_precision)
154
155    def _set_verbosity(self, verbosity):
156        nest.set_verbosity('M_{}'.format(verbosity.upper()))
157    verbosity = property(fset=_set_verbosity)
158
159    def set_status(self, nodes, params, val=None):
160        """
161        Wrapper around nest.SetStatus() to handle time offset
162        """
163        if self._time_offset == 0.0:
164            nest.SetStatus(nodes, params, val=val)
165        else:
166            if val is None:
167                parameters = params
168            else:
169                parameters = {params: val}
170
171            if isinstance(parameters, list):
172                params_copy = []
173                for item in parameters:
174                    params_copy.append(apply_time_offset(item, self._time_offset))
175            else:
176                params_copy = apply_time_offset(parameters, self._time_offset)
177            nest.SetStatus(nodes, params_copy)
178
179    def run(self, simtime):
180        """Advance the simulation for a certain time."""
181        for population in self.populations:
182            if population._deferred_parrot_connections:
183                population._connect_parrot_neurons()
184        for device in self.recording_devices:
185            if not device._connected:
186                device.connect_to_cells()
187                device._local_files_merged = False
188        if not self.running and simtime > 0:
189            # we simulate past the real time by one min_delay, otherwise NEST doesn't give us all the recorded data
190            simtime += self.min_delay
191            self.running = True
192        if simtime > 0:
193            nest.Simulate(simtime)
194
195    def run_until(self, tstop):
196        self.run(tstop - self.t)
197
198    def reset(self):
199        if self.t > 0:
200            if self.t_flush < 0:
201                raise ValueError(
202                    "Full reset functionality is not currently available with NEST. "
203                    "If you nevertheless want to use this functionality, pass the `t_flush`"
204                    "argument to `setup()` with a suitably large value (>> 100 ms)"
205                    "then check carefully that the previous run is not influencing the "
206                    "following one."
207                )
208            else:
209                warnings.warn(
210                    "Full reset functionality is not available with NEST. "
211                    "Please check carefully that the previous run is not influencing the "
212                    "following one and, if so, increase the `t_flush` argument to `setup()`"
213                )
214            self.run(self.t_flush)  # get spikes and recorded data out of the system
215            for recorder in self.recorders:
216                recorder._clear_simulator()
217
218        self._time_offset = self.t_kernel
219
220        for p in self.populations:
221            if hasattr(p.celltype, "uses_parrot") and p.celltype.uses_parrot:
222                # 'uses_parrot' is a marker for spike sources,
223                # which may have parameters that need to be updated
224                # to account for time offset
225                # TODO: need to ensure that get/set parameters also works correctly
226                p._set_parameters(p.celltype.native_parameters)
227            for variable, initial_value in p.initial_values.items():
228                p._set_initial_value_array(variable, initial_value)
229                p._reset()
230        for cs in self.current_sources:
231            cs._reset()
232
233        self.running = False
234        self.segment_counter += 1
235
236    def clear(self):
237        self.populations = []
238        self.current_sources = []
239        self.recording_devices = []
240        self.recorders = set()
241        # clear the sli stack, if this is not done --> memory leak cause the stack increases
242        nest.ll_api.sr('clear')
243        # reset the simulation kernel
244        nest.ResetKernel()
245        # but this reverts some of the PyNN settings, so we have to repeat them (see NEST #716)
246        self.spike_precision = self._spike_precision
247        # set tempdir
248        tempdir = tempfile.mkdtemp()
249        self.tempdirs.append(tempdir)  # append tempdir to tempdirs list
250        nest.SetKernelStatus({'data_path': tempdir, })
251        self.segment_counter = -1
252        self.reset()
253
254
255# --- For implementation of access to individual neurons' parameters -----------
256
257class ID(int, common.IDMixin):
258    __doc__ = common.IDMixin.__doc__
259
260    def __init__(self, n):
261        """Create an ID object with numerical value `n`."""
262        int.__init__(n)
263        common.IDMixin.__init__(self)
264
265    @property
266    def local(self):
267        return self.node_collection.local
268
269
270# --- For implementation of connect() and Connector classes --------------------
271
272class Connection(common.Connection):
273    """
274    Provide an interface that allows access to the connection's weight, delay
275    and other attributes.
276    """
277
278    def __init__(self, parent, index):
279        """
280        Create a new connection interface.
281
282        `parent` -- a Projection instance.
283        `index` -- the index of this connection in the parent.
284        """
285        self.parent = parent
286        self.index = index
287
288    def id(self):
289        """Return a tuple of arguments for `nest.GetConnection()`.
290        """
291        return self.parent.nest_connections[self.index]
292
293    @property
294    def source(self):
295        """The ID of the pre-synaptic neuron."""
296        src = ID(nest.GetStatus(self.id(), 'source')[0])
297        src.parent = self.parent.pre
298        return src
299    presynaptic_cell = source
300
301    @property
302    def target(self):
303        """The ID of the post-synaptic neuron."""
304        tgt = ID(nest.GetStatus(self.id(), 'target')[0])
305        tgt.parent = self.parent.post
306        return tgt
307    postsynaptic_cell = target
308
309    def _set_weight(self, w):
310        nest.SetStatus(self.id(), 'weight', w * 1000.0)
311
312    def _get_weight(self):
313        """Synaptic weight in nA or µS."""
314        w_nA = nest.GetStatus(self.id(), 'weight')[0]
315        if self.parent.synapse_type == 'inhibitory' and common.is_conductance(self.target):
316            w_nA *= -1  # NEST uses negative values for inhibitory weights, even if these are conductances
317        return 0.001 * w_nA
318
319    def _set_delay(self, d):
320        nest.SetStatus(self.id(), 'delay', d)
321
322    def _get_delay(self):
323        """Synaptic delay in ms."""
324        return nest.GetStatus(self.id(), 'delay')[0]
325
326    weight = property(_get_weight, _set_weight)
327    delay = property(_get_delay, _set_delay)
328
329
330def generate_synapse_property(name):
331    def _get(self):
332        return nest.GetStatus(self.id(), name)[0]
333
334    def _set(self, val):
335        nest.SetStatus(self.id(), name, val)
336    return property(_get, _set)
337
338
339setattr(Connection, 'U', generate_synapse_property('U'))
340setattr(Connection, 'tau_rec', generate_synapse_property('tau_rec'))
341setattr(Connection, 'tau_facil', generate_synapse_property('tau_fac'))
342setattr(Connection, 'u0', generate_synapse_property('u0'))
343setattr(Connection, '_tau_psc', generate_synapse_property('tau_psc'))
344
345
346# --- Initialization, and module attributes ------------------------------------
347
348state = _State()  # a Singleton, so only a single instance ever exists
349del _State
350