├── nengo_fpga ├── tests │ ├── __init__.py │ ├── test_misc.py │ ├── conftest.py │ ├── test_config.py │ ├── test_simulator.py │ ├── test_fullstack.py │ ├── fixtures.py │ └── test_id.py ├── utils │ ├── __init__.py │ ├── paths.py │ └── fileio.py ├── networks │ └── __init__.py ├── version.py ├── __init__.py ├── fpga_config.py ├── simulator.py └── id_extractor.py ├── docs ├── _static │ ├── de1.png │ ├── mnist.gif │ ├── pynq.png │ ├── pynqz2.png │ ├── rl_gui.png │ ├── favicon.ico │ ├── pendulum.gif │ ├── mnist_gui.png │ ├── rl_exploit.gif │ ├── rl_explore.gif │ ├── rl_no-learn.gif │ ├── pendulum_gui.png │ ├── pendulum_no-learn.gif │ └── custom.css ├── examples │ ├── notebooks │ │ ├── nengorc │ │ ├── anim_utils.py │ │ ├── 00-communication-channel.ipynb │ │ ├── 03-integrator.ipynb │ │ └── 02-set-neuron-params.ipynb │ ├── basic_example.py │ └── gui │ │ ├── 00-mnist-vision-network.py.cfg │ │ ├── 01-adaptive-pendulum.py.cfg │ │ ├── 00-mnist-vision-network.py │ │ ├── 01-adaptive-pendulum.py │ │ ├── 02-RL-demo.py.cfg │ │ └── 02-RL-demo.py ├── project.rst ├── contact.rst ├── index.rst ├── api.rst ├── conf.py ├── appendix.rst ├── usage.rst └── getting-started.rst ├── pytest.ini ├── .gitlint ├── .pre-commit-config.yaml ├── .gitignore ├── pyproject.toml ├── CONTRIBUTORS.rst ├── .templates ├── remote-script.sh.template ├── LICENSE.rst.template └── slurm-script.sh.template ├── README.rst ├── fpga_config ├── CONTRIBUTING.rst ├── .github ├── workflows │ └── ci.yml └── pull_request_template.md ├── setup.py ├── LICENSE.rst ├── .nengobones.yml ├── setup.cfg └── CHANGES.rst /nengo_fpga/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Include tests in the package.""" 2 | -------------------------------------------------------------------------------- /docs/_static/de1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/de1.png -------------------------------------------------------------------------------- /docs/_static/mnist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/mnist.gif -------------------------------------------------------------------------------- /docs/_static/pynq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/pynq.png -------------------------------------------------------------------------------- /docs/_static/pynqz2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/pynqz2.png -------------------------------------------------------------------------------- /docs/_static/rl_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/rl_gui.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/pendulum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/pendulum.gif -------------------------------------------------------------------------------- /docs/_static/mnist_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/mnist_gui.png -------------------------------------------------------------------------------- /docs/_static/rl_exploit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/rl_exploit.gif -------------------------------------------------------------------------------- /docs/_static/rl_explore.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/rl_explore.gif -------------------------------------------------------------------------------- /docs/_static/rl_no-learn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/rl_no-learn.gif -------------------------------------------------------------------------------- /docs/_static/pendulum_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/pendulum_gui.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | fullstack: Tests that use the full NengoFPGA stack, incl. hardware. 4 | -------------------------------------------------------------------------------- /docs/examples/notebooks/nengorc: -------------------------------------------------------------------------------- 1 | [progress] 2 | progress_bar = False 3 | 4 | [decoder_cache] 5 | enabled = False 6 | -------------------------------------------------------------------------------- /docs/_static/pendulum_no-learn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nengo/nengo-fpga/HEAD/docs/_static/pendulum_no-learn.gif -------------------------------------------------------------------------------- /nengo_fpga/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides utility functions and path information.""" 2 | 3 | from . import fileio, paths 4 | -------------------------------------------------------------------------------- /nengo_fpga/networks/__init__.py: -------------------------------------------------------------------------------- 1 | """Self contained networks to be run on the FPGA.""" 2 | 3 | from .fpga_pes_ensemble_network import FpgaPesEnsembleNetwork 4 | -------------------------------------------------------------------------------- /.gitlint: -------------------------------------------------------------------------------- 1 | [general] 2 | ignore=body-is-missing 3 | 4 | [title-max-length] 5 | line-length=50 6 | 7 | [B1] 8 | # body line length 9 | line-length=72 10 | 11 | [title-match-regex] 12 | regex=^[A-Z] 13 | -------------------------------------------------------------------------------- /docs/project.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Project Information 3 | ******************* 4 | 5 | .. include:: ../CHANGES.rst 6 | 7 | .. include:: ../CONTRIBUTING.rst 8 | 9 | .. include:: ../LICENSE.rst 10 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | .MathJax .mi, .MathJax .mo { 2 | color: inherit; 3 | } 4 | 5 | pre, kdb, samp, code, .rst-content tt, .rst-content code { 6 | word-break: inherit; 7 | } 8 | 9 | code, .rst-content tt, .rst-content code { 10 | white-space: pre-wrap; 11 | } 12 | 13 | #right-column .header div { 14 | float: left; 15 | } 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 21.12b0 6 | hooks: 7 | - id: black 8 | files: \.py$ 9 | - repo: https://github.com/pycqa/isort 10 | rev: 5.6.4 11 | hooks: 12 | - id: isort 13 | files: \.py$ 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.class 3 | *.dist-info/ 4 | *.egg-info 5 | *.eggs/ 6 | *.py[co] 7 | *.swp 8 | *~ 9 | .DS_Store 10 | .cache 11 | .coverage 12 | .idea 13 | .ipynb_checkpoints/ 14 | .pytest_cache/ 15 | .tox 16 | .vagrant 17 | .vscode 18 | Vagrantfile 19 | _build 20 | bones-scripts 21 | build 22 | dist 23 | docs/.doctrees 24 | htmlcov 25 | id_*.txt 26 | log.txt 27 | mnist.pkl.gz 28 | wintest.sh 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | [build-system] 4 | requires = ["setuptools<64", "wheel"] 5 | 6 | [tool.black] 7 | target-version = ['py38'] 8 | 9 | [tool.isort] 10 | profile = "black" 11 | src_paths = ["nengo_fpga"] 12 | 13 | [tool.docformatter] 14 | wrap-summaries = 88 15 | wrap-descriptions = 81 16 | pre-summary-newline = true 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | .. Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | ********************** 4 | NengoFPGA contributors 5 | ********************** 6 | 7 | See https://github.com/nengo/nengo-fpga/graphs/contributors 8 | for a list of the people who have committed to NengoFPGA. 9 | Thank you for your contributions! 10 | 11 | For the full list of the many contributors to the Nengo ecosystem, 12 | see https://www.nengo.ai/people/. 13 | -------------------------------------------------------------------------------- /docs/contact.rst: -------------------------------------------------------------------------------- 1 | .. _contact: 2 | 3 | ********** 4 | Contact Us 5 | ********** 6 | 7 | For questions relating to code usage of the NengoFPGA software, please visit 8 | the `Nengo forums `_. 9 | 10 | For general support questions (hardware or software related), contact us directly 11 | by sending an email to `support@appliedbrainresearch.com`_. 12 | To help our support team provide a prompt response, please start your 13 | subject header with the term "NengoFPGA". 14 | 15 | .. _support@appliedbrainresearch.com: 16 | mailto:support@appliedbrainresearch.com?subject=NengoFPGA\ -\ 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | NengoFPGA 3 | ********* 4 | 5 | NengoFPGA is an extension of :std:doc:`Nengo ` 6 | that allows portions of a network to be run on an FPGA to improve performance 7 | and efficiency. 8 | 9 | Currently NengoFPGA supports the Xilinx PYNQ board from Digilent and TUL, and the 10 | Intel Altera DE1 SoC from Terasic. Each has a slightly different implementation 11 | but NengoFPGA provides a unified frontend for both devices. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | getting-started 17 | usage 18 | examples 19 | api 20 | appendix 21 | contact 22 | project 23 | 24 | .. 25 | .. todolist:: 26 | -------------------------------------------------------------------------------- /nengo_fpga/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | """Tests for some misc incidental functions.""" 2 | from importlib import reload 3 | 4 | import nengo 5 | import pytest 6 | 7 | from nengo_fpga.utils import paths 8 | 9 | 10 | def test_monkey_patch(dummy_net): 11 | """Check monkey patch from __init__ fails with wrong type.""" 12 | 13 | with pytest.raises(nengo.exceptions.NetworkContextError): 14 | with dummy_net: 15 | _ = nengo.Node() 16 | 17 | 18 | def test_windows_path(mocker): 19 | """We test on Linux, so pretend we are on Windows.""" 20 | 21 | mocker.patch("sys.platform", "windows") 22 | reload(paths) 23 | 24 | assert paths.config_dir.endswith(".nengo") 25 | -------------------------------------------------------------------------------- /nengo_fpga/utils/paths.py: -------------------------------------------------------------------------------- 1 | """Provides standard paths to configs and resources.""" 2 | 3 | import os 4 | import sys 5 | 6 | import nengo 7 | 8 | if sys.platform.startswith("win"): 9 | config_dir = os.path.expanduser(os.path.join("~", ".nengo")) 10 | else: 11 | config_dir = os.path.expanduser(os.path.join("~", ".config", "nengo")) 12 | 13 | install_dir = os.path.abspath( 14 | os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) 15 | ) 16 | nengo_dir = os.path.abspath(os.path.join(os.path.dirname(nengo.__file__), os.pardir)) 17 | examples_dir = os.path.join(install_dir, "examples") 18 | 19 | fpga_config = { 20 | "nengo": os.path.join(nengo_dir, "nengo-data", "fpga_config"), 21 | "system": os.path.join(install_dir, "fpga_config"), 22 | "user": os.path.join(config_dir, "fpga_config"), 23 | "project": os.path.abspath(os.path.join(os.curdir, "fpga_config")), 24 | } 25 | -------------------------------------------------------------------------------- /nengo_fpga/version.py: -------------------------------------------------------------------------------- 1 | # Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | # pylint: disable=consider-using-f-string,bad-string-format-type 4 | 5 | """ 6 | NengoFPGA version information. 7 | 8 | We use semantic versioning (see http://semver.org/) and conform to PEP440 (see 9 | https://www.python.org/dev/peps/pep-0440/). '.dev0' will be added to the version 10 | unless the code base represents a release version. Release versions are git 11 | tagged with the version. 12 | """ 13 | 14 | version_info = (0, 2, 3) 15 | 16 | name = "nengo-fpga" 17 | dev = 0 18 | 19 | # use old string formatting, so that this can still run in Python <= 3.5 20 | # (since this file is parsed in setup.py, before python_requires is applied) 21 | version = ".".join(str(v) for v in version_info) 22 | if dev is not None: 23 | version += ".dev%d" % dev # pragma: no cover 24 | 25 | copyright = "Copyright (c) 2018-2023 Applied Brain Research" 26 | -------------------------------------------------------------------------------- /nengo_fpga/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0611 2 | """Setup pytest environment to include fixtures.""" 3 | import pytest 4 | 5 | from nengo_fpga.tests.fixtures import ( 6 | config_contents, 7 | dummy_com, 8 | dummy_extractor, 9 | dummy_net, 10 | dummy_sim, 11 | gen_configs, 12 | params, 13 | ) 14 | 15 | 16 | def pytest_addoption(parser): 17 | """Add fullstack runtime arg.""" 18 | parser.addoption( 19 | "--fullstack", 20 | action="store_true", 21 | default=False, 22 | help="Also run the fullstack tests", 23 | ) 24 | 25 | 26 | def pytest_collection_modifyitems(config, items): 27 | """Do not run the fullstack tests by default.""" 28 | if not config.getvalue("fullstack"): 29 | skip_fullstack = pytest.mark.skip("Fullstack tests skipped by default") 30 | for item in items: 31 | if item.get_closest_marker("fullstack"): 32 | item.add_marker(skip_fullstack) 33 | -------------------------------------------------------------------------------- /.templates/remote-script.sh.template: -------------------------------------------------------------------------------- 1 | {% extends "templates/remote-script.sh.template" %} 2 | 3 | {% block remote_install %} 4 | {{ super() }} 5 | 6 | FPGA_CONFIG="\$HOME/fpga_config" 7 | 8 | # A valid fpga_config should be in the $HOME dir of the remote machine 9 | if [ ! -f "\$FPGA_CONFIG" ]; then 10 | echo "ERROR: There must be a valid fpga_config file in \$HOME" 11 | exit 1 12 | fi 13 | 14 | cp -f "\$FPGA_CONFIG" ~/"$BUILD_DIR"/{{ pkg }} 15 | 16 | {% endblock %} 17 | 18 | {% block after_script %} 19 | # TODO: Remove this block when incorporated into NengoBones 20 | {% if remote_script.endswith("docs") %} 21 | REMOTE_FAILED_FILE="tmp/nengo-fpga-$TRAVIS_JOB_NUMBER/{{ pkg }}/$TRAVIS_JOB_NUMBER.failed" 22 | ssh -q {{ host }} [[ -e "$REMOTE_FAILED_FILE" ]] && exe scp {{ host }}:"$REMOTE_FAILED_FILE" . 23 | exe rsync -azh "{{ host }}:./tmp/{{ pkg }}-$TRAVIS_JOB_NUMBER/{{ pkg }}-docs" .. 24 | {% endif %} 25 | {{ super() }} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://www.nengo.ai/design/_images/nengo-fpga-full-light.svg 2 | :target: https://www.nengo.ai/nengo-fpga 3 | :alt: NengoFPGA 4 | :width: 400px 5 | 6 | ************************ 7 | Connecting Nengo to FPGA 8 | ************************ 9 | 10 | NengoFPGA is an extension for `Nengo `_ that 11 | connects to FPGA. The current implementation supports a single ensemble of 12 | neurons with the PES learning rule. 13 | 14 | You will need a 15 | `supported FPGA device `_, 16 | then simply replace a standard ensemble with the augmented FPGA ensemble: 17 | 18 | .. code-block:: python 19 | 20 | import nengo 21 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 22 | 23 | # This standard Nengo ensemble... 24 | ens = nengo.Ensemble(50, 1) 25 | 26 | # ...is replaced with an FPGA ensemble 27 | fpga_ens = FpgaPesEnsembleNetwork( 28 | 'de1', n_neurons=50, dimensions=1, learning_rate=0) 29 | 30 | Check out the rest of the `documentation `_ 31 | for more information. 32 | -------------------------------------------------------------------------------- /fpga_config: -------------------------------------------------------------------------------- 1 | # Example board_config file. Uncomment the following entries (remove #) 2 | # to use. 3 | # More information on the different configuration options can be found at: 4 | # https://www.nengo.ai/nengo-fpga/getting_started.html#fpga-board 5 | 6 | # # Example host (PC) configuration 7 | # [host] 8 | # ip = 10.162.177.10 9 | 10 | # # Example DE1 FPGA board configuration 11 | # [de1] 12 | # ip = 10.162.177.236 13 | # ssh_port = 22 14 | # ssh_user = root 15 | # ssh_pwd = 16 | # # Refer to the online documentation for SSH key configuration options 17 | # remote_script = /opt/nengo-de1/nengo_de1/single_pes_net.py 18 | # id_script = /opt/nengo-de1/nengo_de1/id_script.py 19 | # remote_tmp = /opt/nengo-de1/params 20 | # udp_port = 0 21 | 22 | # # Example PYNQ FPGA board configuration 23 | # [pynq] 24 | # ip = 10.162.177.99 25 | # ssh_port = 22 26 | # ssh_user = xilinx 27 | # ssh_pwd = xilinx 28 | # # Refer to the online documentation for SSH key configuration options 29 | # remote_script = /opt/nengo-pynq/nengo_pynq/single_pes_net.py 30 | # id_script = /opt/nengo-pynq/nengo_pynq/id_script.py 31 | # remote_tmp = /opt/nengo-pynq/params 32 | # udp_port = 0 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | ************************* 4 | Contributing to NengoFPGA 5 | ************************* 6 | 7 | Issues and pull requests are always welcome! 8 | We appreciate help from the community to make NengoFPGA better. 9 | 10 | Filing issues 11 | ============= 12 | 13 | If you find a bug in NengoFPGA, 14 | or think that a certain feature is missing, 15 | please consider 16 | `filing an issue `_! 17 | Please search the currently open issues first 18 | to see if your bug or feature request already exists. 19 | If so, feel free to add a comment to the issue 20 | so that we know that multiple people are affected. 21 | 22 | Making pull requests 23 | ==================== 24 | 25 | If you want to fix a bug or add a feature to NengoFPGA, 26 | we welcome pull requests. 27 | Ensure that you fill out all sections of the pull request template, 28 | deleting the comments as you go. 29 | 30 | Contributor agreement 31 | ===================== 32 | 33 | We require that all contributions be covered under 34 | our contributor assignment agreement. Please see 35 | `the agreement `_ 36 | for instructions on how to sign. 37 | 38 | More details 39 | ============ 40 | 41 | For more details on how to contribute to Nengo, 42 | please see the `developer guide `_. 43 | -------------------------------------------------------------------------------- /nengo_fpga/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | NengoFPGA 3 | ========= 4 | 5 | NengoFPGA provides an FPGA backend for Nengo (https://www.github.com/nengo/nengo). 6 | 7 | The source code repository for this package is found at 8 | https://www.github.com/nengo/nengo-fpga. Examples of models can be found 9 | in the `examples` directory of the source code repository. 10 | """ 11 | 12 | import nengo 13 | 14 | from . import id_extractor, utils 15 | from .fpga_config import fpga_config 16 | from .simulator import Simulator 17 | from .version import version as __version__ 18 | 19 | __copyright__ = "2013-2021, Applied Brain Research" 20 | __license__ = "Free for non-commercial use; see LICENSE.rst" 21 | 22 | 23 | # Only patch if we haven't patched already 24 | if nengo.Network.add.__module__ == "nengo.network": 25 | net_add = nengo.Network.add 26 | 27 | def add(obj): 28 | """ 29 | monkey-patch Network.add so that we can give a better error message if someone 30 | tries to add new objects. 31 | 32 | TODO: it'd be nice to do this without the monkey-patching, but we'll 33 | probably have to modify nengo 34 | """ 35 | try: 36 | net_add(obj) 37 | except AttributeError as e: 38 | net_type = type(nengo.Network.context[-1]) 39 | raise nengo.exceptions.NetworkContextError( 40 | f"Cannot add new objects to a {net_type}" 41 | ) from e 42 | 43 | nengo.Network.add = staticmethod(add) 44 | -------------------------------------------------------------------------------- /.templates/LICENSE.rst.template: -------------------------------------------------------------------------------- 1 | {% include "templates/LICENSE.rst.template" %} 2 | 3 | This license pertains only to the NengoFPGA interface software. License information 4 | for the supported hardware implementations can be found at their respective license 5 | pages: 6 | 7 | * `NengoDE1 `_ 8 | * `NengoPYNQ `_ 9 | 10 | Licensed Code 11 | ============= 12 | 13 | NengoFPGA imports or vendorizes several open source libraries. 14 | 15 | * `NumPy `_ - Used under 16 | `BSD license `__ 17 | * `Sphinx `_ - Used under 18 | `BSD license `__ 19 | * `numpydoc `_ - Used under 20 | `BSD license `__ 21 | * `IPython `_ - Used under 22 | `BSD license `__ 23 | * `Paramiko `_ - Used under 24 | `LGPL license `__ 25 | * `matplotlib `_ - Used under 26 | `modified PSF license `__ 27 | * `Pillow `__ Used under 28 | `Open Source PIL Software License `__ 29 | -------------------------------------------------------------------------------- /nengo_fpga/fpga_config.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=too-many-ancestors,logging-format-interpolation 2 | 3 | """Read NengoFPGA config that describes available FPGA devices.""" 4 | 5 | import configparser 6 | import logging 7 | 8 | import nengo_fpga.utils.paths 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | fpga_config_files = [ 13 | nengo_fpga.utils.paths.fpga_config["nengo"], 14 | nengo_fpga.utils.paths.fpga_config["system"], 15 | nengo_fpga.utils.paths.fpga_config["user"], 16 | nengo_fpga.utils.paths.fpga_config["project"], 17 | ] 18 | 19 | 20 | class _FPGA_CONFIG(configparser.ConfigParser): 21 | def __init__(self): 22 | configparser.ConfigParser.__init__(self) 23 | self.reload_config() 24 | 25 | def _clear(self): 26 | for s in self.sections(): 27 | self.remove_section(s) 28 | 29 | def read(self, filenames): 30 | """Read config file.""" 31 | logger.info("Reading FPGA configurations from %s", filenames) 32 | return configparser.ConfigParser.read(self, filenames) 33 | 34 | def item_dict(self, section): 35 | """Organize config in a dictionary.""" 36 | items = self.items(section) 37 | item_dict = {} 38 | 39 | for k, v in items: 40 | item_dict[k] = v 41 | 42 | return item_dict 43 | 44 | def reload_config(self, filenames=None): 45 | """Reload configs in case of changes.""" 46 | if filenames is None: 47 | filenames = fpga_config_files 48 | 49 | self._clear() 50 | self.read(filenames) 51 | 52 | 53 | fpga_config = _FPGA_CONFIG() 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI testing 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: 6 | - main 7 | - release-candidate-* 8 | tags: 9 | - v* 10 | workflow_dispatch: 11 | inputs: 12 | debug_enabled: 13 | description: Run the build with SSH debugging enabled 14 | type: boolean 15 | required: false 16 | default: false 17 | 18 | defaults: 19 | run: 20 | shell: bash -el {0} 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 60 26 | strategy: 27 | matrix: 28 | include: 29 | - script: static 30 | - script: test 31 | coverage-name: test 32 | - script: docs 33 | fail-fast: false 34 | env: 35 | SSH_KEY: ${{ secrets.SSH_KEY }} 36 | SSH_CONFIG: ${{ secrets.SSH_CONFIG }} 37 | GH_TOKEN: ${{ secrets.PUBLIC_GH_TOKEN }} 38 | steps: 39 | - uses: nengo/nengo-bones/actions/setup@main 40 | with: 41 | python-version: "3.8" 42 | - name: Install pandoc 43 | if: ${{ matrix.script == 'docs' }} 44 | run: | 45 | micromamba install pandoc 46 | - name: Install ffmpeg for docs 47 | if: ${{ matrix.script == 'docs' }} 48 | uses: FedericoCarboni/setup-ffmpeg@v2 49 | - uses: nengo/nengo-bones/actions/generate-and-check@main 50 | - uses: nengo/nengo-bones/actions/run-script@main 51 | with: 52 | name: ${{ matrix.script }} 53 | - uses: actions/upload-artifact@v3 54 | if: ${{ always() && matrix.coverage-name }} 55 | with: 56 | name: coverage-${{ matrix.coverage-name }} 57 | path: .coverage 58 | coverage: 59 | runs-on: ubuntu-latest 60 | timeout-minutes: 10 61 | needs: 62 | - test 63 | if: ${{ always() }} 64 | steps: 65 | - uses: nengo/nengo-bones/actions/coverage-report@main 66 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | *** 2 | API 3 | *** 4 | 5 | Setting Parameters 6 | ================== 7 | 8 | Many of the Nengo ensemble parameters are omitted from the 9 | ``FpgaPesEnsembleNetwork`` constructor, but all of these ensemble parameters can 10 | easily be changed once an ensemble is created: 11 | 12 | .. code-block:: python 13 | 14 | # First create the ensemble 15 | ens = FpgaPesEnsembleNetwork( 16 | 'de1', n_neurons=100, dimensions=2, learning_rate=1e-3) 17 | 18 | # Modify ensemble parameters 19 | ens.ensemble.neuron_type = nengo.neurons.RectifiedLinear() 20 | ens.ensemble.intercepts = nengo.dists.Choice([-0.5]) 21 | ens.ensemble.max_rates = nengo.dists.Choice([100]) 22 | 23 | Since our ``FpgaPesEnsembleNetwork`` class also encompasses the connection from 24 | the FPGA ensemble, we can similarly change the connection parameters: 25 | 26 | .. code-block:: python 27 | 28 | # Modify connection parameters 29 | ens.connection.synapse = None 30 | ens.connection.solver = nengo.solvers.LstsqL2(reg=0.01) 31 | 32 | The ``02-mnist_vision_network`` example demonstrates this capability. 33 | 34 | If a recurrent connection exists, we can similarly modify that connection: 35 | 36 | .. code-block:: python 37 | 38 | # Modify connection parameters 39 | ens.feedback.synapse = 0.05 40 | ens.feedback.function = lambda x: x*0.5 41 | 42 | 43 | .. seealso:: 44 | Check out the Nengo documentation for a full list of `ensemble parameters 45 | ` and 46 | `connection parameters `. 47 | 48 | 49 | Supported Neuron Types 50 | ====================== 51 | 52 | Currently NengoFPGA supports the following neuron types: 53 | 54 | - `nengo.RecitifiedLinear ` 55 | - `nengo.SpikingRectifiedLinear ` 56 | 57 | 58 | Objects and Functions 59 | ===================== 60 | 61 | .. autoclass:: nengo_fpga.networks.FpgaPesEnsembleNetwork 62 | -------------------------------------------------------------------------------- /nengo_fpga/utils/fileio.py: -------------------------------------------------------------------------------- 1 | """Provides helper functions dealing with file I/O.""" 2 | 3 | import numpy 4 | from numpy.compat import isfileobj, pickle 5 | from numpy.lib.format import ( 6 | _check_version, 7 | _write_array_header, 8 | header_data_from_array_1_0, 9 | ) 10 | 11 | 12 | def write_array( 13 | fp, array, version=None, allow_pickle=True, pickle_kwargs=None 14 | ): # pragma: no cover 15 | """This is basically the NumPy numpy.lib.format.write_array function with the only 16 | change being pickle protocol 2 is used to dump the data (instead of 3 in the most 17 | recent release of NumPy)""" 18 | _check_version(version) 19 | _write_array_header(fp, header_data_from_array_1_0(array), version) 20 | 21 | if array.itemsize == 0: 22 | buffersize = 0 23 | else: 24 | # Set buffer size to 16 MiB to hide the Python loop overhead. 25 | buffersize = max(16 * 1024**2 // array.itemsize, 1) 26 | 27 | if array.dtype.hasobject: 28 | # We contain Python objects so we cannot write out the data 29 | # directly. Instead, we will pickle it out 30 | if not allow_pickle: 31 | raise ValueError("Object arrays cannot be saved when allow_pickle=False") 32 | if pickle_kwargs is None: 33 | pickle_kwargs = {} 34 | 35 | pickle.dump(array, fp, protocol=2, **pickle_kwargs) 36 | elif array.flags.f_contiguous and not array.flags.c_contiguous: 37 | if isfileobj(fp): 38 | array.T.tofile(fp) 39 | else: 40 | for chunk in numpy.nditer( 41 | array, 42 | flags=["external_loop", "buffered", "zerosize_ok"], 43 | buffersize=buffersize, 44 | order="F", 45 | ): 46 | fp.write(chunk.tobytes("C")) 47 | else: 48 | if isfileobj(fp): 49 | array.tofile(fp) 50 | else: 51 | for chunk in numpy.nditer( 52 | array, 53 | flags=["external_loop", "buffered", "zerosize_ok"], 54 | buffersize=buffersize, 55 | order="C", 56 | ): 57 | fp.write(chunk.tobytes("C")) 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | import io 4 | import pathlib 5 | import runpy 6 | 7 | try: 8 | from setuptools import find_packages, setup 9 | except ImportError: 10 | raise ImportError( 11 | "'setuptools' is required but not installed. To install it, " 12 | "follow the instructions at " 13 | "https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py" 14 | ) 15 | 16 | 17 | def read(*filenames, **kwargs): 18 | encoding = kwargs.get("encoding", "utf-8") 19 | sep = kwargs.get("sep", "\n") 20 | buf = [] 21 | for filename in filenames: 22 | with io.open(filename, encoding=encoding) as f: 23 | buf.append(f.read()) 24 | return sep.join(buf) 25 | 26 | 27 | root = pathlib.Path(__file__).parent 28 | version = runpy.run_path(str(root / "nengo_fpga" / "version.py"))["version"] 29 | 30 | install_req = [ 31 | "nengo>=3.0.0", 32 | "numpy>=1.13.0", 33 | "paramiko>=2.4.1", 34 | ] 35 | docs_req = [ 36 | "sphinx>=1.8", 37 | "jupyter", 38 | "matplotlib>=1.4", 39 | "nbsphinx", 40 | "numpydoc>=0.6", 41 | "nengo_sphinx_theme>=0.12.0", 42 | ] 43 | optional_req = [] 44 | tests_req = [ 45 | "nengo[tests]>=3.0.0", 46 | "pytest>=3.6", 47 | "pytest-mock>=2.0", 48 | "pytest-cov>=2.6", 49 | ] 50 | 51 | setup( 52 | name="nengo-fpga", 53 | version=version, 54 | author="Applied Brain Research", 55 | author_email="info@appliedbrainresearch.com", 56 | packages=find_packages(), 57 | url="https://www.nengo.ai/nengo-fpga", 58 | include_package_data=False, 59 | license="Proprietary", 60 | description="FPGA backend for Nengo", 61 | long_description=read("README.rst", "CHANGES.rst"), 62 | zip_safe=False, 63 | install_requires=install_req, 64 | extras_require={ 65 | "all": docs_req + optional_req + tests_req, 66 | "docs": docs_req, 67 | "optional": optional_req, 68 | "tests": tests_req, 69 | }, 70 | python_requires=">=3.8", 71 | package_data={ 72 | "nengo_fpga": [ 73 | "fpga_config", 74 | ], 75 | }, 76 | entry_points={ 77 | "nengo.backends": [ 78 | "fpga = nengo_fpga:Simulator", 79 | ], 80 | }, 81 | classifiers=[ 82 | "License :: Other/Proprietary License", 83 | ], 84 | ) 85 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | .. Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | ***************** 4 | NengoFPGA license 5 | ***************** 6 | 7 | Copyright (c) 2018-2023 Applied Brain Research 8 | 9 | **ABR License** 10 | 11 | NengoFPGA is made available under a proprietary license, the 12 | "ABR TECHNOLOGY LICENSE AND USE AGREEMENT" (the "ABR License"). 13 | The main ABR License file is available for download at 14 | ``_. 15 | The entire contents of this ``LICENSE.rst`` file, including any 16 | terms and conditions herein, form part of the ABR License. 17 | 18 | Commercial Use Licenses are available to purchase for a yearly fee. 19 | Academic and Personal Use Licenses for NengoFPGA are available at 20 | a reduced cost. 21 | Both types of licences can be obtained from the 22 | ABR store at ``_. 23 | 24 | If you have any sales questions, 25 | please contact ``_. 26 | If you have any technical support questions, please post them on the ABR 27 | community forums at ``_ or contact 28 | ``_. 29 | 30 | 31 | This license pertains only to the NengoFPGA interface software. License information 32 | for the supported hardware implementations can be found at their respective license 33 | pages: 34 | 35 | * `NengoDE1 `_ 36 | * `NengoPYNQ `_ 37 | 38 | Licensed Code 39 | ============= 40 | 41 | NengoFPGA imports or vendorizes several open source libraries. 42 | 43 | * `NumPy `_ - Used under 44 | `BSD license `__ 45 | * `Sphinx `_ - Used under 46 | `BSD license `__ 47 | * `numpydoc `_ - Used under 48 | `BSD license `__ 49 | * `IPython `_ - Used under 50 | `BSD license `__ 51 | * `Paramiko `_ - Used under 52 | `LGPL license `__ 53 | * `matplotlib `_ - Used under 54 | `modified PSF license `__ 55 | * `Pillow `__ Used under 56 | `Open Source PIL Software License `__ 57 | -------------------------------------------------------------------------------- /.nengobones.yml: -------------------------------------------------------------------------------- 1 | project_name: NengoFPGA 2 | pkg_name: nengo_fpga 3 | repo_name: nengo/nengo-fpga 4 | 5 | description: FPGA backend for Nengo 6 | copyright_start: 2018 7 | license: abr-nonfree 8 | main_branch: main 9 | 10 | license_rst: {} 11 | 12 | contributing_rst: {} 13 | 14 | contributors_rst: {} 15 | 16 | setup_cfg: {} 17 | 18 | setup_py: 19 | package_data: 20 | nengo_fpga: 21 | - fpga_config 22 | entry_points: 23 | nengo.backends: 24 | - fpga = nengo_fpga:Simulator 25 | install_req: 26 | - nengo>=3.0.0 27 | - numpy>=1.13.0 28 | - paramiko>=2.4.1 29 | docs_req: 30 | - sphinx>=1.8 31 | - jupyter 32 | - matplotlib>=1.4 33 | - nbsphinx 34 | - numpydoc>=0.6 35 | - nengo_sphinx_theme>=0.12.0 36 | tests_req: 37 | - nengo[tests]>=3.0.0 38 | - pytest>=3.6 39 | - pytest-mock>=2.0 40 | - pytest-cov>=2.6 41 | 42 | docs_conf_py: 43 | intersphinx_mapping: 44 | nengo-de1: "https://www.nengo.ai/nengo-de1/" 45 | nengo-pynq: "https://www.nengo.ai/nengo-pynq/" 46 | html_redirects: 47 | getting_started.html: getting-started.html 48 | examples/notebooks/00-communication_channel.html: examples/notebooks/00-communication-channel.html 49 | examples/notebooks/01-learn_communication_channel.html: examples/notebooks/01-learn-communication-channel.html 50 | examples/notebooks/02-set_neuron_params.html: examples/notebooks/02-set-neuron-params.html 51 | examples/notebooks/05-controlled_oscillator.html: examples/notebooks/05-controlled-oscillator.html 52 | examples/notebooks/06-chaotic_attractor.html: examples/notebooks/06-chaotic-attractor.html 53 | nengo_logo: nengo-fpga-full-light.svg 54 | nengo_logo_color: "#541a8b" 55 | sphinx_options: 56 | linkcheck_ignore: [(https:\/\/www\.tulembedded\.com\/FPGA\/ProductsPYNQ-Z2\.html)] 57 | 58 | ci_scripts: 59 | - template: static 60 | - template: test 61 | coverage: true 62 | - template: docs 63 | pip_install: 64 | - numpy>=1.13.0 65 | - matplotlib 66 | # this setup can be used to run CI scripts remotely over slurm: 67 | # - template: slurm-script 68 | # slurm_command: "srun -pCI" 69 | # wrapped_commands: 70 | # - sphinx-build 71 | # slurm_script: docs 72 | # output_name: slurm-docs 73 | # - template: remote-script 74 | # remote_script: slurm-docs 75 | # remote_setup: 76 | # - micromamba install -y pandoc ffmpeg 77 | # output_name: remote-docs 78 | # host: abrghost 79 | 80 | pre_commit_config_yaml: {} 81 | 82 | pyproject_toml: {} 83 | 84 | version_py: 85 | major: 0 86 | minor: 2 87 | patch: 3 88 | release: false 89 | -------------------------------------------------------------------------------- /.templates/slurm-script.sh.template: -------------------------------------------------------------------------------- 1 | {% extends "templates/base_script.sh.template" %} 2 | 3 | {# 4 | 5 | Usage Instructions 6 | ================== 7 | 8 | This script is intended to be used to apply a SLURM wrapper around other NengoBones 9 | generated CI scripts. 10 | 11 | .nengobones.yml parameters: 12 | 13 | `slurm_command`: The SLURM command used to create the SLURM job. Use quotations to avoid 14 | problems with the space character. Examples: 15 | - "srun" 16 | - "srun -pCI -G1" 17 | - "sbatch" 18 | 19 | `wrapped_commands`: A list of commands to be wrapped with `slurm_command`. The command 20 | will be executed as: 21 | Example, if is "srun -pCI", and is "pytest", then the command 22 | will be executed as "srun -pCI pytest" 23 | 24 | `slurm_script`: The CI script to be executed, but using the SLURM-wrapped commands 25 | instead of the typical non-SLURM invocation. 26 | 27 | `output_name`: Desired output name of the generated CI script. 28 | 29 | #} 30 | 31 | {% block install %} 32 | bash bones-scripts/{{ slurm_script }}.sh install || STATUS=1 33 | {% endblock %} 34 | 35 | {% block before_script %} 36 | bash bones-scripts/{{ slurm_script }}.sh before_script || STATUS=1 37 | {% endblock %} 38 | 39 | {% block script %} 40 | # Enable `expand_aliases` option for bash interpreter 41 | shopt -s expand_aliases 42 | 43 | # Create aliases for commands to include SLURM command prefix 44 | {% for command in wrapped_commands %} 45 | alias {{ command }}="{{ slurm_command }} {{ command }}" 46 | echo "Created alias for {{ command }}: " 47 | type -a {{ command }} 48 | 49 | {% endfor %} 50 | # Run script. Use existing shell instead of subshell to ensure alias applies. 51 | . bones-scripts/{{ slurm_script }}.sh script || STATUS=1 52 | {% endblock %} 53 | 54 | {% block before_cache %} 55 | bash bones-scripts/{{ slurm_script }}.sh before_cache || STATUS=1 56 | {% endblock %} 57 | 58 | {% block after_success %} 59 | bash bones-scripts/{{ slurm_script }}.sh after_success || STATUS=1 60 | {% endblock %} 61 | 62 | {% block after_failure %} 63 | bash bones-scripts/{{ slurm_script }}.sh after_failure || STATUS=1 64 | {% endblock %} 65 | 66 | {% block before_deploy %} 67 | bash bones-scripts/{{ slurm_script }}.sh before_deploy || STATUS=1 68 | {% endblock %} 69 | 70 | {% block after_deploy %} 71 | bash bones-scripts/{{ slurm_script }}.sh after_deploy || STATUS=1 72 | {% endblock %} 73 | 74 | {% block after_script %} 75 | bash bones-scripts/{{ slurm_script }}.sh after_script || STATUS=1 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Motivation and context:** 2 | 3 | 4 | 5 | **Interactions with other PRs:** 6 | 7 | 8 | 9 | 10 | **How has this been tested?** 11 | 12 | 13 | 14 | **How long should this take to review?** 15 | 16 | 17 | 18 | 19 | 20 | - Quick (less than 40 lines changed or changes are straightforward) 21 | - Average (neither quick nor lengthy) 22 | - Lengthy (more than 150 lines changed or changes are complicated) 23 | 24 | **Where should a reviewer start?** 25 | 26 | 27 | 28 | 29 | **Types of changes:** 30 | 31 | 32 | 33 | - Bug fix (non-breaking change which fixes an issue) 34 | - New feature (non-breaking change which adds functionality) 35 | - Breaking change (fix or feature that causes existing functionality to change) 36 | - Non-code change (touches things like tests, documentation, build scripts) 37 | 38 | **Checklist:** 39 | 40 | 41 | 42 | 43 | 44 | - [ ] I have read the **CONTRIBUTING.rst** document. 45 | - [ ] I have updated the documentation accordingly. 46 | - [ ] I have included a changelog entry. 47 | - [ ] I have tested this with all supported devices. 48 | - [ ] I have added tests to cover my changes. 49 | - [ ] I have run the test suite locally and all tests passed. 50 | 51 | **Still to do:** 52 | 53 | 54 | -------------------------------------------------------------------------------- /docs/examples/basic_example.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | 4 | import nengo 5 | import numpy as np 6 | 7 | from nengo_fpga import Simulator 8 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 9 | 10 | # This script demonstrates how to build a basic communication channel 11 | # using an adaptive neural ensemble implemented on the FPGA. The adaptive 12 | # neural ensemble is built remotely (on the FPGA) via a command sent over SSH 13 | # (as part of the FpgaPesEnsembleNetwork). This automated step removes the 14 | # need of the user to manually SSH into the FPGA board to start the nengo 15 | # script. Communication between the nengo model on the host (PC) and the remote 16 | # (FPGA board) is handled via udp sockets (automatically configured as part of 17 | # the FpgaPesEnsembleNetwork build process) 18 | 19 | # To run this script in nengo_gui: 20 | # > python S-00-communication_channel.py 21 | # where is the name of the device as it appears in `fpga_config`. 22 | # For example: 23 | # > python S-00-communication_channel.py pynq 24 | 25 | # Set the nengo logging level to 'info' to display all of the information 26 | # coming back over the ssh connection. 27 | logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO) 28 | 29 | parser = argparse.ArgumentParser( 30 | description="A simple communication channel example showing how to" 31 | " script with NengoFPGA." 32 | ) 33 | parser.add_argument( 34 | "board", type=str, help="The name of the FPGA device as it appears in fpga_config." 35 | ) 36 | parser.add_argument( 37 | "--time", 38 | "-t", 39 | type=float, 40 | default=2, 41 | help="The time in seconds over which to run the simulator.", 42 | ) 43 | 44 | args = parser.parse_args() 45 | 46 | 47 | def input_func(t): 48 | return [np.sin(t * 10)] 49 | 50 | 51 | with nengo.Network() as model: 52 | # Reference signal 53 | input_node = nengo.Node(input_func, label="input signal") 54 | 55 | # FPGA neural ensemble 56 | pes_ens = FpgaPesEnsembleNetwork( 57 | args.board, n_neurons=100, dimensions=1, learning_rate=0, label="ensemble" 58 | ) 59 | 60 | nengo.Connection(input_node, pes_ens.input) 61 | 62 | # Reference value passthrough node 63 | ref_node = nengo.Node(size_in=1) 64 | nengo.Connection(input_node, ref_node) 65 | 66 | # Output probes 67 | p_fpga = nengo.Probe(pes_ens.output, synapse=0.005) 68 | p_ref = nengo.Probe(ref_node, synapse=0.005) 69 | 70 | with Simulator(model) as sim: 71 | sim.run(args.time) 72 | 73 | # Compute RMSE between reference node and output of FPGA neural ensemble 74 | rmse = np.sqrt(np.mean(sim.data[p_fpga] - sim.data[p_ref]) ** 2) 75 | print("\nComputed RMSE: %0.05f" % rmse) 76 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Automatically generated by nengo-bones, do not edit this file directly 2 | 3 | [build_sphinx] 4 | source-dir = docs 5 | build-dir = docs/_build 6 | all_files = 1 7 | 8 | [coverage:run] 9 | source = ./ 10 | relative_files = True 11 | 12 | [coverage:report] 13 | # Regexes for lines to exclude from consideration 14 | exclude_lines = 15 | # Have to re-enable the standard pragma 16 | # place ``# pragma: no cover`` at the end of a line to ignore it 17 | pragma: no cover 18 | 19 | # Don't complain if tests don't hit defensive assertion code: 20 | raise NotImplementedError 21 | 22 | # `pass` is just a placeholder, fine if it's not covered 23 | ^[ \t]*pass$ 24 | 25 | 26 | # Patterns for files to exclude from reporting 27 | omit = 28 | */tests/test* 29 | 30 | [flake8] 31 | exclude = 32 | __init__.py 33 | ignore = 34 | E123 35 | E133 36 | E203 37 | E226 38 | E241 39 | E242 40 | E501 41 | E731 42 | F401 43 | W503 44 | max-complexity = 10 45 | max-line-length = 88 46 | 47 | [tool:pytest] 48 | norecursedirs = 49 | .* 50 | *.egg 51 | build 52 | dist 53 | docs 54 | xfail_strict = False 55 | 56 | [pylint] 57 | 58 | [pylint.messages] 59 | disable = 60 | arguments-differ, 61 | assignment-from-no-return, 62 | attribute-defined-outside-init, 63 | blacklisted-name, 64 | comparison-with-callable, 65 | duplicate-code, 66 | fixme, 67 | import-error, 68 | invalid-name, 69 | invalid-sequence-index, 70 | len-as-condition, 71 | literal-comparison, 72 | no-else-raise, 73 | no-else-return, 74 | no-member, 75 | no-name-in-module, 76 | not-an-iterable, 77 | not-context-manager, 78 | protected-access, 79 | redefined-builtin, 80 | stop-iteration-return, 81 | too-few-public-methods, 82 | too-many-arguments, 83 | too-many-branches, 84 | too-many-instance-attributes, 85 | too-many-lines, 86 | too-many-locals, 87 | too-many-return-statements, 88 | too-many-statements, 89 | unexpected-keyword-arg, 90 | unidiomatic-typecheck, 91 | unsubscriptable-object, 92 | unsupported-assignment-operation, 93 | unused-argument, 94 | 95 | [pylint.imports] 96 | known-third-party = 97 | matplotlib, 98 | nengo, 99 | numpy, 100 | pytest, 101 | 102 | [pylint.format] 103 | max-line-length = 88 104 | 105 | [pylint.classes] 106 | valid-metaclass-classmethod-first-arg = metacls 107 | 108 | [pylint.reports] 109 | reports = no 110 | score = no 111 | 112 | [codespell] 113 | skip = ./build,*/_build,*-checkpoint.ipynb,./.eggs,./*.egg-info,./.git,*/_vendor,./.mypy_cache, 114 | -------------------------------------------------------------------------------- /docs/examples/gui/00-mnist-vision-network.py.cfg: -------------------------------------------------------------------------------- 1 | _viz_0 = nengo_gui.components.HTMLView(display_node) 2 | _viz_config[_viz_0].height = 0.111358574610245 3 | _viz_config[_viz_0].width = 0.0881057268722467 4 | _viz_config[_viz_0].y = 1.0580736083705529 5 | _viz_config[_viz_0].label_visible = True 6 | _viz_config[_viz_0].x = 0.2085749057791189 7 | _viz_1 = nengo_gui.components.Pointer(output_spa,args='default') 8 | _viz_config[_viz_1].show_pairs = False 9 | _viz_config[_viz_1].height = 0.16990145065205664 10 | _viz_config[_viz_1].x = 0.532074498870198 11 | _viz_config[_viz_1].width = 0.21170544057165475 12 | _viz_config[_viz_1].y = 1.0509924301244185 13 | _viz_config[_viz_1].label_visible = True 14 | _viz_config[_viz_1].max_size = 1000.0 15 | _viz_2 = nengo_gui.components.SpaSimilarity(output_spa,args='default') 16 | _viz_config[_viz_2].show_pairs = False 17 | _viz_config[_viz_2].y = 1.0662645918673104 18 | _viz_config[_viz_2].height = 0.111358574610245 19 | _viz_config[_viz_2].max_value = 1.5 20 | _viz_config[_viz_2].width = 0.0881057268722467 21 | _viz_config[_viz_2].min_value = -1.5 22 | _viz_config[_viz_2].label_visible = True 23 | _viz_config[_viz_2].x = 0.8419547462803718 24 | _viz_net_graph = nengo_gui.components.NetGraph() 25 | _viz_progress = nengo_gui.components.Progress() 26 | _viz_config[_viz_progress].height = 100 27 | _viz_config[_viz_progress].width = 100 28 | _viz_config[_viz_progress].y = 0 29 | _viz_config[_viz_progress].label_visible = True 30 | _viz_config[_viz_progress].x = 0 31 | _viz_sim_control = nengo_gui.components.SimControl() 32 | _viz_config[_viz_sim_control].kept_time = 4.0 33 | _viz_config[_viz_sim_control].shown_time = 0.5 34 | _viz_config[display_node].pos=(0.32459615193469327, 0.7045326513931195) 35 | _viz_config[display_node].size=(0.04395604395604395, 0.05128205128205128) 36 | _viz_config[ens].pos=(0.33516483516483514, 0.5145949362129114) 37 | _viz_config[ens].size=(0.10989010989010989, 0.062328140710165576) 38 | _viz_config[ens].expanded=False 39 | _viz_config[ens].has_layout=False 40 | _viz_config[input_node].pos=(0.07142857142857142, 0.6025641025641025) 41 | _viz_config[input_node].size=(0.04395604395604395, 0.05128205128205128) 42 | _viz_config[model].pos=(0.0156077816136031, -0.4100679381428243) 43 | _viz_config[model].size=(0.8331732462007347, 0.8331732462007347) 44 | _viz_config[model].expanded=True 45 | _viz_config[model].has_layout=True 46 | _viz_config[output_node].pos=(0.598901098901099, 0.5171402950873961) 47 | _viz_config[output_node].size=(0.04395604395604395, 0.05128205128205128) 48 | _viz_config[output_spa].pos=(0.8626373626373627, 0.5126859266150313) 49 | _viz_config[output_spa].size=(0.10989010989010989, 0.0642371503080458) 50 | _viz_config[output_spa].expanded=False 51 | _viz_config[output_spa].has_layout=False 52 | _viz_config[output_spa.state_ensembles].expanded=False 53 | _viz_config[output_spa.state_ensembles].has_layout=False -------------------------------------------------------------------------------- /nengo_fpga/tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Tests for fpga_config parser.""" 2 | import os 3 | 4 | from nengo_fpga import fpga_config 5 | from nengo_fpga.fpga_config import _FPGA_CONFIG, fpga_config_files 6 | from nengo_fpga.utils import paths 7 | 8 | 9 | def test_load_hierarchy(mocker, gen_configs): 10 | """Ensure we load configs from the correct locations.""" 11 | 12 | # Set project dir to differentiate from system dir (root package dir) 13 | tmp_dir = os.path.join(os.getcwd(), "tmp") 14 | mocker.patch.dict( 15 | paths.fpga_config, {"project": os.path.join(tmp_dir, "fpga_config")} 16 | ) 17 | 18 | # Create a list to use in the test with this tmp project dir 19 | my_config_files = [ 20 | paths.fpga_config["nengo"], 21 | paths.fpga_config["system"], 22 | paths.fpga_config["user"], 23 | paths.fpga_config["project"], 24 | ] 25 | 26 | # Create some dummy configs 27 | sec = "config" 28 | entry = "file" 29 | for k, v in paths.fpga_config.items(): 30 | gen_configs.create_config(v, contents={sec: {entry: k}}) 31 | 32 | # Explicitly define keys in reverse hierarchical order 33 | keys = ["project", "user", "system", "nengo"] 34 | 35 | # Load configs, then remove the highest priority file and reload 36 | for k in keys: 37 | fpga_config.reload_config(my_config_files) 38 | assert fpga_config.item_dict(sec)[entry] == k 39 | os.remove(paths.fpga_config[k]) 40 | 41 | 42 | def test_reload_config(mocker): 43 | """Check the reload_config behaviour.""" 44 | 45 | # Don't actually do anything with configs 46 | clear_mock = mocker.patch.object(_FPGA_CONFIG, "_clear") 47 | read_mock = mocker.patch.object(_FPGA_CONFIG, "read") 48 | 49 | dummy_call = "fake_files" 50 | dummy_config = _FPGA_CONFIG() 51 | dummy_config.reload_config(dummy_call) 52 | 53 | read_mock.assert_has_calls( 54 | [mocker.call(fpga_config_files), mocker.call(dummy_call)] 55 | ) 56 | assert clear_mock.call_count == 2 57 | 58 | 59 | def test_clear(gen_configs): 60 | """Test the clear function of the config parser.""" 61 | 62 | # Create a dummy config and load it 63 | fname = os.path.join(os.getcwd(), "test_config") 64 | gen_configs.create_config(fname) 65 | fpga_config.reload_config(fname) 66 | 67 | # Confirm we loaded our dummy config 68 | assert len(fpga_config.sections()) == len(gen_configs.default_contents) 69 | 70 | # Clear and confirm 71 | fpga_config._clear() 72 | assert len(fpga_config.sections()) == 0 73 | 74 | 75 | def test_read(gen_configs): 76 | """Test the read function of the config parser.""" 77 | 78 | # Create a dummy config 79 | fname = os.path.join(os.getcwd(), "test_config") 80 | gen_configs.create_config(fname) 81 | 82 | # Grab current number of sections 83 | entries = len(fpga_config.sections()) 84 | 85 | # Read config and check we added a section (default one section) 86 | fpga_config.read(fname) 87 | assert len(fpga_config.sections()) == entries + len(gen_configs.default_contents) 88 | 89 | 90 | def test_item_dict(gen_configs): 91 | """Test the item dict function of the config parser.""" 92 | 93 | # Create a dummy config and load it 94 | fname = os.path.join(os.getcwd(), "test_config") 95 | gen_configs.create_config(fname) 96 | fpga_config.reload_config(fname) 97 | 98 | section = list(gen_configs.default_contents.keys())[0] 99 | config_dict = fpga_config.item_dict(section) 100 | 101 | assert config_dict == gen_configs.default_contents[section] 102 | -------------------------------------------------------------------------------- /nengo_fpga/simulator.py: -------------------------------------------------------------------------------- 1 | """Modified Nengo Simulator to integrate FPGA interfaces.""" 2 | 3 | import atexit 4 | 5 | import nengo 6 | 7 | from .networks import FpgaPesEnsembleNetwork 8 | 9 | 10 | class Simulator(nengo.simulator.Simulator): 11 | """Modified Nengo Simulator to integrate FPGA interfaces.""" 12 | 13 | def __init__(self, network, **kwargs): 14 | # Keep a record of the SSH connection details 15 | self.fpga_networks_list = [] 16 | 17 | # Iterate through all of the probes and generate a list of probe 18 | # targets 19 | probe_target_list = [p.target for p in network.all_probes] 20 | 21 | # Iterate through the given network and identify all of the 22 | # RemotePESEnembleNetworks that will require an SSH connection 23 | for net in network.networks: 24 | if ( 25 | isinstance(net, FpgaPesEnsembleNetwork) 26 | and net not in self.fpga_networks_list 27 | ): 28 | # Add network to the list of FPGA networks in the simulation 29 | self.fpga_networks_list.append(net) 30 | 31 | # Set the 'using_fpga_sim' flag in all FPGA networks 32 | net.using_fpga_sim = True 33 | 34 | # Check if FpgaPesEnsembleNetwork dummy ensemble or dummy 35 | # connection are being probed. If they are throw an error. 36 | for target in probe_target_list: 37 | if isinstance(target, nengo.Ensemble) and target is net.ensemble: 38 | raise nengo.exceptions.BuildError( 39 | "FPGA PES Ensembles are currently non-probable." 40 | ) 41 | elif ( 42 | isinstance(target, nengo.ensemble.Neurons) 43 | and target.ensemble is net.ensemble 44 | ): 45 | raise nengo.exceptions.BuildError( 46 | "FPGA PES Neurons are currently non-probable." 47 | ) 48 | elif ( 49 | isinstance(target, nengo.Connection) 50 | and target is net.connection 51 | ): 52 | raise nengo.exceptions.BuildError( 53 | "FPGA PES Connections are currently non-probable." 54 | ) 55 | 56 | # NOTE: Originally, a connect function was used to iterate and open 57 | # all necessary SSH connections to the FPGA networks. However, 58 | # a connect function is not called because the reset function 59 | # should be called before the simulation is run. 60 | 61 | # Register OS signal handler to handle ctrl+C or any abnormal 62 | # termination 63 | atexit.register(self.terminate) 64 | 65 | # Call nengo.Simulator super constructor 66 | super().__init__(network, **kwargs) 67 | 68 | def close(self): 69 | """Close all connections to the remote networks.""" 70 | for net in self.fpga_networks_list: 71 | net.close() 72 | super().close() 73 | 74 | def reset(self, seed=None): 75 | """Call the reset function for each remote network.""" 76 | for net in self.fpga_networks_list: 77 | net.reset() 78 | super().reset(seed) 79 | 80 | def terminate(self): 81 | """ 82 | Terminate the simulation. 83 | 84 | - Close all open UDP / SSH connections 85 | - Cleanup any existing temporary files 86 | """ 87 | self.close() 88 | for net in self.fpga_networks_list: 89 | net.cleanup() 90 | -------------------------------------------------------------------------------- /docs/examples/gui/01-adaptive-pendulum.py.cfg: -------------------------------------------------------------------------------- 1 | _viz_0 = nengo_gui.components.HTMLView(env.pendulum) 2 | _viz_config[_viz_0].height = 0.209033749384813 3 | _viz_config[_viz_0].y = 0.9699956623806034 4 | _viz_config[_viz_0].x = 0.680077216435904 5 | _viz_config[_viz_0].label_visible = True 6 | _viz_config[_viz_0].width = 0.19341644182442852 7 | _viz_1 = nengo_gui.components.Slider(q_target) 8 | _viz_config[_viz_1].height = 0.111358574610245 9 | _viz_config[_viz_1].x = 0.25367755495017624 10 | _viz_config[_viz_1].label_visible = True 11 | _viz_config[_viz_1].min_value = -1 12 | _viz_config[_viz_1].y = 0.9649736978177204 13 | _viz_config[_viz_1].max_value = 1 14 | _viz_config[_viz_1].width = 0.04405286343612335 15 | _viz_2 = nengo_gui.components.Slider(env.extra_mass) 16 | _viz_config[_viz_2].height = 0.111358574610245 17 | _viz_config[_viz_2].x = 0.4272828177995656 18 | _viz_config[_viz_2].label_visible = True 19 | _viz_config[_viz_2].min_value = -3 20 | _viz_config[_viz_2].y = 0.9659538899169426 21 | _viz_config[_viz_2].max_value = 5 22 | _viz_config[_viz_2].width = 0.04405286343612335 23 | _viz_net_graph = nengo_gui.components.NetGraph() 24 | _viz_progress = nengo_gui.components.Progress() 25 | _viz_config[_viz_progress].height = 100 26 | _viz_config[_viz_progress].y = 0 27 | _viz_config[_viz_progress].x = 0 28 | _viz_config[_viz_progress].label_visible = True 29 | _viz_config[_viz_progress].width = 100 30 | _viz_sim_control = nengo_gui.components.SimControl() 31 | _viz_config[_viz_sim_control].shown_time = 0.5 32 | _viz_config[_viz_sim_control].kept_time = 4.0 33 | _viz_config[adapt_ens].pos=(0.7447209664730453, 0.3816416343230178) 34 | _viz_config[adapt_ens].size=(0.136986301369863, 0.05646024597007318) 35 | _viz_config[adapt_ens].expanded=False 36 | _viz_config[adapt_ens].has_layout=False 37 | _viz_config[dq_diff].pos=(0.38488237386660906, 0.683828498699762) 38 | _viz_config[dq_diff].size=(0.0684931506849315, 0.04545454545454545) 39 | _viz_config[dq_target].pos=(0.3196703772130612, 0.6840916419082336) 40 | _viz_config[dq_target].size=(0.0547945205479452, 0.03636363636363636) 41 | _viz_config[env].pos=(0.4208277776840694, 0.3855870491146777) 42 | _viz_config[env].size=(0.136986301369863, 0.06059642281216577) 43 | _viz_config[env].expanded=False 44 | _viz_config[env].has_layout=True 45 | _viz_config[env.dq].pos=(0.8673469387755103, 0.6372549019607844) 46 | _viz_config[env.dq].size=(0.0816326530612245, 0.0392156862745098) 47 | _viz_config[env.extra_mass].pos=(0.1326530612244898, 0.08823529411764705) 48 | _viz_config[env.extra_mass].size=(0.0816326530612245, 0.0392156862745098) 49 | _viz_config[env.pendulum].pos=(0.5, 0.5) 50 | _viz_config[env.pendulum].size=(0.0816326530612245, 0.0392156862745098) 51 | _viz_config[env.q].pos=(0.8673469387755103, 0.3627450980392157) 52 | _viz_config[env.q].size=(0.0816326530612245, 0.0392156862745098) 53 | _viz_config[env.q_target].pos=(0.1326530612244898, 0.911764705882353) 54 | _viz_config[env.q_target].size=(0.0816326530612245, 0.0392156862745098) 55 | _viz_config[env.u].pos=(0.1326530612244898, 0.6372549019607844) 56 | _viz_config[env.u].size=(0.0816326530612245, 0.0392156862745098) 57 | _viz_config[env.u_extra].pos=(0.1326530612244898, 0.3627450980392157) 58 | _viz_config[env.u_extra].size=(0.0816326530612245, 0.0392156862745098) 59 | _viz_config[extra_mass].pos=(0.5697379148564916, 0.5639591627823853) 60 | _viz_config[extra_mass].size=(0.1, 0.1) 61 | _viz_config[model].pos=(-0.04562027085843024, -0.20290612498127858) 62 | _viz_config[model].size=(1.0190494693309908, 1.0190494693309908) 63 | _viz_config[model].expanded=True 64 | _viz_config[model].has_layout=True 65 | _viz_config[q_diff].pos=(0.4991247608023408, 0.6801549911885205) 66 | _viz_config[q_diff].size=(0.0684931506849315, 0.04545454545454545) 67 | _viz_config[q_target].pos=(0.1680544849705667, 0.5598154386592601) 68 | _viz_config[q_target].size=(0.0547945205479452, 0.03636363636363636) -------------------------------------------------------------------------------- /nengo_fpga/tests/test_simulator.py: -------------------------------------------------------------------------------- 1 | """Tests for NengoFPGA Simulator.""" 2 | import nengo 3 | import pytest 4 | 5 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 6 | from nengo_fpga.simulator import Simulator 7 | 8 | 9 | def test_init(mocker): 10 | """Test simulator network with standard nengo network.""" 11 | 12 | # Don't actually create a simulator 13 | super_mock = mocker.patch("nengo.simulator.Simulator.__init__") 14 | 15 | # Test a non-FPGA network first 16 | nengo_net = nengo.Network() 17 | dt = 1 # Check kwargs are passed through 18 | sim = Simulator(nengo_net, dt=dt) 19 | 20 | assert not sim.fpga_networks_list 21 | super_mock.assert_called_once_with(nengo_net, dt=dt) 22 | super_mock.reset_mock() 23 | 24 | # Add an FPGA network 25 | with nengo_net as net: 26 | net.fpga_net = FpgaPesEnsembleNetwork("test", 1, 1, 0.001) 27 | 28 | sim2 = Simulator(nengo_net) 29 | 30 | assert nengo_net.fpga_net.using_fpga_sim 31 | assert sim2.fpga_networks_list == [net.fpga_net] 32 | super_mock.assert_called_once_with(nengo_net) 33 | 34 | 35 | @pytest.mark.parametrize("probe", ["ensemble", "neurons", "connection"]) 36 | def test_probe_list(mocker, probe): 37 | """Test probe checks in init.""" 38 | 39 | # Don't actually create a simulator 40 | super_mock = mocker.patch("nengo.simulator.Simulator.__init__") 41 | 42 | # Create a dummy net with a given illegal probe 43 | with nengo.Network() as net: 44 | fpga_net = FpgaPesEnsembleNetwork("test", 1, 1, 0.001) 45 | 46 | probe_map = { 47 | "ensemble": fpga_net.ensemble, 48 | "neurons": fpga_net.ensemble.neurons, 49 | "connection": fpga_net.connection, 50 | } 51 | nengo.Probe(probe_map[probe]) 52 | 53 | with pytest.raises(nengo.exceptions.BuildError): 54 | Simulator(net) 55 | 56 | assert super_mock.call_count == 0 57 | 58 | 59 | def test_nengo_sim(): 60 | """Test using the nengo simulator with an fpga network.""" 61 | 62 | # Create a dummy net 63 | with nengo.Network() as net: 64 | net.fpga_net = FpgaPesEnsembleNetwork("test", 1, 1, 0.001) 65 | 66 | nengo.Simulator(net) 67 | 68 | assert not net.fpga_net.using_fpga_sim 69 | 70 | 71 | def test_close(dummy_sim, mocker): 72 | """Test the Simulator's close function.""" 73 | 74 | # Grab test objects from fixture 75 | net = dummy_sim[0] 76 | sim = dummy_sim[1] 77 | 78 | # Mock out local and super calls 79 | close_mock = mocker.patch.object(net, "close") 80 | super_close_mock = mocker.patch("nengo.simulator.Simulator.close") 81 | 82 | sim.close() 83 | 84 | assert close_mock.call_count == 2 85 | super_close_mock.assert_called_once() 86 | 87 | 88 | def test_reset(dummy_sim, mocker): 89 | """Test the Simulator's reset function.""" 90 | 91 | # Grab test objects from fixture 92 | net = dummy_sim[0] 93 | sim = dummy_sim[1] 94 | 95 | # Mock out local and super calls 96 | reset_mock = mocker.patch.object(net, "reset") 97 | super_reset_mock = mocker.patch("nengo.simulator.Simulator.reset") 98 | 99 | seed = 5 100 | sim.reset(seed) # Call with args 101 | 102 | assert reset_mock.call_count == 2 103 | super_reset_mock.assert_called_once_with(seed) 104 | 105 | 106 | def test_terminate(dummy_sim, mocker): 107 | """Test the Simulator's terminate function.""" 108 | 109 | # Grab test objects from fixture 110 | net = dummy_sim[0] 111 | sim = dummy_sim[1] 112 | 113 | # Mock out calls 114 | cleanup_mock = mocker.patch.object(net, "cleanup") 115 | close_mock = mocker.patch.object(sim, "close") 116 | 117 | sim.terminate() 118 | 119 | assert cleanup_mock.call_count == 2 120 | close_mock.assert_called_once() 121 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Automatically generated by nengo-bones, do not edit this file directly 4 | 5 | import pathlib 6 | 7 | import nengo_fpga 8 | 9 | extensions = [ 10 | "sphinx.ext.autodoc", 11 | "sphinx.ext.autosummary", 12 | "sphinx.ext.doctest", 13 | "sphinx.ext.githubpages", 14 | "sphinx.ext.intersphinx", 15 | "sphinx.ext.mathjax", 16 | "sphinx.ext.todo", 17 | "nbsphinx", 18 | "nengo_sphinx_theme", 19 | "nengo_sphinx_theme.ext.backoff", 20 | "nengo_sphinx_theme.ext.redirects", 21 | "nengo_sphinx_theme.ext.sourcelinks", 22 | "notfound.extension", 23 | "numpydoc", 24 | ] 25 | 26 | # -- sphinx.ext.autodoc 27 | autoclass_content = "both" # class and __init__ docstrings are concatenated 28 | autodoc_default_options = {"members": None} 29 | autodoc_member_order = "bysource" # default is alphabetical 30 | 31 | # -- sphinx.ext.doctest 32 | doctest_global_setup = """ 33 | import nengo_fpga 34 | """ 35 | 36 | # -- sphinx.ext.intersphinx 37 | intersphinx_mapping = { 38 | "nengo": ("https://www.nengo.ai/nengo/", None), 39 | "numpy": ("https://numpy.org/doc/stable", None), 40 | "python": ("https://docs.python.org/3", None), 41 | "nengo-de1": ("https://www.nengo.ai/nengo-de1/", None), 42 | "nengo-pynq": ("https://www.nengo.ai/nengo-pynq/", None), 43 | } 44 | 45 | # -- sphinx.ext.todo 46 | todo_include_todos = True 47 | 48 | # -- nbsphinx 49 | nbsphinx_timeout = -1 50 | 51 | # -- notfound.extension 52 | notfound_template = "404.html" 53 | notfound_urls_prefix = "/nengo-fpga/" 54 | 55 | # -- numpydoc config 56 | numpydoc_show_class_members = False 57 | 58 | # -- nengo_sphinx_theme.ext.sourcelinks 59 | sourcelinks_module = "nengo_fpga" 60 | sourcelinks_url = "https://github.com/nengo/nengo-fpga" 61 | 62 | # -- sphinx 63 | nitpicky = True 64 | exclude_patterns = [ 65 | "_build", 66 | "**/.ipynb_checkpoints", 67 | ] 68 | linkcheck_timeout = 30 69 | source_suffix = ".rst" 70 | source_encoding = "utf-8" 71 | master_doc = "index" 72 | linkcheck_ignore = [r"http://localhost:\d+"] 73 | linkcheck_anchors = True 74 | default_role = "py:obj" 75 | pygments_style = "sphinx" 76 | user_agent = "nengo_fpga" 77 | linkcheck_ignore = [ 78 | "(https:\\/\\/www\\.tulembedded\\.com\\/FPGA\\/ProductsPYNQ-Z2\\.html)" 79 | ] 80 | 81 | project = "NengoFPGA" 82 | authors = "Applied Brain Research" 83 | copyright = "2018-2023 Applied Brain Research" 84 | version = ".".join(nengo_fpga.__version__.split(".")[:2]) # Short X.Y version 85 | release = nengo_fpga.__version__ # Full version, with tags 86 | 87 | # -- HTML output 88 | templates_path = ["_templates"] 89 | html_static_path = ["_static"] 90 | html_theme = "nengo_sphinx_theme" 91 | html_title = f"NengoFPGA {release} docs" 92 | htmlhelp_basename = "NengoFPGA" 93 | html_last_updated_fmt = "" # Default output format (suppressed) 94 | html_show_sphinx = False 95 | html_favicon = str(pathlib.Path("_static", "favicon.ico")) 96 | html_theme_options = { 97 | "nengo_logo": "nengo-fpga-full-light.svg", 98 | "nengo_logo_color": "#541a8b", 99 | "analytics": """ 100 | 101 | 102 | 108 | 109 | 110 | 127 | 128 | """, 129 | } 130 | html_redirects = [ 131 | ("getting_started.html", "getting-started.html"), 132 | ( 133 | "examples/notebooks/00-communication_channel.html", 134 | "examples/notebooks/00-communication-channel.html", 135 | ), 136 | ( 137 | "examples/notebooks/01-learn_communication_channel.html", 138 | "examples/notebooks/01-learn-communication-channel.html", 139 | ), 140 | ( 141 | "examples/notebooks/02-set_neuron_params.html", 142 | "examples/notebooks/02-set-neuron-params.html", 143 | ), 144 | ( 145 | "examples/notebooks/05-controlled_oscillator.html", 146 | "examples/notebooks/05-controlled-oscillator.html", 147 | ), 148 | ( 149 | "examples/notebooks/06-chaotic_attractor.html", 150 | "examples/notebooks/06-chaotic-attractor.html", 151 | ), 152 | ] 153 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | =============== 3 | 4 | .. Changelog entries should follow this format: 5 | 6 | version (release date) 7 | ====================== 8 | 9 | **section** 10 | 11 | - One-line description of change (link to Github issue/PR) 12 | 13 | .. Changes should be organized in one of several sections: 14 | 15 | - Added 16 | - Changed 17 | - Deprecated 18 | - Removed 19 | - Fixed 20 | 21 | 0.2.3 (Unreleased) 22 | ------------------ 23 | 24 | **Added** 25 | 26 | - Added PC-running instruction clarification in getting started guide. 27 | (`#69 `__) 28 | - Added information about PYNQ-Z2 support to documentation. 29 | (`#71 `__) 30 | 31 | **Changed** 32 | 33 | - Changed remote-script to pip install nengo-bones from git. 34 | (`#67 `__) 35 | 36 | **Fixed** 37 | 38 | - Update Numpy license URL. 39 | (`#64 `__) 40 | - Fixed slack notification link. 41 | (`#66 `__) 42 | 43 | 44 | 0.2.2 (May 7, 2020) 45 | ------------------- 46 | 47 | **Added** 48 | 49 | - Setup Nengo Bones and remote CI. 50 | (`#41 `__) 51 | - Added notebook examples featuring oscillators. 52 | (`#46 `__) 53 | - Added spiking to oscillator notebook examples. 54 | (`#49 `__) 55 | - Add test suite. 56 | (`#55 `__) 57 | - Added More detail to remote termination signals. 58 | (`#61 `__) 59 | 60 | **Changed** 61 | 62 | - Compatibility changes for Nengo 3.0.0. 63 | (`#44 `__) 64 | - Minor changes to oscillator notebooks. 65 | (`#47 `__) 66 | - Minor changes to getting-started documentation. 67 | (`#50 `__) 68 | - Improved socket communication for better performance. 69 | (`#52 `__) 70 | - Throw an error with invalid config in ID script. 71 | (`#53 `__) 72 | - Update deprecated SafeConfigParser. 73 | (`#57 `__) 74 | - Remove unused seed from network builder. 75 | (`#58 `__) 76 | - Make all variable names lowercase. 77 | (`#59 `__) 78 | - Switch to remote doc script that tracks nengo-bones. 79 | (`#60 `__) 80 | - Switch to abrgl for CI scripts. 81 | (`#62 `__) 82 | 83 | **Fixed** 84 | 85 | - Fixed code to remove all linter errors. 86 | (`#45 `__) 87 | - Start using intersphinx, rename docs using '-'. 88 | (`#48 `__) 89 | - Add TRAVIS_JOB_NUMBER to exported variables in remote-docs.sh. 90 | (`#63 `__) 91 | 92 | 93 | 0.2.1 (September 17, 2019) 94 | -------------------------- 95 | 96 | **Added** 97 | 98 | - Add on-chip feedback connection. 99 | (`#35 `__) 100 | - Requirement for numpy<1.17. 101 | (`#39 `__) 102 | 103 | 104 | 0.2.0 (August 27, 2019) 105 | ----------------------- 106 | 107 | **Added** 108 | 109 | - Added script to read device DNA from FPGA board. 110 | (`#11 `__) 111 | - Add PR template, contributors, and update license. 112 | (`#12 `__) 113 | - Rework documentation. 114 | (`#18 `__, 115 | `#20 `__) 116 | - Quickstart guide. 117 | (`#21 `__) 118 | - Notebook examples and example descriptions. 119 | (`#23 `__) 120 | - Add firewall tip to docs. 121 | (`#24 `__) 122 | - Add license to docs. 123 | (`#25 `__) 124 | - Add purchase link to docs. 125 | (`#29 `__) 126 | - Add example setting encoders/decoders. 127 | (`#30 `__) 128 | - Add model size bounds to docs. 129 | (`#31 `__) 130 | 131 | **Changed** 132 | 133 | - Rename "DNA" to "ID" everywhere. 134 | (`#20 `__) 135 | - Docs audit for consistency. 136 | (`#22 `__) 137 | - Receiving a UDP packet with a negative timestep will now cause the Nengo 138 | simulation to terminate with an exception. 139 | (`#26 `__) 140 | - Now throwing an exception on unsupported neuron type. 141 | (`#26 `__) 142 | - Rework usage page in docs. 143 | (`#27 `__) 144 | - Update the docs theme. 145 | (`#32 `__) 146 | 147 | **Fixed** 148 | 149 | - Fixed behaviour of code when provided FPGA name string is not found in the 150 | fpga_config file. 151 | (`#33 `__) 152 | - Fixed simulation hanging error when two simulations are run one after the 153 | other. 154 | (`#34 `__) 155 | 156 | 157 | 0.1.0 (December 19, 2018) 158 | ------------------------- 159 | 160 | Initial release of NengoFPGA! 161 | -------------------------------------------------------------------------------- /docs/examples/gui/00-mnist-vision-network.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import nengo 4 | import numpy as np 5 | from nengo_extras.data import load_mnist 6 | from nengo_extras.gui import image_display_function 7 | from nengo_extras.vision import Gabor, Mask 8 | 9 | # Requires python image library: pip install pillow 10 | from PIL import Image 11 | 12 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 13 | 14 | 15 | # ------ MISC HELPER FUNCTIONS ----- 16 | def resize_img(img, _im_size, _im_size_new): 17 | # Resizes the MNIST images to a smaller size so that they can be processed 18 | # by the FPGA (the FPGA currently has a limitation on the number of 19 | # dimensions and neurons that can be built into the network) 20 | # Note: Requires the python PIL (pillow) library to work 21 | img = Image.fromarray(img.reshape((_im_size, _im_size)) * 256, "F") 22 | img = img.resize((_im_size_new, _im_size_new), Image.ANTIALIAS) 23 | return np.array(img.getdata(), np.float32) / 256.0 24 | 25 | 26 | def one_hot(labels, c=None): 27 | # One-hot function. Converts a given class and label list into a vector 28 | # of 0's (no class match) and 1's (class match) 29 | assert labels.ndim == 1 30 | n = labels.shape[0] 31 | c = len(np.unique(labels)) if c is None else c 32 | y = np.zeros((n, c)) 33 | y[np.arange(n), labels] = 1 34 | return y 35 | 36 | 37 | # ---------------- BOARD SELECT ----------------------- # 38 | # Change this to your desired device name 39 | board = "de1" 40 | # ---------------- BOARD SELECT ----------------------- # 41 | 42 | # Set the nengo logging level to 'info' to display all of the information 43 | # coming back over the ssh connection. 44 | logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO) 45 | 46 | # Set the rng state (using a fixed seed that works) 47 | rng = np.random.RandomState(9) 48 | 49 | # Load the MNIST data 50 | (x_train, y_train), (x_test, y_test) = load_mnist() 51 | 52 | x_train = 2 * x_train - 1 # normalize to -1 to 1 53 | x_test = 2 * x_test - 1 # normalize to -1 to 1 54 | 55 | # Get information about the image 56 | im_size = int(np.sqrt(x_train.shape[1])) # Dimension of 1 side of the image 57 | 58 | # Resize the images 59 | reduction_factor = 2 60 | if reduction_factor > 1: 61 | im_size_new = int(im_size // reduction_factor) 62 | 63 | x_train_resized = np.zeros((x_train.shape[0], im_size_new**2)) 64 | for i in range(x_train.shape[0]): 65 | x_train_resized[i, :] = resize_img(x_train[i], im_size, im_size_new) 66 | x_train = x_train_resized 67 | 68 | x_test_resized = np.zeros((x_test.shape[0], im_size_new**2)) 69 | for i in range(x_test.shape[0]): 70 | x_test_resized[i, :] = resize_img(x_test[i], im_size, im_size_new) 71 | x_test = x_test_resized 72 | 73 | im_size = im_size_new 74 | 75 | # Generate the MNIST training and test data 76 | train_targets = one_hot(y_train, 10) 77 | test_targets = one_hot(y_test, 10) 78 | 79 | # Set up the vision network parameters 80 | n_vis = x_train.shape[1] # Number of training samples 81 | n_out = train_targets.shape[1] # Number of output classes 82 | n_hid = 16000 // (im_size**2) # Number of neurons to use 83 | # Note: the number of neurons to use is limited such that NxD <= 16000, 84 | # where D = im_size * im_size, and N is the number of neurons to use 85 | gabor_size = (int(im_size / 2.5), int(im_size / 2.5)) # Size of the gabor filt 86 | 87 | # Generate the encoders for the neural ensemble 88 | encoders = Gabor().generate(n_hid, gabor_size, rng=rng) 89 | encoders = Mask((im_size, im_size)).populate(encoders, rng=rng, flatten=True) 90 | 91 | # Ensemble parameters 92 | max_firing_rates = 100 93 | ens_neuron_type = nengo.neurons.RectifiedLinear() 94 | ens_intercepts = nengo.dists.Choice([-0.5]) 95 | ens_max_rates = nengo.dists.Choice([max_firing_rates]) 96 | 97 | # Output connection parameters 98 | conn_synapse = None 99 | conn_eval_points = x_train 100 | conn_function = train_targets 101 | conn_solver = nengo.solvers.LstsqL2(reg=0.01) 102 | 103 | # Visual input process parameters 104 | presentation_time = 0.25 105 | 106 | # Nengo model proper 107 | with nengo.Network(seed=3) as model: 108 | # Visual input (the MNIST images) to the network 109 | input_node = nengo.Node( 110 | nengo.processes.PresentInput(x_test, presentation_time), label="input" 111 | ) 112 | 113 | # Ensemble to run on the FPGA. This ensemble is non-adaptive and just 114 | # uses the encoders and decoders to perform the image classification 115 | ens = FpgaPesEnsembleNetwork( 116 | board, 117 | n_neurons=n_hid, 118 | dimensions=n_vis, 119 | learning_rate=0, 120 | function=conn_function, 121 | eval_points=conn_eval_points, 122 | label="output class", 123 | ) 124 | 125 | # Set custom ensemble parameters for the FPGA Ensemble Network 126 | ens.ensemble.neuron_type = ens_neuron_type 127 | ens.ensemble.intercepts = ens_intercepts 128 | ens.ensemble.max_rates = ens_max_rates 129 | ens.ensemble.encoders = encoders 130 | 131 | # Set custom connection parameters for the FPGA Ensemble Network 132 | ens.connection.synapse = conn_synapse 133 | ens.connection.solver = conn_solver 134 | 135 | # Output display node 136 | output_node = nengo.Node(size_in=n_out, label="output class") 137 | 138 | # Projections to and from the fpga ensemble 139 | nengo.Connection(input_node, ens.input, synapse=None) 140 | nengo.Connection(ens.output, output_node, synapse=None) 141 | 142 | # Input image display (for nengo_gui) 143 | image_shape = (1, im_size, im_size) 144 | display_func = image_display_function(image_shape, offset=1, scale=128) 145 | display_node = nengo.Node(display_func, size_in=input_node.size_out) 146 | nengo.Connection(input_node, display_node, synapse=None) 147 | 148 | # Output SPA display (for nengo_gui) 149 | vocab_names = [ 150 | "ZERO", 151 | "ONE", 152 | "TWO", 153 | "THREE", 154 | "FOUR", 155 | "FIVE", 156 | "SIX", 157 | "SEVEN", 158 | "EIGHT", 159 | "NINE", 160 | ] 161 | vocab_vectors = np.eye(len(vocab_names)) 162 | 163 | vocab = nengo.spa.Vocabulary(len(vocab_names)) 164 | for name, vector in zip(vocab_names, vocab_vectors): 165 | vocab.add(name, vector) 166 | 167 | config = nengo.Config(nengo.Ensemble) 168 | config[nengo.Ensemble].neuron_type = nengo.Direct() 169 | with config: 170 | output_spa = nengo.spa.State(len(vocab_names), subdimensions=n_out, vocab=vocab) 171 | nengo.Connection(output_node, output_spa.input) 172 | -------------------------------------------------------------------------------- /docs/examples/gui/01-adaptive-pendulum.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import nengo 4 | import numpy as np 5 | 6 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 7 | 8 | # Set the nengo logging level to 'info' to display all of the information 9 | # coming back over the ssh connection. 10 | logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO) 11 | 12 | # ---------------- BOARD SELECT ----------------------- # 13 | # Change this to your desired device name 14 | board = "de1" 15 | # ---------------- BOARD SELECT ----------------------- # 16 | 17 | 18 | # Pendulum object. Handles the logic and simulation of the pendulum. 19 | class Pendulum: 20 | def __init__( 21 | self, 22 | mass=1.0, 23 | length=1.0, 24 | dt=0.001, 25 | g=10.0, 26 | seed=None, 27 | max_torque=2, 28 | max_speed=8, 29 | limit=2.0, 30 | bounds=None, 31 | ): 32 | self.mass = mass 33 | self.length = length 34 | self.dt = dt 35 | self.g = g 36 | self.max_torque = max_torque 37 | self.max_speed = max_speed 38 | self.limit = limit 39 | self.extra_mass = 0 40 | self.bounds = bounds 41 | self.reset(seed) 42 | 43 | def reset(self, seed): 44 | self.rng = np.random.RandomState(seed=seed) 45 | self.theta = self.rng.uniform(-self.limit, self.limit) 46 | self.dtheta = self.rng.uniform(-1, 1) 47 | 48 | def step(self, u): 49 | u = np.clip(u, -1, 1) * self.max_torque 50 | 51 | mass = self.mass + self.extra_mass 52 | self.dtheta += ( 53 | -3 * self.g / (2 * self.length) * np.sin(self.theta + np.pi) 54 | + 3.0 / (mass * self.length**2) * u 55 | ) * self.dt 56 | self.theta += self.dtheta * self.dt 57 | self.dtheta = np.clip(self.dtheta, -self.max_speed, self.max_speed) 58 | 59 | if self.bounds: 60 | self.theta = np.clip(self.theta, self.bounds[0], self.bounds[1]) 61 | self.theta = (self.theta + np.pi) % (2 * np.pi) - np.pi 62 | 63 | def set_extra_mass(self, mass): 64 | self.extra_mass = mass 65 | 66 | def generate_html(self, desired): 67 | len0 = 40 * self.length 68 | x1 = 50 69 | y1 = 50 70 | x2 = x1 + len0 * np.sin(self.theta) 71 | y2 = y1 - len0 * np.cos(self.theta) 72 | x3 = x1 + len0 * np.sin(desired) 73 | y3 = y1 - len0 * np.cos(desired) 74 | return """ 75 | 76 | 77 | 78 | 79 | """.format( 80 | x1=x1, y1=y1, x2=x2, y2=y2, x3=x3, y3=y3 81 | ) 82 | 83 | 84 | class PendulumNetwork(nengo.Network): 85 | def __init__(self, label=None, **kwargs): 86 | super(PendulumNetwork, self).__init__(label=label) 87 | self.env = Pendulum(**kwargs) 88 | 89 | with self: 90 | 91 | def func(t, x): 92 | self.env.set_extra_mass(x[2]) 93 | self.env.step(x[0]) 94 | func._nengo_html_ = self.env.generate_html(desired=x[1]) 95 | return (self.env.theta, self.env.dtheta) 96 | 97 | self.pendulum = nengo.Node(func, size_in=3, label="Pendulum Obj") 98 | 99 | self.q_target = nengo.Node(None, size_in=1, label="Target Pos") 100 | nengo.Connection(self.q_target, self.pendulum[1], synapse=None) 101 | 102 | self.u = nengo.Node(None, size_in=1, label="Control Signal") 103 | nengo.Connection(self.u, self.pendulum[0], synapse=0) 104 | self.u_extra = nengo.Node(None, size_in=1, label="Adaptive Control Signal") 105 | nengo.Connection(self.u_extra, self.pendulum[0], synapse=0) 106 | 107 | self.q = nengo.Node(None, size_in=1, label="Pendulum Pos (q)") 108 | self.dq = nengo.Node(None, size_in=1, label="Pendulum Pos Deriv (dq)") 109 | nengo.Connection(self.pendulum[0], self.q, synapse=None) 110 | nengo.Connection(self.pendulum[1], self.dq, synapse=None) 111 | 112 | self.extra_mass = nengo.Node(None, size_in=1, label="Extra Mass") 113 | nengo.Connection(self.extra_mass, self.pendulum[2], synapse=None) 114 | 115 | 116 | # Nengo network proper 117 | with nengo.Network(seed=3) as model: 118 | env = PendulumNetwork(mass=4, max_torque=100, seed=1) 119 | 120 | # The target angle for the pendulum (q) 121 | q_target = nengo.Node(np.sin, label="Target Pendulum Angle") 122 | nengo.Connection(q_target, env.q_target, synapse=None) 123 | 124 | # The derivative of the target angle signal (dq) 125 | dq_target = nengo.Node(None, size_in=1) 126 | nengo.Connection(q_target, dq_target, synapse=None, transform=1000) 127 | nengo.Connection(q_target, dq_target, synapse=0, transform=-1000) 128 | 129 | # The difference between the target angle and the actual angle of the 130 | # pendulum 131 | q_diff = nengo.Ensemble(n_neurons=100, dimensions=1) 132 | nengo.Connection(env.q_target, q_diff, synapse=None) 133 | nengo.Connection(env.q, q_diff, synapse=None, transform=-1) 134 | 135 | # The difference between the target dq and the pendulum's dq 136 | dq_diff = nengo.Ensemble(n_neurons=100, dimensions=1) 137 | nengo.Connection(dq_target, dq_diff, synapse=None) 138 | nengo.Connection(env.dq, dq_diff, synapse=None, transform=-1) 139 | 140 | # Compute the control signal (u) where u = k_p * q + k_d * dq 141 | k_p = 1.0 142 | nengo.Connection(q_diff, env.u, transform=k_p, synapse=None) 143 | 144 | k_d = 0.2 145 | nengo.Connection(dq_diff, env.u, transform=k_d, synapse=None) 146 | 147 | # PES Ensemble to compute the adaptive control signal to compensate for 148 | # unknown variables introduced into the environment. 149 | adapt_ens = FpgaPesEnsembleNetwork( 150 | board, 151 | n_neurons=1000, 152 | dimensions=1, 153 | learning_rate=1e-5, 154 | function=lambda x: [0], 155 | label="pes ensemble", 156 | ) 157 | 158 | # Compute the adaptive control signal. The adaptive control signal is 159 | # computed as a mapping between the current angle of the pendulum, and 160 | # an additional control signal (u_extra) added to the control signal (u). 161 | # The error signal used for the adaptive ensemble is simply -u. 162 | nengo.Connection(env.q, adapt_ens.input, synapse=None) 163 | nengo.Connection(env.u, adapt_ens.error, transform=-1) 164 | nengo.Connection(adapt_ens.output, env.u_extra, synapse=None) 165 | 166 | # Extra mass to add to the pendulum. To demonstrate the adaptive 167 | # controller. 168 | extra_mass = nengo.Node(None, size_in=1, label="Extra Mass") 169 | nengo.Connection(extra_mass, env.extra_mass, synapse=None) 170 | -------------------------------------------------------------------------------- /docs/examples/gui/02-RL-demo.py.cfg: -------------------------------------------------------------------------------- 1 | _viz_0 = nengo_gui.components.HTMLView(env) 2 | _viz_config[_viz_0].x = 0.8035981222456973 3 | _viz_config[_viz_0].y = 0.07185692533537424 4 | _viz_config[_viz_0].width = 0.201176406612529 5 | _viz_config[_viz_0].height = 0.2449039421157685 6 | _viz_config[_viz_0].label_visible = False 7 | _viz_1 = nengo_gui.components.Value(movement_node) 8 | _viz_config[_viz_1].max_value = 1.5 9 | _viz_config[_viz_1].min_value = -0.5 10 | _viz_config[_viz_1].show_legend = False 11 | _viz_config[_viz_1].legend_labels = ['label_0'] 12 | _viz_config[_viz_1].synapse = 0.01 13 | _viz_config[_viz_1].x = 0.17643727109567184 14 | _viz_config[_viz_1].y = 0.7179099693283195 15 | _viz_config[_viz_1].width = 0.15577458723948714 16 | _viz_config[_viz_1].height = 0.1478325017967439 17 | _viz_config[_viz_1].label_visible = True 18 | _viz_2 = nengo_gui.components.HTMLView(learn_on) 19 | _viz_config[_viz_2].x = 0.806387865525387 20 | _viz_config[_viz_2].y = 0.34439483523367803 21 | _viz_config[_viz_2].width = 0.17601965994931218 22 | _viz_config[_viz_2].height = 0.06070416740446006 23 | _viz_config[_viz_2].label_visible = False 24 | _viz_3 = nengo_gui.components.Value(stim_radar) 25 | _viz_config[_viz_3].max_value = 4 26 | _viz_config[_viz_3].min_value = 0 27 | _viz_config[_viz_3].show_legend = False 28 | _viz_config[_viz_3].legend_labels = ['label_0', 'label_1', 'label_2', 'label_3', 'label_4'] 29 | _viz_config[_viz_3].synapse = 0.01 30 | _viz_config[_viz_3].x = 0.18348747866106246 31 | _viz_config[_viz_3].y = 0.4330476313590044 32 | _viz_config[_viz_3].width = 0.1574503738076824 33 | _viz_config[_viz_3].height = 0.13972055888223553 34 | _viz_config[_viz_3].label_visible = True 35 | _viz_4 = nengo_gui.components.Raster(thal.actions.ea_ensembles[1]) 36 | _viz_config[_viz_4].n_neurons = 10 37 | _viz_config[_viz_4].x = 0.5277817368032118 38 | _viz_config[_viz_4].y = 0.749644434546293 39 | _viz_config[_viz_4].width = 0.0998003992015968 40 | _viz_config[_viz_4].height = 0.1132686084142395 41 | _viz_config[_viz_4].label_visible = True 42 | _viz_5 = nengo_gui.components.Raster(thal.actions.ea_ensembles[0]) 43 | _viz_config[_viz_5].n_neurons = 10 44 | _viz_config[_viz_5].x = 0.7293582467362808 45 | _viz_config[_viz_5].y = 0.518161337996793 46 | _viz_config[_viz_5].width = 0.0998003992015968 47 | _viz_config[_viz_5].height = 0.1132686084142395 48 | _viz_config[_viz_5].label_visible = True 49 | _viz_7 = nengo_gui.components.Raster(thal.actions.ea_ensembles[2]) 50 | _viz_config[_viz_7].n_neurons = 10 51 | _viz_config[_viz_7].x = 0.9294870038809545 52 | _viz_config[_viz_7].y = 0.7517131002806363 53 | _viz_config[_viz_7].width = 0.09993193521300404 54 | _viz_config[_viz_7].height = 0.11302878714288715 55 | _viz_config[_viz_7].label_visible = True 56 | _viz_9 = nengo_gui.components.Value(thal) 57 | _viz_config[_viz_9].max_value = 1 58 | _viz_config[_viz_9].min_value = -1 59 | _viz_config[_viz_9].show_legend = False 60 | _viz_config[_viz_9].legend_labels = ['label_0', 'label_1', 'label_2'] 61 | _viz_config[_viz_9].synapse = 0.01 62 | _viz_config[_viz_9].x = 0.7272417292815538 63 | _viz_config[_viz_9].y = 0.7507675208541343 64 | _viz_config[_viz_9].width = 0.10386847712880336 65 | _viz_config[_viz_9].height = 0.11445710959626168 66 | _viz_config[_viz_9].label_visible = True 67 | _viz_net_graph = nengo_gui.components.NetGraph() 68 | _viz_progress = nengo_gui.components.Progress() 69 | _viz_config[_viz_progress].x = 0 70 | _viz_config[_viz_progress].y = 0 71 | _viz_config[_viz_progress].width = 100 72 | _viz_config[_viz_progress].height = 100 73 | _viz_config[_viz_progress].label_visible = True 74 | _viz_sim_control = nengo_gui.components.SimControl() 75 | _viz_config[_viz_sim_control].shown_time = 0.5 76 | _viz_config[_viz_sim_control].kept_time = 4.0 77 | _viz_config[adapt_ens].pos=(0.26337164051040235, -0.10683970045432925) 78 | _viz_config[adapt_ens].size=(0.060703897828331764, 0.03057576708877105) 79 | _viz_config[adapt_ens].expanded=False 80 | _viz_config[adapt_ens].has_layout=False 81 | _viz_config[bg].pos=(0.4343416417100492, 0.020357102678659315) 82 | _viz_config[bg].size=(0.0712369375525991, 0.02761779650365088) 83 | _viz_config[bg].expanded=False 84 | _viz_config[bg].has_layout=True 85 | _viz_config[bg.gpe].pos=(0.49999999999999994, 0.45294117647058824) 86 | _viz_config[bg.gpe].size=(0.08264462809917354, 0.11764705882352941) 87 | _viz_config[bg.gpe].expanded=False 88 | _viz_config[bg.gpe].has_layout=False 89 | _viz_config[bg.gpi].pos=(0.7479338842975207, 0.5) 90 | _viz_config[bg.gpi].size=(0.08264462809917354, 0.11764705882352941) 91 | _viz_config[bg.gpi].expanded=False 92 | _viz_config[bg.gpi].has_layout=False 93 | _viz_config[bg.input].pos=(0.053719008264462804, 0.5) 94 | _viz_config[bg.input].size=(0.033057851239669415, 0.023529411764705882) 95 | _viz_config[bg.output].pos=(0.9462809917355373, 0.5) 96 | _viz_config[bg.output].size=(0.033057851239669415, 0.023529411764705882) 97 | _viz_config[bg.stn].pos=(0.25206611570247933, 0.14705882352941177) 98 | _viz_config[bg.stn].size=(0.08264462809917354, 0.11764705882352941) 99 | _viz_config[bg.stn].expanded=False 100 | _viz_config[bg.stn].has_layout=False 101 | _viz_config[bg.strD1].pos=(0.3235521496060226, 0.7873781222582902) 102 | _viz_config[bg.strD1].size=(0.14618776766344885, 0.11764705882352941) 103 | _viz_config[bg.strD1].expanded=False 104 | _viz_config[bg.strD1].has_layout=False 105 | _viz_config[bg.strD2].pos=(0.25206611570247933, 0.5) 106 | _viz_config[bg.strD2].size=(0.08264462809917354, 0.11764705882352941) 107 | _viz_config[bg.strD2].expanded=False 108 | _viz_config[bg.strD2].has_layout=False 109 | _viz_config[env].pos=(0.2567807001007816, 0.21107646238510258) 110 | _viz_config[env].size=(0.06600630614551733, 0.023062644995318456) 111 | _viz_config[errors].pos=(0.26096563410550017, 0.0363512700671417) 112 | _viz_config[errors].size=(0.042256042972247054, 0.04640883977900552) 113 | _viz_config[learn_on].pos=(0.42801811576529386, -0.11084004827364148) 114 | _viz_config[learn_on].size=(0.06513348588863468, 0.03140212303834381) 115 | _viz_config[model].pos=(0.05915260981891309, 0.2387241702830441) 116 | _viz_config[model].size=(0.8590388682272301, 0.8590388682272301) 117 | _viz_config[model].expanded=True 118 | _viz_config[model].has_layout=True 119 | _viz_config[movement].pos=(0.10574529911872615, 0.16625383398360824) 120 | _viz_config[movement].size=(0.036036036036036036, 0.031007751937984496) 121 | _viz_config[movement_node].pos=(0.10681328741888295, 0.03858615947762421) 122 | _viz_config[movement_node].size=(0.036436884512085904, 0.03812154696132591) 123 | _viz_config[stim_radar].pos=(0.11274290461098402, -0.11852482666326905) 124 | _viz_config[stim_radar].size=(0.036036036036036036, 0.031007751937984496) 125 | _viz_config[thal].pos=(0.4340557457688855, 0.16797020218168174) 126 | _viz_config[thal].size=(0.05554034563627228, 0.045071553007500424) 127 | _viz_config[thal].expanded=False 128 | _viz_config[thal].has_layout=True 129 | _viz_config[thal.actions].pos=(0.7093023255813954, 0.5) 130 | _viz_config[thal.actions].size=(0.23255813953488372, 0.4) 131 | _viz_config[thal.actions].expanded=True 132 | _viz_config[thal.actions].has_layout=True 133 | _viz_config[thal.actions.ea_ensembles[0]].pos=(0.49999999999999994, 0.125) 134 | _viz_config[thal.actions.ea_ensembles[0]].size=(0.09803921568627451, 0.0625) 135 | _viz_config[thal.actions.ea_ensembles[1]].pos=(0.49999999999999994, 0.5) 136 | _viz_config[thal.actions.ea_ensembles[1]].size=(0.09803921568627451, 0.0625) 137 | _viz_config[thal.actions.ea_ensembles[2]].pos=(0.49999999999999994, 0.7578169910610818) 138 | _viz_config[thal.actions.ea_ensembles[2]].size=(0.1128202502583752, 0.2421830089389182) 139 | _viz_config[thal.bias].pos=(0.1511627906976744, 0.5) 140 | _viz_config[thal.bias].size=(0.09302325581395349, 0.08) 141 | _viz_config[thal.input].pos=(0.1513508353559524, 0.5384794276382748) 142 | _viz_config[thal.input].size=(0.07843137254901959, 0.05) 143 | _viz_config[thal.output].pos=(0.872549019607843, 0.5) 144 | _viz_config[thal.output].size=(0.07843137254901959, 0.05) -------------------------------------------------------------------------------- /nengo_fpga/tests/test_fullstack.py: -------------------------------------------------------------------------------- 1 | """Tests for the full nengo-fpga stack including all supported devices.""" 2 | 3 | import nengo 4 | import numpy as np 5 | import pytest 6 | from nengo.solvers import NoSolver 7 | 8 | import nengo_fpga 9 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 10 | 11 | # Mark all as fullstack, and add device parameter 12 | pytestmark = [pytest.mark.fullstack, pytest.mark.parametrize("device", ["pynq", "de1"])] 13 | 14 | # Tell Nengo to use 32b, like the FPGA 15 | nengo.rc.set("precision", "bits", "32") 16 | 17 | 18 | def test_ff_learn(params, device): 19 | """Feedforward test with decoders initialized to 0 and learning.""" 20 | 21 | l_rate = 0.001 22 | stim_val = 0.25 23 | tol = 0.001 24 | if isinstance(params["neuron"], nengo.SpikingRectifiedLinear): 25 | tol *= 2 26 | p_syn = 0.05 27 | 28 | with nengo.Network() as net: 29 | stim = nengo.Node([stim_val] * params["dims_in"]) # Input node 30 | 31 | # Nengo reference 32 | ens = nengo.Ensemble( 33 | params["n_neurons"], 34 | params["dims_in"], 35 | neuron_type=params["neuron"], 36 | bias=params["bias"], 37 | encoders=params["encoders"], 38 | gain=params["gain"], 39 | ) 40 | output = nengo.Node(size_in=params["dims_out"]) 41 | nengo_err = nengo.Node(size_in=params["dims_out"]) 42 | 43 | nengo.Connection(stim, ens) 44 | conn = nengo.Connection( 45 | ens, 46 | output, 47 | solver=NoSolver(np.zeros((params["n_neurons"], params["dims_out"]))), 48 | function=params["func"], 49 | learning_rule_type=nengo.PES(l_rate), 50 | ) 51 | nengo.Connection(stim, nengo_err, function=params["func"], transform=-1) 52 | nengo.Connection(output, nengo_err) 53 | nengo.Connection(nengo_err, conn.learning_rule) 54 | 55 | p_nengo = nengo.Probe(output, synapse=p_syn) 56 | 57 | # FPGA 58 | fpga = FpgaPesEnsembleNetwork( 59 | device, 60 | params["n_neurons"], 61 | params["dims_in"], 62 | l_rate, 63 | function=params["func"], 64 | ) 65 | fpga.connection.solver = NoSolver(np.zeros(params["decoders"].shape).T) 66 | fpga.ensemble.neuron_type = params["neuron"] 67 | fpga.ensemble.encoders = params["encoders"] 68 | fpga.ensemble.bias = params["bias"] 69 | fpga.ensemble.gain = params["gain"] 70 | fpga_err = nengo.Node(size_in=params["dims_out"]) 71 | 72 | nengo.Connection(stim, fpga.input) 73 | nengo.Connection(stim, fpga_err, function=params["func"], transform=-1) 74 | nengo.Connection(fpga.output, fpga_err) 75 | nengo.Connection(fpga_err, fpga.error) 76 | 77 | p_fpga = nengo.Probe(fpga.output, synapse=p_syn) 78 | 79 | with nengo_fpga.Simulator(net) as sim: 80 | sim.run(1) 81 | 82 | assert fpga.config_found and fpga.using_fpga_sim # Ensure real FPGA 83 | 84 | idx = 2 if device == "de1" else 1 # Compensate for DE1 off-by-one 85 | assert np.allclose(sim.data[p_fpga][-1], sim.data[p_nengo][-idx], atol=tol) 86 | 87 | 88 | def test_ff_no_learn(params, device): 89 | """Feedforward test with solved decoders and no learning.""" 90 | 91 | tol = 0.005 92 | if isinstance(params["neuron"], nengo.SpikingRectifiedLinear): 93 | tol *= 2 94 | stim_val = 0.25 95 | p_syn = 0.05 96 | 97 | with nengo.Network() as net: 98 | stim = nengo.Node([stim_val] * params["dims_in"]) # Input node 99 | 100 | # Nengo reference 101 | ens = nengo.Ensemble( 102 | params["n_neurons"], 103 | params["dims_in"], 104 | neuron_type=params["neuron"], 105 | bias=params["bias"], 106 | encoders=params["encoders"], 107 | gain=params["gain"], 108 | ) 109 | output = nengo.Node(size_in=params["dims_out"]) 110 | 111 | nengo.Connection(stim, ens) 112 | nengo.Connection( 113 | ens, output, function=params["func"], solver=NoSolver(params["decoders"].T) 114 | ) 115 | 116 | p_nengo = nengo.Probe(output, synapse=p_syn) 117 | 118 | # FPGA 119 | fpga = FpgaPesEnsembleNetwork( 120 | device, params["n_neurons"], params["dims_in"], 0, function=params["func"] 121 | ) 122 | fpga.connection.solver = NoSolver(params["decoders"].T) 123 | fpga.ensemble.neuron_type = params["neuron"] 124 | fpga.ensemble.encoders = params["encoders"] 125 | fpga.ensemble.bias = params["bias"] 126 | fpga.ensemble.gain = params["gain"] 127 | 128 | nengo.Connection(stim, fpga.input) 129 | 130 | p_fpga = nengo.Probe(fpga.output, synapse=p_syn) 131 | 132 | with nengo_fpga.Simulator(net) as sim: 133 | sim.run(1) 134 | 135 | assert fpga.config_found and fpga.using_fpga_sim # Ensure real FPGA 136 | 137 | idx = 2 if device == "de1" else 1 # Compensate for DE1 off-by-one 138 | assert np.allclose(sim.data[p_fpga][-1], sim.data[p_nengo][-idx], atol=tol) 139 | 140 | 141 | def test_feedback(params, device): 142 | """Feedback test with solved decoders.""" 143 | 144 | stim_val = 0.5 145 | tol = 0.005 146 | if isinstance(params["neuron"], nengo.SpikingRectifiedLinear): 147 | tol *= 2 148 | tau = 0.05 149 | p_syn = 0.05 150 | 151 | def stim_func(t): 152 | """Return value for the first 1 second of simulation.""" 153 | val = stim_val * tau if t < 1 else 0 154 | return [val] * params["dims_in"] 155 | 156 | with nengo.Network() as net: 157 | stim = nengo.Node(stim_func) # Input node 158 | 159 | # Nengo reference 160 | ens = nengo.Ensemble( 161 | params["n_neurons"], 162 | params["dims_in"], 163 | neuron_type=params["neuron"], 164 | bias=params["bias"], 165 | encoders=params["encoders"], 166 | gain=params["gain"], 167 | ) 168 | output = nengo.Node(size_in=params["dims_out"]) 169 | 170 | nengo.Connection(stim, ens) 171 | nengo.Connection( 172 | ens, output, function=params["func"], solver=NoSolver(params["decoders"].T) 173 | ) 174 | nengo.Connection( 175 | ens, 176 | ens, 177 | solver=NoSolver( 178 | np.concatenate( 179 | (params["decoders"], np.zeros(params["decoders"].shape)) 180 | ).T 181 | ), 182 | synapse=tau, 183 | ) 184 | 185 | p_nengo = nengo.Probe(output, synapse=p_syn) 186 | 187 | # FPGA 188 | fpga = FpgaPesEnsembleNetwork( 189 | device, 190 | params["n_neurons"], 191 | params["dims_in"], 192 | 0, 193 | function=params["func"], 194 | feedback=1, 195 | ) 196 | fpga.connection.solver = NoSolver(params["decoders"].T) 197 | fpga.ensemble.neuron_type = params["neuron"] 198 | fpga.ensemble.encoders = params["encoders"] 199 | fpga.ensemble.bias = params["bias"] 200 | fpga.ensemble.gain = params["gain"] 201 | fpga.feedback.synapse = tau 202 | fpga.feedback.solver = NoSolver( 203 | np.concatenate((params["decoders"], np.zeros(params["decoders"].shape))).T 204 | ) 205 | 206 | nengo.Connection(stim, fpga.input) 207 | 208 | p_fpga = nengo.Probe(fpga.output, synapse=p_syn) 209 | 210 | with nengo_fpga.Simulator(net) as sim: 211 | sim.run(1) 212 | 213 | assert fpga.config_found and fpga.using_fpga_sim # Ensure real FPGA 214 | 215 | idx = 2 if device == "de1" else 1 # Compensate for DE1 off-by-one 216 | assert np.allclose(sim.data[p_fpga][-1], sim.data[p_nengo][-idx], atol=tol) 217 | -------------------------------------------------------------------------------- /docs/examples/notebooks/anim_utils.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from matplotlib import animation, gridspec 4 | from matplotlib.collections import LineCollection 5 | from mpl_toolkits.mplot3d.art3d import Line3DCollection 6 | 7 | 8 | def make_anim_simple_osc( 9 | x, 10 | y, 11 | dt=0.001, 12 | ms_per_frame=10, 13 | trail_length=500, 14 | figsize=(8, 8), 15 | cmap="gist_yarg", 16 | lw=2, 17 | ): 18 | def make_2d_segments(x_data, y_data): 19 | # Convert the x, y arrays into line segments 20 | vertices = np.array([x_data, y_data]).T.reshape(-1, 1, 2) 21 | segments = np.concatenate([vertices[:-1], vertices[1:]], axis=1) 22 | return segments 23 | 24 | # Make matplotlib figure 25 | fig = plt.figure(figsize=figsize) 26 | ax = plt.subplot(111) 27 | ax.set_aspect("equal") 28 | 29 | # Set the axis limits 30 | axis_limits = max(max(np.absolute(x)), max(np.absolute(y))) * 1.2 31 | ax.set_xlim(-axis_limits, axis_limits) 32 | ax.set_ylim(-axis_limits, axis_limits) 33 | ax.set_xlabel("") 34 | 35 | # Make the initial line collection object 36 | lsegs = LineCollection(make_2d_segments([0], [0]), cmap=cmap, lw=lw) 37 | lsegs.set_array(np.arange(trail_length)) 38 | ax.add_collection(lsegs) 39 | 40 | # Time label 41 | time_str = ax.text(-axis_limits / 1.1, -axis_limits / 1.1, "", fontsize=12) 42 | 43 | # Number of data points to skip per frame 44 | skip = int(ms_per_frame / (1000.0 * dt)) 45 | 46 | def anim_init(): 47 | # Animation initialization function 48 | lsegs.set_segments(None) 49 | time_str.set_text("") 50 | return lsegs, time_str 51 | 52 | def animate_frame(i): 53 | # Animiation function for each frame 54 | lsegs.set_segments( 55 | make_2d_segments( 56 | x[max(i * skip - trail_length, 0) : i * skip + 1], 57 | y[max(i * skip - trail_length, 0) : i * skip + 1], 58 | ) 59 | ) 60 | time_str.set_text("Time: %0.2fs" % (i * skip * dt)) 61 | return lsegs, time_str 62 | 63 | num_frames = len(x) // skip 64 | anim = animation.FuncAnimation( 65 | fig, 66 | animate_frame, 67 | init_func=anim_init, 68 | frames=num_frames, 69 | interval=ms_per_frame, 70 | blit=True, 71 | ) 72 | plt.close(anim._fig) 73 | return fig, ax, anim 74 | 75 | 76 | def make_anim_controlled_osc( 77 | x, 78 | y, 79 | w, 80 | dt=0.001, 81 | ms_per_frame=10, 82 | trail_length=500, 83 | figsize=(8, 6), 84 | cmap="gist_yarg", 85 | lw=2, 86 | dotsize=20, 87 | ): 88 | def make_2d_segments(x_data, y_data): 89 | # Convert the x, y arrays into line segments 90 | vertices = np.array([x_data, y_data]).T.reshape(-1, 1, 2) 91 | segments = np.concatenate([vertices[:-1], vertices[1:]], axis=1) 92 | return segments 93 | 94 | # Make matplotlib figure 95 | fig = plt.figure(figsize=figsize) 96 | gs = gridspec.GridSpec(1, 2, width_ratios=[12, 1]) 97 | gs.update(wspace=0) 98 | ax = plt.subplot(gs[0], aspect="equal") 99 | 100 | # Set the axis limits 101 | axis_limits = max(max(np.absolute(x)), max(np.absolute(y))) * 1.2 102 | ax.set_xlim(-axis_limits, axis_limits) 103 | ax.set_ylim(-axis_limits, axis_limits) 104 | ax.set_xticks( 105 | np.linspace(-np.round(axis_limits, 0), np.round(axis_limits, 0), num=5) 106 | ) 107 | ax.set_yticks( 108 | np.linspace(-np.round(axis_limits, 0), np.round(axis_limits, 0), num=5) 109 | ) 110 | 111 | # Make the initial line collection object 112 | lsegs = LineCollection(make_2d_segments([0], [0]), cmap=cmap, lw=lw) 113 | lsegs.set_array(np.arange(trail_length)) 114 | ax.add_collection(lsegs) 115 | 116 | # Time label 117 | time_str = ax.text(-axis_limits / 1.1, -axis_limits / 1.1, "", fontsize=12) 118 | 119 | # Sidebar plot 120 | ax2 = plt.subplot(gs[1]) 121 | speed_limits = max(np.absolute(w)) * 1.2 122 | ax2.set_ylim(-speed_limits, speed_limits) 123 | ax2.set_yticks(np.linspace(-int(speed_limits), int(speed_limits), num=5)) 124 | ax2.set_xticks([]) 125 | ax2.yaxis.set_label_position("right") 126 | ax2.yaxis.tick_right() 127 | ax2.spines["left"].set_visible(False) 128 | ax2.spines["top"].set_visible(False) 129 | ax2.spines["bottom"].set_visible(False) 130 | (dot,) = ax2.plot([], [], "r.", markersize=dotsize) 131 | 132 | # Number of data points to skip per frame 133 | skip = int(ms_per_frame / (1000.0 * dt)) 134 | 135 | def anim_init(): 136 | # Animation initialization function 137 | lsegs.set_segments(None) 138 | dot.set_data([], []) 139 | time_str.set_text("") 140 | return lsegs, dot, time_str 141 | 142 | def animate_frame(i): 143 | # Animiation function for each frame 144 | lsegs.set_segments( 145 | make_2d_segments( 146 | x[max(i * skip - trail_length, 0) : i * skip + 1], 147 | y[max(i * skip - trail_length, 0) : i * skip + 1], 148 | ) 149 | ) 150 | dot.set_data([0.015], [w[i * skip]]) 151 | time_str.set_text("Time: %0.2fs" % (i * skip * dt)) 152 | return lsegs, dot, time_str 153 | 154 | num_frames = len(x) // skip 155 | anim = animation.FuncAnimation( 156 | fig, 157 | animate_frame, 158 | init_func=anim_init, 159 | frames=num_frames, 160 | interval=ms_per_frame, 161 | blit=True, 162 | ) 163 | plt.close(anim._fig) 164 | return fig, ax, ax2, anim 165 | 166 | 167 | def make_anim_chaotic( 168 | x, 169 | y, 170 | z, 171 | dt=0.001, 172 | ms_per_frame=10, 173 | trail_length=500, 174 | figsize=(8, 8), 175 | cmap="gist_yarg", 176 | lw=2, 177 | ): 178 | def make_3d_segments(x_data, y_data, z_data): 179 | # Convert the x, y arrays into line segments 180 | vertices = np.array([x_data, y_data, z_data]).T.reshape(-1, 1, 3) 181 | segments = np.concatenate([vertices[:-1], vertices[1:]], axis=1) 182 | return segments 183 | 184 | # Make matplotlib figure 185 | fig = plt.figure(figsize=figsize) 186 | ax = fig.add_subplot(111, projection="3d") 187 | 188 | # Set the axis limits 189 | axis_limits = ( 190 | max(max(np.absolute(x)), max(np.absolute(y)), max(np.absolute(z))) * 1.2 191 | ) 192 | ax.set_xlim(-axis_limits, axis_limits) 193 | ax.set_ylim(-axis_limits, axis_limits) 194 | ax.set_zlim(-axis_limits, axis_limits) 195 | 196 | # Make the initial line collection object 197 | lsegs = Line3DCollection(make_3d_segments([0], [0], [0]), cmap=cmap, lw=lw) 198 | lsegs.set_array(np.arange(trail_length)) 199 | ax.add_collection3d(lsegs) 200 | 201 | # Time label 202 | time_str = ax.text( 203 | -axis_limits / 1.05, -axis_limits / 1.05, -axis_limits / 1.05, "", fontsize=12 204 | ) 205 | 206 | # Number of data points to skip per frame 207 | skip = int(ms_per_frame / (1000.0 * dt)) 208 | 209 | def anim_init(): 210 | # Animation initialization function 211 | lsegs.set_segments(None) 212 | time_str.set_text("") 213 | return lsegs, time_str 214 | 215 | def animate_frame(i): 216 | # Animiation function for each frame 217 | lsegs.set_segments( 218 | make_3d_segments( 219 | x[max(i * skip - trail_length, 0) : i * skip + 1], 220 | y[max(i * skip - trail_length, 0) : i * skip + 1], 221 | z[max(i * skip - trail_length, 0) : i * skip + 1], 222 | ) 223 | ) 224 | time_str.set_text("Time: %0.2fs" % (i * skip * dt)) 225 | return lsegs, time_str 226 | 227 | num_frames = len(x) // skip 228 | anim = animation.FuncAnimation( 229 | fig, 230 | animate_frame, 231 | init_func=anim_init, 232 | frames=num_frames, 233 | interval=ms_per_frame, 234 | blit=True, 235 | ) 236 | plt.close(anim._fig) 237 | return fig, ax, anim 238 | -------------------------------------------------------------------------------- /nengo_fpga/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | """Test fixtures used in the test suite.""" 3 | import os 4 | 5 | import nengo 6 | import numpy as np 7 | import pytest 8 | 9 | import nengo_fpga 10 | from nengo_fpga import fpga_config 11 | from nengo_fpga.id_extractor import IDExtractor 12 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 13 | from nengo_fpga.simulator import Simulator 14 | 15 | 16 | @pytest.fixture # noqa: C901 17 | def gen_configs(request): # noqa: C901 18 | """Helper fixture to generate and cleanup some dummy configs.""" 19 | 20 | class MyConfigs: 21 | """To keep track of configs we generate.""" 22 | 23 | def __init__(self): 24 | self.default_contents = {"config": {"a": "1", "b": "2", "c": "3"}} 25 | 26 | self.rm_dirs = [] # Keep track of dirs we create 27 | self.configs = [] # Keep track of configs we create 28 | 29 | def create_config(self, fname, contents=None): 30 | """ 31 | Create a dummy config. 32 | 33 | `fname` - str 34 | The full filepath and file name used for the config 35 | `sections` = list of str 36 | List of sections to create in the config file 37 | `contents` = list of dict 38 | Contents to use for each section 39 | """ 40 | if contents is None: 41 | contents = self.default_contents 42 | 43 | d_name = os.path.dirname(fname) 44 | # Create directory if needed 45 | if not os.path.isdir(d_name): 46 | os.makedirs(d_name) 47 | self.rm_dirs.append(d_name) 48 | 49 | # If a config exists here, create a backup 50 | if os.path.isfile(fname): 51 | os.rename(fname, fname + ".bak") 52 | 53 | # Write config file 54 | with open(fname, "w", encoding="ascii") as f: 55 | self.configs.append(fname) 56 | for sec, content in contents.items(): 57 | f.write(f"[{sec}]\n") 58 | for k, v in content.items(): 59 | f.write(f"{k} = {v}\n") 60 | f.write("\n") 61 | 62 | def cleanup(self): 63 | """Cleanup the configs we made.""" 64 | 65 | # Cleanup files 66 | for f in self.configs: 67 | if os.path.isfile(f): # Removed in some tests before this 68 | os.remove(f) # Delete dummy configs 69 | if os.path.isfile(f + ".bak"): 70 | os.rename(f + ".bak", f) # Restore original config if any 71 | 72 | # Cleanup directories (currently only does leaf dir, no parents) 73 | for d in self.rm_dirs: 74 | os.rmdir(d) 75 | 76 | dummy_configs = MyConfigs() 77 | 78 | def teardown(configs=dummy_configs): 79 | """Run cleanup code on test exit.""" 80 | configs.cleanup() 81 | nengo_fpga.fpga_config.reload_config() # Reload previous config 82 | 83 | # Add cleanup to teardown 84 | request.addfinalizer(teardown) 85 | 86 | return dummy_configs 87 | 88 | 89 | @pytest.fixture 90 | def config_contents(): 91 | """Config contents used to test the ID Extractor class.""" 92 | 93 | # We omit ssh_key and ssh_pwd as they will be tested separately 94 | contents = { 95 | "host": {"ip": "1.2.3.4"}, 96 | "test-fpga": { 97 | "udp_port": "0", 98 | "ssh_port": "1", 99 | "ip": "5.6.7.8", 100 | "ssh_user": "McTester", 101 | "id_script": "id_script.py", 102 | "remote_script": "pes_script.py", 103 | "remote_tmp": "/test/dir", 104 | }, 105 | } 106 | 107 | return contents 108 | 109 | 110 | @pytest.fixture 111 | def dummy_extractor(config_contents, gen_configs, mocker): # noqa: W0521 112 | """ 113 | Setup an ID extractor. 114 | 115 | `config_contents` was kept separate so we can update the config file 116 | """ 117 | 118 | # Create a dummy config for testing 119 | fname = os.path.join(os.getcwd(), "test-config") 120 | fpga_name = list(config_contents.keys())[1] 121 | 122 | gen_configs.create_config(fname, contents=config_contents) 123 | fpga_config.reload_config(fname) 124 | 125 | # Don't actually connect the socket in init 126 | mocker.patch("socket.socket.bind") 127 | mocker.patch("socket.socket.listen") 128 | 129 | return IDExtractor(fpga_name) 130 | 131 | 132 | @pytest.fixture 133 | def dummy_net(config_contents, gen_configs): # noqa: W0521 134 | """ 135 | Setup an FPGA network. 136 | 137 | `config_contents` was kept separate so we can update the config file 138 | """ 139 | 140 | # Create a dummy config for testing 141 | fname = os.path.join(os.getcwd(), "test-config") 142 | fpga_name = list(config_contents.keys())[1] 143 | 144 | gen_configs.create_config(fname, contents=config_contents) 145 | fpga_config.reload_config(fname) 146 | 147 | return FpgaPesEnsembleNetwork(fpga_name, 1, 1, 0.001) 148 | 149 | 150 | @pytest.fixture 151 | def dummy_sim(mocker): 152 | """Setup dummy network and simulator.""" 153 | 154 | class DummyNet: 155 | """Dummy network class with token functions.""" 156 | 157 | def close(self): 158 | """Dummy close function.""" 159 | 160 | def reset(self): 161 | """Dummy reset function.""" 162 | 163 | def cleanup(self): 164 | """Dummy cleanup function.""" 165 | 166 | my_net = DummyNet() 167 | 168 | # Don't actually spin up a simulator 169 | mocker.patch.object(Simulator, "__init__", return_value=None) 170 | 171 | sim = Simulator(my_net) # Using `my_net` as a dummy arg. init is mocked 172 | sim.fpga_networks_list = [my_net, my_net] 173 | 174 | # Simulator cleanup was complaining these weren't defined 175 | sim.closed = False 176 | sim.model = None 177 | 178 | return my_net, sim 179 | 180 | 181 | @pytest.fixture 182 | def dummy_com(): 183 | """ 184 | Dummy class to mock out ssh channel and socket. 185 | 186 | Some socket functions are readonly and require a mock class 187 | """ 188 | 189 | class DummyCom: 190 | """Dummy ssh channel.""" 191 | 192 | def send(self, *args): 193 | """Dummy send functions.""" 194 | 195 | def put(self, *args): 196 | """Dummy send functions.""" 197 | 198 | def recv(self, *args): 199 | """Dummy recv function.""" 200 | 201 | def close(self): 202 | """Dummy close function.""" 203 | 204 | def accept(self): 205 | """Dummy accept function.""" 206 | 207 | def sendto(self, *args): 208 | """Dummy sendto function.""" 209 | 210 | def recv_into(self, *args): 211 | """Dummy recv_into function.""" 212 | 213 | return DummyCom 214 | 215 | 216 | @pytest.fixture(params=[nengo.RectifiedLinear(), nengo.SpikingRectifiedLinear()]) 217 | def params(request): # pragma: no cover 218 | """ 219 | Create a dummy network and extract params for fullstack tests. 220 | 221 | Fixture itself is parametrized for neuron type 222 | """ 223 | 224 | # Tell Nengo to use 32b, like the FPGA 225 | nengo.rc.set("precision", "bits", "32") 226 | 227 | # Arbitrary params (keep `my_func` in mind if changing dims) 228 | neuron = request.param 229 | n_neurons = 200 230 | dims_out = 2 231 | dims_in = 2 * dims_out 232 | seed = 10 233 | 234 | def my_func(x): 235 | """Define function that maps input dims to output dims.""" 236 | 237 | half = int(len(x) / 2) 238 | # Add dims 1 + 3, 2+ 4 239 | return np.add(x[:half], x[half:]) 240 | 241 | with nengo.Network(seed=seed) as param_net: 242 | stim = nengo.Node([0.25] * dims_in) 243 | a = nengo.Ensemble(n_neurons, dims_in, neuron_type=neuron) 244 | b = nengo.Node(size_in=dims_out) 245 | nengo.Connection(stim, a, synapse=None) 246 | conn = nengo.Connection( 247 | a, b, function=lambda x: x[:dims_out] + x[dims_out:], synapse=None 248 | ) 249 | 250 | with nengo.Simulator(param_net) as param_sim: 251 | bias = param_sim.data[a].bias 252 | decoders = param_sim.data[conn].weights 253 | encoders = param_sim.data[a].encoders 254 | gain = param_sim.data[a].gain 255 | 256 | # Return params to be used in fullstack tets 257 | params = { 258 | "bias": bias, 259 | "decoders": decoders, 260 | "encoders": encoders, 261 | "gain": gain, 262 | "func": my_func, 263 | "n_neurons": n_neurons, 264 | "dims_in": dims_in, 265 | "dims_out": dims_out, 266 | "neuron": neuron, 267 | } 268 | 269 | return params 270 | -------------------------------------------------------------------------------- /docs/appendix.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Appendix 3 | ******** 4 | 5 | .. _supported-hardware: 6 | 7 | Supported Hardware 8 | ================== 9 | 10 | IP packages can be purchased through `Applied Brain Research 11 | `_. 12 | 13 | 14 | DE1-SoC 15 | ------- 16 | 17 | .. image:: _static/de1.png 18 | :alt: DE1 board 19 | :width: 30% 20 | :align: right 21 | :target: https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&No=836 22 | 23 | .. Pipes add vertical space 24 | 25 | | 26 | | 27 | 28 | The DE1-SoC board is manufactured by Terasic and uses an Intel Altera Cyclone V 29 | FPGA paired with a dual core ARM Cortex A9. The board is available for `purchase 30 | from Terasic 31 | `_. 32 | See the :std:doc:`NengoDE1 documentation ` for how to use this 33 | board with NengoFPGA. 34 | 35 | | 36 | | 37 | 38 | 39 | PYNQ-Z1 40 | ------- 41 | 42 | .. image:: _static/pynq.png 43 | :alt: PYNQ board 44 | :width: 30% 45 | :align: right 46 | :target: https://store.digilentinc.com/pynq-z1-python-productivity-for-zynq-7000-arm-fpga-soc/ 47 | 48 | | 49 | | 50 | 51 | The PYNQ-Z1 board is manufactured by Digilent and uses a ZYNQ 7020 FPGA paired 52 | with a dual core ARM Cortex A9. The board is available for `purchase from 53 | Digilent 54 | `_. 55 | See the :std:doc:`NengoPYNQ documentation ` for how to use 56 | this board with NengoFPGA. 57 | 58 | | 59 | | 60 | 61 | 62 | PYNQ-Z2 63 | ------- 64 | 65 | .. image:: _static/pynqz2.png 66 | :alt: PYNQ board 67 | :width: 30% 68 | :align: right 69 | :target: https://www.tulembedded.com/FPGA/ProductsPYNQ-Z2.html 70 | 71 | | 72 | | 73 | 74 | The PYNQ-Z2 board is manufactured by TUL and uses a ZYNQ 7020 FPGA paired 75 | with a dual core ARM Cortex A9. The board is available for `purchase from 76 | TUL (or one of their international distributors) 77 | `_. 78 | See the :std:doc:`NengoPYNQ documentation ` for how to use 79 | this board with NengoFPGA. 80 | 81 | | 82 | | 83 | | 84 | 85 | .. _ssh-key: 86 | 87 | Generating and Using SSH keys 88 | ============================= 89 | 90 | SSH keys allow SSH connections to be made without the need for a password. SSH 91 | keys operate in pairs: a *private key* kept on the host (local) machine, and a 92 | *public key* copied to the ``authorized_keys`` file on the FPGA board. 93 | 94 | To generate and use an SSH key with NengoFPGA, follow the instructions below. 95 | To support both the Windows and Unix-based operating systems, NengoFPGA uses 96 | SSH keys using the OpenSSH format. 97 | 98 | Windows 99 | ------- 100 | 101 | The best way to generate an SSH key in Windows is to use PuTTY_. 102 | 103 | .. _PuTTY: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html 104 | 105 | 1. Download and install PuTTY_. 106 | #. Under the **PuTTY** group in the windows start menu, run the **PuTTYgen** 107 | application. 108 | #. Click the **Generate** button, and follow the on-screen instructions to 109 | generate a random SSH key pair. 110 | 111 | Exporting the SSH *private key*: 112 | 113 | 1. Click the **Conversions** drop-down menu, and select **Export OpenSSH key**. 114 | Leave the **Key passphrase** blank. 115 | #. Choose a save location for the SSH key (e.g., the install location of 116 | ``nengo-fpga``). 117 | 118 | Exporting the SSH *public key*: 119 | 120 | 1. Keep PuTTYgen open. Do not regenerate the SSH key pair. 121 | #. SSH into the FPGA board using a username and password combination. 122 | The username used will have the SSH keys generated with these steps 123 | associated with it. 124 | #. Edit the ``~/.ssh/authorized_keys`` file with a text editor 125 | (e.g., ``nano``, ``vi``). 126 | #. Copy the *public key* text from the **Public key for pasting into OpenSSH 127 | authorized_keys file** area of PuTTYgen and paste it at the end of the 128 | ``authorized_keys`` file. 129 | 130 | 131 | Linux and Mac 132 | ------------- 133 | 134 | .. rst-class:: compact 135 | 136 | 1. On your computer, open a terminal. If SSH has not installed been installed 137 | on your computer, install it with ``sudo apt-get install ssh`` (Linux) or 138 | ``sudo brew install ssh`` (Mac). 139 | #. Enter the command: ``ssh-keygen -t rsa`` 140 | #. The command above will prompt for a location to save the SSH *private key*. 141 | Leaving this blank will save it to the default location of ``~/.ssh/id_rsa``. 142 | #. The ``ssh-keygen`` command will also prompt for a passphrase. Leave the 143 | passphrase empty (press **↵ Enter** twice). 144 | #. Copy the SSH *public key* to the FPGA board with the command: 145 | 146 | .. code-block:: bash 147 | 148 | ssh-copy-id -i @ 149 | 150 | For example: 151 | 152 | .. code-block:: bash 153 | 154 | ssh-copy-id -i ~/.ssh/id_rsa.pub xilinx@10.162.177.99 155 | 156 | .. note:: 157 | If the ``ssh-copy-id`` command does not work, or is unavailable, copy the 158 | contents of the ``*.pub`` file (this is the generated SSH *public key*) 159 | located in the same location as the SSH *private key* into the 160 | ``~/.ssh/authorized_keys`` of the appropriate user on the FPGA board. 161 | 162 | 163 | Configuring the ``fpga_config`` File 164 | ------------------------------------ 165 | 166 | After generating the SSH key pair, test that they have been properly installed 167 | by SSH'ing into the FPGA board. If a connection is made without needing to 168 | provide a password, then the SSH key has been successfully installed. 169 | 170 | If the SSH key has been successfully installed NengoFPGA can be configured to 171 | use the keys by replacing the **ssh_pwd** entry with **ssh_key**, and providing 172 | the location of the SSH *private key* on the host system. As an example, if the 173 | SSH *private key* is located in ``~/.ssh/id_rsa`` on the host system, the 174 | **ssh_key** entry would be: 175 | 176 | .. code-block:: none 177 | 178 | ssh_key = ~/.ssh/id_rsa 179 | 180 | 181 | .. note:: 182 | The config file can have *either* an **ssh_pwd** entry or an **ssh_key** 183 | entry but **not both**. 184 | 185 | .. _ip-addr: 186 | 187 | Finding your IP Address 188 | ======================= 189 | 190 | To find the IP address of your computer follow the instructions for your 191 | operating system below. 192 | 193 | 194 | .. note:: 195 | Ignore any address like **127.0.0.1** 196 | 197 | .. 198 | .. todo:: 199 | Maybe add screenshots? 200 | 201 | Windows 202 | ------- 203 | 1. Press **⊞ Win** + **r** to open the **Run** dialog box. 204 | #. Type in ``cmd`` and press **↵ Enter** to open the windows command prompt. 205 | #. Type ``ipconfig``, and look for the **IPv4 Address** entry of the desired 206 | network interface. This is your IP address. 207 | 208 | .. |winkey| unicode:: 0x229E 209 | 210 | Linux and Mac 211 | ------------- 212 | 213 | 1. Open a terminal and type ``ifconfig | grep "inet "`` 214 | 215 | 216 | .. _jupyter: 217 | 218 | Using Jupyter Notebooks 219 | ======================= 220 | 221 | Jupyter notebooks are a nice way to add explanations to your code and step 222 | through systems piece by piece. NengoFPGA uses notebooks to illustrate 223 | some simple examples. The following instructions will get you started with 224 | Jupyter so you can run the examples locally or create your own notebook! 225 | 226 | 227 | .. rst-class:: compact 228 | 229 | 1. Install Jupyter: 230 | 231 | .. code-block:: bash 232 | 233 | pip install jupyter 234 | 235 | #. Start a local Jupyter server: 236 | 237 | i. In a terminal, navigate to the examples folder, 238 | ``nengo_fpga/docs/examples/notebooks``. 239 | #. Start the server with: 240 | 241 | .. code-block:: bash 242 | 243 | jupyter-notebook 244 | 245 | This will open a file explorer in your browser. 246 | 247 | #. Open a notebook by double clicking on one of the ``.ipynb`` files. 248 | #. You may need to select a kernel, the Python engine that will run code 249 | under the hood for you. At the top, click the **kernel** menu then hover 250 | over **Change kernel** and select the desired Python version. 251 | #. Click on the first code cell in the notebook and execute it by clicking 252 | the play button at the top or pressing **Shift+Enter**. This will run the 253 | code in the current cell and move you to the next cell in the notebook. 254 | #. Step through the notebook by continuing to execute cells, you can execute 255 | the text cells as cell, so no need to jump down and click on each code cell! 256 | 257 | .. note:: 258 | Be sure to execute cells in order otherwise you may get unexpected results. 259 | If you make changes to code in a cell be sure to rerun that cell and any 260 | other cells affected by that change! 261 | -------------------------------------------------------------------------------- /docs/examples/notebooks/00-communication-channel.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Communication Channel\n", 8 | "\n", 9 | "This example demonstrates how to create a connection from one neuronal ensemble to\n", 10 | "another that behaves like a communication channel (that is, it transmits information\n", 11 | "without changing it).\n", 12 | "\n", 13 | "Network diagram:\n", 14 | "\n", 15 | " [Input] ---> (FPGA Ensemble) ---> [Output probe]\n", 16 | "\n", 17 | "An abstract input signal is fed into the neural ensemble built remotely on the FPGA. The\n", 18 | "ensemble on the FPGA encodes the input value as a pattern of neural activity. The neural\n", 19 | "activity is then decoded back into an abstract value before being passed to the output\n", 20 | "probe. In this example, the connection weight matrix that performs this decoding is\n", 21 | "computed to replicate the value of the abstract input signal (i.e., the decoded output\n", 22 | "should have the same value as the input signal)." 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "## Step 1: Set up the Python Imports" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "%matplotlib inline\n", 39 | "import matplotlib.pyplot as plt\n", 40 | "import numpy as np\n", 41 | "\n", 42 | "import nengo\n", 43 | "\n", 44 | "import nengo_fpga\n", 45 | "from nengo_fpga.networks import FpgaPesEnsembleNetwork" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## Step 2: Choose an FPGA Device\n", 53 | "\n", 54 | "Define the FPGA device on which the remote FpgaPesEnsembleNetwork will run. This name\n", 55 | "corresponds with the name in your `fpga_config` file. Recall that in the `fpga_config`\n", 56 | "file, device names are identified by the square brackets (e.g., **[de1]** or\n", 57 | "**[pynq]**). The names defined in your configuration file might differ from the example\n", 58 | "below. Here, the device **de1** is being used." 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "board = \"de1\" # Change this to your desired device name" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "## Step 3: Create the Remote FPGA Neural Ensemble\n", 75 | "\n", 76 | "Create a remote FPGA neural ensemble (`FpgaPesEnsembleNetwork`) using the board defined\n", 77 | "above, 50 neurons, 2 dimensions, and with no learning rate." 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "# Create a nengo network object to which we can add\n", 87 | "# ensembles, connections, etc.\n", 88 | "model = nengo.Network(label=\"Communication Channel\")\n", 89 | "\n", 90 | "with model:\n", 91 | " # Remote FPGA neural ensemble\n", 92 | " fpga_ens = FpgaPesEnsembleNetwork(\n", 93 | " board, # The board to use (from above)\n", 94 | " n_neurons=50, # The number of neurons to use in the ensemble\n", 95 | " dimensions=2, # 2 dimensions, to represent a 2D vector\n", 96 | " learning_rate=0, # No learning for this example\n", 97 | " )\n", 98 | "\n", 99 | " # Uncomment the following line to use spiking neuron\n", 100 | " # fpga_ens.ensemble.neuron_type = nengo.SpikingRectifiedLinear()" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "## Step 4: Provide Input to the Ensemble\n", 108 | "\n", 109 | "Create an input node that generates a 2-dimensional signal -- where the first dimension\n", 110 | "is a sine wave, and the second dimension a cosine wave." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "def input_func(t):\n", 120 | " return [np.sin(t * 2 * np.pi), np.cos(t * 2 * np.pi)]\n", 121 | "\n", 122 | "\n", 123 | "with model:\n", 124 | " input_node = nengo.Node(input_func)" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "## Step 5: Connect the Input to the FPGA Ensemble\n", 132 | "\n", 133 | "The FPGA ensemble contains `input` and `output` attributes to allow connections to be\n", 134 | "made to and from the ensemble." 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "with model:\n", 144 | " # Connect the input to the FPGA ensemble\n", 145 | " nengo.Connection(input_node, fpga_ens.input)" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "## Step 6: Add Probes to Collect Data\n", 153 | "\n", 154 | "Even this simple model involves many quantities that change over time, such as membrane\n", 155 | "potentials of individual neurons. Typically there are so many variables in a simulation\n", 156 | "that it is not practical to store them all. If we want to plot or analyze data from the\n", 157 | "simulation we have to \"probe\" the signals of interest.\n", 158 | "\n", 159 | "Many of the internal dynamics of the FPGA ensemble are not probeable since collecting\n", 160 | "and transmitting all of these values would slow down the simulation considerably.\n", 161 | "However, the input and output of the FPGA ensemble are available, and are enough to\n", 162 | "illustrate the network functionality." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "with model:\n", 172 | " # The original input\n", 173 | " input_p = nengo.Probe(input_node, synapse=0.01)\n", 174 | "\n", 175 | " # The output from the FPGA ensemble\n", 176 | " # (filtered with a 10ms post-synaptic filter)\n", 177 | " output_p = nengo.Probe(fpga_ens.output, synapse=0.01)" 178 | ] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "metadata": {}, 183 | "source": [ 184 | "## Step 7: Run the Model!\n", 185 | "\n", 186 | "To run a NengoFPGA model, simply use the `nengo_fpga.Simulator` simulator instead of the\n", 187 | "standard `nengo.Simulator` simulator." 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "with nengo_fpga.Simulator(model) as sim:\n", 197 | " sim.run(2) # Run for 2 seconds" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "## Step 8: Plot the Results" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "plt.figure(figsize=(16, 5))\n", 214 | "plt.subplot(1, 2, 1)\n", 215 | "plt.title(\"Probed Results (Dimension 1)\")\n", 216 | "plt.plot(sim.trange(), sim.data[input_p][:, 0])\n", 217 | "plt.plot(sim.trange(), sim.data[output_p][:, 0])\n", 218 | "plt.ylim(-1.1, 1.1)\n", 219 | "plt.legend((\"Input\", \"Output\"), loc=\"upper right\")\n", 220 | "plt.xlabel(\"Sim time (s)\")\n", 221 | "\n", 222 | "plt.subplot(1, 2, 2)\n", 223 | "plt.title(\"Probed Results (Dimension 2)\")\n", 224 | "plt.plot(sim.trange(), sim.data[input_p][:, 1])\n", 225 | "plt.plot(sim.trange(), sim.data[output_p][:, 1])\n", 226 | "plt.ylim(-1.1, 1.1)\n", 227 | "plt.legend((\"Input\", \"Output\"), loc=\"upper right\")\n", 228 | "plt.xlabel(\"Sim time (s)\")\n", 229 | "\n", 230 | "plt.show()" 231 | ] 232 | }, 233 | { 234 | "cell_type": "markdown", 235 | "metadata": {}, 236 | "source": [ 237 | "The plot above compares each dimension of the input (reference) signal (in blue) with\n", 238 | "the probed output of the FPGA ensemble (in orange). As the plot illustrates, the decoded\n", 239 | "neural activity of the FPGA ensemble is able to replicate the input signal with only a\n", 240 | "slight temporal delay.\n", 241 | "\n", 242 | "## Step 9: Experiment with this Model\n", 243 | "\n", 244 | "Using this Jupyter notebook, it is possible to experiment with the various parameters of\n", 245 | "the FPGA ensemble to observe how they affect the behaviour of the ensemble. Note that\n", 246 | "the experiments below require you to run this notebook within the Jupyter ecosystem. If\n", 247 | "you are viewing this via the NengoFPGA documentations page, please clone the NengoFPGA\n", 248 | "repository to perform these experiments.\n", 249 | "\n", 250 | "### Input Function\n", 251 | "\n", 252 | "Try changing the input function (`input_func`) above. As long as the function outputs a\n", 253 | "2-dimensional value the model should work but you may see the reconstructed signal\n", 254 | "deteriorate as the range and frequency of your input function increase.\n", 255 | "\n", 256 | "### Neuron Type\n", 257 | "\n", 258 | "By default, NengoFPGA uses Rectified Linear Units (ReLU) neurons to simulate the neurons\n", 259 | "in the FPGA neural ensemble. NengoFPGA also supports Spiking Rectified Linear units,\n", 260 | "also known as Integrate and Fire (IF) neurons. To use these neurons, uncomment the line\n", 261 | "noted in the Step 3 above (`fpga_ens.ensemble.neuron_type`)." 262 | ] 263 | } 264 | ], 265 | "metadata": { 266 | "anaconda-cloud": {}, 267 | "language_info": { 268 | "name": "python", 269 | "pygments_lexer": "ipython3" 270 | } 271 | }, 272 | "nbformat": 4, 273 | "nbformat_minor": 2 274 | } 275 | -------------------------------------------------------------------------------- /docs/examples/gui/02-RL-demo.py: -------------------------------------------------------------------------------- 1 | # Reinforcement Learning 2 | 3 | # Here we have a simple agent in a simple world. It has three actions 4 | # (go forward, turn left, and turn right), and its only sense are three 5 | # range finders (radar). 6 | 7 | # Initially, its basal ganglia action selection system is set up to have 8 | # fixed utilities for the three actions (moving forward has a utility of 0.8, 9 | # which is larger than turning left or right (0.7 and 0.6), so it should always 10 | # go forward. 11 | 12 | # The reward system is set up to give a positive value when moving forward, and 13 | # a negative value if the agent crashes into a wall. In theory, this should 14 | # cause it to learn to avoid obstacles. In particular, it will start to turn 15 | # before it hits the obstacle, and so the reward should be negative much less 16 | # often. 17 | 18 | # The error signal in this case is very simple: the difference between the 19 | # computed utility and the instantaneous reward. This error signal should 20 | # only be applied to whatever action is currently being chosen (although it 21 | # isn't quite perfect at doing this). Note that this means it cannot learn 22 | # to do actions that will lead to *future* rewards. 23 | 24 | import logging 25 | import time 26 | 27 | import nengo 28 | import numpy as np 29 | from grid import Cell as GridCell 30 | from grid import ContinuousAgent, GridNode 31 | from grid import World as GridWorld 32 | 33 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 34 | 35 | # Note: Requires the "keyboard_state" branch of nengo_gui for full 36 | # interactive functionality 37 | 38 | 39 | # Set the nengo logging level to 'info' to display all of the information 40 | # coming back over the ssh connection. 41 | logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO) 42 | 43 | # ---------------- BOARD SELECT ----------------------- # 44 | # Change this to your desired device name 45 | board = "de1" 46 | # ---------------- BOARD SELECT ----------------------- # 47 | 48 | 49 | # ----------- WORLD CONFIGURATION --------------------------------------------- 50 | class Cell(GridCell): 51 | def color(self): 52 | return "black" if self.wall else None 53 | 54 | def load(self, char): 55 | if char == "#": 56 | self.wall = True 57 | else: 58 | self.wall = False 59 | 60 | 61 | class WorldConfig: 62 | curr_ind = -1 63 | world_maps = [ 64 | """ 65 | ######### 66 | # # 67 | # # 68 | # ## # 69 | # ## # 70 | # # 71 | #########""", 72 | """ 73 | ######### 74 | # # 75 | # # 76 | # ## # 77 | # ## # 78 | # ## # 79 | #########""", 80 | """ 81 | ######### 82 | # # 83 | # ### # 84 | # # 85 | # ### # 86 | # # 87 | #########""", 88 | """ 89 | ######### 90 | # # 91 | # ##### 92 | # # 93 | ##### # 94 | # # 95 | #########""", 96 | ] 97 | init_pos = [(1, 3, 2), (1, 3, 2), (1, 1, 1), (1, 1, 1)] 98 | 99 | world = None 100 | agent = None 101 | 102 | def get_init_pos(self): 103 | return self.init_pos[self.curr_ind] 104 | 105 | def get_map(self): 106 | return self.world_maps[self.curr_ind] 107 | 108 | def set_ind(self, new_ind): 109 | if 0 <= new_ind < len(self.world_maps): 110 | self.curr_ind = new_ind 111 | lines = self.get_map().splitlines() 112 | 113 | if (len(lines[0])) == 0: 114 | del lines[0] 115 | lines = [x.rstrip() for x in lines] 116 | for j, _ in enumerate(lines): 117 | for i, _ in enumerate(lines[0]): 118 | self.world.get_cell(i, j).load(lines[j][i]) 119 | 120 | def reset_pos(self): 121 | self.agent.x = self.get_init_pos()[0] 122 | self.agent.y = self.get_init_pos()[1] 123 | self.agent.dir = self.get_init_pos()[2] 124 | self.agent.cell = self.world.get_cell(self.agent.x, self.agent.y) 125 | 126 | 127 | world_cfg = WorldConfig() 128 | world = GridWorld(Cell, map=world_cfg.get_map(), directions=4) 129 | agent = ContinuousAgent() 130 | world_cfg.world = world 131 | world_cfg.agent = agent 132 | world_cfg.set_ind(0) 133 | world_cfg.world.add( 134 | agent, 135 | x=world_cfg.get_init_pos()[0], 136 | y=world_cfg.get_init_pos()[1], 137 | dir=world_cfg.get_init_pos()[2], 138 | ) 139 | 140 | # ----------- LEARNING & MODEL PARAMETERS ------------------------------------- 141 | learn_rate = 1e-4 142 | learn_synapse = 0.030 143 | learn_timeout = 60.0 144 | radar_dim = 5 145 | turn_bias = 0.25 146 | action_threshold = 0.1 147 | init_transform = [0.8, 0.6, 0.7] 148 | 149 | # ----------- MODEL SEED CONFIGURATION ---------------------------------------- 150 | seed = int(time.time()) 151 | print("USING SEED: {0}".format(seed)) 152 | 153 | # ----------- MODEL PROPER ---------------------------------------------------- 154 | if "__page__" in locals(): 155 | # Additional options for keyboard-state branch of nengo_gui 156 | # Allows the user to control the status of the learning, and to change the 157 | # map being used by the agent. 158 | print("Press 'q' to enable exploration and reset agent position.") 159 | print("Press 'e' to disable exploration and reset agent position.") 160 | print("Press 'w' to reset agent position.") 161 | print("Press 1-{0} to change maps.".format(len(world_cfg.world_maps))) 162 | 163 | model = nengo.Network(seed=seed) 164 | with model: 165 | # Create the environment 166 | env = GridNode(world_cfg.world, dt=0.005) 167 | 168 | # Handle the movement of the agent, and generate the movement 169 | # "goodness" grade 170 | def move(t, x, my_world=world_cfg): 171 | speed, rotation = x 172 | dt = 0.001 173 | max_speed = 10.0 # 10.0 174 | max_rotate = 10.0 # 10.0 175 | my_world.agent.turn(rotation * dt * max_rotate) 176 | success = my_world.agent.go_forward(speed * dt * max_speed) 177 | if not success: 178 | my_world.agent.color = "red" 179 | return 0 180 | else: 181 | my_world.agent.color = "blue" 182 | return turn_bias + speed 183 | 184 | movement = nengo.Ensemble(n_neurons=100, dimensions=2, radius=1.4) 185 | movement_node = nengo.Node(move, size_in=2, label="reward") 186 | nengo.Connection(movement, movement_node) 187 | 188 | # Generate the context (radar distance to walls front, left, right) 189 | def detect(t): 190 | angles = (np.linspace(-0.5, 0.5, radar_dim) + agent.dir) % world.directions 191 | return [agent.detect(d, max_distance=4)[0] for d in angles] 192 | 193 | stim_radar = nengo.Node(detect) 194 | 195 | # Create the action selection networks 196 | bg = nengo.networks.actionselection.BasalGanglia(3) 197 | thal = nengo.networks.actionselection.Thalamus(3) 198 | nengo.Connection(bg.output, thal.input) 199 | 200 | # Convert the selection actions to movement transforms 201 | nengo.Connection(thal.output[0], movement, transform=[[1], [0]]) 202 | nengo.Connection(thal.output[1], movement, transform=[[0], [1]]) 203 | nengo.Connection(thal.output[2], movement, transform=[[0], [-1]]) 204 | 205 | # Generate the training (error) signal 206 | def error_func(t, x): 207 | actions = np.array(x[:3]) 208 | utils = np.array(x[3:6]) 209 | r = x[6] 210 | activate = x[7] 211 | 212 | max_action = max(actions) 213 | actions[actions < action_threshold] = 0 214 | actions[actions != max_action] = 0 215 | actions[actions == max_action] = 1 216 | 217 | return activate * ( 218 | np.multiply(actions, (utils - r) * (1 - r) ** 5) 219 | + np.multiply((1 - actions), (utils - 1) * (1 - r) ** 5) 220 | ) 221 | 222 | errors = nengo.Node(error_func, size_in=8, size_out=3) 223 | nengo.Connection(thal.output, errors[:3]) 224 | nengo.Connection(bg.input, errors[3:6]) 225 | nengo.Connection(movement_node, errors[6]) 226 | 227 | # the learning is done on the board 228 | adapt_ens = FpgaPesEnsembleNetwork( 229 | board, 230 | n_neurons=100 * radar_dim, 231 | dimensions=radar_dim, 232 | learning_rate=learn_rate, 233 | function=lambda x: init_transform, 234 | seed=1524081122, 235 | label="pes ensemble", 236 | ) 237 | adapt_ens.ensemble.radius = 4 238 | 239 | nengo.Connection(stim_radar, adapt_ens.input, synapse=learn_synapse) 240 | nengo.Connection(errors, adapt_ens.error) 241 | nengo.Connection(adapt_ens.output, bg.input) 242 | 243 | class LearnActive: 244 | """Class to store persistent learning state""" 245 | 246 | def __init__(self, my_world, page_data=None): 247 | self.my_world = my_world 248 | self.page = page_data 249 | 250 | self._is_learning = 1 251 | # _is_learning values: 252 | # < 0: no learning 253 | # 1: learning, will stop at learn_timeout 254 | # 2: continuous learning 255 | 256 | def __call__(self, t): 257 | if self.page is not None: 258 | init_agent_pos = False 259 | # Create a dictionary instead of if/else 260 | # "": (, ) 261 | keyboard_dict = { 262 | "q": (2, True), 263 | "e": (-1, True), 264 | "w": (self._is_learning, True), 265 | } 266 | 267 | for k in self.page.keys_pressed: 268 | if k.isdigit(): 269 | new_map_ind = int(k) - 1 270 | if new_map_ind != self.my_world.curr_ind: 271 | self.my_world.set_ind(new_map_ind) 272 | init_agent_pos = True 273 | elif k in list(keyboard_dict.keys()): 274 | self._is_learning, init_agent_pos = keyboard_dict[k] 275 | 276 | learning = ( 277 | (t <= learn_timeout) or (self._is_learning == 2) 278 | ) and self._is_learning > 0 279 | 280 | self._nengo_html_ = """ 281 | 282 | {1} 284 | 285 | """.format( 286 | "red" if learning else "grey", 287 | "Explore: ON" if learning else "Explore: Off", 288 | ) 289 | 290 | if not learning and self._is_learning == 1: 291 | init_agent_pos = True 292 | self._is_learning = -1 293 | 294 | if init_agent_pos: 295 | self.my_world.reset_pos() 296 | 297 | return int(learning) 298 | else: 299 | # Keyboard state branch not detected. Default to continuous learning 300 | self._nengo_html_ = """ 301 | 302 | 304 | Explore: ON 305 | 306 | """ 307 | 308 | # Return 1, to turn learning on permanently 309 | return 1 310 | 311 | # Need to pass in keyboard handler since it's not local to the class 312 | learn_on = nengo.Node(LearnActive(world_cfg, locals().get("__page__"))) 313 | nengo.Connection(learn_on, errors[7]) 314 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Usage 3 | ***** 4 | 5 | Converting from Standard Nengo 6 | ============================== 7 | 8 | NengoFPGA is an extension of :std:doc:`Nengo core `. 9 | Networks and models are described using the traditional Nengo workflow and a 10 | single ensemble, including PES learning, can be replaced with an FPGA ensemble 11 | using the ``FpgaPesEnsembleNetwork`` class. For example, consider the following 12 | example of a learned communication channel built with standard Nengo: 13 | 14 | .. code-block:: python 15 | 16 | import nengo 17 | import numpy as np 18 | 19 | 20 | def input_func(t): 21 | return [np.sin(t * 2*np.pi), np.cos(t * 2*np.pi)] 22 | 23 | with nengo.Network() as model: 24 | 25 | # Input stimulus 26 | input_node = nengo.Node(input_func) 27 | 28 | # "Pre" ensemble of neurons, and connection from the input 29 | pre = nengo.Ensemble(50, 2) 30 | nengo.Connection(input_node, pre) 31 | 32 | # "Post" ensemble of neurons, and connection from "Pre" 33 | post = nengo.Ensemble(50, 2) 34 | conn = nengo.Connection(pre, post) 35 | 36 | # Create an ensemble for the error signal 37 | # Error = actual - target = "post" - input 38 | error = nengo.Ensemble(50, 2) 39 | nengo.Connection(post, error) 40 | nengo.Connection(input_node, error, transform=-1) 41 | 42 | # Add the learning rule on the pre-post connection 43 | conn.learning_rule_type = nengo.PES(learning_rate=1e-4) 44 | 45 | # Connect the error into the learning rule 46 | nengo.Connection(error, conn.learning_rule) 47 | 48 | The Nengo code above creates two neural ensembles, ``pre`` and ``post``, and 49 | forms a PES-learning connection between these two ensembles. The weights of 50 | this connection are modulated by an error signal computed by a third neural 51 | ensemble (``error``). 52 | 53 | NengoFPGA can be used to replace the ``pre`` ensemble with an ensemble that 54 | will run on the FPGA. Converting the Nengo model above into a NengoFPGA model 55 | proceeds in three steps: 56 | 57 | 1. Replacing the desired neural ensemble with and FPGA ensemble. 58 | #. Making the appropriate connections to and from the FPGA ensemble. 59 | #. If desired (i.e., if learning is required), making the connections to and 60 | from an error-computing neural ensemble. 61 | 62 | Constructing the FPGA Ensemble 63 | ------------------------------ 64 | 65 | To use the FPGA ensemble, first import the ``FpgaPesEnsembleNetwork`` class: 66 | 67 | .. code-block:: python 68 | 69 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 70 | 71 | In the code above, the ``pre`` ensemble is to be replaced by the FPGA 72 | ensemble. The standard Nengo code for the ``pre`` ensemble was: 73 | 74 | .. code-block:: python 75 | 76 | # "Pre" ensemble of neurons, and connection from the input 77 | pre = nengo.Ensemble(50, 2) 78 | 79 | and this is replaced with the ``FpgaPesEnsembleNetwork`` class. Since learning 80 | is desired in the above model, the learning rule definition on the pre-post 81 | connection (``conn.learning_rule_type = nengo.PES(learning_rate=1e-4)``) has 82 | been removed and rolled into the ``FpgaPesEnsembleNetwork`` constructor. 83 | 84 | .. code-block:: python 85 | 86 | # "Pre" ensemble & learning rule 87 | ens_fpga = FpgaPesEnsembleNetwork('de1', n_neurons=50, 88 | dimensions=2, 89 | learning_rate=1e-4) 90 | 91 | Notice that the ``ens_fpga`` ensemble maintains the same arguments as the 92 | original ``pre`` ensemble and the learning rule which it encompasses -- 93 | 50 neurons, 2 dimensions, and a learning rate of 1e-4. The ``ens_fpga`` has 94 | an additional argument, in this case ``'de1'``, which specifies the desired 95 | FPGA device 96 | (see :ref:`NengoFPGA Software Configuration ` 97 | for more details). 98 | 99 | Connecting the FPGA Ensemble 100 | ---------------------------- 101 | 102 | With the FPGA ensemble created, the connections to and from the original 103 | ``pre`` ensemble will have to be updated. The original connections are defined 104 | as: 105 | 106 | .. code-block:: python 107 | 108 | # Connection from input to "pre" ensemble 109 | nengo.Connection(input_node, pre) 110 | 111 | # Connection from "pre" to "post" ensemble 112 | conn = nengo.Connection(pre, post) 113 | 114 | and are replaced with the slightly modified FPGA versions: 115 | 116 | .. code-block:: python 117 | 118 | # Connection from input to "pre" (FPGA) ensemble 119 | nengo.Connection(input_node, ens_fpga.input) # Note the added '.input' 120 | 121 | # Connection from "pre" (FPGA) to "post" ensemble 122 | nengo.Connection(ens_fpga.output, post) # Note the added '.output' 123 | 124 | The NengoFPGA connections are very similar to the original Nengo connections 125 | with the exception that they use the interfaces of the 126 | ``FpgaPesEnsembleNetwork`` object. 127 | The ``ens_fpga.input`` and ``ens_fpga.output`` replace the input and output 128 | of the original ``pre`` ensemble. 129 | 130 | Connecting the Error Ensemble 131 | ----------------------------- 132 | 133 | In the original Nengo model, a neural ensemble was used to compute the error 134 | signal that drives the PES learning rule. Using NengoFPGA, this neural 135 | ensemble is still needed, and the only change required is to modify the 136 | connections from this error ensemble to the FPGA ensemble. The original Nengo 137 | model defined the error ensemble and associated connections as: 138 | 139 | .. code-block:: python 140 | 141 | # Create an ensemble for the error signal 142 | # Error = actual - target = "post" - input 143 | error = nengo.Ensemble(50, 2) 144 | nengo.Connection(post, error) 145 | nengo.Connection(input_node, error, transform=-1) 146 | 147 | # Add the learning rule on the pre-post connection 148 | conn.learning_rule_type = nengo.PES(learning_rate=1e-4) 149 | 150 | # Connect the error into the learning rule 151 | nengo.Connection(error, conn.learning_rule) 152 | 153 | The NengoFPGA equivalent code would be: 154 | 155 | .. code-block:: python 156 | 157 | # Create an ensemble for the error signal 158 | # Error = actual - target = "post" - input 159 | error = nengo.Ensemble(50, 2) # Remains unchanged 160 | nengo.Connection(post, error) # Remains unchanged 161 | nengo.Connection(input_node, error, transform=-1) # Remains unchanged 162 | 163 | # Connect the error into the learning rule 164 | nengo.Connection(error, ens_fpga.error) # Note the added '.error' 165 | 166 | Note that -- as mentioned previously -- in the NengoFPGA equivalent code, the 167 | ``learning_rule_type`` definition of the pre-post connection has been removed 168 | as this is declared in the ``FpgaPesEnsembleNetwork`` object. 169 | 170 | 171 | Final NengoFPGA Model 172 | --------------------- 173 | 174 | Altogether the NengoFPGA version of the learned communication channel would 175 | look something like this: 176 | 177 | .. code-block:: python 178 | 179 | import nengo 180 | import numpy as np 181 | 182 | from nengo_fpga.networks import FpgaPesEnsembleNetwork 183 | 184 | def input_func(t): 185 | return [np.sin(t * 2*np.pi), np.cos(t * 2*np.pi)] 186 | 187 | with nengo.Network() as model: 188 | 189 | # Input stimulus 190 | input_node = nengo.Node(input_func) 191 | 192 | # "Pre" ensemble of neurons, and connection from the input 193 | ens_fpga = FpgaPesEnsembleNetwork('de1', n_neurons=50, 194 | dimensions=2, 195 | learning_rate=1e-4) 196 | nengo.Connection(input_node, ens_fpga.input) # Note the added '.input' 197 | 198 | # "Post" ensemble of neurons, and connection from "Pre" 199 | post = nengo.Ensemble(50, 2) 200 | conn = nengo.Connection(ens_fpga.output, post) # Note the added '.output' 201 | 202 | # Create an ensemble for the error signal 203 | # Error = actual - target = "post" - input 204 | error = nengo.Ensemble(50, 2) 205 | nengo.Connection(post, error) 206 | nengo.Connection(input_node, error, transform=-1) 207 | 208 | # Connect the error into the learning rule 209 | nengo.Connection(error, ens_fpga.error) # Note the added '.error' 210 | 211 | 212 | Basic Use 213 | ========= 214 | 215 | NengoFPGA is designed to work with NengoGUI, however you can see also run 216 | as a script if you prefer not to use the GUI. In either case, if the FPGA device 217 | is not correctly configured, or the NengoFPGA backend is not selected, the 218 | ``FpgaPesEnsembleNetwork`` will be converted to run as standard Nengo objects 219 | and a warning will be printed. 220 | 221 | For any questions please visit the `Nengo Forum `_. 222 | 223 | .. note:: 224 | Ensure you've configured your board **and** NengoFPGA as outlined in the 225 | :ref:`Getting Started Guide `. 226 | 227 | 228 | Using the GUI 229 | ------------- 230 | 231 | To view and run your networks, simply pass ``nengo_fpga`` as the backend to 232 | NengoGUI: 233 | 234 | .. code-block:: bash 235 | 236 | nengo -b nengo_fpga 237 | 238 | This should open the GUI in a browser and display the network from 239 | ``my_file.py``. You can begin execution by clicking the play button in the 240 | bottom left corner. this may take a few moments to establish a connection and 241 | initialize the FPGA device. 242 | 243 | .. _scripting: 244 | 245 | Scripting 246 | ========= 247 | 248 | If you are not using NengoGUI, you can use the ``nengo_fpga.Simulator`` in 249 | Nengo's scripting environment as well. Consider the following example of 250 | running a standard Nengo network: 251 | 252 | .. code-block:: python 253 | 254 | import nengo 255 | 256 | with nengo.Network() as model: 257 | 258 | # Your network description... 259 | 260 | with nengo.Simulator(model) as sim: 261 | sim.run(1) 262 | 263 | Simply replace the ``Simulator`` with the one from NengoFPGA: 264 | 265 | .. code-block:: python 266 | 267 | import nengo 268 | import nengo_fpga 269 | 270 | with nengo.Network() as model: 271 | 272 | # Your network description... 273 | # Including an FpgaPesEnsembleNetwork 274 | 275 | with nengo_fpga.Simulator(model) as sim: 276 | sim.run(1) 277 | 278 | 279 | Maximum Model Size 280 | ================== 281 | 282 | When running Nengo models on other hardware there is no set limit to model or 283 | network size. The system will continue to allocate resources (like memory) until 284 | it runs out which leads to different limits depending on the capabilities of 285 | your hardware. On the other hand, the NengoFPGA design is fixed and therefore we 286 | must provision resources up front. As a result, we have specific upper bounds 287 | which are chosen such that the resource allocation balances performance and 288 | flexibility for the given architecture. We store all neuron parameters on-chip 289 | giving us bounds based on specific memory requirements: 290 | 291 | - The maximum number of neurons, *N*, used to allocate memory for things like 292 | neuron activity and bias. 293 | - The maximum number of representational dimensions (input or output), *D*, 294 | used to allocate memory for things like the input and output vector. 295 | - The maximum product of neurons and dimensions, *NxD*, used to allocate 296 | memory for things like encoder and decoder matrices. 297 | 298 | These maximum model size values are summarized in the hardware-specific 299 | documentation: 300 | 301 | - :ref:`DE1-SoC feasible model size 302 | ` 303 | - :ref:`PYNQ-Z1 / Z2 feasible model size 304 | ` 305 | -------------------------------------------------------------------------------- /docs/examples/notebooks/03-integrator.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Integrator\n", 8 | "\n", 9 | "This example implements a one-dimensional neural integrator using an on-chip recurrent\n", 10 | "connection on the FPGA. It shows how neurons can be used to implement stable dynamics.\n", 11 | "Such dynamics are important for memory, noise cleanup, statistical inference, and many\n", 12 | "other dynamic transformations.\n", 13 | "\n", 14 | "In standard control theoretic form, the dynamics of a state vector can be described as:\n", 15 | "$$\n", 16 | "\\dot{x}(t) = \\mathbf{A}x(t) + \\mathbf{B}u(t)\n", 17 | "$$\n", 18 | "\n", 19 | "where $x(t)$ is the state vector, $u(t)$ is the time-varying input vector, $\\mathbf{A}$\n", 20 | "is the dynamics (feedback) matrix, and $\\mathbf{B}$ is the input matrix. For an\n", 21 | "integrator, $\\mathbf{A} = 0$ and $\\mathbf{B} = 1$.\n", 22 | "\n", 23 | "By applying the dynamics principal of the NEF, for a neural network using exponential\n", 24 | "synapses (like the FpgaPesEnsembleNetwork network), the feedback and input matrices can\n", 25 | "be converted into their equivalent neural forms with:\n", 26 | "$$\n", 27 | "\\begin{align}\n", 28 | " \\mathbf{A}' &= \\tau\\mathbf{A} + \\mathbf{I}\\\\\n", 29 | " \\mathbf{B}' &= \\tau\\mathbf{B}\n", 30 | "\\end{align}\n", 31 | "$$\n", 32 | "\n", 33 | "where $\\mathbf{A}'$ is the neural feedback matrix, $\\mathbf{B}'$ is the neural input\n", 34 | "matrix, and $\\tau$ is the post-synaptic time constant of the feedback connection. For an\n", 35 | "integrator:\n", 36 | "$$\n", 37 | "\\begin{align}\n", 38 | " \\mathbf{A}' &= \\tau \\times 0 + \\mathbf{I} = \\mathbf{I}\\\\\n", 39 | " \\mathbf{B}' &= \\tau \\times 1 = \\tau\n", 40 | "\\end{align}\n", 41 | "$$\n", 42 | "\n", 43 | "This implies that the feedback transform should be an identity, and the input transform\n", 44 | "to the neural population should be scaled by $\\tau$.\n", 45 | "\n", 46 | "When you run this example, it will automatically put in some step functions on the\n", 47 | "input, so you can see that the output is integrating (i.e. summing over time) the input.\n", 48 | "You can also input your own values. Note that since the integrator constantly sums its\n", 49 | "input, it will saturate quickly if you leave the input non-zero. This makes it clear\n", 50 | "that neurons have a finite range of representation. Such saturation effects can be\n", 51 | "exploited to perform useful computations (e.g. soft normalization)." 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## Step 1: Set up the Python Imports" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "%matplotlib inline\n", 68 | "import matplotlib.pyplot as plt\n", 69 | "\n", 70 | "import nengo\n", 71 | "from nengo.processes import Piecewise\n", 72 | "\n", 73 | "import nengo_fpga\n", 74 | "from nengo_fpga.networks import FpgaPesEnsembleNetwork" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## Step 2: Choose an FPGA Device\n", 82 | "\n", 83 | "Define the FPGA device on which the remote FpgaPesEnsembleNetwork will run. This name\n", 84 | "corresponds with the name in your `fpga_config` file. Recall that in the `fpga_config`\n", 85 | "file, device names are identified by the square brackets (e.g., **[de1]** or\n", 86 | "**[pynq]**). The names defined in your configuration file might differ from the example\n", 87 | "below. Here, the device **de1** is being used." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "board = \"de1\" # Change this to your desired device name" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "## Step 3: Create an Input for the Model\n", 104 | "We will use a piecewise step function as input, so we can see the effects of recurrence." 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": null, 110 | "metadata": {}, 111 | "outputs": [], 112 | "source": [ 113 | "model = nengo.Network(label=\"Integrator\")\n", 114 | "# Create a piecewise step function for input\n", 115 | "with model:\n", 116 | " input_node = nengo.Node(Piecewise({0: 0, 0.2: 1, 1: 0, 2: -2, 3: 0, 4: 1, 5: 0}))" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "## Step 4: Create the FPGA Ensemble\n", 124 | "\n", 125 | "Create a remote FPGA neural ensemble (`FpgaPesEnsembleNetwork`) using the board defined\n", 126 | "above, 100 neurons, 1 dimensions, and with no learning rate. We will also specify the\n", 127 | "recurrent connection here." 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "tau = 0.05 # Synaptic time constant\n", 137 | "\n", 138 | "with model:\n", 139 | " # Remote FPGA neural ensemble\n", 140 | " fpga_ens = FpgaPesEnsembleNetwork(\n", 141 | " board, # The board to use (from above)\n", 142 | " n_neurons=100, # The number of neurons to use in the ensemble\n", 143 | " dimensions=1, # 1 dimensions\n", 144 | " learning_rate=0, # No learning for this example\n", 145 | " feedback=1, # Activate the recurrent connection\n", 146 | " )\n", 147 | "\n", 148 | " # Setting `feedback=1` creates a `feedback` connection object\n", 149 | " # with the identity transform. You can set the attributes on this\n", 150 | " # feedback connection object, such as `.function` and `.transform`\n", 151 | " fpga_ens.feedback.synapse = tau # `nengo.LowPass(tau)`" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "## Step 5: Connect Everything Together\n", 159 | "The recurrent connection is housed on the FPGA device and is created as part of the\n", 160 | "`FpgaPesEnsembleNetwork` object, so the only connection that needs to be made is the\n", 161 | "input stimulus to the FPGA ensemble." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [ 170 | "with model:\n", 171 | " # Connect the stimulus, with a scaling transform of tau\n", 172 | " nengo.Connection(input_node, fpga_ens.input, synapse=tau, transform=tau)" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "## Step 6: Add Probes to Collect Data" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": null, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "with model:\n", 189 | " # The original input\n", 190 | " input_p = nengo.Probe(input_node)\n", 191 | "\n", 192 | " # The output from the FPGA ensemble\n", 193 | " # (filtered with a 10ms post-synaptic filter)\n", 194 | " output_p = nengo.Probe(fpga_ens.output, synapse=0.01)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "## Step 7: Run the Model" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": null, 207 | "metadata": {}, 208 | "outputs": [], 209 | "source": [ 210 | "with nengo_fpga.Simulator(model) as sim:\n", 211 | " sim.run(6)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "## Step 8: Plot the Results" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "def plot_results():\n", 228 | " plt.figure(figsize=(8, 5))\n", 229 | " plt.plot(sim.trange(), sim.data[input_p], \"k--\", label=\"Input\")\n", 230 | " plt.plot(sim.trange(), sim.data[output_p], label=\"Integrator output\")\n", 231 | " plot_ideal()\n", 232 | " plt.legend()\n", 233 | " plt.show()\n", 234 | "\n", 235 | "\n", 236 | "def plot_ideal():\n", 237 | " # Obtain the input function data from the input_node\n", 238 | " input_t = list(input_node.output.data.keys())\n", 239 | " input_v = list(input_node.output.data.values())\n", 240 | "\n", 241 | " # Construct the ideal output (assumes 1D signal)\n", 242 | " values = [[0]]\n", 243 | " input_t += [sim.trange()[-1]]\n", 244 | " for i, v in enumerate(input_v):\n", 245 | " values += [values[-1] + v * (input_t[i + 1] - input_t[i])]\n", 246 | "\n", 247 | " # Make the plot\n", 248 | " plt.plot(input_t, values, label=\"Ideal\")\n", 249 | "\n", 250 | "\n", 251 | "plot_results()" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "The plot above shows the neurons effectively integrating the input signal. Because of\n", 259 | "the implementation in neurons, the integration is not perfect (i.e., there will be some\n", 260 | "drift). Run the simulation several times to get a sense of the kinds of drift you might\n", 261 | "expect." 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "## Step 9: Spiking Neurons\n", 269 | "\n", 270 | "The plots above demonstrate the results of an integrator network implemented with\n", 271 | "non-spiking rectified linear neurons. The network can also be simulated using spiking\n", 272 | "neurons to illustrate the similarities and differences between a spiking and a\n", 273 | "non-spiking network.\n", 274 | "\n", 275 | "Below, we configure the FPGA neural ensemble to use spiking neurons, run the simulation,\n", 276 | "and plot the results." 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": null, 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "with model:\n", 286 | " fpga_ens.ensemble.neuron_type = nengo.SpikingRectifiedLinear()\n", 287 | "\n", 288 | "with nengo_fpga.Simulator(model) as sim:\n", 289 | " sim.run(6)\n", 290 | "plot_results()" 291 | ] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "metadata": {}, 296 | "source": [ 297 | "The plot above shows that while the output differs from the non-spiking simulation\n", 298 | "(because the network parameters are regenerated when a new simulation is created), the\n", 299 | "performance of the spiking integrator still conforms to the expected output of an\n", 300 | "integrator." 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "## Step 10: Experiment!\n", 308 | "\n", 309 | "Try playing around with the number of neurons in the FPGA ensemble as well as the\n", 310 | "synaptic time constant (`tau`) to see how it effects performance (e.g., observe how\n", 311 | "changing these numbers affect the drift)!" 312 | ] 313 | } 314 | ], 315 | "metadata": { 316 | "language_info": { 317 | "name": "python", 318 | "pygments_lexer": "ipython3" 319 | } 320 | }, 321 | "nbformat": 4, 322 | "nbformat_minor": 2 323 | } 324 | -------------------------------------------------------------------------------- /nengo_fpga/tests/test_id.py: -------------------------------------------------------------------------------- 1 | """Tests for the ID extraction process.""" 2 | import os 3 | import socket 4 | 5 | import pytest 6 | 7 | from nengo_fpga import fpga_config, id_extractor 8 | from nengo_fpga.id_extractor import IDExtractor 9 | 10 | 11 | def test_script(mocker): 12 | """Test the ID script run as __main__""" 13 | 14 | # Explicitly set args to parse 15 | args = [ 16 | "id_extractor.py", 17 | "test-name", 18 | ] 19 | mocker.patch("sys.argv", args) 20 | 21 | # Force id_script to run under __name__ == __main__ 22 | mocker.patch.object(id_extractor, "__name__", "__main__") 23 | 24 | # We only need to check if main is called, don't actually run here 25 | main_mock = mocker.patch.object(id_extractor, "main") 26 | 27 | id_extractor.run() 28 | 29 | main_mock.assert_called_with(args[1]) 30 | 31 | 32 | def test_missing_arg(mocker): 33 | """Test the ID script run as __main__ without fpga_name arg.""" 34 | 35 | # Explicitly set args to parse 36 | args = [ 37 | "id_extractor.py", 38 | ] 39 | mocker.patch("sys.argv", args) 40 | 41 | # Force id_script to run under __name__ == __main__ 42 | mocker.patch.object(id_extractor, "__name__", "__main__") 43 | 44 | # We only need to check if main is called, don't actually run here 45 | main_mock = mocker.patch.object(id_extractor, "main") 46 | 47 | # Should print help and exit, no call to main 48 | with pytest.raises(SystemExit): 49 | id_extractor.run() 50 | 51 | main_mock.assert_not_called() 52 | 53 | 54 | def test_main(mocker): 55 | """Test the main function of the ID extractor script.""" 56 | 57 | # Don't actually create the driver 58 | mocker.patch.object(IDExtractor, "__init__", return_value=None) 59 | 60 | # Keep track of calls we expect to see 61 | connect_mock = mocker.patch.object(IDExtractor, "connect") 62 | recv_id_mock = mocker.patch.object(IDExtractor, "recv_id") 63 | cleanup_mock = mocker.patch.object(IDExtractor, "cleanup") 64 | 65 | # Create a dummy ID attribute 66 | test_id = 12345 67 | mocker.patch.object(IDExtractor, "id_int", test_id, create=True) 68 | 69 | test_fpga_name = "test" 70 | id_extractor.main(test_fpga_name) 71 | 72 | # Check for calls we expect 73 | connect_mock.assert_called_once() 74 | recv_id_mock.assert_called_once() 75 | cleanup_mock.assert_called_once() 76 | 77 | # Check we wrote a file with the ID and cleanup 78 | id_file = os.path.join(os.getcwd(), "id_" + test_fpga_name + ".txt") 79 | assert os.path.isfile(id_file) 80 | os.remove(id_file) 81 | 82 | 83 | def test_driver_no_config(): 84 | """Test the ID extractor given an invalid fpga name.""" 85 | 86 | with pytest.raises(SystemExit): 87 | IDExtractor("not-a-valid-name") 88 | 89 | 90 | def test_driver_init(dummy_extractor): 91 | """Check a few token values on our dummy extractor init.""" 92 | assert dummy_extractor.ssh_info_str == "" 93 | assert dummy_extractor.config_found 94 | assert dummy_extractor.tcp_port > 0 95 | 96 | 97 | def test_cleanup(dummy_extractor, dummy_com, mocker): 98 | """Test the IDExtractor's cleanup function.""" 99 | 100 | # Mock out close calls 101 | dummy_extractor.tcp_init = dummy_com() 102 | tcp_mock = mocker.patch.object(dummy_extractor.tcp_init, "close") 103 | ssh_mock = mocker.patch.object(dummy_extractor.ssh_client, "close") 104 | 105 | dummy_extractor.cleanup() # Won't close tcp_recv since it's not created 106 | 107 | dummy_extractor.tcp_recv = dummy_com() 108 | recv_mock = mocker.patch.object(dummy_extractor.tcp_recv, "close") 109 | dummy_extractor.cleanup() # Should close tcp_recv now 110 | 111 | assert tcp_mock.call_count == 2 112 | assert ssh_mock.call_count == 2 113 | assert recv_mock.call_count == 1 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "ssh_method", [None, ("ssh_pwd", "passwd"), ("ssh_key", "key-path")] 118 | ) 119 | def test_connect_ssh_client(ssh_method, config_contents, gen_configs, mocker): 120 | """ 121 | Test the IDExtractor's connect_ssh_client function. 122 | 123 | Almost identical to test in "test_networks" 124 | """ 125 | 126 | # Create a dummy config for testing 127 | fname = os.path.join(os.getcwd(), "test-config") 128 | fpga_name = list(config_contents.keys())[1] 129 | 130 | # Add ssh_connection method to dummy config dict 131 | if ssh_method is not None: 132 | config_contents[fpga_name][ssh_method[0]] = ssh_method[1] 133 | 134 | gen_configs.create_config(fname, contents=config_contents) 135 | fpga_config.reload_config(fname) 136 | 137 | # Don't actually connect the socket in init 138 | mocker.patch("socket.socket.bind") 139 | mocker.patch("socket.socket.listen") 140 | 141 | # Create driver 142 | extractor = IDExtractor(fpga_name) 143 | 144 | # Don't actually connect the ssh client 145 | connect_mock = mocker.patch.object(extractor.ssh_client, "connect") 146 | 147 | ssh_user = config_contents[fpga_name]["ssh_user"] 148 | remote_ip = config_contents[fpga_name]["ip"] 149 | ssh_port = config_contents[fpga_name]["ssh_port"] 150 | extractor.connect_ssh_client(ssh_user, remote_ip) 151 | 152 | # Create the expected call based on the config 153 | if ssh_method is None: 154 | connect_mock.assert_called_once_with( 155 | remote_ip, port=ssh_port, username=ssh_user 156 | ) 157 | elif ssh_method[0] == "ssh_pwd": 158 | connect_mock.assert_called_once_with( 159 | remote_ip, port=ssh_port, username=ssh_user, password=ssh_method[1] 160 | ) 161 | elif ssh_method[0] == "ssh_key": 162 | connect_mock.assert_called_once_with( 163 | remote_ip, port=ssh_port, username=ssh_user, key_filename=ssh_method[1] 164 | ) 165 | 166 | 167 | def test_connect_thread_func(dummy_extractor, dummy_com, config_contents, mocker): 168 | """ 169 | Test the IDExtractor's connect_thread_func function. 170 | 171 | Similar to the test in "test_id" 172 | """ 173 | 174 | # Don't use ssh connections 175 | ssh_client_mock = mocker.patch.object(dummy_extractor, "connect_ssh_client") 176 | 177 | dummy_channel = dummy_com() 178 | mocker.patch.object( 179 | dummy_extractor.ssh_client, "invoke_shell", return_value=dummy_channel 180 | ) 181 | chan_send_mock = mocker.patch.object(dummy_channel, "send") 182 | chan_recv_mock = mocker.patch.object(dummy_channel, "recv") 183 | chan_close_mock = mocker.patch.object(dummy_channel, "close") 184 | 185 | # Mock some other class functions 186 | process_mock = mocker.patch.object(dummy_extractor, "process_ssh_output") 187 | check_str_mock = mocker.patch.object(dummy_extractor, "check_ssh_str") 188 | 189 | # Test working case 190 | # First pass we get input, then recv nothing to break on second pass 191 | recv_list = ["something", ""] 192 | chan_recv_mock.side_effect = recv_list 193 | check_str_mock.return_value = (0, []) 194 | dummy_extractor.ssh_info_str = "something\n" 195 | 196 | dummy_extractor.connect_thread_func() 197 | 198 | ssh_client_mock.assert_called_once_with( 199 | config_contents["test-fpga"]["ssh_user"], config_contents["test-fpga"]["ip"] 200 | ) 201 | chan_send_mock.assert_has_calls( 202 | [mocker.call("sudo su\n"), mocker.call(dummy_extractor.ssh_string)] 203 | ) 204 | assert chan_recv_mock.call_count == len(recv_list) 205 | chan_close_mock.assert_called_once() 206 | chan_close_mock.reset_mock() 207 | process_mock.assert_called_once() 208 | check_str_mock.assert_called_once() 209 | 210 | # Test recv fatal error 211 | check_str_mock.return_value = (2, []) 212 | recv_list = ["something", "another thing"] 213 | chan_recv_mock.side_effect = recv_list 214 | dummy_extractor.ssh_info_str = "something\n" 215 | 216 | with pytest.raises(RuntimeError): 217 | dummy_extractor.connect_thread_func() 218 | 219 | chan_close_mock.assert_called_once() 220 | 221 | 222 | def test_connect(dummy_extractor, mocker): 223 | """Test the IDExtractor's connect function.""" 224 | 225 | # Don't actually create and start a thread 226 | thread_mock = mocker.patch("threading.Thread") 227 | 228 | dummy_extractor.connect() 229 | 230 | thread_mock.assert_called_once_with( 231 | target=dummy_extractor.connect_thread_func, args=() 232 | ) 233 | 234 | 235 | def test_process_ssh_output(dummy_extractor): 236 | """ 237 | Test the IDExtractor's process_ssh_output. 238 | 239 | Almost identical to test in "test_networks" 240 | """ 241 | 242 | # Create a test string using carraige returns that should be replaced 243 | strs = ["First", "Second", "Third", "Fourth", "Fifth"] 244 | input_str = "{}\r\n{}\r\r{}\r{}\n{}".format(*strs) 245 | 246 | dummy_extractor.process_ssh_output(input_str.encode("latin1")) 247 | 248 | # New lines should all be '\n', so we should get our strs list back 249 | assert dummy_extractor.ssh_info_str.split("\n") == strs 250 | 251 | 252 | def test_check_ssh_str(dummy_extractor): 253 | """ 254 | Test remote string processing. 255 | 256 | Almost identical to test in "test_networks" 257 | """ 258 | 259 | # Init 260 | error_strs = [] 261 | got_error = 0 262 | 263 | # Test no error condition 264 | got_error, error_strs = dummy_extractor.check_ssh_str( 265 | "No Problem", error_strs, got_error, "1.2.3.4" 266 | ) 267 | assert got_error == 0 268 | assert error_strs == [] 269 | 270 | # Test killed condition 271 | kill_str = "Killed starts the string" 272 | got_error, error_strs = dummy_extractor.check_ssh_str( 273 | kill_str, error_strs, got_error, "1.2.3.4" 274 | ) 275 | assert got_error == 2 276 | assert error_strs[0] == kill_str 277 | 278 | # Reset 279 | error_strs = [] 280 | got_error = 0 281 | 282 | # Test traceback condition 283 | done_token = "done" 284 | trace_str = ["Traceback", " First line", " Second line", done_token] 285 | for s in trace_str: 286 | got_error, error_strs = dummy_extractor.check_ssh_str( 287 | s, error_strs, got_error, "1.2.3.4" 288 | ) 289 | # Expect error 1 until we finish 290 | expected_err = 2 if s == done_token else 1 291 | assert got_error == expected_err 292 | assert error_strs[-1] == s 293 | 294 | 295 | def test_ssh_string(dummy_extractor, config_contents): 296 | """ 297 | Test we have the correct arguments in the string command. 298 | 299 | Almost identical to test in "test_networks" 300 | """ 301 | 302 | # Split up the ssh command string 303 | args = dummy_extractor.ssh_string.split(" ") 304 | 305 | # Check args 306 | assert args[0] == "python" 307 | assert args[1] == config_contents["test-fpga"]["id_script"] 308 | assert args[2].split("=")[0] == "--host_ip" 309 | assert args[2].split("=")[1] == f"\"{config_contents['host']['ip']}\"" 310 | assert args[3].split("=")[0] == "--tcp_port" 311 | assert args[3].split("=")[1] == f"{dummy_extractor.tcp_port}\n" 312 | 313 | # Test default case 314 | dummy_extractor.config_found = False 315 | assert dummy_extractor.ssh_string == "" 316 | 317 | 318 | def test_recv_id(dummy_extractor, dummy_com, mocker): 319 | """Test the IDExtractor's recv_id function.""" 320 | 321 | # Arbitrary expected val 322 | expected_int = 1234 323 | 324 | # Setup mock sockets 325 | dummy_extractor.tcp_init = dummy_com() 326 | recv_socket = dummy_com() 327 | mocker.patch.object( 328 | recv_socket, "recv", return_value=expected_int.to_bytes(4, "big") 329 | ) 330 | accept_mock = mocker.patch.object( 331 | dummy_extractor.tcp_init, "accept", return_value=(recv_socket, 0) 332 | ) 333 | cleanup_mock = mocker.patch.object(dummy_extractor, "cleanup") 334 | 335 | dummy_extractor.recv_id() 336 | 337 | # Not checking bytes value since we explicitly return that in the mock 338 | assert dummy_extractor.id_int == expected_int 339 | 340 | # Update our mock to return an error and test the `except` case 341 | accept_mock.side_effect = socket.timeout() 342 | 343 | with pytest.raises(Exception): 344 | dummy_extractor.recv_id() 345 | 346 | # +1 calls for the initial successful call 347 | assert accept_mock.call_count == dummy_extractor.max_attempts + 1 348 | assert cleanup_mock.call_count == 1 349 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | *************** 2 | Getting Started 3 | *************** 4 | 5 | Things You Need 6 | =============== 7 | 8 | - :std:doc:`Nengo ` 9 | - A :ref:`supported FPGA board ` 10 | - A NengoFPGA Licence (available from `Applied Brain Research 11 | `_) 12 | - (Optional) `NengoGUI `_ 13 | 14 | .. _quick-guide: 15 | 16 | Installation Quick Reference Guide 17 | ================================== 18 | 19 | .. Do we have a troubleshooting piece here in case something doesn't work? 20 | 21 | .. rst-class:: compact 22 | 23 | First, get the your board NengoFPGA ready using the 24 | :ref:`device-specific board setup documentation `. 25 | Once your board is set up, do the following steps on the PC connected to the board: 26 | 27 | 1. :ref:`Install NengoFPGA `. 28 | #. :ref:`Edit the NengoFPGA config file ` to match your setup. 29 | #. Test NengoFPGA by running the ID extractor script: 30 | 31 | i. In a terminal on your computer navigate to the root ``nengo-fpga`` 32 | directory. 33 | #. Run the ID extractor script with: 34 | 35 | .. code-block:: bash 36 | 37 | python nengo_fpga/id_extractor.py 38 | 39 | where **** is the board name as it appears in ``fpga_config``. 40 | See the `Copy Protection`_ section for more information. 41 | 42 | #. Now with the Device ID available, you are ready to 43 | :ref:`acquire your bitstreams `. 44 | #. Once the bitstreams and supporting files have been delivered, copy these 45 | files to the appropriate location as outlined in the 46 | :ref:`device-specific bitstream documentation `. 47 | #. Test NengoFPGA by running an example script: 48 | 49 | i. Navigate to ``nengo-fpga/docs/examples``. 50 | #. Run the basic example script: 51 | 52 | .. code-block:: bash 53 | 54 | python basic_example.py 55 | 56 | where **** is the board name as it appears in ``fpga_config``. 57 | 58 | #. If the script has a successful run you will see a lot of **[INFO]** 59 | printed to the console indicating the status of the NengoFPGA system. 60 | Near the bottom you will see the RMSE of the network printed: 61 | 62 | .. code-block:: none 63 | 64 | Computed RMSE: 0.00105 65 | 66 | You may get a slightly different value but if your NengoFPGA system 67 | is functioning correctly, this should be near 0.001. 68 | 69 | .. note:: 70 | If you run the ``basic_example.py`` script and it hangs waiting for the 71 | simulation to begin but does not display any errors, then it is likely 72 | a firewall issue. Ensure your firewall allows connections to and from 73 | the board IP address. 74 | 75 | 76 | 77 | .. _software-install: 78 | 79 | NengoFPGA Software Installation 80 | =============================== 81 | 82 | Download the NengoFPGA source code from github using git: 83 | 84 | .. code-block:: bash 85 | 86 | git clone https://github.com/nengo/nengo-fpga.git 87 | 88 | or navigate to the `repository `_ and 89 | download the files manually. Once downloaded, navigate to the ``nengo-fpga`` 90 | folder in a terminal window and install with: 91 | 92 | .. code-block:: bash 93 | 94 | pip install -e . 95 | 96 | .. _board-setup: 97 | 98 | FPGA Board Setup 99 | ================ 100 | 101 | Follow documentation for your particular FPGA device: 102 | 103 | - :std:doc:`Board setup for Terasic DE1-SoC ` 104 | (Intel Cyclone V) 105 | - :std:doc:`Board setup for Digilent or TUL PYNQ ` 106 | (Xilinx Zynq) 107 | 108 | The full list of hardware that NengoFPGA supports, and the links to their 109 | respective documentation can be found :ref:`here `. 110 | 111 | .. _nengofpga-config: 112 | 113 | NengoFPGA Software Configuration 114 | ================================ 115 | 116 | NengoFPGA is the frontend that connects to one of many backend FPGA devices. You 117 | will need to have a :ref:`supported FPGA board ` with access 118 | to Applied Brain Research's designs. Each FPGA board will have it's own setup 119 | and configuration procedure outlined in it's own documentation, however, the 120 | NengoFPGA frontend has its own configuration as outlined below. 121 | 122 | The NengoFPGA default config file, ``fpga_config``, is located in the root 123 | directory of ``nengo-fpga`` and contains example settings for your host machine 124 | as well as the FPGA board you are using. You can also create a copy in the 125 | directory in which your project files are located. Anything in square brackets 126 | (eg. **[host]**) defines a new entry name and everything below that name up 127 | until the name defines parameters of that entry. 128 | 129 | Host 130 | ---- 131 | 132 | First we will look at the host configuration; this is information about your 133 | computer and must be called **[host]**: 134 | 135 | .. code-block:: none 136 | 137 | [host] 138 | ip = 10.162.177.10 139 | 140 | Make sure these lines are uncommented (remove the leading # *and* space so it 141 | appears as above). This is just an example value for **ip**, you will need to 142 | replace this with your computer's actual IP address, see :ref:`ip-addr` for 143 | instructions on finding your IP address. 144 | 145 | .. note:: 146 | Your computer IP address will need to be in the same subnet as the board IP 147 | address, follow your board specific instructions to get the board IP and 148 | setup your computer IP before proceeding. 149 | 150 | FPGA Board 151 | ---------- 152 | 153 | .. do we want any of this in the board-specific repos? 154 | 155 | The entries that define the FPGA board parameters have more values than the 156 | host entry, the name (eg. **[pynq]**) can be anything, though we recommend 157 | using a descriptive name such as **[pynq]** or **[de1]**. 158 | 159 | .. caution:: 160 | Every board connected to the same network *must* have its own entry 161 | in the config file. 162 | 163 | .. code-block:: none 164 | 165 | # Example DE1 FPGA board configuration 166 | [de1] 167 | ip = 10.162.177.236 168 | ssh_port = 22 169 | ssh_user = root 170 | ssh_pwd = 171 | # Refer to the online documentation for SSH key configuration options 172 | remote_script = /opt/nengo-de1/nengo_de1/single_pes_net.py 173 | id_script = /opt/nengo-de1/nengo_de1/id_script.py 174 | remote_tmp = /opt/nengo-de1/params 175 | udp_port = 0 176 | 177 | # Example PYNQ FPGA board configuration 178 | [pynq] 179 | ip = 10.162.177.99 180 | ssh_port = 22 181 | ssh_user = xilinx 182 | ssh_pwd = xilinx 183 | # Refer to the online documentation for SSH key configuration options 184 | remote_script = /opt/nengo-pynq/nengo_pynq/single_pes_net.py 185 | id_script = /opt/nengo-pynq/nengo_pynq/id_script.py 186 | remote_tmp = /opt/nengo-pynq/params 187 | udp_port = 0 188 | 189 | For whichever board you are using, make sure the lines in the appropriate 190 | sections are uncommented (remove the leading # *and* space so it 191 | appears as above). These default values should be correct unless you've 192 | modified the settings or installation of your FPGA board. These parameters are 193 | described here but modifications of these values will be described in the 194 | board-specific documentation. 195 | 196 | - **ip**: IP address of the FPGA board. 197 | - **ssh_port**: The port used to open SSH communications between the host 198 | and FPGA board. 199 | - **ssh_user**: SSH username to use to login to the board. 200 | - **ssh_pwd**: Password for **ssh_user** to use to login to the board. Note 201 | that the ``fpga_config`` file supports the use of SSH keys 202 | (see :ref:`ssh-key`) as an alternate form of authentication. 203 | - **remote_script**: The location of the main communication script on the FPGA 204 | board. 205 | - **id_script**: The location of the script that extracts the unique device 206 | identifier. 207 | - **remote_tmp**: Temporary location used to store data as it is transferred 208 | between the host and FPGA board. 209 | - **udp_port**: The port used for UDP communications between the host and FPGA 210 | board. 211 | 212 | .. note:: 213 | It should be noted that the FPGA board should be configured such that 214 | non-root users do not require a password to perform ``sudo`` commands. If you 215 | are using the NengoBrainBoard SD image on your board, this should already be 216 | done. If not, refer to the respective FGPA board documentation for instructions 217 | on how to do this. 218 | 219 | Copy Protection 220 | =============== 221 | 222 | Our hardware design (known as the bitstream) is locked to a specific device. 223 | Each bitstream is compiled with your unique board identifier (called Device ID) 224 | and therefore you will need to provide this unique ID to us before we 225 | can compile and deliver your tailored bitstream. 226 | 227 | .. _device-id: 228 | 229 | Reading Device ID 230 | ------------------ 231 | 232 | To easily read your Device ID, first ensure you have setup your board to be 233 | NengoFPGA ready. Instructions on how to do this can be found in each board's 234 | respective documentation (see :ref:`Board Setup `). 235 | Additionally, ensure you have reviewed the 236 | :ref:`NengoFPGA configuration ` section, 237 | and appropriately modified the ``fpga_config`` file. 238 | 239 | Once done, simply run the ``id_extractor.py`` script located in the 240 | ``nengo_fpga`` directory from within the ``nengo-fpga`` root folder. This will 241 | print the Device ID as well as save it to a file for future reference. The 242 | script requires that you provide the name of your board as it appears in the 243 | ``fpga_config`` file (eg. pynq, de1). From the root directory (``nengo-fpga``) 244 | run: 245 | 246 | .. code-block:: bash 247 | 248 | python nengo_fpga/id_extractor.py 249 | 250 | After running this script you will see some information printed to the console 251 | indicating the status of the NengoFPGA system. Upon successful execution 252 | of the script the final lines should read: 253 | 254 | .. code-block:: none 255 | 256 | Found board ID: 0X0123456789ABCDEF 257 | Written to file id_.txt 258 | 259 | Now that you have your Device ID, you are ready to 260 | :ref:`acquire your bitstreams `. 261 | 262 | Bitstreams 263 | ========== 264 | 265 | Compiled FPGA designs are binary files that configure the hardware, literally 266 | strings of bits, so compiled designs are often called *bitstreams*. When getting 267 | started or updating you NengoFPGA system, you will need to get bitstreams for 268 | your device. The NengoFPGA bitstreams are tailored to your specific device. 269 | 270 | A NengoFPGA license (available from the `Applied Brain Research store 271 | `_) is required to obtain a NengoFPGA 272 | bitstream. After purchasing the appropriate device-specific license, follow the 273 | instructions below to obtain your tailored bitstream. 274 | 275 | .. _get-bitstreams: 276 | 277 | Acquiring NengoFPGA Bitstreams 278 | ------------------------------ 279 | 280 | If you haven't already, you will need to :ref:`get your Device ID `. 281 | 282 | To receive your tailored bitstreams, please send us an email at 283 | `support@appliedbrainresearch.com`_ with the following information: 284 | 285 | - The email address tied to your NengoFPGA license. Typically this is the email address 286 | provided at the store checkout. 287 | - Your Device ID. Either the hex string itself or attach the ``id_.txt`` 288 | file to the email. 289 | - Which :ref:`supported hardware device ` is associated with 290 | that Device ID. 291 | - To help our support team provide a prompt response, please start your 292 | subject header with the term "NengoFPGA". 293 | 294 | 295 | .. _support@appliedbrainresearch.com: 296 | mailto:support@appliedbrainresearch.com?subject=NengoFPGA\ -\ 297 | 298 | 299 | .. _update-bitstreams: 300 | 301 | Updating NengoFPGA Bitstreams 302 | ----------------------------- 303 | 304 | Once you have received your bitstreams, follow documentation for your particular 305 | FPGA device for how to copy them to the board and get them running: 306 | 307 | - :ref:`Updating bitstreams for Terasic DE1-SoC 308 | ` 309 | (Intel Cyclone V) 310 | - :ref:`Updating bitstreams for Digilent or TUL PYNQ 311 | ` (Xilinx Zynq) 312 | 313 | -------------------------------------------------------------------------------- /nengo_fpga/id_extractor.py: -------------------------------------------------------------------------------- 1 | """Top level script for reading device ID.""" 2 | 3 | import argparse 4 | import os 5 | import socket 6 | import sys 7 | import threading 8 | 9 | import numpy as np 10 | import paramiko 11 | 12 | from nengo_fpga.fpga_config import fpga_config 13 | 14 | 15 | class IDExtractor: 16 | """ 17 | Class that connects to the FPGA and extracts the Device ID. 18 | 19 | Parameters 20 | ---------- 21 | fpga_name : str 22 | The name of the fpga defined in the config file. 23 | max_attempts : int, optional (Default: 5) 24 | The number of times the socket will attempt to connect to the board. 25 | timeout : float, optional (Default: 5) 26 | The number of seconds the socket will wait to connect. 27 | """ 28 | 29 | def __init__(self, fpga_name, max_attempts=5, timeout=5): 30 | self.config_found = fpga_config.has_section(fpga_name) 31 | self.fpga_name = fpga_name 32 | self.max_attempts = max_attempts 33 | self.timeout = timeout 34 | 35 | # Make SSHClient object 36 | self.ssh_client = paramiko.SSHClient() 37 | self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 38 | self.ssh_info_str = "" 39 | self.ssh_lock = False 40 | 41 | # Check if the desired FPGA name is defined in the configuration file 42 | if self.config_found: 43 | # Handle the tcp port selection: Use the config specified port. 44 | # If none is provided (i.e., the specified port number is 0), 45 | # choose a random tcp port number between 20000 and 65535. 46 | # We will use the udp port number from the config but use tcp. 47 | self.tcp_port = int(fpga_config.get(fpga_name, "udp_port")) 48 | if self.tcp_port == 0: 49 | self.tcp_port = int(np.random.uniform(low=20000, high=65535)) 50 | 51 | # Make the TCP socket for receiving Device ID. 52 | self.tcp_init = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 53 | self.tcp_init.bind((fpga_config.get("host", "ip"), self.tcp_port)) 54 | self.tcp_init.settimeout(self.timeout) 55 | self.tcp_init.listen(1) # Ready to accept a connection 56 | self.tcp_recv = None # Placeholder until socket is connected 57 | 58 | else: 59 | # FPGA name not found, unable to retrieve ID 60 | print( 61 | "ERROR: Specified FPGA configuration '" + fpga_name + "' not found.", 62 | flush=True, 63 | ) 64 | sys.exit() 65 | 66 | def cleanup(self): 67 | """Shutdown socket and SSH connection.""" 68 | self.tcp_init.close() 69 | self.ssh_client.close() 70 | 71 | if self.tcp_recv is not None: 72 | self.tcp_recv.close() 73 | 74 | def connect_ssh_client(self, ssh_user, remote_ip): 75 | """Helper function to parse config and setup ssh client.""" 76 | 77 | # Get the SSH options from the fpga_config file 78 | ssh_port = fpga_config.get(self.fpga_name, "ssh_port") 79 | 80 | if fpga_config.has_option(self.fpga_name, "ssh_pwd"): 81 | ssh_pwd = fpga_config.get(self.fpga_name, "ssh_pwd") 82 | else: 83 | ssh_pwd = None 84 | 85 | if fpga_config.has_option(self.fpga_name, "ssh_key"): 86 | ssh_key = os.path.expanduser(fpga_config.get(self.fpga_name, "ssh_key")) 87 | else: 88 | ssh_key = None 89 | 90 | # Connect to remote location over ssh 91 | if ssh_key is not None: 92 | # If an ssh key is provided, just use it 93 | self.ssh_client.connect( 94 | remote_ip, port=ssh_port, username=ssh_user, key_filename=ssh_key 95 | ) 96 | elif ssh_pwd is not None: 97 | # If an ssh password is provided, just use it 98 | self.ssh_client.connect( 99 | remote_ip, port=ssh_port, username=ssh_user, password=ssh_pwd 100 | ) 101 | else: 102 | # If no password or key is specified, just use the default connect 103 | # (paramiko will then try to connect using the id_rsa file in the 104 | # ~/.ssh/ folder) 105 | self.ssh_client.connect(remote_ip, port=ssh_port, username=ssh_user) 106 | 107 | def connect_thread_func(self): 108 | """Start SSH in a separate thread to monitor status.""" 109 | 110 | # # Get the IP of the remote device from the fpga_config file 111 | remote_ip = fpga_config.get(self.fpga_name, "ip") 112 | 113 | # # Get the SSH options from the fpga_config file 114 | ssh_user = fpga_config.get(self.fpga_name, "ssh_user") 115 | 116 | self.connect_ssh_client(ssh_user, remote_ip) 117 | 118 | # Invoke a shell in the ssh client 119 | ssh_channel = self.ssh_client.invoke_shell() 120 | 121 | # If board configuration specifies using sudo to run scripts 122 | # - Assume all non-root users will require sudo to run the scripts 123 | # - Note: Also assumes that the fpga has been configured to allow 124 | # the ssh user to run sudo commands WITHOUT needing a password 125 | # (see specific fpga hardware docs for details) 126 | if ssh_user != "root": 127 | print(f"<{remote_ip}> Script to be run with sudo. Sudoing.", flush=True) 128 | ssh_channel.send("sudo su\n") 129 | 130 | # Send required ssh string 131 | print( 132 | f"<{fpga_config.get(self.fpga_name, 'ip')}> Sending cmd to fpga board: \n" 133 | f"{self.ssh_string}", 134 | flush=True, 135 | ) 136 | ssh_channel.send(self.ssh_string) 137 | 138 | # Variable for remote error handling 139 | got_error = 0 140 | error_strs = [] 141 | 142 | # Get and process the information being returned over the ssh 143 | # connection 144 | while True: 145 | data = ssh_channel.recv(256) 146 | if not data: 147 | # If no data is received, the client has been closed, so close 148 | # the channel, and break out of the while loop 149 | ssh_channel.close() 150 | break 151 | 152 | self.process_ssh_output(data) 153 | info_str_list = self.ssh_info_str.split("\n") 154 | for info_str in info_str_list[:-1]: 155 | got_error, error_strs = self.check_ssh_str( 156 | info_str, error_strs, got_error, remote_ip 157 | ) 158 | self.ssh_info_str = info_str_list[-1] 159 | 160 | # The traceback usually contains 3 lines, so collect the first 161 | # three lines then display it. 162 | if got_error == 2: 163 | ssh_channel.close() 164 | raise RuntimeError( 165 | f"Received the following error on the remote side <{remote_ip}>:\n" 166 | + "\n".join(error_strs) 167 | ) 168 | 169 | def connect(self): 170 | """Connect to device via SSH.""" 171 | print( 172 | f"<{fpga_config.get(self.fpga_name, 'ip')}> Open SSH connection", 173 | flush=True, 174 | ) 175 | # Start a new thread to open the ssh connection. Use a thread to 176 | # handle the opening of the connection because it can lag for certain 177 | # devices, and we don't want it to impact the rest of the build process. 178 | connect_thread = threading.Thread(target=self.connect_thread_func, args=()) 179 | connect_thread.start() 180 | 181 | def process_ssh_output(self, data): 182 | """Clean up the data stream coming back over ssh.""" 183 | str_data = data.decode("latin1").replace("\r\n", "\r") 184 | str_data = str_data.replace("\r\r", "\r") 185 | str_data = str_data.replace("\r", "\n") 186 | 187 | # Process and dump the returned ssh data to logger. Data (strings) 188 | # returned over SSH are terminated by a newline, so, keep track of 189 | # the data and write the data to logger only when a newline is 190 | # received. 191 | self.ssh_info_str += str_data 192 | 193 | def check_ssh_str(self, info_str, error_strs, got_error, remote_ip): 194 | """Process info from ssh and check for errors.""" 195 | 196 | if info_str.startswith("Killed"): 197 | print(f"<{remote_ip}> ENCOUNTERED ERROR!", flush=True) 198 | got_error = 2 199 | 200 | if info_str.startswith("Traceback"): 201 | print(f"<{remote_ip}> ENCOUNTERED ERROR!", flush=True) 202 | got_error = 1 203 | elif got_error > 0 and info_str[0] != " ": 204 | # Error string is no longer tabbed, so the actual error 205 | # is bring printed. Collect and terminate (see below) 206 | got_error = 2 207 | 208 | if got_error > 0: 209 | # Once an error is encountered, keep collecting error 210 | # messages until the termination condition (above) 211 | error_strs.append(info_str) 212 | else: 213 | print(f"<{remote_ip}> {info_str}", flush=True) 214 | 215 | return got_error, error_strs 216 | 217 | @property 218 | def ssh_string(self): 219 | """ 220 | Command sent to FPGA device to begin execution. 221 | 222 | Generate the string to be sent over the ssh connection to run the remote 223 | side ssh script (with appropriate arguments) 224 | """ 225 | ssh_str = "" 226 | if self.config_found: 227 | ssh_str = ( 228 | "python " 229 | + fpga_config.get(self.fpga_name, "id_script") 230 | + f" --host_ip=\"{fpga_config.get('host', 'ip')}\"" 231 | + f" --tcp_port={self.tcp_port}" 232 | + "\n" 233 | ) 234 | return ssh_str 235 | 236 | def recv_id(self): 237 | """Read device ID from device.""" 238 | 239 | # Try to connect to FPGA socket a few times 240 | connect_attempts = 0 241 | while True: 242 | try: 243 | self.tcp_recv, _addr = self.tcp_init.accept() 244 | break 245 | except socket.timeout as e: 246 | connect_attempts += 1 247 | if connect_attempts >= self.max_attempts: 248 | self.cleanup() 249 | raise RuntimeError( 250 | f"Could not connect to {self.fpga_name}" 251 | ", please ensure you have the correct" 252 | " FPGA configuration or increase the" 253 | " number of connection attempts." 254 | ) from e 255 | else: 256 | print( 257 | f"WARNING: Could not connect to {self.fpga_name} for " 258 | f"{self.timeout:.1f}s," 259 | " trying again...", 260 | flush=True, 261 | ) 262 | 263 | # Read ID once socket connected successfully 264 | self.id_bytes = self.tcp_recv.recv(8) 265 | self.id_int = int.from_bytes(self.id_bytes, "big") 266 | 267 | 268 | def main(fpga_name): 269 | """Main script to extract device ID.""" 270 | filename = "id_" + fpga_name + ".txt" 271 | 272 | # Connect to FPGA, run script to get ID, write ID to file 273 | fpga = IDExtractor(fpga_name) 274 | fpga.connect() 275 | fpga.recv_id() 276 | 277 | id_str = f"Found board ID: {fpga.id_int:#018X}" 278 | 279 | with open(filename, "w", encoding="ascii") as file: 280 | file.write(id_str) 281 | fpga.cleanup() 282 | print(id_str) 283 | print(f"Written to file {filename}") 284 | 285 | 286 | def run(): 287 | """Wrapped in a function so we can call this in tests.""" 288 | if __name__ == "__main__": 289 | parser = argparse.ArgumentParser( 290 | description="Generic script for running the ID Extractor on the " 291 | + "FPGA board." 292 | ) 293 | 294 | # FPGA board name 295 | parser.add_argument( 296 | "fpga_name", 297 | type=str, 298 | help="Name of the FPGA board as specified in the fpga_config file", 299 | ) 300 | 301 | # Print full help text, otherwise error message isn't very useful 302 | if len(sys.argv) != 2: 303 | parser.print_help() 304 | sys.exit() 305 | 306 | # Parse the arguments 307 | args = parser.parse_args() 308 | 309 | main(args.fpga_name) 310 | 311 | 312 | run() # Run the __name__ == __main__ case by default (wrapper function) 313 | -------------------------------------------------------------------------------- /docs/examples/notebooks/02-set-neuron-params.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Set Neuron Parameters\n", 8 | "\n", 9 | "This is an example of how to set neuron parameters, such as encoder and decoder\n", 10 | "matrices, on the FPGA. This is useful if you've pre-solved weights (encoders and\n", 11 | "decoders) elsewhere or if you want to start in a known state.\n", 12 | "\n", 13 | "The `FpgaPesEnsembleNetwork` encapsulates a Nengo `Ensemble` and `Connection`\n", 14 | "object, each of which you can modify. Modifying the attributes of the `Ensemble`\n", 15 | "object alters the neuron parameters simulated on the FPGA, while modifying the\n", 16 | "attributes of the `Connection` object alters the parameters of the FPGA ensemble\n", 17 | "output connection that is simulated on the FPGA.\n", 18 | "\n", 19 | "In order to change the attributes of the Ensemble or Connection objects, first\n", 20 | "create the FpgaPesEnsembleNetwork object:\n", 21 | "\n", 22 | " fpga_ens = FpgaPesEnsembleNetwork(...)\n", 23 | "\n", 24 | "From which you can access the encapsulated `Ensemble` and `Connection` objects\n", 25 | "(using the `.ensemble` and `.connection` attributes of the `FpgaPesEnsembleNetwork`\n", 26 | "object respectively):\n", 27 | "\n", 28 | " fpga_ens.ensemble\n", 29 | " fpga_ens.connection\n", 30 | "\n", 31 | "Use these two objects to set specific attributes as desired.\n", 32 | "Please look at the Nengo documentation for\n", 33 | "[Ensembles](https://www.nengo.ai/nengo/frontend-api.html#nengo.Ensemble) and\n", 34 | "[Connections](https://www.nengo.ai/nengo/frontend-api.html#nengo.Connection)\n", 35 | "for a full list of options.\n", 36 | "\n", 37 | "## Step 1: Set up Python Imports" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "%matplotlib inline\n", 47 | "import matplotlib.pyplot as plt\n", 48 | "import numpy as np\n", 49 | "\n", 50 | "import nengo\n", 51 | "from nengo.solvers import NoSolver\n", 52 | "\n", 53 | "import nengo_fpga\n", 54 | "from nengo_fpga.networks import FpgaPesEnsembleNetwork" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Step 2: Choose an FPGA Device\n", 62 | "\n", 63 | "Define the FPGA device on which the remote FpgaPesEnsembleNetwork will run.\n", 64 | "This name corresponds with the name in your `fpga_config` file. Recall that\n", 65 | "in the `fpga_config` file, device names are identified by the square brackets\n", 66 | "(e.g., **[de1]** or **[pynq]**). The names defined in your configuration file\n", 67 | "might differ from the example below. Here, the device **de1** is being used." 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "board = \"de1\" # Change this to your desired device name" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "## Step 3: Create Some Weights\n", 84 | "\n", 85 | "Generally, the `encoder` and `decoder` matrices can be generated\n", 86 | "using your desired methods (e.g., through randomization, or by using\n", 87 | "a machine learning algorithm). Typically, the `gain` and `bias` values\n", 88 | "of the neurons need to also be configured to work with the pre-generated\n", 89 | "weight matrices in order to have a computationally functional model.\n", 90 | "In this notebook, Nengo will be used to generate the various neural\n", 91 | "ensemble parameters that functionally define a communication channel.\n", 92 | "\n", 93 | "The example code below uses a 2-dimensional neural ensemble consisting of 4 ReLU\n", 94 | "rate-based neurons. The encoders for each neuron will be configured such\n", 95 | "that each neuron represents one quadrant of a sine and cosine period.\n", 96 | "To use Nengo to generate the various neuron parameters, two steps are\n", 97 | "required. First, a temporary (parameterization) network with the desired\n", 98 | "ensemble properties (e.g., number of neurons, firing rates, etc.) is\n", 99 | "created. Then a `nengo.Simulator` object is initialized using this\n", 100 | "network and in doing so, Nengo computes the neuron parameters specific\n", 101 | "to the parameterization network. These parameters can then be retrieved\n", 102 | "using the `nengo.Simulator` object." 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "# Dummy network to generate neuron params\n", 112 | "with nengo.Network(seed=1) as param_net:\n", 113 | " # Ensemble with our desired parameters:\n", 114 | " a = nengo.Ensemble(\n", 115 | " n_neurons=4, # Number of neurons\n", 116 | " dimensions=2, # Dimensions\n", 117 | " neuron_type=nengo.RectifiedLinear(), # Neuron model\n", 118 | " encoders=[\n", 119 | " [0.707, 0.707],\n", 120 | " [-0.707, 0.707], # Encoders\n", 121 | " [-0.707, -0.707],\n", 122 | " [0.707, -0.707],\n", 123 | " ],\n", 124 | " intercepts=nengo.dists.Choice([0]), # Intercepts\n", 125 | " max_rates=nengo.dists.Choice([100]), # Max rates\n", 126 | " )\n", 127 | "\n", 128 | " # An output connection is needed in order for Nengo to solve\n", 129 | " # for the decoders. Note that because no function is defined\n", 130 | " # for the output connection, Nengo will compute decoders that\n", 131 | " # approximate the identity function\n", 132 | " # (i.e., making a communication channel).\n", 133 | " b = nengo.Node(size_in=2)\n", 134 | " conn = nengo.Connection(a, b)\n", 135 | "\n", 136 | "# Neuron parameters are generated when the simulator object is created.\n", 137 | "with nengo.Simulator(param_net) as param_sim:\n", 138 | " bias = param_sim.data[a].bias\n", 139 | " encoders = param_sim.data[a].encoders\n", 140 | " gain = param_sim.data[a].gain\n", 141 | " decoders = param_sim.data[conn].weights" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "metadata": {}, 147 | "source": [ 148 | "## Step 4: Create the FPGA Ensemble\n", 149 | "\n", 150 | "The network created in the previous step was used generate\n", 151 | "the neuron parameters and is not intended to be run on the\n", 152 | "FPGA. Here, the Nengo model that is to be run on the\n", 153 | "FPGA is created. Note that for this example, the decoders\n", 154 | "have been initialized to zero in order to demonstrate the\n", 155 | "effect of using a network with a decoder weight matrix of\n", 156 | "zero versus a network using the pre-solved decoder weights\n", 157 | "from the previous step." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "with nengo.Network() as model:\n", 167 | " # Use the lambda function to generate [sin, cos]\n", 168 | " input_node = nengo.Node(output=lambda t: [np.sin(t * np.pi), np.cos(t * np.pi)])\n", 169 | "\n", 170 | " # Create the FPGA ensemble\n", 171 | " fpga_ens = FpgaPesEnsembleNetwork(\n", 172 | " board,\n", 173 | " n_neurons=4,\n", 174 | " dimensions=2,\n", 175 | " learning_rate=0,\n", 176 | " function=lambda x: [0, 0], # Initialize decoders to 0\n", 177 | " )\n", 178 | " nengo.Connection(input_node, fpga_ens.input)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "## Step 5: Add Probes to Collect Data\n", 186 | "\n", 187 | "Just so we can monitor the input and output to confirm\n", 188 | "we set the weights correctly." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "with model:\n", 198 | " # The original input\n", 199 | " input_p = nengo.Probe(input_node, synapse=0.01)\n", 200 | "\n", 201 | " # The output from the FPGA ensemble\n", 202 | " # (filtered with a 10ms post-synaptic filter)\n", 203 | " output_p = nengo.Probe(fpga_ens.output, synapse=0.01)" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "## Step 6: Test Before Setting Weights\n", 211 | "\n", 212 | "In order to ensure we are in fact setting the weights, we will\n", 213 | "run the network with the zero-initialized decoders to see what\n", 214 | "happens first." 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "with nengo_fpga.Simulator(model) as sim:\n", 224 | " sim.run(2) # Run for 2 seconds" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "plt.figure(figsize=(16, 5))\n", 234 | "plt.subplot(1, 2, 1)\n", 235 | "plt.title(\"Probed Results (Dimension 1: sin)\")\n", 236 | "plt.plot(sim.trange(), sim.data[input_p][:, 0])\n", 237 | "plt.plot(sim.trange(), sim.data[output_p][:, 0])\n", 238 | "plt.legend((\"Input\", \"Output\"), loc=\"lower left\")\n", 239 | "plt.xlabel(\"Sim time (s)\")\n", 240 | "\n", 241 | "plt.subplot(1, 2, 2)\n", 242 | "plt.title(\"Probed Results (Dimension 2: cos)\")\n", 243 | "plt.plot(sim.trange(), sim.data[input_p][:, 1])\n", 244 | "plt.plot(sim.trange(), sim.data[output_p][:, 1])\n", 245 | "plt.legend((\"Input\", \"Output\"), loc=\"lower left\")\n", 246 | "plt.xlabel(\"Sim time (s)\")\n", 247 | "\n", 248 | "plt.show()" 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "Notice that the output for both dimensions is zero, as expected." 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": {}, 261 | "source": [ 262 | "## Step 7: Set Weights and Test\n", 263 | "\n", 264 | "Now, we will configure our FPGA ensemble to use the neuron\n", 265 | "parameters generated in [Step 3](#Step-3:-Create-Some-Weights).\n", 266 | "In order to demonstrate the correct behaviour of the network,\n", 267 | "a new Nengo simulation is created and run." 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "metadata": {}, 274 | "outputs": [], 275 | "source": [ 276 | "with model:\n", 277 | " # Explicitly set ensemble attributes\n", 278 | " fpga_ens.ensemble.bias = bias\n", 279 | " fpga_ens.ensemble.encoders = encoders\n", 280 | " fpga_ens.ensemble.gain = gain\n", 281 | "\n", 282 | " # To set the decoders, we need to use nengo.NoSolver\n", 283 | " # to tell the builder we will provide out own decoders\n", 284 | " # (you can also set \"weights\" here instead,\n", 285 | " # see nengo.solvers.NoSolver for more info)\n", 286 | " fpga_ens.connection.solver = NoSolver(decoders.T) # Transposed\n", 287 | "\n", 288 | "with nengo_fpga.Simulator(model) as sim:\n", 289 | " sim.run(2) # Run for 2 seconds" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": null, 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [ 298 | "plt.figure(figsize=(16, 5))\n", 299 | "plt.subplot(1, 2, 1)\n", 300 | "plt.title(\"Probed Results (Dimension 1: sin)\")\n", 301 | "plt.plot(sim.trange(), sim.data[input_p][:, 0])\n", 302 | "plt.plot(sim.trange(), sim.data[output_p][:, 0])\n", 303 | "plt.legend((\"Input\", \"Output\"), loc=\"lower left\")\n", 304 | "plt.xlabel(\"Sim time (s)\")\n", 305 | "\n", 306 | "plt.subplot(1, 2, 2)\n", 307 | "plt.title(\"Probed Results (Dimension 2: cos)\")\n", 308 | "plt.plot(sim.trange(), sim.data[input_p][:, 1])\n", 309 | "plt.plot(sim.trange(), sim.data[output_p][:, 1])\n", 310 | "plt.legend((\"Input\", \"Output\"), loc=\"lower left\")\n", 311 | "plt.xlabel(\"Sim time (s)\")\n", 312 | "\n", 313 | "plt.show()" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "As the graphs above illustrate, now that the we've set neuron\n", 321 | "parameters to those we generated, the FPGA ensemble behaves\n", 322 | "like a communication channel and successfully reproduces\n", 323 | "the sine and cosine signals." 324 | ] 325 | } 326 | ], 327 | "metadata": { 328 | "anaconda-cloud": {}, 329 | "language_info": { 330 | "name": "python", 331 | "pygments_lexer": "ipython3" 332 | } 333 | }, 334 | "nbformat": 4, 335 | "nbformat_minor": 2 336 | } 337 | --------------------------------------------------------------------------------