From f24d4c4e7b86c21a0f2d3a52c9d27c8e55537e2d Mon Sep 17 00:00:00 2001 From: Daphne Cornelisse Date: Tue, 3 Oct 2023 13:45:48 +0200 Subject: [PATCH 1/2] Feature: Added Sphinx documentation and GH workflow for docs hosting using Github Pages --- .github/workflows/run_build_docs.yml | 56 +++++++++++ .gitignore | 5 +- .pre-commit-config.yaml | 18 +++- README.md | 8 +- docs/Makefile | 20 ++++ docs/make.bat | 35 +++++++ docs/source/_static/logo.png | Bin 0 -> 89690 bytes docs/source/api_python.md | 9 ++ docs/source/changelog.md | 6 ++ docs/source/coc.md | 6 ++ docs/source/conf.py | 139 +++++++++++++++++++++++++++ docs/source/contributing.md | 6 ++ docs/source/index.md | 35 +++++++ docs/source/references.bib | 0 pyproject.toml | 14 +++ requirements.dev.txt | 2 +- requirements.docs.txt | 87 +++++++++++++++++ requirements.txt | 2 +- 18 files changed, 439 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/run_build_docs.yml create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/logo.png create mode 100644 docs/source/api_python.md create mode 100644 docs/source/changelog.md create mode 100644 docs/source/coc.md create mode 100644 docs/source/conf.py create mode 100644 docs/source/contributing.md create mode 100644 docs/source/index.md create mode 100644 docs/source/references.bib create mode 100644 requirements.docs.txt diff --git a/.github/workflows/run_build_docs.yml b/.github/workflows/run_build_docs.yml new file mode 100644 index 00000000..9c66225d --- /dev/null +++ b/.github/workflows/run_build_docs.yml @@ -0,0 +1,56 @@ +# Workflow for building Sphinx docs and deploying to GH Pages +name: Build Sphinx docs and deploy to GH Pages + +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + branch: + description: Branch to build docs for + required: true + default: main + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, cancelling in-progress runs. +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "This job is now running on a ${{ runner.os }} server." + - run: echo "Running on branch ${{ github.ref }} of repository ${{ github.repository }}." + - name: Check out repository code. + uses: actions/checkout@v3 + - name: Python environment setup + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install dependencies. + run: | + sudo apt-get update + sudo apt-get install libsfml-dev + git submodule sync + git submodule update --init --recursive + python -m pip install -U pip poetry + poetry install --with=docs + - name: Build Sphinx docs + run: poetry run sphinx-build -b html ${{ github.workspace }}/docs/source ${{ github.workspace }}/docs/build/html + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: ./docs/build/html # Upload HTML docs only + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 diff --git a/.gitignore b/.gitignore index 216c8af1..c3cb92a9 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ configs.json .python-version # poetry -poetry.lock \ No newline at end of file +poetry.lock + +# Static doc files +!docs/source/_static/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ed90acc..5ed29a9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,11 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: check-yaml - id: sort-simple-yaml - id: check-json - id: check-merge-conflict - id: check-symlinks - id: debug-statements - - id: requirements-txt-fixer - id: check-added-large-files - repo: https://github.com/python-poetry/poetry rev: 1.6.0 @@ -53,6 +51,21 @@ repos: args: [--autofix, --no-sort] - id: pretty-format-yaml args: [--autofix] +- repo: https://github.com/mwouts/jupytext + rev: v1.15.2 + hooks: + - id: jupytext + args: [--from, ipynb, --to, md:myst, --sync] # fmt: off +- repo: https://github.com/nbQA-dev/nbQA + rev: 1.7.0 + hooks: + - id: nbqa-pyupgrade + args: [--py310-plus] + - id: nbqa-black + - id: nbqa-isort + args: [--profile=black] + - id: nbqa-flake8 + args: [--extend-ignore=E203] - repo: local hooks: - id: pylint @@ -60,3 +73,4 @@ repos: entry: poetry run pylint language: system types: [python] + exclude: ^docs/.* diff --git a/README.md b/README.md index cafb3bff..2ae4dcd6 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ Using this rich data source, Nocturne contains a wide range of scenarios whose s

