def test_solvers(): check_init_args(Lstsq, ["weights", "rcond"]) check_repr(Lstsq(weights=True, rcond=0.1)) assert repr(Lstsq(weights=True, rcond=0.1)) == "Lstsq(weights=True, rcond=0.1)" check_init_args(LstsqNoise, ["weights", "noise", "solver"]) check_repr(LstsqNoise(weights=True, noise=0.2)) assert (repr(LstsqNoise( weights=True, noise=0.2)) == "LstsqNoise(weights=True, noise=0.2)") check_init_args(LstsqL2, ["weights", "reg", "solver"]) check_repr(LstsqL2(weights=True, reg=0.2)) assert repr(LstsqL2(weights=True, reg=0.2)) == "LstsqL2(weights=True, reg=0.2)" check_init_args(LstsqL2nz, ["weights", "reg", "solver"]) check_repr(LstsqL2nz(weights=True, reg=0.2)) assert repr(LstsqL2nz(weights=True, reg=0.2)) == "LstsqL2nz(weights=True, reg=0.2)" check_init_args(NoSolver, ["values", "weights"]) check_repr(NoSolver(values=np.array([[1.2, 3.4, 5.6, 7.8]]), weights=True)) assert (repr(NoSolver([[1.2, 3.4, 5.6, 7.8]], weights=True)) == "NoSolver(values=array([[1.2, 3.4, 5.6, 7.8]]), weights=True)")
def test_set_weight_solver(): with nengo.Network(): a = nengo.Ensemble(10, 2) b = nengo.Ensemble(10, 2) nengo.Connection(a, b, solver=LstsqL2(weights=True)) with pytest.raises(ValidationError): nengo.Connection(a.neurons, b, solver=LstsqL2(weights=True)) with pytest.raises(ValidationError): nengo.Connection(a, b.neurons, solver=LstsqL2(weights=True)) with pytest.raises(ValidationError): nengo.Connection(a.neurons, b.neurons, solver=LstsqL2(weights=True))
def divider(neuronCount=1000, tau=0.002): # Set up neurons model = nengo.Network() with model: dividend = nengo.Node(size_in=1, size_out=1) divisor = nengo.Node(size_in=1, size_out=1) combine = nengo.Ensemble(neuronCount, dimensions=2, radius=200, label='combine') quotient = nengo.Ensemble(neuronCount, dimensions=1, radius=10, label='quotient') nengo.Connection(dividend, combine, synapse=tau, transform=[[1], [0]]) nengo.Connection(divisor, combine, synapse=tau, transform=[[0], [1]]) nengo.Connection(combine, quotient, synapse=tau, solver=LstsqL2(weights=True), function=lambda x: x[1] / (x[0] + 0.001)) return model
def objective(hyperparams): tauRise = hyperparams['tauRise'] tauFall = hyperparams['tauFall'] dt = hyperparams['dt'] dtSample = hyperparams['dtSample'] f = DoubleExp(tauRise, tauFall) spikes = np.load('data/%s_spikes.npz' % hyperparams['name'])['spikes'] targets = np.load('data/%s_target.npz' % hyperparams['name'])['target'] A = np.zeros((0, spikes.shape[2])) Y = np.zeros((0, targets.shape[2])) for n in range(hyperparams['nTrain']): A = np.append(A, f.filt(spikes[n], dt=dt), axis=0) Y = np.append(Y, targets[n], axis=0) if dt != dtSample: A = A[::int(dtSample / dt)] Y = Y[::int(dtSample / dt)] d, _ = LstsqL2(reg=hyperparams['reg'])(A, Y) X = np.dot(A, d) loss = rmse(X, Y) loss += penalty * (10 * tauRise + tauFall) return { 'loss': loss, 'd': d, 'tauRise': tauRise, 'tauFall': tauFall, 'status': STATUS_OK }
def test_weights(Simulator, nl, plt, seed): n1, n2 = 100, 50 def func(t): return [np.sin(4 * t), np.cos(12 * t)] transform = np.array([[0.6, -0.4]]) m = nengo.Network(label='test_weights', seed=seed) with m: m.config[nengo.Ensemble].neuron_type = nl() u = nengo.Node(output=func) a = nengo.Ensemble(n1, dimensions=2, radius=1.5) b = nengo.Ensemble(n2, dimensions=1) bp = nengo.Probe(b) nengo.Connection(u, a) nengo.Connection(a, b, transform=transform, solver=LstsqL2(weights=True)) with Simulator(m) as sim: sim.run(1.) t = sim.trange() x = np.array(func(t)).T y = np.dot(x, transform.T) z = nengo.Lowpass(0.005).filtfilt(sim.data[bp], dt=sim.dt) assert allclose(t, y, z, atol=0.1, buf=0.1, delay=0.01, plt=plt)
def test_compare_solvers(Simulator, plt, seed, allclose): pytest.importorskip("sklearn") N = 70 decoder_solvers = [ Lstsq(), LstsqNoise(), LstsqL2(), LstsqL2nz(), LstsqL1(max_iter=5000), ] weight_solvers = [LstsqL1(weights=True, max_iter=5000), LstsqDrop(weights=True)] tfinal = 4 def input_function(t): return np.interp(t, [1, 3], [-1, 1], left=-1, right=1) model = nengo.Network(seed=seed) with model: u = nengo.Node(output=input_function) a = nengo.Ensemble(N, dimensions=1) nengo.Connection(u, a) ap = nengo.Probe(a) probes = [] names = [] for solver in decoder_solvers + weight_solvers: b = nengo.Ensemble(N, dimensions=1, seed=seed + 1) nengo.Connection(a, b, solver=solver) probes.append(nengo.Probe(b)) names.append( "%s(%s)" % (type(solver).__name__, "w" if solver.weights else "d") ) with Simulator(model) as sim: sim.run(tfinal) t = sim.trange() # ref = sim.data[up] ref = nengo.Lowpass(0.02).filtfilt(sim.data[ap], dt=sim.dt) outputs = np.array([sim.data[probe][:, 0] for probe in probes]).T outputs_f = nengo.Lowpass(0.02).filtfilt(outputs, dt=sim.dt) close = signals_allclose( t, ref, outputs_f, atol=0.07, rtol=0, buf=0.1, delay=0.007, plt=plt, labels=names, individual_results=True, allclose=allclose, ) for name, c in zip(names, close): assert c, "Solver '%s' does not meet tolerances" % name
def __init__(self, pre, post, solver=LstsqL2(), **kwargs): if isinstance(pre, nengo.Ensemble): solver = BiasedSolver(solver) bias = nengo.Node(output=solver.bias_function(post.size_in), label="Bias") nengo.Connection(bias, post, synapse=None) super(Connection, self).__init__(pre, post, solver=solver, **kwargs)
def test_set_weight_solver(): with nengo.Network() as net: # set default Gaussian transform to avoid transform errors net.config[nengo.Connection].transform = nengo.dists.Gaussian(0, 1) ens = nengo.Ensemble(10, 2) node = nengo.Node(lambda t, x: x, size_in=2, size_out=2) nengo.Connection(ens, ens, solver=LstsqL2(weights=True)) with pytest.warns( UserWarning, match="For connections from.*setting the solver has no effect" ): nengo.Connection(node, ens, solver=LstsqL2(weights=True)) with pytest.warns( UserWarning, match="For connections to.*setting `weights=True`.*no effect" ): nengo.Connection(ens, node, solver=LstsqL2(weights=True))
def __init__(self, solver=LstsqL2()): self.solver = solver self.bias = None try: # parent class changed in Nengo 2.1.1 # need to do this because self.weights is read-only super(BiasedSolver, self).__init__(weights=solver.weights) except TypeError: # pragma: no cover super(BiasedSolver, self).__init__() self.weights = solver.weights
def test_solvers(Simulator, nl_nodirect): N = 100 decoder_solvers = [ Lstsq(), LstsqNoise(), LstsqL2(), LstsqL2nz(), LstsqL1() ] weight_solvers = [LstsqL1(weights=True), LstsqDrop(weights=True)] dt = 1e-3 tfinal = 4 def input_function(t): return np.interp(t, [1, 3], [-1, 1], left=-1, right=1) model = nengo.Network('test_solvers', seed=290) with model: u = nengo.Node(output=input_function) a = nengo.Ensemble(nl_nodirect(N), dimensions=1) nengo.Connection(u, a) ap = nengo.Probe(a) probes = [] names = [] for solver in decoder_solvers + weight_solvers: b = nengo.Ensemble(nl_nodirect(N), dimensions=1, seed=99) nengo.Connection(a, b, solver=solver) probes.append(nengo.Probe(b)) names.append( "%s(%s)" % (solver.__class__.__name__, 'w' if solver.weights else 'd')) sim = Simulator(model, dt=dt) sim.run(tfinal) t = sim.trange() # ref = sim.data[up] ref = filtfilt(sim.data[ap], 20) outputs = np.array([sim.data[probe] for probe in probes]) outputs_f = filtfilt(outputs, 20, axis=1) close = allclose(t, ref, outputs_f, plotter=Plotter(Simulator, nl_nodirect), filename='test_decoders.test_solvers.pdf', labels=names, atol=0.05, rtol=0, buf=100, delay=7) for name, c in zip(names, close): assert c, "Solver '%s' does not meet tolerances" % name
def test_compare_solvers(Simulator, plt, seed): N = 70 decoder_solvers = [ Lstsq(), LstsqNoise(), LstsqL2(), LstsqL2nz(), LstsqL1() ] weight_solvers = [LstsqL1(weights=True), LstsqDrop(weights=True)] tfinal = 4 def input_function(t): return np.interp(t, [1, 3], [-1, 1], left=-1, right=1) model = nengo.Network(seed=seed) with model: u = nengo.Node(output=input_function) a = nengo.Ensemble(N, dimensions=1) nengo.Connection(u, a) ap = nengo.Probe(a) probes = [] names = [] for solver in decoder_solvers + weight_solvers: b = nengo.Ensemble(N, dimensions=1, seed=seed + 1) nengo.Connection(a, b, solver=solver) probes.append(nengo.Probe(b)) names.append( "%s(%s)" % (solver.__class__.__name__, 'w' if solver.weights else 'd')) sim = Simulator(model) sim.run(tfinal) t = sim.trange() # ref = sim.data[up] ref = nengo.synapses.filtfilt(sim.data[ap], 0.02, dt=sim.dt) outputs = np.array([sim.data[probe][:, 0] for probe in probes]).T outputs_f = nengo.synapses.filtfilt(outputs, 0.02, dt=sim.dt) close = allclose(t, ref, outputs_f, atol=0.07, rtol=0, buf=0.1, delay=0.007, plt=plt, labels=names, individual_results=True) for name, c in zip(names, close): assert c, "Solver '%s' does not meet tolerances" % name
def test_solverparam(): """SolverParam must be a solver.""" class Test: sp = ConnectionSolverParam("sp", default=None) inst = Test() assert inst.sp is None inst.sp = LstsqL2() assert isinstance(inst.sp, LstsqL2) assert not inst.sp.weights # Non-solver not OK with pytest.raises(ValidationError): inst.sp = "a"
def test_set_learning_rule(): with nengo.Network(): a = nengo.Ensemble(10, 2) b = nengo.Ensemble(10, 2) nengo.Connection(a, b, learning_rule_type=nengo.PES()) nengo.Connection(a, b, learning_rule_type=nengo.PES(), solver=LstsqL2(weights=True)) nengo.Connection(a.neurons, b.neurons, learning_rule_type=nengo.PES()) nengo.Connection(a.neurons, b.neurons, learning_rule_type=nengo.Oja()) n = nengo.Node(output=lambda t, x: t * x, size_in=2) with pytest.raises(ValueError): nengo.Connection(n, a, learning_rule_type=nengo.PES())
def discrete_example(seed, dt): n_neurons = 1000 theta = 0.1 freq = 50 q = 27 radii = 1.0 sys = PadeDelay(theta, q) T = 5000*(dt+0.001) rms = 1.0 signal = WhiteSignal(T, high=freq, rms=rms, y0=0) tau = 0.1 tau_probe = 0.02 reg = 0.1 # Determine radii using direct mode with LinearNetwork( sys, n_neurons_per_ensemble=1, input_synapse=tau, synapse=tau, dt=dt, neuron_type=Direct(), realizer=Balanced()) as model: Connection(Node(output=signal), model.input, synapse=None) p_x = Probe(model.state.input, synapse=None) with Simulator(model, dt=dt, seed=seed+1) as sim: sim.run(T) radii *= np.max(abs(sim.data[p_x]), axis=0) logging.info("Radii: %s", radii) with Network(seed=seed) as model: u = Node(output=signal) kwargs = dict( n_neurons_per_ensemble=n_neurons / len(sys), input_synapse=tau, synapse=tau, radii=radii, solver=LstsqL2(reg=reg), realizer=Balanced()) delay_disc = LinearNetwork(sys, dt=dt, **kwargs) delay_cont = LinearNetwork(sys, dt=None, **kwargs) Connection(u, delay_disc.input, synapse=None) Connection(u, delay_cont.input, synapse=None) p_u = Probe(u, synapse=tau_probe) p_y_disc = Probe(delay_disc.output, synapse=tau_probe) p_y_cont = Probe(delay_cont.output, synapse=tau_probe) with Simulator(model, dt=dt, seed=seed) as sim: sim.run(T) return (theta, dt, sim.trange(), sim.data[p_u], sim.data[p_y_disc], sim.data[p_y_cont])
def objective(hyperparams): taus_ens = [hyperparams['tau_rise'], hyperparams['tau_fall']] h_ens = DoubleExp(taus_ens[0], taus_ens[1]) A = h_ens.filt(np.load('data/%s_ens.npz'%hyperparams['name'])['ens'], dt=hyperparams['dt']) x = np.load('data/%s_x.npz'%hyperparams['name'])['x'] if dt != dt_sample: A = A[::int(dt_sample/dt)] x = x[::int(dt_sample/dt)] if hyperparams['reg']: d_ens = LstsqL2(reg=hyperparams['reg'])(A, x)[0] else: d_ens = Lstsq()(A, x)[0] xhat = np.dot(A, d_ens) loss = rmse(xhat, x) loss += penalty * (10*taus_ens[0] + taus_ens[1]) return {'loss': loss, 'taus_ens': taus_ens, 'd_ens': d_ens, 'status': STATUS_OK}
def test_weights(Simulator, nl): name = 'test_weights' n1, n2 = 100, 50 def func(t): return np.array([np.sin(4 * t), np.cos(12 * t)]) transform = np.array([[0.6, -0.4]]) m = nengo.Network(label=name, seed=3902) with m: m.config[nengo.Ensemble].neuron_type = nl() u = nengo.Node(output=func) a = nengo.Ensemble(n1, dimensions=2, radius=1.5) b = nengo.Ensemble(n2, dimensions=1) bp = nengo.Probe(b) nengo.Connection(u, a) nengo.Connection(a, b, transform=transform, solver=LstsqL2(weights=True)) sim = Simulator(m) sim.run(2.) t = sim.trange() x = func(t).T y = np.dot(x, transform.T) z = filtfilt(sim.data[bp], 10, axis=0) assert allclose(t, y.flatten(), z.flatten(), plotter=Plotter(Simulator, nl), filename='test_connection.' + name + '.pdf', atol=0.1, rtol=0, buf=100, delay=10)
def test_weights(Simulator, AnyNeuronType, plt, seed, allclose): """Tests connections using a solver with weights""" n1, n2 = 100, 50 def func(t): return [np.sin(4 * t), np.cos(12 * t)] transform = np.array([[0.6, -0.4]]) m = nengo.Network(label="test_weights", seed=seed) with m: m.config[nengo.Ensemble].neuron_type = AnyNeuronType() u = nengo.Node(output=func) a = nengo.Ensemble(n1, dimensions=2, radius=1.4) b = nengo.Ensemble(n2, dimensions=1) bp = nengo.Probe(b) nengo.Connection(u, a) nengo.Connection(a, b, synapse=0.01, transform=transform, solver=LstsqL2(weights=True)) with Simulator(m) as sim: sim.run(1.0) t = sim.trange() x = np.array(func(t)).T y = np.dot(x, transform.T) z = nengo.Lowpass(0.01).filt(sim.data[bp], dt=sim.dt) assert signals_allclose(t, y, z, atol=0.15, buf=0.1, delay=0.025, plt=plt, allclose=allclose)
def test_set_learning_rule(): with nengo.Network(): a = nengo.Ensemble(10, 2) b = nengo.Ensemble(10, 2) nengo.Connection(a, b, learning_rule_type=nengo.PES()) nengo.Connection( a, b, learning_rule_type=nengo.PES(), solver=LstsqL2(weights=True) ) nengo.Connection( a.neurons, b.neurons, learning_rule_type=nengo.PES(), transform=1 ) nengo.Connection( a.neurons, b.neurons, learning_rule_type=nengo.Oja(), transform=np.ones((10, 10)), ) n = nengo.Node(output=lambda t, x: t * x, size_in=2) with pytest.raises( ValidationError, match="'pre' must be of type 'Ensemble'.*PES" ): nengo.Connection(n, a, learning_rule_type=nengo.PES())
def tune_ens_parameters(ens, function=None, solver=None, rng=None, n=1000): """Find good ensemble parameters for decoding a particular function. Randomly generate many sets of parameters and determine the decoding error for each. Then set the ensemble parameters to those with the lowest decoding error. The "ensemble parameters" are the encoders, gains, biases, and evaluation points. Parameters ---------- ens : Ensemble The ensemble to optimize. function : callable, optional The target function to optimize for. Defaults to the identity function. solver : nengo.solvers.Solver, optional The solver to use for finding the decoders. Default: ``LstsqL2()`` rng : numpy.random.RandomState, optional The random number generator to use. Default: ``np.random`` n : int, optional The number of random combinations to test. Default: 1000 """ from nengo.dists import Distribution from nengo.neurons import Direct from nengo.solvers import LstsqL2 from nengo.builder.connection import solve_for_decoders from nengo.builder.ensemble import gen_eval_points if solver is None: solver = LstsqL2() if rng is None: rng = np.random if isinstance(ens.neuron_type, Direct): raise ValueError("Parameters do not apply to Direct mode ensembles") sample = lambda dist, n, d=None: (dist.sample(n, d=d, rng=rng) if isinstance(dist, Distribution) else np .asarray(dist)) # use the same evaluation points for all trials eval_points = gen_eval_points(ens, ens.eval_points, rng=rng) targets = (np.array([function(ep) for ep in eval_points]) if function is not None else eval_points) # --- try random parameters and record error errors = [] for i in range(n): # --- generate random parameters if ens.gain is None and ens.bias is None: max_rates = sample(ens.max_rates, ens.n_neurons) intercepts = sample(ens.intercepts, ens.n_neurons) gain, bias = ens.neuron_type.gain_bias(max_rates, intercepts) elif ens.gain is not None and ens.bias is not None: gain = sample(ens.gain, ens.n_neurons) bias = sample(ens.bias, ens.n_neurons) else: raise NotImplementedError("Mixed gain/bias and rates/ints") encoders = sample(ens.encoders, ens.n_neurons, ens.dimensions) # --- determine residual x = np.dot(eval_points, encoders.T / ens.radius) decoders, info = solve_for_decoders(solver, ens.neuron_type, gain, bias, x, targets, rng) error = info['rmses'].mean() errors.append((error, encoders, gain, bias, eval_points)) # --- set parameters to those with the lowest error errors.sort(key=lambda x: x[0]) ens.encoders, ens.gain, ens.bias, ens.eval_points = errors[0][1:]
class Connection(NengoObject): """Connects two objects together. The connection between the two object is unidirectional, transmitting information from the first argument, ``pre``, to the second argument, ``post``. Almost any Nengo object can act as the pre or post side of a connection. Additionally, you can use Python slice syntax to access only some of the dimensions of the pre or post object. For example, if ``node`` has ``size_out=2`` and ``ensemble`` has ``size_in=1``, we could not create the following connection:: nengo.Connection(node, ensemble) But, we could create either of these two connections:: nengo.Connection(node[0], ensemble) nengo.Connection(node[1], ensemble) Parameters ---------- pre : Ensemble or Neurons or Node The source Nengo object for the connection. post : Ensemble or Neurons or Node or Probe The destination object for the connection. synapse : Synapse, optional \ (Default: ``nengo.synapses.Lowpass(tau=0.005)``) Synapse model to use for filtering (see `~nengo.synapses.Synapse`). function : callable, optional (Default: None) Function to compute across the connection. Note that ``pre`` must be an ensemble to apply a function across the connection. transform : (post.size_in, pre.size_out) array_like, optional \ (Default: ``np.array(1.0)``) Linear transform mapping the pre output to the post input. This transform is in terms of the sliced size; if either pre or post is a slice, the transform must be shaped according to the sliced dimensionality. Additionally, the function is applied before the transform, so if a function is computed across the connection, the transform must be of shape ``(len(function(np.zeros(post.size_in))), pre.size_out)``. solver : Solver, optional (Default: ``nengo.solvers.LstsqL2()``) Solver instance to compute decoders or weights (see `~nengo.solvers.Solver`). If ``solver.weights`` is True, a full connection weight matrix is computed instead of decoders. learning_rule_type : LearningRuleType or iterable of LearningRuleType, \ optional (Default: None) Modifies the decoders or connection weights during simulation. eval_points : (n_eval_points, pre.size_out) array_like or int, optional \ (Default: None) Points at which to evaluate ``function`` when computing decoders, spanning the interval (-pre.radius, pre.radius) in each dimension. If None, will use the eval_points associated with ``pre``. scale_eval_points : bool, optional (Default: True) Indicates whether the evaluation points should be scaled by the radius of the pre Ensemble. label : str, optional (Default: None) A descriptive label for the connection. seed : int, optional (Default: None) The seed used for random number generation. Attributes ---------- is_decoded : bool True if and only if the connection is decoded. This will not occur when ``solver.weights`` is True or both pre and post are `~nengo.ensemble.Neurons`. function : callable The given function. function_size : int The output dimensionality of the given function. If no function is specified, function_size will be 0. label : str A human-readable connection label for debugging and visualization. If not overridden, incorporates the labels of the pre and post objects. learning_rule_type : instance or list or dict of LearningRuleType, optional The learning rule types. post : Ensemble or Neurons or Node or Probe or ObjView The given post object. post_obj : Ensemble or Neurons or Node or Probe The underlying post object, even if ``post`` is an ``ObjView``. post_slice : slice or list or None The slice associated with ``post`` if it is an ObjView, or None. pre : Ensemble or Neurons or Node or ObjView The given pre object. pre_obj : Ensemble or Neurons or Node The underlying pre object, even if ``post`` is an ``ObjView``. pre_slice : slice or list or None The slice associated with ``pre`` if it is an ObjView, or None. seed : int The seed used for random number generation. solver : Solver The Solver instance that will be used to compute decoders or weights (see ``nengo.solvers``). synapse : Synapse The Synapse model used for filtering across the connection (see ``nengo.synapses``). transform : (size_mid, size_out) array_like Linear transform mapping the pre function output to the post input. """ probeable = ('output', 'input', 'weights') pre = PrePostParam('pre', nonzero_size_out=True) post = PrePostParam('post', nonzero_size_in=True) synapse = SynapseParam('synapse', default=Lowpass(tau=0.005)) function_info = ConnectionFunctionParam('function', default=None, optional=True) transform = TransformParam('transform', default=np.array(1.0)) solver = ConnectionSolverParam('solver', default=LstsqL2()) learning_rule_type = ConnectionLearningRuleTypeParam('learning_rule_type', default=None, optional=True) eval_points = EvalPointsParam('eval_points', default=None, optional=True, sample_shape=('*', 'size_in')) scale_eval_points = BoolParam('scale_eval_points', default=True) modulatory = ObsoleteParam( 'modulatory', "Modulatory connections have been removed. " "Connect to a learning rule instead.", since="v2.1.0", url="https://github.com/nengo/nengo/issues/632#issuecomment-71663849") def __init__(self, pre, post, synapse=Default, function=Default, transform=Default, solver=Default, learning_rule_type=Default, eval_points=Default, scale_eval_points=Default, label=Default, seed=Default, modulatory=Unconfigurable): super(Connection, self).__init__(label=label, seed=seed) self.pre = pre self.post = post self.synapse = synapse self.transform = transform self.scale_eval_points = scale_eval_points self.eval_points = eval_points # Must be set before function self.function_info = function # Must be set after transform self.solver = solver # Must be set before learning rule self.learning_rule_type = learning_rule_type # set after transform self.modulatory = modulatory def __str__(self): return "<Connection %s>" % self._str def __repr__(self): return "<Connection at 0x%x %s>" % (id(self), self._str) @property def _str(self): if self.label is not None: return self.label desc = "" if self.function is None else " computing '%s'" % (getattr( self.function, '__name__', str(self.function))) return "from %s to %s%s" % (self.pre, self.post, desc) @property def function(self): return self.function_info.function @function.setter def function(self, function): self.function_info = function @property def is_decoded(self): return not (self.solver.weights or (isinstance(self.pre_obj, Neurons) and isinstance(self.post_obj, Neurons))) @property def _label(self): if self.label is not None: return self.label return "from %s to %s%s" % (self.pre, self.post, " computing '%s'" % self.function.__name__ if self.function is not None else "") @property def learning_rule(self): """(LearningRule or iterable) Connectable learning rule object(s).""" if self.learning_rule_type is not None and self._learning_rule is None: types = self.learning_rule_type if isinstance(types, dict): self._learning_rule = types.__class__() # dict of same type for k, v in iteritems(types): self._learning_rule[k] = LearningRule(self, v) elif is_iterable(types): self._learning_rule = [LearningRule(self, v) for v in types] elif isinstance(types, LearningRuleType): self._learning_rule = LearningRule(self, types) else: raise ValidationError("Invalid type %r" % types.__class__.__name__, attr='learning_rule_type', obj=self) return self._learning_rule @property def post_obj(self): return self.post.obj if isinstance(self.post, ObjView) else self.post @property def post_slice(self): return (self.post.slice if isinstance(self.post, ObjView) else slice(None)) @property def pre_obj(self): return self.pre.obj if isinstance(self.pre, ObjView) else self.pre @property def pre_slice(self): return self.pre.slice if isinstance(self.pre, ObjView) else slice(None) @property def size_in(self): """(int) The number of output dimensions of the pre object. Also the input size of the function, if one is specified. """ return self.pre.size_out @property def size_mid(self): """(int) The number of output dimensions of the function, if specified. If the function is not specified, then ``size_in == size_mid``. """ size = self.function_info.size return self.size_in if size is None else size @property def size_out(self): """(int) The number of input dimensions of the post object. Also the number of output dimensions of the transform. """ return self.post.size_in
def test_eval_points_static(plt, rng): solver = LstsqL2() n = 100 d = 5 eval_points = np.logspace(np.log10(300), np.log10(5000), 21) eval_points = np.round(eval_points).astype('int') max_points = eval_points.max() n_trials = 25 # n_trials = 100 rmses = np.nan * np.zeros((len(eval_points), n_trials)) for trial in range(n_trials): # make a population for generating LIF tuning curves a = nengo.LIF(n) gain, bias = a.gain_bias( # rng.uniform(50, 100, n), rng.uniform(-1, 1, n)) rng.uniform(50, 100, n), rng.uniform(-0.9, 0.9, n)) e = get_encoders(n, d, rng=rng) # make one activity matrix with the max number of eval points train = get_eval_points(max_points, d, rng=rng) test = get_eval_points(max_points, d, rng=rng) Atrain = a.rates(np.dot(train, e), gain, bias) Atest = a.rates(np.dot(test, e), gain, bias) for i, n_points in enumerate(eval_points): Di, _ = solver(Atrain[:n_points], train[:n_points], rng=rng) rmses[i, trial] = rms(np.dot(Atest, Di) - test) rmses_norm1 = rmses - rmses.mean(0, keepdims=True) rmses_norm2 = (rmses - rmses.mean(0, keepdims=True)) / rmses.std( 0, keepdims=True) def make_plot(rmses): mean = rmses.mean(1) low = rmses.min(1) high = rmses.max(1) std = rmses.std(1) plt.semilogx(eval_points, mean, 'k-', label='mean') plt.semilogx(eval_points, mean - std, 'k--', label='+/- std') plt.semilogx(eval_points, mean + std, 'k--') plt.semilogx(eval_points, high, 'r-', label='high') plt.semilogx(eval_points, low, 'b-', label='low') plt.xlim([eval_points[0], eval_points[-1]]) # plt.xticks(eval_points, eval_points) plt.legend(fontsize=8, loc=1) plt.figure(figsize=(12, 8)) plt.subplot(3, 1, 1) make_plot(rmses) plt.ylabel('rmse') plt.subplot(3, 1, 2) make_plot(rmses_norm1) plt.ylabel('rmse - mean') plt.subplot(3, 1, 3) make_plot(rmses_norm2) plt.ylabel('(rmse - mean) / std')
class Connection(NengoObject): """Connects two objects together. The connection between the two object is unidirectional, transmitting information from the first argument, ``pre``, to the second argument, ``post``. Almost any Nengo object can act as the pre or post side of a connection. Additionally, you can use Python slice syntax to access only some of the dimensions of the pre or post object. For example, if ``node`` has ``size_out=2`` and ``ensemble`` has ``size_in=1``, we could not create the following connection:: nengo.Connection(node, ensemble) But, we could create either of these two connections:: nengo.Connection(node[0], ensemble) nengo.Connection(node[1], ensemble) Parameters ---------- pre : Ensemble or Neurons or Node The source Nengo object for the connection. post : Ensemble or Neurons or Node or Probe The destination object for the connection. synapse : Synapse or None, optional Synapse model to use for filtering (see `~nengo.synapses.Synapse`). If *None*, no synapse will be used and information will be transmitted without any delay (if supported by the backend---some backends may introduce a single time step delay). Note that at least one connection must have a synapse that is not *None* if components are connected in a cycle. Furthermore, a synaptic filter with a zero time constant is different from a *None* synapse as a synaptic filter will always add a delay of at least one time step. function : callable or (n_eval_points, size_mid) array_like, optional Function to compute across the connection. Note that ``pre`` must be an ensemble to apply a function across the connection. If an array is passed, the function is implicitly defined by the points in the array and the provided ``eval_points``, which have a one-to-one correspondence. transform : (size_out, size_mid) array_like, optional Linear transform mapping the pre output to the post input. This transform is in terms of the sliced size; if either pre or post is a slice, the transform must be shaped according to the sliced dimensionality. Additionally, the function is applied before the transform, so if a function is computed across the connection, the transform must be of shape ``(size_out, size_mid)``. solver : Solver, optional Solver instance to compute decoders or weights (see `~nengo.solvers.Solver`). If ``solver.weights`` is True, a full connection weight matrix is computed instead of decoders. learning_rule_type : LearningRuleType or iterable of LearningRuleType, optional Modifies the decoders or connection weights during simulation. eval_points : (n_eval_points, size_in) array_like or int, optional Points at which to evaluate ``function`` when computing decoders, spanning the interval (-pre.radius, pre.radius) in each dimension. If None, will use the eval_points associated with ``pre``. scale_eval_points : bool, optional Indicates whether the evaluation points should be scaled by the radius of the pre Ensemble. label : str, optional A descriptive label for the connection. seed : int, optional The seed used for random number generation. Attributes ---------- is_decoded : bool True if and only if the connection is decoded. This will not occur when ``solver.weights`` is True or both pre and post are `~nengo.ensemble.Neurons`. function : callable The given function. function_size : int The output dimensionality of the given function. If no function is specified, function_size will be 0. label : str A human-readable connection label for debugging and visualization. If not overridden, incorporates the labels of the pre and post objects. learning_rule_type : instance or list or dict of LearningRuleType, optional The learning rule types. post : Ensemble or Neurons or Node or Probe or ObjView The given post object. post_obj : Ensemble or Neurons or Node or Probe The underlying post object, even if ``post`` is an ``ObjView``. post_slice : slice or list or None The slice associated with ``post`` if it is an ObjView, or None. pre : Ensemble or Neurons or Node or ObjView The given pre object. pre_obj : Ensemble or Neurons or Node The underlying pre object, even if ``post`` is an ``ObjView``. pre_slice : slice or list or None The slice associated with ``pre`` if it is an ObjView, or None. seed : int The seed used for random number generation. solver : Solver The Solver instance that will be used to compute decoders or weights (see ``nengo.solvers``). synapse : Synapse The Synapse model used for filtering across the connection (see ``nengo.synapses``). transform : (size_out, size_mid) array_like Linear transform mapping the pre function output to the post input. Properties ---------- size_in : int The number of output dimensions of the pre object. Also the input size of the function, if one is specified. size_mid : int The number of output dimensions of the function, if specified. If the function is not specified, then ``size_in == size_mid``. size_out : int The number of input dimensions of the post object. Also the number of output dimensions of the transform. """ probeable = ("output", "input", "weights") pre = PrePostParam("pre", nonzero_size_out=True) post = PrePostParam("post", nonzero_size_in=True) synapse = SynapseParam("synapse", default=Lowpass(tau=0.005)) function_info = ConnectionFunctionParam("function", default=None, optional=True) transform = ConnectionTransformParam("transform", default=1.0) solver = ConnectionSolverParam("solver", default=LstsqL2()) learning_rule_type = ConnectionLearningRuleTypeParam("learning_rule_type", default=None, optional=True) eval_points = EvalPointsParam("eval_points", default=None, optional=True, sample_shape=("*", "size_in")) scale_eval_points = BoolParam("scale_eval_points", default=True) modulatory = ObsoleteParam( "modulatory", "Modulatory connections have been removed. " "Connect to a learning rule instead.", since="v2.1.0", url="https://github.com/nengo/nengo/issues/632#issuecomment-71663849", ) _param_init_order = [ "pre", "post", "synapse", "eval_points", "function_info", "transform", "solver", "learning_rule_type", ] def __init__( self, pre, post, synapse=Default, function=Default, transform=Default, solver=Default, learning_rule_type=Default, eval_points=Default, scale_eval_points=Default, label=Default, seed=Default, modulatory=Unconfigurable, ): super().__init__(label=label, seed=seed) self.pre = pre self.post = post self.synapse = synapse self.eval_points = eval_points # Must be set before function self.scale_eval_points = scale_eval_points self.function_info = function self.transform = transform # Must be set after function self.solver = solver # Must be set before learning rule self.learning_rule_type = learning_rule_type # set after transform self.modulatory = modulatory def __str__(self): return self._str(include_id=False) def __repr__(self): return self._str(include_id=True) def _str(self, include_id): desc = "<Connection " if include_id: desc += "at 0x%x " % id(self) if self.label is None: desc += "from %s to %s%s" % ( self.pre, self.post, ("" if self.function is None else " computing '%s'" % (function_name(self.function))), ) else: desc += self.label desc += ">" return desc @property def function(self): return self.function_info.function @function.setter def function(self, function): self.function_info = function @property def is_decoded(self): return not (self.solver.weights or (isinstance(self.pre_obj, Neurons) and isinstance(self.post_obj, Neurons))) @property def _label(self): if self.label is not None: return self.label return "from %s to %s%s" % ( self.pre, self.post, " computing '%s'" % function_name(self.function) if self.function is not None else "", ) @property def learning_rule(self): """(LearningRule or iterable) Connectable learning rule object(s).""" if self.learning_rule_type is None: return None types = self.learning_rule_type if isinstance(types, dict): learning_rule = type(types)() # dict of same type for k, v in types.items(): learning_rule[k] = LearningRule(self, v) elif is_iterable(types): learning_rule = [LearningRule(self, v) for v in types] elif isinstance(types, LearningRuleType): learning_rule = LearningRule(self, types) else: raise ValidationError( "Invalid type %r" % type(types).__name__, attr="learning_rule_type", obj=self, ) return learning_rule @property def post_obj(self): return self.post.obj if isinstance(self.post, ObjView) else self.post @property def post_slice(self): return self.post.slice if isinstance(self.post, ObjView) else slice(None) @property def pre_obj(self): return self.pre.obj if isinstance(self.pre, ObjView) else self.pre @property def pre_slice(self): return self.pre.slice if isinstance(self.pre, ObjView) else slice(None) @property def size_in(self): """(int) The number of output dimensions of the pre object. Also the input size of the function, if one is specified. """ return self.pre.size_out @property def size_mid(self): """(int) The number of output dimensions of the function, if specified. If the function is not specified, then ``size_in == size_mid``. """ size = self.function_info.size return self.size_in if size is None else size @property def size_out(self): """(int) The number of input dimensions of the post object. Also the number of output dimensions of the transform. """ return self.post.size_in
class Temporal(Solver, SupportDefaultsMixin): """Solves for connection weights by accounting for the neural dynamics. This allows the optimization procedure to potentially harness any correlations in spike-timing between neurons, and/or the adaptative dynamics of more detailed neuron models, given the dynamics of the desired function with respect to the evaluation points. This works by explicitly simulating the neurons given the stimulus, and then learning to decode the desired function in the time-domain. To use this method, pass it to the ``solver`` parameter for a :class:`nengo.Connection`. The ``pre`` object on this connection should be a :class:`nengo.Ensemble` that uses some dynamic neuron model. Parameters ---------- synapse : :class:`nengo.synapses.Synapse`, optional The :class:`nengo.synapses.Synapse` model used to filter the pre-synaptic activities of the neurons before being passed to the underlying solver. A value of ``None`` will bypass any filtering. Defaults to a :class:`nengo.Lowpass` filter with a time-constant of 5 ms. solver : :class:`nengo.solvers.Solver`, optional The underlying :class:`nengo.solvers.Solver` used to solve the problem ``AD = Y``, where ``A`` are the (potentially filtered) neural activities (in response to the evaluation points, over time), ``D`` are the Nengo decoders, and ``Y`` are the corresponding targets given by the ``function`` supplied to the connection. Defaults to :class:`nengo.solvers.LstsqL2`. See Also -------- :class:`.RLS` :class:`nengo.Connection` :class:`nengo.solvers.Solver` :mod:`.synapses` Notes ----- Requires ``nengo>=2.5.0`` (specifically, `PR #1313 <https://github.com/nengo/nengo/pull/1313>`_). If the neuron model for the pre-synaptic population includes some internal state that varies over time (which it should, otherwise there is little point in using this solver), then the order of the given evaluation points will matter. You will likely want to supply them as an array, rather than as a distribution. Likewise, you may want to filter your desired output, and specify the function as an array on the connection (see example below). The effect of the solver's regularization has a very different interpretation in this context (due to the filtered spiking error having its own statistics), and so you may also wish to instantiate the solver yourself with some value other than the default regularization. Examples -------- Below we use the temporal solver to learn a filtered communication-channel (the identity function) using 100 low-threshold spiking (LTS) Izhikevich neurons. The training and test data are sampled independently from the same band-limited white-noise process. >>> from nengolib import Temporal, Network >>> import nengo >>> neuron_type = nengo.Izhikevich(coupling=0.25) >>> tau = 0.005 >>> process = nengo.processes.WhiteSignal(period=5, high=5, y0=0, rms=0.3) >>> eval_points = process.run_steps(5000) >>> with Network() as model: >>> stim = nengo.Node(output=process) >>> x = nengo.Ensemble(100, 1, neuron_type=neuron_type) >>> out = nengo.Node(size_in=1) >>> nengo.Connection(stim, x, synapse=None) >>> nengo.Connection(x, out, synapse=None, >>> eval_points=eval_points, >>> function=nengo.Lowpass(tau).filt(eval_points), >>> solver=Temporal(synapse=tau)) >>> p_actual = nengo.Probe(out, synapse=tau) >>> p_ideal = nengo.Probe(stim, synapse=tau) >>> with nengo.Simulator(model) as sim: >>> sim.run(5) >>> import matplotlib.pyplot as plt >>> plt.plot(sim.trange(), sim.data[p_actual], label="Actual") >>> plt.plot(sim.trange(), sim.data[p_ideal], label="Ideal") >>> plt.xlabel("Time (s)") >>> plt.legend() >>> plt.show() """ synapse = SynapseParam('synapse', default=Lowpass(tau=0.005), readonly=True) solver = SolverParam('solver', default=LstsqL2(), readonly=True) def __init__(self, synapse=Default, solver=Default): # We can't use super here because we need the defaults mixin # in order to determine self.solver.weights. SupportDefaultsMixin.__init__(self) self.synapse = synapse self.solver = solver Solver.__init__(self, weights=self.solver.weights) def __call__(self, A, Y, __hack__=None, **kwargs): assert __hack__ is None # __hack__ is necessary prior to nengo PR #1359 (<2.6.1) # and following nengo PR #1507 (>2.8.0) # Note: mul_encoders is never called directly on self. # It is invoked on the sub-solver through the following call. return self.solver.__call__(A, Y, **kwargs)
@pytest.mark.parametrize( "reference, equal, different", ( (True, True, False), # bool (False, False, True), # bool (1.0, 1.0, 2.0), # float (1, 1, 2), # int (1.0 + 2.0j, 1 + 2j, 2.0 + 1j), # complex (b"a", b"a", b"b"), # bytes ((0, 1), (0, 1), (0, 2)), # tuple ([0, 1], [0, 1], [0, 2]), # list ("a", "a", "b"), # str (np.eye(2), np.eye(2), np.array([[0, 1], [1, 0]])), # array (DummyA(), DummyA(), DummyB()), # object instance (DummyA(1), DummyA(1), DummyA(2)), # object instance (LstsqL2(reg=0.1), LstsqL2(reg=0.1), LstsqL2(reg=0.2)), # solver ), ) def test_fingerprinting(reference, equal, different): assert str(Fingerprint(reference)) == str(Fingerprint(equal)) assert str(Fingerprint(reference)) != str(Fingerprint(different)) @pytest.mark.parametrize( "obj", ( np.array([object()]), # array np.array([(1.0, )], dtype=[("field1", "f8")]), # array object(), # object instance dummy_fn, # function ),
def modelController(neuronCount=100, tau=0.001): client = Client() # Set up neurons model = nengo.Network() with model: # Neuron ensembles error = nengo.Ensemble(neuronCount, dimensions=4, radius=100, label='error') control = nengo.Ensemble(neuronCount, dimensions=2, radius=100, label='control') enModel = nengo.Ensemble(neuronCount, dimensions=4, radius=100, label='enModel') feedback = nengo.Ensemble(neuronCount, dimensions=4, radius=100, label='feedback') oracle = nengo.Ensemble(neuronCount, dimensions=4, radius=100) # Non-neural nodes and functions socket_in = nengo.Node(client.get, size_out=8, label='socket_in') state = nengo.Node(stateFilter, size_in=8, size_out=4, label='state') goal = nengo.Node(goalFilter, size_in=8, size_out=4, label='goal') socket_out = nengo.Node(client.put, size_in=2, label='socket_out') # Neural connections # Split up socket input into current observed state and goal state nengo.Connection(socket_in, state) nengo.Connection(socket_in, goal) # Create error signal between current state and goal nengo.Connection(state, error, synapse=tau) nengo.Connection(goal, error, synapse=tau, transform=[[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, -1]]) # Naive first estimate of control signal, where f = 1*error_p + 4*error_v nengo.Connection(error, control, synapse=tau, solver=LstsqL2(weights=True), transform=[[1, 0, 10, 0], [0, 1, 0, 10]]) # Create model of system by double integrating control model_conn = nengo.Connection(control, enModel, synapse=tau, solver=LstsqL2(weights=True), transform=[[0, 0], [0, 0], [-tau, 0], [0, -tau]]) model_fb_conn = nengo.Connection(enModel, enModel, synapse=tau, solver=LstsqL2(weights=True), transform=[[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]]) # Create feedback signals that compare the models to the actual states nengo.Connection(enModel, feedback, synapse=tau, solver=LstsqL2(weights=True)) nengo.Connection(state, feedback, synapse=tau, transform=[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, -1, 0], [0, 0, 0, -1]]) # Implement learning rules for models model_conn.learning_rule_type = nengo.PES(learning_rate=1e-7) model_fb_conn.learning_rule_type = nengo.PES(learning_rate=1e-7) nengo.Connection(feedback, model_conn.learning_rule) nengo.Connection(feedback, model_fb_conn.learning_rule) # Route control signal back out to the socket nengo.Connection(control, socket_out, synapse=tau) return model
class Connection(NengoObject): """Connects two objects together. The connection between the two object is unidirectional, transmitting information from the first argument, ``pre``, to the second argument, ``post``. Almost any Nengo object can act as the pre or post side of a connection. Additionally, you can use Python slice syntax to access only some of the dimensions of the pre or post object. For example, if ``node`` has ``size_out=2`` and ``ensemble`` has ``size_in=1``: .. testcode:: with nengo.Network() as net: node = nengo.Node(np.zeros(2)) ensemble = nengo.Ensemble(10, 1) We could not create the following connection: .. testcode:: with net: nengo.Connection(node, ensemble) .. testoutput:: :hide: Traceback (most recent call last): ... nengo.exceptions.ValidationError: init: Shape of initial value () does not \ match expected shape (1, 2) But, we could create either of these two connections: .. testcode:: with net: nengo.Connection(node[0], ensemble) nengo.Connection(node[1], ensemble) Parameters ---------- pre : Ensemble or Neurons or Node The source Nengo object for the connection. post : Ensemble or Neurons or Node or LearningRule The destination object for the connection. synapse : Synapse or None, optional Synapse model to use for filtering (see `~nengo.synapses.Synapse`). If *None*, no synapse will be used and information will be transmitted without any delay (if supported by the backend---some backends may introduce a single time step delay). Note that at least one connection must have a synapse that is not *None* if components are connected in a cycle. Furthermore, a synaptic filter with a zero time constant is different from a *None* synapse as a synaptic filter will always add a delay of at least one time step. function : callable or (n_eval_points, size_mid) array_like, optional Function to compute across the connection. Note that ``pre`` must be an ensemble to apply a function across the connection. If an array is passed, the function is implicitly defined by the points in the array and the provided ``eval_points``, which have a one-to-one correspondence. transform : (size_out, size_mid) array_like, optional Linear transform mapping the pre output to the post input. This transform is in terms of the sliced size; if either pre or post is a slice, the transform must be shaped according to the sliced dimensionality. Additionally, the function is applied before the transform, so if a function is computed across the connection, the transform must be of shape ``(size_out, size_mid)``. solver : Solver, optional Solver instance to compute decoders or weights (see `~nengo.solvers.Solver`). If ``solver.weights`` is True, a full connection weight matrix is computed instead of decoders. learning_rule_type : LearningRuleType or iterable of LearningRuleType, optional Modifies the decoders or connection weights during simulation. eval_points : (n_eval_points, size_in) array_like or int, optional Points at which to evaluate ``function`` when computing decoders, spanning the interval (-pre.radius, pre.radius) in each dimension. If None, will use the eval_points associated with ``pre``. scale_eval_points : bool, optional Indicates whether the evaluation points should be scaled by the radius of the pre Ensemble. label : str, optional A descriptive label for the connection. seed : int, optional The seed used for random number generation. Attributes ---------- function : callable The given function. function_size : int The output dimensionality of the given function. If no function is specified, function_size will be 0. label : str A human-readable connection label for debugging and visualization. If not overridden, incorporates the labels of the pre and post objects. learning_rule_type : instance or list or dict of LearningRuleType, optional The learning rule types. post : Ensemble or Neurons or Node or Probe or ObjView The given post object. post_obj : Ensemble or Neurons or Node or Probe The underlying post object, even if ``post`` is an ``ObjView``. post_slice : slice or list or None The slice associated with ``post`` if it is an ObjView, or None. pre : Ensemble or Neurons or Node or ObjView The given pre object. pre_obj : Ensemble or Neurons or Node The underlying pre object, even if ``post`` is an ``ObjView``. pre_slice : slice or list or None The slice associated with ``pre`` if it is an ObjView, or None. seed : int The seed used for random number generation. solver : Solver The Solver instance that will be used to compute decoders or weights (see ``nengo.solvers``). synapse : Synapse The Synapse model used for filtering across the connection (see ``nengo.synapses``). transform : (size_out, size_mid) array_like Linear transform mapping the pre function output to the post input. Properties ---------- learning_rule : LearningRule or iterable of LearningRule Connectable learning rule object(s) associated with this connection. size_in : int The number of output dimensions of the pre object. Also the input size of the function, if one is specified. size_mid : int The number of output dimensions of the function, if specified. If the function is not specified, then ``size_in == size_mid``. size_out : int The number of input dimensions of the post object. Also the number of output dimensions of the transform. """ probeable = ("output", "input", "weights") pre = PrePostParam("pre", nonzero_size_out=True) post = PrePostParam("post", nonzero_size_in=True) synapse = SynapseParam("synapse", default=Lowpass(tau=0.005)) function_info = ConnectionFunctionParam("function", default=None, optional=True) transform = ConnectionTransformParam("transform", default=None, optional=True) solver = ConnectionSolverParam("solver", default=LstsqL2()) learning_rule_type = ConnectionLearningRuleTypeParam("learning_rule_type", default=None, optional=True) eval_points = EvalPointsParam("eval_points", default=None, optional=True, sample_shape=("*", "size_in")) scale_eval_points = BoolParam("scale_eval_points", default=True) _param_init_order = [ "pre", "post", "synapse", "eval_points", "function_info", "transform", "solver", "learning_rule_type", ] def __init__( self, pre, post, synapse=Default, function=Default, transform=Default, solver=Default, learning_rule_type=Default, eval_points=Default, scale_eval_points=Default, label=Default, seed=Default, ): super().__init__(label=label, seed=seed) self.pre = pre self.post = post self.synapse = synapse self.eval_points = eval_points # Must be set before function self.scale_eval_points = scale_eval_points self.function_info = function self.transform = transform # Must be set after function self.solver = solver # Must be set before learning rule self.learning_rule_type = learning_rule_type # set after transform def __str__(self): return self._str(include_id=False) def __repr__(self): return self._str(include_id=True) def _str(self, include_id): desc = "<Connection " if include_id: desc += f"at 0x{id(self):x} " if self.label is None: func_txt = ("" if self.function is None else f" computing '{function_name(self.function)}'") desc += f"from {self.pre} to {self.post}{func_txt}" else: desc += self.label desc += ">" return desc @property def function(self): return self.function_info.function @function.setter def function(self, function): self.function_info = function @property def has_weights(self): return not isinstance(self.transform, NoTransform) or ( isinstance(self.pre_obj, Ensemble) and not isinstance(self.pre_obj.neuron_type, Direct)) @property def is_decoded(self): warnings.warn( "is_decoded is deprecated; directly check the pre/post objects for the " "properties of interest instead", DeprecationWarning, ) return not (self.solver.weights or (isinstance(self.pre_obj, Neurons) and isinstance(self.post_obj, Neurons))) @property def _to_neurons(self): return isinstance(self.post_obj, Neurons) or (isinstance( self.pre_obj, Ensemble) and isinstance(self.post_obj, Ensemble) and self.solver.weights) @property def learning_rule(self): """(LearningRule or iterable) Connectable learning rule object(s).""" if self.learning_rule_type is None: return None types = self.learning_rule_type if isinstance(types, dict): learning_rule = type(types)() # dict of same type for k, v in types.items(): learning_rule[k] = LearningRule(self, v) elif is_iterable(types): learning_rule = [LearningRule(self, v) for v in types] elif isinstance(types, LearningRuleType): learning_rule = LearningRule(self, types) else: assert False, "Validation should catch this" return learning_rule @property def post_obj(self): return self.post.obj if isinstance(self.post, ObjView) else self.post @property def post_slice(self): return self.post.slice if isinstance(self.post, ObjView) else slice(None) @property def pre_obj(self): return self.pre.obj if isinstance(self.pre, ObjView) else self.pre @property def pre_slice(self): return self.pre.slice if isinstance(self.pre, ObjView) else slice(None) @property def size_in(self): """(int) The number of output dimensions of the pre object. Also the input size of the function, if one is specified. """ return self.pre.size_out @property def size_mid(self): """(int) The number of output dimensions of the function, if specified. If the function is not specified, then ``size_in == size_mid``. """ size = self.function_info.size return self.size_in if size is None else size @property def size_out(self): """(int) The number of input dimensions of the post object. Also the number of output dimensions of the transform. """ return self.post.size_in
def train(self, function, t, dt, process, seed=None, t_init=0, solver=LstsqL2(), rng=None): """Train an optimal linear readout. Afterwards, the decoded and filtered output will be available in the model by connecting from the ``output`` Node, or by invoking the ``run`` method. Parameters ---------- function : callable A function that maps the input signal obtained from simulating the process (as an ``M``-by-``D`` array, where ``M`` is the number of timesteps, and ``D`` is the input dimensionality), to the desired signal (of the same shape). t : float A positive number indicating how long the training signal should be in simulation seconds. dt : float A positive number indicating the time elapsed between each timestep. The length of the test signal will be ``int(t / dt)``. process : nengo.Process An autonomous process that provides a training signal of appropriate dimensionality to match the input objects. seed : int, optional (Default: ``None``) Seed used to initialize the simulator. t_init : int, optional (Default: ``0``) The number of seconds to discard from the start. solver : nengo.solvers.Solver (Default: ``nengo.solvers.LstsqL2()``) Solves for ``D`` such that ``AD ~ Y``. rng : ``numpy.random.RandomState``, optional (Default: ``None``) Random state passed to the solver. """ # Do a safety check for seeds. Note that even if the overall # network has a seed, that doesn't necessarily mean everything will # be good, because adding components to the network after training # may result in seeds being shuffled around within the objects. for ens in self.network.all_ensembles: if ens.seed is None: warnings.warn("reservoir ensemble (%s) should have its own " "seed to help ensure that its parameters do not " "change between training and testing" % ens) sim, (data_in, data_mid, _) = self.run(t, dt, process, seed) target = np.atleast_1d(function(data_in)) if target.ndim == 1: target = target[:, None] if len(data_in) != len(target): raise RuntimeError( "function expected to return signal of length %d, received %d " "instead" % (len(data_in), len(target))) offset = int(t_init / dt) decoders, info = solver(data_mid[offset:], target[offset:], rng=rng) # Update dummy node self.output.size_in = self.output.size_out = self.size_out = ( target.shape[1]) self._readout.transform = decoders.T info.update({'sim': sim, 'data_in': data_in, 'data_mid': data_mid}) return decoders, info
def pre_fixture(model): brd.add_params(model) solver = solvers.FallbackSolver( [LstsqL2(reg=0.01), solvers.CVXSolver(reg=0.01)]) model.config[nengo.Ensemble].solver = solver
@pytest.mark.parametrize( 'reference, equal, different', ( (True, True, False), # bool (False, False, True), # bool (1.0, 1.0, 2.0), # float (1, 1, 2), # int (1.0 + 2.0j, 1 + 2j, 2.0 + 1j), # complex (b'a', b'a', b'b'), # bytes ((0, 1), (0, 1), (0, 2)), # tuple ([0, 1], [0, 1], [0, 2]), # list ('a', 'a', 'b'), # str (np.eye(2), np.eye(2), np.array([[0, 1], [1, 0]])), # array (DummyA(), DummyA(), DummyB()), # object instance (DummyA(1), DummyA(1), DummyA(2)), # object instance (LstsqL2(reg=.1), LstsqL2(reg=.1), LstsqL2(reg=.2)), # solver )) def test_fingerprinting(reference, equal, different): assert str(Fingerprint(reference)) == str(Fingerprint(equal)) assert str(Fingerprint(reference)) != str(Fingerprint(different)) @pytest.mark.parametrize( 'obj', ( np.array([object()]), # array np.array([(1., )], dtype=[('field1', 'f8')]), # array { 'a': 1, 'b': 2 }, # dict
class Connection(NengoObject): """Connects two objects together. Almost any Nengo object can act as the pre or post side of a connection. Additionally, you can use Python slice syntax to access only some of the dimensions of the pre or post object. For example, if ``node`` has ``size_out=2`` and ``ensemble`` has ``size_in=1``, we could not create the following connection:: nengo.Connection(node, ensemble) But, we could create either of these two connections. nengo.Connection(node[0], ensemble) nengo.Connection(ndoe[1], ensemble) Parameters ---------- pre : Ensemble or Neurons or Node The source Nengo object for the connection. post : Ensemble or Neurons or Node or Probe The destination object for the connection. label : string A descriptive label for the connection. dimensions : int The number of output dimensions of the pre object, including `function`, but not including `transform`. eval_points : (n_eval_points, pre_size) array_like or int Points at which to evaluate `function` when computing decoders, spanning the interval (-pre.radius, pre.radius) in each dimension. synapse : float, optional Post-synaptic time constant (PSTC) to use for filtering. transform : (post_size, pre_size) array_like, optional Linear transform mapping the pre output to the post input. This transform is in terms of the sliced size; if either pre or post is a slice, the transform must be of shape (len(pre_slice), len(post_slice)). solver : Solver Instance of a Solver class to compute decoders or weights (see `nengo.solvers`). If solver.weights is True, a full connection weight matrix is computed instead of decoders. function : callable, optional Function to compute using the pre population (pre must be Ensemble). modulatory : bool, optional Specifies whether the connection is modulatory (does not physically connect to post, for use by learning rules), or not (default). eval_points : (n_eval_points, pre_size) array_like or int, optional Points at which to evaluate `function` when computing decoders, spanning the interval (-pre.radius, pre.radius) in each dimension. scale_eval_points : bool Indicates whether the eval_points should be scaled by the radius of the pre Ensemble. Defaults to True. learning_rule_type : instance or list or dict of LearningRuleType, optional Methods of modifying the connection weights during simulation. Attributes ---------- dimensions : int The number of output dimensions of the pre object, including `function`, but before applying the `transform`. function : callable The given function. function_size : int The output dimensionality of the given function. Defaults to 0. label : str A human-readable connection label for debugging and visualization. Incorporates the labels of the pre and post objects. learning_rule : LearningRule or collection of LearningRule The LearningRule objects corresponding to `learning_rule_type`, and in the same format. Use these to probe the learning rules. learning_rule_type : instance or list or dict of LearningRuleType, optional The learning rule types. post : Ensemble or Neurons or Node or Probe The given pre object. pre : Ensemble or Neurons or Node The given pre object. transform : (post_size, pre_size) array_like Linear transform mapping the pre output to the post input. modulatory : bool Whether the output of this signal is to act as an error signal for a learning rule. seed : int The seed used for random number generation. """ pre = NengoObjectParam(nonzero_size_out=True) post = NengoObjectParam(nonzero_size_in=True) synapse = SynapseParam(default=Lowpass(0.005)) transform = TransformParam(default=np.array(1.0)) solver = ConnectionSolverParam(default=LstsqL2()) function_info = ConnectionFunctionParam(default=None, optional=True) modulatory = BoolParam(default=False) learning_rule_type = ConnectionLearningRuleTypeParam(default=None, optional=True) eval_points = EvalPointsParam(default=None, optional=True, sample_shape=('*', 'size_in')) scale_eval_points = BoolParam(default=True) seed = IntParam(default=None, optional=True) probeable = ListParam(default=['output', 'input', 'transform', 'decoders']) def __init__(self, pre, post, synapse=Default, transform=Default, solver=Default, learning_rule_type=Default, function=Default, modulatory=Default, eval_points=Default, scale_eval_points=Default, seed=Default): self.pre = pre self.post = post self.probeable = Default self.solver = solver # Must be set before learning rule self.learning_rule_type = learning_rule_type self.modulatory = modulatory self.synapse = synapse self.transform = transform self.scale_eval_points = scale_eval_points self.eval_points = eval_points # Must be set before function self.function_info = function # Must be set after transform @property def function(self): return self.function_info.function @function.setter def function(self, function): self.function_info = function @property def pre_obj(self): return self.pre.obj if isinstance(self.pre, ObjView) else self.pre @property def pre_slice(self): return self.pre.slice if isinstance(self.pre, ObjView) else slice(None) @property def post_obj(self): return self.post.obj if isinstance(self.post, ObjView) else self.post @property def post_slice(self): return (self.post.slice if isinstance(self.post, ObjView) else slice(None)) @property def size_in(self): """Output size of sliced `pre`; input size of the function.""" return self.pre.size_out @property def size_mid(self): """Output size of the function; input size of the transform. If the function is None, then `size_in == size_mid`. """ size = self.function_info.size return self.size_in if size is None else size @property def size_out(self): """Output size of the transform; input size to the sliced post.""" return self.post.size_in @property def _label(self): return "from %s to %s%s" % (self.pre, self.post, " computing '%s'" % self.function.__name__ if self.function is not None else "") def __str__(self): return "<Connection %s>" % self._label def __repr__(self): return "<Connection at 0x%x %s>" % (id(self), self._label) @property def learning_rule(self): if self.learning_rule_type is not None and self._learning_rule is None: types = self.learning_rule_type if isinstance(types, dict): self._learning_rule = types.__class__() # dict of same type for k, v in iteritems(types): self._learning_rule[k] = LearningRule(self, v) elif is_iterable(types): self._learning_rule = [LearningRule(self, v) for v in types] elif isinstance(types, LearningRuleType): self._learning_rule = LearningRule(self, types) else: raise ValueError("Invalid type for `learning_rule_type`: %s" % (types.__class__.__name__)) return self._learning_rule