From c5d5caf9103fd3a5320a04cfb934534fa45701f3 Mon Sep 17 00:00:00 2001 From: justinfu Date: Fri, 20 Oct 2023 00:23:44 +0000 Subject: [PATCH] Update docs --- .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ .../notebooks/data_demo.ipynb.txt | 136 ++++ .../notebooks/datatypes_demo.ipynb.txt | 300 +++++++++ .../notebooks/multi_actors_demo.ipynb.txt | 272 ++++++++ .../wosac_submission_via_waymax.ipynb.txt | 348 +++++++++++ docs/_static/scripts/furo.js | 2 +- docs/_static/scripts/furo.js.map | 2 +- docs/_static/styles/furo.css | 2 +- docs/_static/styles/furo.css.map | 2 +- .../waymax/agents/actor_core/index.html | 24 +- .../waymax/agents/agent_builder/index.html | 24 +- .../waymax/agents/constant_speed/index.html | 24 +- docs/autoapi/waymax/agents/expert/index.html | 24 +- docs/autoapi/waymax/agents/index.html | 24 +- .../waymax/agents/sim_agent/index.html | 24 +- .../waypoint_following_agent/index.html | 24 +- docs/autoapi/waymax/config/index.html | 24 +- .../dataloader/dataloader_utils/index.html | 24 +- docs/autoapi/waymax/dataloader/index.html | 24 +- .../dataloader/womd_dataloader/index.html | 24 +- .../dataloader/womd_factories/index.html | 24 +- .../womd_factories_internal/index.html | 24 +- .../waymax/dataloader/womd_utils/index.html | 24 +- .../waymax/datatypes/action/index.html | 24 +- .../autoapi/waymax/datatypes/array/index.html | 24 +- .../waymax/datatypes/constant/index.html | 24 +- docs/autoapi/waymax/datatypes/index.html | 24 +- .../waymax/datatypes/object_state/index.html | 24 +- .../waymax/datatypes/observation/index.html | 24 +- .../waymax/datatypes/operations/index.html | 24 +- .../waymax/datatypes/roadgraph/index.html | 24 +- .../autoapi/waymax/datatypes/route/index.html | 24 +- .../datatypes/simulator_state/index.html | 24 +- .../datatypes/traffic_lights/index.html | 24 +- .../dynamics/abstract_dynamics/index.html | 24 +- .../waymax/dynamics/bicycle_model/index.html | 24 +- docs/autoapi/waymax/dynamics/delta/index.html | 24 +- .../waymax/dynamics/discretizer/index.html | 24 +- docs/autoapi/waymax/dynamics/index.html | 24 +- .../waymax/dynamics/state_dynamics/index.html | 24 +- .../env/abstract_environment/index.html | 24 +- .../waymax/env/base_environment/index.html | 24 +- docs/autoapi/waymax/env/errors/index.html | 24 +- docs/autoapi/waymax/env/index.html | 24 +- .../env/planning_agent_environment/index.html | 24 +- docs/autoapi/waymax/env/rollout/index.html | 24 +- docs/autoapi/waymax/env/typedefs/index.html | 24 +- .../env/wrappers/brax_wrapper/index.html | 24 +- .../env/wrappers/dm_env_wrapper/index.html | 24 +- docs/autoapi/waymax/env/wrappers/index.html | 24 +- docs/autoapi/waymax/index.html | 24 +- .../waymax/metrics/abstract_metric/index.html | 24 +- .../autoapi/waymax/metrics/comfort/index.html | 24 +- .../waymax/metrics/imitation/index.html | 24 +- docs/autoapi/waymax/metrics/index.html | 24 +- .../waymax/metrics/metric_factory/index.html | 24 +- .../autoapi/waymax/metrics/overlap/index.html | 24 +- .../waymax/metrics/roadgraph/index.html | 24 +- docs/autoapi/waymax/metrics/route/index.html | 24 +- .../abstract_reward_function/index.html | 24 +- docs/autoapi/waymax/rewards/index.html | 24 +- .../linear_combination_reward/index.html | 24 +- docs/autoapi/waymax/utils/geometry/index.html | 24 +- docs/autoapi/waymax/utils/index.html | 24 +- .../waymax/utils/test_utils/index.html | 24 +- .../waymax/visualization/color/index.html | 24 +- docs/autoapi/waymax/visualization/index.html | 24 +- .../waymax/visualization/utils/index.html | 24 +- .../waymax/visualization/viz/index.html | 24 +- .../jupyter_execute/notebooks/data_demo.html | 371 ----------- .../notebooks/datatypes_demo.html | 501 --------------- .../notebooks/multi_actors_demo.html | 493 --------------- .../wosac_submission_via_waymax.html | 586 ------------------ .../jupyter_execute/notebooks/data_demo.html | 371 ----------- .../notebooks/datatypes_demo.html | 501 --------------- .../notebooks/multi_actors_demo.html | 493 --------------- .../wosac_submission_via_waymax.html | 586 ------------------ docs/genindex.html | 24 +- docs/getting_started.html | 24 +- docs/index.html | 24 +- docs/notebooks/data_demo.html | 24 +- docs/notebooks/datatypes_demo.html | 24 +- docs/notebooks/multi_actors_demo.html | 24 +- .../wosac_submission_via_waymax.html | 24 +- docs/objects.inv | Bin 10270 -> 10995 bytes docs/py-modindex.html | 24 +- docs/search.html | 24 +- docs/searchindex.js | 2 +- 107 files changed, 7169 insertions(+), 4735 deletions(-) create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt create mode 100644 docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt delete mode 100644 docs/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.html delete mode 100644 docs/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.html delete mode 100644 docs/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.html delete mode 100644 docs/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.html delete mode 100644 docs/build/jupyter_execute/notebooks/data_demo.html delete mode 100644 docs/build/jupyter_execute/notebooks/datatypes_demo.html delete mode 100644 docs/build/jupyter_execute/notebooks/multi_actors_demo.html delete mode 100644 docs/build/jupyter_execute/notebooks/wosac_submission_via_waymax.html diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt new file mode 100644 index 0000000..3e80b3c --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/data_demo.ipynb.txt @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rNYRA6k8Qfyo" + }, + "source": [ + "# Scenario Data Loading\n", + "\n", + "This tutorial demonstrates how to load scenario data from the Waymo Open Motion Dataset (WOMD) using the Waymax dataloader." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0o2sAapxRMAT" + }, + "source": [ + "\n", + "We first create a dataset config, using the default configs provided in the `waymax.config` module. In particular, `config.WOD_1_1_0_TRAINING` is a pre-defined configuration that points to version 1.1.0 of the Waymo Open Dataset.\n", + "\n", + "The data config contains a number of options to configure how and where the dataset is loaded from. By default, the `WOD_1_1_0_TRAINING` loads up to 128 objects (e.g. vehicles, pedestrians) per scenario. Here, we can save memory and compute by loading only the first 32 objects stored in the scenario.\n", + "\n", + "We use the `dataloader.simulator_state_generator` function to create an iterator\n", + "through Open Motion Dataset scenarios. Calling next on the iterator will retrieve the first scenario in the dataset.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "config = dataclasses.replace(_config.WOD_1_1_0_TRAINING, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "q1xyeYpLR8J6" + }, + "source": [ + "Next, we can plot the initial state of this scenario. We use a matplotlib-based visualization available in the `waymax.visualization` package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OY3-OOArsFcU" + }, + "outputs": [], + "source": [ + "# Using logged trajectory\n", + "img = visualization.plot_simulator_state(scenario, use_log_traj=True)\n", + "mediapy.show_image(img)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H0Z15epRSC23" + }, + "source": [ + "The Waymo Open Motion Dataset consists of 9-second trajectory snippets. We can visualize the entire logged trajectory as a video as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "\n", + "state = scenario\n", + "for _ in range(scenario.remaining_timesteps):\n", + " state = datatypes.update_state_by_log(state, num_steps=1)\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=True))\n", + "\n", + "mediapy.show_video(imgs, fps=10)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt new file mode 100644 index 0000000..d18cf0a --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/datatypes_demo.ipynb.txt @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "0IwlAjlibuUE" + }, + "source": [ + "# Understanding and Manipulating Data in Waymax\n", + "\n", + "This tutorial covers data structures in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1NTUsEy_btuU" + }, + "outputs": [], + "source": [ + "%%capture\n", + "import dataclasses\n", + "import jax\n", + "from jax import numpy as jnp\n", + "from matplotlib import pyplot as plt\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "2pcSuBb9cFZN" + }, + "outputs": [], + "source": [ + "# Load example data.\n", + "config = dataclasses.replace(_config.WOD_1_1_0_VALIDATION, max_num_objects=32)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GZAFy24xcLQc" + }, + "source": [ + "## JAX-based data structures\n", + "\n", + "The key property with all data structures in Waymax is that all data is *immutable*. This is a design decision that is inherited from JAX and enables code written using Waymax to be compatible with the powerful functional transforms in JAX, such as `jit`, `vmap`, etc. While efficiency may be a concern with immutable data structures, wrapping your function in `jax.jit` will allow JAX to optimize and replace your operations with in-place operations wherever possible, avoiding the need for excessive data copying.\n", + "\n", + "Additionally, all datastructures in Waymax are implemented as dataclasses. This allows convenient named access to fields, and allows simple nesting of data structures that is easy to manipulate with tree-based operations (such as those in `jax.tree_util`).\n", + "\n", + "The first example we will cover is the `datatypes.Trajectory` data structure, which holds the pose information for all objects. The scenario that we loaded contains a trajectory containing the logged behavior for all agents under the `scenario.log_trajectory` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "sPMaI3ShdPwB" + }, + "outputs": [], + "source": [ + "log_trajectory = scenario.log_trajectory\n", + "\n", + "# Number of objects stored in this trajectory.\n", + "print('Number of objects:', log_trajectory.num_objects)\n", + "print('Number of timesteps:', log_trajectory.num_timesteps)\n", + "print('Trajectory shape (num_objects, num_timesteps):', log_trajectory.shape)\n", + "print('XYZ positions (num_objects, num_timesteps, 3):', log_trajectory.xyz.shape)\n", + "print('XY velocities (num_objects, num_timesteps, 2):', log_trajectory.vel_xy.shape)\n", + "print('Yaw (num_objects, num_timesteps):', log_trajectory.yaw.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "nsKEO7GCelBK" + }, + "source": [ + "The `datatypes` module contains some helper methods that automatically map over datastructures. We can use `datatypes.dynamic_slice` to select out the trajectory belonging to a particular object or at a particular timestep. These operations, as with all JAX operations, will return new copies of the object they are modifying. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k6cWGuVRdlQk" + }, + "outputs": [], + "source": [ + "# Slice by time. Select the trajectory at timestep 23.\n", + "traj_t23 = datatypes.dynamic_slice(log_trajectory, start_index=23, slice_size=1, axis=-1)\n", + "print('XYZ positions (num_objects, 1, 3):', traj_t23.xyz.shape)\n", + "\n", + "# Slice by object. Select the trajectory for object 15.\n", + "traj_obj15 = datatypes.dynamic_slice(log_trajectory, start_index=15, slice_size=1, axis=-2)\n", + "print('XYZ positions (1, num_timesteps, 3):', traj_obj15.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LXrPjhzPf7EJ" + }, + "source": [ + "Of course, JAX functions from the core library also work on Waymax data structures. The `tree_map` function is particularly useful for working with dataclasses, and will apply a single function to all fields in the data structure (recursively if there are nested data structures)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "shVRPmToe9oY" + }, + "outputs": [], + "source": [ + "def max_along_time(x: jax.Array) -> jax.Array:\n", + " return jnp.max(x, axis=-1, keepdims=True)\n", + "\n", + "max_trajectory = jax.tree_util.tree_map(max_along_time, log_trajectory)\n", + "print('XYZ positions (num_objects, 1, 3):', max_trajectory.xyz.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mlPkVLA7Mcx-" + }, + "source": [ + "To modify the values of the data structure, we can use `dataclasses.replace` to replace entire fields, and `Array.at[idx].set(value)` to selectively modify individual values. For example, to set the all yaws for object 1 to zero, we can use the following code snippet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Z5napP9gMutj" + }, + "outputs": [], + "source": [ + "zeroed_traj = dataclasses.replace(\n", + " log_trajectory, \n", + " yaw=log_trajectory.yaw.at[1].set(0.0)\n", + ")\n", + "\n", + "# Should be the original values.\n", + "print('Yaws for object 0, timesteps 0 to 5:', zeroed_traj.yaw[0, 0:5])\n", + "\n", + "# Should be now set to 0.\n", + "print('Yaws for object 1, timesteps 0 to 5:', zeroed_traj.yaw[1, 0:5])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "E7DZS_BKhZLj" + }, + "source": [ + "# Other important data structures\n", + "\n", + "We will now cover the remaining important data structures that are stored in a scenario.\n", + "\n", + "The `datatypes.RoadgraphPoints` data structure holds all static information regarding the road and environment. This includes all lanes markers, road edges, stop signs, speed bumps, and crosswalks. \n", + "- The `x`, `y`, and `z` attributes define the spatial coordinates of the points.\n", + "- The `type` attribute is an integer that defines what type of point (lane, edge, stop sign, etc.) the point is. See `roadgraph_samples/type` of the [Waymo Open Motion Dataset](https://waymo.com/open/data/motion/tfexample) for definitions of which value corresponds to what type of point.\n", + "- The `dir_x` and `dir_y` attributes define the orientation of the points. Lane points will orient in the forward direction of the lane. Edge points are oriented such that the inside of the road is always on the port side (left if facing forward) of the point.\n", + "- The `id` field is a unique identifier for each contiguous lane. Lanes end if there is an intersection or reach the edge of the map." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "k9XQmInAh4vo" + }, + "outputs": [], + "source": [ + "# Plot the roadgraph, with colors corresponding to the road type.\n", + "rg_points = scenario.roadgraph_points\n", + "\n", + "where_valid = rg_points.valid\n", + "plt.scatter(\n", + " x = rg_points.x[where_valid],\n", + " y = rg_points.y[where_valid],\n", + " s=0.1,\n", + " c = rg_points.types[where_valid]\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2MdtujKbjahp" + }, + "source": [ + "The `datatypes.TrafficLights` structure holds time-varying information regarding the color and position of the traffic lights.\n", + "- The `x`, `y`, and `z` attributes define the spatial location of the light.\n", + "- The `state` attribute defines what color the light is at a particular instance in time.\n", + "- The `lane_ids` attribute tells what lanes the traffic light is controlling. These can be cross-referenced with the `RoadgraphPoints.ids` field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zse6OxgaiEbH" + }, + "outputs": [], + "source": [ + "traffic_lights = scenario.log_traffic_light\n", + "\n", + "print('Traffic Light States (num_lights, num_timesteps):', traffic_lights.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QY-rhD1Qkr-C" + }, + "source": [ + "Finally, `datatypes.ObjectMetadata` holds \n", + "- The `object_types` attribute defines whether the object is a vehicle, pedestrian, or cyclist.\n", + "- The `ids` attribute assigns a unique ID to each object.\n", + "- The `is_sdc` attribute defines whether the object is the ego-vehicle (or self-driving car).\n", + "- The `is_modeled` attribute marks whether the object's behavior is meant to be predicted as part of the Waymo Open Motion dataset.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Dohh8d9Rkh70" + }, + "outputs": [], + "source": [ + "metadata = scenario.object_metadata\n", + "print('All object IDS:', metadata.ids)\n", + "\n", + "# Color-code object trajectory by whether it is the SDC or not.\n", + "# The SDC trajectory in the center is shown in blue, and all other trajectories\n", + "# are shown in red.\n", + "flat_trajectory = jax.tree_util.tree_map(lambda x: jnp.reshape(x, [-1]), log_trajectory)\n", + "colors = jnp.zeros(log_trajectory.shape, dtype=jnp.int32).at[metadata.is_sdc].set(1)\n", + "colors = jnp.reshape(colors, [-1])\n", + "\n", + "where_valid = flat_trajectory.valid\n", + "plt.scatter(\n", + " x=flat_trajectory.x[where_valid],\n", + " y=flat_trajectory.y[where_valid],\n", + " s=0.5,\n", + " c=colors[where_valid],\n", + " cmap='RdYlBu'\n", + ")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hmNQVj-clfIr" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "1Ee2coesuhg82e7E9HmZxafZ7raD_ug5q", + "timestamp": 1683520258687 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt new file mode 100644 index 0000000..7ec65e6 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/multi_actors_demo.ipynb.txt @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "qg5s5R7AT8Fj" + }, + "source": [ + "# Multi-agent Simulation\n", + "\n", + "This tutorial demonstrates how to run a simple closed-loop simulation with multiple pre-defined sim agents." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "MtgRcYqmtMwD" + }, + "outputs": [], + "source": [ + "import jax\n", + "from jax import numpy as jnp\n", + "import numpy as np\n", + "import mediapy\n", + "from tqdm import tqdm\n", + "import dataclasses\n", + "\n", + "from waymax import config as _config\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import dynamics\n", + "from waymax import env as _env\n", + "from waymax import agents\n", + "from waymax import visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "dkJwTuSLr0gh" + }, + "outputs": [], + "source": [ + "# Config dataset:\n", + "max_num_objects = 32\n", + "\n", + "config = dataclasses.replace(_config.WOD_1_0_0_VALIDATION, max_num_objects=max_num_objects)\n", + "data_iter = dataloader.simulator_state_generator(config=config)\n", + "scenario = next(data_iter)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DINmUYg7y-jI" + }, + "source": [ + "## Initializing and Running the Simulator\n", + "\n", + "Waymax uses a Gym-like interface for running closed-loop simulation. \n", + "\n", + "The `env.MultiAgentEnvironment` class defines a stateless simulation interface with the two key methods:\n", + "- The `reset` method initializes and returns the first simulation state.\n", + "- The `step` method transitions the simulation and takes as arguments a state and an action and outputs the next state.\n", + "\n", + "Crucially, the `MultiAgentEnvironment` does not hold any simulation state itself, and the `reset` and `step` functions have no side effects. This allows us to use functional transforms from JAX, such as using jit compilation to optimize the compuation. It also allows the user to arbitrarily branch and restart simulation from any state, or save the simulation by simply serializing and saving the state object.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "x7u2dqPCtdeq" + }, + "outputs": [], + "source": [ + "# Config the multi-agent environment:\n", + "init_steps = 11\n", + "\n", + "# Set the dynamics model the environment is using.\n", + "# Note each actor interacting with the environment needs to provide action\n", + "# compatible with this dynamics model.\n", + "dynamics_model = dynamics.StateDynamics()\n", + "\n", + "# Expect users to control all valid object in the scene.\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=dataclasses.replace(\n", + " _config.EnvironmentConfig(),\n", + " max_num_objects=max_num_objects,\n", + " controlled_object=_config.ObjectType.VALID,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "876iboHbYx2H" + }, + "source": [ + "We now create a set of sim agents to run in simulation. By default, the behavior of an object that is not controlled is to replay the behavior stored in the dataset (log playback).\n", + "\n", + "For each sim agent, we define the algorithm (such as IDM), and specify which objects the agent controls via the `is_controlled_func`, which is required to return a boolean mask marking which objects are being controlled.\n", + "\n", + "The IDM agent we use in this example is the `IDMRoutePolicy`, which follows the spatial trajectory stored in the logs, but adjusts the speed profile based on the IDM rule, which will stop or speed up according to the distance between the vehicle and any objects in front of the vehicle. For the remaining agents, we set them to use a constant speed policy which will follow the logged route with a fixed, constant speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IfCHlgJzghUS" + }, + "outputs": [], + "source": [ + "# Setup a few actors, see visualization below for how each actor behaves.\n", + "\n", + "# An actor that doesn't move, controlling all objects with index > 4\n", + "obj_idx = jnp.arange(max_num_objects)\n", + "static_actor = agents.create_constant_speed_actor(\n", + " speed=0.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx > 4,\n", + ")\n", + "\n", + "# IDM actor/policy controlling both object 0 and 1.\n", + "# Note IDM policy is an actor hard-coded to use dynamics.StateDynamics().\n", + "actor_0 = agents.IDMRoutePolicy(\n", + " is_controlled_func=lambda state: (obj_idx == 0) | (obj_idx == 1)\n", + ")\n", + "\n", + "# Constant speed actor with predefined fixed speed controlling object 2.\n", + "actor_1 = agents.create_constant_speed_actor(\n", + " speed=5.0,\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: obj_idx == 2,\n", + ")\n", + "\n", + "# Exper/log actor controlling objects 3 and 4.\n", + "actor_2 = agents.create_expert_actor(\n", + " dynamics_model=dynamics_model,\n", + " is_controlled_func=lambda state: (obj_idx == 3) | (obj_idx == 4),\n", + ")\n", + "\n", + "actors = [static_actor, actor_0, actor_1, actor_2]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JvV82vOXWbI0" + }, + "source": [ + "We can (optionally) jit the step and select action functions to speed up computation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YmzEJy2LUwe5" + }, + "outputs": [], + "source": [ + "jit_step = jax.jit(env.step)\n", + "jit_select_action_list = [jax.jit(actor.select_action) for actor in actors]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iBeI2mdIUdTw" + }, + "source": [ + "We can now write a for loop to all of these agents in simulation together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "06SjvXdRrV3N" + }, + "outputs": [], + "source": [ + "states = [env.reset(scenario)]\n", + "for _ in range(states[0].remaining_timesteps):\n", + " current_state = states[-1]\n", + "\n", + " outputs = [\n", + " jit_select_action({}, current_state, None, None)\n", + " for jit_select_action in jit_select_action_list\n", + " ]\n", + " action = agents.merge_actions(outputs)\n", + " next_state = jit_step(current_state, action)\n", + "\n", + " states.append(next_state)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "s6TmqhLRGc_7" + }, + "source": [ + "## Visualization of simulation.\n", + "\n", + "We can now visualize the result of the simulation loop.\n", + "\n", + "On the left side:\n", + "- Objects 5, 6, and 7 (controlled by static_actor) remain static.\n", + "- Objects 3 and 4 controlled by log playback, and collide with objects 5 and 6.\n", + "\n", + "On the right side:\n", + "- Object 2 controlled by actor_1 is moving at constant speed 5m/s (i.e. slower than log in this case).\n", + "- Object 0 and 1, controlled by the IDM agent, follow the log in the beginning, but object 1 slows down when approaching object 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cYWjd4dE1bB2" + }, + "outputs": [], + "source": [ + "imgs = []\n", + "for state in states:\n", + " imgs.append(visualization.plot_simulator_state(state, use_log_traj=False))\n", + "mediapy.show_video(imgs, fps=10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "liRaNVbE1gWb" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "name": "multi_actors_demo.ipynb", + "private_outputs": true, + "provenance": [ + { + "file_id": "14w5MbrMNLsOsLuD5kXy5-rrNO3ZgsHat", + "timestamp": 1678404744504 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt new file mode 100644 index 0000000..d04c1c3 --- /dev/null +++ b/docs/_sources/build/jupyter_execute/build/jupyter_execute/build/jupyter_execute/notebooks/wosac_submission_via_waymax.ipynb.txt @@ -0,0 +1,348 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "DH236BT5wcH7" + }, + "source": [ + "# Waymo Open Sim Agents Challenge Submission\n", + "\n", + "This tutorial covers how to use Waymax to create a Waymo Open Sim Agents Challenge (WOSAC) submission.\n", + "\n", + "Please also refer to the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb) for additional reference and for setting up a submission without Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "n-sW1wrcvHys" + }, + "outputs": [], + "source": [ + "!pip install waymo-open-dataset-tf-2-11-0==1.6.0\n", + "\n", + "import os\n", + "import jax\n", + "from jax import random\n", + "from jax import numpy as jnp\n", + "import tensorflow as tf\n", + "\n", + "from waymo_open_dataset.protos import sim_agents_submission_pb2\n", + "from waymax import agents\n", + "from waymax import config as _config\n", + "from waymax import dynamics\n", + "from waymax import dataloader\n", + "from waymax import datatypes\n", + "from waymax import env as _env\n", + "\n", + "CURRENT_TIME_INDEX = 10\n", + "N_SIMULATION_STEPS = 80\n", + "N_ROLLOUTS = 32" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9YTPFzmMwu-F" + }, + "source": [ + "## Dataloader\n", + "\n", + "To load data for a WOSAC submission, we write a custom dataloader that processes the scenario IDs. These are normally discarded in the default Waymax dataloader as they are not used during simulation and JAX does not have native support for string data. The scenario ID is stored in the field `scenario/id` as described in the [`tf.Example` spec](https://waymo.com/open/data/motion/tfexample).\n", + "\n", + "This custom dataloader defines a preprocessor `_preprocess` that decodes the scenario ID into an array of bytes, and a postprocessor `_postprocess` that converts those bytes into the string scenario ID. The actual scenario data is processed in the same way as the default dataloader in Waymax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JJAfGGSF74Ym" + }, + "outputs": [], + "source": [ + "data_config = _config.WOD_1_2_0_TEST\n", + "\n", + "# Write a custom dataloader that loads scenario IDs.\n", + "def _preprocess(serialized: bytes) -> dict[str, tf.Tensor]:\n", + " womd_features = dataloader.womd_utils.get_features_description(\n", + " include_sdc_paths=data_config.include_sdc_paths,\n", + " max_num_rg_points=data_config.max_num_rg_points,\n", + " num_paths=data_config.num_paths,\n", + " num_points_per_path=data_config.num_points_per_path,\n", + " )\n", + " womd_features['scenario/id'] = tf.io.FixedLenFeature([1], tf.string)\n", + "\n", + " deserialized = tf.io.parse_example(serialized, womd_features)\n", + " parsed_id = deserialized.pop('scenario/id')\n", + " deserialized['scenario/id'] = tf.io.decode_raw(parsed_id, tf.uint8)\n", + "\n", + " return dataloader.preprocess_womd_example(\n", + " deserialized,\n", + " aggregate_timesteps=data_config.aggregate_timesteps,\n", + " max_num_objects=data_config.max_num_objects,\n", + " )\n", + "\n", + "def _postprocess(example: dict[str, tf.Tensor]):\n", + " scenario = dataloader.simulator_state_from_womd_dict(example)\n", + " scenario_id = example['scenario/id']\n", + " return scenario_id, scenario\n", + "\n", + "def decode_bytes(data_iter):\n", + " for scenario_id, scenario in data_iter:\n", + " scenario_id = scenario_id.tobytes().decode('utf-8')\n", + " yield scenario_id, scenario\n", + "\n", + "data_iter = decode_bytes(dataloader.get_data_generator(\n", + " data_config, _preprocess, _postprocess\n", + "))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hXQk4wG8xmTs" + }, + "source": [ + "## Environment and Agent Configuration\n", + "\n", + "The following code initializes the environment and sim agent used for simulation. In this example, we use a constant speed actor which will maintain the course and speed that the agent has at the initial timestep.\n", + "\n", + "WOSAC evaluates metrics on all agents valid at the initial timestep. Therefore, the `is_controlled` field is set to all valid agents at the 11th timestep.\n", + "\n", + "Other configurations related to the agent and environment are customizable. This includes the dynamics model (here, we use the `InvertibleBicycleModel`) and the type of sim agent to evaluate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XnvXA1z1wwqQ" + }, + "outputs": [], + "source": [ + "env_config = _config.EnvironmentConfig(\n", + " # Ensure that the sim agent can control all valid objects.\n", + " controlled_object=_config.ObjectType.VALID\n", + ")\n", + "\n", + "dynamics_model = dynamics.InvertibleBicycleModel()\n", + "env = _env.MultiAgentEnvironment(\n", + " dynamics_model=dynamics_model,\n", + " config=env_config,\n", + ")\n", + "\n", + "agent = agents.create_constant_speed_actor(\n", + " dynamics_model=dynamics_model,\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled_func=lambda state: state.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + ")\n", + "\n", + "jit_step = jax.jit(env.step)\n", + "jit_select_action = jax.jit(agent.select_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jGI9vfXFyBEo" + }, + "source": [ + "## Generating Rollouts\n", + "\n", + "We can now define a function that will rollout the environment and agent to generate trajectories. The WOSAC submission format consists of multiple protobufs defined in `sim_agents_submission_pb2`. These consist of (copied from the [WOSAC submission notebook](https://github.com/waymo-research/waymo-open-dataset/blob/master/tutorial/tutorial_sim_agents.ipynb)):\n", + "\n", + "- `SimulatedTrajectory` contains one trajectory for a single object, with the fields we need to simulate (x, y, z, heading).\n", + "- `JointScene` is a set of all the object trajectories from a single simulation, describing one of the possible rollouts.\n", + "- `ScenarioRollouts` is a collection of all the parallel simulations for a single initial Scenario.\n", + "- `SimAgentsChallengeSubmission` is used to package submissions for multiple Scenarios (e.g. for the whole testing dataset).\n", + "\n", + "Here, we will write a function `generate_scenario_rollout` that generates a `ScenarioRollouts` protobuf from a single input scenario. By default, WOSAC requires 32 rollouts per scenario. Our actor is deterministic so all 32 rollouts will be identical, but we still generate these rollouts to provide an accurate example of a proper submission.\n", + "\n", + "We also provide a utility function `validate_scenario_rollout` to help ensure that the scenario rollouts have the correct format before uploading.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "-hHb4wXa6Jo-" + }, + "outputs": [], + "source": [ + "def validate_scenario_rollout(scenario_rollouts: sim_agents_submission_pb2.ScenarioRollouts,\n", + " scenario: datatypes.SimulatorState):\n", + " \"\"\"Verifies if scenario_rollouts has correct formatting.\"\"\"\n", + " valid_sim_agents = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + " sim_agent_id_idxs = jnp.where(valid_sim_agents)[0]\n", + " sim_agent_ids = scenario.object_metadata.ids[sim_agent_id_idxs].tolist()\n", + "\n", + " if len(scenario_rollouts.joint_scenes) != N_ROLLOUTS:\n", + " raise ValueError('Incorrect number of parallel simulations. '\n", + " f'(Actual: {len(scenario_rollouts.joint_scenes)}, '\n", + " f'Expected: {N_ROLLOUTS})')\n", + "\n", + " def _raise_if_wrong_length(trajectory, field_name, expected_length):\n", + " if len(getattr(trajectory, field_name)) != expected_length:\n", + " raise ValueError(f'Invalid {field_name} tensor length '\n", + " f'(actual: {len(getattr(trajectory, field_name))}, '\n", + " f'expected: {expected_length})')\n", + "\n", + " for joint_scene in scenario_rollouts.joint_scenes:\n", + " simulated_ids = []\n", + " for simulated_trajectory in joint_scene.simulated_trajectories:\n", + " # Check the length of each of the simulated fields.\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_x', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_y', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'center_z', N_SIMULATION_STEPS)\n", + " _raise_if_wrong_length(simulated_trajectory, 'heading', N_SIMULATION_STEPS)\n", + " # Check that each object ID is present in the original WOMD scenario.\n", + " if simulated_trajectory.object_id not in sim_agent_ids:\n", + " raise ValueError(\n", + " f'Object {simulated_trajectory.object_id} is not a sim agent.')\n", + " simulated_ids.append(simulated_trajectory.object_id)\n", + " # Check that all of the required objects/agents are simulated.\n", + " missing_agents = set(sim_agent_ids) - set(simulated_ids)\n", + " if missing_agents:\n", + " raise ValueError(\n", + " f'Sim agents {missing_agents} are missing from the simulation.')\n", + "\n", + "\n", + "def generate_scenario_rollout(\n", + " scenario_id: str,\n", + " scenario: datatypes.SimulatorState) -> sim_agents_submission_pb2.ScenarioRollouts:\n", + " \"\"\"Simulate 32 rollouts and return a ScenarioRollouts protobuf.\"\"\"\n", + " joint_scenes = []\n", + " key = random.PRNGKey(0)\n", + " for _ in range(N_ROLLOUTS):\n", + " initial_state = current_state = env.reset(scenario)\n", + " # Controlled objects are those valid at t=0.\n", + " is_controlled = scenario.log_trajectory.valid[..., CURRENT_TIME_INDEX]\n", + "\n", + " # Run the sim agent for 80 steps.\n", + " for _ in (range(initial_state.remaining_timesteps)):\n", + " key, actor_key = random.split(key, 2)\n", + " actor_output = jit_select_action({}, current_state, None, actor_key)\n", + " next_state = jit_step(current_state, actor_output.action)\n", + " current_state = next_state\n", + "\n", + " # Write out result\n", + " final_trajectory = current_state.sim_trajectory\n", + " object_ids = current_state.object_metadata.ids # Shape (n_objects,)\n", + " object_ids = jnp.where(is_controlled, object_ids, -1)\n", + "\n", + " simulated_trajectories = []\n", + " for i, object_id in enumerate(object_ids):\n", + " if object_id != -1:\n", + " simulated_trajectory = sim_agents_submission_pb2.SimulatedTrajectory(\n", + " center_x=final_trajectory.x[i, env_config.init_steps:],\n", + " center_y=final_trajectory.y[i, env_config.init_steps:],\n", + " center_z=final_trajectory.z[i, env_config.init_steps:],\n", + " heading=final_trajectory.yaw[i, env_config.init_steps:],\n", + " object_id=object_id,\n", + " )\n", + " simulated_trajectories.append(simulated_trajectory)\n", + " joint_scene = sim_agents_submission_pb2.JointScene(\n", + " simulated_trajectories=simulated_trajectories\n", + " )\n", + " joint_scenes.append(joint_scene)\n", + "\n", + " scenario_rollouts = sim_agents_submission_pb2.ScenarioRollouts(\n", + " scenario_id=scenario_id, joint_scenes=joint_scenes\n", + " )\n", + " validate_scenario_rollout(scenario_rollouts, scenario)\n", + " return scenario_rollouts" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XPlK3Qp_yD8K" + }, + "source": [ + "## Generating the Submission\n", + "\n", + "We are now ready to generate the submission file. Because the data is potentially large (over the 2GB maximum size for a protobuf), we process the data in a streaming fashion and write out results incrementally. The testing set of Waymo Open Motion Dataset v1.2.0 has 44926 segments -- this step may take a significant amount of time if the rollout generation time is long.\n", + "\n", + "After we process all of the data, we zip the individual shards to create a zip file ready for submission. Please refer to the Open dataset website for further instructions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "mjOZoUVYT0OW" + }, + "outputs": [], + "source": [ + "OUTPUT_ROOT_DIRECTORY = '/tmp/waymo_sim_agents/'\n", + "os.makedirs(OUTPUT_ROOT_DIRECTORY, exist_ok=True)\n", + "output_filenames = []\n", + "scenario_rollouts = []\n", + "\n", + "for i, (scenario_id, scenario) in enumerate(data_iter):\n", + " scenario_rollouts.append(generate_scenario_rollout(scenario_id, scenario))\n", + "\n", + " if i % 5 == 0 and i > 0:\n", + " shard_suffix = '.%d' % i\n", + " shard_submission = sim_agents_submission_pb2.SimAgentsChallengeSubmission(\n", + " scenario_rollouts=scenario_rollouts,\n", + " submission_type=sim_agents_submission_pb2.SimAgentsChallengeSubmission.SIM_AGENTS_SUBMISSION,\n", + " account_name='your_account@test.com',\n", + " unique_method_name='waymax_sim_agents_tutorial',\n", + " authors=['test'],\n", + " affiliation='waymo',\n", + " description='Submission from the Waymax - Sim Agents tutorial',\n", + " method_link='https://waymo.com/open/'\n", + " )\n", + " scenario_rollouts = []\n", + " output_filename = f'submission.binproto{shard_suffix}'\n", + " with open(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename), 'wb') as f:\n", + " f.write(shard_submission.SerializeToString())\n", + " output_filenames.append(output_filename)\n", + "\n", + "# Once we have created all the shards, we can package them directly into a\n", + "# tar.gz archive, ready for submission.\n", + "with tarfile.open(\n", + " os.path.join(OUTPUT_ROOT_DIRECTORY, 'submission.tar.gz'), 'w:gz') as tar:\n", + " for output_filename in output_filenames:\n", + " tar.add(os.path.join(OUTPUT_ROOT_DIRECTORY, output_filename),\n", + " arcname=output_filename)" + ] + } + ], + "metadata": { + "colab": { + "last_runtime": { + "build_target": "", + "kind": "local" + }, + "private_outputs": true, + "provenance": [ + { + "file_id": "165MIhRtVwtmR9ontFYJe6suV6CLHerJI", + "timestamp": 1695771240550 + }, + { + "file_id": "1Y3eSCA7LCGrCJ672zHeBnStJKTEorl5z", + "timestamp": 1695771147053 + }, + { + "file_id": "1l1iYQbLAGQ1vv-13AriC2bp78LPv3LH2", + "timestamp": 1695668356011 + } + ] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/docs/_static/scripts/furo.js b/docs/_static/scripts/furo.js index cbf6487..32e7c05 100644 --- a/docs/_static/scripts/furo.js +++ b/docs/_static/scripts/furo.js @@ -1,3 +1,3 @@ /*! For license information please see furo.js.LICENSE.txt */ -(()=>{var t={212:function(t,e,n){var o,r;r=void 0!==n.g?n.g:"undefined"!=typeof window?window:this,o=function(){return function(t){"use strict";var e={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},n=function(t,e,n){if(n.settings.events){var o=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(o)}},o=function(t){var e=0;if(t.offsetParent)for(;t;)e+=t.offsetTop,t=t.offsetParent;return e>=0?e:0},r=function(t){t&&t.sort((function(t,e){return o(t.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},l=function(t,e){var n=t[t.length-1];if(function(t,e){return!(!s()||!c(t.content,e,!0))}(n,e))return n;for(var o=t.length-1;o>=0;o--)if(c(t[o].content,e))return t[o]},a=function(t,e){if(e.nested&&t.parentNode){var n=t.parentNode.closest("li");n&&(n.classList.remove(e.nestedClass),a(n,e))}},i=function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.remove(e.navClass),t.content.classList.remove(e.contentClass),a(o,e),n("gumshoeDeactivate",o,{link:t.nav,content:t.content,settings:e}))}},u=function(t,e){if(e.nested){var n=t.parentNode.closest("li");n&&(n.classList.add(e.nestedClass),u(n,e))}};return function(o,c){var s,a,d,f,m,v={setup:function(){s=document.querySelectorAll(o),a=[],Array.prototype.forEach.call(s,(function(t){var e=document.getElementById(decodeURIComponent(t.hash.substr(1)));e&&a.push({nav:t,content:e})})),r(a)},detect:function(){var t=l(a,m);t?d&&t.content===d.content||(i(d,m),function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.add(e.navClass),t.content.classList.add(e.contentClass),u(o,e),n("gumshoeActivate",o,{link:t.nav,content:t.content,settings:e}))}}(t,m),d=t):d&&(i(d,m),d=null)}},h=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(v.detect)},g=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame((function(){r(a),v.detect()}))};return v.destroy=function(){d&&i(d,m),t.removeEventListener("scroll",h,!1),m.reflow&&t.removeEventListener("resize",g,!1),a=null,s=null,d=null,f=null,m=null},m=function(){var t={};return Array.prototype.forEach.call(arguments,(function(e){for(var n in e){if(!e.hasOwnProperty(n))return;t[n]=e[n]}})),t}(e,c||{}),v.setup(),v.detect(),t.addEventListener("scroll",h,!1),m.reflow&&t.addEventListener("resize",g,!1),v}}(r)}.apply(e,[]),void 0===o||(t.exports=o)}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var c=e[o]={exports:{}};return t[o].call(c.exports,c,c.exports,n),c.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=n(212),e=n.n(t),o=null,r=null,c=window.pageYOffset||document.documentElement.scrollTop;function s(){const t=localStorage.getItem("theme")||"auto";var e;"light"!==(e=window.matchMedia("(prefers-color-scheme: dark)").matches?"auto"===t?"light":"light"==t?"dark":"auto":"auto"===t?"dark":"dark"==t?"light":"auto")&&"dark"!==e&&"auto"!==e&&(console.error(`Got invalid theme mode: ${e}. Resetting to auto.`),e="auto"),document.body.dataset.theme=e,localStorage.setItem("theme",e),console.log(`Changed to ${e} mode.`)}function l(){!function(){const t=document.getElementsByClassName("theme-toggle");Array.from(t).forEach((t=>{t.addEventListener("click",s)}))}(),function(){let t=0,e=!1;window.addEventListener("scroll",(function(n){t=window.scrollY,e||(window.requestAnimationFrame((function(){var n;n=t,0==Math.floor(r.getBoundingClientRect().top)?r.classList.add("scrolled"):r.classList.remove("scrolled"),function(t){t<64?document.documentElement.classList.remove("show-back-to-top"):tc&&document.documentElement.classList.remove("show-back-to-top"),c=t}(n),function(t){null!==o&&(0==t?o.scrollTo(0,0):Math.ceil(t)>=Math.floor(document.documentElement.scrollHeight-window.innerHeight)?o.scrollTo(0,o.scrollHeight):document.querySelector(".scroll-current"))}(n),e=!1})),e=!0)})),window.scroll()}(),null!==o&&new(e())(".toc-tree a",{reflow:!0,recursive:!0,navClass:"scroll-current",offset:()=>{let t=parseFloat(getComputedStyle(document.documentElement).fontSize);return r.getBoundingClientRect().height+.5*t+1}})}document.addEventListener("DOMContentLoaded",(function(){document.body.parentNode.classList.remove("no-js"),r=document.querySelector("header"),o=document.querySelector(".toc-scroll"),l()}))})()})(); +(()=>{var t={212:function(t,e,n){var o,r;r=void 0!==n.g?n.g:"undefined"!=typeof window?window:this,o=function(){return function(t){"use strict";var e={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},n=function(t,e,n){if(n.settings.events){var o=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n});e.dispatchEvent(o)}},o=function(t){var e=0;if(t.offsetParent)for(;t;)e+=t.offsetTop,t=t.offsetParent;return e>=0?e:0},r=function(t){t&&t.sort((function(t,e){return o(t.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},l=function(t,e){var n=t[t.length-1];if(function(t,e){return!(!s()||!c(t.content,e,!0))}(n,e))return n;for(var o=t.length-1;o>=0;o--)if(c(t[o].content,e))return t[o]},a=function(t,e){if(e.nested&&t.parentNode){var n=t.parentNode.closest("li");n&&(n.classList.remove(e.nestedClass),a(n,e))}},i=function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.remove(e.navClass),t.content.classList.remove(e.contentClass),a(o,e),n("gumshoeDeactivate",o,{link:t.nav,content:t.content,settings:e}))}},u=function(t,e){if(e.nested){var n=t.parentNode.closest("li");n&&(n.classList.add(e.nestedClass),u(n,e))}};return function(o,c){var s,a,d,f,m,v={setup:function(){s=document.querySelectorAll(o),a=[],Array.prototype.forEach.call(s,(function(t){var e=document.getElementById(decodeURIComponent(t.hash.substr(1)));e&&a.push({nav:t,content:e})})),r(a)},detect:function(){var t=l(a,m);t?d&&t.content===d.content||(i(d,m),function(t,e){if(t){var o=t.nav.closest("li");o&&(o.classList.add(e.navClass),t.content.classList.add(e.contentClass),u(o,e),n("gumshoeActivate",o,{link:t.nav,content:t.content,settings:e}))}}(t,m),d=t):d&&(i(d,m),d=null)}},h=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame(v.detect)},g=function(e){f&&t.cancelAnimationFrame(f),f=t.requestAnimationFrame((function(){r(a),v.detect()}))};return v.destroy=function(){d&&i(d,m),t.removeEventListener("scroll",h,!1),m.reflow&&t.removeEventListener("resize",g,!1),a=null,s=null,d=null,f=null,m=null},m=function(){var t={};return Array.prototype.forEach.call(arguments,(function(e){for(var n in e){if(!e.hasOwnProperty(n))return;t[n]=e[n]}})),t}(e,c||{}),v.setup(),v.detect(),t.addEventListener("scroll",h,!1),m.reflow&&t.addEventListener("resize",g,!1),v}}(r)}.apply(e,[]),void 0===o||(t.exports=o)}},e={};function n(o){var r=e[o];if(void 0!==r)return r.exports;var c=e[o]={exports:{}};return t[o].call(c.exports,c,c.exports,n),c.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{"use strict";var t=n(212),e=n.n(t),o=null,r=null,c=window.pageYOffset||document.documentElement.scrollTop;const s=64;function l(){const t=localStorage.getItem("theme")||"auto";var e;"light"!==(e=window.matchMedia("(prefers-color-scheme: dark)").matches?"auto"===t?"light":"light"==t?"dark":"auto":"auto"===t?"dark":"dark"==t?"light":"auto")&&"dark"!==e&&"auto"!==e&&(console.error(`Got invalid theme mode: ${e}. Resetting to auto.`),e="auto"),document.body.dataset.theme=e,localStorage.setItem("theme",e),console.log(`Changed to ${e} mode.`)}function a(){!function(){const t=document.getElementsByClassName("theme-toggle");Array.from(t).forEach((t=>{t.addEventListener("click",l)}))}(),function(){let t=0,e=!1;window.addEventListener("scroll",(function(n){t=window.scrollY,e||(window.requestAnimationFrame((function(){var n;n=t,0==Math.floor(r.getBoundingClientRect().top)?r.classList.add("scrolled"):r.classList.remove("scrolled"),function(t){tc&&document.documentElement.classList.remove("show-back-to-top"),c=t}(n),function(t){null!==o&&(0==t?o.scrollTo(0,0):Math.ceil(t)>=Math.floor(document.documentElement.scrollHeight-window.innerHeight)?o.scrollTo(0,o.scrollHeight):document.querySelector(".scroll-current"))}(n),e=!1})),e=!0)})),window.scroll()}(),null!==o&&new(e())(".toc-tree a",{reflow:!0,recursive:!0,navClass:"scroll-current",offset:()=>{let t=parseFloat(getComputedStyle(document.documentElement).fontSize);return r.getBoundingClientRect().height+.5*t+1}})}document.addEventListener("DOMContentLoaded",(function(){document.body.parentNode.classList.remove("no-js"),r=document.querySelector("header"),o=document.querySelector(".toc-scroll"),a()}))})()})(); //# sourceMappingURL=furo.js.map \ No newline at end of file diff --git a/docs/_static/scripts/furo.js.map b/docs/_static/scripts/furo.js.map index 7ed2be8..7b7ddb1 100644 --- a/docs/_static/scripts/furo.js.map +++ b/docs/_static/scripts/furo.js.map @@ -1 +1 @@ -{"version":3,"file":"scripts/furo.js","mappings":";iCAAA,MAQWA,SAWS,IAAX,EAAAC,EACH,EAAAA,EACkB,oBAAXC,OACPA,OACAC,KAbS,EAAF,WACP,OAaJ,SAAUD,GACR,aAMA,IAAIE,EAAW,CAEbC,SAAU,SACVC,aAAc,SAGdC,QAAQ,EACRC,YAAa,SAGbC,OAAQ,EACRC,QAAQ,EAGRC,QAAQ,GA6BNC,EAAY,SAAUC,EAAMC,EAAMC,GAEpC,GAAKA,EAAOC,SAASL,OAArB,CAGA,IAAIM,EAAQ,IAAIC,YAAYL,EAAM,CAChCM,SAAS,EACTC,YAAY,EACZL,OAAQA,IAIVD,EAAKO,cAAcJ,KAQjBK,EAAe,SAAUR,GAC3B,IAAIS,EAAW,EACf,GAAIT,EAAKU,aACP,KAAOV,GACLS,GAAYT,EAAKW,UACjBX,EAAOA,EAAKU,aAGhB,OAAOD,GAAY,EAAIA,EAAW,GAOhCG,EAAe,SAAUC,GACvBA,GACFA,EAASC,MAAK,SAAUC,EAAOC,GAG7B,OAFcR,EAAaO,EAAME,SACnBT,EAAaQ,EAAMC,UACF,EACxB,MA2CTC,EAAW,SAAUlB,EAAME,EAAUiB,GACvC,IAAIC,EAASpB,EAAKqB,wBACd1B,EAnCU,SAAUO,GAExB,MAA+B,mBAApBA,EAASP,OACX2B,WAAWpB,EAASP,UAItB2B,WAAWpB,EAASP,QA4Bd4B,CAAUrB,GACvB,OAAIiB,EAEAK,SAASJ,EAAOD,OAAQ,KACvB/B,EAAOqC,aAAeC,SAASC,gBAAgBC,cAG7CJ,SAASJ,EAAOS,IAAK,KAAOlC,GAOjCmC,EAAa,WACf,OACEC,KAAKC,KAAK5C,EAAOqC,YAAcrC,EAAO6C,cAnCjCF,KAAKG,IACVR,SAASS,KAAKC,aACdV,SAASC,gBAAgBS,aACzBV,SAASS,KAAKE,aACdX,SAASC,gBAAgBU,aACzBX,SAASS,KAAKP,aACdF,SAASC,gBAAgBC,eAqDzBU,EAAY,SAAUzB,EAAUX,GAClC,IAAIqC,EAAO1B,EAASA,EAAS2B,OAAS,GACtC,GAbgB,SAAUC,EAAMvC,GAChC,SAAI4B,MAAgBZ,EAASuB,EAAKxB,QAASf,GAAU,IAYjDwC,CAAYH,EAAMrC,GAAW,OAAOqC,EACxC,IAAK,IAAII,EAAI9B,EAAS2B,OAAS,EAAGG,GAAK,EAAGA,IACxC,GAAIzB,EAASL,EAAS8B,GAAG1B,QAASf,GAAW,OAAOW,EAAS8B,IAS7DC,EAAmB,SAAUC,EAAK3C,GAEpC,GAAKA,EAAST,QAAWoD,EAAIC,WAA7B,CAGA,IAAIC,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASR,aAG7BkD,EAAiBG,EAAI7C,MAQnBiD,EAAa,SAAUC,EAAOlD,GAEhC,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUC,OAAOhD,EAASX,UAC7B6D,EAAMnC,QAAQgC,UAAUC,OAAOhD,EAASV,cAGxCoD,EAAiBG,EAAI7C,GAGrBJ,EAAU,oBAAqBiD,EAAI,CACjCM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,OASVoD,EAAiB,SAAUT,EAAK3C,GAElC,GAAKA,EAAST,OAAd,CAGA,IAAIsD,EAAKF,EAAIC,WAAWE,QAAQ,MAC3BD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASR,aAG1B4D,EAAeP,EAAI7C,MA8LrB,OA1JkB,SAAUsD,EAAUC,GAKpC,IACIC,EAAU7C,EAAU8C,EAASC,EAAS1D,EADtC2D,EAAa,CAUjBA,MAAmB,WAEjBH,EAAWhC,SAASoC,iBAAiBN,GAGrC3C,EAAW,GAGXkD,MAAMC,UAAUC,QAAQC,KAAKR,GAAU,SAAUjB,GAE/C,IAAIxB,EAAUS,SAASyC,eACrBC,mBAAmB3B,EAAK4B,KAAKC,OAAO,KAEjCrD,GAGLJ,EAAS0D,KAAK,CACZ1B,IAAKJ,EACLxB,QAASA,OAKbL,EAAaC,IAMfgD,OAAoB,WAElB,IAAIW,EAASlC,EAAUzB,EAAUX,GAG5BsE,EASDb,GAAWa,EAAOvD,UAAY0C,EAAQ1C,UAG1CkC,EAAWQ,EAASzD,GAzFT,SAAUkD,EAAOlD,GAE9B,GAAKkD,EAAL,CAGA,IAAIL,EAAKK,EAAMP,IAAIG,QAAQ,MACtBD,IAGLA,EAAGE,UAAUM,IAAIrD,EAASX,UAC1B6D,EAAMnC,QAAQgC,UAAUM,IAAIrD,EAASV,cAGrC8D,EAAeP,EAAI7C,GAGnBJ,EAAU,kBAAmBiD,EAAI,CAC/BM,KAAMD,EAAMP,IACZ5B,QAASmC,EAAMnC,QACff,SAAUA,MAuEVuE,CAASD,EAAQtE,GAGjByD,EAAUa,GAfJb,IACFR,EAAWQ,EAASzD,GACpByD,EAAU,QAoBZe,EAAgB,SAAUvE,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,sBAAsBf,EAAWgB,SAOhDC,EAAgB,SAAU3E,GAExByD,GACFxE,EAAOuF,qBAAqBf,GAI9BA,EAAUxE,EAAOwF,uBAAsB,WACrChE,EAAaC,GACbgD,EAAWgB,aAoDf,OA7CAhB,EAAWkB,QAAU,WAEfpB,GACFR,EAAWQ,EAASzD,GAItBd,EAAO4F,oBAAoB,SAAUN,GAAe,GAChDxE,EAASN,QACXR,EAAO4F,oBAAoB,SAAUF,GAAe,GAItDjE,EAAW,KACX6C,EAAW,KACXC,EAAU,KACVC,EAAU,KACV1D,EAAW,MAQXA,EA3XS,WACX,IAAI+E,EAAS,GAOb,OANAlB,MAAMC,UAAUC,QAAQC,KAAKgB,WAAW,SAAUC,GAChD,IAAK,IAAIC,KAAOD,EAAK,CACnB,IAAKA,EAAIE,eAAeD,GAAM,OAC9BH,EAAOG,GAAOD,EAAIC,OAGfH,EAmXMK,CAAOhG,EAAUmE,GAAW,IAGvCI,EAAW0B,QAGX1B,EAAWgB,SAGXzF,EAAOoG,iBAAiB,SAAUd,GAAe,GAC7CxE,EAASN,QACXR,EAAOoG,iBAAiB,SAAUV,GAAe,GAS9CjB,GA7bA4B,CAAQvG,IAChB,QAFM,SAEN,uBCXDwG,EAA2B,GAG/B,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,IAOV,OAHAE,EAAoBL,GAAU1B,KAAK8B,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAGpEK,EAAOD,QCpBfJ,EAAoBO,EAAKF,IACxB,IAAIG,EAASH,GAAUA,EAAOI,WAC7B,IAAOJ,EAAiB,QACxB,IAAM,EAEP,OADAL,EAAoBU,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,GCLRR,EAAoBU,EAAI,CAACN,EAASQ,KACjC,IAAI,IAAInB,KAAOmB,EACXZ,EAAoBa,EAAED,EAAYnB,KAASO,EAAoBa,EAAET,EAASX,IAC5EqB,OAAOC,eAAeX,EAASX,EAAK,CAAEuB,YAAY,EAAMC,IAAKL,EAAWnB,MCJ3EO,EAAoBxG,EAAI,WACvB,GAA0B,iBAAf0H,WAAyB,OAAOA,WAC3C,IACC,OAAOxH,MAAQ,IAAIyH,SAAS,cAAb,GACd,MAAOC,GACR,GAAsB,iBAAX3H,OAAqB,OAAOA,QALjB,GCAxBuG,EAAoBa,EAAI,CAACrB,EAAK6B,IAAUP,OAAOzC,UAAUqB,eAAenB,KAAKiB,EAAK6B,4CCK9EC,EAAY,KACZC,EAAS,KACTC,EAAgB/H,OAAO6C,aAAeP,SAASC,gBAAgByF,UA4EnE,SAASC,IACP,MAAMC,EAAeC,aAAaC,QAAQ,UAAY,OAZxD,IAAkBC,EACH,WADGA,EAaIrI,OAAOsI,WAAW,gCAAgCC,QAI/C,SAAjBL,EACO,QACgB,SAAhBA,EACA,OAEA,OAIU,SAAjBA,EACO,OACgB,QAAhBA,EACA,QAEA,SA9BoB,SAATG,GAA4B,SAATA,IACzCG,QAAQC,MAAM,2BAA2BJ,yBACzCA,EAAO,QAGT/F,SAASS,KAAK2F,QAAQC,MAAQN,EAC9BF,aAAaS,QAAQ,QAASP,GAC9BG,QAAQK,IAAI,cAAcR,WA4E5B,SAASlC,KART,WAEE,MAAM2C,EAAUxG,SAASyG,uBAAuB,gBAChDpE,MAAMqE,KAAKF,GAASjE,SAASoE,IAC3BA,EAAI7C,iBAAiB,QAAS6B,MAKhCiB,GA9CF,WAEE,IAAIC,EAA6B,EAC7BC,GAAU,EAEdpJ,OAAOoG,iBAAiB,UAAU,SAAUuB,GAC1CwB,EAA6BnJ,OAAOqJ,QAE/BD,IACHpJ,OAAOwF,uBAAsB,WAzDnC,IAAuB8D,IA0DDH,EA9GkC,GAAlDxG,KAAK4G,MAAMzB,EAAO7F,wBAAwBQ,KAC5CqF,EAAOjE,UAAUM,IAAI,YAErB2D,EAAOjE,UAAUC,OAAO,YAI5B,SAAmCwF,GAC7BA,EAXmB,GAYrBhH,SAASC,gBAAgBsB,UAAUC,OAAO,oBAEtCwF,EAAYvB,EACdzF,SAASC,gBAAgBsB,UAAUM,IAAI,oBAC9BmF,EAAYvB,GACrBzF,SAASC,gBAAgBsB,UAAUC,OAAO,oBAG9CiE,EAAgBuB,EAqChBE,CAA0BF,GAlC5B,SAA6BA,GACT,OAAdzB,IAKa,GAAbyB,EACFzB,EAAU4B,SAAS,EAAG,GAGtB9G,KAAKC,KAAK0G,IACV3G,KAAK4G,MAAMjH,SAASC,gBAAgBS,aAAehD,OAAOqC,aAE1DwF,EAAU4B,SAAS,EAAG5B,EAAU7E,cAGhBV,SAASoH,cAAc,oBAmBzCC,CAAoBL,GAwDdF,GAAU,KAGZA,GAAU,MAGdpJ,OAAO4J,SA8BPC,GA1BkB,OAAdhC,GAKJ,IAAI,IAAJ,CAAY,cAAe,CACzBrH,QAAQ,EACRsJ,WAAW,EACX3J,SAAU,iBACVI,OAAQ,KACN,IAAIwJ,EAAM7H,WAAW8H,iBAAiB1H,SAASC,iBAAiB0H,UAChE,OAAOnC,EAAO7F,wBAAwBiI,OAAS,GAAMH,EAAM,KA+BjEzH,SAAS8D,iBAAiB,oBAT1B,WACE9D,SAASS,KAAKW,WAAWG,UAAUC,OAAO,SAE1CgE,EAASxF,SAASoH,cAAc,UAChC7B,EAAYvF,SAASoH,cAAc,eAEnCvD","sources":["webpack:///./src/furo/assets/scripts/gumshoe-patched.js","webpack:///webpack/bootstrap","webpack:///webpack/runtime/compat get default export","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/global","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./src/furo/assets/scripts/furo.js"],"sourcesContent":["/*!\n * gumshoejs v5.1.2 (patched by @pradyunsg)\n * A simple, framework-agnostic scrollspy script.\n * (c) 2019 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/gumshoe\n */\n\n(function (root, factory) {\n if (typeof define === \"function\" && define.amd) {\n define([], function () {\n return factory(root);\n });\n } else if (typeof exports === \"object\") {\n module.exports = factory(root);\n } else {\n root.Gumshoe = factory(root);\n }\n})(\n typeof global !== \"undefined\"\n ? global\n : typeof window !== \"undefined\"\n ? window\n : this,\n function (window) {\n \"use strict\";\n\n //\n // Defaults\n //\n\n var defaults = {\n // Active classes\n navClass: \"active\",\n contentClass: \"active\",\n\n // Nested navigation\n nested: false,\n nestedClass: \"active\",\n\n // Offset & reflow\n offset: 0,\n reflow: false,\n\n // Event support\n events: true,\n };\n\n //\n // Methods\n //\n\n /**\n * Merge two or more objects together.\n * @param {Object} objects The objects to merge together\n * @returns {Object} Merged values of defaults and options\n */\n var extend = function () {\n var merged = {};\n Array.prototype.forEach.call(arguments, function (obj) {\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) return;\n merged[key] = obj[key];\n }\n });\n return merged;\n };\n\n /**\n * Emit a custom event\n * @param {String} type The event type\n * @param {Node} elem The element to attach the event to\n * @param {Object} detail Any details to pass along with the event\n */\n var emitEvent = function (type, elem, detail) {\n // Make sure events are enabled\n if (!detail.settings.events) return;\n\n // Create a new event\n var event = new CustomEvent(type, {\n bubbles: true,\n cancelable: true,\n detail: detail,\n });\n\n // Dispatch the event\n elem.dispatchEvent(event);\n };\n\n /**\n * Get an element's distance from the top of the Document.\n * @param {Node} elem The element\n * @return {Number} Distance from the top in pixels\n */\n var getOffsetTop = function (elem) {\n var location = 0;\n if (elem.offsetParent) {\n while (elem) {\n location += elem.offsetTop;\n elem = elem.offsetParent;\n }\n }\n return location >= 0 ? location : 0;\n };\n\n /**\n * Sort content from first to last in the DOM\n * @param {Array} contents The content areas\n */\n var sortContents = function (contents) {\n if (contents) {\n contents.sort(function (item1, item2) {\n var offset1 = getOffsetTop(item1.content);\n var offset2 = getOffsetTop(item2.content);\n if (offset1 < offset2) return -1;\n return 1;\n });\n }\n };\n\n /**\n * Get the offset to use for calculating position\n * @param {Object} settings The settings for this instantiation\n * @return {Float} The number of pixels to offset the calculations\n */\n var getOffset = function (settings) {\n // if the offset is a function run it\n if (typeof settings.offset === \"function\") {\n return parseFloat(settings.offset());\n }\n\n // Otherwise, return it as-is\n return parseFloat(settings.offset);\n };\n\n /**\n * Get the document element's height\n * @private\n * @returns {Number}\n */\n var getDocumentHeight = function () {\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight,\n );\n };\n\n /**\n * Determine if an element is in view\n * @param {Node} elem The element\n * @param {Object} settings The settings for this instantiation\n * @param {Boolean} bottom If true, check if element is above bottom of viewport instead\n * @return {Boolean} Returns true if element is in the viewport\n */\n var isInView = function (elem, settings, bottom) {\n var bounds = elem.getBoundingClientRect();\n var offset = getOffset(settings);\n if (bottom) {\n return (\n parseInt(bounds.bottom, 10) <\n (window.innerHeight || document.documentElement.clientHeight)\n );\n }\n return parseInt(bounds.top, 10) <= offset;\n };\n\n /**\n * Check if at the bottom of the viewport\n * @return {Boolean} If true, page is at the bottom of the viewport\n */\n var isAtBottom = function () {\n if (\n Math.ceil(window.innerHeight + window.pageYOffset) >=\n getDocumentHeight()\n )\n return true;\n return false;\n };\n\n /**\n * Check if the last item should be used (even if not at the top of the page)\n * @param {Object} item The last item\n * @param {Object} settings The settings for this instantiation\n * @return {Boolean} If true, use the last item\n */\n var useLastItem = function (item, settings) {\n if (isAtBottom() && isInView(item.content, settings, true)) return true;\n return false;\n };\n\n /**\n * Get the active content\n * @param {Array} contents The content areas\n * @param {Object} settings The settings for this instantiation\n * @return {Object} The content area and matching navigation link\n */\n var getActive = function (contents, settings) {\n var last = contents[contents.length - 1];\n if (useLastItem(last, settings)) return last;\n for (var i = contents.length - 1; i >= 0; i--) {\n if (isInView(contents[i].content, settings)) return contents[i];\n }\n };\n\n /**\n * Deactivate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var deactivateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested || !nav.parentNode) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Remove the active class\n li.classList.remove(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n deactivateNested(li, settings);\n };\n\n /**\n * Deactivate a nav and content area\n * @param {Object} items The nav item and content to deactivate\n * @param {Object} settings The settings for this instantiation\n */\n var deactivate = function (items, settings) {\n // Make sure there are items to deactivate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Remove the active class from the nav and content\n li.classList.remove(settings.navClass);\n items.content.classList.remove(settings.contentClass);\n\n // Deactivate any parent navs in a nested navigation\n deactivateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeDeactivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Activate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var activateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Add the active class\n li.classList.add(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n activateNested(li, settings);\n };\n\n /**\n * Activate a nav and content area\n * @param {Object} items The nav item and content to activate\n * @param {Object} settings The settings for this instantiation\n */\n var activate = function (items, settings) {\n // Make sure there are items to activate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Add the active class to the nav and content\n li.classList.add(settings.navClass);\n items.content.classList.add(settings.contentClass);\n\n // Activate any parent navs in a nested navigation\n activateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeActivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Create the Constructor object\n * @param {String} selector The selector to use for navigation items\n * @param {Object} options User options and settings\n */\n var Constructor = function (selector, options) {\n //\n // Variables\n //\n\n var publicAPIs = {};\n var navItems, contents, current, timeout, settings;\n\n //\n // Methods\n //\n\n /**\n * Set variables from DOM elements\n */\n publicAPIs.setup = function () {\n // Get all nav items\n navItems = document.querySelectorAll(selector);\n\n // Create contents array\n contents = [];\n\n // Loop through each item, get it's matching content, and push to the array\n Array.prototype.forEach.call(navItems, function (item) {\n // Get the content for the nav item\n var content = document.getElementById(\n decodeURIComponent(item.hash.substr(1)),\n );\n if (!content) return;\n\n // Push to the contents array\n contents.push({\n nav: item,\n content: content,\n });\n });\n\n // Sort contents by the order they appear in the DOM\n sortContents(contents);\n };\n\n /**\n * Detect which content is currently active\n */\n publicAPIs.detect = function () {\n // Get the active content\n var active = getActive(contents, settings);\n\n // if there's no active content, deactivate and bail\n if (!active) {\n if (current) {\n deactivate(current, settings);\n current = null;\n }\n return;\n }\n\n // If the active content is the one currently active, do nothing\n if (current && active.content === current.content) return;\n\n // Deactivate the current content and activate the new content\n deactivate(current, settings);\n activate(active, settings);\n\n // Update the currently active content\n current = active;\n };\n\n /**\n * Detect the active content on scroll\n * Debounced for performance\n */\n var scrollHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(publicAPIs.detect);\n };\n\n /**\n * Update content sorting on resize\n * Debounced for performance\n */\n var resizeHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(function () {\n sortContents(contents);\n publicAPIs.detect();\n });\n };\n\n /**\n * Destroy the current instantiation\n */\n publicAPIs.destroy = function () {\n // Undo DOM changes\n if (current) {\n deactivate(current, settings);\n }\n\n // Remove event listeners\n window.removeEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.removeEventListener(\"resize\", resizeHandler, false);\n }\n\n // Reset variables\n contents = null;\n navItems = null;\n current = null;\n timeout = null;\n settings = null;\n };\n\n /**\n * Initialize the current instantiation\n */\n var init = function () {\n // Merge user options into defaults\n settings = extend(defaults, options || {});\n\n // Setup variables based on the current DOM\n publicAPIs.setup();\n\n // Find the currently active content\n publicAPIs.detect();\n\n // Setup event listeners\n window.addEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.addEventListener(\"resize\", resizeHandler, false);\n }\n };\n\n //\n // Initialize and return the public APIs\n //\n\n init();\n return publicAPIs;\n };\n\n //\n // Return the Constructor\n //\n\n return Constructor;\n },\n);\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","import Gumshoe from \"./gumshoe-patched.js\";\n\n////////////////////////////////////////////////////////////////////////////////\n// Scroll Handling\n////////////////////////////////////////////////////////////////////////////////\nvar tocScroll = null;\nvar header = null;\nvar lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;\nconst GO_TO_TOP_OFFSET = 64;\n\nfunction scrollHandlerForHeader() {\n if (Math.floor(header.getBoundingClientRect().top) == 0) {\n header.classList.add(\"scrolled\");\n } else {\n header.classList.remove(\"scrolled\");\n }\n}\n\nfunction scrollHandlerForBackToTop(positionY) {\n if (positionY < GO_TO_TOP_OFFSET) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n } else {\n if (positionY < lastScrollTop) {\n document.documentElement.classList.add(\"show-back-to-top\");\n } else if (positionY > lastScrollTop) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n }\n }\n lastScrollTop = positionY;\n}\n\nfunction scrollHandlerForTOC(positionY) {\n if (tocScroll === null) {\n return;\n }\n\n // top of page.\n if (positionY == 0) {\n tocScroll.scrollTo(0, 0);\n } else if (\n // bottom of page.\n Math.ceil(positionY) >=\n Math.floor(document.documentElement.scrollHeight - window.innerHeight)\n ) {\n tocScroll.scrollTo(0, tocScroll.scrollHeight);\n } else {\n // somewhere in the middle.\n const current = document.querySelector(\".scroll-current\");\n if (current == null) {\n return;\n }\n\n // https://github.com/pypa/pip/issues/9159 This breaks scroll behaviours.\n // // scroll the currently \"active\" heading in toc, into view.\n // const rect = current.getBoundingClientRect();\n // if (0 > rect.top) {\n // current.scrollIntoView(true); // the argument is \"alignTop\"\n // } else if (rect.bottom > window.innerHeight) {\n // current.scrollIntoView(false);\n // }\n }\n}\n\nfunction scrollHandler(positionY) {\n scrollHandlerForHeader();\n scrollHandlerForBackToTop(positionY);\n scrollHandlerForTOC(positionY);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Theme Toggle\n////////////////////////////////////////////////////////////////////////////////\nfunction setTheme(mode) {\n if (mode !== \"light\" && mode !== \"dark\" && mode !== \"auto\") {\n console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);\n mode = \"auto\";\n }\n\n document.body.dataset.theme = mode;\n localStorage.setItem(\"theme\", mode);\n console.log(`Changed to ${mode} mode.`);\n}\n\nfunction cycleThemeOnce() {\n const currentTheme = localStorage.getItem(\"theme\") || \"auto\";\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) {\n // Auto (dark) -> Light -> Dark\n if (currentTheme === \"auto\") {\n setTheme(\"light\");\n } else if (currentTheme == \"light\") {\n setTheme(\"dark\");\n } else {\n setTheme(\"auto\");\n }\n } else {\n // Auto (light) -> Dark -> Light\n if (currentTheme === \"auto\") {\n setTheme(\"dark\");\n } else if (currentTheme == \"dark\") {\n setTheme(\"light\");\n } else {\n setTheme(\"auto\");\n }\n }\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Setup\n////////////////////////////////////////////////////////////////////////////////\nfunction setupScrollHandler() {\n // Taken from https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event\n let last_known_scroll_position = 0;\n let ticking = false;\n\n window.addEventListener(\"scroll\", function (e) {\n last_known_scroll_position = window.scrollY;\n\n if (!ticking) {\n window.requestAnimationFrame(function () {\n scrollHandler(last_known_scroll_position);\n ticking = false;\n });\n\n ticking = true;\n }\n });\n window.scroll();\n}\n\nfunction setupScrollSpy() {\n if (tocScroll === null) {\n return;\n }\n\n // Scrollspy -- highlight table on contents, based on scroll\n new Gumshoe(\".toc-tree a\", {\n reflow: true,\n recursive: true,\n navClass: \"scroll-current\",\n offset: () => {\n let rem = parseFloat(getComputedStyle(document.documentElement).fontSize);\n return header.getBoundingClientRect().height + 0.5 * rem + 1;\n },\n });\n}\n\nfunction setupTheme() {\n // Attach event handlers for toggling themes\n const buttons = document.getElementsByClassName(\"theme-toggle\");\n Array.from(buttons).forEach((btn) => {\n btn.addEventListener(\"click\", cycleThemeOnce);\n });\n}\n\nfunction setup() {\n setupTheme();\n setupScrollHandler();\n setupScrollSpy();\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Main entrypoint\n////////////////////////////////////////////////////////////////////////////////\nfunction main() {\n document.body.parentNode.classList.remove(\"no-js\");\n\n header = document.querySelector(\"header\");\n tocScroll = document.querySelector(\".toc-scroll\");\n\n setup();\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", main);\n"],"names":["root","g","window","this","defaults","navClass","contentClass","nested","nestedClass","offset","reflow","events","emitEvent","type","elem","detail","settings","event","CustomEvent","bubbles","cancelable","dispatchEvent","getOffsetTop","location","offsetParent","offsetTop","sortContents","contents","sort","item1","item2","content","isInView","bottom","bounds","getBoundingClientRect","parseFloat","getOffset","parseInt","innerHeight","document","documentElement","clientHeight","top","isAtBottom","Math","ceil","pageYOffset","max","body","scrollHeight","offsetHeight","getActive","last","length","item","useLastItem","i","deactivateNested","nav","parentNode","li","closest","classList","remove","deactivate","items","link","activateNested","add","selector","options","navItems","current","timeout","publicAPIs","querySelectorAll","Array","prototype","forEach","call","getElementById","decodeURIComponent","hash","substr","push","active","activate","scrollHandler","cancelAnimationFrame","requestAnimationFrame","detect","resizeHandler","destroy","removeEventListener","merged","arguments","obj","key","hasOwnProperty","extend","setup","addEventListener","factory","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","n","getter","__esModule","d","a","definition","o","Object","defineProperty","enumerable","get","globalThis","Function","e","prop","tocScroll","header","lastScrollTop","scrollTop","cycleThemeOnce","currentTheme","localStorage","getItem","mode","matchMedia","matches","console","error","dataset","theme","setItem","log","buttons","getElementsByClassName","from","btn","setupTheme","last_known_scroll_position","ticking","scrollY","positionY","floor","scrollHandlerForBackToTop","scrollTo","querySelector","scrollHandlerForTOC","scroll","setupScrollHandler","recursive","rem","getComputedStyle","fontSize","height"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"scripts/furo.js","mappings":"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","sources":["webpack:///./src/furo/assets/scripts/gumshoe-patched.js","webpack:///webpack/bootstrap","webpack:///webpack/runtime/compat get default export","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/global","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./src/furo/assets/scripts/furo.js"],"sourcesContent":["/*!\n * gumshoejs v5.1.2 (patched by @pradyunsg)\n * A simple, framework-agnostic scrollspy script.\n * (c) 2019 Chris Ferdinandi\n * MIT License\n * http://github.com/cferdinandi/gumshoe\n */\n\n(function (root, factory) {\n if (typeof define === \"function\" && define.amd) {\n define([], function () {\n return factory(root);\n });\n } else if (typeof exports === \"object\") {\n module.exports = factory(root);\n } else {\n root.Gumshoe = factory(root);\n }\n})(\n typeof global !== \"undefined\"\n ? global\n : typeof window !== \"undefined\"\n ? window\n : this,\n function (window) {\n \"use strict\";\n\n //\n // Defaults\n //\n\n var defaults = {\n // Active classes\n navClass: \"active\",\n contentClass: \"active\",\n\n // Nested navigation\n nested: false,\n nestedClass: \"active\",\n\n // Offset & reflow\n offset: 0,\n reflow: false,\n\n // Event support\n events: true,\n };\n\n //\n // Methods\n //\n\n /**\n * Merge two or more objects together.\n * @param {Object} objects The objects to merge together\n * @returns {Object} Merged values of defaults and options\n */\n var extend = function () {\n var merged = {};\n Array.prototype.forEach.call(arguments, function (obj) {\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) return;\n merged[key] = obj[key];\n }\n });\n return merged;\n };\n\n /**\n * Emit a custom event\n * @param {String} type The event type\n * @param {Node} elem The element to attach the event to\n * @param {Object} detail Any details to pass along with the event\n */\n var emitEvent = function (type, elem, detail) {\n // Make sure events are enabled\n if (!detail.settings.events) return;\n\n // Create a new event\n var event = new CustomEvent(type, {\n bubbles: true,\n cancelable: true,\n detail: detail,\n });\n\n // Dispatch the event\n elem.dispatchEvent(event);\n };\n\n /**\n * Get an element's distance from the top of the Document.\n * @param {Node} elem The element\n * @return {Number} Distance from the top in pixels\n */\n var getOffsetTop = function (elem) {\n var location = 0;\n if (elem.offsetParent) {\n while (elem) {\n location += elem.offsetTop;\n elem = elem.offsetParent;\n }\n }\n return location >= 0 ? location : 0;\n };\n\n /**\n * Sort content from first to last in the DOM\n * @param {Array} contents The content areas\n */\n var sortContents = function (contents) {\n if (contents) {\n contents.sort(function (item1, item2) {\n var offset1 = getOffsetTop(item1.content);\n var offset2 = getOffsetTop(item2.content);\n if (offset1 < offset2) return -1;\n return 1;\n });\n }\n };\n\n /**\n * Get the offset to use for calculating position\n * @param {Object} settings The settings for this instantiation\n * @return {Float} The number of pixels to offset the calculations\n */\n var getOffset = function (settings) {\n // if the offset is a function run it\n if (typeof settings.offset === \"function\") {\n return parseFloat(settings.offset());\n }\n\n // Otherwise, return it as-is\n return parseFloat(settings.offset);\n };\n\n /**\n * Get the document element's height\n * @private\n * @returns {Number}\n */\n var getDocumentHeight = function () {\n return Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight,\n document.body.offsetHeight,\n document.documentElement.offsetHeight,\n document.body.clientHeight,\n document.documentElement.clientHeight,\n );\n };\n\n /**\n * Determine if an element is in view\n * @param {Node} elem The element\n * @param {Object} settings The settings for this instantiation\n * @param {Boolean} bottom If true, check if element is above bottom of viewport instead\n * @return {Boolean} Returns true if element is in the viewport\n */\n var isInView = function (elem, settings, bottom) {\n var bounds = elem.getBoundingClientRect();\n var offset = getOffset(settings);\n if (bottom) {\n return (\n parseInt(bounds.bottom, 10) <\n (window.innerHeight || document.documentElement.clientHeight)\n );\n }\n return parseInt(bounds.top, 10) <= offset;\n };\n\n /**\n * Check if at the bottom of the viewport\n * @return {Boolean} If true, page is at the bottom of the viewport\n */\n var isAtBottom = function () {\n if (\n Math.ceil(window.innerHeight + window.pageYOffset) >=\n getDocumentHeight()\n )\n return true;\n return false;\n };\n\n /**\n * Check if the last item should be used (even if not at the top of the page)\n * @param {Object} item The last item\n * @param {Object} settings The settings for this instantiation\n * @return {Boolean} If true, use the last item\n */\n var useLastItem = function (item, settings) {\n if (isAtBottom() && isInView(item.content, settings, true)) return true;\n return false;\n };\n\n /**\n * Get the active content\n * @param {Array} contents The content areas\n * @param {Object} settings The settings for this instantiation\n * @return {Object} The content area and matching navigation link\n */\n var getActive = function (contents, settings) {\n var last = contents[contents.length - 1];\n if (useLastItem(last, settings)) return last;\n for (var i = contents.length - 1; i >= 0; i--) {\n if (isInView(contents[i].content, settings)) return contents[i];\n }\n };\n\n /**\n * Deactivate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var deactivateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested || !nav.parentNode) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Remove the active class\n li.classList.remove(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n deactivateNested(li, settings);\n };\n\n /**\n * Deactivate a nav and content area\n * @param {Object} items The nav item and content to deactivate\n * @param {Object} settings The settings for this instantiation\n */\n var deactivate = function (items, settings) {\n // Make sure there are items to deactivate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Remove the active class from the nav and content\n li.classList.remove(settings.navClass);\n items.content.classList.remove(settings.contentClass);\n\n // Deactivate any parent navs in a nested navigation\n deactivateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeDeactivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Activate parent navs in a nested navigation\n * @param {Node} nav The starting navigation element\n * @param {Object} settings The settings for this instantiation\n */\n var activateNested = function (nav, settings) {\n // If nesting isn't activated, bail\n if (!settings.nested) return;\n\n // Get the parent navigation\n var li = nav.parentNode.closest(\"li\");\n if (!li) return;\n\n // Add the active class\n li.classList.add(settings.nestedClass);\n\n // Apply recursively to any parent navigation elements\n activateNested(li, settings);\n };\n\n /**\n * Activate a nav and content area\n * @param {Object} items The nav item and content to activate\n * @param {Object} settings The settings for this instantiation\n */\n var activate = function (items, settings) {\n // Make sure there are items to activate\n if (!items) return;\n\n // Get the parent list item\n var li = items.nav.closest(\"li\");\n if (!li) return;\n\n // Add the active class to the nav and content\n li.classList.add(settings.navClass);\n items.content.classList.add(settings.contentClass);\n\n // Activate any parent navs in a nested navigation\n activateNested(li, settings);\n\n // Emit a custom event\n emitEvent(\"gumshoeActivate\", li, {\n link: items.nav,\n content: items.content,\n settings: settings,\n });\n };\n\n /**\n * Create the Constructor object\n * @param {String} selector The selector to use for navigation items\n * @param {Object} options User options and settings\n */\n var Constructor = function (selector, options) {\n //\n // Variables\n //\n\n var publicAPIs = {};\n var navItems, contents, current, timeout, settings;\n\n //\n // Methods\n //\n\n /**\n * Set variables from DOM elements\n */\n publicAPIs.setup = function () {\n // Get all nav items\n navItems = document.querySelectorAll(selector);\n\n // Create contents array\n contents = [];\n\n // Loop through each item, get it's matching content, and push to the array\n Array.prototype.forEach.call(navItems, function (item) {\n // Get the content for the nav item\n var content = document.getElementById(\n decodeURIComponent(item.hash.substr(1)),\n );\n if (!content) return;\n\n // Push to the contents array\n contents.push({\n nav: item,\n content: content,\n });\n });\n\n // Sort contents by the order they appear in the DOM\n sortContents(contents);\n };\n\n /**\n * Detect which content is currently active\n */\n publicAPIs.detect = function () {\n // Get the active content\n var active = getActive(contents, settings);\n\n // if there's no active content, deactivate and bail\n if (!active) {\n if (current) {\n deactivate(current, settings);\n current = null;\n }\n return;\n }\n\n // If the active content is the one currently active, do nothing\n if (current && active.content === current.content) return;\n\n // Deactivate the current content and activate the new content\n deactivate(current, settings);\n activate(active, settings);\n\n // Update the currently active content\n current = active;\n };\n\n /**\n * Detect the active content on scroll\n * Debounced for performance\n */\n var scrollHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(publicAPIs.detect);\n };\n\n /**\n * Update content sorting on resize\n * Debounced for performance\n */\n var resizeHandler = function (event) {\n // If there's a timer, cancel it\n if (timeout) {\n window.cancelAnimationFrame(timeout);\n }\n\n // Setup debounce callback\n timeout = window.requestAnimationFrame(function () {\n sortContents(contents);\n publicAPIs.detect();\n });\n };\n\n /**\n * Destroy the current instantiation\n */\n publicAPIs.destroy = function () {\n // Undo DOM changes\n if (current) {\n deactivate(current, settings);\n }\n\n // Remove event listeners\n window.removeEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.removeEventListener(\"resize\", resizeHandler, false);\n }\n\n // Reset variables\n contents = null;\n navItems = null;\n current = null;\n timeout = null;\n settings = null;\n };\n\n /**\n * Initialize the current instantiation\n */\n var init = function () {\n // Merge user options into defaults\n settings = extend(defaults, options || {});\n\n // Setup variables based on the current DOM\n publicAPIs.setup();\n\n // Find the currently active content\n publicAPIs.detect();\n\n // Setup event listeners\n window.addEventListener(\"scroll\", scrollHandler, false);\n if (settings.reflow) {\n window.addEventListener(\"resize\", resizeHandler, false);\n }\n };\n\n //\n // Initialize and return the public APIs\n //\n\n init();\n return publicAPIs;\n };\n\n //\n // Return the Constructor\n //\n\n return Constructor;\n },\n);\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","import Gumshoe from \"./gumshoe-patched.js\";\n\n////////////////////////////////////////////////////////////////////////////////\n// Scroll Handling\n////////////////////////////////////////////////////////////////////////////////\nvar tocScroll = null;\nvar header = null;\nvar lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;\nconst GO_TO_TOP_OFFSET = 64;\n\nfunction scrollHandlerForHeader() {\n if (Math.floor(header.getBoundingClientRect().top) == 0) {\n header.classList.add(\"scrolled\");\n } else {\n header.classList.remove(\"scrolled\");\n }\n}\n\nfunction scrollHandlerForBackToTop(positionY) {\n if (positionY < GO_TO_TOP_OFFSET) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n } else {\n if (positionY < lastScrollTop) {\n document.documentElement.classList.add(\"show-back-to-top\");\n } else if (positionY > lastScrollTop) {\n document.documentElement.classList.remove(\"show-back-to-top\");\n }\n }\n lastScrollTop = positionY;\n}\n\nfunction scrollHandlerForTOC(positionY) {\n if (tocScroll === null) {\n return;\n }\n\n // top of page.\n if (positionY == 0) {\n tocScroll.scrollTo(0, 0);\n } else if (\n // bottom of page.\n Math.ceil(positionY) >=\n Math.floor(document.documentElement.scrollHeight - window.innerHeight)\n ) {\n tocScroll.scrollTo(0, tocScroll.scrollHeight);\n } else {\n // somewhere in the middle.\n const current = document.querySelector(\".scroll-current\");\n if (current == null) {\n return;\n }\n\n // https://github.com/pypa/pip/issues/9159 This breaks scroll behaviours.\n // // scroll the currently \"active\" heading in toc, into view.\n // const rect = current.getBoundingClientRect();\n // if (0 > rect.top) {\n // current.scrollIntoView(true); // the argument is \"alignTop\"\n // } else if (rect.bottom > window.innerHeight) {\n // current.scrollIntoView(false);\n // }\n }\n}\n\nfunction scrollHandler(positionY) {\n scrollHandlerForHeader();\n scrollHandlerForBackToTop(positionY);\n scrollHandlerForTOC(positionY);\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Theme Toggle\n////////////////////////////////////////////////////////////////////////////////\nfunction setTheme(mode) {\n if (mode !== \"light\" && mode !== \"dark\" && mode !== \"auto\") {\n console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);\n mode = \"auto\";\n }\n\n document.body.dataset.theme = mode;\n localStorage.setItem(\"theme\", mode);\n console.log(`Changed to ${mode} mode.`);\n}\n\nfunction cycleThemeOnce() {\n const currentTheme = localStorage.getItem(\"theme\") || \"auto\";\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n if (prefersDark) {\n // Auto (dark) -> Light -> Dark\n if (currentTheme === \"auto\") {\n setTheme(\"light\");\n } else if (currentTheme == \"light\") {\n setTheme(\"dark\");\n } else {\n setTheme(\"auto\");\n }\n } else {\n // Auto (light) -> Dark -> Light\n if (currentTheme === \"auto\") {\n setTheme(\"dark\");\n } else if (currentTheme == \"dark\") {\n setTheme(\"light\");\n } else {\n setTheme(\"auto\");\n }\n }\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Setup\n////////////////////////////////////////////////////////////////////////////////\nfunction setupScrollHandler() {\n // Taken from https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event\n let last_known_scroll_position = 0;\n let ticking = false;\n\n window.addEventListener(\"scroll\", function (e) {\n last_known_scroll_position = window.scrollY;\n\n if (!ticking) {\n window.requestAnimationFrame(function () {\n scrollHandler(last_known_scroll_position);\n ticking = false;\n });\n\n ticking = true;\n }\n });\n window.scroll();\n}\n\nfunction setupScrollSpy() {\n if (tocScroll === null) {\n return;\n }\n\n // Scrollspy -- highlight table on contents, based on scroll\n new Gumshoe(\".toc-tree a\", {\n reflow: true,\n recursive: true,\n navClass: \"scroll-current\",\n offset: () => {\n let rem = parseFloat(getComputedStyle(document.documentElement).fontSize);\n return header.getBoundingClientRect().height + 0.5 * rem + 1;\n },\n });\n}\n\nfunction setupTheme() {\n // Attach event handlers for toggling themes\n const buttons = document.getElementsByClassName(\"theme-toggle\");\n Array.from(buttons).forEach((btn) => {\n btn.addEventListener(\"click\", cycleThemeOnce);\n });\n}\n\nfunction setup() {\n setupTheme();\n setupScrollHandler();\n setupScrollSpy();\n}\n\n////////////////////////////////////////////////////////////////////////////////\n// Main entrypoint\n////////////////////////////////////////////////////////////////////////////////\nfunction main() {\n document.body.parentNode.classList.remove(\"no-js\");\n\n header = document.querySelector(\"header\");\n tocScroll = document.querySelector(\".toc-scroll\");\n\n setup();\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", main);\n"],"names":["root","g","window","this","defaults","navClass","contentClass","nested","nestedClass","offset","reflow","events","emitEvent","type","elem","detail","settings","event","CustomEvent","bubbles","cancelable","dispatchEvent","getOffsetTop","location","offsetParent","offsetTop","sortContents","contents","sort","item1","item2","content","isInView","bottom","bounds","getBoundingClientRect","parseFloat","getOffset","parseInt","innerHeight","document","documentElement","clientHeight","top","isAtBottom","Math","ceil","pageYOffset","max","body","scrollHeight","offsetHeight","getActive","last","length","item","useLastItem","i","deactivateNested","nav","parentNode","li","closest","classList","remove","deactivate","items","link","activateNested","add","selector","options","navItems","current","timeout","publicAPIs","querySelectorAll","Array","prototype","forEach","call","getElementById","decodeURIComponent","hash","substr","push","active","activate","scrollHandler","cancelAnimationFrame","requestAnimationFrame","detect","resizeHandler","destroy","removeEventListener","merged","arguments","obj","key","hasOwnProperty","extend","setup","addEventListener","factory","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","n","getter","__esModule","d","a","definition","o","Object","defineProperty","enumerable","get","globalThis","Function","e","prop","tocScroll","header","lastScrollTop","scrollTop","GO_TO_TOP_OFFSET","cycleThemeOnce","currentTheme","localStorage","getItem","mode","matchMedia","matches","console","error","dataset","theme","setItem","log","buttons","getElementsByClassName","from","btn","setupTheme","last_known_scroll_position","ticking","scrollY","positionY","floor","scrollHandlerForBackToTop","scrollTo","querySelector","scrollHandlerForTOC","scroll","setupScrollHandler","recursive","rem","getComputedStyle","fontSize","height"],"sourceRoot":""} \ No newline at end of file diff --git a/docs/_static/styles/furo.css b/docs/_static/styles/furo.css index b30989d..3d29a21 100644 --- a/docs/_static/styles/furo.css +++ b/docs/_static/styles/furo.css @@ -1,2 +1,2 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{clip:rect(0,0,0,0)!important;border:0!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,');--icon-pencil:url('data:image/svg+xml;charset=utf-8,');--icon-abstract:url('data:image/svg+xml;charset=utf-8,');--icon-info:url('data:image/svg+xml;charset=utf-8,');--icon-flame:url('data:image/svg+xml;charset=utf-8,');--icon-question:url('data:image/svg+xml;charset=utf-8,');--icon-warning:url('data:image/svg+xml;charset=utf-8,');--icon-failure:url('data:image/svg+xml;charset=utf-8,');--icon-spark:url('data:image/svg+xml;charset=utf-8,');--color-admonition-title--caution:#ff9100;--color-admonition-title-background--caution:rgba(255,145,0,.2);--color-admonition-title--warning:#ff9100;--color-admonition-title-background--warning:rgba(255,145,0,.2);--color-admonition-title--danger:#ff5252;--color-admonition-title-background--danger:rgba(255,82,82,.2);--color-admonition-title--attention:#ff5252;--color-admonition-title-background--attention:rgba(255,82,82,.2);--color-admonition-title--error:#ff5252;--color-admonition-title-background--error:rgba(255,82,82,.2);--color-admonition-title--hint:#00c852;--color-admonition-title-background--hint:rgba(0,200,82,.2);--color-admonition-title--tip:#00c852;--color-admonition-title-background--tip:rgba(0,200,82,.2);--color-admonition-title--important:#00bfa5;--color-admonition-title-background--important:rgba(0,191,165,.2);--color-admonition-title--note:#00b0ff;--color-admonition-title-background--note:rgba(0,176,255,.2);--color-admonition-title--seealso:#448aff;--color-admonition-title-background--seealso:rgba(68,138,255,.2);--color-admonition-title--admonition-todo:grey;--color-admonition-title-background--admonition-todo:hsla(0,0%,50%,.2);--color-admonition-title:#651fff;--color-admonition-title-background:rgba(101,31,255,.2);--icon-admonition-default:var(--icon-abstract);--color-topic-title:#14b8a6;--color-topic-title-background:rgba(20,184,166,.2);--icon-topic-default:var(--icon-pencil);--color-problematic:#b30000;--color-foreground-primary:#000;--color-foreground-secondary:#5a5c63;--color-foreground-muted:#646776;--color-foreground-border:#878787;--color-background-primary:#fff;--color-background-secondary:#f8f9fb;--color-background-hover:#efeff4;--color-background-hover--transparent:#efeff400;--color-background-border:#eeebee;--color-background-item:#ccc;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2962ff;--color-brand-content:#2a5adf;--color-api-background:var(--color-background-hover--transparent);--color-api-background-hover:var(--color-background-hover);--color-api-overall:var(--color-foreground-secondary);--color-api-name:var(--color-problematic);--color-api-pre-name:var(--color-problematic);--color-api-paren:var(--color-foreground-secondary);--color-api-keyword:var(--color-foreground-primary);--color-highlight-on-target:#ffc;--color-inline-code-background:var(--color-background-secondary);--color-highlighted-background:#def;--color-highlighted-text:var(--color-foreground-primary);--color-guilabel-background:#ddeeff80;--color-guilabel-border:#bedaf580;--color-guilabel-text:var(--color-foreground-primary);--color-admonition-background:transparent;--color-table-header-background:var(--color-background-secondary);--color-table-border:var(--color-background-border);--color-card-border:var(--color-background-secondary);--color-card-background:transparent;--color-card-marginals-background:var(--color-background-secondary);--color-header-background:var(--color-background-primary);--color-header-border:var(--color-background-border);--color-header-text:var(--color-foreground-primary);--color-sidebar-background:var(--color-background-secondary);--color-sidebar-background-border:var(--color-background-border);--color-sidebar-brand-text:var(--color-foreground-primary);--color-sidebar-caption-text:var(--color-foreground-muted);--color-sidebar-link-text:var(--color-foreground-secondary);--color-sidebar-link-text--top-level:var(--color-brand-primary);--color-sidebar-item-background:var(--color-sidebar-background);--color-sidebar-item-background--current:var( --color-sidebar-item-background );--color-sidebar-item-background--hover:linear-gradient(90deg,var(--color-background-hover--transparent) 0%,var(--color-background-hover) var(--sidebar-item-spacing-horizontal),var(--color-background-hover) 100%);--color-sidebar-item-expander-background:transparent;--color-sidebar-item-expander-background--hover:var( --color-background-hover );--color-sidebar-search-text:var(--color-foreground-primary);--color-sidebar-search-background:var(--color-background-secondary);--color-sidebar-search-background--focus:var(--color-background-primary);--color-sidebar-search-border:var(--color-background-border);--color-sidebar-search-icon:var(--color-foreground-muted);--color-toc-background:var(--color-background-primary);--color-toc-title-text:var(--color-foreground-muted);--color-toc-item-text:var(--color-foreground-secondary);--color-toc-item-text--hover:var(--color-foreground-primary);--color-toc-item-text--active:var(--color-brand-primary);--color-content-foreground:var(--color-foreground-primary);--color-content-background:transparent;--color-link:var(--color-brand-content);--color-link--hover:var(--color-brand-content);--color-link-underline:var(--color-background-border);--color-link-underline--hover:var(--color-foreground-border)}.only-light{display:block!important}html body .only-dark{display:none!important}@media not print{body[data-theme=dark]{--color-problematic:#ee5151;--color-foreground-primary:#ffffffcc;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2b8cee;--color-brand-content:#368ce2;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body[data-theme=dark] .only-light{display:none!important}body[data-theme=dark] .only-dark{display:block!important}@media(prefers-color-scheme:dark){body:not([data-theme=light]){--color-problematic:#ee5151;--color-foreground-primary:#ffffffcc;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2b8cee;--color-brand-content:#368ce2;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body:not([data-theme=light]) .only-light{display:none!important}body:not([data-theme=light]) .only-dark{display:block!important}}}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto,body[data-theme=dark] .theme-toggle svg.theme-icon-when-dark,body[data-theme=light] .theme-toggle svg.theme-icon-when-light{display:block}body{font-family:var(--font-stack)}code,kbd,pre,samp{font-family:var(--font-stack--monospace)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article{line-height:1.5}h1,h2,h3,h4,h5,h6{border-radius:.5rem;font-weight:700;line-height:1.25;margin:.5rem -.5rem;padding-left:.5rem;padding-right:.5rem}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p{margin-top:0}h1{font-size:2.5em;margin-bottom:1rem}h1,h2{margin-top:1.75rem}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1.125em}h6{font-size:1em}small{font-size:80%;opacity:75%}p{margin-bottom:.75rem;margin-top:.5rem}hr.docutils{background-color:var(--color-background-border);border:0;height:1px;margin:2rem 0;padding:0}.centered{text-align:center}a{color:var(--color-link);text-decoration:underline;-webkit-text-decoration-color:var(--color-link-underline);text-decoration-color:var(--color-link-underline)}a:hover{color:var(--color-link--hover);-webkit-text-decoration-color:var(--color-link-underline--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link{color:inherit}a.muted-link:hover{color:var(--color-link);-webkit-text-decoration-color:var(--color-link-underline--hover);text-decoration-color:var(--color-link-underline--hover)}html{overflow-x:hidden;overflow-y:scroll;scroll-behavior:smooth}.sidebar-scroll,.toc-scroll,article[role=main] *{scrollbar-color:var(--color-foreground-border) transparent;scrollbar-width:thin}.sidebar-scroll::-webkit-scrollbar,.toc-scroll::-webkit-scrollbar,article[role=main] ::-webkit-scrollbar{height:.25rem;width:.25rem}.sidebar-scroll::-webkit-scrollbar-thumb,.toc-scroll::-webkit-scrollbar-thumb,article[role=main] ::-webkit-scrollbar-thumb{background-color:var(--color-foreground-border);border-radius:.125rem}body,html{background:var(--color-background-primary);color:var(--color-foreground-primary);height:100%}article{background:var(--color-content-background);color:var(--color-content-foreground)}.page{display:flex;min-height:100%}.mobile-header{background-color:var(--color-header-background);border-bottom:1px solid var(--color-header-border);color:var(--color-header-text);display:none;height:var(--header-height);width:100%;z-index:10}.mobile-header.scrolled{border-bottom:none;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2)}.mobile-header .header-center a{color:var(--color-header-text);text-decoration:none}.main{display:flex;flex:1}.sidebar-drawer{background:var(--color-sidebar-background);border-right:1px solid var(--color-sidebar-background-border);box-sizing:border-box;display:flex;justify-content:flex-end;min-width:15em;width:calc(50% - 26em)}.sidebar-container,.toc-drawer{box-sizing:border-box;width:15em}.toc-drawer{background:var(--color-toc-background);padding-right:1rem}.sidebar-sticky,.toc-sticky{display:flex;flex-direction:column;height:min(100%,100vh);height:100vh;position:-webkit-sticky;position:sticky;top:0}.sidebar-scroll,.toc-scroll{flex-grow:1;flex-shrink:1;overflow:auto;scroll-behavior:smooth}.content{display:flex;flex-direction:column;justify-content:space-between;padding:0 3em;width:46em}.icon{display:inline-block;height:1rem;width:1rem}.icon svg{height:100%;width:100%}.announcement{align-items:center;background-color:var(--color-announcement-background);color:var(--color-announcement-text);display:flex;height:var(--header-height);overflow-x:auto}.announcement+.page{min-height:calc(100% - var(--header-height))}.announcement-content{box-sizing:border-box;min-width:100%;padding:.5rem;text-align:center;white-space:nowrap}.announcement-content a{color:var(--color-announcement-text);-webkit-text-decoration-color:var(--color-announcement-text);text-decoration-color:var(--color-announcement-text)}.announcement-content a:hover{color:var(--color-announcement-text);-webkit-text-decoration-color:var(--color-link--hover);text-decoration-color:var(--color-link--hover)}.no-js .theme-toggle-container{display:none}.theme-toggle-container{vertical-align:middle}.theme-toggle{background:transparent;border:none;cursor:pointer;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1rem;vertical-align:middle;width:1rem}.theme-toggle-header{float:left;padding:1rem .5rem}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1rem;width:1rem}.nav-overlay-icon,.toc-header-icon{align-items:center;justify-content:center}.toc-content-icon{height:1.5rem;width:1.5rem}.content-icon-container{display:flex;float:right;gap:.5rem;margin-bottom:1rem;margin-left:1rem;margin-top:1.5rem}.content-icon-container .edit-this-page svg{color:inherit;height:1rem;width:1rem}.sidebar-toggle{display:none;position:absolute}.sidebar-toggle[name=__toc]{left:20px}.sidebar-toggle:checked{left:40px}.overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0ms,height 0ms,opacity .25s ease-out;width:0}.sidebar-overlay{z-index:20}.toc-overlay{z-index:40}.sidebar-drawer{transition:left .25s ease-in-out;z-index:30}.toc-drawer{transition:right .25s ease-in-out;z-index:50}#__navigation:checked~.sidebar-overlay{height:100%;opacity:1;width:100%}#__navigation:checked~.page .sidebar-drawer{left:0;top:0}#__toc:checked~.toc-overlay{height:100%;opacity:1;width:100%}#__toc:checked~.page .toc-drawer{right:0;top:0}.back-to-top{background:var(--color-background-primary);border-radius:1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 1px 0 hsla(220,9%,46%,.502);display:none;font-size:.8125rem;left:0;margin-left:50%;padding:.5rem .75rem .5rem .5rem;position:fixed;text-decoration:none;top:1rem;transform:translateX(-50%);z-index:10}.back-to-top svg{fill:currentColor;display:inline-block;height:1rem;width:1rem}.back-to-top span{margin-left:.25rem}.show-back-to-top .back-to-top{align-items:center;display:flex}@media(min-width:97em){html{font-size:110%}}@media(max-width:82em){.toc-content-icon{display:flex}.toc-drawer{border-left:1px solid var(--color-background-muted);height:100vh;position:fixed;right:-15em;top:0}.toc-tree{border-left:none;font-size:var(--toc-font-size--mobile)}.sidebar-drawer{width:calc(50% - 18.5em)}}@media(max-width:67em){.nav-overlay-icon{display:flex}.sidebar-drawer{height:100vh;left:-15em;position:fixed;top:0;width:15em}.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.theme-toggle-header{display:block}.mobile-header{align-items:center;display:flex;justify-content:space-between;position:-webkit-sticky;position:sticky;top:0}.mobile-header .header-left,.mobile-header .header-right{display:flex;height:var(--header-height);padding:0 var(--header-padding)}.mobile-header .header-left label,.mobile-header .header-right label{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.nav-overlay-icon .icon,.theme-toggle svg{height:1.25rem;width:1.25rem}:target{scroll-margin-top:var(--header-height)}.back-to-top{top:calc(var(--header-height) + .5rem)}.page{flex-direction:column;justify-content:center}.content{margin-left:auto;margin-right:auto}}@media(max-width:52em){.content{overflow-x:auto;width:100%}}@media(max-width:46em){.content{padding:0 1em}article aside.sidebar{float:none;margin:1rem 0;width:100%}}.admonition,.topic{background:var(--color-admonition-background);border-radius:.2rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);font-size:var(--admonition-font-size);margin:1rem auto;overflow:hidden;padding:0 .5rem .5rem;page-break-inside:avoid}.admonition>:nth-child(2),.topic>:nth-child(2){margin-top:0}.admonition>:last-child,.topic>:last-child{margin-bottom:0}p.admonition-title,p.topic-title{font-size:var(--admonition-title-font-size);font-weight:500;line-height:1.3;margin:0 -.5rem .5rem;padding:.4rem .5rem .4rem 2rem;position:relative}p.admonition-title:before,p.topic-title:before{content:"";height:1rem;left:.5rem;position:absolute;width:1rem}p.admonition-title{background-color:var(--color-admonition-title-background)}p.admonition-title:before{background-color:var(--color-admonition-title);-webkit-mask-image:var(--icon-admonition-default);mask-image:var(--icon-admonition-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}p.topic-title{background-color:var(--color-topic-title-background)}p.topic-title:before{background-color:var(--color-topic-title);-webkit-mask-image:var(--icon-topic-default);mask-image:var(--icon-topic-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.admonition{border-left:.2rem solid var(--color-admonition-title)}.admonition.caution{border-left-color:var(--color-admonition-title--caution)}.admonition.caution>.admonition-title{background-color:var(--color-admonition-title-background--caution)}.admonition.caution>.admonition-title:before{background-color:var(--color-admonition-title--caution);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.warning{border-left-color:var(--color-admonition-title--warning)}.admonition.warning>.admonition-title{background-color:var(--color-admonition-title-background--warning)}.admonition.warning>.admonition-title:before{background-color:var(--color-admonition-title--warning);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.danger{border-left-color:var(--color-admonition-title--danger)}.admonition.danger>.admonition-title{background-color:var(--color-admonition-title-background--danger)}.admonition.danger>.admonition-title:before{background-color:var(--color-admonition-title--danger);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.attention{border-left-color:var(--color-admonition-title--attention)}.admonition.attention>.admonition-title{background-color:var(--color-admonition-title-background--attention)}.admonition.attention>.admonition-title:before{background-color:var(--color-admonition-title--attention);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.error{border-left-color:var(--color-admonition-title--error)}.admonition.error>.admonition-title{background-color:var(--color-admonition-title-background--error)}.admonition.error>.admonition-title:before{background-color:var(--color-admonition-title--error);-webkit-mask-image:var(--icon-failure);mask-image:var(--icon-failure)}.admonition.hint{border-left-color:var(--color-admonition-title--hint)}.admonition.hint>.admonition-title{background-color:var(--color-admonition-title-background--hint)}.admonition.hint>.admonition-title:before{background-color:var(--color-admonition-title--hint);-webkit-mask-image:var(--icon-question);mask-image:var(--icon-question)}.admonition.tip{border-left-color:var(--color-admonition-title--tip)}.admonition.tip>.admonition-title{background-color:var(--color-admonition-title-background--tip)}.admonition.tip>.admonition-title:before{background-color:var(--color-admonition-title--tip);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.important{border-left-color:var(--color-admonition-title--important)}.admonition.important>.admonition-title{background-color:var(--color-admonition-title-background--important)}.admonition.important>.admonition-title:before{background-color:var(--color-admonition-title--important);-webkit-mask-image:var(--icon-flame);mask-image:var(--icon-flame)}.admonition.note{border-left-color:var(--color-admonition-title--note)}.admonition.note>.admonition-title{background-color:var(--color-admonition-title-background--note)}.admonition.note>.admonition-title:before{background-color:var(--color-admonition-title--note);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition.seealso{border-left-color:var(--color-admonition-title--seealso)}.admonition.seealso>.admonition-title{background-color:var(--color-admonition-title-background--seealso)}.admonition.seealso>.admonition-title:before{background-color:var(--color-admonition-title--seealso);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.admonition-todo{border-left-color:var(--color-admonition-title--admonition-todo)}.admonition.admonition-todo>.admonition-title{background-color:var(--color-admonition-title-background--admonition-todo)}.admonition.admonition-todo>.admonition-title:before{background-color:var(--color-admonition-title--admonition-todo);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition-todo>.admonition-title{text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd{margin-left:2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:first-child{margin-top:.125rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list,dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:last-child{margin-bottom:.75rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list>dt{font-size:var(--font-size--small);text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd:empty{margin-bottom:.5rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul{margin-left:-1.2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p:nth-child(2){margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p+p:last-child:empty{margin-bottom:0;margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{color:var(--color-api-overall)}.sig:not(.sig-inline){background:var(--color-api-background);border-radius:.25rem;font-family:var(--font-stack--monospace);font-size:var(--api-font-size);font-weight:700;margin-left:-.25rem;margin-right:-.25rem;padding:.25rem .5rem .25rem 3em;text-indent:-2.5em;transition:background .1s ease-out}.sig:not(.sig-inline):hover{background:var(--color-api-background-hover)}.sig:not(.sig-inline) a.reference .viewcode-link{font-weight:400;width:3.5rem}.sig:not(.sig-inline) span.pre{overflow-wrap:anywhere}em.property{font-style:normal}em.property:first-child{color:var(--color-api-keyword)}.sig-name{color:var(--color-api-name)}.sig-prename{color:var(--color-api-pre-name);font-weight:400}.sig-paren{color:var(--color-api-paren)}.sig-param{font-style:normal}.versionmodified{font-style:italic}div.deprecated p,div.versionadded p,div.versionchanged p{margin-bottom:.125rem;margin-top:.125rem}.viewcode-back,.viewcode-link{float:right;text-align:right}.line-block{margin-bottom:.75rem;margin-top:.5rem}.line-block .line-block{margin-bottom:0;margin-top:0;padding-left:1rem}.code-block-caption,article p.caption,table>caption{font-size:var(--font-size--small);text-align:center}.toctree-wrapper.compound .caption,.toctree-wrapper.compound :not(.caption)>.caption-text{font-size:var(--font-size--small);margin-bottom:0;text-align:initial;text-transform:uppercase}.toctree-wrapper.compound>ul{margin-bottom:0;margin-top:0}.sig-inline,code.literal{background:var(--color-inline-code-background);border-radius:.2em;font-size:var(--font-size--small--2);overflow-wrap:break-word;padding:.1em .2em}p .sig-inline,p code.literal{border:1px solid var(--color-background-border)}.sig-inline{font-family:var(--font-stack--monospace)}div[class*=" highlight-"],div[class^=highlight-]{display:flex;margin:1em 0}div[class*=" highlight-"] .table-wrapper,div[class^=highlight-] .table-wrapper,pre{margin:0;padding:0}pre{overflow:auto}article[role=main] .highlight pre{line-height:1.5}.highlight pre,pre.literal-block{font-size:var(--code-font-size);padding:.625rem .875rem}pre.literal-block{background-color:var(--color-code-background);border-radius:.2rem;color:var(--color-code-foreground);margin-bottom:1rem;margin-top:1rem}.highlight{border-radius:.2rem;width:100%}.highlight .gp,.highlight span.linenos{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlight .hll{display:block;margin-left:-.875rem;margin-right:-.875rem;padding-left:.875rem;padding-right:.875rem}.code-block-caption{background-color:var(--color-code-background);border-bottom:1px solid;border-radius:.25rem;border-bottom-left-radius:0;border-bottom-right-radius:0;border-color:var(--color-background-border);color:var(--color-code-foreground);display:flex;font-weight:300;padding:.625rem .875rem}.code-block-caption+div[class]{margin-top:0}.code-block-caption+div[class] pre{border-top-left-radius:0;border-top-right-radius:0}.highlighttable{display:block;width:100%}.highlighttable tbody{display:block}.highlighttable tr{display:flex}.highlighttable td.linenos{background-color:var(--color-code-background);border-bottom-left-radius:.2rem;border-top-left-radius:.2rem;color:var(--color-code-foreground);padding:.625rem 0 .625rem .875rem}.highlighttable .linenodiv{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;font-size:var(--code-font-size);padding-right:.875rem}.highlighttable td.code{display:block;flex:1;overflow:hidden;padding:0}.highlighttable td.code .highlight{border-bottom-left-radius:0;border-top-left-radius:0}.highlight span.linenos{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;display:inline-block;margin-right:.875rem;padding-left:0;padding-right:.875rem}.footnote-reference{font-size:var(--font-size--small--4);vertical-align:super}dl.footnote.brackets{color:var(--color-foreground-secondary);display:grid;font-size:var(--font-size--small);grid-template-columns:-webkit-max-content auto;grid-template-columns:max-content auto}dl.footnote.brackets dt{margin:0}dl.footnote.brackets dt>.fn-backref{margin-left:.25rem}dl.footnote.brackets dt:after{content:":"}dl.footnote.brackets dt .brackets:before{content:"["}dl.footnote.brackets dt .brackets:after{content:"]"}dl.footnote.brackets dd{margin:0;padding:0 1rem}aside.footnote{color:var(--color-foreground-secondary);font-size:var(--font-size--small)}aside.footnote>span,div.citation>span{float:left;font-weight:500;padding-right:.25rem}aside.footnote>p,div.citation>p{margin-left:2rem}img{box-sizing:border-box;height:auto;max-width:100%}article .figure,article figure{border-radius:.2rem;margin:0}article .figure :last-child,article figure :last-child{margin-bottom:0}article .align-left{clear:left;float:left;margin:0 1rem 1rem}article .align-right{clear:right;float:right;margin:0 1rem 1rem}article .align-center,article .align-default{display:block;margin-left:auto;margin-right:auto;text-align:center}article table.align-default{display:table;text-align:initial}.domainindex-jumpbox,.genindex-jumpbox{border-bottom:1px solid var(--color-background-border);border-top:1px solid var(--color-background-border);padding:.25rem}.domainindex-section h2,.genindex-section h2{margin-bottom:.5rem;margin-top:.75rem}.domainindex-section ul,.genindex-section ul{margin-bottom:0;margin-top:0}ol,ul{margin-bottom:1rem;margin-top:1rem;padding-left:1.2rem}ol li>p:first-child,ul li>p:first-child{margin-bottom:.25rem;margin-top:.25rem}ol li>p:last-child,ul li>p:last-child{margin-top:.25rem}ol li>ol,ol li>ul,ul li>ol,ul li>ul{margin-bottom:.5rem;margin-top:.5rem}ol.arabic{list-style:decimal}ol.loweralpha{list-style:lower-alpha}ol.upperalpha{list-style:upper-alpha}ol.lowerroman{list-style:lower-roman}ol.upperroman{list-style:upper-roman}.simple li>ol,.simple li>ul,.toctree-wrapper li>ol,.toctree-wrapper li>ul{margin-bottom:0;margin-top:0}.field-list dt,.option-list dt,dl.footnote dt,dl.glossary dt,dl.simple dt,dl:not([class]) dt{font-weight:500;margin-top:.25rem}.field-list dt+dt,.option-list dt+dt,dl.footnote dt+dt,dl.glossary dt+dt,dl.simple dt+dt,dl:not([class]) dt+dt{margin-top:0}.field-list dt .classifier:before,.option-list dt .classifier:before,dl.footnote dt .classifier:before,dl.glossary dt .classifier:before,dl.simple dt .classifier:before,dl:not([class]) dt .classifier:before{content:":";margin-left:.2rem;margin-right:.2rem}.field-list dd ul,.field-list dd>p:first-child,.option-list dd ul,.option-list dd>p:first-child,dl.footnote dd ul,dl.footnote dd>p:first-child,dl.glossary dd ul,dl.glossary dd>p:first-child,dl.simple dd ul,dl.simple dd>p:first-child,dl:not([class]) dd ul,dl:not([class]) dd>p:first-child{margin-top:.125rem}.field-list dd ul,.option-list dd ul,dl.footnote dd ul,dl.glossary dd ul,dl.simple dd ul,dl:not([class]) dd ul{margin-bottom:.125rem}.math-wrapper{overflow-x:auto;width:100%}div.math{position:relative;text-align:center}div.math .headerlink,div.math:focus .headerlink{display:none}div.math:hover .headerlink{display:inline-block}div.math span.eqno{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);z-index:1}abbr[title]{cursor:help}.problematic{color:var(--color-problematic)}kbd:not(.compound){background-color:var(--color-background-secondary);border:1px solid var(--color-foreground-border);border-radius:.2rem;box-shadow:0 .0625rem 0 rgba(0,0,0,.2),inset 0 0 0 .125rem var(--color-background-primary);color:var(--color-foreground-primary);display:inline-block;font-size:var(--font-size--small--3);margin:0 .2rem;padding:0 .2rem;vertical-align:text-bottom}blockquote{background:var(--color-background-secondary);border-left:4px solid var(--color-background-border);margin-left:0;margin-right:0;padding:.5rem 1rem}blockquote .attribution{font-weight:600;text-align:right}blockquote.highlights,blockquote.pull-quote{font-size:1.25em}blockquote.epigraph,blockquote.pull-quote{border-left-width:0;border-radius:.5rem}blockquote.highlights{background:transparent;border-left-width:0}p .reference img{vertical-align:middle}p.rubric{font-size:1.125em;font-weight:700;line-height:1.25}dd p.rubric{font-size:var(--font-size--small);font-weight:inherit;line-height:inherit;text-transform:uppercase}article .sidebar{background-color:var(--color-background-secondary);border:1px solid var(--color-background-border);border-radius:.2rem;clear:right;float:right;margin-left:1rem;margin-right:0;width:30%}article .sidebar>*{padding-left:1rem;padding-right:1rem}article .sidebar>ol,article .sidebar>ul{padding-left:2.2rem}article .sidebar .sidebar-title{border-bottom:1px solid var(--color-background-border);font-weight:500;margin:0;padding:.5rem 1rem}.table-wrapper{margin-bottom:.5rem;margin-top:1rem;overflow-x:auto;padding:.2rem .2rem .75rem;width:100%}table.docutils{border-collapse:collapse;border-radius:.2rem;border-spacing:0;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)}table.docutils th{background:var(--color-table-header-background)}table.docutils td,table.docutils th{border-bottom:1px solid var(--color-table-border);border-left:1px solid var(--color-table-border);border-right:1px solid var(--color-table-border);padding:0 .25rem}table.docutils td p,table.docutils th p{margin:.25rem}table.docutils td:first-child,table.docutils th:first-child{border-left:none}table.docutils td:last-child,table.docutils th:last-child{border-right:none}table.docutils td.text-left,table.docutils th.text-left{text-align:left}table.docutils td.text-right,table.docutils th.text-right{text-align:right}table.docutils td.text-center,table.docutils th.text-center{text-align:center}:target{scroll-margin-top:.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(.8rem + var(--header-height))}}.headerlink{font-weight:100;-webkit-user-select:none;-moz-user-select:none;user-select:none}.code-block-caption>.headerlink,dl dt>.headerlink,figcaption p>.headerlink,h1>.headerlink,h2>.headerlink,h3>.headerlink,h4>.headerlink,h5>.headerlink,h6>.headerlink,p.caption>.headerlink,table>caption>.headerlink{margin-left:.5rem;visibility:hidden}.code-block-caption:hover>.headerlink,dl dt:hover>.headerlink,figcaption p:hover>.headerlink,h1:hover>.headerlink,h2:hover>.headerlink,h3:hover>.headerlink,h4:hover>.headerlink,h5:hover>.headerlink,h6:hover>.headerlink,p.caption:hover>.headerlink,table>caption:hover>.headerlink{visibility:visible}.code-block-caption>.toc-backref,dl dt>.toc-backref,figcaption p>.toc-backref,h1>.toc-backref,h2>.toc-backref,h3>.toc-backref,h4>.toc-backref,h5>.toc-backref,h6>.toc-backref,p.caption>.toc-backref,table>caption>.toc-backref{color:inherit;-webkit-text-decoration-line:none;text-decoration-line:none}figure:hover>figcaption>p>.headerlink,table:hover>caption>.headerlink{visibility:visible}:target>h1:first-of-type,:target>h2:first-of-type,:target>h3:first-of-type,:target>h4:first-of-type,:target>h5:first-of-type,:target>h6:first-of-type,span:target~h1:first-of-type,span:target~h2:first-of-type,span:target~h3:first-of-type,span:target~h4:first-of-type,span:target~h5:first-of-type,span:target~h6:first-of-type{background-color:var(--color-highlight-on-target)}:target>h1:first-of-type code.literal,:target>h2:first-of-type code.literal,:target>h3:first-of-type code.literal,:target>h4:first-of-type code.literal,:target>h5:first-of-type code.literal,:target>h6:first-of-type code.literal,span:target~h1:first-of-type code.literal,span:target~h2:first-of-type code.literal,span:target~h3:first-of-type code.literal,span:target~h4:first-of-type code.literal,span:target~h5:first-of-type code.literal,span:target~h6:first-of-type code.literal{background-color:transparent}.literal-block-wrapper:target .code-block-caption,.this-will-duplicate-information-and-it-is-still-useful-here li :target,figure:target,table:target>caption{background-color:var(--color-highlight-on-target)}dt:target{background-color:var(--color-highlight-on-target)!important}.footnote-reference:target,.footnote>dt:target+dd{background-color:var(--color-highlight-on-target)}.guilabel{background-color:var(--color-guilabel-background);border:1px solid var(--color-guilabel-border);border-radius:.5em;color:var(--color-guilabel-text);font-size:.9em;padding:0 .3em}footer{display:flex;flex-direction:column;font-size:var(--font-size--small);margin-top:2rem}.bottom-of-page{align-items:center;border-top:1px solid var(--color-background-border);color:var(--color-foreground-secondary);display:flex;justify-content:space-between;line-height:1.5;margin-top:1rem;padding-bottom:1rem;padding-top:1rem}@media(max-width:46em){.bottom-of-page{flex-direction:column-reverse;gap:.25rem;text-align:center}}.bottom-of-page .left-details{font-size:var(--font-size--small)}.bottom-of-page .right-details{display:flex;flex-direction:column;gap:.25rem;text-align:right}.bottom-of-page .icons{display:flex;font-size:1rem;gap:.25rem;justify-content:flex-end}.bottom-of-page .icons a{text-decoration:none}.bottom-of-page .icons img,.bottom-of-page .icons svg{font-size:1.125rem;height:1em;width:1em}.related-pages a{align-items:center;display:flex;text-decoration:none}.related-pages a:hover .page-info .title{color:var(--color-link);text-decoration:underline;-webkit-text-decoration-color:var(--color-link-underline);text-decoration-color:var(--color-link-underline)}.related-pages a svg.furo-related-icon,.related-pages a svg.furo-related-icon>use{color:var(--color-foreground-border);flex-shrink:0;height:.75rem;margin:0 .5rem;width:.75rem}.related-pages a.next-page{clear:right;float:right;max-width:50%;text-align:right}.related-pages a.prev-page{clear:left;float:left;max-width:50%}.related-pages a.prev-page svg{transform:rotate(180deg)}.page-info{display:flex;flex-direction:column;overflow-wrap:anywhere}.next-page .page-info{align-items:flex-end}.page-info .context{align-items:center;color:var(--color-foreground-muted);display:flex;font-size:var(--font-size--small);padding-bottom:.1rem;text-decoration:none}ul.search{list-style:none;padding-left:0}ul.search li{border-bottom:1px solid var(--color-background-border);padding:1rem 0}[role=main] .highlighted{background-color:var(--color-highlighted-background);color:var(--color-highlighted-text)}.sidebar-brand{display:flex;flex-direction:column;flex-shrink:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none}.sidebar-brand-text{color:var(--color-sidebar-brand-text);font-size:1.5rem;overflow-wrap:break-word}.sidebar-brand-text,.sidebar-logo-container{margin:var(--sidebar-item-spacing-vertical) 0}.sidebar-logo{display:block;margin:0 auto;max-width:100%}.sidebar-search-container{align-items:center;background:var(--color-sidebar-search-background);display:flex;margin-top:var(--sidebar-search-space-above);position:relative}.sidebar-search-container:focus-within,.sidebar-search-container:hover{background:var(--color-sidebar-search-background--focus)}.sidebar-search-container:before{background-color:var(--color-sidebar-search-icon);content:"";height:var(--sidebar-search-icon-size);left:var(--sidebar-item-spacing-horizontal);-webkit-mask-image:var(--icon-search);mask-image:var(--icon-search);position:absolute;width:var(--sidebar-search-icon-size)}.sidebar-search{background:transparent;border:none;border-bottom:1px solid var(--color-sidebar-search-border);border-top:1px solid var(--color-sidebar-search-border);box-sizing:border-box;color:var(--color-sidebar-search-foreground);padding:var(--sidebar-search-input-spacing-vertical) var(--sidebar-search-input-spacing-horizontal) var(--sidebar-search-input-spacing-vertical) calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size));width:100%;z-index:10}.sidebar-search:focus{outline:none}.sidebar-search::-moz-placeholder{font-size:var(--sidebar-search-input-font-size)}.sidebar-search::placeholder{font-size:var(--sidebar-search-input-font-size)}#searchbox .highlight-link{margin:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0;text-align:center}#searchbox .highlight-link a{color:var(--color-sidebar-search-icon);font-size:var(--font-size--small--2)}.sidebar-tree{font-size:var(--sidebar-item-font-size);margin-bottom:var(--sidebar-item-spacing-vertical);margin-top:var(--sidebar-tree-space-above)}.sidebar-tree ul{display:flex;flex-direction:column;list-style:none;margin-bottom:0;margin-top:0;padding:0}.sidebar-tree li{margin:0;position:relative}.sidebar-tree li>ul{margin-left:var(--sidebar-item-spacing-horizontal)}.sidebar-tree .icon,.sidebar-tree .reference{color:var(--color-sidebar-link-text)}.sidebar-tree .reference{box-sizing:border-box;display:inline-block;height:100%;line-height:var(--sidebar-item-line-height);overflow-wrap:anywhere;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none;width:100%}.sidebar-tree .reference:hover{background:var(--color-sidebar-item-background--hover)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg width='12' height='12' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23607D8B' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M0 0h24v24H0z' stroke='none'/%3E%3Cpath d='M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5M10 14 20 4M15 4h5v5'/%3E%3C/svg%3E");margin:0 .25rem;vertical-align:middle}.sidebar-tree .current-page>.reference{font-weight:700}.sidebar-tree label{align-items:center;cursor:pointer;display:flex;height:var(--sidebar-item-height);justify-content:center;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--sidebar-expander-width)}.sidebar-tree .caption,.sidebar-tree :not(.caption)>.caption-text{color:var(--color-sidebar-caption-text);font-size:var(--sidebar-caption-font-size);font-weight:700;margin:var(--sidebar-caption-space-above) 0 0 0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-transform:uppercase}.sidebar-tree li.has-children>.reference{padding-right:var(--sidebar-expander-width)}.sidebar-tree .toctree-l1>.reference,.sidebar-tree .toctree-l1>label .icon{color:var(--color-sidebar-link-text--top-level)}.sidebar-tree label{background:var(--color-sidebar-item-expander-background)}.sidebar-tree label:hover{background:var(--color-sidebar-item-expander-background--hover)}.sidebar-tree .current>.reference{background:var(--color-sidebar-item-background--current)}.sidebar-tree .current>.reference:hover{background:var(--color-sidebar-item-background--hover)}.toctree-checkbox{display:none;position:absolute}.toctree-checkbox~ul{display:none}.toctree-checkbox~label .icon svg{transform:rotate(90deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label .icon svg{transform:rotate(-90deg)}.toc-title-container{padding:var(--toc-title-padding);padding-top:var(--toc-spacing-vertical)}.toc-title{color:var(--color-toc-title-text);font-size:var(--toc-title-font-size);padding-left:var(--toc-spacing-horizontal);text-transform:uppercase}.no-toc{display:none}.toc-tree-container{padding-bottom:var(--toc-spacing-vertical)}.toc-tree{border-left:1px solid var(--color-background-border);font-size:var(--toc-font-size);line-height:1.3;padding-left:calc(var(--toc-spacing-horizontal) - var(--toc-item-spacing-horizontal))}.toc-tree>ul>li:first-child{padding-top:0}.toc-tree>ul>li:first-child>ul{padding-left:0}.toc-tree>ul>li:first-child>a{display:none}.toc-tree ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:var(--toc-item-spacing-horizontal)}.toc-tree li{padding-top:var(--toc-item-spacing-vertical)}.toc-tree li.scroll-current>.reference{color:var(--color-toc-item-text--active);font-weight:700}.toc-tree .reference{color:var(--color-toc-item-text);overflow-wrap:anywhere;text-decoration:none}.toc-scroll{max-height:100vh;overflow-y:scroll}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here){background:rgba(255,0,0,.25);color:var(--color-problematic)}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here):before{content:"ERROR: Adding a table of contents in Furo-based documentation is unnecessary, and does not work well with existing styling.Add a 'this-will-duplicate-information-and-it-is-still-useful-here' class, if you want an escape hatch."}.text-align\:left>p{text-align:left}.text-align\:center>p{text-align:center}.text-align\:right>p{text-align:right} +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}@media print{.content-icon-container,.headerlink,.mobile-header,.related-pages{display:none!important}.highlight{border:.1pt solid var(--color-foreground-border)}a,blockquote,dl,ol,pre,table,ul{page-break-inside:avoid}caption,figure,h1,h2,h3,h4,h5,h6,img{page-break-after:avoid;page-break-inside:avoid}dl,ol,ul{page-break-before:avoid}}.visually-hidden{clip:rect(0,0,0,0)!important;border:0!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:1px!important}:-moz-focusring{outline:auto}body{--font-stack:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;--font-stack--monospace:"SFMono-Regular",Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace;--font-size--normal:100%;--font-size--small:87.5%;--font-size--small--2:81.25%;--font-size--small--3:75%;--font-size--small--4:62.5%;--sidebar-caption-font-size:var(--font-size--small--2);--sidebar-item-font-size:var(--font-size--small);--sidebar-search-input-font-size:var(--font-size--small);--toc-font-size:var(--font-size--small--3);--toc-font-size--mobile:var(--font-size--normal);--toc-title-font-size:var(--font-size--small--4);--admonition-font-size:0.8125rem;--admonition-title-font-size:0.8125rem;--code-font-size:var(--font-size--small--2);--api-font-size:var(--font-size--small);--header-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*4);--header-padding:0.5rem;--sidebar-tree-space-above:1.5rem;--sidebar-caption-space-above:1rem;--sidebar-item-line-height:1rem;--sidebar-item-spacing-vertical:0.5rem;--sidebar-item-spacing-horizontal:1rem;--sidebar-item-height:calc(var(--sidebar-item-line-height) + var(--sidebar-item-spacing-vertical)*2);--sidebar-expander-width:var(--sidebar-item-height);--sidebar-search-space-above:0.5rem;--sidebar-search-input-spacing-vertical:0.5rem;--sidebar-search-input-spacing-horizontal:0.5rem;--sidebar-search-input-height:1rem;--sidebar-search-icon-size:var(--sidebar-search-input-height);--toc-title-padding:0.25rem 0;--toc-spacing-vertical:1.5rem;--toc-spacing-horizontal:1.5rem;--toc-item-spacing-vertical:0.4rem;--toc-item-spacing-horizontal:1rem;--icon-search:url('data:image/svg+xml;charset=utf-8,');--icon-pencil:url('data:image/svg+xml;charset=utf-8,');--icon-abstract:url('data:image/svg+xml;charset=utf-8,');--icon-info:url('data:image/svg+xml;charset=utf-8,');--icon-flame:url('data:image/svg+xml;charset=utf-8,');--icon-question:url('data:image/svg+xml;charset=utf-8,');--icon-warning:url('data:image/svg+xml;charset=utf-8,');--icon-failure:url('data:image/svg+xml;charset=utf-8,');--icon-spark:url('data:image/svg+xml;charset=utf-8,');--color-admonition-title--caution:#ff9100;--color-admonition-title-background--caution:rgba(255,145,0,.2);--color-admonition-title--warning:#ff9100;--color-admonition-title-background--warning:rgba(255,145,0,.2);--color-admonition-title--danger:#ff5252;--color-admonition-title-background--danger:rgba(255,82,82,.2);--color-admonition-title--attention:#ff5252;--color-admonition-title-background--attention:rgba(255,82,82,.2);--color-admonition-title--error:#ff5252;--color-admonition-title-background--error:rgba(255,82,82,.2);--color-admonition-title--hint:#00c852;--color-admonition-title-background--hint:rgba(0,200,82,.2);--color-admonition-title--tip:#00c852;--color-admonition-title-background--tip:rgba(0,200,82,.2);--color-admonition-title--important:#00bfa5;--color-admonition-title-background--important:rgba(0,191,165,.2);--color-admonition-title--note:#00b0ff;--color-admonition-title-background--note:rgba(0,176,255,.2);--color-admonition-title--seealso:#448aff;--color-admonition-title-background--seealso:rgba(68,138,255,.2);--color-admonition-title--admonition-todo:grey;--color-admonition-title-background--admonition-todo:hsla(0,0%,50%,.2);--color-admonition-title:#651fff;--color-admonition-title-background:rgba(101,31,255,.2);--icon-admonition-default:var(--icon-abstract);--color-topic-title:#14b8a6;--color-topic-title-background:rgba(20,184,166,.2);--icon-topic-default:var(--icon-pencil);--color-problematic:#b30000;--color-foreground-primary:#000;--color-foreground-secondary:#5a5c63;--color-foreground-muted:#646776;--color-foreground-border:#878787;--color-background-primary:#fff;--color-background-secondary:#f8f9fb;--color-background-hover:#efeff4;--color-background-hover--transparent:#efeff400;--color-background-border:#eeebee;--color-background-item:#ccc;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2962ff;--color-brand-content:#2a5adf;--color-api-background:var(--color-background-hover--transparent);--color-api-background-hover:var(--color-background-hover);--color-api-overall:var(--color-foreground-secondary);--color-api-name:var(--color-problematic);--color-api-pre-name:var(--color-problematic);--color-api-paren:var(--color-foreground-secondary);--color-api-keyword:var(--color-foreground-primary);--color-highlight-on-target:#ffc;--color-inline-code-background:var(--color-background-secondary);--color-highlighted-background:#def;--color-highlighted-text:var(--color-foreground-primary);--color-guilabel-background:#ddeeff80;--color-guilabel-border:#bedaf580;--color-guilabel-text:var(--color-foreground-primary);--color-admonition-background:transparent;--color-table-header-background:var(--color-background-secondary);--color-table-border:var(--color-background-border);--color-card-border:var(--color-background-secondary);--color-card-background:transparent;--color-card-marginals-background:var(--color-background-secondary);--color-header-background:var(--color-background-primary);--color-header-border:var(--color-background-border);--color-header-text:var(--color-foreground-primary);--color-sidebar-background:var(--color-background-secondary);--color-sidebar-background-border:var(--color-background-border);--color-sidebar-brand-text:var(--color-foreground-primary);--color-sidebar-caption-text:var(--color-foreground-muted);--color-sidebar-link-text:var(--color-foreground-secondary);--color-sidebar-link-text--top-level:var(--color-brand-primary);--color-sidebar-item-background:var(--color-sidebar-background);--color-sidebar-item-background--current:var( --color-sidebar-item-background );--color-sidebar-item-background--hover:linear-gradient(90deg,var(--color-background-hover--transparent) 0%,var(--color-background-hover) var(--sidebar-item-spacing-horizontal),var(--color-background-hover) 100%);--color-sidebar-item-expander-background:transparent;--color-sidebar-item-expander-background--hover:var( --color-background-hover );--color-sidebar-search-text:var(--color-foreground-primary);--color-sidebar-search-background:var(--color-background-secondary);--color-sidebar-search-background--focus:var(--color-background-primary);--color-sidebar-search-border:var(--color-background-border);--color-sidebar-search-icon:var(--color-foreground-muted);--color-toc-background:var(--color-background-primary);--color-toc-title-text:var(--color-foreground-muted);--color-toc-item-text:var(--color-foreground-secondary);--color-toc-item-text--hover:var(--color-foreground-primary);--color-toc-item-text--active:var(--color-brand-primary);--color-content-foreground:var(--color-foreground-primary);--color-content-background:transparent;--color-link:var(--color-brand-content);--color-link--hover:var(--color-brand-content);--color-link-underline:var(--color-background-border);--color-link-underline--hover:var(--color-foreground-border)}.only-light{display:block!important}html body .only-dark{display:none!important}@media not print{body[data-theme=dark]{--color-problematic:#ee5151;--color-foreground-primary:#ffffffcc;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2b8cee;--color-brand-content:#368ce2;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body[data-theme=dark] .only-light{display:none!important}body[data-theme=dark] .only-dark{display:block!important}@media(prefers-color-scheme:dark){body:not([data-theme=light]){--color-problematic:#ee5151;--color-foreground-primary:#ffffffcc;--color-foreground-secondary:#9ca0a5;--color-foreground-muted:#81868d;--color-foreground-border:#666;--color-background-primary:#131416;--color-background-secondary:#1a1c1e;--color-background-hover:#1e2124;--color-background-hover--transparent:#1e212400;--color-background-border:#303335;--color-background-item:#444;--color-announcement-background:#000000dd;--color-announcement-text:#eeebee;--color-brand-primary:#2b8cee;--color-brand-content:#368ce2;--color-highlighted-background:#083563;--color-guilabel-background:#08356380;--color-guilabel-border:#13395f80;--color-api-keyword:var(--color-foreground-secondary);--color-highlight-on-target:#330;--color-admonition-background:#18181a;--color-card-border:var(--color-background-secondary);--color-card-background:#18181a;--color-card-marginals-background:var(--color-background-hover)}html body:not([data-theme=light]) .only-light{display:none!important}body:not([data-theme=light]) .only-dark{display:block!important}}}body[data-theme=auto] .theme-toggle svg.theme-icon-when-auto,body[data-theme=dark] .theme-toggle svg.theme-icon-when-dark,body[data-theme=light] .theme-toggle svg.theme-icon-when-light{display:block}body{font-family:var(--font-stack)}code,kbd,pre,samp{font-family:var(--font-stack--monospace)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}article{line-height:1.5}h1,h2,h3,h4,h5,h6{border-radius:.5rem;font-weight:700;line-height:1.25;margin:.5rem -.5rem;padding-left:.5rem;padding-right:.5rem}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p{margin-top:0}h1{font-size:2.5em;margin-bottom:1rem}h1,h2{margin-top:1.75rem}h2{font-size:2em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1.125em}h6{font-size:1em}small{font-size:80%;opacity:75%}p{margin-bottom:.75rem;margin-top:.5rem}hr.docutils{background-color:var(--color-background-border);border:0;height:1px;margin:2rem 0;padding:0}.centered{text-align:center}a{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}a:hover{color:var(--color-link--hover);text-decoration-color:var(--color-link-underline--hover)}a.muted-link{color:inherit}a.muted-link:hover{color:var(--color-link);text-decoration-color:var(--color-link-underline--hover)}html{overflow-x:hidden;overflow-y:scroll;scroll-behavior:smooth}.sidebar-scroll,.toc-scroll,article[role=main] *{scrollbar-color:var(--color-foreground-border) transparent;scrollbar-width:thin}.sidebar-scroll::-webkit-scrollbar,.toc-scroll::-webkit-scrollbar,article[role=main] ::-webkit-scrollbar{height:.25rem;width:.25rem}.sidebar-scroll::-webkit-scrollbar-thumb,.toc-scroll::-webkit-scrollbar-thumb,article[role=main] ::-webkit-scrollbar-thumb{background-color:var(--color-foreground-border);border-radius:.125rem}body,html{background:var(--color-background-primary);color:var(--color-foreground-primary);height:100%}article{background:var(--color-content-background);color:var(--color-content-foreground);overflow-wrap:break-word}.page{display:flex;min-height:100%}.mobile-header{background-color:var(--color-header-background);border-bottom:1px solid var(--color-header-border);color:var(--color-header-text);display:none;height:var(--header-height);width:100%;z-index:10}.mobile-header.scrolled{border-bottom:none;box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2)}.mobile-header .header-center a{color:var(--color-header-text);text-decoration:none}.main{display:flex;flex:1}.sidebar-drawer{background:var(--color-sidebar-background);border-right:1px solid var(--color-sidebar-background-border);box-sizing:border-box;display:flex;justify-content:flex-end;min-width:15em;width:calc(50% - 26em)}.sidebar-container,.toc-drawer{box-sizing:border-box;width:15em}.toc-drawer{background:var(--color-toc-background);padding-right:1rem}.sidebar-sticky,.toc-sticky{display:flex;flex-direction:column;height:min(100%,100vh);height:100vh;position:sticky;top:0}.sidebar-scroll,.toc-scroll{flex-grow:1;flex-shrink:1;overflow:auto;scroll-behavior:smooth}.content{display:flex;flex-direction:column;justify-content:space-between;padding:0 3em;width:46em}.icon{display:inline-block;height:1rem;width:1rem}.icon svg{height:100%;width:100%}.announcement{align-items:center;background-color:var(--color-announcement-background);color:var(--color-announcement-text);display:flex;height:var(--header-height);overflow-x:auto}.announcement+.page{min-height:calc(100% - var(--header-height))}.announcement-content{box-sizing:border-box;min-width:100%;padding:.5rem;text-align:center;white-space:nowrap}.announcement-content a{color:var(--color-announcement-text);text-decoration-color:var(--color-announcement-text)}.announcement-content a:hover{color:var(--color-announcement-text);text-decoration-color:var(--color-link--hover)}.no-js .theme-toggle-container{display:none}.theme-toggle-container{vertical-align:middle}.theme-toggle{background:transparent;border:none;cursor:pointer;padding:0}.theme-toggle svg{color:var(--color-foreground-primary);display:none;height:1rem;vertical-align:middle;width:1rem}.theme-toggle-header{float:left;padding:1rem .5rem}.nav-overlay-icon,.toc-overlay-icon{cursor:pointer;display:none}.nav-overlay-icon .icon,.toc-overlay-icon .icon{color:var(--color-foreground-secondary);height:1rem;width:1rem}.nav-overlay-icon,.toc-header-icon{align-items:center;justify-content:center}.toc-content-icon{height:1.5rem;width:1.5rem}.content-icon-container{display:flex;float:right;gap:.5rem;margin-bottom:1rem;margin-left:1rem;margin-top:1.5rem}.content-icon-container .edit-this-page svg{color:inherit;height:1rem;width:1rem}.sidebar-toggle{display:none;position:absolute}.sidebar-toggle[name=__toc]{left:20px}.sidebar-toggle:checked{left:40px}.overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0ms,height 0ms,opacity .25s ease-out;width:0}.sidebar-overlay{z-index:20}.toc-overlay{z-index:40}.sidebar-drawer{transition:left .25s ease-in-out;z-index:30}.toc-drawer{transition:right .25s ease-in-out;z-index:50}#__navigation:checked~.sidebar-overlay{height:100%;opacity:1;width:100%}#__navigation:checked~.page .sidebar-drawer{left:0;top:0}#__toc:checked~.toc-overlay{height:100%;opacity:1;width:100%}#__toc:checked~.page .toc-drawer{right:0;top:0}.back-to-top{background:var(--color-background-primary);border-radius:1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 1px 0 hsla(220,9%,46%,.502);display:none;font-size:.8125rem;left:0;margin-left:50%;padding:.5rem .75rem .5rem .5rem;position:fixed;text-decoration:none;top:1rem;transform:translateX(-50%);z-index:10}.back-to-top svg{fill:currentColor;display:inline-block;height:1rem;width:1rem}.back-to-top span{margin-left:.25rem}.show-back-to-top .back-to-top{align-items:center;display:flex}@media(min-width:97em){html{font-size:110%}}@media(max-width:82em){.toc-content-icon{display:flex}.toc-drawer{border-left:1px solid var(--color-background-muted);height:100vh;position:fixed;right:-15em;top:0}.toc-tree{border-left:none;font-size:var(--toc-font-size--mobile)}.sidebar-drawer{width:calc(50% - 18.5em)}}@media(max-width:67em){.nav-overlay-icon{display:flex}.sidebar-drawer{height:100vh;left:-15em;position:fixed;top:0;width:15em}.toc-header-icon{display:flex}.theme-toggle-content,.toc-content-icon{display:none}.theme-toggle-header{display:block}.mobile-header{align-items:center;display:flex;justify-content:space-between;position:sticky;top:0}.mobile-header .header-left,.mobile-header .header-right{display:flex;height:var(--header-height);padding:0 var(--header-padding)}.mobile-header .header-left label,.mobile-header .header-right label{height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.nav-overlay-icon .icon,.theme-toggle svg{height:1.25rem;width:1.25rem}:target{scroll-margin-top:var(--header-height)}.back-to-top{top:calc(var(--header-height) + .5rem)}.page{flex-direction:column;justify-content:center}.content{margin-left:auto;margin-right:auto}}@media(max-width:52em){.content{overflow-x:auto;width:100%}}@media(max-width:46em){.content{padding:0 1em}article aside.sidebar{float:none;margin:1rem 0;width:100%}}.admonition,.topic{background:var(--color-admonition-background);border-radius:.2rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1);font-size:var(--admonition-font-size);margin:1rem auto;overflow:hidden;padding:0 .5rem .5rem;page-break-inside:avoid}.admonition>:nth-child(2),.topic>:nth-child(2){margin-top:0}.admonition>:last-child,.topic>:last-child{margin-bottom:0}.admonition p.admonition-title,p.topic-title{font-size:var(--admonition-title-font-size);font-weight:500;line-height:1.3;margin:0 -.5rem .5rem;padding:.4rem .5rem .4rem 2rem;position:relative}.admonition p.admonition-title:before,p.topic-title:before{content:"";height:1rem;left:.5rem;position:absolute;width:1rem}p.admonition-title{background-color:var(--color-admonition-title-background)}p.admonition-title:before{background-color:var(--color-admonition-title);-webkit-mask-image:var(--icon-admonition-default);mask-image:var(--icon-admonition-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}p.topic-title{background-color:var(--color-topic-title-background)}p.topic-title:before{background-color:var(--color-topic-title);-webkit-mask-image:var(--icon-topic-default);mask-image:var(--icon-topic-default);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.admonition{border-left:.2rem solid var(--color-admonition-title)}.admonition.caution{border-left-color:var(--color-admonition-title--caution)}.admonition.caution>.admonition-title{background-color:var(--color-admonition-title-background--caution)}.admonition.caution>.admonition-title:before{background-color:var(--color-admonition-title--caution);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.warning{border-left-color:var(--color-admonition-title--warning)}.admonition.warning>.admonition-title{background-color:var(--color-admonition-title-background--warning)}.admonition.warning>.admonition-title:before{background-color:var(--color-admonition-title--warning);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.danger{border-left-color:var(--color-admonition-title--danger)}.admonition.danger>.admonition-title{background-color:var(--color-admonition-title-background--danger)}.admonition.danger>.admonition-title:before{background-color:var(--color-admonition-title--danger);-webkit-mask-image:var(--icon-spark);mask-image:var(--icon-spark)}.admonition.attention{border-left-color:var(--color-admonition-title--attention)}.admonition.attention>.admonition-title{background-color:var(--color-admonition-title-background--attention)}.admonition.attention>.admonition-title:before{background-color:var(--color-admonition-title--attention);-webkit-mask-image:var(--icon-warning);mask-image:var(--icon-warning)}.admonition.error{border-left-color:var(--color-admonition-title--error)}.admonition.error>.admonition-title{background-color:var(--color-admonition-title-background--error)}.admonition.error>.admonition-title:before{background-color:var(--color-admonition-title--error);-webkit-mask-image:var(--icon-failure);mask-image:var(--icon-failure)}.admonition.hint{border-left-color:var(--color-admonition-title--hint)}.admonition.hint>.admonition-title{background-color:var(--color-admonition-title-background--hint)}.admonition.hint>.admonition-title:before{background-color:var(--color-admonition-title--hint);-webkit-mask-image:var(--icon-question);mask-image:var(--icon-question)}.admonition.tip{border-left-color:var(--color-admonition-title--tip)}.admonition.tip>.admonition-title{background-color:var(--color-admonition-title-background--tip)}.admonition.tip>.admonition-title:before{background-color:var(--color-admonition-title--tip);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.important{border-left-color:var(--color-admonition-title--important)}.admonition.important>.admonition-title{background-color:var(--color-admonition-title-background--important)}.admonition.important>.admonition-title:before{background-color:var(--color-admonition-title--important);-webkit-mask-image:var(--icon-flame);mask-image:var(--icon-flame)}.admonition.note{border-left-color:var(--color-admonition-title--note)}.admonition.note>.admonition-title{background-color:var(--color-admonition-title-background--note)}.admonition.note>.admonition-title:before{background-color:var(--color-admonition-title--note);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition.seealso{border-left-color:var(--color-admonition-title--seealso)}.admonition.seealso>.admonition-title{background-color:var(--color-admonition-title-background--seealso)}.admonition.seealso>.admonition-title:before{background-color:var(--color-admonition-title--seealso);-webkit-mask-image:var(--icon-info);mask-image:var(--icon-info)}.admonition.admonition-todo{border-left-color:var(--color-admonition-title--admonition-todo)}.admonition.admonition-todo>.admonition-title{background-color:var(--color-admonition-title-background--admonition-todo)}.admonition.admonition-todo>.admonition-title:before{background-color:var(--color-admonition-title--admonition-todo);-webkit-mask-image:var(--icon-pencil);mask-image:var(--icon-pencil)}.admonition-todo>.admonition-title{text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd{margin-left:2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:first-child{margin-top:.125rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list,dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dd>:last-child{margin-bottom:.75rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list>dt{font-size:var(--font-size--small);text-transform:uppercase}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd:empty{margin-bottom:.5rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul{margin-left:-1.2rem}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p:nth-child(2){margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .field-list dd>ul>li>p+p:last-child:empty{margin-bottom:0;margin-top:0}dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{color:var(--color-api-overall)}.sig:not(.sig-inline){background:var(--color-api-background);border-radius:.25rem;font-family:var(--font-stack--monospace);font-size:var(--api-font-size);font-weight:700;margin-left:-.25rem;margin-right:-.25rem;padding:.25rem .5rem .25rem 3em;text-indent:-2.5em;transition:background .1s ease-out}.sig:not(.sig-inline):hover{background:var(--color-api-background-hover)}.sig:not(.sig-inline) a.reference .viewcode-link{font-weight:400;width:3.5rem}em.property{font-style:normal}em.property:first-child{color:var(--color-api-keyword)}.sig-name{color:var(--color-api-name)}.sig-prename{color:var(--color-api-pre-name);font-weight:400}.sig-paren{color:var(--color-api-paren)}.sig-param{font-style:normal}.versionmodified{font-style:italic}div.deprecated p,div.versionadded p,div.versionchanged p{margin-bottom:.125rem;margin-top:.125rem}.viewcode-back,.viewcode-link{float:right;text-align:right}.line-block{margin-bottom:.75rem;margin-top:.5rem}.line-block .line-block{margin-bottom:0;margin-top:0;padding-left:1rem}.code-block-caption,article p.caption,table>caption{font-size:var(--font-size--small);text-align:center}.toctree-wrapper.compound .caption,.toctree-wrapper.compound :not(.caption)>.caption-text{font-size:var(--font-size--small);margin-bottom:0;text-align:initial;text-transform:uppercase}.toctree-wrapper.compound>ul{margin-bottom:0;margin-top:0}.sig-inline,code.literal{background:var(--color-inline-code-background);border-radius:.2em;font-size:var(--font-size--small--2);padding:.1em .2em}pre.literal-block .sig-inline,pre.literal-block code.literal{font-size:inherit;padding:0}p .sig-inline,p code.literal{border:1px solid var(--color-background-border)}.sig-inline{font-family:var(--font-stack--monospace)}div[class*=" highlight-"],div[class^=highlight-]{display:flex;margin:1em 0}div[class*=" highlight-"] .table-wrapper,div[class^=highlight-] .table-wrapper,pre{margin:0;padding:0}pre{overflow:auto}article[role=main] .highlight pre{line-height:1.5}.highlight pre,pre.literal-block{font-size:var(--code-font-size);padding:.625rem .875rem}pre.literal-block{background-color:var(--color-code-background);border-radius:.2rem;color:var(--color-code-foreground);margin-bottom:1rem;margin-top:1rem}.highlight{border-radius:.2rem;width:100%}.highlight .gp,.highlight span.linenos{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.highlight .hll{display:block;margin-left:-.875rem;margin-right:-.875rem;padding-left:.875rem;padding-right:.875rem}.code-block-caption{background-color:var(--color-code-background);border-bottom:1px solid;border-radius:.25rem;border-bottom-left-radius:0;border-bottom-right-radius:0;border-color:var(--color-background-border);color:var(--color-code-foreground);display:flex;font-weight:300;padding:.625rem .875rem}.code-block-caption+div[class]{margin-top:0}.code-block-caption+div[class] pre{border-top-left-radius:0;border-top-right-radius:0}.highlighttable{display:block;width:100%}.highlighttable tbody{display:block}.highlighttable tr{display:flex}.highlighttable td.linenos{background-color:var(--color-code-background);border-bottom-left-radius:.2rem;border-top-left-radius:.2rem;color:var(--color-code-foreground);padding:.625rem 0 .625rem .875rem}.highlighttable .linenodiv{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;font-size:var(--code-font-size);padding-right:.875rem}.highlighttable td.code{display:block;flex:1;overflow:hidden;padding:0}.highlighttable td.code .highlight{border-bottom-left-radius:0;border-top-left-radius:0}.highlight span.linenos{box-shadow:-.0625rem 0 var(--color-foreground-border) inset;display:inline-block;margin-right:.875rem;padding-left:0;padding-right:.875rem}.footnote-reference{font-size:var(--font-size--small--4);vertical-align:super}dl.footnote.brackets{color:var(--color-foreground-secondary);display:grid;font-size:var(--font-size--small);grid-template-columns:max-content auto}dl.footnote.brackets dt{margin:0}dl.footnote.brackets dt>.fn-backref{margin-left:.25rem}dl.footnote.brackets dt:after{content:":"}dl.footnote.brackets dt .brackets:before{content:"["}dl.footnote.brackets dt .brackets:after{content:"]"}dl.footnote.brackets dd{margin:0;padding:0 1rem}aside.footnote{color:var(--color-foreground-secondary);font-size:var(--font-size--small)}aside.footnote>span,div.citation>span{float:left;font-weight:500;padding-right:.25rem}aside.footnote>p,div.citation>p{margin-left:2rem}img{box-sizing:border-box;height:auto;max-width:100%}article .figure,article figure{border-radius:.2rem;margin:0}article .figure :last-child,article figure :last-child{margin-bottom:0}article .align-left{clear:left;float:left;margin:0 1rem 1rem}article .align-right{clear:right;float:right;margin:0 1rem 1rem}article .align-center,article .align-default{display:block;margin-left:auto;margin-right:auto;text-align:center}article table.align-default{display:table;text-align:initial}.domainindex-jumpbox,.genindex-jumpbox{border-bottom:1px solid var(--color-background-border);border-top:1px solid var(--color-background-border);padding:.25rem}.domainindex-section h2,.genindex-section h2{margin-bottom:.5rem;margin-top:.75rem}.domainindex-section ul,.genindex-section ul{margin-bottom:0;margin-top:0}ol,ul{margin-bottom:1rem;margin-top:1rem;padding-left:1.2rem}ol li>p:first-child,ul li>p:first-child{margin-bottom:.25rem;margin-top:.25rem}ol li>p:last-child,ul li>p:last-child{margin-top:.25rem}ol li>ol,ol li>ul,ul li>ol,ul li>ul{margin-bottom:.5rem;margin-top:.5rem}ol.arabic{list-style:decimal}ol.loweralpha{list-style:lower-alpha}ol.upperalpha{list-style:upper-alpha}ol.lowerroman{list-style:lower-roman}ol.upperroman{list-style:upper-roman}.simple li>ol,.simple li>ul,.toctree-wrapper li>ol,.toctree-wrapper li>ul{margin-bottom:0;margin-top:0}.field-list dt,.option-list dt,dl.footnote dt,dl.glossary dt,dl.simple dt,dl:not([class]) dt{font-weight:500;margin-top:.25rem}.field-list dt+dt,.option-list dt+dt,dl.footnote dt+dt,dl.glossary dt+dt,dl.simple dt+dt,dl:not([class]) dt+dt{margin-top:0}.field-list dt .classifier:before,.option-list dt .classifier:before,dl.footnote dt .classifier:before,dl.glossary dt .classifier:before,dl.simple dt .classifier:before,dl:not([class]) dt .classifier:before{content:":";margin-left:.2rem;margin-right:.2rem}.field-list dd ul,.field-list dd>p:first-child,.option-list dd ul,.option-list dd>p:first-child,dl.footnote dd ul,dl.footnote dd>p:first-child,dl.glossary dd ul,dl.glossary dd>p:first-child,dl.simple dd ul,dl.simple dd>p:first-child,dl:not([class]) dd ul,dl:not([class]) dd>p:first-child{margin-top:.125rem}.field-list dd ul,.option-list dd ul,dl.footnote dd ul,dl.glossary dd ul,dl.simple dd ul,dl:not([class]) dd ul{margin-bottom:.125rem}.math-wrapper{overflow-x:auto;width:100%}div.math{position:relative;text-align:center}div.math .headerlink,div.math:focus .headerlink{display:none}div.math:hover .headerlink{display:inline-block}div.math span.eqno{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);z-index:1}abbr[title]{cursor:help}.problematic{color:var(--color-problematic)}kbd:not(.compound){background-color:var(--color-background-secondary);border:1px solid var(--color-foreground-border);border-radius:.2rem;box-shadow:0 .0625rem 0 rgba(0,0,0,.2),inset 0 0 0 .125rem var(--color-background-primary);color:var(--color-foreground-primary);display:inline-block;font-size:var(--font-size--small--3);margin:0 .2rem;padding:0 .2rem;vertical-align:text-bottom}blockquote{background:var(--color-background-secondary);border-left:4px solid var(--color-background-border);margin-left:0;margin-right:0;padding:.5rem 1rem}blockquote .attribution{font-weight:600;text-align:right}blockquote.highlights,blockquote.pull-quote{font-size:1.25em}blockquote.epigraph,blockquote.pull-quote{border-left-width:0;border-radius:.5rem}blockquote.highlights{background:transparent;border-left-width:0}p .reference img{vertical-align:middle}p.rubric{font-size:1.125em;font-weight:700;line-height:1.25}dd p.rubric{font-size:var(--font-size--small);font-weight:inherit;line-height:inherit;text-transform:uppercase}article .sidebar{background-color:var(--color-background-secondary);border:1px solid var(--color-background-border);border-radius:.2rem;clear:right;float:right;margin-left:1rem;margin-right:0;width:30%}article .sidebar>*{padding-left:1rem;padding-right:1rem}article .sidebar>ol,article .sidebar>ul{padding-left:2.2rem}article .sidebar .sidebar-title{border-bottom:1px solid var(--color-background-border);font-weight:500;margin:0;padding:.5rem 1rem}.table-wrapper{margin-bottom:.5rem;margin-top:1rem;overflow-x:auto;padding:.2rem .2rem .75rem;width:100%}table.docutils{border-collapse:collapse;border-radius:.2rem;border-spacing:0;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1)}table.docutils th{background:var(--color-table-header-background)}table.docutils td,table.docutils th{border-bottom:1px solid var(--color-table-border);border-left:1px solid var(--color-table-border);border-right:1px solid var(--color-table-border);padding:0 .25rem}table.docutils td p,table.docutils th p{margin:.25rem}table.docutils td:first-child,table.docutils th:first-child{border-left:none}table.docutils td:last-child,table.docutils th:last-child{border-right:none}table.docutils td.text-left,table.docutils th.text-left{text-align:left}table.docutils td.text-right,table.docutils th.text-right{text-align:right}table.docutils td.text-center,table.docutils th.text-center{text-align:center}:target{scroll-margin-top:.5rem}@media(max-width:67em){:target{scroll-margin-top:calc(.5rem + var(--header-height))}section>span:target{scroll-margin-top:calc(.8rem + var(--header-height))}}.headerlink{font-weight:100;-webkit-user-select:none;-moz-user-select:none;user-select:none}.code-block-caption>.headerlink,dl dt>.headerlink,figcaption p>.headerlink,h1>.headerlink,h2>.headerlink,h3>.headerlink,h4>.headerlink,h5>.headerlink,h6>.headerlink,p.caption>.headerlink,table>caption>.headerlink{margin-left:.5rem;visibility:hidden}.code-block-caption:hover>.headerlink,dl dt:hover>.headerlink,figcaption p:hover>.headerlink,h1:hover>.headerlink,h2:hover>.headerlink,h3:hover>.headerlink,h4:hover>.headerlink,h5:hover>.headerlink,h6:hover>.headerlink,p.caption:hover>.headerlink,table>caption:hover>.headerlink{visibility:visible}.code-block-caption>.toc-backref,dl dt>.toc-backref,figcaption p>.toc-backref,h1>.toc-backref,h2>.toc-backref,h3>.toc-backref,h4>.toc-backref,h5>.toc-backref,h6>.toc-backref,p.caption>.toc-backref,table>caption>.toc-backref{color:inherit;text-decoration-line:none}figure:hover>figcaption>p>.headerlink,table:hover>caption>.headerlink{visibility:visible}:target>h1:first-of-type,:target>h2:first-of-type,:target>h3:first-of-type,:target>h4:first-of-type,:target>h5:first-of-type,:target>h6:first-of-type,span:target~h1:first-of-type,span:target~h2:first-of-type,span:target~h3:first-of-type,span:target~h4:first-of-type,span:target~h5:first-of-type,span:target~h6:first-of-type{background-color:var(--color-highlight-on-target)}:target>h1:first-of-type code.literal,:target>h2:first-of-type code.literal,:target>h3:first-of-type code.literal,:target>h4:first-of-type code.literal,:target>h5:first-of-type code.literal,:target>h6:first-of-type code.literal,span:target~h1:first-of-type code.literal,span:target~h2:first-of-type code.literal,span:target~h3:first-of-type code.literal,span:target~h4:first-of-type code.literal,span:target~h5:first-of-type code.literal,span:target~h6:first-of-type code.literal{background-color:transparent}.literal-block-wrapper:target .code-block-caption,.this-will-duplicate-information-and-it-is-still-useful-here li :target,figure:target,table:target>caption{background-color:var(--color-highlight-on-target)}dt:target{background-color:var(--color-highlight-on-target)!important}.footnote-reference:target,.footnote>dt:target+dd{background-color:var(--color-highlight-on-target)}.guilabel{background-color:var(--color-guilabel-background);border:1px solid var(--color-guilabel-border);border-radius:.5em;color:var(--color-guilabel-text);font-size:.9em;padding:0 .3em}footer{display:flex;flex-direction:column;font-size:var(--font-size--small);margin-top:2rem}.bottom-of-page{align-items:center;border-top:1px solid var(--color-background-border);color:var(--color-foreground-secondary);display:flex;justify-content:space-between;line-height:1.5;margin-top:1rem;padding-bottom:1rem;padding-top:1rem}@media(max-width:46em){.bottom-of-page{flex-direction:column-reverse;gap:.25rem;text-align:center}}.bottom-of-page .left-details{font-size:var(--font-size--small)}.bottom-of-page .right-details{display:flex;flex-direction:column;gap:.25rem;text-align:right}.bottom-of-page .icons{display:flex;font-size:1rem;gap:.25rem;justify-content:flex-end}.bottom-of-page .icons a{text-decoration:none}.bottom-of-page .icons img,.bottom-of-page .icons svg{font-size:1.125rem;height:1em;width:1em}.related-pages a{align-items:center;display:flex;text-decoration:none}.related-pages a:hover .page-info .title{color:var(--color-link);text-decoration:underline;text-decoration-color:var(--color-link-underline)}.related-pages a svg.furo-related-icon,.related-pages a svg.furo-related-icon>use{color:var(--color-foreground-border);flex-shrink:0;height:.75rem;margin:0 .5rem;width:.75rem}.related-pages a.next-page{clear:right;float:right;max-width:50%;text-align:right}.related-pages a.prev-page{clear:left;float:left;max-width:50%}.related-pages a.prev-page svg{transform:rotate(180deg)}.page-info{display:flex;flex-direction:column;overflow-wrap:anywhere}.next-page .page-info{align-items:flex-end}.page-info .context{align-items:center;color:var(--color-foreground-muted);display:flex;font-size:var(--font-size--small);padding-bottom:.1rem;text-decoration:none}ul.search{list-style:none;padding-left:0}ul.search li{border-bottom:1px solid var(--color-background-border);padding:1rem 0}[role=main] .highlighted{background-color:var(--color-highlighted-background);color:var(--color-highlighted-text)}.sidebar-brand{display:flex;flex-direction:column;flex-shrink:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none}.sidebar-brand-text{color:var(--color-sidebar-brand-text);font-size:1.5rem;overflow-wrap:break-word}.sidebar-brand-text,.sidebar-logo-container{margin:var(--sidebar-item-spacing-vertical) 0}.sidebar-logo{display:block;margin:0 auto;max-width:100%}.sidebar-search-container{align-items:center;background:var(--color-sidebar-search-background);display:flex;margin-top:var(--sidebar-search-space-above);position:relative}.sidebar-search-container:focus-within,.sidebar-search-container:hover{background:var(--color-sidebar-search-background--focus)}.sidebar-search-container:before{background-color:var(--color-sidebar-search-icon);content:"";height:var(--sidebar-search-icon-size);left:var(--sidebar-item-spacing-horizontal);-webkit-mask-image:var(--icon-search);mask-image:var(--icon-search);position:absolute;width:var(--sidebar-search-icon-size)}.sidebar-search{background:transparent;border:none;border-bottom:1px solid var(--color-sidebar-search-border);border-top:1px solid var(--color-sidebar-search-border);box-sizing:border-box;color:var(--color-sidebar-search-foreground);padding:var(--sidebar-search-input-spacing-vertical) var(--sidebar-search-input-spacing-horizontal) var(--sidebar-search-input-spacing-vertical) calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size));width:100%;z-index:10}.sidebar-search:focus{outline:none}.sidebar-search::-moz-placeholder{font-size:var(--sidebar-search-input-font-size)}.sidebar-search::placeholder{font-size:var(--sidebar-search-input-font-size)}#searchbox .highlight-link{margin:0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0;text-align:center}#searchbox .highlight-link a{color:var(--color-sidebar-search-icon);font-size:var(--font-size--small--2)}.sidebar-tree{font-size:var(--sidebar-item-font-size);margin-bottom:var(--sidebar-item-spacing-vertical);margin-top:var(--sidebar-tree-space-above)}.sidebar-tree ul{display:flex;flex-direction:column;list-style:none;margin-bottom:0;margin-top:0;padding:0}.sidebar-tree li{margin:0;position:relative}.sidebar-tree li>ul{margin-left:var(--sidebar-item-spacing-horizontal)}.sidebar-tree .icon,.sidebar-tree .reference{color:var(--color-sidebar-link-text)}.sidebar-tree .reference{box-sizing:border-box;display:inline-block;height:100%;line-height:var(--sidebar-item-line-height);overflow-wrap:anywhere;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-decoration:none;width:100%}.sidebar-tree .reference:hover{background:var(--color-sidebar-item-background--hover)}.sidebar-tree .reference.external:after{color:var(--color-sidebar-link-text);content:url("data:image/svg+xml;charset=utf-8,%3Csvg width='12' height='12' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23607D8B' fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M0 0h24v24H0z' stroke='none'/%3E%3Cpath d='M11 7H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-5M10 14 20 4M15 4h5v5'/%3E%3C/svg%3E");margin:0 .25rem;vertical-align:middle}.sidebar-tree .current-page>.reference{font-weight:700}.sidebar-tree label{align-items:center;cursor:pointer;display:flex;height:var(--sidebar-item-height);justify-content:center;position:absolute;right:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:var(--sidebar-expander-width)}.sidebar-tree .caption,.sidebar-tree :not(.caption)>.caption-text{color:var(--color-sidebar-caption-text);font-size:var(--sidebar-caption-font-size);font-weight:700;margin:var(--sidebar-caption-space-above) 0 0 0;padding:var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal);text-transform:uppercase}.sidebar-tree li.has-children>.reference{padding-right:var(--sidebar-expander-width)}.sidebar-tree .toctree-l1>.reference,.sidebar-tree .toctree-l1>label .icon{color:var(--color-sidebar-link-text--top-level)}.sidebar-tree label{background:var(--color-sidebar-item-expander-background)}.sidebar-tree label:hover{background:var(--color-sidebar-item-expander-background--hover)}.sidebar-tree .current>.reference{background:var(--color-sidebar-item-background--current)}.sidebar-tree .current>.reference:hover{background:var(--color-sidebar-item-background--hover)}.toctree-checkbox{display:none;position:absolute}.toctree-checkbox~ul{display:none}.toctree-checkbox~label .icon svg{transform:rotate(90deg)}.toctree-checkbox:checked~ul{display:block}.toctree-checkbox:checked~label .icon svg{transform:rotate(-90deg)}.toc-title-container{padding:var(--toc-title-padding);padding-top:var(--toc-spacing-vertical)}.toc-title{color:var(--color-toc-title-text);font-size:var(--toc-title-font-size);padding-left:var(--toc-spacing-horizontal);text-transform:uppercase}.no-toc{display:none}.toc-tree-container{padding-bottom:var(--toc-spacing-vertical)}.toc-tree{border-left:1px solid var(--color-background-border);font-size:var(--toc-font-size);line-height:1.3;padding-left:calc(var(--toc-spacing-horizontal) - var(--toc-item-spacing-horizontal))}.toc-tree>ul>li:first-child{padding-top:0}.toc-tree>ul>li:first-child>ul{padding-left:0}.toc-tree>ul>li:first-child>a{display:none}.toc-tree ul{list-style-type:none;margin-bottom:0;margin-top:0;padding-left:var(--toc-item-spacing-horizontal)}.toc-tree li{padding-top:var(--toc-item-spacing-vertical)}.toc-tree li.scroll-current>.reference{color:var(--color-toc-item-text--active);font-weight:700}.toc-tree .reference{color:var(--color-toc-item-text);overflow-wrap:anywhere;text-decoration:none}.toc-scroll{max-height:100vh;overflow-y:scroll}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here){background:rgba(255,0,0,.25);color:var(--color-problematic)}.contents:not(.this-will-duplicate-information-and-it-is-still-useful-here):before{content:"ERROR: Adding a table of contents in Furo-based documentation is unnecessary, and does not work well with existing styling.Add a 'this-will-duplicate-information-and-it-is-still-useful-here' class, if you want an escape hatch."}.text-align\:left>p{text-align:left}.text-align\:center>p{text-align:center}.text-align\:right>p{text-align:right} /*# sourceMappingURL=furo.css.map*/ \ No newline at end of file diff --git a/docs/_static/styles/furo.css.map b/docs/_static/styles/furo.css.map index 92af407..d1dfb10 100644 --- a/docs/_static/styles/furo.css.map +++ b/docs/_static/styles/furo.css.map @@ -1 +1 @@ -{"version":3,"file":"styles/furo.css","mappings":"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","sources":["webpack:///./node_modules/normalize.css/normalize.css","webpack:///./src/furo/assets/styles/base/_print.sass","webpack:///./src/furo/assets/styles/base/_screen-readers.sass","webpack:///./src/furo/assets/styles/base/_theme.sass","webpack:///./src/furo/assets/styles/variables/_fonts.scss","webpack:///./src/furo/assets/styles/variables/_spacing.scss","webpack:///./src/furo/assets/styles/variables/_icons.scss","webpack:///./src/furo/assets/styles/variables/_admonitions.scss","webpack:///./src/furo/assets/styles/variables/_colors.scss","webpack:///./src/furo/assets/styles/base/_typography.sass","webpack:///./src/furo/assets/styles/_scaffold.sass","webpack:///./src/furo/assets/styles/variables/_layout.scss","webpack:///./src/furo/assets/styles/content/_admonitions.sass","webpack:///./src/furo/assets/styles/content/_api.sass","webpack:///./src/furo/assets/styles/content/_blocks.sass","webpack:///./src/furo/assets/styles/content/_captions.sass","webpack:///./src/furo/assets/styles/content/_code.sass","webpack:///./src/furo/assets/styles/content/_footnotes.sass","webpack:///./src/furo/assets/styles/content/_images.sass","webpack:///./src/furo/assets/styles/content/_indexes.sass","webpack:///./src/furo/assets/styles/content/_lists.sass","webpack:///./src/furo/assets/styles/content/_math.sass","webpack:///./src/furo/assets/styles/content/_misc.sass","webpack:///./src/furo/assets/styles/content/_rubrics.sass","webpack:///./src/furo/assets/styles/content/_sidebar.sass","webpack:///./src/furo/assets/styles/content/_tables.sass","webpack:///./src/furo/assets/styles/content/_target.sass","webpack:///./src/furo/assets/styles/content/_gui-labels.sass","webpack:///./src/furo/assets/styles/components/_footer.sass","webpack:///./src/furo/assets/styles/components/_search.sass","webpack:///./src/furo/assets/styles/components/_sidebar.sass","webpack:///./src/furo/assets/styles/components/_table_of_contents.sass","webpack:///./src/furo/assets/styles/_shame.sass"],"sourcesContent":["/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n/* Grouping content\n ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n box-sizing: content-box; /* 1 */\n height: 0; /* 1 */\n overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n border-bottom: none; /* 1 */\n text-decoration: underline; /* 2 */\n text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/* Embedded content\n ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n border-style: none;\n}\n\n/* Forms\n ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n border-style: none;\n padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n * `fieldset` elements in all browsers.\n */\n\nlegend {\n box-sizing: border-box; /* 1 */\n color: inherit; /* 2 */\n display: table; /* 1 */\n max-width: 100%; /* 1 */\n padding: 0; /* 3 */\n white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n box-sizing: border-box; /* 1 */\n padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/* Interactive\n ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n display: list-item;\n}\n\n/* Misc\n ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n display: none;\n}\n","// This file contains styles for managing print media.\n\n////////////////////////////////////////////////////////////////////////////////\n// Hide elements not relevant to print media.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Hide icon container.\n .content-icon-container\n display: none !important\n\n // Hide showing header links if hovering over when printing.\n .headerlink\n display: none !important\n\n // Hide mobile header.\n .mobile-header\n display: none !important\n\n // Hide navigation links.\n .related-pages\n display: none !important\n\n////////////////////////////////////////////////////////////////////////////////\n// Tweaks related to decolorization.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n // Apply a border around code which no longer have a color background.\n .highlight\n border: 0.1pt solid var(--color-foreground-border)\n\n////////////////////////////////////////////////////////////////////////////////\n// Avoid page break in some relevant cases.\n////////////////////////////////////////////////////////////////////////////////\n@media print\n ul, ol, dl, a, table, pre, blockquote\n page-break-inside: avoid\n\n h1, h2, h3, h4, h5, h6, img, figure, caption\n page-break-inside: avoid\n page-break-after: avoid\n\n ul, ol, dl\n page-break-before: avoid\n",".visually-hidden\n position: absolute !important\n width: 1px !important\n height: 1px !important\n padding: 0 !important\n margin: -1px !important\n overflow: hidden !important\n clip: rect(0,0,0,0) !important\n white-space: nowrap !important\n border: 0 !important\n\n:-moz-focusring\n outline: auto\n","// This file serves as the \"skeleton\" of the theming logic.\n//\n// This contains the bulk of the logic for handling dark mode, color scheme\n// toggling and the handling of color-scheme-specific hiding of elements.\n\nbody\n @include fonts\n @include spacing\n @include icons\n @include admonitions\n @include default-admonition(#651fff, \"abstract\")\n @include default-topic(#14B8A6, \"pencil\")\n\n @include colors\n\n.only-light\n display: block !important\nhtml body .only-dark\n display: none !important\n\n// Ignore dark-mode hints if print media.\n@media not print\n // Enable dark-mode, if requested.\n body[data-theme=\"dark\"]\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n // Enable dark mode, unless explicitly told to avoid.\n @media (prefers-color-scheme: dark)\n body:not([data-theme=\"light\"])\n @include colors-dark\n\n html & .only-light\n display: none !important\n .only-dark\n display: block !important\n\n//\n// Theme toggle presentation\n//\nbody[data-theme=\"auto\"]\n .theme-toggle svg.theme-icon-when-auto\n display: block\n\nbody[data-theme=\"dark\"]\n .theme-toggle svg.theme-icon-when-dark\n display: block\n\nbody[data-theme=\"light\"]\n .theme-toggle svg.theme-icon-when-light\n display: block\n","// Fonts used by this theme.\n//\n// There are basically two things here -- using the system font stack and\n// defining sizes for various elements in %ages. We could have also used `em`\n// but %age is easier to reason about for me.\n\n@mixin fonts {\n // These are adapted from https://systemfontstack.com/\n --font-stack: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --font-stack--monospace: \"SFMono-Regular\", Menlo, Consolas, Monaco,\n Liberation Mono, Lucida Console, monospace;\n\n --font-size--normal: 100%;\n --font-size--small: 87.5%;\n --font-size--small--2: 81.25%;\n --font-size--small--3: 75%;\n --font-size--small--4: 62.5%;\n\n // Sidebar\n --sidebar-caption-font-size: var(--font-size--small--2);\n --sidebar-item-font-size: var(--font-size--small);\n --sidebar-search-input-font-size: var(--font-size--small);\n\n // Table of Contents\n --toc-font-size: var(--font-size--small--3);\n --toc-font-size--mobile: var(--font-size--normal);\n --toc-title-font-size: var(--font-size--small--4);\n\n // Admonitions\n //\n // These aren't defined in terms of %ages, since nesting these is permitted.\n --admonition-font-size: 0.8125rem;\n --admonition-title-font-size: 0.8125rem;\n\n // Code\n --code-font-size: var(--font-size--small--2);\n\n // API\n --api-font-size: var(--font-size--small);\n}\n","// Spacing for various elements on the page\n//\n// If the user wants to tweak things in a certain way, they are permitted to.\n// They also have to deal with the consequences though!\n\n@mixin spacing {\n // Header!\n --header-height: calc(\n var(--sidebar-item-line-height) + 4 * #{var(--sidebar-item-spacing-vertical)}\n );\n --header-padding: 0.5rem;\n\n // Sidebar\n --sidebar-tree-space-above: 1.5rem;\n --sidebar-caption-space-above: 1rem;\n\n --sidebar-item-line-height: 1rem;\n --sidebar-item-spacing-vertical: 0.5rem;\n --sidebar-item-spacing-horizontal: 1rem;\n --sidebar-item-height: calc(\n var(--sidebar-item-line-height) + 2 *#{var(--sidebar-item-spacing-vertical)}\n );\n\n --sidebar-expander-width: var(--sidebar-item-height); // be square\n\n --sidebar-search-space-above: 0.5rem;\n --sidebar-search-input-spacing-vertical: 0.5rem;\n --sidebar-search-input-spacing-horizontal: 0.5rem;\n --sidebar-search-input-height: 1rem;\n --sidebar-search-icon-size: var(--sidebar-search-input-height);\n\n // Table of Contents\n --toc-title-padding: 0.25rem 0;\n --toc-spacing-vertical: 1.5rem;\n --toc-spacing-horizontal: 1.5rem;\n --toc-item-spacing-vertical: 0.4rem;\n --toc-item-spacing-horizontal: 1rem;\n}\n","// Expose theme icons as CSS variables.\n\n$icons: (\n // Adapted from tabler-icons\n // url: https://tablericons.com/\n \"search\":\n url('data:image/svg+xml;charset=utf-8,'),\n // Factored out from mkdocs-material on 24-Aug-2020.\n // url: https://squidfunk.github.io/mkdocs-material/reference/admonitions/\n \"pencil\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"abstract\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"info\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"flame\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"question\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"warning\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"failure\":\n url('data:image/svg+xml;charset=utf-8,'),\n \"spark\":\n url('data:image/svg+xml;charset=utf-8,')\n);\n\n@mixin icons {\n @each $name, $glyph in $icons {\n --icon-#{$name}: #{$glyph};\n }\n}\n","// Admonitions\n\n// Structure of these is:\n// admonition-class: color \"icon-name\";\n//\n// The colors are translated into CSS variables below. The icons are\n// used directly in the main declarations to set the `mask-image` in\n// the title.\n\n// prettier-ignore\n$admonitions: (\n // Each of these has an reST directives for it.\n \"caution\": #ff9100 \"spark\",\n \"warning\": #ff9100 \"warning\",\n \"danger\": #ff5252 \"spark\",\n \"attention\": #ff5252 \"warning\",\n \"error\": #ff5252 \"failure\",\n \"hint\": #00c852 \"question\",\n \"tip\": #00c852 \"info\",\n \"important\": #00bfa5 \"flame\",\n \"note\": #00b0ff \"pencil\",\n \"seealso\": #448aff \"info\",\n \"admonition-todo\": #808080 \"pencil\"\n);\n\n@mixin default-admonition($color, $icon-name) {\n --color-admonition-title: #{$color};\n --color-admonition-title-background: #{rgba($color, 0.2)};\n\n --icon-admonition-default: var(--icon-#{$icon-name});\n}\n\n@mixin default-topic($color, $icon-name) {\n --color-topic-title: #{$color};\n --color-topic-title-background: #{rgba($color, 0.2)};\n\n --icon-topic-default: var(--icon-#{$icon-name});\n}\n\n@mixin admonitions {\n @each $name, $values in $admonitions {\n --color-admonition-title--#{$name}: #{nth($values, 1)};\n --color-admonition-title-background--#{$name}: #{rgba(\n nth($values, 1),\n 0.2\n )};\n }\n}\n","// Colors used throughout this theme.\n//\n// The aim is to give the user more control. Thus, instead of hard-coding colors\n// in various parts of the stylesheet, the approach taken is to define all\n// colors as CSS variables and reusing them in all the places.\n//\n// `colors-dark` depends on `colors` being included at a lower specificity.\n\n@mixin colors {\n --color-problematic: #b30000;\n\n // Base Colors\n --color-foreground-primary: black; // for main text and headings\n --color-foreground-secondary: #5a5c63; // for secondary text\n --color-foreground-muted: #646776; // for muted text\n --color-foreground-border: #878787; // for content borders\n\n --color-background-primary: white; // for content\n --color-background-secondary: #f8f9fb; // for navigation + ToC\n --color-background-hover: #efeff4ff; // for navigation-item hover\n --color-background-hover--transparent: #efeff400;\n --color-background-border: #eeebee; // for UI borders\n --color-background-item: #ccc; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #2962ff;\n --color-brand-content: #2a5adf;\n\n // API documentation\n --color-api-background: var(--color-background-hover--transparent);\n --color-api-background-hover: var(--color-background-hover);\n --color-api-overall: var(--color-foreground-secondary);\n --color-api-name: var(--color-problematic);\n --color-api-pre-name: var(--color-problematic);\n --color-api-paren: var(--color-foreground-secondary);\n --color-api-keyword: var(--color-foreground-primary);\n --color-highlight-on-target: #ffffcc;\n\n // Inline code background\n --color-inline-code-background: var(--color-background-secondary);\n\n // Highlighted text (search)\n --color-highlighted-background: #ddeeff;\n --color-highlighted-text: var(--color-foreground-primary);\n\n // GUI Labels\n --color-guilabel-background: #ddeeff80;\n --color-guilabel-border: #bedaf580;\n --color-guilabel-text: var(--color-foreground-primary);\n\n // Admonitions!\n --color-admonition-background: transparent;\n\n //////////////////////////////////////////////////////////////////////////////\n // Everything below this should be one of:\n // - var(...)\n // - *-gradient(...)\n // - special literal values (eg: transparent, none)\n //////////////////////////////////////////////////////////////////////////////\n\n // Tables\n --color-table-header-background: var(--color-background-secondary);\n --color-table-border: var(--color-background-border);\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: transparent;\n --color-card-marginals-background: var(--color-background-secondary);\n\n // Header\n --color-header-background: var(--color-background-primary);\n --color-header-border: var(--color-background-border);\n --color-header-text: var(--color-foreground-primary);\n\n // Sidebar (left)\n --color-sidebar-background: var(--color-background-secondary);\n --color-sidebar-background-border: var(--color-background-border);\n\n --color-sidebar-brand-text: var(--color-foreground-primary);\n --color-sidebar-caption-text: var(--color-foreground-muted);\n --color-sidebar-link-text: var(--color-foreground-secondary);\n --color-sidebar-link-text--top-level: var(--color-brand-primary);\n\n --color-sidebar-item-background: var(--color-sidebar-background);\n --color-sidebar-item-background--current: var(\n --color-sidebar-item-background\n );\n --color-sidebar-item-background--hover: linear-gradient(\n 90deg,\n var(--color-background-hover--transparent) 0%,\n var(--color-background-hover) var(--sidebar-item-spacing-horizontal),\n var(--color-background-hover) 100%\n );\n\n --color-sidebar-item-expander-background: transparent;\n --color-sidebar-item-expander-background--hover: var(\n --color-background-hover\n );\n\n --color-sidebar-search-text: var(--color-foreground-primary);\n --color-sidebar-search-background: var(--color-background-secondary);\n --color-sidebar-search-background--focus: var(--color-background-primary);\n --color-sidebar-search-border: var(--color-background-border);\n --color-sidebar-search-icon: var(--color-foreground-muted);\n\n // Table of Contents (right)\n --color-toc-background: var(--color-background-primary);\n --color-toc-title-text: var(--color-foreground-muted);\n --color-toc-item-text: var(--color-foreground-secondary);\n --color-toc-item-text--hover: var(--color-foreground-primary);\n --color-toc-item-text--active: var(--color-brand-primary);\n\n // Actual page contents\n --color-content-foreground: var(--color-foreground-primary);\n --color-content-background: transparent;\n\n // Links\n --color-link: var(--color-brand-content);\n --color-link--hover: var(--color-brand-content);\n --color-link-underline: var(--color-background-border);\n --color-link-underline--hover: var(--color-foreground-border);\n}\n\n@mixin colors-dark {\n --color-problematic: #ee5151;\n\n // Base Colors\n --color-foreground-primary: #ffffffcc; // for main text and headings\n --color-foreground-secondary: #9ca0a5; // for secondary text\n --color-foreground-muted: #81868d; // for muted text\n --color-foreground-border: #666666; // for content borders\n\n --color-background-primary: #131416; // for content\n --color-background-secondary: #1a1c1e; // for navigation + ToC\n --color-background-hover: #1e2124ff; // for navigation-item hover\n --color-background-hover--transparent: #1e212400;\n --color-background-border: #303335; // for UI borders\n --color-background-item: #444; // for \"background\" items (eg: copybutton)\n\n // Announcements\n --color-announcement-background: #000000dd;\n --color-announcement-text: #eeebee;\n\n // Brand colors\n --color-brand-primary: #2b8cee;\n --color-brand-content: #368ce2;\n\n // Highlighted text (search)\n --color-highlighted-background: #083563;\n\n // GUI Labels\n --color-guilabel-background: #08356380;\n --color-guilabel-border: #13395f80;\n\n // API documentation\n --color-api-keyword: var(--color-foreground-secondary);\n --color-highlight-on-target: #333300;\n\n // Admonitions\n --color-admonition-background: #18181a;\n\n // Cards\n --color-card-border: var(--color-background-secondary);\n --color-card-background: #18181a;\n --color-card-marginals-background: var(--color-background-hover);\n}\n","// This file contains the styling for making the content throughout the page,\n// including fonts, paragraphs, headings and spacing among these elements.\n\nbody\n font-family: var(--font-stack)\npre,\ncode,\nkbd,\nsamp\n font-family: var(--font-stack--monospace)\n\n// Make fonts look slightly nicer.\nbody\n -webkit-font-smoothing: antialiased\n -moz-osx-font-smoothing: grayscale\n\n// Line height from Bootstrap 4.1\narticle\n line-height: 1.5\n\n//\n// Headings\n//\nh1,\nh2,\nh3,\nh4,\nh5,\nh6\n line-height: 1.25\n font-weight: bold\n\n border-radius: 0.5rem\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n margin-left: -0.5rem\n margin-right: -0.5rem\n padding-left: 0.5rem\n padding-right: 0.5rem\n\n + p\n margin-top: 0\n\nh1\n font-size: 2.5em\n margin-top: 1.75rem\n margin-bottom: 1rem\nh2\n font-size: 2em\n margin-top: 1.75rem\nh3\n font-size: 1.5em\nh4\n font-size: 1.25em\nh5\n font-size: 1.125em\nh6\n font-size: 1em\n\nsmall\n opacity: 75%\n font-size: 80%\n\n// Paragraph\np\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n\n// Horizontal rules\nhr.docutils\n height: 1px\n padding: 0\n margin: 2rem 0\n background-color: var(--color-background-border)\n border: 0\n\n.centered\n text-align: center\n\n// Links\na\n text-decoration: underline\n\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n &:hover\n color: var(--color-link--hover)\n text-decoration-color: var(--color-link-underline--hover)\n &.muted-link\n color: inherit\n &:hover\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline--hover)\n","// This file contains the styles for the overall layouting of the documentation\n// skeleton, including the responsive changes as well as sidebar toggles.\n//\n// This is implemented as a mobile-last design, which isn't ideal, but it is\n// reasonably good-enough and I got pretty tired by the time I'd finished this\n// to move the rules around to fix this. Shouldn't take more than 3-4 hours,\n// if you know what you're doing tho.\n\n// HACK: Not all browsers account for the scrollbar width in media queries.\n// This results in horizontal scrollbars in the breakpoint where we go\n// from displaying everything to hiding the ToC. We accomodate for this by\n// adding a bit of padding to the TOC drawer, disabling the horizontal\n// scrollbar and allowing the scrollbars to cover the padding.\n// https://www.456bereastreet.com/archive/201301/media_query_width_and_vertical_scrollbars/\n\n// HACK: Always having the scrollbar visible, prevents certain browsers from\n// causing the content to stutter horizontally between taller-than-viewport and\n// not-taller-than-viewport pages.\n\nhtml\n overflow-x: hidden\n overflow-y: scroll\n scroll-behavior: smooth\n\n.sidebar-scroll, .toc-scroll, article[role=main] *\n // Override Firefox scrollbar style\n scrollbar-width: thin\n scrollbar-color: var(--color-foreground-border) transparent\n\n // Override Chrome scrollbar styles\n &::-webkit-scrollbar\n width: 0.25rem\n height: 0.25rem\n &::-webkit-scrollbar-thumb\n background-color: var(--color-foreground-border)\n border-radius: 0.125rem\n\n//\n// Overalls\n//\nhtml,\nbody\n height: 100%\n color: var(--color-foreground-primary)\n background: var(--color-background-primary)\n\narticle\n color: var(--color-content-foreground)\n background: var(--color-content-background)\n\n.page\n display: flex\n // fill the viewport for pages with little content.\n min-height: 100%\n\n.mobile-header\n width: 100%\n height: var(--header-height)\n background-color: var(--color-header-background)\n color: var(--color-header-text)\n border-bottom: 1px solid var(--color-header-border)\n\n // Looks like sub-script/super-script have this, and we need this to\n // be \"on top\" of those.\n z-index: 10\n\n // We don't show the header on large screens.\n display: none\n\n // Add shadow when scrolled\n &.scrolled\n border-bottom: none\n box-shadow: 0 0 0.2rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.4rem rgba(0, 0, 0, 0.2)\n\n .header-center\n a\n color: var(--color-header-text)\n text-decoration: none\n\n.main\n display: flex\n flex: 1\n\n// Sidebar (left) also covers the entire left portion of screen.\n.sidebar-drawer\n box-sizing: border-box\n\n border-right: 1px solid var(--color-sidebar-background-border)\n background: var(--color-sidebar-background)\n\n display: flex\n justify-content: flex-end\n // These next two lines took me two days to figure out.\n width: calc((100% - #{$full-width}) / 2 + #{$sidebar-width})\n min-width: $sidebar-width\n\n// Scroll-along sidebars\n.sidebar-container,\n.toc-drawer\n box-sizing: border-box\n width: $sidebar-width\n\n.toc-drawer\n background: var(--color-toc-background)\n // See HACK described on top of this document\n padding-right: 1rem\n\n.sidebar-sticky,\n.toc-sticky\n position: sticky\n top: 0\n height: min(100%, 100vh)\n height: 100vh\n\n display: flex\n flex-direction: column\n\n.sidebar-scroll,\n.toc-scroll\n flex-grow: 1\n flex-shrink: 1\n\n overflow: auto\n scroll-behavior: smooth\n\n// Central items.\n.content\n padding: 0 $content-padding\n width: $content-width\n\n display: flex\n flex-direction: column\n justify-content: space-between\n\n.icon\n display: inline-block\n height: 1rem\n width: 1rem\n svg\n width: 100%\n height: 100%\n\n//\n// Accommodate announcement banner\n//\n.announcement\n background-color: var(--color-announcement-background)\n color: var(--color-announcement-text)\n\n height: var(--header-height)\n display: flex\n align-items: center\n overflow-x: auto\n & + .page\n min-height: calc(100% - var(--header-height))\n\n.announcement-content\n box-sizing: border-box\n padding: 0.5rem\n min-width: 100%\n white-space: nowrap\n text-align: center\n\n a\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-announcement-text)\n\n &:hover\n color: var(--color-announcement-text)\n text-decoration-color: var(--color-link--hover)\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for theme\n////////////////////////////////////////////////////////////////////////////////\n.no-js .theme-toggle-container // don't show theme toggle if there's no JS\n display: none\n\n.theme-toggle-container\n vertical-align: middle\n\n.theme-toggle\n cursor: pointer\n border: none\n padding: 0\n background: transparent\n\n.theme-toggle svg\n vertical-align: middle\n height: 1rem\n width: 1rem\n color: var(--color-foreground-primary)\n display: none\n\n.theme-toggle-header\n float: left\n padding: 1rem 0.5rem\n\n////////////////////////////////////////////////////////////////////////////////\n// Toggles for elements\n////////////////////////////////////////////////////////////////////////////////\n.toc-overlay-icon, .nav-overlay-icon\n display: none\n cursor: pointer\n\n .icon\n color: var(--color-foreground-secondary)\n height: 1rem\n width: 1rem\n\n.toc-header-icon, .nav-overlay-icon\n // for when we set display: flex\n justify-content: center\n align-items: center\n\n.toc-content-icon\n height: 1.5rem\n width: 1.5rem\n\n.content-icon-container\n float: right\n display: flex\n margin-top: 1.5rem\n margin-left: 1rem\n margin-bottom: 1rem\n gap: 0.5rem\n\n .edit-this-page svg\n color: inherit\n height: 1rem\n width: 1rem\n\n.sidebar-toggle\n position: absolute\n display: none\n// \n.sidebar-toggle[name=\"__toc\"]\n left: 20px\n.sidebar-toggle:checked\n left: 40px\n// \n\n.overlay\n position: fixed\n top: 0\n width: 0\n height: 0\n\n transition: width 0ms, height 0ms, opacity 250ms ease-out\n\n opacity: 0\n background-color: rgba(0, 0, 0, 0.54)\n.sidebar-overlay\n z-index: 20\n.toc-overlay\n z-index: 40\n\n// Keep things on top and smooth.\n.sidebar-drawer\n z-index: 30\n transition: left 250ms ease-in-out\n.toc-drawer\n z-index: 50\n transition: right 250ms ease-in-out\n\n// Show the Sidebar\n#__navigation:checked\n & ~ .sidebar-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .sidebar-drawer\n top: 0\n left: 0\n // Show the toc sidebar\n#__toc:checked\n & ~ .toc-overlay\n width: 100%\n height: 100%\n opacity: 1\n & ~ .page\n .toc-drawer\n top: 0\n right: 0\n\n////////////////////////////////////////////////////////////////////////////////\n// Back to top\n////////////////////////////////////////////////////////////////////////////////\n.back-to-top\n text-decoration: none\n\n display: none\n position: fixed\n left: 0\n top: 1rem\n padding: 0.5rem\n padding-right: 0.75rem\n border-radius: 1rem\n font-size: 0.8125rem\n\n background: var(--color-background-primary)\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), #6b728080 0px 0px 1px 0px\n\n z-index: 10\n\n margin-left: 50%\n transform: translateX(-50%)\n svg\n height: 1rem\n width: 1rem\n fill: currentColor\n display: inline-block\n\n span\n margin-left: 0.25rem\n\n .show-back-to-top &\n display: flex\n align-items: center\n\n////////////////////////////////////////////////////////////////////////////////\n// Responsive layouting\n////////////////////////////////////////////////////////////////////////////////\n// Make things a bit bigger on bigger screens.\n@media (min-width: $full-width + $sidebar-width)\n html\n font-size: 110%\n\n@media (max-width: $full-width)\n // Collapse \"toc\" into the icon.\n .toc-content-icon\n display: flex\n .toc-drawer\n position: fixed\n height: 100vh\n top: 0\n right: -$sidebar-width\n border-left: 1px solid var(--color-background-muted)\n .toc-tree\n border-left: none\n font-size: var(--toc-font-size--mobile)\n\n // Accomodate for a changed content width.\n .sidebar-drawer\n width: calc((100% - #{$full-width - $sidebar-width}) / 2 + #{$sidebar-width})\n\n@media (max-width: $full-width - $sidebar-width)\n // Collapse \"navigation\".\n .nav-overlay-icon\n display: flex\n .sidebar-drawer\n position: fixed\n height: 100vh\n width: $sidebar-width\n\n top: 0\n left: -$sidebar-width\n\n // Swap which icon is visible.\n .toc-header-icon\n display: flex\n .toc-content-icon, .theme-toggle-content\n display: none\n .theme-toggle-header\n display: block\n\n // Show the header.\n .mobile-header\n position: sticky\n top: 0\n display: flex\n justify-content: space-between\n align-items: center\n\n .header-left,\n .header-right\n display: flex\n height: var(--header-height)\n padding: 0 var(--header-padding)\n label\n height: 100%\n width: 100%\n user-select: none\n\n .nav-overlay-icon .icon,\n .theme-toggle svg\n height: 1.25rem\n width: 1.25rem\n\n // Add a scroll margin for the content\n :target\n scroll-margin-top: var(--header-height)\n\n // Show back-to-top below the header\n .back-to-top\n top: calc(var(--header-height) + 0.5rem)\n\n // Center the page, and accommodate for the header.\n .page\n flex-direction: column\n justify-content: center\n .content\n margin-left: auto\n margin-right: auto\n\n@media (max-width: $content-width + 2* $content-padding)\n // Content should respect window limits.\n .content\n width: 100%\n overflow-x: auto\n\n@media (max-width: $content-width)\n .content\n padding: 0 $content-padding--small\n // Don't float sidebars to the right.\n article aside.sidebar\n float: none\n width: 100%\n margin: 1rem 0\n","// Overall Layout Variables\n//\n// Because CSS variables can't be used in media queries. The fact that this\n// makes the layout non-user-configurable is a good thing.\n$content-padding: 3em;\n$content-padding--small: 1em;\n$content-width: 46em;\n$sidebar-width: 15em;\n$full-width: $content-width + 2 * ($content-padding + $sidebar-width);\n","//\n// The design here is strongly inspired by mkdocs-material.\n.admonition, .topic\n margin: 1rem auto\n padding: 0 0.5rem 0.5rem 0.5rem\n\n background: var(--color-admonition-background)\n\n border-radius: 0.2rem\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n font-size: var(--admonition-font-size)\n\n overflow: hidden\n page-break-inside: avoid\n\n // First element should have no margin, since the title has it.\n > :nth-child(2)\n margin-top: 0\n\n // Last item should have no margin, since we'll control that w/ padding\n > :last-child\n margin-bottom: 0\n\np.admonition-title, p.topic-title\n position: relative\n margin: 0 -0.5rem 0.5rem\n padding-left: 2rem\n padding-right: .5rem\n padding-top: .4rem\n padding-bottom: .4rem\n\n font-weight: 500\n font-size: var(--admonition-title-font-size)\n line-height: 1.3\n\n // Our fancy icon\n &::before\n content: \"\"\n position: absolute\n left: 0.5rem\n width: 1rem\n height: 1rem\n\n// Default styles\np.admonition-title\n background-color: var(--color-admonition-title-background)\n &::before\n background-color: var(--color-admonition-title)\n mask-image: var(--icon-admonition-default)\n mask-repeat: no-repeat\n\np.topic-title\n background-color: var(--color-topic-title-background)\n &::before\n background-color: var(--color-topic-title)\n mask-image: var(--icon-topic-default)\n mask-repeat: no-repeat\n\n//\n// Variants\n//\n.admonition\n border-left: 0.2rem solid var(--color-admonition-title)\n\n @each $type, $value in $admonitions\n &.#{$type}\n border-left-color: var(--color-admonition-title--#{$type})\n > .admonition-title\n background-color: var(--color-admonition-title-background--#{$type})\n &::before\n background-color: var(--color-admonition-title--#{$type})\n mask-image: var(--icon-#{nth($value, 2)})\n\n.admonition-todo > .admonition-title\n text-transform: uppercase\n","// This file stylizes the API documentation (stuff generated by autodoc). It's\n// deeply nested due to how autodoc structures the HTML without enough classes\n// to select the relevant items.\n\n// API docs!\ndl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)\n // Tweak the spacing of all the things!\n dd\n margin-left: 2rem\n > :first-child\n margin-top: 0.125rem\n > :last-child\n margin-bottom: 0.75rem\n\n // This is used for the arguments\n .field-list\n margin-bottom: 0.75rem\n\n // \"Headings\" (like \"Parameters\" and \"Return\")\n > dt\n text-transform: uppercase\n font-size: var(--font-size--small)\n\n dd:empty\n margin-bottom: 0.5rem\n dd > ul\n margin-left: -1.2rem\n > li\n > p:nth-child(2)\n margin-top: 0\n // When the last-empty-paragraph follows a paragraph, it doesn't need\n // to augument the existing spacing.\n > p + p:last-child:empty\n margin-top: 0\n margin-bottom: 0\n\n // Colorize the elements\n > dt\n color: var(--color-api-overall)\n\n.sig:not(.sig-inline)\n font-weight: bold\n\n font-size: var(--api-font-size)\n font-family: var(--font-stack--monospace)\n\n margin-left: -0.25rem\n margin-right: -0.25rem\n padding-top: 0.25rem\n padding-bottom: 0.25rem\n padding-right: 0.5rem\n\n // These are intentionally em, to properly match the font size.\n padding-left: 3em\n text-indent: -2.5em\n\n border-radius: 0.25rem\n\n background: var(--color-api-background)\n transition: background 100ms ease-out\n\n &:hover\n background: var(--color-api-background-hover)\n\n // adjust the size of the [source] link on the right.\n a.reference\n .viewcode-link\n font-weight: normal\n width: 3.5rem\n\n // Break words when they're too long\n span.pre\n overflow-wrap: anywhere\n\nem.property\n font-style: normal\n &:first-child\n color: var(--color-api-keyword)\n.sig-name\n color: var(--color-api-name)\n.sig-prename\n font-weight: normal\n color: var(--color-api-pre-name)\n.sig-paren\n color: var(--color-api-paren)\n.sig-param\n font-style: normal\n\n.versionmodified\n font-style: italic\ndiv.versionadded, div.versionchanged, div.deprecated\n p\n margin-top: 0.125rem\n margin-bottom: 0.125rem\n\n// Align the [docs] and [source] to the right.\n.viewcode-link, .viewcode-back\n float: right\n text-align: right\n",".line-block\n margin-top: 0.5rem\n margin-bottom: 0.75rem\n .line-block\n margin-top: 0rem\n margin-bottom: 0rem\n padding-left: 1rem\n","// Captions\narticle p.caption,\ntable > caption,\n.code-block-caption\n font-size: var(--font-size--small)\n text-align: center\n\n// Caption above a TOCTree\n.toctree-wrapper.compound\n .caption, :not(.caption) > .caption-text\n font-size: var(--font-size--small)\n text-transform: uppercase\n\n text-align: initial\n margin-bottom: 0\n\n > ul\n margin-top: 0\n margin-bottom: 0\n","// Inline code\ncode.literal, .sig-inline\n background: var(--color-inline-code-background)\n border-radius: 0.2em\n // Make the font smaller, and use padding to recover.\n font-size: var(--font-size--small--2)\n padding: 0.1em 0.2em\n\n overflow-wrap: break-word\n\n p &\n border: 1px solid var(--color-background-border)\n\n.sig-inline\n font-family: var(--font-stack--monospace)\n\n// Code and Literal Blocks\n$code-spacing-vertical: 0.625rem\n$code-spacing-horizontal: 0.875rem\n\n// Wraps every literal block + line numbers.\ndiv[class*=\" highlight-\"],\ndiv[class^=\"highlight-\"]\n margin: 1em 0\n display: flex\n\n .table-wrapper\n margin: 0\n padding: 0\n\npre\n margin: 0\n padding: 0\n overflow: auto\n\n // Needed to have more specificity than pygments' \"pre\" selector. :(\n article[role=\"main\"] .highlight &\n line-height: 1.5\n\n &.literal-block,\n .highlight &\n font-size: var(--code-font-size)\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n // Make it look like all the other blocks.\n &.literal-block\n margin-top: 1rem\n margin-bottom: 1rem\n\n border-radius: 0.2rem\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n\n// All code is always contained in this.\n.highlight\n width: 100%\n border-radius: 0.2rem\n\n // Make line numbers and prompts un-selectable.\n .gp, span.linenos\n user-select: none\n pointer-events: none\n\n // Expand the line-highlighting.\n .hll\n display: block\n margin-left: -$code-spacing-horizontal\n margin-right: -$code-spacing-horizontal\n padding-left: $code-spacing-horizontal\n padding-right: $code-spacing-horizontal\n\n/* Make code block captions be nicely integrated */\n.code-block-caption\n display: flex\n padding: $code-spacing-vertical $code-spacing-horizontal\n\n border-radius: 0.25rem\n border-bottom-left-radius: 0\n border-bottom-right-radius: 0\n font-weight: 300\n border-bottom: 1px solid\n\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n border-color: var(--color-background-border)\n\n + div[class]\n margin-top: 0\n pre\n border-top-left-radius: 0\n border-top-right-radius: 0\n\n// When `html_codeblock_linenos_style` is table.\n.highlighttable\n width: 100%\n display: block\n tbody\n display: block\n\n tr\n display: flex\n\n // Line numbers\n td.linenos\n background-color: var(--color-code-background)\n color: var(--color-code-foreground)\n padding: $code-spacing-vertical $code-spacing-horizontal\n padding-right: 0\n border-top-left-radius: 0.2rem\n border-bottom-left-radius: 0.2rem\n\n .linenodiv\n padding-right: $code-spacing-horizontal\n font-size: var(--code-font-size)\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n\n // Actual code\n td.code\n padding: 0\n display: block\n flex: 1\n overflow: hidden\n\n .highlight\n border-top-left-radius: 0\n border-bottom-left-radius: 0\n\n// When `html_codeblock_linenos_style` is inline.\n.highlight\n span.linenos\n display: inline-block\n padding-left: 0\n padding-right: $code-spacing-horizontal\n margin-right: $code-spacing-horizontal\n box-shadow: -0.0625rem 0 var(--color-foreground-border) inset\n","// Inline Footnote Reference\n.footnote-reference\n font-size: var(--font-size--small--4)\n vertical-align: super\n\n// Definition list, listing the content of each note.\n// docutils <= 0.17\ndl.footnote.brackets\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\n display: grid\n grid-template-columns: max-content auto\n dt\n margin: 0\n > .fn-backref\n margin-left: 0.25rem\n\n &:after\n content: \":\"\n\n .brackets\n &:before\n content: \"[\"\n &:after\n content: \"]\"\n\n dd\n margin: 0\n padding: 0 1rem\n\n// docutils >= 0.18\naside.footnote\n font-size: var(--font-size--small)\n color: var(--color-foreground-secondary)\n\naside.footnote > span,\ndiv.citation > span\n float: left\n font-weight: 500\n padding-right: 0.25rem\n\naside.footnote > p,\ndiv.citation > p\n margin-left: 2rem\n","//\n// Figures\n//\nimg\n box-sizing: border-box\n max-width: 100%\n height: auto\n\narticle\n figure, .figure\n border-radius: 0.2rem\n\n margin: 0\n :last-child\n margin-bottom: 0\n\n .align-left\n float: left\n clear: left\n margin: 0 1rem 1rem\n\n .align-right\n float: right\n clear: right\n margin: 0 1rem 1rem\n\n .align-default,\n .align-center\n display: block\n text-align: center\n margin-left: auto\n margin-right: auto\n\n // WELL, table needs to be stylised like a table.\n table.align-default\n display: table\n text-align: initial\n",".genindex-jumpbox, .domainindex-jumpbox\n border-top: 1px solid var(--color-background-border)\n border-bottom: 1px solid var(--color-background-border)\n padding: 0.25rem\n\n.genindex-section, .domainindex-section\n h2\n margin-top: 0.75rem\n margin-bottom: 0.5rem\n ul\n margin-top: 0\n margin-bottom: 0\n","ul,\nol\n padding-left: 1.2rem\n\n // Space lists out like paragraphs\n margin-top: 1rem\n margin-bottom: 1rem\n // reduce margins within li.\n li\n > p:first-child\n margin-top: 0.25rem\n margin-bottom: 0.25rem\n\n > p:last-child\n margin-top: 0.25rem\n\n > ul,\n > ol\n margin-top: 0.5rem\n margin-bottom: 0.5rem\n\nol\n &.arabic\n list-style: decimal\n &.loweralpha\n list-style: lower-alpha\n &.upperalpha\n list-style: upper-alpha\n &.lowerroman\n list-style: lower-roman\n &.upperroman\n list-style: upper-roman\n\n// Don't space lists out when they're \"simple\" or in a `.. toctree::`\n.simple,\n.toctree-wrapper\n li\n > ul,\n > ol\n margin-top: 0\n margin-bottom: 0\n\n// Definition Lists\n.field-list,\n.option-list,\ndl:not([class]),\ndl.simple,\ndl.footnote,\ndl.glossary\n dt\n font-weight: 500\n margin-top: 0.25rem\n + dt\n margin-top: 0\n\n .classifier::before\n content: \":\"\n margin-left: 0.2rem\n margin-right: 0.2rem\n\n dd\n > p:first-child,\n ul\n margin-top: 0.125rem\n\n ul\n margin-bottom: 0.125rem\n",".math-wrapper\n width: 100%\n overflow-x: auto\n\ndiv.math\n position: relative\n text-align: center\n\n .headerlink,\n &:focus .headerlink\n display: none\n\n &:hover .headerlink\n display: inline-block\n\n span.eqno\n position: absolute\n right: 0.5rem\n top: 50%\n transform: translate(0, -50%)\n z-index: 1\n","// Abbreviations\nabbr[title]\n cursor: help\n\n// \"Problematic\" content, as identified by Sphinx\n.problematic\n color: var(--color-problematic)\n\n// Keyboard / Mouse \"instructions\"\nkbd:not(.compound)\n margin: 0 0.2rem\n padding: 0 0.2rem\n border-radius: 0.2rem\n border: 1px solid var(--color-foreground-border)\n color: var(--color-foreground-primary)\n vertical-align: text-bottom\n\n font-size: var(--font-size--small--3)\n display: inline-block\n\n box-shadow: 0 0.0625rem 0 rgba(0, 0, 0, 0.2), inset 0 0 0 0.125rem var(--color-background-primary)\n\n background-color: var(--color-background-secondary)\n\n// Blockquote\nblockquote\n border-left: 4px solid var(--color-background-border)\n background: var(--color-background-secondary)\n\n margin-left: 0\n margin-right: 0\n padding: 0.5rem 1rem\n\n .attribution\n font-weight: 600\n text-align: right\n\n &.pull-quote,\n &.highlights\n font-size: 1.25em\n\n &.epigraph,\n &.pull-quote\n border-left-width: 0\n border-radius: 0.5rem\n\n &.highlights\n border-left-width: 0\n background: transparent\n\n// Center align embedded-in-text images\np .reference img\n vertical-align: middle\n","p.rubric\n line-height: 1.25\n font-weight: bold\n font-size: 1.125em\n\n // For Numpy-style documentation that's got rubrics within it.\n // https://github.com/pradyunsg/furo/discussions/505\n dd &\n line-height: inherit\n font-weight: inherit\n\n font-size: var(--font-size--small)\n text-transform: uppercase\n","article .sidebar\n float: right\n clear: right\n width: 30%\n\n margin-left: 1rem\n margin-right: 0\n\n border-radius: 0.2rem\n background-color: var(--color-background-secondary)\n border: var(--color-background-border) 1px solid\n\n > *\n padding-left: 1rem\n padding-right: 1rem\n\n > ul, > ol // lists need additional padding, because bullets.\n padding-left: 2.2rem\n\n .sidebar-title\n margin: 0\n padding: 0.5rem 1rem\n border-bottom: var(--color-background-border) 1px solid\n\n font-weight: 500\n\n// TODO: subtitle\n// TODO: dedicated variables?\n",".table-wrapper\n width: 100%\n overflow-x: auto\n margin-top: 1rem\n margin-bottom: 0.5rem\n padding: 0.2rem 0.2rem 0.75rem\n\ntable.docutils\n border-radius: 0.2rem\n border-spacing: 0\n border-collapse: collapse\n\n box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05), 0 0 0.0625rem rgba(0, 0, 0, 0.1)\n\n th\n background: var(--color-table-header-background)\n\n td,\n th\n // Space things out properly\n padding: 0 0.25rem\n\n // Get the borders looking just-right.\n border-left: 1px solid var(--color-table-border)\n border-right: 1px solid var(--color-table-border)\n border-bottom: 1px solid var(--color-table-border)\n\n p\n margin: 0.25rem\n\n &:first-child\n border-left: none\n &:last-child\n border-right: none\n\n // MyST-parser tables set these classes for control of column alignment\n &.text-left\n text-align: left\n &.text-right\n text-align: right\n &.text-center\n text-align: center\n",":target\n scroll-margin-top: 0.5rem\n\n@media (max-width: $full-width - $sidebar-width)\n :target\n scroll-margin-top: calc(0.5rem + var(--header-height))\n\n // When a heading is selected\n section > span:target\n scroll-margin-top: calc(0.8rem + var(--header-height))\n\n// Permalinks\n.headerlink\n font-weight: 100\n user-select: none\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\ndl dt,\np.caption,\nfigcaption p,\ntable > caption,\n.code-block-caption\n > .headerlink\n margin-left: 0.5rem\n visibility: hidden\n &:hover > .headerlink\n visibility: visible\n\n // Don't change to link-like, if someone adds the contents directive.\n > .toc-backref\n color: inherit\n text-decoration-line: none\n\n// Figure and table captions are special.\nfigure:hover > figcaption > p > .headerlink,\ntable:hover > caption > .headerlink\n visibility: visible\n\n:target >, // Regular section[id] style anchors\nspan:target ~ // Non-regular span[id] style \"extra\" anchors\n h1,\n h2,\n h3,\n h4,\n h5,\n h6\n &:nth-of-type(1)\n background-color: var(--color-highlight-on-target)\n // .headerlink\n // visibility: visible\n code.literal\n background-color: transparent\n\ntable:target > caption,\nfigure:target\n background-color: var(--color-highlight-on-target)\n\n// Inline page contents\n.this-will-duplicate-information-and-it-is-still-useful-here li :target\n background-color: var(--color-highlight-on-target)\n\n// Code block permalinks\n.literal-block-wrapper:target .code-block-caption\n background-color: var(--color-highlight-on-target)\n\n// When a definition list item is selected\n//\n// There isn't really an alternative to !important here, due to the\n// high-specificity of API documentation's selector.\ndt:target\n background-color: var(--color-highlight-on-target) !important\n\n// When a footnote reference is selected\n.footnote > dt:target + dd,\n.footnote-reference:target\n background-color: var(--color-highlight-on-target)\n",".guilabel\n background-color: var(--color-guilabel-background)\n border: 1px solid var(--color-guilabel-border)\n color: var(--color-guilabel-text)\n\n padding: 0 0.3em\n border-radius: 0.5em\n font-size: 0.9em\n","// This file contains the styles used for stylizing the footer that's shown\n// below the content.\n\nfooter\n font-size: var(--font-size--small)\n display: flex\n flex-direction: column\n\n margin-top: 2rem\n\n// Bottom of page information\n.bottom-of-page\n display: flex\n align-items: center\n justify-content: space-between\n\n margin-top: 1rem\n padding-top: 1rem\n padding-bottom: 1rem\n\n color: var(--color-foreground-secondary)\n border-top: 1px solid var(--color-background-border)\n\n line-height: 1.5\n\n @media (max-width: $content-width)\n text-align: center\n flex-direction: column-reverse\n gap: 0.25rem\n\n .left-details\n font-size: var(--font-size--small)\n\n .right-details\n display: flex\n flex-direction: column\n gap: 0.25rem\n text-align: right\n\n .icons\n display: flex\n justify-content: flex-end\n gap: 0.25rem\n font-size: 1rem\n\n a\n text-decoration: none\n\n svg,\n img\n font-size: 1.125rem\n height: 1em\n width: 1em\n\n// Next/Prev page information\n.related-pages\n a\n display: flex\n align-items: center\n\n text-decoration: none\n &:hover .page-info .title\n text-decoration: underline\n color: var(--color-link)\n text-decoration-color: var(--color-link-underline)\n\n svg.furo-related-icon,\n svg.furo-related-icon > use\n flex-shrink: 0\n\n color: var(--color-foreground-border)\n\n width: 0.75rem\n height: 0.75rem\n margin: 0 0.5rem\n\n &.next-page\n max-width: 50%\n\n float: right\n clear: right\n text-align: right\n\n &.prev-page\n max-width: 50%\n\n float: left\n clear: left\n\n svg\n transform: rotate(180deg)\n\n.page-info\n display: flex\n flex-direction: column\n overflow-wrap: anywhere\n\n .next-page &\n align-items: flex-end\n\n .context\n display: flex\n align-items: center\n\n padding-bottom: 0.1rem\n\n color: var(--color-foreground-muted)\n font-size: var(--font-size--small)\n text-decoration: none\n","//\n// Search Page Listing\n//\nul.search\n padding-left: 0\n list-style: none\n\n li\n padding: 1rem 0\n border-bottom: 1px solid var(--color-background-border)\n\n//\n// Highlighted by links in search page\n//\n[role=main] .highlighted\n background-color: var(--color-highlighted-background)\n color: var(--color-highlighted-text)\n","// This file contains the styles for the contents of the left sidebar, which\n// contains the navigation tree, logo, search etc.\n\n////////////////////////////////////////////////////////////////////////////////\n// Brand on top of the scrollable tree.\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-brand\n display: flex\n flex-direction: column\n flex-shrink: 0\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n text-decoration: none\n\n.sidebar-brand-text\n color: var(--color-sidebar-brand-text)\n overflow-wrap: break-word\n margin: var(--sidebar-item-spacing-vertical) 0\n font-size: 1.5rem\n\n.sidebar-logo-container\n margin: var(--sidebar-item-spacing-vertical) 0\n\n.sidebar-logo\n margin: 0 auto\n display: block\n max-width: 100%\n\n////////////////////////////////////////////////////////////////////////////////\n// Search\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-search-container\n display: flex\n align-items: center\n margin-top: var(--sidebar-search-space-above)\n\n position: relative\n\n background: var(--color-sidebar-search-background)\n &:hover,\n &:focus-within\n background: var(--color-sidebar-search-background--focus)\n\n &::before\n content: \"\"\n position: absolute\n left: var(--sidebar-item-spacing-horizontal)\n width: var(--sidebar-search-icon-size)\n height: var(--sidebar-search-icon-size)\n\n background-color: var(--color-sidebar-search-icon)\n mask-image: var(--icon-search)\n\n.sidebar-search\n box-sizing: border-box\n\n border: none\n border-top: 1px solid var(--color-sidebar-search-border)\n border-bottom: 1px solid var(--color-sidebar-search-border)\n\n padding-top: var(--sidebar-search-input-spacing-vertical)\n padding-bottom: var(--sidebar-search-input-spacing-vertical)\n padding-right: var(--sidebar-search-input-spacing-horizontal)\n padding-left: calc(var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size))\n\n width: 100%\n\n color: var(--color-sidebar-search-foreground)\n background: transparent\n z-index: 10\n\n &:focus\n outline: none\n\n &::placeholder\n font-size: var(--sidebar-search-input-font-size)\n\n//\n// Hide Search Matches link\n//\n#searchbox .highlight-link\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0\n margin: 0\n text-align: center\n\n a\n color: var(--color-sidebar-search-icon)\n font-size: var(--font-size--small--2)\n\n////////////////////////////////////////////////////////////////////////////////\n// Structure/Skeleton of the navigation tree (left)\n////////////////////////////////////////////////////////////////////////////////\n.sidebar-tree\n font-size: var(--sidebar-item-font-size)\n margin-top: var(--sidebar-tree-space-above)\n margin-bottom: var(--sidebar-item-spacing-vertical)\n\n ul\n padding: 0\n margin-top: 0\n margin-bottom: 0\n\n display: flex\n flex-direction: column\n\n list-style: none\n\n li\n position: relative\n margin: 0\n\n > ul\n margin-left: var(--sidebar-item-spacing-horizontal)\n\n .icon\n color: var(--color-sidebar-link-text)\n\n .reference\n box-sizing: border-box\n color: var(--color-sidebar-link-text)\n\n // Fill the parent.\n display: inline-block\n line-height: var(--sidebar-item-line-height)\n text-decoration: none\n\n // Don't allow long words to cause wrapping.\n overflow-wrap: anywhere\n\n height: 100%\n width: 100%\n\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n &:hover\n background: var(--color-sidebar-item-background--hover)\n\n // Add a nice little \"external-link\" arrow here.\n &.external::after\n content: url('data:image/svg+xml,')\n margin: 0 0.25rem\n vertical-align: middle\n color: var(--color-sidebar-link-text)\n\n // Make the current page reference bold.\n .current-page > .reference\n font-weight: bold\n\n label\n position: absolute\n top: 0\n right: 0\n height: var(--sidebar-item-height)\n width: var(--sidebar-expander-width)\n\n cursor: pointer\n user-select: none\n\n display: flex\n justify-content: center\n align-items: center\n\n .caption, :not(.caption) > .caption-text\n font-size: var(--sidebar-caption-font-size)\n color: var(--color-sidebar-caption-text)\n\n font-weight: bold\n text-transform: uppercase\n\n margin: var(--sidebar-caption-space-above) 0 0 0\n padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal)\n\n // If it has children, add a bit more padding to wrap the content to avoid\n // overlapping with the