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