diff --git a/.ci/test.sh b/.ci/test.sh index b0ebee4c5..3e0183e9a 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -18,6 +18,8 @@ if [[ "$COMMAND" == "install" ]]; then pip install -e . elif [[ "$COMMAND" == "run" ]]; then coverage run -m pytest nengo_loihi -v --duration 20 --plots && coverage report +elif [[ "$COMMAND" == "run-nengo" ]]; then + pytest --pyargs nengo elif [[ "$COMMAND" == "upload" ]]; then eval "bash <(curl -s https://codecov.io/bash)" else diff --git a/.gitignore b/.gitignore index e3eaa533e..f9a6e5097 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ nengo_loihi/snips/nengo_io.c # For .ipynb examples *.pdf *.pkl +*.pkl.gz docs/examples/adaptive_motor_control.py docs/examples/communication_channel.py docs/examples/integrator.py diff --git a/.travis.yml b/.travis.yml index f001840ed..42c771671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,14 +6,16 @@ notifications: env: global: + - NENGO="false" - STATIC="false" - DOCS="false" matrix: include: - - env: PYTHON="3.5.5" - - env: PYTHON="3.5.5" STATIC="true" - - env: PYTHON="3.5.5" DOCS="true" + - env: PYTHON="3.5.2" + - env: PYTHON="3.5.2" NENGO="true" + - env: PYTHON="3.5.2" STATIC="true" + - env: PYTHON="3.5.2" DOCS="true" before_install: - source .ci/conda.sh install @@ -34,6 +36,8 @@ script: .ci/docs.sh run; elif [[ "$DOCS" == "true" ]]; then .ci/docs.sh check; + elif [[ "$NENGO" == "true" ]]; then + .ci/test.sh run-nengo; else .ci/test.sh run; fi diff --git a/CHANGES.rst b/CHANGES.rst index ca8aa5273..8d846dfaf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,10 +22,7 @@ Release history 0.4.0 (unreleased) ================== -**Changed** -- An error is now raised if - a learning rule is applied to a non-decoded connection. **Fixed** diff --git a/conftest.py b/conftest.py index aa789fea3..3a04b471d 100644 --- a/conftest.py +++ b/conftest.py @@ -3,8 +3,8 @@ import os from functools import partial -import nengo.utils.numpy as npext import matplotlib as mpl +import nengo.utils.numpy as npext import numpy as np import pytest @@ -13,7 +13,6 @@ from nengo.utils.compat import ensure_bytes import nengo_loihi - from nengo_loihi.loihi_cx import CxSimulator @@ -38,8 +37,8 @@ def pytest_addoption(parser): def pytest_report_header(config, startdir): target = config.getoption("--target") - return "Nengo Loihi is using {}".format( - "Loihi hardware" if target == "loihi" else "the Numpy simulator") + return "Nengo Loihi is using Loihi {}".format( + "hardware" if target == "loihi" else "emulator") def pytest_terminal_summary(terminalreporter): @@ -58,9 +57,9 @@ def pytest_terminal_summary(terminalreporter): def pytest_runtest_setup(item): - if (getattr(item.obj, "hang", False) and - item.config.getvalue("--target") == "loihi" and - item.config.getvalue("--no-hang")): + if (getattr(item.obj, "hang", False) + and item.config.getvalue("--target") == "loihi" + and item.config.getvalue("--no-hang")): pytest.xfail("This test causes Loihi to hang indefinitely") @@ -190,3 +189,45 @@ def _allclose(a, b, rtol=1e-5, atol=1e-8, xtol=0, return result return _allclose + + +def pytest_collection_modifyitems(session, config, items): + target = config.getoption("--target") + if target != "loihi": + return + + hanging_nengo_tests = [ + "nengo/tests/test_connection.py::test_slicing[LIF]", + "nengo/tests/test_connection.py::test_slicing[SpikingRectifiedLinear]", + "nengo/tests/test_connection.py::test_slicing_function", + "nengo/tests/test_learning_rules.py::test_slicing", + "nengo/tests/test_ensemble.py::test_vector[LIF]", + "nengo/tests/test_ensemble.py::test_vector[SpikingRectifiedLinear]", + "nengo/tests/test_neurons.py::test_direct_mode_nonfinite_value", + "nengo/tests/test_neurons.py::test_lif_min_voltage[-inf]", + "nengo/tests/test_neurons.py::test_lif_min_voltage[-1]", + "nengo/tests/test_neurons.py::test_lif_min_voltage[0]", + "nengo/tests/test_neurons.py::test_lif_zero_tau_ref", + "nengo/tests/test_node.py::test_none", + "nengo/tests/test_node.py::test_invalid_values[inf]", + "nengo/tests/test_node.py::test_invalid_values[nan]", + "nengo/tests/test_node.py::test_invalid_values[string]", + "nengo/utils/tests/test_network.py::" # no comma + "test_activate_direct_mode_learning[learning_rule0-False]", + "nengo/utils/tests/test_network.py::" # no comma + "test_activate_direct_mode_learning[learning_rule1-True]", + "nengo/utils/tests/test_network.py::" # no comma + "test_activate_direct_mode_learning[learning_rule2-True]", + "nengo/utils/tests/test_network.py::" # no comma + "test_activate_direct_mode_learning[learning_rule3-False]", + ] + deselected = [ + item for item in items if item.nodeid in hanging_nengo_tests + ] + config.hook.pytest_deselected(items=deselected) + for item in deselected: + items.remove(item) + + +nengo.conftest.RefSimulator = Simulator +nengo.conftest.Simulator = Simulator diff --git a/nengo_loihi/builder.py b/nengo_loihi/builder.py index 2bd853f67..76233b722 100644 --- a/nengo_loihi/builder.py +++ b/nengo_loihi/builder.py @@ -6,6 +6,7 @@ import nengo from nengo import Network, Ensemble, Connection, Node, Probe +from nengo.builder.connection import BuiltConnection from nengo.dists import Distribution, get_samples from nengo.connection import LearningRule from nengo.ensemble import Neurons @@ -135,6 +136,12 @@ def inter_scale(self): """ return 1. / (self.dt * self.inter_rate * self.inter_n) + def __getstate__(self): + raise NotImplementedError("Can't pickle nengo_loihi.builder.Model") + + def __setstate__(self, state): + raise NotImplementedError("Can't pickle nengo_loihi.builder.Model") + def __str__(self): return "Model: %s" % self.label @@ -326,12 +333,12 @@ def build_ensemble(model, ens): gain, bias, max_rates, intercepts = get_gain_bias( ens, rng, model.intercept_limit) - if isinstance(ens.neuron_type, nengo.Direct): - raise NotImplementedError() - else: + if isinstance(ens.neuron_type, (nengo.LIF, nengo.SpikingRectifiedLinear)): group = CxGroup(ens.n_neurons, label='%s' % ens) group.bias[:] = bias model.build(ens.neuron_type, ens.neurons, group) + else: + raise NotImplementedError() # set default filter just in case no other filter gets set group.configure_default_filter(model.inter_tau, dt=model.dt) @@ -402,11 +409,6 @@ def build_node(model, node): raise NotImplementedError() -BuiltConnection = collections.namedtuple( - 'BuiltConnection', - ('eval_points', 'solver_info', 'weights', 'transform')) - - def get_eval_points(model, conn, rng): if conn.eval_points is None: view = model.params[conn.pre_obj].eval_points.view() @@ -534,7 +536,7 @@ def build_connection(model, conn): # TODO: this identity transform may be avoidable transform = np.eye(conn.pre.size_out) else: - assert transform.ndim == 2 + assert transform.ndim == 2, "transform shape not handled yet" assert transform.shape[1] == conn.pre.size_out assert transform.shape[1] == conn.pre.size_out @@ -565,7 +567,7 @@ def build_connection(model, conn): needs_interneurons = True elif isinstance(conn.pre_obj, Neurons): assert conn.pre_slice == slice(None) - assert transform.ndim == 2 + assert transform.ndim == 2, "transform shape not handled yet" weights = transform / model.dt neuron_type = conn.pre_obj.ensemble.neuron_type else: diff --git a/nengo_loihi/loihi_cx.py b/nengo_loihi/loihi_cx.py index 212e33341..0acc76743 100644 --- a/nengo_loihi/loihi_cx.py +++ b/nengo_loihi/loihi_cx.py @@ -386,7 +386,8 @@ def set_full_weights(self, weights): assert weights.shape[0] == self.n_axons idxBits = int(np.ceil(np.log2(self.max_ind() + 1))) - assert idxBits <= SynapseFmt.INDEX_BITS_MAP[-1] + assert idxBits <= SynapseFmt.INDEX_BITS_MAP[-1], ( + "idxBits out of range, ensemble too large?") idxBits = next(i for i, v in enumerate(SynapseFmt.INDEX_BITS_MAP) if v >= idxBits) self.format(compression=3, idxBits=idxBits, fanoutType=1, @@ -904,7 +905,7 @@ def step(self): # noqa: C901 for probe in group.probes: x_slice = self.group_cxs[probe.target] p_slice = probe.slice - assert hasattr(self, probe.key) + assert hasattr(self, probe.key), "probe key not found" x = getattr(self, probe.key)[x_slice][p_slice].copy() self.probe_outputs[probe].append(x) diff --git a/nengo_loihi/loihi_interface.py b/nengo_loihi/loihi_interface.py index 966f0e965..a93d11214 100644 --- a/nengo_loihi/loihi_interface.py +++ b/nengo_loihi/loihi_interface.py @@ -358,7 +358,7 @@ def build_axons(n2core, core, group, axons, cx_ids): def build_probe(n2core, core, group, probe, cx_idxs): - assert probe.key in ('u', 'v', 's') + assert probe.key in ('u', 'v', 's'), "probe key not found" key_map = {'s': 'spike'} key = key_map.get(probe.key, probe.key) @@ -696,7 +696,7 @@ def get_probe_output(self, probe): def create_io_snip(self): # snips must be created before connecting - assert not self.is_connected() + assert not self.is_connected(), "still connected" snips_dir = os.path.join(os.path.dirname(__file__), "snips") env = jinja2.Environment( diff --git a/nengo_loihi/simulator.py b/nengo_loihi/simulator.py index 87a9b38d0..9d8879ff3 100644 --- a/nengo_loihi/simulator.py +++ b/nengo_loihi/simulator.py @@ -44,7 +44,7 @@ def __getitem__(self, key): if key in fallback: target = fallback.raw break - assert key in target + assert key in target, "probed object not found" if (key not in self._cache or len(self._cache[key]) != len(target[key])): @@ -144,13 +144,18 @@ class Simulator(object): unsupported = [] def __init__(self, network, dt=0.001, seed=None, model=None, # noqa: C901 - precompute=False, target=None): + precompute=False, target=None, progress_bar=None): self.closed = True # Start closed in case constructor raises exception + self._time = 0 + if progress_bar is not None: + raise NotImplementedError("progress bars not implemented") if model is None: # Call the builder to make a model self.model = Model(dt=float(dt), label="%s, dt=%f" % (network, dt)) else: + assert isinstance(model, Model), ( + "model is not type 'nengo_loihi.builder.Model'") self.model = model assert self.model.dt == dt @@ -295,7 +300,8 @@ def _probe(self): for probe in self.model.probes: if probe in self.model.chip2host_params: continue - assert probe.sample_every is None + assert probe.sample_every is None, ( + "probe.sample_every not implemented") assert ("loihi" not in self.sims or "emulator" not in self.sims) if "loihi" in self.sims: @@ -510,7 +516,7 @@ def run_steps(self, steps): logger.info("Finished running for %d steps", steps) self._probe() - def trange(self, sample_every=None): + def trange(self, sample_every=None, dt=None): """Create a vector of times matching probed data. Note that the range does not start at 0 as one might expect, but at diff --git a/nengo_loihi/splitter.py b/nengo_loihi/splitter.py index 205ee6990..f8f6675ed 100644 --- a/nengo_loihi/splitter.py +++ b/nengo_loihi/splitter.py @@ -154,8 +154,9 @@ def place_ensembles(networks): # User-specified config takes precedence if config[ens].on_chip is not None: networks.move(ens, "chip" if config[ens].on_chip else "host") - # Direct mode ensembles must be off chip - elif isinstance(ens.neuron_type, nengo.Direct): + # Unsupported neuron types must be off-chip + elif not isinstance(ens.neuron_type, + (nengo.LIF, nengo.SpikingRectifiedLinear)): networks.move(ens, "host") for conn in networks.original.all_connections: @@ -323,27 +324,40 @@ def split_chip_to_host(networks, conn): d=conn.size_mid, rng=np.random.RandomState(seed=seed)) - probe = nengo.Probe( - conn.pre, - synapse=None, - solver=conn.solver, - add_to_container=False, - ) - networks.chip2host_params[probe] = dict( - learning_rule_type=conn.learning_rule_type, - function=conn.function, - eval_points=conn.eval_points, - scale_eval_points=conn.scale_eval_points, - transform=transform - ) + if (isinstance(conn.pre, nengo.ensemble.Neurons) + and transform.ndim == 2): + # decoders manually specified in the transform + # should be handled like a normal decoder + probe = nengo.Probe( + conn.pre.ensemble, + synapse=None, + solver=nengo.solvers.NoSolver(transform.T), + add_to_container=False, + ) + dims = transform.shape[0] + networks.chip2host_params[probe] = dict( + learning_rule_type=conn.learning_rule_type, + function=lambda x, dims=dims: np.zeros(dims), + transform=np.array(1), + ) + else: + probe = nengo.Probe( + conn.pre, + synapse=None, + solver=conn.solver, + add_to_container=False, + ) + networks.chip2host_params[probe] = dict( + learning_rule_type=conn.learning_rule_type, + function=conn.function, + eval_points=conn.eval_points, + scale_eval_points=conn.scale_eval_points, + transform=transform + ) networks.add(probe, "chip") networks.chip2host_receivers[probe] = receive if conn.learning_rule_type is not None: - if not isinstance(conn.pre_obj, nengo.Ensemble): - raise NotImplementedError( - "Learning rule presynaptic object must be an Ensemble " - "(got %r)" % type(conn.pre_obj).__name__) networks.needs_sender[conn.learning_rule] = PESModulatoryTarget(probe) networks.remove(conn) diff --git a/nengo_loihi/tests/test_precompute.py b/nengo_loihi/tests/test_precompute.py index 356a80d0b..436c102ee 100644 --- a/nengo_loihi/tests/test_precompute.py +++ b/nengo_loihi/tests/test_precompute.py @@ -45,10 +45,10 @@ def test_precompute(allclose, Simulator, seed, plt): assert allclose(sim1.data[p_out], sim2.data[p_out], atol=0.2) -@pytest.mark.xfail(pytest.config.getoption("--target") == "loihi", - reason="Fails allclose check") @pytest.mark.skipif(pytest.config.getoption("--target") != "loihi", reason="Loihi only test") +@pytest.mark.xfail(pytest.config.getoption("--target") == "loihi", + reason="Fails allclose check") def test_input_node_precompute(allclose, Simulator, plt): input_fn = lambda t: np.sin(2 * np.pi * t) targets = ["sim", "loihi"] diff --git a/nengo_loihi/tests/test_splitter.py b/nengo_loihi/tests/test_splitter.py index 881745adf..ecd4dfad2 100644 --- a/nengo_loihi/tests/test_splitter.py +++ b/nengo_loihi/tests/test_splitter.py @@ -55,18 +55,13 @@ def test_manual_decoders( pre_probe = nengo.Probe(pre.neurons, synapse=None) post_probe = nengo.Probe(post, synapse=None) - if not use_solver and learn: - with pytest.raises(NotImplementedError): - with Simulator(model) as sim: - pass - else: - with Simulator(model) as sim: - sim.run(0.1) + with Simulator(model, precompute=False) as sim: + sim.run(0.1) - # Ensure pre population has a lot of activity - assert np.mean(sim.data[pre_probe]) > 100 - # But that post has no activity due to the zero weights - assert np.all(sim.data[post_probe] == 0) + # Ensure pre population has a lot of activity + assert np.mean(sim.data[pre_probe]) > 100 + # But that post has no activity due to the zero weights + assert np.all(sim.data[post_probe] == 0) def test_place_nodes(): @@ -235,7 +230,13 @@ def test_split_chip_to_host(): ens_onchip.neurons, ens_offchip, transform=np.ones((1, 10))), nengo.Connection( ens_onchip.neurons, node_offchip, transform=np.ones((1, 10))), + nengo.Connection(ens_onchip.neurons, node_offchip, + transform=np.ones((1, 10)), + learning_rule_type=nengo.PES()), ] + connections.append( + nengo.Connection(ens_onchip, connections[-1].learning_rule) + ) connections.append( nengo.Connection(ens_onchip, connections[1].learning_rule) ) diff --git a/pytest.ini b/pytest.ini index 6c0288b6b..a6dc64cec 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,21 @@ [pytest] -addopts = -p nengo.tests.options +addopts = -p nengo.tests.options --simulator nengo_loihi.Simulator --neurons nengo.LIF,nengo.SpikingRectifiedLinear filterwarnings = ignore:Seed will be ignored when running on Loihi log_format=%(levelname).1s %(module)-18s %(message)s log_level=DEBUG norecursedirs = .* *.egg build dist docs *.analytics *.logs *.plots +nengo_test_tolerances = + nengo/tests/test_connection.py:test_ensemble_to_neurons* atol=0.15 # originally 0.1 + nengo/tests/test_connection.py:test_node_to_ensemble* atol=0.15 # originally 0.1 + nengo/tests/test_connection.py:test_neurons_to_neurons* atol=0.15 # originally 0.1 + nengo/tests/test_connection.py:test_function_and_transform atol=0.4 # originally 0.1 + nengo/tests/test_connection.py:test_weights* atol=0.2 # originally 0.1 + nengo/tests/test_connection.py:test_vector* atol=0.15 # originally 0.1 + nengo/tests/test_connection.py:test_slicing* atol=0.4 # originally 0.1 + nengo/tests/test_connection.py:test_function_output_size atol=0.3 # originally 0.1 + nengo/tests/test_connection.py:test_function_points* atol=0.15 # originally 0.05 + nengo/tests/test_ensemble.py:test_scalar* + nengo/tests/test_ensemble.py:test_scalar* atol=0.2 # originally 0.1 + nengo/tests/test_ensemble.py:test_vector* + nengo/tests/test_ensemble.py:test_vector* atol=0.15 # originally 0.1