--> -![Intersection Scene with Obscured View](./docs/readme_files/git_intersection_combined.gif) +![Intersection Scene with Obscured View](https://github.com/facebookresearch/nocturne/raw/main/docs/readme_files/git_intersection_combined.gif) Nocturne features a rich variety of scenes, ranging from parking lots, to merges, to roundabouts, to unsignalized intersections. -![Intersection Scene with Obscured View](./docs/readme_files/nocturne_3_by_3_scenes.gif) +![Intersection Scene with Obscured View](https://github.com/facebookresearch/nocturne/raw/main/docs/readme_files/nocturne_3_by_3_scenes.gif) More videos can be found [here](https://www.nathanlct.com/research/nocturne). @@ -36,7 +36,7 @@ The corresponding paper is available at: [https://arxiv.org/abs/2206.09889](http ## Dependencies -[CMake](https://cmake.org/) is required to compile the C++ library. +[CMake](https://cmake.org/) is required to compile the C++ library. Run `cmake --version` to see whether CMake is already installed in your environment. If not, refer to the CMake website instructions for installation, or you can use: @@ -92,7 +92,7 @@ python setup.py develop If you are not using Conda, simply run the last command to build and install Nocturne at your default Python path. -You should then be all set to use the library. To find an example of constructing a Gym environment, using a basic Simulation, or rendering scenes, go to +You should then be all set to use the library. To find an example of constructing a Gym environment, using a basic Simulation, or rendering scenes, go to ```examples``` and run respectively, ```create_env.py```, ```nocturne_functions.py``` or ```rendering.py```. Python tests can be run with `pytest`. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..dc1312ab --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c26e1d79c291d031b1e52cdde87870c723b0ce GIT binary patch literal 89690 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Ģerw)JfL|2;4AHuq;|&K%8|>l={iZ%g@e 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 H*+={jSM`+S literal 0 HcmV?d00001 diff --git a/docs/source/api_python.md b/docs/source/api_python.md new file mode 100644 index 00000000..0fd775fc --- /dev/null +++ b/docs/source/api_python.md @@ -0,0 +1,9 @@ +--- +title: Nocturne | API Reference +--- +# API Reference + +```{eval-rst} +.. autosummary:: nocturne + :toctree: generated +``` diff --git a/docs/source/changelog.md b/docs/source/changelog.md new file mode 100644 index 00000000..9695d069 --- /dev/null +++ b/docs/source/changelog.md @@ -0,0 +1,6 @@ +--- +title: Changelog +--- + +```{include} ../../CHANGELOG.md +``` diff --git a/docs/source/coc.md b/docs/source/coc.md new file mode 100644 index 00000000..39a1be4f --- /dev/null +++ b/docs/source/coc.md @@ -0,0 +1,6 @@ +--- +title: Code of Conduct +--- + +```{include} ../../CODE_OF_CONDUCT.md +``` diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..6cb631d4 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,139 @@ +""" +Configuration file for the Sphinx documentation builder. + +For the full list of built-in configuration values, see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +# -- Path setup -------------------------------------------------------------- + +import pathlib +import sys + +sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) + + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "Nocturne" +copyright = "2023, The Nocturne Authors" +author = "The Nocturne Authors" + +# The short X.Y version +version = "0.0.1" +# The full version, including alpha/beta/rc tags +release = "0.0.1" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.autosectionlabel", + "sphinx.ext.autosummary", + "sphinx.ext.duration", + "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "myst_nb", # See: https://myst-nb.readthedocs.io/en/latest/ + "sphinxcontrib.bibtex", # See: https://sphinxcontrib-bibtex.readthedocs.io/en/latest/quickstart.html + "sphinx_autodoc_typehints", # See: https://github.com/tox-dev/sphinx-autodoc-typehints +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.8", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy-1.8.1/", None), +} + +bibtex_bibfiles = ["references.bib"] + +autosummary_generate = True + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_book_theme" +html_theme_options = { + "path_to_docs": "docs", + "use_download_button": True, + "use_edit_page_button": True, + "use_fullscreen_button": True, + "use_issues_button": True, + "use_source_button": True, + "use_repository_button": True, + "use_sidenotes": True, + "repository_url": "https://github.com/emerge-lab/nocturne", + "repository_branch": "main", + "launch_buttons": {"colab_url": "https://colab.research.google.com"}, + "home_page_in_toc": True, + "show_navbar_depth": 1, + "show_toc_level": 2, + "icon_links": [ + { + "name": "Nocturne GitHub", + "url": "https://github.com/emerge-lab/nocturne", + "icon": "fa-brands fa-github", + }, + ], +} +html_static_path = ["_static"] +html_logo = "_static/logo.png" +# html_favicon = "_static/logo-square.svg" +html_title = "Nocturne" +html_copy_source = True + +html_sidebars = {"**/**": ["sbt-sidebar-nav.html"]} + +# -- Options for MySt-NB output ------------------------------------------------- +# https://myst-nb.readthedocs.io/en/latest/index.html +source_suffix = { + ".rst": "restructuredtext", + ".ipynb": "myst-nb", + ".myst": "myst-nb", +} +myst_heading_anchors = 3 # auto-generate 3 levels of heading anchors +myst_enable_extensions = [ + "amsmath", + "colon_fence", + "deflist", + "dollarmath", + "html_image", +] +myst_url_schemes = ("http", "https", "mailto") +nb_execution_mode = "force" +nb_execution_allow_errors = False +nb_merge_streams = True + +nb_execution_excludepatterns = [ + # Slow notebook + # 'notebooks/Neural_Network_and_Data_Loading.*', +] + +# -- Options for autodoc ---------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration + +# Automatically extract typehints when specified and place them in +# descriptions of the relevant function/method. +autodoc_typehints = "description" + +# Don't show class signature with the class' name. +autodoc_class_signature = "separated" + +# -- Extension configuration ------------------------------------------------- + +# Tell sphinx-autodoc-typehints to generate stub parameter annotations including +# types, even if the parameters aren't explicitly documented. +always_document_param_types = True + + +# Tell sphinx autodoc how to render type aliases. +autodoc_type_aliases = { + "ArrayLike": "ArrayLike", + "DTypeLike": "DTypeLike", +} diff --git a/docs/source/contributing.md b/docs/source/contributing.md new file mode 100644 index 00000000..60539c0e --- /dev/null +++ b/docs/source/contributing.md @@ -0,0 +1,6 @@ +--- +title: Contributing +--- + +```{include} ../../CONTRIBUTING.md +``` diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 00000000..92a61873 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,35 @@ +--- +title: Nocturne +--- + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Quickstart +self +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Tutorial +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Reference +api_python +``` + +```{toctree} +:maxdepth: 1 +:hidden: +:caption: Developer +contributing +changelog +coc +``` + +```{include} ../../README.md +``` diff --git a/docs/source/references.bib b/docs/source/references.bib new file mode 100644 index 00000000..e69de29b diff --git a/pyproject.toml b/pyproject.toml index 72532877..7a84b39c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,9 @@ torch = "^2.0.1" gym = "^0.26.2" pybind11 = "^2.11.1" +[tool.poetry.group.dev] +optional = true + [tool.poetry.group.dev.dependencies] pre-commit = "^3.4.0" flake8 = "^6.1.0" @@ -53,6 +56,16 @@ isort = "^5.12.0" pylint = "^3.0.0" tomli = "^2.0.1" +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] +sphinx = "^5.3.0" +sphinx-book-theme = "^1.0.1" +myst-nb = "^0.17.2" +sphinxcontrib-bibtex = "^2.6.1" +sphinx-autodoc-typehints = "^1.23.0" + [tool.poetry.build] script = "build.py" generate-setup-file = true @@ -69,6 +82,7 @@ convention = "google" [tool.pylint] max-line-length = 120 +exclude = "^docs/*" [tool.isort] profile = "black" diff --git a/requirements.dev.txt b/requirements.dev.txt index 149e21d4..be8f9e62 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -21,7 +21,7 @@ pyflakes==3.1.0 ; python_version >= "3.10" and python_version < "3.13" pylint==3.0.0 ; python_version >= "3.10" and python_version < "3.13" pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "3.13" setuptools==68.2.2 ; python_version >= "3.10" and python_version < "3.13" -tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" +tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.13" tomlkit==0.12.1 ; python_version >= "3.10" and python_version < "3.13" typing-extensions==4.8.0 ; python_version >= "3.10" and python_version < "3.11" virtualenv==20.24.5 ; python_version >= "3.10" and python_version < "3.13" diff --git a/requirements.docs.txt b/requirements.docs.txt new file mode 100644 index 00000000..475180f2 --- /dev/null +++ b/requirements.docs.txt @@ -0,0 +1,87 @@ +accessible-pygments==0.0.4 ; python_version >= "3.10" and python_version < "3.13" +alabaster==0.7.13 ; python_version >= "3.10" and python_version < "3.13" +appnope==0.1.3 ; python_version >= "3.10" and python_version < "3.13" and (platform_system == "Darwin" or sys_platform == "darwin") +asttokens==2.4.0 ; python_version >= "3.10" and python_version < "3.13" +attrs==23.1.0 ; python_version >= "3.10" and python_version < "3.13" +babel==2.13.0 ; python_version >= "3.10" and python_version < "3.13" +backcall==0.2.0 ; python_version >= "3.10" and python_version < "3.13" +beautifulsoup4==4.12.2 ; python_version >= "3.10" and python_version < "3.13" +certifi==2023.7.22 ; python_version >= "3.10" and python_version < "3.13" +cffi==1.16.0 ; python_version >= "3.10" and python_version < "3.13" and implementation_name == "pypy" +charset-normalizer==3.3.0 ; python_version >= "3.10" and python_version < "3.13" +click==8.1.7 ; python_version >= "3.10" and python_version < "3.13" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.13" and (sys_platform == "win32" or platform_system == "Windows") +comm==0.1.4 ; python_version >= "3.10" and python_version < "3.13" +debugpy==1.8.0 ; python_version >= "3.10" and python_version < "3.13" +decorator==5.1.1 ; python_version >= "3.10" and python_version < "3.13" +docutils==0.17.1 ; python_version >= "3.10" and python_version < "3.13" +exceptiongroup==1.1.3 ; python_version >= "3.10" and python_version < "3.11" +executing==2.0.0 ; python_version >= "3.10" and python_version < "3.13" +fastjsonschema==2.18.1 ; python_version >= "3.10" and python_version < "3.13" +greenlet==3.0.0 ; python_version >= "3.10" and python_version < "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") +idna==3.4 ; python_version >= "3.10" and python_version < "3.13" +imagesize==1.4.1 ; python_version >= "3.10" and python_version < "3.13" +importlib-metadata==6.8.0 ; python_version >= "3.10" and python_version < "3.13" +ipykernel==6.25.2 ; python_version >= "3.10" and python_version < "3.13" +ipython==8.16.1 ; python_version >= "3.10" and python_version < "3.13" +jedi==0.19.1 ; python_version >= "3.10" and python_version < "3.13" +jinja2==3.1.2 ; python_version >= "3.10" and python_version < "3.13" +jsonschema-specifications==2023.7.1 ; python_version >= "3.10" and python_version < "3.13" +jsonschema==4.19.1 ; python_version >= "3.10" and python_version < "3.13" +jupyter-cache==0.6.1 ; python_version >= "3.10" and python_version < "3.13" +jupyter-client==8.3.1 ; python_version >= "3.10" and python_version < "3.13" +jupyter-core==5.3.2 ; python_version >= "3.10" and python_version < "3.13" +latexcodec==2.0.1 ; python_version >= "3.10" and python_version < "3.13" +markdown-it-py==2.2.0 ; python_version >= "3.10" and python_version < "3.13" +markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "3.13" +matplotlib-inline==0.1.6 ; python_version >= "3.10" and python_version < "3.13" +mdit-py-plugins==0.3.5 ; python_version >= "3.10" and python_version < "3.13" +mdurl==0.1.2 ; python_version >= "3.10" and python_version < "3.13" +myst-nb==0.17.2 ; python_version >= "3.10" and python_version < "3.13" +myst-parser==0.18.1 ; python_version >= "3.10" and python_version < "3.13" +nbclient==0.7.4 ; python_version >= "3.10" and python_version < "3.13" +nbformat==5.9.2 ; python_version >= "3.10" and python_version < "3.13" +nest-asyncio==1.5.8 ; python_version >= "3.10" and python_version < "3.13" +packaging==23.2 ; python_version >= "3.10" and python_version < "3.13" +parso==0.8.3 ; python_version >= "3.10" and python_version < "3.13" +pexpect==4.8.0 ; python_version >= "3.10" and python_version < "3.13" and sys_platform != "win32" +pickleshare==0.7.5 ; python_version >= "3.10" and python_version < "3.13" +platformdirs==3.11.0 ; python_version >= "3.10" and python_version < "3.13" +prompt-toolkit==3.0.39 ; python_version >= "3.10" and python_version < "3.13" +psutil==5.9.5 ; python_version >= "3.10" and python_version < "3.13" +ptyprocess==0.7.0 ; python_version >= "3.10" and python_version < "3.13" and sys_platform != "win32" +pure-eval==0.2.2 ; python_version >= "3.10" and python_version < "3.13" +pybtex-docutils==1.0.3 ; python_version >= "3.10" and python_version < "3.13" +pybtex==0.24.0 ; python_version >= "3.10" and python_version < "3.13" +pycparser==2.21 ; python_version >= "3.10" and python_version < "3.13" and implementation_name == "pypy" +pydata-sphinx-theme==0.14.1 ; python_version >= "3.10" and python_version < "3.13" +pygments==2.16.1 ; python_version >= "3.10" and python_version < "3.13" +python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "3.13" +pywin32==306 ; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_version >= "3.10" and python_version < "3.13" +pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "3.13" +pyzmq==25.1.1 ; python_version >= "3.10" and python_version < "3.13" +referencing==0.30.2 ; python_version >= "3.10" and python_version < "3.13" +requests==2.31.0 ; python_version >= "3.10" and python_version < "3.13" +rpds-py==0.10.3 ; python_version >= "3.10" and python_version < "3.13" +six==1.16.0 ; python_version >= "3.10" and python_version < "3.13" +snowballstemmer==2.2.0 ; python_version >= "3.10" and python_version < "3.13" +soupsieve==2.5 ; python_version >= "3.10" and python_version < "3.13" +sphinx-autodoc-typehints==1.23.0 ; python_version >= "3.10" and python_version < "3.13" +sphinx-book-theme==1.0.1 ; python_version >= "3.10" and python_version < "3.13" +sphinx==5.3.0 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-applehelp==1.0.7 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-bibtex==2.6.1 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-devhelp==1.0.5 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-htmlhelp==2.0.4 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-qthelp==1.0.6 ; python_version >= "3.10" and python_version < "3.13" +sphinxcontrib-serializinghtml==1.1.9 ; python_version >= "3.10" and python_version < "3.13" +sqlalchemy==2.0.21 ; python_version >= "3.10" and python_version < "3.13" +stack-data==0.6.3 ; python_version >= "3.10" and python_version < "3.13" +tabulate==0.9.0 ; python_version >= "3.10" and python_version < "3.13" +tornado==6.3.3 ; python_version >= "3.10" and python_version < "3.13" +traitlets==5.11.1 ; python_version >= "3.10" and python_version < "3.13" +typing-extensions==4.8.0 ; python_version >= "3.10" and python_version < "3.13" +urllib3==2.0.6 ; python_version >= "3.10" and python_version < "3.13" +wcwidth==0.2.8 ; python_version >= "3.10" and python_version < "3.13" +zipp==3.17.0 ; python_version >= "3.10" and python_version < "3.13" diff --git a/requirements.txt b/requirements.txt index 4bbef33e..414311fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cloudpickle==2.2.1 ; python_version >= "3.10" and python_version < "3.13" filelock==3.12.4 ; python_version >= "3.10" and python_version < "3.13" -gym==0.26.2 ; python_version >= "3.10" and python_version < "3.13" gym-notices==0.0.8 ; python_version >= "3.10" and python_version < "3.13" +gym==0.26.2 ; python_version >= "3.10" and python_version < "3.13" jinja2==3.1.2 ; python_version >= "3.10" and python_version < "3.13" markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "3.13" mpmath==1.3.0 ; python_version >= "3.10" and python_version < "3.13" From 2ffec40f05eb789385ba47f7460ff7a65443ca3c Mon Sep 17 00:00:00 2001 From: Daphne Cornelisse Date: Thu, 5 Oct 2023 02:25:13 +0200 Subject: [PATCH 2/2] Feature: Added initial examples to docs --- .pre-commit-config.yaml | 3 +- docs/source/01_data_structure.ipynb | 4230 ++++++++++++++++++++++++ docs/source/02_nocturne_concepts.ipynb | 786 +++++ docs/source/03_basic_rl_usage.ipynb | 216 ++ docs/source/04_ppo_with_sb3.ipynb | 164 + docs/source/index.md | 4 + 6 files changed, 5402 insertions(+), 1 deletion(-) create mode 100644 docs/source/01_data_structure.ipynb create mode 100644 docs/source/02_nocturne_concepts.ipynb create mode 100644 docs/source/03_basic_rl_usage.ipynb create mode 100644 docs/source/04_ppo_with_sb3.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3aa5e3e..018753c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,10 +56,11 @@ repos: - id: nbqa-pyupgrade args: [--py310-plus] - id: nbqa-black + args: [--line-length=120] - id: nbqa-isort args: [--profile=black] - id: nbqa-flake8 - args: [--extend-ignore=E203] + args: [--max-line-length=120, --extend-ignore=E203] - repo: local hooks: - id: pylint diff --git a/docs/source/01_data_structure.ipynb b/docs/source/01_data_structure.ipynb new file mode 100644 index 00000000..62a7ccca --- /dev/null +++ b/docs/source/01_data_structure.ipynb @@ -0,0 +1,4230 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Data format of a traffic scene\n", + "\n", + "This notebook dives into the data format used to create simulations in Nocturne.\n", + "\n", + "_Last update: 10/2023_" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import os\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "\n", + "os.chdir(\"..\")\n", + "\n", + "cmap = [\"r\", \"g\", \"b\", \"y\", \"c\"]\n", + "%config InlineBackend.figure_format = 'svg'\n", + "sns.set(\"notebook\", font_scale=1.1, rc={\"figure.figsize\": (8, 3)})\n", + "sns.set_style(\"ticks\", rc={\"figure.facecolor\": \"none\", \"axes.facecolor\": \"none\"})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Traffic scenes are constructed by utilizing the [Waymo Open Motion dataset](https://waymo.com/open/). Though every scene is unique, they all have the same basic data structure. \n", + "\n", + "To load a traffic scene:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['name', 'objects', 'roads', 'tl_states'])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Take an example scene\n", + "data_path = \"./data/example_scenario.json\"\n", + "\n", + "with open(data_path) as file:\n", + " traffic_scene = json.load(file)\n", + "\n", + "traffic_scene.keys()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Global Overview \n", + "A traffic scene consists of:\n", + "- `name`: the name of the traffic scenario.\n", + "- `objects`: the road objects or moving vehicles in the scene.\n", + "- `roads`: the road points in the scene, these are all the stationary objects.\n", + "- `tl_states`: the states of the traffic lights, which are filtered out for now. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "traffic_scene[\"tl_states\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'tfrecord-00358-of-01000_65.json'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "traffic_scene[\"name\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-10-03T10:23:25.972593\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.8.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "

" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "pd.Series([traffic_scene[\"objects\"][idx][\"type\"] for idx in range(len(traffic_scene[\"objects\"]))]).value_counts().plot(\n", + " kind=\"bar\", rot=45, color=cmap\n", + ")\n", + "plt.title(f'Distribution of road objects in traffic scene. Total # objects: {len(traffic_scene[\"objects\"])}')\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This traffic scenario only contains vehicles and pedestrians, some scenes have cyclists as well." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-10-03T10:23:26.839616\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.8.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "pd.Series([traffic_scene[\"roads\"][idx][\"type\"] for idx in range(len(traffic_scene[\"roads\"]))]).value_counts().plot(\n", + " kind=\"bar\", rot=45, color=cmap\n", + ")\n", + "plt.title(f'Distribution of road points in traffic scene. Total # points: {len(traffic_scene[\"roads\"])}')\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### In-Depth: Road Objects\n", + "\n", + "This is a list of different road objects in the traffic scene. For each road object, we have information about its position, velocity, size, in which direction it's heading, whether it's a valid object, the type, and the final position of the vehicle." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['position', 'width', 'length', 'heading', 'velocity', 'valid', 'goalPosition', 'type'])" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Take the first object\n", + "idx = 0\n", + "\n", + "# For each object, we have this information:\n", + "traffic_scene[\"objects\"][idx].keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " {\n", + " \"x\": 9037.7138671875,\n", + " \"y\": -2720.373779296875\n", + " },\n", + " {\n", + " \"x\": 9037.7607421875,\n", + " \"y\": -2720.306640625\n", + " },\n", + " {\n", + " \"x\": 9037.822265625,\n", + " \"y\": -2720.217529296875\n", + " },\n", + " {\n", + " \"x\": 9037.8916015625,\n", + " \"y\": -2720.146240234375\n", + " },\n", + " {\n", + " \"x\": 9037.9482421875,\n", + " \"y\": -2720.070068359375\n", + " },\n", + " {\n", + " \"x\": 9038.01953125,\n", + " \"y\": -2719.994384765625\n", + " },\n", + " {\n", + " \"x\": 9038.1005859375,\n", + " \"y\": -2719.903076171875\n", + " },\n", + " {\n", + " \"x\": 9038.1953125,\n", + " \"y\": -2719.830810546875\n", + " },\n", + " {\n", + " \"x\": 9038.279296875,\n", + " \"y\": -2719.74462890625\n", + " },\n", + " {\n", + " \"x\": 9038.3564453125,\n", + " \"y\": -2719.674560546875\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "# Position contains the (x, y) coordinates for the vehicle at every time step\n", + "print(json.dumps(traffic_scene[\"objects\"][idx][\"position\"][:10], indent=4))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.6877052187919617, 0.6777269244194031)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Width and length together make the size of the object, and is used to see if there is a collision\n", + "traffic_scene[\"objects\"][idx][\"width\"], traffic_scene[\"objects\"][idx][\"length\"]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An object's heading refers to the direction it is pointing or moving in. The default coordinate system in Nocturne is right-handed, where the positive x and y axes point to the right and downwards, respectively. In a right-handed coordinate system, 0 degrees is located on the x-axis and the angle increases counter-clockwise.\n", + "\n", + "Because the scene is created from the viewpoint of an ego driver, there may be instances where the heading of certain vehicles is not available. These cases are represented by the value `-10_000`, to indicate that these steps should be filtered out or are invalid." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-10-03T10:23:28.800884\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.8.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Heading is the direction in which the vehicle is pointing\n", + "plt.plot(traffic_scene[\"objects\"][idx][\"heading\"])\n", + "plt.xlabel(\"Time step\")\n", + "plt.ylabel(\"Heading\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " {\n", + " \"x\": 0.634765625,\n", + " \"y\": 0.72265625\n", + " },\n", + " {\n", + " \"x\": 0.46875,\n", + " \"y\": 0.67138671875\n", + " },\n", + " {\n", + " \"x\": 0.615234375,\n", + " \"y\": 0.89111328125\n", + " },\n", + " {\n", + " \"x\": 0.693359375,\n", + " \"y\": 0.712890625\n", + " },\n", + " {\n", + " \"x\": 0.56640625,\n", + " \"y\": 0.76171875\n", + " },\n", + " {\n", + " \"x\": 0.712890625,\n", + " \"y\": 0.7568359375\n", + " },\n", + " {\n", + " \"x\": 0.810546875,\n", + " \"y\": 0.9130859375\n", + " },\n", + " {\n", + " \"x\": 0.947265625,\n", + " \"y\": 0.72265625\n", + " },\n", + " {\n", + " \"x\": 0.83984375,\n", + " \"y\": 0.86181640625\n", + " },\n", + " {\n", + " \"x\": 0.771484375,\n", + " \"y\": 0.70068359375\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "# Velocity shows the velocity in the x- and y- directions\n", + "print(json.dumps(traffic_scene[\"objects\"][idx][\"velocity\"][:10], indent=4))" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2023-10-03T10:23:29.389521\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.8.0, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Valid indicates if the state of the vehicle was observed for each timepoint\n", + "plt.xlabel(\"Time step\")\n", + "plt.ylabel(\"IS VALID\")\n", + "plt.plot(traffic_scene[\"objects\"][idx][\"valid\"], \"_\", lw=5)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'x': 9041.1259765625, 'y': -2716.647216796875}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Each object has a goalPosition, an (x, y) position within the scene\n", + "traffic_scene[\"objects\"][idx][\"goalPosition\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'pedestrian'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Finally, we have the type of the vehicle\n", + "traffic_scene[\"objects\"][idx][\"type\"]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### In-Depth: Road Points\n", + "\n", + "Road points are static objects in the scene." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['geometry', 'type'])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "traffic_scene[\"roads\"][idx].keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'road_edge'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This point represents the edge of a road\n", + "traffic_scene[\"roads\"][idx][\"type\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " {\n", + " \"x\": 8922.911733810946,\n", + " \"y\": -2849.426741530589\n", + " },\n", + " {\n", + " \"x\": 8923.216436260553,\n", + " \"y\": -2849.038518766975\n", + " },\n", + " {\n", + " \"x\": 8923.50673911804,\n", + " \"y\": -2848.63941352788\n", + " },\n", + " {\n", + " \"x\": 8923.782254084921,\n", + " \"y\": -2848.2299596442986\n", + " },\n", + " {\n", + " \"x\": 8924.042612639492,\n", + " \"y\": -2847.8107047886665\n", + " },\n", + " {\n", + " \"x\": 8924.287466537296,\n", + " \"y\": -2847.382209743547\n", + " },\n", + " {\n", + " \"x\": 8924.516488266596,\n", + " \"y\": -2846.945047650609\n", + " },\n", + " {\n", + " \"x\": 8924.729371495881,\n", + " \"y\": -2846.49980324385\n", + " },\n", + " {\n", + " \"x\": 8924.91688626026,\n", + " \"y\": -2846.067714357487\n", + " },\n", + " {\n", + " \"x\": 8925.087545312272,\n", + " \"y\": -2845.6286986979553\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "# Geometry contains the (x, y) position(s) for a road point\n", + "# Note that this will be a list for road lanes and edges but a single (x, y) tuple for stop signs and alike\n", + "print(json.dumps(traffic_scene[\"roads\"][idx][\"geometry\"][:10], indent=4));" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nocturne-research", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/02_nocturne_concepts.ipynb b/docs/source/02_nocturne_concepts.ipynb new file mode 100644 index 00000000..a31d262c --- /dev/null +++ b/docs/source/02_nocturne_concepts.ipynb @@ -0,0 +1,786 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Nocturne concepts\n", + "\n", + "This page introduces the most basic elements of nocturne. You can find further information about these [in Section 3 of the Nocturne paper](https://arxiv.org/abs/2206.09889).\n", + "\n", + "_Last update: 10/2023_" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import numpy as np\n", + "\n", + "os.chdir(\"..\")\n", + "\n", + "data_path = \"./data/example_scenario.json\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Summary\n", + "\n", + "- Nocturne simulations are **discretized traffic scenarios**. A scenario is a constructed snapshot of traffic situation at a particular timepoint.\n", + "- The state of the vehicle of focus is referred to as the **ego state**. Each vehicle has their **own partial view of the traffic scene**; and a visible state is constructed by parameterizing the view distance, head angle and cone radius of the driver. The action for each vehicle is a `(1, 3)` tuple with the acceleration, steering and head angle of the vehicle. \n", + "- The **step method advances the simulation** with a desired step size. By default, the dynamics of vehicles are driven by a kinematic bicycle model. If a vehicle is set to expert-controlled mode, its position, heading, and speed will be updated according to a trajectory recorded from a human driver." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Simulation\n", + "\n", + "In Nocturne, a simulation discretizes an existing traffic scenario. At the moment, Nocturne supports traffic scenarios from the Waymo Open Dataset, but can be further extended to work with other driving datasets. \n", + "\n", + "
\n", + "
\n", + "\n", + "
An example of a set of traffic scenario's in Nocturne. Upon initialization, a start time is chosen. After each iteration we take a step in the simulation, which gets us to the next scenario. This is done until we reach the end of the simulation.
\n", + "
\n", + "\n", + "We show an example of this using `example_scenario.json`, where our traffic data is extracted from the Waymo open motion dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from nocturne import Simulation\n", + "\n", + "scenario_config = {\n", + " \"start_time\": 0, # When to start the simulation\n", + " \"allow_non_vehicles\": True, # Whether to include cyclists and pedestrians\n", + " \"max_visible_road_points\": 10, # Maximum number of road points for a vehicle\n", + " \"max_visible_objects\": 10, # Maximum number of road objects for a vehicle\n", + " \"max_visible_traffic_lights\": 10, # Maximum number of traffic lights in constructed view\n", + " \"max_visible_stop_signs\": 10, # Maximum number of stop signs in constructed view\n", + "}\n", + "\n", + "# Create simulation\n", + "sim = Simulation(data_path, scenario_config)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scenario\n", + "\n", + "A simulation consists of a set of scenarios. A scenario is a snapshot of the traffic scene at a particular timepoint. \n", + "\n", + "Here is how to create a scenario object:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# Get traffic scenario at timepoint\n", + "scenario = sim.getScenario()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `scenario` objects holds information we are interested in. Here are a couple of examples:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "33" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The number of road objects in the scene\n", + "len(scenario.getObjects())" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total # moving objects: 15\n", + "\n", + "Object IDs of moving vehicles: \n", + " [0, 1, 2, 3, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] \n" + ] + } + ], + "source": [ + "# The road objects that moved at a particular timepoint\n", + "objects_that_moved = scenario.getObjectsThatMoved()\n", + "\n", + "print(f\"Total # moving objects: {len(objects_that_moved)}\\n\")\n", + "print(f\"Object IDs of moving vehicles: \\n {[obj.getID() for obj in objects_that_moved]} \")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "128" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Number of road lines\n", + "len(scenario.road_lines())" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "scenario.getVehicles()[:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# No cyclists in this scene\n", + "scenario.getCyclists()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 2 moving vehicles in scene: [3, 32]\n" + ] + } + ], + "source": [ + "# Select all moving vehicles that move\n", + "moving_vehicles = [obj for obj in scenario.getVehicles() if obj in objects_that_moved]\n", + "\n", + "print(f\"Found {len(moving_vehicles)} moving vehicles in scene: {[vehicle.getID() for vehicle in moving_vehicles]}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Ego state\n", + "\n", + "The **ego state** is an array with features that describe the current vehicle. This array holds the following information: \n", + "- 0: length of ego vehicle\n", + "- 1: width of ego vehicle\n", + "- 2: speed of ego vehicle\n", + "- 3: distance to the goal position of ego vehicle\n", + "- 4: angle to the goal (target azimuth) \n", + "- 5: desired heading at goal position\n", + "- 6: desired speed at goal position\n", + "- 7: current acceleration\n", + "- 8: current steering position\n", + "- 9: current head angle" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Selected vehicle # 3\n" + ] + }, + { + "data": { + "text/plain": [ + "array([ 4.4936213 , 1.9770377 , 0.07662283, 4.24219 , -0.05617166,\n", + " -0.05909407, 1.6792779 , 0. , 0. , 0. ],\n", + " dtype=float32)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Select an arbitrary vehicle\n", + "ego_vehicle = moving_vehicles[0]\n", + "\n", + "print(f\"Selected vehicle # {ego_vehicle.getID()}\")\n", + "\n", + "# Get the state for ego vehicle\n", + "scenario.ego_state(ego_vehicle)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Visible state\n", + "\n", + "We use the ego vehicle state, together with a view distance (how far the vehicle can see) and a view angle to construct the **visible state**. The figure below shows this procedure for a simplified traffic scene. \n", + "\n", + "Calling `scenario.visible_state()` returns a dictionary with four matrices:\n", + "- `stop_signs`: The visible stop signs \n", + "- `traffic_lights`: The states for the traffic lights from the perspective of the ego driver(red, yellow, green).\n", + "- `road_points`: The observable road points (static elements in the scene).\n", + "- `objects`: The observable road objects (vehicles, pedestrians and cyclists).\n", + "\n", + "
\n", + "
\n", + "\n", + "
To investigate coordination under partial observability, agents in Nocturne can only see an obstructed view of their environment. In this simplified traffic scene, we construct the state for the red ego driver. Note that Nocturne assumes that stop signs can be viewed, even if they are behind another driver.
\n", + "
\n", + "\n", + "\\begin{align*}\n", + "\\end{align*}\n", + "\n", + "
\n", + "
\n", + "\n", + "
The same scene, this time showing the view of the yellow car.
\n", + "
" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The shape of the visible state is a function of the maximum number of visible objects defined at initialization (traffic lights, stop signs, road objects, and road points) and whether we add padding. If `padding = True`, an array is of size `(max visible objects, # features)` is always constructed, even if there are no visible objects. Otherwise, if `padding = False` new entries are only created when objects are visible. \n", + "\n", + "For example, say a vehicle does not observe any stop signs at a given timepoint. If we set `padding=False`, and run `visible_state['stop_signs']`, we'll get back an empty array with the shape `(0, 3)`, where 3 is the number of features per stop sign. However, if the vehicle observes two stop signs using the same setting, then `visible_state['stop_signs']` will return an array with the shape `(2, 3)`.\n", + "\n", + "On the other hand, if we set `padding=True`, the resulting array will always have a shape of `(max visible stop signs, 3)`, irrespective of how many stop signs the vehicle actually observes." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['stop_signs', 'traffic_lights', 'road_points', 'objects'])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define viewing distance, radius and head angle\n", + "view_distance = 80\n", + "view_angle = np.radians(120)\n", + "head_angle = 0\n", + "padding = True\n", + "\n", + "# Construct the visible state for ego vehicle\n", + "visible_state = scenario.visible_state(\n", + " ego_vehicle,\n", + " view_dist=view_distance,\n", + " view_angle=view_angle,\n", + " head_angle=head_angle,\n", + " padding=padding,\n", + ")\n", + "\n", + "visible_state.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# There are no visible stop signs at this point\n", + "visible_state[\"stop_signs\"].T" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],\n", + " [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Traffic light states are filtered out in this version of Nocturne\n", + "visible_state[\"traffic_lights\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(10, 13)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Max visible road points x 13 features\n", + "visible_state[\"road_points\"].shape" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(10, 13)" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Number of visible road objects x 13 features\n", + "visible_state[\"objects\"].shape" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dimension flattened visible state: 410\n" + ] + } + ], + "source": [ + "visible_state_dim = sum([val.flatten().shape[0] for key, val in visible_state.items()])\n", + "\n", + "print(f\"Dimension flattened visible state: {visible_state_dim}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(410,)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can also flatten the visible state\n", + "# flattened has padding: if we miss an object --> zeros\n", + "visible_state_flat = scenario.flattened_visible_state(\n", + " ego_vehicle,\n", + " view_dist=view_distance,\n", + " view_angle=view_angle,\n", + " head_angle=head_angle,\n", + ")\n", + "\n", + "visible_state_flat.shape" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that `.flattened_visible_state()` adds padding by default." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step \n", + "\n", + "`step(dt)` is a method call on an instance of the Simulation class, where `dt` is a scalar that represents the length of each simulation timestep in seconds. It advances the simulation by one timestep, which can result in changes to the state of the simulation (for example, new positions of objects, updated velocities, etc.) based on the physical laws and rules defined in the simulation.\n", + "\n", + "In the Waymo dataset, the length of the expert data is 9 seconds, a step size of 0.1 is used to discretize each traffic scene. The first second is used as a warm-start, leaving the remaining 8 seconds (80 steps) for the simulation (Details in Section 3.3)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "dt = 0.1\n", + "\n", + "# Step the simulation\n", + "sim.step(dt)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Vehicle control\n", + "\n", + "By default, vehicles in Nocturne are driven by a **kinematic bicycle model**. This means that calling the `step(dt)` method evolves the dynamics of a vehicle according to the following set of equations (Appendix D in the paper):\n", + "\n", + "\\begin{align*}\n", + " \\textbf{position: } x_{t+1} &= x_t + \\dot{x} \\, \\Delta t \\\\\n", + " y_{t+1} &= y_t + \\dot{y} \\, \\Delta t \\\\\n", + " \\textbf{heading: } \\theta_{t+1} &= \\theta_t + \\dot{\\theta} \\, \\Delta t \\\\ \n", + " \\textbf{speed: } v_{t+1} &= \\text{clip}(v_t + \\dot{v} \\, \\Delta t, -v_{\\text{max}}, v_{\\text{max}}) \\\\\n", + "\\end{align*}\n", + "\n", + "with\n", + "\n", + "\\begin{align*}\n", + " \\dot{v} &= a \\\\ \n", + " \\bar{v} &= \\text{clip}(v_t, + 0.5 \\, \\dot{v} \\, \\Delta \\, t ,\\, - v_{\\text{max}}, v_{\\text{max}}) \\\\\n", + " \\beta &= \\tan^{-1} \\left( \\frac{l_r \\tan (\\delta)}{L} \\right) \\\\\n", + " &= \\tan^{-1} (0.5 \\tan(\\delta)) \\\\\n", + " \\dot{x} &= \\bar{v} \\cos (\\theta + \\beta) \\\\\n", + " \\dot{y} &= \\bar{v} \\sin (\\theta + \\beta) \\\\\n", + " \\dot{\\theta} &= \\frac{\\bar{v} \\cos (\\beta)\\tan(\\delta)}{L}\n", + "\\end{align*}\n", + "\n", + "where $(x_t, y_t)$ is the position of a vehicle at time $t$, $\\theta_t$ is the vehicles heading angle, $a$ is the acceleration and $\\delta$ is the steering angle. Finally, $L$ is the length of the car and $l_r = 0.5L$ is the distance to the rear axle of the car.\n", + "\n", + "If we set a vehicle to be **expert-controlled** instead, it will follow the same path as the respective human driver. This means that when we call the `step(dt)` function, the vehicle's position, heading, and speed will be updated to match the next point in the recorded human trajectory." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# By default, all vehicles are not expert controlled\n", + "ego_vehicle.expert_control" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "# Set a vehicle to be expert controlled:\n", + "ego_vehicle.expert_control = True" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "> **Pseudocode**: How `step(dt)` advances the simulation for every vehicle. Full code is implemented in [scenario.cc](https://github.com/facebookresearch/nocturne/blob/ae0a4e361457caf6b7e397675cc86f46161405ed/nocturne/cpp/src/scenario.cc#L264)\n", + "\n", + "---\n", + "\n", + "```Python\n", + "for vehicle in vehicles:\n", + "\n", + " if object is not expert controlled:\n", + " step vehicle dynamics following the kinematic bicycle model\n", + " \n", + " if vehicle is expert controlled:\n", + " get current time & vehicle idx\n", + " vehicle position = expert trajectories[vehicle_idx, time]\n", + " vehicle heading = expert headings[vehicle_idx, time]\n", + " vehicle speed = expert speeds[vehicle_idx, time]\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Action space\n", + "\n", + "The action set for a vehicle consists of three components: acceleration, steering and the head angle. Actions are discretized based on a provided upper and lower bound.\n", + "\n", + "The experiments in the paper use:\n", + "- 6 discrete actions for **acceleration** uniformly split between $[-3, 2] \\, \\frac{m}{s^2}$\n", + "- 21 discrete actions for **steering** between $[-0.7, 0.7]$ radians \n", + "- 5 discrete actions for **head tilt** between $[-1.6, 1.6]$ radians\n", + "\n", + "This is how you can access an expert action for a vehicle in Nocturne:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{acceleration: -0.224648, steering: -0.360994, head_angle: 0.000000}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Choose an arbitrary timepoint\n", + "time = 5\n", + "\n", + "# Show expert action at timepoint\n", + "scenario.expert_action(ego_vehicle, time)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "expert_action = scenario.expert_action(ego_vehicle, time)\n", + "\n", + "expert_action = expert_action.numpy()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "acceleration = expert_action[0]\n", + "steering = expert_action[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "nocturne_cpp.Action" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(scenario.expert_action(ego_vehicle, time))" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.005859, 0.004639)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# How did the vehicle's position change after taking this action?\n", + "scenario.expert_pos_shift(ego_vehicle, time)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "-0.0007097125053405762" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# How did the head angle change?\n", + "scenario.expert_heading_shift(ego_vehicle, time)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nocturne-research", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/03_basic_rl_usage.ipynb b/docs/source/03_basic_rl_usage.ipynb new file mode 100644 index 00000000..76ed79b9 --- /dev/null +++ b/docs/source/03_basic_rl_usage.ipynb @@ -0,0 +1,216 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic RL usage" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initializing environments\n", + "\n", + "\n", + "#### **Environment settings**\n", + "\n", + "- Initializing an environment is done with the `BaseEnv` class. The `BaseEnv` class leverages the `nocturne` simulator to create a basic RL interface, based on the provided traffic scenario(s). \n", + "\n", + "---\n", + "> 📝 The `env_config.yaml` file defines our environment settings, such as the action space, observation space and traffic scenarios to use.\n", + "---\n", + "\n", + "Check out `configs/env_config` for all the details!" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "ename": "Exception", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/Users/dejongmathijs/Library/CloudStorage/OneDrive-TheBostonConsultingGroup,Inc/Documents/Personal/nocturne_lab/examples/03_basic_rl_usage.ipynb Cell 3\u001b[0m line \u001b[0;36m2\n\u001b[1;32m 1\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39myaml\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mnocturne\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39menvs\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mbase_env\u001b[39;00m \u001b[39mimport\u001b[39;00m BaseEnv\n\u001b[1;32m 4\u001b[0m \u001b[39m# Load environment settings\u001b[39;00m\n\u001b[1;32m 5\u001b[0m \u001b[39mwith\u001b[39;00m \u001b[39mopen\u001b[39m(\u001b[39mf\u001b[39m\u001b[39m\"\u001b[39m\u001b[39m../configs/env_config.yaml\u001b[39m\u001b[39m\"\u001b[39m, \u001b[39m\"\u001b[39m\u001b[39mr\u001b[39m\u001b[39m\"\u001b[39m) \u001b[39mas\u001b[39;00m stream:\n", + "File \u001b[0;32m~/.pyenv-i386/versions/nocturne_lab/lib/python3.10/site-packages/nocturne/envs/__init__.py:2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m\"\"\"Import file for tests.\"\"\"\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mnocturne\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39menvs\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mbase_env\u001b[39;00m \u001b[39mimport\u001b[39;00m BaseEnv\n\u001b[1;32m 4\u001b[0m __all__ \u001b[39m=\u001b[39m [\n\u001b[1;32m 5\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mBaseEnv\u001b[39m\u001b[39m\"\u001b[39m,\n\u001b[1;32m 6\u001b[0m ]\n", + "File \u001b[0;32m~/.pyenv-i386/versions/nocturne_lab/lib/python3.10/site-packages/nocturne/envs/base_env.py:24\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mgym\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mspaces\u001b[39;00m \u001b[39mimport\u001b[39;00m Box, Discrete\n\u001b[1;32m 22\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mnocturne\u001b[39;00m \u001b[39mimport\u001b[39;00m Action, Simulation, Vector2D, Vehicle\n\u001b[0;32m---> 24\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mException\u001b[39;00m()\n\u001b[1;32m 26\u001b[0m _MAX_NUM_TRIES_TO_FIND_VALID_VEHICLE \u001b[39m=\u001b[39m \u001b[39m1_000\u001b[39m\n\u001b[1;32m 28\u001b[0m logging\u001b[39m.\u001b[39mgetLogger(\u001b[39m__name__\u001b[39m)\n", + "\u001b[0;31mException\u001b[0m: " + ] + } + ], + "source": [ + "import os\n", + "\n", + "import yaml\n", + "\n", + "from nocturne.envs.base_env import BaseEnv\n", + "\n", + "os.chdir(\"..\")\n", + "\n", + "# Load environment settings\n", + "with open(\"./configs/env_config.yaml\") as stream:\n", + " env_config = yaml.safe_load(stream)\n", + "\n", + "# Initialize environment\n", + "env = BaseEnv(config=env_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"controlling agents # {[agent.id for agent in env.controlled_vehicles]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### **Data**\n", + "\n", + "- Within `env_config.yaml`, we specify the path to the folder containing the traffic scenarios to use as follows:\n", + "\n", + "```yaml\n", + "# Path to folder with traffic scene(s) from which to create an environment\n", + "data_path: ../data\n", + "```\n", + "\n", + "- [Here](https://github.com/facebookresearch/nocturne/tree/main#downloading-the-dataset) are the instructions to access the complete dataset of traffic scenes. \n", + "\n", + "- The data folder also has a file named `valid_files.json`. This file lists the names of all the valid traffic scenarios along with the ids of the vehicles that are not valid. These vehicles are excluded from our experiment.\n", + "\n", + "For simplicity, we currently added a single traffic scenario that includes two vehicles in our data folder. Both vehicles can be used, so our `valid_files.json` looks like this:\n", + "\n", + "```yaml\n", + "{\n", + " \"example_scenario.json\": []\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Interacting with the environment\n", + "\n", + "The classic agent-environment loop of reinforcement learning is implemented as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reset\n", + "obs_dict = env.reset()\n", + "\n", + "# Get info\n", + "agent_ids = [agent_id for agent_id in obs_dict.keys()]\n", + "dead_agent_ids = []\n", + "num_agents = len(agent_ids)\n", + "rewards = {agent_id: 0 for agent_id in agent_ids}\n", + "\n", + "for step in range(1000):\n", + " # Sample actions\n", + " action_dict = {agent_id: env.action_space.sample() for agent_id in agent_ids if agent_id not in dead_agent_ids}\n", + "\n", + " # Step in env\n", + " obs_dict, rew_dict, done_dict, info_dict = env.step(action_dict)\n", + "\n", + " for agent_id in action_dict.keys():\n", + " rewards[agent_id] += rew_dict[agent_id]\n", + "\n", + " # Update dead agents\n", + " for agent_id, is_done in done_dict.items():\n", + " if is_done and agent_id not in dead_agent_ids:\n", + " dead_agent_ids.append(agent_id)\n", + "\n", + " # Reset if all agents are done\n", + " if done_dict[\"__all__\"]:\n", + " print(f\"Done after {env.step_num} steps -- total return in episode: {rewards}\")\n", + " obs_dict = env.reset()\n", + " dead_agent_ids = []\n", + " rewards = {agent_id: 0 for agent_id in agent_ids}\n", + "\n", + "# Close environment\n", + "env.close()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Accessing information about the environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The observation space\n", + "env.observation_space" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The size of the joint action space\n", + "env.action_space" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Which agents are controlled?\n", + "env.controlled_vehicles" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### \n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/04_ppo_with_sb3.ipynb b/docs/source/04_ppo_with_sb3.ipynb new file mode 100644 index 00000000..3969a88e --- /dev/null +++ b/docs/source/04_ppo_with_sb3.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## PPO with single-agent control\n", + "\n", + "In this notebook, we show how to use Proximal Policy Optimization (PPO) with Nocturne and [Stable Baselines 3 (SB3)](https://stable-baselines3.readthedocs.io/en/master/index.html). SB3 is a library that has implementations of various well-known RL algorithms." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Wrappers\n", + "\n", + "The Nocturne `BaseEnv` returns output as dictionaries, but the SB3 `PPO` class expects numpy arrays. To make our environment compatible with SB3, we create a wrapper class. Wrappers modify an environment without altering code directly, which reduces boilerplate and increasing modularity." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import yaml\n", + "\n", + "# Import base environment and wrapper\n", + "from nocturne.envs.base_env import BaseEnv\n", + "from nocturne.wrappers.sb3_wrappers import NocturneToSB3\n", + "\n", + "os.chdir(\"..\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# Load environment settings\n", + "with open(\"./configs/env_config.yaml\") as stream:\n", + " env_config = yaml.safe_load(stream)\n", + "\n", + "# Make sure to only control a single agent at a time. This is achieved by setting max_num_vehicles = 1\n", + "env_config[\"max_num_vehicles\"] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize env and wrap it with SB3 wrapper\n", + "env = NocturneToSB3(BaseEnv(env_config))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PPO\n", + "\n", + "Now all we have to do is initialize the SB3 `PPO` class and we're ready to learn! We use Weights & Biases (`wandb`) to take care of the logging. If you prefer not to use `wandb`, set `LOGGING = False` and `verbose=1`. \n", + "\n", + "\n", + "---\n", + "\n", + "> 🔦 More info on PPO and settings can be found in the [SB3 docs](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html).\n", + "\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import wandb\n", + "from stable_baselines3 import PPO" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "LOGGING = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if LOGGING:\n", + " wandb.login()\n", + " run = wandb.init(\n", + " project=\"single_agent_control_sb3_ppo\",\n", + " sync_tensorboard=True,\n", + " )\n", + " run_id = run.id\n", + "else:\n", + " run_id = None\n", + "\n", + "# Init PPO algorithm\n", + "model = PPO(\n", + " policy=\"MlpPolicy\", # Policy type\n", + " n_steps=4096, # Number of steps per rollout\n", + " batch_size=128, # Minibatch size\n", + " env=env, # Our wrapped environment\n", + " seed=42, # Always seed for reproducibility\n", + " verbose=0,\n", + " tensorboard_log=f\"runs/{run_id}\" if run_id is not None else None, # Sync with wandb\n", + ")\n", + "\n", + "# Learn\n", + "model.learn(total_timesteps=200_000)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 🤔 How good is your policy?\n", + "\n", + "Hooray! You have just trained your first PPO agent in Nocturne! 🏁 \n", + "\n", + "Now take a look at information you've logged over training; did we learn? (if you want to compare, [this is how my run looks like](https://api.wandb.ai/links/daphnecor/iarufxw9))\n", + "\n", + "One important metric for assess the effectiveness of your policy is the average cumulative reward per episode. In our case, the **maximum** achievable return per episode is approximately between 9 and 10 (it varies per traffic scene and per agent). With the configurations above, your policy should approach this value in 150,000 steps. Here, steps (the `global_step`) represents the total number of **frames** our policy network has seen, you can think of it as the accumulated experience." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "nocturne_lab", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/source/index.md b/docs/source/index.md index 92a61873..8db35682 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -13,6 +13,10 @@ self :maxdepth: 1 :hidden: :caption: Tutorial +01_data_structure +02_nocturne_concepts +03_basic_rl_usage +04_ppo_with_sb3 ``` ```{toctree}