class RandomizedSVD(LeastSquaresSolver): """Solve a least-squares system using a randomized (partial) SVD. Useful for solving large matrices quickly, but non-optimally. Parameters ---------- n_components : int, optional The number of SVD components to compute. A small survey of activity matrices suggests that the first 60 components capture almost all the variance. n_oversamples : int, optional The number of additional samples on the range of A. n_iter : int, optional The number of power iterations to perform (can help with noisy data). See also -------- sklearn.utils.extmath.randomized_svd : Function used by this class """ n_components = IntParam("n_components", low=1) n_oversamples = IntParam("n_oversamples", low=0) n_iter = IntParam("n_iter", low=0) def __init__(self, n_components=60, n_oversamples=10, n_iter=0): from sklearn.utils.extmath import ( # pylint: disable=import-outside-toplevel randomized_svd, ) assert randomized_svd super().__init__() self.n_components = n_components self.n_oversamples = n_oversamples self.n_iter = n_iter def __call__(self, A, Y, sigma, rng=np.random): from sklearn.utils.extmath import ( # pylint: disable=import-outside-toplevel randomized_svd, ) Y, m, n, _, matrix_in = format_system(A, Y) if min(m, n) <= self.n_components + self.n_oversamples: # more efficient to do a full SVD return SVD()(A, Y, sigma, rng=rng) U, s, V = randomized_svd( A, self.n_components, n_oversamples=self.n_oversamples, n_iter=self.n_iter, random_state=rng, ) si = s / (s ** 2 + m * sigma ** 2) X = np.dot(V.T, si[:, None] * np.dot(U.T, Y)) info = {"rmses": npext.rms(Y - np.dot(A, X), axis=0)} return X if matrix_in else X.ravel(), info
class Superposition(Network): """Network for superposing multiple inputs. Parameters ---------- n_inputs : int Number of inputs. vocab : Vocabulary or int The vocabulary to use to interpret the vector. If an integer is given, the default vocabulary of that dimensionality will be used. neurons_per_dimension : int, optional (Default: 200) Number of neurons to use in each dimension. kwargs : dict Keyword arguments passed through to `nengo_spa.Network`. Attributes ---------- inputs : sequence Inputs. output : nengo.Node Output. """ vocab = VocabularyOrDimParam('vocab', default=None, readonly=True) neurons_per_dimension = IntParam('neurons_per_dimension', default=200, low=1, readonly=True) n_inputs = IntParam('n_inputs', optional=False, low=1, readonly=True) def __init__(self, n_inputs, vocab=Default, neurons_per_dimension=Default, **kwargs): kwargs.setdefault('label', "Superposition") super(Superposition, self).__init__(**kwargs) self.vocab = vocab self.neurons_per_dimension = neurons_per_dimension self.n_inputs = n_inputs with self: self.superposition_net, self.inputs, self.output = ( self.vocab.algebra.implement_superposition( self.neurons_per_dimension, self.vocab.dimensions, self.n_inputs)) for inp in self.inputs: self.declare_input(inp, self.vocab) self.declare_output(self.output, self.vocab)
class TensorNode(Node): """ Inserts TensorFlow code into a Nengo model. Parameters ---------- tensor_func : callable A function that maps node inputs to outputs size_in : int (Default: 0) The number of elements in the input vector size_out : int (Default: None) The number of elements in the output vector (if None, value will be inferred by calling ``tensor_func``) label : str (Default: None) A name for the node, used for debugging and visualization """ tensor_func = TensorFuncParam('tensor_func') size_in = IntParam('size_in', default=0, low=0, optional=True) size_out = IntParam('size_out', default=None, low=1, optional=True) def __init__(self, tensor_func, size_in=Default, size_out=Default, label=Default): # pylint: disable=non-parent-init-called,super-init-not-called # note: we bypass the Node constructor, because we don't want to # perform validation on `output` NengoObject.__init__(self, label=label, seed=None) self.size_in = size_in self.size_out = size_out self.tensor_func = tensor_func @property def output(self): """ Ensure that nothing tries to evaluate the `output` attribute (indicating that something is trying to simulate this as a regular `nengo.Node` rather than a TensorNode. """ def output_func(*_): raise SimulationError( "Cannot call TensorNode output function (this probably means " "you are trying to use a TensorNode inside a Simulator other " "than NengoDL)") return output_func
class RandomizedSVD(LeastSquaresSolver): """Solve a least-squares system using a randomized (partial) SVD. Parameters ---------- n_components : int (default is 60) The number of SVD components to compute. A small survey of activity matrices suggests that the first 60 components capture almost all the variance. n_oversamples: int (default is 10) The number of additional samples on the range of A. n_iter : int (default is 0) The number of power iterations to perform (can help with noisy data). See also -------- ``sklearn.utils.extmath.randomized_svd`` for details about the parameters. """ n_components = IntParam('n_components', low=1) n_oversamples = IntParam('n_oversamples', low=0) n_iter = IntParam('n_iter', low=0) def __init__(self, n_components=60, n_oversamples=10, n_iter=0): from sklearn.utils.extmath import randomized_svd # error early if DNE assert randomized_svd super(RandomizedSVD, self).__init__() self.n_components = n_components self.n_oversamples = n_oversamples self.n_iter = n_iter def __call__(self, A, Y, sigma, rng=np.random): from sklearn.utils.extmath import randomized_svd Y, m, n, _, matrix_in = format_system(A, Y) if min(m, n) <= self.n_components + self.n_oversamples: # more efficient to do a full SVD return SVD()(A, Y, sigma, rng=rng) U, s, V = randomized_svd(A, self.n_components, n_oversamples=self.n_oversamples, n_iter=self.n_iter, random_state=rng) si = s / (s**2 + m * sigma**2) X = np.dot(V.T, si[:, None] * np.dot(U.T, Y)) info = {'rmses': npext.rms(Y - np.dot(A, X), axis=0)} return X if matrix_in else X.ravel(), info
class Scalar(Network): """Represents a single scalar. Parameters ---------- n_neurons : int, optional (Default: 50) Number of neurons to represent the scalar. **kwargs : dict Keyword arguments passed through to `nengo_spa.Network`. Attributes ---------- input : nengo.Node Input. output : nengo.Node Output. """ n_neurons = IntParam("n_neurons", default=50, low=1, readonly=True) def __init__(self, n_neurons=Default, **kwargs): super(Scalar, self).__init__(**kwargs) self.n_neurons = n_neurons with self: self.scalar = nengo.Ensemble(self.n_neurons, 1) self.input = self.scalar self.output = self.scalar self.declare_input(self.input, None) self.declare_output(self.output, None)
class WeightSymmetryLR(nengo.learning_rules.LearningRuleType, SupportDefaultsMixin): modifies = 'decoders' seed = IntParam('seed', default=None, optional=True, readonly=True) def __init__(self, learning_rate=1., seed=Default): super(WeightSymmetryLR, self).__init__(learning_rate, size_in=1) self.seed = seed
class TensorNode(Node): """Inserts TensorFlow code into a Nengo model. A TensorNode operates in much the same way as a :class:`~nengo:nengo.Node`, except its inputs and outputs are defined using TensorFlow operations. The TensorFlow code is defined in a function or callable class (``tensor_func``). This function accepts the current simulation time as input, or the current simulation time and a Tensor ``x`` if ``node.size_in > 0``. ``x`` will have shape ``(sim.minibatch_size, node.size_in``), and the function should return a Tensor with shape ``(sim.minibatch_size, node.size_out)``. ``node.size_out`` will be inferred by calling the function once and checking the output, if it isn't set when the Node is created. If ``tensor_func`` has a ``pre_build`` attribute, that function will be called once when the model is constructed. This can be used to compute any constant values or set up variables -- things that don't need to execute every simulation timestep. Parameters ---------- tensor_func : callable a function that maps node inputs to outputs size_in : int, optional (Default: 0) the number of elements in the input vector size_out : int, optional (Default: None) the number of elements in the output vector (if None, value will be inferred by calling ``tensor_func``) label : str, optional (Default: None) a name for the node, used for debugging and visualization """ tensor_func = TensorFuncParam('tensor_func') size_in = IntParam('size_in', default=0, low=0, optional=True) size_out = IntParam('size_out', default=None, low=1, optional=True) def __init__(self, tensor_func, size_in=Default, size_out=Default, label=Default): # note: we bypass the Node constructor, because we don't want to # perform validation on `output` NengoObject.__init__(self, label=label, seed=None) self.size_in = size_in self.size_out = size_out self.tensor_func = tensor_func
def test_copy_instance_params(): with nengo.Network() as original: original.config[nengo.Ensemble].set_param( "test", IntParam("test", optional=True)) ens = nengo.Ensemble(10, 1) original.config[ens].test = 42 cp = original.copy() assert cp.config[cp.ensembles[0]].test == 42
class Compare(Network): """Computes the dot product of two inputs. Parameters ---------- vocab : Vocabulary or int The vocabulary to use to interpret the vector. If an integer is given, the default vocabulary of that dimensionality will be used. neurons_per_dimension : int, optional (Default: 200) Number of neurons to use in each product computation. **kwargs : dict Keyword arguments passed through to `nengo_spa.Network`. Attributes ---------- input_a : nengo.Node First input vector. input_b : nengo.Node Second input vector. output : nengo.Node Output. """ vocab = VocabularyOrDimParam("vocab", default=None, readonly=True) neurons_per_dimension = IntParam("neurons_per_dimension", default=200, low=1, readonly=True) def __init__(self, vocab=Default, neurons_per_dimension=Default, **kwargs): super(Compare, self).__init__(**kwargs) self.vocab = vocab self.neurons_per_dimension = neurons_per_dimension with self: with nengo.Config(nengo.Ensemble) as cfg: cfg[nengo.Ensemble].eval_points = nengo.dists.CosineSimilarity( self.vocab.dimensions + 2) cfg[nengo.Ensemble].intercepts = nengo.dists.CosineSimilarity( self.vocab.dimensions + 2) self.product = nengo.networks.Product( self.neurons_per_dimension, self.vocab.dimensions) self.output = nengo.Node(size_in=1, label="output") nengo.Connection( self.product.output, self.output, transform=np.ones((1, self.vocab.dimensions)), ) self.input_a = self.product.input_a self.input_b = self.product.input_b self.declare_input(self.input_a, self.vocab) self.declare_input(self.input_b, self.vocab) self.declare_output(self.output, None)
class ConstrainedConnectivity(Connectivity): """ This class can be used to specify convergence and divergence parameters for a connection. Furthermore, an optional probability matrix or callable can be supplied that computes connection probabilities between individual neurons. """ convergence = IntParam( name="convergence", optional=True, readonly=True, ) divergence = IntParam( name="divergence", optional=True, readonly=True, ) probabilities = ConnectivityProbabilitiesParam( name="probabilities", default=None, optional=True, readonly=True, ) @property def _argreprs(self): return [ "convergence={}".format(self.convergence), "divergence={}".format(self.divergence), "probabilities={}".format(self.probabilities), ] def __init__(self, convergence=None, divergence=None, probabilities=None): super().__init__() self.convergence = convergence self.divergence = divergence self.probabilities = probabilities
def add_spinnaker_params(config): """Add SpiNNaker specific parameters to a configuration object.""" # Add simulator parameters config.configures(Simulator) config[Simulator].set_param("placer", CallableParameter(default=par.place)) config[Simulator].set_param("placer_kwargs", DictParam(default={})) config[Simulator].set_param("allocater", CallableParameter(default=par.allocate)) config[Simulator].set_param("allocater_kwargs", DictParam(default={})) config[Simulator].set_param("router", CallableParameter(default=par.route)) config[Simulator].set_param("router_kwargs", DictParam(default={})) config[Simulator].set_param("node_io", Parameter(default=Ethernet)) config[Simulator].set_param("node_io_kwargs", DictParam(default={})) # Add function_of_time parameters to Nodes config[nengo.Node].set_param("function_of_time", BoolParam(default=False)) config[nengo.Node].set_param("function_of_time_period", NumberParam(default=None, optional=True)) # Add multiple-core options to Nodes config[nengo.Node].set_param( "n_cores_per_chip", IntParam(default=None, low=1, high=16, optional=True)) config[nengo.Node].set_param("n_chips", IntParam(default=None, low=1, optional=True)) # Add optimisation control parameters to (passthrough) Nodes. None means # that a heuristic will be used to determine if the passthrough Node should # be removed. config[nengo.Node].set_param("optimize_out", BoolParam(default=None, optional=True)) # Add profiling parameters to Ensembles config[nengo.Ensemble].set_param("profile", BoolParam(default=False)) config[nengo.Ensemble].set_param("profile_num_samples", NumberParam(default=None, optional=True))
class AML(nengo.learning_rules.LearningRuleType, SupportDefaultsMixin): """Association matrix learning rule (AML). Error is used as target vector and the pre-synaptic ensemble provides the cue vector. """ error_type = 'decoded' modifies = 'decoders' seed = IntParam('seed', default=None, optional=True, readonly=True) def __init__(self, d, learning_rate=1., seed=Default): super(AML, self).__init__(learning_rate, size_in=d + 2) self.seed = seed
class TestClass: """For testing that defaults are properly rendered in docs.""" int_param = IntParam("int_param", default=1) str_param = StringParam("str_param", default="hello") def __init__(self, int_param=Default, str_param=Default, module_param=np.random): """Init method""" def another_method(self, module_param=np.random): """A method"""
class Process(object): """A general system with input, output, and state. Attributes ---------- default_size_out : int If `d` isn't specified in `run` or `run_steps`, this will be used. Default: 1. default_dt : float If `dt` isn't specified in `run`, `run_steps`, `ntrange`, or `trange`, this will be used. Default: 0.001 (1 millisecond). """ default_size_out = IntParam(low=0) default_dt = NumberParam(low=0, low_open=True) def __init__(self): self.default_size_out = 1 self.default_dt = 0.001 def make_step(self, size_in, size_out, dt, rng): raise NotImplementedError("Process must implement `make_step` method.") def run_steps(self, n_steps, d=None, dt=None, rng=np.random): # TODO: allow running with input d = self.default_size_out if d is None else d dt = self.default_dt if dt is None else dt step = self.make_step(0, d, dt, rng) output = np.zeros((n_steps, d)) for i in range(n_steps): output[i] = step(i * dt) return output def run(self, t, d=None, dt=None, rng=np.random): # TODO: allow running with input dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.run_steps(n_steps, d=d, dt=dt, rng=rng) def ntrange(self, n_steps, dt=None): dt = self.default_dt if dt is None else dt return dt * np.arange(1, n_steps + 1) def trange(self, t, dt=None): dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.ntrange(n_steps, dt=dt)
def test_copy_instance_params(): with nengo.Network() as orig_net: orig_net.config[nengo.Ensemble].set_param( "test", IntParam("test", optional=True)) orig_net.config[nengo.Ensemble].set_param( "test2", NdarrayParam("test2", optional=True)) orig_ens = nengo.Ensemble(10, 1) orig_net.config[orig_ens].test = 42 orig_net.config[orig_ens].test2 = np.array([49]) # test copy function copy_net = orig_net.copy() copy_ens = copy_net.ensembles[0] assert copy_net.config[copy_ens].test == 42 assert np.array_equal(copy_net.config[copy_ens].test2, np.array([49])) # test copy via pickle copy_net = pickle.loads(pickle.dumps(orig_net)) copy_ens = copy_net.ensembles[0] assert copy_net.config[copy_ens].test == 42 assert np.array_equal(copy_net.config[copy_ens].test2, np.array([49]))
class Product(Network): """Multiplies two scalars. Parameters ---------- n_neurons : int, optional (Default: 200) Number of neurons to use in product computation. kwargs : dict Keyword arguments passed through to `nengo_spa.Network`. Attributes ---------- input_a : nengo.Node First input. input_b : nengo.Node Second input. output : nengo.Node Output. """ n_neurons = IntParam('n_neurons', default=200, low=1, readonly=True) def __init__(self, n_neurons=Default, **kwargs): kwargs.setdefault('label', "Product") super(Product, self).__init__(**kwargs) self.n_neurons = n_neurons with self: self.product = nengo.networks.Product(self.n_neurons, 1) self.input_a = self.product.input_a self.input_b = self.product.input_b self.output = self.product.output self.declare_input(self.input_a, None) self.declare_input(self.input_b, None) self.declare_output(self.output, None)
class Convolution(Transform): """An N-dimensional convolutional transform. The dimensionality of the convolution is determined by the input shape. .. versionadded:: 3.0.0 Parameters ---------- n_filters : int The number of convolutional filters to apply input_shape : tuple of int or `.ChannelShape` Shape of the input signal to the convolution; e.g., ``(height, width, channels)`` for a 2D convolution with ``channels_last=True``. kernel_size : tuple of int, optional Size of the convolutional kernels (1 element for a 1D convolution, 2 for a 2D convolution, etc.). strides : tuple of int, optional Stride of the convolution (1 element for a 1D convolution, 2 for a 2D convolution, etc.). padding : ``"same"`` or ``"valid"``, optional Padding method for input signal. "Valid" means no padding, and convolution will only be applied to the fully-overlapping areas of the input signal (meaning the output will be smaller). "Same" means that the input signal is zero-padded so that the output is the same shape as the input. channels_last : bool, optional If ``True`` (default), the channels are the last dimension in the input signal (e.g., a 28x28 image with 3 channels would have shape ``(28, 28, 3)``). ``False`` means that channels are the first dimension (e.g., ``(3, 28, 28)``). init : `.Distribution` or `~numpy:numpy.ndarray`, optional A predefined kernel with shape ``kernel_size + (input_channels, n_filters)``, or a ``Distribution`` that will be used to initialize the kernel. Notes ----- As is typical in neural networks, this is technically correlation rather than convolution (because the kernel is not flipped). """ n_filters = IntParam("n_filters", low=1) input_shape = ChannelShapeParam("input_shape", low=1) kernel_size = ShapeParam("kernel_size", low=1) strides = ShapeParam("strides", low=1) padding = EnumParam("padding", values=("same", "valid")) channels_last = BoolParam("channels_last") init = DistOrArrayParam("init") _param_init_order = ["channels_last", "input_shape"] def __init__( self, n_filters, input_shape, kernel_size=(3, 3), strides=(1, 1), padding="valid", channels_last=True, init=Uniform(-1, 1), ): super().__init__() self.n_filters = n_filters self.channels_last = channels_last # must be set before input_shape self.input_shape = input_shape self.kernel_size = kernel_size self.strides = strides self.padding = padding self.init = init if len(kernel_size) != self.dimensions: raise ValidationError( "Kernel dimensions (%d) do not match input dimensions (%d)" % (len(kernel_size), self.dimensions), attr="kernel_size", ) if len(strides) != self.dimensions: raise ValidationError( "Stride dimensions (%d) do not match input dimensions (%d)" % (len(strides), self.dimensions), attr="strides", ) if not isinstance(init, Distribution): if init.shape != self.kernel_shape: raise ValidationError( "Kernel shape %s does not match expected shape %s" % (init.shape, self.kernel_shape), attr="init", ) @property def _argreprs(self): argreprs = [ "n_filters=%r" % (self.n_filters, ), "input_shape=%s" % (self.input_shape.shape, ), ] if self.kernel_size != (3, 3): argreprs.append("kernel_size=%r" % (self.kernel_size, )) if self.strides != (1, 1): argreprs.append("strides=%r" % (self.strides, )) if self.padding != "valid": argreprs.append("padding=%r" % (self.padding, )) if self.channels_last is not True: argreprs.append("channels_last=%r" % (self.channels_last, )) return argreprs def sample(self, rng=np.random): if isinstance(self.init, Distribution): # we sample this way so that any variancescaling distribution based # on n/d is scaled appropriately kernel = [ self.init.sample(self.input_shape.n_channels, self.n_filters, rng=rng) for _ in range(np.prod(self.kernel_size)) ] kernel = np.reshape(kernel, self.kernel_shape) else: kernel = np.array(self.init, dtype=rc.float_dtype) return kernel @property def kernel_shape(self): """Full shape of kernel.""" return self.kernel_size + (self.input_shape.n_channels, self.n_filters) @property def size_in(self): return self.input_shape.size @property def size_out(self): return self.output_shape.size @property def dimensions(self): """Dimensionality of convolution.""" return self.input_shape.dimensions @property def output_shape(self): """Output shape after applying convolution to input.""" output_shape = np.array(self.input_shape.spatial_shape, dtype=rc.float_dtype) if self.padding == "valid": output_shape -= self.kernel_size output_shape += 1 output_shape /= self.strides output_shape = tuple(np.ceil(output_shape).astype(rc.int_dtype)) output_shape = (output_shape + (self.n_filters, ) if self.channels_last else (self.n_filters, ) + output_shape) return ChannelShape(output_shape, channels_last=self.channels_last)
class Ensemble(NengoObject): """A group of neurons that collectively represent a vector. Parameters ---------- n_neurons : int The number of neurons. dimensions : int The number of representational dimensions. radius : int, optional The representational radius of the ensemble. encoders : Distribution or ndarray (`n_neurons`, `dimensions`), optional The encoders, used to transform from representational space to neuron space. Each row is a neuron's encoder, each column is a representational dimension. intercepts : Distribution or ndarray (`n_neurons`), optional The point along each neuron's encoder where its activity is zero. If e is the neuron's encoder, then the activity will be zero when dot(x, e) <= c, where c is the given intercept. max_rates : Distribution or ndarray (`n_neurons`), optional The activity of each neuron when dot(x, e) = 1, where e is the neuron's encoder. eval_points : Distribution or ndarray (`n_eval_points`, `dims`), optional The evaluation points used for decoder solving, spanning the interval (-radius, radius) in each dimension, or a distribution from which to choose evaluation points. Default: ``UniformHypersphere``. n_eval_points : int, optional The number of evaluation points to be drawn from the `eval_points` distribution. If None (the default), then a heuristic is used to determine the number of evaluation points. neuron_type : Neurons, optional The model that simulates all neurons in the ensemble. noise : StochasticProcess, optional Random noise injected directly into each neuron in the ensemble as current. A sample is drawn for each individual neuron on every simulation step. seed : int, optional The seed used for random number generation. label : str, optional A name for the ensemble. Used for debugging and visualization. """ n_neurons = IntParam(default=None, low=1) dimensions = IntParam(default=None, low=1) radius = NumberParam(default=1, low=1e-10) neuron_type = NeuronTypeParam(default=LIF()) encoders = DistributionParam(default=UniformHypersphere(surface=True), sample_shape=('n_neurons', 'dimensions')) intercepts = DistributionParam(default=Uniform(-1.0, 1.0), optional=True, sample_shape=('n_neurons', )) max_rates = DistributionParam(default=Uniform(200, 400), optional=True, sample_shape=('n_neurons', )) n_eval_points = IntParam(default=None, optional=True) eval_points = DistributionParam(default=UniformHypersphere(), sample_shape=('*', 'dimensions')) bias = DistributionParam(default=None, optional=True, sample_shape=('n_neurons', )) gain = DistributionParam(default=None, optional=True, sample_shape=('n_neurons', )) noise = StochasticProcessParam(default=None, optional=True) seed = IntParam(default=None, optional=True) label = StringParam(default=None, optional=True) def __init__(self, n_neurons, dimensions, radius=Default, encoders=Default, intercepts=Default, max_rates=Default, eval_points=Default, n_eval_points=Default, neuron_type=Default, gain=Default, bias=Default, noise=Default, seed=Default, label=Default): self.n_neurons = n_neurons self.dimensions = dimensions self.radius = radius self.encoders = encoders self.intercepts = intercepts self.max_rates = max_rates self.label = label self.n_eval_points = n_eval_points self.eval_points = eval_points self.bias = bias self.gain = gain self.neuron_type = neuron_type self.noise = noise self.seed = seed self._neurons = Neurons(self) def __getitem__(self, key): return ObjView(self, key) def __len__(self): return self.dimensions @property def neurons(self): return self._neurons @neurons.setter def neurons(self, dummy): raise AttributeError("neurons cannot be overwritten.") @property def probeable(self): return ["decoded_output", "input"] @property def size_in(self): return self.dimensions @property def size_out(self): return self.dimensions
class BasalGanglia(Network): """Winner take all network, typically used for action selection. The basal ganglia network outputs approximately 0 at the dimension with the largest value, and is negative elsewhere. While the basal ganglia is primarily defined by its winner-take-all function, it is also organized to match the organization of the human basal ganglia. It consists of five ensembles: * Striatal D1 dopamine-receptor neurons (*strD1*) * Striatal D2 dopamine-receptor neurons (*strD2*) * Subthalamic nucleus (*stn*) * Globus pallidus internus / substantia nigra reticulata (*gpi*) * Globus pallidus externus (*gpe*) Interconnections between these areas are also based on known neuroanatomical connections. See [1]_ for more details, and [2]_ for the original non-spiking basal ganglia model by Gurney, Prescott & Redgrave that this model is based on. .. note:: The default `nengo.solvers.Solver` for the basal ganglia is `nengo.solvers.NnlsL2nz`, which requires SciPy. If SciPy is not installed, the global default solver will be used instead. Parameters ---------- action_count : int Number of actions. n_neuron_per_ensemble : int, optional Number of neurons in each ensemble in the network. output_weight : float, optional A scaling factor on the output of the basal ganglia (specifically on the connection out of the GPi). input_bias : float, optional An amount by which to bias all dimensions of the input node. Biasing the input node is important for ensuring that all input dimensions are positive and easily comparable. ampa_synapse : Synapse, optional Synapse for connections corresponding to biological connections to AMPA receptors (i.e., connections from STN to to GPi and GPe). gaba_synapse : Synapse, optional Synapse for connections corresponding to biological connections to GABA receptors (i.e., connections from StrD1 to GPi, StrD2 to GPe, and GPe to GPi and STN). kwargs Passed through the `nengo_spa.Network`. Attributes ---------- bias_input : nengo.Node or None If *input_bias* is non-zero, this node will be created to bias all of the dimensions of the input signal. gpe : nengo.networks.EnsembleArray Globus pallidus externus ensembles. gpi : nengo.networks.EnsembleArray Globus pallidus internus ensembles. input : nengo.Node Accepts the input signal. output : nengo.Node Provides the output signal. stn : nengo.networks.EnsembleArray Subthalamic nucleus ensembles. strD1 : nengo.networks.EnsembleArray Striatal D1 ensembles. strD2 : nengo.networks.EnsembleArray Striatal D2 ensembles. References ---------- .. [1] Stewart, T. C., Choo, X., & Eliasmith, C. (2010). Dynamic behaviour of a spiking model of action selection in the basal ganglia. In Proceedings of the 10th international conference on cognitive modeling (pp. 235-40). .. [2] Gurney, K., Prescott, T., & Redgrave, P. (2001). A computational model of action selection in the basal ganglia. Biological Cybernetics 84, 401-423. """ input_synapse = SynapseParam('input_synapse', default=Lowpass(0.002)) ampa_synapse = SynapseParam('ampa_synapse', default=Lowpass(0.002)) gaba_synapse = SynapseParam('gaba_synapse', default=Lowpass(0.008)) n_neurons_per_ensemble = IntParam('n_neurons_per_ensemble', default=100, low=1, readonly=True) output_weight = NumberParam('output_weight', default=-3., readonly=True) input_bias = NumberParam('input_bias', default=0., readonly=True) def __init__(self, action_count, n_neurons_per_ensemble=Default, output_weight=Default, input_bias=Default, ampa_synapse=Default, gaba_synapse=Default, sBCBG_params=None, **kwargs): kwargs.setdefault('label', "Basal ganglia") super(BasalGanglia, self).__init__(**kwargs) self.action_count = action_count self.input_connections = {} self.input_bias = input_bias if BasalGanglia.sBCBG: # parameters filter_tau = .01 if output_weight == Default: self.output_weight = -0.001 import sBCBG if sBCBG_params == None: sBCBG_params = {} sBCBG.nengo_instantiate( self.action_count, self, sBCBG_params if sBCBG_params != None else {}) with self: # connect input to CSN self.input = nengo.Node(label="input", size_in=self.action_count) scale = nengo.Ensemble(100, self.input.size_out) nengo.Connection(self.input, scale) for d in range(self.action_count): nengo.Connection(scale[d], self.pops['CSN'][d], function=lambda x: 10 * x, synapse=.01, label='CSN input') # add bias input (BG performs best in the range 0.5--1.5) if abs(self.input_bias) > 0.0: self.bias_input = nengo.Node(np.ones(self.action_count) * input_bias, label="basal ganglia bias") nengo.Connection(self.bias_input, self.input) # connect GPi to output (inhibitory) decoding_weight = 1 # scaling of decoding GPi->out self.output = nengo.Node(label="output", size_in=self.action_count) for d in range(self.action_count): GPi_ens = self.pops["GPi"][d] decoder_values = np.ones( (GPi_ens.n_neurons, 1)) * decoding_weight nengo.Connection( GPi_ens, self.output[d], synapse=nengo.synapses.Lowpass(filter_tau), transform=self.output_weight, #eval_points=eval_points) solver=nengo.solvers.NoSolver(decoder_values)) else: self.n_neurons_per_ensemble = n_neurons_per_ensemble self.ampa_synapse = ampa_synapse self.gaba_synapse = gaba_synapse self.output_weight = output_weight # Affects all ensembles / connections in the BG # unless overwritten with general_config config = nengo.Config(nengo.Ensemble, nengo.Connection) config[nengo.Ensemble].radius = 1.5 config[nengo.Ensemble].encoders = nengo.dists.Choice([[1]]) try: # Best, if we have SciPy config[nengo.Connection].solver = nengo.solvers.NnlsL2nz() except ImportError: warnings.warn("SciPy is not installed, so BasalGanglia will " "use the default decoder solver. Installing " "SciPy may improve BasalGanglia performance.") ea_params = { 'n_neurons': self.n_neurons_per_ensemble, 'n_ensembles': self.action_count } with self, config: self.strD1 = EnsembleArray(label="Striatal D1 neurons", intercepts=nengo.dists.Uniform( Weights.e, 1), **ea_params) self.strD2 = EnsembleArray(label="Striatal D2 neurons", intercepts=nengo.dists.Uniform( Weights.e, 1), **ea_params) self.stn = EnsembleArray(label="Subthalamic nucleus", intercepts=nengo.dists.Uniform( Weights.ep, 1), **ea_params) self.gpi = EnsembleArray(label="Globus pallidus internus", intercepts=nengo.dists.Uniform( Weights.eg, 1), **ea_params) self.gpe = EnsembleArray(label="Globus pallidus externus", intercepts=nengo.dists.Uniform( Weights.ee, 1), **ea_params) self.input = nengo.Node(label="input", size_in=self.action_count) self.output = nengo.Node(label="output", size_in=self.action_count) # add bias input (BG performs best in the range 0.5--1.5) if abs(self.input_bias) > 0.0: self.bias_input = nengo.Node(np.ones(self.action_count) * self.input_bias, label="basal ganglia bias") nengo.Connection(self.bias_input, self.input) # spread the input to StrD1, StrD2, and STN nengo.Connection(self.input, self.strD1.input, synapse=None, transform=Weights.ws * (1 + Weights.lg)) nengo.Connection(self.input, self.strD2.input, synapse=None, transform=Weights.ws * (1 - Weights.le)) nengo.Connection(self.input, self.stn.input, synapse=None, transform=Weights.wt) # connect the striatum to the GPi and GPe (inhibitory) strD1_output = self.strD1.add_output('func_str', Weights.str_func) strD2_output = self.strD2.add_output('func_str', Weights.str_func) self.gaba = nengo.Network("GABAergic connections") self.gaba.config[nengo.Connection].synapse = self.gaba_synapse with self.gaba: nengo.Connection(strD1_output, self.gpi.input, transform=-Weights.wm) nengo.Connection(strD2_output, self.gpe.input, transform=-Weights.wm) # connect the STN to GPi and GPe (broad and excitatory) tr = Weights.wp * np.ones( (self.action_count, self.action_count)) stn_output = self.stn.add_output('func_stn', Weights.stn_func) self.ampa = nengo.Network("AMPAergic connectiions") self.ampa.config[nengo.Connection].synapse = self.ampa_synapse with self.ampa: nengo.Connection(stn_output, self.gpi.input, transform=tr) nengo.Connection(stn_output, self.gpe.input, transform=tr) # connect the GPe to GPi and STN (inhibitory) gpe_output = self.gpe.add_output('func_gpe', Weights.gpe_func) with self.gaba: nengo.Connection(gpe_output, self.gpi.input, transform=-Weights.we) nengo.Connection(gpe_output, self.stn.input, transform=-Weights.wg) # connect GPi to output (inhibitory) gpi_output = self.gpi.add_output('func_gpi', Weights.gpi_func) nengo.Connection(gpi_output, self.output, synapse=None, transform=self.output_weight) def connect_input(self, source, transform=Default, index=None): self.input_connections[index] = nengo.Connection( source, self.input[index], transform=transform, synapse=self.input_synapse)
class Conjgrad(LeastSquaresSolver): """Solve a least-squares system using conjugate gradient.""" tol = NumberParam('tol', low=0) maxiters = IntParam('maxiters', low=1, optional=True) X0 = NdarrayParam('X0', shape=('*', '*'), optional=True) def __init__(self, tol=1e-2, maxiters=None, X0=None): super(Conjgrad, self).__init__() self.tol = tol self.maxiters = maxiters self.X0 = X0 def __call__(self, A, Y, sigma, rng=None): Y, m, n, d, matrix_in = format_system(A, Y) X = np.zeros((n, d)) if self.X0 is None else np.array(self.X0) if X.shape != (n, d): raise ValidationError("Must be shape %s, got %s" % ((n, d), X.shape), attr='X0', obj=self) damp = m * sigma**2 rtol = self.tol * np.sqrt(m) G = lambda x: np.dot(A.T, np.dot(A, x)) + damp * x B = np.dot(A.T, Y) iters = -np.ones(d, dtype='int') for i in range(d): X[:, i], iters[i] = self._conjgrad_iters(G, B[:, i], X[:, i], maxiters=self.maxiters, rtol=rtol) info = {'rmses': rmses(A, X, Y), 'iterations': iters} return X if matrix_in else X.ravel(), info @staticmethod def _conjgrad_iters(calcAx, b, x, maxiters=None, rtol=1e-6): """Solve a single-RHS linear system using conjugate gradient.""" if maxiters is None: maxiters = b.shape[0] r = b - calcAx(x) p = r.copy() rsold = np.dot(r, r) for i in range(maxiters): Ap = calcAx(p) alpha = rsold / np.dot(p, Ap) x += alpha * p r -= alpha * Ap rsnew = np.dot(r, r) beta = rsnew / rsold if np.sqrt(rsnew) < rtol: break if beta < 1e-12: # no perceptible change in p break # p = r + beta*p p *= beta p += r rsold = rsnew return x, i + 1
class Node(NengoObject): """Provide non-neural inputs to Nengo objects and process outputs. Nodes can accept input, and perform arbitrary computations for the purpose of controlling a Nengo simulation. Nodes are typically not part of a brain model per se, but serve to summarize the assumptions being made about sensory data or other environment variables that cannot be generated by a brain model alone. Nodes can also be used to test models by providing specific input signals to parts of the model, and can simplify the input/output interface of a `~nengo.Network` when used as a relay to/from its internal ensembles (see `~nengo.networks.EnsembleArray` for an example). Parameters ---------- output : callable, array_like, or None Function that transforms the Node inputs into outputs, a constant output value, or None to transmit signals unchanged. size_in : int, optional The number of dimensions of the input data parameter. size_out : int, optional The size of the output signal. If None, it will be determined based on the values of ``output`` and ``size_in``. label : str, optional A name for the node. Used for debugging and visualization. seed : int, optional The seed used for random number generation. Note: no aspects of the node are random, so currently setting this seed has no effect. Attributes ---------- label : str The name of the node. output : callable, array_like, or None The given output. size_in : int The number of dimensions for incoming connection. size_out : int The number of output dimensions. """ probeable = ('output', ) output = OutputParam('output', default=None) size_in = IntParam('size_in', default=None, low=0, optional=True) size_out = IntParam('size_out', default=None, low=0, optional=True) def __init__(self, output=Default, size_in=Default, size_out=Default, label=Default, seed=Default): if not (seed is Default or seed is None): raise NotImplementedError( "Changing the seed of a node has no effect") super().__init__(label=label, seed=seed) self.size_in = size_in self.size_out = size_out self.output = output # Must be set after size_out; may modify size_out def __getitem__(self, key): return ObjView(self, key) def __len__(self): return self.size_out
class SqrtBeta(Distribution): """Distribution of the square root of a Beta distributed random variable. Given ``n + m`` dimensional random unit vectors, the length of subvectors with ``m`` elements will be distributed according to this distribution. Parameters ---------- n: int Number of subvectors. m: int, optional Length of each subvector. See also -------- nengo.dists.SubvectorLength """ n = IntParam("n", low=0) m = IntParam("m", low=0) def __init__(self, n, m=1): super().__init__() self.n = n self.m = m def sample(self, num, d=None, rng=np.random): shape = self._sample_shape(num, d) return np.sqrt(rng.beta(self.m / 2.0, self.n / 2.0, size=shape)) def cdf(self, x): """Cumulative distribution function. .. note:: Requires SciPy. Parameters ---------- x : array_like Evaluation points in [0, 1]. Returns ------- cdf : array_like Probability that ``X <= x``. """ from scipy.special import betainc # pylint: disable=import-outside-toplevel sq_x = x * x return np.where(sq_x < 1.0, betainc(self.m / 2.0, self.n / 2.0, sq_x), np.ones_like(x)) def pdf(self, x): """Probability distribution function. .. note:: Requires SciPy. Parameters ---------- x : array_like Evaluation points in [0, 1]. Returns ------- pdf : array_like Probability density at ``x``. """ from scipy.special import beta # pylint: disable=import-outside-toplevel return (2 / beta(0.5 * self.m, 0.5 * self.n) * x**(self.m - 1) * (1 - x * x)**(0.5 * self.n - 1)) def ppf(self, y): """Percent point function (inverse cumulative distribution). .. note:: Requires SciPy. Parameters ---------- y : array_like Cumulative probabilities in [0, 1]. Returns ------- ppf : array_like Evaluation points ``x`` in [0, 1] such that ``P(X <= x) = y``. """ from scipy.special import betaincinv # pylint: disable=import-outside-toplevel sq_x = betaincinv(self.m / 2.0, self.n / 2.0, y) return np.sqrt(sq_x)
class Ensemble(NengoObject): """A group of neurons that collectively represent a vector. Parameters ---------- n_neurons : int The number of neurons. dimensions : int The number of representational dimensions. radius : int, optional (Default: 1.0) The representational radius of the ensemble. encoders : Distribution or (n_neurons, dimensions) array_like, optional \ (Default: UniformHypersphere(surface=True)) The encoders used to transform from representational space to neuron space. Each row is a neuron's encoder; each column is a representational dimension. intercepts : Distribution or (n_neurons,) array_like, optional \ (Default: ``nengo.dists.Uniform(-1.0, 1.0)``) The point along each neuron's encoder where its activity is zero. If ``e`` is the neuron's encoder, then the activity will be zero when ``dot(x, e) <= c``, where ``c`` is the given intercept. max_rates : Distribution or (n_neurons,) array_like, optional \ (Default: ``nengo.dists.Uniform(200, 400)``) The activity of each neuron when the input signal ``x`` is magnitude 1 and aligned with that neuron's encoder ``e``; i.e., when ``dot(x, e) = 1``. eval_points : Distribution or (n_eval_points, dims) array_like, optional \ (Default: ``nengo.dists.UniformHypersphere()``) The evaluation points used for decoder solving, spanning the interval (-radius, radius) in each dimension, or a distribution from which to choose evaluation points. n_eval_points : int, optional (Default: None) The number of evaluation points to be drawn from the ``eval_points`` distribution. If None, then a heuristic is used to determine the number of evaluation points. neuron_type : `~nengo.neurons.NeuronType`, optional \ (Default: ``nengo.LIF()``) The model that simulates all neurons in the ensemble (see `~nengo.neurons.NeuronType`). gain : Distribution or (n_neurons,) array_like (Default: None) The gains associated with each neuron in the ensemble. If None, then the gain will be solved for using ``max_rates`` and ``intercepts``. bias : Distribution or (n_neurons,) array_like (Default: None) The biases associated with each neuron in the ensemble. If None, then the gain will be solved for using ``max_rates`` and ``intercepts``. noise : Process, optional (Default: None) Random noise injected directly into each neuron in the ensemble as current. A sample is drawn for each individual neuron on every simulation step. normalize_encoders : bool, optional (Default: True) Indicates whether the encoders should be normalized. label : str, optional (Default: None) A name for the ensemble. Used for debugging and visualization. seed : int, optional (Default: None) The seed used for random number generation. Attributes ---------- bias : Distribution or (n_neurons,) array_like or None The biases associated with each neuron in the ensemble. dimensions : int The number of representational dimensions. encoders : Distribution or (n_neurons, dimensions) array_like The encoders, used to transform from representational space to neuron space. Each row is a neuron's encoder, each column is a representational dimension. eval_points : Distribution or (n_eval_points, dims) array_like The evaluation points used for decoder solving, spanning the interval (-radius, radius) in each dimension, or a distribution from which to choose evaluation points. gain : Distribution or (n_neurons,) array_like or None The gains associated with each neuron in the ensemble. intercepts : Distribution or (n_neurons) array_like or None The point along each neuron's encoder where its activity is zero. If ``e`` is the neuron's encoder, then the activity will be zero when ``dot(x, e) <= c``, where ``c`` is the given intercept. label : str or None A name for the ensemble. Used for debugging and visualization. max_rates : Distribution or (n_neurons,) array_like or None The activity of each neuron when ``dot(x, e) = 1``, where ``e`` is the neuron's encoder. n_eval_points : int or None The number of evaluation points to be drawn from the ``eval_points`` distribution. If None, then a heuristic is used to determine the number of evaluation points. n_neurons : int or None The number of neurons. neuron_type : NeuronType The model that simulates all neurons in the ensemble (see ``nengo.neurons``). noise : Process or None Random noise injected directly into each neuron in the ensemble as current. A sample is drawn for each individual neuron on every simulation step. radius : int The representational radius of the ensemble. seed : int or None The seed used for random number generation. """ probeable = ('decoded_output', 'input', 'scaled_encoders') n_neurons = IntParam('n_neurons', low=1) dimensions = IntParam('dimensions', low=1) radius = NumberParam('radius', default=1.0, low=1e-10) encoders = DistOrArrayParam('encoders', default=UniformHypersphere(surface=True), sample_shape=('n_neurons', 'dimensions')) intercepts = DistOrArrayParam('intercepts', default=Uniform(-1.0, 1.0), optional=True, sample_shape=('n_neurons', )) max_rates = DistOrArrayParam('max_rates', default=Uniform(200, 400), optional=True, sample_shape=('n_neurons', )) eval_points = DistOrArrayParam('eval_points', default=UniformHypersphere(), sample_shape=('*', 'dimensions')) n_eval_points = IntParam('n_eval_points', default=None, optional=True) neuron_type = NeuronTypeParam('neuron_type', default=LIF()) gain = DistOrArrayParam('gain', default=None, optional=True, sample_shape=('n_neurons', )) bias = DistOrArrayParam('bias', default=None, optional=True, sample_shape=('n_neurons', )) noise = ProcessParam('noise', default=None, optional=True) normalize_encoders = BoolParam('normalize_encoders', default=True, optional=True) def __init__(self, n_neurons, dimensions, radius=Default, encoders=Default, intercepts=Default, max_rates=Default, eval_points=Default, n_eval_points=Default, neuron_type=Default, gain=Default, bias=Default, noise=Default, normalize_encoders=Default, label=Default, seed=Default): super(Ensemble, self).__init__(label=label, seed=seed) self.n_neurons = n_neurons self.dimensions = dimensions self.radius = radius self.encoders = encoders self.intercepts = intercepts self.max_rates = max_rates self.n_eval_points = n_eval_points self.eval_points = eval_points self.bias = bias self.gain = gain self.neuron_type = neuron_type self.noise = noise self.normalize_encoders = normalize_encoders def __getitem__(self, key): return ObjView(self, key) def __len__(self): return self.dimensions @property def neurons(self): """A direct interface to the neurons in the ensemble.""" return Neurons(self) @neurons.setter def neurons(self, dummy): raise ReadonlyError(attr="neurons", obj=self) @property def size_in(self): """The dimensionality of the ensemble.""" return self.dimensions @property def size_out(self): """The dimensionality of the ensemble.""" return self.dimensions
class NengoObject(with_metaclass(NetworkMember)): """A base class for Nengo objects. Parameters ---------- label : string A descriptive label for the object. seed : int The seed used for random number generation. Attributes ---------- label : string A descriptive label for the object. seed : int The seed used for random number generation. """ label = StringParam('label', default=None, optional=True) seed = IntParam('seed', default=None, optional=True) def __init__(self, label, seed): self.label = label self.seed = seed def __getstate__(self): raise NotImplementedError("Nengo objects do not support pickling") def __setstate__(self, state): raise NotImplementedError("Nengo objects do not support pickling") def __setattr__(self, name, val): if hasattr(self, '_initialized') and not hasattr(self, name): warnings.warn( "Creating new attribute '%s' on '%s'. " "Did you mean to change an existing attribute?" % (name, self), SyntaxWarning) if val is Default: val = Config.default(type(self), name) if rc.getboolean('exceptions', 'simplified'): try: super(NengoObject, self).__setattr__(name, val) except ValidationError: exc_info = sys.exc_info() reraise(exc_info[0], exc_info[1], None) else: super(NengoObject, self).__setattr__(name, val) def __str__(self): return self._str( include_id=not hasattr(self, 'label') or self.label is None) def __repr__(self): return self._str(include_id=True) def _str(self, include_id): return "<%s%s%s>" % ( self.__class__.__name__, "" if not hasattr(self, 'label') else " (unlabeled)" if self.label is None else ' "%s"' % self.label, " at 0x%x" % id(self) if include_id else "") @classmethod def param_list(cls): """Returns a list of parameter names that can be set.""" return (attr for attr in dir(cls) if is_param(getattr(cls, attr))) @property def params(self): """Returns a list of parameter names that can be set.""" return self.param_list()
class Process(FrozenObject): """A general system with input, output, and state. For more details on how to use processes and make custom process subclasses, see :doc:`examples/processes`. Parameters ---------- default_size_in : int (Default: 0) Sets the default size in for nodes using this process. default_size_out : int (Default: 1) Sets the default size out for nodes running this process. Also, if ``d`` is not specified in `~.Process.run` or `~.Process.run_steps`, this will be used. default_dt : float (Default: 0.001 (1 millisecond)) If ``dt`` is not specified in `~.Process.run`, `~.Process.run_steps`, `~.Process.ntrange`, or `~.Process.trange`, this will be used. seed : int, optional (Default: None) Random number seed. Ensures random factors will be the same each run. Attributes ---------- default_dt : float If ``dt`` is not specified in `~.Process.run`, `~.Process.run_steps`, `~.Process.ntrange`, or `~.Process.trange`, this will be used. default_size_in : int The default size in for nodes using this process. default_size_out : int The default size out for nodes running this process. Also, if ``d`` is not specified in `~.Process.run` or `~.Process.run_steps`, this will be used. seed : int or None Random number seed. Ensures random factors will be the same each run. """ default_size_in = IntParam('default_size_in', low=0) default_size_out = IntParam('default_size_out', low=0) default_dt = NumberParam('default_dt', low=0, low_open=True) seed = IntParam('seed', low=0, high=maxint, optional=True) def __init__(self, default_size_in=0, default_size_out=1, default_dt=0.001, seed=None): super(Process, self).__init__() self.default_size_in = default_size_in self.default_size_out = default_size_out self.default_dt = default_dt self.seed = seed def apply(self, x, d=None, dt=None, rng=np.random, copy=True, **kwargs): """Run process on a given input. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- x : ndarray The input signal given to the process. d : int, optional (Default: None) Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional (Default: None) Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.RandomState` (Default: ``numpy.random``) Random number generator used for stochstic processes. copy : bool, optional (Default: True) If True, a new output array will be created for output. If False, the input signal ``x`` will be overwritten. """ shape_in = as_shape(np.asarray(x[0]).shape, min_dim=1) shape_out = as_shape(self.default_size_out if d is None else d) dt = self.default_dt if dt is None else dt rng = self.get_rng(rng) step = self.make_step(shape_in, shape_out, dt, rng, **kwargs) output = np.zeros((len(x), ) + shape_out) if copy else x for i, xi in enumerate(x): output[i] = step((i + 1) * dt, xi) return output def get_rng(self, rng): """Get a properly seeded independent RNG for the process step. Parameters ---------- rng : `numpy.random.RandomState` The parent random number generator to use if the seed is not set. """ seed = rng.randint(maxint) if self.seed is None else self.seed return np.random.RandomState(seed) def make_step(self, shape_in, shape_out, dt, rng): """Create function that advances the process forward one time step. This must be implemented by all custom processes. Parameters ---------- shape_in : tuple The shape of the input signal. shape_out : tuple The shape of the output signal. dt : float The simulation timestep. rng : `numpy.random.RandomState` A random number generator. """ raise NotImplementedError("Process must implement `make_step` method.") def run(self, t, d=None, dt=None, rng=np.random, **kwargs): """Run process without input for given length of time. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- t : float The length of time to run. d : int, optional (Default: None) Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional (Default: None) Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.RandomState` (Default: ``numpy.random``) Random number generator used for stochstic processes. """ dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.run_steps(n_steps, d=d, dt=dt, rng=rng, **kwargs) def run_steps(self, n_steps, d=None, dt=None, rng=np.random, **kwargs): """Run process without input for given number of steps. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- n_steps : int The number of steps to run. d : int, optional (Default: None) Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional (Default: None) Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.RandomState` (Default: ``numpy.random``) Random number generator used for stochstic processes. """ shape_in = as_shape(0) shape_out = as_shape(self.default_size_out if d is None else d) dt = self.default_dt if dt is None else dt rng = self.get_rng(rng) step = self.make_step(shape_in, shape_out, dt, rng, **kwargs) output = np.zeros((n_steps, ) + shape_out) for i in range(n_steps): output[i] = step((i + 1) * dt) return output def ntrange(self, n_steps, dt=None): """Create time points corresponding to a given number of steps. Parameters ---------- n_steps : int The given number of steps. dt : float, optional (Default: None) Simulation timestep. If None, ``default_dt`` will be used. """ dt = self.default_dt if dt is None else dt return dt * np.arange(1, n_steps + 1) def trange(self, t, dt=None): """Create time points corresponding to a given length of time. Parameters ---------- t : float The given length of time. dt : float, optional (Default: None) Simulation timestep. If None, ``default_dt`` will be used. """ dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.ntrange(n_steps, dt=dt)
class TensorNode(Node): """Inserts TensorFlow code into a Nengo model. A TensorNode operates in much the same way as a :class:`~nengo:nengo.Node`, except its inputs and outputs are defined using TensorFlow operations. The TensorFlow code is defined in a function or callable class (``tensor_func``). This function accepts the current simulation time as input, or the current simulation time and a Tensor ``x`` if ``node.size_in > 0``. ``x`` will have shape ``(sim.minibatch_size, node.size_in``), and the function should return a Tensor with shape ``(sim.minibatch_size, node.size_out)``. ``node.size_out`` will be inferred by calling the function once and checking the output, if it isn't set when the Node is created. If ``tensor_func`` has a ``pre_build`` attribute, that function will be called once when the model is constructed. This can be used to compute any constant values or set up variables -- things that don't need to execute every simulation timestep. .. code-block:: python def pre_build(shape_in, shape_out): print(shape_in) # (minibatch_size, node.size_in) print(shape_out) # (minibatch_size, node.size_out) If ``tensor_func`` has a ``post_build`` attribute, that function will be called after the simulator is created and whenever it is reset. This can be used to set any random elements in the TensorNode or perform any post-initialization setup required by the node (e.g., loading pretrained weights). .. code-block:: python def post_build(sess, rng): print(sess) # the TensorFlow simulation session object print(rng) # random number generator (np.random.RandomState) Parameters ---------- tensor_func : callable A function that maps node inputs to outputs size_in : int, optional (Default: 0) The number of elements in the input vector size_out : int, optional (Default: None) The number of elements in the output vector (if None, value will be inferred by calling ``tensor_func``) label : str, optional (Default: None) A name for the node, used for debugging and visualization """ tensor_func = TensorFuncParam('tensor_func') size_in = IntParam('size_in', default=0, low=0, optional=True) size_out = IntParam('size_out', default=None, low=1, optional=True) def __init__(self, tensor_func, size_in=Default, size_out=Default, label=Default): # pylint: disable=non-parent-init-called,super-init-not-called # note: we bypass the Node constructor, because we don't want to # perform validation on `output` NengoObject.__init__(self, label=label, seed=None) self.size_in = size_in self.size_out = size_out self.tensor_func = tensor_func @property def output(self): raise BuildError( "TensorNode does not have an `output` attribute (this probably " "means you are trying to use a TensorNode inside a Simulator " "other than Nengo DL)")
class Process(FrozenObject): """A general system with input, output, and state. For more details on how to use processes and make custom process subclasses, see :doc:`examples/advanced/processes`. Parameters ---------- default_size_in : int Sets the default size in for nodes using this process. default_size_out : int Sets the default size out for nodes running this process. Also, if ``d`` is not specified in `~.Process.run` or `~.Process.run_steps`, this will be used. default_dt : float If ``dt`` is not specified in `~.Process.run`, `~.Process.run_steps`, `~.Process.ntrange`, or `~.Process.trange`, this will be used. seed : int, optional Random number seed. Ensures random factors will be the same each run. Attributes ---------- default_dt : float If ``dt`` is not specified in `~.Process.run`, `~.Process.run_steps`, `~.Process.ntrange`, or `~.Process.trange`, this will be used. default_size_in : int The default size in for nodes using this process. default_size_out : int The default size out for nodes running this process. Also, if ``d`` is not specified in `~.Process.run` or `~.Process.run_steps`, this will be used. seed : int or None Random number seed. Ensures random factors will be the same each run. """ default_size_in = IntParam("default_size_in", low=0) default_size_out = IntParam("default_size_out", low=0) default_dt = NumberParam("default_dt", low=0, low_open=True) seed = IntParam("seed", low=0, high=maxint, optional=True) def __init__(self, default_size_in=0, default_size_out=1, default_dt=0.001, seed=None): super().__init__() self.default_size_in = default_size_in self.default_size_out = default_size_out self.default_dt = default_dt self.seed = seed def apply(self, x, d=None, dt=None, rng=np.random, copy=True, **kwargs): """Run process on a given input. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- x : ndarray The input signal given to the process. d : int, optional Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.mtrand.RandomState` Random number generator used for stochstic processes. copy : bool, optional If True, a new output array will be created for output. If False, the input signal ``x`` will be overwritten. """ shape_in = as_shape(np.asarray(x[0]).shape, min_dim=1) shape_out = as_shape(self.default_size_out if d is None else d) dt = self.default_dt if dt is None else dt rng = self.get_rng(rng) state = self.make_state(shape_in, shape_out, dt) step = self.make_step(shape_in, shape_out, dt, rng, state, **kwargs) output = np.zeros((len(x), ) + shape_out) if copy else x for i, xi in enumerate(x): output[i] = step((i + 1) * dt, xi) return output def get_rng(self, rng): """Get a properly seeded independent RNG for the process step. Parameters ---------- rng : `numpy.random.mtrand.RandomState` The parent random number generator to use if the seed is not set. """ seed = rng.randint(maxint) if self.seed is None else self.seed return np.random.RandomState(seed) def make_state(self, shape_in, shape_out, dt, dtype=None): """Get a dictionary of signals to represent the state of this process. The builder uses this to allocate memory for the process state, so that the state can be represented as part of the whole simulator state. Parameters ---------- shape_in : tuple The shape of the input signal. shape_out : tuple The shape of the output signal. dt : float The simulation timestep. dtype : `numpy.dtype` The data type requested by the builder. If `None`, then this function is free to choose the best type for the signals involved. Returns ------- initial_state : {string: `numpy.ndarray`} A dictionary mapping keys to arrays containing the initial state values. The keys will be used to identify the signals in `.Process.make_step`. """ return {} # Implement if the process has a state def make_step(self, shape_in, shape_out, dt, rng, state): """Create function that advances the process forward one time step. This must be implemented by all custom processes. The parameters below indicate what information is provided by the builder. Parameters ---------- shape_in : tuple The shape of the input signal. shape_out : tuple The shape of the output signal. dt : float The simulation timestep. rng : `numpy.random.mtrand.RandomState` A random number generator. state : {string: `numpy.ndarray`} A dictionary mapping keys to signals, where the signals fully represent the state of the process. The signals are initialized by `.Process.make_state`. """ raise NotImplementedError("Process must implement `make_step` method.") def run(self, t, d=None, dt=None, rng=np.random, **kwargs): """Run process without input for given length of time. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- t : float The length of time to run. d : int, optional Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.mtrand.RandomState` Random number generator used for stochstic processes. """ dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.run_steps(n_steps, d=d, dt=dt, rng=rng, **kwargs) def run_steps(self, n_steps, d=None, dt=None, rng=np.random, **kwargs): """Run process without input for given number of steps. Keyword arguments that do not appear in the parameter list below will be passed to the ``make_step`` function of this process. Parameters ---------- n_steps : int The number of steps to run. d : int, optional Output dimensionality. If None, ``default_size_out`` will be used. dt : float, optional Simulation timestep. If None, ``default_dt`` will be used. rng : `numpy.random.mtrand.RandomState` Random number generator used for stochstic processes. """ shape_in = as_shape(0) shape_out = as_shape(self.default_size_out if d is None else d) dt = self.default_dt if dt is None else dt rng = self.get_rng(rng) state = self.make_state(shape_in, shape_out, dt) step = self.make_step(shape_in, shape_out, dt, rng, state, **kwargs) output = np.zeros((n_steps, ) + shape_out) for i in range(n_steps): output[i] = step((i + 1) * dt) return output def ntrange(self, n_steps, dt=None): """Create time points corresponding to a given number of steps. Parameters ---------- n_steps : int The given number of steps. dt : float, optional Simulation timestep. If None, ``default_dt`` will be used. """ dt = self.default_dt if dt is None else dt return dt * np.arange(1, n_steps + 1) def trange(self, t, dt=None): """Create time points corresponding to a given length of time. Parameters ---------- t : float The given length of time. dt : float, optional Simulation timestep. If None, ``default_dt`` will be used. """ dt = self.default_dt if dt is None else dt n_steps = int(np.round(float(t) / dt)) return self.ntrange(n_steps, dt=dt)
class Transcode(Network): """Transcode from, to, and between Semantic Pointers. This can thought of the equivalent of a `nengo.Node` for Semantic Pointers. Either the *input_vocab* or the *output_vocab* argument must not be *None*. (If you want both arguments to be *None*, use a normal `nengo.Node`.) Which one of the parameters in the pairs *input_vocab/size_in* and *output_vocab/size_out* is not set to *None*, determines whether a Semantic Pointer input/output or a normal vector input/output is expected. Parameters ---------- function : func, optional (Default: None) Function that transforms the input Semantic Pointer to an output Semantic Pointer. The function signature depends on *input_vocab*: * If *input_vocab* is *None*, the allowed signatures are the same as for a `nengo.Node`. Either ``function(t)`` or ``function(t, x)`` where *t* (float) is the current simulation time and *x* (NumPy array) is the current input to transcode with size *size_in*. * If *input_vocab* is not *None*, the signature has to be ``function(t, sp)`` where *t* (float) is the current simulation time and *sp* (`.SemanticPointer`) is the current Semantic Pointer input. The associated vocabulary can be obtained via ``sp.vocab``. The allowed function return value depends on *output_vocab*: * If *output_vocab* is *None*, the return value must be a NumPy array (or equivalent) of size *size_out* or *None* (i.e. no return value) if *size_out* is *None*. * If *output_vocab* is not *None*, the return value can be either of: NumPy array, `.SemanticPointer` instance, or an SemanticPointer expression or symbolic expression as string that gets parsed with the *output_vocab*. input_vocab : Vocabulary, optional (Default: None) Input vocabulary. Mutually exclusive with *size_in*. output_vocab : Vocabulary, optional (Default: None) Output vocabulary. Mutually exclusive with *size_out*. size_in : int, optional (Default: None) Input size. Mutually exclusive with *input_vocab*. size_out : int, optional (Default: None) Output size. Mutually exclusive with *output_vocab*. **kwargs : dict Additional keyword arguments passed to `nengo_spa.Network`. Attributes ---------- input : nengo.Node Input. output : nengo.Node Output. """ function = TranscodeFunctionParam( "function", optional=True, default=None, readonly=True ) input_vocab = VocabularyOrDimParam( "input_vocab", optional=True, default=None, readonly=True ) output_vocab = VocabularyOrDimParam( "output_vocab", optional=True, default=None, readonly=True ) size_in = IntParam("size_in", optional=True, default=None, readonly=True) size_out = IntParam("size_out", optional=True, default=None, readonly=True) def __init__( self, function=Default, input_vocab=Default, output_vocab=Default, size_in=Default, size_out=Default, **kwargs ): super(Transcode, self).__init__(**kwargs) # Vocabs need to be set before function which accesses vocab for # validation. self.input_vocab = input_vocab self.output_vocab = output_vocab self.size_in = size_in self.size_out = size_out if self.input_vocab is None and self.output_vocab is None: raise ValidationError( "At least one of input_vocab and output_vocab needs to be " "set. If neither the input nor the output is a Semantic " "Pointer, use a basic nengo.Node instead.", self, ) if self.input_vocab is not None and self.size_in is not None: raise ValidationError( "The input_vocab and size_in arguments are mutually " "exclusive.", "size_in", self, ) if self.output_vocab is not None and self.size_out is not None: raise ValidationError( "The output_vocab and size_out arguments are mutually " "exclusive.", "size_in", self, ) self.function = function node_size_in = ( self.input_vocab.dimensions if self.input_vocab is not None else self.size_in ) node_size_out = ( self.output_vocab.dimensions if self.output_vocab is not None else self.size_out ) if self.function is None: if node_size_in is None: node_size_in = self.output_vocab.dimensions node_size_out = None with self: self.node = nengo.Node( TranscodeFunctionParam.to_node_output( self.function, self.input_vocab, self.output_vocab ), size_in=node_size_in, size_out=node_size_out, ) self.input = self.node self.output = self.node if self.input_vocab is not None: self.declare_input(self.input, self.input_vocab) if self.output_vocab is not None: self.declare_output(self.output, self.output_vocab)
class NengoObject(SupportDefaultsMixin, metaclass=NetworkMember): """A base class for Nengo objects. Parameters ---------- label : string A descriptive label for the object. seed : int The seed used for random number generation. Attributes ---------- label : string A descriptive label for the object. seed : int The seed used for random number generation. """ # Order in which parameters have to be initialized. # Missing parameters will be initialized last in an undefined order. # This is needed for pickling and copying of Nengo objects when the # parameter initialization order matters. _param_init_order = [] label = StringParam("label", default=None, optional=True) seed = IntParam("seed", default=None, optional=True) def __init__(self, label, seed): super().__init__() self._initialized = False self.label = label self.seed = seed def __getstate__(self): state = self.__dict__.copy() state["_initialized"] = False for attr in self.params: param = getattr(type(self), attr) if self in param: state[attr] = getattr(self, attr) return state def __setstate__(self, state): for attr in self._param_init_order: setattr(self, attr, state.pop(attr)) for attr in self.params: if attr in state: setattr(self, attr, state.pop(attr)) for k, v in state.items(): setattr(self, k, v) self._initialized = True if len(nengo.Network.context) > 0: warnings.warn(NotAddedToNetworkWarning(self)) def __setattr__(self, name, val): initialized = hasattr(self, "_initialized") and self._initialized if initialized and not hasattr(self, name): warnings.warn( "Creating new attribute '%s' on '%s'. " "Did you mean to change an existing attribute?" % (name, self), SyntaxWarning, ) super().__setattr__(name, val) def __str__(self): return self._str( include_id=not hasattr(self, "label") or self.label is None) def __repr__(self): return self._str(include_id=True) def _str(self, include_id): return "<%s%s%s>" % ( type(self).__name__, "" if not hasattr(self, "label") else " (unlabeled)" if self.label is None else ' "%s"' % self.label, " at 0x%x" % id(self) if include_id else "", ) @property def params(self): """Returns a list of parameter names that can be set.""" return list(iter_params(self)) def copy(self, add_to_container=True): with warnings.catch_warnings(): # We warn when copying since we can't change add_to_container. # However, we deal with it here, so we ignore the warning. warnings.simplefilter("ignore", category=NotAddedToNetworkWarning) c = copy(self) if add_to_container: nengo.Network.add(c) return c
class Network: """A network contains ensembles, nodes, connections, and other networks. A network is primarily used for grouping together related objects and connections for visualization purposes. However, you can also use networks as a nice way to reuse network creation code. To group together related objects that you do not need to reuse, you can create a new ``Network`` and add objects in a ``with`` block. For example: .. testcode:: network = nengo.Network() with network: with nengo.Network(label="Vision"): v1 = nengo.Ensemble(n_neurons=100, dimensions=2) with nengo.Network(label="Motor"): sma = nengo.Ensemble(n_neurons=100, dimensions=2) nengo.Connection(v1, sma) To reuse a group of related objects, you can create a new subclass of ``Network``, and add objects in the ``__init__`` method. For example: .. testcode:: class OcularDominance(nengo.Network): def __init__(self): self.column = nengo.Ensemble(n_neurons=100, dimensions=2) network = nengo.Network() with network: left_eye = OcularDominance() right_eye = OcularDominance() nengo.Connection(left_eye.column, right_eye.column) Parameters ---------- label : str, optional Name of the network. seed : int, optional Random number seed that will be fed to the random number generator. Setting the seed makes the network's build process deterministic. add_to_container : bool, optional Determines if this network will be added to the current container. If None, this network will be added to the network at the top of the ``Network.context`` stack unless the stack is empty. Attributes ---------- connections : list `.Connection` instances in this network. ensembles : list `.Ensemble` instances in this network. label : str Name of this network. networks : list `.Network` instances in this network. nodes : list `.Node` instances in this network. probes : list `.Probe` instances in this network. seed : int Random seed used by this network. """ context = ThreadLocalStack(maxsize=100) # static stack of Network objects label = StringParam("label", optional=True, readonly=False) seed = IntParam("seed", optional=True, readonly=False) def __init__(self, label=None, seed=None, add_to_container=None): self.label = label self.seed = seed self._config = self.default_config() self._objects = { Ensemble: [], Node: [], Connection: [], Network: [], Probe: [] } self._ensembles = self.objects[Ensemble] self._nodes = self.objects[Node] self._connections = self.objects[Connection] self._networks = self.objects[Network] self._probes = self.objects[Probe] # By default, we want to add to the current context, unless there is # no context; i.e., we're creating a top-level network. if add_to_container is None: add_to_container = len(Network.context) > 0 if add_to_container: Network.add(self) @staticmethod def add(obj): """Add the passed object to ``Network.context``.""" if len(Network.context) == 0: raise NetworkContextError( "'%s' must either be created inside a ``with network:`` " "block, or set add_to_container=False in the object's " "constructor." % obj) network = Network.context[-1] if not isinstance(network, Network): raise NetworkContextError("Current context (%s) is not a network" % network) for cls in type(obj).__mro__: if cls in network.objects: network.objects[cls].append(obj) break else: raise NetworkContextError("Objects of type %r cannot be added to " "networks." % type(obj).__name__) @staticmethod def default_config(): """Constructs a `~.Config` object for setting defaults.""" return Config(Connection, Ensemble, Node, Probe) def _all_objects(self, object_type): """Returns a list of all objects of the specified type.""" # Make a copy of this network's list objects = list(self.objects[object_type]) for subnet in self.networks: objects.extend(subnet._all_objects(object_type)) return objects @property def all_objects(self): """(list) All objects in this network and its subnetworks.""" objects = [] for object_type in self.objects: objects.extend(self._all_objects(object_type)) return objects @property def all_ensembles(self): """(list) All ensembles in this network and its subnetworks.""" return self._all_objects(Ensemble) @property def all_nodes(self): """(list) All nodes in this network and its subnetworks.""" return self._all_objects(Node) @property def all_networks(self): """(list) All networks in this network and its subnetworks.""" return self._all_objects(Network) @property def all_connections(self): """(list) All connections in this network and its subnetworks.""" return self._all_objects(Connection) @property def all_probes(self): """(list) All probes in this network and its subnetworks.""" return self._all_objects(Probe) @property def objects(self): return self._objects @objects.setter def objects(self, _): raise ReadonlyError(attr="objects", obj=self) @property def ensembles(self): return self._ensembles @ensembles.setter def ensembles(self, _): raise ReadonlyError(attr="ensembles", obj=self) @property def nodes(self): return self._nodes @nodes.setter def nodes(self, _): raise ReadonlyError(attr="nodes", obj=self) @property def networks(self): return self._networks @networks.setter def networks(self, _): raise ReadonlyError(attr="networks", obj=self) @property def connections(self): return self._connections @connections.setter def connections(self, _): raise ReadonlyError(attr="connections", obj=self) @property def probes(self): return self._probes @probes.setter def probes(self, _): raise ReadonlyError(attr="probes", obj=self) @property def config(self): """(`.Config`) Configuration for this network.""" return self._config @config.setter def config(self, _): raise ReadonlyError(attr="config", obj=self) @property def n_neurons(self): """(int) Number of neurons in this network, including subnetworks.""" return sum(ens.n_neurons for ens in self.all_ensembles) def __contains__(self, obj): return type(obj) in self.objects and obj in self.objects[type(obj)] def __enter__(self): Network.context.append(self) self._config.__enter__() return self def __exit__(self, dummy_exc_type, dummy_exc_value, dummy_tb): if len(Network.context) == 0: raise NetworkContextError( "Network.context in bad state; was empty when " "exiting from a 'with' block.") config = Config.context[-1] if config is not self._config: raise ConfigError("Config.context in bad state; was expecting " "current context to be '%s' but instead got " "'%s'." % (self._config, config)) network = Network.context.pop() if network is not self: raise NetworkContextError( "Network.context in bad state; was expecting current context " "to be '%s' but instead got '%s'." % (self, network)) self._config.__exit__(dummy_exc_type, dummy_exc_value, dummy_tb) def __getstate__(self): state = self.__dict__.copy() state["label"] = self.label state["seed"] = self.seed return state def __setstate__(self, state): for k, v in state.items(): setattr(self, k, v) if len(Network.context) > 0: warnings.warn(NotAddedToNetworkWarning(self)) def __str__(self): return "<%s %s>" % ( type(self).__name__, '"%s"' % self.label if self.label is not None else "(unlabeled) at 0x%x" % id(self), ) def __repr__(self): return "<%s %s %s>" % ( type(self).__name__, '"%s"' % self.label if self.label is not None else "(unlabeled)", "at 0x%x" % id(self), ) def copy(self, add_to_container=None): with warnings.catch_warnings(): # We warn when copying since we can't change add_to_container. # However, we deal with it here, so we ignore the warning. warnings.simplefilter("ignore", category=NotAddedToNetworkWarning) c = deepcopy(self) if add_to_container is None: add_to_container = len(Network.context) > 0 if add_to_container: Network.add(c) return c